We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
回溯算法,也叫试探法,通过遍历将每一种可能都走一遍,一旦遇到不符合结果的,就会回溯到上一层,再找另一子节点,以此类推,直到找到符合结果为止。
在生活里面,最常见莫过于迷宫找出路了,在一个分叉路口,当一条路发现不通后,就要回到分叉路口,接着找下一条路,以此类推。
可以看到,回溯算法可以说是穷举法,即把每一种可能都得查询一次,时间复杂度贼高。因此回溯算法常常需要结合剪枝来进行优化。
所谓剪枝,就是在遍历过程中将那些不符合结果的排除掉,具体看下面栗子就会很容易理解。
回溯算法代码段
从代码层面来看,回溯算法基本都是一个套路,先来看下:
const result = []; const backTrack = (index, targetArr, tempArr) => { if (结束条件) { result.push([...tempArr]); return; } for (let i = 0; i < targetArr.length; i++) { // 剪枝行为 // 选择当前元素 tempArr.push(targetArr[i]); backTrack(i, targetArr, tempArr); // 撤销当前元素 tempArr.pop(); } }
回溯算法的题目大体上可以分为三类:子集、组合和全排列。只要解决这三类问题,其实回溯算法就基本不是问题了。
子集 I 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集。 说明:结果里面不能包含重复的子集。 例如:nums = [1, 2, 3],输出为:[1], [1, 2], [1, 3], [2], [2, 3], [3], [1, 2, 3], []
子集 I
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集。
说明:结果里面不能包含重复的子集。
例如:nums = [1, 2, 3],输出为:[1], [1, 2], [1, 3], [2], [2, 3], [3], [1, 2, 3], []
现在我们就来套用上面代码段。
const getChildSet = nums => { if (!nums || !Array.isArray(nums)) return null; const result = []; const backTrack = (index, targetArr, tempArr) => { result.push([...tempArr]); for (let i = 0; i < targetArr.length; i++) { tempArr.push(targetArr[i]); backTrack(i + 1, targetArr, tempArr); tempArr.pop(); } } backTrack(0, nums, []); return result; }
套用起来确实简单,但是当你去执行上面代码时,会发现栈溢出。
好明显,我们还没有做剪枝,接下来我们就来做剪枝操作。
const getChildSet = nums => { if (!nums || !Array.isArray(nums)) return null; const result = []; const backTrack = (index, targetArr, tempArr) => { result.push([...tempArr]); for (let i = index; i < targetArr.length; i++) { tempArr.push(targetArr[i]); backTrack(i + 1, targetArr, tempArr); tempArr.pop(); } } backTrack(0, nums, []); return result; }
只需要将 i 从 index 开始遍历即可,而前面遍历过的元素可以直接跳过,因为没必要再遍历。
子集 II 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集。 说明:结果不能包含重复的子集。 例如:nums = [1, 2, 2],结果为:[1], [1, 2], [1, 2, 2], [2], [2, 2], []
子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集。
说明:结果不能包含重复的子集。
例如:nums = [1, 2, 2],结果为:[1], [1, 2], [1, 2, 2], [2], [2, 2], []
乍一看,跟上面意思是一样,不同点就在于对于 nums 中重复元素就不要再计算了,不然得到的结果是一样的。
const getChildSet = nums => { if (!nums || !Array.isArray(nums)) return null; nums.sort((a, b) => a - b); const result = []; const backTrack = (index, targetArr, tempArr) => { result.push([...tempArr]); for (let i = index; i < targetArr.length; i++) { if (i > index && targetArr[i] === targetArr[i - 1]) continue; tempArr.push(targetArr[i]); backTrack(i + 1, targetArr, tempArr); tempArr.pop(); } } backTrack(0, nums, []); return result; }
可以看到,比子集 I 多了两个,一个是先对数组进行排序,另一个则是判断元素是否已经遍历过,直接跳过。换句话说,排序就是为了让判断前后元素是否同一个,从而避免重复遍历。
而这也是剪枝,因此剪枝要根据实际情况进行剪。
组合总和 I 给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。 说明: 所有数字(包括 target)都是正整数。 结果不能包含重复的组合。 例如:candidates = [2, 3, 6, 7], target = 7, 输出为:[7], [2, 2, 3]
组合总和 I
给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
例如:candidates = [2, 3, 6, 7], target = 7, 输出为:[7], [2, 2, 3]
如果顺利理解上面子集问题后,那么组合问题就会变得很简单了,我接着套用代码段。
const getComposeSum = (candidates, target) => { if (!candidates || !Array.isArray(candidates)) return null; const result = []; const backTrack = (index, targetArr, tempArr, target) => { if (target === 0) { result.push([...tempArr]); return; } if (target < 0) return; for (let i = index; i < targetArr.length; i++) { tempArr.push(targetArr[i]); target -= targetArr[i]; backTrack(i, targetArr, tempArr, target); target += targetArr[i]; tempArr.pop(); } } backTrack(0, candidates, [], target); return result; }
组合总和 II 给定一个数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中每个数字再每个组合中只能使用一次。 说明: 所有数字(包括 target)都是正整数。 结果不能包含重复的组合。 例如:candidates = [10, 1, 2, 7, 6, 1, 5], target = 8,输出为:[1, 7], [1, 2, 5], [2, 6], [1, 1, 6]
组合总和 II
给定一个数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中每个数字再每个组合中只能使用一次。
例如:candidates = [10, 1, 2, 7, 6, 1, 5], target = 8,输出为:[1, 7], [1, 2, 5], [2, 6], [1, 1, 6]
明显比组合总和 II 中多了 candidates 是允许出现重复元素的,因此我们要剔除那些重复元素的遍历,避免重复遍历,跟子集 II 类似。
const getComposeSum = (candidates, target) => { if (!candidates || !Array.isArray(candidates)) return null; candidates.sort((a, b) => a - b); const result = []; const backTrack = (index, targetArr, tempArr, target) => { if (target === 0) { result.push([...tempArr]); return; } if (target < 0) return; for (let i = index; i < targetArr.length; i++) { if (i > index && targetArr[i] === targetArr[i - 1]) continue; tempArr.push(targetArr[i]); target -= targetArr[i]; backTrack(i + 1, targetArr, tempArr, target); target += targetArr[i]; tempArr.pop(); } } backTrack(0, candidates, [], target); return result; }
全排列 I 给定一个没有重复数字的序列 nums,返回其所有可能的全排列。 例如:nums = [1, 2, 3],输出为:[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]
全排列 I
给定一个没有重复数字的序列 nums,返回其所有可能的全排列。
例如:nums = [1, 2, 3],输出为:[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]
全排列的问题与前面的子集和组合是一个道理,将每一种情况都列举出来。
const getRange = nums => { if (!nums || !Array.isArray(nums)) return null; const result = []; const backTrack = (index, targetArr, tempArr) => { if (tempArr.length === targetArr.length) { result.push([...tempArr]); return; } for (let i = 0; i < targetArr.length; i++) { if (tempArr.indexOf(targetArr[i]) > -1) continue; tempArr.push(targetArr[i]); backTrack(i + 1, targetArr, tempArr); tempArr.pop(); } } backTrack(0, nums, []); return result; }
全排列 II 给定一个可包含重复数字的序列 nums,返回所有不可重复的全排列。 例如:nums = [1, 1, 2], 输出为:[1, 1, 2], [1, 2, 1], [2, 1, 1]。
全排列 II
给定一个可包含重复数字的序列 nums,返回所有不可重复的全排列。
例如:nums = [1, 1, 2], 输出为:[1, 1, 2], [1, 2, 1], [2, 1, 1]。
由于 nums 中存在重复元素,因此使用全排列 I 中的方式是走不通的。
转个角度,我们可以使用另一个数组专门存储已经遍历过的元素,一旦该元素已经遍历过,直接跳过即可。
另外还需要考虑一个问题,对于重复元素,如果前面那个已经有结果了,那么需要去重处理,进而避免结果中出现重复集合。
const getRange = nums => { if (!nums || !Array.isArray(nums)) return null; nums.sort((a, b) => a - b); const result = []; const backTrack = (index, targetArr, tempArr, storage) => { if (tempArr.length === targetArr.length) { result.push([...tempArr]); return; } for (let i = 0; i < targetArr.length; i++) { if (storage[i]) continue; if (i > 0 && targetArr[i] === targetArr[i - 1] && storage[i - 1]) break; tempArr.push(targetArr[i]); storage[i] = targetArr[i]; backTrack(i + 1, targetArr, tempArr, storage); storage[i] = undefined; tempArr.pop(); } } backTrack(0, nums, [], []); return result; }
回溯算法相比前面的动态规划和贪心算法都好理解,基本都是在三大类型题目上---子集、组合、排列。
直接套用代码段,但是需要考虑好剪枝问题,根据实际情况进行剪枝。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
回溯算法,也叫试探法,通过遍历将每一种可能都走一遍,一旦遇到不符合结果的,就会回溯到上一层,再找另一子节点,以此类推,直到找到符合结果为止。
在生活里面,最常见莫过于迷宫找出路了,在一个分叉路口,当一条路发现不通后,就要回到分叉路口,接着找下一条路,以此类推。
可以看到,回溯算法可以说是穷举法,即把每一种可能都得查询一次,时间复杂度贼高。因此回溯算法常常需要结合剪枝来进行优化。
所谓剪枝,就是在遍历过程中将那些不符合结果的排除掉,具体看下面栗子就会很容易理解。
从代码层面来看,回溯算法基本都是一个套路,先来看下:
回溯算法的题目大体上可以分为三类:子集、组合和全排列。只要解决这三类问题,其实回溯算法就基本不是问题了。
现在我们就来套用上面代码段。
套用起来确实简单,但是当你去执行上面代码时,会发现栈溢出。
好明显,我们还没有做剪枝,接下来我们就来做剪枝操作。
只需要将 i 从 index 开始遍历即可,而前面遍历过的元素可以直接跳过,因为没必要再遍历。
乍一看,跟上面意思是一样,不同点就在于对于 nums 中重复元素就不要再计算了,不然得到的结果是一样的。
可以看到,比子集 I 多了两个,一个是先对数组进行排序,另一个则是判断元素是否已经遍历过,直接跳过。换句话说,排序就是为了让判断前后元素是否同一个,从而避免重复遍历。
而这也是剪枝,因此剪枝要根据实际情况进行剪。
如果顺利理解上面子集问题后,那么组合问题就会变得很简单了,我接着套用代码段。
明显比组合总和 II 中多了 candidates 是允许出现重复元素的,因此我们要剔除那些重复元素的遍历,避免重复遍历,跟子集 II 类似。
全排列的问题与前面的子集和组合是一个道理,将每一种情况都列举出来。
由于 nums 中存在重复元素,因此使用全排列 I 中的方式是走不通的。
转个角度,我们可以使用另一个数组专门存储已经遍历过的元素,一旦该元素已经遍历过,直接跳过即可。
另外还需要考虑一个问题,对于重复元素,如果前面那个已经有结果了,那么需要去重处理,进而避免结果中出现重复集合。
回溯算法相比前面的动态规划和贪心算法都好理解,基本都是在三大类型题目上---子集、组合、排列。
直接套用代码段,但是需要考虑好剪枝问题,根据实际情况进行剪枝。
The text was updated successfully, but these errors were encountered: