信息学奥赛一本通基础算法 5 - 搜索与回溯算法

1317:【例5.2】组合的输出

#include <iostream>
#include <vector>
using namespace std;

// dfs函数用于深度优先搜索
void dfs(int start, int n, int r, vector<int>& combo) {
    // 如果组合的长度等于r,打印当前组合
    if (combo.size() == r) {
        for (int i = 0; i < r; i++) {
            cout << "  " << combo[i]; // 每个元素占三个字符的位置
        }
        cout << endl;
        return;
    }

    // 从start到n遍历每个可能的元素
    for (int i = start; i <= n; i++) {
        combo.push_back(i); // 将当前元素添加到组合中
        dfs(i + 1, n, r, combo); // 递归调用,注意i+1作为新的起点
        combo.pop_back(); // 回溯,移除最后一个元素,尝试下一个可能的元素
    }
}

int main() {
    int n, r;
    cin >> n >> r; // 从用户输入读取n和r的值

    vector<int> combo; // 用于存储当前的组合
    dfs(1, n, r, combo); // 从1开始搜索

    return 0;
}

1318:【例5.3】自然数的拆分

#include <iostream>
#include <vector>
using namespace std;

void dfs(int n, vector<int>& path, int start, int originalN) {
    if (n == 0 && path.size() > 1) { // 确保路径包含至少两个数字
        cout << originalN << "=";
        for (size_t i = 0; i < path.size(); ++i) {
            cout << path[i];
            if (i < path.size() - 1) cout << "+";
        }
        cout << endl;
        return;
    }
    
    for (int i = start; i <= n; ++i) {
        path.push_back(i);
        dfs(n - i, path, i, originalN);
        path.pop_back();
    }
}

int main() {
    int n;
    cin >> n;

    vector<int> path;
    dfs(n, path, 1, n);
    return 0;
}

1212:LETTERS

针对运行超时的问题,我们需要优化上述代码。考虑到题目的要求是不可以移向曾经经过的字母,我们实际上需要一个方式来确保在每条路径探索中不会遇到重复的字母。这意味着我们需要追踪每条路径上已经访问过的字母,而不仅仅是位置。为了避免超时,我们可以采用位运算来高效地追踪已访问的字母状态。

优化思路

  • 使用一个整数(假设是32位)的每一位来代表一个字母是否被访问过。由于大写字母总共有26个,因此我们只需要26位就足够了。例如,如果’A’被访问过,那么我们就将第0位设为1。
  • 每次访问一个新的字母时,我们检查相应的位是否为0(表示该字母还未被访问过)。如果是,则继续DFS;否则,回溯。
  • 通过这种方式,我们可以快速检查任何字母的访问状态,并且在回溯时轻松地撤销状态更改。

优化后的C++代码

#include <iostream>
#include <vector>
using namespace std;

int R, S;
vector<string> grid;
int dx[] = {0, 0, 1, -1}; // 四个方向
int dy[] = {1, -1, 0, 0};
int maxLen = 0;

// 使用位运算来追踪访问过的字母
void dfs(int x, int y, int len, int visited) {
    maxLen = max(maxLen, len);
    
    for (int i = 0; i < 4; ++i) {
        int nx = x + dx[i];
        int ny = y + dy[i];

        if (nx >= 0 && nx < R && ny >= 0 && ny < S) {
            int bit = 1 << (grid[nx][ny] - 'A');
            if (!(visited & bit)) { // 如果该字母未被访问过
                dfs(nx, ny, len + 1, visited | bit); // 标记为已访问并继续DFS
            }
        }
    }
}

int main() {
    cin >> R >> S;
    grid.resize(R);

    for (int i = 0; i < R; ++i) {
        cin >> grid[i];
    }

    int initialVisited = 1 << (grid[0][0] - 'A'); // 初始化访问状态
    dfs(0, 0, 1, initialVisited); // 从(0,0)开始DFS

    cout << maxLen << endl;
    return 0;
}

这段代码通过位运算和DFS相结合的方式来避免重复访问字母,并且可以有效地解决超时问题。通过这种方式,我们不仅能够保证不会重复访问相同的字母,还能以更高效的方式追踪每条路径上的字母状态,从而优化整个搜索过程。
当使用位运算来跟踪已访问过的字母时,我们实际上是将一个整数的每一位用作一个标志位,来表示对应的字母是否已经被访问过。这种方法非常高效,因为它允许我们在常数时间内检查和更新访问状态。

让我们以一个简化的例子来演示这个过程。

示例

假设字母矩阵如下:

AB
CD

并且我们从左上角的A开始移动。

  1. 初始状态

    • 访问A,我们设置一个整数visited来跟踪访问状态。由于A是第一个字母,我们将visited的第0位设为1。如果用二进制表示,visited = 0001(这里为了简化只展示了4位,实际上需要26位来表示所有大写字母)。
  2. 移动到B

    • 接下来,我们尝试移动到BB对应于visited中的第1位。我们检查这一位是否为0,发现是的(因为visited = 0001),这意味着B尚未被访问。
    • 我们将B标记为已访问,通过将visited0010进行OR操作(|),结果是visited = 0011
  3. 尝试移动到C

    • B我们尝试向下移动到C。同样,C对应于visited中的第2位。检查这一位,发现是0(visited = 0011)。
    • 标记C为已访问,通过将visited0100进行OR操作,结果是visited = 0111
  4. 回溯到B并尝试其他方向

    • 假设我们回溯到B尝试其他方向,此时visited不变,仍然为0111
    • 但由于AC已经在visited中标记为已访问,我们不能再访问它们。

通过上述过程,我们可以看到,使用位运算跟踪访问状态非常高效和直接。对于每个字母,我们只需要检查和更新visited的对应位即可。这种方法的空间效率非常高,因为一个32位的整数就足够表示所有26个大写英文字母的访问状态,而且操作简单、执行快速,非常适合解决这类问题。

1216:红与黑

这个问题可以通过深度优先搜索(DFS)或广度优先搜索(BFS)算法来解决。我们将使用DFS算法的思路来解决这个问题,因为DFS非常适合于此类在网格上进行探索的问题。

思路:

  1. 初始化: 创建一个二维数组作为地图,用来存储每个瓷砖的颜色,同时记录地图的宽度W和高度H

  2. 找到起始点: 遍历地图,找到标记为'@'的瓷砖,这是起始点。记录起始点的位置。

  3. 深度优先搜索:

    • 从起始点开始,探索所有可以到达的黑色瓷砖('.')。
    • 对于当前的瓷砖,尝试向四个方向(上、下、左、右)移动。
    • 如果下一个瓷砖是黑色的,并且之前没有访问过,那么递归地继续探索这个新瓷砖。
    • 使用一个二维布尔数组来记录每个瓷砖是否已经被访问过,以防止重复计数或无限循环。
  4. 计数:

    • 使用一个全局变量或者传递一个引用/指针参数来记录已经访问过的黑色瓷砖数量。
  5. 输出结果: 每完成一组数据的DFS后,输出已经访问过的黑色瓷砖数量。

C++代码实现:

#include <iostream>
#include <vector>
using namespace std;

int W, H;
vector<vector<char>> grid;
vector<vector<bool>> visited;
int count;

// 方向数组,分别代表上、下、左、右移动
int dx[4] = {0, 0, -1, 1};
int dy[4] = {-1, 1, 0, 0};

void dfs(int x, int y) {
    visited[y][x] = true; // 标记当前瓷砖为已访问
    count++; // 增加可以到达的黑色瓷砖数量

    for (int i = 0; i < 4; ++i) { // 遍历四个方向
        int nx = x + dx[i]; // 计算新位置的x坐标
        int ny = y + dy[i]; // 计算新位置的y坐标

        // 检查新位置是否有效,包括边界检查、是否为黑色瓷砖、是否未访问
        if (nx >= 0 && nx < W && ny >= 0 && ny < H && grid[ny][nx] == '.' && !visited[ny][nx]) {
            dfs(nx, ny); // 对有效的新位置进行深度优先搜索
        }
    }
}

int main() {
    while (cin >> W >> H && (W || H)) {
        grid.assign(H, vector<char>(W));
        visited.assign(H, vector<bool>(W, false));
        count = 0;
        int startX = -1, startY = -1;

        for (int i = 0; i < H; ++i) {
            for (int j = 0; j < W; ++j) {
                cin >> grid[i][j];
                if (grid[i][j] == '@') {
                    startX = j; // 记录起始位置的x坐标
                    startY = i; // 记录起始位置的y坐标
                }
            }
        }

        if (startX != -1 && startY != -1 && grid[startY][startX] == '@') {
            dfs(startX, startY); // 从起始位置开始深度优先搜索
        }
        
        cout << count << endl; // 输出结果
    }
    return 0;
}

这段代码首先读入地图的尺寸和每个瓷砖的颜色,然后找到起始位置并从那里开始进行深度优先搜索。它使用visited数组来避免重复访问相同的瓷砖,并使用count变量来记录可以到达的黑色瓷砖数。最后,它输出从给定起始位置能到达的黑色瓷砖数量。

1217:棋盘问题

为了解决这个问题,我们可以采用回溯法。回溯法是一种通过逐步构建解决方案并且一旦当前步骤不能继续往前走就退回上一步然后再次尝试的算法。对于这个问题,我们需要在棋盘上摆放k个棋子,同时保证它们不在同一行或同一列。

思路

  1. 遍历棋盘:从棋盘的第一行开始,尝试在每个允许的位置(即#表示的位置)放置一个棋子。
  2. 检查冲突:在放置每个棋子之前,检查当前列以及之前的行中是否已经放置了棋子,因为棋子不能放在同一行或同一列。
  3. 递归:对于每个允许放置棋子的位置,放置一个棋子后,递归地尝试在剩下的行中放置剩余的棋子。
  4. 回溯:如果当前行没有合法的位置可以放置棋子或已经放置了所有棋子,回溯到上一步,即移除当前行的棋子,尝试下一个可能的位置。
  5. 计数:每当成功地在棋盘上放置了k个棋子,就增加一种方案。

C++代码实现

#include <iostream>
#include <vector>
using namespace std;

int n, k, ans;
vector<string> board;
vector<bool> colUsed;

void dfs(int row, int placed) {
    if (placed == k) {
        ans++;
        return;
    }
    if (row >= n) return;
    for (int col = 0; col < n; ++col) {
        if (board[row][col] == '#' && !colUsed[col]) {
            colUsed[col] = true;
            dfs(row + 1, placed + 1);
            colUsed[col] = false;
        }
    }
    dfs(row + 1, placed); // 尝试不在这一行放置棋子
}

int main() {
    while (cin >> n >> k && n != -1 && k != -1) {
        board.resize(n);
        for (int i = 0; i < n; ++i) {
            cin >> board[i];
        }
        colUsed.assign(n, false);
        ans = 0;
        dfs(0, 0);
        cout << ans << endl;
    }
    return 0;
}

这段代码首先读取输入,包括棋盘的大小和棋子的数量,然后通过dfs函数递归地尝试在棋盘的每一行放置棋子。colUsed数组用于记录每一列是否已经放置了棋子,以确保不会在同一列放置多个棋子。每次递归尝试在当前行的每个可能的位置放置一个棋子,然后递归处理下一行。如果放置了k个棋子,就增加方案数ans。最后,输出所有可能的方案数。

1218:取石子游戏

要解决这个问题,我们可以使用深度优先搜索(DFS)来遍历所有可能的游戏状态,并应用给定的提示规则来判断先手是否能赢。这个问题的关键在于理解当一堆石子的数量是另一堆的两倍或更多时,先手玩家有可能直接赢得游戏。否则,游戏会进入一个状态,其中每一步都有唯一的取法,直到其中一堆石子被完全取走。

以下是用C++编写的解决方案的大致思路:

  1. 基本逻辑:根据题目描述,我们需要判断在给定的两堆石子数量下,先手是否能够获胜。这可以通过递归地模拟取石子的过程来实现,每次递归时交换玩家的角色。

  2. 递归终止条件:如果当前状态下,一堆石子的数量是另一堆的两倍或更多,则当前操作的玩家赢。如果两堆石子中的任何一堆数量为0,则当前操作的玩家输。

  3. 搜索过程:从较多的那堆石子中取出一定数量的石子,数量是较少那堆的整数倍,然后递归地调用函数模拟下一步操作。

  4. 优化:考虑到可能存在重复状态,我们可以使用记忆化搜索来避免重复计算已经计算过的状态。

以下是对应的C++代码示例:

#include <iostream>
#include <cstring>
using namespace std;

bool dfs(int a, int b) {
    if (a < b) swap(a, b); // 确保a是较大的那堆石子
    if (a % b == 0) return true; // 如果a是b的倍数,则先手必胜
    if (a / b >= 2) return true; // 如果a至少是b的两倍,先手也必胜
    return !dfs(a - b, b); // 取走b个石子,交换玩家,如果对手输,则当前玩家赢
}

int main() {
    int a, b;
    while (cin >> a >> b && (a || b)) { // 读取a和b,直到遇到0 0
        if (dfs(a, b)) cout << "win" << endl;
        else cout << "lose" << endl;
    }
    return 0;
}

这个程序首先检查a是否大于等于b的两倍,或者a是否是b的倍数。如果满足任何一个条件,先手玩家将赢得比赛。如果不满足,程序递归地考虑取石子后的情况,直到找到赢的策略或确定无法赢得比赛。在递归调用中,我们通过!dfs(a - b, b)来模拟交换玩家角色,如果这个调用返回false(即对手输了),当前玩家就赢了。

1219:马走日

为了解决这个问题,我们可以使用回溯法来尝试每一种可能的移动,并计算出遍历整个棋盘的所有可能途径。在这个问题中,马的移动规则遵循中国象棋中的“日”字形规则,这意味着马可以从当前位置移动到8个可能的位置,具体取决于它们是否在棋盘上。

这里是解决这个问题的基本步骤:

  1. 初始化棋盘:创建一个n×m的棋盘,所有格子初始标记为未访问。

  2. 马的移动规则:确定马能移动的8个方向。在中国象棋中,马的移动可以用坐标变化来表示,例如:(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2)

  3. 回溯搜索:从初始位置开始,尝试每一种可能的移动。如果移动有效(即移动后的位置在棋盘内且未被访问过),则继续从新位置递归搜索。

  4. 统计途径总数:每当成功访问到棋盘上的所有点时,途径总数加一。需要注意的是,每访问一个新点,就标记为已访问,回溯后需要撤销这个标记。

下面是对应的C++代码示例:

#include <iostream>
#include <vector>

using namespace std;

int dx[] = {2, 2, -2, -2, 1, 1, -1, -1}; // 马的8个可能移动方向
int dy[] = {1, -1, 1, -1, 2, -2, 2, -2};
int n, m, cnt;

void dfs(vector<vector<bool>>& visited, int x, int y, int steps) {
    if (steps == n * m) { // 所有点都已经访问过
        cnt++;
        return;
    }
    for (int i = 0; i < 8; ++i) {
        int nx = x + dx[i], ny = y + dy[i];
        if (nx >= 0 && nx < n && ny >= 0 && ny < m && !visited[nx][ny]) {
            visited[nx][ny] = true; // 标记为已访问
            dfs(visited, nx, ny, steps + 1); // 递归搜索
            visited[nx][ny] = false; // 回溯,撤销标记
        }
    }
}

int main() {
    int T, x, y;
    cin >> T;
    while (T--) {
        cin >> n >> m >> x >> y;
        vector<vector<bool>> visited(n, vector<bool>(m, false)); // 初始化棋盘
        cnt = 0; // 初始化途径总数
        visited[x][y] = true; // 标记初始位置为已访问
        dfs(visited, x, y, 1); // 从初始位置开始搜索
        cout << cnt << endl; // 输出结果
    }
    return 0;
}

这段代码首先定义了一个全局变量cnt来统计马遍历棋盘的途径总数。dfs函数是核心函数,它尝试所有可能的移动并递归地搜索整个棋盘。每次成功遍历整个棋盘时,cnt会增加。最后,输出每组数据的途径总数。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天秀信奥编程培训

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值