[NOI2020]制作菜品

本题看似非常像网络流,但实际上我们非常难限制只使用两种调料,这时候我们要敢于放弃去选择另一条路。

从部分分出发,看到数据范围当中出现了 \(m = n - 1\),刚好大小为 \(n\) 的树的边数不正好是 \(n - 1\) 吗?恰巧的是,每种菜只能选择不超过两种的调料,而菜有 \(n - 1\) 道,调料有 \(n\) 种,对于没道菜,在它所选的两个调料中间连一条边,既然题目刚好给了我们 \(m = n - 1\) 那么我们可以直接考虑一下最后连出来的图是一颗树的情况。

不难发现这一棵决策树的所有叶子节点的权值 \(d_i < k\),并且对于每个点它对周围边的贡献都是确定的(从叶子节点开始确定)所以对于任意一个点 \(u\) 去除掉它子树内的所有点并且减去这个点对其儿子边的贡献剩下的部分其实是另一个 \(u\) 为叶子节点的子问题。因此我们有了一个思路,能否每次确定叶子节点后将这些叶子节点删掉不断递归继续确定整棵树呢?实际上是可以的,不难发现问题在于我们怎样找到可供选择的叶子节点,继而可以发现这样一个事实,不论在哪个时刻,对于当前剩余的所有点中的最小值 \(d\) 一定是小于 \(k\) 的,也就是它能作为叶子节点。因为假如 \(d \ge k\),那么就会有 \(\sum d_i \ge n \times k > (n - 1) \times k\)。选定了这个叶子节点以后,我们还要选择其在决策点上的父亲才能不断递归下去,不难发现如果找到另一个 \(D, D + d \ge k\) 就可以让 \(D\) 作为 \(d\) 的父亲,于是我们又可以发现剩余 \(d_i\) 中的最大值 \(D\) 是满足 \(D + d \ge k\) 的,否则 \(\sum d_i < \lceil \frac{n}{2} \rceil k \le (n - 1)k\),那么我们将 \(d\) 在决策树上删去,将 \(D = D - (k - d)\) 重新放入当前新点就能递归下去了。因为每次我们都能找到合适的叶子节点和其父亲,因此在这种情况下是一定有解的。分析一下这个过程的复杂度,一共递归 \(n - 1\) 次,每次需要找到当前的最大值和最小值,可以使用 \(\rm std :\ : multiset\) 来实现,复杂度 \(O(n \log n)\)。

继续观察下一档部分分 \(m \ge n\),回想刚刚 \(m = n - 1\) 是如何解决问题的,主要是在于任意时刻都会存在 \(d < k, D + d \ge k\)。继而在 \(m \ge n\) 时我们可以发现,在任意时刻都存在 \(D \ge k\),否则 \(\sum d_i < n \times k \le m \times k\),因为可以只使用一种调料,我们直接一直找到 \(d_i \ge k\) 的调料使用一次即可,于是最后能划归成 \(m = n - 1\) 的子问题,复杂度 \(O((n + m) \log n)\)。

最后我们来思考 \(m = n - 2\) 的情况,直觉告诉我们如果有解那么一定存在一种解是左边一颗树右边一颗树。假如存在一种解不是上面那种形式,那么我们依然按照上面的方式进行连边,每个联通块会是 \(m \ge n - 1\) 的一个子问题,那么对于 \(m \ge n\) 的连通块,其上面连成自环的数量 \(S = n - 2 -\) 联通块数量,我们完全可以使用这些自环在不同联通块之间连边,这样恰好最后会生成 \(2\) 棵分开的树。于是我们现在的目的在于找出一个集合 \(S\),使得 \(\sum\limits_{i \in S} d_i = (|S| - 1)k\)。可以考虑使用一个 \(dp\),令 \(dp_{i, j, k}\) 表示当前是否能选到第 \(i\) 个数,选出了 \(j\) 个数,选出来的数的和为 \(k\),但这样的复杂度是 \(O(n ^ 3k)\) 的,实际上最后的合法状态是关乎两个维度的,一般的方法是把合法状态压到一维,观察上面的合法判定条件:\(\sum\limits_{i \in S} d_i = (|S| - 1)k\) ,将右边设置为定值即 \(|S|k - \sum\limits_{i \in S} d_i = k\),那么我们可以重新定义状态令 \(dp_{i, j}\) 表示当前选到第 \(i\) 个数,\(j = |S|k - \sum\limits_{i \in S} d_i\) 是否可行。再使用 \(\rm bitset\) 优化即可做到 \(O(\frac{n ^ 2k}{w})\),最后逐步确定集合内的数两边跑一遍 \(n - 1\) 的子任务即可。

#include<bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for(int i = l; i <= r; ++i)
#define Next(i, u) for(int i = h[u]; i; i = e[i].next)
const int N = 500 + 5;
const int M = 5000 + 5;
struct node{
    int p, w;
    bool operator < (const node &x) const{
        return w < x.w;
    }
}a[N];
bool F, book[N];
int T, n, m, k, cnt, d[N], ans[M][4];
bitset <2 * N * M> dp[N];
multiset <node> S;
multiset <node> :: iterator it;
int read(){
    char c; int x = 0, f = 1;
    c = getchar();
    while(c > '9' || c < '0'){ if(c == '-') f = -1; c = getchar();}
    while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
void solve(int n){
    S.clear(); rep(i, 1, n) S.insert(a[i]);
    while(S.size() > 1){
        it = S.begin(); int Mi = (*it).w, Mip = (*it).p; S.erase(it);
        it = (--S.end()); int Ma = (*it).w, Map = (*it).p; S.erase(it);
        ans[++cnt][0] = Map, ans[cnt][1] = k - Mi, ans[cnt][2] = Mip, ans[cnt][3] = Mi;
        S.insert((node){Map, Ma - k + Mi});
    }
}
void solve1(){
    int x = m;
    while(x > n - 1){
        rep(i, 1, n) if(d[i] >= k){ ans[++cnt][0] = i, ans[cnt][1] = k, d[i] -= k; break;}
        --x;
    }
    rep(i, 1, n) a[i].p = i, a[i].w = d[i];
    solve(n);
}
void dfs(int i, int j){
    if(!i) return;
    if(dp[i - 1][j + m * k]) dfs(i - 1, j);
    else book[i] = true, dfs(i - 1, j - (k - d[i]));
}
void solve2(){
    dp[0].reset(), dp[0][m * k] = 1;
    rep(i, 1, n){
        if(k - d[i] >= 0) dp[i] = dp[i - 1] | (dp[i - 1] << (k - d[i]));
        else dp[i] = dp[i - 1] | (dp[i - 1] >> (d[i] - k));
    }
    if(!dp[n][k + m * k]) F = 1;
    else{
        dfs(n, k); 
        int cnt = 0;
        rep(i, 1, n) if(book[i]) a[++cnt].w = d[i], a[cnt].p = i;
        solve(cnt);
        cnt = 0;
        rep(i, 1, n) if(!book[i]) a[++cnt].w = d[i], a[cnt].p = i;
        solve(cnt);
    }
}
int main(){
    T = read();
    while(T--){
        n = read(), m = read(), k = read();
        F = cnt = 0, memset(ans, 0, sizeof(ans)), memset(book, 0, sizeof(book));
        rep(i, 1, n) d[i] = read(); 
        if(n == 1){ rep(i, 1, m) printf("%d %d\n", 1, k); continue;}
        if(m >= n - 1) solve1();
        else solve2();
        if(F) puts("-1");
        else{
            rep(i, 1, m){
                if(ans[i][3] > 0) printf("%d %d %d %d\n", ans[i][0], ans[i][1], ans[i][2], ans[i][3]);
                else printf("%d %d\n", ans[i][0], ans[i][1]);
            }
        }
    }
    return 0;
}
上一篇:洛谷 P1993 【小K的农场】


下一篇:《算法导论》第三章函数的增长