博学之,审问之,慎思之,明辨之,笃行之。
第三章 搜索与图论
一、DFS
全排列问题(按字典序):
详情:https://www.acwing.com/problem/content/844/
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 #include <iostream> #include <vector> using namespace std;int a[10 ],n;vector<int > path; bool st[10 ];void dfs (int u) { if (u==n) { for (auto x : path) cout<<x<<' ' ; cout<<endl; return ; } for (int i=1 ;i<=n;i++){ if (!st[i]) { st[i]=true ; path.push_back (i); dfs (u+1 ); path.pop_back (); st[i]=false ; } } } int main () { cin>>n; dfs (0 ); return 0 ; }
n皇后问题
详情:https://www.acwing.com/problem/content/845/
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 #include <iostream> #include <cstring> using namespace std;const int N=20 ;bool col[N],dg[N],udg[N];int n;char path[N][N];void dfs (int u) { if (u==n) { for (int i=0 ;i<n;i++) puts (path[i]); cout<<endl; return ; } for (int i=0 ;i<n;i++){ if (!col[i] && !dg[u-i+n] && !udg[u+i]){ path[u][i]='Q' ; col[i]=dg[u-i+n]=udg[u+i]=true ; dfs (u+1 ); path[u][i]='.' ; col[i]=dg[u-i+n]=udg[u+i]=false ; } } } int main () { cin>>n; for (int i=0 ;i<n;i++){ for (int j=0 ;j<n;j++) path[i][j]='.' ; } dfs (0 ); return 0 ; }
二、BFS
走迷宫问题
详情:https://www.acwing.com/problem/content/846/
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 #include <iostream> #include <cstring> #include <vector> #include <queue> using namespace std;typedef pair<int ,int > PII;const int N=110 ;int d[N][N],map[N][N];int n,m;queue<PII> q; int dx[4 ]={0 ,0 ,1 ,-1 },dy[4 ]={1 ,-1 ,0 ,0 };int bfs (int x,int y) { memset (d,-1 ,sizeof d); d[0 ][0 ]=0 ; q.push ({0 ,0 }); while (q.size ()){ auto t=q.front (); q.pop (); for (int i=0 ;i<4 ;i++){ int a=t.first+dx[i],b=t.second+dy[i]; if (a<0 || a>=n || b<0 || b>=m || map[a][b]==1 || d[a][b]!=-1 ) continue ; q.push ({a,b}); d[a][b]=d[t.first][t.second]+1 ; } } 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>>map[i][j]; } } cout<<bfs (0 ,0 ); return 0 ; }
最少操作问题之“八数码”----数字华容道
此类问题难点在于抽象成bfs问题
详情:https://www.acwing.com/problem/content/847/
Q1:如何抽象出“节点”,用什么数据结构存储?
Q2:如何记录每个状态的“距离”(即需要移动的次数)?
Q3:队列如何定义?
A1:由初始状态进行多次变换得到的状态可以抽象成一个节点,将其用string存储,映射如下:
同时给出矩阵与字符串的转换方式:
A2:开一个哈希表,unordered_map<string,int>
后者int代表每个状态到初始状态的距离
A3:queue<string>
代码:
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 #include <iostream> #include <algorithm> #include <queue> #include <unordered_map> using namespace std;string start; string End="12345678x" ; unordered_map<string,int > d; int dx[4 ]={0 ,0 ,1 ,-1 },dy[4 ]={1 ,-1 ,0 ,0 }; int bfs (string start) { queue<string> q; q.push (start); d[start]=0 ; while (q.size ()){ auto t=q.front (); q.pop (); int distance=d[t]; if (t==End) return distance; int k=t.find ('x' ); int x=k/3 ,y=k%3 ; for (int i=0 ;i<4 ;i++){ int a=x+dx[i],b=y+dy[i]; if (a<0 || a>=3 || b<0 || b>=3 ) continue ; swap (t[k],t[3 *a+b]); if (!d.count (t)){ d[t]=distance+1 ; q.push (t); } swap (t[k],t[3 *a+b]); } } return -1 ; } int main () { for (int i=0 ;i<9 ;i++) { char ch; cin>>ch; start+=ch; } cout<<bfs (start)<<endl; return 0 ; }
三、树和图
树和图的存储
树是一种特殊的无环联通图,故都可以用图的存储来表示
有向图:a------>b
无向图:a--------b
无向图是特殊的有向图
有向图的存储方式:
树和图的遍历
深度优先遍历
1 2 3 4 5 6 7 void dfs (int u) { st[u]=true ; for (int i=h[u];i!=-1 ;i=ne[i]){ int j=e[i]; if (!st[j]) dfs (j); } }
树的重心:
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
详情:https://www.acwing.com/problem/content/848/
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 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 1e5 + 10 ; const int M = 2 * N; int h[N]; int e[M]; int ne[M]; int idx; int n; int ans = N; bool st[N]; void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; } int dfs (int u) { int res = 0 ; st[u] = true ; int sum = 1 ; for (int i = h[u]; i != -1 ; i = ne[i]) { int j = e[i]; if (!st[j]) { int s = dfs (j); res = max (res, s); sum += s; } } res = max (res, n - sum); ans = min (res, ans); return sum; } int main () { memset (h, -1 , sizeof h); cin >> n; for (int i = 0 ; i < n - 1 ; i++) { int a, b; cin >> a >> b; add (a, b), add (b, a); } dfs (1 ); cout << ans << endl; return 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 56 57 58 #include <cstring> #include <iostream> using namespace std;const int N=1e5 +10 ;int h[N], e[N], idx, ne[N];int d[N]; int n, m; int q[N]; void add (int a, int b) { e[idx]=b,ne[idx]=h[a],h[a]=idx++; } int bfs () { int hh=0 ,tt=0 ; q[0 ]=1 ; memset (d,-1 ,sizeof d); d[1 ]=0 ; while (hh<=tt) { int t=q[hh++]; for (int i=h[t];i!=-1 ;i=ne[i]) { int j=e[i]; if (d[j]==-1 ) { d[j]=d[t]+1 ; q[++tt] = j; } } } return d[n]; } int main () { cin>>n>>m; memset (h,-1 ,sizeof h); for (int i=0 ;i<m;i++) { int a,b; cin>>a>>b; add (a,b); } cout<<bfs ()<<endl; }
拓扑排序
有向无环图(拓扑图)才有拓扑序
详情:https://www.acwing.com/problem/content/850/
核心思路:
在存储图的时候记录各个点的入度
参考宽度优先遍历,将入度为0的点入队列
将队列里的点依次出队列,然后找出以所有出队列的为始点的边,删除所有边,并将另一侧的点入度减1,若该点入度为0则入队列
如果所有点都进过队列,则说明可以拓扑排序,输出顶点即可
使用数组模拟队列更好,因为可以记录队列长度,以及可以记录每一个入队元素的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool topsort () { int hh=0 ,tt=-1 ; for (int i=1 ;i<=n;i++) if (!d[i]) q[++tt]=i; while (hh<=tt){ int t=q[hh++]; for (int i=h[t];i!=-1 ;i=ne[i]){ int j=e[i]; d[j]--; if (!d[j]) q[++tt]=j; } } return tt==n-1 ; }
四、最短路
最短路问题:给定一个有向图或者无向图,求一个点到另一个点的最短距离
图的存储方式选择:对于n个点,m条边的图
n<m && n2<=10 8 则为稠密图 ,用邻接表 存储
n和m一个量级则为稀疏图 ,用邻接矩阵 存储
Dijkstra算法
朴素版本O(n^2):
核心思路:
初始化距离数组,memset(d,0x3f,sizeof)
d[1]=0
1⃣️
经过n次循环,每次循环确定一个min
加入集合st
中,n次就可以得出所有点到起点的最短距离 2⃣️
找到不在集合st
中d[]
最小的点t
3⃣️ O(n^2) -----> O(n)
将t
加入到集合st
中去 4⃣️ O(n)
用t
更新t
到所有其他点的距离(松弛操作) 5⃣️ O(m) ------>O(mlogn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int dijkstra () { memset (d,0x3f ,sizeof d); d[1 ]=0 ; for (int i=0 ;i<n;i++){ int t=-1 ; for (int j=1 ;j<=n;j++){ if (!st[j] && (t==-1 || d[t]>d[j])) t=j; } st[t]=true ; for (int j=1 ;j<=n;j++){ d[j]=min (d[j],d[t]+g[t][j]); } } if (d[n]==0x3f3f3f3f ) return -1 ; else return d[n]; }
堆优化版本O(mlogn)
核心思路:类比宽搜。对朴素版步骤3⃣️中寻找最小值做了优化,即使用了优先队列(堆),将其时间复杂度从O(n^2)优化到O(n);利用堆顶来更新其他点的距离,这样步骤5⃣️中的时间复杂度变为O(mlogn),所以总体时间复杂度为O(mlogn)
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 int dijkstra () { memset (d,0x3f ,sizeof d); d[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,dist=t.first; if (st[ver]) continue ; st[ver]=true ; for (int i=h[ver];i!=-1 ;i=ne[i]){ int j=e[i]; if (d[j]>dist+w[i]){ d[j]=dist+w[i]; heap.push ({d[j],j}); } } } if (d[n]==0x3f3f3f3f ) return -1 ; else return d[n]; }
Bellman_Ford算法
擅长解决有负权边且有限制边数的最短路问题
核心思路:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新
for n次
for 所有边
松弛操作 d[b]=min(d[b],backup[a]+w)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 typedef pair<int ,int > PII;pair<PII,int > edges[M]; int n,m,k;int d[N],backup[N]; int bellman_ford () { memset (d,0x3f ,sizeof d); d[1 ]=0 ; for (int i=0 ;i<k;i++){ memcpy (backup,d,sizeof d); for (int j=0 ;j<m;j++){ int a=edges[j].first.first,b=edges[j].first.second; int c=edges[j].second; d[b]=min (d[b],backup[a]+c); } } if (d[n]>0x3f3f3f3f /2 ) return -1 ; else return d[n]; }
Q1:dijkstra为啥不能处理带有负权边的最短路问题?
A1:在上图中运用dijkstra算法求出来的最短路径是1–>2–>4–>5,然而正确的最短路径是1–>3–>4–>5
故而dijkstra失效
Q2:backup数组有何用处?
A2:在上图中,假设最多松弛一次,如果没有backup数组,结果如下:
会产生串联的后果,即在松弛了边1–>2后,d[2]=1,如果用d[2]=1来松弛节点3,那么就多了一条边,并且 d[3]=2,很显然是错误的,于是backup数组的作用在于,只用上次松弛后的结果,避免产生因使用当前松弛更新后的节点串联更新而造成限制边数+1的后果
SPFA算法(多用!!!)
使用spfa求带负权边的最短路
引入:Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。
spfa与dijkstra的区别:
Dijkstra算法中的st数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为true后改变为false);SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,且spfa中的st数组可逆(可以在标记为true之后又标记为false)。顺带一提的是BFS中的st数组记录的是当前已经被遍历过的点。
Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。
spfa与bellman–ford的区别:
Bellman_ford算法里最后return -1的判断条件写的是dist[n]>0x3f3f3f3f/2;而spfa算法写的是dist[n]==0x3f3f3f3f;其原因在于Bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。
Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
由于SPFA算法是由Bellman_ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O(nm)O(nm) ,假如题目时间允许可以直接用SPFA算法去解Dijkstra算法的题目。
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 int spfa () { memset (d,0x3f ,sizeof d); d[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 (d[j]>d[t]+w[i]){ d[j]=d[t]+w[i]; if (!st[j]){ q.push (j); st[j]=true ; } } } } return d[n]; }
使用spfa判断负环:
求负环一般使用SPFA算法,方法是用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。
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 int cnt[N]; bool spfa () { queue<int > q; for (int i=1 ;i<=n;i++) { q.push (i); st[i]=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 (d[j]>d[t]+w[i]){ d[j]=d[t]+w[i]; cnt[j]=cnt[t]+1 ; if (cnt[j]>=n) return true ; if (!st[j]){ st[j]=true ; q.push (j); } } } } return false ; }
Floyd算法
本质:dp思想
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 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N=211 ;int d[N][N];int n,m,T;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 () { cin>>n>>m>>T; memset (d,0x3f ,sizeof d); for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= n; j ++ ) if (i == j) d[i][j] = 0 ; for (int i=0 ;i<m;i++){ int a,b,c; cin>>a>>b>>c; d[a][b]=min (d[a][b],c); } floyd (); while (T--){ int x,y; cin>>x>>y; if (d[x][y]>0x3f3f3f3f /2 ) puts ("impossible" ); else cout<<d[x][y]<<endl; } return 0 ; }
五、最小生成树
定义:给定一张带权无向图 ,由n顶点和n-1条边构成的无向联通子图被称为该图的一棵生成树,其中的权值之和最小的生成树被称为无向图的最小生成树
prime算法
用于解决稠密图,使用邻接矩阵存储
核心思想:每次将离连通部分的最近的点和点对应的边加入的连通部分,连通部分逐渐扩大,最后将整个图连通起来,并且边长之和最小
代码可以类比朴素版dijkstra,不同点在于,dijkstra中的d数组是每个点到源点的距离,而prim中的d数组则是每个点到当前生成树的距离
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 int pre[N]; int prim () { memset (d,0x3f ,sizeof d); d[1 ]=0 ; int res=0 ; for (int i=0 ;i<n;i++){ int t=-1 ; for (int j=1 ;j<=n;j++) if (!st[j] && (t==-1 || d[t]>d[j])) t=j; res+=d[t]; if (d[t]==0x3f3f3f3f ) return 0x3f3f3f3f ; st[t]=true ; for (int j=1 ;j<=n;j++) { d[j]=min (d[j],g[t][j]); } } return res; } void getPath () { for (int i=n;i>1 ;i--) { cout<<i<<' ' <<pre[i]<<' ' <<endl; } }
kruskal算法
核心思路:
将所有边按照权值大小进行升序排序,然后从小到大遍历
如果这个边的两个端点在同一集合中,说明形成了回路,舍弃;反之加入这条边,并更新集合
若筛选出来的边数小于n-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 int n,m,p[N];struct edge { int a,b,c; bool operator < (const edge &C) const { return c<C.c; } }edges[N]; int find (int x) { if (x!=p[x]) p[x]=find (p[x]); return p[x]; } int kruskal () { sort (edges,edges+m); for (int i=1 ;i<=n;i++) p[i]=i; int cnt=0 ,res=0 ; for (int i=0 ;i<m;i++){ int a=edges[i].a,b=edges[i].b,c=edges[i].c; a=find (a),b=find (b); if (a!=b){ p[a]=b; cnt++; res+=c; } } if (cnt<n-1 ) return 0x3f3f3f3f ; else return res; }
六、二分图
定义:
重要性质:二分图一定没有奇数环
eg:
下图是 二分图:
下图不是二分图:
染色法
该算法用于判断二分图
核心步骤:
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> #include <algorithm> #include <cstring> using namespace std;const int N=100010 ,M=200010 ;int e[M],ne[M],h[N],idx,color[N];int n,m;void add (int a,int b) { e[idx]=b;ne[idx]=h[a],h[a]=idx++; } bool dfs (int u,int c) { color[u]=c; for (int i=h[u];i!=-1 ;i=ne[i]){ int j=e[i]; if (!color[j]){ if (!dfs (j,3 -c)) return false ; } else if (color[j]==c) return false ; } return true ; } int main () { cin>>n>>m; memset (h,-1 ,sizeof h); for (int i=0 ;i<m;i++){ int a,b; cin>>a>>b; add (a,b),add (b,a); } bool flag=true ; for (int i=1 ;i<=n;i++){ if (!color[i]){ if (!dfs (i,1 )) { flag=false ; break ; } } } if (flag) puts ("Yes" ); else puts ("No" ); return 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 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N=510 ,M=100010 ;int e[M],ne[M],h[N],idx,;int n1,n2,m;int match[N]; int st[N];void add (int a,int b) { e[idx]=b; ne[idx]=h[a]; h[a]=idx++; } bool find (int x) { for (int i = h[x] ; i != -1 ;i = ne[i]) { int j = e[i]; if (!st[j]) { st[j] = true ; if (!match[j]||find (match[j])) { match[j] = x; return true ; } } } return false ; } int main () { cin>>n1>>n2>>m; memset (h,-1 ,sizeof h); while (m--){ int a,b; cin>>a>>b; add (a,b); } int res=0 ; for (int i=1 ;i<=n1;i++){ memset (st,0 ,sizeof st); if (find (i)) res++; } cout<<res<<endl; return 0 ; }