10.26-11.1总结

这两天切了不少搜索好题水题,来水个总结吧。

因为毛概要期末考试了,没时间写

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值