2017清北学堂集训笔记——图论

Posted Memoryヾノ战心

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2017清北学堂集训笔记——图论相关的知识,希望对你有一定的参考价值。

我们进入一个新的模块——图论!

emmmmm这个专题更出来可能有点慢别介意,原因是要划的图和要给代码加的注释比较多,更重要的就是。。。这几个晚上我在追剧!!我们的少年时代超级超级超级好看,剧情很燃啊!!咳咳,好吧下面回归正题。

一、图的存储:

1、邻接矩阵:

假设有n个节点,建立一个n×n的矩阵,第i号节点能到达第j号节点就将[i][j]标记为1(有权值标记为权值),

样例如下图:

 1 /*无向图,无权值*/
 2 int a[MAXN][MAXN];//邻接矩阵 
 3 int x,y;//两座城市 
 4 for(int i=1;i<=n;i++)
 5 {
 6     for(int j=1;j<=n;j++)
 7     {
 8         scanf("%d%d",&x,&y);//能到达,互相标记为1 
 9         a[x][y]=1;
10         a[y][x]=1;
11     }
12 }
13 /*无向图,有权值*/
14 int a[MAXN][MAXN];//邻接矩阵 
15 int x,y,w;//两座城市,路径长度 
16 for(int i=1;i<=n;i++)
17 {
18     for(int j=1;j<=n;j++)
19     {
20         scanf("%d%d%d",&x,&y,&w);//能到达,互相标记为权值w 
21         a[x][y]=w;
22         a[y][x]=w;
23     }
24 }
25 /*有向图,无权值*/
26 int a[MAXN][MAXN];//邻接矩阵 
27 int x,y;//两座城市 
28 for(int i=1;i<=n;i++)
29 {
30     for(int j=1;j<=n;j++)
31     {
32         scanf("%d%d",&x,&y);//能到达,仅仅是x到y标记为1 
33         a[x][y]=1;
34     }
35 }
36 /*有向图,有权值*/
37 int a[MAXN][MAXN];//邻接矩阵 
38 int x,y,w;//两座城市,路径长度 
39 for(int i=1;i<=n;i++)
40 {
41     for(int j=1;j<=n;j++)
42     {
43         scanf("%d%d%d",&x,&y,&w);//能到达,仅仅是x到y标记为权值w 
44         a[x][y]=w;
45     }
46 }

邻接矩阵很方便,但是在n过大或者为稀疏图时,就会很损耗时空,不建议使用!

2.邻接表:

邻接表是一个二维容器,第一维描述某个点,第二维描述这个点所对应的边集们。

邻接表由表头point,链点构成,如下图是一个简单无向图构成的邻接表:

我们可以用指针来创建链表,当然,这是很复杂也很麻烦的事情,下面来介绍一种用数组模拟链表的方法:

 1 //有向图邻接表存储 
 2 const int N=1005;
 3 const int M=10050;
 4 int point[N]={0};//i节点所对应链表起始位置(表头) 
 5 int to[M]={0};
 6 int next[M]={0};//i节点下一个所指的节点 
 7 int cc=0;//计数器(表示第几条边) 
 8 void AddEdge(int x,int y)//节点x到y
 9 {
10     cc++;
11     to[cc]=y;
12     next[cc]=point[x];
13     point[x]=cc;
14 }
15 void find(int x)
16 {
17     int now=point[x];
18     while(now)
19     {
20         printf("%d\\n",to[now]);
21         now=next[now];
22     }
23 }
24 int main()
25 {
26     
27 }

具体的过程我也不是很懂怎么描述,反正如果要加强记忆的话可以用我所给的例子模拟一下point[],to[],next[],然后再调用函数find(x)来输出x这个节点能到的点,大概就能YY到数组是怎么存储邻接表的了。

还是不理解的话,推一个blog,这里面说的和我这里给出的思路很相似:http://developer.51cto.com/art/201404/435072.htm

二、树的遍历:

1.BFS:运用队列,一开始队列中有一个点, 将一个点出队,将它的子结点全都入队。

算法会在遍历完一棵树中每一层的每个结点之后,才会转到下一层继续,在这一基础上,队列将会对算法起到很大的帮助:

 1 //广度优先搜索
 2 void BreadthFirstSearch(BitNode *root)
 3 {
 4     queue<BitNode*> nodeQueue;
 5     nodeQueue.push(root);//将根节点压入队列 
 6     while (!nodeQueue.empty())//队列不为空,继续压入队列 
 7     {
 8         BitNode *node = nodeQueue.front();
 9         nodeQueue.pop();//弹出根节点 
10         if (node->left)//左儿子不为空 
11         {
12             nodeQueue.push(node->left);//压入队列 
13         }
14         if (node->right)//右儿子不为空 
15         {
16             nodeQueue.push(node->right);//压入队列 
17         }
18     }
19 }

2.DFS:运用栈,递归到一个点时,依次递归它的子结点。

还可以利用堆栈的先进后出的特点,现将右子树压栈,再将左子树压栈,这样左子树就位于栈顶,可以保证结点的左子树先与右子树被遍历:

 1 //深度优先搜索
 2 //利用栈,现将右子树压栈再将左子树压栈
 3 void DepthFirstSearch(BitNode *root)
 4 {
 5     stack<BitNode*> nodeStack;
 6     nodeStack.push(root);//将根节点压栈 
 7     while (!nodeStack.empty())//栈不为空,继续压栈 
 8     {
 9         BitNode *node = nodeStack.top();//引用栈顶 
10         cout << node->data << \' \';
11         nodeStack.pop();//弹出根节点 
12         if (node->right)//优先遍历右子树 
13         {
14             nodeStack.push(node->right);
15         }
16         if (node->left)
17         {
18             nodeStack.push(node->left);
19         }
20     }
21 }

三、无根树变成有根树:

  选择一个点作为根结点, 开始遍历。

  遍历到一个点时, 枚举每一条连接它和另一个点的边。 若另一个点不是它的父结点, 那就是它的子结点。 递归到子结点。

  我们可以更加形象的比喻为:抓住一个点,把它拎起来构成一棵新的树。

四、并查集:

这是我学OI这么久以来觉得性价比最高的算法(简单又实用啊!!),用来处理不相交合并和查询问题。

给大家推个超超超超级易懂的blog,保证一看就懂,这里我就不再详解了:http://blog.csdn.net/dellaserss/article/details/7724401

五、最小生成树:

1.Prim算法(适用于稠密图):

算法描述

1).输入:一个加权连通图,其中顶点集合为V,边集合为E;
2).初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
3).重复下列操作,直到Vnew = V:
a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;
4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。
 1 #include<stdio.h>//普里姆算法
 2 const int N=1050;
 3 const int M=10050;
 4 struct Edge//定义图类型结构体,a到b权值为c
 5 {
 6     int a,b,c;
 7 }edge[M];
 8 int n,m;//n个点,m条边 
 9 bool black[N];//染黑这个点,表示这个点已经被选过了 
10 int ans=0;//最小生成树权值和
11 int main()
12 {
13     int i,j,k;
14     scanf("%d%d",&n,&m);
15     for(i=1;i<=m;i++)
16     scanf("%d%d%d",&edge[i].a,&edge[i].b,&edge[i].c);
17     black[1]=1;//把第一个点染黑(从第一个点找起)
18     for(k=1;k<n;k++)
19     {
20         int mind,minz=123456789;
21         for(i=1;i<=m;i++)//开始! 
22         {
23             if(black[edge[i].a]!=black[edge[i].b]&&edge[i].c<minz)//如果这个点未被找过并且权值比当前最优值还要小,更新之 
24             {
25                 mind=i;//记录当前最优点 
26                 minz=edge[i].c;//记录当前最小边权 
27             }
28         }
29         /*******************************************///将这最优点归入 
30         ans+=minz;//答案加上 
31         black[edge[mind].a]=1;//染黑两个节点 
32         black[edge[mind].b]=1;
33         /*******************************************/
34     }
35     printf("%d\\n",ans);//输出答案 
36     return 0;
37 }

2.kruskal算法(适用于稀疏图):

算法描述

克鲁斯卡尔算法从另一途径求网的最小生成树。

假设连通网N=(V,{E}),则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{∮}),图中每个顶点自成一个连通分量。

在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。

依次类推,直至T中所有顶点都在同一连通分量上为止。

 

 1 #include<cstdio>//克鲁斯卡尔算法
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 const int N=1050;
 6 const int M=10050;
 7 struct Edge//定义图类型结构体
 8 {
 9     int a,b,c;//a到b的权值为c
10 }edge[M];
11 int fa[N];//父亲数组
12 int n,m;//n个节点,m条边
13 int ans=0;//最小生成树权值和
14 bool cmp(Edge x,Edge y)//比较权值大小 
15 {
16     return (x.c<y.c);
17 }
18 int getf(int x)//寻找x的最原始祖先(并查集) 
19 {
20     if(fa[x]!=x)
21     fa[x]=getf(fa[x]);
22     return fa[x];//返回最原始祖先 
23 }
24 int main()
25 {
26     int i,j;
27     scanf("%d%d",&n,&m);
28     for(i=1;i<=m;i++)
29     scanf("%d%d%d",&edge[i].a,&edge[i].b,&edge[i].c);
30     sort(edge+1,edge+m+1,cmp);//从小到大排序边数组 
31     for(i=1;i<=n;i++)
32     fa[i]=i;//初始值,每个节点的父亲就是自己 
33     for(i=1;i<=m;i++)
34     {
35         int a=edge[i].a;
36         int b=edge[i].b;
37         a=getf(a);//寻找a的最原始祖先 
38         b=getf(b);//寻找b的最原始祖先 
39         if(a!=b)//如果两个的最终祖先不相同(不会构成回路) 
40         {
41             ans+=edge[i].c;//加入 
42             fa[a]=b;//加入当前父亲的儿子们中(合并并查集)
43         }
44     }
45     printf("%d\\n",ans);
46     return 0; 
47 }

经典例题:繁忙的都市(Luogu 2330)

城市C是一个非常繁忙的大都市,城市中的道路十分的拥挤,于是市长决定对其中的道路进行改造。城市C的道路是这样分布的:城市中有n个交叉路口,有些交叉路口之间有道路相连,两个交叉路口之间最多有一条道路相连接。这些道路是双向的,且把所有的交叉路口直接或间接的连接起来了。每条道路都有一个分值,分值越小表示这个道路越繁忙,越需要进行改造。但是市政府的资金有限,市长希望进行改造的道路越少越好,于是他提出下面的要求:

1.改造的那些道路能够把所有的交叉路口直接或间接的连通起来。

2.在满足要求1的情况下,改造的道路尽量少。

3.在满足要求1、2的情况下,改造的那些道路中分值最大的道路分值尽量小。

任务:作为市规划局的你,应当作出最佳的决策,选择那些道路应当被修建。

这题是经典的最小瓶颈生成树问题只用边权小于等于x的边,看看能不能构成最小生成树

在kruskal算法中,我们已经对边从小到大排过序了,所以只要用≤x的前若干条边即可

3.最小生成树计数问题:

题目:现在给出了一个简单无向加权图。你不满足于求出这个图的最小生成树,而希望知道这个图中有多少个不同的最小生成树(如果两颗最小生成树中至少有一条边不同,则这两个最小生成树就是不同的)

解法:按边权排序,先选小的,相同边权的暴力求出有几种方案,将边按照权值大小排序,将权值相同的边分到一组,统计下每组分别用了多少条边。然后对于每一组进行dfs,判断是否能够用这一组中的其他边达到相同的效果。最后把每一组的方案数相乘就是答案。 

换句话说:就是不同的最小生成树方案,每种权值的边的数量是确定的,每种权值的边的作用是确定的, 排序以后先做一遍最小生成树,得出每种权值的边使用的数量x然后对于每一种权值的边搜索,得出每一种权值的边选择方案。

 1 #include<cstdio>
 2 #include<algorithm>
 3 #define N 105
 4 #define M 1005
 5 #define MOD 31011
 6 using namespace std;
 7 struct node//定义图类型结构体 
 8 {
 9     int a,b;//节点a,b 
10     int zhi;//a到b的权值 
11 }xu[M];
12 int n,m;
13 int fa[N];
14 int lian[N];
15 int ans=1;
16 int cmp(struct node x,struct node y)//从小到大排序函数 
17 {
18     return (x.zhi<y.zhi);
19 }
20 int getf(int x)
21 {
22     if(fa[x]!=x)
23         fa[x]=getf(fa[x]);
24     return(fa[x]);
25 }
26 int getlian(int x)
27 {
28     if(lian[x]==x)
29         return x;
30     return ( getlian(lian[x]) );
31 }
32 int dfs(int now,int end,int last)
33 {
34     if(now==end)
35     {
36         if(last==0)
37             return 1;
38         return 0;
39     }
40     int res=dfs(now+1,end,last);
41     int s=getlian(xu[now].a);
42     int t=getlian(xu[now].b);
43     if(s!=t)
44     {
45         lian[s]=t;
46         res+=dfs(now+1,end,last-1);
47         lian[s]=s;
48     }
49     return res;
50 }
51 int main()
52 {
53     int i,j,k;
54     int s,t;
55     int now;
56     int sum=0;
57     scanf("%d%d",&n,&m);
58     for(i=1;i<=n;i++)//初始化,每个节点的父亲就是自己 
59         fa[i]=i;
60     for(i=1;i<=m;i++)
61         scanf("%d%d%d",&xu[i].a,&xu[i].b,&xu[i].zhi);
62     sort(xu+1,xu+m+1,cmp);//从小到大排序边数组 
63     for(i=1;i<=m;)
64     {
65         for(j=1;j<=n;j++)
66             lian[j]=j;
67         k=i;
68         while(i<=m&&xu[i].zhi==xu[k].zhi)
69         {
70             xu[i].a=getf(xu[i].a);
71             xu[i].b=getf(xu[i].b);
72             i++;
73         }
74         now=sum;
75         for(j=k;j<i;j++)
76         {
77             s=getf(xu[j].a);
78             t=getf(xu[j].b);
79             if(s!=t)
80             {
81                 sum++;
82                 fa[s]=t;
83             }
84         }
85         ans*=dfs(k,i,sum-now);
86         ans%=MOD;//防止溢出 
87     }
88     if(sum!=n-1)
89         ans=0;
90     printf("%d\\n",ans);
91     return 0;
92 }

六、最短路径:

1.Floyd算法(插点法):

通过一个图的权值矩阵求出它的每两点间的最短路径(多源最短路)。

算法描述

一个十分暴力又经典的DP,假设i到j的路径有两种状态:

①i和j直接有路径相连:

②i和j间接联通,中间有k号节点联通:

假设dis[i][j]表示从i到j的最短路径,对于存在的每个节点k,我们检查一遍dis[i][k]+dis[k][j]。

 1 //Floyd算法,时间复杂度:O(n^3) 
 2 int dis[MAXN][MAXN];
 3 for(k=1;k<=n;k++)//枚举 
 4 {
 5     for(i=1;i<=n;i++)
 6     {
 7         for(j=1;j<=n;j++)
 8         {
 9             dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);//DP
10         }
11     }
12 }

2.Dijkstra算法(无向图,无负权边):

算法描述

多源最短路!

a.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。

b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。

c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。

d.重复步骤b和c直到所有顶点都包含在S中。

啊~上面的的乱七八糟的概念太难懂了,还是举个例子吧!如下图!

暑假清北学堂集训笔记

清北学堂-图论--二进制拆分--最短路--潜伏

清北学堂模拟赛d2t1 一道图论神题(god)

网课总结——2022清北学堂

清北学堂模拟赛d1t4 一道图论好题(graph)

清北学堂国庆day3解题报告