搜索问题总结
剪枝技巧
-
一道专门考察剪枝的搜索题,在这里我把一些常用的剪枝手段进行一些总结:
-
有时候先对所有的东西进行排序,然后再从大的开始搜索,剪枝效果更明显,原因是比较大的在选择之后变数较小,即在比较靠近根的位置进行剪枝
-
在搜索有序的时候,可以记录上一次选了什么东西,这样有时候可以确定下一次枚举的起始位置,也有时候可以减少出现相邻两次操作效果抵消的问题
-
利用极限进行剪枝,比如把xx都按最大算,或者把xx都按最小算,如果这样都不行,就cut掉(可行性和最优性都有)
-
循环+dfs时,每轮循环完了之后,可以判断一下是不是已经把答案搜出来了,如果搜出来了,就return
-
桶有时候可以优化我们的搜索
-
A*算法使用时,为了减少priority_queue的调整,可以用大于等于/小于等于号。
-
一些数学公式预测未来的行为(比如求和公式等)
本题代码如下:
#include <cstdio> #include <algorithm> #include <cmath> #define ll long long #define INF 999999999 using namespace std; ll n,m,s=INF; void dfs(ll step,ll r,ll h,ll sum,ll cost); int main(){ ll r,h; scanf("%lld %lld",&n,&m); for(r=sqrt((n/(m)))+1;r>=m;r--){ //先搜大的再搜小的 for(h=n/((m)*(m))+1;h>=m;h--){ dfs(1,r,h,2*r*h+r*r,r*r*h); } } if(s==INF){ printf("0\n"); return 0; } printf("%lld\n",s); return 0; } void dfs(ll step,ll r,ll h,ll sum,ll cost){ ll x=m-step; if(sum+(x*(x+1)*(2*x+1))/3>s){ //最优性剪枝,后面的按照1^2+2^2+……的最小情况算 return; } if(cost+x*(r-1)*(r-1)*(h-1)<n){ //最大也拼不成 return; } if(cost+(x*x*(x+1)*(x+1))/4>n){ //可行性剪枝,这样不够拼成蛋糕 ,负优化?? return; } if(step==m){ if(cost==n){ s=min(s,sum); return; } else { return; } } else { if(cost>=n){ //体积超了 return; } else { if(r<=0 || h<=0){ //前面用多了 return ; } else { for(ll i=r-1;i>=m-step;i--){ //枚举确保能够拼下去 for(ll j=h-1;j>=m-step;j--){ dfs(step+1,i,j,sum+2*i*j,cost+i*i*j); } } } } } }
-
启发式搜索
-
这道题地图太大了,状压都压不住,所以我们得考虑用dfs之类的搜索方法去做。但是裸的dfs又不行,所以考虑一下启发式迭代加深搜索,启发函数挺好写的,就是不在目标位置上的点的个数/2。然后就直接用启发函数剪枝就好了。另外,为了更有效的剪枝,我们需要在深搜的时候,避免上一次和这一次的移动抵消了,所以dfs的时候我们最好传一下上一次进行了什么操作这样一个参数。事实上,这个剪枝很重要,没有它的话即使是IDA*也没法通过此题(只有20pts)。
AC代码如下:
#include <cstdio> #include <algorithm> #include <cstdlib> #include <cstring> #define ll long long using namespace std; ll t,flag=0; char graph[7][7]; char goal[7][7]; int dx[8]={-1,-1,2,2,-2,-2,1,1}; //对称式存储方法比较好判断抵消的情况 int dy[8]={-2,2,-1,1,-1,1,-2,2}; ll evaluate(); void dfs(ll step,ll maxstep,ll x,ll y,ll last); int main(){ scanf("%lld",&t); for(int i=0;i<5;i++){ for(int j=0;j<i;j++){ goal[i][j]='0'; } for(int j=i;j<5;j++){ goal[i][j]='1'; } } goal[2][2]='*'; goal[3][3]='0'; goal[4][4]='0'; for(int i=0;i<t;i++){ for(int j=0;j<5;j++){ scanf("%s",graph[j]); } flag=0; ll x,y; for(ll j=0;j<5;j++){ for(ll k=0;k<5;k++){ if(graph[j][k]=='*'){ x=j; y=k; } } } for(ll j=1;j<=15;j++){ if(flag) break; dfs(0,j,x,y,8); } if(flag==0){ printf("-1\n"); } } return 0; } void dfs(ll step,ll maxstep,ll x,ll y,ll last){ ll ok=1; if(flag) return; ll value=evaluate(); if(step+value>maxstep) return; if(step==maxstep) { for(int i=0;i<5;i++){ for(int j=0;j<5;j++){ if(graph[i][j]!=goal[i][j]){ ok=0; break; } } if(ok==0) break; } if(ok){ //已经找到了 flag=1; printf("%lld\n",maxstep); return; } return; } else { for(int i=0;i<8;i++){ ll xx=x+dx[i],yy=y+dy[i]; if(xx>=0 && xx<5 && yy>=0 && yy<5 && last+i!=7){ //一定不要和上一次抵消了! swap(graph[xx][yy],graph[x][y]); dfs(step+1,maxstep,xx,yy,i); swap(graph[xx][yy],graph[x][y]); } if(flag) return; } } } ll evaluate(){ ll value=0; for(int i=0;i<5;i++){ for(int j=0;j<5;j++){ if(graph[i][j]!=goal[i][j]){ value++; } } } return value/2; }
-
题目大意见原题。
这个题是我做过的第一道启发式搜索的题目。在做这个题目之前,我们不难想到可以用双向广搜去做这个题,并且大概率能通过。但是,我这个题不会写双向广搜啊,所以就得另寻他路了。之前以为启发式很玄,但其实拿这个例子来讲,或者用八数码的例子讲,那些抽象的概念就变得容易理解多了。这道题用IDA\(*\)是过不了的,毕竟它的时间消耗还是挺大的,为了通过这道题,我们需要用A\(*\) 算法。对于这道题,就是用bfs,只不过在bfs的过程中,所有入队的状态都要先算一下它的启发函数和已经走过的距离,并且队列要用优先队列,这样能保证那些更有希望走到终点的状态先被搜到。注意不能只按照启发函数大小确定优先级,还有原先已经走过的步数!值得注意的是,这里利用了剪枝技巧中的6号技巧,这样能减少堆的调整(您可能会发现我的代码如果不用这个技巧就会TLE,用的话会跑得飞快)。整个题从裸的bfs,到双向bfs,再到IDA\(*\)和A\(*\),我写了一整天才通过,好艰难!
A\(*\)代码如下:
#include <bits/stdc++.h> #define ll long long using namespace std; typedef struct{ int state; int c[5]; }Node; typedef struct{ int allstate; int value; int step; int path[20]; }AllState; Node node[13]; int sign[1<<24]; priority_queue<AllState> q; AllState bfs(); int evaluate(); int getHashValue(); void setState(AllState s); int main(){ for(int i=1;i<=12;i++){ scanf("%d%d%d%d%d",&node[i].state,&node[i].c[1],&node[i].c[2],&node[i].c[3],&node[i].c[4]); node[i].state--; } AllState ans=bfs(); printf("%d\n",ans.step); for(int i=1;i<=ans.step;i++) printf("%d ",ans.path[i]); return 0; } AllState bfs(){ AllState start; start.allstate=getHashValue(); start.step=0; start.path[0]=0; start.value=evaluate(); q.push(start); sign[start.allstate]=1; while(!q.empty()){ AllState now=q.top(); q.pop(); setState(now); if(now.value==0){ return now; } for(int i=1;i<=12;i++){ int i_state=node[i].state; int follow=node[i].c[i_state+1]; int follow_state=node[follow].state; if(i_state==0 && follow_state==0) continue; node[i].state=(node[i].state+1)%4; node[follow].state=(node[follow].state+1)%4; AllState newstate; newstate.allstate=getHashValue(); newstate.value=evaluate(); newstate.step=now.step+1; for(int j=1;j<=now.step;j++) newstate.path[j]=now.path[j]; newstate.path[newstate.step]=i; if(!sign[newstate.allstate]) { sign[newstate.allstate]=1; q.push(newstate); } node[i].state=i_state; node[follow].state=follow_state; } } printf("fuck\n"); return q.top(); } int evaluate(){ int value=0; for(int i=1;i<=12;i++){ if(node[i].state!=0){ value+=4-node[i].state; } } return value/2; } void setState(AllState s){ int value=s.allstate; for(int i=12;i>=1;i--){ node[i].state=value%4; value/=4; } } int getHashValue(){ int value=0; for(int i=1;i<=12;i++) value=4*value+node[i].state; return value; } bool operator <(AllState a,AllState b){ if(a.value+a.step>=b.value+b.step) //利用了6号剪枝技巧 return true; return false; }
meet in the middle
-
题目大意:有一些物品,物品有价值和重量两个属性,在不超过人能承受的最大重量的情况下,求有多少种取物品的方案。
如果题目数据小,那就是一个01背包求不同方案数的裸题。但是这个题目承受重量是1e18,太大了,不过物品却较少,<=40,所以我们可能要考虑换一种方案。显然每个物品有选与不选两个可能,如果暴力搜索,那么有\(2^{40}\) 种方案,太多了,根本算不完。我们可能想到了双向搜索,但是之前只写过双向广搜解决地图走路问题,这个题似乎要双向深搜?咋做呢?根据学习meet in the middle算法,我知道了这个题的解决方案:正着搜一半,反着搜另一半,我们可以把前一半的结果记下来,排个序,然后每进行完一次后一半的搜索,就在前一半中二分查找一下,看看有多少个情况能够使得重量不超,这样整合答案的时间每次是对数级的,状态数也少了。这个题目是典型的meet in the middle的思想,不难有这个思路,但是二分合并答案可能是没有想到的。另外,前一半搜索的结果被记录下来了,这是典型的以空间换时间的做法。先对结果进行sort,然后再进行其他操作,也可能能够使得后续操作免除一些麻烦(比如非负,可以去掉绝对值),或者更快(比如二分)。
这道题特别注意的一点实现细节:我们存下来了前半部分和后半部分的状态,所以状态数组要足够大,得超过1e6吧。
代码如下:
#include <cstdio> #include <algorithm> #include <cstdlib> #define ll unsigned long long using namespace std; const int N=1e7+9; ll left[N],right[N],n,m,a[50],cnt,cntleft,cntright,ans; void dfs(ll step,ll maxstep,ll sum,ll s[]); ll b_search(ll l,ll r,ll s[],ll key); int main(){ scanf("%llu %llu",&n,&m); for(ll i=1;i<=n;i++){ scanf("%llu",&a[i]); } dfs(1,n/2,0,left); cntleft=cnt; cnt=0; dfs(n/2+1,n,0,right); cntright=cnt; sort(left+1,left+cntleft+1); // for(int i=1;i<=cntleft;i++){ // printf("%lld ",left[i]); // } // printf("\n"); for(ll i=1;i<=cntright;i++){ ll pos=b_search(1,cntleft,left,m-right[i]); ans=ans+pos-1; } printf("%llu\n",ans); return 0; } ll b_search(ll l,ll r,ll s[],ll key){ ll mid; while(l<=r){ mid=(l+r)/2; if(s[mid]<=key){ l=mid+1; } else { r=mid-1; //? } } return l; } void dfs(ll step,ll maxstep,ll sum,ll s[]){ if(sum>m){ return; } if(step>maxstep){ if(sum<=m){ s[++cnt]=sum; return; } else { return; } } dfs(step+1,maxstep,sum+a[step],s); dfs(step+1,maxstep,sum,s); }