构图总结

构图总结

最近了解了一些新的建图方法,比较难啃,但却非常实用。在此作一个小结。

两个基本构图方法:邻接表邻接矩阵

1. 邻接矩阵

名字听起来复杂,但其实是最简单的,用得也比较广吧,能用来解决一般图类问题。

若要表示顶点数V=n,边树E=m的无向图G(V, E),可定义一个二维数组g[n][n],其中g[u][v]表示顶点uv的连接状态,若有边相连可令其为1,否则为0。根据题目要求,还可令其等于uv的距离(此时无边时可令其等于无穷或-1)等等。

这样的一个二维数组,把它的各元素按矩阵排开,即所谓邻接矩阵。

对于有向图,则若有u到v的单向边,即令g[u][v]=1g[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 }

 

 

作者语言组织能力不足,对算法的理解也还欠深刻,有很多解释地不清晰的地方,为此对大家造成理解上的困难,还请大家谅解!欢迎私信戳我交流!

 

 

 

 

 

posted @ 2020-12-20 15:09  liacaca  阅读(133)  评论(0)    收藏  举报