Loading

搜索问题总结

剪枝技巧

  • P1731生日蛋糕

    一道专门考察剪枝的搜索题,在这里我把一些常用的剪枝手段进行一些总结:

    1. 有时候先对所有的东西进行排序,然后再从大的开始搜索,剪枝效果更明显,原因是比较大的在选择之后变数较小,即在比较靠近根的位置进行剪枝

    2. 在搜索有序的时候,可以记录上一次选了什么东西,这样有时候可以确定下一次枚举的起始位置,也有时候可以减少出现相邻两次操作效果抵消的问题

    3. 利用极限进行剪枝,比如把xx都按最大算,或者把xx都按最小算,如果这样都不行,就cut掉(可行性和最优性都有)

    4. 循环+dfs时,每轮循环完了之后,可以判断一下是不是已经把答案搜出来了,如果搜出来了,就return

    5. 桶有时候可以优化我们的搜索

    6. A*算法使用时,为了减少priority_queue的调整,可以用大于等于/小于等于号。

    7. 一些数学公式预测未来的行为(比如求和公式等)

    本题代码如下:

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

启发式搜索

  • P2324 骑士精神

    这道题地图太大了,状压都压不住,所以我们得考虑用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;
    }
    
  • P5507 机关

    题目大意见原题。

    这个题是我做过的第一道启发式搜索的题目。在做这个题目之前,我们不难想到可以用双向广搜去做这个题,并且大概率能通过。但是,我这个题不会写双向广搜啊,所以就得另寻他路了。之前以为启发式很玄,但其实拿这个例子来讲,或者用八数码的例子讲,那些抽象的概念就变得容易理解多了。这道题用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

  • P4799 世界冰球锦标赛

    题目大意:有一些物品,物品有价值和重量两个属性,在不超过人能承受的最大重量的情况下,求有多少种取物品的方案。

    如果题目数据小,那就是一个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);
    }
    
posted @ 2020-07-16 00:06  BUAA-Wander  阅读(452)  评论(0编辑  收藏  举报