Codeforces Round 889 (Div. 2)

Dalton the Teacher

如果伤心的学生有 \(x\) 个,则答案为 \(\lceil \frac{x}{2}\rceil\)。步骤如下:如果至少有两个伤心的学生,则交换他们的椅子;如果只有一个伤心的学生,则让他和任意其他学生交换椅子。

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

Longest Divisors Interval

这道题感觉很妙啊,比赛时看到 \(n\) 的范围很大,所以猜了一个结论也没有细想,结果是对的。假设我们已经找到区间 \([l,r]\) 对每个满足 \(l\leq i\leq r\) 的 \(i\),\(n\bmod i=0\)。然后我们可以将区间 \([l,r]\) 转化为区间 \([1,r-l+1]\),因为对每个满足 \(1\leq x\leq r-l+1\) 的 \(x\),在区间 \([l,r]\) 中总是可以找到一个数 \(y\),使得 \(y\bmod x=0\),因而也满足 \(n\bmod x=0\)。

为什么总是可以找到呢?因为一个连续的数列,对 \(x\) 取余得到的余数的周期为 \(x\),所以一个长度为 \(x\) 的区间内,总是可以找到一个数 \(y\),使得 \(y\bmod x=0\)。

时间复杂度 \(O(\log{(\max n)})\),具体不知道怎么算的。

1
2
3
4
5
6
7
8
9
public static void solve() {
long n = io.nextLong();
for (int i = 1; ; i++) {
if (n % i != 0) {
io.println(i - 1);
return;
}
}
}

Dual (Easy Version), Dual (Hard Version)

比赛时想到找最大或最小的数和倍增,但是没弄明白。首先,如果所有数都非负或非正,那么只要做前缀或后缀和就可以得到非递减的数组,最多操作 \(19\) 次。此时我们还剩下 \(31-19=12\) 次操作机会,我们考虑如何在 \(12\) 次操作内把数组中的数都变为非负或非正:

  • 当最大的正数加最小的负数大于等于零时:如果负数的数量小于等于 \(12\),那么我们可以在 \(12\) 次操作内把所有负数变为正数;反之,我们可以选择一个负数让它倍增 \(5\) 次,它就会变为最小的负数,并且最大的正数加最小的负数一定小于零,然后我们就可以在 \(7\) 次操作内把所有正数变为负数(因为此时正数的数量小于 \(7\))。
  • 当最大的正数加最小的负数小于等于零时:同理。
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
56
57
58
59
60
61
62
public static void solve() {
int n = io.nextInt();
int[] arr = new int[n];
int minPos = 0, maxPos = 0, neg = 0, pos = 0;
for (int i = 0; i < n; i++) {
arr[i] = io.nextInt();
if (arr[i] < 0) neg++;
if (arr[i] > 0) pos++;
if (arr[i] < arr[minPos]) minPos = i;
if (arr[i] > arr[maxPos]) maxPos = i;
}
List<int[]> ans = new ArrayList<>();
if (arr[minPos] + arr[maxPos] >= 0) {
if (neg <= 12) {
for (int i = 0; i < n; i++) {
if (arr[i] < 0) {
ans.add(new int[]{i, maxPos});
}
}
for (int i = 0; i < n - 1; i++) {
ans.add(new int[]{(i + 1), i});
}
} else {
for (int i = 0; i < 5; i++) {
ans.add(new int[]{minPos, minPos});
}
for (int i = 0; i < n; i++) {
if (arr[i] > 0) {
ans.add(new int[]{i, minPos});
}
}
for (int i = n - 2; i >= 0; i--) {
ans.add(new int[]{i, (i + 1)});
}
}
} else {
if (pos <= 12) {
for (int i = 0; i < n; i++) {
if (arr[i] > 0) {
ans.add(new int[]{i, minPos});
}
}
for (int i = n - 2; i >= 0; i--) {
ans.add(new int[]{i, (i + 1)});
}
} else {
for (int i = 0; i < 5; i++) {
ans.add(new int[]{maxPos, maxPos});
}
for (int i = 0; i < n; i++) {
if (arr[i] < 0) {
ans.add(new int[]{i, maxPos});
}
}
for (int i = 0; i < n - 1; i++) {
ans.add(new int[]{(i + 1), i});
}
}
}
io.println(ans.size());
ans.forEach(k -> io.println((k[0] + 1) + " " + (k[1] + 1)));
}

Earn or Unlock

每种方案都有一个可以到达的最远位置 \(x\),对于该位置我们能够得到的点数是确定的,即为 \(\sum_{i=0}^{x}a_{i} - x\) 点。所以我们只需要枚举每一个最远位置就能够解决问题,如果使用 DFS 时间复杂度是指数级别的,通过使用状压 DP 可以降低时间复杂度。假设当前枚举到位置 \(i\),当前的可达位置是 \(dp_{i}\),那么下一个可达位置就是 \(dp_{i+1}=dp_{i}|(dp_{i}<<a_{i})\),然后如果当前位置可达,我们计算完答案之后需要将当前位置置为 \(0\),因为对于下一个位置来说,当前位置已经解锁。如果使用 C++ 实现可以直接使用 \(bitset\),而使用 Java 实现则需要手动写位图,因为 Java 内置的位图没有移位操作。

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[] arr = new int[2 * n + 1];
for (int i = 0; i < n; i++) {
arr[i] = io.nextInt();
}
// 位图
int m = n * 2 / 64 + 1;
long[] dp = new long[m];
long[] dq = new long[m];
dp[0] = 1L;
long sum = 0L, ans = 0L;
for (int i = 0; i < 2 * n; i++) {
sum += arr[i];
// 位图左移 arr[i] 位,并且或上它本身
int p = arr[i] / 64, q = arr[i] % 64;
for (int j = 0; j < m; j++) {
dq[j] = dp[j];
if (j >= p) {
dq[j] |= dp[j - p] << q;
if (j > p && q > 0) dq[j] |= dp[j - p - 1] >>> (64 - q);
}
}
long[] tmp = dp;
dp = dq;
dq = tmp;
// 判断当前位是否可达
p = i / 64;
q = i % 64;
if (((dp[p] >> q) & 1) == 1) {
dp[p] ^= 1L << q;
ans = Math.max(ans, sum - i);
}
}
io.println(ans);
}

AtCoder Beginner Contest 312

Chord

简单模拟,比赛时打错了一个字母。

1
2
3
4
5
public static void solve() {
String s = io.next();
Set<String> set = Set.of("ACE", "BDF", "CEG", "DFA", "EGB", "FAC", "GBD");
io.println(set.contains(s) ? "Yes" : "No");
}

TaK Code

因为左上角和右下角是中心对称的,所以判断右下角时可以使用形如 i + 8 - x 的下标来简化代码。

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(), m = io.nextInt();
String[] arr = new String[n];
for (int i = 0; i < n; i++) arr[i] = io.next();
for (int i = 0; i + 8 < n; i++) {
for (int j = 0; j + 8 < m; j++) {
boolean ok = true;
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {
if (arr[i + x].charAt(j + y) != (x != 3 && y != 3 ? '#' : '.')) {
ok = false;
}
if (arr[i + 8 - x].charAt(j + 8 - y) != (x != 3 && y != 3 ? '#' : '.')) {
ok = false;
}
}
}
if (ok) io.println((i + 1) + " " + (j + 1));
}
}
}

Invisible Hand

其实第一眼看到感觉是可以二分做的,不过比赛时使用的是两个优先队列模拟解决的,边界想了半天,结果最优解很妙啊。我们要求最小的 \(x\),使得可能卖 \(x\) 元的卖家数量 \(f(x)\) 大于等于可能花 \(x\) 元买的买家数量 \(g(x)\)。其实我们要求的就是使 \(f(x)-g(x) >= 0\) 时的最小 \(x\),而 \(f(x) - g(x)\) 是随 \(x\) 非严格递增的,当 \(x = 0\) 时,\(f(x)-g(x)=-M\),并且答案的取值在 \(A_{1},\dots,A_{N},B_{1}+1,\dots,B_{M}+1\) 中,所以可以直接排序(或者快速选择),然后输出第 \(M\) 个数即为答案。

1
2
3
4
5
6
7
8
9
10
public static void solve() {
int n = io.nextInt(), m = io.nextInt();
int[] arr = new int[n + m];
for (int i = 0; i < n; i++) arr[i] = io.nextInt();
// 当价格大于买家的价格时,买家才会减一
for (int i = 0; i < m; i++) arr[i + n] = io.nextInt() + 1;
// 可以使用快速选择替换
Arrays.sort(arr);
io.println(arr[m - 1]);
}

Count Bracket Sequences

动态规划,不太会做。首先定义状态 \(dp[i][j]\),表示区间 \([1,i]\) 中左括号比右括号多 \(j\) 个的方案数(也可以定义为其他形式)。然后写状态转移方程,可以画图看下转移方向,每层会分别向左下和右下转移 \(n\) 次,然后就可以写出不用特判边界的转移方程。还可以使用滚动数组优化空间,此处略过。

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

public static void solve() {
String s = io.next();
int n = s.length();
// dp[i][j] 表示区间 [1, i] 中左括号比右括号多 j 个的方案数
int[][] dp = new int[n + 1][n + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
char c = s.charAt(i - 1);
for (int j = 0; j < n; j++) {
if (c != ')') dp[i][j + 1] = dp[i - 1][j];
if (c != '(') dp[i][j] = (dp[i][j] + dp[i - 1][j + 1]) % MOD;
}
}
io.println(dp[n][0]);
}

第 356 场力扣周赛

满足目标工作时长的员工数目

方法一:遍历

1
2
3
4
5
6
7
class Solution {
public int numberOfEmployeesWhoMetTarget(int[] hours, int target) {
int ans = 0;
for (int x : hours) if (x >= target) ans++;
return ans;
}
}

复杂度分析

  • 时间复杂度:\(O(n)\)。
  • 空间复杂度:\(O(1)\)。

统计完全子数组的数目

方法一:暴力优化

比赛时本来是想滑窗的,但是当时没想通。而枚举左右端点再遍历的暴力方法,时间复杂度为 \(O(n^{3})\) 会超时。结果想半天发现可以使用前缀和的思路,先枚举左端点,然后一边遍历一边枚举右端点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int countCompleteSubarrays(int[] nums) {
int n = nums.length;
Set<Integer> set = new HashSet<>();
for (int x : nums) set.add(x);
int cnt = set.size(), ans = 0;
// 至少要有 cnt 个元素
for (int i = 0; i <= n - cnt; i++) {
set.clear();
for (int j = i; j < n; j++) {
set.add(nums[j]);
if (set.size() == cnt) {
ans += n - j;
break;
}
}
}
return ans;
}
}

复杂度分析

  • 时间复杂度:\(O(n^{2})\)。
  • 空间复杂度:\(O(n)\)。

方法二:滑动窗口

枚举右端点,并且让窗口是完全子数组的前提下,使左端点尽可能靠右,此时所有小于等于左端点的位置,都满足条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int countCompleteSubarrays(int[] nums) {
int n = nums.length;
Set<Integer> set = new HashSet<>();
for (int x : nums) set.add(x);
int cnt = set.size();
int lo = 0, hi = 0, ans = 0;
Map<Integer, Integer> map = new HashMap<>();
while (hi < n) {
map.merge(nums[hi++], 1, Integer::sum);
if (map.size() == cnt) {
while (map.get(nums[lo]) > 1) {
map.merge(nums[lo++], -1, Integer::sum);
}
ans += lo + 1;
}
}
return ans;
}
}

复杂度分析

  • 时间复杂度:\(O(n)\)。
  • 空间复杂度:\(O(n)\)。

包含三个字符串的最短字符串

方法一:枚举

枚举字符串 \(a,b,c\) 的全排列,然后从前往后合并,以消除公共字符。需要注意,如果字符串存在包含关系,则不需要进行合并操作。

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
class Solution {
public String minimumString(String a, String b, String c) {
List<String> list = new ArrayList<>();
list.add(merge(merge(a, b), c));
list.add(merge(merge(a, c), b));
list.add(merge(merge(b, a), c));
list.add(merge(merge(b, c), a));
list.add(merge(merge(c, a), b));
list.add(merge(merge(c, b), a));
list.sort((s1, s2) -> {
int m = s1.length(), n = s2.length();
if (m != n) return m - n;
return s1.compareTo(s2);
});
return list.get(0);
}

private String merge(String a, String b) {
if (a.contains(b)) return a;
int m = a.length(), n = b.length();
for (int i = Math.min(m, n); ; i--) {
if (a.substring(m - i).equals(b.substring(0, i))) {
return a + b.substring(i);
}
}
}
}

复杂度分析

  • 时间复杂度:\(O(n^{2})\),其中 \(n\) 为字符串 \(a,b,c\) 长度的最大值。
  • 空间复杂度:\(O(n)\)。

统计范围内的步进数字数目

方法一:数位DP

感觉有点像 DFS,枚举当前位的数字,多传递一个参数 isLimit 可以省去很多判断。

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
class Solution {
private static final int MOD = (int) 1e9 + 7;

public int countSteppingNumbers(String low, String high) {
int m = low.length(), n = high.length();
// dp[i][j] 表示 i 位数的最高位为 j 的步进数字的数目
int[][] dp = new int[n][10];
Arrays.fill(dp[0], 1);
for (int i = 1; i < n; i++) {
for (int j = 0; j < 10; j++) {
if (j - 1 >= 0) dp[i][j] = (dp[i][j] + dp[i - 1][j - 1]) % MOD;
if (j + 1 <= 9) dp[i][j] = (dp[i][j] + dp[i - 1][j + 1]) % MOD;
}
}
// 字符串不方便做减法,所以先减,如果 low 是步进数字则加回来
return (f(dp, high, 0, -1, true) - f(dp, low, 0, -1, true) + valid(low) + MOD) % MOD;
}

private int f(int[][] dp, String s, int i, int pre, boolean isLimit) {
int n = s.length();
// 如果数字不为空,则计数值加一
if (i == n) return pre != -1 ? 1 : 0;
if (pre != -1 && !isLimit) return dp[n - i][pre];
int cur = s.charAt(i) - '0', res = 0;
int hi = isLimit ? cur : 9;
// 如果选 0 并且数字为空,则表示跳过当前位
for (int j = 0; j <= hi; j++) {
if (pre == -1 || Math.abs(j - pre) == 1) {
res = (res + f(dp, s, i + 1, (pre == -1 && j == 0) ? -1 : j, isLimit && j == hi)) % MOD;
}
}
return res;
}

private int valid(String s) {
int n = s.length();
for (int i = 1; i < n; i++) {
if (Math.abs(s.charAt(i) - s.charAt(i - 1)) != 1) {
return 0;
}
}
return 1;
}
}

复杂度分析

  • 时间复杂度:\(O(nm^{2})\),其中 \(n\) 为 high 的长度,\(m = 10\)。
  • 空间复杂度:\(O(nm)\)。