一命通关深度优先遍历


前言

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

为什么我们在学习基础数据结构的时候,都没有出现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;
                }
            }
        }
    }
};

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
2048游戏的通关条件是达到2048分或者出现了一个数字为2048的方块。以下是一个简单的示例代码: ``` #include <stdio.h> #include <stdlib.h> #include <time.h> #define SIZE 4 #define WIN_SCORE 2048 int board[SIZE][SIZE]; // 保存游戏面板的二维数组 // 初始化游戏面板 void init_board() { srand(time(NULL)); // 设置随机数种子 for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { board[i][j] = 0; // 将游戏面板上的所有方块置为0 } } // 随机生成两个数字为2的方块 int count = 0; while (count < 2) { int x = rand() % SIZE; int y = rand() % SIZE; if (board[x][y] == 0) { board[x][y] = 2; count++; } } } // 在游戏面板上随机生成一个数字为2或4的方块 void generate_block() { int x, y; do { x = rand() % SIZE; y = rand() % SIZE; } while (board[x][y] != 0); // 随机生成的位置必须是空的 board[x][y] = (rand() % 2 + 1) * 2; // 生成2或4的方块 } // 判断游戏是否结束 int is_game_over() { for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { if (board[i][j] == WIN_SCORE) { // 出现了一个数字为2048的方块,游戏胜利 return 1; } if (board[i][j] == 0) { // 还有空位,游戏未结束 return 0; } if (i < SIZE - 1 && board[i][j] == board[i+1][j]) { // 上下相邻的方块值相等,游戏未结束 return 0; } if (j < SIZE - 1 && board[i][j] == board[i][j+1]) { // 左右相邻的方块值相等,游戏未结束 return 0; } } } return 1; // 游戏结束 } // 向左移动所有方块 void move_left() { for (int i = 0; i < SIZE; i++) { int last_merged = -1; // 上一个合并的方块的位置 for (int j = 1; j < SIZE; j++) { if (board[i][j] == 0) { // 空方块,跳过 continue; } int k = j - 1; while (k >= 0 && board[i][k] == 0) { // 向左移动方块 board[i][k] = board[i][k+1]; board[i][k+1] = 0; k--; } if (k >= 0 && board[i][k] == board[i][k+1] && k != last_merged) { // 合并相邻的相同方块 board[i][k] *= 2; board[i][k+1] = 0; last_merged = k; } } } } // 打印游戏面板 void print_board() { printf("\n"); for (int i = 0; i < SIZE; i++) { for (int j = 0; j < SIZE; j++) { printf("%d\t", board[i][j]); } printf("\n"); } } int main() { init_board(); // 初始化游戏面板 print_board(); // 打印游戏面板 while (!is_game_over()) { char c = getchar(); if (c == 'w') { // 向上移动 // TODO } else if (c == 's') { // 向下移动 // TODO } else if (c == 'a') { // 向左移动 move_left(); generate_block(); print_board(); } else if (c == 'd') { // 向右移动 // TODO } } printf("\nCongratulations! You win the game!\n"); return 0; } ``` 注意:上述代码仅实现了向左移动方块的功能,还需要实现向上、向下、向右移动的功能,这里留给读者自行实现。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值