woj4320 sequence

题面

给定一个长度为$n$的由$[′0′..′9′]$成的字符串s$,v[i,j]$表示由字符串$s$第i到第$j$位组成的十进制数字。

将它的某一个上升序列定义为:将这个字符串切割成m段不含前导′0′的串,切点分别为$k_1,k_2…k_{m−1}$,使得$v[1,k1]<v[k_1+1,k_2]<…<v[k_{m−2},k_{m−1}]$。

请你求出该字符串s的上升序列个数,答案对$10^9+7$取模。

输入

第一行一个整数$n$,表示字符串长度;

第二行$n$个$[′0′..′9′]$内的字符,表示给出的字符串$s$。

输出

仅一行表示给出字符串$s$的上升序列个数对$10^9+7$取模的值。

样例

输入1

6
123434

输出1

8

输入2

8
20152016

输出2

4

解答

一道非常好的题

做法1.$30 ptr$

dfs爆搜,枚举每一个断点,剪剪枝,就过了,其实不剪枝也可以,因为$10!$不是特别大

做法2.$100ptr$

dp我一开始看到这道题的时候想到了加乘号那道题,用$sum[i][i]$数组表示$s[i…j]$构成的一些数,然后设计出如下方程式 \(dp[i][k] = \sum dp[i-k][len]\) 表示i结尾,最后一个数长度为k的方案数,其中保证i-k结尾长度为len的数大小小于i结尾,长度为k的数,复杂度$O(n^3)$ 但是这个思路是对的,我们考虑如何优化到n=5000的情况。用$dp[i][j]$表示i到j这个数为结尾的数的可能性的和(其实代码表示的是当且所有合法状态的和,为什么呢?因为这样可以用前缀和优化转移)。 那么dp[i][j]可以由哪些状态转移过来呢?首先如果前面一个数长度小于这个数,那么肯定可以转移,因为长度短,大小一定小。也就是说[i-1-(j-i+1)+1,i-1]都可以转过来,并且也不会去改变它的值,这里就可用前缀和优化,后面在细说。那么对于i-1-(j-i+1)+1可不可以转移过来呢?我们就要比大小了,怎么比?考虑一位一K=位地来(就向我写的DBigNumbers一样),复杂度$O(n)$,但是这样有点爆炸啊,复杂度$O(n^3)$,5000的范围就是870 0K也帮不了我,但是考虑改了事时限,改成了2s,我们考虑套个log是可以的。那从哪里优化log呢?首先,遍历状态是不可的,那么只能是比大小的时候优化!怎么优化呢?是个技巧.我们做个哈希,然后二分对于两个数[a,b][c,d]如果前半部分哈希值相等,说明前面一半是一样的。那么就在后面找不一样的地方。否则在前面找。找到以后,比较那个不一样的地方的大小就好了,复杂度$O(logN)$ 至于之前说的前缀和优化,我们发现[i-1-(j-i+1)+1,i-1]里面所有状态都可以直接转移,不需要一个一个计算,那么我们直接dp[i-1][j] - dp[i-1-(j-i+1)][j]就可以得到这些状态的结果的和了。但是在计算完dp[i][j]以后,要把dp[i-1][j]的值加进去,这样才是完整的维护前缀和。最后dp[n][n]就是答案。 代码如下

//
// Created by dhy on 19-1-20.
//

# include <iostream>
using namespace std;
const int MAXN = 5010;
const int mod = (int)1e9+7;
char c[MAXN];
int hsh[MAXN][MAXN];
long long dp[MAXN][MAXN];
int cmp(int a,int b){
    if(c[a]==c[b])return 0;
    if(c[a]<c[b])return -1;
    else return 1;
}
int cmp1(int a,int b,int i,int j){//二分比大小
    if(c[a]!=c[i])return cmp(a,i);
    if(hsh[a][b]==hsh[i][j]){//can't transfer when equal
        return 0;
    }
    int l = 1,r = b-a;
    while(l<r){
        int mid = (l+r)>>1;
        if(hsh[a][a+mid]==hsh[i][i+mid])l = mid+1;
        else r = mid;
    }
    return cmp(a+l,i+l);
}
int work(int l,int r){//这个地方是长度相等的时候即状态数计算,也是前缀和,只不过为了方便,打成函数
    return ((dp[l][r]-dp[l-1][r])%mod+mod)%mod;
}
int n;
int main(){
    cin>>n;
    cin>>c+1;
    for(int i = 1;i<=n;i++){//预处理把哈希处理出来
        hsh[i][i] = c[i]-'0';
        for(int j = i+1;j<=n;j++){
            hsh[i][j] = (hsh[i][j-1]*10+c[j]-'0')%mod;
        }
    }
    for(int i = 1;i<=n;i++)dp[1][i] = 1;

    for(int i = 2;i<=n;i++){
        for(int j = i;j<=n;j++){
            int r = i-1,l = r-j+i;//可以直接转移的区间
            dp[i][j] = ((dp[r][r]-dp[max(l,0)][r])%mod+mod)%mod;//转移,前缀和优化
            if(l>=1&&cmp1(l,r,i,j)==-1){//对于长度相等的两个区间可否转移,小于即可转移
                dp[i][j] = (dp[i][j]+work(l,r))%mod;
            }
            if(c[i]=='0')dp[i][j] = 0;//如果是0开头,就不是合法的状态,计数清空
            dp[i][j] = (dp[i-1][j]+dp[i][j])%mod;//顺便维护前缀和
        }
    }
    cout<<dp[n][n];//计算答案
    return 0;
}