概述
图
是表示对象之间两两关系的数据结构。
图可以直观的由节点
和边
表示。
无向图:所有的边都是双向的。

有向图:所有的边都是单向的。

权重的图:图的每一条边是有权重的。
如1->4->3
的权重:3+2=5

有环图:从一个节点出发,能够回到同一个节点,这个路径就是一个环,不包含环的叫无环图。
树是无向无环图
图的表示:
邻接矩阵
用一个N*N的二维数组表示N个节点之间的关系。
如:
以下的图可以用矩阵表示:0表示节点i,j直接没有连接关系,1表示连接关系
>
i/j: 1 2 3 4
1 : 0 1 0 1
2 : 1 0 1 0
3 : 0 1 0 1
4 : 1 0 1 0
查找$ O(1) $, 空间$ O(V^2) $

邻接链表
如:
以下的图可以表示为
>
A1 → 2
A2 → 4
A3 → 1 → 4
A4 → 2
空间复杂度$ O(V+E) $

广度优先搜索(Breadth First Search)
BFS广度优先搜索是遍历图的一种方法。时间复杂度$ O(V+E) $
- 首先遍历当前节点层的节点
- 然后向下搜索

由于可能存在环,所有需要设置一个标志位。表示是否被访问过。
我们用一个队列(FIFO)来保存节点。
伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| BFS (G, s) let Q be queue. Q.enqueue( s ) mark s as visited. while ( Q is not empty) v = Q.dequeue( ) for all neighbours w of v in Graph G if w is not visited Q.enqueue( w ) mark w as visited.
|
过程演示

应用:计算树种节点的高度

实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| vector <int> v[10] ; int level[10]; bool vis[10]; void bfs(int s) { queue <int> q; q.push(s); level[ s ] = 0 ; vis[ s ] = true; while(!q.empty()) { int p = q.front(); q.pop(); for(int i = 0;i < v[ p ].size() ; i++) { if(vis[ v[ p ][ i ] ] == false) { level[ v[ p ][ i ] ] = level[ p ]+1; q.push(v[ p ][ i ]); vis[ v[ p ][ i ] ] = true; } } } }
|
深度优先搜索(Depth First Search)
DFS深度优先搜索利用回溯法。时间复杂度$ O(V+E) $
- 一直向后查找子节点
- 不再有任何子节点,回溯。
我们使用栈来保存节点,首先选一个节点,将相邻的节点都入栈,然后从栈中去一个节点,重复之前的操作,直到栈为空。
伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| DFS-iterative (G, s): let S be stack S.push( s ) mark s as visited. while ( S is not empty): v = S.top( ) S.pop( ) for all neighbours w of v in Graph G: if w is not visited : S.push( w ) mark w as visited DFS-recursive(G, s): mark s as visited for all neighbours w of s in Graph G: if w is not visited: DFS-recursive(G, w)
|
图解

应用:查找连通分量
连通分量
:通过边连接的节点集合。
如:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <iostream> #include <vector> using namespace std; vector <int> adj[10]; bool visited[10]; void dfs(int s) { visited[s] = true; for(int i = 0;i < adj[s].size();++i) { if(visited[adj[s][i]] == false) dfs(adj[s][i]); } } void initialize() { for(int i = 0;i < 10;++i) visited[i] = false; } int main() { int nodes, edges, x, y, connectedComponents = 0; cin >> nodes; cin >> edges; for(int i = 0;i < edges;++i) { cin >> x >> y; adj[x].push_back(y); adj[y].push_back(x); } initialize(); for(int i = 1;i <= nodes;++i) { if(visited[i] == false) { dfs(i); connectedComponents++; } } cout << "Number of connected components: " << connectedComponents << endl; return 0; }
|
input
output
1
| Number of connected components: 3
|
最小生成树(Minimum Spanning Tree)
用G=(V,E)表示V个节点,E条边的图。生存树是G的子集。
生成树的代价是所有边权重的和。最小生成树
是代价最小的生成树。
应用于网络设计、图像分割、手写识别、群集分析、邮递员问题、多端最小割问题和加权最少费和。

两种著名的查找最小生成树算法:克鲁斯卡尔算法
和普里姆
Kruskal’s Algorithm
Kruskal采用了贪心算法。
- 将图的边按照权重排序。
- 从边的权重最小的开始添加,直到最大。
- 只添加连接未连通分量的边。
那么如果判断两个节点是否连接呢?
使用DFS的时间复杂度$ O(V+E) $,最好的算法是并查集,时间复杂度$ O(ElogV) $。(Disjoint Sets)
如:

实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| #include <iostream> #include <vector> #include <utility> #include <algorithm> using namespace std; const int MAX = 1e4 + 5; int id[MAX], nodes, edges; pair <long long, pair<int, int> > p[MAX]; void initialize() { for(int i = 0;i < MAX;++i) id[i] = i; } int root(int x) { while(id[x] != x) { id[x] = id[id[x]]; x = id[x]; } return x; } void union1(int x, int y) { int p = root(x); int q = root(y); id[p] = id[q]; } long long kruskal(pair<long long, pair<int, int> > p[]) { int x, y; long long cost, minimumCost = 0; for(int i = 0;i < edges;++i) { x = p[i].second.first; y = p[i].second.second; cost = p[i].first; if(root(x) != root(y)) { minimumCost += cost; union1(x, y); } } return minimumCost; } int main() { int x, y; long long weight, cost, minimumCost; initialize(); cin >> nodes >> edges; for(int i = 0;i < edges;++i) { cin >> x >> y >> weight; p[i] = make_pair(weight, make_pair(x, y)); } sort(p, p + edges); minimumCost = kruskal(p); cout << minimumCost << endl; return 0; } `
|
Prim’s Algorithm
Prim算法也使用贪心算法,不同的是Prim算法增加的是节点。时间复杂度$ O((V+E)logV) $
- 保持两个并查集,一个作为生成树。
- 选择连接当前生成树权重最小的节点,添加到生成树种。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| #include <iostream> #include <vector> #include <queue> #include <functional> #include <utility> using namespace std; const int MAX = 1e4 + 5; typedef pair<long long, int> PII; bool marked[MAX]; vector <PII> adj[MAX]; long long prim(int x) { priority_queue<PII, vector<PII>, greater<PII> > Q; int y; long long minimumCost = 0; PII p; Q.push(make_pair(0, x)); while(!Q.empty()) { p = Q.top(); Q.pop(); x = p.second; if(marked[x] == true) continue; minimumCost += p.first; marked[x] = true; for(int i = 0;i < adj[x].size();++i) { y = adj[x][i].second; if(marked[y] == false) Q.push(adj[x][i]); } } return minimumCost; } int main() { int nodes, edges, x, y; long long weight, minimumCost; cin >> nodes >> edges; for(int i = 0;i < edges;++i) { cin >> x >> y >> weight; adj[x].push_back(make_pair(weight, y)); adj[y].push_back(make_pair(weight, x)); } minimumCost = prim(1); cout << minimumCost << endl; return 0; }
|
最短路径算法(Shortest Path Algorithms)
找到两个节点之间的最短路径,如果边的权重是1,可以使用BFS解决。
三种算法:贝尔曼算法
、迪杰斯特拉算法
和弗洛伊德算法
Bellman Ford’s Algorithm
最小的路径至少包含n-1条边,且不存在环。因为没必要经过一个节点2次。
时间复杂度:$ O(V*E) $
- 外层循环0~n-1
- 遍历所有的边,检查the next node distance > current node distance + edge weight。如果是, update the next node distance to “current node distance + edge weight”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| vector <int> v [2000 + 10]; int dis [1000 + 10]; for(int i = 0; i < m + 2; i++){ v[i].clear(); dis[i] = 2e9; } for(int i = 0; i < m; i++){ scanf("%d%d%d", &from , next , &weight); v[i].push_back(from); v[i].push_back(next); v[i].push_back(weight); } dis[0] = 0; for(int i = 0; i < n - 1; i++){ int j = 0; while(v[j].size() != 0){ if(dis[ v[j][0] ] + v[j][2] < dis[ v[j][1] ] ){ dis[ v[j][1] ] = dis[ v[j][0] ] + v[j][2]; } j++; } }
|
Dijkstra’s Algorithm
- 将所有的节点距离=无穷,除了开始节点distance=0
- 将source节点放在最小优先级队列中(distance,vertex)
- 最小优先级队列pop()一个节点,最开始时source节点
- 如果pop()的节点已经访问过,忽略
- 直到队列空
时间复杂度$ O(V+ElogV) $
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #definde SIZE 100000 + 1 vector < pair < int , int > > v [SIZE]; int dist [SIZE]; bool vis [SIZE]; void dijkstra(){ memset(vis, false , sizeof vis); dist[1] = 0; multiset < pair < int , int > > s; s.insert({0 , 1}); while(!s.empty()){ pair <int , int> p = *s.begin(); s.erase(s.begin()); int x = p.s; int wei = p.f; if( vis[x] ) continue; vis[x] = true; for(int i = 0; i < v[x].size(); i++){ int e = v[x][i].f; int w = v[x][i].s; if(dist[x] + w < dist[e] ){ dist[e] = dist[x] + w; s.insert({dist[e], e} ); } } } }
|
Floyd–Warshall’s Algorithm
1 2 3 4 5 6 7
| for(int k = 1; k <= n; k++){ for(int i = 1; i <= n; i++){ for(int j = 1; j <= n; j++){ dist[i][j] = min( dist[i][j], dist[i][k] + dist[k][j] ); } } }
|