1. 程式人生 > >聊聊演算法——回溯演算法

聊聊演算法——回溯演算法

 

“遞迴只應天上有,迭代還須在人間”,從這句話我們可以看出遞迴的精妙,確實厲害,遞迴是將問題規模逐漸減小,

然後再反推回去,但本質上是從最小的規模開始,直到目標值,思想就是數學歸納法,舉個例子,求階乘 N!=(N-1)!*N ,

而迭代是數學中的極限思想,利用前次的結果,逐漸靠近目標值,迭代的過程中規模不變,舉例如For迴圈,直到終止條件。

遞迴的思想不復雜,但程式碼理解就麻煩了,要理解一個斐波那契陣列遞迴也不難,比如下面的回溯演算法遞迴,for 迴圈裡面

帶遞迴,看程式碼是不是暈了?好,下面我們專門來聊聊這個框架!

 

作者原創文章,謝絕一切形式轉載,違者必究!

 

準備:

Idea2019.03/JDK11.0.4

難度: 新手--戰士--老兵--大師

目標:

  1. 回溯演算法分析與應用

1 回溯演算法

先給出個回溯演算法框架:

backtrack(路徑,選擇列表){
    //結束條件
    將中間結果加入結果集
    for 選擇 in 選擇列表:
        //做選擇,並將該選擇從選擇列表中移除
        路徑.add(選擇)
        backtrack(路徑,選擇列表)      
        //撤銷選擇 
        路徑.remove(選擇)
}
 

為了理解上述演算法,回想一下,我前篇文章中有說到,多路樹的遍歷演算法框架:

private static class Node {
    public int value;
    public Node[] children;
}
public static void dfs(Node root){
    if (root == null){
        return;
    }
    // 前序遍歷位置,對node做點事情
    for (Node child:children
    ) {
        dfs(child);
    }
    // 後序遍歷位置,對node做點事情
}
 

如果去掉路徑增加/撤銷的邏輯,是不是和多路樹的遍歷演算法框架一樣了呢?其實就是一個多路樹DFS的變種演算法!

另外,雖然遞迴程式碼的理解難度大,執行時是棧實現,但看官不要掉進了遞迴棧,否則就出不來了,如果試著用打斷

點逐行跟進的辦法非要死磕,那對不起,估計三頓飯功夫也可能出不來,甚至我懷疑起自己的智商來,所以,理解遞迴,

核心就是抓住函式體來看,抽象的理解,只看懂 N 和 N-1 的轉移邏輯即可!不懂的先套用再說,也不定哪天就靈感來了,

一下頓悟!

 

那就先上菜了!先是經典回溯演算法,代號A,我們要做個數組全排列,我看別人說回溯演算法也都是拿這個例子說事,

我就落個俗套:

class Permutation {
    // 排列組合演算法
    private static List<List<Integer>> output = new LinkedList();
    static List<List<Integer>> permute( List<Integer> nums, // 待排列陣列
                                         int start //起始位置
     ){
        if (start == nums.size()){
            output.add(new ArrayList<>(nums));
        }
        for (int i = start; i < nums.size(); i++) {
            // 做選擇,交換元素位置
            Collections.swap(nums, start, i);
            // 遞迴,縮小規模
            permute( nums,start +1);
            // 撤銷選擇,回溯,即恢復到原狀態,
            Collections.swap(nums, start, i);
        }
        return output;
    }
    // 測試
    public static void main(String[] args) {
        List<Integer> nums = Arrays.asList(1,2,3,4);
        List<List<Integer>> lists = permute(nums,0);
        lists.forEach(System.out::println);
    }
}
 

程式碼理解:陣列 {1,2,3} 的全排列,我們馬上知道有{1,2,3},{1,3,2},{2,1,3},{2,3,1},{3,1,2},{3,2,1}排列,具體過程就是通過遞迴縮小規模,

做 {1,2,3} 排列,先做 {2,3} 排列,前面在加上 1 即可,繼續縮小,就是做 {3} 的排列。排列就是同一個位置把所有不同的數都放一次,

那麼程式碼實現上可使用交換元素法,比如首個位置和所有元素都交換一遍,不就是全部可能了嗎。這樣,首個位置所有可能就遍歷了

一遍,然後在遞迴完後,恢復(回溯)一下,就是說每次交換都是某一個下標位置,去交換其他所有元素。

再來個全排列的演算法實現,代號B,也是使用回溯的思想:

public class Backtrack {
    public static void main(String[] args) {
       int[] nums = {1,2,3,4};
        List<Integer> track = new LinkedList<>();
        List<List<Integer>>  res = backtrack(nums,track);
        System.out.println(res);
    }
    // 儲存最終結果
    private static List<List<Integer>> result = new LinkedList<>();
    // 路徑:記錄在 track 中
    // 選擇列表:nums 中不存在於 track 的那些元素
    // 結束條件:nums 中的元素全都在 track 中出現
    private static List<List<Integer>> backtrack(int[] nums,List<Integer> track){
        // 結束條件
         if (track.size() == nums.length){
             result.add(new LinkedList<>(track));
             return null;
         }
        for (int i = 0; i < nums.length; i++) {
            if (track.contains(nums[i]))
                continue;
            // 做選擇
            track.add(nums[i]);
            backtrack(nums,track);
            // 撤銷選擇
            track.remove(track.size()-1);
        }
        return result;
    }
}
 

程式碼解析:對 {1,2,3} 做全排列,先將 List[0] 放入連結串列,如果連結串列中存在該元素,就忽略繼續,繼續放入List[0+1],同樣的,

存在即忽略繼續,直到將List中所有元素,無重複的放入連結串列,這樣就完成了一次排列。這個演算法的技巧,是利用了連結串列的

有序性,第一個位置會因為回溯而嘗試放入所有的元素,同樣,第二個位置也會嘗試放入所有的元素。

 

畫出個決策樹:

以 {1-3-2} 為例,如果連結串列第一個位置為1,那第二個位置為 {2,3} 之一,{1}由於屬於存在的重複值忽略,

如果第二個位置放了{3},那第三個位置就是{2},就得出了一個結果。

我們對比一下以上兩個演算法實現: 特別注意,演算法B是真正的遞迴嗎?有沒有縮小計算規模?

時間複雜度計算公式:分支個數 * 每個分支的計算時間

演算法A的分支計算只有元素交換,按Arraylist處理,視為O(1),演算法B分支計算包含連結串列查詢為O(N),

演算法A:N!* O(1) ,階乘級別,耗時不送。

演算法B:N^n * O(N) ,指數級別,會爆炸!

 

我使用10個數全排測試如下(嚴謹的講,兩者有資料結構不同的影響,並不是說僅有演算法上的差異):

 

總結:回溯和遞迴是兩種思想,可以融合,也可以單獨使用!

 

全文完!


我近期其他文章:

  • 1 Redis高階應用
  • 2 聊聊演算法——BFS和DFS
  • 3 微服務通訊方式——gRPC
  • 4 分散式任務排程系統
  • 5 Dubbo學習系列之十八(Skywalking服務跟蹤)

       只寫原創,敬請關注 

相關推薦

聊聊演算法——回溯演算法

  “遞迴只應天上有,迭代還須在人間”,從這句話我們可以看出遞迴的精妙,確實厲害,遞迴是將問題規模逐漸減小, 然後再反推回去,但本質上是從最小的規模開始,直到目標值,思想就是數學歸納法,舉個例子,求階乘 N!=(N-1)!*N , 而迭代是數學中的極限思想,利用前次的結果,逐漸靠近目標值,迭代的過程

五大常用演算法 ----回溯演算法

1、概念       回溯演算法實際上一個類似列舉的搜尋嘗試過程,主要是在搜尋嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。    回溯法是一種選優搜尋法,按選優條件向前搜尋,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,

Java回溯演算法解數獨問題

    下面來詳細講一下如何用回溯演算法來解數獨問題。     下圖是一個數獨題,也是號稱世界上最難的數獨。當然了,對於計算機程式來說,只要演算法是對的,難不難就不知道了,反正計算機又不累。回溯演算法基本上就是窮舉,解這種數獨類的問題邏輯比較簡

遞迴、回溯-演算法框架

之前已經學習過回溯法的一些問題,從這篇文章開始,繼續深入學習一下回溯法以及其他經典問題。 回溯法有通用的解題法之稱。用它可以系統的搜尋一個問題的所有解或任一解,回溯法是一個既帶有系統性又帶有跳躍性的搜尋演算法。 它的問題的解空間樹中,按深度優先策略,從根結點出發搜尋解空間樹。演算法搜尋至

0-1揹包問題—回溯演算法—java實現

0-1揹包問題 【問題描述】 有n種可選物品1,…,n ,放入容量為c的揹包內,使裝入的物品具有最大效益。 表示 n :物品個數 c :揹包容量 p1,p2, …, pn:個體物品效益值 w1,w2, …,wn:個體物品容量 【問題解析】 0-1揹包問題的解指:物品1,…,n的一種放

0-1揹包問題-回溯演算法

    回溯演算法類似於遍歷的求解,但不同於無腦遍歷的的地方是它在每一步都判斷是否滿足約束條件,及回溯點,所以可以理解為有條件的遍歷。使用回溯演算法求解01揹包最優解時需要建立二叉樹,樹有業務意義的深度為物品數量n,加上根節點總深度為n+1,除了終端節點外,每個葉子

回溯演算法初涉

什麼是回溯演算法? 回溯演算法也是深度搜索演算法(DFS),也是遞迴。 回溯演算法是最基本的暴力解決演算法,可以很好的解決大多數問題,由此我們需要掌握它。   遞迴有兩點要素: 1. 遞迴邊界。 2. 遞迴的邏輯——遞迴"公式"。 遞迴邊界即是需

4.6 Heuristics for Backtracking Algorithms回溯演算法的啟發式

當使用回溯搜尋解決CSP時,必須對要分支或例項化的變數以及要給該變數的值做出一系列決策。這些決策稱為變數和值排序。已有研究表明,對於許多問題,變數的選擇和值的排序對於有效解決問題是至關重要的(如[5,50,55,63])。 變數或值排序可以是靜態的(排序在搜尋之前是固定的並確定),也可以是動態的

貪婪+回溯演算法------迷宮問題(遞迴實現)

前提 很明顯,初始迷宮的路和牆需要定義和儲存,(這裡用的迷宮用陣列儲存,用1表示牆,用0表示未走過的路。) 需要明確判斷下一步朝哪個方向走?(這裡的方向是:下->右->上->左,這裡將方向用一個二維陣列來儲存) 如何判斷下一步是否在迷宮外?這

回溯演算法(十進位制轉二進位制)

【概念】 回溯演算法實際上一個類似列舉的搜尋嘗試過程,主要是在搜尋嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。回溯法是一種選優搜尋法,按選優條件向前搜尋,以達到目標

【NOJ1006】【演算法實驗二】【回溯演算法】堡壘問題_方格類資料表示方法

1006.堡壘問題 時限:1000ms 記憶體限制:10000K  總時限:3000ms 描述 城堡是一個4×4的方格,為了保衛城堡,現需要在某些格子裡修建一些堡壘。城堡中的某些格子是牆,其餘格子都是空格,堡壘只能建在空格里,每個堡壘都可以向上下左右四個方向射擊,如

【NOJ1007】【演算法實驗二】【回溯演算法】八皇后問題

1007.8皇后問題 時限:1000ms 記憶體限制:10000K  總時限:3000ms 描述 輸出8皇后問題所有結果。 輸入 沒有輸入。 輸出 每個結果第一行是No n:的形式,n表示輸出的是第幾個結果; 下面8行,每行8個字元,‘A’表示皇后,‘

【NOJ1143】【演算法實驗二】【回溯演算法】字母轉換

1143.字母轉換 時限:1000ms 記憶體限制:10000K  總時限:3000ms 描述 通過棧交換字母順序。給定兩個字串,要求所有的進棧和出棧序列(i表示進棧,o表示出棧),使得字串2在求得的進出棧序列的操作下,變成字串1。輸出結果需滿足字典序。 例如TROT

【NOJ1144】【演算法實驗二】【回溯演算法】農場灌溉問題

1144.農場灌溉問題 時限:1000ms 記憶體限制:10000K  總時限:3000ms 描述 一農場由圖所示的十一種小方塊組成,藍色線條為灌溉渠。若相鄰兩塊的灌溉渠相連則只需一口水井灌溉。 輸入 給出(m,n)表示農場大小,若干由字母表示的農場圖(最大不超

【NOJ1145】【演算法實驗二】【回溯演算法】求影象的周長

1145.求影象的周長 時限:1000ms 記憶體限制:10000K  總時限:3000ms 描述 給一個用 ‘ . ’ 和 ' X ' 表示的圖形,圖形在上、下、左、右、左上、左下、右上、右下8個方向都被看作是連通的,並且影象中間不會出現空洞,求這個圖形的邊長。

【NOJ1575】【演算法實驗二】【回溯演算法】圖的m著色問題

1575.圖的m著色問題 時限:1000ms 記憶體限制:10000K  總時限:3000ms 描述 給定無向連通圖G和m種不同的顏色。用這些顏色為圖G的各頂點著色,每個頂點著一種顏色。如果有一種著色法使G中每條邊的2個頂點著不同顏色,則稱這個圖是m可著色的。圖的m著色

8皇后以及N皇后演算法探究,回溯演算法的JAVA實現,非遞迴,迴圈控制及其優化

研究了遞迴方法實現回溯,解決N皇后問題,下面我們來探討一下非遞迴方案 實驗結果令人還是有些失望,原來非遞迴方案的效能並不比遞迴方案效能高 程式碼如下: package com.newflypig.eightqueen; import java.util.Date; /**

8皇后以及N皇后演算法探究,回溯演算法的JAVA實現,非遞迴,資料結構“棧”實現

是使用遞迴方法實現回溯演算法的,在第一次使用二維矩陣的情況下,又做了一次改一維的優化 但是演算法效率仍然差強人意,因為使用遞迴函式的緣故 下面提供另一種回溯演算法的實現,使用資料結構”棧“來模擬,遞迴函式的手工實現,因為我們知道計算機在處理遞迴時的本質就是棧 時間複雜度是一樣的,空間

8皇后以及N皇后演算法探究,回溯演算法的JAVA實現,遞迴方案(一)

八皇后問題,是一個古老而著名的問題,是回溯演算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。 高斯認為有76種方案。1854年在柏林的象棋雜誌

手把手教你中的回溯演算法——多一點套路

< leetcode>是一個很強大的OJ(OnlineJudge)演算法平臺,其中不少題目都很經典。其中有一個系列的考察回溯演算法,例如Combination Sum 系列 Subsets系列等。根據百度百科定義:回溯法(探索與回溯法)是一種選優搜