嗯,以后博客内容就这样规定吧:
近期,以下三类题目做完之后必须写题解,其他的任意
数学、字符串、网络流
好了进入正题
题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=1396
题目大意:
给定长度为
n
n
n的字符串
a
a
a, 对每一个
i
∈
[
1
,
n
]
i\in [1,n]
i∈[1,n]求包含
i
i
i这个位置的最短的只出现一次的子串。
题解:
这道题目充分暴露了我SB的事实。
讲个笑话:以下两个问题我各想了半小时都没有想出来。
1. 如何求对于SAM中Right集合大小为1的点求出Right集合。
2. 如何支持插入一条先斜率为-1下降再平的直线,最后求每个位置上的最小值。
建出
a
a
a串的
S
A
M
SAM
SAM. 求出每一个节点的
R
i
g
h
t
Right
Right集合大小即为其所代表子串的出现次数。
对于每一个
R
i
g
h
t
Right
Right集合大小为
1
1
1的状态,我们求出它的
R
i
g
h
t
Right
Right集合,也就是这个串具体出现在哪个位置。怎么做?很简单,直接插入SAM的时候记录每个状态在第几次被插入的即为出现位置。有性质:
R
i
g
h
t
Right
Right集合大小为
1
1
1当且仅当这个点在
P
a
r
e
n
t
Parent
Parent树上是叶子节点。 简单吧,这都想不出来,我真是个大SB。
然后我们考虑它对答案的贡献:设
m
a
x
l
e
n
[
i
]
,
m
i
n
l
e
n
[
i
]
maxlen[i], minlen[i]
maxlen[i],minlen[i]分别表示
i
i
i代表子串的最大最小长度
r
i
g
h
t
[
i
]
right[i]
right[i]表示
i
i
i的出现位置右端点,画一画可以发现,对于
[
r
i
g
h
t
[
i
]
−
m
i
n
l
e
n
[
i
]
+
1
,
r
i
g
h
t
[
i
]
]
[right[i]-minlen[i]+1,right[i]]
[right[i]−minlen[i]+1,right[i]]这段区间,我们用
m
i
n
l
e
n
[
i
]
minlen[i]
minlen[i]更新每一个点原答案(这是为了保证出现次数为
1
1
1);对于
[
r
i
g
h
t
[
i
]
−
m
a
x
l
e
n
[
i
]
+
1
,
r
i
g
h
t
[
i
]
−
m
i
n
l
e
n
[
i
]
]
[right[i]-maxlen[i]+1,right[i]-minlen[i]]
[right[i]−maxlen[i]+1,right[i]−minlen[i]]这段区间中下标为
k
k
k的点,我们用
r
i
g
h
t
[
i
]
+
m
a
x
l
e
n
[
i
]
−
k
right[i]+maxlen[i]-k
right[i]+maxlen[i]−k更新
k
k
k的答案(这是为了在出现次数为
1
1
1的基础上保证这个右端点为
r
i
g
h
t
right
right的子串跨过
i
i
i)。
emm, 这样说可能比较难以理解,举个例子:
假设当前节点
r
i
g
h
t
=
7
,
m
a
x
l
e
n
=
5
,
m
i
n
l
e
n
=
3
right=7, maxlen=5, minlen=3
right=7,maxlen=5,minlen=3
则它对答案的贡献是: (从左往右是
3
,
4
,
5
,
6
,
7
3,4,5,6,7
3,4,5,6,7)
5
,
4
,
3
,
3
,
3
5,4,3,3,3
5,4,3,3,3
对于
5
,
6
,
7
5,6,7
5,6,7两格,当串长为
3
3
3时该串为
a
[
5
,
7
]
a[5,7]
a[5,7]恰好能够覆盖
5
,
6
,
7
5,6,7
5,6,7三个点。若串长为
2
2
2则小于
m
i
n
l
e
n
minlen
minlen, 跳到了
S
A
M
SAM
SAM别的节点上,不合法。
对于
4
4
4格,当串长为
3
3
3时该串为
a
[
5
,
7
]
a[5,7]
a[5,7], 虽然满足出现了一次,但是这个串太短不足以覆盖
4
4
4这个点。因此扩大串长至
4
4
4, 该串为
a
[
4
,
7
]
a[4,7]
a[4,7]
对于
3
3
3格同理扩大串长至
5
5
5
然后推一波就可以得到刚才的式子。
好了现在我们已经把问题转化成了:每次给一个区间做上述奇奇怪怪的update, 最后询问每个点的值。
我们先观察:“奇奇怪怪的update”, 实际上是要支持区间插入一条斜率为
−
1
-1
−1或
0
0
0的直线,最后求每个点上直线的最低位置。看上去最暴力的想法是李超树直接上,但是无敌的Creed_巨佬告诉我我自闭了。
于是就只好继续想简单做法,发现可以根据斜率为
−
1
,
0
-1,0
−1,0的优美性质搞个在线线段树,最后口胡完了发现就是个整数版李超树……
最后,不知道为啥我硬想
1
h
1h
1h想不出来,然后看题解,看了几个字,发现这东西巨蠢……
维护两棵线段树,分别表示斜率为
0
0
0和
−
1
-1
−1的,取
min
\min
min即可
看吧,我果然是个SB
这个做法完美地利用了这个实际情景中的离线性质。
然后我们就可以开心地去写两棵线段树啦。
等等……诶代码量有点大??
我们考虑斜率为
−
1
-1
−1的直线,如果我们对每一个最终的数值
a
i
a_i
ai作变换
a
i
=
a
i
+
i
a_i=a_i+i
ai=ai+i, 那么斜率为
−
1
-1
−1的直线就变成了斜率为
0
0
0的!继续用刚才方法维护!
一遍线段树,做完了。(SB退役选手终于自己想出了这道题的一部分)
另外,第一次写标记永久化线段树。在这道题里为了保证常数,个人认为标记永久化会快一些。反正怎么做都可以。
时间复杂度
O
(
n
log
n
)
O(n\log n)
O(nlogn).
代码实现
//Wrong Coding:
//Forget to return in mdfmin()
//lb = rb-... -> lb = lb-...
//SGT Range is sam.siz<<2 not n<<2
//segtree1.query()... not segtree2
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#define llong long long
using namespace std;
const int N = 2e5;
const int S = 26;
const int INF = 1e7;
char a[N+3];
int n;
struct SAM
{
int len[N+3];
int fa[N+3];
int son[N+3][S+3];
int buc[N+3];
int oid[N+3];
int rid[N+3];
int sz[N+3];
int siz,lstpos,rtn;
void init()
{
rtn = siz = lstpos = 1;
}
void insertstr(char ch,int id)
{
int p = lstpos,np; siz++; lstpos = np = siz; len[np] = len[p]+1; sz[np] = 1; rid[np] = id;
for(; p && son[p][ch]==0; p=fa[p]) {son[p][ch] = np;}
if(p==0) {fa[np] = rtn;}
else
{
int q = son[p][ch];
if(len[q]==len[p]+1) {fa[np] = q;}
else
{
siz++; int nq = siz; len[nq] = len[p]+1;
memcpy(son[nq],son[q],sizeof(son[q]));
fa[nq] = fa[q]; fa[np] = fa[q] = nq;
for(; p && son[p][ch]==q; p=fa[p]) {son[p][ch] = nq;}
}
}
}
void sortnode()
{
for(int i=1; i<=siz; i++) buc[len[i]]++;
for(int i=1; i<=siz; i++) buc[i] += buc[i-1];
for(int i=siz; i>=1; i--) oid[buc[len[i]]--] = i;
for(int i=siz; i>=1; i--) {int u = oid[i]; if(fa[u]) sz[fa[u]]+=sz[u];}
}
} sam;
struct SegmentTree
{
struct SgTNode
{
int tag;
} sgt[(N<<2)+2];
void mdfmin(int pos,int le,int ri,int lb,int rb,int val)
{
if(lb>rb) return;
if(le==lb && ri==rb) {sgt[pos].tag = min(sgt[pos].tag,val); return;}
int mid = (le+ri)>>1;
if(rb<=mid) mdfmin(pos<<1,le,mid,lb,rb,val);
else if(lb>mid) mdfmin(pos<<1|1,mid+1,ri,lb,rb,val);
else {mdfmin(pos<<1,le,mid,lb,mid,val); mdfmin(pos<<1|1,mid+1,ri,mid+1,rb,val);}
}
int queryval(int pos,int le,int ri,int lrb)
{
if(le==ri) {return sgt[pos].tag;}
int mid = (le+ri)>>1;
if(lrb<=mid) return min(queryval(pos<<1,le,mid,lrb),sgt[pos].tag);
else return min(queryval(pos<<1|1,mid+1,ri,lrb),sgt[pos].tag);
}
} segtree1,segtree2;
int main()
{
scanf("%s",a+1); n = strlen(a+1);
sam.init();
for(int i=1; i<=n; i++) sam.insertstr(a[i]-96,i);
for(int i=0; i<=(sam.siz<<2); i++) segtree1.sgt[i].tag = segtree2.sgt[i].tag = INF;
sam.sortnode();
for(int i=sam.siz; i>=1; i--)
{
if(sam.sz[i]==1)
{
int minlen = sam.len[sam.fa[i]]+1,maxlen = sam.len[i];
int rb = sam.rid[i],lb = rb-minlen+1;
segtree2.mdfmin(1,1,n,lb,rb,minlen);
rb = lb-1; lb = lb-(maxlen-minlen);
segtree1.mdfmin(1,1,n,lb,rb,maxlen+lb);
}
}
for(int i=1; i<=n; i++)
{
int ans1 = segtree1.queryval(1,1,n,i)-i,ans2 = segtree2.queryval(1,1,n,i);
int ans = min(ans1,ans2);
printf("%d\n",ans);
}
return 0;
}
/*
bacaca
*/