回溯算法
回溯算法基本理论
是什么:
回溯采用是错得方法解决问题,一旦发现当前步骤失败,回溯算法就会返回上一个步骤
解决哪些问题:
组合问题(组合不强调顺序,而排列强调)
切割问题
子集问题
排列问题
棋盘问题
如何理解回溯法
回溯法都可以抽象为一个树形结构。一个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 };