01-CF2053-Solution
【思维场!】CF2053解析
今天带来的是 CF2053:Good Bye 2024: 2025 is NEAR 前六题的解析,每道题配备了图片留白,你能坚持到第几关呢?
时间限制均为 2s ,内存限制均为 512 MB 。
A. Tender Carpenter
【题目描述】
定义一个集合 是“好的”,当且仅当对于 中任意三个数 (可以相同),存在三边长为 的非退化三角形。
给出一个数组 ,要求把 分成若干个非空连续子段,使得:对于每个子段,包含其中所有元素的集合是“好的” 。
询问是否存在两种不同的分割方式。(YES or NO)
【数据规模与约定】
【图片留白】
【个人解析】
第一种方式:每个元素单独一段,一共 段。
第二种方式:在 段的基础上,合并两个相邻的段,那就需要找到一组下标 ,满足 .
附上个人 AC 代码:
#include <bits/stdc++.h> using namespace std; void solve () { int n; cin >> n; vector<int> a(n, 0); for (int i = 0; i < n; i++) cin >> a[i]; int f = 0; for (int i = 0; i + 1 < n; i++) { int u = max(a[i], a[i + 1]); int v = min(a[i], a[i + 1]); if (v * 2 > u) f = 1; } if (f) cout << "YES\n"; else cout << "NO\n"; } int main () { int _; cin >> _; while (_--) solve(); return 0; }
B. Outstanding Impressionist
【题目描述】
对于一个长度为 的数组 ,我们只知道对于每个 , 满足 的限制 。 会给出。
称下标 是唯一的,当且仅当可以构造出一个满足限制的 ,使得对所有 , 成立。
请你判断每个 是否唯一。
【数据规模与约定】
【图片留白】
【个人解析】
记每个下标 的选择数量是 区间的长度。
下标 可以分为两类:
-
:只有一个选择。
-
:有多个选择。
需要先想到,如果所有下标 都是第二类,每个 都是唯一的。因为:
- 对于当前的 ,选中区间内一个点,那么对于剩下的 ,选择数减一。但是它们本来就有多个选择,所以一定有得选。
然后推广到一般情况,分别考虑两类下标的策略。
为了方便描述,我们记一个第一类下标 ,占领了数轴上的 这个点。
对于第二类下标 :
- 如果 区间内的每个点都被第一类下标占领了,那么无论 选哪个,一定会让某个第一类下标没得选,因此 不独立。
- 否则, 选择一个没有被任何第一类下标占领的点即可。所有第一类下标都有得选,剩下的只有第二类下标。
对于第一类下标 : ,它的选择唯一,对于剩下的 ,选择数减一。其中:
-
对于第二类下标,本来就有多个选择,不构成影响;
-
对于 的第一类下标,本来唯一的选择就和 不同,不构成影响。
-
对于 的第一类下标,选择数变为零。这种情况一旦发生,就说明 不是唯一的!
我们在代码中讨论清楚上面的情况即可。
可以用前缀和等技巧优化时间复杂度到 。
#include <bits/stdc++.h> using namespace std; void solve () { int n; cin >> n; vector<pair<int, int> > a(n); vector<int> b(2 * n + 1, 0); for (int i = 0; i < n; i++) cin >> a[i].first >> a[i].second; map<int, int> mp; for (int i = 0; i < n; i++) { if (a[i].first != a[i].second) continue; mp[a[i].first]++; } for (int i = 1; i <= 2 * n; i++) if (!mp[i]) b[i] = 1; for (int i = 1; i <= 2 * n; i++) b[i] += b[i - 1]; for (int i = 0; i < n; i++) { auto [l, r] = a[i]; if (l == r) { if (mp[l] == 1) cout << "1"; else cout << "0"; continue; } if (b[r] - b[l - 1]) cout << "1"; else cout << "0"; } cout << "\n"; } int main () { int _; cin >> _; while (_--) solve(); return 0; }
C. Bewitching Stargazer
【题目描述】
天空中有 颗星星,排成一排。有一架望远镜,它用来观察星星。
最初,观测到的星星位于 段,它的幸运值为 。
希望在它观测到的每个星段 中寻找位于中间位置的恒星。因此,使用了下面的递归程序:
-
首先,它将计算 .
-
如果星段的长度(即 )是偶数,将把它分成两个同样长的星段 和 ,以便进一步观测 .
-
否则,将把望远镜瞄准 星,幸运值增加 ;随后,如果
,将继续观测星段 和 .
随着观察的进行,不会继续观察任何长度严格小于 的线段 。在这种情况下,请预测它的最终幸运值。
【数据规模与约定】
【图片留白】
【个人解析】
做法是显然的,暴力递归模拟题意即可。大致代码如下:
void dfs(int l, int r) { int mid = (l + r) / 2; if ((r - l + 1) % 2 == 0) { dfs(l, mid); dfs(mid + 1, r); } else { ans += mid; dfs(l, mid - 1); dfs(mid + 1, r); } }
需要想到,每次往右边的递归,是没有必要的。
设当前区间 ,中点为 。不妨设区间长度为偶数。
-
往左区间 递归,和往右区间 递归,幸运值的增加次数,是一样的,因为它们在数轴上的长度相等,每次取中点,分割,取中点,分割 ... 是完全相同的两个过程。
-
唯一的区别在于,往右区间,幸运值每次增加,要多加 。
-
因此,让程序往左区间递归计算,记录幸运值的增加总量 ,和增加次数 。
-
右区间的整体答案可以被描述为 。
-
区间 的幸运值增加次数可以被描述为 。
-
记录下这个答案,返回上一层函数即可。
整体时间复杂度
具体看代码。
#include <bits/stdc++.h> using namespace std; int n, k; pair<long long, long long> dfs(int l, int r) // first: 答案 // second: 提取的数量 { if (r - l + 1 < k) return {0, 0}; long long u = 0, v = 0; int mid = (l + r) / 2; if ((r - l + 1) % 2 == 0) { auto ans = dfs(l, mid); u = ans.first; v = ans.second; u += ans.first + 1ll * mid * ans.second; v = v * 2; return {u, v}; } else { u = u + mid; v = v + 1; auto ans = dfs(l, mid - 1); u += ans.first; v += ans.second; u += ans.first + 1ll * mid * ans.second; v += ans.second; return {u, v}; } } void solve () { cin >> n >> k; cout << dfs(1, n).first << "\n"; } int main () { int _; cin >> _; while (_--) solve(); return 0; }
D. Refined Product Optimality
【题目描述】
- 给定两个数组 和 ,它们都包含 个整数。
- Iris关心的是通过任意重新排列数组 后, 的最大值。注意,她只关心 的最大值,并不需要实际重新排列 。
- 将有 次修改。每次修改可以用两个整数 和 表示,其中 为 或 ()。若 ,则表示 增加 ;若 ,则表示 增加 。
- Iris会向Chris询问 的最大值,共有 次:一次是在任何修改之前,然后是每次修改之后。
- 由于 可能非常大,Chris只需要计算 。
Chris很快解决了这个问题,但他太累了,睡着了。现在,除了感谢Chris,你的任务是编写程序,计算给定输入数据的答案。
【数据规模与约定】
保证
【图片留白】
【个人解析】
首先考虑如何解决不带修改的版本。
询问重新排列数组 后, 的最大值,就等价于重新排列数组 后, 的最大值。因为本质上是找一个对应关系,对每个 找到一个 和它对应,同时不能有两个 共用一个 。
这里需要猜到,对数组 各自从小都大排序, 就可以让 最大。
证明:
-
设 ,一定有: 。
-
讨论 和 这两个区间的相对位置就可以证明上面不等式的正确性。为了方便讨论,不妨设 。
具体的:
-
如果 整体在 的左侧,即 ,那么。
。
不等式成立。
-
如果 和 构成交叉但不覆盖的关系,即 ,那么:
后者显然大于前者。不等式成立。
-
如果 和 构成覆盖的关系,即 ,那么:
后者显然大于前者。不等式成立。
-
因此得证。
现在我们的问题转化为:
- 每次单点修改时,如何快速的维护 ?
如果是一般的修改,其实是没法维护的,因为要维护 排序后的配对。
需要注意到题目中,只有 这种修改操作。那就有的说了。
额外维护两个 map
,我们记为 map_a
和 map_b
。Key 类型是 int
,Value 类型是 pair<int, int>
。
map_a[u]
存储的信息是:数组 从小到大排序后,u
这个值分布的区间左右端点。(都从小到大排序了,u
一定是连续出现的)map_b[u]
存储的信息类似,只不过是对数组 存的。
额外维护两个数组 ,表示数组 从小到大排序后的状态。
思路的关键是:利用 map_a
定位到 这个值在 中的最后一次出现位置,修改那个位置。
下面讨论让 ,如何维护 值。
- 用
map_a
定位到 这个值在排序后的数组 中,分布的区间的右端点 ; - 除掉 。这里我们计划修改 中 这个值最后一次出现位置。模数意义下的除法用逆元来做。
- 让右端点减 。如果 这个值在数组 中出现的次数已经是 了,就删除
map_a
中的这个 Key 。 - . .
- 如果现在 这个值在
map_a
中,让它的左端点减一。 - 如果不存在,插入 这个键值对到
map_a
中。
对于 的操作,维护方法类似。
#include <bits/stdc++.h> using namespace std; const int mod = 998244353; long long qpow(long long a, long long b) { long long ans = 1, base = a; while (b) { if (b & 1) ans = ans * base % mod; base = base * base % mod; b /= 2; } return ans; } void solve () { int n, q; cin >> n >> q; vector<int> a(n, 0); vector<int> b(n, 0); for (int i = 0; i < n; i++) cin >> a[i]; for (int i = 0; i < n; i++) cin >> b[i]; vector<int> pa(n, 0), pb(n, 0); for (int i = 0; i < n; i++) pa[i] = a[i], pb[i] = b[i]; sort(pa.begin(), pa.end()); sort(pb.begin(), pb.end()); long long ans = 1; for (int i = 0; i < n; i++) { ans = ans * min(pa[i], pb[i]) % mod; } map<int, pair<int, int> > mpa, mpb; for (int i = 0; i < n; i++) { int j; for (j = i; j < n; j++) { if (pa[j] != pa[i]) break; } mpa[pa[i]] = {i, j - 1}; i = j - 1; } for (int i = 0; i < n; i++) { int j; for (j = i; j < n; j++) { if (pb[j] != pb[i]) break; } mpb[pb[i]] = {i, j - 1}; i = j - 1; } cout << ans << " "; while (q--) { int p, x; cin >> p >> x; x--; if (p == 1) { auto [l, r] = mpa[a[x]]; ans = ans * qpow(min(pa[r], pb[r]), mod - 2) % mod; r = r - 1; mpa[a[x]].second--; if (r < l) mpa.erase(a[x]); a[x]++; pa[r + 1]++; if (mpa.count(a[x])) { mpa[a[x]].first--; ans = ans * min(pa[r + 1], pb[r + 1]) % mod; } else { mpa[a[x]] = {r + 1, r + 1}; ans = ans * min(pa[r + 1], pb[r + 1]) % mod; } } else { auto [l, r] = mpb[b[x]]; ans = ans * qpow(min(pa[r], pb[r]), mod - 2) % mod; r = r - 1; mpb[b[x]].second--; if (r < l) mpb.erase(b[x]); b[x]++; pb[r + 1]++; if (mpb.count(b[x])) { mpb[b[x]].first--; ans = ans * min(pa[r + 1], pb[r + 1]) % mod; } else { mpb[b[x]] = {r + 1, r + 1}; ans = ans * min(pa[r + 1], pb[r + 1]) % mod; } } cout << ans << " "; } cout << "\n"; } int main () { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int _; cin >> _; while (_--) solve(); return 0; }
E. Resourceful Caterpillar Sequence
【题目描述】
有一棵由 个顶点组成的树。让一对整数 ( , )表示一条毛毛虫:它的头部位于顶点 ,尾部位于顶点 ,它支配着从 到 的简单路径上的所有顶点(包括 和 )。 的毛毛虫序列被定义为仅由简单路径上的顶点组成的序列,按照到 的距离升序排序。
诺拉和阿伦轮流移动毛毛虫,诺拉先移动。两位玩家都将使用各自的最优策略:
- 他们会让自己赢;
- 但是,如果不可能,他们就会努力阻止对方获胜(因此,游戏将以平局结束)。
轮到诺拉时,她必须选择一个与顶点 相邻的顶点 ,该顶点不受毛毛虫支配,并将其中的所有顶点向顶点 移动一条边。轮到阿伦时,他必须选择一个与顶点 相邻的顶点 ,该顶点不受毛毛虫的支配,并将其中的所有顶点向顶点 移动一条边。注意两位棋手允许的移动是不同的。
每当 是叶子 时,诺拉获胜 。每当 是叶子时,阿伦获胜。如果最初 和 都是叶子,或者在 个回合之后游戏还没有结束,那么结果就是平局。
请计算 与 和 的整数对的数目,如果毛毛虫最初是 ,那么阿伦获胜。
换句话说:假设当前的毛毛虫序列是 ,那么移动之后,新的毛毛虫序列就变成了 。这里, 是从 到 的简单路径上的下一个顶点。
在一棵树中,当且仅当一个顶点的阶数为 时,它才被称为树叶。
因此,当游戏还没有结束时,诺拉绝不会不选择顶点 。阿伦也是如此。
【数据规模与约定】
【图片留白】
【个人解析】
把树上的点分为三类:
- A类点:本身是叶子。
- B类点:本身不是叶子,但和叶子相邻。
- C类点:既不是 A 类,也不是 B 类。
容易发现 ABC 这三类点是没有交集的。
阿伦获胜的方案也可以分为下面两类来统计:
-
是 A 类点, 是 BC 类点。这里说人话就是 是叶子, 不是叶子,算下叶子的数量就可以算出方案数。
-
是 C 类点,但是 往 路径以外的任何一个点拉动毛毛虫, 都会被拉到 B 类点。那已知 拉动一步后没到叶子, 被拉到了 B 类点, 操作一次就可以赢。
这里可以枚举 ,然后在此基础上枚举和 相邻的点 ,如果 是 B 类点,那么以 为根,子树 中的 C 类点都是合法的 ,统计数量即可。
具体维护手法见代码。
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 5; vector<int> g[N]; int sz[N], fa[N], b[N]; void dfs (int u) { if (g[u].size() != 1 && !b[u]) sz[u] = 1; for (int v : g[u]) { if (v == fa[u]) continue; fa[v] = u; dfs(v); sz[u] += sz[v]; } } void solve () { int n; cin >> n; for (int i = 1; i <= n; i++) { g[i].clear(); b[i] = 0; sz[i] = 0; } for (int i = 1; i < n; i++) { int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } int cnt = 0; for (int i = 1; i <= n; i++) { if (g[i].size() == 1) { cnt++; continue; } int f = 0; for (int v : g[i]) { if (g[v].size() == 1) f = 1; } if (f) b[i] = 1; } dfs(1); long long ans = 1ll * cnt * (n - cnt); int sum = 0; for (int i = 1; i <= n; i++) { if (g[i].size() != 1 && !b[i]) sum++; } for (int i = 1; i <= n; i++) { if (g[i].size() == 1) continue; for (int v : g[i]) { if (b[v]) { if (v != fa[i]) { ans += sz[v]; } else { ans += sum - sz[i]; } } } } cout << ans << "\n"; } int main () { int _; cin >> _; while (_--) solve(); }
F. Earnest Matrix Complement
【题目描述】
Aquawave 有一个大小为 的矩阵 ,其元素只能是范围在 (含)以内的整数。在矩阵中,有些单元格已经填满了整数,而其余单元格目前还没有填满,用 表示。
您将填入 中所有未填写的位置。之后,让 成为元素 在第 行中出现的次数。Aquawave 将矩阵的美定义为
你必须找到填空后最美的 。
输出式子的最大值即可。
【数据规模与约定】
【图片留白】
【提示1】
- 如果保证每一行只有一个
-1
,你有什么填空的策略吗?
【图片留白】
【个人解析】
为了方便描述,记第 行 -1
的数量为 ,记数字 在第 行的出现次数为 。
首先需要想到,一行中的所有 -1
都填相同的数,一定是这一行的其中一个解。
证明:
- 假设第 行和第 行都已经填完了,第 行是最后处理的,那么在第 行中的一个
-1
处填数字 ,对答案的贡献是 ,和这个-1
的位置没有关系。 - 因此完全可以全部填同一个 ,使得 最大化。
然后可以推广出一个 的做法,具体如下:
-
先忽略
-1
,计算出答案的一部分,也就是这一步可以 计算。
-
设计 数组, 表示仅考虑前 行,第 行不存在,在第 行填数字 的答案。
-
基于 的 个状态,如何计算 呢?有如下转移:
-
上一行选的是 ,其中 ,有
.
其中, 这一部分,可以直接取所有 中的最大值,可以提前 计算好。
因此这一步转移的复杂度是 的。
-
上一行选的也是 ,有
这一步转移也是 的。
所有的转移都是 的,那么复杂度的瓶颈是 枚举所有的 。
的上界是 ,因此整体复杂度 。
可以写出如下代码:
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 5; void solve () { int n, m, k; cin >> n >> m >> k; int a[n + 1][m + 1] = {0}; long long dp[n + 1][k + 1] = {0}; long long c[n + 1] = {0}; long long d[n + 1][k + 1] = {0}; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> a[i][j]; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] == -1) c[i]++; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1) d[i][a[i][j]]++; long long ans = 0; for (int i = 2; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1) ans += d[i - 1][a[i][j]]; for (int i = 2; i <= n; i++) { long long Max = 0; for (int w = 1; w <= k; w++) { Max = max(Max, dp[i - 1][w] + c[i - 1] * d[i][w]); } for (int j = 1; j <= k; j++) { dp[i][j] = max(dp[i][j], Max + c[i] * d[i - 1][j]); dp[i][j] = max(dp[i][j], dp[i - 1][j] + c[i] * c[i - 1] + c[i] * d[i - 1][j] + c[i - 1] * d[i][j]); } } long long t = 0; for (int i = 1; i <= k; i++) t = max(t, dp[n][i]); cout << ans + t << "\n"; } int main () { int _; cin >> _; while (_--) solve(); }
现在设计 的做法。
观察刚刚的 dp
转移过程,可以做出如下优化:
long long Max = 0; for (int w = 1; w <= k; w++) { Max = max(Max, dp[i - 1][w] + c[i - 1] * d[i][w]); }
对于这部分求解,可以在 for
循环外定义一个 Max
,用来存储 中的状态最大值,然后只需要更新 不为 的 个状态就可以了。
long long Max = 0; for (int i = 2; i <= n; i++) { for (int j = 1; j <= m; j++) { int w = a[i][j]; if (w == -1) continue; Max = max(Max, dp[w] + c[i - 1] * d[i][w]); } ...// 记得计算当前这轮 dp 状态的时候更新 Max
dp[i][j] = max(dp[i][j], Max + c[i] * d[i - 1][j]); dp[i][j] = max(dp[i][j], dp[i - 1][j] + c[i] * c[i - 1] + c[i] * d[i - 1][j] + c[i - 1] * d[i][j]);
对于这两部分转移,可以重新描述为:
- 先让所有 加上 ,作为
那么 ,对所有 都是固定的,可以设计一个类似”懒惰标记“的变量 ,每次让 。(这里需要提前接触过线段树中的”懒惰标记“思想)
,这一部分只有在上一行出现过的 ,才需要考虑,可以 计算。
,这一部分只有在当前行出现过的 ,才需要考虑,可以 计算。
和 取较大值,可以分为两类:
-
如果 在上一行没有出现过,那等价于和 取较大值。
这也可以用一个全局懒惰标记实现。
-
如果 在上一行出现过,那 暴力枚举这样的 ,更新 状态。
下面是我的 AC 代码,其中对懒惰标记、Max
变量的更新还有很多细节。
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 5; void solve () { int n, m, k; cin >> n >> m >> k; int a[n + 1][m + 1] = {0}; long long dp[k + 1] = {0}; long long c[n + 1] = {0}; map<int, int> d[n + 1]; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) cin >> a[i][j]; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] == -1) c[i]++; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1) d[i][a[i][j]]++; long long ans = 0; for (int i = 2; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i][j] != -1 && d[i - 1].count(a[i][j])) ans += d[i - 1][a[i][j]]; long long lz = 0, lz1 = -1e18; long long Max = 0; for (int i = 2; i <= n; i++) { for (int j = 1; j <= m; j++) { int w = a[i][j]; if (w == -1) continue; dp[w] = max(dp[w], lz1); Max = max(Max, dp[w] + c[i - 1] * d[i][w]); } long long t = Max; lz += c[i] * c[i - 1]; set<int> st; for (int j = 1; j <= m; j++) { int w = a[i - 1][j]; if (w == -1) continue; if (st.count(w)) continue; dp[w] = max(dp[w], lz1); dp[w] += c[i] * d[i - 1][w]; t = max(t, dp[w]); st.insert(w); } st.clear(); for (int j = 1; j <= m; j++) { int w = a[i][j]; if (w == -1) continue; if (st.count(w)) continue; dp[w] += c[i - 1] * d[i][w]; t = max(t, dp[w]); st.insert(w); } for (int j = 1; j <= m; j++) { int w = a[i - 1][j]; if (w == -1) continue; dp[w] = max(dp[w], Max + c[i] * d[i - 1][w] - c[i] * c[i - 1]); t = max(t, dp[w]); } lz1 = max(lz1, Max - c[i] * c[i - 1]); Max = t; } long long t = 0; for (int i = 1; i <= k; i++) t = max(t, dp[i] + lz); cout << ans + t << "\n"; } int main () { int _; cin >> _; while (_--) solve(); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端