欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 会展 > 【6】搜索剪枝优化学习笔记

【6】搜索剪枝优化学习笔记

2025/3/15 17:21:15 来源:https://blog.csdn.net/WSD3319_/article/details/146189382  浏览:    关键词:【6】搜索剪枝优化学习笔记

前言

WFLS 2023 寒假集训 Day4 Day 5

搜索剪枝的复杂度很玄学,最好还是能剪枝就剪枝,只要不是错误的,总没有坏处。

最优化剪枝

当题目要求求最优解的时候,此时如果已经求出一个可行解,那么答案超过这个可行解的分支一定不是最优解,所以这些分支可以剪掉。

找到可行解

if(check()&&now<ans){ans=now;return;}

超过可行解

if(now>ans)return;

例题 1 1 1

P1213 [USACO1.4][IOI1994]时钟 The Clocks

剪枝 1 1 1 :等效冗余

我们知道,如果同一个操作执行了 4 4 4 次,那么时钟就转了 360 360 360 度,相当于没有转。所以如果同一个操作执行了超过 4 4 4 次,那么一定会有更优解,这些分支是冗余的,所以剪枝。

剪枝 2 2 2最优化剪枝

如果已经求出一个可行解,那么答案超过这个可行解的分支一定不是最优解,所以这些分支可以剪掉。

剪枝 3 3 3 :记忆化剪枝

如果这个时钟状态已经访问过了,那么后面的分支就已经计算过了,没有必要再计算。由搜索顺序得到,第一次搜索到一个时钟状态一定是最优解,所以可以剪枝。

#include<bits/stdc++.h>
using namespace std;
int cl[9],ans=99999999,an[100],hu[100],did[100];
bool bef[4][4][4][4][4][4][4][4][4]; 
int cha[9][9]=
{
//A B C D E F G H I{1,1,0,1,1,0,0,0,0},{1,1,1,0,0,0,0,0,0},{0,1,1,0,1,1,0,0,0},{1,0,0,1,0,0,1,0,0},{0,1,0,1,1,1,0,1,0},{0,0,1,0,0,1,0,0,1},{0,0,0,1,1,0,1,1,0},{0,0,0,0,0,0,1,1,1},{0,0,0,0,1,1,0,1,1}
};
void nxt(int h)
{for(int i=0;i<9;i++)if(cha[h][i]){cl[i]=cl[i]+3;if(cl[i]==15)cl[i]=3;}
}void pre(int h)
{for(int i=0;i<9;i++)if(cha[h][i]){cl[i]=cl[i]-3;if(!cl[i])cl[i]=12;}
}bool check()
{for(int i=0;i<9;i++)if(cl[i]!=12)return 0;return 1;
}void dfs(int now)
{if(now>ans||now>40)return;if(check()&&now<ans){ans=now;for(int i=0;i<ans;i++)an[i]=hu[i];return;}if(bef[cl[0]/3-1][cl[1]/3-1][cl[2]/3-1][cl[3]/3-1][cl[4]/3-1][cl[5]/3-1][cl[6]/3-1][cl[7]/3-1][cl[8]/3-1])return;bef[cl[0]/3-1][cl[1]/3-1][cl[2]/3-1][cl[3]/3-1][cl[4]/3-1][cl[5]/3-1][cl[6]/3-1][cl[7]/3-1][cl[8]/3-1]=1;for(int i=0;i<9;i++){if(did[i]>=3)continue;nxt(i);hu[now]=i+1;did[i]++;dfs(now+1);did[i]--;hu[now]=0;pre(i);}return;
}int main()
{for(int i=0;i<9;i++)scanf("%d",&cl[i]);dfs(0);for(int i=0;i<ans;i++)printf("%d ",an[i]);return 0;
}

可行性剪枝

根据题目的要求,把已经明显不符合题目要求的分支剪去,可以避免搜索无效信息,降低程序的时间复杂度。

例题 2 2 2

P1731 [NOI1999] 生日蛋糕

剪枝 1 1 1 :最优化剪枝

如果已经求出一个可行解,那么答案超过这个可行解的分支一定不是最优解,所以这些分支可以剪掉。

剪枝 2 2 2 :搜索顺序

可以贪心一下,先从大的搜索,这样后面的选择就会较多,得到可行解的几率也会更大。所以对于半径和高可以从大到小枚举。

剪枝 3 3 3可行性剪枝

如果这一层蛋糕体积总和已经超过 N N N ,那么这个分支就是不可行的,剪枝。

剪枝 4 4 4可行性剪枝

如果这一层蛋糕半径和高小于剩余层数,由于 R i > R i + 1 R_i\gt R_{i+1} Ri>Ri+1 H i > H i + 1 H_i\gt H_{i+1} Hi>Hi+1 ,所以接下来就算每次只减少 1 1 1 也不够放那么多层蛋糕,所以这个分支就是不可行的,剪枝。

剪枝 5 5 5可行性剪枝

设这一层蛋糕半径为 R n R_n Rn ,高为 H n H_n Hn ,那么这一层蛋糕的体积是:

V n = R n 2 × H n V_n=R_n^2\times H_n Vn=Rn2×Hn

由于 R i > R i + 1 R_i\gt R_{i+1} Ri>Ri+1 H i > H i + 1 H_i\gt H_{i+1} Hi>Hi+1 ,所以接下来每层只可能比这一层小。如果接下来全与这一层相等都无法到达 N N N ,那么剪枝。

剪枝 6 6 6可行性剪枝

由于 R i > R i + 1 R_i\gt R_{i+1} Ri>Ri+1 H i > H i + 1 H_i\gt H_{i+1} Hi>Hi+1 ,逆向推理得到上面第一层最小 R n = 1 , H n = 1 R_n=1,H_n=1 Rn=1,Hn=1 ,第二层最小 R n = 2 , H n = 2 R_n=2,H_n=2 Rn=2,Hn=2 ,第三层 … \dots

可以把这些最小体积打成一张表,如果剩余体积不足这么多,就证明连最小情况的不合法,剪枝。

#include <bits/stdc++.h>
using namespace std;
int n,m,ans=99999999,mr[10000],mh[10000],t[16]={0,0,1,9,36,100,225,441,784,1296,2025,3025,4356,6084,8281,11025};
void dfs(int now,int sum,int cnt)
{if(sum>=n-t[m-now]&&now!=m)return;if(cnt>=ans)return;if(now==m){if(sum==n)ans=min(ans,cnt);return;}for(int i=mr[now]-1;i>=0;i--)for(int j=mh[now]-1;j>=0;j--){long long v=i*i*j;if(i<(m-now)||j<(m-now)||sum+v>n-t[m-now]||v<(n-sum)/(m-now))continue;mr[now+1]=i;mh[now+1]=j;if(now==0)cnt=i*i;dfs(now+1,sum+i*i*j,cnt+2*i*j);}
}int main()
{scanf("%d%d",&n,&m);mr[0]=sqrt(n)+1;mh[0]=n;dfs(0,0,0);printf("%d",ans);return 0;
}

记忆化剪枝

如果一个状态已经搜索过了,为了不重复搜索,可以把这个状态记录下来,下次再次搜索到就直接剪枝。

例题 3 3 3

P1189 SEARCH

剪枝 1 1 1记忆化剪枝

设状态 f [ x ] [ y ] [ d ] = 1 f[x][y][d]=1 f[x][y][d]=1 表示在第 d d d 步时访问过点 ( x , y ) (x,y) (x,y) ,后面扩展的信息已经计算过了,没必要重复计算。如果再次搜索到,直接剪枝。

#include <bits/stdc++.h>
using namespace std;
int n,m,k,x,y;
int f[200][200][200];
char map1[60][60];
char ch,str[1000];
int dir[3000],cnt=1;
int x1[4]={-1,1,0,0};
int y2[4]={0,0,-1,1};
int dfs(int x,int y,int di)
{if(di==k+1){map1[x][y]='*';return 0;}if(f[x][y][di])return 0;for(int i=1;;i++){if(!(x+x1[dir[di]]*i<n&&x+x1[dir[di]]*i>=0&&y+y2[dir[di]]*i<m&&y+y2[dir[di]]*i>=0))break;if(map1[x+x1[dir[di]]*i][y+y2[dir[di]]*i]=='X')break;dfs(x+x1[dir[di]]*i,y+y2[dir[di]]*i,di+1);f[x][y][di]=1;}return 0;
}int main()
{scanf("%d%d",&n,&m);for(int i=0;i<n;i++){for(int j=0;j<m;j++){while(!(ch=='.'||ch=='X'||ch=='*'))ch=getchar();if(ch=='*'){x=i;y=j;}map1[i][j]=ch;ch='\0';}}scanf("%d",&k);for(int i=0;i<k;i++){scanf("%s",str);switch(str[0]){case 'N':dir[cnt++]=0;break;case 'S':dir[cnt++]=1;break;case 'W':dir[cnt++]=2;break;case 'E':dir[cnt++]=3;break;}}map1[x][y]='.';dfs(x,y,1);for(int i=0;i<n;i++){for(int j=0;j<m;j++)printf("%c",map1[i][j]);printf("\n");}return 0;
}

双向搜索

如果搜索有确定的始态和终态,那么可以从始态和终态分别出发进行搜索,把原搜索树分为深度为原搜索树一半的两颗子树。最后在交汇处进行计算。也就是所谓的

Meet in the Middle

对于指数级增长的搜索,这无疑是个大优化。

例题 4 4 4

P4799 [CEOI2015 Day2] 世界冰球锦标赛

剪枝 1 1 1双向搜索

首先搜索前一半序列,把所有选择计算出来,存入一个数组并排序。

然后搜索后一半序列,搜索完成后进行 Meet in the Middle

在前一半序列中二分查找,找到与这一次搜索结果之和最接近但不超过钱数 M M M 的数组元素。由于序列有序,那么这个以及其之前的元素与结果相加均不超过 M M M ,加到 a n s ans ans 里就行了。

时间复杂度: O ( 2 n 2 + 2 n 2 log ⁡ 2 n 2 ) O(2^{\frac{n}{2}}+2^{\frac{n}{2}}\log2^{\frac{n}{2}}) O(22n+22nlog22n)

虽然式子看起来很奇怪,但是确实是正确的时间复杂度。

#include <bits/stdc++.h>
using namespace std;
long long n,m,a[100],q[(1<<21)+1],cq=0,ch=0,ans=0;
void dfs1(long long now,long long sum,long long dep)
{if(now==dep){q[cq++]=sum;return;}dfs1(now+1,sum,dep);if(sum+a[now]<=m)dfs1(now+1,sum+a[now],dep);return ;
}void search(long long a)
{long long k=m-a;long long l=0,r=cq-1;while(l<r){long long mid=(l+r+1)/2;if(q[mid]<=k)l=mid;else r=mid-1;}ans+=(l+1);
}void dfs2(long long now,long long sum,long long dep)
{if(now==dep){search(sum);return;}dfs2(now+1,sum,dep);if(sum+a[now]<=m)dfs2(now+1,sum+a[now],dep);return ;
}int main()
{scanf("%lld%lld",&n,&m);for(int i=0;i<n;i++)scanf("%lld",&a[i]);sort(a,a+n);reverse(a,a+n);dfs1(0,0,n/2);sort(q,q+cq);dfs2(n/2,0,n);printf("%lld",ans);return 0;
}

后记

教练说搜索剪枝也是很考验思维的,需要仔仔细细思考。

所以搜索剪枝还是很难的,只不过最近几年似乎都没有考。

那就引用教练的一句话来收尾吧:

搜索是博大精深的。

搜索剪枝的讨论

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词