1 条题解

  • 0
    @ 2026-4-2 23:49:13

    题目题解

    问题理解

    给定字符串 ssnn 个人,第 ii 个人的理解区间为 [li,ri][l_i, r_i],要求 ss"docker" 作为连续子串的出现次数在该区间内。
    可以修改 ss 中的字符,求最少修改次数,使得最多的人能理解报告。


    第一步:最大可满足人数

    t="docker"t = \text{"docker"},长度 66,且字符互不相同,因此 tt 的出现不会重叠。
    m=s/6m = \lfloor |s|/6 \rfloor 为理论最大出现次数。

    对于每个人,将其区间与 [0,m][0, m] 取交,得到实际可接受的次数范围。
    统计每个次数 cc 有多少人能接受,得到数组 dx[c]dx[c]
    mx=maxdx[c]mx = \max dx[c] 为最大可满足人数。

    我们的目标是:使最终出现次数 kk 满足 dx[k]=mxdx[k] = mx,并最小化修改次数。


    第二步:分类讨论

    设原字符串中 tt 的出现次数为 curcur

    • kcurk \le cur:只需删除一些已有的 tt(通过破坏它们),每个删除需修改 66 个字符,但我们可以选择只修改部分字符使其不再是 tt。实际上,每个已有 tt 可通过修改 11 个字符破坏(因为 tt 的字符互不相同,改一个即可)。因此,从 curcur 降到 kk 需要修改 curkcur - k 个字符。

    • k>curk > cur:需要新增一些 tt,这需要修改某些位置使其变成 tt。这是一个最小代价问题。


    第三步:新增 tt 的最小代价

    dif[i]dif[i] 表示将 s[i..i+5]s[i..i+5] 变成 tt 所需的最少修改次数(即不同字符个数)。
    我们想选择 kcurk - cur 个互不重叠的长度为 66 的区间,使得它们的 difdif 之和最小。

    这是一个经典的选择 xx 个互不重叠区间的最小代价问题,可用 DP 解决。

    定义 dp[i][j]dp[i][j] 表示考虑前 ii 个字符,选择了 jjtt 的最小修改次数。
    转移:

    • 不选以 ii 结尾的区间:dp[i][j]=dp[i1][j]dp[i][j] = dp[i-1][j]
    • 选以 ii 结尾的区间(要求 i6i \ge 6):dp[i][j]=min(dp[i][j],dp[i6][j1]+dif[i5])dp[i][j] = \min(dp[i][j], dp[i-6][j-1] + dif[i-5])

    直接做是 O(nm)O(n \cdot m),不可行。


    第四步:凸优化(Aliens Trick)

    观察 g(x)=dp[n][x]g(x) = dp[n][x],即选择 xx 个区间的最小代价。
    可以证明 g(x)g(x) 是凸函数(差分非递减)。
    因此可用二分斜率 dd,将 DP 转化为:

    dp[i]=min(dp[i1],dp[i6]+dif[i5]+d)dp'[i] = \min(dp'[i-1], dp'[i-6] + dif[i-5] + d)

    同时记录选择的区间个数。
    通过调整 dd,我们可以控制选择的区间数量,最终得到 g(k)g(k) 的值。

    这被称为 Lagrange 乘子法Aliens Trick


    第五步:整体算法

    1. 预处理 dif[i]dif[i]curcur(原 tt 的出现次数)。
    2. 统计 dx[c]dx[c],找到最大可满足人数 mxmx 及对应的 kk
    3. kcurk \le cur,答案为 curkcur - k(破坏已有 tt)。
    4. 否则,用凸优化求 g(k)g(k)(新增 kcurk - curtt 的最小代价),加上破坏已有 tt 的代价(curkcur - k 为负,即不破坏,实际是 00,因为 k>curk > cur 时不需要破坏),总代价即为 g(k)g(k)

    第六步:时间复杂度

    • 预处理:O(s)O(|s|)
    • 二分斜率:O(logC)O(\log C) 次 DP,每次 DP O(s)O(|s|)
    • 总复杂度 O(slogC)O(|s| \log C),满足 s5×105|s| \le 5\times 10^5

    代码实现

    #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
    

    与题目输出一致。


    总结

    本题的关键在于:

    1. 将问题分为“破坏已有 tt”和“新增 tt”两种情况。
    2. 新增 tt 是一个带权区间选择问题,可用凸优化(Aliens Trick)在 O(nlogC)O(n \log C) 内求解。
    • 1

    信息

    ID
    6302
    时间
    1000ms
    内存
    256MiB
    难度
    2
    标签
    递交数
    1
    已通过
    1
    上传者