AtCoder Grand Contest 014

题目传送门:AtCoder Grand Contest 014

显而易见的是,如果可以执行,三个数都会互相接近,必然在 \(\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\n", 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 \rceil\)

而判断第一步能否到达 \((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\n", 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\) 分类):

  • 引理 \(\boldsymbol{1}\)只包含 \([2, N]\) 的序列在 \(T - 1\) 步后的开头的元素一定不是 \(2\)
  • 证明 \(\boldsymbol{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]\) 中的元素):

  • 引理 \(\boldsymbol{2}\)在前 \(T - 1\) 步中的任意时刻,除非元素 \(f\) 作为第一个元素出现,否则 \(f\) 均不会成为 high 类元素。
  • 也就是说如果 \(f\) 成为了 high 类元素,当且仅当它处于序列的开头(第一个元素永远是 high 类元素)。
  • 证明 \(\boldsymbol{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\n", t[1]);
	return 0;
}
posted @ 2020-08-02 19:52  粉兔  阅读(654)  评论(0编辑  收藏  举报