图论相关

内容

  1. 并查集
  2. 邻接表
  3. 最短路1:Dijkstra算法
  4. 最短路2:Bellman-Ford算法
  5. 最短路3:Floyd-Warshall算法
  6. 最小生成树:Prime算法
  7. 最小生成树:Kruskal算法

并查集

大佬博客:讲的很生动。

 

邻接表

 刚开始学邻接表时,看的是网上的数组模拟链表版本,很久才看懂是什么意思。后面发现白书也有领接表,发现更为简单(早知道看白书了😭)。所以这里我就讲邻接表的简单实现。

 

vector:

这里的vector是c++ stl的一个容器,可以参见这里

 

二维数组存图:

对于这个图来说:

在离散数学中,我们知道可以用一个二维矩阵来存这个图:

相应地在C语言中要开一个大小为G[7][7]的二维数组,其中:

1.数组全部初始化为0

2.G[u][v] == 1代表u和v之间有一条边,G[u][v] == 0代表u和v之间没有直接相连的边

于是我们可以这样存图:当数组全都初始化为0后,G[2][1] = 1, G[2][3] = 1, G[2][4] = 1, G[2][5] = 1, G[2][6] = 1。

但是,我们发现:这个二维数组有一片很大的空间,没有存储其他边的信息。假如有10的5次方这么多个点,在C/C++中是开不了这么大的数组(10的10次方,超过一亿了)。那我们如何高效地存下这些边,摒弃无用的信息?答案就是邻接表。

 

邻接表:

还是这个图:

我们通过观察发现:源点是2,而它的邻接点是:1,3,4,5,6。所以,我们可不可以用一个方法,存:以源点为核心,然后通过这个源点来遍历所有的邻接点的图?答案就是这样存(vector数组实现):

 

 1 #include <cstdio>
 2 #include <iostream>
 3 #include <vector>
 4 using namespace std;
 5 vector<int> G[7];
 6 int u = 2;
 7 int to[7] = {1,3,4,5,6};
 8 
 9 int main(){
10     for(int i = 0; i < 5; i++){
11         G[u].push_back(to[i]);
12     }
13     return 0;
14 }

 

遍历边的方式(知道源点u):

1     int u = 2;
2     for(int i = 0; i < G[u].size(); i++){
3         printf("%d ", G[u][i]);
4     }

为什么这样可以节省空间?因为我们用了vector的特点:不定长。当要加入元素时,vector就会自动申请空间。可能有的同学就有疑问:可不可以用链表实现?当然可以,只不过因为acm的缘故,这个邻接表的实现方法是最简便的,想要追求速度,可以用链表代替vector。另外还有一种常见的实现方式:数组模拟链表,这里我就不多讲了,网上很多实现方式,重点还是要理解邻接表的核心(以点为中心,存边)是什么,抓住重点看应该会好很多。下面给出做题模板:

 1 #include <cstdio>
 2 #include <iostream>
 3 #include <vector>
 4 using namespace std;
 5 const int maxn = 1e5+5;
 6 int n, m;   //n:点的数量  m:边的数量
 7 struct edge{   //一般关于图的问题中, 边都是由权值的,这里就用结构体存边
 8     int to, cost;
 9     //to:邻接点  cost:邻接边的权值
10 };
11 vector<edge> G[maxn];   //vector数组存边
12 
13 int main(){
14     scanf("%d%d", &n, &m);
15     int u, v, cost;  //u:源点  v:目标点  cost:权值 
16     edge temp;    //临时的边
17     
18     for(int i = 0; i < m; i++){
19         scanf("%d%d%d", &u, &v, &cost);
20         temp.cost = cost;
21         
22         temp.to = v;
23         G[u].push_back(temp);
24         //如果是双向的边, 还要加语句: temp.to = u; G[v].push_back(temp);
25     }
26     return 0;
27 }

 

 

Dijkstra算法

当初自己看的一篇比较友好的博客:点我

解决什么问题:从一个点(称为源点)到剩余所有点的距离。

基本思想:贪心

算法过程:每次从:离源点最近,且未访问过的点,去更新邻接点到源点的距离。

过程讲述:

图:

我们要计算:从1到各个点的最短距离。

刚开始应该是这样:

自己到自己的距离初始化为0,其余设为无穷大。

从源点1开始,它的邻接点是2,所以1到2的距离更新为dis[1]+1 = 1。

这时,离源点1的最近,且没作为“主动更新”的点是2,所以我们从2开始,去更新它的邻接点:

dis[3] = dis[2] + 4 = 5

dis[4] = dis[2] + 8 = 9

dis[5] = dis[2] + 5 = 6

dis[6] = dis[2] + 2 = 3

更新完后:

我们发现,这时离源点最近,且没作为“主动更新“的点是6,所以从6开始更新邻接点:

在更新点2时,因为dis[6] + 2 = 3 + 2 = 5 > dis[2] = 1,所以不更新dis[2]。

接下来,离源点最近,且没作为“主动更新”的点是3,所以从3开始更新邻接点:

这时:dis[3] + 3 = 6 < dis[4] = 9,所以更新dis[4]为6;dis[3] + 2 = 7 > dis[2] = 1,所以不更新dis[2]。

更新完后就是这样:

这时,离源点最近,且没作为“主动更新”的点有4和5,所以随便选一个点来更新。最终的更新结果:

写成代码(领接表实现):

 

 1 #include <cstdio>
 2 #include <iostream>
 3 #include <vector>
 4 using namespace std;
 5 const int maxn = 1e5+5;
 6 const int inf = 1e9;
 7 int n, m;
 8 struct edge{
 9     int to, cost;
10 };
11 vector<edge> G[maxn];
12 
13 int dis[maxn];    //到源点的距离
14 int vis[maxn];    //“主动更新”点的标记
15 
16 void dijkstra(){
17     //初始化
18     for(int i = 1; i <= n; i++){
19         dis[i] = inf;
20     }
21     dis[1] = 0;
22     
23     while(1){
24         //找离源点最近, 且未“主动更新”别人的点
25         int minn = 1e9, u = -1;
26         for(int i = 1; i <= n; i++){
27             if(vis[i] == 0 && dis[i] > minn){   
28                 minn = dis[i];
29                 u = i;
30             }
31         }
32         if(u == -1) break;   //找不到说明所有点已更新完毕, 退出即可
33         
34         vis[u] = 1;    //打上标记
35         
36         int to, cost;
37         //更新邻接点
38         for(int i = 0; i < G[u].size(); i++){
39             to = G[u][i].to;
40             cost = G[u][i].cost;
41             if(dis[to] > dis[u]+cost){
42                 dis[to] = dis[u]+cost;
43             } 
44         }
45     }
46 }
47 
48 int main(){
49     //输入同邻接表
50     scanf("%d%d", &n, &m);
51     int u, v, cost;
52     edge temp;
53     
54     for(int i = 0; i < m; i++){
55         scanf("%d%d%d", &u, &v, &cost);
56         temp.cost = cost;
57         
58         temp.to = v;
59         G[u].push_back(temp);
60         temp.to = u;
61         G[v].push_back(temp);
62     }
63     
64     dijkstra();   //dijkstra算法
65     
66     return 0;
67 }

 

代码比较长,要有耐心看(〃` 3′〃)(可能会有些小错误,欢迎指正ヾ(•ω•`)o)

 

这个算法的时间复杂度是O(n*m),当点n == 1e5(10的5次方),m == 1e5时,这个复杂度要爆炸!是O(1e10)的复杂度。我们设计出来的算法时间复杂度应该要小于O(1e7)(极限1e8),才能在1s内运行完,否则会超时。那么我们怎么优化时间呢?

我们发现,每个边肯定是要遍历的,所以这里的m优化不了,那只能优化n了,n的复杂度在代码中是:找离源点最近的点。如果我们能节省这部分的时间,就可以优化成功。这里较好的解决办法是用优先队列(C++ STL,内部用数据结构:“堆”,来实现),可以把n优化成logn,然后复杂度变成了O(logn * m)。当n == 1e5时,logn大概是17,也就是总的复杂度是O(1e6),完全可以接受。我这里直接给出代码:

 

 1 #include <cstdio>
 2 #include <iostream>
 3 #include <vector>
 4 #include <queue>
 5 using namespace std;
 6 const int maxn = 1e5+5;
 7 const int inf = 1e9;
 8 int n, m;
 9 struct edge{
10     int to, cost;
11 };
12 vector<edge> G[maxn];
13 
14 int dis[maxn];    //到源点的距离
15 int vis[maxn];    //“主动更新”点的标记
16 
17 struct cmp{
18     bool operator () (int a, int b){
19         return dis[a] > dis[b];  //注意这里的比较级
20     }
21 };
22 
23 void dijkstra(){
24     //初始化
25     for(int i = 1; i <= n; i++){
26         dis[i] = inf;
27     }
28     dis[1] = 0;
29 
30     priority_queue<int, vector<int>, cmp> q;  //创建队列
31 
32     int u;
33     while(!q.empty()){
34         u = q.top(); q.pop();   //取出离源点最近的点, 然后弹出队列
35         if(vis[u]) continue;    //被作为“主动更新”的点就跳过
36 
37         vis[u] = 1;    //打上标记
38 
39         int to, cost;
40         //更新邻接点
41         for(int i = 0; i < G[u].size(); i++){
42             to = G[u][i].to;
43             cost = G[u][i].cost;
44             if(dis[to] > dis[u]+cost){
45                 dis[to] = dis[u]+cost;
46                 q.push(to);
47             }
48         }
49     }
50 }
51 
52 int main(){
53     //输入同邻接表
54     scanf("%d%d", &n, &m);
55     int u, v, cost;
56     edge temp;
57 
58     for(int i = 0; i < m; i++){
59         scanf("%d%d%d", &u, &v, &cost);
60         temp.cost = cost;
61 
62         temp.to = v;
63         G[u].push_back(temp);
64         temp.to = u;
65         G[v].push_back(temp);
66     }
67 
68     dijkstra();   //dijkstra算法
69 
70     return 0;
71 }

 

Bellman-Ford算法

 

Floyd-Warshall算法

网上有很多解释,这里仅给出模板和用法:

用在什么地方:需要求任意两点间的最短路。复杂度:O(n^3)

模板:

 

 1 #include <cstdio>
 2 #include <iostream>
 3 using namespace std;
 4 const int maxn = 1005;
 5 const int inf = 1e9;   //无穷大
 6 int n, m;   //n:点的数量  m:边的数量
 7 int G[maxn][maxn];   //
 8 
 9 int main(){
10     scanf("%d%d", &n, &m);
11     int u, v, cost;    //u:源点  v:终点  cost:权值
12     
13     //初始化
14     for(int i = 1; i <= n; i++){
15         for(int j = 1; j <= n; j++){
16             G[i][j] = inf;
17         }
18         G[i][i] = 0;
19     }
20     
21     //输入边
22     for(int i = 0; i < m; i++){
23         scanf("%d%d%d", &u, &v, &cost);
24         G[u][v] = cost;
25     }
26     
27     //Floyd算法
28     for(int i = 1; i <= n; i++){
29         for(int j = 1; j <= n; j++){
30             for(int k = 1; k <= n; k++){
31                 if(G[i][j] > G[i][k] + G[k][j])
32                     G[i][j] = G[i][k] + G[k][j];
33                 //可以这样记:如果有中转站k,使得i-k的距离+k-j的距离小于i-j的直接距离,那么更新
34             }
35         }
36     }
37     
38     return 0;
39 }

Prime算法

Kruskal算法

暂更

 

posted @ 2019-03-10 17:44  MrEdge  阅读(154)  评论(0编辑  收藏  举报