1 条题解
-
0
题目题解
问题理解
给定字符串 和 个人,第 个人的理解区间为 ,要求 中
"docker"作为连续子串的出现次数在该区间内。
可以修改 中的字符,求最少修改次数,使得最多的人能理解报告。
第一步:最大可满足人数
设 ,长度 ,且字符互不相同,因此 的出现不会重叠。
设 为理论最大出现次数。对于每个人,将其区间与 取交,得到实际可接受的次数范围。
统计每个次数 有多少人能接受,得到数组 。
设 为最大可满足人数。我们的目标是:使最终出现次数 满足 ,并最小化修改次数。
第二步:分类讨论
设原字符串中 的出现次数为 。
-
若 :只需删除一些已有的 (通过破坏它们),每个删除需修改 个字符,但我们可以选择只修改部分字符使其不再是 。实际上,每个已有 可通过修改 个字符破坏(因为 的字符互不相同,改一个即可)。因此,从 降到 需要修改 个字符。
-
若 :需要新增一些 ,这需要修改某些位置使其变成 。这是一个最小代价问题。
第三步:新增 的最小代价
设 表示将 变成 所需的最少修改次数(即不同字符个数)。
我们想选择 个互不重叠的长度为 的区间,使得它们的 之和最小。这是一个经典的选择 个互不重叠区间的最小代价问题,可用 DP 解决。
定义 表示考虑前 个字符,选择了 个 的最小修改次数。
转移:- 不选以 结尾的区间:
- 选以 结尾的区间(要求 ):
直接做是 ,不可行。
第四步:凸优化(Aliens Trick)
观察 ,即选择 个区间的最小代价。
可以证明 是凸函数(差分非递减)。
因此可用二分斜率 ,将 DP 转化为:同时记录选择的区间个数。
通过调整 ,我们可以控制选择的区间数量,最终得到 的值。这被称为 Lagrange 乘子法 或 Aliens Trick。
第五步:整体算法
- 预处理 和 (原 的出现次数)。
- 统计 ,找到最大可满足人数 及对应的 。
- 若 ,答案为 (破坏已有 )。
- 否则,用凸优化求 (新增 个 的最小代价),加上破坏已有 的代价( 为负,即不破坏,实际是 ,因为 时不需要破坏),总代价即为 。
第六步:时间复杂度
- 预处理:。
- 二分斜率: 次 DP,每次 DP 。
- 总复杂度 ,满足 。
代码实现
#include <bits/stdc++.h> using namespace std; const int INF = 1e8; const string T = "docker"; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { string s; cin >> s; int n = s.size(); int mx = n / 6; // 最大可能出现次数 int m; cin >> m; vector<int> dx(mx + 2, 0); for (int i = 0; i < m; i++) { int l, r; cin >> l >> r; r = min(r, mx); if (l > r) continue; dx[l]++; dx[r + 1]--; } for (int i = 1; i <= mx + 1; i++) { dx[i] += dx[i - 1]; } // 计算 dif[i]:将 s[i..i+5] 变成 T 的最小修改次数 vector<int> dif(max(0, n - 5)); int cur = 0; for (int i = 0; i + 5 < n; i++) { int cnt = 0; for (int j = 0; j < 6; j++) { cnt += (s[i + j] != T[j]); } dif[i] = cnt; if (cnt == 0) cur++; } int mxcnt = *max_element(dx.begin(), dx.end()); int k = -1; int ans = INF; // 情况1:k <= cur for (int i = 0; i <= mx; i++) { if (dx[i] != mxcnt) continue; if (i <= cur) { ans = min(ans, cur - i); continue; } k = i; break; } if (k == -1) { cout << ans << "\n"; continue; } // 情况2:k > cur,用凸优化求 g(k) auto calc = [&](int d) { vector<pair<long long, int>> dp(n + 1, {1e18, -1}); dp[0] = {0, 0}; for (int i = 0; i < n; i++) { // 不选以 i 结尾的区间 dp[i + 1] = min(dp[i + 1], dp[i]); // 选以 i 结尾的区间 if (i + 6 <= n) { auto nd = dp[i]; nd.first += dif[i] + d; nd.second--; dp[i + 6] = min(dp[i + 6], nd); } } return dp[n]; }; int l = -INF, r = 0, sv = INF; while (l <= r) { int mid = (l + r) / 2; auto [val, cnt] = calc(mid); if (-cnt >= k) { sv = val - 1LL * k * mid; l = mid + 1; } else { r = mid - 1; } } ans = min(ans, sv); cout << ans << "\n"; } return 0; }
验证样例
输入:
2 dockerdockerxxxxxx 3 3 3 2 4 1 5 ljglsjfkdieufj 5 1 5 3 3 2 4 3 7 2 9输出:
6 11与题目输出一致。
总结
本题的关键在于:
- 将问题分为“破坏已有 ”和“新增 ”两种情况。
- 新增 是一个带权区间选择问题,可用凸优化(Aliens Trick)在 内求解。
-
- 1
信息
- ID
- 6302
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者