这两天切了不少搜索好题水题,来水个总结吧。
因为毛概要期末考试了,没时间写
1.洛谷P5194:题目给出的数据范围是N<=1000,但是题目中有一句关键信息:每个砝码的质量至少等于前面两个砝码,是很像斐波那契数列的!而C<=2^30!于是我们可以大概估算一下N的真实范围其实是N<=40。于是我的第一种思路就是直接硬搜,加个剪枝也就过了(欸,真的有剪枝么)。下面是核心代码。
void dfs(int x,int num){
if(x==n+1){
ans=min(ans,num);
return;
}
if(num>=ans)return;//当前选出的组数已经大于最优解,则不继续考虑
for(int i=1;i<=num;i++){
if(b[i]+a[x]<=m){
b[i]+=a[x];
dfs(x+1,num);
b[i]-=a[x];
}
}
b[num+1]+=a[x];
dfs(x+1,num+1);
b[num+1]-=a[x];
}
但是我做这题的目的其实是学一下折半搜索:
假如一类问题有明确的初态和末态,那我们可以从初态和末态开始搜索,各搜索一半的深度,最后合成我们想要的答案,来替代整颗递归搜索树。直接放代码吧(懒得画图)
void dfs1(int x,int s){//考虑前一半的礼物
if(x==mid){
b[cnt++]=s;//存储某个合法的重量
return;
}
//每个礼物可以选或者不选
dfs1(x+1,s);
if(s+a[x]<=m){
dfs1(x+1,s+a[x]);
}
}
void dfs2(int x,int s){
if(x==n){
ans=max(ans,*(upper_bound(b,b+cnt,m-s)-1)+s);//二分从数组中查找答案
return;
}
dfs2(x+1,s);
if(s+a[x]<=m){
dfs2(x+1,s+a[x]);
}
}
2.洛谷P1731生日蛋糕:也是一道搜索的好题啦。题意:给定体积N和层数m,找出蛋糕最大的表面积,上层蛋糕的高度hi和ri是一定比下层高的。注意到蛋糕的表面积S=每层蛋糕的侧面积+最下面一层的蛋糕的底面积,就可以减少代码量了。(如图)
优化:1.类似前缀和的思想,存储minv[i]和mins[i],记录上层最少还需要的体积与表面积,若当前体积已经不够我们做出蛋糕了,剪枝。即(V+minv[x-1]>n时return)
2.加上mins上层的最佳答案若大于当前的最优解,剪枝。即(S+mins[x-1]>=ans时return)
3.注意枚举顺序,显然我们从下往上枚举比较好,上一层的从H=min(h-1,(n-V-minv[x-1])/i/i)开始枚举,可以在循环中省掉很多。
4.这步剪枝非常巧妙。注意到圆柱的体积V=PI*r²*h,表面积S=2*PI*r*h,这两者之间是有很微妙的关系的。S=2*V/r,若由当前体积推出的表面积已经超过了我们当前的最优解,剪掉。
void dfs(int x,int r,int h,int s1,int s2){
//参数含义:x代表当前在搜第几层,r,h代表当前层的半径高度
//s1则是当前体积V,而s2则是表面积
if(x==0){
if(s1==n)ans=min(ans,s2);
return;
}
if(s1+minv[x-1]>n)return;
if(s2+mins[x-1]>=ans)return;
if(s2+2*(n-s1)/r>=ans)return;
for(int i=r-1;i>=x;i--){
int d=0;
if(x==m)d=i*i;
int H=min(h-1,(n-s1-minv[x-1])/i/i);
for(int j=H;j>=x;j--){
dfs(x-1,i,j,s1+i*i*j,s2+2*i*j+d);
}
}
}
3.P1120小木棍:相信佬们早就切过了,只有我一直不会做。这道题很经典了,剪枝必切的好题,运用了114514很多种剪枝。题意很简单:有若干根小木棍,拼成几根长度一样的大木棍,找这个木棍的最小长度。纯搜索应该很好写,基本没加什么剪枝,喜提20分,于是我们开始考虑优化。
优化:1.注意枚举顺序。这个应该很好想,显然我们应该先枚举木棍长度长一些的,方便拼凑木棍。
2.记录所有读入木棍的和sum,我们要搜的答案一定是sum的一个因子,注意的是这个因子应大于所有读入木棍的最大值。
3.当前拼出来的木棍长度已经大于当前答案长度则退出。
4.我当时是真的想不到这一点啊,应该是太菜了。我们用一个next数组存储每一个长度出现的最后位置,防止重复搜索搜过的长度。
5.我们当前木棍的长度为sum,二分找到第一根小于sum-now的木棍,不过好像意义不算太大,因为n<=65,算是一个小优化吧。
6.很难想的一个优化,由于我们的枚举顺序是从大到小的,若发现sum-now=a[i],说明不用继续枚举了,因为一定有更优的方法。
最后从sum的因子从小到大找答案,找到答案立刻退出dfs。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N=110;
int n,a[N],_max,s,ans=0,flag=0,NEXT[N],cnt,vis[N];//NEXT用来表示下一根不同长度的棍子
bool cmp(int x,int y){
return x>y;
}
vector<int>f(int x){
vector<int>res;
for(int i=2;i<=x/i;i++){
if(x%i==0){
res.push_back(i);
if(i!=x/i)res.push_back(x/i);
}
}
res.push_back(x);
return res;
}
int cal(int l,int r,int x){
while(l<r){
int mid=l+r>>1;
if(a[mid]>x)l=mid+1;
else r=mid;
}
return l;
}
void dfs(int k,int sum,int last,int now){//当前拼了k根,需要拼接木棍的长度,当前拼的长度
//cout<<k;
if(flag)return;
if(now>sum)return;
//if(ans)return;
if(sum==now){//当前木棍拼完咯~
if(k*sum==s){ans=sum;flag=1;return;}//找到答案了
for(int i=1;i<=n;i++){
if(!vis[i]){
vis[i]=1;
dfs(k+1,sum,i,a[i]);
vis[i]=0;
if(flag)return;
//break;
return;
}
}
}
int x=cal(last+1,n,sum-now);
for(int i=x;i<=n;i++){
if(!vis[i]&&now+a[i]<=sum){
vis[i]=1;
if(now+a[i]<=sum)dfs(k,sum,i,now+a[i]);
vis[i]=0;
if(flag)return;
if(sum-now==a[i]||now==0)return;//当前没有拿木棍的不往下考虑,后面长度只会越来越小 所以退出
i=NEXT[i];//找到最后一个自己相等的位置 不要重复找相同长度
if(i==n)return;
}
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
s+=a[i];
_max=max(_max,a[i]);
}
vector<int>prim=f(s);
sort(prim.begin(),prim.end());
sort(a+1,a+n+1,cmp);
NEXT[n]=n;
for(int i=n-1;i>0;i--){
if(a[i]==a[i+1])NEXT[i]=NEXT[i+1];
else NEXT[i]=i;
}
for(vector<int>::iterator it=prim.begin();it!=prim.end();it++){
if(*it>=_max){
//开搜
//cout<<*it<<" ";
if(*it==s){
ans=s;
break;
}
dfs(1,*it,0,0);
if(flag){break;}
}
}
cout<<ans;
return 0;
}