LianYunGang OI Camp: Contest #2

LianYunGang OI Camp: Contest #2

本次比赛选择了一些背包动态规划问题。

总体难度适中,预期人均通过三题以上。

A: Knapsack 1

Link: https://atcoder.jp/contests/dp/tasks/dp_d?lang=en

简单的 01背包问题。题解及代码略。

B: Match Matching

Link: https://atcoder.jp/contests/abc118/tasks/abc118_d?lang=en

如果把火柴棍拼出的数字视作物品,可以发现本题是一个完全背包模型。物品的重量就是所需火柴棍的个数,物品的价值则需要综合考量数字的位数与大小。

首先考虑答案的位数。一种自然的做法是,用状态 \(f_i\) 表示能由 \(i\) 根火柴拼出最大数字的位数,显然有:

\[\large f_i = \max_{k = A_1,\dots,A_m} \{ f_i,~f_{i-w_k} + 1 \} \]

其中 \(w_k\) 是拼出数字 \(k\) 对应所需的火柴棍根数。

求出最长位数之后,可以反过来寻找状态转移的路径(答案的每一个数位),注意优先取数值大的数字即可。复杂度是 \(O(MN + M\log M)\),证明留给读者。

#include <bits/stdc++.h>

const int w[] = {0, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int n, m, a[10], dp[16384];

int main() {
    std::cin >> n >> m;
    for (int i = 0; i != m; ++i) {
        std::cin >> a[i];
    }
    std::sort(a, a + m, std::greater<int>());

    memset(dp, -1, sizeof(dp));
    dp[0] = 0;
    for (int i = 0; i != m; ++i) {
        for (int j = w[a[i]]; j <= n; ++j) {
            if (dp[j - w[a[i]]] == -1) continue;
            dp[j] = std::max(dp[j], dp[j - w[a[i]]] + 1);
        }
    }

    while (n) {
        for (int i = 0; i != m; ++i) {
            if (n < w[a[i]]) continue;
            if (dp[n] - dp[n - w[a[i]]] == 1) {
                n -= w[a[i]];
                std::cout << a[i];
                break;
            }
        }
    }
    std::cout << std::endl;
    return 0;
}

C: Baby Ehab Partitions Again

Link: https://codeforces.com/problemset/problem/1516/C

先考虑一个子问题:如何检查某一组数 \(a_1, \dots, a_k\) 是否能被分为和相等的两部分?

当然应该检查 \(S_k = \sum_{i=1}^k a_i\) 的奇偶性。若 \(S\) 为奇数则必定无解,否则问题转化为:在 \(a_1, \dots, a_k\) 中选出一些数,使它们的和为 \(\frac{S_k}{2}\)

这是一个01背包模型。我们用状态 \(f_{i,j}\) 表示前 \(i\) 个数是否能表示出数字 \(j\),很自然地有:

\[\large f_{i, j} = f_{i-1, j} \or f_{i-1,j-a_i} \]

这个式子可以滚动,甚至可以 bitset 加速,但这不是很重要。

接着我们考虑原问题。原数列天然不能被分成两部分的不论,我们考虑 \(S_n\) 为偶数且数列可以被分成相等两部分的情况。

若:

  1. 存在一个奇的 \(a_i\),那么将其移除即可。
  2. 所有 \(a_i\) 均为偶数,容易发现我们令所有 \(a_i = a_i / 2\) 不会影响答案。于是我们可以不断进行除以二的操作,直到数列中出现奇数为止——或者可以一步到位,令 \(a_i = a_i / \gcd(a_1,\dots,a_n)\) 即可。
#include <bits/stdc++.h>

int n, com, sum, ans, a[128];
std::bitset<262144> f;

bool check() {
    if (sum & 1)  return 0;
    f[0] = true;
    for (int i = 0; i != n; ++i) {
        f |= (f << a[i]);
    }
    return f[sum / 2];
}

int gcd(int a, int b) {
    return (b == 0)? a: gcd(b, a % b);
}

int main() {
    std::cin >> n;
    for (int i = 0; i != n; ++i) {
        std::cin >> a[i];
        com = gcd(a[i], com);
    }
    for (int i = 0; i != n; ++i) {
        a[i] /= com;
        sum += a[i];
        if (a[i] & 1) ans = i;
    }
    
    if (check()) std::cout << 1 << '\n' << ans + 1 << std::endl;
    else         std::cout << 0 << std::endl;

    return 0;
}

D: Bottles

Link: https://codeforces.com/problemset/problem/730/J

我们课堂上讲过的原题。做法见之前分发的讲义。

E: Chef Monocarp

Link: https://codeforces.com/problemset/problem/1437/C

一个显要的观察是,对 \(t_i\) 升序排列之后依次取出的答案一定更优。证明很简单:假设 \(t_i < t_j, T_i > T_j\),那么有不等式 \(|t_i - T_i| + |t_j - T_j| > |t_i - T_j|+|t_j - T_i|\) 成立。所以一定是升序更优。

接着考虑在排序后的 \(t_i\) 上dp,设状态 \(f_{i, j}\) 表示前 \(i\) 道菜都已取出,且当前时间 \(T = j\) 时顾客的最小不满度,显然有转移:

\[\large f_{i, j} = \min(f_{i, j -1},~f_{i-1, j-1} + |t_i - j|) \]

#include <bits/stdc++.h>

int n, t[256], f[256][512];

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i = 0; i != n; ++i) {
            scanf("%d", &t[i]);
            t[i]--;
        }
        std::sort(t, t + n);

        memset(f, 0x7f, sizeof(f));
        f[0][0] = 0;
        for (int i = 0; i <= n; ++i) {
            for (int j = 0, siz = 2 * n - 1; j != siz; ++j) {
                f[i][j + 1] = std::min(f[i][j + 1], f[i][j]);
                if (i >= n) continue;
                f[i + 1][j + 1] = std::min(f[i + 1][j + 1], f[i][j] + std::abs(t[i] - j));
                
            }
        }
        printf("%d\n", f[n][2 * n - 1]);
    }
    return 0;
}

F: Unmerge

Link: https://codeforces.com/problemset/problem/1381/B

首先观察到一个结论:某段连续的 \(a_i, a_{i + 1}, \dots, a_k, ~a_i = \max_{i \leq j \leq k} a_j\) 一定不会被拆分到两个不同子序列中去,否则就不能保证归并后 \(a_i\) 的位置。

于是我们可以考虑将给定的 \(a_1, \dots, a_{2n}\) 分成一些满足上述性质的连续子段 \(t_1,\dots,t_k\),再考虑这些子段的组合。
以样例 3 2 6 1 5 7 8 4 为例,我们可以将其分为 [3 2][6 1 5][7][8 4],并可以发现 [3 2 | 8 4], [6 1 5 | 7] 可归并成原序列。

容易进一步观察到,无论这些子段如何归属到两个子序列中,归并结果都总是原序列。我们只需要保证题设的两个子序列长度相等即可。于是问题转化为给定一些数字 \(|t_1|,\dots,|t_k|\),询问是否能从中选出一些数字,使它们的和为 \(n\)。这是一个容易dp的问题。

考虑用状态 \(f_{i, j}\) 表示在前 \(i\) 个数中选出一些,它们的和为 \(j\) 的可行性。显然有转移:

\[\large f_{i, j} = f_{i - 1, j} \or f_{i - 1, j - |t_i|} \]

容易压缩一维,虽然不必要。

#include <bits/stdc++.h>

const int N = 4096;
int n, a[N], t[N];
bool f[N];

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i = 0; i != 2 * n; ++i) {
            scanf("%d", a + i);
        }

        int siz = 0, ind = 0, cnt = 0, head = a[0];
        while (ind != 2 * n) {
            if (a[ind] <= head) {
                ++cnt, ++ind;
            } else {
                t[siz++] = cnt;
                cnt = 0, head = a[ind];
            }
        }
        if (cnt) {
            t[siz++] = cnt;
        }

        memset(f, false, sizeof(f));
        f[0] = true;

        for (int i = 0; i != siz; ++i) {
            for (int j = n; j >= t[i]; --j) {
                f[j] |= f[j - t[i]];
            }
        }
        if (f[n]) puts("YES");
        else puts("NO");
    }
    return 0;
}
posted @ 2021-07-17 21:06  Jane_leaves  阅读(70)  评论(0编辑  收藏  举报