0
点赞
收藏
分享

微信扫一扫

Java回溯详解(组合、排列、装载问题)

爱喝酒的幸福人 2022-03-24 阅读 66

回溯算法有“通用的解题法“之称,但是往往在学习的过程中会有这样几个困惑:

1、无法理解其精髓之要

2、理解之后不能写出代码

3、能写出代码之后又不能推广

本篇文章将详细讲解回溯的思想以及组合、排列、装载等三个经典且具有代表性的问题。

本文章涉及基础:

有了以上知识点会更利于阅读此帖子。

其实很多人对于单独的某一个回溯的算法能理解,但是真正自己面对到问题后没有头绪。这里介绍一个回溯算法的固定模板:

private static void backtracking(List<List<Object>> list, Arraylist<Integer> tempList, int[] a,....)
{
	//终止条件,也就是一次结果或者不符合条件
	if(false)//false代表条件不符合
		return false;
	if(true)//当符合需要的结果
		list.add(new ArrayList(tempList))//注意这里要重新创建,因为tempList是一个对象,改变的话,会改变结果值。所以重新创建
	//对每个值进行回溯
	for(int i = start; i < a.length; i++)
	{
		if(true)//存在某个限定条件的,比如出现重复值,跳过(根据条件限定而存在与否)
			continue;
		mask(used(i));//将i标记为已使用(根据条件限定而存在与否)
		backtracking(list, tempList, a, i+1)//此处的i+1也可以根据实际情况判断题目中的数字是否可以重复使用
		unmask(used(i));//回溯完要记得取消掉
		tempList.remove(tempList.size() - 1);//回溯回父节点.寻找下一个节点
	}
}

 好,接下来我们直接在实战中慢慢了解:

 

 观察这张图,根节点中包含1、2、3,然后我们每一次取一个,框中为剩余的未选择的数字,注意一下我们这道题目中说的是排列,而不是组合,在接下来的一题中会有组合,这张图就是老师或者书本上提及到的“解空间”,可以这么理解。然后我们可以观察,这棵树的每一次深度遍历都是题目对应的一个解,所以我们将所有的深度遍历结果表示出来即可。好,看代码:

import org.junit.Test;

import java.util.*;

/**
 * 给定一个没有重复数字的序列,返回它的全排列
 * @author wanhailin
 * @creat 2022-03-23-21:18
 */
public class test1 {
    List<List<Integer>> res = new ArrayList<>();//装所有结果的集合
    List<Integer> tmp = new ArrayList<>();//装一条路径的结果

    public List<List<Integer>> permute(int[] nums) {
        if (nums.length == 0) {
            return res;
        }
        backtracking(nums);
        return res;
    }

    private void backtracking(int[] nums) {
        if (tmp.size() == nums.length) {//当单条路径的长度达到数组长度的时候,说明该路径已经完成
            res.add(new ArrayList<>(tmp));//将该结果集合加入到总结果集合中
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (tmp.contains(nums[i]))//遇到使用过的数字就跳过
                continue;
            tmp.add(nums[i]);//将数组的数字加入到单条结果集合中
            backtracking(nums);//递归
            tmp.remove(tmp.size() - 1);//回溯到上一层
        }
    }

    @Test
    public void test() {
        int[] arr1=new int[]{1,2,3};
        List<List<Integer>> list=permute(arr1);
        System.out.println(list);
    }
}

 重点在于以下几点:

1、理解递归的这个过程,建议去详细了解递归时候内存创建的过程。

2、理解结果集合产生的过程,首先是每一条路径用List<>存储,然后所有的路径也存储在List<>中,所以最终结果集合是List<List<>>的形式。

3、结合递归然后理解 (tmp.remove(tmp.size() - 1);//回溯到上一层),之所以能够进行回溯,也是由于递归的机制,真实的每一条路径都已经记录,它只是通过这样回退再进入另一条路径。。。以这样的方式遍历完所有的路径。、

 

 观察这张图,这也就是我们所说的解空间,这里存在的问题是有顺序问题,每一层就相当于选择了一个数字。这里给大家推荐一个哔哩哔哩讲的比较好的视频,视频比文字来的更加直观:带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili

 好,我们来看代码:

import org.junit.Test;
import java.util.ArrayList;
import java.util.List;

/**
 * 给定一个数组,返回所有两个数字组合的情况
 */
public class test2 {
    List<List<Integer>> res = new ArrayList<>();//装总结果的集合
    List<Integer> temp = new ArrayList<>();//装单条结果的集合

    public List<List<Integer>> f(int[] nums) {
        backtracking(4, 2, 1);
        return res;
    }

    public void backtracking(int n, int k, int start_index) {
        //start_index为每次递归搜索的起始位置,k是组合的大小,n为传入数组长度
        if (temp.size() == k) {
            res.add(new ArrayList<>(temp));
            return;
        }
        for (int i = start_index; i <= n; i++) {
            temp.add(i);
            backtracking(n, k, i + 1);
            temp.remove(temp.size() - 1);
        }
    }

    @Test
    public void test() {
        int[] arr1 = new int[]{1, 2, 3, 4};
        List<List<Integer>> lists = f(arr1);
        System.out.println(lists);
    }
}

重点与问题一相同。

例:

输入:

int[] weight = new int[]{0, 20, 30, 60, 40, 40};
int first_load_weight = 100;
int second_load_weight = 100;

输出:

第一艘船的载重量:90
第二艘船的载重量:100
第0件货物装入第一艘船
第1件货物装入第一艘船
第2件货物装入第一艘船
第4件货物装入第一艘船
第3件货物装入第二艘船
第5件货物装入第二艘船
 

 

很明显应该用深搜去做这道题,来看一下代码:

import org.junit.Test;
import java.util.ArrayList;
import java.util.List;

/**
 * 装载问题
 */
public class test3 {
    static int n;//货物数量
    static int[] weight;//货箱重量数组
    static int first_load_weight;//第一艘船的可载重量
    static int[] best_way;//已产生的最佳方案
    static int best_way_weight = 0;//最佳方案重量
    static int[] current_way;//当前装载方案
    static int current_way_eight = 0;//当前已装载重量
    static int rest;//货物剩余重量

    public static int f(int[] w, int f_l_w) {//货物重量数组,第一艘船的重量
        //初始化成员变量
        n = w.length - 1;//通过货物重量数组获取货物数量
        first_load_weight = f_l_w;//初始化第一艘船的载重量
        weight = w;//初始化货箱重量数组
        current_way_eight = 0;//初始化当前装载重量为0
        best_way_weight = 0;//初始化当前最优装载重量为0
        current_way = new int[n + 1];//初始化当前装载方案
        best_way = new int[n + 1];//初始化最优装载方案

        for (int i = 0; i <= n; i++) {
            rest += weight[i];//初始化剩余最大量
        }
        backtracking(1);
        return best_way_weight;
    }

    public static void backtracking(int t) {//遍历的位置
        //遍历到叶子节点就结束当前路径
        if (t > n) {
            if (current_way_eight > best_way_weight) {//判断是否最优
                for (int i = 0; i <= n; i++) {
                    best_way[i] = current_way[i];//替换最优方案重量数组
                }
                best_way_weight = current_way_eight;//替换最优方案总重量
            }
            return;
        }
        rest -= weight[t - 1];//计算剩余货物重量
        if (current_way_eight + weight[t - 1] < first_load_weight) {//遍历左子树
            current_way[t - 1] = 1;
            current_way_eight += weight[t - 1];//增加当前载重量
            backtracking(t+1);//递归
            current_way_eight -= weight[t - 1];//回溯
        }
        if (current_way_eight + weight[t - 1] > first_load_weight) {//遍历右子树
            current_way[t - 1] = 0;
            backtracking(t+1);//递归
        }
        rest += weight[t - 1];//恢复递归后的剩余量
    }

    @Test
    public void test() {
        int[] weight = new int[]{0, 20, 30, 60, 40, 40};
        int first_load_weight = 100;
        int second_load_weight = 100;
        int n = weight.length-1;
        f(weight, first_load_weight);
        int weight_of_second = 0;
        for (int i = 0; i <= n; i++) {
            weight_of_second += weight[i] * (1 - best_way[i]);
        }
        if (weight_of_second > second_load_weight) {
            System.out.println("无解");
        } else {
            System.out.println("第一艘船的载重量:" + best_way_weight);
            System.out.println("第二艘船的载重量:" + weight_of_second);
            for (int i = 0; i <= n; i++) {
                if (best_way[i] == 1) {
                    System.out.println("第" + i + "件货物装入第一艘船");
                }
            }
            for (int i = 0; i <= n; i++) {
                if (best_way[i] == 0) {
                    System.out.println("第" + i + "件货物装入第二艘船");
                }
            }
        }
    }
}

 通过对这三道题真正的了解后,大家应该对回溯法有了进一步的了解,俗话说的好,真正的掌握需要多多的实践,所以从理解到掌握还需要大量的刷题与总结。文章较长,谢谢阅读,有错误之处还望指正,谢谢!

 

举报

相关推荐

0 条评论