Codeforces CodeTON Round 2

题目传送门:Codeforces CodeTON Round 2

A. Two 0-1 Sequences

题意简述

给定长度为 \(n\) 的 01 串 \(a\) 和长度为 \(m\) 的 01 串 \(b\),其中 \(b\) 的长度不比 \(a\) 的更长,即 \(m \le n\)

你可以执行如下操作零或更多次:将 \(a\) 的前两个字符替换为它们的较小值或较大值,操作后 \(a\) 的长度减少 \(1\)

请问是否能将 \(a\) 变为 \(b\)

\(T\) 组数据。

数据范围:\(1 \le m \le n \le 50\)\(T \le 2000\)

AC 代码
#include <cstdio>
#include <cstring>
#include <algorithm>

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

const int MN = 55;

int n, m;
char a[MN], b[MN];

void Solve() {
	scanf("%d%d%s%s", &n, &m, a + 1, b + 1);
	int k = n - m + 1;
	if (strcmp(a + k + 1, b + 2))
		return puts("NO"), void();
	char mn = *std::min_element(a + 1, a + k + 1);
	char mx = *std::max_element(a + 1, a + k + 1);
	puts(mn <= b[1] && b[1] <= mx ? "YES" : "NO");
}

由于每次操作 \(a\) 的长度恰好减少 \(1\),这说明恰好要执行 \(n - m\) 次操作。

\(1\) 次操作将前 \(2\) 个字符压缩为一个字符,同理,\(2\) 次操作将前 \(3\) 个字符压缩为一个字符,进一步地,\(n - m\) 次操作将前 \(n - m + 1\) 个字符压缩为一个字符。并且操作结束后,对于后续的部分不做任何改变。

\(k = n - m + 1\),即是问 \(a\) 的前 \(k\) 个字符 \(a_1 a_2 \cdots a_k\) 能否被压缩为 \(b_1\),同时还需要判断 \(a\) 的后续部分与 \(b\) 的后续部分(即 \(a_{k + 1} a_{k + 2} \cdots a_n\)\(b_2 b_3 \cdots b_n\))是否完全一致。

可以发现,如果 \(a\) 的前 \(k\) 个字符中既有 \(0\) 又有 \(1\),则可以通过每次操作替换为较小值最终得到 \(0\),也可以通过每次操作替换为较大值最终得到 \(1\)。但如果前 \(k\) 个字符完全相同,即全部为 \(0\) 或全部为 \(1\),则每次操作得到的字符都也相同,最终也只能得到相同的字符。

所以只需要判断 \(b_1\) 是否在 \(\displaystyle \min_{i = 1}^{k} a_i\)\(\displaystyle \max_{i = 1}^{k} a_i\) 之间,再加上对 \(a_{k + 1 \sim n}\)\(b_{2 \sim n}\) 是否相等的判断即可。

时间复杂度为 \(\mathcal O (n)\)

B. Luke is a Foodie

题意简述

给定一个长度为 \(n\) 的正整数序列 \(a_1, a_2, \ldots, a_n\) 和一个正整数 \(x\)

你需要设置一个整数参数 \(v\),然后按从左至右的顺序遍历 \(a_1 \sim a_n\),在遍历到每个数前,你可以修改 \(v\) 的值,以保证在遍历到每个数时均有 \(\lvert a_i - v \rvert \le x\) 成立。

请求出最少需要修改 \(v\) 的值的次数,一开始设置 \(v\) 的值不算作修改。

\(T\) 组数据。

数据范围:\(\sum n \le 2 \times {10}^5\)\(1 \le a_i, x \le {10}^9\)\(T \le {10}^4\)

AC 代码
#include <cstdio>
#include <algorithm>
using std::scanf;
using std::printf;

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

const int Inf = 0x3f3f3f3f;

const int MN = 200005;

int n, x;
int a[MN];

void Solve() {
	scanf("%d%d", &n, &x);
	for (int i = 1; i <= n; ++i) scanf("%d", a + i);
	int mn = -Inf, mx = Inf;
	int ans = -1;
	for (int i = 1; i <= n; ++i) {
		mn = std::min(mn, a[i]);
		mx = std::max(mx, a[i]);
		if (mx - mn <= 2 * x)
			continue;
		++ans;
		mn = mx = a[i];
	}
	printf("%d\n", ans);
}

如果我们将序列 \(a\) 按每次修改 \(v\) 的位置切开,可以发现相当于 \(a\) 被划分成了若干个区间。则修改 \(v\) 的次数就等于区间的数量减去 \(1\)

在每个区间内,遍历元素时 \(v\) 的值都是相同的,这就说明 \(v\) 必须满足对于每个区间内的元素 \(a_i\) 都有 \(\lvert a_i - v \rvert \le x\) 成立。这个式子对 \(v\) 有解当且仅当区间内的所有 \(a_i\)极差不超过 \(2 x\)。反过来,如果区间内的所有数的极差不超过 \(2 x\),总是可以找到一个整数 \(v\) 使得每个 \(\lvert a_i - v \rvert \le x\) 均成立。于是可以将原题目转化为将序列划分为若干个区间,使得每个区间内元素的极差不超过 \(2 x\),并最小化区间的数量。

我们发现,总是贪心地让第一段区间尽量长不会更劣,因为这样可以让后面的部分更短,不会增加划分区间的数量。维护区间内的最大值和最小值,每次判断极差是否 \(\le 2 x\) 即可。

时间复杂度为 \(\mathcal O (n)\)

C. Virus

题意简述

有编号为 \(1 \sim n\)\(n\) 个格子排列成环状,即 \(i\) 号格子与 \(i + 1\) 号格子相邻(\(1 \le i < n\))且 \(n\) 号格子与 \(1\) 号格子相邻。

一开始有 \(m\) 个格子被感染了,分别是 \(a_1, a_2, \ldots, a_m\) 号格子。

接下来的每个时刻,两个事件将依次发生:

  1. 保护:你可以选择一个未被感染的格子并保护它,被保护的格子将永远不会被感染。
  2. 感染:对于所有未被感染且未被你保护的格子,如果在上一时刻中,与它相邻的格子中有被感染的,则它在这一时刻也被感染。

你希望最小化最终被感染的格子的数量,请求出这个值。

\(T\) 组数据。

数据范围:\(5 \le n \le \color{red}{{10}^9}\)\(1 \le m \le n\)\(\sum m \le {10}^5\)\(T \le {10}^4\)

AC 代码
#include <cstdio>
#include <algorithm>
#include <vector>
#include <functional>
using std::scanf, std::printf;

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

const int MM = 100005;

int n, m;
int a[MM];

void Solve() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; ++i)
		scanf("%d", &a[i]);
	std::sort(a + 1, a + m + 1);
	a[m + 1] = a[1] + n;
	std::vector<int> s;
	for (int i = 1; i <= m; ++i)
		s.push_back(a[i + 1] - a[i] - 1);
	std::sort(s.begin(), s.end(), std::greater());
	int ans = 0;
	int c = 0;
	for (int x : s) {
		int y = x - 2 * c;
		if (y <= 0)
			break;
		if (y >= 3)
			ans += y - 1,
			c += 2;
		else
			ans += 1,
			++c;
	}
	printf("%d\n", n - ans);
}

可以把连续的未被感染的格子看作独立的一段,可以发现不同的段之间相互独立,并且每一段只有长度信息是有用的。将 \(a_1 \sim a_m\) 排序后即可计算出每一段的长度。

如果不使用保护操作,则每个时刻过去后,每一段的长度都会减去 \(2\)(两端同时被感染一格),直到这一段消失(长度变为 \(0\))。即 \(c\) 个时刻后,初始时长度为 \(\mathit{len}\) 的段的长度会变为 \(\max(\mathit{len} - 2 \cdot c, 0)\)

比较显然的事实是,每次保护格子时,必然是选取一段的端点处进行保护。那么接下来要考虑的是端点的保护顺序的问题。

考虑每过一个时刻,就相当于多感染了剩余未被保护(且这一段还未消失)的端点的数量个格子。为了最小化被感染的格子数量,也就是要最小化每个时刻剩余未被保护的格子的数量之和。每个时刻我们只能保护一个格子,所以即是要最大化未被保护,但所在的段已经消失的端点的数量。

基于此角度,可以发现最优策略即是按照段的长度从长到短保护端点,这样可以让较短的段在未被保护前消失,即可最大化每个时刻的未被保护但已消失的端点的数量。也可以理解为牺牲长度小的段去保护长度更大的段。

在处理每段长度并从大到小排序时,需要对 \(a_1, a_2, \ldots, a_m\) 先进行排序,然后再对每段长度进行排序。最后模拟保护端点的过程即可。

时间复杂度为 \(\mathcal O (m \log m)\)

D. Magical Array

题意简述

有一个长度为 \(m\) 的非负整数数组 \(b\)。将 \(b\) 复制 \(n\) 份得到 \(n\) 个数组 \(c_1, c_2, \ldots, c_n\),每个 \(c_i\) 都是长度为 \(m\) 的数组。

现有两种对其中的某个数组 \(c_t\) 的操作:

  1. 选取两个下标 \(i, j\) 满足 \(2 \le i < j \le m - 1\),将 \(c_t[i]\)\(c_t[j]\) 减去 \(1\),将 \(c_t[i - 1]\)\(c_t[\color{red}{j + 1}]\) 加上 \(1\)
  2. 选取两个下标 \(i, j\) 满足 \(2 \le i < j \le m - 2\),将 \(c_t[i]\)\(c_t[j]\) 减去 \(1\),将 \(c_t[i - 1]\)\(c_t[\color{red}{j + 2}]\) 加上 \(1\)

需要保证每次操作结束后,数组中不会有负数,即需要保证 \(c_t[i], c_t[j] \ge 1\)

在这 \(n\) 个数组中,有一个数组 \(c_k\)\(1 \le k \le n\))是特别的数组。

对每个非特别的数组 \(c_i\)(即 \(i \ne k\)),执行至少一次操作 1,不执行操作 2。
对特别的数组 \(c_k\)执行至少一次操作 2,不执行操作 1。

现给定 \(m, n\) 以及操作结束后的每个数组 \(c_1, c_2, \ldots, c_n\) 的信息,但不给定原数组 \(b\) 和特别的数组的编号 \(k\)。请还原出 \(k\) 的值,以及操作 2 在特别数组上的执行次数。你不需要还原出原数组 \(b\)

\(T\) 组数据。

数据范围:\(3 \le n \le {10}^5\)\(7 \le m \le 3 \times {10}^5\),保证 \(b\) 数组中的每个数在 \([0, {10}^6]\) 内,故有 \(0 \le c_i[j] \le 3 \times {10}^{11}\)\(\sum n \cdot m \le {10}^6\)\(T \le {10}^4\)

AC 代码
#include <cstdio>
#include <algorithm>
using std::scanf, std::printf;

void Solve();

int main() {
	int tests = 1;
	scanf("%d", &tests);
	while (tests--)
		Solve();
	return 0;
}

typedef long long LL;
const int MN = 100005;

int n, m;
LL v[MN];

void Solve() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i) {
		v[i] = 0;
		LL x;
		for (int j = 1; j <= m; ++j)
			scanf("%lld", &x),
			v[i] += x * j;
	}
	int p = (int)(std::max_element(v + 1, v + n + 1) - v);
	printf("%d %lld\n", p, v[p] - v[p % n + 1]);
}

如果我们把数组中的每个数 \(c_t[i]\) 看成 \(c_t[i]\) 个小球,则每次操作 1 相当于把一个小球往左移动一格,一个小球往右移动一格,而每次操作 2 相当于把一个小球往左移动一格,一格小球往右移动两格。

如果我们考虑所有小球的重心,可以发现操作 1 不改变重心,而操作 2 会将重心往右移动一点点。

将重心形式化地说,就是操作 1 下有不变量 \(\sum_{i = 1}^{m} i \cdot c_t[i]\),而每次操作 2 会将这个量增加 \(1\)

于是,记 \(v_t = \sum_{i = 1}^{m} i \cdot c_t[i]\),找出 \(v_1, v_2, \ldots, v_n\) 中的最大值,其下标恰好为 \(k\),其值与其他值的差即为操作 2 的执行次数。数据范围保证了可以在 long long 范围下存储 \(v_t\) 的值。

时间复杂度为 \(\mathcal O (n m)\)

E. Count Seconds

题意简述

给定一张 \(n\) 个点 \(m\) 条边的简单(无重边)有向无环图,结点编号为 \(1 \sim n\),图中恰好有一个结点没有出边。

每个结点均有点权,结点 \(i\) 的权值为 \(a_i\),初始时权值均为非负整数。

接下来的每个时刻,以下两个事件依次发生:

  • \(S\) 为点权大于 \(0\) 的点集,即 \(S = \{ x \in V \mspace{3mu} \vert \mspace{3mu} a_x > 0 \}\)
  • 对于每个 \(S\) 中的点 \(u\),将 \(u\) 的点权 \(a_u\) 减去 \(1\),将 \(u\) 的每条出边连向的点 \(v\) 的点权加上 \(1\)

请求出所有点权均归零需要经过的最短时刻数,对 \(998244353\) 取模。

\(T\) 组数据。

数据范围:\(1 \le n, m \le 1000\)\(0 \le a_i \le {10}^9\)\(\sum n, \sum m \le {10}^4\)\(T \le 1000\)

AC 代码

我们先找出 DAG 的唯一最后的那个点,将它称为汇点,记作 \(t\)

一个朴素的想法是,由于每个时刻汇点恰好流出 \(1\) 的权值,则汇点总共流入的权值(也包括汇点本身的权值 \(a_t\))就恰等于流干汇点权值所要花费的时刻数。

F. Colouring Game

题意简述
AC 代码

G. Mio and Lucky Array

题意简述
AC 代码

H2. Game of AI (hard version)

题意简述
AC 代码
posted @ 2022-08-11 00:09  粉兔  阅读(318)  评论(0编辑  收藏  举报