搜索(dfs+bfs+回溯+记忆化搜索)
End1essSummer · · 个人记录
序言
搜索是什么?
搜索,也就是对状态空间进行枚举,通过穷尽所有的可能来找到最优解,或者统计合法解的个数,有一定的方向性和目标性。
搜索有很多优化方式,如减小状态空间,更改搜索顺序,剪枝等。
搜索是一些高级算法的基础。在
OI 中,纯粹的搜索往往也是得到部分分的手段,但可以通过纯粹的搜索拿到满分的题目非常少。 (来自oiwiki )
目录QAQ
-序言
- 搜索是什么?
-目录
-1.dfs
-
- 如何实现它?
-2.bfs
-
- 如何实现它?
-3.回溯
- 回溯是什么?
- 如何实现它?
-4.记忆化搜索
- 记忆化搜索是什么?
- 如何实现它?
-5.好题分享
-
P3956 棋盘
-
P1406 方格填数
-
P1034 矩形覆盖
-
P1092 虫食算
-
P1363 幻象迷宫
函数是一个非常简单的东西。
由于我在此篇博客里的所有搜索都是用函数写的,所以如果你不会函数的话请点这里QAQ点我,五分钟就可以懂了。如果不想看的话也没事,只是会造成一些理解上的问题。
另外,队列也是一个非常简单的东西。
由于bfs的实现是需要用队列来写的,所以如果你不会队列的话,可以来看看这里,也是一个非常简单的东西,五分钟以后也能回来。不看也会造成一些理解的问题,无伤大雅。
1.dfs
- dfs 是什么?
- 如何实现?
//伪代码
dfs(v){
if 符合要求{
操作
返回
}v上打标记
进行操作
for u in v的相邻节点{
if u没标记{
dfs(u);
}
}
}
2.bfs
- bfs 是什么?
最基础、最重要的搜索算法之一。 所谓宽度优先。就是每次都尝试访问同一层的节点。如果同一层都访问完了,再访问下一层。 这样做的结果是,$bfs$算法找到的路径是从起点开始的 最短合法路径。换言之,这条路所包含的边数最小。 在$bfs$结束时,每个节点都是通过从起点到该点的最短路径访问的。(来自$oiwiki$)
- 实现?
//伪代码
bfs(初始节点 s){
新队列 q
s进队
标记s
while(队列不为空){
u=队首
弹出
for u的相邻结点v{
if v未标记{
操作
v进队
}
}
}
}
3.回溯
- 回溯是什么?
回溯法是一种经常被用在 深度优先搜索(
dfs ) 和 广度优先搜索(bfs ) 的技巧。其本质是:走不通就回头。 (来自
oiwiki )
- 代码实现?
在本次放代码前先让我们考虑一道经典例题:全排列 P1706
按照字典序输出自然数
1 到n 所有不重复的排列,即n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
对于这道题,我们考虑用一个布尔数组记录没用过的数字,再用一个ans数组记录选的是什么,具体实现请看代码:
#include<bits/stdc++.h>
using namespace std;
int n,ans[110];
bool a[110];
void dfs(int s){
if(s==n+1){
for(int i=1;i<=n;i++){
printf(" %d",ans[i]);
}printf("\n");
return;
}for(int i=1;i<=n;i++){
if(!a[i]){
a[i]=true;
ans[s]=i;
dfs(s+1);
a[i]=false;
}
}
}int main(){
cin>>n;
dfs(1);
return 0;
}
那么接下来就奉上伪代码了QWQ
//伪代码
dfs(v){
if 符合要求{
操作
返回
}进行操作
for u in v的相邻节点{
if u没标记{
进行操作
dfs(u);
撤销操作
}
}
}
4.记忆化(讲完这个就正餐了QAQ)
- 记忆化搜索是什么?
最常用的剪枝有三种,记忆化搜索、最优性剪枝、可行性剪枝。
因为在搜索中,相同的传入值往往会带来相同的解,并且不比之前优的传入值还可能带来不比之前优的解,那我们就可以用数组来记忆。(oiwiki yyds!)
- 如何实现?
//伪代码
g[MAXN](记忆化数组),ans=inf;
dfs(now(目前到的点),sum(总和)){
if sum>g[now] return(之前走过的都比你这大了你还有机会麻QAQ);
else{
if(now==目的地){
ans取最优;
return;
}else{
for 遍历可能性{
if 彳亍qwq!{
操作
dfs;
撤回操作
}
}
}
}
}......
int main(){
......
初始化g极小/极大值;
......
}
5.好题分享
温馨提示:难度可能并不按顺序排序。
- 1 历年真题-棋盘
题面大意:给定你
m *m 的棋盘,每个棋盘都被染上了红色,黄色或者没被染色。如果你走向一个颜色相同的各自的话花费为0 ,走向不同颜色的格子的花费为1 ,让一个无色格子变为你想要的颜色(两轮)的花费为2 ,且不能连续使用。现在求从(1 ,1 )走到(m ,m )的最小花费。对于
100 %的数据, 1≤m ≤100,1≤n ≤1000
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
对于这道题用搜索我们有两种方法——dfs与bfs
1.dfs做法:
对于
我的
#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int q[110][110],n,m,ans=1e9,d[110][110];
bool b[110][110];
int fx[4][2]={{1,0},{0,1},{-1,0},{0,-1}};
void dfs(int nowx,int nowy,int spend,bool magic){
if(nowx==m&&nowy==m){
ans=min(ans,spend);
return;
}else{
if(nowx>m||nowy>m||nowx<1||nowy<1){
return;
}else{
for(int i=0;i<4;i++){
int x=nowx+fx[i][0];int y=nowy+fx[i][1];
if(b[x][y]) continue;
if(q[x][y]!=0){
int nextspend=spend;
bool xk=true;
if(q[x][y]!=q[nowx][nowy]) nextspend++;
if(nextspend>=d[x][y]||nextspend>=ans) continue;
d[x][y]=nextspend;
b[x][y]=true;
dfs(x,y,nextspend,xk);
b[x][y]=false;
}else{
if(magic){
bool xk=false;
int nextspend=spend+2;
if(nextspend>=d[x][y]||nextspend>=ans) continue;
d[x][y]=nextspend;
b[x][y]=true;
q[x][y]=q[nowx][nowy];
dfs(x,y,nextspend,xk);
b[x][y]=false;
q[x][y]=0;
}
}
}
}
}
}
int main(){
cin>>m>>n;
b[1][1]=true;
for(int i=1;i<=m;i++){
for(int j=1;j<=m;j++){
d[i][j]=1e9;
}
}for(int i=1;i<=n;i++){
int x,y,z;
cin>>x>>y>>z;
q[x][y]=z+1;
}dfs(1,1,0,1);
if(ans==1e9) ans=-1;
cout<<ans;
return 0;
}
2.
考虑优先队列+记忆化。
#include<bits/stdc++.h>
using namespace std;
int n,m,d[110][110],f[110][110];
int fx[4][2]={{1,0},{0,1},{-1,0},{0,-1}};
bool b[110][110];
struct p{
int nowx,nowy,spend,color;
bool magic;
};struct cmp{
bool operator()(p a,p b){
return a.spend>b.spend;
}
};
int bfs(int nx,int ny,int sp,bool ma){
priority_queue<p,vector<p>,cmp> q;
q.push({nx,ny,sp,d[nx][ny],ma});
b[nx][ny]=true;
while(!q.empty()){
p s=q.top();
q.pop();
if(s.nowx>m||s.nowy>m||s.nowx<1||s.nowy<1||f[s.nowx][s.nowy]<s.spend) continue;
f[s.nowx][s.nowy]=s.spend;
b[s.nowx][s.nowy]=true;
if(s.nowx==m&&s.nowy==m){
return s.spend;
}for(int i=0;i<=3;i++){
int nextx=s.nowx+fx[i][0],nexty=s.nowy+fx[i][1];
if(b[nextx][nexty]) continue;
if(s.color==d[nextx][nexty]){
q.push({nextx,nexty,s.spend,s.color,1});
}else{
if(d[nextx][nexty]==0){
if(s.magic){
q.push({nextx,nexty,s.spend+2,s.color,0});
}
}else{
q.push({nextx,nexty,s.spend+1,d[nextx][nexty],1});
}
}
}
}return -1;
}int main(){
memset(f,0x3f,sizeof(f));
cin>>m>>n;
for(int i=1;i<=n;i++){
int x,y,c;
cin>>x>>y>>c;
d[x][y]=c+1;
}int ans=bfs(1,1,0,1);
cout<<ans;
return 0;
}
- 2 练习好题-方格填数
题目大意:给你
n 以及n*n 个数字,让你把这些数字让填入矩阵,使得每行每列每个对角线上整数的和都相等,并输出矩阵。如有多种填法,请输出字典序最小的。其中3<=
n <=4,-1e8<=a_i <=1e8
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
这道题只是一道绿题,难度也比较适中。
对于正解,我们可知其每行每列的
对于最后,考虑暴力判断每行每列加起来等不等于 加和/
对于如何实现字典序,对输入进来的数进行排序即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,a[20],sum,ans[20][20];
bool b[20];
bool cmp(int a,int b){
return a<b;
}void dfs(int nowi,int nowj){
if(nowi==n+1){
for(int j=1;j<=n;j++){
int sum2=0;
for(int i=1;i<=n;i++){
sum2+=ans[i][j];
}if(sum2!=sum) return;
}int sum3=0,sum4=0;
for(int i=1;i<=n;i++){
sum3+=ans[i][i];
sum4+=ans[i][n-i+1];
}if(sum3!=sum||sum4!=sum){
return;
}else{
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cout<<ans[i][j]<<" ";
}cout<<"\n";
}exit(0);
}
}else{
if(nowj==n+1){
int sum1=0;
for(int j=1;j<=n;j++){
sum1+=ans[nowi][j];
}if(sum1==sum){
dfs(nowi+1,1);
}
}else{
for(int i=1;i<=n*n;i++){
if(!b[i]){
b[i]=true;
ans[nowi][nowj]=a[i];
dfs(nowi,nowj+1);
b[i]=false;
}
}
}
}
}signed main(){
cin>>n;
for(int i=1;i<=n*n;i++){
cin>>a[i];
sum+=a[i];
}sum/=n;
sort(a+1,a+(n*n)+1,cmp);
cout<<sum<<"\n";
dfs(1,1);
return 0;
}
- 3 历年真题-矩形覆盖
题目大意:给你
n 个点的坐标x ,y ,让你用不互相重复的k个矩形去覆盖他们,问这k 个矩形的最小总面积。其中
n <=50,k <=4,0<x ,y <=500
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
对于正确做法,我们考虑
#include<bits/stdc++.h>
using namespace std;
// P1034
int n,k,ans=1e9,nx[100],ny[100];
struct M{
int x[10],y[10];
bool yes;
} t[10];
void dfs(int step,int s){
if(s>=ans) return;
else{
if(step==n+1){
for(int i=1;i<=k;i++){
for(int j=1;j<=k;j++){
if(i==j){
continue;
}else{
for(int nowx=t[j].x[1];nowx>=t[j].x[2];nowx--){
for(int nowy=t[j].y[2];nowy>=t[j].y[1];nowy--){
if(t[i].x[1]>=nowx&&t[i].x[2]<=nowx&&t[i].y[2]>=nowy&&t[i].y[1]<=nowy){
return;
}
}
}
}
}
}int sum=0;
for(int i=1;i<=k;i++){
sum+=(t[i].x[1]-t[i].x[2])*(t[i].y[2]-t[i].y[1]);
}ans=min(sum,ans);
return;
}else{
for(int i=1;i<=k;i++){
M lzm=t[i];
if(!t[i].yes){
t[i].x[1]=t[i].x[2]=ny[step];
t[i].y[1]=t[i].y[2]=nx[step];
t[i].yes=true;
}else{
if(nx[step]<t[i].y[1]){
t[i].y[1]=nx[step];
}else{
if(nx[step]>t[i].y[2]){
t[i].y[2]=nx[step];
}
}if(ny[step]>t[i].x[1]){
t[i].x[1]=ny[step];
}else{
if(ny[step]<t[i].x[2]){
t[i].x[2]=ny[step];
}
}
}int sum=0;
for(int i=1;i<=k;i++){
sum+=(t[i].x[1]-t[i].x[2])*(t[i].y[2]-t[i].y[1]);
}dfs(step+1,sum);
t[i]=lzm;
}
}
}
}int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>nx[i]>>ny[i];
}dfs(1,0);
cout<<ans;
}
-4 历年真题-虫食算
题意简述:给你
n 以及三行长度为n 的字符串,上两行代表加数,最后一行代表结果。现在告诉你相同的字母代表相同的数字并且这个算式是n 进制的,求最后A ,B ......(我们规定用前n 个字母来代表0-n )所代表的数字。
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
先来夹带一点私货。
改了一个月
对于这道题而言我们首先考虑到的是产生n个数的全排列,再去一个个检查,但是很显然,对于极限数据来讲它的时间复杂度肯定会爆炸。
接下来我们去考虑剪枝。
首先第一个剪枝,便是我们边搜边去检查,又没有不合法的地方。在没有确定进位的情况下,只要不符合
接下来第二个剪枝,便是我们考虑从最低位搜起,这样的话能使第一个剪枝跑的飞快。
其实接下来还有一个玄学的优化:便是从
#include<bits/stdc++.h>
using namespace std;
int a[30],b[30],c[30],nex[30],nextop,num[30],n;
char sta[30],stb[30],stc[30];
bool nu[30];
bool CP(){
for(int i=1;i<=n;i++){
if(num[a[i]]==-1||num[b[i]]==-1||num[c[i]]==-1){
continue;
}else{
int t=num[a[i]]+num[b[i]];
if(!(((t+1)%n!=num[c[i]])^((t)%n!=num[c[i]]))){
return 1;
}
}
}return 0;
}bool J(){
for(int i=1,x=0;i<=n;i++){
int t=(num[a[i]]+num[b[i]]+x);
if(t%n!=num[c[i]]){
return 1;
}x=t/n;
}return 0;
}void init(){
for(int i=1;i<=n;i++){
a[i]=(sta[n-i]-'A')+1;
b[i]=(stb[n-i]-'A')+1;
c[i]=(stc[n-i]-'A')+1;
}return;
}void print(){
for(int i=1;i<n;i++){
cout<<num[i]<<" ";
}cout<<num[n]<<"\n";
exit(0);
}void dfs(int step){
if(CP()) return;
if(step==n+1){
if(J()) return;
else{
print();
return;
}
}else{
for(int i=n-1;i>=0;i--){
if(!nu[i]){
nu[i]=1;
num[nex[step]]=i;
dfs(step+1);
num[nex[step]]=-1;
nu[i]=0;
}
}
}
}void GN(int x){
if(!nu[x]){
nu[x]=1;
nextop++;
nex[nextop]=x;
}return;
}int main(){
//cout<<1<<" ";
scanf("%d",&n);
scanf("%s",sta);
scanf("%s",stb);
scanf("%s",stc);
//cout<<sta<<'\n'<<stb<<'\n'<<stc<<'\n';
for(int i=n-1;i>=0;i--){
num[i]=-1;
}init();
for(int i=1;i<=n;i++){
GN(a[i]);
GN(b[i]);
GN(c[i]);
}memset(nu,0,sizeof(nu));
dfs(1);
return 0;
}
-5 练习好题-幻象迷宫
题目大意:给你一个行为
n ,列为m 的由字符 # ,. ,S 组成的矩阵迷宫。其中#代表障碍,.代表可走的路,S 代表起点。我们认为这个矩阵迷宫是可以无限延伸的。而我们在规定走出去的定义是能够走出无限远的距离。现在请你判断给你的迷宫可不可以走出去。
输入有多组数据,其中
n ,m \le 1500。
输入文件以EOF结尾
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
(防剧透QAQ)
这道题其实用(绝对不是因为我懒) 所以只讨论
首先,将迷宫拓展这种做法是不可取的。首先,这一组数据就能够卡掉你:
6 20
#.##.##.##.##.##.##.
#.##.##.##.##.##.##.
#.##.##.##.##.##.##.
S.#..#..#..#..#..#..
##..#..#..#..#..#..#
#..#..#..#..#..#..##
那怎么办呢?
我们注意到,因为它是可以无限拓展的,所以你在
那么,对于每一个原矩阵的格子,我们考虑去记录上一个取模后坐标与它相等的真实坐标。如果上一次与这一次的真实坐标不相等,那么直接返回,因为它可以走无限远后又回到这个点。而相等的话就可以直接找下一个了,因为你其实就相当于在这几个格子之间反复横跳。
上代码:
#include<iostream>
#include<queue>
#include<cstring>
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,ni,nj,x[4]={0,1,0,-1},y[4]={1,0,-1,0},vis[1510][1510][2],finf;
bool Map[1510][1510];
struct node{
int i,j,li,lj;
};bool bfs(){
queue<node> q;
node s={ni,nj,ni,nj};
q.push(s);
while(!q.empty()){
node s=q.front();
q.pop();
if(vis[s.i][s.j][0]==s.li&&vis[s.i][s.j][1]==s.lj){
continue;
}else{
if((vis[s.i][s.j][0]!=s.li||vis[s.i][s.j][1]!=s.lj)&&(vis[s.i][s.j][0]!=finf&&vis[s.i][s.j][1]!=finf)){
return true;
}
}vis[s.i][s.j][0]=s.li;
vis[s.i][s.j][1]=s.lj;
for(int i=0;i<=3;i++){
int mi=(s.i+x[i]+n)%n,mj=(s.j+y[i]+m)%m,nmi=(s.li+x[i]),nmj=(s.lj+y[i]);
if(Map[mi][mj]){
q.push({mi,mj,nmi,nmj});
}
}
}return false;
}signed main(){
while(scanf("%d%d",&n,&m)!=EOF){
memset(vis,-1,sizeof(vis));
finf=vis[1][1][1];
for(int i=0;i<n;i++){
char s[1510];
scanf("%s",s);
for(int j=0;j<m;j++){
if(s[j]=='.'||s[j]=='S'){
Map[i][j]=true;
if(s[j]=='S'){
ni=i;nj=j;
}
}else{
Map[i][j]=false;
}
}
}bool maybe=bfs();
if(maybe){
cout<<"Yes\n";
}else{
cout<<"No\n";
}
}
return 0;
}
完结撒花