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\) 的任意子集。可得:
令 \(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;
}
如果您是从洛谷题解过来的,觉得这篇题解解决了您的疑惑,帮到了您,别忘了回到洛谷题解区给我题解点个赞!