有趣的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;
    }
}

构建的二叉树,图示详见:https://blog.csdn.net/nupt123456789/article/details/21193175

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种结果,它的搜索空间为:

有趣的BackTracking回溯算法_第1张图片

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();
        }
    }
}

附录

  • 代码随想录(含图)https://zhuanlan.zhihu.com/p/483066417
  • labuladong的算法讲解:https://labuladong.github.io/algo/4/31/104/

你可能感兴趣的:(【算法】,算法,java,数据结构)