【HDU】5470 Typewriter 后缀自动机+dp

首先暴力的方法就是,对第i个位置,我枚举j(j < i),然后看子串[j+1…i]是不是在[1…j]中存在(假设串从下标1开始),然后dp[i]=min{dp[i-1]+cost[s[i]],dp[j]+(i-j)*A+2*B)},首先我们可以找到一个最小的合法的j,这样从j~i的这个区间内全部都是合法的,这样我们就可以直接在区间[j,i]内寻找最小值即可,可以用后缀数组(SA)求lcp来实现O(logn)找到最小的j,然后区间查询最小值,外加单点更新,这样整道题可以在O(NlogN)的时间复杂度内实现。 
幸运的是,区间求最小值不需要线段树(省去一些编程复杂度)。因为当i->i+1时,j是单调不减的!因为如果在添加第i+1个字符时j减小了,那么在第i个字符的时候我们就可以减小,矛盾。这样我们就可以用单调队列来求区间最小值了(单调队列求最小值的原理是维护一个递增的队列,当枚举的左端点大于队列最左端表示的点,就把队列最左端的点丢弃,加入一个点就维护这个队列,使得维持递增这个性质。最后区间最小值就是队列最左端的值)。 
现在我们要做的是,将算法复杂度优化到O(N),这里我选择的是后缀自动机(SAM)代替后缀数组(SA)(其实我也只会SAM和SA……)。SAM的具体操作是,假设第i-1个位置处理完,开始枚举第i个字符时,假设上一个合法的最小值为j(注意这时候只添加了1~j-1内的字符,合法位置内的字符不添加进SAM),[j…i-1]表示的串在SAM中的表示的节点为point,现在我们判断[j…i]是否在SAM中,即point时候有后继s[i],如果有那么j不变,否则将s[j]加入SAM,然后++j,并且point调到他的最长后缀。重复之前的判断直到j==i或者point有后继s[i]。接下来只要套上上面的单调队列求区间最小值即可。

PS:SAM上的某一个节点的最长后缀(非本身),不一定是fail,可能是自身,其跳到自身的意义就是换了一条能通过后继到达他的边。

如果用一个指针记录当前自动机中某个状态,并且一边建后缀自动机一边维护它,要记住在第三种情况时(会在中间插入节点)更改指针。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#define Rep(i, x, y) for (int i = x; i <= y; i ++)
#define Dwn(i, x, y) for (int i = x; i >= y; i --)
#define RepE(i, x) for(int i = pos[x]; i; i = g[i].nex)
#define u sam[x]
#define su sam[nx]
#define o sam[y]
#define w sam[z]
#define v u.t[c]
#define vn sam[v]
using namespace std;
typedef long long LL;
const int N = 200005;
const LL inf = 1LL << 62;
struct Sam_node { int t[27], par, l; } sam[N * 3];
int T, n, tz, lt, s1[N], q[N], a[N], test, nx, l;
LL A, B, f[N]; char s0[N];
void Insert(int c) {
    int z = ++ tz, x = lt; lt = z; w.l = u.l + 1;
    memset(w.t, 0, sizeof(w.t));
    for (; !v && x; x = u.par) v = z;
    if (!x) w.par = 1;
    else if (u.l + 1 == vn.l) w.par = v;
    else {
        int y = ++ tz, tv = v; o = vn, o.l = u.l + 1, vn.par = w.par = y;
        if (nx == tv && l <= o.l) nx = y;
        for (; v == tv; x = u.par) v = y;
    }
}
int main()
{
    scanf ("%d", &T);
    while (T --) {
        tz = 1, lt = 1;
        scanf ("%s", s0 + 1);
        n = strlen(s0 + 1);
        Rep(i, 1, n) s1[i] = s0[i] - 'a';
        Rep(i, 0, 25) scanf ("%d", &a[i]);
        scanf ("%I64d%I64d", &A, &B);
        int k = 1, hd = 1, tl = 0; nx = 1;
        memset(sam[1].t, 0, sizeof(sam[1].t));
        Rep(i, 1, n) {
            while (k <= i && !su.t[ s1[i] ]) {
                if (sam[ su.par ].l == i - k - 1) nx = su.par;
                l = i - k - 1, Insert(s1[k]), k ++;
            } if (k <= i) nx = su.t[ s1[i] ];
            while (hd <= tl && q[hd] < k - 1) hd ++;
            if (hd <= tl) f[i] = f[ q[hd] ] + (i - q[hd]) * A + 2 * B;
            else f[i] = inf;
            f[i] = min(f[i], f[i - 1] + a[ s1[i] ]);
            while (hd <= tl && f[ q[tl] ] - q[tl] * A >= f[i] - i * A) tl --;
            q[++ tl] = i;
        }
        printf("Case #%d: %I64d\n", ++ test, f[n]);
    }

    return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值