题解:P10959 月之谜

求一个区间内满足某种限制条件的数有多少个,是一个经典的数位 dp 问题。

\(f_{i,j,k,l}\) 表示由 \(i\) 位数字构成、各位数字之和是 \(j\)、对 \(k\) 取模余数是 \(l\) 的数有多少个。

在计算 \(f\) 时,允许前导 \(0\) 的存在,枚举第 \(i\) 位的数字 \(p\),得到状态转移方程:

\[f_{i,j,k,l} = \sum_{p=0}^{9} f_{i-1,j-p,k,(l - p \cdot 10^{i-1})\mod k} \]

闭区间 \([L, R]\) 种月之数的个数,等于 \([1, R]\) 种月之数的个数减去 \([1, L-1]\) 种月之数的个数。接下来以 \([1,R]\) 进行说明。

采取 “试填法” 的思想,从高位到低位给每一位填数,只要填了一个比上限 \(R\) 小的数位,那么后边的数位无论是多少,整个数值都不会超过 \(R\),此时就可以立即把 dp 预处理出的结果累加到答案中。只有在每一位上始终填写与上限 \(R\) 相同的数字时,才需要继续向后扫描,所以最终的计算量是数值的 “位数” 级别的,非常小。

我们枚举最终的各位数字之和 \(sum\),然后从左到右扫描每个数位,设当前正在处理第 \(i\) 位(最高位为第 \(N\) 位,最低位为第 \(1\) 位),当前已经填写的数字之和是 \(t\),当前数值对 \(sum\) 取模余数是\(q\),我们从小到大枚举第 \(i\) 位要填的数字 \(p\)

\(p\) 小于上限 \(R\) 在第 \(i\) 位上的数字,则后边 \(i - 1\) 位可以随便填,因为最终的数值能被 \(sum\) 整除,所以第 \(1 \sim i\) 位构成的数值对 \(sum\) 取模的余数应该是 \(sum - q\),因此答案直接累加 \(f_{i-1,sum-t-p,sum,(sum-q-p \cdot 10^{i-1}) \mod sum}\)

否则,令 \(t = t + p, q = q + p \cdot 10^{i-1} \mod sum\),开始处理第 \(i-1\)\([1, L-1]\) 同理。

代码:

#include<bits/stdc++.h>
using namespace std;
const int SIZE=1e5+10;
int L,R;
int dp[12][85][85][85],s[85][10]; 
int num[12];

int val(int pos,int sum,int mod,int x,bool flag) {
    if(x<sum) return 0;
    if(!flag) return dp[pos+1][x][x-sum][(x-mod)%x]; 
    if(pos==-1) return (mod==0&&sum==x);
    int res=0;
    for(int d=0; d<=num[pos]; d++) {
        bool e=(d==num[pos]);
        res+=val(pos-1,sum+d,(mod+(s[x][pos])*d)%x,x,e);
    }
    return res;
}

int Calc(int x) {
    int len=0,res=0;
    while(x) num[len++]=x%10,x/=10;
    for(int i=1; i<=81; i++) res+=val(len-1,0,0,i,true);
    return res;
}

void pre() {
    for(int i=1; i<=81; i++) {
        memset(dp[0][i],0,sizeof(dp[0][i]));
        dp[0][i][0][0]=1;
        s[i][0]=1%i;
        for(int j=1; j<=9; j++) s[i][j]=(s[i][j-1]*10)%i;
        for(int j=1; j<=9; j++)
            for(int k=0; k<=j*9; k++)
                for(int p=0; p<=i; p++)
                    for(int q=0; q<=9&&k>=q; q++) 
                        dp[j][i][k][p]+=dp[j-1][i][k-q][((p-s[i][j-1]*q)%i+i)%i];
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    pre();
    while(cin>>L>>R) cout<<Calc(R)-Calc(L-1)<<endl;
    return 0;
}

posted @ 2024-09-16 11:35  cly312  阅读(20)  评论(0)    收藏  举报