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) 根火柴拼出最大数字的位数,显然有:
其中 (w_k) 是拼出数字 (k) 对应所需的火柴棍根数。
求出最长位数之后,可以反过来寻找状态转移的路径(答案的每一个数位),注意优先取数值大的数字即可。复杂度是 (O(MN + Mlog 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),很自然地有:
这个式子可以滚动,甚至可以 bitset
加速,但这不是很重要。
接着我们考虑原问题。原数列天然不能被分成两部分的不论,我们考虑 (S_n) 为偶数且数列可以被分成相等两部分的情况。
若:
- 存在一个奇的 (a_i),那么将其移除即可。
- 所有 (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 << '
' << 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) 时顾客的最小不满度,显然有转移:
#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
", 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) 的可行性。显然有转移:
容易压缩一维,虽然不必要。
#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;
}