P6775 NOI2020 制作菜品

P6775 NOI2020 制作菜品

给定正整数 \(n\)\(m\)\(k\)

有一个 \(m\)\(k\) 列网格,每个网格可以被涂上 \(n\) 种颜色之一,要求:

  • 一行最多出现两种颜色。
  • \(i\) 种颜色必须恰好被使用 \(a_i\) 次。

\(\{a_i\}\) 给定,保证 \(\sum a_i = m \times k\)。请构造涂色方案或判定不存在。

多测,最多 \(10\) 组数据。\(1 \le n \le 500\)\(\boldsymbol{n - 2 \le m} \le 5000\)\(m \ge 1\)\(1 \le k \le 5000\)

\(\boldsymbol{m = n - 1}\)

虽然这题是个黑题,但是我们仍然可以发现,\(a\) 的顺序和答案完全无关。两种套路:

  • 先对 \(a\) 排序。
  • \(a\) 建立权值数组,在权值数组上做。

这里一看就是第一种,那就先对 \(a\) 排个序。

然后我们开始观察这个仅次于暴力的第一档部分分。

首先我们发现,如果没有每行颜色种类限制,只要按照 \(a_i\) 随便涂就可以了。因为有了颜色种类限制,所以如果我们选择把某两个要求使用次数很少的颜色涂在同一行,使得这行没被涂完,就会导致不合法。

因此,对于要求使用次数最少的颜色,我们可以让它和次数最多的颜色一起涂,这样贪心还是比较优秀的。

简单观察不难发现(这里的 \(a_1\)\(a_n\) 是按 \(a\) 不降排序后意义上的):

  • \(a_1 < k\)
    • 否则 \(\sum a \ge nk > mk = \sum a\) 显然不成立。
  • \(a_1 + a_n \ge k\)(\(n \ge 2\) 时)。
    • 反证法。假设 \(a_1 + a_n < k\),则 \(a_n < k - a_1\),则 \(\sum a_i < a_1 + (n - 1)(k - a_1) = (2 - n)a_1 + (n - 1)k \le (n- 1)k\),和 \(\sum a = (n - 1)k\) 矛盾。

注意到上面两条的证明依赖于 \(a_i \ge 0\)\(a_i\) 可以为 \(0\)

综上所述,我们可以在第一行直接涂上 \(a_1\) 个颜色 \(1\),以及 \(k - a_1\) 个颜色 \(n\)

这样以来,对于第 \(2 \sim m\) 行的涂色,可以看做颜色种类数少了 \(1\),涂色行数少了 \(1\),同时仍然有 \(\sum a_i = (n - 1)k\) 的一个子问题。这里,第一种颜色一定会被涂完,我们直接丢弃这种颜色;而第 \(n\) 种颜色可能会被涂完,此时我们也不将第 \(n\) 种颜色丢弃,而是看做 \(a_n = 0\) 的一种颜色,这样也是合法的。于是问题成功归纳。

归纳的边界是 \(n = 1\)\(m = 0\),显然此时已经不需要涂色了,问题解决。

(此时剩下的那个颜色 \(a_1\) 一定有 \(a_1 = 0\)。)

\(\boldsymbol{n - 1 \le m \le 5 \times 10^3}\)

其实整道题不难发现,在 \(a_i\) 不变的情况下,\(m\) 越大(对应地 \(k\) 越小)时,涂色越容易。所以这个问题应该是比上面那个问题弱的。

事实上确实如此,直接把颜色数补齐到 \(m + 1\) 就行了。具体来说,就是新建 \(m + 1 - n\)\(a_i = 0\) 的颜色即可。。

到这里已经解决 45 分了。

我们观察一下新建颜色的实质,其实就是让前 \(m + 1 - n\) 次涂色都是将 \(a\) 最大的那个颜色涂完一整行。根据新建颜色,并套用 \(m = n - 1\) 的证明,可以得到在 \(m > n - 1\) 时,\(\max\{a_i\} \ge k\)。当然,这个结论也可以很简单地通过 \(m \ge n\) 时,\(\sum a_i = mk \ge nk\) 所以最大值肯定不小于 \(k\) 得到。

所以代码实现就不用新建颜色了,让头 \(m + 1 - n\) 次颜色都让 \(a\) 最大的颜色涂完一整行,转到 \(m = n - 1\) 的情况即可。

\(\boldsymbol{m = n - 2}\)

到这里没啥思路了,不妨考虑构造最常用的方法:建图。尤其是每行最多涂两个颜色的限制,启发我们对每行所涂的两种颜色连边。

那么题目变成:对 \(n\) 个点连接 \(m = n - 2\) 条边,并把点权按任意非负整数比例分配,贡献给它所连接的边的边权上,使得所有边边权恰好为 \(k\)。显然分配结束后所有点权应恰为 \(0\)

这里一行颜色全为 \(u\),可以看做这个颜色点和其它任何一个点 \(v\) 连了一条边,并且 \(v\) 没有给这条边分配权值,只有 \(u\) 给这条边分配了恰好为 \(k\) 的权值。

看起来无从下手,但是其实 \(m \ge n - 1\) 的情况刚刚已经解决,保证给出一组方案了,只要把刚刚的思路放在图上即可,具体如下:

  • 对于前 \(m - n + 1\) 条边,我们连接点权最大的点 \(u\) 和任意点 \(v\),并将 \(u\) 的点权分配 \(k\) 的权值给这条边。(这里分配完权值后,\(u\) 的点权也要动态地减去 \(k\))。
  • 对于后 \(n - 1\) 条边,我们连接点权最大的点 \(u\) 和点权最小的 未标记点 \(v\),然后:
    • \(u\) 的点权分配 \(k - a_v\) 给这条边。
    • \(v\) 的点权 \(a_v\) 全部分配给这条边,并 标记 \(v\)

这里一个点被标记,等价于之前对 \(m \ge n - 1\) 方案的讨论中,一个颜色种类被丢掉。

那么对于 \(m = n - 2\) 我们如何构造?观察到 \(m = n - 2\) 时如果有解,任何一组解生成的图,一定不连通,即至少有 \(2\) 个以上的连通块。下设全集 \(U = \{1, 2, \ldots, n\}\),第 \(i\) 个连通块点集为 \(S_i\)\(n_i = |S_i|\),并且这个连通块内部边数为 \(m_i\)。有以下发现:

  • \(\sum n_i = n\)\(\sum m_i = m = n - 2\)
  • 根据连通块的连通性,\(m_i \ge n_i - 1\)
  • \(i\) 个连通块内部的点权明显要被这 \(m_i\) 条边分配完(因为其它边不分配这些点权),所以 \(\sum\limits_{u \in S_i}a_u =m_ik\)
  • 至少存在两个 \(i\) 满足 \(m_i = n_i - 1\)
    • 否则,若最多存在一个 \(i\) 满足 \(m_i = n_i - 1\),会得到 \((\sum m_i) \ge (\sum n_i) - 1\),也即 \(m \ge n - 1\),矛盾。

因此,\(m = n - 2\) 存在解的一个必要条件是:存在一个 \(S \subseteq U\),使得 \(\sum\limits_{u \in S}a_u = (|S| - 1)k\)(也即对上面满足 \(m_i = n_i - 1\) 的两个集合之一的描述)。

下面给出找到这样一个 \(S\) 后的构造方案。

因为 \(\sum\limits_{u \in S}a_u = (|S| - 1)k\),则考虑 \(T = U \setminus S\),也有 \(\sum\limits_{u \in T}a_u = (|T| - 1)k\)。所以只需要分别对 \(S\)\(T\) 分配 \(|S| - 1\)\(|T| - 1\) 条边,分别构造。对 \(S\) 构造 \(|S| - 1\) 条边的方案是前面已经解决过的问题。

所以存在一个 \(S \subseteq U\),使得 \(\sum\limits_{u \in S}a_u = (|S| - 1)k\) 不仅是原问题有解的必要条件,也是充分的。接下来只需解决一个问题:如何快速找到这个 \(S\)

这个问题很类似背包恰满问题,也就是在 \(n\) 个有体积的物品中,找到 \(|S|\) 个物品,恰满容量为 \((|S| - 1)k\) 的背包。

恰满的容量和物品选择的数量有关,不太好处理。可以对 \(\sum\limits_{u \in S}a_u = (|S| - 1)k\) 处理成 \(\sum\limits_{u \in S} a_u - k = -k\)

也就是 \(n\) 个物品,第 \(i\) 个物品体积为 \(a_i - k\),求一个恰满体积 \(-k\) 背包的容量组合。那就变成经典的容量恰满问题了。

根据 \(0 \le \sum\limits_{u \subseteq S} a_u \le (n - 2) \times k\)。这里 \(S\) 代表 \(U\) 的任意子集。可得:

\[-nk \le -|S|k \le \sum\limits_{u \subseteq S} a_u - k \le (n - 2 - |S|)k \le (n - 2)k \]

\(f(i, j)\) 表示前 \(i\) 个物品能否凑出体积 \(j\),根据上面的式子,\(j\) 的范围应为 \([-nk, (n - 2)k]\)

转移是 \(f(i, j) = f(i - 1, j) \lor f(i - 1, j - v_i)\),其中 \(v_i = a_i - k\)

可以考虑用 bitset 优化,设 \(f(i)\) 为布尔型数组,第 \(j\) 项为原先的 \(f(i, j)\)。在 bitset 上有:

f[i] = f[i - 1] | (f[i - 1] << v[i])。滚动一下得到 f |= f << v[i]。当然后面要输出方案所以别滚动了。

输出方案:设 \(f(i, j) = \mathrm{true}\),检查 \(f(i - 1, j)\)\(f(i - 1, j - v_i)\) 哪个是 \(\mathrm{true}\) 即可(这两个肯定有一个是 \(\mathrm{true}\)),如果前者 \(\mathrm{true}\) 就不取第 \(i\) 个物品,转到 \(f(i - 1, j)\);否则就取第 \(i\) 个物品,转到 \(f(i - 1, j - v_i)\)。如果两个都是 \(\mathrm{true}\) 说明无论取不取第 \(i\) 个物品都行。从 \(f(n, -k)\) 倒推做上面的操作即可。

背包复杂度是物品数量 \(\times\) 物品子集和的值域大小 \(\div\) bitset 优化的常数,即 \(\Theta\left(\dfrac{nt}{w}\right) = \Theta\left(\dfrac{n^2k}{w}\right)\)。这里 \(t\) 表示 \(a_i\) 任意子集的和的值域范围的长度,也即 \([-nk, (n - 2)k]\) 的长度,为 \(\Theta(nk)\) 量级。

然后转化为 \(m \ge n - 1\) 就是取 \(m\)\(a\) 的最大最小值, 直接暴力,复杂度是 \(\Theta(nm)\)

所以总复杂度 \(\Theta\left(T\left(\dfrac{n^2k}{w} + nm\right)\right)\),算下来大概 \(2 \times 10^8\),还可以。

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2023-07-13 10:02:06 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2023-07-13 11:10:23
 */
#include <bits/stdc++.h>
inline int read() {
	int x = 0;
	bool f = true;
	char ch = getchar();
	for (; !isdigit(ch); ch = getchar())
		if (ch == '-')
			f = false;
	for (; isdigit(ch); ch = getchar())
		x = (x << 1) + (x << 3) + (ch ^ '0');
	return f ? x : (~(x - 1));
}

const int N = 505;
struct node {
	int val, id;
	bool operator < (node b) {
		if (val != b.val)
			return val < b.val;
		return id < b.id;
	}
};
int n, m, k;
std :: bitset <500 * 5000 * 2> f[N];

inline void easy(std :: vector <node> a) {
	int n = (int)a.size();
	for (int i = 1; i < n; ++i) {
		int x = std :: min_element(a.begin(), a.end()) - a.begin();
		int y = std :: max_element(a.begin(), a.end()) - a.begin();
		int p = a[x].val, q = k - p;
		if (p)
			printf("%d %d %d %d\n", a[x].id, p, a[y].id, q);
		else
			printf("%d %d\n", a[y].id, k);
		a[y].val -= q;
		std :: swap(a[x], a.back());
		a.pop_back();
	}
}

inline void solve() {
	n = read(); m = read(); k = read();
	std :: vector <node> a;
	for (int i = 1; i <= n; ++i)
		a.push_back({read(), i});
	if (m >= n - 1) {
		for (int i = 1; i <= m - n + 1; ++i) {
			int x = std :: max_element(a.begin(), a.end()) - a.begin();
			a[x].val -= k;
			printf("%d %d\n", a[x].id, k);
		}
		easy(a);
	} else {
		f[0].reset();
		f[0].set(n * k);
		for (int i = 1; i <= n; ++i) {
			int v = a[i - 1].val - k;
			if (v > 0)
				f[i] = (f[i - 1] | (f[i - 1] << v));
			else
				f[i] = (f[i - 1] | (f[i - 1] >> (-v)));
		}
		if (!f[n][-k + n * k])
			return void(puts("-1"));
		std :: vector <node> S, T;
		for (int i = n, j = -k + n * k; i; --i) {
			int v = a[i - 1].val - k;
			if (f[i - 1][j])
				T.push_back(a[i - 1]);
			else {
				S.push_back(a[i - 1]);
				j -= v;
			}
		}
		easy(S); easy(T);
	}
	return ;
}

int main() {
	int T = read();
	while (T--)
		solve();
	return 0;
}

如果您是从洛谷题解过来的,觉得这篇题解解决了您的疑惑,帮到了您,别忘了回到洛谷题解区给我题解点个赞!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值