The "Winning" Gene (USACO Silver 2024 Open)
这个题和 25JAN 的 Cow checkups 类似,也是基于位置计算贡献值来降低复杂度的思路,同时还需要想到单调栈和 LCP 的技巧,所以难度比较大,是个蓝题。
首先估计暴力算法的复杂度。循环所有的 两个 ,然后扫描所有位置生成所有子串再加两个 ,得到子串后与前面的子串比较得到最小值再加一个 ,所以暴力算法是 的。
实际上 ,我们需要 或者 的算法。
首先字符串的比较可以使用 LCP 优化(longest common prefix),即 表示 和 的最长公共前缀长度。这样我们比较两个子串的时候就可以直接比较 和 ,或者当 时就返回相等。这样可以降低一个 的复杂度。 的计算如果直接暴力进行,是 的,可以通过 8 之外的所有测试点。要通过所有测试点,可以用以下优化:。另外,标准的 LCP 算法 是 RMQ。
下面我们通过分析贡献值来降低复杂度。我们考虑对一个开始于位置i的长度为L的子串s,我们可以计算出这个子串左侧第一个小于等于s的子串s1开始位置,以及右侧第一个小于s的子串s2开始位置。如下图所示:
----s1----
| -----s-----
| | ----s2----
| | |
lt[i] i rt[i]
我们可以看到,当整体 长的字符串范围在 到 之间的时候,子串 就会被选成结果。也就是说,当 最大为 ,最小为 (子串长度)的时候,这个子串就会对 产生贡献。
/ 的计算使用单调栈进行,从左往右扫描计算 ,如果栈顶子串大于 (使用上面的lcp快速比较),则 pop 掉栈顶,然后栈顶就是 。 的计算类似,从右往左扫描。
这里面我们可以看到一个优化,就是因为结果要求的是 的数量,而 是在结果子串的位置维度上进行累加,并不关心 长区域的起止位置,而上面的计算刚好给出的就是 的最小和最大值,也是和区域起止位置无关。所以我们就不需要进行起始位置的循环了,这里省掉一个 。
这样我们就得到一个算法:定义 为 下的子串位置数量, 基于 进行差分累加计算得到,则最终结果就是 。
#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;
}