DFS原理與案例

最近刷leetcode時遇到了好多DFS的題目,一開始的迷茫,只能看着答案一步一步的分析,而現在,對於DFS這類題目有了自己的一些見解。
首先DFS是什麼呢?
D—depth,F—first,S—serach,形象一點的講法就是一條道路走到黑,直到走到終點或者前面沒有路,可以理解爲一根筋。

DFS算法大同小異,只要抓住其核心思想,那麼關於DFS的題目在你看來就像是一個模子刻出來的。

那麼其核心思想是什麼呢?

dfs(這一步){
    if(終點或者前面沒有路){// 1
        return ...
    }
    if(未達終點且可以繼續走下去){// 2
        //那麼就走下一步
        dfs(下一步);
    }
}

對於不同的題目,唯一不同的就是代碼“2”處,不同的題目有不同的判斷標準一決定是否能繼續往下走。

知道了核心算法,那麼就實戰練習一下吧!

案例1

LeetCode:combinations

Given two integers n and k, return all possible combinations of k numbers out of 1 ... n.
(從數集(1,2,....,n-1,n)取K個不重複的數)
For example,
If n = 4 and k = 2, a solution is:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

我們以題目出現的情況分析:
我們先從(1,2,3,4)中取一個數T,之後再從剩下的數集C中取下一個數,爲了不重複副,我們對於C必須是T下邊以後的數集。

我們看一下流程:

1.假設第一個數取1後,從(2,3,4)取一個數
2.如果第一個數取2,就從(3,4)取一個數
3.如果第一個數取3,就從(4)取一個數。

我們帶入DFS核心思想的那個流程代碼:

dfs(取一個數T){
    if(取的數已經兩個了或者不能再取了(越界了)){// 1
        //do something
        return ...
    }
    if(還能繼續取數){// 2
        //那麼就走下一步
        dfs(從剩下的數C中取數);
    }
}

DFS代碼如下:

public class Solution {
    public ArrayList<ArrayList<Integer>> combine(int n, int k) {
        ArrayList<ArrayList<Integer>> result = new ArrayList<>();
        ArrayList<Integer> round = new ArrayList<>();
        doCombine(1, n, k, result, round);
        return result;
    }
    // k表示取多少個數添加到list中,start表示從哪裏開始取
    private void doCombine(int start, int n, int k,
            ArrayList<ArrayList<Integer>> result, ArrayList<Integer> round) {
        if (k == 0) {  //  1.
            result.add(new ArrayList<>(round));
            return;
        }
        if (start > n) return; // 1.
        round.add(start);  // 2.
        doCombine(start + 1, n, k - 1, result, round); // 2.
        // 如果不取呢?
        round.remove(round.size() - 1); // 2.
        doCombine(start + 1, n, k, result, round); // 2.
    }
}

說明如下:
start:表示取得樹在數集中的下標
n:表示數集(1,2,….,n-1,n)
k:表示剩下還需要從數集中取k個數。

代碼中標註1的DFS核心思想流程中對應的代碼1,而對於2,這道題我們根據題目得出下一步驟的做法:
1、如果取start對應的數,將start加入round,那麼dfs(剩下的數取一個,k-1)
2、如果不取start對應的數,將剛剛加入round的start刪除,那麼dfs(剩下的數取一個,k不變)。

怎麼樣,是不是很簡單?如果你學習過設計模式,就知道這不就是模板模式嗎?


案例2

題目:N—Queues,N皇后問題,這應該是比較經典的關於DFS算法的題目了。

The n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two queens attack each other.
這裏寫圖片描述
Given an integer n, return all distinct solutions to the n-queens puzzle.
Each solution contains a distinct board configuration of the n-queens’ placement, where’Q’and’.’both indicate a queen and an empty space respectively.
For example,
There exist two distinct solutions to the 4-queens puzzle:

我們先看一看如何解決?
1.從格子中取第一行,再從【a,h】列中選擇一個可以放置的位置
2.從格子中取第2行,再從【a,h】列中選擇一個可以放置的位置

從【a,h】列中選擇一個可以放置的位置——這句話如何理解?

for(int i=a;i<h;i++){
    if{位置i可以放置}{
        //那就放置
    }else{
       //那就選擇下一個位置放置
   }
}

那麼來看一下N—Queue的代碼:

public class Solution {
    public ArrayList<String[]> solveNQueens(int n) {
        // 先初始化
        ArrayList<String[]> result = new ArrayList<>();
        String[] round = new String[n];
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < n; i++)
            sb.append(".");
        Arrays.fill(round, sb.toString());
        // 開始執行
        doSolveNQueens(0, n, n, result, round);
        return result;
    }

    public void doSolveNQueens(int nowRows, int rows, int cols,
            ArrayList<String[]> result, String[] round) {
        if (nowRows == rows) { // 1.處
            String[] copyOf = Arrays.copyOf(round, round.length);
            result.add(copyOf);
        }
        // 查詢這一行那一列可以添加
        for (int j = 0; j < cols; j++) {
            if (canAdd(nowRows, rows,j,cols, round)) { // 2.處
                //這一步的操作
                char[] charArray = round[nowRows].toCharArray();
                charArray[j] = 'Q';
                String string = String.valueOf(charArray);
                round[nowRows] = string;
                //執行下一步
                doSolveNQueens(nowRows+1, rows, cols, result, round);
                //將Q設置回“.”,回覆原狀
                charArray = round[nowRows].toCharArray();
                charArray[j] = '.';
                string = String.valueOf(charArray);
                round[nowRows] = string;
            }
        }
    }

    //判斷是否可以放置的代碼
    private boolean canAdd(int nowRows, int rows,int nowCols ,int cols, String[] round) {
        // 判斷這cols列是否可以放置
        for (int i = 0; i <= nowRows; i++) {
            if (round[i].charAt(nowCols) == 'Q')
                return false;
        }
        // 判斷反斜行是否可以
        for (int i = nowRows - 1, j = nowCols - 1; i >= 0 && j >= 0; i--, j--) {
            if (round[i].charAt(j) == 'Q')
                return false;
        }
        // 判斷正斜行是否可以
        for (int i = nowRows - 1, j = nowCols + 1; i >= 0 && j<cols; i--, j++) {
            if (round[i].charAt(j) == 'Q')
                return false;
        }
        return true;
    }
}

參數說明:
nowRows:此時的行數
rows:總的行數
nowCols:此時的列
cols:總列

重點關注代碼1、2處。
1處代碼還是老樣子,這是DFS算法中改變最小的地方,這裏不解釋了。
2處代碼,這是DFS算法中改變最大的地方。本案例我們需要通過下面的canAdd函數判斷是否可以繼續進行下一步。

帶DFS核心流程:

dfs(這一行放置一個Q){
    if(所有的Q已經放置){// 1
        //do something
        return ...
    }
    for(從a列到h列選擇一列){
        if(此列是否能放置Q){// 2
        //那麼就走下一步
        dfs(從下一行放置Q);
    }
  }
}

案例3

Given a set of candidate numbers ( C ) and a target number ( T ), find
all unique combinations in C where the candidate numbers sums to T .
The same repeated number may be chosen from C unlimited number of
times. Note: All numbers (including target) will be positive integers.
Elements in a combination (a 1, a 2, … , a k) must be in
non-descending order. (ie, a 1 ≤ a 2 ≤ … ≤ a k). The solution set must
not contain duplicate combinations.

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

題目的意思就是算24,只不過只能使用加法,但是元素可以重複使用。

使用DFS來解決的話,我們的想法如下:
1.從C【2,3,6,7】中取2加,此時T=5,只要從【2,3,6,7】取數使得sum爲5集合即可結果集合{2}
2.從C【2,3,6,7】中取2加,此時T=3,只要從【2,3,6,7】取數使得sum爲3集合即可結果集合{2,2}
3.從C【2,3,6,7】中取2加,此時T=1,只要從【2,3,6,7】取數使得sum爲1集合即可
結果集合{2,2,2}
4.從C【2,3,6,7】不能取1,此時去除最新加入的2,結果集合{2,2},此時T=3
5.從C【2,3,6,7】中取3加,此時T=0完成任務了,結果集合{2,2,3}

dfs(要加入的數,剩下的T){
    if(T==0,表示獲得解){// 1.
        //do something
        return ...
    }
    //從數集合C中取一個數
    if(加入以後,T還是>=0){ // 2.
        //那就將此取得的數加入
    }
    //執行下一步
    dfs(要加入的數,T = T-加入的數);
    //如果不加入這個數
    dfs(要加入的數,T還是不變);
  }
}

下面是解法:

public class Solution {
    public ArrayList<ArrayList<Integer>> combinationSum(int[] candidates, int target) {
        ArrayList<ArrayList<Integer>> result = new ArrayList<>();
        ArrayList<Integer> round = new ArrayList<>();
        Arrays.sort(candidates);
        doCombinationSum(0,target, candidates, round, result);
        return  result;
    }
    public void doCombinationSum(int index, int target, int[] candidates, ArrayList<Integer> round, ArrayList<ArrayList<Integer>> result) {
        if (target == 0) {  // 1.
            result.add(new ArrayList<>(round));
            return;
        }
        for (int i = index; i < candidates.length; i++) {//選擇一個數
            if (target - candidates[i] < 0) return;//2.
            round.add(candidates[i]);  //2.
            doCombinationSum(i, target - candidates[i], candidates, round, result);//2.
            round.remove(round.size() - 1); //如果不加入此數
        }
    }
}

好了,通過這三個案例,希望大家對DFS有一些瞭解,起碼不會害怕筆試的時候出現。(建議大家刷leetcode,裏面的一些高級算法是各個大廠比較會考的,而劍指offer在我看來,有時間再刷把,裏面的題目很少涉及DFS,DP,Greedy以及回溯等算法)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章