Java回溯详解(组合、排列、装载问题)
回溯算法有“通用的解題法“之稱,但是往往在學習的過程中會有這樣幾個困惑:
1、無法理解其精髓之要
2、理解之后不能寫出代碼
3、能寫出代碼之后又不能推廣
本篇文章將詳細講解回溯的思想以及組合、排列、裝載等三個經典且具有代表性的問題。
本文章涉及基礎:
1、Java集合理解應用
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);//回溯回父節點.尋找下一個節點} }?好,接下來我們直接在實戰中慢慢了解:
題目一:給定一個沒有重復數字的序列,返回它的全排列:
例:input=[1,2,3]
? ? ? ? ouput:[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 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);//回溯到上一層}}@Testpublic 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);//回溯到上一層),之所以能夠進行回溯,也是由于遞歸的機制,真實的每一條路徑都已經記錄,它只是通過這樣回退再進入另一條路徑。。。以這樣的方式遍歷完所有的路徑。、
題目二:給定一個數組,返回所有的兩個數字組合的情況、
例:輸入:[1,2,3,4]
? ? ? ?輸出:[[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
?
?觀察這張圖,這也就是我們所說的解空間,這里存在的問題是有順序問題,每一層就相當于選擇了一個數字。這里給大家推薦一個嗶哩嗶哩講的比較好的視頻,視頻比文字來的更加直觀:帶你學透回溯算法-組合問題(對應力扣題目: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);}}@Testpublic void test() {int[] arr1 = new int[]{1, 2, 3, 4};List<List<Integer>> lists = f(arr1);System.out.println(lists);} }重點與問題一相同。
題目三:經典的裝載問題,有n個集裝箱要裝上2艘載重量分別為c1和c2的輪船,其中集裝箱i的重量為wi,且∑wi?<=?c1?+?c2。
問是否有一個合理的裝載方案,可將這n個集裝箱裝上這2艘輪船。如果有,找出一種裝載方案。
問題分析:
如果一個給定裝載問題有解,則采用下面的策略可得到最優裝載方案。
(1)首先將第一艘輪船盡可能裝滿;
(2)將剩余的集裝箱裝上第二艘輪船。
將第一艘輪船盡可能裝滿等價于選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近c1。由此可知,裝載問題等價于以下特殊的0-1背包問題:
?max∑?wi?*?xi?&&?∑?wi?*?xi?<=?c1,?xi?∈?{0,?1},?1?<=?i?<=?n;
例:
輸入:
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;//初始化當前裝載重量為0best_way_weight = 0;//初始化當前最優裝載重量為0current_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];//恢復遞歸后的剩余量}@Testpublic 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 + "件貨物裝入第二艘船");}}}} }?通過對這三道題真正的了解后,大家應該對回溯法有了進一步的了解,俗話說的好,真正的掌握需要多多的實踐,所以從理解到掌握還需要大量的刷題與總結。文章較長,謝謝閱讀,有錯誤之處還望指正,謝謝!
?
總結
以上是生活随笔為你收集整理的Java回溯详解(组合、排列、装载问题)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 知识图谱第3享:数据生命周期
- 下一篇: Java--API习题