C++ 深度优先搜索(DFS)详解
深度优先搜索(Depth-First Search,简称 DFS)是一种 "先走到底、再回头" 的遍历 / 搜索算法,核心思想是优先沿着一条路径探索到底,直到无法前进时回溯,再选择其他未探索的分支。它在 C++ 中广泛应用于图遍历、组合枚举、迷宫求解、树的遍历等场景,实现方式主要有递归(最直观)和栈模拟(非递归)两种。
核心原理:
DFS 的递归实现本质是利用函数调用栈完成 "回溯",核心步骤可概括为:
- 选择路径:从当前节点选择一条未走过的路径前进;
- 递归探索:对新节点重复上述选择,直到触发 "终止条件"(如到达目标、越界、路径重复);
- 回溯恢复:探索完当前路径后,撤销当前选择(恢复状态),回到上一节点选择其他路径。
例题:
题目: 弹簧长廊
cpp### 描述 学校里有一条由 n 个房间首尾排成的长廊,房间编号为 1,2,...,n。小 A 从房间 1 出发, 目标是到达房间 n。 对于每个房间 i(1 ≤ i < n),都安装了一组弹簧装置。若当前站在房间 i, 则他可以一跃最多跳 aᵢ 步,也就是可以跳到下面任意一个房间: i+1, i+2, ..., min(n, i+aᵢ) 其中 aᵢ 由输入给出;若 aᵢ = 0,则表示从房间 i 无法继续前进。 请你计算:从房间 1 到房间 n 一共有多少种不同的走法。由于答案可能很大, 请对 10⁹ + 7 取模后输出。 --- ### 输入描述 第一行输入一个整数 n。 第二行输入 n−1 个整数 a₁,a₂,...,a_{n−1}。 数据范围: - 1 ≤ n ≤ 3000 - 0 ≤ aᵢ ≤ 3000 - Σ_{i=1}^{n−1} aᵢ ≤ 20000 --- ### 输出描述 输出一行,一个整数,表示不同走法对 10⁹ + 7 取模后的结果。 --- ### 用例输入 1 6 ### 用例输出 1 7 --- ### 提示 样例解释: 所有合法走法分别是: 1. 1 → 2 → 3 → 4 → 5 → 6 2. 1 → 2 → 3 → 4 → 6 3. 1 → 2 → 4 → 5 → 6 4. 1 → 2 → 4 → 6 5. 1 → 2 → 5 → 6 6. 1 → 3 → 4 → 5 → 6 7. 1 → 3 → 4 → 6 因此答案为 7。思路:使用深度搜索并使用记忆化缩减时间复杂度,注意每个地方都要多mod取余
代码:
cpp#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=3010,mod=1e9+7; ll n,a[maxn],vis[maxn],ans; ll dfs(ll x){ if(x==n)return 1; if(vis[x]!=-1)return vis[x]%mod; vis[x]=0; for(int i=1;i<=a[x];i++){ vis[x]=(vis[x]+dfs(x+i))%mod; } return vis[x]%mod; } void sol(){ cin>>n; for(int i=1;i<n;i++)cin>>a[i]; memset(vis,-1,sizeof(vis)); cout<<dfs(1)%mod<<endl; } int main(){ sol(); return 0; }
题目:黄金矿工
cpp### 描述 给定一个由0和1组成的矿洞,共有n行m列。 - 1 表示这个格子里有 1 枚金币; - 0 表示这个格子里没有金币。 如果两个有金币的格子在上下左右四个方向之一相邻,则它们属于同一个金币区域。 由于挖矿很费时间,你今天只能选择一个金币区域进入,然后挖走里面的全部金币。 请你求出最多能拿到多少枚金币。 --- ### 输入描述 第一行输入两个整数,表示地图的行数和列数:n, m 接下来 n 行,每行一个长度为 m 的仅由 0 和 1 组成的字符串,表示整张地图。 数据范围: 1 <= n, m <= 200 --- ### 输出描述 输出一个整数,表示只选择一个金币连通块时,最多能拿到的金币数。 --- ### 用例输入 1 5 6 001100 001110 000010 110000 110100 ### 用例输出 1 6思路:和上一题《弹簧长廊》一样使用深搜,在dfs函数里加入对每个方向的递归深搜
代码:
cpp#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=1e5+10; ll n,m,ans,vis[205][205],cnt; char s[205][205]; void dfs(ll x,ll y){ if(x<1 || y<1 || x>n || y>m || s[x][y]=='0' || vis[x][y])return; cnt++; vis[x][y]=1; dfs(x-1,y); dfs(x+1,y); dfs(x,y-1); dfs(x,y+1); } void sol(){ cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>s[i][j]; } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(!vis[i][j] && s[i][j]=='1'){ cnt=0; dfs(i,j); ans=max(ans,cnt); } } } cout<<ans<<endl; } int main(){ sol(); return 0; }
题目:坤坤打篮球
cpp### 描述 一天,坤坤穿着背带裤和朋友们出来打球。休息时,他们n个人坐成一圈,在玩一个游戏。 一开始球在坤坤(1号)手上,每一回合,拿着球的人都可以把球传给左边或者右边的人。 现在坤坤想知道,球传了m回合之后,又回到了自己手上的传球方式有多少种呢? 两种传球方法被视作不同的方法,当且仅当这两种方法中, 接到球的人按接球顺序组成的序列是不同的。 例如3个人传3回合,一共有2种不同方式,使得最后又传回1号: - 1->2->3->1 - 1->3->2->1 --- ### 输入描述 输入两个数,分别代表n和m,空格隔开。 其中 n≤30, m≤30。 --- ### 输出描述 输出一个数,代表满足题意的传球方式数量。 --- ### 用例输入 1 3 3 ### 用例输出 1 2思路:主要是计算出在一个圆中如何计算当前拿球的人是第几个
代码:
cpp#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=35; ll n,m,vis[maxn][maxn]; ll dfs(ll id,ll k){//id:当前拿球的人 k:已经传了多少回合 if(k==m)return id==1; if(vis[id][k]!=-1)return vis[id][k]; vis[id][k]=0; vis[id][k]+=dfs(id%n+1,k+1);//传给右边的人 vis[id][k]+=dfs((id-2+n)%n+1,k+1);//传给左边的人 return vis[id][k]; } void sol(){ cin>>n>>m; memset(vis,-1,sizeof(vis)); cout<<dfs(1,0)<<endl; } int main(){ sol(); return 0; }
题目:坤坤打篮球2
cpp### 描述 一天,坤坤穿着背带裤和朋友们出来打球。休息时,他们n个人坐成一圈,在玩一个游戏。 一开始球在坤坤(1号)手上,每一回合,拿着球的人都可以把球传给左边或者右边的人。 已知每个人都有一个正整数得分 \(a_i\)。如果某次传球后,球传到了编号为 \(i\) 的人手中, 那么本次传球获得的得分就是 \(a_i\)。 现在坤坤想知道,球传了 \(k\) 回合之后,刚好回到自己手上时最大得分之和是多少? 注意:第 \(k\) 次传球后如果球回到了1号手中,那么这一次仍然要计入 \(a_1\) 的得分。 --- ### 输入描述 第一行输入两个整数 \(n, k\)。 第二行输入 \(n\) 个正整数 \(a_1, a_2, \dots, a_n\)。 数据范围: - \(2 \le n \le 10\) - \(1 \le k \le 100\) - \(1 \le a_i \le 10^9\) --- ### 输出描述 输出一个整数,表示答案。 如果不存在满足条件的传球序列,输出 -1。 --- ### 用例输入 1 5 6 5 9 1 7 4 ### 用例输出 1 42 --- ### 用例输入 2 4 3 3 1 2 8 ### 用例输出 2 -1 --- ### 提示 样例一解释: 一种最优传球方案为: \[1 \rightarrow 2 \rightarrow 1 \rightarrow 2 \rightarrow 1 \rightarrow 2 \rightarrow 1\] 对应得到的总分为: \[a_2 + a_1 + a_2 + a_1 + a_2 + a_1 = 9 + 5 + 9 + 5 + 9 + 5 = 42\] 最后一次传球回到1号,仍然要计入 \(a_1\)。思路:需要比上一题多用一个sum来计算最大得分和,记得要加上max来取最大值
代码:
cpp#include<bits/stdc++.h> using namespace std; typedef long long ll; const ll maxn=1e5; ll n,k,a[maxn],vis[1001][1001]; ll dfs(ll id,ll step){//id:当前拿球的人,step:已经传了 if(step==k)return id==1?0:-1e18; if(vis[id][step]!=-1)return vis[id][step]; vis[id][step]=-1; ll sum=-1e18;//sum:当前传球方式的得分之和 ll l=id%n+1,r=(id-2+n)%n+1; sum=max(sum,dfs(l,step+1)+a[l]);//传给左边的人 sum=max(sum,dfs(r,step+1)+a[r]);//传给右边的人 return vis[id][step]=sum; } void sol(){ cin>>n>>k; for(ll i=1;i<=n;i++)cin>>a[i]; memset(vis,-1,sizeof(vis)); if(dfs(1,0)<0)cout<<-1<<endl; else cout<<dfs(1,0)<<endl; } int main(){ sol(); return 0; }
题目:洛谷 P13015 [GESP202506 六级] 学习小组
cpp# P13015 [GESP202506 六级] 学习小组 ## 题目背景 对应的选择、判断题:<https://ti.luogu.com.cn/problemset/1186> ## 题目描述 班主任计划将班级里的 $ n $ 名同学划分为若干个学习小组,每名同学都需要分入 某一个学习小组中。观察发现,如果一个学习小组中恰好包含 $ k $ 名同学,则该学习小组的 讨论积极度为 $ a_k $。 给定讨论积极度 $ a_1, a_2, \ldots, a_n $,请你计算将这 $ n $ 名同学划分为学习小组 的所有可能方案中,讨论积极度之和的最大值。 ## 输入格式 第一行,一个正整数 $ n $,表示班级人数。 第二行,$ n $ 个非负整数 $ a_1, a_2, \ldots, a_n $,表示不同人数学习小组 的讨论积极度。 ## 输出格式 输出共一行,一个整数,表示所有划分方案中,学习小组讨论积极度之和的最大值。 ## 输入输出样例 #1 ### 输入 #1 ``` 4 1 5 6 3 ``` ### 输出 #1 ``` 10 ``` ## 输入输出样例 #2 ### 输入 #2 8 0 2 5 6 4 3 3 4 ### 输出 #2 12 ## 说明/提示 对于 $40\%$ 的测试点,保证 $1\le n\le 10$。 对于所有测试点,保证 $1\le n\le 1000$,$0\le a_i\le 10^4$。思路:推导出状态转移方程
代码:
cpp#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=1005; ll n,a[maxn],vis[maxn]; ll dfs(int x){ if(x==0)return 0;//分完了 if(vis[x]!=-1)return vis[x]; ll ans=-1e18; for(int i=1;i<=x;i++){ ans=max(ans,dfs(x-i)+a[i]);//分出一个小组,剩下的继续分 } return vis[x]=ans;//记忆化 } void sol(){ cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; memset(vis,-1,sizeof(vis)); cout<<dfs(n)<<endl; } int main(){ sol(); }
题目:aki大盗
cpp### 描述 一名经验丰富的大盗,打算洗劫一条街上的店铺。 这条街上一共有 N 家店铺,每家店中都有一些现金。大盗事先踩点得知,如果他同时洗劫 两家相邻的店铺,街上的报警系统就会自动启动,然后警察就会蜂拥而至。 作为会编程的你,为了帮助警察破案,请你计算下,这名大盗今晚最多可以盗窃多少现金。 --- ### 输入描述 输入的第一行是一个整数 T,表示一共有 T 组数据。 接下来的每组数据,第一行是一个整数 N,表示一共有 N 家店铺。 第二行是 N 个被空格分开的正整数,表示每一家店铺中的现金数量。每家店铺中的现金数量 均不超过 1000。 数据范围与提示: T ≤ 50, 1 ≤ N ≤ 100000 --- ### 输出描述 对于每组数据,输出一行。该行包含一个整数,表示大盗在不惊动警察的情况下可以得到的现金数量。 --- ### 用例输入 1 2 3 1 8 2 4 10 7 6 14 ### 用例输出 1 8 24思路:由于洗劫相邻的两家就会被发现,所以递归得写两种取最大值
代码:
cpp#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=1e5+10; ll t,n,a[maxn],vis[maxn]; ll dfs(ll x){ if(x>n)return 0; if(vis[x]!=-1)return vis[x]; vis[x]=-10; vis[x]=max(dfs(x+1),dfs(x+2)+a[x]); return vis[x]; } void sol(){ cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; memset(vis,-1,sizeof(vis)); cout<<dfs(1)<<endl; } int main(){ cin>>t; while(t--){ sol(); } return 0; }