搜索算法
洛谷1605题
要注意的点:
在地图中,有障碍的地方和已经走过的地方性质是一样的,不必为了障碍专门开一个数组,浪费空间。
记得初始化:初始点相当于已经走过了,不要再走。(巨坑,时隔半年才被我发现)
code:
#include <bits/stdc++.h> using namespace std; int total,n,m,sx,sy,fx,fy,t; bool a[7][7]; int dx[]={1,-1,0,0}; int dy[]={0,0,1,-1}; void f(int x,int y){ if(x==fx && y==fy){ total++; return; } for(int i=0;i<4;i++){ int tx=x+dx[i],ty=y+dy[i]; if(tx>=1 && ty>=1 && tx<=m && ty<=n && a[tx][ty]==false){ a[tx][ty]=true; f(tx,ty); a[tx][ty]=false; } } } int main(){ cin>>n>>m>>t>>sx>>sy>>fx>>fy; a[sx][sy]=true; for(int i=1;i<=t;i++){ int x,y; cin>>x>>y; a[x][y]=true; } f(sx,sy); cout<<total; return 0; }
洛谷1644题:
非常简单的DFS,连回溯都用不上:因为小马只会往右跳,根本跳不回左边去。所以连图都不用建,毕竟建图是为了储存已经走过的信息,这里不可能有重复走的可能(有递进条件只会往右跳决定),所以不建图嘿嘿。
code:
#include <bits/stdc++.h> using namespace std; int total,n,m; bool a[20][20]; int dx[]={1,1,2,2}; int dy[]={2,-2,1,-1}; void f(int x,int y){ if(x==m && y==n){ total++; return; } for(int i=0;i<4;i++){ int tx=x+dx[i],ty=y+dy[i]; if(tx>=0 && ty>=0 && tx<=m && ty<=n && a[tx][ty]==false){ a[tx][ty]=true; f(tx,ty); a[tx][ty]=false; } } } int main(){ cin>>n>>m; a[0][0]=true; f(0,0); cout<<total; return 0; }
洛谷1219:
经典的八皇后问题啊,关键之处在于用下标之和,下标之差表示双对角线,这里的对角线和跳马,迷宫之中的地图是一样的,只不过这里的”地图“不止一个,而是有行,左右对角线三处。此题中还有一个关键点在于表示出前三个回答,我们在走迷宫类问题中也会遇到这种需求:写出走迷宫的路径,怎么解决呢?当然是在搜索过程中把每一步储存在一个路径数组stepx,stepy中,一旦可以到达终点那就输出这两个step数组中记录的路径,不用专门去清空这两个数组,因为每次搜索新的路径都会覆盖原有的路径,无论新路径是对的还是错的,最终能不能走到终点,都会覆盖,但只有成功的路径才会被输出。
code:
#include <bits/stdc++.h> using namespace std; int total,n,a[15],b[15],c[30],d[30]; void f(int step){ if(step==n+1){ total++; if(total<4){ for(int i=1;i<=n;i++){ cout<<a[i]<<' '; } cout<<endl; } return; } for(int j=1;j<=n;j++){ if(b[j]==0 && c[step+j]==0 && d[step-j+n]==0){ b[j]=1,c[step+j]=1,d[step-j+n]=1,a[step]=j; f(step+1); b[j]=0,c[step+j]=0,d[step-j+n]=0; } } } int main(){ cin>>n; f(1); cout<<total; return 0; }
洛谷1596:
看看哪个点是有水的,顺着这个点延伸出去,把所有和它是一个坑的都变成干旱,最后数一数整个图中有几个点是有水的,那就有几个水坑。我愿称之为缩点法——缩片成点。
#include <bits/stdc++.h> using namespace std; int total,n,m; char a[105][105]; int dx[]={0,0,-1,-1,-1,1,1,1}; int dy[]={1,-1,-1,0,1,-1,0,1}; void f(int x,int y){ for(int i=0;i<8;i++){ int tx=x+dx[i],ty=y+dy[i]; if(tx>=1&&ty>=1&&tx<=n&&ty<=m&&a[tx][ty]=='W'){//注意,tx对应的是n,而不是x坐标 a[tx][ty]='.'; f(tx,ty); } } } int main(){ cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>a[i][j]; } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(a[i][j]=='W'){ a[i][j]='.'; f(i,j); total++; } } } cout<<total; return 0; }
注意横纵坐标,比如跳马那一题,数据先给列数再给行数,就是为了误导你,让你把行当列用,把列当行用,让你过不了。
注意,BFS如果要回溯的话时间复杂度是指数级,所以数据规模通常不大。BFS通常用于解决方案数(1219八皇后,1644跳马,马拉车),数个数(1596水坑数)类问题。
DFS:主要解决最优解类问题。
洛谷1379八数码难题
这题的思路是简单的BFS,没什么好说,问题在于如何表示状态,但如果用一个字符串(哈希)来储存该结构,我就可以用map和int作为键和值来储存每种状态的步数(从初始状态走到现在走了多少步)。这里注意map的用法。
注意用字符串表示图案时,用字符串的find函数找出0的位置,再用/和%找到横纵坐标。
在写代码过程中有个错误:BFS是用不到函数递归的,整个过程中只调用了一次函数,而DFS则用到递归,要反复调用函数。
自己的一个小优化:如果找到的话就直接return,不用把每种方案都搜索出来,节省了许多时间。
code:
#include <bits/stdc++.h> using namespace std; int total; string res="123804765"; int dx[]={0,0,-1,1}; int dy[]={1,-1,0,0}; map<string,int> d; queue<string>q; void f(int step){ while(q.size()){ string t=q.front(); if(t==res){ cout<<step; return; } step=d[t]; q.pop(); int k=t.find('0'); int x=k/3,y=k%3; for(int i=0;i<4;i++){ int tx=x+dx[i],ty=y+dy[i]; if(tx>=0&&ty>=0&&tx<3&&ty<3){ swap(t[k],t[tx*3+ty]); if(d[t]==0)d[t]=step+1,q.push(t); swap(t[k],t[tx*3+ty]); } } } } int main(){ string begin; cin>>begin; q.push(begin); f(0); return 0; }
一道很有趣的条件纠缠问题:
某侦察队接到一项紧急任务,要求在A、B、C、D、E、F六个队员中尽可能多地挑若干人,但有以下限制条件:
1)A和B两人中至少去一人;
2)A和D不能一起去;
3)A、E和F三人中要派两人去;
4)B和C都去或都不去;
5)C和D两人中去一个;
6)若D不去,则E也不去。
试编写一个程序,输出问应当让哪几个人去?
解法:用深搜遍历每一种可能性。将约束条件写成函数即可求出所有合理的解法,在解法中找到去的人数最多的那一种即可。
code:(本题是C语言程序设计实践的上机题,所以使用C语言写成)(注意约束条件是怎么写的,不要把”ABC有两个人去“写成“ABC都去不行”||“ABC都不去不行”||“A去了BC没去”……这种形式,这一看头都快破掉了,然而我最初就真是这么写的……我这个脑子呦)
#include <bits/stdc++.h> using namespace std; bool a[7]; int res,total; bool right(){ int t=0; if(a[0]==true)t++; if(a[1]==true)t++; if(t==0)return false; t=0; if(a[0]==true)t++; if(a[3]==true)t++; if(t==2)return false; t=0; if(a[0]==true)t++; if(a[4]==true)t++; if(a[5]==true)t++; if(t!=2)return false; t=0; if(a[1]==true)t++; if(a[2]==true)t++; if(t==1)return false; t=0; if(a[3]==true)t++; if(a[2]==true)t++; if(t!=1)return false; if(a[3]==false && a[4]==true)return false; return true; } void f(int step){ if(step==7){ if(res>total && right()){ total=res; for(int i=0;i<7;i++){ cout<<a[i]<<' '; } cout<<endl; } return; } else{ a[step]=true; res++; f(step+1); res--; a[step]=false; f(step+1); } } int main(){ for(int i=0;i<7;i++){ cout<<char('A'+i)<<' '; } cout<<endl; f(0); cout<<total; return 0; }
再注意一点:DFS是有模板的,虽说不要求背诵模板,但起码要有个大致的印象,比如本题中前6层的递归都是在找答案,到了第七层就输出答案,所以出口的条件就是if(n==6)
对了,if的出口之后记得加一个else,这是必备的,否则就会数组越界。
记住,汉诺塔问题需要用高进度
与其自己手写高精度加法,不如直接使用python3:
n = eval(input()) inf = 2147483647 f1=[inf for i in range(0,n+1)] f2=[inf for i in range(0,n+1)] f=[f1,f2] f[0][0]=f[1][0]=0 f[0][1]=f[1][1]=1 for i in range(2,n+1): f[0][i]=2*f[0][i-1]+1 for i in range(2,n+1): for j in range(1,i+1): f[1][i]=min(f[1][i],f[1][j]*2+f[0][i-j]) print(f[1][n])
搜索的时候要注意剪枝
不需要的数组不要开,这题一开始我还加了标记是否为空的empty数组,是否为真的real数组,并且用vector来储存v,这些都是可以省的。平时没什么,但这题的时间卡的很死,就需要高度剪枝了。
#include<iostream> using namespace std; int n,px,py; int clue[505],v[3],p; bool check(){//px,py空,满足条件吗 p=0; for(int i=1;i<=n;i++){ if(p>2)return 0; if(clue[i]>0){ if(clue[i]==px||clue[i]==py){ v[p++]=i; } }else{ if(-clue[i]!=px&&-clue[i]!=py){ v[p++]=i; } } } if(p==2){ int fal_ful=0,fal_emp=0; for(int i=0;i<2;i++){ if(v[i]==px||v[i]==py)fal_emp++; else fal_ful++; } if(fal_emp==1&&fal_ful==1)return 1; } return 0; } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",&clue[i]); for(px=1;px<=n;px++){ for(py=px+1;py<=n;py++){ if(check()){ printf("%d %d",px,py); return 0; } } } printf("No Solution"); return 0; }
数独这种经典题目能不能在三秒之内写出来:
#include <bits/stdc++.h> using namespace std; bool col[9][10],row[9][10],nine[3][3][10]; char a[10][10]; bool dfs(int x,int y){ if(x==9){ for(int i=0;i<9;i++)puts(a[i]); return 1; } if(y==9)return dfs(x+1,0); if(a[x][y]!='.')return dfs(x,y+1); for(int i=1;i<=9;i++){ if(!col[y][i]&&!row[x][i]&&!nine[x/3][y/3][i]){ col[y][i]=row[x][i]=nine[x/3][y/3][i]=1; a[x][y]='0'+i; if(dfs(x,y+1))return 1; col[y][i]=row[x][i]=nine[x/3][y/3][i]=0; a[x][y]='.'; } } return 0; } int main(){ cin.tie(0); cin.sync_with_stdio(false); for(int i=0;i<9;i++){ scanf("%s",a[i]); for(int j=0;j<9;j++){ if(a[i][j]!='.'){ col[j][a[i][j]-'0']=row[i][a[i][j]-'0']=nine[i/3][j/3][a[i][j]-'0']=1; } } } dfs(0,0); return 0; }
经典深搜剪枝问题:
小木棍+小猫爬山,两题极为相似,唯一不同之处在于木棍的长度等于分开木棍长度之和;缆车重量只要大于小猫重量之和即可。
这里的剪枝方式非常经典,务必记住。
小猫爬山:
最初试图用return 0表示不可行,return 1表示可行,但是在在新找了一辆缆车的分支中没有将状态回退,所以debug好几个小时。
记录一个神奇的宽搜题目:立体推箱子,本质是宽搜,但形式极其操蛋
#include<bits/stdc++.h> using namespace std; const int N=510,INF=0x7f7f7f7f; int n,m,ans,visit[N][N][3]; char c,a[N][N]; //立0,横1,标记左,竖2,标记上; //上右下左 //竖着x横着y,竖着n横着m int dx[3][4]={ {-2,0,1,0}, {-1,0,1,0}, {-1,0,2,0} }; int dy[3][4]={ {0,1,0,-2}, {0,2,0,-1}, {0,1,0,-1} }; int ds[3][4]={ {2,1,2,1}, {1,0,1,0}, {0,2,0,2} }; struct node{int x,y,state;}final,start; bool check(node x){ if(x.x<1||x.y<1||x.x>n||x.y>m)return 0; if(x.state==0&&(a[x.x][x.y]=='#'||a[x.x][x.y]=='E'))return 0; if(x.state==1&&( x.y+1>m|| a[x.x][x.y]=='#'|| a[x.x][x.y+1]=='#' ))return 0; if(x.state==2&&( x.x+1>n|| a[x.x][x.y]=='#'|| a[x.x+1][x.y]=='#' ))return 0; return 1; } void bfs(){ queue<pair<node,int>> q; q.push({start,0}); while(q.size()){ auto u=q.front(); int ux=u.first.x,uy=u.first.y,us=u.first.state; q.pop(); for(int i=0;i<4;i++){ node v={ux+dx[us][i],uy+dy[us][i],ds[us][i]}; if(visit[v.x][v.y][v.state]==0&&check(v)){ if( v.x==final.x&& v.y==final.y&& v.state==final.state ){ans=u.second+1;return;} visit[v.x][v.y][v.state]=1; q.push({v,u.second+1}); } } } } int main(){ while(scanf("%d%d",&n,&m)){ if(!n)return 0; int first=-1; ans=INF; memset(visit,0,sizeof visit); getchar(); for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ a[i][j]=getchar(); if(a[i][j]=='X'){ if(first==-1){ start={i,j,0}; first=0; }else{ if(start.x==i-1){ start.state=2; }else{ start.state=1; } } } if(a[i][j]=='O'){ final={i,j,0}; } } getchar(); } bfs(); if(ans==INF)printf("Impossible\n"); else printf("%d\n",ans); } return 0; }
一道需要剪枝的深搜题目:
剪枝方法是:记忆化搜索。
最初我的记忆化内容是从某个点走到终点的最短距离,计算出某个点的该值以后,再次进入该点时就直接返回该值;
实际上每次两次到达某点时visit中已经走过的点不同,所以最短距离不能一概而论;
真正的记忆化搜索是记录走到当前点的