省一:

目前,C ~ F 题已经在 dotcpp 取得 AC,G 和 H 题已知解法错误:
A. 拼正方形
题目描述
有 7385137888721
个 2×22 \times 22×2 的方块和 10470245
个 1×11 \times 11×1 的方块,求能拼出的正方形的最大边长。
解题思路
用 1×11 \times 11×1 的方块给已经用 2×22 \times 22×2 的方块拼成的大正方形“镶边”,每次使其边长加 111。
public class Main {
public static void main(String[] args) {
final long SQ2 = 7385137888721L;
final long SQ1 = 10470245L;
// 用 2x2 的方块拼成一个大正方形
long ans = (long) Math.sqrt(SQ2) * 2;
// 用 1x1 的方块镶边
long sq1 = SQ1;
while (true) {
if ((sq1 -= ans * 2 + 1) < 0) break;
ans++;
}
System.out.println(ans);
}
}
输出:
5435122
答案
5435122
B. 召唤数学精灵
题目描述
计算 iii 的数量,满足:
1
≤i≤\leq i \leq≤i≤2024041331404202
- A(i)−B(i) mod 100=0A(i) - B(i) \space {\rm mod} \space 100 = 0A(i)−B(i) mod 100=0
其中,A(x)=sum(1, x)A(x) = {\rm sum}(1,\space x)A(x)=sum(1, x),B(x)=x!B(x) = x!B(x)=x!。
解题思路
观察到 ∀i≥100\forall i \geq 100∀i≥100,B(i) mod 100=0B(i) \space {\rm mod} \space 100 = 0B(i) mod 100=0(赛后发现 ∀i≥10\forall i \geq 10∀i≥10 即可),那么算式 A(i)−B(i) mod 100=0A(i) - B(i) \space {\rm mod} \space 100 = 0A(i)−B(i) mod 100=0 就化简为 A(i) mod 100=0A(i) \space {\rm mod} \space 100 = 0A(i) mod 100=0。
考虑 sum(1, 200)=20100{\rm sum}(1,\space 200) = 20100sum(1, 200)=20100,20100 mod 100=020100 \space {\rm mod} \space 100 = 020100 mod 100=0,可以以 200200200 为一个周期,计算一个周期内 A(i) mod 100=0A(i) \space {\rm mod} \space 100 = 0A(i) mod 100=0 的次数,与周期数相乘。
将 1
~ 2024041331404202
拆成 1
~ 200
和 201
~ 2024041331404202
两部分:
- 第一部分的特殊之处在于 i=1i = 1i=1 和 i=3i = 3i=3,它们都满足要求,而之后的例如 i=201i = 201i=201 和 i=203i = 203i=203 都不满足要求,因此额外加 222。
- 第二部分以每 200200200 一个周期,算得周期内有 444 个 iii 满足要求(代码略),乘以总周期数。
public class Main {
public static void main(String[] args) {
final long ed = 2024041331404202L;
// 分别计算两部分答案
long ans1 = 4 + 2;
long ans2 = (ed - 200) / 200 * 4;
System.out.println(ans1 + ans2);
}
}
输出:
40480826628086
答案
40480826628086
C. 数字诗意
题目描述
输入 n
个数字,输出其中无法被表示为连续多个(至少 2 个)正整数之和的数字个数。
输入输出示例
输入共 2 行,第 1 行含一个正整数 n
,第 2 行含 n
个正整数 x
:
3
3 6 8
范围:
- 1≤1 \leq1≤
n
≤2×105\leq 2 \times 10^5≤2×105 - 1≤1 \leq1≤
x
≤1×1016\leq 1 \times 10^{16}≤1×1016
输出一个整数表示答案:
1
解释:
3 = 1 + 2
6 = 1 + 2 + 3
8
无法表示为连续多个正整数之和
解题思路
手写 / 暴力打一个小表,下划线 _
表示无法表出的数字:
_ _ 3 _ 5
6 7 _ 9 10
11 12 13 14 15
_ 17 18 19 20
找规律,发现只有 2n2^n2n 无法表出。
记 long
内最大的 2n2^n2n 为 POW
,对输入的每个数 x
,POW % x == 0
时答案加 1。
答案
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(System.out);
final long POW = 1L << 62;
int n = Integer.parseInt(in.readLine());
StringTokenizer st = new StringTokenizer(in.readLine());
int ans = 0;
while (n-- > 0) {
if (POW % Long.parseLong(st.nextToken()) == 0) ans++;
}
out.println(ans);
out.flush();
out.close();
}
}
D. 回文数组
题目描述
输入一个长为 n
的数组,输出使其变得回文的最少操作次数。
在 1 次操作中,可以二选一:
- 选择一个元素,对其增减 111
- 选择相邻的两个元素,对其增减 111
输入输出示例
输入共 2 行,第 1 行含一个正整数 n
,第 2 行含 n
个正整数 x
:
4
1 2 3 4
范围:
- 1⩽1 \leqslant1⩽
n
⩽1×105\leqslant 1 \times 10^5⩽1×105 - −1×106⩽-1 \times 10^6 \leqslant−1×106⩽
x
⩽1×106\leqslant 1 \times 10^6⩽1×106
输出一个整数表示答案:
3
解释:
- 第 1 次操作后:
[2, 3, 3, 4]
- 第 2 次操作后:
[3, 3, 3, 4]
- 第 3 次操作后:
[4, 3, 3, 4]
,已经是回文
解题思路
双指针 i
、j
从两端向中间看,每次先用“操作 1”使 arr[i] == arr[j]
,然后看 arr[i+1]
和 arr[j-1]
是否与它们同号,如果同号,可以将一部分操作改为“操作 2”,从而节约操作次数。
实现时,可以先一次遍历,更新 arr[i] -= arr[j]
,再次遍历时仅比较 arr[i]
和 arr[i+1]
是否同号即可。
答案
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(System.out);
int n = Integer.parseInt(in.readLine());
int[] arr = new int[n];
StringTokenizer st = new StringTokenizer(in.readLine());
for (int i = 0; i < n; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
// 预处理
for (int i = 0, j = n - 1; i < j; i++, j--) {
arr[i] -= arr[j];
}
// 遍历前一半
long ans = 0;
for (int i = 0; i < n / 2; i++) {
if (arr[i] == 0) continue;
// 先用 "操作 1" 变 arr[i]
ans += Math.abs(arr[i]);
// 再看能否顺便变一下 arr[i+1]
if (i + 1 < n / 2 && arr[i] > 0 == arr[i + 1] > 0) {
arr[i + 1] = Math.abs(arr[i]) > Math.abs(arr[i + 1]) ? 0 : arr[i + 1] - arr[i];
}
}
out.println(ans);
out.flush();
out.close();
}
}
E. 吊坠
题目描述
输入 n
个长为 m
的字符串,作为图的结点,每两个结点间都有一条带权边,权值为两字符串的最长公共 子串 的长度。
注意,本题中的字符串都可以旋转,如 "abba"
可以旋转变换为 "aabb"
,从而 "abba"
和 "acca"
之间的边权值为 2
。
选出 最少 的边让所有结点连通,且成本 最大,输出这个成本。
输入输出示例
输入共 n + 1
行,第 1 行含两个正整数 n
、m
,随后 n
行各含一个长为 m
的字符串:
4 4
aabb
abba
acca
abcd
范围:
- 1⩽1 \leqslant1⩽
n
⩽200\leqslant 200⩽200 - 1⩽1 \leqslant1⩽
x
⩽50\leqslant 50⩽50
输出一个整数表示答案:
8
解释:连接 <1, 2>
,<2, 3>
,<2, 4>
,边权和为 4+2+2=84 + 2 + 2 = 84+2+2=8。
解题思路
分 2 大步:
- 求出所有边的边权,如:
[0, 4, 2, 2]
[0, 0, 2, 2]
[0, 0, 0, 1]
[0, 0, 0, 0]
本题中的字符串可旋转,考虑在每个字符串后重复一次自身,如 "abcd"
变为 "abcdabcd"
,这样遍历每个长为 m
的窗口就得到了自身的全部旋转结果。
用动态规划,对两个字符串滑一次,求得它们的最长公共子串长度,即边权。
- 用 kruskal 算法——原本求最小生成树的算法——求 最大生成树。
所有边入堆,并查集判环,无环时加入边并 union()
。
答案
import java.util.*;
import java.io.*;
public class Main {
private static char[][] arr;
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(System.out);
StringTokenizer st = new StringTokenizer(in.readLine());
int n = Integer.parseInt(st.nextToken());
int m = Integer.parseInt(st.nextToken());
// 每个字符串重复自己
arr = new char[n][m * 2];
for (int i = 0; i < n; i++) {
char[] s = in.readLine().toCharArray();
for (int j = 0; j < m; j++) {
arr[i][j] = arr[i][j + m] = s[j];
}
}
// 求所有边权
int[][] dist = new int[n][n];
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
dist[i][j] = lcs(i, j);
}
}
// 所有边入堆
Queue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt((int[] a) -> -a[2]));
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
pq.offer(new int[]{i, j, dist[i][j]});
}
}
// Kruskal 算法
int[] root = new int[n];
Arrays.setAll(root, a -> a);
int take = 0;
int ans = 0;
while (take < n - 1) {
int[] p = pq.poll();
if (find(root, p[0]) != find(root, p[1])) {
union(root, p[0], p[1]);
take++;
ans += p[2];
}
}
out.println(ans);
out.flush();
out.close();
}
// 求两旋转字符串的最长公共子串长度
private static int lcs(int x, int y) {
int m = arr[0].length;
int max = 0;
int[][] dp = new int[m + 1][m + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= m; j++) {
if (arr[x][i - 1] == arr[y][j - 1]) {
dp[i][j] = Math.max(dp[i - 1][j - 1] + 1, dp[i][j]);
max = Math.max(dp[i][j], max);
}
}
}
return Math.min(max, m / 2);
}
// 并查集
private static int find(int[] root, int i) {
return root[i] == i ? i : (root[i] = find(root, root[i]));
}
// 并查集
private static void union(int[] root, int x, int y) {
root[find(root, x)] = find(root, y);
}
}
F. 砍柴
题目描述
A、B 两人轮流砍柴,轮到某一方砍柴时,设原木剩余长度为 x
:
- 如果
x
为 000 或 111,本方无法继续砍柴,从而输掉比赛。 - 如果
x
⩾2\geqslant 2⩾2,本方可选择一个长度 2⩽2 \leqslant2⩽p
⩽\leqslant⩽x
,砍掉长为p
的一段。
共 n
段原木(n
局游戏),每局 A 先手,双方都采用最佳策略。
对每局游戏,如果 A 能够取胜,输出 1
,否则输出 0
。
输入输出示例
输入共 n + 1
行,第 1 行含一个正整数 n
,随后 n
行各含一个正整数 x
:
3
1
2
6
范围:
- 1⩽1 \leqslant1⩽
n
⩽104\leqslant 10^4⩽104 - 1⩽1 \leqslant1⩽
x
⩽105\leqslant 10^5⩽105
输出 n
行,每行含一个整数 0
或 1
:
0
1
1
解释:
- 对
x
=1= 1=1,A 已无法砍柴,A 失败。 - 对
x
=2= 2=2,A 砍掉p
=2= 2=2,轮到 B 时x
=0= 0=0,已无法砍柴,A 获胜。 - 对
x
=6= 6=6,A 砍掉p
=5= 5=5,轮到 B 时x
=1= 1=1,已无法砍柴,A 获胜。
解题思路
典型的博弈问题,预处理范围内的质数集,boolean win(int x)
用 DFS 判断当前剩余长度能否获胜,递归交换对手。
剪枝:对 win(x)
,二分质数集查询 ⩽\leqslant⩽ x
的最大位置,倒序遍历,更快接近 DFS 基态,否则 10510^5105 容易栈溢出。
答案
import java.util.*;
import java.io.*;
public class Main {
private static int[] primes;
private static int[] memo;
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(System.out);
int n = Integer.parseInt(in.readLine());
int[] arr = new int[n];
// 取输入的最大值
int max = 0;
for (int i = 0; i < n; i++) {
arr[i] = Integer.parseInt(in.readLine());
max = Math.max(arr[i], max);
}
// 预处理质数
getPrimes(max);
memo = new int[max + 1];
for (int x : arr) {
out.println(win(x) ? 1 : 0);
}
out.flush();
out.close();
}
// 筛法预处理得到范围内所有质数
private static void getPrimes(int max) {
boolean[] v = new boolean[max + 1];
Arrays.fill(v, true);
for (int x = 2; x <= max; x++) {
if (!v[x]) continue;
for (int y = x * 2; y <= max; y += x) {
v[y] = false;
}
}
int n = 0;
for (int x = 2; x <= max; x++) {
if (v[x]) n++;
}
primes = new int[n];
int i = 0;
for (int x = 2; x <= max; x++) {
if (v[x]) primes[i++] = x;
}
}
// DFS 博弈
private static boolean win(int x) {
if (memo[x] != 0) return memo[x] == 1;
if (x <= 1) return false;
// 剪枝: 倒序遍历质数集, 更快接近 DFS 基态
for (int i = bis(x); i >= 0; i--) {
if (!win(x - primes[i])) {
memo[x] = 1;
return true;
}
}
memo[x] = -1;
return false;
}
// 二分, 从质数集中查找 <= x 的最大位置
private static int bis(int x) {
int l = 0;
int r = primes.length - 1;
while (l <= r) {
int m = l + r >> 1;
if (primes[m] <= x) l++;
else r--;
}
return r;
}
}