算法基础课笔记(十七)

例题2:847. 图中点的层次(模板题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1

输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。

输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。

数据范围
1≤n,m≤10^5
输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1

BFS遍历图求最短路。

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
#include <iostream>
#include <cstring>
using namespace std;
#define IOS \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
const int N = 1e5+ 5;
int h[N], e[N], ne[N], idx;
int d[N],q[N];
int n,m;

void add(int a, int b) // 添加一条边a->b
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

int bfs(){
q[0] = 1;
int head = 0,tail = 0;

memset(d,-1,sizeof d);
d[1] = 0;

while (head <= tail){
int t = q[head++];
for (int i = h[t]; ~i;i = ne[i]){
int j = e[i];
if (d[j] == -1){
d[j] = d[t] + 1;
q[++tail] = j;
}
}
}
return d[n];// 能到达n号点则返回距离,否则返回-1
}

int main(){
IOS;

memset(h, -1, sizeof h);
cin >> n >> m;
int a,b;
for (int i = 0; i < m; i ++ ){
cin >> a >> b;
add(a, b);
}
cout << bfs() << '\n';
return 0;
}

3.4:有向图的拓扑排序(topological sort)

参考资料: https://time.geekbang.org/column/article/76207。

拓扑排序是基于有向无环图(也称为拓扑图)的一个算法。可以用Kahn 算法和DFS算法来实现。

在很多时候,拓扑排序的序列并不是唯一的。

拓扑排序其实就是基于有向无环图,遍历所有的结点,每两个结点之间有先后关系。并且在搜索下一个结点的时候,这个结点之前的结点已经全部被搜索过。

image-20210817135909534

首先得了解入度和出度的概念。

拓扑排序的起点必须是入度为0的点。

拓扑排序之Kahn 算法:根据入度的性质来实现拓扑排序,利用了贪心思想。

对于每个顶点,计数其入度,然后找到一个入度为0的顶点,如果不存在这样的顶点,则必定存在圈。因为入度为0的顶点没有其他顶点的有向边进入,表明可以将此顶点放在拓扑排序的首位,然后将该顶点的有向边所连接的顶点的入度分别减去1,再寻找下一个入度为0的顶点,放到拓扑排序的第二位,依此循环,直到将所有顶点放入拓扑排序序列中,如果尚存在不能放入排序的顶点,则表明图中存在圈。在寻找入度为0的顶点过程中,如果同时有多个入度为0的顶点,则有多种不同的拓扑排序,顶点选择的先后顺序不同会产生不同的拓扑排序。

参考自:《Programming Challenges》

在做拓扑排序的时候可以顺便判断一下图中是否存在环。(存在拓扑序列等价于无环)

image-20210817154736186

例题:848. 有向图的拓扑序列(模板题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。

输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。
否则输出 −1

数据范围
1≤n,m≤10^5
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 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
50
51
52
53
#include <iostream>
#include <cstring>
using namespace std;
#define IOS \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
const int N = 1e5+ 5;
int h[N],e[N],ne[N],idx;
int n,m;
int d[N],q[N];

void add(int a,int b){
e[idx] = b,ne[idx] = h[a],h[a] = idx ++;
}

bool toposort(){
int head = 0,tail = -1;// 初始化队列

for (int i = 1;i <= n;i ++){
if (!d[i]) q[++tail] = i;// 将所有入度为0的点都加入队列
}

while (head <= tail){// 队列非空
int t = q[head++];// 取出队头并出队

for (int i = h[t]; ~i;i = ne[i]){// 拓展队头的所有出边j
int j = e[i];
d[j] --;// 删除t-->j这条边,也就是j的入度-1
if (!d[j]) q[++tail] = j;// 当j的入度为0时,加入队列
}
}
return tail == n-1;// 刚好有0~n-1共n个点入队时,排序完毕
}

int main(){
IOS;
memset(h,-1,sizeof h);

cin >> n >> m;
int x,y;
while (m--){
cin >> x >> y;
add(x,y);
d[y] ++;// 入度++
}

if(toposort()){// 拓扑排序的顺序也就是入队顺序!队列的入队顺序等于出队顺序!
for (int i = 0;i < n;i ++) cout << q[i] << ' ';
}
else cout << -1;// 若不是所有点都入队说明存在环
return 0;
}

3.5:练习

3.5.1BFS.acwing.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
在一个 3×3 的网格中,188 个数字和一个 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
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。

输入格式
输入占一行,将 3×3 的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8

输出格式
输出占一行,包含一个整数,表示最少交换次数。

如果不存在解决方案,则输出 −1

输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19

求最少步数,用BFS做。

如何转化为BFS求解?

将图形的每个状态记为图的一个点,每次数字交换也就是加上一条a到b的边,从状态1转移到状态2,如果能从初始状态转移到终止状态,也就找到一条最短路,否则不存在解决方案。

本题的难点在于图形的状态表示非常复杂,难以想到建图方式。

bfs需要把遍历过的状态标记,而且需要dist数组记录距离,那么,需要开辟一个数组使得这个数组中的元素,能够和矩阵的所有状态(长度为9的字符串的全排列)一一对应。

状态可以用queue<string> q来表示,距离可以用unordered_map<string,int> dist来表示。

用map效率更低,所以用unordered_map。

如何进行状态转移?

将状态1的字符串还原成3*3矩阵,再将矩阵中相应元素交换位置得到状态2,再将状态2变成字符串。

image-20210817165647808

参考题解: https://www.acwing.com/solution/content/15149/。

算法1:BFS+哈希表。

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
#include <iostream>
#include <algorithm>
#include <string>
#include <queue>
#include <unordered_map>
using namespace std;
#define IOS \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
queue<string> q;
unordered_map<string,int> d;

int bfs(string start){
q.push(start);// 起始状态入队
d[start] = 0;// 起始状态距离初始化为0

string end = "12345678x";

int dx[] = {-1,1,0,0},dy[] = {0,0,-1,1};// 坐标偏移量
while (q.size()){
string t = q.front();
q.pop();// 取队头并出队

int dist = d[t];// 存储当前状态距离
if (t == end) return dist;// 走到终止状态,退出
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){
// 这个string内部字符的swap操作太妙了!!!
swap(t[k],t[3*a+b]);// 状态转移,二维数组下标-->一维数组下标
if (!d.count(t)){// t状态未遍历过
d[t] = dist + 1;// 更新距离
q.push(t);// 入队
}
swap(t[k],t[3*a+b]);// 还原状态,为下一种转换情况做准备
}
}
}// 无法转换到目标状态,返回-1
return -1;
}

int main(){
IOS;
string start;
char c;
for (int i = 0;i < 3;i ++)
for (int j = 0;j < 3;j ++){
cin >> c;
start += c;
}

cout << bfs(start) << '\n';
return 0;
}

算法2:BFS+手写哈希。

参考题解: https://www.acwing.com/solution/content/21288/。

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
#include <iostream>
#include <algorithm>
#include <string>
#include <queue>
using namespace std;
typedef unsigned long long ULL;
#define IOS \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
const int M = 1e7,P = 131;
ULL h[11];
int d[M];
queue<string> q;

int gethash(string str){
h[0]=0;
for(int i=1;i< 9;i++)
{
h[i]=h[i-1]*P+(str[i-1]-'0');

}
return h[8]%M;// 必须Mod M,否则哈希值太大,作为数组下标会越界
}

int bfs(string start){
q.push(start);// 起始状态入队
d[gethash(start)] = 0;// 起始状态距离初始化为0

string end = "12345678x";

int dx[] = {-1,1,0,0},dy[] = {0,0,-1,1};// 坐标偏移量
while (q.size()){
string t = q.front();
q.pop();// 取队头并出队

int dist = d[gethash(t)];// 存储当前状态距离
if (t == end) return dist;// 走到终止状态,退出
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){
swap(t[k],t[3*a+b]);// 状态转移,二维数组下标-->一维数组下标
if (!d[gethash(t)]){// t状态未遍历过
d[gethash(t)] = dist + 1;// 更新距离
q.push(t);// 入队
}
swap(t[k],t[3*a+b]);// 还原状态,为下一种转换情况做准备
}
}
}// 无法转换到目标状态,返回-1
return -1;
}

int main(){
IOS;
string start;
char c;
for (int i = 0;i < 3;i ++)
for (int j = 0;j < 3;j ++){
cin >> c;
start += c;
}

cout << bfs(start) << '\n';
return 0;
}

算法3:BFS+全排列哈希。

参考题解:https://www.acwing.com/solution/content/2481/。

坚持原创技术分享,您的支持将鼓励我继续创作!