回溯法
- 子集树 与 排序树
对于n = 3
的 0-1 背包问题,可以用一棵完全二叉树表示解空间 当所给的问题是从 n 个元素中找出满足某种性质的子集时,相应的解空间树称为子集树
,这类子集树通常有个 2 n 2^n 2n 叶节点,其节点总数为 2 n + 1 − 1 2^{n+1} - 1 2n+1−1 ,遍历子集树的任何算法均需要 Ω ( 2 n ) \Omega(2^n) Ω(2n) 的计算时间
对于旅行售货员的解空间树为
当所给的问题是确定 n n n 个元素满足某种性质的排列时,相应的解空间树称为排列树
,通常有个 2 ! 2! 2! 叶节点,其节点总数为 2 n + 1 − 1 2^{n+1} - 1 2n+1−1 ,遍历子集树的任何算法均需要 Ω ( n ! ) \Omega(n!) Ω(n!) 的计算时间 - 子集树模板
用回溯法搜索子集树的一般算法:
void backtrack(int t) {
if (t > n) Output(x);
else
for (int i = 0; i <= 1; i++) { // 0 或 1 选或不选
x[t] = i;
if (Constraint(t) && Bound(t)) backtrack(t + 1);
// Constraint() 约束函数 Bound() 限界函数
}
}
- 排序树模板
用回溯法搜索排列树的一般算法:
void backtrack(int t) {
if (t > n) Output(x);
else
for (int i = t; i <= n; i++) {
swap(x[t],x[i]); //交换
if (Constraint(t) && Bound(t)) backtrack(t + 1);
swap(x[t],x[i]);
}
}
如何解决重复问题,常用有两种方法,使用 used
和使用 begin
什么时候使用 used 数组,什么时候使用 begin 变量
排列问题
,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used
数组;
组合问题
,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin
变量。
题 78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
My Answer:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> x = new ArrayList<Integer>();
public List<List<Integer>> subsets(int[] nums) {
backtrack(1,nums);
return result;
}
public void backtrack(int t, int[] nums){
//最后一层退出条件
if(t > nums.length){
result.add(new ArrayList(x));
}else{
//取
x.add(nums[t-1]);
backtrack(t+1,nums);
//不取
x.remove(x.size()-1);
backtrack(t+1,nums);
}
}
}
题 90. 子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
My Answer:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> x = new ArrayList<Integer>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtrack(1,nums,true);
return result;
}
//如果与前一个相同,并且前面一个没有取,则不能跳过上一个数,即不取
public void backtrack(int t, int[] nums,boolean choose){
//最后一层退出条件
if(t > nums.length){
result.add(new ArrayList(x));
}else{
if(t != 1 && nums[t-1] == nums[t-2] && !choose){
backtrack(t+1,nums,false);
}else{
//取
x.add(nums[t-1]);
backtrack(t+1,nums,true);
//不取
x.remove(x.size()-1);
backtrack(t+1,nums,false);
}
}
}
}
题 46.全排序
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
My Answer:
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> x = new ArrayList<>();
int n;
public List<List<Integer>> permute(int[] nums) {
for(int i = 0; i < nums.length; i++){
x.add(nums[i]);
}
n = nums.length;
backtrack(0);
return result;
}
public void backtrack(int t){
if(t == n){
result.add(new ArrayList<>(x));
}else{
for(int i = t; i < n; i++){
swap(x,t,i);
backtrack(t+1);
swap(x,t,i);
}
}
}
public void swap(List<Integer> x, int t, int i){
int temp = x.get(t);
x.set(t,x.get(i));
x.set(i,temp);
}
}
题 47.全排序II
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
My Answer:
class Solution {
Set<List<Integer>> result = new HashSet<>();
List<Integer> x = new ArrayList<>();
int n;
public List<List<Integer>> permuteUnique(int[] nums) {
for(int i = 0; i < nums.length; i++){
x.add(nums[i]);
}
n = nums.length;
backtrack(0);
return new ArrayList<>(result);
}
public void backtrack(int t){
if(t == n){
result.add(new ArrayList<>(x));
}else{
for(int i = t; i < n; i++){
swap(x,t,i);
backtrack(t+1);
swap(x,t,i);
}
}
}
public void swap(List<Integer> x, int t, int i){
int temp = x.get(t);
x.set(t,x.get(i));
x.set(i,temp);
}
}
题 51. N皇后
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
My Answer:
import java.util.ArrayList;
import java.util.List;
class Solution {
List<List<Integer>> resultInt = new ArrayList<>();
List<Integer> x;
int n;
public List<List<String>> solveNQueens(int n) {
this.n = n;
this.x = new ArrayList<>(n);
backtrack(0);
return get(resultInt);
}
public void backtrack(int t){
if(t == n){
resultInt.add(new ArrayList<>(x));
}else{
for(int i = 0; i < n; i++){
x.add(i);
if(place(t)) backtrack(t+1);
x.remove(x.size()-1);
}
}
}
public boolean place(int t){
for(int i = 0; i < t; i++){
if((Math.abs(t-i) == Math.abs(x.get(i) - x.get(t))) || (x.get(i) == x.get(t))) return false;
}
return true;
}
public List<List<String>> get(List<List<Integer>> res){
List<List<String>> result = new ArrayList<>();
List<String> list;
StringBuilder sb = new StringBuilder();
for(int i = 0; i < n; i++){
sb.append(".");
}
String str = sb.toString();
for(List<Integer> x : res){
list = new ArrayList<>();
for(Integer y : x){
StringBuilder builder = new StringBuilder(str);
builder.setCharAt(y,'Q');
list.add(builder.toString());
}
result.add(list);
}
return result;
}
}
题 784. 字母大小写全排列
给定一个字符串 S
,通过将字符串 S
中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。
示例:
输入: S = "a1b2"
输出: ["a1b2", "a1B2", "A1b2", "A1B2"]
输入: S = "3z4"
输出: ["3z4", "3Z4"]
输入: S = "12345"
输出: ["12345"]
注意:
S
的长度不超过12。S
仅由数字和字母组成。
My Answer:
class Solution {
List<String> result = new ArrayList<>();
public List<String> letterCasePermutation(String S) {
char[] str = S.toCharArray();
backtrack(0, str);
return result;
}
public void backtrack(int t, char[] str) {
if (t == str.length) {
result.add(new String(str));
} else {
//不转
backtrack(t + 1, str);
//转
char s = str[t];
if (!Character.isLetter(s)) {
return;
}
s ^= (1 << 5);
str[t] = s;
backtrack(t + 1, str);
}
}
}
2018-08-24 更新
题 Alibaba 在线编程. 物流派送员送快递最短路径问题
如下图,某物流派送员p,需要给 a、b、c、d. 4个快递点派送包裹,请问派送员需要选择什么样的路线,才能完成最短路程的派送。假设如图派送员的起点坐标(0,0),派送路线只能沿着图中的方格边行驶,每个小格都是正方形,且边长为1,如p到d的距离就是4。随机输入n个派送点坐标,求输出最短派送路线值。
(从起点开始完成n个点派送并回到起始点的距离)
输入示例:
4 2,2 2,4 4,4 7,2
输出:
30
My Answer:
package leetcode;
import java.util.Scanner;
public class Main {
private static final Point start = new Point(0,0);
private static Integer res = Integer.MAX_VALUE;
private static class Point{
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getDistanceTo(Point p) {
Integer pathX = Math.abs(p.x - x);
Integer pathY = Math.abs(p.y - y);
return pathX + pathY;
}
}
public static Integer backtrack(int t, Point[] points) {
int n = points.length;
if (t == n) {
int sum = start.getDistanceTo(points[0]);
for (int i = 1; i < n; i++) {
sum += points[i-1].getDistanceTo(points[i]);
}
sum += points[n-1].getDistanceTo(start);
res = Math.min(res, sum);
} else {
for (int i = t; i < n; i++) {
swap(points, t, i);
backtrack(t + 1, points);
swap(points, t, i);
}
}
return res;
}
private static void swap(Point[] points, int i, int j) {
if (i == j) return;
Point temp = points[i];
points[i] = points[j];
points[j] = temp;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
//read
int n = Integer.parseInt(sc.nextLine());
Point[] points = new Point[n];
for (int i = 0; i < n; i++) {
String line = sc.nextLine();
String[] xy = line.split(",");
//Ps:这是阿里在线编程题,我说怎么做不对,下面 new Point() 的后边分割符下标都写成0了,org
points[i] = new Point(Integer.parseInt(xy[0]), Integer.parseInt(xy[1]));
}
backtrack(0, points);
System.out.println(res);
}
}
题 401. 二进制手表
二进制手表顶部有 4 个 LED 代表小时(0-11),底部的 6 个 LED 代表分钟(0-59)。
每个 LED 代表一个 0 或 1,最低位在右侧。
[外链图片转存中…(img-INfkoCqZ-1721934484219)]
例如,上面的二进制手表读取 “3:25”。
给定一个非负整数 n 代表当前 LED 亮着的数量,返回所有可能的时间。
输入: n = 1
返回: ["1:00", "2:00", "4:00", "8:00", "0:01", "0:02", "0:04", "0:08", "0:16", "0:32"]
注意事项:
- 输出的顺序没有要求。
- 小时不会以零开头,比如 “01:00” 是不允许的,应为 “1:00”。
- 分钟必须由两位数组成,可能会以零开头,比如 “10:2” 是无效的,应为 “10:02”。
My Answer:
public class Solution {
private List<String> res = new ArrayList<>();
private int n = 10;
private int[] time = new int[10];
public List<String> readBinaryWatch(int num) {
backtrack(0,num);
return res;
}
public void backtrack(int t, int num) {
if(t == n) {
if (num == 0) output();
return;
}
if (num > 0) {
time[t] = 1;
num--;
backtrack(t + 1,num);
time[t] = 0;
num++;
backtrack(t + 1,num);
}else {
backtrack(n,num);
}
}
public void output() {
StringBuilder stringBuilder = new StringBuilder();
int hour = 8 * time[0] + 4 * time[1] + 2 * time[2] + time[3];
if (hour >= 12) return;
stringBuilder.append(hour + ":");
int minute = 32 * time[4] + 16 * time[5] + 8 * time[6] + 4 * time[7] + 2 * time[8] + time[9];
if (minute > 59) return;
if (minute < 10) stringBuilder.append("0" + minute);
else stringBuilder.append(minute);
String t = stringBuilder.toString();
res.add(t);
}
}
题 22. 括号生成
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
My Answer:
class Solution {
private static ArrayList<String> res = new ArrayList<>();
// 基于左括号数时刻大于等于右括号数
public List<String> generateParenthesis(int n) {
backtrack(0,0,0,"",2 * n);
return res;
}
public void backtrack(int t, int left, int right, String s, int n) {
if (t == n) {
if (left == right) res.add(s);
return;
}
if (left < right) return;
if (left >= 0) backtrack(t + 1, left + 1, right, s + "(", n);
if (left > right) backtrack(t + 1, left, right + 1, s + ")", n);
}
}