1 题目
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合。
示例 1:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
示例 2:
输入:n = 1
输出:["()"]
提示:
1 <= n <= 8
2 代码实现
c++
cpp
class Solution {
private:
void backtrack(int left ,int right , string ¤t , vector<string> &result ){
if (left == 0 && right == 0){
result.push_back(current);
return ;
}
if (left > 0 ){
current.push_back('(');
backtrack(left - 1 , right , current , result);
current.pop_back();
}
if (right > left ){
current.push_back(')');
backtrack(left , right - 1 ,current,result);
current.pop_back();
}
}
public:
vector<string> generateParenthesis(int n) {
vector<string> result ;
string current ;
backtrack(n,n,current,result);
return result;
}
};
思考
。。。这是什么,为什么一点思路都没有啊,用回溯,这样怎么配对好有效的括号呢?还要用到栈配对吗,怎么做啊一点都不会,选择 - 撤销,然后呢?怎么做啊?
看了题解,有关配对的在左右括号加入的时候就做好逻辑,有左括号了,才能顺着加一个右括号(也就是当前兜里右括号比左括号多)
题解
别着急!括号生成这个问题确实是回溯算法的经典应用,刚开始接触会觉得无从下手很正常。我会一步步拆解思路,再给出完整的 C++ 代码,保证你能看懂。
核心思路分析
要生成有效的括号组合,关键要遵守两个规则:
- 左括号规则 :生成过程中,左括号的数量不能超过给定的对数
n - 右括号规则 :任何时刻,右括号的数量不能超过左括号的数量(否则就会出现
())这种无效组合)
回溯的核心就是:做选择 → 递归 → 撤销选择
- 选择:加左括号 或 加右括号(必须满足上面的规则)
- 递归:继续构建括号字符串
- 撤销:删掉刚加的括号,尝试另一种选择
不需要用栈来验证有效性,我们可以在生成过程中就保证有效性,这是更高效的做法。
完整 C++ 代码实现
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> result; // 存储最终结果
string current; // 存储当前正在构建的括号字符串
// 回溯函数:参数分别是 剩余左括号数、剩余右括号数、当前字符串、结果集
backtrack(n, n, current, result);
return result;
}
private:
void backtrack(int left, int right, string& current, vector<string>& result) {
// 递归终止条件:左右括号都用完了,说明生成了一个有效组合
if (left == 0 && right == 0) {
result.push_back(current);
return;
}
// 选择1:添加左括号(前提:还有剩余的左括号)
if (left > 0) {
current.push_back('('); // 做选择:加左括号
backtrack(left - 1, right, current, result); // 递归
current.pop_back(); // 撤销选择:删掉最后加的左括号
}
// 选择2:添加右括号(前提:剩余右括号数 > 剩余左括号数,保证有效性)
if (right > left) {
current.push_back(')'); // 做选择:加右括号
backtrack(left, right - 1, current, result); // 递归
current.pop_back(); // 撤销选择:删掉最后加的右括号
}
}
};
// 测试代码
int main() {
Solution s;
int n = 3;
vector<string> res = s.generateParenthesis(n);
cout << "n = " << n << " 时的有效括号组合:" << endl;
for (const string& str : res) {
cout << str << " ";
}
cout << endl;
// 测试n=1的情况
n = 1;
res = s.generateParenthesis(n);
cout << "n = " << n << " 时的有效括号组合:" << endl;
for (const string& str : res) {
cout << str << " ";
}
cout << endl;
return 0;
}
代码关键部分解释
-
递归函数参数:
left:剩余可用的左括号数量right:剩余可用的右括号数量current:当前正在构建的括号字符串(引用传递,避免拷贝)result:存储所有有效组合的结果集(引用传递)
-
递归终止条件 :当
left和right都为 0 时,说明已经生成了一个完整且有效的括号组合,加入结果集。 -
选择左括号 :只有当
left > 0时才能加左括号(左括号没用完),加完后递归(left-1),递归返回后撤销选择(pop_back)。 -
选择右括号 :只有当
right > left时才能加右括号(保证右括号不超过左括号数量),加完后递归(right-1),递归返回后撤销选择。
执行流程示例(n=2)
bash
初始状态:left=2, right=2, current=""
1. 加左括号 → current="(", left=1, right=2
a. 加左括号 → current="((", left=0, right=2
- 加右括号 → current="(()", left=0, right=1
- 加右括号 → current="(())" → 加入结果集
- 撤销右括号 → current="(("
b. 撤销左括号 → current="("
c. 加右括号 → current="()", left=1, right=1
- 加左括号 → current="()(", left=0, right=1
- 加右括号 → current="()()" → 加入结果集
- 撤销左括号 → current="()"
2. 撤销左括号 → current=""
最终结果:["(())", "()()"]
总结
- 核心规则:生成有效括号的关键是「左括号不超 n,右括号不超左括号」,在生成过程中直接保证有效性,无需事后验证。
- 回溯逻辑:先尝试加左括号(满足条件时),递归到底后回溯;再尝试加右括号(满足条件时),递归到底后回溯。
- 效率优势:这种做法不会生成无效组合,比「生成所有组合再验证」的方式效率高得多(n=8 时尤为明显)。
3 题目
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCCED"
输出:true
示例 2:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "SEE"
输出:true
示例 3:

输入:board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']], word = "ABCB"
输出:false
提示:
m == board.lengthn = board[i].length1 <= m, n <= 61 <= word.length <= 15board和word仅由大小写英文字母组成
进阶: 你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?
4 代码实现
思考
蛤????????什么鬼啊,这个不是图吗。这样要怎么做啊??!!!
题解
79. 单词搜索 超详细题解(新手友好版)
这道题是二维网格 + 回溯(DFS) 的经典题,刚开始学确实会觉得难,但只要把 "走迷宫" 的逻辑拆解开,就会发现它和你之前学的括号生成本质是一样的 ------ 都是「做选择→递归探索→撤销选择」。我会从题意拆解→核心思路→代码逐行解释→执行流程模拟 一步步讲,保证你能看懂。
一、题意拆解(先把问题说人话)
题目要求
给定一个 m×n 的字符网格 board 和一个字符串 word,判断:
- 能否从网格中任意位置出发;
- 沿着上下左右相邻的单元格走;
- 每个单元格只能用一次;
- 按顺序拼出整个
word。能拼出来返回true,否则返回false。
示例直观理解
比如示例 3 中 board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]],word = "ABCB":
- 路径
A(0,0)→B(0,1)→C(0,2)后,下一步要找B; - 周围只有
B(0,1)是目标,但这个位置已经用过了,所以拼不出来,返回false。
二、核心思路(用 "走迷宫" 类比)
想象你在网格里 "走迷宫" 找单词,整个过程分两步:
第一步:找起点
遍历网格的每一个位置 (i,j),如果 board[i][j] 等于 word 的第一个字母,就把这个位置当作 "迷宫起点"。
第二步:从起点开始 DFS(深度优先搜索)走迷宫
DFS 的核心是「试错 + 回退」,规则如下:
- 当前步:检查当前位置是否合法(没走过 + 字母匹配);
- 做选择:标记当前位置 "已走过"(避免重复踩);
- 探下一步:往上下左右四个方向走,递归检查下一个字母;
- 回退(回溯):如果四个方向都走不通,取消 "已走过" 标记,回到上一步试其他方向;
- 终止条件:如果走到了单词的最后一个字母,说明找到路径了。
三、代码逐行解释(新手级注释)
先贴完整代码(带极致详细注释),再逐模块拆解:
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Solution {
public:
// 定义四个移动方向:上、下、左、右(用二维数组简化方向遍历)
// dirs[0] = {-1,0} → 行-1,列不变(向上走)
// dirs[1] = {1,0} → 行+1,列不变(向下走)
// dirs[2] = {0,-1} → 列-1,行不变(向左走)
// dirs[3] = {0,1} → 列+1,行不变(向右走)
vector<vector<int>> dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// 主函数:对外暴露的接口,判断单词是否存在
bool exist(vector<vector<char>>& board, string word) {
int m = board.size(); // 网格的行数
int n = board[0].size(); // 网格的列数
// 剪枝优化1:如果单词长度超过网格总字符数,直接返回false(不可能匹配)
if (word.size() > m * n) return false;
// 第一步:遍历网格所有位置,找可能的起点
for (int i = 0; i < m; ++i) { // 遍历每一行
for (int j = 0; j < n; ++j) { // 遍历每一列
// 找到和单词第一个字母匹配的位置,作为起点开始DFS
if (board[i][j] == word[0]) {
// 创建visited数组:标记位置是否被访问过,初始全为false
vector<vector<bool>> visited(m, vector<bool>(n, false));
// 调用DFS函数,只要有一个起点能找到路径,就返回true
if (dfs(board, word, visited, i, j, 0)) {
return true;
}
}
}
}
// 所有起点都试完了,没找到路径
return false;
}
private:
// DFS递归函数:核心探索逻辑
// 参数说明:
// board → 字符网格(引用传递,避免拷贝)
// word → 要找的单词(引用传递)
// visited → 访问标记数组(引用传递)
// x,y → 当前探索的网格位置(行x,列y)
// index → 当前要匹配的单词下标(比如index=0是第一个字母,index=word.size()-1是最后一个)
bool dfs(vector<vector<char>>& board, string& word, vector<vector<bool>>& visited,
int x, int y, int index) {
// 第一步:合法性检查(优先级最高!)
// 1. visited[x][y] == true → 这个位置已经走过了,不能再走
// 2. board[x][y] != word[index] → 当前位置字母和目标字母不匹配
// 满足任意一个,直接返回false(这条路走不通)
if (visited[x][y] || board[x][y] != word[index]) {
return false;
}
// 第二步:终止条件(找到完整路径)
// 如果当前匹配的是单词最后一个字母(index到末尾了),且上面的合法性检查通过
// 说明已经拼出整个单词,返回true
if (index == word.size() - 1) {
return true;
}
// 第三步:做选择(标记当前位置为已访问)
// 避免后续递归中重复访问这个位置
visited[x][y] = true;
// 第四步:遍历四个方向,探索下一个字母
for (auto& dir : dirs) { // 依次尝试上、下、左、右四个方向
int nx = x + dir[0]; // 新位置的行坐标(当前行+方向行偏移)
int ny = y + dir[1]; // 新位置的列坐标(当前列+方向列偏移)
// 检查新坐标是否在网格范围内(不能走出网格)
// nx >= 0 → 行不越上界;nx < board.size() → 行不越下界
// ny >= 0 → 列不越左界;ny < board[0].size() → 列不越右界
if (nx >= 0 && nx < board.size() && ny >= 0 && ny < board[0].size()) {
// 递归探索下一个字母(index+1),只要有一个方向找到路径,就返回true
if (dfs(board, word, visited, nx, ny, index + 1)) {
return true;
}
}
}
// 第五步:回溯(撤销选择)
// 四个方向都试完了,都走不通 → 取消当前位置的访问标记
// 让这个位置可以被其他路径使用(比如上一步换个方向走)
visited[x][y] = false;
// 所有方向都探索过,没找到路径,返回false
return false;
}
};
// 测试代码:可以直接运行,验证示例
int main() {
Solution s;
// 示例网格
vector<vector<char>> board = {
{'A','B','C','E'},
{'S','F','C','S'},
{'A','D','E','E'}
};
// 测试用例1:ABCCED → 预期true
string word1 = "ABCCED";
cout << "ABCCED: " << (s.exist(board, word1) ? "true" : "false") << endl;
// 测试用例2:SEE → 预期true
string word2 = "SEE";
cout << "SEE: " << (s.exist(board, word2) ? "true" : "false") << endl;
// 测试用例3:ABCB → 预期false
string word3 = "ABCB";
cout << "ABCB: " << (s.exist(board, word3) ? "true" : "false") << endl;
return 0;
}
模块 1:方向数组(核心简化技巧)
cpp
vector<vector<int>> dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
- 作用:把 "上下左右" 四个方向转换成数组,避免写 4 次重复的
if判断; - 用法:遍历
dirs,每次取一个方向dir,计算新坐标nx = x+dir[0]、ny = y+dir[1]。
模块 2:主函数exist(找起点)
- 剪枝优化 :先判断单词长度是否超过网格总字符数,超过直接返回
false(比如网格只有 6 个字符,单词有 7 个,不可能匹配); - 遍历网格 :逐行逐列找和
word[0]匹配的位置,作为 DFS 的起点; - 调用 DFS :每个起点调用一次 DFS,只要有一个返回
true,整体就返回true。
模块 3:DFS 函数(核心探索逻辑)
这是整个题的关键,拆成 5 步理解:
步骤 1:合法性检查(最优先)
必须先检查「是否已访问」和「字母是否匹配」,这是避免错误的核心 ------ 比如示例 3 中,走到C(0,2)后找B,会先检查B(0,1)是否已访问(是),直接返回false,不会误判。
步骤 2:终止条件(找到完整路径)
只有当index到单词最后一位,且合法性检查通过时,才说明拼出了整个单词,返回true。
步骤 3:做选择(标记已访问)
把visited[x][y]设为true,防止后续递归重复访问这个位置(比如走回头路)。
步骤 4:遍历方向(探索下一步)
- 遍历四个方向,计算新坐标
nx, ny; - 先检查新坐标是否在网格内(避免越界);
- 递归调用 DFS,匹配下一个字母(
index+1); - 只要有一个方向返回
true,就立刻返回true(提前终止,不浪费时间)。
步骤 5:回溯(撤销选择)
如果四个方向都走不通,把visited[x][y]改回false------ 比如从A(0,0)走到B(0,1),发现走不通,就取消B(0,1)的标记,回到A(0,0)试其他方向(虽然A(0,0)只有右方向,但复杂网格里会有多个方向可选)。
四、执行流程模拟(以示例 3:ABCB 为例)
我们一步步走word = "ABCB"的执行过程,理解为什么返回false:
第一步:找起点
网格中A(0,0)和word[0](A)匹配,作为起点,调用dfs(board, "ABCB", visited, 0, 0, 0)。
第二步:DFS (0,0,0)(匹配 A)
- 合法性检查:
visited[0][0]=false,board[0][0] = A == word[0],通过; - 终止条件:
index=0 != 3(单词长度 4,最后一位是 3),不触发; - 做选择:
visited[0][0] = true; - 遍历四个方向:
- 上:
nx=-1(越界),跳过; - 下:
nx=1, ny=0(S),board[1][0] != B(word [1]),DFS 返回false; - 左:
ny=-1(越界),跳过; - 右:
nx=0, ny=1(B),调用dfs(0,1,1)。
- 上:
第三步:DFS (0,1,1)(匹配 B)
- 合法性检查:
visited[0][1]=false,board[0][1] = B == word[1],通过; - 终止条件:
index=1 !=3,不触发; - 做选择:
visited[0][1] = true; - 遍历四个方向:
- 上:越界,跳过;
- 下:
nx=1, ny=1(F),F != C(word [2]),返回false; - 左:
nx=0, ny=0(A),visited[0][0]=true,返回false; - 右:
nx=0, ny=2(C),调用dfs(0,2,2)。
第四步:DFS (0,2,2)(匹配 C)
- 合法性检查:
visited[0][2]=false,board[0][2] = C == word[2],通过; - 终止条件:
index=2 !=3,不触发; - 做选择:
visited[0][2] = true; - 遍历四个方向:
- 上:越界,跳过;
- 下:
nx=1, ny=2(C),C != B(word [3]),返回false; - 左:
nx=0, ny=1(B),visited[0][1]=true,返回false; - 右:
nx=0, ny=3(E),E != B,返回false;
- 回溯:
visited[0][2] = false,返回false。
第五步:回溯 + 返回
dfs(0,2,2)返回false→ 回到dfs(0,1,1),四个方向都走完,回溯visited[0][1]=false,返回false;dfs(0,1,1)返回false→ 回到dfs(0,0,0),四个方向都走完,回溯visited[0][0]=false,返回false;- 主函数中
A(0,0)这个起点返回false,遍历其他位置(没有其他 A),最终返回false。
五、新手常见问题解答
1. 为什么要回溯(撤销 visited 标记)?
比如网格是[["A","B"],["C","D"]],单词是"ABCD":
- 从
A(0,0)→B(0,1),发现走不通,撤销B的标记; - 回到
A,试下方向C(1,0),再走D(1,1),就能找到路径; - 如果不撤销
B的标记,后续其他路径也无法使用B,会漏掉正确路径。
2. 为什么方向数组要这么定义?
- 行号
x:向上走行号减 1(x-1),向下走行号加 1(x+1); - 列号
y:向左走列号减 1(y-1),向右走列号加 1(y+1); - 数组
{行偏移, 列偏移}刚好对应这个规则,是行业通用写法。
3. 能不能不用 visited 数组?
可以!用原地标记优化(省空间):
- 做选择:把
board[x][y]改成特殊字符(比如'#'); - 回溯:把
board[x][y]改回原来的字符; - 优点:不用额外创建
visited数组,空间复杂度更低; - 注意:必须用引用传递
board,且回溯时恢复原值。
示例修改(DFS 函数内):
cpp
// 替代visited的写法
bool dfs(...) {
// 合法性检查:用board[x][y] == '#'判断是否访问过
if (board[x][y] == '#' || board[x][y] != word[index]) {
return false;
}
if (index == word.size() - 1) {
return true;
}
// 做选择:标记为已访问
char temp = board[x][y]; // 保存原字符
board[x][y] = '#';
// 遍历方向...
// 回溯:恢复原字符
board[x][y] = temp;
return false;
}
六、总结(核心要点)
- 核心逻辑:单词搜索 = 「遍历起点 + 四向 DFS 回溯」,和括号生成的回溯本质一致(选择→递归→撤销);
- 关键顺序:DFS 中必须先做「合法性检查」,再判断「终止条件」,否则会出现重复访问的错误;
- 简化技巧:用方向数组遍历四个方向,避免重复代码;
- 优化思路:剪枝(提前终止、预判长度)+ 原地标记(省空间)。
5 小结
没学会啊没学会,感觉太难了,回头再看看,/(ㄒoㄒ)/。。。。