77. 组合
分析搜索起点的上界进行剪枝
事实上,如果 n = 7, k = 4,从 5 开始搜索就已经没有意义了,这是因为:即使把 5选上,后面的数只有 6和 7,一共就 3个候选数,凑不出 4 个数的组合。
搜索起点的上界 + 接下来要选择的元素个数 - 1 = n
其中,接下来要选择的元素个数 = k - path.size(),整理得到:搜索起点的上界 = n - (k - path.size()) + 1
https://leetcode-cn.com/problems/combinations/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-ma-/
var combine = function(n, k) {let result=[]let path=[]function tranverse(startIndex){if(path.length===k){ //回溯终止条件result.push([...path])return;}//耗时太久,需要剪枝优化,建议画图for(let i=startIndex;i<=n-k+path.length+1;i++){path.push(i)tranverse(i+1)path.pop() //回溯撤销处理}}tranverse(1);return result;};
17. 电话号码的字母组合
/*** @param {string} digits* @return {string[]}*/var letterCombinations = function(digits) {let result=[]const length= digits.lengthif(length<1) return []const letters=["abc","def","ghi","jkl","mno","pqrs","tuv","wxyz",]let path=""function tranverse(startIndex,path){if(path.length===length){result.push(path)return;}const letter=letters[digits[startIndex]-2]for(let j=0;j<letter.length;j++){tranverse(startIndex+1,path+letter[j])}}tranverse(0,path)return result;};
131. 分割回文串
与复原IP地址类似,加入path前都有一定的校验。
/*** @param {string} s* @return {string[][]}*/var partition = function(s) {let result=[];let path=[];function backtracking(startIndex){if(startIndex > s.length-1){result.push([...path])return;}for(let i=startIndex;i<s.length;i++){let str=s.substr(startIndex,i-startIndex+1)if(isPalindrome(str)) {path.push(str)backtracking(i+1)path.pop()}else{continue;}}}backtracking(0)return result;};var isPalindrome =function(s){let left=0,right=s.length-1while(left<right){if(s[left]!==s[right]){return false}left++;right--;}return true;}
93. 复原 IP 地址
与分割回文串类似,加入path前都有一定的校验。
/*** @param {string} s* @return {string[]}*/var isValid=function(s){if(s.length>1 && s[0]==='0') return false;if(Number(s)>255) return false;return true;}var restoreIpAddresses = function(s) {let result=[]let path=[]function backtracking(startIndex){if(path.length===4 && startIndex>s.length-1){if(startIndex>s.length-1){result.push(path.join('.'));}return}for(let i=startIndex;i<s.length;i++){let str=s.substr(startIndex,i-startIndex+1)if(isValid(str)){path.push(str)backtracking(i+1)path.pop()}}}backtracking(0)return result;};
78. 子集
与组合的区别:组合取叶子节点,子集取每个节点
var subsets = function(nums) {//组合取叶子节点,子集取每个节点function backtracking(startIndex){result.push([...path])for(let i=startIndex;i<nums.length;i++){path.push(nums[i])backtracking(i+1)path.pop()}}let result=[],path=[];backtracking(0)return result;};
90. 子集 II
与子集①相比,需要使用used来判断同一层是否用过相同数字,来去重。
如果用set来判断的话,空间复杂度和时间复杂度更高。
/*** @param {number[]} nums* @return {number[][]}*/var subsetsWithDup = function(nums) {let result=[],path=[];let used=new Array(nums.length).fill(false);nums.sort((a, b) => a - b);function backtracking(startIndex){result.push([...path])for(let i=startIndex;i<nums.length;i++){if(i>0 && nums[i]===nums[i-1] && !used[i-1]) continue;used[i]=true;path.push(nums[i])backtracking(i+1)used[i]=false;path.pop()}}backtracking(0)return result;};
46. 全排列
和组合的区别是:
- 每层都是从0开始搜索而不是startIndex
需要used数组记录path里都放了哪些元素了
/*** @param {number[]} nums* @return {number[][]}*/var permute = function(nums) {let result=[],used=[],path=[];function backtracking(used,path){if(path.length===nums.length) {result.push([...path])return;}for(let i=0;i<nums.length;i++){const num=nums[i];if(used[i]){continue;}used[i]=true;path.push(num);backtracking(used,path);path.pop();used[i]=false;}}backtracking(used,path)return result;};
47. 全排列 II
需要去重,和子集②的去重的方法类似。
var permuteUnique = function(nums) {let result=[],path=[];let used=new Array(nums.length-1).fill(false)function backtracking(array){if(path.length===array.length){result.push([...path])return;}for(let i=0;i<array.length;i++){if(used[i]) continue; //全排列因为从0开始,需要对同一枝是否用过判断if(i>0 && array[i-1]===array[i] && !used[i-1]){continue; //used[i-1]为false说明同层相等数在之前用过了,为true表示是同一枝}used[i]=true;path.push(array[i])backtracking(array)path.pop();used[i]=false;}}nums.sort((a, b) => a - b);backtracking(nums)return result;};
22. 括号生成
/*** @param {number} n* @return {string[]}*/var generateParenthesis = function(n) {//剩余左括号的数量必须小于剩余右括号的数量//回溯function backtracking(path,l,r){if(l===0 && r===0){result.push(path);return;}if(l===r)backtracking(path+"(",l-1,r)if(l<r) {if(l>0) backtracking(path+"(",l-1,r)backtracking(path+")",l,r-1)}}let result=[],path="";backtracking(path,n,n)return result;};
51. N 皇后
/*** @param {number} n* @return {string[][]}*/var isValid=function(row,column,dp){const n=dp.length;for(let r=0;r<row;r++){if(dp[r][column]==='Q') return false;}for(let r=row-1,c=column-1;r>=0 && c>=0;r--,c--){if(dp[r][c]==='Q') return false;}for(let r=row-1,c=column+1;r>=0 && c<n;r--,c++){if(dp[r][c]==='Q') return false;}return true;}var solveNQueens = function(n) {let result=[];let dp=new Array(n).fill([]).map(() => new Array(n).fill('.'))function backtracking(row){if(row===n){result.push(dp.map((i)=>{return i.join('')}))return;}for(let j=0;j<n;j++){if(!isValid(row,j,dp)) continue;dp[row][j]='Q'backtracking(row+1);dp[row][j]='.'}}backtracking(0)return result;};
组合总和1
var combinationSum = function (candidates, target) {let result = [], path = [];candidates.sort((a, b) => a - b);//不要写成candidates.sort(),这是默认从高到低function backtracking(startIndex, sum) {if (sum === target) {result.push([...path])return}for (let i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) { //需要剪枝path.push(candidates[i])backtracking(i, sum + candidates[i])path.pop()}}backtracking(0, 0)return result;};
组合总和2
与上题不同的点:
1、candidates 中的每个数字在每个组合中只能使用 一次 。即要传入startIndex
2、解集不能包含重复的组合。需要排序+used保证同级没有重复数字
var combinationSum2 = function (candidates, target) {let result = []let path = [];candidates.sort((a, b) => a - b);let used = Array(candidates.length).fill(false)function backtracking(startIndex, sum) {if (sum === target) {result.push([...path])return;}for (let i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {if (i > 0 && candidates[i] === candidates[i - 1] && !used[i - 1]) {continue}used[i] = true;path.push(candidates[i])backtracking(i + 1, sum + candidates[i])path.pop();used[i] = false;}}backtracking(0, 0)return result;};
组合总和3
这题和77.组合类似,是组合总和中最简单的一题。
var combinationSum3 = function (k, n) {let result = []let path = []function backtracking(startIndex, sum) {if (path.length === k && sum === n) {result.push([...path])return;}if (path.length >= k) return;for (let i = startIndex; i <= 9 && sum + i <= n; i++) {path.push(i)backtracking(i + 1, sum + i)path.pop();}}backtracking(1, 0);return result;};
组合总和 Ⅳ
这道题用回溯会爆栈,代码和组合总和I一样
var combinationSum4 = function (nums, target) {let result = [], path = [];nums.sort((a, b) => a - b);function backtracking(sum) {if (sum === target) {result.push([...path])return;}for (let i = 0; i < nums.length && sum + nums[i] <= target; i++) {path.push(nums[i]);backtracking(sum + nums[i]);path.pop();}}backtracking(0)return result.length;};

怎么还可以用动态规划?噢因为这道题求总数,而不要求具体的结果,所以不必用回溯。
记住!求结果数量时想到动态规划,相当于回溯+备忘录。而求具体组合结果时必须用回溯穷举了。
//怎么还可以用动态规划??var combinationSum4 = function(nums, target) {//完全背包,排列方案总和,需要累加let dp=Array(target+1).fill(0);dp[0]=1;for(let i=1;i<=target;i++){for(let j=0;j<nums.length;j++){if(nums[j]>i) continue;dp[i]=dp[i]+dp[i-nums[j]]}}return dp[target];};
140. 单词拆分 II
与单词拆分①的区别:不求具体结果的用动规,求具体穷举结果的用回溯。
这题无限使用
var wordBreak = function (s, wordDict) {let result = [], path = []var backtracking = function (startIndex) {if (startIndex === s.length) {result.push(path.join(" "));return;}for (let i = 0; i < wordDict.length; i++) {if (s.substr(startIndex, wordDict[i].length) === wordDict[i]) {path.push(wordDict[i]);backtracking(startIndex + wordDict[i].length)path.pop();}}}backtracking(0)return result;};
