有趣的BackTracking回溯算法

最近无意中看了一些需要使用到「回溯算法」的实例,想起了读书时候的几种类型的提名,时隔多年,做个简单的回顾;

1.二叉树的遍历

public class TreeSearchDemo {

public static void main(String[] args) {
new TreeSearchDemo().testBinTree1();
}

public void testBinTree1() {
TreeNode root = makeTestTree();
List<TreeNode> result = new ArrayList<>();
preOrder(root, result);
//inOrder(root, result);
//postOrder(root, result);
for (TreeNode item : result) {
System.out.print(item.data);
}
System.out.println();
}

/**
* 前序遍历
*
* @param root
* @param result
*/
public void preOrder(TreeNode root, List<TreeNode> result) {
if (root == null) {
return;
}
result.add(root);
preOrder(root.left, result);
preOrder(root.right, result);
}

/**
* 中序遍历
*
* @param root
* @param result
*/
public void inOrder(TreeNode root, List<TreeNode> result) {
if (root == null) {
return;
}
inOrder(root.left, result);
result.add(root);
inOrder(root.right, result);
}

/**
* 后续遍历
*
* @param root
* @param result
*/
public void postOrder(TreeNode root, List<TreeNode> result) {
if (root == null) {
return;
}
postOrder(root.left, result);
result.add(root);
postOrder(root.right, result);
}

public static class TreeNode {
TreeNode left;
TreeNode right;
String data;
}

private TreeNode makeTestTree() {
TreeNode root = new TreeNode();
root.data = "A";
TreeNode nodeB = new TreeNode();
nodeB.data = "B";
TreeNode nodeC = new TreeNode();
nodeC.data = "C";
TreeNode nodeD = new TreeNode();
nodeD.data = "D";
TreeNode nodeE = new TreeNode();
nodeE.data = "E";
TreeNode nodeF = new TreeNode();
nodeF.data = "F";
//设置节点的关系
root.left = nodeB;
root.right = nodeC;
nodeB.left = nodeD;
nodeB.right = nodeE;
nodeE.right = nodeF;
return root;
}
}

构建的二叉树,图示详见:​​javascript:void(0)​​

2.二叉树遍历的遍历「路径」

以「先序」遍历为例,我们如何记录遍历的路径呢?

  • 以先序遍历为例
public void testBinTreeTracking() {
//构建测试的二叉树
TreeNode root = makeTestTree();
//保存遍历的结果
List<String> result = new ArrayList<>();
//遍历时搜索的路径
LinkedList<TreeNode> tracking = new LinkedList<>();
//保存所有遍历时搜索的路径
List<LinkedList<TreeNode>> trackingResult = new ArrayList<>();

//先顺遍历先遍历根节点
tracking.add(root);
preOrderWithTracking(root, result, tracking, trackingResult);
//遍历路径
for (LinkedList<TreeNode> trackingItem : trackingResult) {
for (TreeNode node : trackingItem) {
if (node != null) {
System.out.print("->");
System.out.print(node.data);
}
if (node == null) {
System.out.print("->");
System.out.print(" NULL");
}
}
System.out.println();
}
}

/**
* 先序遍历的遍历路径
*
* @param root
* @param result
* @param tracking
* @param trackingResult
*/
public void preOrderWithTracking(TreeNode root, List<String> result, LinkedList<TreeNode> tracking, List<LinkedList<TreeNode>> trackingResult) {

if (root == null) {
trackingResult.add(new LinkedList<>(tracking));
return;
}

result.add(root.data);

tracking.add(root.left);
preOrderWithTracking(root.left, result, tracking, trackingResult);
tracking.removeLast();


tracking.add(root.right);
preOrderWithTracking(root.right, result, tracking, trackingResult);
tracking.removeLast();

}
  • 输出结果
->A->B->D-> NULL
->A->B->D-> NULL
->A->B->E-> NULL
->A->B->E->F-> NULL
->A->B->E->F-> NULL
->A->C-> NULL
->A->C-> NULL

可以看出,之前我们遍历二叉树,以root==null作为判断条件时,所有的搜索路径,这里面对于叶子节点,会有2条重复的搜索结果,主要是由于分别遍历其左右子树,均为null,所有会出现2次搜索结果;

3.二叉树的最大深度

由上面二叉树的遍历,我们把遍历终止时的每一个遍历路径都打印了一遍,因此可以根据路径的size判断出二叉树的深度;

4.多叉树的搜索

  • 文件目录的搜索
/**
* 递归遍历文件下的所有文件=>查找多叉树的叶子节点
*
* @param dir
* @param allFile
*/
public void listAllFile(File dir, List<File> allFile) {
if (dir.isFile()) {
allFile.add(dir);
return;
}
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
listAllFile(children[i], allFile);
}
}
  • 文件搜索时,搜索到叶子节点的所有路径
public void listAllFilesWithTracking(File dir, LinkedList<File> tracking, List<LinkedList<File>> allTracking) {
if (dir.isFile()) {
//遍历到[文件],也就是叶子节点,返回,记录tracking路径
allTracking.add(new LinkedList<>(tracking));
return;
}
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
tracking.add(children[i]);
listAllFilesWithTracking(children[i], tracking, allTracking);
tracking.removeLast();
}
}

5.回溯算法的模板

public void backtracking(选择列表,路径LinkedList tracking,所有路径resultTracking){
if(结束条件){
resultTracking.add(new LinkedList<>(tracking));//保存路径
return;
}
for (选择 in 选择列表){
tracking.add(选择)//做选择,将选择加入到选择列表
backtracking(选择列表,路径LinkedList tracking,所有路径List<LinkedList> resultTracking)
tracking.removeLast()//删除最后一个,撤销选择
}
}

从回溯算法的模板,再回看二叉树的遍历,其实相当于选择列表是二叉树的两个根节点[root.left,root.right],而且选择列表的具体引用是「变化」的;而回溯算法的「选择列表」一般是比较稳定的

6.暴力破解密码问题

小明无意中听到同坐的密码是有[1,2,3,4]4个数字组成,而且密码有6位数字,小明如何枚举所有的密码组成?有排列组合知识我们知道,总共有4的6次方种,那代码实现具体是什么呢?

package com.mochuan.test.bt;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
* 所有密码的组合
*/
public class AllPassword {

private int depth = 6;

public static void main(String[] args) {
new AllPassword().test();
}

public void test() {
int[] word = {1, 2, 3, 4};
LinkedList<Integer> tracking = new LinkedList<>();
List<LinkedList<Integer>> allResult = new ArrayList<>();
backtracking(word, tracking, allResult);
System.out.println("结果总数:" + allResult.size());
for (LinkedList trackingItem : allResult) {
System.out.println(trackingItem);
}
}

public void backtracking(int[] word, LinkedList<Integer> tracking, List<LinkedList<Integer>> allResult) {
//搜索的深度:即密码的长度
if (tracking.size() >= depth) {
allResult.add(new LinkedList<>(tracking));
return;
}
for (int i = 0; i < word.length; i++) {
tracking.add(word[i]);//做选择
backtracking(word, tracking, allResult);
tracking.removeLast();//回溯:撤销选择
}
}
}

从代码运行可见,时间复杂度很高O(N^K),N的K次方;后续的很多的问题,都是以这个为模板,对深度为K的完全N叉树进行搜索;以word={1,2},depth = 3为例,有2^3=8种结果,如下:

结果总数:8
[1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[1, 2, 2]
[2, 1, 1]
[2, 1, 2]
[2, 2, 1]
[2, 2, 2]

如果以word={1,2,3},depth = 3为例,有3^3=27种结果,它的搜索空间为:

【无标题】_数据结构

7.全排列问题

全排列问题,与上述的密码组合问题相比,做了部分的「剪枝」,将搜索的复杂度降低到O(n!),每次的tracking结果,无重复的元素,做一下去重;因此,全排列问题如下:

  • 搜索终止条件
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class PermutationsDemo {

public static void main(String[] args) {
new PermutationsDemo().test();
}

public void test() {
int[] word = {1, 2, 3};
LinkedList<Integer> tracking = new LinkedList<>();
List<LinkedList<Integer>> allResult = new ArrayList<>();
backtracking(word, tracking, allResult);
System.out.println("结果总数:" + allResult.size());
for (LinkedList trackingItem : allResult) {
System.out.println(trackingItem);
}
}

public void backtracking(int[] word, LinkedList<Integer> tracking, List<LinkedList<Integer>> allResult) {
//搜索的深度:即密码的长度
if (tracking.size() == word.length) {
allResult.add(new LinkedList<>(tracking));
return;
}
for (int i = 0; i < word.length; i++) {
if (tracking.contains(word[i])) {
//去除重复元素
continue;
}
tracking.add(word[i]);//做选择
backtracking(word, tracking, allResult);
tracking.removeLast();//回溯:撤销选择
}
}
}

demo的全排列结果

结果总数:6
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]

8.子集问题

一个集合[1,2,3],它有多少个子集?这个也是用到回溯,遍历集合里的所有元素,但

  • 迭代去重1:集合里的元素是不重复的,需要做去重
  • 搜集元素:集合里的元素,没有顺序,需要对不同顺序的进行去重;
  • 终止条件不变
  • 搜集元素的位置和终止条件不同,这块需要注意;
package com.mochuan.test.bt;

import java.util.*;

public class AllSubSet {

public static void main(String[] args) {
new AllSubSet().test();
}

public void test() {
int[] word = {1, 2, 3};
LinkedList<Integer> tracking = new LinkedList<>();
HashMap<String, LinkedList<Integer>> memo = new HashMap<>();
backtracking(word, tracking, memo);
System.out.println("结果总数:" + memo.size());
for (Map.Entry<String, LinkedList<Integer>> trackingItem : memo.entrySet()) {
System.out.println(trackingItem.getValue());
}
}

public String genKey(LinkedList<Integer> tracking) {
if (tracking == null || tracking.size() == 0) {
return "0_0";
}
int sum = 0;
for (int value : tracking) {
sum += value;
}
return String.format("%s_%s", tracking.size(), sum);
}

public void backtracking(int[] word, LinkedList<Integer> tracking, HashMap<String, LinkedList<Integer>> memo) {
//搜集结果
String key = genKey(tracking);
memo.put(key, new LinkedList<>(tracking));
//搜索的深度:即密码的长度
if (tracking.size() == word.length) {
return;
}
for (int i = 0; i < word.length; i++) {
if (tracking.contains(word[i])) {
//去除重复元素
continue;
}
tracking.add(word[i]);//做选择

//去除重复的字串,key按照元素的数量+元素的和作为联合key
String newKey = genKey(tracking);
if (memo.get(newKey) != null) {
tracking.removeLast();
continue;
}
backtracking(word, tracking, memo);
tracking.removeLast();//回溯:撤销选择
}
}
}

仔细体会一下,子集问题是对排量搜索进行条件限制,找到符合想要的结果。demo的运行结果示例:

结果总数:8
[]
[1]
[2]
[3]
[1, 2]
[1, 3]
[2, 3]
[1, 2, 3]

另外一种思路

在深度搜索的时候,选过的元素不再选择,通过start指针来控制每一层的选择范围,每次搜索都从当前元素开始往后搜索,而不是从0开始搜索,则正好构成子集;(通过start指针,将搜索空间进行压缩)

public void subSeqOrSubSet(int start, int[] arr, LinkedList<Integer> tracking, List<LinkedList<Integer>> result) {
result.add(new LinkedList<>(tracking));
for (int i = start; i < arr.length; i++) {
tracking.add(arr[i]);
subSeqOrSubSet(i + 1, arr, tracking, result);
tracking.removeLast();
}
}

9.组合总和

给定⼀个⽆重复元素的正整数数组 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]]

元素可以重复选择,但是选择的顺序不要重复。对上述的密码问题进行这道题目就非常简单了。但需要注意终止条件。

import java.util.*;

public class TargetSum {

public static void main(String[] args) {
new TargetSum().test();
}

private int target = 7;

public void test() {
int[] word = {2, 3, 6, 7};
LinkedList<Integer> tracking = new LinkedList<>();
HashMap<String, LinkedList<Integer>> memo = new HashMap<>();
backtracking(word, target, 0, tracking, memo);
System.out.println("结果总数:" + memo.size());
for (Map.Entry<String, LinkedList<Integer>> trackingItem : memo.entrySet()) {
System.out.println(trackingItem.getValue());
}
}

public String genKey(LinkedList<Integer> tracking) {
if (tracking == null || tracking.size() == 0) {
return "0_0";
}
int sum = 0;
for (int value : tracking) {
sum += value;
}
return String.format("%s_%s", tracking.size(), sum);
}

public void backtracking(int[] word, int target, int sum, LinkedList<Integer> tracking, HashMap<String, LinkedList<Integer>> memo) {
//搜集结果
if (sum == target) {
String key = genKey(tracking);
memo.put(key, new LinkedList<>(tracking));
return;
}
if (sum > target) {
return;
}
for (int i = 0; i < word.length; i++) {
tracking.add(word[i]);//做选择

//去除重复的字串,key按照元素的数量+元素的和作为联合key
String newKey = genKey(tracking);
if (memo.get(newKey) != null) {
tracking.removeLast();
continue;
}
sum += word[i];
backtracking(word, target, sum, tracking, memo);
tracking.removeLast();//回溯:撤销选择
sum -= word[i];
}
}
}

与「子集」的题目很类似,只是不需要对选择的元素做去重,而只需要对trace做去重即可。

10.括号的生成

数字 n 代表⽣成括号的对数,请你设计⼀个函数,⽤于能够⽣成所有可能的并且有效的括号组合。
有效括号组合需满⾜:左括号必须以正确的顺序闭合。

示例:
输⼊:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

再看下回溯算法的模板,我们的word数组,其实是String []word = {“(”,“)”};然后对其进行回溯搜索,在搜索的过程中,终止条件是tracking的size为2n;合法的结果,是2n中有效合法括号的数量。搜索的时间复杂度是O(2^2n)

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ParenthesesDemo {

public static void main(String[] args) {
new ParenthesesDemo().test();
}


public void test() {
String[] word = {"(", ")"};
LinkedList<String> tracking = new LinkedList<>();
List<LinkedList<String>> allResult = new ArrayList<>();
int N = 3;
backtracking(word, N, tracking, allResult);
System.out.println("结果总数:" + allResult.size());
for (LinkedList<String> trackingItem : allResult) {
for (String item : trackingItem) {
System.out.print(item);
}
System.out.println();
}
}

public void backtracking(String[] word, int N, LinkedList<String> tracking, List<LinkedList<String>> allResult) {
//搜索的深度
if (tracking.size() == 2 * N) {
if (isValid(tracking)) {//有条件的搜集结果
allResult.add(new LinkedList<>(tracking));
}
return;
}
for (int i = 0; i < word.length; i++) {
tracking.add(word[i]);//做选择
backtracking(word, N, tracking, allResult);
tracking.removeLast();//回溯:撤销选择
}
}

/**
* 是否是有效括号
*
* @param tracking
* @return
*/
private boolean isValid(LinkedList<String> tracking) {
int sum = 0;
for (String item : tracking) {
if (item.equals("(")) {
sum += 1;
} else {
sum -= 1;
}
if (sum < 0) {
return false;
}
}
return sum == 0;
}
}

运行结果的示例:

结果总数:5
((()))
(()())
(())()
()(())
()()()

但是上述代码的时间复杂度有很大问题,过滤是在终止条件是进行的。时间的复杂度没有降低,这里面其实在迭代过程,可以过滤掉一些无效的搜索的,比如中间状态的tracking,左括号的数量已经过半,或者已经出现右括号的数量大于左括号的数量,最终的结果肯定无法符合条件,这种直接contine,不需要递归了。

public void backtracking(String[] word, int N, int sum, LinkedList<String> tracking, List<LinkedList<String>> allResult) {
//搜索的深度
if (tracking.size() == 2 * N) {
if (isValid(tracking)) {//有条件的搜集结果
allResult.add(new LinkedList<>(tracking));
}
return;
}
for (int i = 0; i < word.length; i++) {
tracking.add(word[i]);//做选择
if (word[i].equals("(")) {
sum += 1;
} else {
sum -= 1;
}
if (sum < 0 || sum > N) {//右括号多,或者左括号过半,已经无法形成最终的结果
tracking.removeLast();//回溯:撤销选择
continue;
}
backtracking(word, N, sum, tracking, allResult);
tracking.removeLast();//回溯:撤销选择
}
}

仍然有优化空间;性能更优秀的解法:

...
LinkedList<String> tracking = new LinkedList<>();
List<LinkedList<String>> allResult = new ArrayList<>();
backtrack(N, N, tracking, allResult);
...

public static void backtrack(int left, int right, LinkedList<String> tracking, List<LinkedList<String>> result) {
if (right < left) {
return;
}
if (left < 0 || right < 0) {
return;
}
if (left == 0 && right == 0) {
result.add(new LinkedList<>(tracking));
return;
}

tracking.add("(");
backtrack(left - 1, right, tracking, result);
tracking.removeLast();

tracking.add(")");
backtrack(left, right - 1, tracking, result);
tracking.removeLast();
}

11.关于0-1背包问题

a.回溯暴力搜索解决

其实从「回溯」的角度去想,可以通过暴力求解去解决,也就是对背包中的物品进行组合。比如,“物品重量分别是{1,3,4},价值分别是{15, 20, 30}。书包的容量为4,书包能够装下的最大价值是多少?”

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
* 0-1背包问题:暴力回溯求解
*/
public class ZeroOneBagDemo {


public static void main(String[] args) {
new ZeroOneBagDemo().test();

}

private int maxValue = 0;
private LinkedList<Integer> maxTrack = new LinkedList<>();

public void test() {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagSize = 4;
LinkedList<Integer> track = new LinkedList<>();
List<LinkedList<Integer>> result = new ArrayList<>();
backtrack(0, weight, value, bagSize, track, result, 0, 0);

System.out.println("最多能装的价值为:" + maxValue);
System.out.println("装入的物品有:");
System.out.println(maxTrack);

System.out.println("回溯过程的所有可能结果:");
for (LinkedList<Integer> item : result) {
System.out.println(item);
}

}

/**
* 暴力回溯搜索,类似「子集」的问题
*
* @param start 控制遍历的复杂度,防止重复遍历一些组合结果
* @param weight 物品的重量
* @param value 物品对应的价值
* @param bagSize 背包的大小
* @param track 回溯的路径,也就是背包的路径
* @param result 所有的路径集合
* @param sumSize 回溯过程中的重量和
* @param sumValue 回溯过程中的价值和
*/
public void backtrack(int start, int[] weight, int[] value, int bagSize, LinkedList<Integer> track, List<LinkedList<Integer>> result, int sumSize, int sumValue) {

if (sumSize <= bagSize) {
//这里搜集各种符合条件的结果
result.add(new LinkedList<>(track));
if (sumValue > maxValue) {
maxValue = sumValue;
maxTrack = new LinkedList<>(track);
}
}

//终止条件
if (track.size() == weight.length || sumSize >= bagSize) {
return;
}

for (int i = start; i < weight.length; i++) {

track.add(weight[i]);
sumValue += value[i];
sumSize += weight[i];

backtrack(i + 1, weight, value, bagSize, track, result, sumSize, sumValue);

sumValue -= value[i];
sumSize -= weight[i];
track.removeLast();
}
}

}

运行的结果为:

最多能装的价值为:35
装入的物品有:
[1, 3]
回溯过程的所有可能结果:
[]
[1]
[1, 3]
[3]
[4]

b.动态规划解决

上述通过回溯算法解题的最大问题是时间复杂度问题。而动态规划则是解决该问题最高效的方式;回顾下题目:“物品重量分别是{1,3,4},价值分别是{15, 20, 30}。书包的容量为4,书包能够装下的最大价值是多少?”。使用动态规划算法,则是完全不同的思路,是通过寻到递推关系进行问题划归为子问题。

1.定义dp数组;并解释清楚dp数据的含义。

首先我们定义DP状态为dp[i][j],它的含义为:任选0-i中的物品,放进容量为j的背包中。

dp[i][j] = Max(dp[i-1][j],dp[i-1][j-w[j]]+v[j])

public class ZeroOneBagDemo {


public static void main(String[] args) {
new ZeroOneBagDemo().maxValueUsingDp();
}

public void maxValueUsingDp() {

final int[] weight = {1, 3, 4};
final int[] value = {15, 20, 30};
final int bagSize = 4;

//申请dp数组
int[][] dp = new int[weight.length][bagSize + 1];

//初始化"列"
for (int i = 0; i < weight.length; i++) {
dp[i][0] = 0;
}

//初始化"行",注意0 ~ 第一个原生的重量,和第一个重量到背包的重量
for (int j = 0; j <= bagSize; j++) {
if (j < weight[0]) {
dp[0][j] = 0;
} else {
dp[0][j] = value[0];
}
}

for (int i = 1; i < weight.length; i++) {//遍历物品
for (int j = weight[0]; j <= bagSize; j++) {//遍历背包容量
if (j < weight[i]) {//背包的容量小于第i个物品的重量,第i个物品不装入
dp[i][j] = dp[i - 1][j];
} else {
//背包的容量:以下两种情况取最大值
// 1.第i个物品不放入到背包中的价值dp[i-1][j]
// 2.第i个物品放入到背包中的价值value[i] 再加上0~i-1物品任取的(j - weight[i])容量的价值
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
System.out.println("最大值为:");
System.out.println(dp[weight.length - 1][bagSize]);
}
}

12.​​零钱兑换​

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。

举例子:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

这个使用回溯算法,是很快可以写出来的,每个硬币可以重复使用。

public class CoinChangeDemo {

private int minCount = Integer.MAX_VALUE / 2;

/**
*
* @param coins
* @param amount
* @param trackSum 拼凑的金额
* @param trackCount 拼凑的次数
*/
private void backtrack(int[] coins, int amount, int trackSum, int trackCount) {
if (trackSum == amount) {
//搜集结果,并更新最小数量
if (trackCount < minCount) {
minCount = trackCount;
}
}
//已经超出总金额,停止搜索
if (trackSum >= amount) {
return;
}

for (int coin : coins) {
if (coin > amount) {
//硬币的数量大于总金额,无需再拼,做剪枝
continue;
}
if (trackCount > minCount) {
//已经大于之前的最优结果;无需再尝试本次搜索;
continue;
}

trackSum += coin;
trackCount++;
backtrack(coins, amount, trackSum, trackCount);
trackCount--;
trackSum -= coin;
}
}

public static void main(String[] args) {
//int[] coins = {1, 2, 5};
//int amount = 11;
//[186,419,83,408]
//6249
//int result = new CoinChangeDemo().coinChangeWithBackTracking(coins, amount);
//System.out.println(result);
}

public int coinChangeWithBackTracking(int[] coins, int amount) {
if (amount == 0) {
return 0;
}
backtrack(coins, amount, 0, 0);
if (minCount == Integer.MAX_VALUE / 2) {
return -1;
}
return minCount;
}

}

但是以上回溯算法最大的问题就是超时,时间复杂度太高。即便做了适当的剪枝,时间复杂度仍然很高。这道题的正确解法是动态规划,先略看下一下:

12.2 动态规划解法

  • 1.定义dp数组dp[i][j],它的含义是使用i种硬币,凑成总金额为j所需的「最少的硬币个数」
  • 2.递推关系:

​dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);​

也就是dp[i-1][j]是使用前i-1种硬币,兑换成金额j的最少硬币个数;和使用i种硬币兑换成j-coins[i-1]的最少硬币个数,再加上新增的硬币1;

  • 3.初始化:dp[0][j]为使用0种硬币,兑换金额j的最少硬币个数。为了递推方便,我们先赋值为最大Int值的一半(防止+1溢出);
public int coinChange(int[] coins, int amount) {
if (amount == 0) {
return 0;
}
//定义dp数组,dp[i][j]的含义是使用i种硬币,凑成总金额为j所需的「最少的硬币个数」
int[][] dp = new int[coins.length + 1][amount + 1];
java.util.Arrays.fill(dp[0], Integer.MAX_VALUE / 2);
dp[1][0] = 0;

for (int i = 1; i <= coins.length; i++) {
for (int j = coins[i - 1]; j <= amount; j++) {
dp[i][j] = dp[i - 1][j];
if (j - coins[i - 1] >= 0) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
}
}
}

//仍然是初始状态,我们按照题目要求返回-1
if (dp[coins.length][amount] == Integer.MAX_VALUE / 2) {
return -1;
}
return dp[coins.length][amount];
}

13.解数独

用1-N个数字,解决N*N的数独问题;横竖不能有重复的数组。

public class SudokuDemo {

public static int resultCount = 0;

public void backtrack(String[][] board, String[] items, int x, int y) {

if (y == board[0].length) {//一行走完了,走下一列
backtrack(board, items, x + 1, 0);
return;
}

if (x == board.length) {//每一列和每一行均走完了,即得到结果
resultCount++;
//搜集结果;最好是copy一份board,demo直接打印了
System.out.println("一组解:");
for (String[] row : board) {
for (String k : row) {
System.out.print(k);
}
System.out.println();
}
return;
}

if (!board[x][y].equals(".")) {//当前元素已有数字,继续向前走
backtrack(board, items, x, y + 1);
return;
}

//对当前坐标x,y进行回溯;从1-N中进行选择
for (int i = 0; i < items.length; i++) {
//无效的继续重试
if (!isValid(board, x, y, items[i])) {
continue;
}
board[x][y] = items[i];
backtrack(board, items, x, y + 1); //搜索下一个坐标
board[x][y] = ".";

}
}

/**
* 只对垂直和水平方向判重
*
* @param board
* @param x
* @param y
* @param item
* @return
*/
private boolean isValid(String[][] board, int x, int y, String item) {
for (int i = 0; i < board.length; i++) {
if (board[i][y].equals(item)) {
return false;
}
if (board[x][i].equals(item)) {
return false;
}
}
return true;
}

public static void main(String[] args) {

// //一个3*3的宫格
// String[][] board = {{"1", ".", "."},
// {".", ".", "."},
// {".", ".", "1"}};
// String[] items = {"1", "2", "3"};
//一个9*9的宫格
String[][] board = {{"5", "3", ".", ".", "7", ".", ".", ".", "."},
{"6", ".", ".", "1", "9", "5", ".", ".", "."},
{".", "9", "8", ".", ".", ".", ".", "6", "."},
{"8", ".", ".", ".", "6", ".", ".", ".", "3"},
{"4", ".", ".", "8", ".", "3", ".", ".", "1"},
{"7", ".", ".", ".", "2", ".", ".", ".", "6"},
{".", "6", ".", ".", ".", ".", "2", "8", "."},
{".", ".", ".", "4", "1", "9", ".", ".", "5"},
{".", ".", ".", ".", "8", ".", ".", "7", "9"}};
String[] items = {"1", "2", "3", "4", "5", "6", "7", "8", "9"};
new SudokuDemo().backtrack(board, items, 0, 0);
System.out.println("共有" + resultCount + "个解");
}
}

14.N皇后问题

N皇后的问题在每一行的N个元素中,选择一个坐标进行保存和回溯。按照「回溯」算法的框架进行;

public class NQueenDemo {

public static void main(String[] args) {
new NQueenDemo().queen(8);
}

private int resultNumber = 0;

private static final String Q_STRING = "Q";
private static final String STAR_STR = "*";

public void queen(int N) {
String[][] board = new String[N][N];
for (String[] row : board) {
java.util.Arrays.fill(row, STAR_STR);
}
backtrack(board, 0);
System.out.println("共有" + resultNumber + "种方案");
}

public void backtrack(String[][] board, int row) {

if (row == board.length) {
//搜集结果
resultNumber++;
System.out.println("----方案" + resultNumber + "----");
printResult(board);
return;
}
//选择当前row行的每一个元素做尝试
for (int col = 0; col < board[row].length; col++) {
if (!isValidPos(board, row, col)) {
continue;
}
board[row][col] = Q_STRING;
backtrack(board, row + 1);
board[row][col] = STAR_STR;
}
}

/**
* 检测上下左右以及左上右上
*
* @param board
* @param x
* @param y
* @return
*/
private boolean isValidPos(String[][] board, int x, int y) {

final int N = board.length;
for (int i = 0; i < N; i++) {
//水平和垂直方向
if (Q_STRING.equals(board[x][i]) || Q_STRING.equals(board[i][y])) {
return false;
}

if (x - i >= 0 && y - i >= 0) {
if (Q_STRING.equals(board[x - i][y - i])) {
return false;
}
}

if (x + i < N && y + i < N) {
if ("Q".equals(board[x + i][y + i])) {
return false;
}
}

if (x + i < N && y - i >= 0) {
if ("Q".equals(board[x + i][y - i])) {
return false;
}
}

if (x - i >= 0 && y + i < N) {
if (Q_STRING.equals(board[x - i][y + i])) {
return false;
}
}
}
return true;
}

private void printResult(String[][] board) {
for (String[] row : board) {
for (String item : row) {
System.out.print(item);
}
System.out.println();
}
}
}

附录