CCPC2019Final C Mr.Panda and Typewriter

    科技2023-12-24  97

    Codeforces题目链接

    问题重述

    给定一个字符串S, 你要从一个空串打出字符串S. 你可以选用的三种操作如下:

    花费X, 在已经打出的字符串后面打一个字符花费Y, 复制已有的字符串中任意一个子串花费Z, 把在剪切板的最后一个子串放到文末

    求打出字符串S的最小代价.

    动态规划

    显然用DP. 但是需要记录什么状态?

    最容易想到的状态必然是F[n][i][j]表示现在已经打了n个字符, 并且剪切板上的内容为S[i:j].

    这样的复杂度起码是n3的. 而且还会存在S[i:j]重复的情况. (字串相等)

    如何简化这个DP?

    考虑有效的粘贴操作, 在剪切板的部分一定是当前S[:n]的一个后缀.

    而它所对应的复制操作可以看成是在这个后缀前一次出现的时候复制的.也就是说, 复制的部分也一定是当前已打出的字符串的一个后缀.

    那我们就设计一个状态F[n][k], 表示现在已经打了n个字符, 剪切板上是S[n-k+1 : n]这一部分.

    Why?这能表示全吗

    等价到上面的状态表示, 我们相当于只取了n=j的部分. 然后我们考虑转移的时候也就只需要进行n的枚举, 就可以简化了对j的枚举.

    同时, 我们把所有F[n][k]取min记录到F[n][0], 表示后面不需要剪切板内容时, 最小的代价

    来看看转移方程

    记录pre[i][j]=k表示S[i:j] = S[k-(j-i):k]. 为空的话记录pre[i][j]=-1(关于这个的求法一会再讨论)

    考虑F[n][k]:

    { F [ n ] [ 0 ] = min ⁡ ( F [ n − 1 ] [ 0 ] + X , F [ n ] [ k ] ) F [ n ] [ k ] = min ⁡ ( F [ n − k ] [ 0 ] + Y + Z , F [ p r e [ n − k + 1 ] [ k ] ] [ k ] + ( n − k − p r e [ n − k + 1 : k ] ) ∗ X + Z ) , i f p r e [ n − k + 1 : k ] ≠ − 1 \begin{cases} F[n][0] = \min{(F[n-1][0] + X, F[n][k])}\\ F[n][k] = \min(F[n-k][0] + Y + Z, F[pre[n-k+1][k]][k] + (n-k-pre[n-k+1:k])*X+Z) &,if\quad pre[n-k+1:k]\neq -1 \end{cases} {F[n][0]=min(F[n1][0]+X,F[n][k])F[n][k]=min(F[nk][0]+Y+Z,F[pre[nk+1][k]][k]+(nkpre[nk+1:k])X+Z),ifpre[nk+1:k]=1

    解释一下: F[n][0]可能是从F[n-1][0]直接打个字出来. 也可能是F[n][k]忽略掉剪切板的内容, 更新到这里的

    F[n][k]如果这个后缀在之前出现过, 那么我们就有两种可能:

    剪切板上的内容一直没动过, 在最后一次复制之后全都是一个字符一个字符的敲上去的. F[pre[n-k+1][n]][k]+(n-k-pre[n-k+1:k])*X+Z.剪切板上的内容动过, 那就在n-k长度的时候给它复制一下,然后粘贴即可. 代价为F[n-k][0] + Y + Z.

    求pre数组

    可能最好想的就是哈希吧

    但是因为我们需要求所有的pre, 所以我们不妨用LCS(最长公共后缀子串?)来求.

    另外, 看到上面用pre的时候都是一个尾n, 外加一个长度k. 所以也不妨稍微改动一下定义. pre[n][k]表示S[n-k+1:n]最后一次出现的位置.

    然后引入LCS[i][j]表示S[1:i]和S[1:j]的最长公共后缀子串的长度. 转移方程:

    { L C S [ i ] [ j ] = L C S [ i − 1 ] [ j − 1 ] + 1 , S [ i ] = S [ j ] L C S [ i ] [ j ] = 0 , S [ i ] ≠ S [ j ] \begin{cases} LCS[i][j] = LCS[i-1][j-1]+1 &, S[i] = S[j]\\ LCS[i][j] = 0 &, S[i] \neq S[j] \end{cases} {LCS[i][j]=LCS[i1][j1]+1LCS[i][j]=0,S[i]=S[j],S[i]=S[j]

    在得到LCS之后

    p r e [ i ] [ min ⁡ ( i − j , L C S [ i ] [ j ] ) ] = j , i ≥ j p r e [ i ] [ j ] = max ⁡ ( p r e [ i ] [ j ] , p r e [ i ] [ j + 1 ] . . . p r e [ i ] [ i ] ) pre[i][ \min(i-j, LCS[i][j])] = j, i\geq j\\ pre[i][j] = \max{(pre[i][j], pre[i][j+1]...pre[i][i])} pre[i][min(ij,LCS[i][j])]=j,ijpre[i][j]=max(pre[i][j],pre[i][j+1]...pre[i][i])

    先解释第一个式子:

    L C S [ i ] [ j ] < i − j LCS[i][j] < i-j LCS[i][j]<ij 也就是说, i-LCS[i][j] > j. 也就是说公共后缀的第一个字符是在j后面的, 那没问题, 不会重叠 L C S [ i ] [ j ] > i − j LCS[i][j] > i-j LCS[i][j]>ij 也就是说, 如果还是用LCS[i][j]的话会产生重叠. 考虑我们的定义, 他不能重叠. 所以不能直接用LCS. 因为j长度的这个后缀一定是LCS的一个后缀子串, 所以可以直接取i-j作为最长的后缀.

    然后解释第二个式子:

    这个简单, k+1长度的后缀一定是k长度的后缀.

    代码

    #include<bits/stdc++.h> using namespace std; typedef long long ll; const int N = 5e3+17; const long long INF = 1e18+17; int n; long long X, Y, Z; void work(int cas){ scanf("%d%lld%lld%lld",&n,&X,&Y,&Z); vector<int>s(n+17); for(int i=1; i<=n; ++i) scanf("%d", &s[i]); vector< vector<int> >lcs(n+17, vector<int>(n+17)); vector< vector<int> >pre(n+17, vector<int>(n+17, -1)); //cerr<<"1"<<endl; for(int i = 1; i <= n; ++i) for(int j=1; j <= i;++j) if(s[i] == s[j]) lcs[i][j] = lcs[i-1][j-1] + 1; else lcs[i][j] = 0; //cerr<<"2"<<endl; for(int i=1;i<=n;++i) for(int j=1;j<i;++j) pre[i][min(i-j, lcs[i][j])] = j; //cerr<<"3"<<endl; for(int i=1;i<=n;++i) for(int j=i-1;j;--j) pre[i][j] = max(pre[i][j], pre[i][j+1]); //cerr<<"4"<<endl; vector< vector<ll> >f(n+17, vector<ll>(n+17, INF)); f[1][0] = X; //cerr<<"5"<<endl; for(int i=2;i<=n;++i){ f[i][0] = f[i-1][0] + X; for(int j=1; j<=i;++j){ if(pre[i][j] == -1) continue; // printf("%d %d %d\n", i, j, pre[i][j]); f[i][j] = min(f[i][j], f[i-j][0] + Y + Z); // printf("%d %d %d\n", i, j, pre[i][j]); f[i][j] = min(f[i][j], f[pre[i][j]][j] + X*(i-j-pre[i][j]) + Z); // printf("%d %d %d\n", i, j, pre[i][j]); f[i][0] = min(f[i][0], f[i][j]); // printf("%d %d %d\n", i, j, pre[i][j]); } } printf("Case #%d: %lld\n", cas, f[n][0]); return; } int main(){ int T; scanf("%d",&T); for(int i=1;i<=T;++i) work(i); return 0; }
    Processed: 0.019, SQL: 8