#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;
}
原文地址:https://www.cnblogs.com/JiaZP/p/13537553.html