题解/算法 {5728. 截取子串}
@LINK: https://www.acwing.com/problem/content/description/5731/
;
首先 你要理解字符串切割的本质, 比如"abcdef"
, 那么{"a", "cde"}
是一个方案, 换句话说 他是若干个{[Li, Ri]}
区间的集合 且这些区间没有交集; (他的本质 就是若干个区间的集合 而不是若干个下标 即他和隔板法无关);
因此 任意一个方案 都可以写成是一个区间的序列 比如[ [0-1], [3-4], [7-9] ]
; 其实 这就很像DP了, 我们根据其最后的区间[7-9]
进行枚举, 换句话说 我们区间[l,r]
然后求出来有多少个方案 他们的最后区间是当前[l,r]
区间;
看一个错误做法: 我们枚举S中所有T
的子串 即枚举l,r
满足S[l-r] = T
, 然后我们求 所有形如[..., [L,R]]
的方案 其中L<=l, R>=r
;
这是错误的, 因为[L,R]
他里面可以有多个T子串 就导致 你的方案重复计算了, 比如S = "abacababa", T="aba"
, 那么对于["abac", "ababa"]
这个方案, 当l=4,r=6
时 会枚举到L=4,R=8
即这个方案, 而当l=6,r=8
时 也会枚举到L=4,R=8
这个方案 重复了;
正确的做法是: 对于[..., [L,R]]
我们把R
端点给固定住, 换句话说 我们枚举所有的R
然后再求形如[..., [?,R]]
的方案个数;
我们令&Lef
为 满足S[Lef, R]
区间里包含T
的最大的Lef
, 令&Cont[r]: S[0...r]里 形如[...,[?,<=r]]的方案个数
, 于是此时对于形如[..., [?,R]]
的方案 其中? 一定是<= Lef
更准确的说 ?的范围是 [0, Lef]
, 也就是 对于任意的? = [0, Lef]
我们执行cur += Cont[?-1] + 1
(加一的意思是 [?,R]
是单独的一个) 此时cur
就是所有形如[..., [?,R]]
的方案个数, 接下来更新Cont
即Cont[Rig, ..., N-1] += cur
;
vector<Mod_> Cont( S.size(), 0);
Mod_ ANS = 0;
int Lef = -1;
FOR_( rig, T.size()-1, S.size()-1){
if( S.substr( rig-T.size()+1, T.size()) == T){
Lef = rig - T.size() + 1;
}
if( Lef == -1){ continue;}
Mod_ cur = 0;
FOR_( l, 0, Lef){
cur += 1;
if( l > 0){ cur += Cont[ l-1];}
}
ANS += cur;
FOR_( r, rig, S.size()-1){ Cont[r] += cur;}
}
cout<< ANS.Value;
于是 区间修改 + 区间查询, 用懒标记线段树即可;
然后那个获取S
中 所有等于T
的子串下标, 用KMP或字符串哈希 都可以;
FOR_( rig, T.size()-1, S.size()-1){
if( HS.GetHash_SubString( rig-T.size()+1, rig) == HT){ // 字符串哈希
Lef = rig - T.size() + 1;
}
if( Lef == -1){ continue;}
Mod_ cur = 0;
cur += 1; // [0...rig]
if( Lef-1 >= 0){
auto ret = Seg.Query_Interval( 0, Lef-1);
ret.Sum += Lef;
cur += ret.Sum;
}
ANS += cur;
if( rig <= (int)S.size()-1){
Seg.Modify_Interval( rig, S.size()-1, {cur});
}
}
cout<< ANS.Value;