1. 程式人生 > >【[AHOI2009]同類分佈】

【[AHOI2009]同類分佈】

這是一篇有些賴皮的題解

(如果不賴皮的話,bzoj上也是能卡過去的)

首先由於我這個非常\(sb\)的方法複雜度高達\(O(171^4)\),所以面對極限的\(1e18\)的資料實在是卡死了

但是這個時候可以騙一下

一般來說肯定會有一個點的資料到達了\(1e18\),所以我們先將\(1\)\(1e18\)之間的答案算出來,這樣再去算另一個左邊界的話至少可以節省一半的常數,就算左邊界不是很小也有可能還算點希望

如果左邊界特別小的話,可能就能幸運的卡過去

這道題的左邊界就非常小啊,我估計不超過\(1e6\)

於是就卡過去了

再來看看我這個非常\(sb\)的dp,我覺得可能沒有人這麼寫

我們設\(dp[i][j][s][k]\)表示一個數填到了\(i\)位,最高位填的是\(j\),數位和是\(s\),且這些數中對於某一個數取模得\(k\)的數的個數

至於這個某一個數是什麼,我們當然是要最外面套上一個列舉數位和了

那麼答案很簡單啊,如果我們當前列舉的數位和是\(x\)的話,答案肯定就跟\(dp[][][x][0]\)有關係了

那麼這個方程怎麼轉移呢

顯然有

\[dp[i+1][p][j+p][(p*10^i+k)\%x]=\sum_{t=0}^9dp[i][t][j][k]\]

\(t\)表示上一位填的數,\(i\)是位數,\(p\)是這一位填的數,\(j\)是數位和,\(k\)

是對當前列舉的數位和取模之後的值,\(x\)表示當前列舉的數位和

同時我們發現好像直接去列舉\(t\)有些奢侈,我們可以直接把\(\sum_{t=0}^9dp[i][t][j][k]\)算好,於是我用\(dp[i][10][j][t]\)來存下來\(\sum_{t=0}^9dp[i][t][j][k]\),這樣就可以優化轉移了

之後就是數位\(dp\)的套路卡上界了,大概就是注意一下卡上界的時候存一下前面的數位和

複雜度大概是\(O((log_{10}n*9)^4)\),確實這是一個很垃圾的複雜度

程式碼

#include<iostream>
#include<cstring>
#include<cstdio>
#define re register
#define maxn 172
#define LL long long
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
LL dp[20][11][maxn][maxn];
LL L,R;
LL ans;
int num[2],a[20][2];
LL base[20];
LL mod;
inline LL qm(LL x) {return x>=mod?x-mod:x;}//優化一下取模
inline void spilt(LL x,int pd)
{
    num[pd]=0;
    while(x) a[++num[pd]][pd]=x%10,x/=10;
}//分解數位
inline void work(int x,int Len)
{
    mod=x;
    memset(dp,0,sizeof(dp));
    for(re int i=0;i<=9;++i)
        dp[1][i][i][qm(i)]+=1,dp[1][10][i][qm(i)]+=1;
    for(re int i=1;i<Len;++i)//列舉長度
        for(re int j=0;j<=min(x,i*9);++j)//列舉數位和
            for(re int k=0;k<x;++k)//列舉對當前列舉的數位和x取模後的值
                {
                    if(!dp[i][10][j][k]) continue;
                    for(re int p=0;p<=9;++p)
                        dp[i+1][p][j+p][(p*base[i]+k)%x]+=dp[i][10][j][k],dp[i+1][10][j+p][(p*base[i]+k)%x]+=dp[i][10][j][k];
                }
}
inline LL slove(int pd,int x)
{
    LL tot=0;
    for(re int i=1;i<num[pd];++i)
        tot+=dp[i][10][x][0]-dp[i][0][x][0];//統計所有位數小於給定數的,注意首位不能填0
    for(re int i=1;i<a[num[pd]][pd];++i)
        tot+=dp[num[pd]][i][x][0];//統計所有位數和給定數相同的,但是最高位小於給定數的
    LL now=a[num[pd]][pd],cnt=now;
    //now表示前面所有的數位和,cnt表示前面的數的值是多少
    //(比如說12345,卡到三這一位上,now=1+2=3,cnt=1*10+2*1=12)
    if(x-now<0) return tot;
    for(re int i=num[pd]-1;i;--i)//當前不同的那一位,[i+1,num]與x完全相同 
    {
        LL t=qm(x-cnt*base[i]%x);//根據算出後面的數位所需要的餘數是多少
        for(re int j=0;j<a[i][pd];j++)
            tot+=dp[i][j][x-now][t];
        //當前第i位可以填的數必須要小於給定數當前的這一位,這裡就按照dp的方式來統計答案
        now+=a[i][pd];
        cnt=cnt*10+a[i][pd];
        cnt=qm(cnt);
        if(x-now<0) break;
    }
    return tot;
}
int main()
{
    scanf("%lld%lld",&L,&R);
    spilt(L,0),spilt(R+1,1);
    base[0]=1;
    for(re int i=1;i<=18;++i) base[i]=base[i-1]*10;
    if(R==1000000000000000000) 
    {
        ans+=29410615796612778;
        for(re int i=1;i<=num[0]*9;++i)
            work(i,num[0]),ans-=slove(0,i);
        printf("%lld\n",ans);
        return 0;
    }//去掉這個if在bzoj上也能卡過去
    for(re int i=1;i<=num[1]*9;++i)//列舉數位和
        work(i,num[1]),ans+=slove(1,i)-slove(0,i);
    printf("%lld\n",ans);
    return 0;
}