回溯算法
回溯算法基本理论
是什么:
回溯采用是错得方法解决问题,一旦发现当前步骤失败,回溯算法就会返回上一个步骤
解决哪些问题:
- 组合问题(组合不强调顺序,而排列强调)
- 切割问题
- 子集问题
- 排列问题
- 棋盘问题
如何理解回溯法
回溯法都可以抽象为一个树形结构。一个n叉树
回溯法的模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void backtrading(参数){ if (终止条件){ return } for (集合得元素集){ } return }
|
回溯算法剪枝
剪枝一般都是在for循环的i的范围上做文章,例如下面的组合题目,确定i至多从哪里开始。
回溯算法题目
组合问题
题目:https://leetcode.cn/problems/combinations/description/
未剪枝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| var combine = function(n, k) { var path = []; var res = []; const backTrading = (n, k, startIndex) => { if(path.length === k){ res.push([...path]); return; } for(let i = startIndex; i <= n; i++){ path.push(i); backTrading(n, k, i + 1); path.pop(); } } backTrading(n, k, 1); return res; };
|
剪枝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
var combine = function(n, k) { var path = []; var res = []; const backTrading = (n, k, startIndex) => { if(path.length === k){ res.push([...path]); return; } for(let i = startIndex; i <= (n-(k-path.length)+1); i++){ path.push(i); backTrading(n, k, i + 1); path.pop(); } } backTrading(n, k, 1); return res; };
|
组合总和 III
https://leetcode.cn/problems/combination-sum-iii/description/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var combinationSum3 = function(k, n) { let path = [] let res = [] const backTrading = (n,k,sum,startIndex)=>{ if(sum>n) return if(path.length ===k){ if(sum === n){ res.push([...path]) } return } for(let i = startIndex;i<=(9-(k-path.length)+1);i++){ sum += i path.push(i) backTrading(n,k,sum,i+1) sum -= i path.pop() } } backTrading(n,k,0,1) return res };
|
电话号码的字母组合
https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/
解题思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| var letterCombinations = function(digits) { const map = new Map() map.set('2','abc') map.set('3','def') map.set('4','ghi') map.set('5','jkl') map.set('6','mno') map.set('7','pqrs') map.set('8','tuv') map.set('9','wxyz') let str = '' let res = [] const backTrading = (digits,index)=>{ if(!digits) return if(index === digits.length){ res.push(str) return } let digit = digits[index] let letters = map.get(digit) for(let i=0;i<letters.length;i++){ str += letters[i] backTrading(digits,index+1) str = str.slice(0,str.length-1) } } backTrading(digits,0) return res };
|
组合总和
https://leetcode.cn/problems/combination-sum/description/
数组中没有0且没有重复数字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| var combinationSum = function(candidates, target) { let path = [] let res = [] const backTraking = (candidates,target,sum,startIndex)=>{ if (sum > target) return if (sum === target) { res.push(path.slice()) return } for(let i = startIndex;i<candidates.length;i++){ path.push(candidates[i]) sum += candidates[i] backTraking(candidates,target,sum,i) sum -= candidates[i] path.pop() } } backTraking(candidates,target,0,0) return res };
|
剪枝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var combinationSum = function(candidates, target) { let path = [] let res = [] const backTraking = (candidates,target,sum,startIndex)=>{ if (sum === target) { res.push(path.slice()) return } for(let i = startIndex;i<candidates.length;i++){ if (candidates[i] + sum > target) continue; path.push(candidates[i]) sum += candidates[i] backTraking(candidates,target,sum,i) sum -= candidates[i] path.pop() } } backTraking(candidates,target,0,0) return res };
|
组合总和 II
https://leetcode.cn/problems/combination-sum-ii/description/
难点在于:结果中不能有重复的结果[1,7],[7,1]。但是能原数组中有两个1,有可以有[1,1,6]。
树层去重和树枝去重
https://www.programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html#%E6%80%9D%E8%B7%AF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| var combinationSum2 = function(candidates, target) { let path = [] let res = [] candidates.sort((a,b)=>a-b) const backTracking = (candidates,target,sum,startIndex,used)=>{ if (sum === target){ res.push(path.slice()) return } for(let i = startIndex;i<candidates.length;i++){ if (sum + candidates[i] > target) continue if (i > 0 && candidates[i] === candidates[i-1] && !used[i-1]) continue path.push(candidates[i]) sum += candidates[i] used[i] = true backTracking(candidates,target,sum,i+1,used) sum -= candidates[i] path.pop() used[i] = false } } backTracking(candidates,target,0,0,new Array(candidates.length).fill(false)) return res };
|
分割回文子串
https://leetcode.cn/problems/palindrome-partitioning/description/
难点:
- 切割问题和组合是一样的,之前选择的元素下标,相当于这里的切割线
- 如何表示切割的字串[startIndex,i]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| var partition = function(s) { let path = [] let res = [] const backTraking = (s,startIndex)=>{ if(startIndex >= s.length){ res.push(path.slice()) return } for(let i=startIndex;i<s.length;i++){ if(isPlalindrome(s,startIndex,i)){ path.push(s.substring(startIndex,i+1)) backTraking(s,i+1) path.pop() } } } const isPlalindrome = (s,left,right)=>{ while(left<right){ if(s[left] !== s[right]) return false left++ right-- } return true } backTraking(s,0) return res };
|
复原IP地址
https://leetcode.cn/problems/restore-ip-addresses/description/
合法的IP地址:有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| var restoreIpAddresses = function(s) { let path = [] let res = [] const backTracking = (s,startIndex)=>{ if(path.length === 4 && startIndex === s.length){ res.push(path.join('.')) return } if(path.length === 4 && startIndex < s.length) return for(let i= startIndex;i<s.length && i<= startIndex+3;i++){ if (isValid(s,startIndex,i)){ path.push(s.substring(startIndex,i+1)) backTracking(s,i+1) path.pop() } } } const isValid = (s,startIndex,i)=>{ if(s[startIndex] === '0' && i !== startIndex) return false if(parseInt(s.substring(startIndex,i+1)) > 255) return false return true } backTracking(s,0) return res };
|
前面的一系列都是在叶子节点收获结果,终止条件的写法基本一致
后面开始要从每一个节点收获结果,终止条件的写法发生变化。
子集
https://leetcode.cn/problems/subsets/description/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var subsets = function(nums) { let path = [] let res = [] const backTraking = (nums,startIndex)=>{ res.push([...path]) if(startIndex >= nums.length){ return }
for(let i=startIndex;i<nums.length;i++){ path.push(nums[i]) backTraking(nums,i+1) path.pop() } } backTraking(nums,0) return res };
|
子集II
树层去重:确定当前在树层used[i-1]===fasle,当前元素与前一元素相等,i>0
https://leetcode.cn/problems/subsets-ii/description/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| var subsetsWithDup = function(nums) { let path = [] let res = [] used = new Array(nums.length).fill(false) nums.sort((a, b) => a - b) const backTraking = (nums, startIndex, used) => { res.push([...path]) if (startIndex >= nums.length) { return } for (let i = startIndex; i < nums.length; i++) { if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) { continue } path.push(nums[i]) used[i] = true backTraking(nums, i + 1, used) path.pop() used[i] = false } } backTraking(nums, 0, used) return res };
|
递增子序列
https://leetcode.cn/problems/non-decreasing-subsequences/
去重方法:由于不能排序,所以这里的树层去重和之前有所差别
之前的题目基本使用used来记录事都使用过某一元素,因为是有序的,只需判断used[i-1] ,nums[i-1]===nums[i]
现在得到题目是无序的,使用set,每次循环判断当前数字是否在set中,如果在,跳过,否则将数字放入set,实现去重。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| var findSubsequences = function(nums) { let path = [] let res = [] const backTraking = (nums,startIndex)=>{ if (path.length > 1){ res.push([...path]) } if (startIndex >= nums.length){ return } let set = new Set() for(let i=startIndex;i<nums.length;i++){ if (nums[i] < path[path.length-1] || !path ){ continue } if(set.has(nums[i])){ continue } else{ set.add(nums[i]) } path.push(nums[i]) backTraking(nums,i+1) path.pop() } } backTraking(nums,0) return res };
|
全排列
https://leetcode.cn/problems/permutations/description/
首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var permute = function(nums) { let path =[] let res = [] 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 } used[i] = true path.push(nums[i]) backTracking(nums) path.pop() used[i] = false } } backTracking(nums) return res };
|
全排列II
https://leetcode.cn/problems/permutations-ii/description/
加一个数层去重
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| var permuteUnique = function(nums) { let path = [] let res = [] let used = new Array(nums.length).fill(false) let used1 = new Array(nums.length).fill(false) nums.sort((a,b)=>a-b) 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] === nums[i-1] && !used1[i-1]){ continue } used[i] = true used1[i] = true path.push(nums[i]) backTracking(nums) path.pop() used[i] = false used1[i] = false } } backTracking(nums) return res };
|
重新安排行程
https://leetcode.cn/problems/reconstruct-itinerary/description/
N皇后
https://leetcode.cn/problems/n-queens/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| var solveNQueens = function(n) { let res = [] let checkboard = Array.from({length:n},()=>Array.from({length:n},()=>'.')) const isVaild = (row,col,checkboard,n)=>{ for(let i=0;i<row;i++){ if(checkboard[i][col] === 'Q'){ return false } } for(let i= row-1,j=col-1;i>=0 && j>=0;i--,j--){ if(checkboard[i][j] === 'Q'){ return false } } for(let i=row-1,j=col+1;i>=0 && j<n;i--,j++){ if(checkboard[i][j] === 'Q'){ return false } } return true }
const backTraking = (checkboard,n,row)=>{ if(row==n){ res.push(checkboard.map(row=>row.join(''))) return } for (let col=0;col<n;col++){ if (isVaild(row,col,checkboard,n)){ checkboard[row][col] = 'Q' backTraking(checkboard,n,row+1) checkboard[row][col] = '.' } } } backTraking(checkboard,n,0) return res };
|
解数独
https://leetcode.cn/problems/sudoku-solver/
https://www.programmercarl.com/0037.%E8%A7%A3%E6%95%B0%E7%8B%AC.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| 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 val = 1; val <= 9; val++) { if (isValid(i, j, `${val}`, board)) { board[i][j] = `${val}` if (backTracking()) { return true }
board[i][j] = `.` } } return false } } return true } backTracking(board) return board };
|