LeetCode 搜索技术<一>

119 阅读6分钟

这篇整体思想来自于获得ACM金牌朋友送的书 算法竞赛: 入门到进阶

  • 递归与排列
  • 子集的生成和组合问题
  • BFS和队列
  • A * 算法
  • DFS和递归
  • 八皇后问题
  • 回溯

搜素技术是“暴力法”算法思想的具体实现。虽然暴力法是低效的代名词,但它仍然很有用,原因如下:

  1. 很多问题只能用暴力法解决。
  2. 对于LeetCode Medium,暴力法完全够用。
  3. 把暴力法当成参照。既然暴力法是最差的,那么可以把它当成一个比较来衡量另外的算法有多“好”。 拿到题目后,如果没有别的思路,可以先试试暴力法,看能否找到灵感。

虽然暴力搜素的思路很简单,但操作起来并不容易。一般有以下操作:

  1. 找到所有的数据,并且用数据结构表示和存储
  2. 剪枝,尽可能多的排除不符合条件的数据,减少搜索空间
  3. 用某个算法快速检索这些数据

削减数据的必要性: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 递归和排列

22. Generate Parentheses

思路有点特别的题目。

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 + ")");
    }
};

39. Combination Sum

40. Combination Sum II

77. Combinations 基础题,思路一致,注意思路的整理和细节。

第二遍做的稀巴烂,回溯的格式写出来了,没有明确回溯停止的条件(剪枝)的条件,i = idx; i <= n; 这里也没写对。思路清晰才能写对。

216.Combination Sum III

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的例题。

2.jpg

3.jpg

在这里,我们最多只能swap俩个数字。这时候,我们用tostring 将int 转化为string。用i,j 穷举所有可能性。非常有意思的一道题。

4.jpg

4.2 子集生成和组合问题

在书里,他写的是用bit manipulation做的题。但是其实可以直接用递归这一套模板去做这道题。用递归这一套模板去做,反而会减少思考的难度。

78 Subsets

这道题很有意思,可以用俩个方法去解。

基础的回溯法。自己写出来的。

subsets也有用(1 << n) 二进制数的对应方法去做。递归是最基础的方法,也是其他所有排列的基础。比如subset II 用递归法做出来简单非常非常多。所以基础的方法一定要掌握好。

90 Subsets 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)

image.png

当停止条件是 组合的大小 == n: if(vec.size() == k ) { ans.push_back(vec); return; }

还有其他的停止条件,比如()()()()) 题: 停止条件是很有趣的left ( == right ) == n

很容易犯的错误, for loop 中的dfsrecall的时候 应该用i+1 而不是idx+1.这个错误犯俩三次了。可以用start命名,而不是idx 这样会更清晰一点。

我个人觉得画图是非常方便思考停止条件,写递归的一种方法。非常推荐。但图要画全。画不全容易理解偏。