本文最后更新于:7 小时前

落地成盒

题目内容

给定一个长度为 n 的整数数组 {a1,a2,…,ana1​,a2​,…,an​​}。我们按顺序将相邻元素两两成 “盒”:

  • 盒 11 为 (a1,a2)(a1​,a2​),盒 22 为 (a3,a4)(a3​,a4​),以此类推;
  • 若 n 为奇数,则最后一个盒为单元素盒 (an)。

你需要恰好选择 x 个盒,并从每个被选择的盒中选出且仅选出一个数字,使得所有被选数字的和为奇数。判断是否可以做到。

思路

这是一道经典的贪心 + 奇偶性分析题目。代码的核心思路是将问题转化为“在可选范围内,能否凑出奇数个奇数”。

以下是简洁明了的算法解析:

1. 核心思想

要使 $x$ 个数的和为奇数,这 $x$ 个数中必须包含奇数个奇数(例如 1个、3个、5个…),其余的数必须是偶数。

因此,问题转化为:在选出的 $x$ 个盒子中,能否选出 $k$ 个奇数,使得 $k$ 是奇数?

2. 代码逻辑分步解析

第一步:统计盒子的能力
代码遍历数组,将相邻两个数视为一个“盒子”。

  • oddBox:统计有多少个盒子能选出奇数(只要盒子里有一个奇数即可)。
  • evenBox:统计有多少个盒子能选出偶数(只要盒子里有一个偶数即可)。
    • 注意:一个盒子可能既能选奇数也能选偶数(如 {1, 2}),所以两者之和可能大于总盒子数。

第二步:确定“奇数个数 $k$”的合法范围
假设我们在选中的 $x$ 个盒子中,最终拿出了 $k$ 个奇数。那么 $k$ 必须满足以下两个限制:

  1. 上限限制(资源够不够): 我们最多只能从 oddBox 个盒子里拿出奇数,且不能超过总共选的 $x$ 个。
    • $k \le \min(x, \text{oddBox})$ (对应代码中的 right
  2. 下限限制(偶数够不够): 我们选了 $x$ 个数,其中 $k$ 个是奇数,剩下的 $x-k$ 个必须是偶数。这就要求必须有足够的盒子能拿出偶数。
    • $x - k \le \text{evenBox} \implies k \ge x - \text{evenBox}$
    • 同时 $k \ge 0$。
    • 所以 $k \ge \max(0, x - \text{evenBox})$ (对应代码中的 left

第三步:判断是否存在解
现在我们要找一个整数 $k$,满足:

  1. $left \le k \le right$
  2. $k$ 必须是奇数

代码通过 firstOdd 找到区间 $[left, right]$ 内的第一个奇数。如果这个奇数没有超出右边界 right,说明存在合法的 $k$,输出 Yes,否则 No

总结

该算法的时间复杂度为 $O(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
54
55
56
57
import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
StringBuilder sb = new StringBuilder();

int t = in.nextInt();

while (t-- > 0) {
int n = in.nextInt();
int x = in.nextInt();

int oddBox = 0; // 能选出奇数的盒子数
int evenBox = 0; // 能选出偶数的盒子数

// 按盒子读
for (int i = 1; i <= n; i += 2) {
int a = in.nextInt();

if (i == n) {
// 最后一个单元素盒
if ((a & 1) == 1) {
oddBox++;
} else {
evenBox++;
}
} else {
int b = in.nextInt();

// 这个盒子里只要有奇数,就能贡献奇数
if (((a & 1) == 1) || ((b & 1) == 1)) {
oddBox++;
}

// 这个盒子里只要有偶数,就能贡献偶数
if (((a & 1) == 0) || ((b & 1) == 0)) {
evenBox++;
}
}
}

int left = Math.max(0, x - evenBox);
int right = Math.min(x, oddBox);

int firstOdd = (left % 2 == 1) ? left : left + 1;

if (firstOdd <= right) {
sb.append("Yes\n");
} else {
sb.append("No\n");
}
}

System.out.print(sb.toString());
}
}

最长非重复子串

题目内容

我们定义一个仅由小写字母组成的字符串 s 的奇怪度为:其中的最长非重复子串数量。此处 “数量” 按起止位置计数,不按内容去重。

现在,给定两个整数 n 和 k,要求构造一个长度为 n 、仅由小写字母组成的字符串 s,其奇怪度恰好为 k 。如果无解,则输出 −1。

【名词解释】

非重复子串:每个字符出现的次数不超过 1 次的字符串。例如 “abcg” 是非重复子串,而 “aba” 不是非重复子串(因为 ‘a’ 出现了两次)。

子串:从原字符串中,连续的选择一段字符(可以全选、可以不选)得到的新字符串。

思路

关键观察

最长非重复子串的长度取决于字符串中连续不同字符的最大个数:

  • 全相同字符(如”aaa”)→ 最长非重复子串长度为1
  • 交替字符(如”abab”)→ 最长非重复子串长度为2(只有a和b两种字符)

构造策略

情况1:k = n

  • 输出n个相同字符(如”aaa…a”)
  • 最长非重复子串长度为1,共有n个位置,数量恰好为n ✓

情况2:k < n

  • 前k+1个字符交替使用’a’和’b’(如”ababa…”)
  • 剩余n-(k+1)个字符全部填充为最后一个字符
  • 例如:n=5, k=3 → “ababb”

正确性证明

对于构造的字符串(前k+1位交替,后面全相同):

  1. 最长非重复子串长度 = 2(因为只有a和b,且交替出现)
  2. 长度为2的非重复子串数量 = k
    • 交替部分长度为k+1
    • 相邻位置对的数量 = (k+1) - 1 = k
  3. 后面填充相同字符不会产生新的长度为2的非重复子串

时间复杂度

  • **O(n)**:只需一次线性构造

示例验证

n k 构造结果 最长非重复子串 数量
3 3 “aaa” “a” 3 ✓
5 3 “ababb” “ab”, “ba”, “ab” 3 ✓
4 2 “abaa” “ab”, “ba” 2 ✓

代码

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
import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);

int n = in.nextInt();
int k = in.nextInt();

StringBuilder sb = new StringBuilder();

// k = n:输出全相同字符
if (k == n) {
for (int i = 0; i < n; i++) {
sb.append('a');
}
System.out.println(sb);
return;
}

// 1 <= k < n:
// 前 k+1 位交替放 a/b,制造恰好 k 次相邻不同
for (int i = 0; i <= k; i++) {
if ((i & 1) == 0) {
sb.append('a');
} else {
sb.append('b');
}
}

// 后面的字符全部补成和最后一个字符相同,避免再增加新的相邻不同
char last = sb.charAt(sb.length() - 1);
while (sb.length() < n) {
sb.append(last);
}

System.out.println(sb);
}
}

我即唯一

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import java.io.*;
import java.util.*;

public class Main {
static final long MOD = 1000000007L;
static final int LOG = 31;

// 求 1+2+...+x
static long tri(long x) {
return x % MOD * ((x + 1) % MOD) % MOD * ((MOD + 1) / 2) % MOD;
}

// 倍增求从 start 出发前 step 次来到城市的编号和
static long pathSum(int start, long step, int[][] up, long[][] sum) {
long ans = 0;
int x = start;
int b = 0;
while (step > 0) {
if ((step & 1) == 1) {
ans = (ans + sum[b][x]) % MOD;
x = up[b][x];
}
step >>= 1;
b++;
}
return ans;
}

static long cycleSegSum(int cid, int start, int len, ArrayList<int[]> cycleList, ArrayList<long[]> preList) {
if (len == 0) return 0;
int[] nodes = cycleList.get(cid);
int L = nodes.length;
long[] pre = preList.get(cid);
if (start + len <= L) {
return (pre[start + len] - pre[start] + MOD) % MOD;
} else {
return (pre[L] - pre[start] + pre[(start + len) % L] + MOD) % MOD;
}
}

static void solve() throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int q = Integer.parseInt(st.nextToken());

int[] a = new int[n + 1];
st = new StringTokenizer(br.readLine());
for (int i = 1; i <= n; i++) {
a[i] = Integer.parseInt(st.nextToken());
}

// 反图与入度
ArrayList<Integer>[] rev = new ArrayList[n + 1];
for (int i = 1; i <= n; i++) rev[i] = new ArrayList<>();
int[] indeg = new int[n + 1];
for (int i = 1; i <= n; i++) {
rev[a[i]].add(i);
indeg[a[i]]++;
}

// 拓扑删点找环
boolean[] inCycle = new boolean[n + 1];
Arrays.fill(inCycle, true);
ArrayDeque<Integer> dq = new ArrayDeque<>();
for (int i = 1; i <= n; i++) {
if (indeg[i] == 0) {
dq.add(i);
inCycle[i] = false;
}
}

while (!dq.isEmpty()) {
int u = dq.poll();
int v = a[u];
indeg[v]--;
if (indeg[v] == 0) {
inCycle[v] = false;
dq.add(v);
}
}

// 基本信息
int[] cid = new int[n + 1];
int[] pos = new int[n + 1];
int[] dist = new int[n + 1];
int[] entryPos = new int[n + 1];
Arrays.fill(cid, -1);
Arrays.fill(pos, -1);
Arrays.fill(dist, -1);
Arrays.fill(entryPos, -1);

boolean[] vis = new boolean[n + 1];

ArrayList<int[]> cycleList = new ArrayList<>();
ArrayList<Long> cycleSum = new ArrayList<>();
ArrayList<long[]> preList = new ArrayList<>();

// 枚举所有环
for (int i = 1; i <= n; i++) {
if (inCycle[i] && !vis[i]) {
ArrayList<Integer> list = new ArrayList<>();
int x = i;
while (!vis[x]) {
vis[x] = true;
pos[x] = list.size();
list.add(x);
x = a[x];
}

int id = cycleList.size();
int m = list.size();
int[] nodes = new int[m];
long[] pre = new long[m + 1];
long total = 0;

for (int j = 0; j < m; j++) {
int u = list.get(j);
nodes[j] = u;
cid[u] = id;
dist[u] = 0;
entryPos[u] = pos[u];
total = (total + u) % MOD;
pre[j + 1] = (pre[j] + u) % MOD;
}

cycleList.add(nodes);
cycleSum.add(total);
preList.add(pre);
}
}

// 反图 BFS 求树上点信息
dq.clear();
for (int i = 1; i <= n; i++) {
if (inCycle[i]) dq.add(i);
}

while (!dq.isEmpty()) {
int u = dq.poll();
for (int v : rev[u]) {
if (dist[v] != -1) continue;
if (inCycle[v]) continue;
dist[v] = dist[u] + 1;
cid[v] = cid[u];
entryPos[v] = entryPos[u];
dq.add(v);
}
}

// 倍增预处理
int[][] up = new int[LOG][n + 1];
long[][] sum = new long[LOG][n + 1];

for (int i = 1; i <= n; i++) {
up[0][i] = a[i];
sum[0][i] = i;
}

for (int j = 1; j < LOG; j++) {
for (int i = 1; i <= n; i++) {
int mid = up[j - 1][i];
up[j][i] = up[j - 1][mid];
sum[j][i] = (sum[j - 1][i] + sum[j - 1][mid]) % MOD;
}
}

StringBuilder out = new StringBuilder();

for (int i = 0; i < q; i++) {
st = new StringTokenizer(br.readLine());
int p = Integer.parseInt(st.nextToken());
long k = Long.parseLong(st.nextToken());

int d = dist[p];

// 未入环
if (k <= d) {
out.append(pathSum(p, k, up, sum)).append('\n');
continue;
}

// 入环前部分
long res = pathSum(p, d, up, sum);

// 环上部分
int id = cid[p];
int start = entryPos[p];
int L = cycleList.get(id).length;
long total = cycleSum.get(id);

long m = k - d;
long r = m / L;
int rem = (int) (m % L);

res = (res + tri(r) * total) % MOD;
res = (res + ((r + 1) % MOD) * cycleSegSum(id, start, rem, cycleList, preList)) % MOD;

out.append(res % MOD).append('\n');
}

System.out.print(out);
}

public static void main(String[] args) throws Exception {
solve();
}
}


https://alleyf.github.io/2026/04/5e5f9cc786ce.html
作者
alleyf
发布于
2026年4月20日
更新于
2026年4月20日
许可协议