AcWing算法提高课【第二章搜索】多源BFS、最小步数模型、双端队列广搜、双向搜索、A*
多源BFS
题目:
给定一个 N 行 M 列的 01矩阵 A,A[i][j] 与 A[k][l] 之间的曼哈顿距离定义为: dist(A[i][j],A[k][l])=|i−k|+|j−l| 输出一个 N 行 M 列的整数矩阵 B,其中: B[i][j]=min1≤x≤N,1≤y≤M,A[x][y]=1dist(A[i][j],A[x][y]) 输入格式 第一行两个整数 N,M。 接下来一个 N 行 M 列的 01 矩阵,数字之间没有空格。 输出格式 一个 N行 M 列的矩阵 B,相邻两个整数之间用一个空格隔开。 数据范围 1≤N,M≤1000 输入样例: 3 4 0001 0011 0110 输出样例: 3 2 1 0 2 1 0 0 1 0 0 1
分析:
就是求0所在的位置,到1的最近的曼哈顿距离。
我们可以将所有的1放入队列中,表示bfs的第一层,然后搜下去。
代码:
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 5 using namespace std; 6 7 typedef pair<int, int> PII; 8 9 #define x first 10 #define y second 11 12 const int N = 1010; 13 14 int n, m; 15 char g[N][N]; 16 PII q[N * N]; 17 int d[N][N]; 18 int hh, tt; 19 20 void bfs() 21 { 22 int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; 23 while (hh <= tt) 24 { 25 PII t = q[++ hh]; 26 27 for (int i = 0; i < 4; i ++ ) 28 { 29 int x = t.x + dx[i], y = t.y + dy[i]; 30 if (x < 0 || x >= n || y < 0 || y >= m) continue; 31 if (g[x][y] == '1') continue; 32 if (d[x][y] != -1) continue; 33 34 d[x][y] = d[t.x][t.y] + 1; 35 q[++ tt] = {x, y}; 36 } 37 } 38 } 39 int main() 40 { 41 scanf("%d%d", &n, &m); 42 for (int i = 0; i < n; i ++ ) scanf("%s", g[i]); 43 44 memset(d, -1, sizeof d); 45 46 for (int i = 0; i < n; i ++ ) 47 for (int j = 0; j < m; j ++ ) 48 if (g[i][j] == '1') 49 { 50 q[ ++ tt] = {i, j}; 51 d[i][j] = 0; 52 } 53 54 bfs(); 55 56 for (int i = 0; i < n; i ++ ) 57 { 58 for (int j = 0; j < m; j ++ ) 59 printf("%d ", d[i][j]); 60 puts(""); 61 } 62 63 64 return 0; 65 }
最小步数模型
题目:
Rubik 先生在发明了风靡全球的魔方之后,又发明了它的二维版本——魔板。 这是一张有 8 个大小相同的格子的魔板: 1 2 3 4 8 7 6 5 我们知道魔板的每一个方格都有一种颜色。 这 8 种颜色用前 8 个正整数来表示。 可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。 对于上图的魔板状态,我们用序列 (1,2,3,4,5,6,7,8) 来表示,这是基本状态。 这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态): A:交换上下两行; B:将最右边的一列插入到最左边; C:魔板中央对的4个数作顺时针旋转。 下面是对基本状态进行操作的示范: A: 8 7 6 5 1 2 3 4 B: 4 1 2 3 5 8 7 6 C: 1 7 2 4 8 6 3 5 对于每种可能的状态,这三种基本操作都可以使用。 你要编程计算用最少的基本操作完成基本状态到特殊状态的转换,输出基本操作序列。 注意:数据保证一定有解。 输入格式 输入仅一行,包括 8 个整数,用空格分开,表示目标状态。 输出格式 输出文件的第一行包括一个整数,表示最短操作序列的长度。 如果操作序列的长度大于0,则在第二行输出字典序最小的操作序列。 数据范围 输入数据中的所有数字均为 1 到 8 之间的整数。 输入样例: 2 6 8 4 5 7 3 1 输出样例: 7 BCABCCB
分析:
还是最短路模板,这次我们不是将路放到了棋盘上,而是变成了状态的变化,一个思想。
代码:
1 #include <bits/stdc++.h> 2 3 using namespace std; 4 5 unordered_map<string, int> dist;//表示到当前字符的距离 6 unordered_map<string, pair<char, string> > pre;//标记路径 7 8 char g[2][4]; 9 10 void sets(string s) 11 { 12 for (int i = 0; i < 4; i ++ ) g[0][i] = s[i]; 13 for (int i = 7, j = 0; i >= 4; i -- , j ++ ) g[1][j] = s[i]; 14 } 15 16 string get() 17 { 18 string s; 19 for (int i = 0; i < 4; i ++ ) s += g[0][i]; 20 for (int i = 3; i >= 0; i -- ) s += g[1][i]; 21 return s; 22 } 23 24 string get0(string s) 25 { 26 sets(s); 27 //兑换两行 28 swap(g[0], g[1]); 29 30 return get(); 31 } 32 33 string get1(string s) 34 { 35 sets(s); 36 //将最后一列放到第一列前面 37 char c1 = g[0][3], c2 = g[1][3]; 38 for (int i = 0; i < 2; i ++ ) 39 for (int j = 3; j >= 1; j -- ) 40 g[i][j] = g[i][j - 1]; 41 g[0][0] = c1, g[1][0] = c2; 42 return get(); 43 } 44 45 string get2(string s) 46 { 47 sets(s); 48 //将中间的四个数顺时针旋转90度 49 swap(g[0][1], g[1][1]); 50 swap(g[1][1], g[1][2]); 51 swap(g[1][2], g[0][2]); 52 return get(); 53 } 54 void bfs(string start, string end) 55 { 56 if (start == end) return; 57 58 queue<string> q; 59 q.push(start); 60 dist[start] = 0; 61 62 while (q.size()) 63 { 64 string t = q.front(); 65 q.pop(); 66 67 string m[3]; 68 m[0] = get0(t); 69 m[1] = get1(t); 70 m[2] = get2(t); 71 72 for (int i = 0; i < 3; i ++ ) 73 if (!dist.count(m[i])) 74 { 75 dist[m[i]] = dist[t] + 1; 76 pre[m[i]] = {'A' + i, t}; 77 q.push(m[i]); 78 if (m[i] == end) return; 79 } 80 } 81 return; 82 } 83 84 int main() 85 { 86 string start, end; 87 for (int i = 0; i < 8; i ++ ) 88 { 89 char c = i + '1'; 90 start += c; 91 } 92 93 for (int i = 1; i <= 8; i ++ ) 94 { 95 int x; cin >> x; 96 end += x + '0'; 97 } 98 99 bfs(start, end); 100 101 cout << dist[end] << "\n"; 102 103 string s; 104 while (start != end) 105 { 106 s += pre[end].first; 107 end = pre[end].second; 108 } 109 reverse(s.begin(), s.end()); 110 if (s.size()) cout << s << "\n"; 111 112 return 0; 113 }
双端队列模型
题目:175. 电路维修
达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。
翰翰的家里有一辆飞行车。
有一天飞行车的电路板突然出现了故障,导致无法启动。
电路板的整体结构是一个 R 行 C 列的网格(R,C≤500),如下图所示。
每个格点都是电线的接点,每个格子都包含一个电子元件。
电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。
在旋转之后,它就可以连接另一条对角线的两个接点。
电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。
达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。
她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。
不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。
注意:只能走斜向的线段,水平和竖直线段不能走。
输入格式
输入文件包含多组测试数据。
第一行包含一个整数 TT,表示测试数据的数目。
对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。
之后 R 行,每行 C 个字符,字符是
"/"
和"\"
中的一个,表示标准件的方向。输出格式
对于每组测试数据,在单独的一行输出一个正整数,表示所需的缩小旋转次数。
如果无论怎样都不能使得电源和发动机之间连通,输出
NO SOLUTION
。数据范围
1≤R,C≤500,
1≤T≤5输入样例:
1 3 5 \\/\\ \\/// /\\\\
输出样例:
1
样例解释
样例的输入对应于题目描述中的情况。
只需要按照下面的方式旋转标准件,就可以使得电源和发动机之间连通。
分析:
题目使用双端队列来做BFS,但是,用了双端队列后,因为为了满足二段性,将权值为0的查到队首,将权值为1的点插入到了队尾,这也就导致了,一个点有可能会被更新多次,这很像对优化版的dijkstra算法,巴拉巴拉~~~
这不是自己做出的,也没有吃透这个算法,还要再多次做做这个题
代码:
#include <cstdio> #include <cstring> #include <deque> using namespace std; typedef pair<int, int> PII; #define x first #define y second const int N = 510; int n, m; char g[N][N]; int dist[N][N]; bool st[N][N]; int bfs() { memset(dist, 0x3f, sizeof dist); memset(st, 0, sizeof st); dist[0][0] = 0; deque<PII> q; q.push_back({0, 0}); int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1}; int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1}; char c[10] = "\\/\\/"; while (q.size()) { PII t = q.front(); q.pop_front(); if (st[t.x][t.y]) continue; st[t.x][t.y] = true; for (int i = 0; i < 4; i ++ ) { int x = t.x + dx[i], y = t.y + dy[i]; int cx = t.x + ix[i], cy = t.y + iy[i]; if (x < 0 || x > n || y < 0 || y > m) continue; if (st[x][y]) continue; int w = g[cx][cy] != c[i]; int d = dist[t.x][t.y] + w; if (dist[x][y] > d) { dist[x][y] = d; if (w) { q.push_back({x, y}); } else { q.push_front({x, y}); } } } } return dist[n][m]; } void work() { scanf("%d%d", &n, &m); for (int i = 0; i < n; i ++ ) scanf("%s", g[i]); if ((n + m) & 1) puts("NO SOLUTION"); else printf("%d\n", bfs()); return; } int main() { int T; scanf("%d", &T); while (T -- ) { work(); } return 0; }
双向搜索【双向广搜一般用于最小步数模型,因为每层的状态会是非常庞大的,一般不用与其他,如最短路模型】:
双向广搜,每次选择当前队列元素较少的扩展
题目:
已知有两个字串 A, B 及一组字串变换的规则(至多 6 个规则): A1→B1 A2→B2 … 规则的含义为:在 A 中的子串 A1 可以变换为 B1、A2 可以变换为 B2…。 例如:A=abcd B=xyz 变换规则为: abc → xu ud → y y → yz 则此时,A 可以经过一系列的变换变为 B,其变换的过程为: abcd → xud → xy → xyz 共进行了三次变换,使得 A 变换为 B。 输入格式 输入格式如下: A B A1 B1 A2 B2 … … 第一行是两个给定的字符串 A 和 B。 接下来若干行,每行描述一组字串变换的规则。 所有字符串长度的上限为 20。 输出格式 若在 10 步(包含 10 步)以内能将 A 变换为 B ,则输出最少的变换步数;否则输出 NO ANSWER!。 输入样例: abcd xyz abc xu ud y y yz 输出样例: 3
分析:
其实一开始像的就是和八数码一样做,用map将将所有起点存进去,然后做宽搜的 但是,这个题要搜的状态太庞大了,字符串长度为20, 最多会有6中变化,那么我们第一层状态的维护120中,搜索10层,这就是120的10次方,大的要死,我暴力写,直接MLE内存超限了。 所以,题解为双向搜索。从出状态往末状态搜索,同时从末状态向着初状态搜索。
代码:
#include <iostream> #include <cstdio> #include <cstring> #include <queue> #include <unordered_map> using namespace std; const int N = 100010; int len; string a[N], b[N]; unordered_map<string, int> d1, d2; int bfs(string start, string end) { queue<string> q1, q2; q1.push(start), d1[start] = 0; q2.push(end), d2[end] = 0; //当q1和q2有一个队列元素被扩展完的时候,说明两个队列扩展的元素没有交集,可以推出循环了 while (q1.size() && q2.size()) { //如果q1队列里面的元素较少的话,先搜索q1 int s = q1.size(), t = q2.size(); // if (q1.size() <= q2.size()) if (s <= t) { string x = q1.front(); q1.pop(); // cout << "q1:" << x << "\n"; for (int i = 0; i < x.size(); i ++ ) for (int j = 0; j < len; j ++ ) if (x.substr(i, a[j].size()) == a[j]) { string y = x.substr(0, i) + b[j] + x.substr(i + a[j].size()); // cout << "q1: " << x << "||" << a[j] << "||" << y << "\n"; if (d2.count(y)) return d2[y] + d1[x] + 1; if (!d1.count(y)) { d1[y] = d1[x] + 1; q1.push(y); } } } else { string x = q2.front(); q2.pop(); // cout << "q2:" << x << "\n"; for (int i = 0; i < x.size(); i ++ ) for (int j = 0; j < len; j ++ ) if (x.substr(i, b[j].size()) == b[j]) { string y = x.substr(0, i) + a[j] + x.substr(i + b[j].size()); // cout << "q2: " << x << "||" << a[j] << "||" << y << "\n"; if (d1.count(y)) return d1[y] + d2[x] + 1; if (!d2.count(y)) { d2[y] = d2[x] + 1; q2.push(y); } } } } return 11; } int main() { string start, end; cin >> start >> end; while (cin >> a[len] >> b[len]) len ++; int ans = bfs(start, end); // cout << ans << "\n"; if (ans < 0 || ans > 10) puts("NO ANSWER!"); else printf("%d\n", ans); return 0; }
A*
起点到当前实际距离,d[x] 当前点到终点的预估函数,f[x] 当前点到终点的真是距离,g[x] 优先队列优化的BFS或者叫堆优化版的dijkstra算法。
A*算法一定能保证终点在第一次出队时是最优的,但是不能保证其他点一定是最优的
在一个 3×3 的网格中,1∼8 这 8 个数字和一个 X 恰好不重不漏地分布在这 3×3 的网格中。 例如: 1 2 3 X 4 6 7 5 8 在游戏过程中,可以把 X 与其上、下、左、右四个方向之一的数字交换(如果存在)。 我们的目的是通过交换,使得网格变为如下排列(称为正确排列): 1 2 3 4 5 6 7 8 X 例如,示例中图形就可以通过让 X 先后与右、下、右三个方向的数字交换成功得到正确排列。 交换过程如下: 1 2 3 1 2 3 1 2 3 1 2 3 X 4 6 4 X 6 4 5 6 4 5 6 7 5 8 7 5 8 7 X 8 7 8 X 把 X 与上下左右方向数字交换的行动记录为 u、d、l、r。 现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列。 输入格式 输入占一行,将 3×3 的初始网格描绘出来。 例如,如果初始网格如下所示: 1 2 3 x 4 6 7 5 8 则输入为:1 2 3 x 4 6 7 5 8 输出格式 输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。 如果答案不唯一,输出任意一种合法方案即可。 如果不存在解决方案,则输出 unsolvable。 输入样例: 2 3 4 1 5 x 7 6 8 输出样例 ullddrurdllurdruldr
分析:
代码:
#include <cmath> #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <unordered_map> #include <queue> using namespace std; typedef pair<int, string> PIS; #define x first #define y second //A*(将dist和f) int f(string s)//当前状态s到达最终状态end的曼哈顿距离 { int ans = 0; for (int i = 0; i < 9; i ++ ) if (s[i] != 'x') { int t = s[i] - '1'; ans += abs(i / 3 - t / 3) + abs(i % 3 - t % 3); } return ans; } string bfs(string start, string end) { int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; char c[] = "urdl"; unordered_map<string, int> dist; unordered_map<string, pair<string, char>> pre; priority_queue<PIS, vector<PIS>, greater<PIS>> q; q.push({f(start), start}); while (q.size()) { PIS t = q.top(); q.pop(); string state = t.y; if (state == end) break; int x, y; // cout << state.size() << "\n"; for (int i = 0; i < state.size(); i ++ ) if (state[i] == 'x') { x = i / 3, y = i % 3; // cout << state[i] << " " << x << " " << y << "\n"; } // cout << state << "\n"; for (int i = 0; i < 4; i ++ ) { int a = x + dx[i], b = y + dy[i]; // cout << a << " " << b << "\n"; if (a < 0 || a >= 3 || b < 0 || b >= 3) continue; string s = state; swap(s[3 * x + y], s[3 * a + b]); // cout << s << "||" << state << "\n"; if (!dist[s] || dist[s] > dist[state] + 1) { dist[s] = dist[state] + 1; pre[s] = {state, c[i]}; q.push({dist[s] + f(s), s}); } } } // cout << "start: " << start << "\n" << "end: " << end << '\n'; // cout << "end.x: " << pre[end].x << " " << "end.y: " << pre[end].y << "\n"; string res; while (end != start) { res += pre[end].y; end = pre[end].x; } reverse(res.begin(), res.end()); return res; } int main() { string start, end, sep; end = "12345678x"; char c; for (int i = 0; i < 9; i ++ ) { cin >> c; start += c; if (c != 'x') sep += c; } int cnt = 0; for (int i = 0; i < sep.size(); i ++ ) for (int j = i + 1; j < sep.size(); j ++ ) if (sep[i] > sep[j]) cnt ++; // cout << cnt << "\n"; if (cnt % 2 == 0) cout << bfs(start, end) << "\n"; else puts("unsolvable"); return 0; }
题目:
给定一张 N 个点(编号 1,2…N),M 条边的有向图,求从起点 S 到终点 T 的第 K 短路的长度,路径允许重复经过点或边。 注意: 每条最短路中至少要包含一条边。 输入格式 第一行包含两个整数 N 和 M。 接下来 M 行,每行包含三个整数 A,B 和 L,表示点 A 与点 B 之间存在有向边,且边长为 L。 最后一行包含三个整数 S,T 和 K,分别表示起点 S,终点 T 和第 K 短路。 输出格式 输出占一行,包含一个整数,表示第 K 短路的长度,如果第 K 短路不存在,则输出 −1。 数据范围 1≤S,T≤N≤1000, 0≤M≤105, 1≤K≤1000, 1≤L≤100 输入样例: 2 2 1 2 5 2 1 4 1 2 2 输出样例: 14
分析:
这题是求所有路径当中第k小的路径,也就意味着,我们要从全局(所有路径)当中,找到第k小的那条路径,所以,我们可能甚至需要将所有路径都给求出来呢。 这时候,就与一般的堆优化版dijkstra算法有所不同了,我们将点x所有可达(也就是等扩展到)的点y都给加入到队列中。然后不断的扩展。 在堆优化版的dijkstra算法中,我们是只将更新后的点y加入到队列中。 注意,这里有个结论,【我还没搞懂对错,因为这题的预估函数确实选的挺巧妙,f=g】 只能保证终点第k次被取出的时候,是第k小的,而其他点是不能保证的。
代码:
#include <cstdio> #include <cstring> #include <algorithm> #include <queue> using namespace std; typedef pair<int, int> PII; typedef pair<int, PII> PIII; #define x first #define y second const int N = 1010, M = 200010; int n, m; int head[N], rhead[N], ver[M], edge[M], Next[M], tot; int S, T, K; void add(int head[], int x, int y, int z) { ver[++ tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot; } int f[N]; bool st[N]; void dijkstra() { memset(f, 0x3f, sizeof f); f[T] = 0; priority_queue<PII, vector<PII>, greater<PII>> q; q.push({0, T}); while (q.size()) { PII t = q.top(); q.pop(); int x = t.y; if (st[x]) continue;//第一次被取出的点,一定是从起点到当前点的最短路 st[x] = true;//既然已经求出这个点的最短路,也用这个x的最短路扩展过其他的y,那么说明不需要再用到它了 for (int i = rhead[x]; i; i = Next[i]) { int y = ver[i], z = edge[i]; if (f[y] > f[x] + z) { f[y] = f[x] + z; q.push({f[y], y}); } } } } int bfs() { priority_queue<PIII, vector<PIII>, greater<PIII>> q; q.push({f[S], {0, S}}); int cnt[N] = {0}; while (q.size()) { PIII t = q.top(); q.pop(); int x = t.y.y, distance = t.y.x; cnt[x] ++; if (cnt[T] == K) return distance; for (int i = head[x]; i; i = Next[i]) { int y = ver[i], z = edge[i]; if (cnt[y] < K) q.push({distance + z + f[y], {distance + z, y}}); } } return -1; } int main() { scanf("%d%d", &n, &m); while (m -- ) { int x, y, z; scanf("%d%d%d", &x, &y, &z); add(head, x, y, z), add(rhead, y, x, z); } scanf("%d%d%d", &S, &T, &K); //从终点T向每个点求下最短路,f[x] dijkstra(); //当S==T的时候,最短路为0,又因为题目中说最短要包含一条边,所以,给K++,算是跳一下0这条边 if (S == T) K ++; //然后,我们正着做一遍A*算法,注意要将x所有可达的点全部 //都加入优先队列中去,不需要判断距离的大小,不需要将扩展过的才放入队列中 printf("%d\n", bfs()); return 0; }