1. 程式人生 > >手把手教你中的回溯演算法——多一點套路

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

< leetcode>是一個很強大的OJ(OnlineJudge)演算法平臺,其中不少題目都很經典。其中有一個系列的考察回溯演算法,例如Combination Sum 系列 Subsets系列等。根據百度百科定義:回溯法(探索與回溯法)是一種選優搜尋法,又稱為試探法,按選優條件向前搜尋,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。

博主在學習回溯演算法到應用其完成演算法題經歷了很多的困惑,檢視別人部落格的時候基本都是解決某個特定問題,而不是注重方法,相信不少讀者看完和我一樣一臉懵逼。所以博主想要嘗試下寫下自己總結的方法。希望這篇部落格能夠幫助和我一樣在學習演算法的人!第一次寫部落格,如有疏漏,歡迎指正。

首先我們來看一道題目:

Combinations:Given two integers n and k,return all possible combinations of k numbersout of 1 … n. For example, If n = 4 and k =2, a solution is:

[

[2,4],

[3,4],

[2,3],

[1,2],

[1,3],

[1,4],

]

(做一個白話版的描述,給你兩個整數 n和k,從1-n中選擇k個數字的組合。比如n=4,那麼從1,2,3,4中選取兩個數字的組合,包括圖上所述的四種。)

然後我們看看題目給出的框架:

public class Solution {

   public List<List<Integer>> combine(int n, int k) {

       

    }

}

要求返回的型別是List<List> 也就是說將所有可能的組合list(由整數構成)放入另一個list(由list構成)中。

現在進行套路教學:要求返回List<List>,那我就給你一個List<List>,因此

(1) 定義一個全域性List<List> result=new ArrayList<List>();

(2) 定義一個輔助的方法(函式)public void backtracking(int n,int k, Listlist){}

n k 總是要有的吧,加上這兩個引數,前面提到List 是數字的組合,也是需要的吧,這三個是必須的,沒問題吧。(可以嘗試性地寫引數,最後不需要的刪除)

(3) 接著就是我們的重頭戲了,如何實現這個演算法?對於n=4,k=2,1,2,3,4中選2個數字,我們可以做如下嘗試,加入先選擇1,那我們只需要再選擇一個數字,注意這時候k=1了(此時只需要選擇1個數字啦)。當然,我們也可以先選擇2,3 或者4,通俗化一點,我們可以選擇(1-n)的所有數字,這個是可以用一個迴圈來描述?每次選擇一個加入我們的連結串列list中,下一次只要再選擇k-1個數字。那什麼時候結束呢?當然是k<0的時候啦,這時候都選完了。

有了上面的分析,我們可以開始填寫public void backtracking(int n,int k, List list){}中的內容。

public void backtracking(int n,int k,int start,List<Integer> list){
        if(k<0)        return;
        else if(k==0){
                       //k==0表示已經找到了k個數字的組合,這時候加入全域性result中
            result.add(new ArrayList(list));
 
        }else{
            for(int i=start;i<=n;i++){
                list.add(i);//嘗試性的加入i
                    //開始回溯啦,下一次要找的數字減少一個所以用k-1,i+1見後面分析
                backtracking(n,k-1,i+1,list);
                //(留白,有用=。=)
            }
        }
    }

觀察一下上述程式碼,我們加入了一個start變數,它是i的起點。為什麼要加入它呢?比如我們第一次加入了1,下一次搜尋的時候還能再搜尋1了麼?肯定不可以啊!我們必須從他的下一個數字開始,也就是2 、3或者4啦。所以start就是一個開始標記這個很重要啦!

這時候我們在主方法中加入backtracking(n,k,1,list);除錯後發現答案不對啊!為什麼我的答案比他長那麼多? 在這裡插入圖片描述

回溯回溯當然要退回再走啦,你不退回,當然又臭又長了!所以我們要在剛才程式碼註釋留白處加上退回語句。仔細分析剛才的過程,我們每次找到了1,2這一對答案以後,下一次希望2退出然後讓3進來,1 3就是我們要找的下一個組合。如果不回退,找到了2 ,3又進來,找到了3,4又進來,所以就出現了我們的錯誤答案。正確的做法就是加上:list.remove(list.size()-1);他的作用就是每次清除一個空位 讓後續元素加入。尋找成功,最後一個元素要退位,尋找不到,方法不可行,那麼我們回退,也要移除最後一個元素。

所以完整的程式如下:

public class Solution {
   List<List<Integer>> result=new ArrayList<List<Integer>>();
   public List<List<Integer>> combine(int n, int k) {
       List<Integer> list=new ArrayList<Integer>();
       backtracking(n,k,1,list);
       return result;
    }
   public void backtracking(int n,int k,int start,List<Integer>list){
       if(k<0) return ;
       else if(k==0){
           result.add(new ArrayList(list));
       }else{
           for(int i=start;i<=n;i++){
                list.add(i);
                backtracking(n,k-1,i+1,list);
                list.remove(list.size()-1);
            }
       }
    }
}

是不是有點想法了?那麼我們操刀一下。

Combination Sum

Given a set ofcandidate numbers © and a target number (T), findall unique combinations in C where thecandidate numbers sums toT.

The same repeated numbermay be chosen from C unlimited numberof times.

Note:

All numbers (including target) will be positive integers.
The solution set must not contain duplicate combinations.

For example,given candidate set [2, 3, 6, 7] and target 7, A solution set is:

[

[7],

[2,2, 3]

]

(容我囉嗦地白話下,給你一個正數陣列candidate[],一個目標值target,尋找裡面所有的不重複組合,讓其和等於target,給你[2,3,6,7] 2+2+3=7 ,7=7,所以可能組合為[2,2,3],[7])

按照前述的套路走一遍:

public class Solution {

   List<List<Integer>> result=new ArrayList<List<Integer>>();

   public List<List<Integer>> combinationSum(int[] candidates,int target) {

       Arrays.sort(candidates);

       List<Integer> list=new ArrayList<Integer>();

       return result;

    }

   public void backtracking(int[] candidates,int target,int start,){       

    }

}

(1) 全域性List<List> result先定義

(2) 回溯backtracking方法要定義,陣列candidates 目標target 開頭start 輔助連結串列List list都加上。

(3) 分析演算法:以[2,3,6,7] 每次嘗試加入陣列任何一個值,用迴圈來描述,表示依次選定一個值

for(inti=start;i<candidates.length;i++){

                    list.add(candidates[i]);

                }

接下來回溯方法再呼叫。比如第一次選了2,下次還能再選2是吧,所以每次start都可以從當前i開始(ps:如果不允許重複,從i+1開始)。第一次選擇2,下一次要湊的數就不是7了,而是7-2,也就是5,一般化就是remain=target-candidates[i]所以回溯方法為: backtracking(candidates,target-candidates[i],i,list); 然後加上退回語句:list.remove(list.size()-1);

那麼什麼時候找到的解符合要求呢?自然是remain(注意區分初始的target)=0了,表示之前的組合恰好能湊出target。如果remain<0 表示湊的數太大了,組合不可行,要回退。當remain>0 說明湊的還不夠,繼續湊。

所以完整方法如下:

publicclass Solution {
    List<List<Integer>> result=newArrayList<List<Integer>>();
    public List<List<Integer>>combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);//所給陣列可能無序,排序保證解按照非遞減組合
        List<Integer> list=newArrayList<Integer>();
        backtracking(candidates,target,0,list);//給定target,start=0表示從陣列第一個開始
        return result;//返回解的組合連結串列
    }
    public void backtracking(int[]candidates,int target,int start,List<Integer> list){
       
            if(target<0)    return;//湊過頭了
            else if(target==0){
               
                result.add(newArrayList<>(list));//正好湊出答案,開心地加入解的連結串列
               
            }else{
                for(inti=start;i<candidates.length;i++){//迴圈試探每個數
                    list.add(candidates[i]);//嘗試加入
		   //下一次湊target-candidates[i],允許重複,還是從i開始
                   backtracking(candidates,target-candidates[i],i,list);                   
		   list.remove(list.size()-1);//回退
                }
            }
       
    }
}

是不是覺得還是有跡可循的?下一篇部落格將部分回溯演算法拿出來,供大家更好地發現其中的套路。

連結如下:

回溯法欣賞