SAM入门三题:字符串水题, LCS, P5546 [POI2000]公共串
刚学完SAM(也就学了三个月还没切板子), 学习笔记什么的话估计得过段时间
那先来几篇题解吧
字符串水题
传送门
题意
给出一个数字串S, 询问若干数字串T,问T中有多少子串在S中出现过,且满足各位数字之和在区间\([L, R]\)之间
注意:位置不同的两个相同子串算作两个子串
题解
显然的一个思路, 对于T中的每一个位置x, 我们统计T中以x开头的子串有多少符合条件的。
先考虑数字和, 显然的一个思路我们做前缀和然后二分, 得到一个l, r, l表示以x开头的子串, 最少要以l结尾, 最多以r结尾
这时候我们只要知道l,r之间有多少在s中出现过, 显然, 假如\(T_{x....y}\)出现过, 那么\(T_{x...z}(z<y)\)也出现过
仔细思考, 我们只要求出T中以x开头, 最远能匹配到哪里即可
好那我们现在求一个数组f, \(f_i\)表示T中以i开头最远能匹配到的位置,在s中出现过
怎么求呢: 我们先求\(f_1\), 考虑直接一位一位往右在sam上匹配,匹配到一个位置后无法在匹配, 就直接跳到link处匹配, 并且把$f_{1...link-1}都设为这个最远的位置
如果你还没懂的话直接看代码理解也不错
当然这个做法是为在没写板子的时候独立想出来的, (虽然sam是扣的
所以我重点来写一下我对sam用法的理解
想到这个做法的关键思路,在于对endpos的理解, 后缀自动机之所以有如此优秀的复杂度, 是因为它将所有endpos相同的子串归于一个等价类, 这些在同一等价类中的子串, 出现位置完全相同,他们的很多性质都一样, 这是优化复杂度的关键所在,
在这道题中, 同一个等价类中的子串的f值显然相同, (读者自证)
所以我们才能够每次跳到他的link, 由于节点数是O(n)的, 复杂度也是O(n)的
我觉得sam的题都应该往这个方向去考虑
(虽然我刚学
实现
细节还是很多的, 比如当我们无法匹配的时候, 也就是p变成虚拟状态, 也就是连空串都无法匹配的时候, 就需要让x++,pos++, 直接匹配下一个位置, 同时要让p=1,回到空串的状态
如果你要问ddd是什么的话, 本来是end的............, 关键字真难受
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <map>
#include <string>
#define ll long long
using namespace std;
int read(){
int num=0, flag=1; char c=getchar();
while(!isdigit(c) && c!='-') c=getchar();
if(c=='-') flag=-1, c=getchar();
while(isdigit(c)) num=num*10+c-'0', c=getchar();
return num*flag;
}
namespace sam{
struct state {
int len, link;
std::map<char, int> next;
};
const int MAXLEN = 300000;
state st[MAXLEN * 2];
int sz, last;
void init() {
st[0].len = 0;
st[0].link = -1;
sz++;
last = 0;
}
void extddd(char c) {
int cur = sz++;
st[cur].len = st[last].len + 1;
int p = last;
while (p != -1 && !st[p].next.count(c)) {
st[p].next[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].next[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = sz++;
st[clone].len = st[p].len + 1;
st[clone].next = st[q].next;
st[clone].link = st[q].link;
while (p != -1 && st[p].next[c] == q) {
st[p].next[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}
}
const int N = 200005;
string s, t;
int Q, L, R;
int n, m;
int ddd[N];
void build(){
sam::init();
for(int i=0; i<s.size(); i++){
sam::extddd(s[i]);
}
}
int sum[N];
ll ans;
void solve(){
ans = 0;
int now=0, pos=0, np=0;
while(now < m){
while(pos<m && sam::st[np].next.count(t[pos])) {
np = sam::st[np].next[t[pos]];
pos++;
}
ddd[now] = pos-1;
np = sam::st[np].link;
if(np != -1){
int nex = pos - sam::st[np].len;
for(int i=now+1; i<nex; i++) ddd[i] = ddd[now];
now = nex;
}else{
np=0, now++, pos++;
}
}
for(int i=1; i<=m; i++){
sum[i] = sum[i-1] + t[i-1] - '0';
}
for(int i=1; i<=m; i++){
int l = lower_bound(sum+1, sum+1+m, L+sum[i-1])-sum;
int r = upper_bound(sum+1, sum+1+m, R+sum[i-1])-sum-1;
if(l>r || l>m || r>m) continue;
int ex = ddd[i-1]+1;
ans += max(0, min(ex, r)-max(l, i)+1);
}
cout << ans << endl;
}
void scan(){
cin >> s; n=s.size();
build();
Q = read();
while(Q--){
cin >> t >> L >> R; m=t.size();
solve();
}
}
signed main(){
// freopen("6.in", "r", stdin);
// freopen("6.txt", "w", stdout);
scan();
return 0;
}
LCS & P5546 [POI2000]公共串
LCS
让我们先来考虑如何求两个串的最长公共子串, 这题由于科学原因无法提交, 所以没有题目和代码(你可以在spoj上找到)
好吧两个串的lcs不就是上面f数组的最大值吗.........emm
所以要多思考算法的本质
多个串的LCS
传送门
当然这道题的正解我还不会, 下次upd
这里讲讲我的做法:
很简单:每个串都建一个sam不就行了!!!
很暴力把, 但很实用
我们用第一个串跟后面所有串匹配, 给后面每个串都建一个sam
然后求f数组, 由于我们f数组的含义是, 从i开头最远能匹配的距离, 那么从i开头的的最长的lcs,
显然这个总的fi值就是第一个串和后面每个串匹配的fi最小值(读自证)
那么就求求出来
但是xjq大佬说如果子串数量变多(这道题只有10)就会炸
当然所有字符串的长度和不变
我们来分析一下复杂度
每次匹配的复杂度显然是第一个串的长度\(|S|\)
假设有m个串,共匹配m次, 每次要建一个自动机, 复杂度为被匹配串的长度\(|T|\)
总复杂度就是\(O(m|S| + \sum|T|)\)
由于\(\sum|T|\) 是 \(O(n)\)的, 关键在于\(|S|\)
显然假如S长度为\(n/2\), 其他串的长度为1时, 复杂度最大为\(O(n^2)\)
所以我假了
不不不, 这里有一个很显然的优化啊啊啊, 由于S可以是任意字符串, 那么当s为长度最小的串时复杂度最优
这时候,当所有串长度都为\(\sqrt n\)时复杂度最大
复杂度为\(O(\sqrt n \sqrt n + n)\) 即\(O(n)\)
所以没有假, 这个算法优化后能过
实现
这题给出通过此题的代码, 未加优化, 刚学sam可能比较丑....抱歉
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
using namespace std;
string s[10];
const int N = 20045;
int n, f[N];
namespace sam{
struct node{
int len, link;
int ch[26];
void init(){
memset(ch, 0, sizeof(ch));
}
}st[N*2];
int sz, las=1;
void init(){
sz = 0;
st[1].len=0;
st[1].link=0;
sz++, las=1;
for(int i=0; i<N*2; i++) st[i].init();
}
void extend(int c){
int cur=++sz, p=las; st[cur].len=st[las].len+1;
while(p && !st[p].ch[c]) st[p].ch[c]=cur, p=st[p].link;
if(p){
int nex = st[p].ch[c];
if(st[p].len+1 == st[nex].len){
st[cur].link = nex;
}else{
int clone = ++sz; st[clone].len=st[p].len+1, st[clone].link=st[nex].link;
for(int i=0; i<26; i++) st[clone].ch[i] = st[nex].ch[i];
st[nex].link = clone; st[cur].link = clone;
while(st[p].ch[c]==nex) st[p].ch[c] = clone, p=st[p].link;
}
}else{
st[cur].link = 1;
}
las = cur;
}
}
int ans = 0;
int main(){
cin >> n; for(int i=1; i<=n; i++) cin >> s[i];
sam::init();
for(int i=0; i<s[1].size(); i++) sam::extend(s[1][i]-'a');
int x=0, p=1, pos=0;
while(x<s[2].size()){
while(sam::st[p].ch[s[2][pos]-'a']) p = sam::st[p].ch[s[2][pos]-'a'], pos++;
if(p){
p = sam::st[p].link;
int nexpos = pos - sam::st[p].len;
for(int i=x; i<nexpos; i++) f[i] = pos - i;
x = nexpos;
}else x++, pos++,p=1;
}
for(int j=3; j<=n; j++){
sam::init();
for(int i=0; i<s[j].size(); i++) sam::extend(s[j][i]-'a');
x=0, p=1, pos=0;
while(x<s[2].size()){
while(sam::st[p].ch[s[2][pos]-'a']) p = sam::st[p].ch[s[2][pos]-'a'], pos++;
if(p){
p = sam::st[p].link;
int nexpos = pos - sam::st[p].len;
for(int i=x; i<nexpos; i++) f[i] = min(f[i], pos - i);
x = nexpos;
}else f[x]=0, x++, pos++, p=1;
}
}
for(int i=0; i<s[2].size(); i++) ans = max(f[i], ans);
cout << ans << endl;
return 0;
}
/*
5
fassssdfadfs22
fdfsfasdssss
dddddddddfasdsdfa
ssdafffffffffdddfffffddfa
intintintintfintintfifaint
5
faddddddfffd
ddddddfaddddd
dddfaddd
dddddddddfadd
dddddddddddddddfa
2
abcd
ceddbc
*/