回溯算法题目

回溯算法题目

1. 组合问题

1. 77. 组合

push进res的条件:path的长度符合要求

push进path的条件:存在于1到n之间的数

这是组合的基础问题

/**
 * @param {number} n
 * @param {number} k
 * @return {number[][]}
 */
var combine = function(n, k) {
    let res = [];
    let path = [];
    const backtracking = (start) => {
        if(path.length === k) {
            res.push([...path]);
            return;
        }
        for(let i = start; i <= n; i++) {
            path.push(i);
            backtracking(i+1);
            path.pop();
        }
    }
    backtracking(1);
    return res;
};

2. ⭐17. 电话号码的字母组合

这道题目的关键是在多个组合中选数字,相当于树的每一层的数是在不同的地方选出的。相当于是两个遍历,先遍历digits,在遍历每个digits里对应的数。

/**
 * @param {string} digits
 * @return {string[]}
 */
var letterCombinations = function(digits) {
    const map = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"];
    let res = [];
    let path = [];
    if(!digits.length){
        return res;
    }
    const backtracking = (start) => {
        if(path.length === digits.length) {
            res.push(path.join(""));
            return;
        }
        for(let v of map[digits[start]]){
            path.push(v);
            backtracking(start+1);
            path.pop()
        }
    }
    backtracking(0);
    return res;
};

3. 39. 组合总和

push进res的条件:path的和符合要求

push进path的条件:加上增加的数的和不超过target

由于组合的数是可以重复的,所有下一层的start是上一层刚被选上的数

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum = function(candidates, target) {
    let res = [];
    let path = [];
    const backtracking = (pathSum,start) => {
        if(pathSum === target) {
            res.push([...path]);
            return ;
        }
        
        for(let i = start; i< candidates.length;i++){
            if(pathSum + candidates[i] > target) {
                continue;
            }
            path.push(candidates[i]);
            backtracking(pathSum + candidates[i],i);
            path.pop();
        }
    }
    backtracking(0,0);
    return res;
};

4.40. 组合总和 II

⭐不能有重复的组合,也就是说树的每一层的数不能重复。解决方法:先给数组排序,使得一样的数聚集在一起,i>start判断是否是第一个数,candidates[i] === candidates[i-1]判断是否相等,不是第一个数且相等要跳过。

⭐每个数字只能用一次。解决方法:往下一层的时候从加入path的下一个数开始

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum2 = function(candidates, target) {
    let res = [];
    let path = [];
    candidates.sort();
    const backtracking = (start,pathSum) => {
        if(pathSum === target) {
            res.push([...path]);
            return;
        }
        for(let i = start;i<candidates.length;i++) {
            if(pathSum + candidates[i] > target || (i>start && candidates[i] === candidates[i-1])) {
                continue;
            }
            path.push(candidates[i]);
            backtracking(i+1,pathSum + candidates[i])
            path.pop()
        }
    }
    backtracking(0,0);
    return res;
};

5. 216. 组合总和 III

push进res的条件:path的和等于target,path的长度等于k

push进path的条件:加上增加的数的和不超过target

组合的数是不可以重复,所有下一层的start是上一层的下一个

/**
* @param {number} k
* @param {number} n
* @return {number[][]}
*/
var combinationSum3 = function(k, n) {
   let res = [];
   let path = [];
   const backtracking = (start,pathSum) => {
       if(pathSum === n && path.length === k){
           res.push([...path]);
           return;
       }
       for(let i = start;i<=9;i++){
           if(pathSum + i > n) {
               continue;
           }
           path.push(i);
           backtracking(i+1,pathSum+i);
           path.pop();
       }
   }
   backtracking(1,0);
   return res;
};

2. 分割问题

1. 131. 分割回文串

重点是判断新切割的子串是否是回文串

/**
 * @param {string} s
 * @return {string[][]}
 */
var partition = function(s) {
    let path = []
    let res = []

    const isP = (s,start,end)=>{
        for(let i = start,j = end;i<j;i++,j--) {
            if(s[i] !== s[j]){
                return false;
            }
        }
        return true
    }

    const backtracking = (s,start) => {
        if(start >= s.length){
            res.push([...path])
            return;
        }
        for(let i = start; i< s.length;i++) {
            if(!isP(s,start,i)) {
                continue;
            }
            path.push(s.substr(start,i-start+1));
            backtracking(s,i+1);
            path.pop()
        }
    }
    backtracking(s,0)
    return res;
};

2. 93. 复原 IP 地址

push进res的条件:start到了最后,path的长度等于4

push进path的条件:字串不大于255,且前面第一位不能是0

这题的重点是+str可以将字符串转换成可以跟数字作比较

/**
 * @param {string} s
 * @return {string[]}
 */
var restoreIpAddresses = function(s) {
    let res = [];
    let path = [];
    const backtracking = (start) => {
        if(start === s.length && path.length === 4) {
            res.push(path.join('.'));
            return;
        }
        if(path.length > 4) {
            return;
        }
        for(let i = start; i < s.length; i++) {
            let str = s.substr(start,i-start+1);

            if(str.length > 1 && str[0] === "0"){
                break;
            }

            if(str.length > 3 || +str > 255) {
                break;
            }

            path.push(str);
            backtracking(i+1);
            path.pop();
        }
    }
    backtracking(0);
    return res;
};

3. 子集问题

1. 78. 子集

加入path条件:都可以

加入res条件:都可以

返回条件:start超出nums的长度范围

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    let res = [];
    let path = [];
    
    const backtracking = (start,nums) => {
        if (path.length <= nums.length){
            res.push([...path]);
        }else {
            return;
        }
        for (let i = start; i < nums.length; i++) {
            path.push(nums[i]);
            backtracking(i+1,nums);
            path.pop();
        }
    }
    backtracking(0,nums)
    return res;
};

2. 90. 子集 II

加入path条件:同一层中不同的数

加入res的条件:都可以

返回条件:start超出nums的范围

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsetsWithDup = function(nums) {
    let res = [];
    let path = [];
    nums.sort();

    const backtracking = (nums,start) => {
      res.push([...path]);
      if (start >= nums.length) {
          return;
      }
        for (let i = start; i < nums.length; i++) {
            if (i > start && nums[i] === nums[i-1]) {
                continue;
            }
            path.push(nums[i]);
            backtracking(nums,i+1);
            path.pop();
        }
    }
    backtracking(nums,0);
    return res;
};

4. 排序问题

1. 46. 全排列

加入path条件:在path中没有的数(这里可以用findIndex,可以新开辟一个used数组存放是否使用过的数)

加入res的条件:path的程度跟nums的长度相等

返回条件:res,push完返回

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
    let res = [];
    let path = [];

    const backtracking = (nums) => {
      if (path.length === nums.length) {
          res.push([...path]);
          return;
      }
        for (let i = 0; i < nums.length; i++) {
            if (path.findIndex((value => value === nums[i])) !== -1) {
                continue;
            }
            path.push(nums[i]);
            backtracking(nums);
            path.pop();
        }
    }
    backtracking(nums);
    return res;
};

2. ⭐47. 全排列 II

这个加入的条件很特殊,需要判断同一层的相同的数是否已经加入过path,加入过就不加

⭐同一个数组中加入过的数不能再加,怎么判断是否加入过?用全局数组used,加入了就设置为true,没有加入设置为false

⭐同一层中不能加入一样的数,怎么去重?排序,判断(i>0 && nums[i-1] === nums[i] && !used[i-1])要判断前面的数是否被用了,被用了代表虽然是相同的但是不在同一层,如果没有被用代表是在同一层

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permuteUnique = function(nums) {
    let res = [];
    let path = [];
    nums.sort();
    let used = new Array(nums.length).fill(false);

    const backtracking = (nums) => {
        if (path.length === nums.length) {
            res.push([...path]);
            return;
        }
        for (let i = 0; i < nums.length; i++) {
            // 如果这个数用过就跳过
            if (used[i]){
                continue;
            }
            // 如果这个是跟前面的一个数相同而且前面的一个数没用过
            if (i>0 && nums[i-1] === nums[i] && !used[i-1]){
                continue;
            }
            path.push(nums[i]);
            used[i] = true;
            backtracking(nums);
            path.pop();
            used[i] = false;
        }
    }
    backtracking(nums);
    return res;
};

5. 棋盘问题

1. 51. N 皇后

思路:以每一行为单位,看一个每一行的那个位置可以走,就放在那个位置,如果有一行的全部位置都不可以走,那么就回到上一行,移动上一行的棋子到别的地方,直到所有的棋子都放完

/**
 * @param {number} n
 * @return {string[][]}
 */
var solveNQueens = function(n) {
    function isValid(row, col, chessBoard, n) {
        for (let i = 0; i < row; i++) {
            if (chessBoard[i][col] === 'Q') {
                return false;
            }
        }

        for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (chessBoard[i][j] === 'Q') {
                return false;
            }
        }

        for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (chessBoard[i][j] === 'Q') {
                return false;
            }
        }

        return true;
    }

    function transformChessBoard(chessBoard) {
        let chessBoardBack = [];
        chessBoard.forEach(row => {
            let rowStr = '';
            row.forEach(value => {
                rowStr += value;
            })
            chessBoardBack.push(rowStr)
        })

        return chessBoardBack
    }

    let chessBoard = Array.from(new Array(n),()=>new Array(n).fill('.'));
    let res = [];
    const backtracking = (row,chessBoard) => {
      if (row === n){
          res.push(transformChessBoard(chessBoard))
          return 
      }
        for (let col = 0; col < n; col++) {
            if (isValid(row,col,chessBoard,n)) {
                chessBoard[row][col] = 'Q'
                backtracking(row+1,chessBoard)
                chessBoard[row][col] = '.'
            }
        }
    }
    backtracking(0,chessBoard);
    return res
};

2. 37. 解数独

跟上一题是一样的,我记得这题在大一学java的时候做过,那时候没学算法,写了一整天都没写出来

/**
 * @param {character[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
var solveSudoku = function(board) {
    function isValid(row, col, val, board) {
        let len = board.length;
    //    验证行
        for (let i = 0; i < len; i++) {
            if (board[row][i] === val) {
                return false
            }
        }
    //    验证列
        for (let i = 0; i < len; i++) {
            if (board[i][col] === val) {
                return false
            }
        }
    //    九个格子内不一样
        let startRow = Math.floor(row / 3) * 3;
        let startCol = Math.floor(col / 3) * 3;

        for (let i = startRow; i < startRow + 3; i++) {
            for (let j = startCol; j < startCol + 3; j++) {
                if (board[i][j] === val) {
                    return false
                }
            }
        }
        return true
    }

    function backtracking() {
        for (let i = 0; i < board.length; i++) {
            for (let j = 0; j < board[0].length; j++) {
                if (board[i][j] !== '.'){
                    continue;
                }
                for (let k = 1; k <= 9; k++) {
                    if (isValid(i,j,String(k),board)) {
                        board[i][j] = String(k);
                        if (backtracking()){
                            return true
                        }

                        board[i][j] = '.'
                    }
                }
                return false
            }
        }
        return true
    }
    backtracking();
    return board
};

6. 总结

  1. 归纳一个回溯的模板

    function backtracking() {
        if(终止条件){
            收集结果
            return
        }
        for(集合元素){
            // 处理节点看能不能放进path
            backtracking()
            // 回溯(撤销操作)
        }
    }
    
  2. 回溯需要确定的三个问题:怎样可以放进path,怎样可以放进res,什么时候返回

posted @ 2022-04-26 16:03  kihyun  阅读(30)  评论(0编辑  收藏  举报