2024-12-24 写的。今天是2025-3-11. 看得出来因为刷题所以自己进步很大了。今天回头看 觉得写的也太幼稚了。直接看灵神的视频然后刷题就好。
做了leetcode 有一阵子了,Bloomberg的面试正好也是考的全排列。对于全排列的感受更深了,所以又写一篇记录感受。全排列相对来说是很简单的一种题型。它的本质是把所有可能的情况列出来。
Don‘t Freak out. 用代码 把所有情况写一遍。
template:
vector<vector<int>> ans;
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums, {});
return ans;
}
void dfs(...) {
if(到达结束条件){
res.push_back(已选列表);
return;
}
for(选择 : 选择列表){
排除无效的选择(剪枝)
做选择;
dfs(路径,选择列表);
处理撤回
撤销处理结果
}
}
就很简单 把所有可能性排列出来就可以了。常常和string一起考 需要牢记substr等一些string处理方法。
同时,注意细节,比如 暂停后,记得return。比如 string 需要记住 不要超过它本身的长度。
其他排列例题: blog.lichangao.com/daily_pract…
这里面所有的关于回溯的题,在2024/12/24 之前已经全部做完了。mid系列只需要记住template 直接套就行。
例题:46. Permutations
class Solution {
vector<vector<int>> ans;
unordered_set<int> dict;
public:
vector<vector<int>> permute(vector<int>& nums) {
dfs(nums, {});
return ans;
}
void dfs(vector<int> nums, vector<int> temp){
if(temp.size() == nums.size()) ans.push_back(temp);
for(int i = 0; i < nums.size(); i++){
if(dict.find(nums[i]) == dict.end()){
dict.insert(nums[i]);
temp.push_back(nums[i]);
dfs(nums, temp);
temp.pop_back();
dict.erase(nums[i]);
}
}
}
};
经验:
-
有没有重复的数字:用
unordered_set<int>
去解决。并且只要在ans那里写unordered_set<int>/<char>
即可,因为所有的排列都是depth first search,先从最下面走,走不动再稍微往上走,不用担心会混在一起。比如上题。比如37. Sudoku Solver (37老是遗忘条件)(有空多做做) -
在写dfs的时候,我们会使用void或者bool。我让chatgpt写了一个什么时候用什么方法的方法论,觉得很有帮助,贴到下面了。在用 DFS(Depth First Search)做搜索或者回溯(backtracking) 时,是否需要返回
bool
很大程度上取决于你想实现的逻辑以及是否需要剪枝。我们通常会看到两种常见写法: -
bool dfs(...)
-
void dfs(...)
这两种写法本质上没有绝对的“好”或“坏”,它们更像是从需求角度出发,选择合适的函数返回值和函数功能。下面我们来探讨为什么在有些场景下需要 bool
来做“剪枝”,而在另外一些场景下只需要 void
。
1. 为什么使用 bool dfs(...)
?
1.1 用于剪枝或提早结束搜索
-
场景: 比如搜索一个迷宫,想知道是否能走到终点。一旦找到一条可行路径,就没必要继续搜索其他路径了。
-
实现思路: DFS 函数在搜索到可行解时返回
true
,告诉上层调用者“已经找到解了”。上层调用者检查返回值,如果是true
,就可以停止其它分支的搜索(即“剪枝”)。 -
代码示例 (示意):
bool dfs(int x, int y) { if (到达目标) { return true; // 找到可行解,返回 true } // 尝试扩展 for (每个可能的方向) { if (该方向可行) { if (dfs(新x, 新y)) { return true; // 如果子问题返回 true,直接传递 true } } } return false; // 所有方向都走不通,就返回 false }
这种写法使得一旦找到可行解,就能快速停止剩余搜索,达到剪枝的效果。
1.2 用于判断当前搜索分支是否成功
- 场景: 例如做回溯寻找一个满足某种条件的组合或排列,一旦这个组合找到,就不必再继续下去了。
- 思路: 你可以利用
dfs
的返回值标记某个分支有没有成功。一旦成功,外层就停止再向下深搜。 - 好处: 通过返回
bool
,不用再额外定义全局变量或类成员变量来记录是否已经找到解,显得逻辑更加“内聚”。
2. 为什么使用 void dfs(...)
?
2.1 不需要剪枝或不需要提前结束
-
场景: 你可能需要遍历搜索树上的所有结果。例如要输出所有满足条件的解、不需要只找到任意一个就够了。
-
实现思路: 既然要全部遍历,就不需要一旦找到满足条件就立刻返回。因此没必要通过
bool
中途返回。 -
代码示例 (示意):
void dfs(int depth, vector<int>& path) { if (depth == n) { // 输出/记录当前 path return; } // 枚举所有可能选项 for (auto choice : someChoices) { path.push_back(choice); dfs(depth + 1, path); path.pop_back(); } }
这里搜索的目的是列举所有可行解,也就不再需要通过返回值剪枝或中断搜索。
2.2 逻辑简单,只做遍历标记
-
场景: 如果只是想对一幅图/树做访问标记,或者统计某些全局信息(如连通分量、访问次数等),根本不涉及到“找到就停”这样的逻辑。
-
实现思路: 这种场景下,用
void
来实现 DFS 更加自然,函数体内只做标记、访问、累加等操作,不需要返回任何结果。 -
示例:
vector<bool> visited; void dfs(int u) { visited[u] = true; for (auto v : adj[u]) { if (!visited[v]) { dfs(v); } } }
这种写法只做简单的遍历标记,没有任何要“提早结束搜索”的需求。
3. 如何选择?
- 如果你的搜索需求是“一旦找到结果就可以停止” ,或者“要根据子函数的返回值来确定是否剪枝”——就考虑返回
bool
,用返回值做早停控制。 - **如果你需要“遍历所有可能”**或者“对搜索过程中所有信息都要处理”——可以使用
void
,因为你的搜索不会在中途结束,更不会需要用返回值传递状态。
3.1 可能的“混合”情况
有时,我们既想得到某些搜索分支的结果,又想把部分信息带回给上层,这时可能还会用其他方式(如全局变量、传引用、返回复杂结构体等等)来辅助。选择哪种方案主要看哪种方式可以让你的代码结构更简洁、可读性更好。
总结
bool dfs(...)
主要服务于“找到就返回”的场景,可以快速剪枝、停止搜索。void dfs(...)
则多用于需要全部遍历、或者需要用 DFS 做标记、统计等操作而不需要提前结束的情况。
因此,“如果 DFS 情况复杂且存在需要剪枝的逻辑,就用 bool
来返回;如果 DFS 的逻辑只是普通遍历或是无须剪枝,就用 void
”——这个理解是非常常见、也很符合实际情况的。
记住:具体怎么写,还是要看最终的需求,把需求和代码的实现逻辑对应起来,才能做出合适的选择。
对于leetcode hard的俩道题: 37 和Word Break II 因为我们可以提早结束,37其实只有一种解,所以我们用bool,word breakII 我们需要所有情况而非一种情况,所以我们用void dfs
- 去重
有时候重复情况:sort(nums.begin(), nums.end()) + if(i != idx && candidates[i] == candidates[i-1]) continue;就可以去重。感觉不错。比如下面这道经典题:Combination Sum II
class Solution {
vector<vector<int>> ans;
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
dfs(candidates, target, {}, 0);
return ans;
}
void dfs(vector<int>& candidates, int target, vector<int> temp, int idx){
if(target == 0){
ans.push_back(temp);
return;
}
if(target < 0){
return;
}
for(int i = idx; i < candidates.size(); i++){
if(i != idx && candidates[i] == candidates[i-1]) continue;
temp.push_back(candidates[i]);
dfs(candidates, target - candidates[i], temp, i+1);
temp.pop_back();
}
}
};
但对于491. Non-decreasing Subsequences 去重是不能用sort(nums.begin(), nums.end()) + if(i != idx && candidates[i] == candidates[i-1]) continue;而是需要每层增加一个 unordered_set<int> nums
每层单独去重。所以每道题都很有意思,是很有趣的智力测试。最基础的去重还是 unordered_set 进化为sorted。很有趣。
这道题的解自己可以点进去看。
-
经常犯的错误
for(int i = idx; i < candidates.size(); i++){ if(i != idx && candidates[i] == candidates[i-1]) continue; temp.push_back(candidates[i]); dfs(candidates, target - candidates[i], temp, i+1); temp.pop_back(); }
里面的i和idx常常乱写。该写i的地方写成idx