AcWing算法提高课【第二章搜索】多源BFS、最小步数模型、双端队列广搜、双向搜索、A*

多源BFS

173. 矩阵距离 

题目:

给定一个 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 }
View Code

最小步数模型

 1107. 魔板

题目:

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 }
View Code

 双端队列模型

题目:175. 电路维修

 

达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。

翰翰的家里有一辆飞行车。

有一天飞行车的电路板突然出现了故障,导致无法启动。

电路板的整体结构是一个 R 行 C 列的网格(R,C500),如下图所示。

电路.png

每个格点都是电线的接点,每个格子都包含一个电子元件。

电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。

在旋转之后,它就可以连接另一条对角线的两个接点。

电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。

达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。

她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。

不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。

注意:只能走斜向的线段,水平和竖直线段不能走。

输入格式

输入文件包含多组测试数据。

第一行包含一个整数 TT,表示测试数据的数目。

对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。

之后 R 行,每行 C 个字符,字符是"/""\"中的一个,表示标准件的方向。

输出格式

对于每组测试数据,在单独的一行输出一个正整数,表示所需的缩小旋转次数。

如果无论怎样都不能使得电源和发动机之间连通,输出 NO SOLUTION

数据范围

1R,C500,
1T5

输入样例:

1
3 5
\\/\\
\\///
/\\\\

输出样例:

1

样例解释

样例的输入对应于题目描述中的情况。

只需要按照下面的方式旋转标准件,就可以使得电源和发动机之间连通。

电路2.png

分析:

题目使用双端队列来做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;
}

双向搜索【双向广搜一般用于最小步数模型,因为每层的状态会是非常庞大的,一般不用与其他,如最短路模型】:

双向广搜,每次选择当前队列元素较少的扩展

  

190. 字串变换

题目:

已知有两个字串 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*算法一定能保证终点在第一次出队时是最优的,但是不能保证其他点一定是最优的

179. 八数码

在一个 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;
}

  

178. 第K短路

题目:

给定一张 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;
}

  

 

posted @ 2021-04-25 21:07  rookie161  阅读(227)  评论(0编辑  收藏  举报