round 9
大寄。
我忏悔,我对不起组长 zwb,我对不起 AK 的 Cindy,对不起比我高的 lxf 和 lzm。
T3 数组开小。
T4 计算每天翘课所对应的答案时没用暴力,而是自作聪明的用了双指针(贪心),导致一分没有。
T5 没调出来,改成了暴力。
T6 暴力打错了,导致换根没想出来。
T1
一眼题。求出前缀和 \(sum_i\) 表示 \(0 \sim a_i\) 中有多少数没有出现在数列中。
由于 \(a\) 无序,所以先排一下序,此时 \(sum_i = sum_{i - 1} + a_i - a_{i - 1} - 1\)。
然后对于输入的数 \(k\) 我们找到 \(sum\) 中第一个比它小的下标 \(p\),则答案一定在 \(a_p\) 和 \(a_{p + 1}\) 之间,可以知道 \(0 \sim a_p\) 已有 \(sum_p\) 个数,所以答案为 \(a_p + k - sum_p\)。
code
#include <cstdio>
#include <algorithm>
#define LL long long
using namespace std;
const int N = 1e5 + 5;
LL n, m, a[N], s[N], k;
int main() {
scanf("%lld %lld", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; i++)
s[i] = s[i - 1] + a[i] - a[i - 1] - 1;
s[n + 1] = 1e18 - n;
for (int i = 1; i <= m; i++) {
scanf("%lld", &k);
int p = lower_bound(s + 1, s + n + 2, k) - s - 1;
printf("%lld\n", a[p] + k - s[p]);
}
return 0;
}
T2
计算在每个 '.' 处放时的答案,取最大值为答案。
对于每次,我们先判断能否放在此处。如果放上去后导致有白棋被黑棋吃掉就不能放。
如果可以放,那就计算能吃掉几个黑棋。
code
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 105;
int n, ans, tot;
char a[N];
int main() {
scanf("%d %s", &n, a + 1);
for (int i = 1; i <= n; i++)
if (a[i] == '.') {
bool f1 = true, f2 = true;
for (int j = i - 1; j; j--)
if (a[j] == '.')
break;
else if (a[j] == 'B') { //如果在遇到'.'之前遇到'B',说明这一段白棋左边为黑棋。
f1 = false;
break;
}
for (int j = i + 1; j <= n; j++)
if (a[j] == '.')
break;
else if (a[j] == 'B') { //右边同理。
f2 = false;
break;
}
if (f1 || f2) { //如果两边有一边不是黑棋说明可以放白棋在此处。
int s = 0;
for (int j = i - 1; j; j--)
if (a[j] == 'B') //计算到左边下一个白棋中间的黑棋数。
s++;
else if (a[j] == 'W') { //到白棋,计算答案。
ans = max(ans, s);
break;
} else //在到白棋之前遍历到'.'说明吃不掉。
break;
s = 0;
for (int j = i + 1; j <= n; j++) //右边同理。
if (a[j] == 'B')
s++;
else if (a[j] == 'W') {
ans = max(ans, s);
break;
} else
break;
}
}
printf("%d\n", ans);
return 0;
}
T3
做了很久,即使做过。
最开始的时候我写了二维 dp,\(f_{i, j}\) 表示第 \(i\) 个位置作为 \(j\) 这个东西时的答案。(\(0,1,2\) 就为题目中的 '0', '1', '2',\(3\) 为 '*')。
但这样明显是错误的,因为转移时会导致一些不符合要求的答案被计算出来,还会算重复一些答案。
于是我就想到如果在此基础上枚举 \(i\) 周围两个点是否是 '*' 就可以了。
如果当前位置是 '0', 那么 \(f_{i, 0, 0, 0} = f_{i - 1, 0, 0, 0} + f_{i - 1, 1, 1, 0}\);
如果当前位置是 '1',那么 \(f_{i, 1, 0, 1} = f_{i - 1, 0, 0, 0} + f_{i - 1, 1, 1, 0}, f_{i, 1, 1, 0} = f_{i - 1, 3, 1, 0} + f_{i - 1, 3, 0, 0}\)。
剩下两种情况可以照此推推。
如果是 '?',就分别将它当做以上 \(4\) 种计算。
特殊地,如果 \(i = 1\),那么第 \(3\) 维一定为 \(0\),因为前面不能放东西,初始化时注意;同理 \(i = n\) 时,第 \(4\) 维一定为 \(0\),所以答案为 \(f_{n, 0, 0, 0} + f_{n, 1, 1, 0} + f_{n, 3, 1, 0} + f_{n, 3, 0, 0}\)。
别忘了取模。
code
#include <cstdio>
#include <cstring>
#define LL long long
const int N = 1e6 + 5;
const LL Mod = 1e9 + 7;
int n;
LL f[N][5][2][2];
char a[N];
int main() {
while (~scanf("%s", a + 1)) {
n = strlen(a + 1);
for (int i = 0; i <= n; i++)
for (int j = 0; j < 4; j++)
f[i][j][0][0] = f[i][j][0][1] = f[i][j][1][0] = f[i][j][1][1] = 0;
if (a[1] == '0' || a[1] == '?')
f[1][0][0][0] = 1;
if (a[1] == '1' || a[1] == '?')
f[1][1][0][1] = 1;
if (a[1] == '*' || a[1] == '?') {
f[1][3][0][0] = 1;
f[1][3][0][1] = 1;
}
for (int i = 2; i <= n; i++) {
if (a[i] == '0' || a[i] == '?')
f[i][0][0][0] = (f[i - 1][0][0][0] + f[i - 1][1][1][0]) % Mod;
if (a[i] == '1' || a[i] == '?') {
f[i][1][1][0] = (f[i - 1][3][1][0] + f[i - 1][3][0][0]) % Mod;
f[i][1][0][1] = (f[i - 1][0][0][0] + f[i - 1][1][1][0]) % Mod;
}
if (a[i] == '2' || a[i] == '?')
f[i][2][1][1] = (f[i - 1][3][1][0] + f[i - 1][3][0][0]) % Mod;
if (a[i] == '*' || a[i] == '?') {
f[i][3][0][0] = (f[i - 1][1][0][1] + f[i - 1][2][1][1]) % Mod;
f[i][3][1][0] = (f[i - 1][3][0][1] + f[i - 1][3][1][1]) % Mod;
f[i][3][0][1] = (f[i - 1][1][0][1] + f[i - 1][2][1][1]) % Mod;
f[i][3][1][1] = (f[i - 1][3][0][1] + f[i - 1][3][1][1]) % Mod;
}
}
printf("%lld\n", (f[n][0][0][0] + f[n][1][1][0] + f[n][3][0][0] + f[n][3][1][0]) % Mod);
}
return 0;
}
T4
背包。
我们先计算出来如果不逃课,需要在教学楼待多久,然后再通过 dp 求出通过逃课最多可以少待多久,相减就是答案。
我们可以在输入时把当前第 \(i\) 天哪些时间在上课放到一个数组中,那么 \(x_{tot} - x_1 + 1\) 就是这一天本来要在教学楼待多久。
求到了这个之后我们继续枚举这一天逃每个数量的课时,最多减少多少在教学楼的时间,用 \(s_{i, j}\) 表示第 \(i\) 天逃 \(j\) 节课最多减少多少时间。至于求发,暴力即可(不要写贪心)。
然后就是 dp,考试的时候没有注意到是背包,但还是很自然地打了出来(虽然是二维背包)。
\(f_{i, j}\) 表示前 \(i\) 天逃 \(j\) 节课的答案,很明显:
最后求到 \(f\) 中最大值与总时间相减即可。
code
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 505;
int n, m, k, s[N][N], f[N][N], x[N], tot, ans, res;
char a[N];
int main() {
scanf("%d %d %d", &n, &m, &k);
for (int i = 1; i <= n; i++) {
scanf(" %s", a + 1);
tot = 0;
for (int j = 1; j <= m; j++)
if (a[j] == '1')
x[++tot] = j;
if (tot)
ans += x[tot] - x[1] + 1;
for (int l = 1; l <= tot; l++)
for (int r = l; r <= tot; r++)
s[i][tot - (r - l + 1)] = max(s[i][tot - (r - l + 1)], x[tot] - x[1] + 1 - (x[r] - x[l] + 1));
for (int j = 0; j <= k; j++) {
for (int k = 0; k <= j; k++)
f[i][j] = max(f[i][j], s[i][k] + f[i - 1][j - k]);
res = max(res, f[i][j]);
}
}
printf("%d\n", ans - res);
return 0;
}
T5
有源汇上下界最小流。在考场上不知道为什么没调出来。
对于每横排,至少种 \(r_i\) 棵树,所以向源点连一条上界为 \(inf\),下界为 \(r_i\)的边,对于每竖排,至少种 \(c_i\) 棵树,所以向汇点连一条上界为 \(inf\) 下界为 \(c_i\) 的边。
然后枚举每个点,如果没被焦化,说明可以在此种树。假设这个点是 \((i, j)\),那么第 \(i\) 行就向第 \(j\) 行建一条上界为 \(1\),下界为 \(0\) 的边。
对于无解,我们只需让每个点只要能种就都种,如果这样都不能满足,就肯定无解。
code
#include <cstdio>
#include <queue>
#include <algorithm>
#include <iostream>
#define LL long long
#define inf 1e15
using namespace std;
const int N = 1e5 + 5, M = 1e6 + 5;
struct Edge {
int to, next;
LL c1, c2, c;
};
struct node {
int n, m, head[N], tot, h[N], now[N];
LL flow;
Edge edge[M << 1];
queue<int> q;
inline node() { tot = 1; }
inline void add(int u, int v, LL c1, LL c2, LL c) {
edge[++tot].to = v, edge[tot].c1 = c1, edge[tot].c2 = c2, edge[tot].c = c;
edge[tot].next = head[u], head[u] = tot;
}
inline bool bfs(int s, int t) {
while (!q.empty())
q.pop();
for (int i = 1; i <= n; i++)
h[i] = 0, now[i] = head[i];
h[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
LL c = edge[i].c;
if (c && !h[v]) {
h[v] = h[u] + 1;
q.push(v);
if (v == t)
return true;
}
}
}
return false;
}
inline int dfs(int u, LL flo, int t) {
if (u == t)
return flo;
LL x = 0;
for (int i = now[u]; i; i = edge[i].next) {
now[u] = i;
int v = edge[i].to;
LL c = edge[i].c;
if (c && h[u] + 1 == h[v]) {
LL fl = dfs(v, min(flo - x, c), t);
edge[i].c -= fl;
edge[i ^ 1].c += fl;
x += fl;
if (flo == x)
return flo;
}
}
return x;
}
inline void dinic(int s, int t) {
flow = 0;
while (bfs(s, t))
flow += dfs(s, inf, t);
}
} F;
int n, m, k, s, t, S, T, x[N], y[N];
LL ans, c[N];
bool flag[105][105];
int main() {
scanf("%d %d %d", &n, &m, &k);
s = n + m + 1, t = s + 1;
S = t + 1, T = S + 1;
F.add(s, t, 0, 0, 0), F.add(t, s, 0, 0, inf);
LL c1, c2;
for (int i = 1; i <= n; i++) {
scanf("%d", &x[i]);
c[s] += x[i], c[i] -= x[i];
F.add(s, i, x[i], inf, inf - x[i]), F.add(i, s, 0, 0, 0);
}
for (int i = 1; i <= m; i++) {
scanf("%d", &y[i]);
c[i + n] += y[i], c[t] -= y[i];
F.add(i + n, t, y[i], inf, inf - y[i]), F.add(t, i + n, 0, 0, 0);
}
for (int i = 1, xx, yy; i <= k; i++) { //表记焦化的土地。
scanf("%d %d", &xx, &yy);
flag[xx][yy] = true;
}
for (int i = 1; i <= n; i++) {
int s = 0;
for (int j = 1; j <= m; j++)
s += !flag[i][j];
if (s < x[i])
return puts("Unable to plant"), 0;
}
for (int j = 1; j <= m; j++) {
int s = 0;
for (int i = 1; i <= n; i++)
s += !flag[i][j];
if (s < y[j])
return puts("Unable to plant"), 0;
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (!flag[i][j]) //如果没被焦化,建边。
F.add(i, j + n, 0, 1, 1), F.add(j + n, i, 0, 0, 0);
for (int i = 1; i <= t; i++)
if (c[i] > 0)
F.add(i, T, 0, 0, c[i]), F.add(T, i, 0, 0, 0);
else if (c[i] < 0)
F.add(S, i, 0, 0, -c[i]), F.add(i, S, 0, 0, 0);
F.n = T; //板子。
F.dinic(S, T);
ans = F.edge[2].c;
F.edge[2].c = F.edge[3].c = 0;
F.dinic(t, s);
printf("%lld\n", ans - F.flow);
return 0;
}
T6
换根 dp。
首先明确暴力怎么打,对于根节点,我们求出到所有标记了的点并返回根节点所需时间,存在 \(f\) 里。
因为最后不需要回到起点,所以用数组 \(g\) 求出根节点离最远的标记了的点需要所需时间。
那么对于根节点答案就是 \(f_i - g_i\)。
所以 \(O(n^2)\) 的做法就出来了。
每个点当根节点都算一遍。
但是既然都这样了,换根就可以换了。
对于当前根节点 \(u\) 我们枚举它的子节点,考虑让子节点变成根。
我们先消除 \(v\) 对于 \(u\) 的影响,再用 \(u\) 更新 \(v\) 的答案。
但是由于需要用 \(g_u\) 更新 \(g_v\),但如果 \(g_u\) 就是用 \(g_v\) 转移得到的,所以我们还要记录每个点是由那个点的 \(g\) 转移达得到最大值,并记录次大值。
code
#include <cstdio>
#include <algorithm>
#define LL long long
using namespace std;
const int N = 5e5 + 5;
int n, head[N], tot, k;
LL f[N], siz[N], g[N][2], id[N][2], ans[N];
bool flag[N];
struct Edge {
int to, next;
LL w;
} edge[N << 1];
inline void add(int u, int v, LL w) {
edge[++tot].to = v, edge[tot].w = w;
edge[tot].next = head[u], head[u] = tot;
}
inline void dfs1(int x, int u) {
if (flag[u])
siz[u] = 1;
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
LL w = edge[i].w;
if (v == x)
continue;
dfs1(u, v);
siz[u] += siz[v]; //siz用来统计以当前节点为根的子树下有多少标记点。
if (siz[v]) { //如果有标记点才转移。
f[u] += f[v] + w;
if (g[u][0] < g[v][0] + w)
g[u][1] = g[u][0], id[u][1] = id[u][0], g[u][0] = g[v][0] + w, id[u][0] = v;
else if (g[u][1] < g[v][0] + w)
g[u][1] = g[v][0] + w, id[u][1] = v;
}
}
}
inline void dfs2(int x, int u) {
ans[u] = (f[u] << 1) - g[u][0];
for (int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
LL w = edge[i].w;
if (v == x)
continue;
LL fu = f[u], su = siz[u], gu0 = g[u][0], idu0 = id[u][0];
if (siz[v]) //如果siz[v]有值,说明对u产生了影响,消除它。
f[u] -= f[v] + w, siz[u] -= siz[v];
if (id[u][0] == v) //如果让给g[u]取最大值的是v,就将g[u]换成次大值。
g[u][0] = g[u][1], id[u][0] = id[u][1];
siz[v] += siz[u];
if (siz[u]) {
f[v] += f[u] + w;
if (g[v][0] < g[u][0] + w)
g[v][1] = g[v][0], id[v][1] = id[v][0], g[v][0] = g[u][0] + w, id[v][0] = u;
else if (g[v][1] < g[u][0] + w)
g[v][1] = g[u][0] + w, id[v][1] = u;
}
dfs2(u, v);
f[u] = fu, siz[u] = su, g[u][0] = gu0, id[u][0] = idu0;
}
}
int main() {
scanf("%d %d", &n, &k);
for (int i = 1, u, v, w; i < n; i++) {
scanf("%d %d %d", &u, &v, &w);
add(u, v, w), add(v, u, w);
}
for (int i = 1, x; i <= k; i++) {
scanf("%d", &x);
flag[x] = true; //标记。
}
dfs1(0, 1);
dfs2(0, 1);
for (int i = 1; i <= n; i++)
printf("%lld\n", ans[i]);
return 0;
}