我看的b站上的这个视频,感觉讲得还不错~https://www.bilibili.com/video/BV1S24y1p7iH/?spm_id_from=333.880.my_history.page.click
递归实现指数型枚举
从 1∼n这 n个整数中 随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数 n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好 1个空格隔开。
对于没有选任何数的方案,输出空行。
本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。
数据范围
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3
(本来尝试用画图工具的,但我还是太拉了,没有ipad好伤
//这道题要求任意选取多个
//也就是对每一个数都有选或不选两种情况
//要求输出要按照字典序,
//因此我们遍历每一个数,选或不选,并保存其状态
//遍历每一个数字,选了就将其输出
感觉写这种题之前还是要把思路搞清楚才能写清楚。
#include <iostream>
#include <algorithm>
using namespace std;
const int N =20;
int n;
int st[N];//记录状态 0表示还没考虑,1表示选,2表示不选
void dfs(int x)
{
if(x>n)//递归退出条件
{
for(int i=0;i<=n;i++)
{
if(st[i]==1)
{
cout<<i<<" ";
}
}
cout<<endl;
return ;
}
st[x]=2;//先选
dfs(x+1);
st[x]=0;
st[x]=1;
dfs(x+1);
st[x]=0;
}
int main()
{
cin>>n;
dfs(1);
return 0;
}
递归实现排列型枚举
题目链接:P1706 全排列问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
按照字典序输出自然数 1 到 n 所有不重复的排列,即 n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
输入格式
一个整数 n。
输出格式
由 1∼n 组成的所有不重复的数字序列,每行一个序列。
每个数字保留
5 个场宽。
输入输出样例
输入 #1复制
3
输出 #1复制
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
首先分析一下这道题叭
C++中全排列函数next_permutation 用法_Marcus-Bao的博客-CSDN博客库函数牛逼
1.依次枚举每个位置应该放哪个数
#include <bits/stdc++.h>
using namespace std;
const int N=15;
int n;
bool st[N];
int arr[N];
//全排列问题,每个数字都要选,只是选的位置不同
//因此我们遍历每个位置,讨论选哪一个数
//同时记录在这次排列中,哪些数已经被选过了
//因为要输出所有全排列,因此我们用数组记录答案
void dfs(int x)//对位置进行遍历
{
if(x>n)
{
for(int i=1;i<=n;i++)
{
printf("%5d",arr[i]);
}
cout<<endl;
}
for(int i=1;i<=n;i++)//对每一个数字进行讨论
{
if(!st[i])//如果没有访问过当前数
{
st[i]=true;
arr[x]=i;//记录位置上填的数
dfs(x+1);
arr[x]=0;
st[i]=false;
}
}
}
signed main()
{
scanf("%d",&n);
dfs(1);
return 0;
}
STL牛逼
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int N=10;
int n;
int a[N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
a[i]=i;
}
do{
for(int i=1;i<=n;i++)
{
printf("%5d",a[i]);
}
printf("\n");
}while(next_permutation(a+1,a+n+1));
return 0;
}
递归实现组合型枚举
题目链接:P1157 组合的输出 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这题说人话,就是从n个数中取出r个,就是数学中的组合,注意数字不要求顺序。
简单画个示意图:
#include <bits/stdc++.h>
using namespace std;
const int N=15;
int n,r;
bool st[N];
int arr[N];
//从n个数中抽出r个数,不分顺序
//也就是只选r个数,即遍历r个位置
//对每一个位置遍历所有数,选或不选,选过就不再选,因此要记录状态
//最后要输出结果,用数组记录
//***这是一个组合问题,观察输出答案可以知道后面的数都会比第一位大
//***这样做才能保证不出现重复的
void dfs(int x,int start)//对位置进行遍历
{
if(x>r)
{
for(int i=1;i<=r;i++)
{
printf("%3d",arr[i]);
}
cout<<endl;
return;
}
for(int i=start;i<=n;i++)
{
arr[x]=i;
dfs(x+1,i+1);
arr[x]=0;
}
}
signed main()
{
cin>>n>>r;
dfs(1,1);
return 0;
}
简单分析一下这里为什么要传入两个参数
看运行结果明显错误的在后面出现了比他大的元素(如5,4,3),这就导致了组合出现多个。因此要额外传入一个start的限制,使其遍历后面的数字。(纯菜鸟,初学,求指点)
补充一下剪枝:
还是这个图,这里剪枝的原理是能选的数已经不够填充位置了,
小总结
对以上三种模型做个小总结:
指数型枚举时,每个数对应几种情况,就需要st数组记录是哪一种情况,每种情况都要在dfs中写出,最后遍历st数组输出需要的情况下的数。只用st
排列型枚举时,比如123的全排列,就需要对每个位置每个数进行遍历,要判断这个数是否被选择过,没有被选择过就需要更新状态同时保存答案,要用到st和arr
组合型枚举时,从n个数中取出r个,这种题是不要求顺序的,但最后输出要求按字典序;因此,我们还是遍历位置,对每一个位置上的数进行枚举,然后接着进行下一个位置,但注意枚举的数字会减少,因此加入start,同时arr保存答案。
选数
题目链接:P1036 [NOIP2002 普及组] 选数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这种不要求顺序的,求组合数都可以这样遍历位置,dfs传入两个参数记录位置和下一位遍历数
要求顺序的排列数问题,就需要加一个if语句判断是否使用过,同样的遍历位置进行递归
这题就是一个求组合数的变形题。。
#include <bits/stdc++.h>
using namespace std;
const int N=25;
int n,k;
int arr[N];
int b[N];
long long sum;
int ans;
//从n个数中任选k个整数相加,组合数问题
bool isprime(int x)
{
for(int i=2;i<=x/i;i++)
{
if(x%i==0) return false;
}
return true;
}
void dfs(int x,int start)//对位置进行遍历
{
if(x>k)
{ sum=0;
for(int i=1;i<=k;i++)
{
sum+=arr[i];
}
if(isprime(sum)) ans++;
return ;
}
for(int i=start;i<=n;i++)
{
arr[x]=b[i];
dfs(x+1,i+1);
arr[x]=0;
}
}
signed main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
scanf("%d",&b[i]);
}
dfs(1,1);
cout<<ans;
return 0;
}
这里学到了一个小细节 ,判断素数的函数里,一般是写成i*i<=x,这里为了防止爆数据写成i<=x/i
for(int i=2;i<=x/i;i++)
了解一下剪枝叭
就是图上的这种情况,比如3,19这条线没有数可选了,但位置还没有填满这种分枝剪掉就可以啦;
也就是再dfs里加上一个判断,删去 已选择数+可选择数<k 的分支
对于选数这道题,代码实现一下看看:
if((x-1)+n-start+1<k)//x代表当前位置,x-1求出已经选择数
{
return ;
}
#include <iostream>
#include <algorithm>
using namespace std;
const int N =30;
int n,k;
int arr[N];//存答案
int number[N];//存数组
int cnt=0;
bool isprime(int x)
{
for(int i=2;i<=x/i;i++)
{
if(x%i==0)
{
return false;
}
}
return true;
}
void dfs(int x,int start)//遍历位置 ,start代表应该从这开始遍历数
{
if((x-1)+n-start+1<k)
{
return ;
}
if(x>k)//要选k个数
{
int sum=0;
for(int i=1;i<=k;i++)
{
sum+=arr[i];
}
if(isprime(sum)) cnt++;
return ;
}
for(int i=start;i<=n;i++)//遍历数字
{
arr[x]=number[i];
dfs(x+1,i+1);
arr[x]=0;
}
}
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>number[i];//输入数据
}
dfs(1,1);
cout<<cnt<<endl;
return 0;
}
剪枝大概就是这样,比想象的要简单。同时,剪枝可以缩短很多时间哦~
烤鸡问题
题目链接:P2089 烤鸡 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
简单分析一下题目,10 种配料,每个配料1~3克,美味程度就在10~30这个区间。
10个配料,每个调料有3种选择,选择数就有3的10次方个,配料的选择之间没有关系,也就是指数型枚举。
依次枚举每个调料放几克
#include <iostream>
#include <cstdio>
using namespace std;
int n;
int res=0;//存方案数
int arr[11];//存临时方案
int mem[10001][11];
void dfs(int x,int sum)
{
if(sum>n) return;//剪枝
if(x>10)
{
if(sum==n)
{
res++;
for(int i=1;i<=10;i++)
{
mem[res][i]=arr[i];
}
}
return ;
}
for(int i=1;i<=3;i++)
{
arr[x]=i;
dfs(x+1,sum+i);
arr[x]=0;
}
}
int main()
{
cin>>n;
dfs(1,0);
cout<<res<<endl;
for(int i=1;i<=res;i++)
{
for(int j=1;j<=10;j++)
{
printf("%d ",mem[i][j]);
}
printf("\n");
}
return 0;
}
火星人
题目链接:P1088 [NOIP2004 普及组] 火星人 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include <iostream>
#include <cstdio>
using namespace std;
const int n=10010;
int N,M; //手指数目和要加上的小整数M<=100
int mars[n];//手指排列顺序
int arr[n];//记录方案
bool st[n];//记录状态
int res=0;
int cnt;
int flag=0;
void dfs(int x)//列举位置
{
if(flag) return ;
if(x>N)
{
res++;//完成了一次全排列
if(res==M+1)
{
flag=1;//只要找到了答案下一次就不用递归了
for(int i=1;i<=N;i++)
{
cout<<arr[i]<<" ";
}
}
return;
}
for(int i=1;i<=N;i++)//遍历数字
{
if(res==0)//这道题唯一的变化就在这,一次全排列都没有完成的时候让i=火星人原始的手指
{
i=mars[x];//第一次从火星人刚开始的手,即火星人的手指排列arr数组第一次存的数据
}
if(!st[i])
{
arr[x]=i;
st[i]=true;
dfs(x+1);//
arr[x]=0;
st[i]=false;
}
}
}
int main()
{
cin>>N>>M;
for(int i=1;i<=N;i++)
{
cin>>mars[i];
}
dfs(1);//直接从火星人目前手指开始枚举,枚举M次就可以了
return 0;
}
这题问了佬,知道了一种next_permutation库函数的方法,查了一下是c++的一个全排列函数
C++ STL全排列 next_permutation 用法 - 知乎 (zhihu.com)
火柴棒等式
题目链接:P1149 [NOIP2008 提高组] 火柴棒等式 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<iostream>
#include<stdio.h>
using namespace std;
int count[10010]={6,2,5,5,4,5,6,3,7,6};
int n;
int cnt=0;
int arr[1000];
void dfs(int x,int sum)
{
if(sum>n) return;
if(x>3)
{
if(sum==n&&arr[1]+arr[2]==arr[3]) cnt++;
return;
}
for(int i=0;i<=1000;i++)//遍历数字
{
arr[x]=i;
dfs(x+1,sum+count[i]);
arr[x]=0;
}
}
int main()
{
cin>>n;
n-=4;//减去加号和等号的火柴数
for(int i=10;i<=1000;i++)
{
count[i]=count[i%10]+count[i/10];
}
dfs(1,0);
cout<<cnt;
return 0;
}
PERKET
题目链接:P2036 [COCI2008-2009#2] PERKET - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
补一个求绝对值:(15条消息) C\C++ 中的绝对值函数:abs()、cabs()、fabs()、labs()_c++绝对值函数_YogLn的博客-CSDN博客
#include<bits/stdc++.h>
using namespace std;
const int N=15;
int n;//种类数
int s[N];
int b[N];
int res=1e9;
int st[N];
void dfs(int x)
{
if(x>n)
{
bool tl=false;
int sum1=1;
int sum2=0;
for(int i=1;i<=n;i++)
{
if(st[i]==1)
{
tl=true;
sum1*=s[i];
sum2+=b[i];
}
}
if(tl) res=min(res,abs(sum1-sum2));
return ;
}
st[x]=1;
dfs(x+1);
st[x]=0;
st[x]=2;
dfs(x+1);
st[x]=0;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&s[i],&b[i]);
}
dfs(1);
cout<<res<<endl;
return 0;
}
奇怪的电梯
题目链接:P1135 奇怪的电梯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<bits/stdc++.h>
using namespace std;
const int N=210;
int n,a,b;//n总楼层,从a楼到b楼
int k[N];
int res=1e9;
bool st[N];//存每层楼走没走过
void dfs(int x,int cnt)//当前按了cnt次按钮
{
//剪枝
if(cnt>=res) return;
if(x<0||x>n) return ;
if(x==b)
{
res=min(res,cnt);
return;
}
st[x]=true;
if(x+k[x]<=n&&!st[x+k[x]])//上
{
st[x+k[x]]=true;
dfs(x+k[x],cnt+1);
st[x+k[x]]=false;
}
if(x>k[x]&&!st[x-k[x]])//下
{
st[x-k[x]]=true;
dfs(x-k[x],cnt+1);
st[x-k[x]]=false;
}
}
int main()
{
cin>>n>>a>>b;
for(int i=1;i<=n;i++)
{
cin>>k[i];
}
dfs(a,0);
if(res==1e9)
{
cout<<"-1"<<endl;
return 0;
}
cout<<res;
return 0;
}
每层楼最多走一次的方案一定比每层楼走多次的方案要好
被卡掉了,暴力搜索过不了,应该是要用bfs。
P1683 入门
题目链接:P1683 入门 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
遇到的问题:
地图怎么存? 用字符类型的二维数组
怎么走?
怎么转弯?
怎么只走'.'
瓷砖计数问题?能重复走,但不能重复计数,定义一个bool类型数组记录状态就可以了。
#include<bits/stdc++.h>
using namespace std;
const int N=30;
int n,m;//n行m列
char g[N][N];
int res=0; //记录走过的瓷砖数
int dx[]={-1,0,1,0};
int dy[]={0,1,0,-1};
bool st[N][N];
void dfs(int x,int y)//当前按了cnt次按钮
{
for(int i=0;i<n;i++)
{
int a=x+dx[i]; int b=y+dy[i];
if(a<0||a>=n||b<0||b>=m) continue;//越界,
if(g[a][b]!='.') continue;//不能走
if(st[a][b]) continue;//如果这个点已经走过了,不走了
//开始走a,b这个点
st[a][b]=true;
res++;
dfs(a,b);
}
}
int main()
{
cin>>m>>n;
for(int i=0;i<n;i++)
{
scanf("%s",g[i]);
}
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(g[i][j]=='@')
{
st[i][j]=true;
dfs(i,j);
}
}
}
res++;
cout<<res;
return 0;
}
不回溯的原因是,在每一次dfs里都会往四个方向走,也就是会再考虑一遍上一个起点。
如果回溯的话,下一次dfs里又会走一遍上次的点,又会重复计数。
FLOOD FILL 洪水填充模型 最大联通集问题
题目链接:P1596 [USACO10OCT]Lake Counting S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<bits/stdc++.h>
using namespace std;
const int N=110;
const int M=110;
char s[N][M];//存地形
int n,m;
int dx[]={-1,-1,0,1,1,1,0,-1};
int dy[]={0,1,1,1,0,-1,-1,-1};
int res;
bool st[N][M];
void dfs(int x,int y)
{
for(int i=0;i<8;i++)
{
int a=x+dx[i]; int b=y+dy[i];
if(a<0||a>=n||b<0||b>=m) continue;//要记得是否越界
if(s[a][b]=='W'&&!st[a][b])
{
st[a][b]=true;
dfs(a,b);
//st[x][y]=false; 不能回溯
//要注意这种问题,每次进行dfs都会考虑到8个方向,一定不要回溯!!
}else continue;
}
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
cin>>s[i];
}
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(s[i][j]=='W'&&!st[i][j])//注意W要大写,服了。。
{
st[i][j]=true; //1
dfs(i,j);
res++;
}
}
}
cout<<res;
return 0;
}
这题有1或无1这步都能AC,但从逻辑上讲,有1在dfs中就不会再访问这个点了,也就相当于剪枝。
如果没有1,那么在dfs中还会进行访问
洛谷的记录也证明了这个想法;
无1
有1
棋盘问题
这题的主要点在于,要求同一行同一列上最多只有一个棋子,也就是对每一行都遍历选列,画出递归树就可以发现跟排列型相似。
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N=10;
int n,k;//在n*n的矩阵里放k个棋子
int dx[]={-1,0,1,0};
int dy[]={0,1,0,-1};
char g[N][N];
bool st[N];//记录每列放没放过
int res=0; //摆放方案数
void dfs(int x,int cnt)//遍历行
{
if(cnt==k)//如果已经选了k个,方案数就++
{
res++;
return ;
}
if(x>=n) return ;//越界了
for(int i=0;i<n;i++)//遍历列
{
if(!st[i]&&g[x][i]=='#')
{
st[i]=true;
dfs(x+1,cnt+1);//当前行已经选择了i列,直接往下搜
st[i]=false;//回溯
}
}
dfs(x+1,cnt);//如果上面的cnt+1之后直接等于k了,循环直接退出,发生了错误
//这里的作用是在最后一次cnt+1=cnt之后强行再次遍历下一行
}
int main()
{
while(cin>>n>>k,n>0&&k>0)//很多组数据
{
for(int i=0;i<n;i++)
{
cin>>g[i];
}
res=0;//对每一组数据,res初始都为0
dfs(0,0);
cout<<res<<endl;
}
return 0;
}
数的划分
题目链接:P1025 [NOIP2001 提高组] 数的划分 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N=10;
int n,k;
int arr[N];
int res=0;
void dfs(int x,int start,int sum)//从第x个位
{
if(sum>n) return;
if(x>k)
{
if(sum==n)
{
res++;
}
return;
}
for(int i=start;sum+i*(k-x+1)<=n;i++)//剪枝之后才能AC
{
arr[x]=i;
dfs(x+1,i,sum+i);
arr[x]=0;//恢复现场
}
}
int main()
{
cin>>n>>k;
dfs(1,1,0);
cout<<res;
return 0;
}
单词接龙
题目链接:P1019 [NOIP2000 提高组] 单词接龙 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)