力扣日记12:回溯


回溯法,一般可以解决如下几种问题:

组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等

  • 回溯模板,实质上就是多层的for嵌套,只不过可以在其中加入剪枝操作
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

77. 组合
  • 核心就是记录startIndex,向其后遍历
var combine = function(n, k) {
    let res = [], path = [];
    const backtracking = (startIndex) => {
        if (path.length === k) {
            res.push([...path]);
            return;
        }
        for (let i = startIndex; i <= n; i++) {
            path.push(i);
            backtracking(i + 1);
            path.pop();
        }
    }
    backtracking(1);
    return res;
};
  • 剪枝操作
    在这里插入图片描述

接下来看一下优化过程如下:

已经选择的元素个数:path.size();

还需要的元素个数为: k - path.size();

在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历

为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。

从2开始搜索都是合理的,可以是组合[2, 3, 4]。

for (let i = startIndex; i <= n - (k - path.length) + 1; i++)
216. 组合总和 III
  • 和上题类似,注意剪枝
var combinationSum3 = function(k, n) {
    let res = [], path = [], sum = 0;
    const backtracking = (index) => {
        if (sum > n) return;
        if (path.length === k) {
            if (sum === n) res.push([...path]);
            return;
        }
        for (let i = index; i <= 9 - (k - path.length) + 1; i++) {
            path.push(i);
            sum += i;
            backtracking(i + 1);
            sum -= i;
            path.pop();
        }
    }
    backtracking(1);
    return res;
};
17. 电话号码的字母组合
  • 这里传入的index应该是digits的第几个数字
var letterCombinations = function(digits) {
    const n = digits.length;
    const map = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"];
    let res = [], path = [];
    if (!n) {
        return [];
    }
    const backtracking = (index) => {
        if (path.length === n) {
            res.push(path.join(''));
            return;
        }
        let str = map[digits[index]];
        for (let i = 0; i < str.length; i++) {
            path.push(str[i]);
            backtracking(index + 1);
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
39. 组合总和
  • 本题需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合

var combinationSum = function(candidates, target) {
    const n = candidates.length;
    let res = [], path = [], sum = 0;
    const backtracking = (index) => {
        if (sum > target) return;
        else if (sum === target) {
            res.push([...path]);
            return;
        }
        for (let i = index; i < n && sum + candidates[i] <= target; i++) {
            path.push(candidates[i]);
            sum += candidates[i];
            backtracking(i); // 关键点:不用i+1了,表示可以重复读取当前的数
            sum -= candidates[i];
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
40. 组合总和 II
  • 这里要说一下js中的sort

sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的
在这里插入图片描述

  • 因此我们要想得到一个升序或降序的数组,就需要写对应的回调函数
    const numbers = [2, 1, 3];
    numbers.sort(function (a, b) {
        return a - b;
    });
    console.log(numbers); // [1,2,3]
    numbers.sort(function (a, b) {
        return b - a;
    })
    console.log(numbers); // [3,2,1]
  • 排序过后我们需要在树层中去重
var combinationSum2 = function(candidates, target) {
    const n = candidates.length;
    let res = [], path = [], sum = 0;
    candidates.sort((a,b) => a - b);
    const backtracking = (index) => {
        if (sum === target) {
            res.push([...path]);
            return;
        }
        for (let i = index; i < n && sum + candidates[i] <= target; i++) {
            if (i > index && candidates[i] === candidates[i - 1]) {
              continue;  
            }
            path.push(candidates[i]);
            sum += candidates[i];
            backtracking(i + 1);
            sum -= candidates[i];
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
131. 分割回文串
  • 需要一个判断回文的函数,传入的i是截取的长度
const isPalindrome = (s, l, r) => {
    for (let i = l, j = r; i < j; i++, j--) {
        if (s[i] !== s[j]) return false;
    }
    return true;
}
var partition = function (s) {
    let n = s.length;
    let res = [], path = [];
    const backtracking = (i) => {
        if (i >= n) {
            res.push([...path]);
            return;
        }
        for (let j = i; j < n; j++) {
            let str = s.slice(i, j + 1);
            if (!isPalindrome(s, i, j)) continue;
            path.push(str);
            backtracking(j + 1);
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
93. 复原 IP 地址
  • ip分为四段,所以当path长度为4且使用了字符串整串构建时为结束
var restoreIpAddresses = function(s) {
    let res = [], path = [];
    const backtracking = (i) => {
        const len = path.length;
        if (len > 4) return;
        // 当path有四段,而且使用了全部字符
        if (len === 4 && i === s.length) {
            res.push(path.join('.'));
            return;
        }
        for (let j = i; j < s.length; j++) {
            const str = s.slice(i, j + 1);
            if (str.length > 3 || +str > 255) break;
            if (str.length > 1 && str[0] === '0') break;
            path.push(str);
            backtracking(j + 1);
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
78. 子集
  • 模板题,提交所有结果即可
  • 子集问题和组合问题、分割问题的的区别,子集是收集树形结构中树的所有节点的结果。
    而组合问题、分割问题是收集树形结构中叶子节点的结果。
var subsets = function(nums) {
    const n = nums.length;
    let res = [], path = [];
    const backtracking = (i) => {
        res.push([...path]);
        for (let j = i; j < n; j++) {
            path.push(nums[j]);
            backtracking(j + 1);
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
90. 子集 II
  • 在上题的基础加上去重,在同一树层
var subsetsWithDup = function(nums) {
    let res = [], path = [];
    nums.sort((a, b) => a - b);
    const backtracking = (i) => {
        res.push([...path]);
        for (let j = i; j < nums.length; j++) {
            // 同树层,相同元素只用一次
            if (j > i && nums[j] === nums[j - 1]) continue;
            path.push(nums[j]);
            backtracking(j + 1);
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
491. 递增子序列
  • 一样的同树层不能使用相同的元素
var findSubsequences = function(nums) {
    let res = [], path = [];
    const backtracking = (i) => {
        if (path.length >= 2) {
            res.push([...path]);
        }
        let set = [];
        for (let j = i; j < nums.length; j++) {
            if (path.length && nums[j] < path[path.length - 1] || set[nums[j] + 100]) {
                continue;
            }
            set[nums[j] + 100] = true;  // -100 ~ 100
            path.push(nums[j]);
            backtracking(j + 1);
            path.pop();
        }
    }
    backtracking(0);
    return res;
};
46. 全排列
  • 和组合问题不同的时,排列是有序的,[1,2]和[2,1]是两个不同的排序
  • 因此我们需要一个used数组标记已选择的元素,并且每次搜索要索引从0开始
    在这里插入图片描述
var permute = function (nums) {
    let res = [], path = [], used = [];
    const n = nums.length;
    const backtracking = () => {
        if (path.length === n) {
            res.push([...path]);
            return;
        }
        for (let i = 0; i < n; i++) {
            if (used[i]) {
                continue;
            }
            path.push(nums[i]);
            used[i] = true;
            backtracking();
            used[i] = false;
            path.pop();
        }
    }
    backtracking();
    return res;
};
47. 全排列 II
  • 在全排列的基础上加上同树层不出现同一元素
var permuteUnique = function(nums) {
    const n = nums.length;
    let res = [], path = [], used = [];
    const backtracking = () => {
        if (path.length === n) {
            res.push([...path]);
            return;
        }
        let set = [];
        for (let i = 0; i < n; i++) {
            if (used[i] || set[nums[i] + 10]) continue;
            path.push(nums[i]);
            used[i] = true;
            set[nums[i] + 10] = true;
            backtracking();
            path.pop();
            used[i] = false;
        }
    }
    backtracking();
    return res;
};
51. N 皇后
var solveNQueens = function (n) {
    function isValid(row, col, cheesBorad, 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(arr) {
        let chessBoard = [];
        arr.forEach((row) => {
            let s = "";
            row.forEach((item) => {
                s += item;
            })
            chessBoard.push(s);
        })
        return chessBoard;
    }
    let res = [];
    let chessBoard = new Array(n).fill().map(() => new Array(n).fill('.'));

    const backtracking = (row) => {
        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[row][col] = '.';
            }
        }
    }
    backtracking(0);
    return res;
};
37. 解数独
/**
 * @param {character[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
var solveSudoku = function(board) {
    const isValid = (row, col, val) => {
        for (let i = 0; i < board.length; i++) {
            if (board[i][col] === val) return false;
        }
        for (let i = 0; i < board.length; i++) {
            if (board[row][i] === 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;
    }
    const 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 val = 1; val <= 9; val++) {
                    if (isValid(i, j, val.toString())) {
                        board[i][j] = val.toString();
                        if (backtracking()) return true;
                        board[i][j] = '.';
                    }
                }
                return false;
            }
        }
        return true;
    }
    backtracking();
    return board;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值