CF494B Obsessive String KMP+前缀和优化DP
给一份看起来字数比较少的题解?
题意
给一个字符串 \(s\),和一个字符串 \(t\)。在 \(s\) 中取出任意多个不重叠的子串 (可以有子串没有被取出),使得每个子串都包含 \(t\) ,求方案数。
具体题意见原题面。
思路
- 由于需要每个子串中都包含 \(t\),必然需要一个字符串匹配算法,kmp 或者 hash 找出来所有的匹配位置即可,代码中用
vector<int> pos
来记录了所有位置。
- 求方案数考虑下 dp,注意题目图片中的公式,每一小段 \(ab\) 序列,都由一个 \(b_i\) 来结尾。
- 设计状态:\(dp[i]\) 表示后面子串不取,只考虑前 \(i\) 个字符,第 \(i\) 位作为 \(b_x\) 的方案数。
- 考虑转移:若 \(i\) 位作为 \(b_x\) ,那么我们需要找到对应的 \(a_x\) ,显然 \(a_x\) 到 \(b_x\) 势必包含一个匹配位置,可以二分或者线性记录找到对应的最近匹配位置 \(p\) 。
- 如果没有找到一个匹配位置,\(dp[i]=0\) 。
- 否则有一个显然的方案,整个子串只有一个 ab 序列,即 \(dp[i] = p\) 。
- 然后考虑从前面的状态转移,设 \(i\) 对应 b 序列前一个 \(b_y\) 在 \(j\) 下标,则
dp[i] += dp[j] * (p - j)
。 - 解释一下转移,对于 \(s[j+1,p]\) 位置的所有下标,都可以作为 \(b_x\) 对应的 \(a_x\) ,再用乘法原理计算。
- 对于任意 \(j \leq p-1\) 都有以上的转移,\(dp[i] =dp[i]+ (\sum_{j=1}^{p-1} (p - j) * dp[j])\)
- 发现可以前缀和优化,维护 \(i * dp[i]\) 和 \(dp[i]\) 两个前缀和即可。
- 时间复杂度 \(O(n)\),代码中的二分查找可以优化掉,
这里就懒得写了。
const int N = 1e5 + 10, mod = 1e9 + 7;
ll dp[N], pre[N], ipre[N];
// dp[i] 表示第 i 位作为 b_j 的方案数。pos 记录匹配起始位置
int main() {
string s, t;
IOS;
cin >> s >> t;
int n = s.size(), m = t.size();
s = " " + s, t = " " + t;
kmp.get_ne(t.c_str(), SZ(t) - 1);
pos.pb(-m);
kmp.match(s.c_str(), t.c_str(), n, m);
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1];
ipre[i] = ipre[i - 1];
auto p = *prev(upper_bound(ALL(pos), i - m + 1));
if (p == pos[0]) continue;
dp[i] = p;
dp[i] = (dp[i] + p * pre[p - 1] % mod - ipre[p - 1] + mod) % mod;
pre[i] += dp[i];
ipre[i] = (ipre[i] + i * dp[i]) % mod;
if (pre[i] >= mod) pre[i] -= mod;
}
ll ans = 0;
for (int i = 1; i <= n; i++) ans = (ans + dp[i]) % mod;
printf("%lld\n", ans);
return 0;
}