这篇整体思想来自于获得ACM金牌朋友送的书 算法竞赛: 入门到进阶
- 递归与排列
- 子集的生成和组合问题
- BFS和队列
- A * 算法
- DFS和递归
- 八皇后问题
- 回溯
搜素技术是“暴力法”算法思想的具体实现。虽然暴力法是低效的代名词,但它仍然很有用,原因如下:
- 很多问题只能用暴力法解决。
- 对于LeetCode Medium,暴力法完全够用。
- 把暴力法当成参照。既然暴力法是最差的,那么可以把它当成一个比较来衡量另外的算法有多“好”。 拿到题目后,如果没有别的思路,可以先试试暴力法,看能否找到灵感。
虽然暴力搜素的思路很简单,但操作起来并不容易。一般有以下操作:
- 找到所有的数据,并且用数据结构表示和存储
- 剪枝,尽可能多的排除不符合条件的数据,减少搜索空间
- 用某个算法快速检索这些数据
削减数据的必要性:eg Dijkstra 算法:用贪心法,进行从局部扩散到全局的搜索,不用列举所有可能的路径。
递归和排列(Permutation)
递归是把大问题逐步缩小,直到变成最小的同类问题的过程。递归和分治的思路非常相近,分治是把一个大问题分解为多个类型相似的子问题,事实上,对于一些涉及分治法的问题可以用递归进行编程,典型的有快速排序,归并排序。
对于编程初级者来说,递归是一个难以理解的编程概念,很容易绕晕。为了帮助理解,可以一步步打印出递归函数的输出,看他从大到小解决问题的过程。
n个数的全排列
next_permutation c++
vector<vector<int>> ans;
int data[4] = {5, 3, 4, 1};
sort(data.begin(), data.end());
do{
ans.push_back(data);
}while(next_permutation(data.begin(), data.end());
return ans;
next_permutation(arr.begin(), arr.end()) 输出true/false俩种。Time Complexity: O(n)
LeetCode Related Questions
46. Permutations
给出某数组的全部排列可能。code见上。一样解法:47. Permutations ii
31. Next Permutation
整数数组的下一个排列是指其整数的下一个字典序更大的排列。eg. {1,2,3} -> {1,3,2}. next-permutation: 给你一个整数数组 nums ,找出 nums 的下一个排列。必须 原地 修改,只允许使用额外常数空间。
next_permutation(nums.begin(), nums.end());
60. Permutation Sequence
267. Palindrome Permutation II
next_permutation can be use in vector, string, array etc. and reverse(str.begin(), str.end()) can also be used in vector, string and array as well.
用递归求全排列 recursion->permutation
递归的模板
vector<...> ans;
public:
vedf;{
dfs(....);
return;
}
void dfs(string len, string digits, int num){
if( //停止条件){
加入ans;或者统计数据etc
return;
}
处理数据,
继续dfs
for(int ,,..){
//处理数据
dfs
//回退
}
}
LeetCode Questions
17. Letter Combinations of a Phone Number 这道题是递归的基础。
class Solution {
string dict[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
vector<string> ans;
public:
vector<string> letterCombinations(string digits) {
if(digits == "") return ans;
dfs("", digits, 0);
return ans;
}
void dfs(string len, string digits, int num){
if(num == digits.size()) {
ans.push_back(len);
return;
}
int k = digits[num] - '0';
string temp = dict[k];
for(int i = 0; i < temp.size(); i++){
len = len + temp[i];
dfs(len, digits, num+1);
len.pop_back();
}
}
};
1.1 递归和排列
思路有点特别的题目。
class Solution {
vector<string> ans;
public:
vector<string> generateParenthesis(int n) {
dfs(0,0,n,"");
return ans;
}
void dfs(int lc, int rc, int n, string seq){
if(lc == n && rc == n){
ans.push_back(seq);
return;
}
if(lc < n) dfs(lc + 1, rc, n, seq + "(");
if(rc < n && rc < lc) dfs(lc, rc + 1, n, seq + ")");
}
};
77. Combinations 基础题,思路一致,注意思路的整理和细节。
第二遍做的稀巴烂,回溯的格式写出来了,没有明确回溯停止的条件(剪枝)的条件,i = idx; i <= n; 这里也没写对。思路清晰才能写对。
class Solution {
vector<vector<int>> ans;
public:
vector<vector<int>> combinationSum3(int k, int n) {
dfs(k, n, 0, 1, {});
return ans;
}
void dfs(int k, int n, int sum, int idx, vector<int> temp){
if(temp.size() == k){
if(sum == n) ans.push_back(temp);
return;
}
for(int i = idx; i <= 9; i++){
temp.push_back(i);
dfs(k, n, sum + i, i+1, temp);
temp.pop_back();
}
}
};
Palindrome Partition
class Solution {
vector<vector<string>> ans;
bool isPalindrome(string str){
//check if a str is a palindrome;
int left = 0, right = str.size()-1;
while(left < right){
if(str[left] != str[right]) return false;
left++, right--;
}
return true;
}
public:
vector<vector<string>> partition(string s) {
dfs(s, 0, {}, "");
return ans;
}
void dfs(string s, int idx, vector<string> temp, string str){
if(idx == s.size()) {
ans.push_back(temp);
return;
}
for(int i = idx; i < s.size(); i++){
str += s[i];
if(!isPalindrome(str)) continue;
temp.push_back(str);
dfs(s, i+1, temp, "");
temp.pop_back();
}
}
};
这道题也蛮有意思的。也是排列题。dfs 的题 全排列 递归先画图。画图不画全就容易出现很minor的错误。即使是框架摆对了。
有趣的延伸题 - 半排列
上面讲了全排列,但是半排列我们怎么做呢?
先看一道来自Databricks OA的例题。
在这里,我们最多只能swap俩个数字。这时候,我们用tostring 将int 转化为string。用i,j 穷举所有可能性。非常有意思的一道题。
4.2 子集生成和组合问题
在书里,他写的是用bit manipulation做的题。但是其实可以直接用递归这一套模板去做这道题。用递归这一套模板去做,反而会减少思考的难度。
这道题很有意思,可以用俩个方法去解。
基础的回溯法。自己写出来的。
subsets也有用(1 << n) 二进制数的对应方法去做。递归是最基础的方法,也是其他所有排列的基础。比如subset II 用递归法做出来简单非常非常多。所以基础的方法一定要掌握好。
//subset 题+ 去重
class Solution {
vector<vector<int>> ans;
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> temp = {};
sort(nums.begin(), nums.end());
dfs(nums, 0, temp);
return ans;
}
void dfs(vector<int>& nums, int idx, vector<int>& temp){
ans.push_back(temp);
if(idx == nums.size()) return;
for(int i = idx; i < nums.size(); i++){
//本质就是如果和前一个一样,就跳过 这里错了
if(i != idx && nums[i] == nums[i-1]) continue;
temp.push_back(nums[i]);
dfs(nums, i+1, temp);
temp.pop_back();
}
}
};
这个是真的生气,这个是真的最简单的去重,但是我想了一遍又一遍,不知道自己在做什么。应该直接问chatgpt自己哪里做错了。 很简单的思路:回溯+去重。
递归的dfs写完要写树的dfs和图的dfs
去重
LeetCode 78 Subset ->LeetCode 90 Subset II 加了一个需要去重的操作。 LeetCode 127 Combination Sum II 也是在Combination sum上加了一个去重的操作
不熟练,思考时间比较长。
//先sort 整体array
sort(combination.begin(), combination.end());
dfs(){
if(i != idx && nums[i] == nums[i-1]) continue;
}
画图和思路
当停止条件为if(idx == n)
当停止条件是 组合的大小 == n: if(vec.size() == k ) { ans.push_back(vec); return; }
还有其他的停止条件,比如()()()()) 题: 停止条件是很有趣的left ( == right ) == n
很容易犯的错误, for loop 中的dfsrecall的时候 应该用i+1 而不是idx+1.这个错误犯俩三次了。可以用start命名,而不是idx 这样会更清晰一点。
我个人觉得画图是非常方便思考停止条件,写递归的一种方法。非常推荐。但图要画全。画不全容易理解偏。