深度优先搜索DFS

核心模型

模型一:全排列

适用场景

  • 需要排列所有元素(顺序不同算不同)

  • 例如:数字排列、字符串排列、N 皇后(每行选一列)

cpp 复制代码
void dfs(int x) {  // x: 当前要填第几个位置
    if (x > n) {
        // 得到一个完整排列
        return;
    }
    for (int i = 1; i <= n; i++) {
        if (!used[i]) {          // 数字 i 还没用过
            used[i] = true;
            path[x] = i;
            dfs(x + 1);
            used[i] = false;     // 回溯
        }
    }
}

特点

  • used[] 记录数字/元素有没有被用过

  • 每个位置都要填一个数,所有位置填满才结束

  • 循环遍历所有可能的值

模型二:子集/组合

适用场景

  • 从 n 个元素中选若干个(顺序不重要)

  • 例如:选调料、选物品、选人参加活动

这是选或不选模型

cpp 复制代码
void dfs(int x) {  // x: 当前考虑第几个元素
    if (x > n) {
        // 得到一种选择方案
        return;
    }
    
    // 选第 x 个
    choose[x] = 1;
    dfs(x + 1);
    
    // 不选第 x 个
    choose[x] = 0;
    dfs(x + 1);
}

模型三:组合数

适用场景

  • 从 n 个元素中选 k 个(顺序不重要)

  • 例如:选 k 个数、选 k 个队员

这是start模型

cpp 复制代码
void dfs(int x, int start) {  // x: 已经选了几个;start: 下一个从哪开始选
    if (x == k) {
        // 得到一个组合
        return;
    }
    //枚举每个数
	for(int i=start;i<=n;i++){
			arr[x]=i;
			//遍历下一个位置
			dfs(x+1,i+1);
			//恢复现场
			arr[x]=0;
		}
}

特点

  • start 参数控制枚举起点,避免重复

  • 不记录"用没用过",因为 start 保证了顺序

  • 典型应用:C(n, k) 组合数枚举

写法 适用场景 特点
选或不选 子集问题(每个元素独立决策) 两个分支,不需要循环
start 参数 组合数问题(选恰好 k 个,顺序无关) 用循环 + start 保证不重复

模型四:N 皇后 / 棋盘问题

适用场景

  • 在棋盘/网格上放棋子,每行/每列/对角线有约束

  • 例如:N 皇后、数独、八数码

cpp 复制代码
void dfs(int row) {  // row: 当前要放第几行
    if (row > n) {
        // 找到一种解法
        return;
    }
    for (int col = 1; col <= n; col++) {
        if (isValid(row, col)) {   // 检查能否放
            place(row, col);       // 放棋子
            dfs(row + 1);          // 去下一行
            remove(row, col);      // 拿掉棋子
        }
    }
}

特点

  • 每行选一列(类似全排列)

  • 但约束条件更复杂(对角线、列不能重复)

  • colUsed[] 记录哪些列被占,用 diag1[] diag2[] 记录对角线

问题类型 核心特征 循环方式 状态记录
全排列 顺序重要,所有元素 for i 1..n + if(!used[i]) used[i]
子集 选或不选,顺序无关 两个分支(选/不选) 可选 choose[]
组合数 选 k 个,顺序无关 for i start..n + 递归传 i+1 start 控制
N 皇后 每行选一列,有约束 for col 1..n + if(valid) colUsed[]diag[]

1.补充知识点

cin,cout,printf,scanf区别

记一些常用的数值

杨辉三角和组合系数的关系

2.例题

2.1 从1-n个整数中随机选取任意多个

思路

对于当前数字 x ,有两种独立的可能性:

  1. 选中它(放入子集)

  2. 不选中它(跳过它)

这两种情况都会继续递归处理下一个数字,所以是两个不同的递归分支

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=20;
typedef long long ll;
int n;
//记录每个树的状态,0表示还没有考虑,1表示选这个数,2表示不选这个数
int st[N];
void dfs(int x){//x表示当前枚举到了哪个位置
	//终止标志
	if(x>n){
		for(int i=1;i<=n;i++){
			//如果选了就输出
			if(st[i]==1)cout<<i<<" ";
		}
		cout<<'\n';
		//一定要退出这次深搜
		return;
	}
	//选
	st[x]=1;
	dfs(x+1);
	st[x]=0;//恢复现场
	//不选
	st[x]=2;
	dfs(x+1);
	st[x]=0;//恢复现场
}
int main(){
	//输入输出
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	cin>>n;
	//深搜,
	dfs(1);
	
	return 0;
}

2.2全排列

思路

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=20;
typedef long long ll;
int n;
//记录每个数的状态,0表示还没有选,1表示选这个数
bool st[N];
int arr[N];//存的是答案
void dfs(int x){//x表示当前枚举到了哪个位置
	//终止标志
	if(x>n){
		for(int i=1;i<=n;i++){
			cout<<arr[i]<<" ";
		}
		cout<<'\n';
		//一定要退出这次深搜
		return;
	}
	//枚举每个数
	for(int i=1;i<=n;i++){
		if(!st[i]){
			st[i]=true;
			//如果这个数没有选过,将这个位置放这个数
			arr[x]=i;
			//遍历下一个位置
			dfs(x+1);
			//恢复现场
			st[i]=false;
			arr[x]=0;
		}
	}

}
int main(){
	//输入输出
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	cin>>n;
	//深搜,
	dfs(1);
	return 0;
}

2.3组合:从n个数中选k个

思路

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=20;
typedef long long ll;
int n,r;
int arr[N];//存的是答案
//start表示从哪个位置开始
void dfs(int x,int start){//x表示当前枚举到了哪个位置
	//终止标志
	if(x>r){
		for(int i=1;i<=r;i++){
			cout<<arr[i]<<" ";
		}
		cout<<'\n';
		//一定要退出这次深搜
		return;
	}
	//枚举每个数
	for(int i=start;i<=n;i++){
			arr[x]=i;
			//遍历下一个位置
			dfs(x+1,i+1);
			//恢复现场
			arr[x]=0;
		}
}
int main(){
	//输入输出
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n;cin>>r;
	//深搜,
	dfs(1,1);
	return 0;
}

2.4从n个数选k个,且和为素数

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=20;
typedef long long ll;
int n,r;
int arr[N];
int sum=0;
int ans=0;
bool isSu(int sum1){
	if(sum1==2)return true;
	for(int i=2;i<=sqrt(sum1);i++){
		if(sum1%i==0)return false;
	}
	return true;
}
//start表示从哪个位置开始
void dfs(int x,int start){//x表示当前枚举到了哪个位置
	//剪枝操作
	//(x-1)是前面选了几个位置
	//(n-start+1)是还剩几个数可以选 
	//if((x-1)+(n-start+1)<r)return;
	// 剪枝:已选数量 + 剩余数量 < 需要数量
	if(x + (n - start) < r) return;
	
	// 终止标志
	if(x == r){
		if(isSu(sum)){
			ans++;
		}
		return;  // 不管是不是素数,都要返回
	}
	//枚举每个数
	for(int i=start;i<n;i++){
	
			sum+=arr[i];
			//遍历下一个位置
			dfs(x+1,i+1);
			//恢复现场
			sum-=arr[i];
		}
	

}
int main(){
	//输入输出
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	cin>>n;cin>>r;
	int num=0;
	for(int i=0;i<n;i++){
		cin>>num;
		arr[i]=num;
	}
	//深搜,
	dfs(0,0);
	cout<<ans<<'\n';
	return 0;
}

法二这个方法更好

cpp 复制代码
#include<bits/stdc++.h>
 using namespace std;
 const int N=2000;
 typedef long long ll;
// 全局变量
 int ans=0;
 vector<vector<int>>result;
 vector<int>path;
 bool isSu(int m){
 	for(int i=2;i<=sqrt(m);i++){
 		if(m%i==0)return false;
 	}
 	return true;
 }
 void backtracking(int n,int k,int num[],int start){
 	if((int)path.size()==k){
 		//判断是否合法
 		int sum=0;
 		for(int j=0;j<k;j++){
 			sum+=path[j];
 		}
 		if(isSu(sum))result.push_back(path);
 		return;
 	}
 	for(int i=start;i<n;i++){
 		path.push_back(num[i]);
 		backtracking(n,k,num,i+1);
 		path.pop_back();
 	}
 }

 int main(){
 	//禁止stdio同步,提高输入输出效率
 	ios::sync_with_stdio(0);
 	cin.tie(0);
 	cout.tie(0);
    int n;cin>>n;
 	int k;cin>>k;
 	int num[N];
 	for(int i=0;i<n;i++){
 		cin>>num[i];
 	}
 	backtracking(n,k,num,0);
 	cout<<result.size()<<'\n';
 	return 0;
 }

2.5 搭配方案

python 复制代码
#include<bits/stdc++.h>
 using namespace std;
 const int N=20;
 typedef long long ll;
// 全局变量
 int ans=0;//方案数
int path[N];//装方案数
int sum=0;
int n;
//x表示当前枚举到了哪个位置,sum表示当前已经选的调料总质量
 void dfs(int x,int sum){
	 if(sum>n)return;//剪枝操作
	 	 //停止条件
	 if(x>10){
		 if(sum==n){
			 ans++;
			 for(int i=1;i<=10;i++){
				 cout<<path[i]<<" ";
			 }
			 cout<<'\n';
		 }
	//一定要return
		 return;
	 }
 for(int i=1;i<=3;i++){
	 path[x]=i;
	 sum+=i;
	 dfs(x+1,sum);
	 path[x]=0;
	 sum-=i;
 }
 }

 int main(){
 	//禁止stdio同步,提高输入输出效率
 	ios::sync_with_stdio(0);
 	cin.tie(0);
 	cout.tie(0);
    cin>>n;
 	dfs(1,0);
	 cout<<ans;
 	return 0;
 }

2.6固定位数的数字全排列,第x个序列

//本题代码的关键,要从给过的序列之后进行全排列
if(!ans)i=mars[x];

cpp 复制代码
#include<bits/stdc++.h>
 using namespace std;
 const int N=10010;
 typedef long long ll;
// 全局变量
int n,m;
 int ans=0;//方案数
int path[N];//装方案数
int mars[N];//记录火星人的初始排列
bool st[N];//记录每个数选没选过
bool find1=false;//记录找没找到答案
//x表示当前枚举到了哪个位置
 void dfs(int x){
	 //剪枝操作
	 if(find1)return;//已经找到了就剪枝
	 //停止条件
	 if(x>n){
		 ans++;
		 if(ans==m+1){
			 find1=true;//记录已经找到了
			 for(int i=1;i<=n;i++){
				 cout<<path[i]<<" ";
			 }
			 cout<<'\n';
		 }
		 return;
	 }
	for(int i=1;i<=n;i++){
		//本题代码的关键,要从给过的序列之后进行全排列
		if(!ans)i=mars[x];
		if(!st[i]){//如果这个数没有选过
			path[x]=i;//表示位置x已经占用了
			st[i]=true;//表示数字i已经被用过了
			dfs(x+1);
			//恢复现场
			path[x]=0;
			st[i]=false;
		}
	}
 }

 int main(){
 	//禁止stdio同步,提高输入输出效率
 	ios::sync_with_stdio(0);
 	cin.tie(0);
 	cout.tie(0);
    cin>>n;cin>>m;
	 for(int i=1;i<=n;i++)cin>>mars[i];
	 dfs(1);
 	
 	return 0;
 }

2.7火柴组成的数字,枚举

P1149 [NOIP 2008 提高组] 火柴棒等式 - 洛谷力扣题目

cpp 复制代码
#include<bits/stdc++.h>
 using namespace std;
 const int N=10010;
 typedef long long ll;
// 全局变量
int n;
int ans=0;//方案数
int nums[10010]={6,2,5,5,4,5,6,3,7,6};//存火柴棍
int path[4];//记录答案A,B,C 3个数
//x表示当前枚举到了哪个位置
//sum表示火柴棍的和
 void dfs(int x,int sum){
	 //剪枝操作,如果还没到第3个数,和已经超过总火柴数就结束
	 if(sum>n)return;
	 //停止条件
	 if(x>3){
		 if(path[1]+path[2]==path[3]&&sum==n)ans++;
		 return;
	 }
	//直接暴力枚举
	 for(int i=0;i<=1000;i++){
		 path[x]=i;
		 sum+=nums[i];
		 dfs(x+1,sum);
		 path[x]=0;
		 sum-=nums[i];
	 }
 }

 int main(){
 	//禁止stdio同步,提高输入输出效率
 	ios::sync_with_stdio(0);
 	cin.tie(0);
 	cout.tie(0);
    cin>>n;
	 //把+=4个火柴棍去掉
	 n-=4;
	 //可以利用递推式将每个数字包含的火柴棍,算出来
	 //eg,199=9+19;9,19之前已经算过了,所以直接相加便是结果
	 for(int i=10;i<=1000;i++){
		 nums[i]=nums[i%10]+nums[i/10];
	 }
	 dfs(1,0);
 	cout<<ans;
 	return 0;
 }

2.8 调料问题,选/没选

P2036 [COCI 2008/2009 #2] PERKET - 洛谷

cpp 复制代码
#include<bits/stdc++.h>
 using namespace std;
 const int N=20;
 typedef long long ll;
// 全局变量
int n;
int s[N],b[N];
//因为要求差值最小,那么就要初始赋值为最大的
int ans=1e9;
bool st[N];//0表示不选,1表示选,
//x表示当前枚举到了哪个位置
 void dfs(int x){
	 //停止条件,说明位置已经枚举完了
	 if(x>n){
		 int suan=1,ku=0;
		  bool hasSelected = false;  // 记录是否选了至少一种
		 for(int i=1;i<=n;i++){
			 if(st[i]){
				 suan*=s[i];
				 ku+=b[i];
				 hasSelected=true;
			 }		
		 }
		 //如果至少选了一种,那么就计算最小值
          if(hasSelected) ans = min(ans, abs(suan - ku));
		 return;
	 }
	 //选
	 st[x]=1;
	 dfs(x+1);
	 //不选
	 st[x]=0;
	 dfs(x+1);
 }
 int main(){
 	//禁止stdio同步,提高输入输出效率
 	ios::sync_with_stdio(0);
 	cin.tie(0);
 	cout.tie(0);
    cin>>n;
	for(int i=1;i<=n;i++)
		cin>>s[i]>>b[i];
	dfs(1);
	 cout<<ans<<'\n';
 	return 0;
 }

2.9走迷宫---走最多的安全瓷砖

P1683 入门 - 洛谷

cpp 复制代码
#include<bits/stdc++.h>
 using namespace std;
 const int N=20;
 typedef long long ll;
// 全局变量
int n,m;//n行m列
char g[N][N];//记录地图
int res=0;//统计安全的瓷砖数
bool st[N][N];//记录每个瓷砖是否走过
int dx[]={0,0,-1,1};//左右上下四个位置进行遍历
int dy[]={-1,1,0,0};


 void dfs(int x,int y){
	 
	 for(int i=0;i<4;i++){
		 int a=x+dx[i];
		 int b=y+dy[i];
		 //检查遍历的位置是否合法
		 if(a<0||a>=n||b<0||b>=m)continue;
		 //检查是否选过
		 if(st[a][b])continue;
		 //检查是否是安全的瓷砖
		 if(g[a][b]!='.')continue;
		 //可以走这个点了
		 st[a][b]=true;
		 res++;
		 dfs(a,b);
	 }
 }

 int main(){
 	//禁止stdio同步,提高输入输出效率
 	ios::sync_with_stdio(0);
 	cin.tie(0);
 	cout.tie(0);
    cin>>m;cin>>n;
	 //直接扫描一行字符串
    for(int i=0;i<n;i++)
		cin>>g[i];
	 for(int i=0;i<n;i++){
		 for(int j=0;j<m;j++){
			 //开始的起点进行深搜
			 if(g[i][j]=='@'){
				 //把这个也标记走过了
				 st[i][j]=true;
				 res=1;
				 dfs(i,j);
			 }
		 }
	 }
	 cout<<res<<'\n';
	 
 	return 0;
 }

2.10 岛屿问题

https://www.lanqiao.cn/problems/178/learning/?page=1&first_category_id=1&second_category_id=3&difficulty_level=5

cpp 复制代码
#include<bits/stdc++.h>
 using namespace std;
 const int N=1010;
 typedef long long ll;
// 全局变量
int n;//n行n列
char g[N][N];//记录地图
int res=0;//统计淹没的岛屿
bool st[N][N];//记录每个小岛是否走过
int dx[]={0,0,-1,1};//左右上下四个位置进行遍历
int dy[]={-1,1,0,0};
bool flag=false;//判断岛屿是否安全

 void dfs(int x,int y){
	 //判断当前格子是否安全
	 if(g[x][y-1]=='#'&&g[x][y+1]=='#'&&g[x+1][y]=='#'&&g[x-1][y]=='#')
		 flag=true;//只要有一个安全的岛屿就不会被完全淹没
	 
	 for(int i=0;i<4;i++){
		 int a=x+dx[i];
		 int b=y+dy[i];
		 //检查遍历的位置是否合法
		 if(a<0||a>=n||b<0||b>=n)continue;
		 //检查是否选过
		 if(st[a][b])continue;
		 //检查是否是小岛
		 if(g[a][b]!='#')continue;
		 //可以走这个点了
		 st[a][b]=true;
		 
		 dfs(a,b);
	 }
 }

 int main(){
 	//禁止stdio同步,提高输入输出效率
 	ios::sync_with_stdio(0);
 	cin.tie(0);
 	cout.tie(0);
    cin>>n;
	 //直接扫描一行字符串
    for(int i=0;i<n;i++)
		cin>>g[i];
	 for(int i=0;i<n;i++){
		 for(int j=0;j<n;j++){
			 //开始的起点进行深搜
			if(g[i][j]=='#'&&!st[i][j]){
				flag=false;//假设这个岛屿没有幸存地
				dfs(i,j);
				//如果岛屿确实没有幸存陆地,计数
				if(!flag)res++;
			}
		 }
	 }
	
	 cout<<res;
 	return 0;
 }

2.11洪水连通性统计

P1596 [USACO10OCT] Lake Counting S - 洛谷

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=510;
typedef long long ll;
// 全局变量
unsigned long long  res=0;
int n,m;
char mp[N][N];
bool st[N][N];//标记走了没有
int ans=0;
int dx[]={-1,0,1,-1,0,1,-1,1};
int dy[]={-1,-1,-1,1,1,1,0,0};
void dfs(int x,int y){
	//事先就把它标记走过了
		st[x][y]=true;
	//遍历8个方向
	for(int i=0;i<8;i++){
		int a=x+dx[i],b=y+dy[i];
		if(a<1||a>n||b<1||b>m)continue;
		if(st[a][b])continue;
		if(mp[a][b]=='.')continue;
	
		dfs(a,b);
	}
}
int main(){
	//禁止stdio同步,提高输入输出效率
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
 
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>(mp[i]+1);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(!st[i][j]&&mp[i][j]=='W'){
				//发现陆地就可以++,因为dfs会把与它连通的陆地进行标记
				ans++;
				dfs(i,j);	
			}
		}
	}
	cout<<ans;
	return 0;
}

2.12下棋,方案数

这一题没有学会,代码也不通,呜呜呜呜~~~

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=510;
typedef long long ll;
// 全局变量
unsigned long long  res=0;
int n,k;
char mp[N][N];
int ans=0;
int cnt=0;
bool st[N];
//x表示当前遍历到了哪一行,cnt表示当前放了几棋子
void dfs(int x,int cnt){
	if(cnt==k){
		ans++;
		return ;
	}
	//如果将所有行遍历完毕之后就结束
	if(k>n)return;
	for(int i=0;i<n;i++){
		//遍历列,第i列没有放过棋子
		if(!st[x]&&mp[x][i]=='#'){
			st[i]=true;
			dfs(x+1,cnt+1);
			st[i]=false;
		}
		dfs(x+1,cnt);
	}
	//其实有个漏掉了最后一行遍历
	dfs(x+1,cnt);
}
int main(){
	//禁止stdio同步,提高输入输出效率
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
 
	//因为有多组实验数据
	//因为当识别到-1,-1就停止循环了
	while(cin>>n>>k,n>0&&k>0){
		 for(int i=0;i<n;i++){
			 cin>>mp[i];
		 }
		ans=0;//清0,每次使用时
		dfs(0,0);
		cout<<ans<<'\n';
	}
	return 0;
}

2.13 组合特殊可选重复数字

P1025 [NOIP 2001 提高组] 数的划分 - 洛谷

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=510;
typedef long long ll;
//思路
//类似组合,但是可以选重复的数,所以start不用++,另外还有附属条件和为n

// 全局变量
int n,k;
int ans=0;
int nowSum=0;
int a[N];//存方案内容
//x表示当前遍历到了哪一行,nowSum表示当前的和
void dfs(int x,int start,int nowSum){
	if(x>k){//先看位置是否已经到达
		if(nowSum==n){//再看和是否为n
			ans++;
			for(int i=1;i<=k;i++)cout<<a[i]<<" ";
			cout<<endl;
		}
		return;//一定要记得结束
	}
	//这里可以优化一下,根据和
	for(int i=start;nowSum+i*(k-x+1)<=n;i++){
		a[x]=i;
		dfs(x+1,i,nowSum+i);
		a[x]=0;
	}
}
int main(){
	//禁止stdio同步,提高输入输出效率
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
 
cin>>n>>k;
	dfs(1,1,0);
	cout<<ans;
	
	return 0;
}
相关推荐
小糯米6012 小时前
C++ 并查集
java·c++·算法
IronMurphy2 小时前
【算法三十四】39. 组合总和
算法·深度优先
重庆小透明2 小时前
力扣刷题【3】相交链表
算法·leetcode·链表
算法鑫探2 小时前
C语言实战:学生成绩统计与分析
c语言·数据结构·算法·新人首发
IAUTOMOBILE2 小时前
Code Marathon 项目源码解析与技术实践
java·前端·算法
Lyyaoo.2 小时前
【JAVA基础面经】深拷贝与浅拷贝
java·开发语言·算法
x_xbx2 小时前
LeetCode:202. 快乐数
算法·leetcode·职场和发展
老虎06272 小时前
LeetCode热题100 刷题笔记(第四天)二分 「 寻找两个正序数组的中位数」
笔记·算法·leetcode
_日拱一卒2 小时前
LeetCode:最小覆盖字串
java·数据结构·算法·leetcode·职场和发展