C++ 进阶知识点详细教程 - 第3部分
11. 搜索算法
11.1 深度优先搜索(DFS)
11.1.1 基本概念
DFS是一种递归的搜索算法,沿着一条路径深入到底,然后回溯。
基本模板
void dfs(int state) {// 1. 终止条件if (满足条件) {处理结果;return;}// 2. 剪枝if (不满足要求) {return;}// 3. 尝试所有可能for (每个可能的选择) {做选择;dfs(下一个状态);撤销选择; // 回溯}
}
11.1.2 全排列问题
题目描述:给定一个正整数n,输出1到n的所有排列。
输入:一个正整数n (1 ≤ n ≤ 8)
输出:所有可能的排列,每行一个排列
示例:
输入:3
输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
#include <iostream>
using namespace std;int n;
int arr[10];
bool used[10];void dfs(int depth) {// 终止条件:已经选择了n个数字if (depth == n) {for (int i = 0; i < n; i++) {cout << arr[i] << " ";}cout << endl;return;}// 尝试每个数字for (int i = 1; i <= n; i++) {if (!used[i]) {arr[depth] = i; // 做选择used[i] = true; // 标记已使用dfs(depth + 1); // 递归到下一层used[i] = false; // 撤销选择(回溯)}}
}int main() {cout << "请输入n: ";cin >> n;cout << "所有排列:" << endl;dfs(0);return 0;
}
11.1.3 组合问题
题目描述:从1到n这n个数中选择k个数的所有组合。
输入:两个正整数n和k (1 ≤ k ≤ n ≤ 20)
输出:所有可能的组合,每行一个组合
示例:
输入:4 2
输出:
1 2
1 3
1 4
2 3
2 4
3 4
#include <iostream>
using namespace std;int n, k;
int path[25];void dfs(int start, int depth) {// 终止条件:已经选择了k个数if (depth == k) {for (int i = 0; i < k; i++) {cout << path[i] << " ";}cout << endl;return;}// 从start开始选择,保证组合的有序性for (int i = start; i <= n; i++) {path[depth] = i; // 选择当前数字dfs(i + 1, depth + 1); // 下一个从i+1开始,避免重复}
}int main() {cout << "请输入n和k: ";cin >> n >> k;cout << "所有组合:" << endl;dfs(1, 0);return 0;
}
11.1.4 迷宫问题
题目描述:给定一个n×m的迷宫,'.'表示可以通过,'#'表示墙壁。判断是否能从左上角(0,0)走到右下角(n-1,m-1)。
输入:
- 第一行:两个整数n和m (1 ≤ n,m ≤ 100)
- 接下来n行:每行m个字符,'.'或'#'
输出:如果能到达输出"找到路径",否则输出"无路径"
示例:
输入:
4 4
....
.##.
....
###.输出:找到路径
#include <iostream>
using namespace std;int n, m;
char maze[105][105];
bool visited[105][105];
int dx[] = {-1, 1, 0, 0}; // 上下左右
int dy[] = {0, 0, -1, 1};bool dfs(int x, int y) {// 到达终点if (x == n - 1 && y == m - 1) {return true;}visited[x][y] = true; // 标记当前位置已访问// 尝试四个方向for (int i = 0; i < 4; i++) {int nx = x + dx[i];int ny = y + dy[i];// 检查边界、障碍和是否已访问if (nx >= 0 && nx < n && ny >= 0 && ny < m && maze[nx][ny] != '#' && !visited[nx][ny]) {if (dfs(nx, ny)) {return true;}}}return false;
}int main() {cout << "请输入迷宫大小n m: ";cin >> n >> m;cout << "请输入迷宫('.表示通路,#表示墙):" << endl;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> maze[i][j];}}if (dfs(0, 0)) {cout << "找到路径" << endl;} else {cout << "无路径" << endl;}return 0;
}
11.1.5 N皇后问题
题目描述:在n×n的棋盘上放置n个皇后,使得任意两个皇后都不能相互攻击(即不在同一行、同一列或同一对角线上)。
输入:一个正整数n (1 ≤ n ≤ 10)
输出:所有可能的解决方案和总数
示例:
输入:4
输出:
.Q..
...Q
Q...
..Q...Q.
Q...
...Q
.Q..总共 2 种解法
#include <iostream>
using namespace std;int n;
int board[20]; // board[i]表示第i行皇后在第几列
int cnt = 0;bool isValid(int row, int col) {for (int i = 0; i < row; i++) {// 检查同列或同对角线if (board[i] == col || abs(board[i] - col) == abs(i - row)) {return false;}}return true;
}void dfs(int row) {if (row == n) {cnt++;// 输出当前解cout << "解法 " << cnt << ":" << endl;for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {cout << (board[i] == j ? "Q" : ".");}cout << endl;}cout << endl;return;}// 尝试在当前行的每一列放置皇后for (int col = 0; col < n; col++) {if (isValid(row, col)) {board[row] = col; // 放置皇后dfs(row + 1); // 递归到下一行}}
}int main() {cout << "请输入棋盘大小n: ";cin >> n;cout << "N皇后问题的所有解:" << endl;dfs(0);cout << "总共 " << cnt << " 种解法" << endl;return 0;
}
11.2 广度优先搜索(BFS)
11.2.1 基本概念
BFS使用队列,一层一层地搜索,适合求最短路径。
基本模板
#include <queue>
void bfs(int start) {queue<int> q;q.push(start);visited[start] = true;while (!q.empty()) {int curr = q.front();q.pop();// 处理当前节点// 扩展相邻节点for (每个相邻节点 next) {if (!visited[next]) {visited[next] = true;q.push(next);}}}
}
11.2.2 迷宫最短路径
题目描述:给定一个n×m的迷宫,求从左上角(0,0)到右下角(n-1,m-1)的最短路径长度。
输入:
- 第一行:两个整数n和m (1 ≤ n,m ≤ 100)
- 接下来n行:每行m个字符,'.'表示可通过,'#'表示墙壁
输出:最短路径长度,如果无法到达输出-1
示例:
输入:
4 4
....
.##.
....
###.输出:最短路径长度: 6
#include <iostream>
#include <queue>
using namespace std;struct Point {int x, y, dist;
};int n, m;
char maze[105][105];
bool visited[105][105];
int dx[] = {-1, 1, 0, 0}; // 上下左右
int dy[] = {0, 0, -1, 1};int bfs() {queue<Point> q;q.push({0, 0, 0}); // 起点坐标和距离visited[0][0] = true;while (!q.empty()) {Point curr = q.front();q.pop();// 到达终点if (curr.x == n - 1 && curr.y == m - 1) {return curr.dist;}// 尝试四个方向for (int i = 0; i < 4; i++) {int nx = curr.x + dx[i];int ny = curr.y + dy[i];// 检查边界、障碍和访问状态if (nx >= 0 && nx < n && ny >= 0 && ny < m && maze[nx][ny] != '#' && !visited[nx][ny]) {visited[nx][ny] = true;q.push({nx, ny, curr.dist + 1});}}}return -1; // 无法到达
}int main() {cout << "请输入迷宫大小n m: ";cin >> n >> m;cout << "请输入迷宫('.表示通路,#表示墙):" << endl;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> maze[i][j];}}int result = bfs();if (result != -1) {cout << "最短路径长度: " << result << endl;} else {cout << "无法到达" << endl;}return 0;
}
11.2.3 图的BFS遍历
题目描述:给定一个无向图,从指定起点开始进行广度优先遍历,输出遍历顺序。
输入:
- 第一行:两个整数n和m,表示n个节点和m条边 (1 ≤ n ≤ 1000, 0 ≤ m ≤ 10000)
- 接下来m行:每行两个整数u和v,表示节点u和v之间有一条边
输出:从节点1开始的BFS遍历序列
示例:
输入:
5 6
1 2
1 3
2 4
2 5
3 4
4 5输出:1 2 3 4 5
#include <iostream>
#include <vector>
#include <queue>
using namespace std;vector<int> graph[1005];
bool visited[1005];void bfs(int start) {queue<int> q;q.push(start);visited[start] = true;cout << "BFS遍历序列: ";while (!q.empty()) {int curr = q.front();q.pop();cout << curr << " ";// 遍历当前节点的所有邻居for (int next : graph[curr]) {if (!visited[next]) {visited[next] = true;q.push(next);}}}cout << endl;
}int main() {int n, m;cout << "请输入节点数n和边数m: ";cin >> n >> m;cout << "请输入" << m << "条边:" << endl;for (int i = 0; i < m; i++) {int u, v;cin >> u >> v;graph[u].push_back(v); // 无向图,两个方向都要添加graph[v].push_back(u);}bfs(1); // 从节点1开始遍历return 0;
}
11.2.4 岛屿数量
题目描述:给定一个由'1'(陆地)和'0'(水)组成的二维网格,计算岛屿的数量。岛屿被水包围,通过水平或垂直连接相邻的陆地形成。
输入:
- 第一行:两个整数n和m (1 ≤ n,m ≤ 100)
- 接下来n行:每行m个字符,'1'表示陆地,'0'表示水
输出:岛屿的数量
示例:
输入:
4 5
11110
11010
11000
00000输出:岛屿数量: 1输入:
4 5
11000
11000
00100
00011输出:岛屿数量: 3
#include <iostream>
#include <queue>
using namespace std;int n, m;
char grid[105][105];
bool visited[105][105];
int dx[] = {-1, 1, 0, 0}; // 上下左右
int dy[] = {0, 0, -1, 1};void bfs(int x, int y) {queue<pair<int, int>> q;q.push({x, y});visited[x][y] = true;while (!q.empty()) {auto [cx, cy] = q.front();q.pop();// 检查四个方向for (int i = 0; i < 4; i++) {int nx = cx + dx[i];int ny = cy + dy[i];// 如果是相邻的陆地且未访问过if (nx >= 0 && nx < n && ny >= 0 && ny < m && grid[nx][ny] == '1' && !visited[nx][ny]) {visited[nx][ny] = true;q.push({nx, ny});}}}
}int countIslands() {int count = 0;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {// 发现新的未访问陆地,开始BFSif (grid[i][j] == '1' && !visited[i][j]) {bfs(i, j); // 标记整个岛屿count++; // 岛屿数量+1}}}return count;
}int main() {cout << "请输入网格大小n m: ";cin >> n >> m;cout << "请输入网格(1表示陆地,0表示水):" << endl;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> grid[i][j];}}cout << "岛屿数量: " << countIslands() << endl;return 0;
}
11.3 DFS vs BFS 对比
| 特性 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈(递归) | 队列 |
| 空间复杂度 | O(h) 深度 | O(w) 宽度 |
| 最短路径 | 不保证 | 保证 |
| 适用场景 | 全排列、组合 | 最短路径 |
| 实现方式 | 递归简单 | 迭代 |
11.4 剪枝优化
11.4.1 可行性剪枝
题目描述:给定一个数组,判断是否能选择其中一些数字使得它们的和等于目标值target。
示例代码:
#include <iostream>
using namespace std;int n, target;
int arr[20];
bool found = false;void dfs(int depth, int sum) {// 剪枝:当前和已经超过目标,没必要继续if (sum > target) return;// 剪枝:如果已经找到解,不需要继续搜索if (found) return;if (depth == n) {if (sum == target) {found = true;cout << "找到目标和 " << target << endl;}return;}// 选择当前数字dfs(depth + 1, sum + arr[depth]);// 不选择当前数字dfs(depth + 1, sum);
}int main() {cout << "请输入数组大小n: ";cin >> n;cout << "请输入数组元素: ";for (int i = 0; i < n; i++) {cin >> arr[i];}cout << "请输入目标和: ";cin >> target;dfs(0, 0);if (!found) {cout << "无法找到目标和 " << target << endl;}return 0;
}
11.4.2 最优性剪枝
题目描述:旅行商问题简化版 - 从起点出发,访问所有城市后回到起点,求最小路径代价。
示例代码:
#include <iostream>
#include <climits>
using namespace std;int n;
int dist[10][10]; // 距离矩阵
bool visited[10];
int best = INT_MAX;
int currentPath[10];
int bestPath[10];void dfs(int depth, int cost, int currentCity) {// 剪枝:当前代价已经超过最优解if (cost >= best) return;if (depth == n) {// 回到起点的代价int totalCost = cost + dist[currentCity][0];if (totalCost < best) {best = totalCost;// 保存最优路径for (int i = 0; i < n; i++) {bestPath[i] = currentPath[i];}}return;}// 尝试访问每个未访问的城市for (int i = 1; i < n; i++) {if (!visited[i]) {visited[i] = true;currentPath[depth] = i;dfs(depth + 1, cost + dist[currentCity][i], i);visited[i] = false; // 回溯}}
}int main() {cout << "请输入城市数量n: ";cin >> n;cout << "请输入距离矩阵:" << endl;for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {cin >> dist[i][j];}}currentPath[0] = 0; // 从城市0开始dfs(1, 0, 0);cout << "最小路径代价: " << best << endl;cout << "最优路径: ";for (int i = 0; i < n; i++) {cout << bestPath[i] << " ";}cout << "0" << endl; // 回到起点return 0;
}
11.4.3 记忆化搜索
题目描述:计算从网格左上角(0,0)到右下角(m,n)的路径数量,只能向右或向下移动。
示例代码:
#include <iostream>
#include <cstring>
using namespace std;int memo[1005][1005];int dfs(int i, int j) {// 已经计算过,直接返回结果if (memo[i][j] != -1) {return memo[i][j];}// 边界条件:到达边界只有一种路径if (i == 0 || j == 0) {return memo[i][j] = 1;}// 递归计算:从上方来 + 从左方来memo[i][j] = dfs(i - 1, j) + dfs(i, j - 1);return memo[i][j];
}int main() {int m, n;cout << "请输入网格大小m n: ";cin >> m >> n;// 初始化记忆化数组memset(memo, -1, sizeof(memo));int result = dfs(m, n);cout << "从(0,0)到(" << m << "," << n << ")的路径数: " << result << endl;return 0;
}
记忆化搜索的优势:
- 避免重复计算
- 时间复杂度从指数级降到多项式级
- 保持递归思路的清晰性
11.5 综合练习题
11.5.1 数独求解
题目描述:给定一个9×9的数独谜题,其中0表示空格,1-9表示已填入的数字。请填入空格使得每行、每列、每个3×3宫格都包含1-9的数字且不重复。
输入:9行,每行9个数字,0表示空格
输出:完整的数独解,如果无解输出"无解"
示例:
输入:
5 3 0 0 7 0 0 0 0
6 0 0 1 9 5 0 0 0
0 9 8 0 0 0 0 6 0
8 0 0 0 6 0 0 0 3
4 0 0 8 0 3 0 0 1
7 0 0 0 2 0 0 0 6
0 6 0 0 0 0 2 8 0
0 0 0 4 1 9 0 0 5
0 0 0 0 8 0 0 7 9输出:
5 3 4 6 7 8 9 1 2
6 7 2 1 9 5 3 4 8
1 9 8 3 4 2 5 6 7
8 5 9 7 6 1 4 2 3
4 2 6 8 5 3 7 9 1
7 1 3 9 2 4 8 5 6
9 6 1 5 3 7 2 8 4
2 8 7 4 1 9 6 3 5
3 4 5 2 8 6 1 7 9
#include <iostream>
using namespace std;int board[9][9];bool isValid(int row, int col, int num) {// 检查行:该行不能有重复数字for (int j = 0; j < 9; j++) {if (board[row][j] == num) return false;}// 检查列:该列不能有重复数字for (int i = 0; i < 9; i++) {if (board[i][col] == num) return false;}// 检查3x3宫格:该宫格不能有重复数字int startRow = (row / 3) * 3;int startCol = (col / 3) * 3;for (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {if (board[startRow + i][startCol + j] == num) {return false;}}}return true;
}bool solveSudoku() {// 寻找第一个空格for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if (board[i][j] == 0) {// 尝试填入1-9for (int num = 1; num <= 9; num++) {if (isValid(i, j, num)) {board[i][j] = num; // 填入数字if (solveSudoku()) { // 递归求解return true;}board[i][j] = 0; // 回溯}}return false; // 1-9都不能填入,无解}}}return true; // 所有格子都填满,找到解
}int main() {cout << "请输入9x9数独谜题(0表示空格):" << endl;for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {cin >> board[i][j];}}cout << "求解中..." << endl;if (solveSudoku()) {cout << "数独解:" << endl;for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {cout << board[i][j] << " ";}cout << endl;}} else {cout << "无解" << endl;}return 0;
}
11.5.2 单词搜索
题目描述:给定一个二维字符网格和一个单词,判断单词是否存在于网格中。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是水平或垂直方向上相邻的。同一个单元格内的字母不允许被重复使用。
输入:
- 第一行:两个整数n和m,表示网格大小 (1 ≤ n,m ≤ 10)
- 接下来n行:每行m个字符,表示网格
- 最后一行:要搜索的单词
输出:如果找到单词输出"找到单词",否则输出"未找到"
示例:
输入:
3 4
ABCE
SFCS
ADEE
ABCCED输出:找到单词输入:
3 4
ABCE
SFCS
ADEE
SEE输出:找到单词输入:
3 4
ABCE
SFCS
ADEE
ABCB输出:未找到
#include <iostream>
#include <string>
using namespace std;char board[10][10];
bool visited[10][10];
int n, m;
string word;
int dx[] = {-1, 1, 0, 0}; // 上下左右
int dy[] = {0, 0, -1, 1};bool dfs(int x, int y, int index) {// 成功匹配整个单词if (index == word.length()) {return true;}// 边界检查、访问检查、字符匹配检查if (x < 0 || x >= n || y < 0 || y >= m || visited[x][y] || board[x][y] != word[index]) {return false;}visited[x][y] = true; // 标记当前位置已访问// 尝试四个方向for (int i = 0; i < 4; i++) {if (dfs(x + dx[i], y + dy[i], index + 1)) {return true;}}visited[x][y] = false; // 回溯,取消标记return false;
}bool exist() {// 从每个位置开始尝试匹配单词for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {if (dfs(i, j, 0)) {return true;}}}return false;
}int main() {cout << "请输入网格大小n m: ";cin >> n >> m;cout << "请输入网格:" << endl;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> board[i][j];}}cout << "请输入要搜索的单词: ";cin >> word;if (exist()) {cout << "找到单词" << endl;} else {cout << "未找到" << endl;}return 0;
}
11.6 搜索算法总结
DFS适用场景:
- 全排列、组合问题
- 连通性问题
- 路径问题(不要求最短)
- 回溯问题
BFS适用场景:
- 最短路径问题
- 层次遍历
- 状态转移最少步数
优化技巧:
- 剪枝:减少无效搜索
- 记忆化:避免重复计算
- 双向搜索:从两端同时搜索
- 启发式搜索:A*算法
总结
本教程涵盖了C++的重要进阶知识点:
- do while循环:至少执行一次的循环
- switch语句:多分支选择结构
- 流程图:算法可视化工具
- string字符串:强大的字符串处理
- 引用:高效的参数传递
- 联合体:节省内存的数据结构
- 文件重定向:输入输出到文件
- 调试技巧:输出调试和GDB调试
- 断言调试:检查程序假设
- 二分答案:优化搜索问题
- 搜索算法:DFS和BFS
掌握这些知识点,你的C++编程能力将大幅提升!
练习建议:
- 每个知识点都要动手实践
- 多做题巩固理解
- 学会调试和优化代码
- 理解算法思想,不要死记硬背