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以及回溯等算法)。

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