算法题之优先搜索


总结:对于求深度的问题使用DFS要更方便一些,但涉及到“最小”,“最短”一类的问题使用BFS似乎会更有优势。

1、水域大小&岛屿数量

题目

在这里插入图片描述
在这里插入图片描述

分析

这类题目可以归类下,就是三板斧:

1、创建一个used数组,记录找过的区域。
2、进入dfs,设立dir_x、dir_y,进行探索,找出目标点。
3、进行边界、是否合适和used判断。

第二题代码如下:

class Solution {
public:
	int numIslands(vector<vector<char>>& grid) {
		row = grid.size();
		if (row == 0) return 0;
		col = grid[0].size();

		vector<vector<int>> used(row, vector<int>(col, 0));
		int ret = 0;
		for (int i = 0; i < row; ++i) {
			for (int j = 0; j < col; ++j) {
				if (used[i][j] || grid[i][j] == '0') continue;
				dfs(grid, used, i, j);
				++ret;
			}
		}

		return ret;
	}

	void dfs(vector<vector<char>>& grid, vector<vector<int>>& used, int x, int y) {
        used[x][y] = 1;
		int dir_x[4] = { -1,1,0,0 };
		int dir_y[4] = { 0,0,-1,1 };
		
		for (int i = 0; i < 4; ++i) {
			int cur_x = x + dir_x[i];
			int cur_y = y + dir_y[i];

			if (cur_x < 0 || cur_x >= row || cur_y < 0 || cur_y >= col ||
			      grid[cur_x][cur_y] == '0'|| used[cur_x][cur_y] == 1) continue;
			dfs(grid, used, cur_x, cur_y);
		}

		return;
	}

private:
	int row, col;
};

复杂度

时间复杂度 O(N)
空间复杂度 O(N)

2、二叉树中的最大路径和(DFS)

题目

在这里插入图片描述

分析

首先这题审题要严,他要找的是从任意结点出发能到达的路径,也就是说可以从子节点到父节点,但是不能再往回走,所以会有下面三种情况:

    a
   / \
  b   c

1、b + a + c。
2、b + a + a 的父结点。
3、c + a + a 的父结点。

注:第一种情况中可以从b找到a再找到c,但这个路线中的b和c都只能是2 、 3这种单线情况。

另外结点有可能是负值,最大和肯定就要想办法舍弃负值(max(0, x))(max(0,x))。
但是上面 3 种情况,无论哪种,a 作为联络点,都不能够舍弃。

int maxPathSum(TreeNode* root, int &val)
{
	if (root == nullptr) return 0;
	int left = maxPathSum(root->left, val);
	int right = maxPathSum(root->right, val);
	int lmr = root->val + max(0, left) + max(0, right);
	int ret = root->val + max(0, max(left, right));
	val = max(val, lmr);
	return ret;
}

int maxPathSum(TreeNode* root) 
{
	int val = INT_MIN;
	maxPathSum(root, val);
	return val;
}

复杂度

时间复杂度 O(N)
空间复杂度 O(1)

3、恢复二叉树(DFS)

题目

在这里插入图片描述

分析

其实一开始完全没读懂这个题目的意思,还是看过别人的解析才懂。
这题最直观的解法就是中序遍历,因为二叉搜索树的中序遍历是按升序进行的,所以两个节点替换后就会打破升序。
这里举个栗子:
在这里插入图片描述
如图所示,中序遍历顺序是 4,2,3,1,我们只要找到节点4和节点1交换顺序即可!

第一个节点,是第一个按照中序遍历时候前一个节点大于后一个节点,我们选取前一个节点,这里指节点4;

第二个节点,是在第一个节点找到之后, 后面出现前一个节点大于后一个节点,我们选择后一个节点,这里指节点1;

这里还有一个情况是交换的两个数是中序遍历中连续的两个值,这里的做法是维护一个last指针指向当前节点的前一个节点。
第一次遇到降序时就让left指向last节点,right指向当前节点。如果第二次遇到降序,就把right指向当前节点。
dfs结束后就将left和right的值交换即可。

这里有几点需要注意:

1、last节点赋值的位置。
2、给left和right节点赋值的位置。

复杂度

时间复杂度 O(N)
空间复杂度 O(1)

4、01矩阵(BFS)

题目

在这里插入图片描述

分析

我想通过这题来总结下DFS和BFS的适用性分析:

这题我原计划使用DFS,原因很简单,这题需要“求深度”,而DFS基于递归,可以将深度作为参数和返回值,很容易就实现了。
而BFS则是基于迭代和队列的混合使用,在深度的传递上要困难许多,目前了解的技术就是再专门维护个数组来记录深度。

但是,使用完DFS后我发现有很多问题,问题的核心在于这题要做到遇到0立停,而且要以最短的深度作为实际深度。
因此在这题上DFS就很明显劣于BFS了,因为BFS可以做到停止时的深度就是实际深度。

回到之前的问题,BFS的缺点在于难以记录深度。
这题我参考了题解,他的做法是:
先将最外层的1的深度记为1,并入队。
然后从外向内进行BFS,深度直接记录到输出数组上面。

复杂度

时间复杂度:O(n * m),每个点入队出队一次
空间复杂度: O(n * m),虽然我们是直接原地修改的原输入数组来存储结果,但最差的情况下即全都是 0 时,需要把 m * n 个 0 都入队

5、单词接龙(BFS)

题目

在这里插入图片描述

分析

这题是一个综合性题目,用到了BFS和哈希表。
这题看上很复杂,完全就是自己把自己唬住了,说破了就是把beginWord中的每个的单词按’a’到‘z’的顺序替换后与wordList中的字符串比较,看有没有一样的,直到找到endWord或者没有匹配项了。
哈希表:

哈希表在这里的用途主要是保存wordList中的字符串,这样将单词替换后就能在常数时间内知道是否含有匹配项。
同样的,这里还需要一个哈希表来保存已经用过的单词,究其原因是因为这题属于无向图,变换能够双向进行。

BFS而不是DFS:

本题应该使用BFS,也是因为需要找的是最短路径,对于这种问题DFS是较为吃力的。
那么老生常谈的一个问题,BFS的深度怎么记录,这里需要有一个布置,每次迭代前先记录队列的长度qsize。
然后每次迭代一次性处理qsize个对象,在最后将深度加1。
这个方法很nice!!!

复杂度

时间复杂度:最差的情况下O(26wordLenN),其中因为每个字符都需要从’a’换到‘z’所以要乘以26,wordLen是每个字符串的长度
空间复杂度:O(N)

6、二叉树的右视图(BFS)

题目

在这里插入图片描述

分析

这题的难点在于最后一层的最后侧的值可能连在上一层的任何位置上,例如极端情况下最后一层仅一个元素,是上一层最左侧元素的左子树。
我在这里用了一个比较极端的解法,在执行BFS的时候将每一层的元素都添加进去,进行层次遍历,每次迭代执行一整层,而在出队的时候只记录最右端的值。

复杂度

时间复杂度:O(N)
空间复杂度:O(N),因为队列最差时是在满二叉树的情况下装填所有的叶子节点,此时叶子节点的个数是N/2

7、删除无效括号(DFS、BFS)

题目

在这里插入图片描述

分析

本题的话,使用DFS和BFS均可,但我用BFS解题时,出现了越界的情况,所以这里主要说下DFS的解答吧!
DFS的思路很明确,先遍历一遍找到非法的括号,并记录下数量:

        int left=0;
        int right=0;
        for(char i:s){
            if(i=='('){
                left++;
            }
            if(i==')'){
                if(left>0)left--;
                else right++;
            }
        }

然后进行dfs,

    void dfs(string s, int st, int l, int r){
        if(l==0&&r==0){
            if(check(s)){
                ans.push_back(s);
            }
            return;
        }
        for(int i=st;i<s.size();i++){
             // 去重
            if(i-1>=st&&s[i]==s[i-1])continue;
            if(l>0&&s[i]=='('){
                dfs(s.substr(0, i)+s.substr(i+1, s.size()-i-1), i, l-1, r);
            }
            if(r>0&&s[i]==')'){
                dfs(s.substr(0, i)+s.substr(i+1, s.size()-i-1), i, l, r-1);
            }
        }
    }

这里因为l和r是非法括号的数量,所以当他们等于0时s就为解了。
注意去重的方法,两个相同括号在一起时,依次删除就会得到两个同样的结果。
然后他这里删除字符的方式也是值得参考一番的,他这里并没有直接删除,然后取前后的子字符串拼接,这样就没必要再定义一个临时变量然后在上面操作了

8、被围绕的区域

题目

在这里插入图片描述

分析

这题我首先想到的是使用回溯法+DFS,但是有个案例没能过,而且那个数据着实不好调,所有参考了题解link
他的做法是先把外围遍历一遍,找出为 ‘O’ 的节点,然后进行DFS,将所有的’O’都改为’B’,最后再整体遍历一遍,把’B’改为’O’,把’O’改为’X’,代码如下:

    int row;
    int col;
    
    void solve(vector<vector<char>>& board) {
        row=board.size();
        if(row==0) return;
        col=board[0].size();

        vector<vector<int>> used(row,(vector<int>(col,0)));
        for(int i=0;i<row;++i){
            if(board[i][0]=='O') dfs(board,used,i,0);
            if(board[i][col-1]=='O') dfs(board,used,i,col-1);
        }
        for(int j=0;j<col;++j){
            if(board[0][j]=='O') dfs(board,used,0,j);
            if(board[row-1][j]=='O') dfs(board,used,row-1,j);
        }
        
        for(int i=0;i<row;++i){
            for(int j=0;j<col;++j){
                if(board[i][j]=='O') board[i][j]='X';
                if(board[i][j]=='B') board[i][j]='O';
            }
        }
    }

    bool dfs(vector<vector<char>> &board,vector<vector<int>> &used,int x,int y){
        if(board[x][y]=='X') return true;
        int dir_x[4]={-1,1,0,0};
        int dir_y[4]={0,0,-1,1};

        board[x][y]='B';
        used[x][y]=1;
        for(int i=0;i<4;++i){
            int cur_x=x+dir_x[i];
            int cur_y=y+dir_y[i];
            if(cur_x<0||cur_x>=row||cur_y<0||cur_y>=col||used[cur_x][cur_y]) continue;

            dfs(board,used,cur_x,cur_y);
        }

        return true;
    }

复杂度

时间复杂度:O(MN),这里M为行数,N为列数
空间复杂度:O(MN)

9、生命游戏

题目

在这里插入图片描述在这里插入图片描述

分析

参考:link
这题的难点在于它的进阶,因为一边需要改变矩阵,一边又需要根据原矩阵来求出如何变化,所以正常思路是再建一个矩阵,但如果要求不许使用额外的空间的话,就很尴尬了。
解题的关键是下面这句话:

一个 int 有 32 bit,输入数据只用了一个 bit,所以我们可以利用其他空闲的bit位进行“原地修改”。

因为矩阵中的值不是0就是1,即只用到了最低的一位,所以我们可以用倒数第二位来记录更新后的值,这里注意它可能变化可能没变化,无论如何都应在倒数第二位上写下其值,最后进行移位,将倒数第二位的值移动到最后一位上。代码如下:

    void gameOfLife(vector<vector<int>>& board) {
        int dx[] = {-1,  0,  1, -1, 1, -1, 0, 1};
        int dy[] = {-1, -1, -1,  0, 0,  1, 1, 1};

        int row=board.size();
        int col=board[0].size();

        for(int i=0;i<row;++i){
            for(int j=0;j<col;++j){
                int count=0;

                for(int k=0;k<8;++k){
                    int x=i+dx[k];
                    int y=j+dy[k];

                    if(x<0||x>=row||y<0||y>=col) continue;

                    if((board[x][y]&1)) ++count;
                }

                if((board[i][j]==1&&(count==2||count==3))||(board[i][j]==0&&count==3)) board[i][j]|=2;
            }
        }

        for(int i=0;i<row;++i){
            for(int j=0;j<col;++j){
                board[i][j]>>=1;
            }
        }
    }

复杂度

时间复杂度:O(MN),这里M为行数,N为列数
空间复杂度:O(1)

10、课程表

题目

在这里插入图片描述

分析

参考:link
这里使用了拓扑排序,其原理如下,

对有向无环图(DAG)的顶点进行排序,使得对每一条有向边 (u, v),均有 u(在排序记录中)比 v 先出现。
亦可理解为对某点 v 而言,只有当 v 的所有源点均出现了,v 才能出现。

思路是通过拓扑排序 判断此课程安排图是否是 有向无环图(DAG) 。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。

这里用到了邻接表和入度表两个概念,

所谓入度表,以这题为例子,
就是用一个数组来记录每节课有多少的前置课程没上。

算法流程:

1、在开始排序前,扫描对应的存储空间(使用邻接表),将入度为 0 的结点放入队列。

2、只要队列非空,就从队首取出入度为 0 的结点,将这个结点输出到结果集中,并且将这个结点的所有邻接结点(它指向的结点)的入度减 1,在减 1 以后,如果这个被减 1 的结点的入度为 0 ,就继续入队。

3、当队列为空的时候,检查结果集中的顶点个数是否和课程数相等即可。

在代码具体实现的时候,除了保存入度为 0 的队列,我们还需要两个辅助的数据结构:

1、邻接表:通过结点的索引,我们能够得到这个结点的后继结点;

2、入度数组:通过结点的索引,我们能够得到指向这个结点的结点个数。

接着上代码,

    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        int n=prerequisites.size();
        //入度,用来记录要各个课程分别还需要完成几个前置课程
        vector<int> indegree(numCourses,0);
        //邻接表,这里做表头是前置课程
        vector<vector<int>> adjacency(numCourses);
        queue<int> q;
        int num=numCourses;
        for(int i=0;i<n;++i){
            ++indegree[prerequisites[i][0]];
            adjacency[prerequisites[i][1]].push_back(prerequisites[i][0]);
        }

        for(int i=0;i<numCourses;++i){
            if(indegree[i]==0){
                q.push(i);
                --num;
            }
        }

        while(!q.empty()){
            int cur=q.front();
            q.pop();
            for(int i=0;i<adjacency[cur].size();++i){
                if(--indegree[adjacency[cur][i]]==0){
                    q.push(adjacency[cur][i]);
                    --num;
                }
            }
        }

        if(num==0) return true;
        return false;
    }

这里用队列模拟上课,即当一节课的入度为0时,表示此刻它可以上了,那么我们就通过邻接表一一访问以它为前置课程的课,并将该课的入度减1,并检查该课的入度此时是否为0,是就入队。并通过num来记录还有几节课没上。
当队列为空时表示能上的课都已经上完了,这时再检查num是否为0,是就表示已完成所有课程。

复杂度

时间复杂度 O(N + M)
空间复杂度 O(N + M)

11、矩阵中的最长递增路径

题目

在这里插入图片描述

分析

这题的备忘录设计的比较巧妙,它记录的是以当前位置为起点的最长递增路径的长度。
当搜索到一个备忘录不为0的位置时,就直接返回其值。
代码如下:

class Solution {
public:
	int longestIncreasingPath(vector<vector<int>>& matrix) {
		row = matrix.size();
		if (row == 0) return 0;
		col = matrix[0].size();

		vector<vector<int>> used(row, vector<int>(col, 0));
		int dir_x[4] = { -1,1,0,0 };
		int dir_y[4] = { 0,0,-1,1 };
		ret = 0;
		for (int i = 0; i < row; ++i) {
			for (int j = 0; j < col; ++j) {
				if (!used[i][j]) {
					ret=max(ret,dfs(matrix, used, i, j, dir_x, dir_y));
				}
			}
		}
		return ret;
	}

	int dfs(vector<vector<int>>& matrix, vector<vector<int>> &used, int x, int y, int *dir_x, int *dir_y) {
        if(used[x][y]){
            return used[x][y];
        }

		++used[x][y];
        for (int i = 0; i < 4; ++i) {
			int cur_x = x + dir_x[i];
			int cur_y = y + dir_y[i];
			if (cur_x < 0 || cur_x >= row || cur_y < 0 || cur_y >= col) continue;
			if (matrix[cur_x][cur_y] > matrix[x][y]) {
				used[x][y]=max(used[x][y],dfs(matrix, used, cur_x, cur_y, dir_x, dir_y)+1);
			}
		}
		return used[x][y];
	}

private:
	int row, col, ret;
};

注:dfs中 ++used[x][y] 这一步其实是初始化,因为任何位置的递增路径长度至少为1。

复杂度

时间复杂度:O(mn)
空间复杂度:O(mn)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值