核心模型
模型一:全排列
适用场景
-
需要排列所有元素(顺序不同算不同)
-
例如:数字排列、字符串排列、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 ,有两种独立的可能性:
-
选中它(放入子集)
-
不选中它(跳过它)
这两种情况都会继续递归处理下一个数字,所以是两个不同的递归分支
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走迷宫---走最多的安全瓷砖

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 岛屿问题

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;
}