P6775 NOI2020 制作菜品

P6775 NOI2020 制作菜品

给定正整数 nnmmkk

有一个 mmkk 列网格,每个网格可以被涂上 nn 种颜色之一,要求:

  • 一行最多出现两种颜色。
  • ii 种颜色必须恰好被使用 aia_i 次。

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

多测,最多 1010 组数据。1n5001 \le n \le 500 n2m5000\boldsymbol{n - 2 \le m} \le 5000m1m \ge 11k50001 \le k \le 5000

m=n1\boldsymbol{m = n - 1}

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

  • 先对 aa 排序。
  • aa 建立权值数组,在权值数组上做。

这里一看就是第一种,那就先对 aa 排个序。

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

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

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

简单观察不难发现(这里的 a1a_1ana_n 是按 aa 不降排序后意义上的):

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

注意到上面两条的证明依赖于 ai0a_i \ge 0aia_i 可以为 00

综上所述,我们可以在第一行直接涂上 a1a_1 个颜色 11,以及 ka1k - a_1 个颜色 nn

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

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

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

n1m5×103\boldsymbol{n - 1 \le m \le 5 \times 10^3}

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

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

到这里已经解决 45 分了。

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

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

m=n2\boldsymbol{m = n - 2}

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

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

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

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

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

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

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

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

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

下面给出找到这样一个 SS 后的构造方案。

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

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

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

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

也就是 nn 个物品,第 ii 个物品体积为 aika_i - k,求一个恰满体积 k-k 背包的容量组合。那就变成经典的容量恰满问题了。

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

nkSkuSauk(n2S)k(n2)k-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)f(i, j) 表示前 ii 个物品能否凑出体积 jj,根据上面的式子,jj 的范围应为 [nk,(n2)k][-nk, (n - 2)k]

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

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

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

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

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

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

所以总复杂度 Θ(T(n2kw+nm))\Theta\left(T\left(\dfrac{n^2k}{w} + nm\right)\right),算下来大概 2×1082 \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;
}

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

posted @   dbxxx  阅读(176)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示