Codeforces gym 链接 : https://codeforces.com/gym/105143

最意难平的一次经历,详见 https://www.zhihu.com/question/648600132/answer/3485823785 。
人生就是不断喜悦与不断遗憾的交替啊。
一些总结与技巧
- 树的直径的一些性质:
- 直径两端点一定是两个叶子节点
- 若树上所有边边权均为正,则树的所有直径中点重合
- 有两颗分离的树
A
,
B
A, B
A,B,
(
u
,
v
)
(u, v)
(u,v) 是树 A 的一条直径,
(
x
,
y
)
(x, y)
(x,y) 是树
B
B
B 的一条直径,在
A
,
B
A, B
A,B 间任意连一条边,
(
u
,
v
,
x
,
y
)
(u, v, x, y)
(u,v,x,y) 两两组成的
6
6
6 条路径中必有至少一条是新树的一条直径。
- 特别地,若树 B B B 只有一个点 w w w , ( u , v ) , ( u , w ) , ( v , w ) (u, v), (u, w), (v, w) (u,v),(u,w),(v,w) 中至少有一个是新树的一条直径。
I. Cyclic Apple Strings
给定一个长度为 n n n 的仅由 0 0 0 和 1 1 1 组成的字符串 s s s 。我可以进行如下操作任意次:选择 s s s 的一个子串和一个正整数 k k k ,并将子串向左循环移动 k k k 位。需要求出将 s s s 变为有序的所需的最小操作次数。
统计不在开头的连续的 0 0 0 的段数即可。
B. Countless Me
一个大小为 n n n 的数组,在不改变其总和、所有数字都是非负整数的前提下,随意进行调整,求 a 1 ∣ a 2 ∣ … ∣ a n a_1\,|\,a_2\,|\,\dots\,|\,a_n a1∣a2∣…∣an 的最小值。
考虑从高位向低位遍历:若遍历到了代表 2 i 2^i 2i 的这一位,考虑剩下的 s u m sum sum 能不能全部放到更低的那些位去,如果可以,答案的 2 i 2^i 2i 这一位为 0 0 0 ,否则设为 1 1 1 ,并将 s u m sum sum 尽可能多地填到这一位。
#include<bits/stdc++.h>
using namespace std;
long long x, ans, sum;
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
long long n; cin >> n;
for(int i = 0; i < n; i++)
cin >> x, sum += x;
for(int i = (1 << 30); i > 0; i >>= 1)
if(sum > (i - 1) * n)
ans |= i, sum -= i * min(n, sum / i);
cout << ans << endl;
return 0;
}
K. Party Games
有 n n n 个整数 1 , 2 , 3 , … , n 1, 2, 3, \dots, n 1,2,3,…,n 从左到右顺序排成一行,若剩下的整数的异或和不为 0,当前操作者移走这一行整数中最左边的数或最右边的数,并不改变其余数字的顺序。若当前行动者无法操作,那么其输掉游戏。
我们知道
⊕
i
=
1
n
i
=
{
n
n
m
o
d
4
=
0
1
n
m
o
d
4
=
1
n
+
1
n
m
o
d
4
=
2
0
n
m
o
d
4
=
3
\mathop\oplus\limits_{i = 1}^{n} i = \begin{cases} n & n \bmod 4 = 0 \\ 1 & n \bmod 4 = 1 \\ n + 1 & n \bmod 4 = 2 \\ 0 & n \bmod 4 = 3 \end{cases}
i=1⊕ni=⎩
⎨
⎧n1n+10nmod4=0nmod4=1nmod4=2nmod4=3
异或和为 1 1 1 或 n n n 时,先手拿走 1 1 1 或 n n n ,先手胜;异或和为 0 0 0 时,后手胜;异或和 n + 1 n + 1 n+1 时, n n n 是偶数, n + 1 = n ⊕ 1 n + 1 = n \oplus 1 n+1=n⊕1 ,先手和后手拿走两端的 1 1 1 和 n n n ,后手胜。
F. Custom-Made Clothes
谭老师看了看题:这个东西我在 leetcode 上见过,秒切。WA 了两次,具体记不得了,似乎是因为下标起点搞错了。
有一个 n × n n \times n n×n ( 1 ≤ n ≤ 1000 ) (1 \le n \le 1000) (1≤n≤1000) 的正整数方阵,所有元素 a i , j a_{i,j} ai,j 满足 1 ≤ a i , j ≤ n × n 1 \le a_{i,j} \le n \times n 1≤ai,j≤n×n ,且每一个元素都大于等于其上方元素与左方元素(如果有的话)。每次可以询问某个位置的 a i , j a_{i,j} ai,j 是否不大于 x x x 。需要使用不超过 50000 50000 50000 次询问找出矩阵中 n × n n \times n n×n 个元素从大到小排序后的第 k k k 个元素的值。
考虑二分。 50000 50000 50000 大约等于 2.5 n log ( n × n ) 2.5 n \log(n \times n) 2.5nlog(n×n) ,其中 n × n n \times n n×n 是二分的值域。有没有什么办法用 2.5 n 2.5n 2.5n 次询问得到小于 m i d mid mid 的数字个数呢?以 m i d mid mid 为界,把矩阵分成两块,沿着分割线有大概 2 n 2n 2n 个元素,因此只要沿着分割线询问即可。
#include<bits/stdc++.h>
using namespace std;
// return aij <= x
bool ask(int i, int j, int x) {
if(j == 0) return 1;
cout << "? " << i << ' ' << j << ' ' << x << '\n';
cout.flush(); cin >> x; return x;
}
int main() {
int n, k; cin >> n >> k;
int L = 1, R = n * n;
while(L < R) {
int mid = (L + R) / 2, cnt = 0;
// cnt counts how many aij > mid
for(int i = 1, j = n; i <= n;) {
int x = ask(i, j, mid);
if(!x) {j--;}
else {cnt += n - j; i++;}
}
if(cnt < k) R = mid;
else L = mid + 1;
}
cout << "! " << L << '\n';
return 0;
}
D. ICPC
谭老师很快就出了思路,但好像是有些不完全,我参与讨论了一会儿之后谭老师完善了思路。样例解释非常好,调了一会儿就过了。赛后写复盘的时候我发现我不会做这题了,这也是这篇记录2025年才发出的重要原因。
在长桌上有 n n n 个座位排成一排,其中从左至右的第 i i i 个座位上放有一道份量为 a i a_i ai 的菜品。幽幽子初始位于第 s s s 个座位,并且在每秒结束时能够移动至任意一个与当前座位相邻的座位(她也可以待在原座位不动)。任何幽幽子到达过的座位上的菜品都会被她吃掉。对于所有满足 1 ≤ s ≤ n , 1 ≤ t ≤ 2 n 1 \le s \le n, 1 \le t \le 2n 1≤s≤n,1≤t≤2n 的正整数 s , t s, t s,t,求出幽幽子从第 s s s 个座位出发移动 t t t 秒时能吃掉的菜品的份量总和的最大值。 1 ≤ n ≤ 5000 1 \le n \le 5000 1≤n≤5000 。
需要求的矩阵的大小为 2 n 2 2n^2 2n2 ,也就是说只能 O ( s i z e ) O(size) O(size) 地求出这个矩阵。
先假设最后一步幽幽子是向右移动的,令 d i , j d_{i, j} di,j 代表从位置 i i i 出发走最多 j j j 步且最后一步是向右移动的条件下可以获得的最大价值。显然中途改变方向超过一次是不优的,那么总共只有两种情况:先向左再向右,或者一直向右。一直向右的情况可以用前缀和 O ( 1 ) O(1) O(1) 求出。如果先向左 k k k 步再向右 j − k j - k j−k 步,那么实际上这和从 i − k i - k i−k 点出发向右走 j − k j - k j−k 步没有区别。但是需要注意一点,上述 k k k 不可大于 j 2 \frac{j}{2} 2j,否则从 i − k i - k i−k 点出发向右走 j − k j - k j−k 步吃到的食物是先向左 k k k 步再向右 j − k j - k j−k 步吃到食物的子集,即,会让答案偏小。但是你会发现,这种情况会在假设最后一步向左移动时更好地覆盖到。所以,可以大胆地写下 d i , j = max ( d i − 1 , j − 1 , ∑ t = i i + j a t ) d_{i, j} = \max(d_{i - 1, j - 1}, \sum_{t = i}^{i + j}a_t) di,j=max(di−1,j−1,∑t=ii+jat) 的转移式。这里有一个前缀和一样的东西,可能成为 d i , j d_{i, j} di,j 答案的段的集合是 S i , j S_{i, j} Si,j 实际是 S i − 1 , j − 1 ∪ { [ i , i + j ] } S_{i - 1, j - 1} \cup \{[i, i + j]\} Si−1,j−1∪{[i,i+j]},因此可以 O ( 1 ) O(1) O(1) 递推。
然后假设最后一步幽幽子是向左移动的,转移式为 d i , j = max ( d i + 1 , j − 1 , ∑ t = i − j i a t ) d_{i, j} = \max(d_{i + 1, j - 1}, \sum_{t = i - j}^{i}a_t) di,j=max(di+1,j−1,∑t=i−jiat) 。
上述转移式需要额外定义 a i = 0 a_i = 0 ai=0 当 i < 1 i < 1 i<1 或 i > n i > n i>n 时,这可以在前缀和查询中处理。
#include<bits/stdc++.h>
using namespace std;
typedef long long i64;
const int N = 5050;
int a[N], n;
i64 p[N], f1[N][N << 1], f2[N][N << 1];
inline i64 query(int L, int R) {
if(R > n) R = n; if(L < 1) L = 1;
return p[R] - p[L - 1];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i];
p[i] = p[i - 1] + a[i];
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= 2 * n; j++) {
f1[i][j] = max(f1[i - 1][j - 1], query(i, i + j));
}
}
for(int i = n; i >= 1; i--) {
for(int j = 1; j <= 2 * n; j++) {
f2[i][j] = max(f2[i + 1][j - 1], query(i - j, i));
}
}
i64 ans = 0;
for(int i = 1; i <= n; i++) {
i64 temp = 0;
for(int j = 1; j <= 2 * n; j++) {
temp ^= (j * max(f1[i][j], f2[i][j]));
}
ans ^= (i + temp);
}
cout << ans << endl;
return 0;
}
E. Boomerang
最后两个半小时,双开 E 题和 M 题,双双遗憾落败。E 题思考了很久,谭老师提出了一个想法:在一棵树上加上一个点之后,树的直径要么不变,要么就是新点和原来直径的一个端点。赵老师尝试证明了这个结论,于是开写。事实上这个结论是对的,见本文开头。中途写挂了一些东西,debug 很久才发现。最后一个 bug 是 4 小时 58 分发现的爆 int,谭老师改了一下,但是因为服务器崩溃未能交上,痛失金牌,万分遗憾。
在本题中,真相能够反驳谣言的充分必要条件是,在某一时刻 t t t,真相树的直径开始大于等于谣言树的直径。这样,你就可以选定辟谣开始点 r ′ r' r′ 为 t t t 时刻的谣言树的直径中点。即,只要求出每一时刻谣言树的直径即可。每一时刻的谣言树的直径不会减少,只会增加,因此可以二分/双指针求出从 t 0 t_0 t0 开始辟谣的最短时间。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
const int N = 2e5 + 10;
vector<int> edge[N], add[N];
i64 r, t0, n, d[N * 2], dep[N], fa[N][20];
void dfs(int u, int f) {
dep[u] = dep[f] + 1;
add[dep[u]].push_back(u);
fa[u][0] = f;
for(int i = 1; i < 20; i++) {
fa[u][i] = fa[fa[u][i - 1]][i - 1];
}
for(int x : edge[u]) {
if(x != f) {
dfs(x, u);
}
}
}
int lca(int u, int v) {
if(dep[u] > dep[v]) swap(u, v);
for(int i = 0, t = dep[v] - dep[u]; i < 20; i++) {
if((1 << i) & t) v = fa[v][i];
}
if(u == v) return u;
for(int i = 19; i >= 0; i--) {
if(fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}
i64 dis(int u, int v) {
return dep[u] + dep[v] - 2 * dep[lca(u, v)];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
cin >> r >> t0;
dep[0] = -1; dfs(r, 0);
pii p = {r, r}; i64 nowd = 0;
for(int i = 1; i <= n; i++) {
for(int x : add[i]) {
i64 w1 = dis(x, p.first);
i64 w2 = dis(x, p.second);
if(w1 > nowd) {
p = {x, p.first};
nowd = w1;
}
if(w2 > nowd) {
p = {x, p.second};
nowd = w2;
}
}
d[i] = nowd;
}
for(int i = n + 1; i <= 2 * n; i++) {
d[i] = d[n];
}
for(i64 k = 1, ans = 2 * n; k <= n; k++) {
while(2LL * k * (ans - t0 - 1) >= d[ans - 1]) ans--;
cout << ans << ' ';
}
return 0;
}
M. Merge
因为只能将两个相差 1 1 1 的数字合并,因此偶数一定不可能合成,非 1 1 1 的奇数可以合成。一个奇数可以拆成一个奇数和一个偶数,奇数可以再拆分。但因为只有一个可以递归拆分,因此只会递归 O ( log x ) O(\log x) O(logx) 次数。可以发现,由于拆分约等于除以 2 2 2,一个数字拆分到底后,其组成部分几乎各不相同,除了一个例外: 5 5 5 可以拆分成 2 + 2 + 1 2 + 2 + 1 2+2+1,因此在尝试递归拆分时,对于这种情况需要特殊判定。赛场上我们一直没过这题就是这个原因。
但这个问题其实比较好发现,只要测试一下一个 1 1 1 和一个 2 2 2 就可以测试出程序的问题。另外就是第三个样例也算是一个提示。我们看见是 TLE 判定就一直在测试大数据,没有想到是递归出了问题导致死循环。总之,赛场上不能急,大数据和简单数据都测一测才对。
下面的程序中,我为了保险, x = 1 , 3 , 5 x = 1, 3, 5 x=1,3,5 的情况都特判了一下。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define all(x) (x).begin(),(x).end()
typedef long long i64;
typedef pair<int, int> pii;
unordered_map<i64, int> mp;
#define mycount(x) (mp.count(x) && mp[x] != 0)
i64 a[200005];
vector<i64> ans;
bool find(i64 x) {
if(x & 1) {
if(x == 5) {
if(mp[5]) return true;
else if(mp[3] && mp[2]) return true;
else if(mp[1] && mp[2] > 1) return true;
return false;
} else if(x == 3) {
if(mp[3]) return true;
else if(mp[2] && mp[1]) return true;
return false;
} else if(x == 1) {
return (mp[1] != 0);
}
if(mycount(x)) return true;
i64 t1 = x / 2, t2 = x / 2 + 1;
return find(t1) && find(t2);
} else {
return mycount(x);
}
}
void erase(i64 x) {
if(x & 1) {
if(x == 5) {
if(mp[5]) mp[5]--;
else if(mp[3] && mp[2]) mp[3]--, mp[2]--;
else if(mp[1] && mp[2] > 1) mp[2] -= 2, mp[1]--;
return;
} else if(x == 3) {
if(mp[3]) mp[3]--;
else if(mp[2] && mp[1]) mp[2]--, mp[1]--;
return;
} else if(x == 1) {
mp[1]--;
return;
}
if(mycount(x)) {
mp[x]--;
return;
}
i64 t1 = x / 2, t2 = x / 2 + 1;
erase(t1); erase(t2);
return;
} else {
mp[x]--;
return;
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int n; cin >> n;
for(int i = 0; i < n; i++) {
cin >> a[i];
mp[a[i]]++;
}
sort(a, a + n, greater<i64>());
for(int i = 0; i < n; i++) {
if(!mycount(a[i])) continue;
else if(find(a[i] * 2 + 1)) {
ans.push_back(a[i] * 2 + 1);
erase(a[i] * 2 + 1);
} else if(find(a[i] * 2 - 1)) {
ans.push_back(a[i] * 2 - 1);
erase(a[i] * 2 - 1);
} else {
ans.push_back(a[i]);
erase(a[i]);
}
}
cout << ans.size() << endl;
for(i64 x : ans) cout << x << ' ';
return 0;
}
4524

被折叠的 条评论
为什么被折叠?



