题目传送门:AtCoder Grand Contest 014。
A - Cookie Exchanges
显而易见的是,如果可以执行,三个数都会互相接近,必然在 (mathcal O (log)) 步内结束,模拟这个过程。
#include <cstdio>
int main() {
int A, B, C, Ans = 0;
scanf("%d%d%d", &A, &B, &C);
while (Ans < 99) {
if (A & 1 || B & 1 || C & 1) break;
int D = B + C, E = A + C, F = A + B;
A = D / 2, B = E / 2, C = F / 2;
++Ans;
}
printf("%d
", Ans < 99 ? Ans : -1);
return 0;
}
B - Unplanned Queries
我们将每条边被经过的次数的奇偶性异或到它的两个端点上,每条边被经过偶数次等价于每个端点的权值是 (0)(考虑自下往上)。
而一条路径仅会让两端点的权值改变,于是这等价于每个点都作为过偶数次的路径端点,可以在 (mathcal O (N)) 的时间内开桶统计。
#include <cstdio>
const int MN = 100005;
int N, Q, b[MN], x;
int main() {
scanf("%d%d", &N, &Q), Q *= 2;
while (Q--) scanf("%d", &x), b[x] ^= 1;
for (int i = 1; i <= N; ++i) if (b[i]) return puts("NO"), 0;
puts("YES");
return 0;
}
C - Closed Rooms
假设已知最终的行进路径,那么每一次删除 (K) 个障碍物时,都可以删除接下来最近的 (K) 个障碍物,从而保证下一次行走不受阻。
也就是说除了第一次行走,之后的每一次都可以看做是忽略障碍物的,直接走向最近的出口即可。
从 ((x, y)) 出发到达终点需要的步数也就是 (lceil min(x - 1, H - x, y - 1, W - y) / K ceil)。
而判断第一步能否到达 ((x, y)) 只需要做一个 BFS 即可。
#include <cstdio>
#include <algorithm>
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};
const int MN = 805;
int N, M, K, sx, sy;
char A[MN][MN];
int qx[MN * MN], qy[MN * MN], ql, qr;
int dis[MN][MN];
int Ans;
int main() {
scanf("%d%d%d", &N, &M, &K), Ans = N;
for (int i = 1; i <= N; ++i) {
scanf("%s", A[i] + 1);
for (int j = 1; j <= M; ++j) {
dis[i][j] = -1;
if (A[i][j] == 'S') sx = i, sy = j;
}
}
dis[sx][sy] = 0, ql = qr = 1, qx[1] = sx, qy[1] = sy;
while (ql <= qr) {
int x = qx[ql], y = qy[ql]; ++ql;
int di = std::min({x - 1, N - x, y - 1, M - y});
Ans = std::min(Ans, 1 + (di + K - 1) / K);
if (dis[x][y] == K) continue;
for (int d = 0; d < 4; ++d) {
int nx = x + dx[d], ny = y + dy[d];
if (nx < 1 || nx > N || ny < 1 || ny > M || A[nx][ny] == '#' || ~dis[nx][ny]) continue;
dis[nx][ny] = dis[x][y] + 1;
++qr, qx[qr] = nx, qy[qr] = ny;
}
}
printf("%d
", Ans);
return 0;
}
D - Black and White Tree
考虑一个叶子,先手把这个叶子连接的那个点染白,则后手必须染黑那个叶子,否则下一步先手即可把叶子染白赢得游戏。
考虑如果有一个点连接了两个叶子,那么先手先把它染白,后手就因为无法顾及两个叶子而输掉游戏。
假设已经不存在这种情况,考虑连续的三个点 (a, b, c),考虑一个时刻 (a, c) 周围的除了 (b) 之外的点全部染白,此时先手将 (b) 染白。
那么同上,后手将会输掉游戏。我们此时考虑以 (b) 为根时 (a, c) 的子树,如果除了 (a, c) 本身外均有偶数个点,可以证明后手必败。
这是因为先手可以每次找到 (a) 或 (c) 子树中深度最大的叶子节点,把它的双亲结点染白,逼迫后手染黑那个叶子。
此时完全等价于把这两个被染色的点从树中删去,用数学归纳法可知最终会删成 (a, c) 无孩子节点的情况,将 (b) 染白赢得游戏。
也就是说:如果存在一个节点,以它为根时存在至少两个子树方向上的点数为奇数则先手必胜。
显然如果 (N) 为奇数,任取一个连接叶子节点的点,必存在另一个点数为奇数的子树,这是因为除了它和叶子还有奇数个点。
所以 (N) 为奇数时先手必胜。如果 (N) 为偶数?我们可以做一次 DFS 来计算是否存在这样的点,如果存在也是先手必胜。
如果不存在呢?我并不清楚为何不存在时先手必败,此时我直接把代码提交上去就 AC 了(我当时在 virtual participating)。
赛后查看题解,发现我的 DP 过程(请看代码)竟然等价于求树上是否存在一个完美匹配,即选取一半的边覆盖所有点。
此时思路就清晰了,如果存在完美匹配,先手每染白一个点,后手就染黑它的匹配点,很显然每个点最终都会被染黑。
#include <cstdio>
#include <vector>
const int MN = 100005;
int N;
std::vector<int> G[MN];
int Ans, siz[MN];
void DFS(int u, int p) {
siz[u] = 1;
int s = 0;
for (int v : G[u]) if (v != p) {
DFS(v, u);
s += siz[v];
siz[u] ^= siz[v];
}
if (s >= 2) Ans = 1;
}
int main() {
scanf("%d", &N);
if (N & 1) return puts("First"), 0;
for (int i = 1, x, y; i < N; ++i) {
scanf("%d%d", &x, &y);
G[x].push_back(y);
G[y].push_back(x);
}
DFS(1, 0);
puts(Ans ? "First" : "Second");
return 0;
}
E - Blue and Red Tree
操作即是删去一条蓝边,然后加上一条红边以连通删去后留下的两个连通块。
此时加上的红边必须要满足其两端点都处在删蓝边前,那条蓝边所在的通过蓝边能到达的连通子图中。
考虑删去的第一条蓝边,从蓝树上考虑,显然那条被加入的红边两端点在蓝树上经过的路径覆盖了这条蓝边。
而且此后就不能再有红边对应的路径覆盖这条蓝边了,否则将违反上述限制。
那么做法就呼之欲出了:只需找到被覆盖次数恰好为 (1) 的蓝边,以及覆盖它的路径,把路径经过的蓝边的被覆盖次数减去 (1)。
不断重复上述过程直到每条蓝边都被删去即可,如果过程中找不到合法的蓝边则无法成功变换成红树。
具体实现时使用树链剖分 + 线段树对每条蓝边维护它的被覆盖次数,以及将其覆盖的所有路径的编号的异或和。
时间复杂度为 (mathcal O (N log^2 N))。
#include <cstdio>
#include <algorithm>
#include <vector>
const int Inf = 0x3f3f3f3f;
const int MN = 100005, MS = 1 << 18 | 7;
int N, ex[MN], ey[MN];
std::vector<int> G[MN];
int par[MN], dep[MN], siz[MN], pref[MN], top[MN], dfn[MN], dfc;
void DFS0(int u, int p) {
dep[u] = dep[par[u] = p] + 1, siz[u] = 1;
for (int v : G[u]) if (v != p) {
DFS0(v, u);
siz[u] += siz[v];
if (siz[pref[u]] < siz[v]) pref[u] = v;
}
}
void DFS1(int u, int t) {
dfn[u] = ++dfc, top[u] = t;
if (pref[u]) DFS1(pref[u], t);
for (int v : G[u]) if (v != par[u] && v != pref[u]) DFS1(v, v);
}
#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
struct dat {
int mn, v, i;
dat() { mn = Inf; }
dat(int z) { mn = v = 0, i = z; }
dat(int x, int y, int z) { mn = x, v = y, i = z; }
} tr[MS];
struct tag {
int a, w;
tag() { a = w = 0; }
tag(int x, int y) { a = x, w = y; }
} tg[MS];
inline dat operator + (dat a, dat b) { return a.mn < b.mn ? a : b; }
inline void operator += (tag &a, tag b) { a.a += b.a, a.w ^= b.w; }
inline void operator += (dat &a, tag b) { a.mn += b.a, a.v ^= b.w; }
inline void P(int i, tag x) { tr[i] += x, tg[i] += x; }
void Pushdown(int i) { if (tg[i].a || tg[i].w) P(li, tg[i]), P(ri, tg[i]), tg[i] = tag(); }
void Build(int i, int l, int r) {
if (l == r) return tr[i] = dat(l), void();
Build(ls), Build(rs);
tr[i] = tr[li] + tr[ri];
}
void Mdf(int i, int l, int r, int a, int b, tag x) {
if (r < a || b < l) return ;
if (a <= l && r <= b) return P(i, x);
Pushdown(i);
Mdf(ls, a, b, x), Mdf(rs, a, b, x);
tr[i] = tr[li] + tr[ri];
}
inline void Modify(int x, int y, tag z) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) std::swap(x, y);
Mdf(1, 1, N, dfn[top[x]], dfn[x], z);
x = par[top[x]];
}
if (dep[x] > dep[y]) std::swap(x, y);
if (x != y) Mdf(1, 1, N, dfn[x] + 1, dfn[y], z);
}
int main() {
scanf("%d", &N);
for (int i = 1, x, y; i < N; ++i) {
scanf("%d%d", &x, &y);
G[x].push_back(y);
G[y].push_back(x);
}
DFS0(1, 0), DFS1(1, 1);
Build(1, 1, N);
Mdf(1, 1, N, 1, 1, tag(Inf, 0));
for (int i = 1; i < N; ++i) {
scanf("%d%d", &ex[i], &ey[i]);
Modify(ex[i], ey[i], tag(1, i));
}
for (int i = 1; i < N; ++i) {
if (tr[1].mn != 1) return puts("NO"), 0;
int j = tr[1].v;
Mdf(1, 1, N, tr[1].i, tr[1].i, tag(Inf, 0));
Modify(ex[j], ey[j], tag(-1, j));
}
puts("YES");
return 0;
}
F - Strange Sorting
我们注意到一个关键性质:值为 (1) 的元素所在的位置,并不影响其它元素是 high 或者 low,也不影响操作后它们的相对位置。
这提示我们先删去元素 (1) 然后观察剩下的值在 ([2, N]) 中的元素构成的序列。
更进一步地我们可以考虑递推:让 (i) 从 (N) 到 (1),每次只考虑值在 ([i, N]) 中的元素构成的序列。
此时我们以如何从 (i = 2) 转移到 (i = 1) 为例说明算法流程:
假设 ([2, N]) 内的元素需要恰好 (T) 步排好序。
如果 (T = 0),也就是说初始时就排好序了,则如果元素 (1) 在开头,答案就为 (0),否则答案为 (1)。
如果 (T ge 1),则考虑包含元素 (1) 的序列在 (T) 步后,元素 (1) 的位置,如果恰好在序列开头答案就为 (T),否则答案为 (T + 1)。
如何判断元素 (1) 在 (T) 步后是否会在序列的开头?
考察 (T - 1) 步后的情况(注意只有 (T ge 1) 时才有意义,这就是为什么要对 (T) 分类):
- 引理 (oldsymbol{1}):只包含 ([2, N]) 的序列在 (T - 1) 步后的开头的元素一定不是 (2)。
- 证明 (oldsymbol{1}):反证法,假设是 (2),且此时序列还未排好序,这等价于存在 low 元素,则再进行一次操作 (2) 必不在开头。
令开头元素为 (f),上面证明了 (f > 2)。
注意到如果 (T - 1) 步后,元素 (1) 的位置落在 (f) 与 (2) 的位置之间,则再进行一次操作 (1) 就会在开头,否则 (1) 一定不在开头。
我们需要判断 (T - 1) 步后元素 (1) 的位置是否会在 (f) 与 (2) 之间。
我们先给出一个结论:如果在初始时元素 (1, 2, f) 的「循环顺序」,恰等于 ((f, 1, 2)),则最终元素 (1) 的位置就会在 (f, 2) 之间。
此处循环顺序相等,等价于在循环移位下同构。即 ((a, b, c)) 与 ((b, c, a)) 和 ((c, a, b)) 相等。
对这个结论的证明,我们通过证明「在任意时刻下,一次操作均不会改变 (1, 2, f) 的循环顺序」来显示。
要证明此结论,先证明一个引理(此引理的背景为,忽略元素 (1),即只考虑值在 ([2, N]) 中的元素):
- 引理 (oldsymbol{2}):在前 (T - 1) 步中的任意时刻,除非元素 (f) 作为第一个元素出现,否则 (f) 均不会成为 high 类元素。
- 也就是说如果 (f) 成为了 high 类元素,当且仅当它处于序列的开头(第一个元素永远是 high 类元素)。
- 证明 (oldsymbol{2}):假设在某一个时刻 (f) 成为了非开头的 high 类元素,操作后它将会处于在它前面的第一个 high 类元素后一位。
- 此时 (f) 比它前一个元素大,这意味着它们始终会紧挨着,除非它前一个元素为 low 而 (f) 为 high 的情况发生了。
- 如果它们始终紧挨着,则 (f) 就永远没有机会成为第一个元素,而这正是 (T - 1) 步后所要求的情况。
- 如果特殊情况发生了,则操作时 (f) 实际上还是一个非开头的 high 类元素,回到初始情况,这种情况不可能无限次发生。
有了这个结论,我们考虑任意时刻下 (1, 2, f) 的所有情况:
(注意到如果循环顺序改变,仅有可能是从左到右按照 high, low, high 或者 low, high, low 的顺序排列)
- 如果 (f) 为开头元素,则 (f) 为 high 类元素,(1, 2) 均为 low 类元素,操作后循环顺序不变。
- 如果 (2) 为开头元素,则 (2) 为 high 类元素,(1, f) 均为 low 类元素,操作后循环顺序不变。
- 如果 (1) 为开头元素:
- 如果 (f) 为第二个元素,则 (1, f) 均为 high 类元素,(2) 为 low 类元素,操作后循环顺序不变。
- 如果 (2) 为第二个元素,则 (1, 2) 均为 high 类元素,(f) 为 low 类元素,操作后循环顺序不变。
- 否则 (1) 为 high 类元素,(2, f) 均为 low 类元素,操作后循环顺序不变。
- 否则 (1, 2, f) 均为 low 类元素,操作后循环顺序不变。
由此证明了在任意时刻下,一次操作均不会改变 (1, 2, f) 的循环顺序。
于是证明了初始结论:如果在初始时元素 (1, 2, f) 的循环顺序等于 ((f, 1, 2)),则答案为 (T) 否则为 (T + 1)。
这是从 (i = 2) 推演到 (i = 1) 的情况,对于一般的 (i) 结论是类似的。
在实现时,对于一个 (i),仅考虑值在 ([i, N]) 中的元素,维护 (T_i) 表示排好序的次数,以及 (f_i) 表示 (T_i - 1) 步后的第一个元素。
如果 (T_i = 0) 则 (f_i) 视作未定义。
要计算 (T_i) 与 (f_i) 时:
- 如果 (T_{i + 1} = 0):
- 如果元素 (i) 所在的位置在元素 (i + 1) 所在的位置的左边,则 (T_i = 0),(f_i) 未定义。
- 否则 (T_i = 1),(f_i = i + 1)。
- 否则 (T_{i + 1} ge 1):
- 如果元素 (i)、元素 (i + 1) 与元素 (f_{i + 1}) 所在的位置的循环顺序是 ((f_{i + 1}, i, i + 1)),则 (T_i = T_{i + 1}),(f_i = f_{i + 1})。
- 否则 (T_i = T_{i + 1} + 1),(f_i = i + 1)。
最后输出 (T_1) 作为答案即可。
#include <cstdio>
const int MN = 200005;
int N, p[MN], q[MN];
int t[MN], f[MN];
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%d", &p[i]), q[p[i]] = i;
t[N] = 0;
for (int i = N - 1; i >= 1; --i) {
if (t[i + 1] == 0) {
if (q[i] > q[i + 1]) t[i] = 1, f[i] = i + 1;
else t[i] = 0;
} else {
if ( /* 1. */ (q[i] < q[i + 1] && q[i + 1] < q[f[i + 1]]) ||
/* 2. */ (q[i + 1] < q[f[i + 1]] && q[f[i + 1]] < q[i]) ||
/* 3. */ (q[f[i + 1]] < q[i] && q[i] < q[i + 1]))
t[i] = t[i + 1], f[i] = f[i + 1];
else t[i] = t[i + 1] + 1, f[i] = i + 1;
}
}
printf("%d
", t[1]);
return 0;
}