C++信息学奥赛

搜索与回溯算法详解

掌握搜索与回溯算法,攻克信息学奥赛中的复杂问题

搜索与回溯在信息学奥赛中的重要性

搜索与回溯算法是信息学奥林匹克竞赛中的核心解题方法,尤其在处理不确定性问题、组合优化问题和路径查找问题时表现出色。

许多看似复杂的问题,通过巧妙地运用搜索与回溯思想,可以转化为可解的形式。本教程将系统讲解深度优先搜索、广度优先搜索、回溯法及其优化技巧,帮助你在竞赛中灵活运用这些算法解决实际问题。

搜索与回溯基本概念

搜索算法是指在问题的解空间中寻找满足特定条件的解的过程。回溯法则是一种特殊的搜索方法,它在搜索过程中不断尝试并在发现当前路径无法到达目标时回退到上一步,尝试其他可能的路径。

搜索算法的分类

  • 盲目搜索:不依赖问题本身的特性,按固定策略进行搜索(如深度优先、广度优先)
  • 启发式搜索:利用问题相关的启发信息指导搜索,提高效率(如A*算法)
  • 精确搜索:保证找到最优解(如回溯法、分支定界)
  • 近似搜索:在有限时间内找到接近最优的解(如模拟退火、遗传算法)

回溯法的基本思想

回溯法采用"尝试-失败-回退-再尝试"的策略,通常可以用递归实现:

  1. 选择:在当前状态下选择一个可能的选项
  2. 约束:检查选择是否满足问题的约束条件
  3. 递归:如果满足约束,递归处理下一个状态
  4. 回溯:如果递归结束或选择不满足约束,撤销当前选择,尝试其他选项

回溯法可以形象地理解为"走迷宫":遇到死胡同就退回到上一个岔路口,选择另一条路继续走。

适合用搜索与回溯解决的问题

组合问题

子集、排列、组合等问题,如N皇后问题、子集和问题

路径问题

迷宫求解、最短路径、连通性问题

优化问题

在有限选项中寻找最优解,如旅行商问题

深度优先搜索 (DFS)

算法原理

深度优先搜索(DFS)是一种沿着树的深度优先遍历树的节点的算法,当无法继续前进时,回溯到上一个节点,选择另一条路继续搜索。

基本步骤:

  1. 访问当前节点,并标记为已访问
  2. 对当前节点的每个未访问的邻接节点,递归执行DFS
  3. 当所有邻接节点都被访问后,回溯到上一级节点

特点与应用

  • 空间复杂度低,只需存储一条路径的信息
  • 适合解决连通性问题、拓扑排序、迷宫问题等
  • 可以用递归或栈实现
  • 不保证找到最短路径

DFS遍历过程示意图

DFS遍历示意图

DFS遍历顺序:1 → 2 → 4 → 8 → 5 → 3 → 6 → 7

递归实现代码

// 图的深度优先搜索(递归实现)
#include <iostream>
#include <vector>
using namespace std;

const int MAXN = 1005;
vector<int> adj[MAXN]; // 邻接表表示图
bool visited[MAXN];    // 标记节点是否被访问

// 从节点u开始进行DFS
void dfs(int u) {
    visited[u] = true;  // 标记为已访问
    cout << u << " ";   // 访问节点u
    
    // 遍历所有未访问的邻接节点
    for (int v : adj[u]) {
        if (!visited[v]) {
            dfs(v);     // 递归访问
        }
    }
}

int main() {
    int n, m;
    cin >> n >> m;      // n:节点数, m:边数
    
    // 读入边
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u); // 无向图
    }
    
    // 初始化访问标记
    for (int i = 1; i <= n; i++) {
        visited[i] = false;
    }
    
    // 对所有连通分量执行DFS
    for (int i = 1; i <= n; i++) {
        if (!visited[i]) {
            cout << "DFS遍历: ";
            dfs(i);
            cout << endl;
        }
    }
    
    return 0;
}

非递归实现与应用示例

非递归实现(栈)

// 图的深度优先搜索(非递归实现)
#include <iostream>
#include <vector>
#include <stack>
using namespace std;

const int MAXN = 1005;
vector<int> adj[MAXN];
bool visited[MAXN];

// 从节点start开始进行DFS
void dfs_non_recursive(int start) {
    stack<int> stk;
    stk.push(start);
    visited[start] = true;
    
    cout << "DFS遍历: ";
    
    while (!stk.empty()) {
        int u = stk.top();
        stk.pop();
        cout << u << " ";
        
        // 将未访问的邻接节点入栈
        // 注意:逆序入栈以保持与递归版本相同的访问顺序
        for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
            int v = *it;
            if (!visited[v]) {
                visited[v] = true;
                stk.push(v);
            }
        }
    }
    cout << endl;
}

int main() {
    // 与递归版本相同
    int n, m;
    cin >> n >> m;
    
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    
    for (int i = 1; i <= n; i++) {
        visited[i] = false;
    }
    
    for (int i = 1; i <= n; i++) {
        if (!visited[i]) {
            dfs_non_recursive(i);
        }
    }
    
    return 0;
}

应用示例:岛屿数量

问题描述:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算岛屿的数量。岛屿由相邻的陆地连接形成,每个单元格只与其水平或垂直方向上的单元格相邻。

// DFS应用:岛屿数量
#include <iostream>
#include <vector>
using namespace std;

// 方向数组:上、右、下、左
const int dx[] = {-1, 0, 1, 0};
const int dy[] = {0, 1, 0, -1};

// 深度优先搜索,将所有相连的陆地标记为已访问
void dfs(vector<vector= n || y < 0 || y >= m) return;
    // 如果不是陆地或已访问,则返回
    if (grid[x][y] == '0' || visited[x][y]) return;
    
    visited[x][y] = true; // 标记为已访问
    
    // 访问四个方向
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        dfs(grid, visited, nx, ny);
    }
}

int numIslands(vector<vector

广度优先搜索 (BFS)

算法原理

广度优先搜索(BFS)是一种按层次顺序遍历的算法,先访问完当前层的所有节点,再访问下一层的节点。

基本步骤:

  1. 创建一个队列,将起始节点入队并标记为已访问
  2. 当队列非空时,出队一个节点u并访问它
  3. 将u的所有未访问的邻接节点入队,并标记为已访问
  4. 重复步骤2-3,直到队列为空

特点与应用

  • 空间复杂度较高,需要存储当前层的所有节点
  • 保证找到最短路径(在无权图中)
  • 适合解决最短路径、层序遍历、连通性问题等
  • 通常用队列实现

BFS遍历过程示意图

BFS遍历示意图

BFS遍历顺序:1 → 2 → 3 → 4 → 5 → 6 → 7 → 8

实现代码

// 图的广度优先搜索
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

const int MAXN = 1005;
vector<int> adj[MAXN]; // 邻接表表示图
bool visited[MAXN];    // 标记节点是否被访问
int dist[MAXN];        // 存储到起始节点的距离

// 从节点start开始进行BFS
void bfs(int start) {
    queue<int> q;
    
    // 初始化起始节点
    visited[start] = true;
    dist[start] = 0;
    q.push(start);
    
    cout << "BFS遍历: ";
    
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        cout << u << " ";
        
        // 遍历所有未访问的邻接节点
        for (int v : adj[u]) {
            if (!visited[v]) {
                visited[v] = true;
                dist[v] = dist[u] + 1; // 距离+1
                q.push(v);
            }
        }
    }
    cout << endl;
}

int main() {
    int n, m;
    cin >> n >> m;      // n:节点数, m:边数
    
    // 读入边
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u); // 无向图
    }
    
    // 初始化
    for (int i = 1; i <= n; i++) {
        visited[i] = false;
        dist[i] = -1;
    }
    
    // 对所有连通分量执行BFS
    for (int i = 1; i <= n; i++) {
        if (!visited[i]) {
            bfs(i);
            
            // 输出距离示例
            cout << "距离起始点" << i << "的距离: ";
            for (int j = 1; j <= n; j++) {
                if (visited[j]) {
                    cout << j << ":" << dist[j] << " ";
                }
            }
            cout << endl << endl;
        }
    }
    
    return 0;
}

BFS应用示例

应用一:迷宫最短路径

问题描述:给定一个m×n的迷宫,1表示墙壁,0表示通路。从左上角(0,0)到右下角(m-1,n-1)的最短路径长度,每次只能上下左右移动。

// BFS应用:迷宫最短路径
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

// 方向数组:上、右、下、左
const int dx[] = {-1, 0, 1, 0};
const int dy[] = {0, 1, 0, -1};

// 求解迷宫最短路径
int shortestPath(vector<vector= 0 && nx < m && ny >= 0 && ny < n && 
                maze[nx][ny] == 0 && dist[nx][ny] == -1) {
                dist[nx][ny] = dist[x][y] + 1;
                q.push({nx, ny});
            }
        }
    }
    
    // 无法到达终点
    return -1;
}

int main() {
    // 示例迷宫
    vector<vector

应用二:单词接龙

问题描述:给定两个单词beginWord和endWord,以及一个单词列表wordList,找出从beginWord到endWord的最短转换序列的长度。转换规则:每次只能改变一个字母,转换过程中的中间单词必须是wordList中的单词。

// BFS应用:单词接龙
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_set>
#include <string>
using namespace std;

// 判断两个单词是否只相差一个字符
bool isOneLetterDiff(const string& a, const string& b) {
    if (a.size() != b.size()) return false;
    int diff = 0;
    for (int i = 0; i < a.size(); i++) {
        if (a[i] != b[i]) {
            diff++;
            if (diff > 1) return false;
        }
    }
    return diff == 1;
}

// 计算单词接龙的最短长度
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
    // 将单词列表存入集合,方便查找
    unordered_set<string> wordSet(wordList.begin(), wordList.end());
    
    // 如果endWord不在单词列表中,直接返回0
    if (wordSet.find(endWord) == wordSet.end()) return 0;
    
    // BFS队列,存储当前单词和当前长度
    queue<pair<string, int>> q;
    // 已访问集合,避免重复访问
    unordered_set<string> visited;
    
    // 初始化队列
    q.push({beginWord, 1});
    visited.insert(beginWord);
    
    while (!q.empty()) {
        auto [currentWord, length] = q.front();
        q.pop();
        
        // 如果到达目标单词,返回长度
        if (currentWord == endWord) {
            return length;
        }
        
        // 尝试所有可能的单词转换
        for (const string& word : wordSet) {
            // 如果未访问且只相差一个字符
            if (visited.find(word) == visited.end() && 
                isOneLetterDiff(currentWord, word)) {
                visited.insert(word);
                q.push({word, length + 1});
            }
        }
    }
    
    // 无法转换到endWord
    return 0;
}

int main() {
    string beginWord = "hit";
    string endWord = "cog";
    vector<string> wordList = {"hot","dot","dog","lot","log","cog"};
    
    cout << "最短转换序列长度: " << ladderLength(beginWord, endWord, wordList) << endl;
    // 输出: 5 (hit → hot → dot → dog → cog)
    
    return 0;
}

回溯法

回溯法是一种通过探索所有可能的候选解来找出所有解的算法。当探索到某一步发现当前候选解不可能是解时,就退回到上一步重新选择,这种走不通就退回再走的技术称为回溯。

适用场景

  • 组合问题:如子集、组合总和
  • 排列问题:如全排列、字母异位词
  • 棋盘问题:如N皇后、数独
  • 选择问题:如0-1背包

算法框架

void backtrack(路径, 选择列表) {
    if (满足结束条件) {
        记录路径;
        return;
    }
    
    for (选择 : 选择列表) {
        做选择;
        backtrack(路径, 选择列表);
        撤销选择; // 回溯
    }
}

优缺点

优点:

  • 可以找出所有解
  • 实现简单直观

缺点:

  • 时间复杂度高
  • 可能存在大量重复计算

示例一:全排列

问题描述:给定一个不含重复数字的数组nums,返回其所有可能的全排列。

// 回溯法:全排列
#include <iostream>
#include <vector>
using namespace std;

// 回溯函数
// nums: 输入数组
// used: 标记元素是否已使用
// path: 当前排列路径
// result: 存储所有结果
void backtrack(vector<int>& nums, vector<bool>& used, 
               vector<int>& path, vector<vector<int>>& result) {
    // 结束条件:路径长度等于数组长度
    if (path.size() == nums.size()) {
        result.push_back(path);
        return;
    }
    
    // 遍历所有选择
    for (int i = 0; i < nums.size(); i++) {
        if (used[i]) continue; // 跳过已使用的元素
        
        // 做选择
        used[i] = true;
        path.push_back(nums[i]);
        
        // 递归
        backtrack(nums, used, path, result);
        
        // 撤销选择(回溯)
        path.pop_back();
        used[i] = false;
    }
}

// 计算全排列
vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> path;
    vector<bool> used(nums.size(), false);
    
    backtrack(nums, used, path, result);
    return result;
}

int main() {
    vector<int> nums = {1, 2, 3};
    vector<vector<int>> permutations = permute(nums);
    
    cout << "全排列结果:" << endl;
    for (auto& p : permutations) {
        for (int num : p) {
            cout << num << " ";
        }
        cout << endl;
    }
    
    return 0;
}

示例二:N皇后问题

问题描述:在n×n的棋盘上放置n个皇后,使得它们不能互相攻击。即:任意两个皇后不能处于同一行、同一列或同一斜线上。

// 回溯法:N皇后问题
#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 检查在(row, col)位置放置皇后是否合法
bool isValid(vector<string>& board, int row, int col) {
    int n = board.size();
    
    // 检查同一列是否有皇后
    for (int i = 0; i < row; i++) {
        if (board[i][col] == 'Q') return false;
    }
    
    // 检查左上到右下的斜线
    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q') return false;
    }
    
    // 检查右上到左下的斜线
    for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
        if (board[i][j] == 'Q') return false;
    }
    
    return true;
}

// 回溯函数
void backtrack(vector<string>& board, int row, vector<vector<string>>& result) {
    int n = board.size();
    // 结束条件:所有行都放置了皇后
    if (row == n) {
        result.push_back(board);
        return;
    }
    
    // 尝试在当前行的每一列放置皇后
    for (int col = 0; col < n; col++) {
        if (!isValid(board, row, col)) continue;
        
        // 做选择
        board[row][col] = 'Q';
        
        // 递归处理下一行
        backtrack(board, row + 1, result);
        
        // 撤销选择(回溯)
        board[row][col] = '.';
    }
}

// 求解N皇后问题
vector<vector<string>> solveNQueens(int n) {
    vector<vector<string>> result;
    // 初始化棋盘,全部为'.'
    vector<string> board(n, string(n, '.'));
    
    backtrack(board, 0, result);
    return result;
}

int main() {
    int n = 4;
    vector<vector<string>> solutions = solveNQueens(n);
    
    cout << "N皇后(" << n << ")的解共有 " << solutions.size() << " 种:" << endl;
    for (int i = 0; i < solutions.size(); i++) {
        cout << "解 " << i + 1 << ":" << endl;
        for (string& row : solutions[i]) {
            cout << row << endl;
        }
        cout << endl;
    }
    
    return 0;
}

剪枝技巧

剪枝是提高搜索与回溯算法效率的关键技术,它通过提前判断某些分支不可能找到解或不可能找到更优解,从而避免不必要的搜索,减少计算量。

常见的剪枝策略

1. 可行性剪枝

在搜索过程中,根据问题的约束条件,判断当前路径是否可能到达目标解,如果不可能则立即回溯。

例如:N皇后问题中,检查当前位置是否会被已放置的皇后攻击,如果会则不再继续搜索该路径。

2. 最优性剪枝

在求解最优化问题时,如果当前路径的代价已经超过了已知的最优解,那么即使继续搜索也不可能找到更优的解,可以提前回溯。

例如:旅行商问题中,如果当前路径的长度已经超过了已经找到的最短路径,则无需继续搜索。

3. 重复性剪枝

避免对同一状态进行多次搜索,通过记录已访问的状态来避免重复计算。

例如:在数独求解中,避免在同一单元格尝试相同的数字。

剪枝效果与实现示例

剪枝的重要性

对于许多问题,不剪枝可能导致时间复杂度极高,无法在有限时间内求解:

  • N皇后问题:不剪枝的复杂度约为O(n^n),剪枝后可大幅降低
  • 子集和问题:不剪枝可能需要枚举所有2^n个子集
  • 数独问题:不剪枝几乎无法求解较难的数独

示例:组合总和(剪枝优化)

// 组合总和(剪枝优化版)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 回溯函数
// candidates: 候选数组
// target: 目标和
// start: 起始索引(用于剪枝,避免重复组合)
// sum: 当前和
// path: 当前路径
// result: 结果集
void backtrack(vector<int>& candidates, int target, int start, 
               int sum, vector<int>& path, vector<vector<int>>& result) {
    // 找到一个有效组合
    if (sum == target) {
        result.push_back(path);
        return;
    }
    
    // 可行性剪枝:如果当前和已经超过目标,直接返回
    if (sum > target) {
        return;
    }
    
    // 遍历所有选择
    for (int i = start; i < candidates.size(); i++) {
        // 重复性剪枝:跳过相同的元素
        if (i > start && candidates[i] == candidates[i-1]) {
            continue;
        }
        
        // 做选择
        path.push_back(candidates[i]);
        sum += candidates[i];
        
        // 递归(注意:这里传入i而不是i+1,允许重复使用同一元素)
        backtrack(candidates, target, i, sum, path, result);
        
        // 撤销选择(回溯)
        sum -= candidates[i];
        path.pop_back();
    }
}

// 求解组合总和
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<vector<int>> result;
    vector<int> path;
    
    // 排序,为剪枝做准备
    sort(candidates.begin(), candidates.end());
    
    backtrack(candidates, target, 0, 0, path, result);
    return result;
}

int main() {
    vector<int> candidates = {2, 3, 6, 7};
    int target = 7;
    
    vector<vector<int>> solutions = combinationSum(candidates, target);
    
    cout << "组合总和为" << target << "的解:" << endl;
    for (auto& solution : solutions) {
        for (int num : solution) {
            cout << num << " ";
        }
        cout << endl;
    }
    
    return 0;
}

高级剪枝:数独求解

数独是一种经典的需要大量剪枝的问题,通过多种剪枝策略可以大幅提高求解效率。

// 数独求解(带剪枝优化)
#include <iostream>
#include <vector>
#include <bitset>
using namespace std;

const int SIZE = 9;
const int SUB_SIZE = 3;

// 检查在(row, col)位置放置数字num是否合法
bool isValid(vector<vector selectBestCell(vector<vector bestCell = {-1, -1};
    
    for (int i = 0; i < SIZE; i++) {
        for (int j = 0; j < SIZE; j++) {
            if (board[i][j] == 0) { // 找到空白单元格
                // 计算该单元格可填的数字数量
                int options = 0;
                for (int num = 1; num <= SIZE; num++) {
                    if (isValid(board, i, j, num)) {
                        options++;
                    }
                }
                
                // 找到约束最多(可选数字最少)的单元格
                if (options < minOptions) {
                    minOptions = options;
                    bestCell = {i, j};
                    
                    // 最优情况:只有一个可选数字
                    if (minOptions == 1) {
                        return bestCell;
                    }
                }
            }
        }
    }
    
    return bestCell;
}

// 回溯求解数独
bool backtrack(vector<vector cell = selectBestCell(board);
    int row = cell.first;
    int col = cell.second;
    
    // 如果没有空白单元格,求解完成
    if (row == -1) {
        return true;
    }
    
    // 尝试填充数字1-9
    for (int num = 1; num <= SIZE; num++) {
        if (isValid(board, row, col, num)) {
            // 做选择
            board[row][col] = num;
            
            // 递归求解
            if (backtrack(board)) {
                return true;
            }
            
            // 撤销选择(回溯)
            board[row][col] = 0;
        }
    }
    
    // 没有可行解
    return false;
}

// 求解数独
bool solveSudoku(vector<vector

经典例题解析

1

子集和问题

问题描述:给定一个非负整数数组和一个目标值,判断数组中是否存在一个子集,使得该子集的元素之和等于目标值。

解题思路:

可以使用回溯法求解,对于每个元素,有两种选择:包含或不包含在子集中。通过递归探索所有可能的组合,检查是否存在和为目标值的子集。

优化策略:

  • 排序数组,便于剪枝
  • 如果当前和已经超过目标值,立即回溯
  • 如果找到一个有效子集,立即返回结果

代码实现

// 子集和问题
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 回溯函数
// nums: 输入数组
// target: 目标和
// start: 起始索引
// currentSum: 当前和
bool backtrack(vector<int>& nums, int target, int start, int currentSum) {
    // 找到一个有效子集
    if (currentSum == target) {
        return true;
    }
    
    // 剪枝:如果当前和已经超过目标,直接返回
    if (currentSum > target) {
        return false;
    }
    
    // 遍历剩余元素
    for (int i = start; i < nums.size(); i++) {
        // 剪枝:跳过相同的元素,避免重复计算
        if (i > start && nums[i] == nums[i-1]) {
            continue;
        }
        
        // 选择当前元素
        currentSum += nums[i];
        
        // 递归探索
        if (backtrack(nums, target, i + 1, currentSum)) {
            return true;
        }
        
        // 撤销选择
        currentSum -= nums[i];
    }
    
    // 没有找到有效子集
    return false;
}

// 判断是否存在和为target的子集
bool hasSubsetSum(vector<int>& nums, int target) {
    // 排序,便于剪枝
    sort(nums.begin(), nums.end());
    return backtrack(nums, target, 0, 0);
}

int main() {
    vector<int> nums = {2, 3, 7, 8, 10};
    int target = 11;
    
    if (hasSubsetSum(nums, target)) {
        cout << "存在和为" << target << "的子集" << endl;
    } else {
        cout << "不存在和为" << target << "的子集" << endl;
    }
    
    return 0;
}
2

迷宫问题(最短路径与所有路径)

问题描述:给定一个m×n的迷宫,1表示墙壁,0表示通路。从左上角(0,0)到右下角(m-1,n-1),每次只能上下左右移动。要求:

  1. 找到最短路径的长度
  2. 找出所有可能的路径

解题思路:

1. 最短路径:使用BFS,因为BFS能保证找到最短路径。

2. 所有路径:使用回溯法(DFS),探索所有可能的路径,并记录有效路径。

代码实现

// 迷宫问题:最短路径与所有路径
#include <iostream>
#include <vector>
#include <queue>
#include <string>
using namespace std;

// 方向数组:上、右、下、左,对应字符'U', 'R', 'D', 'L'
const int dx[] = {-1, 0, 1, 0};
const int dy[] = {0, 1, 0, -1};
const char dir[] = {'U', 'R', 'D', 'L'};

// 1. BFS求解最短路径
int shortestPathLength(vector<vector= 0 && nx < m && ny >= 0 && ny < n && 
                maze[nx][ny] == 0 && dist[nx][ny] == -1) {
                dist[nx][ny] = dist[x][y] + 1;
                q.push({nx, ny});
            }
        }
    }
    
    return -1; // 无法到达
}

// 2. 回溯法求解所有路径
void findAllPaths(vector<vector= 0 && nx < m && ny >= 0 && ny < n && 
            maze[nx][ny] == 0 && !visited[nx][ny]) {
            
            // 做选择
            path.push_back(dir[i]);
            
            // 递归探索
            findAllPaths(maze, nx, ny, visited, path, result);
            
            // 撤销选择
            path.pop_back();
        }
    }
    
    // 取消标记
    visited[x][y] = false;
}

vector<string> getAllPaths(vector<vector
3

骑士巡游问题

问题描述:在一个n×n的棋盘上,有一个国际象棋的骑士(马),按照"马走日"的规则移动。要求找出一条路径,使骑士能够访问棋盘上的每个格子恰好一次。

解题思路:

使用回溯法求解,每次尝试骑士的8种可能移动方式。为了提高效率,采用Warnsdorff规则进行剪枝:每次选择下一步可移动位置最少的格子,这样可以尽早发现死胡同,减少无效搜索。

代码实现

// 骑士巡游问题
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int SIZE = 8; // 8x8棋盘
// 马的8种可能移动
const int dx[] = {2, 1, -1, -2, -2, -1, 1, 2};
const int dy[] = {1, 2, 2, 1, -1, -2, -2, -1};

// 计算从(x,y)出发可以移动的位置数量
int countAvailableMoves(vector<vector= 0 && nx < SIZE && ny >= 0 && ny < SIZE && board[nx][ny] == 0) {
            count++;
        }
    }
    return count;
}

// 回溯函数求解骑士巡游
bool backtrack(vector<vector= 0 && nx < SIZE && ny >= 0 && ny < SIZE && board[nx][ny] == 0) {
            moves.push_back({nx, ny});
        }
    }
    
    // 应用Warnsdorff规则排序:按可移动位置数量从小到大排序
    sort(moves.begin(), moves.end(), [&](const pair<int, int>& a, const pair<int, int>& b) {
        return countAvailableMoves(board, a.first, a.second) < 
               countAvailableMoves(board, b.first, b.second);
    });
    
    // 尝试每一种可能的移动
    for (auto& move : moves) {
        int nx = move.first;
        int ny = move.second;
        
        // 做选择
        board[nx][ny] = step;
        
        // 递归探索
        if (backtrack(board, nx, ny, step + 1)) {
            return true;
        }
        
        // 撤销选择
        board[nx][ny] = 0;
    }
    
    // 没有可行的移动
    return false;
}

// 求解骑士巡游问题
vector<vector

搜索与回溯算法练习题

1

组合总和 II

问题描述:给定一个候选人编号的集合candidates和一个目标数target,找出candidates中所有可以使数字和为target的组合。candidates中的每个数字在每个组合中只能使用一次,且解集不能包含重复的组合。

提示:

  • 使用回溯法,注意去重处理
  • 可以先对数组排序,便于剪枝和去重
  • 如果当前和已经超过目标值,进行剪枝
2

单词搜索

问题描述:给定一个m x n 二维字符网格board和一个字符串单词word。如果word存在于网格中,返回true;否则,返回false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

提示:

  • 对每个单元格进行DFS搜索
  • 使用visited数组记录已访问的单元格
  • 当当前字符不匹配时,及时剪枝
3

解数独

问题描述:编写一个程序,通过填充空格来解决数独问题。数独的解法需遵循如下规则:1. 数字1-9在每一行只能出现一次;2. 数字1-9在每一列只能出现一次;3. 数字1-9在每一个以粗实线分隔的3x3宫内只能出现一次。

提示:

  • 使用回溯法,逐个填充空白单元格
  • 使用剪枝策略,优先填充约束条件最多的单元格
  • 可以使用位运算优化判断过程
4

太平洋大西洋水流问题

问题描述:给定一个m x n的非负整数矩阵来表示一片大陆上各个单元格的高度。"太平洋"处于大陆的左边界和上边界,而"大西洋"处于大陆的右边界和下边界。水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。请找出那些水流既可以流动到太平洋,又能流动到大西洋的陆地单元的坐标。

提示:

  • 反向思维:从海洋边界开始BFS/DFS
  • 分别标记可以到达太平洋和大西洋的单元格
  • 结果为两种标记的交集
5

火柴棍摆数字

问题描述:用火柴棍可以摆出0-9这10个数字,已知每个数字需要的火柴棍数量:0需要6根,1需要2根,2需要5根,3需要5根,4需要4根,5需要5根,6需要6根,7需要3根,8需要7根,9需要6根。现在给定n根火柴棍,能摆出多少个不同的等式?等式格式为"A+B=C",其中A、B、C均为非负整数,用火柴棍摆成。

提示:

  • 先计算每个数字需要的火柴棍数量
  • 遍历可能的A和B,计算C=A+B,判断总火柴数是否等于n
  • 注意等式中的"+"和"="需要额外的火柴棍(共4根)