Luogu P3975 TJOI2015 弦论 题解 [ 紫 ] [ 后缀自动机 ] [ 动态规划 ] [ 拓扑排序 ]

弦论:本来不想写板子题题解的,但奈何这道题的题解都太垃圾了,导致我理解了一个晚上都没想明白 dp 转移啥意思/fn/fn/fn,所以记录一下。

思路

t=0

考虑 SAM 思路,建出后缀自动机后,该字符串内所有本质不同的子串都可以用一条从根到某节点的路径表示出来,且不可能表示出其他任何非子串的字符串。

那么我们就可以进行类似平衡树查找的操作,每次先选择一个字典序更小的节点,判断进去后有没有这么多的子串,然后利用 dfs 输出方案即可。

那么进入一个节点后能构成的子串数如何计算呢?根据前面的性质,不难发现它就是从该节点出发,到任意一个其他节点的方案数。

于是我们就可以在 DAG 上拓扑了,由于 SAM 的优良性质,所以它的转移边组成的图一定是一个 DAG。

定义 dpi 表示走到 i 前的路径已确定,接着走的方案数。转移方程如下:

dpi=1+j=1|vi|dpvi,j

为啥要加那个 1 呢?因为它可以在当前节点停下,后面不走了。也因此在 dfs 进入一个节点时,要先把停留在该节点的方案数减掉。如果剩余的方案数小于等于 0,就说明走完了。

同时,走到 i 前的路径已确定的条件也很重要,下文会有提及。

时间复杂度 O(n||),瓶颈在于构建自动机和 dfs。

t=1

考虑沿用上一问的思路,这次由于子串能重复计算,所以尝试修改 dp 的转移。

我们先求出每个节点的 endpos 集合的字符串会出现多少次,假设这个值为 wi。显然一个 endpos 集合内子串的出现次数应该是相同的。于是在后缀链接树上树形 dp 一遍即可。

那么我们回到 dp 状态定义中“走到 i 前的路径已确定”的条件,可以写出如下的转移:

dpi=wi+j=1|vi|dpvi,j

为什么一定要保证前面的路径确定呢?如果前面的路径不确定,那么 i 处的 endpos 中所有的字符串都有可能成为当前的状态,那么 wi 就要乘 endpos 的大小了,这显然是不符合题意的,因为 dfs 时走到 i 的路径已经被定下来了,对应着的是 endpos 里唯一的一个字符串。

wi 的实际含义是把以 i 为结尾的子串个数算进 dp 值中,因此要加上它的出现次数。

同理,dfs 时减去的就不是 1,而是 wu 了。

时间复杂度 O(n||)

注意 w1 设为 0,因为空串是不能计入其中的。

代码

#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
int n,t,tp,rk,ch[1000005][26],fa[1000005],tot=1,np=1,len[1000005],rd[1000005];
ll w[1000005],dp[1000005];
char s[1000005];
vector<int>g[1000005],g2[1000005];
void extend(int c)
{
    int p=np;
    np=++tot;
    len[np]=len[p]+1;w[np]=1;
    for(;p&&ch[p][c]==0;p=fa[p])ch[p][c]=np;
    if(p==0)fa[np]=1;
    else
    {
        int q=ch[p][c];
        if(len[q]==len[p]+1)fa[np]=q;
        else
        {
            int nq=++tot;
            len[nq]=len[p]+1;
            fa[nq]=fa[q],fa[q]=nq,fa[np]=nq;
            for(;p&&ch[p][c]==q;p=fa[p])ch[p][c]=nq;
            memcpy(ch[nq],ch[q],sizeof(ch[q]));
        }
    }
}
void dfs2(int u)
{
    for(auto v:g2[u])
    {
        dfs2(v);
        w[u]+=w[v];
    }
}
void init()
{
    if(tp==0)
    {
        for(int i=2;i<=tot;i++)w[i]=1;
        w[1]=0;
    }
    else
    {
        for(int i=2;i<=tot;i++)g2[fa[i]].push_back(i);
        dfs2(1);
        w[1]=0;
    }
    queue<int>q;
    for(int i=1;i<=tot;i++)
    {
        for(int j=0;j<26;j++)
        {
            int v=ch[i][j];
            if(v)
            {
                rd[i]++;
                g[v].push_back(i);
            }
        }
    }
    for(int i=1;i<=tot;i++)
    {
        if(rd[i]==0)q.push(i);
        dp[i]=w[i];
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        for(auto v:g[u])
        {
            if(v)
            {
                rd[v]--;
                if(rd[v]==0)q.push(v);
                dp[v]+=dp[u];
            }
        }
    }
}
void dfs(int u,int rk)
{
    rk-=w[u];
    if(rk<=0)return;
    for(int i=0;i<26;i++)
    {
        int v=ch[u][i];
        if(v==0)continue;
        if(rk<=dp[v])
        {
            cout<<char(i+'a');
            dfs(v,rk);
            return;
        }
        rk-=dp[v];
    }
}
void solve()
{
    if(rk>dp[1])
    {
        cout<<-1<<'\n';
        return;
    }
    dfs(1,rk);
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>s+1;
    n=strlen(s+1);
    for(int i=1;i<=n;i++)extend(s[i]-'a');
    cin>>tp>>rk;
    init();
    t=1;
    while(t--)solve();
    return 0;
}
posted @   KS_Fszha  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示