BZOJ 1396 识别子串 (后缀自动机、线段树)

嗯,以后博客内容就这样规定吧:
近期,以下三类题目做完之后必须写题解,其他的任意
数学、字符串、网络流

好了进入正题

题目链接: 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
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最短回文是指一个字符中能够通过修改最多两个字符,使得该字符变为回文并且长度最短的。 对于这个问题,我们可以使用动态规划来解决。假设给定的字符为s,我们可以定义一个二维数组dp,dp[i][j]表示从第i个字符到第j个字符所需修改的字符数。 首先,我们需要初始化dp数组。对于任意的i,都有dp[i][i]=0,因为单个字符本身就是回文。然后,我们需要计算长度为2的,即dp[i][i+1]。如果s[i]和s[i+1]不相等,则dp[i][i+1]=1,表示需要修改一个字符使得为回文,否则dp[i][i+1]=0。 接下来,我们需要计算长度大于2的。我们可以得到递推关系式: 如果s[i] == s[j],则dp[i][j] = dp[i+1][j-1]; 如果s[i] != s[j],则dp[i][j] = min(dp[i+1][j], dp[i][j-1]) + 1。 最后,我们找到dp数组中最小的值,所对应的即是最短回文。我们可以得到这个的起始索引和终止索引,然后将原字符的对应字符进行修改。这样,原字符就变为了回文,并且长度最短。 Python代码如下: ```python def shortestPalindrome(s): n = len(s) dp = [[0] * n for _ in range(n)] for i in range(n - 1): if s[i] != s[i + 1]: dp[i][i + 1] = 1 for k in range(2, n): for i in range(n - k): j = i + k if s[i] == s[j]: dp[i][j] = dp[i + 1][j - 1] else: dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1 min_changes = float('inf') start, end = 0, 0 for i in range(n): if dp[0][i] < min_changes: min_changes = dp[0][i] start, end = 0, i for i in range(n): if dp[i][-1] < min_changes: min_changes = dp[i][-1] start, end = i, n - 1 modified_s = list(s) while start < end: if modified_s[start] != modified_s[end]: if dp[start][end] == dp[start + 1][end] + 1: modified_s[start] = modified_s[end] else: modified_s[end] = modified_s[start] start += 1 end -= 1 return ''.join(modified_s) ``` 这样,函数shortestPalindrome就可以返回最短回文了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值