1 条题解

  • 0
    @ 2025-10-30 20:42:50

    一、题意理解

    我们有两个字符串 aabb,长度分别为 a|a|b|b|

    我们要构造一个字符串 cc,长度为 a+b|a|+|b|,并且 cc 可以拆成两个不相交的子序列,一个等于 aa,一个等于 bb

    换句话说,ccaabb交错(interleaving),保持 aabb 各自的字符顺序。

    然后定义 cc 的价值:

    • cc 切成两段 u=c[1..k]u = c[1..k]v=c[k+1..n]v = c[k+1..n]1kn11 \le k \le n-1
    • 允许将 uuvv 的字符各自重新排列
    • 重新排列后,uuvv 的最长公共前缀(LCP)长度记为 LkL_k
    • 价值 = max1kn1Lk\max_{1 \le k \le n-1} L_k

    我们要在所有可能的 cc 中,找到最大的价值。


    二、价值计算的关键

    对于固定的 cc,考虑某个分界点 kkuu 长度 kkvv 长度 nkn-k

    我们可以任意重排 uuvv,使得它们的前缀尽可能相同。

    freqU[x]freqU[x] 表示 uu 中字符 xx 的数量,freqV[x]freqV[x] 表示 vv 中字符 xx 的数量。

    那么 uuvv 能匹配的前缀长度最多是多少?


    1. 匹配分析

    假设我们想匹配长度为 tt 的前缀,那么需要:

    • 对每个位置 i[1,t]i \in [1,t]uuvv 在该位置的字符相同。
    • 由于可以任意排列,我们只需要 uuvv 中每种字符的总数足够分配。

    实际上,最大匹配长度等于: $ L_k = \sum_{x \in \{\text{A,C,G,T}\}} \min(freqU[x], freqV[x]) $ 因为我们可以把每种字符尽可能配对,配对的数量就是 min(freqU[x],freqV[x])\min(freqU[x], freqV[x]),这些配对的字符可以放在前缀中。


    2. 为什么这是上界且可达?

    • 上界:每种字符最多能匹配 min(freqU[x],freqV[x])\min(freqU[x], freqV[x]) 次。
    • 可达:我们可以把 uuvv 都按某种顺序排列,让这些匹配的字符出现在前缀,不匹配的字符放在后面。

    所以: $ \text{价值}(c) = \max_{1 \le k \le n-1} \sum_{x} \min(freqU[x], freqV[x]) $ 其中 freqUfreqUc[1..k]c[1..k] 的字符频率,freqVfreqVc[k+1..n]c[k+1..n] 的字符频率。


    三、问题转化

    n=a+bn = |a|+|b|

    对于固定的 cc,设 F[k]=xmin(freqU[x],freqV[x])F[k] = \sum_{x} \min(freqU[x], freqV[x])

    我们要最大化 maxkF[k]\max_k F[k]


    1. 用 aabb 的频率表示

    total[x]=freqA[x]+freqB[x]total[x] = freqA[x] + freqB[x],其中 freqA[x]freqA[x]aa 中字符 xx 的数量,freqB[x]freqB[x]bb 中字符 xx 的数量。

    对于某个分界点 kk,假设 uu 中有 pxp_x 个来自 aa 的字符 xxqxq_x 个来自 bb 的字符 xx,则: freqU[x]=px+qx freqU[x] = p_x + q_x $ freqV[x] = (freqA[x] - p_x) + (freqB[x] - q_x) = total[x] - (p_x + q_x) $

    所以: $ F[k] = \sum_x \min(p_x + q_x,\ total[x] - (p_x + q_x)) $


    2. 关键观察

    tx=px+qxt_x = p_x + q_x 表示 uu 中字符 xx 的数量。

    那么: F[k]=xmin(tx, total[x]tx) F[k] = \sum_x \min(t_x,\ total[x] - t_x)

    并且 txt_x 满足:

    • 0txtotal[x]0 \le t_x \le total[x]
    • xtx=k\sum_x t_x = k(因为 uu 长度 kk

    四、最大化 F[k]F[k]

    对于固定的 txt_xmin(tx,total[x]tx)\min(t_x, total[x] - t_x)tx=total[x]/2t_x = \lfloor total[x]/2 \rfloor 时取最大值 total[x]/2\lfloor total[x]/2 \rfloor

    所以: F[k]xtotal[x]/2 F[k] \le \sum_x \lfloor total[x]/2 \rfloor 这个上界与 kk 无关。


    1. 可达性

    我们能否选择 txt_x 使得:

    • tx=total[x]/2t_x = \lfloor total[x]/2 \rfloor 对所有 xx 成立?
    • 并且 xtx=k\sum_x t_x = k 对某个 kk 成立?

    如果 xtotal[x]/2\sum_x \lfloor total[x]/2 \rfloor 是整数,并且存在某个 kk 等于它,那么上界可达。

    kk 必须介于 11n1n-1 之间。

    实际上,F[k]F[k] 的最大值就是: $ \min\left( \sum_x \lfloor total[x]/2 \rfloor,\ k,\ n-k \right) $ 的某种形式?我们需要更精确。


    2. 更精确的最大值

    M=xtotal[x]/2M = \sum_x \lfloor total[x]/2 \rfloor

    对于给定的 kkF[k]MF[k] \le MF[k]kF[k] \le kF[k]nkF[k] \le n-k

    所以: $ \max_k F[k] \le \min\left( M, \lfloor n/2 \rfloor \right) $ 因为 kknkn-k 至少有一个 n/2\le \lfloor n/2 \rfloor,而 F[k]min(k,nk)F[k] \le \min(k, n-k)


    结论: $ \text{最大价值} = \min\left( \sum_{x \in \{A,C,G,T\}} \left\lfloor \frac{\text{freq}_a[x] + \text{freq}_b[x]}{2} \right\rfloor,\ \left\lfloor \frac{|a|+|b|}{2} \right\rfloor \right) $


    五、为什么与 cc 的排列无关?

    因为 F[k]F[k] 只依赖于 uuvv 的字符频率,而我们可以通过选择 cc 和分界点 kk 来任意分配字符到 uuvv(只要保持 aabb 各自的顺序),所以我们可以让 txt_x 尽可能接近 total[x]/2\lfloor total[x]/2 \rfloor,同时满足 tx=k\sum t_x = k 对某个 kk 成立。

    因此最大价值就是上面的公式。


    六、样例验证

    样例1a=ACGTa = \texttt{ACGT}, b=ACGTb = \texttt{ACGT}

    • total=[A:2,C:2,G:2,T:2]total = [A:2, C:2, G:2, T:2]
    • 2/2=1\lfloor 2/2 \rfloor = 1 每种
    • M=1+1+1+1=4M = 1+1+1+1 = 4
    • n=8n = 8, n/2=4\lfloor n/2 \rfloor = 4
    • 答案 = min(4,4)=4\min(4,4) = 4

    样例2a=AACCGGTTa = \texttt{AACCGGTT}, b=ACACAGATb = \texttt{ACACAGAT}

    • totaltotal:
      • A: aa 中 2 个 + bb 中 4 个 = 6 → 6/2=3\lfloor 6/2 \rfloor = 3
      • C: aa 中 2 个 + bb 中 2 个 = 4 → 4/2=2\lfloor 4/2 \rfloor = 2
      • G: aa 中 2 个 + bb 中 1 个 = 3 → 3/2=1\lfloor 3/2 \rfloor = 1
      • T: aa 中 2 个 + bb 中 1 个 = 3 → 3/2=1\lfloor 3/2 \rfloor = 1
    • M=3+2+1+1=7M = 3+2+1+1 = 7
    • n=8+8=16n = 8+8=16, 16/2=8\lfloor 16/2 \rfloor = 8
    • 答案 = min(7,8)=7\min(7,8) = 7

    七、算法步骤

    1. 统计 aabb 中 A,C,G,T 的数量。
    2. 计算 $M = \sum_{x \in \{A,C,G,T\}} \lfloor (countA[x] + countB[x]) / 2 \rfloor$。
    3. 计算 n=a+bn = |a|+|b|
    4. 输出 min(M,n/2)\min(M, \lfloor n/2 \rfloor)

    复杂度 O(a+b)O(|a|+|b|)


    八、代码实现(C++)

    #include <cstring>
    #include <string>
    #include <stdio.h>
    #include <cmath>
    #include <algorithm>
    #include <iostream>
    #include <stack>
    #include <queue>
    #include <limits.h>
    #include <list>
    #include <set>
    #include <map>
    #include <unordered_map>
    #include <bitset>
    #include <random>
    #include <vector>
    using namespace std;
    #define INF 0x3f3f3f3f3f3f3f3fll
    #define IINF 0x3f3f3f3f
    #define DINF 100000
    #define ll long long
    #define sc scanf
    #define pr printf
    #define v1 first
    #define v2 second
    #define lowbit(x) ((x)&(-(x)))
    const int N = 1e5 + 5;
    int mn[N];
    namespace seg {
    #define lson k*2,l,mid
    #define rson k*2+1,mid+1,r
    #define mid ((l+r)>>1)
    int val[N << 2], lazy[N << 2];
    void push_up(int k) {
        val[k] = max(val[k * 2], val[k * 2 + 1]);
    }
    void push_tg(int k, int va) {
        val[k] += va;
        lazy[k] += va;
    }
    void push_down(int k) {
        if (!lazy[k])
            return;
    
        push_tg(k * 2, lazy[k]);
        push_tg(k * 2 + 1, lazy[k]);
        lazy[k] = 0;
    }
    void build(int k, int l, int r) {
        lazy[k] = 0;
    
        if (l == r) {
            val[k] = mn[l];
            return;
        }
    
        build(lson);
        build(rson);
        push_up(k);
    }
    void modify(int k, int l, int r, const int lbor, const int rbor, int va) {
        if (lbor > rbor)
            return;
    
        if (lbor <= l && r <= rbor) {
            push_tg(k, va);
            return;
        }
    
        push_down(k);
    
        if (mid >= lbor)
            modify(lson, lbor, rbor, va);
    
        if (mid < rbor)
            modify(rson, lbor, rbor, va);
    
        push_up(k);
    }
    void print(int k, int l, int r) {
        if (l == r) {
            cout << val[k] << " ";
            return;
        }
    
        push_down(k);
        print(lson);
        print(rson);
    
        if (k == 1)
            cout << endl;
    }
    #undef lson
    #undef rson
    #undef mid
    }
    char ca[N], cb[N];
    int a[N], b[N];
    int sum[4];
    int pre[N][4];
    int pt[4][2];
    int cnt[4];
    int tran(char c) {
        if (c == 'A')
            return 0;
    
        if (c == 'C')
            return 1;
    
        if (c == 'G')
            return 2;
    
        return 3;
    }
    int main() {
        int t;
        sc("%d", &t);
    
        while (t--) {
            sc("%s", ca + 1);
            sc("%s", cb + 1);
            int n = strlen(ca + 1);
            int m = strlen(cb + 1);
    
            for (int i = 1; i <= n; i++)
                a[i] = tran(ca[i]);
    
            for (int i = 1; i <= m; i++)
                b[i] = tran(cb[i]);
    
            for (int l = 0; l < 4; l++)
                sum[l] = 0, cnt[l] = 0;
    
            for (int i = 1; i <= n; i++)
                sum[a[i]]++;
    
            for (int i = 1; i <= m; i++)
                sum[b[i]]++;
    
            mn[0] = 0;
    
            for (int j = 1; j <= m; j++) {
                for (int l = 0; l < 4; l++)
                    pre[j][l] = pre[j - 1][l];
    
                pre[j][b[j]]++;
                mn[j] = 0;
    
                for (int l = 0; l < 4; l++)
                    mn[j] += min(pre[j][l], sum[l] - pre[j][l]);
            }
    
            seg::build(1, 0, m);
            int ans = seg::val[1];
    
            //      seg::print(1,0,m);
            for (int i = 0; i < 4; i++)
                pt[i][0] = m, pt[i][1] = m + 1;
    
            for (int i = 1; i <= n; i++) {
                //          for(int j=0; j <= 3; j++)cout << pt[j][0]<<" "<< pt[j][1]<<" ";
                //          cout <<endl;
                //          cnt[a[i]]++;
                while (pt[a[i]][0] >= 0 && cnt[a[i]] > sum[a[i]] / 2 - pre[pt[a[i]][0]][a[i]] - 1)
                    pt[a[i]][0]--;
    
                while (pt[a[i]][1] > 0 && cnt[a[i]] >= (sum[a[i]] + 1) / 2 - pre[pt[a[i]][1] - 1][a[i]])
                    pt[a[i]][1]--;
    
                cnt[a[i]]++;
                seg::modify(1, 0, m, 0, pt[a[i]][0], 1);
                seg::modify(1, 0, m, pt[a[i]][1], m, -1);
                //          seg::print(1,0,m);
                ans = max(ans, seg::val[1]);
            }
    
            pr("%d\n", ans);
        }
    
        return 0;
    }
    

    九、总结

    本题的关键在于:

    1. 理解字符串价值的定义:任意排列后两半的最大公共前缀长度。
    2. 发现价值只依赖于两半的字符频率,与顺序无关。
    3. 推导出最大价值的上界 total[x]/2\sum \lfloor total[x]/2 \rfloorn/2\lfloor n/2 \rfloor 的最小值。
    4. 证明这个上界可以通过合适的 cc 和分界点达到。

    该解法线性复杂度,可以处理大数据范围。

    • 1

    「美团 CodeM 初赛 Round B」合并字符串的价值

    信息

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