2200专项:C. Compress String(后缀数组 之前出现过的字符串)

原题: http://codeforces.com/problemset/problem/1120/C

题意:

给一个字符串,你可以将字符串分成很多个部分,这个部分在前面出现过的话(可以是多个部分并起来后,其中的字符串),那么这个部分花费可以变成b。否则,你只能选择一个长度为1的部分,其花费为a。求最小花费。

解析:

首先,你需要处理出所有的子串在之前是否出现过: S x , y S_{x,y} 是否出现在 S 1 , x 1 S_{1,x-1} 中。

开始对于每个height为单位做处理,发现一个bug:一个串只会和排名最近的串比较,因为可能有重叠的原因,这个串可能不是最优解。

改为每个后缀往两边做height的递减操作。注意长度有三个限制:之前串是否在当前串前,之前串到当前串的长度,当前串到结尾长度。

#include<bits/stdc++.h>
using namespace std;
const int N = 5009;//max(用到字符,字符串长度),如果字符串的组成是数字的话注意上限
#define rep(i,a,b) for(register int i=a;i<=b;i++)
char x[N];
int wa[N],wb[N],wv[N],Ws[N];
int cmp(int *r,int a,int b,int l){
    return r[a]==r[b]&&r[a+l]==r[b+l];
}

/* str,sa,len+1,max(ASCII)+1 */
void da(const char r[],int sa[],int n,int m){
      int i,j,p,*x=wa,*y=wb,*t;
      for(i=0; i<m; i++) Ws[i]=0;
      for(i=0; i<n; i++) Ws[x[i]=r[i]]++;//以字符的ascii码为下标
      for(i=1; i<m; i++) Ws[i]+=Ws[i-1];
      for(i=n-1; i>=0; i--) sa[--Ws[x[i]]]=i;
      for(j=1,p=1; p<n; j*=2,m=p){
            for(p=0,i=n-j; i<n; i++) y[p++]=i;
            for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j;
            for(i=0; i<n; i++) wv[i]=x[y[i]];
            for(i=0; i<m; i++) Ws[i]=0;
            for(i=0; i<n; i++) Ws[wv[i]]++;
            for(i=1; i<m; i++) Ws[i]+=Ws[i-1];
            for(i=n-1; i>=0; i--) sa[--Ws[wv[i]]]=y[i];
            for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++)
                  x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
      }
}

int sa[N],Rank[N],height[N];
//求height数组
/* str,sa,len */
void calheight(const char *r,int *sa,int n){
      int i,j,k=0;
      for(i=1; i<=n; i++) Rank[sa[i]]=i;
      for(i=0; i<n; height[Rank[i++]]=k)
            for(k?k--:0,j=sa[Rank[i]-1]; r[i+k]==r[j+k]; k++);
      for(int i=n;i>=1;--i) ++sa[i],Rank[i]=Rank[i-1];
}

char str[N];
bool vis[N][N];
int dp[N];
int main(){//字符串下标读入时0~len-1,height数组1~len
    int n,a,b;scanf("%d%d%d",&n,&a,&b);
    scanf("%s",str);
    int len=strlen(str);
    da(str,sa,len+1,130);   //sa
    calheight(str,sa,len);  //height rank

    rep(i,2,len){
        int r=Rank[i];
        int h=0;
        int mi=1e9;
        while(height[r]){
            mi=min(mi,height[r]);
            int id=sa[r-1];
            if(id>i){
                r--;continue;
            }
            h=max(h,min(mi,min(len-i+1,i-id)));
            r--;
        }
        r=Rank[i]+1;
        mi=1e9;
        while(r<=len&&height[r]){
            mi=min(mi,height[r]);
            int id=sa[r];
            if(id>i){
                r++;continue;
            }
            h=max(h,min(mi,min(len-i+1,i-id)));
            r++;
        }
        rep(j,i,i+h-1){
            vis[i][j]=1;
        }
    }
    /* 错误代码
    rep(i,2,len){
        if(height[i]){
            int x=sa[i],y=sa[i-1];
            int h=min(height[i],min(len-y+1,y-x));
            rep(l,1,h){
                rep(j,y,n){
                    int r=j+l-1;
                    if(r>n)break;
                    vis[j][r]=1;
                }
            }
        }
    }*/

    memset(dp,0x3f,sizeof dp);
    dp[0]=0;
    rep(i,0,n-1){
        dp[i+1]=min(dp[i+1],dp[i]+a);
        rep(j,i+1,n){
            if(vis[i+1][j]){
                dp[j]=min(dp[j],dp[i]+b);
            }
            else break;
        }
    }
    printf("%d\n",dp[n]);
}


别人家的思路

直接LCS求出 i i j j 两个前缀的公共后缀长度,再dp。

#include<bits/stdc++.h>
using namespace std;
char x[5009];
int dp[5009];
int lcs[5009][5009];
int main(){
    int n,a,b;
    scanf("%d%d%d",&n,&a,&b);
    scanf("%s",x+1);
    for(int i=1;i<=n;i++){
        dp[i]=dp[i-1]+a;
        for(int j=1;j<i;j++){
            lcs[i][j]=(x[i]==x[j]?lcs[i-1][j-1]+1:0);
            int len=min(lcs[i][j],i-j);
            dp[i]=min(dp[i],dp[i-len]+b);
        }
    }
    printf("%d\n",dp[n]);
}

猜你喜欢

转载自blog.csdn.net/jk_chen_acmer/article/details/88701425