垃圾ACMer的暑假训练220722

垃圾ACMer的暑假训练220722

14.5 多源BFS

14.5.1 矩阵距离
题意

给定一个 n × m    ( 1 ≤ n , m ≤ 1000 ) n\times m\ \ (1\leq n,m\leq1000) n×m  (1n,m1000) 01 01 01矩阵 A A A,定义 A [ i ] [ j ] A[i][j] A[i][j] A [ k ] [ l ] A[k][l] A[k][l]间的Manhattan距离 d i s ( A [ i ] [ j ] , A [ k ] [ l ] ) = ∣ i − k ∣ + ∣ j − l ∣ dis(A[i][j],A[k][l])=|i-k|+|j-l| dis(A[i][j],A[k][l])=ik+jl.输出一个 n × m n\times m n×m的整数矩阵 B B B,其中 B [ i ] [ j ] = min ⁡ 1 ≤ x ≤ n , 1 ≤ y ≤ m , A [ x ] [ y ] = 1 d i s ( A [ i ] [ j ] , A [ x ] [ y ] ) \displaystyle B[i][j]=\min_{1\leq x\leq n,1\leq y\leq m,A[x][y]=1}dis(A[i][j],A[x][y]) B[i][j]=1xn,1ym,A[x][y]=1mindis(A[i][j],A[x][y]).

思路

即求每个元素到最近的 1 1 1的距离.

类比图论中有多个起点,求点到其最近的起点的最短路时,可建立与多个起点相连的虚拟源点,再求目标点到虚拟源点的最短路.在本题中,只需先将 B B B中对应的 A A A中所有 1 1 1的位置初始化为 0 0 0并入队即可.

代码
const int MAXN = 1005, MAXM = MAXN * MAXN;
int n, m;
char graph[MAXN][MAXN];
int dis[MAXN][MAXN];

void bfs() {
	memset(dis, -1, so(dis));

	qii que;
	for (int i = 1; i <= n; i++) {  // 将A中所有1的位置入队
		for (int j = 1; j <= m; j++) {
			if (graph[i][j] == '1') {
				dis[i][j] = 0;
				que.push({ i,j });
			}
		}
	}

	while (que.size()) {
		pii tmp = que.front(); que.pop();
		for (int i = 0; i < 4; i++) {
			int curx = tmp.first + dx[i], cury = tmp.second + dy[i];
			if (curx < 1 || curx > n || cury < 1 || cury > m) continue;
			if (~dis[curx][cury]) continue;

			dis[curx][cury] = dis[tmp.first][tmp.second] + 1;
			que.push({ curx,cury });
		}
		
	}
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> graph[i] + 1;

	bfs();

	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++) cout << dis[i][j] << " \n"[j == m];
}


14.6 BFS的最小步数模型

14.6 魔板

题意
1 2 3 4
8 7 6 5

上图是一张有 8 8 8个大小相同的格子的魔板,魔板的每个格子有一种颜色,这 8 8 8种颜色用 1 ∼ 8 1\sim 8 18的整数表示.用颜色序列表示魔板的一种状态,规定从魔板左上角开始,沿顺时针方向取出整数,构成一个颜色序列,如上图的颜色序列为 { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } \{1,2,3,4,5,6,7,8\} {1,2,3,4,5,6,7,8},这是基本状态.

现有三种操作,可通过这些操作改变魔板状态:

A:交换上下两行.如初始时的魔板经一次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 8 8个整数(数的范围 [ 1 , 8 ] [1,8] [1,8]),表示目标状态.

第一行输出一个整数,表示最短操作序列的长度 l e n len len.若 l e n > 0 len>0 len>0,第二行输入字典序最小的操作序列.

思路

可用哈希表存已经搜到过的状态.

BFS入队时,按做操作A、B、C依次入队即可保证答案字典序最小.

代码
char board[2][4];  // 魔板
umap<string, pair<char, string>> pre;  // 记录每个状态的前驱和经过的操作
umap<string, int> dis;

void set_board(string state) {  // 将魔板更新为state对应的状态
	for (int i = 0; i < 4; i++) board[0][i] = state[i];
	for (int i = 7, j = 0; j < 4; i--, j++) board[1][j] = state[i];
}

string get_board() {
	string res;
	for (int i = 0; i < 4; i++) res += board[0][i];
	for (int i = 3; i >= 0; i--) res += board[1][i];
	return res;
}

string opA(string state) {
	set_board(state);
	for (int i = 0; i < 4; i++) swap(board[0][i], board[1][i]);
	return get_board();
}

string opB(string state) {
	set_board(state);
	int tmp0 = board[0][3], tmp1 = board[1][3];
	for (int i = 3; i >= 0; i--) board[0][i] = board[0][i - 1], board[1][i] = board[1][i - 1];
	board[0][0] = tmp0, board[1][0] = tmp1;
	return get_board();
}

string opC(string state) {
	set_board(state);
	int tmp = board[0][1];
	board[0][1] = board[1][1], board[1][1] = board[1][2], board[1][2] = board[0][2], board[0][2] = tmp;
	return get_board();
}

int bfs(string start, string end) {
	if (start == end) return 0;

	queue<string> que;
	que.push(start);
	dis[start] = 0;

	while (que.size()) {
		auto tmp = que.front(); que.pop();
		string cur[3] = { opA(tmp),opB(tmp),opC(tmp) };
		for (int i = 0; i < 3; i++) {
			if (!dis.count(cur[i])) {
				dis[cur[i]] = dis[tmp] + 1;
				pre[cur[i]] = { 'A' + i,tmp };
				if (cur[i] == end) return dis[end];
				que.push(cur[i]);
			}
		}
	}
}

int main() {
	string start = "12345678";
	string end;
	for (int i = 0; i < 8; i++) {
		int x; cin >> x;
		end.push_back(x + '0');
	}

	int step = bfs(start, end);
	cout << step << endl;

	if (step) {
		string ans;
		while (start != end) {
			ans += pre[end].first;
			end = pre[end].second;
		}
		reverse(all(ans));
		cout << ans;
	}
}


14.7 双端队列BFS

14.7.1 电路维修

题意
image-20220722093755535

如上图,电路板的整体结构是一个 r × c r\times c r×c的网格,每个格点都是电线的接点,每个格子包含一个电子元件.电子元件的主要部分是一个可旋转的、连接一条对角线上的两端点的短电缆,旋转后它可连接另一对角线的两接点.电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置.初始时,电路可能处于断路状态.现需旋转最少的元件使得电源与发动装置接通.注意电流只能走斜向的线段.

t    ( 1 ≤ t ≤ 5 ) t\ \ (1\leq t\leq 5) t  (1t5)组测试数据.每组测试数据第一行输入整数 r , c    ( 1 ≤ r , c ≤ 500 ) r,c\ \ (1\leq r,c\leq 500) r,c  (1r,c500),表示电路板的行数和列数.之后输入一个 r × c r\times c r×c的字符为’/‘或’\'的字符矩阵,表示初始时元件的方向.

对每组测试数据,若能通过旋转使得电路接通,则输出最小的旋转次数;否则输出"NO SOLUTION".

思路

将接点视为图中的节点,无需旋转元件即可相连的节点间边权为 0 0 0,否则边权为 1 1 1.可建图后用Dijkstra跑最短路.

因只能沿斜线走,则每走一步横纵坐标同时变化 1 1 1,则起点的横纵坐标之和与终点的横纵坐标之和的奇偶性不同时无解.

image-20220722113401074

注意到上图中红色圈出的节点不可到达,每个格子内只有对角线上的点能到达,同一条边上的点不能到达(因方案中每个元件的角度固定,不能同时用到两组对角线上的点).

对边权为 0 0 0 1 1 1的图,可用双端队列BFS,即考察当前节点扩展出来的边的边权,若边权为 0 0 0,则插入队首;否则插入队尾.注意每个点可能不止入队一次,则每个点出队时才能确定最小值.

代码
const int MAXN = 505, MAXM = MAXN * MAXN;
int n, m;
char graph[MAXN][MAXN];
int dis[MAXN][MAXN];
bool vis[MAXN][MAXN];

int bfs() {
	memset(vis, 0, so(vis));
	memset(dis, INF, so(dis));

	dis[0][0] = 0;
	deque<pii> que;
	que.push_back({ 0,0 });

	const char str[] = "\\/\\/";  // 注意\是转义字符,故要2个
	const int dx[4] = { -1,-1,1,1 }, dy[4] = { -1,1,1,-1 };  // 左上、左下、右下、右上的偏移量
	const int ix[4] = { -1,-1,0,0 }, iy[4] = { -1,0,0,-1 };  // 格点到graph[][]的偏移量

	while (que.size()) {
		auto tmp = que.front(); que.pop_front();
		int x = tmp.first, y = tmp.second;
		if (x == n && y == m) return dis[x][y];  // 每个点出队时才能确定最小值

		if (vis[x][y]) continue;
		vis[x][y] = true;

		for (int i = 0; i < 4; i++) {
			int curx = x + dx[i], cury = y + dy[i];
			if (curx < 0 || curx > n || cury < 0 || cury > m) continue;

			int gx = x + ix[i], gy = y + iy[i];  // 对应到graph[][]中的坐标
			int w = graph[gx][gy] != str[i];  // 无需旋转边权为0,否则边权为1
			int d = dis[x][y] + w;
			if (d <= dis[curx][cury]) {
				dis[curx][cury] = d;
				if (!w) que.push_front({ curx,cury });
				else que.push_back({ curx,cury });
			}
		}
	}
}

int main() {
	CaseT{
		cin >> n >> m;
		for (int i = 0; i < n; i++) cin >> graph[i];

		if (n + m & 1) cout << "NO SOLUTION" << endl;
		else cout << bfs() << endl;
	}
}


14.8 双向BFS

双向BFS常用于优化BFS的最小步数模型,一般不用于BFS的最短路模型.



14.8.1 字串变换

题意

已知有两字串 A A A B B B和一组(至多 6 6 6个)字串变换规则: A 1 → B 1 , A 2 → B 2 , ⋯ A_1\rightarrow B_1,A_2\rightarrow B_2,\cdots A1B1,A2B2,,表示 A A A中子串 A 1 A_1 A1可变换为 B 1 B_1 B1,子串 A 2 A_2 A2可变换为 B 2 , ⋯ B_2,\cdots B2,.

第一行输入两给定字符串 A A A B B B.接下来若干行每行输入两个字符串 A i A_i Ai B i B_i Bi描述一组字串变换规则.数据保证所有字符串长度不超过 20 20 20.

若在 10 10 10步内(含 10 10 10步)能将 A A A变换为 B B B,则删除最小变换步数;否则输出"NO ANSWER!".

思路

最坏情况下每个字符串长度都为 20 20 20且都能应用 6 6 6个规则,每个规则都是单个字符替换,则每一步有 20 × 6 = 120 20\times 6=120 20×6=120种情况, 10 10 10步以内共 12 0 10 120^{10} 12010种情况.故朴素BFS从起点搜到终点可能会TLE或MLE.

双向BFS同时从起点和终点往中间搜,若在中间相遇,则有解;否则无解.这样从起点和终点各搜 5 5 5步,共 2 × 6 5 2\times 6^5 2×65种情况.

进一步优化,每次选择元素较少的队列的方向扩展.

代码
const int MAXN = 6;
int n;  // 规则数
string A, B;
string str1[MAXN], str2[MAXN];  // 变换规则

// 队列、到起点的距离、到终点的距离、变换规则a→b
int extend(queue<string>& que, umap<string, int>& disA, umap<string, int>& disB, string a[MAXN], string b[MAXN]) {
	int d = disA[que.front()];
	while (que.size() && disA[que.front()] == d) {
		auto tmp = que.front(); que.pop();
		for (int i = 0; i < n; i++) {  // 枚举变换规则
			for (int j = 0; j < tmp.size(); j++) {  // 枚举应用变化规则的起点
				if (tmp.substr(j, a[i].size()) == a[i]) {
					string state = tmp.substr(0, j) + b[i] + tmp.substr(j + a[i].size());
					if (disB.count(state)) return disA[tmp] + disB[state] + 1;  // 相遇
					if (disA.count(state)) continue;
					
					disA[state] = disA[tmp] + 1;
					que.push(state);
				}
			}
		}
	}
	return 11;  // 返回一个比10大的数
}

int bfs() {
	if (A == B) return 0;

	queue<string> queA, queB;  // 从起点、终点开始搜索的队列
	umap<string, int> disA, disB;  // 与起点、终点的距离
	queA.push(A), disA[A] = 0, queB.push(B), disB[B] = 0;

	while (queA.size() && queB.size()) {
		int step = queA.size() <= queB.size() ?  // 选择元素较少的队列的方向扩展
			extend(queA, disA, disB, str1, str2) : extend(queB, disB, disA, str2, str1);  // 注意后者规则反着用
		if (step <= 10) return step;
		if (++step == 10) return 11;  // 返回一个比10大的数
	}
	return 11;  // 返回一个比10大的数
}

int main() {
	cin >> A >> B;
	while (cin >> str1[n] >> str2[n])n++;

	int ans = bfs();
	cout << (ans <= 10 ? to_string(ans) : "NO ANSWER!");
}


14.9 A*

A*算法类似于Dijkstra算法,是对BFS的优化.朴素BFS中直接从起点搜到终点可能会经过很多状态,而A*算法中加入启发函数(估价函数),使得只需搜较少的状态即可找到从起点到终点的一条最短路.A*算法只在搜索空间很大时才有明显的优化效果.A*算法和Dijkstra算法都能解决边权非负的图中的最短路.Dijkstra算法可看作从每个点到终点的估计距离都是 0 0 0的最短路.

A*算法将BFS中的队列换为小根堆,队列中不仅存起点到当前点的真实距离,还存该点到终点的估计距离.每次选择与终点的估计距离最小的点扩展.

A*算法中每个点可能会被扩展多少次.

①BFS入队时判重;②Dijkstra算法出队时判重;③A*算法不判重.

A*算法的成立条件:估计距离$\leq $真实距离.

A*算法的应用场景:确定有解.若无解时,A*算法会将整个搜索空间都搜索一遍,效率低于朴素BFS.

​ 但实际应用中未必知道是否有解,也可用A*算法,大部分时候比朴素BFS快.

当终点第一次出队时已确定最短距离.

[] 设终点 u u u当前出队,此时 u u u与起点的距离为 d ( u ) d(u) d(u).

设起点到 u u u的真实距离为 d i s ( u ) dis(u) dis(u), u u u到终点的估计距离和真实距离分别为 f ( u ) f(u) f(u) g ( u ) g(u) g(u).

设真实的最短路径为 d d d,则 d = d i s ( u ) + g ( u ) ≥ d i s ( u ) + f ( u ) d=dis(u)+g(u)\geq dis(u)+f(u) d=dis(u)+g(u)dis(u)+f(u).

u u u出队时不是最短距离,则 d ( u ) > d ≥ d i s ( u ) + f ( u ) d(u)>d\geq dis(u)+f(u) d(u)>ddis(u)+f(u),且是最短距离的节点在队列中.

​ 这表明:当前出队的不是队列中的最小值,矛盾.

[] A*算法只能保证终点出队时是最短距离,不能保证其他节点.



14.9.1 八数码

题意

3 × 3 3\times 3 3×3的网格中不重不漏地填入 1 ∼ 8 1\sim 8 18 8 8 8个数字,空位用’X’表示.游戏过程中,可将X与其上、下、左、右四个方向之一的数字交换(若存在),目标是通过交换将初始网格变为如下形式(称为正确排列):

1 2 3
4 5 6
7 8 X

将X与其上、下、左、右四个方向的数字交换分别记作u、d、l、r.现给定一个初始网格,求通过最少的移动次数将其变为正确排列的交换序列,若有多种方案,输出任一种;若无方案,输出"unsolvable".

用一个字符串描述 3 × 3 3\times 3 3×3的初始网格.若初始网格为:

1 2 3 
x 4 6 
7 5 8 

则输入 1   2   3   x   4   6   7   5   8 1\ 2\ 3\ x\ 4\ 6\ 7\ 5\ 8 1 2 3 x 4 6 7 5 8.

思路

八数码问题有解的充要条件:初始序列的逆序对数量是偶数.

[] (充) 较难,略.

(必) 显然左右交换不改变逆序对数量,上下交换只会改变两对逆序对,故初始序列的逆序对数与正确排列的逆序对数同奇偶.

注意到最优解中每次交换可使得交换的数与其目标位置的Manhattan距离减 1 1 1,估价函数取每个数码与其目标位置的Manhattan距离之和.

代码
int f(string state) {  // 估价函数
	int res = 0;
	for (int i = 0; i < state.size(); i++) {
		if (state[i] != 'x') {
			int tmp = state[i] - '1';
			res += abs(i / 3 - tmp / 3) + abs(i % 3 - tmp % 3);
		}
	}
	return res;
}

string bfs(string start) {
	const int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 }; 
	const char op[] = "urdl";  // 与上面的偏移量对应
	string end = "12345678x";
	umap<string, int> dis;
	umap<string, pair<string, char>> pre;  // 记录前驱和操作
	priority_queue<pair<int, string>, vector<pair<int, string>>, greater<pair<int, string>>> heap;
	heap.push({ f(start),start });
	dis[start] = 0;

	while (heap.size()) {
		auto tmp = heap.top(); heap.pop();
		string state = tmp.second;
		if (state == end) break;

		int x, y;  // x的位置
		for (int i = 0; i < state.size(); i++) {
			if (state[i] == 'x') {
				x = i / 3, y = i % 3;
				break;
			}
		}

		int step = dis[state];
		string backup = state;
		for (int i = 0; i < 4; i++) {
			int curx = x + dx[i], cury = y + dy[i];
			if (curx < 0 || curx >= 3 || cury < 0 || cury >= 3) continue;

			swap(state[x * 3 + y], state[curx * 3 + cury]);
			if (!dis.count(state) || dis[state] > step + 1) {
				dis[state] = step + 1;
				pre[state] = { backup,op[i] };
				heap.push({ dis[state] + f(state),state });
			}
			swap(state[x * 3 + y], state[curx * 3 + cury]);  // 恢复现场
		}
	}

	string res;
	while (end != start) {
		res += pre[end].second;
		end = pre[end].first;
	}
	reverse(all(res));
	return res;
}

int main() {
	string s1, s2;  // s1为原序列,s2为去掉x的序列
	char c;
	while (cin >> c) {
		s1.push_back(c);
		if (c != 'x') s2.push_back(c);
	}

	int cnt = 0;  // 逆序对个数
	for (int i = 0; i < s2.size(); i++)
		for (int j = i + 1; j < s2.size(); j++)
			if (s2[i] > s2[j]) cnt++;

	if (cnt & 1) cout << "unsolvable";
	else cout << bfs(s1);
}


14.9.2 第k短路

题意

给定一个包含 n n n个点(编号 1 ∼ n 1\sim n 1n)、 m m m条边的有向图,求起点 S S S到终点 T T T的第 k k k短路的长度,路径允许重复经过点或边.要求每条最短路中至少包含一条边,即 S = T S=T S=T时, k + + k++ k++.

第一行输入整数 n , m    ( 1 ≤ n ≤ 1000 , 0 ≤ m ≤ 1 e 4 ) n,m\ \ (1\leq n\leq 1000,0\leq m\leq 1\mathrm{e}4) n,m  (1n1000,0m1e4).接下来 m m m行每行输入三个整数 u , v , w    ( 1 ≤ u , v ≤ n , 1 ≤ l ≤ 100 ) u,v,w\ \ (1\leq u,v\leq n,1\leq l\leq 100) u,v,w  (1u,vn,1l100).最后一行输入三个整数 S , T , k    ( 1 ≤ S , T ≤ n , 1 ≤ k ≤ 100 ) S,T,k\ \ (1\leq S,T\leq n,1\leq k\leq 100) S,T,k  (1S,Tn,1k100),表示求起点 S S S到终点 T T T的第 k k k短路.

若最短路存在,输出最短路长度;否则输出 − 1 -1 1.

思路

与最短路问题不同,扩展时应将当前点能扩展到的所有点都入队.显然路径中存在环时,起点到终点的路径可能有无数条,故搜索空间很大,考虑A*算法.

估价函数可取每个点到终点的最短距离,该距离可通过以终点为起点跑一遍Dijksta算法得到.

A*算法中当终点出队时确定最短路,一直出队至第 k k k次即第 k k k短路.

[] 仿照终点出队时确定最短路的方法即证.

代码
const int MAXN = 1005, MAXM = 2e5 + 5;  // 两倍边
int n, m;  // 点数、边数
int S, T, k;  // 起点S到终点T的第k短路
int head[MAXN], rhead[MAXN], edges[MAXM], w[MAXM], nxt[MAXM], idx = 0;  // head[]为正向边的头节点,rhead[]为反向边的头节点
int dis[MAXN];
bool vis[MAXN];
int cnt[MAXN];  // cnt[u]表示节点u出队的次数

void add(int h[], int a, int b, int c) {
	edges[idx] = b, w[idx] = c, nxt[idx] = h[a], h[a] = idx++;
}

void dijkstra() {
	priority_queue<pii, vii, greater<pii>> heap;
	heap.push({ 0,T });  // 终点
	memset(dis, INF, so(dis));
	dis[T] = 0;

	while (heap.size()) {
		auto tmp = heap.top(); heap.pop();

		int u = tmp.second;
		if (vis[u]) continue;

		vis[u] = true;
		for (int i = rhead[u]; ~i; i = nxt[i]) {
			int v = edges[i];
			if (dis[v] > dis[u] + w[i]) {
				dis[v] = dis[u] + w[i];
				heap.push({ dis[v],v });
			}
		}
	}
}

int Astar() {
	priority_queue<tiii, vector<tiii>, greater<tiii>> heap;  // 估价距离、真实距离、节点编号
	heap.push({ dis[S],0,S });
	
	while (heap.size()) {
		auto tmp = heap.top(); heap.pop();

		int d, u;  // 真实距离、节点编号
		tie(ignore, d, u) = tmp;
		cnt[u]++;
		if (cnt[T] == k) return d;  // 终点出队k次即找到第k短路

		for (int i = head[u]; ~i; i = nxt[i]) {
			int v = edges[i];
			if (cnt[v] < k) heap.push({ d + w[i] + dis[v],d + w[i],v });  // 出队不足k次才扩展
		}
	}
	return -1;  // 无解
}

int main() {
	memset(head, -1, so(head)), memset(rhead, -1, so(rhead));
	
	cin >> n >> m;
	for (int i = 0; i < m; i++) {
		int a, b, c; cin >> a >> b >> c;
		add(head, a, b, c), add(rhead, b, a, c);  // 分别建正向边和反向边
	}

	cin >> S >> T >> k;
	if (S == T) k++;  // 最短路至少包含一条边

	dijkstra();  // 求终点到每个节点的最短距离

	cout << Astar();
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值