DFS解题总结

1,346 阅读5分钟

在图和树的题中,经常会碰到需要遍历的情况。解决需要遍历的问题,就需要DFS或者BFS。这两种方法的思路都是很简单很自然的

  1. DFS:从一个点出发,接着进入与它相连的任意一个点,不断深入向下搜索,直到搜索无法进行下去就回退到上一个节点,处理与之相连的其他未搜索的节点。在树中体现为先序遍历。
  2. BFS:从一个点出发,找到与他相连的所有节点,依次访问这些节点。在树中体现为层次遍历。

由于DFS和BFS在行为逻辑上的不同,所以BFS一般用队列来实现,而DFS一般用栈来实现。而函数递归的本质就是栈,所以DFS可以很容易地写成递归函数。

DFS的递归一般形式

void dfs(node, visited){
    if(到达边界条件){
        相应的操作;
        return;
    }
    else{
        if(check满足继续搜索的条件)){
            标记当前点已经访问过,修改visited;
            DFS(与该节点相邻的所有节点);       //如果是一维的只有下一个点,如果是二维的依题意是四个方向上或八个方向上的点
            恢复未标记前的状态,恢复visited;    //回溯法需要恢复状态,如果题意是不需要回溯的就不需要这一步
        }
    }
}

在对应到具体问题的时候需要具体的分析,有些不需要回溯的就不需要恢复visited。在参数传递的时候,如果要在遍历的过程中保存需要返回的值的,那么通常还需要再传一个参数的引用,以便在内层的递归能够最终修改这个需要返回的值。
对于不同的问题通常是check是否满足条件那里的不同,如果check部分过于复杂,可以另写一个函数来实现。

DFS解决一维全排列问题

class Solution {
public:
	vector<string> Permutation(string str) {
		int length = str.length();
		vector<string> result;
		if (length == 0)
			return result;
		vector<bool> visited(length, 0);
		string s = "";
		DFS(0, s, str, visited, result);
		return result;
	}
	void DFS(int step, string s, string str, vector<bool> visited, vector<string>& result) {
		int length = str.size();
		if (step == length) {
			result.push_back(s);
			return;
		}
		for (int i = 0; i < length; i++) {
			if (visited[i] == 0) {
				s = s + str[i];
				visited[i] = 1;
				DFS(step + 1, s, str, visited, result);
				visited[i] = 0;
				s = s.substr(0, s.size() - 1);
			}
		}
	}
};

以上是采用仿照模板的编程思路。使用了visited来记录哪些位置上的字符访问过,同时也用了额外的空间来存储当前搜索的字符串。另外一种更简洁的方式是直接搜索到某个节点的时候,和的节点上的字符进行交换。由于只和后面的字符交换,前面访问过的不交换,所以实际上也实现了visited数组的功能。另外,由于交换是就地的,也就不需要额外一个字符串来存储结果。同时这样还有一个优点是当存在重复字符的时候,能够很方便地去重,只要相同就不交换。代码如下:

class Solution {
public:
    vector<string> Permutation(string str) {
        int length = str.length();
        vector<string> result;
        if (length == 0)
            return result;
        placeKthChar(str, result, 0);
        return result;
    }
    
    void placeKthChar(string str, vector<string>& result, int k) {
        int length = str.length();
        if (k == length) {
            result.push_back(str);
            //cout << str << endl;
            return;
        }

        for (int i = k; i < length; i++) {
            if (i != k && str[i] == str[k])
                continue;
            char c = str[k];
            str[k] = str[i];
            str[i] = c;
            placeKthChar(str, result, k + 1);
        }
    }
};

DFS解决二维路径遍历问题

在做这部分题目的时候,我就会想这题目的DFS解法怎么和剑指offer里的回溯法这么像。后来想一想,回溯法是建立在DSF的基础上,要回溯必须要先实现以DFS的方式遍历图中的节点。因此其实回溯法是DFS的一种应用。当然在某些场景下,DFS只需要搜索,而不用回溯,那就是单纯的DFS。
二维的情况下,DFS的整体框架还是和上述的模板类似。一般是从一个节点出发,向它的四个方向(有时候是8个)搜索。当然在进入搜索下一个节点前,需要先判断是否到达边界条件,以及check当前节点是否可进入。

例题:
给定一个由'1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。

示例 1:

输入:

11110
11010
11000
00000

输出: 1

示例2:

输入:
11000
11000
00100
00011

输出: 3

这一题的思路很简单,只需要遍历整个图,从一个是'1'的点出发,搜索到所有通过一定路径它能达到的所有'1'的节点。直到图中所有的'1'都被找到,假设一共出发过n次,那么就有n个岛屿。可以发现这里的搜索既可以是DFS也可以是BFS。另外,对于已经搜索过的点不需要回溯的操作。因此代码如下:

class Solution {
public:
    int numIslands(vector<vector<char>>& grid) {
        int rows = grid.size();
        if(rows == 0)
            return 0;
        int cols = grid[0].size();
        int count = 0;
        vector<vector<bool>> visited(rows, vector<bool>(cols, 0) );
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                if(visited[i][j] == 0 && grid[i][j] == '1'){
                    DFS(i, j, visited, grid);
                    count++;
                }
            }
        }
        return count;
    }
    
    void DFS(int x, int y, vector<vector<bool>>& visited, vector<vector<char>>& grid){
        int rows = grid.size();
        int cols = grid[0].size();
        if(x >= 0 && x < rows && y >= 0 && y < cols && grid[x][y] == '1' && visited[x][y] == 0){
            visited[x][y] = 1;
            DFS(x - 1, y, visited, grid);
            DFS(x + 1, y, visited, grid);
            DFS(x, y - 1, visited, grid);
            DFS(x, y + 1, visited, grid);
        }
    }
};

总结

用递归法来实现DFS,比较好理解,就一直往下找,知道走不通后在回来尝试其它的地方。一个DFS一般要判断边界,check来判断是否符合相应条件,visited来记录是否已经被用过,递归进行下一步操作。如果场景是需要回溯的话,那么就需要恢复一些变量的值,通常是visited。是否要回溯取决于应用的场景。