AtCoder NOMURA 2020 题解
题目传送门:AtCoder NOMURA Programming Competition 2020。
A - Study Scheduling
略。
#include <cstdio>
int main() {
int H1, M1, H2, M2, K;
scanf("%d%d%d%d%d", &H1, &M1, &H2, &M2, &K);
printf("%d\n", (H2 - H1) * 60 + M2 - M1 - K);
return 0;
}
B - Postdocs
易证存在一种最优解为把所有 ?
替换成 D
。
#include <cstdio>
char str[200005];
int main() {
scanf("%s", str + 1);
for (int i = 1; str[i]; ++i)
printf("%c", str[i] == '?' ? 'D' : str[i]);
puts("");
return 0;
}
C - Folia
令 \(C_d\) 为最优方案中深度为 \(d\) 的非叶子节点数量。
则有限制 \(C_d \le 2 C_{d - 1} - A_d\) 和 \(C_d \le C_{d + 1} + A_{d + 1}\)。
从 \(d = 0\) 和 \(d = N\) 向中心推进限制即可。
#include <cstdio>
typedef long long LL;
const int MN = 200005;
int N;
LL A[MN], B[MN], C[MN];
int main() {
scanf("%d", &N);
for (int i = 0; i <= N; ++i) scanf("%lld", &A[i]);
if (!N) return puts(A[0] == 1 ? "1" : "-1"), 0;
for (int i = N; i >= 0; --i) B[i] = B[i + 1] + A[i];
C[0] = 1 - A[0];
for (int i = 1; i <= N; ++i) {
C[i] = 2 * C[i - 1] - A[i];
if (C[i] < 0 || C[i] + A[i] == 0) return puts("-1"), 0;
if (C[i] + A[i] > B[i]) C[i] = B[i] - A[i];
}
LL Ans = 0;
for (int i = 0; i <= N; ++i) Ans += A[i] + C[i];
printf("%lld\n", Ans);
return 0;
}
D - Urban Planning
假设原图由 \(M\) 个基环内向树和 \(K\) 个内向树构成。
并且令 \(K\) 个内向树的大小依次为 \(b_1, b_2, \ldots , b_K\)。
要求将给定点对连通的最小边数,也就相当于求 \(N\) 减去连通块个数。
只要求出所有图中的连通块个数,再用 \(N {(N - 1)}^K\) 减去这个值即可。
显然至少有 \(M\) 个连通块,给总连通块个数贡献 \(M {(N - 1)}^K\)。
在其余的 \(K\) 个内向树中,可以将每个内向树看作一个整体,每在其中形成一个环,就为连通块个数贡献 \(1\)。
考虑枚举某个环中的所有内向树,假设为 \(k\)(\(k \ge 1\))个,为 \(\{ a_1, a_2, \ldots , a_k \}\)(无顺序)。
那么它们形成环的方案数就为 \(\displaystyle (k - 1)! \prod_{i = 1}^{k} b_{a_i}\)。
即枚举一种圆排列(共有 \((k - 1)!\) 种),每一个内向树的根可以连向下一个内向树的任何一个点,所以把所有 \(b\) 值相乘即可。
除了一种例外情况,当 \(k = 1\) 时,方案数应该为 \((b_{a_1} - 1)\),因为有不能连向自己的限制。
据此可以考虑一个 DP:对于每个 \(k\) 满足 \(1 \le k \le K\),求出在所有内向树中选出 \(k\) 个时它们的 \(b\) 值的乘积之和。
依次加入每个内向树即可维护。
求出后每项乘上 \((k - 1)!\),再乘上 \({(N - 1)}^{K - k}\)(剩下的其它内向树的方案数)贡献给答案即可,对于 \(k = 1\) 特殊处理一下。
#include <cstdio>
#include <vector>
typedef long long LL;
const int Mod = 1000000007;
const int MN = 5005;
inline int qPow(int b, int e) {
int a = 1;
for (; e; e >>= 1, b = (LL)b * b % Mod)
if (e & 1) a = (LL)a * b % Mod;
return a;
}
int N, M, A[MN], Fac[MN];
std::vector<int> G[MN];
int K, B[MN];
int vis[MN];
void DFS(int u) {
++B[K], vis[u] = 1;
for (int v : G[u]) DFS(v);
}
void DFS2(int u, int o) {
vis[u] = 1;
for (int v : G[u])
if (!vis[v]) DFS2(v, o);
else if (v == o) ++M;
}
int dp[MN][MN];
int main() {
scanf("%d", &N), Fac[0] = 1;
for (int i = 1; i <= N; ++i) scanf("%d", &A[i]), Fac[i] = (LL)Fac[i - 1] * i % Mod;
for (int i = 1; i <= N; ++i) if (~A[i]) G[A[i]].push_back(i);
for (int i = 1; i <= N; ++i) if (!~A[i]) ++K, DFS(i);
for (int i = 1; i <= N; ++i) if (!vis[i]) DFS2(i, i);
dp[0][0] = 1;
for (int i = 1; i <= K; ++i) {
dp[i][0] = 1;
for (int j = 1; j <= i; ++j)
dp[i][j] = (dp[i - 1][j] + (LL)dp[i - 1][j - 1] * B[i]) % Mod;
}
dp[K][1] -= K;
int Ans = (LL)M * qPow(N - 1, K) % Mod;
for (int j = 1; j <= K; ++j) Ans = (Ans + (LL)dp[K][j] * Fac[j - 1] % Mod * qPow(N - 1, K - j)) % Mod;
Ans = ((LL)N * qPow(N - 1, K) % Mod - Ans + Mod) % Mod;
printf("%d\n", Ans);
return 0;
}
E - Binary Programming
令 \(N = |T|\),等价于执行以下操作 \(N\) 次:
- 所有奇数位的数之和贡献给答案。
- 删去任意一位。
首先可以发现当 0
还没删光时,删 1
是不优的。
然而当 0
删光了的时候,之后的操作对答案的贡献就很显然了。所以这里考虑如何删 0
是最优的。
可以发现如果有两个相邻的 1
,那么每次操作时它们对答案的贡献都是 \(1\)。所以可以先不考虑。
那么一直删除相邻的两个 1
后,只剩下 \(k\) 个 1
时,就形成了 \(a_0\mathtt{0} \quad \mathtt{1} \quad a_1\mathtt{0} \quad \mathtt{1} \quad \cdots \quad \mathtt{1} \quad a_k\mathtt{0}\) 的情况。
对于其中的第 \(i\) 个 1
,它最多给答案贡献 \((a_0 + \cdots + a_{i - 1})\) 的一半(取整方式取决于位置的奇偶性)加 \((a_i + \cdots + a_k)\)。
而事实上这个上界是可以被达到的,也就是需要对每个 1
都满足在删它后面的 0
时它的位置在奇数:
先把 \(a_0\) 个最开始的 0
删光,然后删 \(a_1 - 1\) 个 0
留下恰好一个,以此类推,删掉 \(a_{k - 1} - 1\) 个 0
留下恰好一个。
这时形如 \(\mathtt{1010100000000 \cdots}\),然后从右到左把所有的 0
删掉即可。
所以使用上述结论求答案即可。
#include <cstdio>
#include <cstring>
typedef long long LL;
const int MN = 200005;
int N, C0, C1, K;
char T[MN];
int stk[MN], cnt[MN], tp;
LL Ans;
int main() {
scanf("%s", T + 1), N = strlen(T + 1);
for (int i = 1; i <= N; ++i) ++(T[i] & 1 ? C1 : C0);
Ans = (LL)(C1 + 1) * (C1 + 1) / 4;
T[0] = T[N + 1] = '0';
for (int i = 0; i <= N + 1; ++i) {
int x = T[i] - '0';
if (!x) {
if (tp && !stk[tp]) ++cnt[tp];
else stk[++tp] = 0, cnt[tp] = 1;
} else {
if (tp && stk[tp]) --tp, Ans += C0;
else stk[++tp] = 1, cnt[tp] = 1;
}
} --cnt[1], --cnt[tp];
for (int i = 1, s = 0; 2 * i <= tp; ++i)
s += cnt[2 * i - 1],
Ans += (s + i + 1) / 2 - (i + 1) / 2 + (C0 - s);
printf("%lld\n", Ans);
return 0;
}
F - Sorting Game
注意到,一个序列是合法的,当且仅当对于所有的两个下标 \(i, j\)(\(1 \le i < j \le M\)),都有:
按二进制位从高到低比较 \(a_i\) 与 \(a_j\),出现 \(a_i\) 该位为 \(1\) 且 \(a_j\) 该位为 \(0\) 后,其余的更低位 \(a_i\) 必须与 \(a_j\) 完全相同。
考虑 \(a_1 \sim a_M\) 的最高位,有两种情况:
- 依次为形如 \(\mathtt{000111}\),也就是先 \(0\) 后 \(1\):不存在两个下标先 \(1\) 后 \(0\),转化为 \((N - 1, M)\) 的子问题,共有 \((M + 1)\) 种方案。
- 依次为形如 \(\mathtt{0001????0111}\),也就是删去前缀连续的 \(0\) 和后缀连续的 \(1\) 后非空的情况:
所以删去后的那一部分的更低位必须都全部相同了,而前缀连续的 \(0\) 和后缀连续的 \(1\) 则不受任何影响。
这就相当于把中间的部分缩成同一个数了,假设缩完之后的长度为 \(i\),则转化为 \((N - 1, i)\) 的子问题。
假设缩完之后的长度为 \(i\),实际上有恰好 \(i\) 种方案去分配被缩起来的部分,而对于 \(\mathtt{????}\) 部分,共有 \(2^{M - i - 1}\) 种方案。
也就是说,有如下 DP 转移方程:
后面的部分可以使用前缀和的思想以加速处理,时间复杂度为 \(\mathcal O (N M)\)。
#include <cstdio>
#include <vector>
typedef long long LL;
const int Mod = 1000000007;
const int MN = 5005;
int N, M, f[MN][MN];
int main() {
scanf("%d%d", &N, &M);
for (int j = 1; j <= M; ++j) f[0][j] = 1;
for (int i = 1; i <= N; ++i) {
for (int j = 2; j <= M; ++j)
f[i][j] = (2 * f[i][j - 1] + (LL)f[i - 1][j - 1] * (j - 1)) % Mod;
for (int j = 1; j <= M; ++j)
f[i][j] = (f[i][j] + (LL)f[i - 1][j] * (j + 1)) % Mod;
}
printf("%d\n", f[N][M]);
return 0;
}