回溯算法
1.组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
- 注意点:
path
是一个可变的列表对象,直接加入res
时,res
中的元素将共享相同的path
引用。也就是说,当path
发生变化时,res
中的对应元素也会发生变化,导致结果不正确。- 解决这个问题的方法是,每次将
path
添加到res
时,创建一个新的列表副本。
class Solution {
private List<Integer> path = new ArrayList<>();
private List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTracking(n, k, 1);
return res;
}
public void backTracking(int n, int k, int startIndex) {
if (path.size() == k) {
res.add(new ArrayList<>(path)); // 创建 path 的副本加入结果
return;
}
for (int i = startIndex; i <= n; i++) {
path.add(i);
backTracking(n, k, i + 1);
path.remove(path.size() - 1);
}
}
}
- 剪枝优化
接下来看一下优化过程如下:
- 已经选择的元素个数:path.size();
- 所需需要的元素个数为: k - path.size();
- 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())
- 在集合n中至多要从该起始位置 : i <= 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]。
-
但是从3 或者 4 开始就会和前面的重复了,所以可以剪掉
-
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
path.push_back(i); // 处理节点
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤销处理的节点
}
###2.组合总和III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
- 剪枝道理同上
class Solution {
private List<Integer>path = new ArrayList();
private List<List<Integer>>res = new ArrayList();
public List<List<Integer>> combinationSum3(int k, int n) {
backTracking(k,n,0,1);
return res;
}
public void backTracking(int k,int n,int sum,int startIndex){
if(path.size()==k){
if(sum==n){
res.add(new ArrayList<>(path));
}
return;
}
for(int i=startIndex;i<=9-(k-path.size())+1;i++){
sum+=i;
path.add(i);
backTracking(k,n,sum,i+1);
sum-=i;
path.remove(path.size()-1);
}
}
}
3.电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
- 输入:“23”
- 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
class Solution {
//设置全局列表存储最后的结果
List<String> list = new ArrayList<>();
StringBuilder sb = new StringBuilder();
public List<String> letterCombinations(String digits) {
if(digits==null||digits.length()==0){
return list;
}
String[] numString={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
backTracking(digits,numString,0);
return list;
}
public void backTracking(String digits,String[] numString,int num){
if(num==digits.length()){
list.add(sb.toString());
return;
}
//获取当前数字代表的字母组合
String now = numString[digits.charAt(num)-'0']; //这个才是待遍历的列表 在这里卡了一下
for(int i=0;i<now.length();i++){
sb.append(now.charAt(i));
backTracking(digits,numString,num+1);
sb.deleteCharAt(sb.length()-1);
}
}
}
4.组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例 1:
- 输入:candidates = [2,3,6,7], target = 7,
- 所求解集为: [ [7], [2,2,3] ]
示例 2:
- 输入:candidates = [2,3,5], target = 8,
- 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
对于组合问题:
如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合(opens new window)
class Solution {
private List<Integer>path = new ArrayList<>();
private List<List<Integer>>res = new ArrayList<>();
public void backTracking(int[] candidates,int target,int sum,int startIndex){
if(sum>target){
return;
}
if(sum==target){
res.add(new ArrayList<>(path));
return;
}
for(int i=startIndex;i<candidates.length;i++){
sum+=candidates[i];
path.add(candidates[i]);
backTracking(candidates,target,sum,i); //可以选重复的 就体现在这里的i不用+1
path.remove(path.size()-1);
sum-=candidates[i];
}
}
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracking(candidates,target,0,0);
return res;
}
}
5.组合总和
给定一个数组 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]
]
这道题目和上题有如下区别:
- 本题candidates 中的每个数字在每个组合中只能使用一次。
- 本题数组candidates的元素是有重复的,而上题是无重复元素的数组candidates
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)
强调一下,树层去重的话,需要对数组排序!
用used数组实现去重
如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
此时for循环里就应该做continue的操作。
为什么 used[i - 1] == false 就是同一树层呢,因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。
而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上。
class Solution {
List<List<Integer>>result = new ArrayList<>();
List<Integer>path=new ArrayList<>();
boolean[] used ;
public void backTracking(int[] candidates,int target,int sum,int startIndex){
if(sum==target){
result.add(new ArrayList<>(path));
}
for(int i=startIndex;i<candidates.length;i++ ){
if (sum + candidates[i] > target) {
break;
}
if(i>0&&candidates[i]==candidates[i-1]&&!used[i-1]){
continue;
}else{
used[i]=true;
path.add(candidates[i]);
backTracking(candidates,target,sum+candidates[i],i+1); //注意每个节点仅能选一次
used[i]=false;
path.remove(path.size()-1);
}
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
used = new boolean[candidates.length];
Arrays.fill(used,false);
Arrays.sort(candidates);
backTracking(candidates,target,0,0);
return result;
}
}
6.分割回文串
class Solution {
private List<List<String>> result = new ArrayList<>();
private List<String> path = new ArrayList<>();
public List<List<String>> partition(String s) {
backtracking(s,0,new StringBuilder());//注意这里第三个参数传的是stringBuilder
return result;
}
private void backtracking(String s, int startIndex, StringBuilder sb){
if(startIndex==s.length()){
result.add(new ArrayList<>(path));
return;
}
for(int i=startIndex;i<s.length();i++){
sb.append(s.charAt(i));
if(check(sb)){
path.add(sb.toString());
backtracking(s,i+1,new StringBuilder()); //如果前面有回文串 后面就要重新分割 所以传入new sb对象
path.remove(path.size()-1);
}
}
}
private boolean check(StringBuilder sb){
for (int i = 0; i < sb.length()/ 2; i++){
if (sb.charAt(i) != sb.charAt(sb.length() - 1 - i)){return false;}
}
return true;
}
}
7.复原IP地址
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效的 IP 地址。
- 解题方法和上一题相似
class Solution {
private List<String> res = new ArrayList();
public List<String> restoreIpAddresses(String s) {
backTracking(0,s,0,new StringBuilder());
return res;
}
public void backTracking(int startIndex,String s,int num,StringBuilder sb){
//这里切割了4段 并且用完了所有字符
if(num==4&&startIndex==s.length()){
res.add(sb.toString());
return;
}
if(num>=4) return;
for(int i=startIndex;i<s.length()&&i<startIndex+3;i++){
String sub = s.substring(startIndex,i+1);
if(check(sub)){
int len = sb.length();//记录当前长度
if(num>0) sb.append(".");//只有不是一段才加'.'
sb.append(sub);
backTracking(i+1,s,num+1,sb);
sb.setLength(len);//回溯
}
}
}
public boolean check(String s){
if(s.length()>1&&s.charAt(0)=='0'){
return false;
}
int num =Integer.parseInt(s);
return num>=0 && num<=255;
}
}
8.子集问题
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<List<Integer>> result = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
private void backtracking(int[] nums, int startIndex) {
result.add(new ArrayList<>(path)); // 先加入当前子集
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]); // 添加当前元素
backtracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums, 0);
return result;
}
}
9.子集2
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
- 输入: [1,2,2]
- 输出: [ [2], [1], [1,2,2], [2,2], [1,2], []
这道题目和上一题的区别就是集合里有重复元素了,而且求取的子集要去重。
那么关于回溯算法中的去重问题,在组合总和II 中已经详细讲解过了,和本题是一个套路。
用示例中的[1, 2, 2] 来举例,如图所示: (注意去重需要先对集合排序)
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer>path = new ArrayList<>();
boolean[] used;
public void backTrack(int[] nums,int startIndex){
res.add(new ArrayList<>(path)); //先添加 注意顺序不能反
if (startIndex >= nums.length){ //如果发现越界了就返回
return;
}
for(int i=startIndex;i<nums.length;i++){
if(i>0&&nums[i]==nums[i-1]&&!used[i-1]){
continue;
}
path.add(nums[i]);
used[i]=true;
backTrack(nums,i+1);
used[i]=false;
path.remove(path.size()-1);
}
}
public List<List<Integer>> subsetsWithDup(int[] nums) {
if(nums.length==0){
return res;
}
used = new boolean[nums.length];
Arrays.sort(nums);
backTrack(nums,0);
return res;
}
}
10.递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
- 输入: [4, 6, 7, 7]
- 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
- 给定数组的长度不会超过15。
- 数组中的整数范围是 [-100,100]。
- 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。
在90.子集II (opens new window)中我们是通过排序,再加一个标记数组来达到去重的目的。
而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。
所以不能使用之前的去重逻辑!
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public void backTrack(int[] nums,int startIndex){
if(path.size()>1) res.add(new ArrayList<>(path)); //至少要有两个
Set<Integer> uset = new HashSet<>();//到了新的一层就要新的set
for(int i= startIndex;i<nums.length;i++){
if((!path.isEmpty()&&nums[i]<path.get(path.size()-1))||uset.contains(nums[i])){
continue;//如果不符合递增 就去遍历该层的下一个
}
path.add(nums[i]);
uset.add(nums[i]);
backTrack(nums,i+1);
path.remove(path.size()-1);
}
}
public List<List<Integer>> findSubsequences(int[] nums) {
backTrack(nums,0);
return res;
}
}
11.全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
- 输入: [1,2,3]
- 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
-
递归参数
- 可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
- 但排列问题需要一个used数组,标记已经选择的元素
-
递归终止条件
- 叶子节点
-
单层搜索逻辑
- 不用startIndex,用used数组
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] used;
public void backTrack(int[] nums){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
if(!used[i]){
path.add(nums[i]);
used[i]=true;
backTrack(nums);
used[i]=false;
path.remove(path.size()-1);
}
}
}
public List<List<Integer>> permute(int[] nums) {
if(nums.length==0){
return res;
}
used = new boolean[nums.length];
backTrack(nums);
return res;
}
}
12.全排列2
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
- 输入:nums = [1,1,2]
- 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
- 输入:nums = [1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
boolean[] used;
public void backTrack(int[] nums){
if(path.size()==nums.length){
res.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
//主要是检查同一树层
if(i>0&&!used[i-1]&&nums[i]==nums[i-1]){
continue;
}
//主要检查同一树枝
if(!used[i]){
path.add(nums[i]);
used[i]=true;
backTrack(nums);
path.remove(path.size()-1);
used[i]=false;
}
}
}
public List<List<Integer>> permuteUnique(int[] nums) {
if(nums.length==0) return res;
used = new boolean[nums.length];
Arrays.sort(nums);
backTrack(nums);
return res;
}
}
13.重新安排行程(hard 以后再做)
给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。
提示:
- 如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前
- 所有的机场都用三个大写字母表示(机场代码)。
- 假定所有机票至少存在一种合理的行程。
- 所有的机票必须都用一次 且 只能用一次。
示例 1:
- 输入:[[“MUC”, “LHR”], [“JFK”, “MUC”], [“SFO”, “SJC”], [“LHR”, “SFO”]]
- 输出:[“JFK”, “MUC”, “LHR”, “SFO”, “SJC”]
示例 2:
- 输入:[[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]]
- 输出:[“JFK”,“ATL”,“JFK”,“SFO”,“ATL”,“SFO”]
- 解释:另一种有效的行程是 [“JFK”,“SFO”,“ATL”,“JFK”,“ATL”,“SFO”]。但是它自然排序更大更靠后。
14.N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
- 输入:n = 4
- 输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
- 解释:如上图所示,4 皇后问题存在两个不同的解法。
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(n, 0, chessboard);
return res;
}
public void backTrack(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(charToString(chessboard));
return;
}
for (int col = 0; col < n; col++) {
if (check(chessboard, row, col, n)) {
chessboard[row][col] = 'Q';
backTrack(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯撤销
}
}
}
public List<String> charToString(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(new String(c)); // 也可以用 String.copyValueOf(c)
}
return list;
}
public boolean check(char[][] chessboard, int row, int col, int n) {
// 检查当前列是否有皇后
for (int i = 0; i < row; i++) { // 只需检查前面的行
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查 45° 左上对角线
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查 135° 右上对角线
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
###15.解数独(hard 以后再做)