洛谷P4799 世界冰球锦标赛 搜索+二分

前言

本来是在做搜索剪枝题单的倒数第二题,发现不会做,看了看题解里大佬说这一题更容易,于是打算先从简单入手。

自己YY了个做法,推了推复杂度,好像能过,码了码交上去WA一片,仔细观察了一下发现是二分之前忘记排序了……排序后交上去果然过了。

AC之后片刻,突然意识到自己像个sb:为什么不用upper_bound()呢?


题目描述

题目链接:洛谷P4799 世界冰球锦标赛.

大意是,有 n n n个数,还有一个数 m m m,求 n n n个数任选若干数的和不超过 m m m的方案数有多少。

例如:

	n = 5, m = 1000
	a[] = 100 1500 500 500 1000

一共有 8 8 8种方案。


题目分析

观察到 n n n只有40,于是我们先考虑暴力。

枚举每一个数 选 or 不选,在dfs的过程中记录当前已选数的和,最后判断是否小于 m m m,复杂度 O ( 2 n ) O(2^n) O(2n).

可是这个 n n n有40啊,要是能让 n n n减半该多好。

于是这么思考了起来:将 n n n个数分成前 n / 2 n/2 n/2个数与后 n / 2 n/2 n/2个数两部分,对前半部分枚举每一个数 选 or 不选,每种方案所选数的和放入 s 1 [ ] s1[] s1[];对后半部分进行同样的操作,将每种方案所选数的和存入 s 2 [ ] s2[] s2[]。对于 s 1 [ ] s1[] s1[]中的一个和 a a a s 2 [ ] s2[] s2[]中所有小于等于 m − a m-a ma的方案都是可行的。

于是只需要对枚举 s 1 [ ] s1[] s1[]中的每一个 a a a,计算 s 2 [ ] s2[] s2[]中小于等于 m − a m-a ma的数有多少个,那么答案就加多少。这里可以对 s 2 [ ] s2[] s2[]排序之后,进行二分查找,总复杂度 O ( 2 n 2 l o g ( 2 n 2 ) ) = O ( n 2 n 2 ) O(2^\frac{n}{2}log(2^\frac{n}{2}))=O(n2^\frac{n}{2}) O(22nlog(22n))=O(n22n),可以过掉。

我忘了有个upper_bound()于是手写的二分,不得不说要注意的细节还是有一些的。


示例代码

  • 手写二分:

vector<int> s[2]; //s[0]与s[1]分别对应上文说的s1与s2
int bi_search(LL v){
	int l, r, mid;
	l = 0;	r = (int)s[1].size()-1;
	if(r == -1)	return 0;	//这句不要也行,可以想想为什么
	
	while(l < r){ // 最后l与r指向第一个大于v的数
		mid = (l+r) >> 1;
		if(s[1][mid] <= v)
			l = mid+1;
		else	r = mid;
	}
	if(l==(int)s[1].size()-1 && v>=s[1][l])//特判v比最大值的情况
		++ l;
	return l;
}

  • upper_bound()二分 (秒杀)
int pos = upper_bound(s[1].begin(), s[1].end(), m-s[0][i]) - s[1].begin() //太简单了点hhh
  • 完整代码
#include<bits/stdc++.h>
using namespace std;

typedef long long LL;

int n, ed[2]; //ed[0]:前半段的结束位置    ed[1]:后半段的结束位置
LL m, Ans;
LL a[45];
vector<LL> s[2];// 文中的s1、s2

void dfs(int now, int tp, LL sum){//now:当前为a[now],  tp:0:前半段/1:后半段,  sum:当前已选数的和
	if(sum > m)	return; //小剪枝
	if(now > ed[tp]){ // 枚举完这半段
		s[tp].push_back(sum);
		return;
	}
	
	dfs(now+1, tp, sum+a[now]); //选
	dfs(now+1, tp, sum); //不选
}

int main(){
	cin >> n >> m;
	for(int i=1; i<=n; ++i)
		cin >> a[i];
	
	ed[0] = n/2;		ed[1] = n; //前半段和后半段的结束位置分别为n/2 与 n
	dfs(1, 0, 0);	dfs(n/2+1, 1, 0);
	
	sort(s[1].begin(), s[1].end()); // 二分前千万不要忘了排序!
	int l = s[0].size();
	for(int i=0; i<l; ++i){
		int pos = upper_bound(s[1].begin(), s[1].end(), m-s[0][i]) - s[1].begin(); // 二分
		Ans += pos;
	}
	
	printf("%lld", Ans);
	return 0;
}

后记

二分前忘记排序,忘记upper_bound()这些小失误依旧存在,不过能够明显感觉自己码力变强了不少,以前碰到这样的题可能思考都不会思考,写个暴力就走人了,现在能够进一步的有新的想法,还是对自己很有期待的。

继续加油!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值