#3342. 「NOI2020」制作菜品

题目链接

考场上想到 70pts,结果没清空+没特判转移来源丢掉 35pts

不那么显然,如果 \(m \ge n - 1\),那么一定有解。

考虑如果只剩下一堆大于 \(k\) 的材料,那么每次我们取一个,不够再取另一个,一定能取完。于是我们只用考虑怎么消灭小于 \(k\) 的材料。因为每次我们可以把最小的那个取走,不够再从大于 \(k-v_i\) 的那些材料里面找,每取一次肯定能消灭一个小的,并且我们能保证能找到大于 \(k-v_i\) 的材料,否则剩下的平均将小于 \(k-v_i\),仔细想想发现 \(m \ge n - 1\) 的时候不可能。因此我们能够在 \(m \ge n - 1\) 的情况下找到解。

现在考虑 \(m = n - 2\)。如果有解,一定时拿着拿着发现正好有两个的和为 \(k\),一下子消掉两个。换言之,我们能够将原材料分成两个集合,对于每个集合都符合 \(\sum_{i \in S} v_i = (|S| - 1) * k\) 的性质。那么我们直接背包 DP 即可拿到 70pts。

发现我们要记一个集合大小,很不方便。我们可以统一让 \(v_i\) 减去 \(k\),最后判断是否可能为 \(-k\) 即可。

发现我们的 DP 状态设成布尔类型肯定不好,并且发现每次就是对数组进行左移和或操作,因此直接上 bitset 优化即可。因为我们要知道转移路径,因此不能滚动数组,但是在 bitset 的强力优化下还是可行的(DP数组大概有几千万字节)。不过绝对不能记录 \(pre\),要当场手动模拟,看如果选择 \(cur\) 之前的状态是否合法,据此决定是否选择 \(cur\)

关键代码:

bool vis[567];
inline void Get_ans() {
	memset(vis, 0, sizeof(vis));
	for (register int i = 1; i <= stop; ++i) {
		nd[i] = (node){stk[i], d[stk[i]]};
		vis[stk[i]] = true;
	}
	sol(stop, stop - 1);
	int ntot = 0;
	for (register int i = 1; i <= n; ++i) {
		if (!vis[i])	nd[++ntot] = (node){i, d[i]};
	}
	sol(ntot, ntot - 1);
	Success();
}

bitset<NN> bt[505];
inline bool Try() {
	bt[0][base] = true;
	for (register int i = 1; i <= n; ++i) {
		int cha = d[i] - k;
		bt[i] = bt[i - 1] | (cha > 0 ? bt[i - 1] << cha : bt[i - 1] >> (-cha));
	}
	if (!bt[n][-k + base])	return false;
	int nw = -k;
	for (register int i = n; i; --i) {
		int cha = d[i] - k;
		if (nw - cha + base >= 0 && bt[i - 1][nw - cha + base]) {
			stk[++stop] = i;
			nw -= cha;
		}
	}
	return true;
}
posted @ 2020-08-20 19:59  JiaZP  阅读(189)  评论(0编辑  收藏  举报