[GDKOI2016]项链

4 篇文章 0 订阅
2 篇文章 0 订阅

题目大意

给定一个首尾相接的项链,共有 n 位,每一位都是一个小写字母。我们要剪掉连续的一段(注意首尾相接,首尾也是连续的),使得剩余部分拼接起来能够对称(注意首尾相接,重要的事情说三遍)。最大化剩余串长度。

1n105


题目分析

简化模型

这题看起来和GDKOI2015Day1T1很像,都是对称。对称即是双回文串(想想为什么)。上一次我们是使用 Manacher 算法解决的,这次我们做同样的考虑。
显然我们先要将其倍长,然后将其变成 Manacher 串(相邻两位和首尾加入#字符)。直接找双回文串就行了?显然不对,长度有时会大于 n 。那长度大于n是否代表答案为 n 呢?显然不对,两个回文串相交的恶心情况能作为反例。
那么题目模型现在就变成找出该串里面长度小于等于n的最长双回文子串。
我们将这个约束用数学符号表示。对于回文中心 i j i<j ),显然长度小于等于 n 就是jin。设 pi 为回文半径(这里的回文半径是指最右端位置-回文中心),那么 i j的回文串相接即 i+pijpj
所以现在问题变成求满足 jin i+pijpj 的数对 (i,j) 中最大的 ji
这个可以使用各种数据结构如线段树、树状数组等解决,但是在这里我要说一种更机智的解法。

扫描线&并查集

扫描线算法的基本思想是扫描处理询问,同时修改询问位置影响到的值。
我们考虑从左到右枚举位置 x ,如果存在i使得 i+pi=x ,那么我们就可以处理关于 i 的答案。显然,满足jp[j]x左的 j 都可能对答案有贡献。这时还剩下限制jin,我们将其化成 ji+n ,那么我们相当于查找所有这些 j 中最大的小于等于i+n的数。
这个怎么实现呢?我们开一个黑白点数组,初始时所有位置都为白点。我们处理询问 i ,就是查询从位置i+n开始往左第一个黑点的位置。处理完询问之后我们要将所有的满足 jpj=x j 都改为黑点。这样黑白点数组就可以看成一条线段,我们每次删除一些位置,询问某些点往左第一个断点。这个是需要一个log级别的数据结构维护的。
怎么办?我们改变枚举顺序,从右到左枚举,我们就可以将删除改为添加,使用并查集维护。因为反着枚举的话我添加的点都是非法的,然后合法解显然为一连串非法解的左边第一个(特判自己本身合法的情况)。
于是时间复杂度就变成 O(nα(n)) 了。
具体细节见代码实现。


代码实现

#include <iostream>
#include <cstring>
#include <cstdio>

using namespace std;

const int N=400500;

struct list
{
    int last[N],next[N],adj[N];
    int tot;

    void insert(int x,int y)
    {
        adj[++tot]=y;
        next[tot]=last[x];
        last[x]=tot;
    }
}q,c;

int p[N],fa[N];
char s[N],S[N];
bool color[N];
int n,ans,l;

int getfather(int son)
{
    return fa[son]==son?son:fa[son]=getfather(fa[son]);
}

void manacher()
{
    int pos=0,lim=1;
    p[0]=1;
    for (int i=1;i<l;i++)
    {
        if (i<lim)
            p[i]=min(p[(pos<<1)-i],lim-i);
        while (i-p[i]>=0&&i+p[i]<l&&S[i-p[i]]==S[i+p[i]])
            p[i]++;
        if (i+p[i]>lim)
            pos=i,lim=i+p[i];
    }
    for (int i=0;i<l;i++)
        p[i]--;
}

int get(int x)
{
    if (!color[x])
        return x;
    x=getfather(x);
    return x-1;
}

void merge(int x,int y)
{
    int fx=getfather(x),fy=getfather(y);
    fa[fy]=fx;
}

int main()
{
    freopen("necklace2.in","r",stdin);
    freopen("necklace2.out","w",stdout);
    scanf("%s",s);
    n=strlen(s);
    for (int i=n;i<n<<1;i++)
        s[i]=s[i-n];
    s[n<<1]='\0';
    n<<=1;
    l=n<<1|1;
    for (int i=0;i<n;i++)
        S[i<<1]='#',S[i<<1|1]=s[i];
    S[l-1]='#';
    manacher();
    for (int i=0;i<l;i++)
        fa[i]=i;
    for (int i=0;i<l;i++)
    {
        c.insert(i-p[i],i);
        q.insert(i+p[i],i);
    }
    for (int i=l-1;i>=0;i--)
    {
        int j=q.last[i],x;
        while (j)
        {
            x=q.adj[j];
            int y=get(min(l-1,x+n/2));
            ans=max(ans,y-x);
            j=q.next[j];
        }
        j=c.last[i];
        while (j)
        {
            x=c.adj[j];
            color[x]=1;
            if (x-1>=0&&color[x-1])
                merge(x-1,x);
            if (x+1<l&&color[x+1])
                merge(x,x+1);
            j=c.next[j];
        }
    }
    printf("%d\n",ans);
    fclose(stdin);
    fclose(stdout);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值