Codeforces Round 902 (Div. 2, based on COMPFEST 15 - Final Round)

Goals of Victory

所有效率之和等于零。

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

Helmets in Night Light

贪心选择最小花费的通知,注意维护已经被通知的下标,根据该下标判断是否需要加 \(p\),并且如果某个人通知其他人的成本大于 \(p\),则他不会通知其他人。发现大佬的解法好简单,取一个最小值,维护一个剩余人数即可。

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
public static void solve() {
int n = io.nextInt(), p = io.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = io.nextInt();
}
int[] b = new int[n];
for (int i = 0; i < n; i++) {
b[i] = io.nextInt();
}

var aux = new Integer[n];
for (int i = 0; i < n; i++) {
aux[i] = i;
}
Arrays.sort(aux, (i, j) -> b[i] - b[j]);

long ans = p;
int r = n - 1;
for (int i : aux) {
int x = Math.min(r, a[i]);
ans += (long) x * Math.min(p, b[i]);
r -= x;
}
io.println(ans);
}

Joyboard

分类讨论,注意被 \(n\) 整除的数。

1
2
3
4
5
6
7
public static void solve() {
int n = io.nextInt(), m = io.nextInt(), k = io.nextInt();
if (k == 3) io.println(Math.max(0, m - n - m / n + 1));
else if (k == 2) io.println(m / n + Math.min(n - 1, m));
else if (k == 1) io.println(1);
else io.println(0);
}

Effects of Anti Pimples

很容易的想到计算选择每个索引位置,能够得到的最大分数,时间复杂度为 \(O(n\log{n})\)。然后计算每个位置有多少种方案,比赛时我以为选择一个索引位置,默认就选择了它的所有倍数位置,导致不会做。其实只要排序,然后每个位置的方案数就是它左边的数选或不选的方案数,这样可以保证不会重复计算,所以快速幂计算方案数(可以在遍历的时候计算方案数),再乘以得分累加到答案即可。

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

public static void solve() {
int n = io.nextInt();
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
dp[i] = io.nextInt();
}

dp[0] = Integer.MAX_VALUE;
for (int i = 1; i <= n; i++) {
for (int j = 2 * i; j <= n; j += i) {
dp[i] = Math.max(dp[i], dp[j]);
}
}

Arrays.sort(dp);
long ans = 0L, pow = 1L;
for (int i = 0; i < n; i++) {
ans = (ans + dp[i] * pow) % MOD;
pow = pow * 2 % MOD;
}
io.println(ans);
}

Autosynthesis

方法一:内向基环树

对于每个 \(i\),建立一条从 \(i\) 指向 \(a_{i}\) 的边,最终会得到多个内向基环树。规则一:入度为零的节点不会被选择;规则二:如果一个节点的所有入度节点都被选择,那么它不会被选择;规则三:如果一个节点不被选择,那么它指向的节点会被选择。(以上规则可以使用拓扑序 + 染色法实现)

如图,数组为 \([2,3,4,5,6,3,3,4]\),剩余索引 \([1,5,7,8]\),剩余元素 \([2,6,3,4]\),选择索引 \([2,6,3,4]\)。应用规则一,得出索引 \([1,7,8]\) 不被选择;应用规则三,得出索引 \([2,3,4]\) 被选择;应用规则二,得出索引 \([5]\) 不被选择;应用规则三,得出索引 \([6]\) 被选择。

特殊情况,如果内向基环树没有环外节点,无法应用上述规则,那么需要单独对环交替染色,若此时环的长度为奇数,则没有解,其他情况均有解。也就是说,当且仅当内向基环树没有环外节点,且环的长度为奇数时,无解。

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();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = io.nextInt() - 1;
}

int[] in = new int[n];
for (int i = 0; i < n; i++) {
in[a[i]]++;
}

// 拓扑序染色(-1 表示不选择,1 表示选择)
int[] col = new int[n];
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (in[i] == 0) {
q.offer(i);
col[i] = -1;
}
}
while (!q.isEmpty()) {
int x = q.poll(), y = a[x];
if (col[y] != 0) continue;
if (--in[y] == 0 || col[x] == -1) {
q.offer(y);
col[y] = -col[x];
}
}

// 单独处理没有环外节点的基环树
for (int i = 0; i < n; i++) {
if (col[i] != 0) continue;
int j = i, pre = -1;
while (col[j] == 0) {
col[j] = -pre;
pre = col[j];
j = a[j];
}
if (col[j] == pre) {
io.println(-1);
return;
}
}

List<Integer> ans = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (col[i] == -1) ans.add(a[i] + 1);
}
io.println(ans.size());
for (int x : ans) {
io.print(x + " ");
}
io.println();
}

方法二:外向基环树

好像有建立外向基环树的解法,没时间看,在此

第 366 场力扣周赛

分类求和并作差

数学性质,\([1,n]\) 中能被 \(m\) 整除的数有 \(\lfloor \frac{n}{m}\rfloor\) 个。

1
2
3
4
5
class Solution {
public int differenceOfSums(int n, int m) {
return (1 + n) * n / 2 - (1 + (n / m)) * (n / m) * m;
}
}

最小处理时间

排序 + 贪心。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int minProcessingTime(List<Integer> processorTime, List<Integer> tasks) {
Collections.sort(processorTime);
Collections.sort(tasks, (a , b) -> b - a);
int n = processorTime.size(), ans = 0;
for (int i = 0; i < n; i++) {
ans = Math.max(ans, processorTime.get(i) + tasks.get(i * 4));
}
return ans;
}
}

执行操作使两个字符串相等

方法一:动态规划

今天脑子有点笨啊,本来做出来了,但是我将 dp 的初始值设置为 Integer.MAX_VALUE,将 dfs 不满足条件时的返回值也设置为该值,而判断是否记忆化的条件也设置为该值,所以所有不满足条件的方案都没有记忆化上。

我经常会写出从上到下记忆化的代码,但是每次都比从下到上的记忆化慢,经过分析,原因如下:从下到上的记忆化,只要该节点计算过,就会直接返回;而从上到下的记忆化,只有在当前节点的值比记忆化的值大时,才会直接返回,也就是说,这样的代码只会将所有大于记忆化的值的方案给剪枝掉,所有小于记忆化的值的方案会重复计算。

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
class Solution {
public int minOperations(String s1, String s2, int x) {
int n = s1.length();
int[][][] dp = new int[n][n + 1][2];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n + 1; j++) {
Arrays.fill(dp[i][j], -1);
}
}
int ans = dfs(0, 0, 0, s1, s2, x, dp);
return ans >= (int) 1e9 ? -1 : ans;
}

private int dfs(int i, int c, int r, String s1, String s2, int x, int[][][] dp) {
if (i == s1.length()) {
if (c % 2 == 0 && r == 0) {
return -c / 2 * x;
}
return (int) 1e9;
}
if (dp[i][c][r] != -1) return dp[i][c][r];
int res = 0;
if ((s1.charAt(i) == s2.charAt(i)) == (r == 0)) {
res = dfs(i + 1, c, 0, s1, s2, x, dp);
} else {
res = dfs(i + 1, c + 1, 0, s1, s2, x, dp) + x;
res = Math.min(res, dfs(i + 1, c, 1, s1, s2, x, dp) + 1);
}
return dp[i][c][r] = res;
}
}

方法二:动态规划

时间复杂度 \(O(n)\),空间复杂度 \(O(1)\) 的解法,其实比赛时第一眼差不多就想到这种解法,但是没有细想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int minOperations(String s1, String s2, int x) {
int n = s1.length();
int pre = -1, cnt = 0;
int dp0 = (int) 1e9, dp1 = 0;
for (int i = 0; i < n; i++) {
if (s1.charAt(i) != s2.charAt(i)) {
cnt ^= 1;
int t = dp1;
dp1 = Math.min(dp1 + (cnt == 1 ? x : 0), dp0 + i - pre);
dp0 = t;
pre = i;
}
}
return cnt == 1 ? -1 : dp1;
}
}

对数组执行操作使平方和最大

挺简单一道题,T3 卡太久,脑子短路没时间做这题。对于每一位,每次操作其实就是交换两个数之间的 \(0\) 和 \(1\),我们应该总是把 \(1\) 交换到更大的数上,这样平方和最大。所以统计每一位的 \(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
class Solution {
private static final int MOD = (int) 1e9 + 7;

public int maxSum(List<Integer> nums, int k) {
int[] cnt = new int[30];
for (int x : nums) {
for (int i = 0; i < 30; i++) {
cnt[i] += x >> i & 1;
}
}

long ans = 0L;
while (k-- != 0) {
int x = 0;
for (int i = 0; i < 30; i++) {
if (cnt[i] > 0) {
cnt[i]--;
x |= 1 << i;
}
}
ans = (ans + (long) x * x) % MOD;
}

return (int) ans;
}
}

第 365 场力扣周赛

有序三元组中的最大值 I

同下。

有序三元组中的最大值 II

方法一:枚举 j

比赛时第一想法是枚举 \(j\),然后取左边和右边的最大值计算答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public long maximumTripletValue(int[] nums) {
int n = nums.length;
int[] pre = new int[n + 1];
int[] suf = new int[n + 1];
for (int i = 0; i < n; i++) {
pre[i + 1] = Math.max(pre[i], nums[i]);
}
for (int i = n - 1; i >= 0; i--) {
suf[i] = Math.max(suf[i + 1], nums[i]);
}
long ans = 0L;
for (int j = 1; j < n - 1; j++) {
ans = Math.max(ans, (long) (pre[j] - nums[j]) * suf[j + 1]);
}
return ans;
}
}

方法二:枚举 k

参考灵神的题解,可以枚举 \(k\),使空间复杂度降为 \(O(1)\)。主要想法就是枚举时,维护前缀最大差值。

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public long maximumTripletValue(int[] nums) {
long ans = 0;
int maxDiff = 0, preMax = 0;
for (int x : nums) {
ans = Math.max(ans, (long) maxDiff * x);
maxDiff = Math.max(maxDiff, preMax - x);
preMax = Math.max(preMax, x);
}
return ans;
}
}

无限数组的最短子数组

比赛时思路很快出来,但是实现的时候漏掉一些边界条件,导致 WA 多次,本来有机会进第一页的。比较直接的想法是枚举起始位置,然后利用前缀和二分结束的位置,如果满足条件就记入答案。也可以使用哈希表来优化,避免二分。这里贴一下灵神的滑动窗口解法,时间复杂度 \(O(n)\),空间复杂度 \(O(1)\)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int minSizeSubarray(int[] nums, int target) {
int n = nums.length;
long sum = 0L, pre = 0L;
for (int x : nums) sum += x;
int q = (int) (target / sum), r = (int) (target % sum);
int lo = 0, hi = 0, ans = Integer.MAX_VALUE;
while (hi < 2 * n) {
pre += nums[hi++ % n];
while (pre > r) {
pre -= nums[lo++ % n];
}
if (pre == r) {
ans = Math.min(ans, hi - lo);
}
}
return ans == Integer.MAX_VALUE ? -1 : ans + q * n;
}
}

有向图访问计数

看完题目就知道是内向基环树,直接修改上次的代码,轻松通过,本题是上次的简化版。

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
class Solution {
public int[] countVisitedNodes(List<Integer> edges) {
int n = edges.size();
// 建立环外节点的反向边
int[] in = new int[n];
List<Integer>[] reverse = new List[n];
Arrays.setAll(reverse, r -> new ArrayList<>());
for (int i = 0; i < n; i++) {
in[edges.get(i)]++;
reverse[edges.get(i)].add(i);
}
// 拓扑序去除环外节点
Queue<Integer> q = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (in[i] == 0) q.add(i);
}
while (!q.isEmpty()) {
int x = q.poll();
if (--in[edges.get(x)] == 0) {
q.offer(edges.get(x));
}
}
// 记录每个节点在哪个环
int[] cirNum = new int[n];
boolean[] vis = new boolean[n];
List<Integer> circles = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (!vis[i] && in[i] != 0) {
int cnt = 0;
for (int cur = i; !vis[cur]; cur = edges.get(cur)) {
vis[cur] = true;
cirNum[cur] = circles.size();
cnt++;
}
circles.add(cnt);
}
}
// 对环内的每个节点向环外进行 dfs
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
if (in[i] != 0) dfs(i, reverse, in, circles.get(cirNum[i]), ans);
}
return ans;
}

private void dfs(int x, List<Integer>[] reverse, int[] in, int len, int[] ans) {
ans[x] = len;
for (int y : reverse[x]) {
if (in[y] != 0) continue;
dfs(y, reverse, in, len + 1, ans);
}
}
}

有个非常简单的写法,参考题解

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
class Solution {
public int[] countVisitedNodes(List<Integer> edges) {
int n = edges.size();
int[] ans = new int[n];
int[] vis = new int[n];
for (int i = 0; i < n; i++) {
if (ans[i] == 0) {
int cnt = 0, cirLen = 0, totLen = 0, pos = i;
while (vis[pos] == 0) {
vis[pos] = ++cnt;
pos = edges.get(pos);
}
if (ans[pos] == 0) {
cirLen = cnt - vis[pos] + 1;
totLen = cnt;
} else {
totLen = cnt + ans[pos];
}
pos = i;
while (ans[pos] == 0) {
ans[pos] = Math.max(totLen--, cirLen);
pos = edges.get(pos);
}
}
}
return ans;
}
}