一命通关深度优先遍历


前言

在解释深度优先遍历之前,我想先让大家去思考一个可能从未想过的问题:

为什么我们在学习基础数据结构的时候,都没有出现dfs和bfs这两个词,而在学习二叉树和图的时候,突然蹦出了深度优先遍历和广度优先遍历这两个概念?

而解答这个问题也很简单。

  • 数组,链表,双端队列等数据结构,无论产生如何变化,其最终都是线性结构,遍历无非只有一直往后一条路可走。
  • 而二叉树,图等结构,是非线性结构,也便是遍历到每一个结点的时候,如果想进入下一层,会出现很多方向(例如二叉树的左子树和右子树),其遍历的时候就会出现多种选择。

所以,我们在对非线性结构进行遍历的时候,会有两种最常见的方案:

  1. 一条路走到黑,一直走到无路可走,依次遍历完所有路径
  2. 先遍历一步就能到达的结点,再遍历两步才能到达的结点,一直遍历到最远的结点

我们分别将其取名,深度优先遍历和广度优先遍历,这就是他们的来源。

红色路径的遍历在绿色之前

深度优先遍历

简介

深度优先遍历

虽然深度优先遍历是一路走到黑,但是他也不是瞎寄把飞。为了让遍历结果有一定的参考价值,我们在每一个结点的往下遍历,都要遵循一个相同的顺序。
就比如二叉树的前序中序后序遍历,我们不能第一个结点是前序,第二个结点是后续,第三个结点又变成了中序。

我们在二叉树的遍历都知道,只有在左子树全部走完之后,才会回到上一层,继续去遍历右子树。因为在二叉树的遍历就已经非常非常非常详细讲解了,篇幅问题,就不多作解释了。我们把这种当前层遍历完之后,回到上一层的操作称之为回溯。

一般来说,解决回溯问题都采用栈和递归。但是,栈和递归其实是一回事,所以为了让代码更容易理解,我们在这里只用递归来实现。

dfs解决问题

其实原本想叫这个为公式的,但是概念太广了,所以只能称之为公式的简介。

dfs解决的问题,其实就是由它的定义所产生的两个问题:

  1. 选不选?
  2. 选哪个?

而解决这两个问题,只需要注意两个细节:

  1. 是否回头?
  2. 如何剪枝?

关于第一个问题,选不选和选哪个,会在具体的题目里详细说明他的公式。

在这里,先介绍那两个细节:

1.是否回头? 

什么是回头?

假如我们在打一个boss,boss放了一个技能,你现在有左右两个方向可以躲闪:

你往左走,到了最左边的位置,发现不行,还是躲不掉技能。
于是你在最左边的位置,继续遍历,往右走,到了最初的位置,发现不行,还是躲不掉技能。
于是你在最初的位置,继续遍历,往左走,到了最左边的位置,发现不行,还是躲不掉技能。

最终,boss就看着你:

有人说,这不很简单吗,只需要记录上一个结点的位置,防止递归到上一个结点,不就可以了。

所以,boss又放了另一个技能:

结果就不必我多说了,直接放图:
 

饿啊

所以,我们不得不需要额外开一个哈希数组,用来标记所有走过的地方。而这个标记数组,往往会大量使用于大多数dfs和bfs题目中。

2.如何剪枝?

剪枝,说人话,就是在某一条路走到中间,提前排除掉不可能的情况,从而提升效率。

就比如在迷宫中间,如果放置了一颗炸弹。那么当我们遍历到这颗炸弹的时候,炸弹后的路我们就全部不考虑了,这便叫将不可能的情况全部剪枝掉。图就不放上了,因为迷宫实在是太难画了( 

剪枝是公式里唯一需要思考的地方,但是其实也不难,而且大部分情况就算少剪一点,对答案也不会产生太大的影响。


dfs题目分类

dfs题目,以我的经验来说,只分为了四类:

  1. 子集问题——选不选
  2. 全排列问题——选哪个
  3. 爬塔问题——选哪个
  4. 矩阵路线问题——选哪个 

but:

豪德,那就依次看看四种公式分别是什么:


子集

高中都上过吧?那概念就不多解释了。直接来看题目:

不含相同元素的子集 

78. 子集 - 力扣(LeetCode)

解题步骤

在题目分类中,我们就已经说明了,这道题属于选不选的问题。什么意思?这和dfs有什么关系?

我们把整个数组列出来,对于每个元素,我们有两条路可走:

然后,我们接着往后遍历。对于每个元素,我们都是有这两条路:

  1. 将该元素放入子集中
  2. 将该元素不放在子集中 

而遍历到最后一个元素,我们便遍历到了所有情况。

像这样列出所有选择和所有情况的图,我们称之为决策树决策树,就是将每一步有什么选择,如何选择,最后带来了什么结果,全部列在一棵树中。

而我们发现,每一层的两个选择就是选不选当前元素,这就是我所说的,选不选的问题。 

解题代码 

我们在递归章节中有讲到,递归代码的完成只需要考虑三步:

  1. 什么是子问题?
  2. 解决这个问题需要哪些条件,怎么处理?
  3. 递归的结束条件是什么?

 什么?没见过?那赶紧去递归章节看看:一命通关递归-CSDN博客

我们来分步解析:

1.什么是子问题?

在题目特性就已经说清楚了, 子问题是每一层选还是不选。

2.函数条件和函数体

在分析的时候也很明了,完成这个dfs函数只需要知道,当前遍历到哪一层,和该层的元素是什么,便足够了。

而函数体,便是这个函数要干什么。这个也是在分析问题的时候理清楚了:

  1. 选择当前元素,继续向下遍历;
  2. 不选择当前元素,继续向下遍历。
3.结束条件 

一直到遍历完最后一个元素,我们将遍历出的情况插入到返回值数组中,就完成了一条路线的遍历,这条路线的递归就结束了。 

class Solution {
    vector<vector<int>> ret;//返回数组
    vector<int> tmp;//子集数组
    int n;//nums的大小
public:
    void dfs(vector<int>& nums, int p)
    {
        if(p==n)//结束条件
        {
            ret.push_back(tmp);
            return;
        }

        //不选当前结点
        dfs(nums,p+1);

        //选当前结点
        tmp.push_back(nums[p]);
        dfs(nums,p+1);
    }

    vector<vector<int>> subsets(vector<int>& nums) {
        n=nums.size();
        dfs(nums,0);
        return ret;
    }
};

有人可能喜欢给递归函数传一大堆参数,把返回值,临时数组都放在函数参数中,比如:

void dfs(vector<vector<int>>& ret,vector<int>& tmp,int p,vector<int>& nums)

但是这种写法,太太太臃肿了,我们其实把一些在递归中不会影响递归的变量放在全局中就好了。

回溯的还原现场

好,我们提交一下代码,然后发现了:

所以说了这么半天,教我一个伪代码是吧?

别急,我们来看输出的结果。我们发现,明明只有三个数产生的子集,最后突然蹦出了一大长串数字。而且越往后,蹦的数字越多。

会产生这个结果的唯一可能,便是一个数字被重复选取了,但是按我们刚刚的思想,这种情况不可能发生啊?

我们来看中间的某个情况:

而我们看那些输出结果,都是在之前出现过的结果中,不断增加数字,而没有删减过数字,自然就会不断累积,导致出现一堆乱七八糟的结果。

而解决这一问题的方法,便是在返回到上一层递归的同时,清除掉这一层递归对全局变量产生的影响。

打个比方:你在某游戏中遇到了一个分岔路口,你进入了其中一个房间,但是发现这个房间打不过, 你决定用作弊代码回档到上一个房间的时候,那么那个打不过的房间对你产生的所有影响,都将随着回档而消失,你的HP,你身上的道具,也都将回到进入这个房间之前的状态,不可能回档之后,在那个房间受到的伤害还是虚空索敌结算到了你的身上。

而这便称之为,回溯的还原现场我们在回到上一层回溯的时候,必须要消除掉当前层对全局变量产生的所有影响,使其没有遍历过当前路径的痕迹,这样才可以正常遍历其他路径。

而对这道题目,还原现场便是,在子集数组中删掉当前遍历过的元素,往后的遍历便不会出现这个元素。

class Solution {
    vector<vector<int>> ret;
    vector<int> tmp;
    int n;
public:
    void dfs(vector<int>& nums, int p)
    {
        if(p==n)
        {
            ret.push_back(tmp);
            return;
        }

        //不选当前结点
        dfs(nums,p+1);

        //选当前结点
        tmp.push_back(nums[p]);
        dfs(nums,p+1);
        tmp.pop_back();//还原现场
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        n=nums.size();
        dfs(nums,0);
        return ret;
    }
};

含相同元素的子集

 90. 子集 II - 力扣(LeetCode)

解题步骤

不管他含不含相同元素,既然是子集问题,那么就是同一种思路:选还是不选。
只不过,如果我们还是按照之前的思路来做,会发现一个问题:

这也便是重复元素最棘手的问题:怎么去重

在这里,我们介绍一种方法:

  • 对于一串相同的数字,如果前一个选了,那么后一个则一定要选上。

而按照这个规律一直递推下去,其实也可以换一句话来说:

  • 对一串相同的数字,如果某个位置的数字选了,那么该位置往后的所有数字都要选

采用这种方法,就可以不重不漏涵盖整个子集。别问我这种方法怎么想到的,我也是看题解才学会的。 

解题代码 

因为需要知道上一个数选不选,所以不得不在递归中加入一个新的参数。
同时,为了让所有相同的元素相邻,我们可以先将数组排序来达到这个目的:

class Solution {
public:
    vector<int> tmp;
    vector<vector<int>> ret;

    void dfs(bool choosePre, int cur, vector<int>& nums) {
        if (cur == nums.size()) {
            ret.push_back(tmp);
            return;
        }

        //如果上一个选了,并且是相同元素,则这一个必须要选
        if(choosePre&&nums[cur]==nums[cur-1])
        {
            tmp.push_back(nums[cur]);
            dfs(true,cur+1,nums);
            tmp.pop_back();
        }
        //否则正常递归
        else
        {
            dfs(false,cur+1,nums);

            tmp.push_back(nums[cur]);
            dfs(true,cur+1,nums);
            tmp.pop_back();
        }
    }

    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());//为了让相同的元素相邻,我们先将数组排序
        dfs(false, 0, nums);
        return ret;
    }
};

全排列

还是高中概念,不多解释。

不含相同元素的全排列

LCR 083. 全排列 - 力扣(LeetCode) 

解题步骤

子集是选不选的问题,而全排列则是选哪个的问题。什么是选哪个?

因为我们知道,全排列临时数组的大小就等于原数组nums的大小,我们先将全排列的数组列出来:也就是,我们有一个背包装着很多不同的食物,每天我们都要从中选一样吃掉。而每天的选择不同,就构成了全排列,这便是选什么的问题。

但是,因为如果前面选择了,后面则无法再选择,这便是我们说到的细节——不能回头我们需要搭配一个标记数组,来标记哪些元素选过了,不能再选择了。
同样的,我们在还原现场的时候,也需要还原标记数组

解题代码
class Solution {
    vector<vector<int>> ret;
    vector<int> tmp;
    vector<bool> record;//标记数组
public:
    void dfs(vector<int>& nums)
    {
        if(tmp.size()==nums.size())
        {
            ret.push_back(tmp);
            return;
        }

        for(int i=0;i<nums.size();i++)//每个元素都尝试一遍
        {
            if(record[i]==false)//没有被标记,表示之前没有被选过,则当前可以被选到
            {
                record[i]=true;//标记
                tmp.push_back(nums[i]);
                dfs(nums);
                //还原现场
                tmp.pop_back();
                record[i]=false;
            }
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        record.resize(nums.size());
        dfs(nums);
        return ret;
    }
};

含相同元素的全排列

 LCR 084. 全排列 II - 力扣(LeetCode)

解题步骤

和子集中的问题一样,我们此刻的最大问题就是:如何去重?

我们还是采用一种从题解中学到的方法:

  • 对一串相同的元素,如果上一个元素没有选择,则下一个元素也不能选

或者说:

  • 对一串相同的元素,只有上一个元素选了, 下一个元素才可以选

我们这样做的目的,其实是为了保证,相同元素的顺序是有序的

啥意思?

采用这种方法,就能保证重复元素不会乱序,也就达到了去重的目的。

解题代码
class Solution {
    vector<vector<int>> ret;
    vector<int> tmp;
    vector<bool> record;
public:
    void dfs(vector<int>& nums)
    {
        if(tmp.size()==nums.size())
        {
            ret.push_back(tmp);
            return;
        }

        for(int i=0;i<nums.size();i++)
        {
            if(record[i]==false&&!(i>0&&nums[i]==nums[i-1]&&record[i-1]==false))
            {
                record[i]=true;
                tmp.push_back(nums[i]);
                dfs(nums);
                tmp.pop_back();
                record[i]=false;
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        record.resize(nums.size());
        dfs(nums);
        return ret;
    }
};

爬塔问题

什么是爬塔问题?

爬塔问题其实很广泛,他涵盖了子集问题,全排列问题,还有自己延伸出的新问题。但是其实,这个名字是我自己瞎编的,只是作为一个资深的游戏玩家,我觉得把他命名为爬塔问题非常非常违和。 

介绍这个问题,我来讲一个故事:

你重生了,在一个异世界当了一名勇士。这个异世界有一个魔王,生存在一个魔塔里,你或手无寸铁,或已经磨练许久有着充足的自信,但是最终目的都是爬到塔的顶端打败这个魔王。

在这个塔的每一层,都有着许多分叉路口,每个路口你可能会遭遇怪物,可能会触发特殊事件,可能会遇到商人,你可以在每个分岔路口选择不同的路线,然后获得不同的奖励或一些不同的惩罚,最终磕磕撞撞到了魔王面前。

不过,见到了魔王不等于可以打败魔王,你还需要在魔塔中不断遇到新的怪物后,还有着打败魔王的素质,天哪!这太难了!我怎么知道我会在塔里遭遇哪些事!

于是,你使出了肉鸽绝技:存档大法

好了,有了存档,你就可以放肆尝试所有路口,大不了回档嘛。但是,我该怎么不重不漏走过所有路线,找到最优解呢?

OK,以上这个问题,我们总结出两个特点:

  1. 每一层都有很多选择
  2. 我们在爬楼的中途,就可以排除掉很多不可能的情况,比如在半路上就被怪物杀死了,这条路后面的也就走不下去了。 

而这类每一层都有很多选择的问题,都和爬一个魔塔一样,我为他取的最恰当的名字——爬塔问题。

爬塔问题有三类:

  1. 子集
  2. 全排列,即选择的东西是固定,之前选择过的之后不能再选择
  3. 爬塔,即每一层的选择都不同,上一层的选项和下一层的选项并不相同

对于第一二类,直接套子集和全排列的公式就可以了,而第三类,实际上是最简单的。直接根据题目,把题目翻译一遍,就做出来了。 

17. 电话号码的字母组合 - 力扣(LeetCode) 

 这便是一个最典型的爬塔问题,每一层的选择都是相互独立的。

解题步骤

题目的意思便是,我们按下哪个键,就对应了哪个键上的字母供我们选择我们按下了2和3,那么第一层,我们可以选择2对应的abc;第二层,我们可以选择3对应的def。

ok,题目翻译出来,我们就已经做出来了。digits给我们路线,我们在每个路口做出选择,over

解题代码

class Solution {
    vector<string> ret;
    string tmp;
    //每个键对应的选择
    vector<string> arr =  { " "," ","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz" };
public:
    void dfs(string& digits,int cur)
    {
        if(cur==digits.size())
        {
            ret.push_back(tmp);
            return;
        }

        int key=digits[cur]-'0';//按下的键的数字
        for(int i=0;i<arr[key].size();i++)//每一个选择
        {
            tmp.push_back(arr[key][i]);
            dfs(digits,cur+1);
            tmp.pop_back();
        }

    }
    vector<string> letterCombinations(string& digits) {
        if(digits.size()==0)
            return ret;
        dfs(digits,0);
        return ret;
    }
};

 矩阵路线问题

矩阵路线问题,就像躲boss技能 

你有很多方向可以选择,而你到了下一个位置以后,又会有新的方向可以选择。如果把这些方向也理解成每一层的选择,你会发现,他其实也是一个爬塔问题。 
只不过,这个问题解题方法非常非常固定,而且有很多区别于爬塔问题的地方,我们就将其单独列了出来:

  1. 到一个新的地方后,之前的位置很可能会成为当前的选择,是最典型的不能回头问题
  2. 到达矩阵的边界后,因为不能越界,所以选择会突然变少,需要判断边界条件
  3. 你的初始位置可能是不固定的,即递归的起点也需要遍历到每一个点 

79. 单词搜索 - 力扣(LeetCode)

解题步骤

我们通过那三个区别,来分析这个题目:

1.方向选择

假设我们从F开始,有四个方向:上下左右

这四个方向,便是爬塔中的四个选择。不过,爬塔问题多个选择都会给一个选择的数组,这里我们应该怎么很好去表达四个方向呢?
很简单,四个方向不过是row和col,横纵坐标的增减,那我们先初始化两个数组:

int drow[4]={1,-1,0,0};
int dcol[4]={0,0,1,-1};

我们去遍历四个方向的时候,只需要遍历四个方向的数组:

//假设已知了当前的坐标row和col
for(int i=0;i<4;i++)
{
    int newrow=row+drow[i];
    int newcol=col+dcol[i];
    //newrow和newcol便是每个选择中,新的横纵坐标值
}

这样就表示了所有四个方向移动后对应的新坐标值。
同理,如果有更多的方向,也可以这么实现。

而如果我们选择了往右走,则会也有四个方向:上下左右

但是,如果我们此时向左走,就回到了F,也就是新的结点的选择中,很可能会出现回头路。

解决方案很简单,便是解决回头路的通解:配以一个标记数组。

vector<vector<bool>> record(row,vector<bool>(col));//标记数组

if(record[newrow][newcol]==false)
{
    record[newrow][newcol]=true;
    dfs(...);
}
2.边界条件 

加入我们从F走一步,走到了S,那么S可以选择的方向就会减少一个。因为如果S再向左走,那么就会超出矩阵的范围,产生越界,所以必须要加一个选择的判断。

//假如计算出了新的坐标值newrow和newcol
//m和n分别为矩阵的横纵长度
if(newrow>=0&&newrow<m&&newcol>=0&&newcol<n)//判断是否越界
    dfs(...);
//只有不越界,遍历才可以继续
3.多源遍历

题目说了从哪个点开始遍历吗?没有。因为单词搜索的开头,每个位置都有可能,所以我们要考虑所有点作为源点的情况,用每种情况都去遍历一遍。

for(int i=0;i<m;i++)
    for(int j=0;j<n;j++)
        dfs(...);
//每个点都作为源头进行遍历

而解决了以上三个问题,矩阵路线问题,其实和爬塔问题也并无两异

解题代码

class Solution {
    vector<vector<bool>> record;//标记数组
    int dx[4]={0,0,1,-1};
    int dy[4]={1,-1,0,0};//方向数组
public:
    bool dfs(vector<vector<char>>& board,string& word,int cur,int x,int y)
    {
        if(cur==word.size())
            return true;

        bool ret=false;
        for(int i=0;i<4;i++)
        {
            int nextx=x+dx[i];
            int nexty=y+dy[i];//新的横纵坐标
            if(nextx>=0&&nextx<board[0].size()&&nexty>=0&&nexty<board.size()//判断越界
            &&record[nexty][nextx]==false//判断是否回头
            &&board[nexty][nextx]==word[cur])//判断是否满足题目要求
            {
                record[nexty][nextx]=true;
                ret=dfs(board,word,cur+1,nextx,nexty);//以新的结点开始遍历
                record[nexty][nextx]=false;

                //剪枝
                if(ret==true)
                    return true;
            }
        }

        return false;
    }
    bool exist(vector<vector<char>>& board, string& word) {
        record.resize(board.size());
        for(auto& e:record)
            e.resize(board[0].size());
        bool ret=false;

        //对每个结点都去遍历
        for(int i=0;i<board.size();i++)
        {
            for(int j=0;j<board[0].size();j++)
            {
                if(board[i][j]==word[0])
                {
                    record[i][j]=true;
                    ret=dfs(board,word,1,j,i);
                    record[i][j]=false;
                }
                
                //剪枝
                if(ret)
                    return true;

            }
        }

        return false;
    }
};

剪枝

我们在解题代码中,发现了这么一段代码:

我们知道,对于题目示例的那种情况,在遍历到第一个源头的时候,就已经找到了答案,可以返回true了

但是,如果没有这个判断,程序还是会继续遍历下去,遍历完了所有情况,最后发现,哦!第一种情况符合题意!我返回true吧!

在已经可以判断,后续的情况不需要遍历的时候,我们就直接舍弃掉那些不需要遍历的情况,提升效率,这个操作就叫做剪枝

但是我们不剪枝,会不会对答案产生影响?当然不会,后续的遍历又不可能改变先前的结果。所以剪枝的目的只是为了提升效率,而不会对算法产生太大的影响。

并且,不同的题目剪枝也是不同的,这些都是依据题目要求来稍加思考,没有太多的公式可靠。

 

dfs系列题目

22. 括号生成 - 力扣(LeetCode) 

LCR 080. 组合 - 力扣(LeetCode)

LCR 102. 目标和 - 力扣(LeetCode) 

LCR 081. 组合总和 - 力扣(LeetCode) 

784. 字母大小写全排列 - 力扣(LeetCode) 

526. 优美的排列 - 力扣(LeetCode) 

51. N 皇后 - 力扣(LeetCode) 

36. 有效的数独 - 力扣(LeetCode) 

37. 解数独 - 力扣(LeetCode) 

1219. 黄金矿工 - 力扣(LeetCode) 

62. 不同路径 - 力扣(LeetCode) 


公式

和之前的章节不同,为什么在这篇文章,我最后才把公式放出来?

因为dfs的题目无论是剪枝还是判断路径,都是需要独立思考可以自己写出代码之后,才能复述出公式的每一步都在干什么。对于dfs,如果直接告诉你,可以套公式(可以是当然可以),是极不负责任的,因为如果出现了某些题目稍加修改,而你完全不知道递归的每一步在干什么,那么你完全不知道要从哪个地方修改。 

所以,我在最后放出公式,不是鼓励大家直接去记公式,而是告诉大家,一个dfs题目应该怎么去求解,应该需要那些先制条件,求解的时候每个函数在干什么,函数体应该怎么设计。这也是dfs与套路化的二分和前缀和之类的章节,最大的不同。  

爬塔问题

class solution
{
//全局变量部分:
	//判断是否有回头路,如果有的话必须要一个标记数组
	vector<bool> record;
	//一般来说,遍历的结果会存在一个二维数组中,我们一般设为全局变量
	vector<vector<int>> ret;
	vector<int> tmp;
public:
//函数部分:
	//函数的参数由记录每一步的选择数组,和当前遍历到的层数构成
	void dfs(vector<vector<int>>& nums, int cur)
	{
		//结束条件
		if (...)
		{
			...
			//进行一些操作
			return;
		}

		//剪枝
		if (...)
		{
			return;
		}

		//遍历当前层的每个选择
		for (int i = 0; i < nums[cur].size(); i++)
		{
			tmp.push_back(nums[cur][i]);
			record[cur] = true;
			
			dfs(nums, cur + 1);//继续向下遍历

			//还原现场
			tmp.pop_back();
			record[cur] = false;
		}
	}

	vector<vector<int>> main(vector<vector<int>>& nums)
	{
		//初始化标记数组
		record.resize(nums.size());

		dfs(nums, 0);//从0开始遍历
		return ret;
	}
};

矩阵路线问题

class Solution {
    int dx[4]={0,0,1,-1};
    int dy[4]={1,-1,0,0};//方向数组
    vector<vector<bool>> record;//标记数组
    int m,n;//矩阵长宽
public:
    void dfs(vector<vector<int>>& grid,int row,int col)
    {
        if(...)//递归结束
        {
            ...
            //进行一些操作
            return;
        }
        
        if(...)//剪枝
        {
            return;
        }

        for(int i=0;i<4;i++)
        {
            int nextrow=row+dy[i];
            int nextcol=col+dx[i];//新的坐标
            if(nextrow>=0&&nextrow<m&&nextcol>=0&&nextcol<n//不越界
            &&record[nextrow][nextcol]==false//没有走回头路
            &&grid[nextrow][nextcol]...)//满足题目要求
            {
                record[nextrow][nextcol]=true;
                dfs(grid,nextrow,nextcol);//遍历到新的位置
                record[nextrow][nextcol]=false;//还原现场
            }
        }

    }
    ... main(vector<vector<int>>& grid) {
        m=grid.size();
        n=grid[0].size();
        record.resize(m,vector<bool>(n));

        //遍历每一个源头
        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)
            {
                if(grid[i][j]!=0)
                {
                    record[i][j]=true;
                    dfs(grid,i,j);
                    record[i][j]=false;
                }
            }
        }
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/470889.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

力扣 第 126 场双周赛 解题报告 | 珂学家 | 贡献法思维场 + 贪心构造 + 0-1背包

前言 整体评价 T3是道好题&#xff0c;一开始思路偏了往按字母前缀和和DP去想了&#xff0c;但是感觉很难下手&#xff0c;后来发现从贡献的角度&#xff0c;其实和位子无关系&#xff0c;只需要贪心即可。 T4也是一道贡献思路题&#xff0c;理清核心的点&#xff0c;就能简单…

Vue项目使用process.env关键字及Vue.config.js配置解决前端跨域问题

1.process.env 是Node.js 中的一个环境 1.打开命令行查看环境: 2.process.env与Vue CLI 项目 Vue Cli 有以下三种运行模式 development 模式用于 vue-cli-service serve test 模式用于 vue-cli-service test:unit production 模式用于 vue-cli-service build 和 vue-cli-se…

每日一练:LeeCode-167. 两数之和 II - 输入有序数组【双指针】

给你一个下标从 1 开始的整数数组 numbers &#xff0c;该数组已按 非递减顺序排列 &#xff0c;请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] &#xff0c;则 1 < index1 < index2 < numbers.…

如何本地部署1Panel面板

文章目录 前言1. Linux 安装1Panel2. 安装cpolar内网穿透3. 配置1Panel公网访问地址4. 公网远程访问1Panel管理界面5. 固定1Panel公网地址 前言 1Panel 是一个现代化、开源的 Linux 服务器运维管理面板。高效管理,通过 Web 端轻松管理 Linux 服务器&#xff0c;包括主机监控、…

YOLOv8改进 | 主干篇 | 利用图像分割网络UNetV2改善图像分割检测性能(全网独家首发)

一、本文介绍 本文给大家带来的改进机制是利用图像分割网络UNetV2的主干来改进我们的YOLOv8分割模型&#xff08;本文的内容虽然YOLOv8所有的功能的用户都能使用&#xff0c;但是还是建议分割的用户使用&#xff09;&#xff0c;U-Net v2 旨在改进医学图像分割的性能&#xff…

C# 方法(函数)

文章目录 C# 方法&#xff08;函数&#xff09;简单示例程序代码运行效果 值传递和引用传递示例程序 运行效果按输出传递参数运行结果 C# 方法&#xff08;函数&#xff09; 简单示例 程序代码 访问的限制符 using System; using System.Collections.Generic; using Syste…

GO-初识包管理

初识包管理&#xff0c;知道项目中文件和文件夹之间的关系 输出&#xff0c;代码&#xff0c;在go编译器运行时会显示在屏幕中 初识数据类型 整型&#xff0c;数字。例如&#xff1a;1、2、3、4 字符串类型&#xff0c;表示文本信息的。例如:“张三”“李四” 布尔类型&#x…

Day51-Nginx多实例知识与大厂企业级实战

Day51-Nginx多实例知识与大厂企业级实战 1. 什么是nginx多实例&#xff1f;2. 为什么要用多实例&#xff1f;3. 大厂数百个业务项目&#xff0c;如何来管理&#xff1f;4. 大厂上百项目web分用户解决方案4.1 编译nginx环境实践&#xff1a;4.2 zuma实例(利用普通用户权限将不同…

Arduino 开发 野火ESP8266模块的串口通信

一、硬件连接 Arduino ESP8266 串口通信是一个常见的任务&#xff0c;允许通过串行接口与其他设备或计算机进行通信。 连接硬件&#xff1a;将野火ESP8266模块通过USB连接到电脑。注意电源为3.3V。 选择开发板和端口&#xff0c;在Arduino IDE中&#xff0c;选择ESP8266开发板…

图神经网络实战(5)——常用图数据集

图神经网络实战&#xff08;5&#xff09;——常用图数据集 0. 前言0. 图数据集介绍2. Cora 数据集3. Facebook Page-Page 数据集小结系列链接 0. 前言 图数据集往往比单纯的连接集合更丰富&#xff0c;节点和边也可以具有表示分数、颜色、单词等的特征。在输入数据中包含这些…

NLP---Bert分词

目录&#xff1a; Q&#xff1a;bert分词步骤1&#xff1a;构建N * N 的相关性矩阵&#xff0c;计算相邻两个字的相关性&#xff0c;低的话&#xff08;<阈值&#xff09;就切割。2&#xff1a;将A词进行mask计算出A的embedding&#xff0c;然后将AB两个词一起mask&#xff…

【滑动窗口】长度最小的子数组|无重复字符的最长子串|最大连续1的个数 III|将 x 减到 0 的最小操作数

1. 长度最小的子数组 - 力扣&#xff08;LeetCode&#xff09; 1.题目解析&#xff1a; 2.算法原理 &#xff08;1&#xff09;方法一&#xff1a;暴力列举出所有的子数组的和 时间复杂度&#xff1a;O&#xff08;n**2&#xff09;&#xff1a;枚举所有子数组O&#xff08;…

使用 Redux 管理 React 应用状态

使用 Redux 管理 React 应用状态 在复杂的 React 应用中&#xff0c;管理组件状态变得越来越复杂&#xff0c;这时候引入 Redux 可以帮助我们更好地管理状态。Redux 是一个可预测状态容器&#xff0c;它可以帮助我们统一管理应用的状态&#xff0c;使得状态变化更加可控。本文…

机器学习基本算法(一)

1.线性回归算法 Linear Regression 线性回归算法&#xff08;Linear Regression&#xff09;是一种预测性的建模技术&#xff0c;它研究的是因变量&#xff08;目标&#xff09;和自变量&#xff08;预测器&#xff09;之间的关系。线性回归假设目标值与特征之间线性相关&…

MacOS Xcode 使用LLDB调试Qt的 QString

环境&#xff1a; MacOS&#xff1a; 14.3Xcode&#xff1a; Version 15.0Qt&#xff1a;Qt 6.5.3 前言 Xcode 中显示 预览 QString 特别不方便, 而Qt官方的 lldb 脚本debugger/lldbbridge.py一直加载失败&#xff0c;其他第三方的脚本都 不兼容当前的 环境。所以自己研究写…

【how2j练习题】JS部分阶段练习

练习一 <!--练习&#xff0c;做一个简单的加法计算器--><html><input type"text" size "2" id "num1" ><input type"text" size "2" id "num2" ><input type"text" siz…

Density Profile Tool 程序(1D):通过 VMD 可计算 LAMMPS 轨迹的数密度分布(二)

​ 给大家推荐一个结构轨迹后处理程序 Density Profile Tool&#xff0c;目前尝试过的轨迹文件只有LAMMPS文件&#xff0c;感兴趣的大家可以试试其他轨迹文件。这个后处理软件可以计算数密度、质量、电荷和电子分布等性质。 感谢论文的原作者&#xff01; VMD 插件&#xff1…

stm32之GPIO电路介绍

文章目录 1 GPIO介绍2 GPIO的工作模式2.1 浮空输入2.2 上拉输入2.3 下拉输入2.4 模拟输入2.5 开漏输出2.6 推挽输出2.7 复用开漏输出2.8 复用推挽输出2.9 其他 3 应用方式4 常用库函数 1 GPIO介绍 保护二极管&#xff1a;保护引脚&#xff0c;让引脚的电压位于正常的范围施密特…

基于java校园在线打印预约系统设计与实现

摘 要 二十一世纪以来&#xff0c;计算机行业应用技术不断发展&#xff0c;人们的观念也在不断改变。传统打印行业&#xff0c;用户已经意识到传统的打印文件方法等待时间太长。校园在线打印预约系统可以通过网络来打印文件&#xff0c;用户可以在特定的时间预约打印文件&#…

设计模式中的UML基础

设计模式中的UML基础 目录 1、UML概述 2、UML的用途 3、UML的构成 4、UML图 5、UML类图 5.1、类的构成 5.2、类与类之间的关系 6、绘制UML图的软件工具 在讲解设计模式时&#xff0c;会使用到UML建模中的类图去讲解类与类之间的关系&#xff0c;所以这里需要给大家普…