22. 括号生成

这个的回溯技法 和 46 一样给了我很大的震撼。

  1. var generateParenthesis = function(n) {
  2. let ans = [];
  3. function dfs(leftP, rightP, strPath) {
  4. if (strPath.length === 2 * n) {
  5. ans.push(strPath);
  6. }
  7. if (leftP > 0) {
  8. dfs(leftP - 1, rightP, `${strPath}(`)
  9. }
  10. if (leftP < rightP) {
  11. dfs(leftP, rightP - 1, `${strPath})`)
  12. }
  13. }
  14. dfs(n, n, '');
  15. return ans;
  16. };

40. 组合总和 II

组合很简单,但是去重逻辑还是使用昨天和 491 以及 18

  1. var combinationSum2 = function(candidates, target) {
  2. let n = candidates.length;
  3. if (n === 0) {
  4. return;
  5. }
  6. candidates.sort((a, b) => a - b);
  7. let ans = [];
  8. function dfs(path, start) {
  9. let sum = path.reduce((pre, cur) => pre + cur, 0);
  10. if (sum === target) {
  11. ans.push(path);
  12. }
  13. if (sum >= target) {
  14. return;
  15. }
  16. let used = new Set();
  17. for (let i = start; i < n; i++) {
  18. if (used.has(candidates[i])) {
  19. continue;
  20. }
  21. used.add(candidates[i]);
  22. dfs([...path, candidates[i]], i + 1);
  23. }
  24. }
  25. dfs([], 0);
  26. return ans;
  27. };

216. 组合总和 III

  1. var combinationSum3 = function(k, n) { // 老方法
  2. let ans = [];
  3. function dfs(path, start) {
  4. if (path.length === k) {
  5. let sum = path.reduce((pre, cur) => pre + cur, 0);
  6. if (sum === n) {
  7. ans.push(path);
  8. }
  9. return;
  10. }
  11. for (let i = start; i <= 9; i++) {
  12. dfs([...path, i], i + 1);
  13. }
  14. }
  15. dfs([], 1);
  16. return ans;
  17. };

46. Permutations

在微软的面试中,一点都没写出来,奇耻大辱啊。

  1. var permute = function(nums) {
  2. let n = nums.length;
  3. if (n <= 1) {
  4. return [[nums]]
  5. }
  6. let ans = [];
  7. let used = new Array(n).fill(false); // 只借助一个多余数组就可以完成
  8. function dfs(path) { // 本来是一个无数次的 for 循环,被轻松破解
  9. if (path.length === n) { // 人为去找终止条件!!!
  10. ans.push(path);
  11. return;
  12. }
  13. for (let i = 0; i < n; i++) {
  14. if (used[i]) {
  15. continue;
  16. }
  17. used[i] = true; // 仍然是前序操作
  18. dfs([...path, nums[i]]);
  19. used[i] = false; // 回溯
  20. }
  21. }
  22. dfs([]);
  23. return ans;
  24. };

47. Permutations II

加点料

  1. var permuteUnique = function(nums) {
  2. let n = nums.length;
  3. if (n <= 1) {
  4. return [[nums]];
  5. }
  6. nums.sort((a, b) => a - b);
  7. let ans = [];
  8. let used = new Array(n).fill(false); // 通用状态记录
  9. function dfs(path) {
  10. if (path.length === n) { // 人为去找终止条件!!!
  11. ans.push(path);
  12. return;
  13. }
  14. let pre;
  15. for (let i = 0; i < n; i++) {
  16. if (used[i]) { // 状态1: 是否被祖先节点使用过
  17. continue;
  18. }
  19. if (nums[i] === pre) { // 状态2:是否与兄弟节点的值相同
  20. continue;
  21. }
  22. pre = nums[i];
  23. if (!used[i]) {
  24. used[i] = true;
  25. dfs([...path, nums[i]]);
  26. used[i] = false;
  27. }
  28. // 不认真思考,很容易写成下面的样子
  29. // if (nums[i] !== pre) {
  30. // pre = nums[i];
  31. // if (!used[i]) {
  32. // used[i] = true;
  33. // dfs([...path, nums[i]]);
  34. // used[i] = false;
  35. // }
  36. // } else {
  37. // continue;
  38. // }
  39. }
  40. }
  41. dfs([]);
  42. return ans;
  43. };

78. 子集

以下这个太复杂:(包含了使用set去重的步骤)

  1. /**
  2. * @param {number[]} nums
  3. * @return {number[][]}
  4. */
  5. /**
  6. * @param {number[]} nums
  7. * @return {number[][]}
  8. */
  9. var subsets = function(nums) {
  10. let n = nums.length;
  11. if (n <= 0) {
  12. return [];
  13. }
  14. let used = new Array(n).fill(false);
  15. let ans_set = new Set(); // 使用set去重
  16. function dfs(path) {
  17. for (let i = 0; i < n; i++) {
  18. if (used[i]) {
  19. continue;
  20. }
  21. used[i] = true;
  22. let childPath = [...path, nums[i]].sort();
  23. ans_set.add(childPath.join(','));
  24. dfs(childPath);
  25. used[i] = false;
  26. }
  27. }
  28. dfs([]);
  29. let result = Array.from(ans_set).map(item => item.split(','));
  30. result.push([]);
  31. return result;
  32. };

简单些:(把去重一并做了)

  1. var subsets = function(nums) {
  2. let n = nums.length;
  3. if (n <= 0) {
  4. return [];
  5. }
  6. let ans = [];
  7. function dfs(path, start) {
  8. ans.push(path);
  9. for (let i = start; i < n; i++) {
  10. dfs([...path, nums[i]], i + 1); // 注意这里没有回溯的步骤
  11. }
  12. }
  13. dfs([], 0);
  14. return ans;
  15. };

把path写开,就有回溯了:

  1. var subsets = function(nums) {
  2. let n = nums.length;
  3. if (n <= 0) {
  4. return [];
  5. }
  6. let ans = [];
  7. let path = [];
  8. function dfs(start) {
  9. ans.push(path.slice(0));
  10. for (let i = start; i < n; i++) {
  11. path.push(nums[i]);
  12. dfs(i + 1);
  13. path.pop(); // 有了回溯
  14. }
  15. }
  16. dfs(0);
  17. return ans;
  18. };

90. 子集 II

和 47 排列问题一样的手段,要去重,先排序。

  1. var subsetsWithDup = function(nums) {
  2. let n = nums.length;
  3. nums.sort((a, b) => a - b); // 先排序去对付这种去重的问题
  4. let ans_set = new Set();
  5. function dfs(path, start) {
  6. ans_set.add(path);
  7. let pre;
  8. for (let i = start; i < n; i++) {
  9. if (pre !== nums[i]) {
  10. pre = nums[i];
  11. } else {
  12. continue;
  13. }
  14. dfs([...path, nums[i]], i + 1);
  15. }
  16. }
  17. dfs([], 0);
  18. return Array.from(ans_set);
  19. };

491. 递增子序列

重点在于去重,看这样一个例子:
数组:x, y, 1(a), z,1(b), 1(c) , 有3个相同的1,但是用括号 a b c 标明三个1 在 不同位置上。我们要从其中挑出两个元素,要求值不能重复:1(a), 1(b) 和 1(b) ,1(c) 是重复的
去重原理:
image.png
上图红色框到的两个部分就是相同的!
看代码实现:

  1. var findSubsequences = function(nums) {
  2. let n = nums.length;
  3. if (n <= 1) {
  4. return [];
  5. }
  6. let ans = [];
  7. // let used = [];
  8. // function dfs(path, start) {
  9. // if (path.length > 1) {
  10. // ans.push(path);
  11. // }
  12. // let pre; // 只能检测相邻相等元素
  13. // for (let i = start; i < n; i++) {
  14. // if (pre === nums[i]) {
  15. // continue;
  16. // }
  17. // pre = nums[i];
  18. // used[nums[i]] = true;
  19. // let last = path.length ? path[path.length - 1] : -Infinity;
  20. // if (last <= nums[i]) {
  21. // dfs([...path, nums[i]], i + 1);
  22. // }
  23. // }
  24. // }
  25. function dfs(path, start) {
  26. if (path.length >= 2) {
  27. ans.push(path.slice(0));
  28. }
  29. let used = new Set(); // 能检测相邻或不相邻的 相等元素
  30. for (let i = start; i < n; i++) {
  31. if (used.has(nums[i])) {
  32. continue;
  33. }
  34. used.add(nums[i]);
  35. let last = (path.length > 0) ? path[path.length - 1] : -Infinity;
  36. if (last <= nums[i]) {
  37. dfs([...path, nums[i]], i + 1);
  38. }
  39. }
  40. }
  41. dfs([], 0);
  42. return ans;
  43. };

18. 四数之和(和491相同的去重思路)

  1. /**
  2. * @param {number[]} nums
  3. * @param {number} target
  4. * @return {number[][]}
  5. */
  6. var fourSum = function(nums, target) {
  7. nums.sort((a, b) => a - b);
  8. let ans = [];
  9. function dfs(path, start) {
  10. if (path.length === 4) {
  11. let sum = path.reduce((pre, cur) => pre + cur);
  12. if (sum === target) {
  13. ans.push(path);
  14. }
  15. return sum;
  16. }
  17. let used = new Set();
  18. for (let i = start; i < nums.length; i++) {
  19. if (used.has(nums[i])) {
  20. continue;
  21. }
  22. used.add(nums[i]);
  23. let sum = dfs([...path, nums[i]], i + 1);
  24. if (sum !== undefined && sum >= target) { // add
  25. break;
  26. }
  27. }
  28. }
  29. dfs([], 0);
  30. return ans;
  31. };

除了四数之和,还能解决 三数之和
小结:在求数组排列时,我们知道如何使用递归的方式去做,这里也是同样的要求选择三个数,道理一样。但是盲目递归是暴力解法,对于本题的条件
我们先对数组进行排列,将会大大减少时间复杂度。

51. N-Queens

动画解释(没玩过):

  1. var solveNQueens = function(n) {
  2. if (n <= 1) {
  3. return [[nums]]
  4. }
  5. let ans = [];
  6. let cols = new Set(); // used1
  7. let dias1 = new Set(); // used2
  8. let dias2 = new Set(); // used3
  9. function dfs(row) {
  10. if (row === n) { // 人为去找终止条件!!!
  11. let tmp = [];
  12. for (let col of cols) {
  13. let _tmp = new Array(n).fill('.');
  14. _tmp[col] = 'Q';
  15. tmp.push(_tmp);
  16. }
  17. ans.push(tmp);
  18. return;
  19. }
  20. for (let i = 0; i < n; i++) {
  21. if (cols.has(i)) {
  22. continue;
  23. }
  24. if (dias1.has(i - row)) {
  25. continue;
  26. }
  27. if (dias2.has(i + row)) {
  28. continue;
  29. }
  30. cols.add(i); // 仍然是前序操作
  31. dias1.add(i - row);
  32. dias2.add(i + row);
  33. dfs(row + 1);
  34. cols.delete(i)
  35. dias1.delete(i - row);
  36. dias2.delete(i + row);
  37. }
  38. }
  39. dfs(0);
  40. return ans;
  41. };

这里提出一个性概念,对于矩阵有斜列:
image.png
根据这样的新分法,矩阵里的内容可以重新被划分一次。非常巧妙。

37. 解数独

解题思路
亮点,不断从头重试
递归函数 用 两个 for 循环 嵌套 查询 列与行 的 格子,如果

  1. 不为空格

    不为空,有两种情况,1)棋盘初始值,2)棋盘的尝试值 (其实第二种情况在递归里被统一了)
    啥都不做,继续看下一个格子

  2. 为空格

每一个 空格试 1-9个数字,
1)如果9 个数字都不满足,则需要回溯;
2)如果有数字 i 满足,则将该空格,记录为 i, 再从头重试。这里填入的值,构成的 新的初始棋盘 再去求解。

何时是边界:

  1. 递归到 棋盘的最后一个空格,这时 试 1-9个数字,可以选出符合条件的数字,找到了本题的解。
  2. 如果 最后一个空格 也是 棋盘最后一行最后一列的位置,这时 棋盘全有值,构成的 新的初始棋盘 再去求解,全部通过,返回 true,无需回溯修改。
  1. var solveSudoku = function(board) {
  2. function isValid(row, col, val) {
  3. let len = board.length
  4. // 行不能重复
  5. for(let i = 0; i < len; i++) {
  6. if(board[row][i] == val) {
  7. return false
  8. }
  9. }
  10. // 列不能重复
  11. for(let i = 0; i < len; i++) {
  12. if(board[i][col] == val) {
  13. return false
  14. }
  15. }
  16. let startRow = Math.floor(row / 3) * 3
  17. let startCol = Math.floor(col / 3) * 3
  18. for(let i = startRow; i < startRow + 3; i++) {
  19. for(let j = startCol; j < startCol + 3; j++) {
  20. if(board[i][j] == val) {
  21. return false
  22. }
  23. }
  24. }
  25. return true
  26. }
  27. function backTracking() {
  28. for(let i = 0; i < board.length; i++) {
  29. for(let j = 0; j < board[0].length; j++) {
  30. if(board[i][j] !== '.') continue;
  31. for(let k = 1; k <= 9; k++) {
  32. if (isValid(i, j, k)) {
  33. board[i][j] = String(k);
  34. if (backTracking()) return true;
  35. board[i][j] = `.`
  36. }
  37. }
  38. return false
  39. }
  40. }
  41. return true;
  42. }
  43. backTracking()
  44. };

39. Combination Sum

这道题我没有使用回溯,但是使用两处剪枝。

  1. var combinationSum = function(candidates, target) {
  2. let ans = new Set();
  3. function dfs(cans, tar, path) {
  4. cans = cans.filter(can => can <= tar); // 剪枝 1
  5. if (cans.length === 0) { // 人为去找终止条件!!!
  6. let sum = path.reduce((pre, cur) => pre + cur, 0);
  7. if (sum === target) {
  8. let tmp = path.sort((a, b) => a - b).join(',');
  9. ans.add(tmp);
  10. }
  11. return;
  12. }
  13. for (let num of cans) {
  14. dfs(cans, tar - num, [...path, num]); // 剪枝 2
  15. }
  16. }
  17. dfs(candidates, target, []);
  18. return Array.from(ans).map(item => item.split(','))
  19. };

在去重的效率非常低:
image.png
之后看看别人是怎么写的,目前我对这类题的认知只有这么高了。

https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/

45. 跳跃游戏 II

这种技法可以解决很多问题,超乎想象

  1. var jump = function(nums) {
  2. let ans = [];
  3. function dfs(start, depth) {
  4. if (start >= nums.length - 1) {
  5. ans.push(depth);
  6. return;
  7. }
  8. let steps = nums[start];
  9. for (let i = 1; i <= steps; i++) {
  10. dfs(start + i, depth + 1);
  11. }
  12. }
  13. dfs(0, 0);
  14. return Math.min(...ans);
  15. };