The "Winning" Gene (USACO Silver 2024 Open)

The "Winning" Gene (USACO Silver 2024 Open)

这个题和 25JAN 的 Cow checkups 类似,也是基于位置计算贡献值来降低复杂度的思路,同时还需要想到单调栈和 LCP 的技巧,所以难度比较大,是个蓝题。

首先估计暴力算法的复杂度。循环所有的 K,LK, L 两个 NN,然后扫描所有位置生成所有子串再加两个 NN,得到子串后与前面的子串比较得到最小值再加一个 NN,所以暴力算法是 O(N5)O(N^5) 的。

实际上 N=3000N=3000,我们需要 O(N2)O(N^2) 或者 O(N2logN)O(N^2\log N) 的算法。

首先字符串的比较可以使用 LCP 优化(longest common prefix),即 lcp[i][j]lcp[i][j] 表示 s[i..]s[i..]s[j..]s[j..] 的最长公共前缀长度。这样我们比较两个子串的时候就可以直接比较 s[i+lcp[i][j]]s[i+lcp[i][j]]s[j+lcp[i][j]]s[j+lcp[i][j]],或者当 lcp[i][j]Llcp[i][j] \geq L 时就返回相等。这样可以降低一个 NN 的复杂度。lcplcp 的计算如果直接暴力进行,是 O(N3)O(N^3) 的,可以通过 8 之外的所有测试点。要通过所有测试点,可以用以下优化:lcp[i][j]=s[i]==s[j] ? 1+lcp[i+1][j+1]:0lcp[i][j] = s[i] == s[j] \ ?\ 1 + lcp[i+1][j+1] : 0。另外,标准的 LCP 算法 是 RMQ。

下面我们通过分析贡献值来降低复杂度。我们考虑对一个开始于位置i的长度为L的子串s,我们可以计算出这个子串左侧第一个小于等于s的子串s1开始位置lt[i]lt[i],以及右侧第一个小于s的子串s2开始位置rt[i]rt[i]。如下图所示:

----s1----
|        -----s-----
|        |             ----s2----
|        |             |
lt[i]    i             rt[i]

我们可以看到,当整体 KK 长的字符串范围在 lt[i]+1lt[i] + 1rt[i]+L2rt[i] + L - 2 之间的时候,子串 ii 就会被选成结果。也就是说,当 KK 最大为 rt[i]+L2lt[i]rt[i] + L - 2 - lt[i],最小为 LL(子串长度)的时候,这个子串就会对 KK 产生贡献。

lt[]lt[]/rt[]rt[] 的计算使用单调栈进行,从左往右扫描计算 lt[]lt[],如果栈顶子串大于 ss(使用上面的lcp快速比较),则 pop 掉栈顶,然后栈顶就是 lt[i]lt[i]rt[]rt[] 的计算类似,从右往左扫描。

这里面我们可以看到一个优化,就是因为结果要求的是 (K,L)(K, L) 的数量,而 vv 是在结果子串的位置维度上进行累加,并不关心 KK 长区域的起止位置,而上面的计算刚好给出的就是 KK 的最小和最大值,也是和区域起止位置无关。所以我们就不需要进行起始位置的循环了,这里省掉一个 NN

这样我们就得到一个算法:定义 ans[K][L]ans[K][L](K,L)(K, L) 下的子串位置数量,ans[K][L]ans[K][L] 基于 KK 进行差分累加计算得到,则最终结果就是 res[v]={(K,L)ans[K][L]=v}res[v] = |\{(K, L) \mid ans[K][L] = v\}|

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pi;
 
int n;
string s;
int lt[3005], rt[3005];
int lcp[3005][3005];
int ans[3005][3005];
int res[3005];
 
int cmp(int i, int j, int k) {
    if (lcp[i][j] >= k) return 0;
    return s[i+lcp[i][j]] == s[j+lcp[i][j]] ? 0 :
           s[i+lcp[i][j]] < s[j+lcp[i][j]] ? -1 : 1;
}
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    cin >> s;
 
    for (int i=n-1; i>=0; i--) {
        for (int j=n-1; j>=0; j--) {
            lcp[i][j] = s[i] == s[j] ? 1+lcp[i+1][j+1] : 0;
        }
    }
 
    for (int L=1; L<=n; L++) {
        stack<int> st;
        for (int i=0; i<=n-L; i++) {  // calc lt[]
            while (st.size() && cmp(st.top(), i, L) > 0) st.pop();
            lt[i] = st.size() ? st.top() : -1;
            st.push(i);
        }
        st = stack<int>();
        for (int i=n-L; i>=0; i--) {  // calc rt[]
            while (st.size() && cmp(st.top(), i, L) >= 0) st.pop();
            rt[i] = st.size() ? st.top() : n-L+1;
            st.push(i);
        }
        for (int i=0; i<=n-L; i++) {  // differential
            ans[L][L]++;
            ans[rt[i]-lt[i]+L-1][L]--;
        }
    }
 
    for (int K=1; K<=n; K++) {
        for (int L=1; L<=n; L++) {
            ans[K][L] += ans[K-1][L];
            res[ans[K][L]]++;
        }
    }
 
    for (int i=1; i<=n; i++) {
        cout << res[i] << endl;
    }
 
    return 0;
}