———————————————————–还没整理好——————————————————————————–
图论有哪些部分? 1 2 3 4 5 6 7 8 9 10 1、 深度优先搜索 DFS 2、 宽度优先搜索 BFS 3、 树与图的存储 4、 树与图的深度优先遍历 5、 树与图的宽途优先遍历 6、 拓扑排序 7、 单源最短路径 8、 多源最短路径 9、 最小生成树 10、二分图
DFS 和 BFS
数据结构
空间
特点
DFS
stack
O(h)
不具有最短性
BFS
queue
O(2^h)
最短路径
DFS例题 给定一个整数 n ,将数字 1 ∼ n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式 共一行,包含一个整数 n。
输出格式 按字典序输出所有排列方案,每个方案占一行。
数据范围 1 ≤ n ≤ 7
输入样例:
输出样例: 1 2 3 4 5 6 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
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 #include <iostream> using namespace std;const int N = 10 ;int n;int path[N];bool st[N];void dfs (int u) { if (u == n) { for (int i = 0 ; i < n; i ++ ) cout << path[i] << " " ; cout << endl; return ; } for (int i = 1 ; i <= n; i ++ ) { if (st[i]) continue ; path[u] = i; st[i] = true ; dfs (u + 1 ); st[i] = false ; } } int main () { cin >> n; dfs (0 ); return 0 ; }
n-皇后问题 n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数 n,请你输出所有的满足条件的棋子摆法。
输入格式
共一行,包含整数 n。
输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中 .
表示某一个位置的方格状态为空,Q
表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1 ≤ n ≤ 9
输入样例 :
输出样例 :
1 2 3 4 5 6 7 8 9 .Q.. ...Q Q... ..Q. ..Q. Q... ...Q .Q..
方法一、已知每一行就一个皇后,不需要每个格子都遍历
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 #include <iostream> using namespace std;const int N = 20 ;int n;char g[N][N];bool col[N], dg[N], udg[N];void dfs (int u) { if (u == n) { for (int i = 0 ; i < n; i ++ ) puts (g[i]); puts ("" ); return ; } for (int i = 0 ; i < n; i ++ ) if (!col[i] && !dg[u + i] && !udg[n - u + i]) { g[u][i] = 'Q' ; col[i] = dg[u + i] = udg[n - u + i] = true ; dfs (u + 1 ); col[i] = dg[u + i] = udg[n - u + i] = false ; g[u][i] = '.' ; } } int main () { cin >> n; for (int i = 0 ; i < n; i ++ ) for (int j = 0 ; j < n ; j ++ ) g[i][j] = '.' ; dfs (0 ); return 0 ; }
我们检查棋盘是按行来检查的,其中u就是控制棋盘的行,i是控制列的。
(u, i)就是棋盘中的坐标,在一条对角线上 u + i 的值是不变的,所以只要有一个u + i 设置为true,那么就表示这条对角线已经有皇后了。
同理,反对角线的值都是n - u + i…
按照格子枚举
一行行的遍历 ,到头之后再设置到新的 一行
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 #include <iostream> using namespace std;const int N = 20 ;int n;char g[N][N];bool row[N], col[N],dg[N], udg[N];void dfs (int x, int y, int s) { if (y == n) y = 0 , x ++ ; if (x == n) { if (s == n) { for (int i = 0 ; i < n; i ++ ) puts (g[i]); puts ("" ); } return ; } g[x][y] = '.' ; dfs (x, y + 1 , s); if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) { row[x] = col[y] = dg[x + y] = udg[x - y + n] = true ; g[x][y] = 'Q' ; dfs (x, y + 1 , s + 1 ); g[x][y] = '.' ; row[x] = col[y] = dg[x + y] = udg[x - y + n] = false ; } } int main () { cin >> n; dfs (0 , 0 , 0 ); return 0 ; }
BFS 所有边的权重都是1的时候,才用BFS
例题 给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围
1 ≤ n, m ≤ 100
输入样例 :
1 2 3 4 5 6 5 5 0 1 0 0 0 0 1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0 0 1 0
输出样例 :
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 #include <iostream> #include <cstring> #include <queue> #include <algorithm> using namespace std;const int N = 110 ;typedef pair<int , int > PII;int n, m;int g[N][N], d[N][N];int bfs () { queue<PII> q; memset (d, -1 , sizeof d); d[0 ][0 ] = 0 ; q.push ({0 , 0 }); int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; while (q.size ()) { auto t = q.front (); q.pop (); for (int i = 0 ; i < 4 ; i ++ ) { int x = t.first + dx[i], y = t.second + dy[i]; if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1 ) { d[x][y] = d[t.first][t.second] + 1 ; q.push ({x, y}); } } } return d[n -1 ][m - 1 ]; } int main () { cin >> n >> m; for (int i = 0 ; i < n; i ++ ) for (int j = 0 ; j < m; j ++ ) cin >> g[i][j]; cout << bfs () << endl; return 0 ; }
树与图的深度优先遍历 无向图是一种特殊的有向图,搞两条边就可以了
有向图:邻接矩阵(二维数组)、邻接表(单链表)
树与图的广度(宽度)优先遍历
拓扑排序 有向无环图—>拓扑图 分为入度和出度,如果入度为0,说明没有任何点指向当前节点,就是把所有入度为0的点入队
1 2 3 4 5 6 7 8 while que{ t = 队头; 枚举t的所有出边t->j 删掉t->j, d[j] -- ; if d[j] = 0 d[j]入队 }
最短路 1️⃣ 单源最短路 1、所有边权都是整数(朴素Dijkstra算法 O(n²) 堆优化版的Dijkstra算法 O(mlogn)) 例题
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 nn 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×10^5 图中涉及边长均不小于 0,且不超过 10000 数据保证:如果最短路存在,则最短路的长度不超过 10^9.
输入样例 :
输出样例 :
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 <cstring> #include <iostream> #include <algorithm> #include <queue> using namespace std;typedef pair<int , int > PII;const int N = 1e6 + 10 ;int n, m;int h[N], e[N], ne[N], w[N], idx;int dist[N];bool st[N];void add (int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++; } int dijkstra () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; priority_queue<PII, vector<PII>, greater<PII>> heap; heap.push ({0 , 1 }); while (heap.size ()) { auto t = heap.top (); heap.pop (); int ver = t.second, distance = t.first; if (st[ver]) continue ; st[ver] = true ; for (int i = h[ver]; i != -1 ; i = ne[i]) { int j = e[i]; if (dist[j] > dist[ver] + w[i]) { dist[j] = dist[ver] + w[i]; heap.push ({dist[j], j}); } } } if (dist[n] == 0x3f3f3f3f ) return -1 ; return dist[n]; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); while (m -- ) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add (a, b, c); } printf ("%d\n" , dijkstra ()); return 0 ; }
2、存在负权边(Bellman-Ford O(nm) SPFA O(m), 最坏O(nm)) 例题
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数 。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible
。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible
。
数据范围
1≤n,k≤500, 1≤m≤10000, 1≤x,y≤n, 任意边长的绝对值不超过 10000。
输入样例 :
输出样例 :
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 #include <cstring> #include <iostream> #include <algorithm> using namespace std;const int N = 510 , M = 10010 ;struct Edge { int a, b, c; }edges[M]; int n, m, k;int dist[N];int last[N];void bellman_ford () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; for (int i = 0 ; i < k; i ++ ) { memcpy (last, dist, sizeof dist); for (int j = 0 ; j < m; j ++ ) { auto e = edges[j]; dist[e.b] = min (dist[e.b], last[e.a] + e.c); } } } int main () { scanf ("%d%d%d" , &n, &m, &k); for (int i = 0 ; i < m; i ++ ) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); edges[i] = {a, b, c}; } bellman_ford (); if (dist[n] > 0x3f3f3f3f / 2 ) puts ("impossible" ); else printf ("%d\n" , dist[n]); return 0 ; }
2️⃣ 多源最短路
Floyd算法 O(n³)
朴素Dijkstra算法(不能存在负权边) 1️⃣ dist[1] = 0, dist[i] = +♾️ s:当前 已经确定最短距离的点
2️⃣ for(i: 0 - n)
t<-不在s中的,距离最近的点
s<-t 用t更新其它点的距离
稠密图:邻接矩阵 稀疏图:邻接表
使用有向图的方法解决无向图的问题
堆优化版的Dijkstra算法
存储方式改成邻接表的形式
Bellman-Ford算法 (有负环 可以用这个) for() n次
for(所有边) a, b w w是 a-> b 的距离
dist[b] = min (dist[b], dist[a] + w);
三角不等式 :dist[b] <= dist[a] + w
SPFA算法(没有负环就可以用这个) 把起点放到队列里面去
while queue不空
1️⃣ t<-q.front;
q.pop();
2️⃣更新t的所有出边 t->(W) b
queue<-b
dist[N]最短距离
cnt[N] 边数
dist[x] = dist[t] + w[i];
cnt[x] = cnt[t] + 1;
cnt[x] >= n; 经过了n + 1个点
求最短路径
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数 。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible
。
数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 impossible
。
数据范围
1≤n,m≤10^5, 图中涉及边长绝对值均不超过 10000。
输入样例 :
输出样例 :
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 72 73 #include <iostream> #include <cstring> #include <algorithm> #include <queue> using namespace std;const int N = 100010 ;int n, m;int h[N], w[N], e[N], ne[N], idx;int dist[N];bool st[N];void add (int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ; } int spfa () { memset (dist, 0x3f , sizeof dist); dist[1 ] = 0 ; queue<int > q; q.push (1 ); st[1 ] = true ; while (q.size ()) { int t = q.front (); q.pop (); st[t] = false ; for (int i = h[t]; i != -1 ; i = ne[i]) { int j = e[i]; if (dist[j] > dist[t] + w[i]) { dist[j] = dist[t] + w[i]; if (!st[j]) { q.push (j); st[j] = true ; } } } } return dist[n]; } int main () { scanf ("%d%d" , &n, &m); memset (h, -1 , sizeof h); while (m -- ) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); add (a, b, c); } int t = spfa (); if (t == 0x3f3f3f3f ) puts ("impossible" ); else printf ("%d\n" , t); return 0 ; }
Floyd算法 邻接矩阵来存储
1 2 3 4 5 6 for (int k = 1 ; k <= n; k ++ ) for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= n; j ++ ) d[i][j] = min (d[i][j], d[i][k] + d[k, j]);
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible
。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。
输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible
。
数据范围
1≤n≤200, 1≤k≤n^2 1≤m≤20000, 图中涉及边长绝对值均不超过 10000。
输入样例 :
1 2 3 4 5 6 3 3 2 1 2 1 2 3 2 1 3 1 2 1 1 3
输出样例 :
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 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 210 , INF = 1e9 ;int n, m, Q;int d[N][N];void floyd () { for (int k = 1 ; k <= n; k ++ ) for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= n; j ++ ) d[i][j] = min (d[i][j], d[i][k] + d[k][j]); } int main () { scanf ("%d%d%d" , &n, &m, &Q); for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= n; j ++ ) if (i == j) d[i][j] = 0 ; else d[i][j] = INF; while (m -- ) { int a, b, c; scanf ("%d%d%d" , &a, &b, &c); d[a][b] = min (d[a][b], c); } floyd (); while (Q -- ) { int a, b; scanf ("%d%d" , &a, &b); int t = d[a][b]; if (t > INF / 2 ) puts ("impossible" ); else printf ("%d\n" , t); } return 0 ; }
最小生成树 1️⃣ Prim算法
稀疏图:朴素版 O(n²)
稠密图:堆优化版 O(mlogn)
1 2 3 4 5 dist[i]<- +♾️ for (int i = 0 ; i < n ; i ++ ) t<- 找到集合外距离最近的点 用 t 更新其它点到集合的距离 st[t] = true ;
2️⃣克鲁斯卡尔算法(Kruskal) O(mlogm)
一般来说 ,稠密图 朴素版Prim算法 稀疏图 克鲁斯卡尔算法
现将所有边按照权重从小到大排序O(mlogm)
枚举每条边 a, b 权重 c O(m)
if(a, b)不连通
将这条边加入集合
二分图 就是顶点集V可分割为两个互不相交的子集 ,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集 ,两个子集内的顶点不相邻 。(不一定是连通图)
1️⃣ 染色法 O(m + n)
判断是不是二分图 当且仅当途中不含奇数环(边的数量是奇数)
对每一个点进行染色操作 ,只用黑白两种颜色;
💡如果图中存在奇数环(构成环的顶点数量是奇数),那一定不是二分图),下图可以看到,依次选一个点,进行染色(原则是相邻的点要染于该点不同色),奇数环的染色结果会出现矛盾。;【必要性】
💡如果不含奇数环,一定是二分图【充分性】 如果没有奇数环,那么剩下的点的关系就是:偶数环or单链。这两种情况都能保证同一条边上相邻顶点在不同集合中,所以也是成立的;
💡总结
用dfs和bfs两种方式去实现,对图进行遍历并染色;由于上面原理可知,二分图一定不含奇数环,不含奇数环的图一定为二分图
所以,只要在染色过程中不存在矛盾(这里用黑白进行染色,即一个点不能即为黑色,又为红色),整个图遍历完成之后,所有顶点都顺利染上色。就说明这是一个二分图!
2️⃣ 匈牙利算法 O(mn) 实际运行时间一般远小于O(mn)
什么是二分图的最大匹配? 💡匹配(本质是一个边的集合!) 给定一个二分图S,在S的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
💡极大匹配 极大匹配是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数。(也就是说,再加入任意一条不在匹配集合中的边,该边肯定有一个顶点已经在集合中的边中了)
💡最大匹配 所有极大匹配当中边数最大的一个匹配
💡最大匹配问题 选择这样的边数最大的子集称为图的最大匹配问题