构图总结
最近了解了一些新的建图方法,比较难啃,但却非常实用。在此作一个小结。
两个基本构图方法:邻接表 和 邻接矩阵
1. 邻接矩阵
名字听起来复杂,但其实是最简单的,用得也比较广吧,能用来解决一般图类问题。
若要表示顶点数V=n,边树E=m的无向图G(V, E),可定义一个二维数组g[n][n],其中g[u][v]表示顶点u与v的连接状态,若有边相连可令其为1,否则为0。根据题目要求,还可令其等于u与v的距离(此时无边时可令其等于无穷或-1)等等。
这样的一个二维数组,把它的各元素按矩阵排开,即所谓邻接矩阵。
对于有向图,则若有u到v的单向边,即令g[u][v]=1,g[v][u]=0即可。对于边有权值的情况与无向图的构造类似。邻接矩阵实现比较直观,不做过多解释。直接上代码:
1 const int maxn = 10000;/*构造无向图例子*/ 2 int vertex, edge; 3 int value; 4 int g[maxn][maxn]; 5 ... 6 int u, v; 7 memset(g, 0xff, sizeof(g)); 8 for(int i=1;i<=edge;i++) 9 { 10 cin>>u>>v>>value; 11 g[u][v]=value; 12 g[v][u]=value; 13 } 14 ...
2. 邻接表
这里的表可理解为链表,即对于每一个顶点v,将与其向连的点(有向图则是以其为起点的边的终点)存入对应数组或者连接成链表。
如要遍历某个顶点的边,则遍历其对应数组(或指针)或链表即可。
在这里,同样可以用二维数组g[n][n]来构图,但是得加个记录每个顶点当前连接的点的数量的数组c[n],稍微麻烦些。这里采用vector(其实就可以理解为一个长度可变的数组,只不过STL为它提供了一些函数)表示:
1 const int maxn = 10000;/*构造无向图例子*/ 2 int vertex, edge; 3 vector <int> g[maxn]; 4 ... 5 int u, v; 6 for(int i=1;i<=edge;i++) 7 { 8 cin>>u>>v; 9 g[u].push_back(v); 10 g[v].push_back(u); 11 } 12 ...
小小的比较
可以观察到,对于邻接矩阵,能很方便地表示两点之间的连接关系以及边的权值,但要查看与某个点的相连的边的情况就没那么方便了;而对于邻接表,刚好相反,它能很好地表示点的连接边的情况,但要同时表示边的权值也就麻烦些了(当然,可以改成顶点的结构体,记录编号及权值即可)。
那么,有没有更完美的表示方法呢?
这里整理了我理解的两种方法,它们分别使用了数组和指针,可谓行云流水。
3. 数组
先来讲讲它的大概代码情况吧
首先需要一个节点结构体:
1 struct node{ 2 int nex, to, v;//下个遍历边 终点 权值 3 };
它的构图主要由一个边数组edge[m]和顶点遍历头数组head[n]完成:
1 node edge[maxe]; 2 int head[maxv];
然后,我们要定义一个计数器cnt。构图的时候添加新边用以下函数:
1 void addedge(int a, int b, int c)//起点 终点 权值 2 { 3 edge[cnt].to=b; 4 edge[cnt].v=c; 5 edge[cnt].nex=head[a];//这里就是为什么用nex可以实现边遍历了 6 head[a]=cnt++;//head[a]赋值为当前边编号 7 }
构图时就是一些基本操作了:
for(int i=1;i<=m;i++) { cin>>u>>v>>c; addedge(u, v, c); addedge(v, u, c); }
那么,如何实现顶点的边遍历呢?看操作:
1 int i;//假设i为当前要遍历边的顶点 2 int to; 3 for(int j=head[i];j!=0;j=edge[j].nex)//循环结束条件不一定是j!=0,可由题目情况更改 4 { 5 to=edge[j].to; 6 /*接下来就可以进行相关操作了 7 * 比如打印各边权值: 8 * cout<<edge[j].v<<endl; 9 * 当然,更多地是用于进行顶点i与终点to之间的一些操作 10 */ 11 }
好了,现在好好简单说说思路及原理。主要就在那个加边函数里面,后面两句代码实现的是使head[a]始终表示顶点a当前最后连接的边,当加入新边时,就把head[a]=cnt即可。而各edge[v].nex表示的便是head[a]改变的轨迹,那不就是记录了顶点a连接的所有边了嘛。
至于它的用途,我感觉对于稍复杂的图类问题还是很有效果的,比如用于Dijkstra单源最短路算法的优先队列优化版其实我就是从这里面改的
下面贴一下我理解的Dijkstra优先队列实现算法:
1 #include <iostream> 2 #include <stdio.h> 3 #include <string.h> 4 #include <queue> 5 6 using namespace std; 7 struct node{ 8 int nex, to, v;//下一个遍历边 终点 权值 9 }; 10 struct que{ 11 int i, d;//顶点编号 距离 12 bool operator < (const que &qu)const{ 13 return d>qu.d;//保持当前最小距离点排在队首 14 } 15 }; 16 int cnt, m, n; 17 int head[110], dis[110];//head[a]表示顶点a的初始遍历边/头 18 node edge[20110]; 19 priority_queue <que> q; 20 void addedge(int a, int b, int c) 21 { 22 edge[cnt].to=b; 23 edge[cnt].v=c; 24 edge[cnt].nex=head[a]; 25 head[a]=cnt++; 26 } 27 int dijkstra(int s, int t) 28 { 29 que x; 30 x.i=s; 31 x.d=0; 32 dis[s]=0;//自己到自己的距离为0 33 q.push(x); 34 while(q.top().i!=t)//终点还没入队 35 { 36 int i=q.top().i;//出队 取编号 37 for(int j=head[i];j!=0;j=edge[j].nex) 38 { 39 int to=edge[j].to; 40 if(dis[to]==-1||dis[to]>dis[i]+edge[j].v)//更新其他各点到源点的距离 41 { 42 dis[to]=dis[i]+edge[j].v; 43 x.i=to; 44 x.d=dis[to]; 45 q.push(x); 46 } 47 } 48 q.pop(); 49 } 50 return dis[t]; 51 } 52 int main() 53 { 54 int u, v, c; 55 while(cin>>n>>m&&n) 56 { 57 cnt=1; 58 memset(head, 0, sizeof(head)); 59 memset(edge, 0, sizeof(edge)); 60 memset(dis, -1, sizeof(dis)); 61 while(!q.empty())q.pop(); 62 for(int i=1;i<=m;i++) 63 { 64 cin>>u>>v>>c; 65 addedge(u, v, c); 66 addedge(v, u, c); 67 } 68 cout<<dijkstra(1, n)<<endl; 69 } 70 return 0; 71 }
4. 指针
要掌握这个方法,我们需要先理解指针的概念及定义,充分利用指针指向地址这一概念。
另外,这个方法其实也可以看作是邻接表的更高阶版本,所以,我们理解的时候可以结合一下邻接表的概念及思路。
在了解到上述数组的用法之后(数组表示遍历边),那么,这里我们直接使用数组指向顶点v最后一次记录的边(点)!
准备材料:节点结构体及其指针数组node *g[maxn],以及理解抽象的想象力!
1 struct node{ 2 node(int vv=0, node* nn=NULL)//构造器 3 { 4 v=vv; 5 next=nn; 6 } 7 int v;//这个v表示顶点(vertex)编号 8 int cost;//根据需要可添加权值 9 node *next; 10 }*g[maxn];
增加新边:
1 for(int i=1;i<=m;i++) 2 { 3 cin>>v1>>v2>>c; 4 g[v1]=new node(v2, g[v1]); 5 g[v2]=new node(v1, g[v2]); 6 }
遍历边:
1 int i;//假设i为需要遍历边的顶点 2 int v;//同样为顶点编号 3 for(node* j=g[i];j!=NULL;j=j->nex) 4 { 5 v=j->v;//这时v就能表示与i相连的顶点了 6 /*相关操作可写在这里*/ 7 }
这个方法核心就是增加新边时的操作了。首先,表面上可以观察到,g[v]表示的应该是当前最后一次记录的顶点v的连接情况。那又是怎样能通过它实现对顶点所有连接边的遍历的呢?
看它的加边代码:g[v1]=new node(v2, g[v1])它的意思就是,使g[v1]表示的顶点为终点,并且它还能指向顶点v1的改变前的状态,那也就是一层一层地从当前边指向最初边了呀!
最后,想贴一下余老师的代码(🐟老师,yyds):
1 /*购物问题 2 * 商场打折 但是对于优惠物品有限制: 3 * 物品u与物品v不能同时购买 4 * 不会出现环 5 * 求最大节省金额数(最大能买物品金额数) 6 */ 7 #include<iostream> 8 #include<string.h> 9 10 using namespace std; 11 int k, s[1001];//商品种类与其节省金额 12 struct node //图结点定义 13 { 14 node(int vv=0, node* nn=NULL) 15 { 16 v=vv; 17 next=nn; 18 } 19 int v; 20 node *next; 21 }*g[1001];//g[i]表示第i个结点相邻的结点组成的链表 22 int tag[1001];//标记结点是否搜索过没有,1表示已搜索,0则没有 23 int funf[1001], fung[1001];//深度优先遍历该树并生成相应的有根树 24 void search(int v);//搜索出以v为根生成的有根树 25 int getf(int v);//计算以v为根的子树中选择v时,该子树的最大节省金额数 26 int getg(int v);//计算以v为根的子树中不选择v时,该子树的最大节省金额数 27 28 int main(void) 29 { 30 int m, i, v1, v2; 31 int ans = 0; 32 memset(g, 0, sizeof(g));//每个指针初始化为NULL 33 memset(tag, 0, sizeof(tag)); 34 memset(funf,-1,sizeof(funf)); 35 memset(fung,-1,sizeof(fung)); 36 cin >> k >> m;//读入商品种类与不能同时购买商品对数 37 for(i=1; i<=k; i++)//读入各类节省金额 38 cin >> s[i]; 39 for(i=1; i<=m; i++)//读入不能同时购买商品对 40 { 41 cin >> v1 >> v2; 42 g[v1]=new node(v2,g[v1]);//以插入形成邻接表 43 g[v2]=new node(v1,g[v2]); 44 } 45 //对每个还没有被访问的结点,以该结点为根生成相应的有根树,对该树用动态规划的方法求解目标函数,并累加起来作为最终答案 46 for(i=1; i<=k; i++) 47 { 48 if(tag[i] == 0) 49 { 50 search(i); 51 ans += (getf(i) > getg(i) ? getf(i) : getg(i)); 52 } 53 } 54 cout << ans; 55 return 0; 56 } 57 void search(int v)//搜索出以v为根生成的有根树 58 {//关键是在邻接表中去掉回父结点的边 59 tag[v]=1;//对每个相邻的结点 60 for(node* loop=g[v]; loop!=NULL; loop=loop->next)//对子女依次循环 61 { 62 if(tag[loop->v] == 0)//该子女没有访问过 63 { 64 node *pre=NULL, *temp; 65 for(temp=g[loop->v]; temp!=NULL; temp=temp->next) 66 { 67 if(temp->v == v) break;//邻结点是父亲,跳出 68 pre=temp; 69 } 70 if(temp)//从break退出 71 {//跳过可回到父亲的边(有根树,向下) 72 if(pre == NULL) g[loop->v] = g[loop->v]->next;//注意考虑NULL 73 else pre->next = temp->next; 74 } 75 search(loop->v);//深度搜索下一层 76 } 77 } 78 } 79 int getf(int v)//计算以v为根的子树中选择v时,该子树的最大节省金额数 80 { 81 if(funf[v] >= 0)//已经计算过 82 return funf[v]; 83 funf[v] = s[v]; 84 for(node *loop=g[v]; loop!=NULL; loop=loop->next) 85 funf[v] += getg(loop->v);//逐个累加(不选子女) 86 return funf[v]; 87 } 88 int getg(int v)//计算以v为根的子树中不选择v时,该子树的最大节省金额数 89 { 90 if(fung[v] >= 0) 91 return fung[v]; 92 fung[v] = 0; 93 for(node* loop=g[v]; loop!=NULL; loop=loop->next) 94 fung[v] += (getf(loop->v) > getg(loop->v) ? getf(loop->v) : getg(loop->v)); //判断子女选好还是不选好 95 return fung[v]; 96 }
作者语言组织能力不足,对算法的理解也还欠深刻,有很多解释地不清晰的地方,为此对大家造成理解上的困难,还请大家谅解!欢迎私信戳我交流!
浙公网安备 33010602011771号