Codeforces Round 893 (Div. 2)

Buttons

优先选择公共按钮,当且仅当先手的按钮数量大于后手的按钮数量时,先手者胜。

1
2
3
4
5
public static void solve() {
int a = io.nextInt(), b = io.nextInt(), c = io.nextInt();
if (a + c % 2 > b) io.println("First");
else io.println("Second");
}

The Walkway

模拟题,特别需要注意头尾的边界处理,加上哨兵真的会方便很多。可以假设位置 \(1-d\) 和位置 \(n+1\) 有卖家,这样就不用特判,可以直接处理!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static void solve() {
int n = io.nextInt(), m = io.nextInt(), d = io.nextInt();
int[] s = new int[m + 2];
for (int i = 1; i <= m; i++) {
s[i] = io.nextInt();
}
s[0] = 1 - d;
s[m + 1] = n + 1;

int ans = m - 1, delta = Integer.MAX_VALUE, cnt = 0;
for (int i = 1; i <= m; i++) {
int A = (s[i] - s[i - 1] - 1) / d;
int B = (s[i + 1] - s[i] - 1) / d;
int C = (s[i + 1] - s[i - 1] - 1) / d;
int D = C - A - B;

if (D < delta) {
delta = D;
cnt = 1;
} else if (D == delta) {
cnt++;
}
ans += A;
}
ans += (s[m + 1] - s[m] - 1) / d + delta - 1;
io.println(ans + " " + cnt);
}

Yet Another Permutation Problem

构造题,首先需要发现什么公约数不可能出现,很明显不可能得到 \(d_{i}=\gcd (a_{i},a_{(i\bmod n)+1})> \lfloor \frac{n}{2}\rfloor\)。然后考虑所有小于等于 \(\lfloor \frac{n}{2}\rfloor\) 的数是否能被包含,可以发现对于每个 \(a_{i}=x\leq \lfloor \frac{n}{2}\rfloor\) 总有 \(a_{(i\bmod n)+1}=2\cdot x\leq n\),所以我们可以枚举所有奇数乘以二的幂来构造答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void solve() {
int n = io.nextInt(), idx = 0;
int[] ans = new int[n];
for (int i = 1; i <= n; i += 2) {
for (int j = i; j <= n; j *= 2) {
ans[idx++] = j;
}
}
for (int i = 0; i < n; i++) {
io.print(ans[i] + " ");
}
io.println();
}

Trees and Segments

难以描述,看代码吧,调试半天。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static void solve() {
int n = io.nextInt(), k = io.nextInt();
char[] s = io.next().toCharArray();
int[][] prefix = new int[n + 1][k + 1];
int[][] suffix = new int[n + 1][k + 1];
// 枚举可以在 k 次操作内变为全 0 的子数组,并将其长度记录到所属的前后缀中
for (int i = 0; i < n; i++) {
int cnt1 = 0;
for (int j = i; j < n; j++) {
cnt1 += s[j] - '0';
if (cnt1 > k) break;
prefix[j + 1][cnt1] = Math.max(prefix[j + 1][cnt1], j - i + 1);
suffix[i][cnt1] = Math.max(suffix[i][cnt1], j - i + 1);
}
}
// 在前缀 [0, i] 中最多操作 j 次,可以得到的连续 0 的最长长度
for (int i = 0; i < n; i++) {
for (int j = 0; j <= k; j++) {
prefix[i + 1][j] = Math.max(prefix[i + 1][j], prefix[i][j]);
if (j > 0) prefix[i + 1][j] = Math.max(prefix[i + 1][j], prefix[i + 1][j - 1]);
}
}
// 在后缀 [i, n - 1] 中最多操作 j 次,可以得到的连续 0 的最长长度
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j <= k; j++) {
suffix[i][j] = Math.max(suffix[i][j], suffix[i + 1][j]);
if (j > 0) suffix[i][j] = Math.max(suffix[i][j], suffix[i][j - 1]);
}
}
// 枚举连续 1 的起点和终点,并记录该连续 1 的长度对应的连续 0 的最长长度(注意包含长度为 0 的情况)
int[] max0by1 = new int[n + 1];
Arrays.fill(max0by1, -1);
max0by1[0] = suffix[0][k];
for (int i = 0; i < n; i++) {
int cnt0 = 0;
for (int j = i; j < n; j++) {
cnt0 += (s[j] - '0') ^ 1;
if (cnt0 > k) break;
max0by1[j - i + 1] = Math.max(max0by1[j - i + 1], prefix[i][k - cnt0]);
max0by1[j - i + 1] = Math.max(max0by1[j - i + 1], suffix[j + 1][k - cnt0]);
}
}
// 计算答案
int[] ans = new int[n + 1];
for (int a = 1; a <= n; a++) {
for (int i = 0; i <= n; i++) {
if (max0by1[i] == -1) continue;
ans[a] = Math.max(ans[a], i + max0by1[i] * a);
}
}
for (int i = 1; i <= n; i++) {
io.print(ans[i] + " ");
}
io.println();
}

Codeforces Round 892 (Div. 2)

United We Stand

要使数组 \(c_{j}\) 不是 \(b_{i}\) 的约数,只要让数组 \(b\) 中只存最小的数,或者让数组 \(c\) 中只存最大的数,就可以满足要求。特别的,如果所有数都相等,那么不存在解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void solve() {
int n = io.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = io.nextInt();
}
Arrays.sort(arr);
if (arr[0] == arr[n - 1]) {
io.println(-1);
return;
}
int it = 0;
while (arr[it] == arr[0]) it++;
io.println(it + " " + (n - it));
for (int i = 0; i < it; i++) io.print(arr[i] + " ");
io.println();
for (int i = it; i < n; i++) io.print(arr[i] + " ");
io.println();
}

Olya and Game with Arrays

要最大化 \(\sum_{i=1}^{n}\min_{j=1}^{m_{i}}a_{i,j}\),一开始想到最大化最小值,二分?但是有点不太对。然后发现规律,只需要关注数组的最小值和次小值就行。首先所有数组的最小值的最小值一定会被包含在内,这样只要把其他数组的最小值移动到该最最小值所属的数组就可以让答案最大。也就是说答案等于所有数组次小值的和加上最最小值,再减去最最小值对应的次小值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void solve() {
int n = io.nextInt();
int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
long sum = 0;
List<Integer>[] arr = new List[n];
Arrays.setAll(arr, k -> new ArrayList<>());
for (int i = 0; i < n; i++) {
int m = io.nextInt();
for (int j = 0; j < m; j++) {
arr[i].add(io.nextInt());
}
// 可以不排序,直接遍历找
Collections.sort(arr[i]);
sum += arr[i].get(1);
min1 = Math.min(min1, arr[i].get(0));
min2 = Math.min(min2, arr[i].get(1));
}
io.println(sum - min2 + min1);
}

Another Permutation Problem

题目要求 \((\sum_{i=1}^{n}p_{i}\cdot i)-(\max_{j=1}^{n}p_{j}\cdot j)\) 的最大值,前半部分的最大值的情况就是从小到大排列,但是后半部分不好处理,所以考虑枚举后半部分。从大到小枚举 \(\max_{j=1}^{n}p_{j}\cdot j\) 的值,然后在不超过该值的情况下尽可能使 \(\sum_{i=1}^{n}p_{i}\cdot i\) 的值变大。要让求和的部分变大,也就是让大的 \(p\) 尽可能靠后,可以使用 \(\frac{\max_{j=1}^{n}p_{j}\cdot j}{p}\) 求得 \(p\) 可以放置的最大 \(i\) 是多少,然后如果该位置已经占用,那么就向左寻找第一个未占用的位置。我们可以使用并查集维护位置的占用情况,如果当前位置占用就将它和左边的位置合并,这样 find(Math.min(n, i)) 就是左边第一个的未占用的位置。如果可以放置的位置不存在,那么说明枚举值太小,终止枚举。(也可以使用栈来维护位置的占用情况)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private static int[] f;

private static int find(int x) {
if (x != f[x]) f[x] = find(f[x]);
return f[x];
}

public static void solve() {
int n = io.nextInt(), ans = 0;
// 枚举公式的后半部分的值
for (int mx = n * n; mx >= 1; mx--) {
int sum = 0;
boolean ok = true;
f = new int[n + 1];
for (int i = 0; i <= n; i++) {
f[i] = i;
}
// 枚举排列的值
for (int i = n; i >= 1; i--) {
// 该值可以放置的最大位置
int x = find(Math.min(n, mx / i));
if (x == 0) {
ok = false;
break;
}
sum += i * x;
// 当前位置已占用,f[x] 存储左边可以放置的第一个位置
f[x] = f[x - 1];
}
if (!ok) break;
ans = Math.max(ans, sum - mx);
}
io.println(ans);
}

还有一个解法,但是不知道如何证明正确性,也是可以过的。其实比赛的时候我就猜了这个结论,但是当时没试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void solve() {
int n = io.nextInt(), ans = 0;
// 枚举一个位置,然后反转它以及它后面的值
for (int i = 1; i <= n; i++) {
int sum = 0, max = 0;
for (int j = 1; j <= n; j++) {
int x = j < i ? j : n - j + i;
sum += x * j;
max = Math.max(max, x * j);
}
ans = Math.max(ans, sum - max);
}
io.println(ans);
}

Andrey and Escape from Capygrad

首先,显然向左传送不会比向右传送到达更远的地方。考虑只有一个区间的情况:如果起点在 \([l,b]\) 之间,那么可以最远到达 \(b\) 点;如果起点在 \((b,r]\) 之间(即不在 \([l,b]\) 之间),那么当前点就是最远的点。可以发现,能够到达的最远位置只与 \(l\) 和 \(b\),以及起点位置有关。所以考虑将所有区间 \([l,b]\) 合并,对每个查询都查找当前起点所在的区间。如果在某个区间内,最远位置即为该区间的右端点;如果不在任何区间内,那么最远位置即为当前位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static void solve() {
int n = io.nextInt();
int[][] portals = new int[n][2];
// 只需要考虑 l 和 b
for (int i = 0; i < n; i++) {
portals[i][0] = io.nextInt();
io.nextInt();
io.nextInt();
portals[i][1] = io.nextInt();
}
// 区间合并
Arrays.sort(portals, (a, b) -> a[0] - b[0]);
List<int[]> intervals = new ArrayList<>();
intervals.add(new int[]{Integer.MIN_VALUE, Integer.MIN_VALUE});
for (int i = 0; i < n; i++) {
int m = intervals.size();
if (intervals.get(m - 1)[1] < portals[i][0]) {
intervals.add(new int[]{portals[i][0], portals[i][1]});
} else {
intervals.get(m - 1)[1] = Math.max(intervals.get(m - 1)[1], portals[i][1]);
}
}
// 二分找区间
int q = io.nextInt();
for (int i = 0; i < q; i++) {
int x = io.nextInt();
int lo = 0, hi = intervals.size() - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (intervals.get(mid)[0] > x) hi = mid - 1;
else lo = mid + 1;
}
io.print(Math.max(x, intervals.get(hi)[1]) + " ");
}
io.println();
}

Codeforces Round 891 (Div. 3)

Array Coloring

要将数组分为奇偶性相同的两部分,那么奇数的个数一定要是偶数。

1
2
3
4
5
6
7
public static void solve() {
int n = io.nextInt(), sum = 0;
for (int i = 0; i < n; i++) {
sum += io.nextInt();
}
io.println(sum % 2 == 0 ? "YES" : "NO");
}

Maximum Rounding

题目有点难读,其实就是大于等于 \(5\) 的数可以向前进位,并且包括自己在内的所有低位全部置为 \(0\)。

1
2
3
4
5
6
7
8
9
10
11
12
public static void solve() {
char[] s = io.next().toCharArray();
int n = s.length, c = 0, p = n;
for (int i = n - 1; i > 0; i--) {
if (s[i] >= '5') {
s[i - 1]++;
p = i;
}
}
if (s[0] >= '5') io.println("1" + "0".repeat(n));
else io.println(new String(s, 0, p) + "0".repeat(n - p));
}

Assembly via Minimums

对数组排序,最小值会出现 \(n - 1\) 次,次小值会出现 \(n - 2\) 次,以此类推,次大值出现 \(1\) 次,最大值出现 \(0\) 次,所以最后需要补一个最大值。

1
2
3
4
5
6
7
8
9
10
11
12
public static void solve() {
int n = io.nextInt(), m = n * (n - 1) / 2;
int[] b = new int[m];
for (int i = 0; i < m; i++) {
b[i] = io.nextInt();
}
Arrays.sort(b);
for (int i = 0; i < m; i += --n) {
io.print(b[i] + " ");
}
io.println(b[m - 1]);
}

Strong Vertices

将公式变形,易知 \(a_{u} - b_{u}\) 的值最大的元素是强壮的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void solve() {
int n = io.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = io.nextInt();
}
int max = Integer.MIN_VALUE, cnt = 0;
for (int i = 0; i < n; i++) {
a[i] -= io.nextInt();
if (a[i] > max) {
max = a[i];
cnt = 1;
} else if (a[i] == max) {
cnt++;
}
}
io.println(cnt);
for (int i = 0; i < n; i++) {
if (a[i] == max) {
io.print(i + 1 + " ");
}
}
io.println();
}

Power of Points

对于每个 \(x_{i}\) 构成的区间,\(\sum_{p=1}^{10^9}f_{p}\) 表示所有区间包含的元素的个数的和。暴力计算的时间复杂度是 \(O(n^{2})\),但是我们可以考虑 \(x\) 从从小到大转移时,元素个数的变化量,从而使用 \(O(n\log n)\) 的时间复杂度计算出所有答案。(也可以像官解一样推公式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void solve() {
int n = io.nextInt();
long sum = 0L;
int[] x = new int[n];
Integer[] aux = new Integer[n];
for (int i = 0; i < n; i++) {
x[i] = io.nextInt();
sum += x[i];
aux[i] = i;
}
Arrays.sort(aux, (i, j) -> x[i] - x[j]);
long[] ans = new long[n];
ans[aux[0]] = sum -= (long) n * (x[aux[0]] - 1);
for (int k = 1; k < n; k++) {
sum += (long) (k - (n - k)) * (x[aux[k]] - x[aux[k - 1]]);
ans[aux[k]] = sum;
}
for (long s : ans) io.print(s + " ");
io.println();
}

Sum and Product

解方程。。因为要求是整数解,所以根号下必须是完全平方数。还有要注意 \(\Delta\) 小于零的情况,不过 Java 的开根函数在小于零的情况下会返回 NaN,转成整数就是零,在该题目的判断中不会引发问题,但还是最好特判一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void solve() {
int n = io.nextInt();
Map<Long, Integer> map = new HashMap<>();
for (int i = 0; i < n; i++) {
map.merge((long) io.nextInt(), 1, Integer::sum);
}
int q = io.nextInt();
for (int i = 0; i < q; i++) {
long x = io.nextInt(), y = io.nextLong();
long d = x * x - 4 * y, s = (long) Math.sqrt(d);
if (d < 0 || s * s != d) {
io.print(0 + " ");
continue;
}
long c1 = map.getOrDefault((x + s) / 2, 0);
long c2 = map.getOrDefault((x - s) / 2, 0);
if (s != 0) io.print(c1 * c2 + " ");
else io.print(c1 * (c1 - 1) / 2 + " ");
}
io.println();
}

Counting Graphs

如果要在 \(u\) 和 \(v\) 之间添加一条边,那么首先要求 \(u\) 和 \(v\) 之间没有直接相连的边,并且新添加的边的权重要大于 \(w\) 小于 \(S\),这样才能保证最小生成树是给定的树。暴力求解的时间复杂度是 \(O(n^{2})\),我们可以利用 Kruskal 算法优化,对边按权重从小到大排序,然后在连接两个顶点时计算两棵树之间顶点连接的方案数,将所有计算结果相乘就是答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static final int MOD = 998244353;

public static void solve() {
int n = io.nextInt(), S = io.nextInt();
List<int[]> edges = new ArrayList<>();
for (int i = 0; i < n - 1; i++) {
int u = io.nextInt(), v = io.nextInt(), w = io.nextInt();
edges.add(new int[]{u, v, w});
}
edges.sort((a, b) -> a[2] - b[2]);
long ans = 1L;
UnionFind uf = new UnionFind(n + 1);
for (int[] e : edges) {
int u = e[0], v = e[1], w = e[2];
ans = (ans * fastPower(S - w + 1, (long) uf.size(u) * uf.size(v) - 1)) % MOD;
uf.union(u, v);
}
io.println(ans);
}