私以为很巧妙的一道计数题
(不会正经的dp做法)
题意:给一个长度为n,仅包含前m个小写字母的字符串S,求有多少个字符串由前m个小写字母构成的T,满足LCS(S,T) == n - 1
n <= 1e5,2 <= m <= 26
我们考虑枚举LCS,容易发现一定是S去掉某个字母得到的,又因为去掉连续一段的字母中的任何一个都一样,所以LCS有(S的连续段数个)
eg:aaabcccdd,删掉第1个a,删掉第2个a,得到的LCS显然相同,所以LCS有m个
考虑删完后如何插入一个字母.我们不妨强行设去掉的那个字母在删除的最前段.为避免重复,我们可以令添加后的字符在连续段内的第一个,即添加的字符不能和前面的一个字符相同
所以方案数有(n - 1) * (m - 1) + m - 1 = n * (m - 1) ;
后面的+m是因为第一个空位前面没有字符,可以插m个,-1是因为我们插的字符如果在删去的字符的位置,不能和删去的字符相同,否则LCS = n
有人可能会问:那会不会出现删去的字符和前一个字符相同所以多减一遍之类的鬼畜情况?
所以我们强行设去掉的那个字母在删除的最前端.这样就不会了.
所以ans = 段数 * n * (m - 1)???
如果你这样发现连样例都过不了
考虑以下情况 /s/ /str/ /t/
发生以下两种移位 /s/ /t/ /str/
/str/ /s/ /t/
易发现如果/str/的循环同构循环节恰好为2,会算重一次.所以答案还要减掉所有循环节为2的子串即abab....ab之类的
代码如下
/*CF578D LCS Again*/ #include<bits/stdc++.h> using namespace std; #define ll long long int read(){ char c = getchar(); int x = 0; while(c < '0' || c > '9') c = getchar(); while(c >= '0' && c <= '9') x = x * 10 + c - 48,c = getchar(); return x; } const int N = 1e5 + 10; char s[N]; int main(){ int n = read(),m = read(); scanf("%s",s+1); ll ans = 0; for(int i = 1; i <= n; ++i){ if(s[i] != s[i-1]) ans += 1ll * n * (m - 1); } int len = 1; for(int i = 2; i <= n; ++i){ if(len == 1){ if(s[i] == s[i-1]) continue; else len++; } else if(s[i] == s[i-2]){ len++; } else{ ans -= 1ll * len * (len - 1) / 2; if(s[i] == s[i-1]) len = 1; else len = 2; } } ans -= 1ll * len * (len - 1) / 2; cout<<ans<<endl; return 0; }
思考下我们做了什么(本sb想这题花了两个晚上)
首先分析题目的性质,找到LCS
然后便是一步步去重.该约束条件很简洁,让我们有办法直接做,直接去重
不然可能要进一步分析,找到更简洁的充要条件(比如本题本质不同的LCS要考虑段数之类的)