[kuangbin带你飞] 专题一 简单搜索 题解(超详细注释,史上最强题解)

前言

[kuangbin带你飞]是ACMer中名气最大的题单之一,起源是上海大学的kuangbin大佬,是从基础到进阶阶段的最好题单之一,虽然题量适中,题解繁多,不过大多冗杂,注释偏少。

本专栏致力于用尽量简短的描述,以及大量的代码注释,帮助同学们真正理解题目的含义与解题方法,详细地解析代码,从而帮助同学们提高代码能力(当然还是要自己动手),让同学们体验到算法的乐趣。

本专栏会持续更新迭代,欢迎各位算法爱好者提供意见与建议,从而督促本人做出更好的题解。能够迭代升级的,才是最强的题解。


相信大多数看这个题解的同学均是已经接触到kuangbin专题的了,但是为了让同学们学习路径更平滑,我还是想稍作补充:

包含kuangbin专题在内的,ACMer学习路径(必看):ACM 的正确入门方式是什么?

kuangbin专题合集,最全的合集:[kuangbin带你飞]专题1-23,本专题所有题目均在上面提交

kuangbin专题合集,ACwing评测(好处是不用忍受老OJ的老旧评测机,坏处是暂时没有更新完所有专题)


注意!kuangbin每个专题需要一定的知识基础,所以在学习之前希望各位同学有一定的学习途径。

以下是我常用的学习途径:

  1. (首推)ACwing算法基础课 + 算法提高课,性价比最高的网课,包含大量基础到进阶的知识点,强烈推荐入手,既可当算法百科查阅,也可系统学习打好基础。
  2. 《算法竞赛进阶指南》,非常契合kuangbin专题的一本书,既有很多知识点讲解,同时也提供了很多例题帮助理解,也是个很不错的题单,牛客 ACwing 上都可以刷题。
  3. OIWIKI,相当全的知识点介绍网站,可以通过这个网站初步了解知识点的原理与基本实现。

注意!本专栏的所有题解都会写出所有题目的主要知识点,方便同学们查找学习


A.棋盘问题

主要知识点:DFS,枚举

题意概括:

给定一个 n × n n\times n n×n 正方形的迷宫,其中每一行和每一列只能放一个棋子,而且只有特定的 “#” 号格子,才允许放一颗棋子

解题思路:

类似经典的“八皇后”问题,只不过加上了只有特定位置才能放棋子这一特点

最朴素的方式是用DFS枚举所有 “#” 号格子,如果同一行或同一列没有放过棋子(用一维数组记录这一行/列是否放过棋子进行标记),才会放下棋子

也可以像我这样,仅枚举每行的 ”#“ 号格子,节约了记录行的空间。

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10 + 10;

int n, k;
char g[N][N];		// 记录迷宫
int col[N];			// 对列上是否存在棋子进行标记
int ans;

void dfs(int row, int cnt)	// row是当前行,cnt是已使用的棋子数
{
    if (cnt == k)	// 已经使用k颗棋子,该分支无需继续(剪枝)
    {
        ans ++;
        return;
    }
    if (row == n) return;	// 所有行都枚举完了,而棋子数还未达条件
    for (int i = 0; i < n; i ++ )
    {
        // 枚举从所有列上的区域
        // 只有当前格子可以放棋子,且当前列上没有棋子,才能尝试放下这颗棋子
        if (g[row][i] == '#' && !col[i])
        {
            col[i] = 1;	// 在当前列做标记
            // 放下这颗棋子,并枚举下一行
            dfs(row + 1, cnt + 1);	
            col[i] = 0;	// 这列不放棋子,回到原状态
        }
    }
    // 这一行不放棋子,继续枚举下一行
    dfs(row + 1, cnt);
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    while(cin >> n >> k && n != -1 )
    {
        ans = 0;
        memset(col, 0, sizeof col);
        // 行数从0 ~ n - 1
        for (int i = 0; i < n; i ++ ) cin >> g[i];
        // 初始值,第0行开始,当前一共使用了0颗棋子
        dfs(0, 0);
        cout << ans << endl;
    }
    // fclose(stdout);//输出结束
    return 0;
}

B.Dungeon Master(地牢大师)

主要知识点:BFS求最短路

题意概括:

给定一个 L × R × C L \times R \times C L×R×C 大小的3D地牢,其中 “S” 点为起点, “E” 点为终点,每单位时间能够向一个方格移动一格,包括上下层,无法进入 "#"号 格子。求能否到达终点,如果能到达,最小的步数是多少?

解题思路:

很明显的BFS问题,从起点开始,一步一步扩散,直到遍历到终点或者遍历完所有可到达的点而不能到达终点。

这道题除了代码量稍微大一点之外,就是一道正常的BFS题

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 30 + 10;

struct Position
{
    int floor, x, y;
};

int L, R, C;
char g[N][N][N];
int dist[N][N][N];

int bfs(Position st, Position ed)	// st为起点,ed为终点
{
    // 前后左右方向的向量
    int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
    // 用队列存储当前坐标
    queue<Position> q;
    q.push(st);
    int floor = st.floor, x = st.x, y = st.y;
    dist[floor][x][y] = 0;
    while (q.size())
    {
        Position t = q.front(); q.pop();
        floor = t.floor, x = t.x, y = t.y;
        if (g[floor][x][y] == 'E') return dist[floor][x][y]; 
        for (int i = -1; i <= 1; i += 2)
        {
            if (floor + i < 0 || floor + i >= L) continue;
            if (dist[floor + i][x][y] != -1) continue;
            if (g[floor + i][x][y] == '#') continue;
            dist[floor + i][x][y] = dist[floor][x][y] + 1;
            // 由于POJ的古老评测机,这里必须有一个中间变量才能编译
            Position k = {floor + i, x, y};
            q.push(k);
        }
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            if (a < 0 || a >= R || b < 0 || b >= C) continue;
            if (g[floor][a][b] == '#') continue;
            if (dist[floor][a][b] != -1) continue;
            dist[floor][a][b] = dist[floor][x][y] + 1;
            // 由于POJ的古老评测机,这里必须有一个中间变量才能编译
            Position k = {floor, a, b};
            q.push(k);
        }
    }
    return -1;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    while(cin >> L >> R >> C && L && R && C)
    {
        memset(dist, -1, sizeof dist);
        for (int i = 0; i < L; i ++ )
            for (int j = 0; j < R; j ++ )
                cin >> g[i][j];
        Position st, ed;
        for (int i = 0; i < L; i ++ )
            for (int j = 0; j < R; j ++ )
                for (int k = 0; k < C; k ++ )
                {
                    // 由于POJ的古老评测机,这里必须有一个中间变量才能编译
                    Position t = {i, j, k};
                    if (g[i][j][k] == 'S') st = t;
                    if (g[i][j][k] == 'E') ed = t;
                }
        // 用一个变量存储函数返回值
        int distance = bfs(st, ed);
        // -1 表示到达不了终点,其他表示到达终点的时间
        if (distance == -1) puts("Trapped!");
        else printf("Escaped in %d minute(s).\n", distance);
    }
    // fclose(stdout);//输出结束
    return 0;
}

C.Catch That Cow(抓住那头牛)

主要知识点:BFS求最短路

题意概括:

牛和农夫都在一个数轴上,假设牛不动,而农夫有以下三种移动方式

(1)向正方向移动,X + 1

(2)向负方向移动,X - 1

(3)自身坐标翻倍,X * 2

求农夫需要移动多少次,才能到达牛的位置,抓住牛?

解题思路:

一维BFS问题,模拟,秒杀一气呵成。

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

int n, k;
int dist[2 * N]; // 这里需要二倍长度,否则倍增的时候有可能会爆空间

int bfs()
{
    queue<int> q;
    q.push(n);
    dist[n] = 0;
    while (q.size())
    {
        int t = q.front(); q.pop();
        if (t == k) return dist[k];
        // t + 1 不得超过 牛的极限坐标 
        if (t + 1 < N && dist[t + 1] == -1) 
        {
            dist[t + 1] = dist[t] + 1;
            q.push(t + 1);
        }
        // t - 1 不得小于 0
        if (t - 1 >= 0 && dist[t - 1] == -1)
        {
            dist[t - 1] = dist[t] + 1;
            q.push(t - 1);
        }
        // t * 2 不得超过 2倍 牛的极限坐标
        if (t * 2 < 2 * N && dist[2 * t] == -1)
        {
            dist[2 * t] = dist[t] + 1;
            q.push(2 * t);
        }
    }
    return -1;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    cin >> n >> k;
    memset(dist, -1, sizeof dist);
    cout << bfs();
    // fclose(stdout);//输出结束
    return 0;
}

D.Fliptile(枚举)

主要知识点:状态压缩,枚举,递推,模拟

题意概括:

给定一个 M × N M \times N M×N 的0-1矩阵,要将这个矩阵转换成完全只有0的矩阵,每次翻转会连带着翻转相邻的四个格子(如果有的话),求最小翻转数方案中字典序最小的。

解题思路:

首先排除枚举所有节点的方案, 2 15 × 15 2 ^ {15 \times 15} 215×15 必定超时,这是一道有点思维难度的题,我们必须具有递推思想,要想明白:在第一行的方案确定的情况下,为了保证上一行能够全为0,后面的所有行都可以由前一行的状态递推出来,也就是说,我们唯一需要枚举的,仅仅是第一行。

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 15 + 10;

int m, n;   
// st为每个方格的状态, backup用于记录初始状态,方便回到初始状态
int st[N][N], backup[N][N];	
// way代表当前操作的具体矩阵,last代表最终结果的操作矩阵
int way[N][N], last[N][N];
// ans记录最小操作个数
int ans = 0x3f3f3f3f;
// 翻转操作
void turn(int x, int y)
{
    way[x][y] = 1;	// 记录该操作
    // 一系列翻转操作
    if (x - 1 > 0) st[x - 1][y] = !st[x - 1][y];
    if (x + 1 <= m) st[x + 1][y] = !st[x + 1][y];
    st[x][y] = !st[x][y];
    if (y - 1 > 0) st[x][y - 1] = !st[x][y - 1];
    if (y + 1 <= n) st[x][y + 1] = !st[x][y + 1];
}
// 进行第一行的操作
int count (int state)
{
    int cnt = 0;	// cnt记录操作个数
    for (int i = 1; i <= n; i ++ )
    {
        if (state & (1 << i - 1)) 
        {
            // 字典序从小到大的顺序,i应该从右往左进行操作
            turn(1, n - i + 1);
            cnt ++;
        }
    }
    return cnt;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    cin >> m >> n;
    // 输入初始状态
    for (int i = 1; i <= m; i ++ )
        for (int j = 1; j <= n; j ++ )
            cin >> st[i][j];
    // 将初始状态做备份
    memcpy(backup, st, sizeof st);
    // 枚举的是第一行的操作(状态压缩,字典序从小到大)
    for (int k = 0; k < 1 << n; k ++ ) 
    {
        // 每次回到初始状态
        memcpy(st, backup, sizeof st);
        memset(way, 0, sizeof way);
        // count函数:进行第一行的操作
        int t = count(k);
        // 由第一行的状态,递推之后m - 1行的状态
        for (int i = 2; i <= m; i ++ )
        {
            for (int j = 1; j <= n; j ++ )
            {
                // 如果前一行的这一列的状态为1
                // 那么必须在当前一行的这一列进行一次翻转
                // 从而保证上一行全部为0
                if (st[i - 1][j])	
                {
                    turn(i, j); 
                    t ++;
                }
            }
        }
        int ok = 1;	// 用于标记是否成功将最后一行也变为0
        for (int i = 1; i <= n; i ++ )
            if (st[m][i])	// 只要最后一行有状态1存在
                ok = 0;		// 就标记为不成功
        if (ok)
            // 更新最小解,因为第一行是按字典序从小到大枚举的
            // 所以操作次数相同,第一次更新的就是最小字典序解
            if (t < ans)	
            {
                ans = t;
                memcpy(last, way, sizeof way);
            }
    }
    // 找不到可行解
    if (ans == 0x3f3f3f3f) puts("IMPOSSIBLE");
    else
    {
        // cout << ans << endl;
        for (int i = 1; i <= m; i ++ )
        {
            for (int j = 1; j <= n; j ++ )
                cout << last[i][j] << ' ';
            cout << endl;
        }
    }
    // fclose(stdout);//输出结束
    return 0;
}

E.Find The Multiple(找倍数)

主要知识点:BFS,完全二叉树,枚举

题意概括:

给定 1 — — 200 1——200 1200 的整数 n n n ,求 n n n 的倍数,这个倍数需要满足,在十进制表示中,仅由1和0两个数字组成,这个倍数在十进制上不超过100位。

解题思路:

这道题最基本的思路是BFS一位数一位数地拓展,直到找到能够整除的方案。但是 i n t int int 类型的数组不能存储足够大的数, l o n g long long l o n g long long 类型也是如此,只有 u n s i g n e d unsigned unsigned l o n g long long l o n g long long 才能勉强存下。

我们可以用同余进行优化,只存储每种方案的余数,这样我们就可以使用 i n t int int ,从而可以扩展更多方案。

这是一道数据很模糊的题,复杂度分析很困难,多项输入,但不知道项数,倍数的100位也完全是干扰项,最终测出的数据,最大为 2 19 = 524288 2^{19} = 524288 219=524288 ,但为了求稳,还是选择了将数组开到足够大 。

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 1e8 + 10;

int n;
int mod[N];     // mod[i]代表的是第i个数 MOD n的余数
                // 第i个数是指完全二叉树的第i个节点
                // 通过构建完全二叉树来枚举

int bfs()
{
    queue<int> q;
    q.push(1);
    while(q.size())
    {
        int t = q.front(); q.pop();
        if (mod[t] == 0) return t;
        // 拓展左孩子
        if (2 * t < N)
        {
            mod[2 * t] = mod[t] * 10 % n;
            q.push(2 * t);
        }
        // 拓展右孩子
        if (2 * t + 1 < N)
        {
            mod[2 * t + 1] = (mod[t] * 10 + 1) % n;
            q.push(2 * t + 1);
        }
    }
    return 0;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    while (cin >> n && n)
    {
        mod[1] = 1 % n;
        // 利用BFS,构建完全二叉树,每一层代表一位数字,最高位为1
        // 当枚举到mod == 0的情况停止
        int i = bfs(); // 提取mod == 0的那一个节点
        // 知道这个节点编号就可以求出他所有的祖先编号
        vector<int> ans;
        while(i)
        {
            ans.push_back(i % 2);
            i /= 2;     // 访问父亲节点
        }
        reverse(ans.begin(), ans.end());
        for (i = 0; i < ans.size(); i ++ ) cout << ans[i];
        cout << endl;
    }
    // fclose(stdout);//输出结束
    return 0;
}

F.Prime Path(质数路径)

题意概括:

给定两个四位数的质数 a a a b b b a a a 为初状态, b b b 为末状态,要想从初状态变成末状态,可以进行以下操作:

选择当前质数,可以通过更换某一位的数字,来将这个质数变成另一个四位质数。

求最少需要多少次操作,才能将初状态变成末状态。

解题思路:

BFS问题,将每个四位质数作为一个状态,并通过条件判断能否转移,

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 1e4 + 10;

int a, b;
bool st[N];
int dist[N];	// 距离数组,记录需要几步到这个状态(质数)
int primes[N], p[N];
int cnt = 0, num = 0;

// 求出所有的四位质数(质数筛)
void init()
{
    for (int i = 2; i < N; i ++ )
    {
        if (!st[i])
        {
            primes[cnt ++] = i;
            if (i >= 1000 && i <= 9999) p[num ++] = i;
        }
        for (int j = 0; primes[j] <= N / i; j ++ )
        {
            st[primes[j] * i] = 1;
            if (i % primes[j] == 0) break;
        }
    }
}
// 判断是否能够转移
bool ok(int x, int y)
{
    int ans = 0;
    while (x)
    {
        int t1 = x % 10;
        int t2 = y % 10;
        x /= 10, y /= 10;
        if (t1 != t2) ans ++;
    }
    return ans == 1;
}

void solve()
{
    // 记得初始化距离数组
    memset(dist, -1, sizeof dist);
    cin >> a >> b;
    queue<int> q;
    while(q.size()) q.pop();
    q.push(a);
    dist[a] = 0;
    while(q.size())
    {
        int t = q.front(); q.pop();
        if (t == b) 
        {
            cout << dist[b] << endl;
            return;
        }
        // 枚举所有状态(四位质数)
        for (int j = 0; j < num; j ++ )	
        {
            // 判断能否转移 以及 是否到达过该状态
            if (ok(p[j], t) && dist[p[j]] == -1)
            {
                dist[p[j]] = dist[t] + 1;
                q.push(p[j]);
            }
        }
    }
    if (dist[b] == -1) cout << "Impossible\n";
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    int t;
    cin >> t;
    init();
    while (t --)
    {
        solve();
    }
    // fclose(stdout);//输出结束
    return 0;
}

G.Shuffle’m Up(洗牌)

主要知识点:BFS求最短路

题意概括:

给定两个字符串s1,s2,对其进行洗牌操作,即以下方式进行重组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9lmtCeRJ-1653023066918)(C:\Users\86133\AppData\Roaming\Typora\typora-user-images\image-20220520083415986.png)]

并将重组后的数组对半分,前半为s1,后半为s2,即可继续进行洗牌操作。

求最少需要多少次洗牌,才能将初状态变成末状态。

解题思路:

BFS问题,将s1+s2(合并)的长字符串作为状态,用洗牌操作更新状态,直到出现重复状态或目标状态

AC代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>

using namespace std;

const int N = 1e5 + 10;

int t;
int c;
string s1, s2, s;
map<string, bool> st;	// 记录状态是否出现过

int bfs()
{
    st.clear();
    string state = s1; string end = s;
    st[state] = 1;
    int ans = 0;
    while (state != end)	// 洗牌,直到出现目标状态
    {
        string cur = "";	
        for (int i = 0; i < c; i ++ ) 
        {
            // 先加上s2的牌
            cur += state[i + c];
            // 再加上s1的牌
            cur += state[i];
        }
        if (st[cur]) return -1;		// 出现重复状态,不可能达到目标状态
        st[cur] = 1;
        ans ++;
        state = cur;
    }
    return ans;
}

void solve()
{
    cin >> c;
    cin >> s1 >> s2 >> s;
    s1 += s2;	// 将s1,s2合并成为一个字符串作为状态
    cout << t << ' ' << bfs() << endl;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    int cnt;
    cin >> cnt;
    for (t = 1; t <= cnt; t ++ )
    {
        solve();
    }
    // fclose(stdout);//输出结束
    return 0;
}

H.Pots(水罐)

主要知识点:BFS求最短路

题意概括:

有两个水罐,可以进行以下三类操作(六种):

(1) 装满某个水罐(1号 或 2号)

(2) 清空某个水罐(1号 或 2号)

(3) 将某个水罐的水倒到另一个水罐(直到其中一个水罐满 或者 空)

状态设置是两个水罐中的水。

求最少需要多少次操作,才能将初状态变成末状态。

解题思路:

BFS问题,将两个水罐的水设为状态,枚举所有可能的操作,并记录前移状态以及其操作种类,通过末状态进行反向递推得到操作路径。

AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;

typedef pair<int, int> State;	// 用一对整数来代表状态

const int N = 1e5 + 10;

int a, b, c;

struct Ways		// 操作方式
{
    int op, a, b;	// 操作种类,操作对象
    State st;		// 前一个状态
};
map<State, Ways> pre;	// 记录一个状态的前一个,反向递推出路径
State ed;

int bfs()
{
    State t;
    map<State, int> dist;	// 初状态到这个状态所需的步数
    for (int i = 0; i <= a; i ++ )
    for (int j = 0; j <= b; j ++ )
    {
        t = {i, j};
        dist[t] = -1;
    }
    queue<State> q;
    t = {0, 0};
    q.push(t);
    dist[t] = 0;
    pre[t] = {0, 0, 0, t};
    while(q.size())
    {
        State t = q.front(); q.pop();
        // 达到目标状态时,返回步数
        if (t.x == c || t.y == c) 
        {
            ed = t;		// 记录最后状态,方便反推
            return dist[t];
        }
        State next;
        // 操作1,将第一个水罐装满
        if (t.x < a)
        {
            next = {a, t.y};
            if (dist[next] == -1) 
            {
                q.push(next);
                dist[next] = dist[t] + 1;
                pre[next] = {1, 1, 0, t};
            }
        }
        // 操作2,将第二个水罐装满
        if (t.y < b)
        {
            next = {t.x, b};
            if (dist[next] == -1) 
            {
                q.push(next);
                dist[next] = dist[t] + 1;
                pre[next] = {1, 2, 0, t};
            }
        }
        // 操作3,将第一个水罐清空
        if (t.x > 0)
        {
            next = {0, t.y};
            if (dist[next] == -1) 
            {
                q.push(next);
                dist[next] = dist[t] + 1;
                pre[next] = {2, 1, 0, t};
            }
        }
        // 操作4,将第二个水罐清空
        if (t.y > 0)
        {
            next = {t.x, 0};
            if (dist[next] == -1) 
            {
                q.push(next);
                dist[next] = dist[t] + 1;
                pre[next] = {2, 2, 0, t};
            }
        }
        // 操作5,将第一个水罐的水倒到第二个水罐
        if (t.x > 0 && t.y < b)
        {
            int delta = min(b - t.y, t.x);
            next = {t.x - delta, t.y + delta};
            if (dist[next] == -1) 
            {
                q.push(next);
                dist[next] = dist[t] + 1;
                pre[next] = {3, 1, 2, t};
            }
        }
        // 操作6,将第二个水罐的水倒到第一个水罐
        if (t.y > 0 && t.x < a)
        {
            int delta = min(a - t.x, t.y);
            next = {t.x + delta, t.y - delta};
            if (dist[next] == -1) 
            {
                q.push(next);
                dist[next] = dist[t] + 1;
                pre[next] = {3, 2, 1, t};
            }
        }
    }
    return -1;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    cin >> a >> b >> c;
    int n = bfs();		// n为达到目标状态的操作数
    if (n == -1) 
    {
        puts("impossible");
        return 0;
    }
    cout << n << endl;
    vector<Ways> v;
    for (int i = 0; i < n; i ++ )
    {
        v.push_back(pre[ed]);	// 递推n个操作
        ed = pre[ed].st;		// 并得到前一个状态
    }
    // 反向输出,才是正确的顺序,因为我们是从末状态反向推出来的操作
    for (int i = n - 1; i >= 0; i -- )
    {
        Ways path = v[i];
        int op, x, y;
        op = path.op;
        x = path.a;
        y = path.b;
        if (op == 1)
        {
            printf("FILL(%d)\n", x);
        }else if (op == 2)
        {
            printf("DROP(%d)\n", x);
        }else 
            printf("POUR(%d,%d)\n", x, y);
    }
    // fclose(stdout);//输出结束
    return 0;
}

I.Fire Game(玩火)(本题提交与ACwing评测机)

主要知识点:BFS求最短路

题意概括:

两个小孩在玩火,只有草丛 “#” 才能烧起来,两个小孩会在同时点燃两个草丛(也可能是同一个),求把地图上所有草丛都烧着的最短时间。

解题思路:

BFS问题,首先需要判断连通块个数,如果连通块大于2,则两个小孩不能通过一次点火点燃所有草丛;如果连通块等于2,则两个小孩需要分开点燃两堆草丛;如果连通块等于1,则两个小孩可以在这一堆里面任选 1~2 个草丛点燃。

点燃之后,计算所有草丛被点燃时的时间dist,最大的时间就是所有草丛都烧着的时间,枚举所有方案,并记录其中的最小值。

AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;

typedef pair<int, int> PII;

const int N = 100 + 10;

int n, m;
char g[N][N];
int dist[N][N];		// 当前草丛烧起来的所花的最短时间(顺带记录是否烧起来过)
vector<PII> p[N];	// 记录连通块中的节点
int cnt;			// 连通块(草丛)的个数cnt

int bfs(int flag, int x1, int y1, int x2, int y2)
{
    int mx = 0;
    // 判断连通块时千万不能每次都更新dist(记录是否访问)!
    if (flag > 1) memset(dist, -1, sizeof dist);
    int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
    queue<PII> q;
    q.push({x1, y1});	
    dist[x1][y1] = 0;
    if (flag == 1 || (x1 == x2 && y1 == y2));
    else 
    {
        q.push({x2, y2});	// 有时需要两个火源进行BFS
        dist[x2][y2] = 0;
    }
    while (q.size())
    {
        PII t = q.front(); q.pop();
        int x = t.x, y = t.y;
        if (flag == 1) 
        {
            p[cnt].push_back(t);
        }
        mx = max(mx, dist[x][y]);
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            PII next = {a, b};
            if (a <= 0 || a > n || b <= 0 || b > m) continue;
            if (g[a][b] == '#' && dist[a][b] == -1)
            {
                q.push(next);
                dist[a][b] = dist[x][y] + 1;
            }
        }
    }
    // cout << mx << endl;
    return mx;
}
// 判断连通块
void check()
{
    cnt = 0;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            if (g[i][j] == '#' && dist[i][j] == -1) 
            {
                cnt ++;
                if (cnt > 2) return;
                bfs(1, i, j, 0, 0);		// 对当前连通块进行标记
            }
}
// 枚举两个连通块的情况
int both()
{
    int mn = 1e9;
    // 两个小孩必须分别在两个连通块中放火
    for (int i = 0; i < p[1].size(); i ++ )
        for (int j = 0; j < p[2].size(); j ++ )
        {
            PII t1 = p[1][i], t2 = p[2][j];
            mn = min(mn, bfs(2, t1.x, t1.y, t2.x, t2.y));
        }
    return mn;
}
// 枚举一个连通块的情况
int only()
{
    int mn = 1e9;
    // 两个小孩在同一个连通块中放火,可以是同个点
    for (int i = 0; i < p[1].size(); i ++ )
        for (int j = 0; j < p[1].size(); j ++ )
        {
            PII t1 = p[1][i], t2 = p[1][j];
            mn = min(mn, bfs(3, t1.x, t1.y, t2.x, t2.y));
        }
    return mn;
}

int solve()
{
    memset(dist, -1, sizeof dist);
    p[1].clear();
    p[2].clear();
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) cin >> g[i] + 1;
    check();	// 判断连通块(草丛)的个数cnt
    int ans;
    if (cnt > 2) ans = -1;
    else if (cnt == 2) ans = both();
    else if (cnt) ans = only();
    else ans = -1;
    return ans;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    int t;
    cin >> t;
    for (int i = 1; i <= t; i ++ )
    {
        int ans = solve();
        printf("Case %d: %d\n", i, ans);
    }
    // fclose(stdout);//输出结束
    return 0;
}

I.Fire!(火灾)

主要知识点:BFS求最短路

题意概括:

在一个迷宫里,有复数个火源 和 一位逃生者,逃生者和火源都会以1格每单位时间的速度移动,逃生者需要从迷宫边缘逃生,求最短时间。

解题思路:

BFS问题,先对所有火源进行多源BFS,求出每个点被火焰燃烧的时间点,然后让逃生者进行模拟逃生(BFS),每个点,只有在火焰燃烧到之前到达,才能更新到达时间,否则就无法到达。

当BFS拓展到任何一个边界时,下一步就可以直接走出迷宫。

AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;

typedef pair<int, int> PII;

const int N = 1000 + 10;

int n, m;
char g[N][N];
// dist是火焰到达的时间点, d是逃生者到达的时间点
int dist[N][N], d[N][N];		
int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
// 火源先拓展
void bfs1()
{
    // 这里必须用一个较大的值来表示这个点火焰没有到达
    memset(dist, 0x3f, sizeof dist);
    queue<PII> q1;
    while (q1.size()) q1.pop();
    // 导入所有火源
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            if (g[i][j] == 'F')
            {
                PII t = {i, j};
                q1.push(t);
                dist[i][j] = 0;
            }
   	// 正常的BFS拓展
    while (q1.size())    
    {
        PII t = q1.front(); q1.pop();
        int x = t.x, y= t.y;
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            if (g[a][b] == '#') continue;
            if (a <= 0 || a > n || b <= 0 || b > m) continue;
            if (dist[a][b] == 0x3f3f3f3f)
            {
                dist[a][b] = dist[x][y] + 1;
                PII next = {a, b};
                q1.push(next);
            } 
        }
    }
}
// 逃生者模拟逃生
int bfs2()
{
    // 这里就不需要用大数标记,用-1表示是否到达过就行了。
    memset(d, -1, sizeof d);
    queue<PII> q;
    while (q.size()) q.pop();
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            if (g[i][j] == 'J')
            {
                PII t = {i, j};
                q.push(t);
                d[i][j] = 0;
            }   
    while (q.size())
    {
        PII t = q.front(); q.pop();
        int x = t.x, y = t.y;
        // 第一次到达边界,答案就是加一步出迷宫
        if (x == 1 || x == n || y == 1 || y == m) return d[x][y] + 1;
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            if (g[a][b] == '#') continue;
            if (a <= 0 || a > n || b <= 0 || b > m) continue;
            // 只有dist大于d,才能进行拓展(这就是为什么要用大数标记dist)
            if (d[x][y] + 1 < dist[a][b] && d[a][b] == -1)
            {
                PII next = {a, b};
                q.push(next);
                d[a][b] = d[x][y] + 1;
            }
        }   
    }    
    return -1;
}

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> g[i] + 1; 

    bfs1();
    int ans = bfs2();
    if (ans == -1) puts("IMPOSSIBLE");
    else cout << ans << endl;
    // for (int i = 1; i <= n; i ++ )  
    // {
    //     for (int j = 1; j <= n; j ++ )
    //         cout << dist[i][j] << ' ';
    //     cout << endl;
    // }
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    int t;
    cin >> t;
    for (int i = 1; i <= t; i ++ )
    {
        solve();
    }
    // fclose(stdout);//输出结束
    return 0;
}

J.迷宫问题

主要知识点:BFS求最短路

题意概括:

给定一个 $5 \times 5 $ 的迷宫矩阵,求左上角 ( 0 , 0 ) (0,0) (0,0) 到 右下角 ( 4 , 4 ) (4,4) (4,4) 的最短路径

注意,输出所有走过的点,即为路径

解题思路:

BFS问题,只不过需要记录路径。

在水罐那道题我们已经见识了反推路径,这道题我们来用个新的方法,从终点开始走到起点,这样直接得到的就是正向路径了。

AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;

typedef pair<int, int> PII;

const int N = 10 + 10;

int g[N][N];
int st[N][N];		// 用一个数组记录是否重复走过就行了
map<PII, PII> pre;	// 记录前一个节点

int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};

void bfs(PII ed)
{
    memset(st, 0, sizeof st);
    queue<PII> q;
    q.push(ed);
    st[ed.x][ed.y] = 1;
    while(q.size())
    {
        PII t = q.front(); q.pop();
        int x = t.x, y = t.y;
        if (x == 0 && y == 0) return;
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            PII next;
            next = {a, b};
            if (a < 0 || b < 0 || a >= 5 || b >= 5) continue;
            if (g[a][b] == 1) continue;
            if (st[a][b] == 1) continue;
            q.push(next);
            pre[next] = t;
            st[a][b] = 1;
        }
    }
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    for (int i = 0; i < 5; i ++ )
        for (int j = 0; j < 5; j ++ )
            cin >> g[i][j];
    PII ed;
    ed = {4, 4};
    bfs(ed);
    PII state;
    state = {0, 0};
    while(state.x != 4 || state.y != 4)
    {
        // 得到节点,直接输出
        printf("(%d, %d)\n", state.x, state.y);
        state = pre[state];
    }
    // 最后一个节点手动输出
    printf("(%d, %d)\n", state.x, state.y);
    // fclose(stdout);//输出结束
    return 0;
}

K.Oil Deposits(石油储量)

主要知识点:BFS求最短路

题意概括:

可以认为,在以某个石油的九宫格内的石油,都是同一批石油,否则则认为不是同一批;给定一个地图,求地图上有多少批不同的石油。

解题思路:

BFS判断连通块问题,判断有多少个连通块即可,连通块的注释可以看下"玩火"那道题

AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;

typedef pair<int, int> PII;

const int N = 100 + 10;

int n, m;
char g[N][N];
int st[N][N];

int cnt;
void bfs(int i, int j)
{
    queue<PII> q;
    q.push({i, j});
    st[i][j] = cnt;
    while (q.size())
    {
        PII t = q.front(); q.pop();
        int x = t.x, y = t.y;
        for (int i = -1; i <= 1; i ++ )
            for (int j = -1; j <= 1; j ++ )
            {
                int a = x + i, b = y + j;
                if (a <= 0 || a > n || b <= 0 || b > m) continue;
                if (st[a][b]) continue;
                if (g[a][b] != '@') continue;
                PII next = {a, b};
                q.push(next);
                st[a][b] = 1;
            }
    }
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    while (cin >> n >> m && n)
    {
        memset(st, 0, sizeof st);
        cnt = 0;
        for (int i = 1; i <= n; i ++ ) cin >> g[i] + 1;
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
            {
                if (g[i][j] == '@' && st[i][j] == 0) 
                {
                    cnt ++;
                    bfs(i, j);
                }
            }
        cout << cnt << endl;
    }
    
    // fclose(stdout);//输出结束
    return 0;
}

L.非常可乐

主要知识点:BFS求最短路

题意概括:

一瓶可乐的容量为s,有两个空杯子容量分别为n,m;

三个杯/瓶可以相互倒,每次倒可乐算一步,求恰好平分可乐的最小步数。

解题思路:

类似于水罐那道题,只不过这次我们需要自己分析操作的种类。

将三个杯/瓶分别称为 a, b, c。

(1)a -> b

(2)a -> c

(3)b -> a

(4)b -> c

(5)c -> a

(6)c -> b

倒水的方式跟水罐那道题相互倒水几乎一样。

AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;

typedef pair<int, int> PII;
typedef pair<int, pair<int, int>> PIII;
const int N = 100 + 10;

int s, n, m;

int dist[N][N][N];	// 记录到某个状态的最短步数

int bfs()
{
    memset(dist, -1, sizeof dist);
    queue<PIII> q;
    PIII start = {s, {0, 0}};	// 这里可以用结构体定义,可读性会强点
    q.push(start);
    dist[s][0][0] = 0;
    while (q.size())
    {
        PIII t = q.front(); q.pop();
        int a = t.x, b = t.y.x, c = t.y.y;
        // 终止状态
        // 如果其中一杯已经满足有一半的可乐了,那么另一半一定在另外两杯中
        if (a == s / 2 || c == s / 2 || b == s / 2) 
        {
            int ans = dist[a][b][c];
            // 如果没有一个为空(则另外一个也是一半)
            // 则需要再多加一步将两个合并为另一半可乐
            if (a && b && c) ans ++;
            return ans;
        }
        if (a < s)
        {
        	// (1)a -> b
            if (b > 0)
            {
                int da = min(s - a, b);
                PIII next = {a + da, {b - da, c}};
                if (dist[a + da][b - da][c] == -1) 
                {
                    dist[a + da][b - da][c] = dist[a][b][c] + 1;
                    q.push(next);
                }
            }

            // (2)a -> c
            if (c > 0)
            {
                int da = min(s - a, c);
                PIII next = {a + da, {b, c - da}};
                if (dist[a + da][b][c - da] == -1)
                {
                    dist[a + da][b][c - da] = dist[a][b][c] + 1;
                    q.push(next);
                }
            }
        }
        if (b < n)
        {

            // (3)b -> a
            if (a > 0)
            {
                int db = min(n - b, a);
                PIII next = {a - db, {b + db, c}};
                if (dist[a - db][b + db][c] == -1)
                {
                    dist[a - db][b + db][c] = dist[a][b][c] + 1;
                    q.push(next);
                }
            }

            // (4)b -> c
            if (c > 0)
            {
                int db = min(n - b, c);
                PIII next = {a, {b + db, c - db}};
                if (dist[a][b + db][c - db] == -1)
                {
                    dist[a][b + db][c - db] = dist[a][b][c] + 1;
                    q.push(next);
                }
            }
        }
        if (c < m)
        {

            // (5)c -> a
            if (a > 0)
            {
                int dc = min(m - c, a);
                PIII next = {a - dc, {b, c + dc}};
                if (dist[a - dc][b][c + dc] == -1) 
                {
                    dist[a - dc][b][c + dc] = dist[a][b][c] + 1;
                    q.push(next);
                }
            }

            // (6)c -> b
            if (b > 0)
            {
                int dc = min(m - c, b);
                PIII next = {a, {b - dc, c + dc}};
                if (dist[a][b - dc][c + dc] == -1) 
                {
                    dist[a][b - dc][c + dc] = dist[a][b][c] + 1;
                    q.push(next);
                }
            }
        }
    }
    return -1;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    while (cin >> s >> n >> m && s && n && m)
    {
        if (s % 2)
        {
            cout << "NO\n";
            continue;
        }
        int ans = bfs();
        if (ans == -1) puts("NO");
        else cout << ans << endl;
    }
    
    // fclose(stdout);//输出结束
    return 0;
}

M.Find a way(找"去KFC的"路)

主要知识点:BFS求最短路

题意概括:

地图上标记了两个人和若干个KFC的位置。

这两个人想去同一个KFC,求两个人花的总时间最少为多少。

解题思路:

BFS问题,两次BFS分别预处理两个人到所有KFC的距离,最后求距离之和最小的即可

AC代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#define x first
#define y second
using namespace std;

typedef pair<int, int> PII;
typedef pair<int, pair<int, int>> PIII;
const int N = 200 + 10;

int dx[] = {1, 0, -1, 0}, dy[] = {0, 1, 0, -1};
int n, m;
char g[N][N];
// dis:第一个人到地图上点的最短距离
// d:第二个人到地图上点的最短距离
int dis[N][N], d[N][N];		
PII a, b;	// 记录两个人的坐标
// 预处理第一个人到地图上点的最短距离
void bfs1()
{
    // 求最短距离的时候最好还是用大数标记,否则有的KFC走不到就麻烦了
    memset(dis, 0x3f, sizeof dis);
    queue<PII> q1;
    q1.push(a);
    dis[a.x][a.y] = 0;
    while (q1.size())
    {
        PII t = q1.front(); q1.pop();
        int x = t.x , y = t.y;
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            if (a <= 0 || a > n || b <= 0 || b > m) continue;
            if (g[a][b] == '#') continue;
            if (dis[a][b] == 0x3f3f3f3f) 
            {
                dis[a][b] = dis[x][y] + 1;
                q1.push({a, b});
            }
        }
    }
}
// 预处理第二个人到地图上点的最短距离
void bfs2()
{
    memset(d, 0x3f, sizeof d);
    queue<PII> q;
    q.push(b);
    d[b.x][b.y] = 0;
    while (q.size())
    {
        PII t = q.front(); q.pop();
        int x = t.x , y = t.y;
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            if (a <= 0 || a > n || b <= 0 || b > m) continue;
            if (g[a][b] == '#') continue;
            if (d[a][b] == 0x3f3f3f3f) 
            {
                d[a][b] = d[x][y] + 1;
                q.push({a, b});
            }
        }
    }
}

void solve()
{
    vector<PII> ans;	// ans 记录KFC的位置
    ans.clear();
    for (int i = 1; i <= n; i ++ ) cin >> g[i] + 1;
    for (int i = 1; i <= n; i ++ )
        for (int j  = 1; j <= m; j ++ )
        {
            if (g[i][j] == 'Y') a = {i, j};
            if (g[i][j] == 'M') b = {i, j};
            if (g[i][j] == '@') ans.push_back({i, j});
        }
    bfs1();
    bfs2();
    int mn = 1e9;
    // 分别求所有
    for (int i = 0; i < ans.size(); i ++ )
    {
		int x = ans[i].x, y = ans[i].y;
		mn = min(mn, dis[x][y] + d[x][y]);
    }
    cout << mn * 11 << endl;
}

int main()
{
    // freopen("data.in","r",stdin);  // 文件输入
    // freopen("data.out","w",stdout);// 文件输出
    while (cin >> n >> m)
    {
        solve();
    }
    // fclose(stdout);//输出结束
    return 0;
}
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值