序
入门图的大门,首先要学习的就是图的存储和遍历,笔者经过三天猛学,稍微理解了一些相关知识(学习顺序问题导致走了些弯路,没学数组如何模拟链表上来就直接看数组模拟邻接表存储图活该我看不懂),本帖不详细展开存储和遍历的教程,只会提一下基本思路,主要是为了存一下模版代码,方便将来学习复习方便
图的存储分为两个主流,根据点和边的数量关系(稠密图或稀疏图),可以用邻接矩阵(稀疏图)和邻接表(稠密图)来存储,邻接矩阵的含义好理解,将来再找一个例题用邻接矩阵写写。本次的例子我们练习最主流的邻接表
想明白如何数组模拟邻接表,我们要先对数组模拟链表非常熟悉(下一个帖子专门讲讲怎么模拟的),而邻接表就是在读入数据时,将终点插入到起点链表的头节点
遍历过程是结合了遍历链表和dfs的合体版本,要求有较为扎实的基本功,详细解释我们在代码的注释里结合代码去理解
这里我们先给出邻接表存储图和遍历图的模版
邻接表存储图和遍历图的模版
存储模版:
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;//题目给定的数据范围
int e[N],ne[N],h[N],idx;//详细含义见链表,其中h数组是将链表中head做一个集合存在数组中
将b插入到a的链表中,表示从a能到达b,这里对应将元素插入链表头节点
void add_b_into_link_a(int a,int b){
e[idx]=b;ne[idx]=h[a];h[a]=idx;idx++;
}
int main(){
int n,m;//n个点,m条边
cin>>n>>m;
memset(h,-1,sizeof h);//将头节点全部初始化为-1,参考链表操作中head=-1
for(int i=0;i<m;i++){//读入m条边的信息
int a,b;//记录起点和终点,这里暂时不考虑边权重的问题(其实就是多开几个e数组)
cin>>a>>b;
add_b_into_link_a(a,b);add_b_into_link_a(b,a);/*这里是无向图,a能到b,b也能到a;将b头插入到a链表中记录a能到b,a头插入b链表中,可自由变换形式(有向图等)*/
}
}
dfs遍历模版:
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int e[N],ne[N],h[N],idx;
bool check[N];//防止走回之前走过的路,dfs都有的check数组
void bfs(int u){
check[u]=true;//标记当前点已经遍历过
for(int i=h[u];i!=-1;i=ne[i]){//遍历u号点所有相连的点,再通过递归不断往下走直到遍历所有点
int j=e[i];
if(check[j]==0)//如果u链表上的元素e[i]还没有遍历过,或者说该和u相连的路径还没有走过
dfs(j);//走出下一步,如从一号点走到二号点,从u号点走向和u相连的下一个点
}
}
int main(){
int n,m;//n个点,m条边
cin>>n>>m;
memset(h,-1,sizeof h);//将头节点全部初始化为-1,参考链表操作中head=-1
for(int i=0;i<m;i++){
int a,b;cin>>a>>b;
insert(a,b);insert(b,a);//就是上一个存储时的add_b_into_link_a(int a,int b)函数
}
dfs(1);
}
模版代码光看没有具体数据或例子感觉挺抽象挺虚的,这里给出一个例题,结合给定的数据带到模版里看看怎么存储,遍历的过程是什么样子
题目描述如下
我们先不考虑这个题怎么写,先把图的数据存储下来,再想想dfs遍历的过程
本题数据的存储和遍历代码如下
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int e[N],ne[N],h[N],idx;
bool check[N];
int n;
int ans=N;//删掉重心后最大子树的节点个数
//头插法,对应单链表中一直往头里插入,邻接表的h数组代替单链表的head
//ps:不要把邻接表的头插和链表的b插入第k个元素后面混淆了
void insert(int a,int b){//将b插入a链表的头节点
e[idx]=b;ne[idx]=h[a];h[a]=idx;idx++;
}
//对比单链表写法(head,idx,e[N],ne[N])
void insert_head(int b){
e[idx]=b;ne[idx]=head;head=idx;idx++;
}
//dfs遍历图的板子
void bfs(int u){
check[u]=true;
for(int i=h[u];i!=-1;i=ne[i]){//遍历1号点所有相连的点,再通过递归不断往下走直到遍历所有点
int j=e[i];
if(check[j]==0)//如果u链表上的元素e[i]还没有遍历过
dfs(j);//走出下一步,如从一号点走到二号点
}
}
int main(){
cin>>n;
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++){
int a,b;cin>>a>>b;
insert(a,b);insert(b,a);
}
dfs(1);
cout<<ans<<endl;
}
既然题目已经剖出来了,顺带就把板子结合思考把题目做一下,进一步理解图的题型
本题要找在所有删掉一个节点的情况中得到的最大联通块的最小值 (反复读这句话),也就是说,我们要遍历每一个节点,并求出删除该节点的情况下得到的联通块的最大值,并在遍历完后的到的所有最大联通块中取最小值输出
如何找删除某节点的情况下得到的联通块的最大值呢,我们可以遍历该被删除节点的子树节点有几个,同时他头上的联通块我们可以用n(总结点数)减去该被删除节点的子树节点,剩余的就是与他父节点(头上)相连通的节点,我们在各个子树和头上的联通块的个数里取一个最大值,就是我们删掉该节点后剩余联通块的最大值
遍历所有节点重复上述操作,不断取最大值的最小值,当遍历完成后,我们的到的最小值就是题目要求的答案
ac代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int e[N],ne[N],h[N],idx;
bool check[N];
int n;
int ans=N;//删掉重心后最大子树的节点个数
//头插法,对应单链表中一直往头里插入,邻接表的h数组代替单链表的head
void insert(int a,int b){//将b插入a链表的头节点
e[idx]=b;ne[idx]=h[a];h[a]=idx;idx++;
}
//对比单链表写法(head,idx,e[N],ne[N])
/*void insert_head(int b){
e[idx]=b;ne[idx]=head;head=idx;idx++;
}*/
/*-------------------------------------------------------*/
//dfs遍历图的板子
/*void bfs(int u){
check[u]=true;
for(int i=h[u];i!=-1;i=ne[i]){//遍历1号点所有相连的点,再通过递归不断往下走直到遍历所有点
int j=e[i];
if(check[j]==0)//如果u链表上的元素e[i]还没有遍历过
dfs(j);//走出下一步,如从一号点走到二号点
}
}*/
//返回以u为根的子树的大小
int dfs(int u){
check[u]=true;
int sum=1,res=0;//res表示将u这个点删除以后每一个连通块的最大值
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(check[j]==0){
int s=dfs(j);//s表示当前子树的大小
res=max(res,s);
sum=sum+s;//所有除去u后子树的大小也是以u为根节点的子树的一部分
}
}
int leave=n-sum;
res=max(res,leave);
ans=min(ans,res);
return sum;
}
int main(){
cin>>n;
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++){
int a,b;cin>>a>>b;
insert(a,b);insert(b,a);
}
dfs(1);
cout<<ans<<endl;
}
题目来源:846. 树的重心 - AcWing题库