【DG特长生2012 T3】【SSL 2240】栅栏的木料

栅栏的木料

题目链接:SSL 2240

题目大意

给你一些数,你可以把它拆开成几个加起来等于它的数。
然后再给你一些目标数,问你可以得到多少个其中这样的数。
如果两个目标数相同,那你如果要得到这两个,就要得到两个这样的数。

思路

这道题我们其实可以直接暴力 dfs,问题是如何剪枝。

首先我们要发现你选的数肯定是从小到大开始选,能选就选。
因为如果你选了大的而不选小的,那这个大的用这个小的数代替,也是可行的,还会留下更多的空间。

那你会发现可以枚举选前面多少个数,看能不能被凑出来。
然后你会发现到一定的位置就凑不出来了,再往后也凑不出来,那它就满足了单调性,可以进行二分。

那这里优化完了,我们就考虑如何看这些数能不能凑出你要的那些数。
那我们就 dfs 暴力,对于每个要的数看它可以由那个数拆出来给。

但是这样很明显就会超时,于是我们考虑剪枝优化:

剪枝1:如果你要凑出的数的和大于你有的数的和,那很明显就不能凑出。
剪枝2:有一些你有的出连最小的要凑出的数都凑不出,那这个木板可以直接舍去不要。
剪枝3:在操作的过程总如果有一些数被减到来弄最小的需要的木板都凑不出,那这个木板就直接不要了。
剪枝4:我们可以搞一个变量,记录还可以损失多少的数,然后操作的时候维护一下。如果它要被减到负数,那这种方案就是不可行的。
剪枝5:我们可以把给你的数和要拼的数都从小到大排序一下,算的时候会减少时间,也可以根据这个进行下面的剪枝6。
剪枝6:如果两个要拼的数一样大,那你可以直接从拼出上一个要拼的数的位置开始看选哪个数拼出来。(因为根据前面,你前面的一定不能拼出它,就算能,它就像 a b ab ab b a ba ba 一样, a b ab ab 相等,它是重复的)

然后大概这么搞搞就好了。

代码

#include<cstdio>
#include<algorithm>
#define rr register

using namespace std;

int n, board[31], l, r, sb[31], wc, nb[31];
int m, wood[1051], ans, sw[1051];
bool kill[31];
int little;

bool cmp(int x, int y) {
	return x > y; 
}

bool dfs(int now, int last) {
	if (now < 1) return 1;
	
	//如果需要的数跟前面的一样,那就直接从上次枚举的继续(剪枝)
	if (wood[now] != wood[now + 1])//否则就从开头找
		last = little;
	
	for (int i = last; i <= n; i++)
		if (!kill[i] && nb[i] >= wood[now]) {
			nb[i] -= wood[now];
			if (nb[i] < wood[little]) {//这个数剩余的不能拆出任何的你需要的数,连最小的都不可以
				if (wc < nb[i]) {//不能浪费这么多的数,那就不能让它选
					nb[i] += wood[now];//记得回溯
					continue;
				}
				wc -= nb[i];
				kill[i] = 1;//记录这个木板已经不能用了(因为你后面枚举到的要的数会更大,就更加不可能用它)
				if (dfs(now - 1, i)) return 1;
				wc += nb[i];
				kill[i] = 0;
			}
			else {//用这个数拆
				if (dfs(now - 1, i)) return 1;
			}
			nb[i] += wood[now];
		}
	
	return 0;
}

bool check(int now) {
	if (sb[n] < sw[now]) return 0;//所有的数加起来都无法凑到你要的数的和,就不肯能有解(剪枝)
	wc = sb[n] - sw[now];//求出顶多可以浪费多少的数
	little = 1;
	while (board[little] < wood[1] && little <= n) {//有一些给出的数根本不能满足任何要的数,直接舍去不管(剪枝)
													//【这个可以放到排序后,不用二分的时候搞】
		wc -= board[little];
		if (wc < 0) return 0;
		little++;
	}
	for (int i = 1; i <= n; i++)//初始化(一定要,因为你会中途退出)
		nb[i] = board[i], kill[i] = 0;
	return dfs(now, 1);
}

int main() {
//	freopen("fence.in", "r", stdin);
//	freopen("fence.out", "w", stdout);
	
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &board[i]);
	scanf("%d", &m);
	for (rr int i = 1; i <= m; i++) scanf("%d", &wood[i]);
	
	sort(board + 1, board + n + 1);//将他们从小到大排序(按一定顺序搞时间会短,而且可以进行剪枝)
	sort(wood + 1, wood + m + 1);
	
	for (int i = 1; i <= n; i++)//算前缀和
		sb[i] = sb[i - 1] + board[i];
	for (int i = 1; i <= m; i++)
		sw[i] = sw[i - 1] + wood[i];
	
	l = 1;
	r = m;//二分
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (check(mid)) {
			ans = mid;
			l = mid + 1;
		}
		else r = mid - 1;
	}
	
	printf("%d", ans);
	
	fclose(stdin);
	fclose(stdout); 
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值