解题报告-洛谷P2481 [SDOI2010] 代码拍卖会

P2481 [SDOI2010] 代码拍卖会

题目描述

随着 iPig 在 P++ 语言上的造诣日益提升,他形成了自己一套完整的代码库。猪王国想参加 POI 的童鞋们都争先恐后问 iPig 索要代码库。iPig 不想把代码库给所有想要的小猪,只想给其中的一部分既关系好又肯出钱的小猪,于是他决定举行了一个超大型拍卖会。

在拍卖会上,所有的 \(N\) 头小猪将会按照和 iPig 的好感度从低到高,从左到右地在 iPig 面前站成一排。每个小猪身上都有 \(9\) 猪币(与人民币汇率不明),从最左边开始,每个小猪依次举起一块牌子,上面写上想付出的买代码库的猪币数量(\(1\)\(9\) 之间的一个整数)。大家都知道,如果自己付的钱比左边的猪少,肯定得不到梦寐以求的代码库,因此从第二只起,每只猪出的钱都大于等于左边猪出的价钱。最终出的钱最多的小猪(们)会得到 iPig 的代码库真传,向着保送 PKU(Pig Kingdom University)的梦想前进。

iPig 对自己想到的这个点子感到十分满意,在去现场的路上,iPig 就在想象拍卖会上会出现的场景,例如一共会出现多少种出价情况之类的问题,但这些问题都太简单了,iPig 早已不感兴趣了,他想要去研究更加困难的问题。iPig 发现如果他从台上往下看,所有小猪举的牌子从左到右将会正好构成一个 \(N\) 位的整数,他现在想要挑战的问题是所有可能构成的整数中能正好被 \(P\) 整除的有多少个。由于答案过大,他只想要知道答案 \(\bmod\ 999911659\) 就行了。

输入格式

有且仅有一行两个数 \(N\ (1 \le N \le 10^{18})\)\(P\ (1 \le P \le 500)\),用一个空格分开。

输出格式

有且仅有一行一个数,表示答案除以 \(999911659\) 的余数。

输入输出样例 #1

输入 #1

2 3

输出 #1

15

说明/提示

样例解释

方案可以是:\(12,15,18,24,27,33,36,39,45,48,57,66,69,78,99\),共 \(15\) 种。

数据规模


解题报告

最开始的时候觉得:这不明显的数位DP吗?感觉和P4127 [AHOI2009] ]同类分布 - 洛谷差不多,然后一看数据范围: \(10^{18}\)!!!正常的数位DP连数组都开不下!!!

在这个范围内直接解决问题是不现实的,于是开始考虑怎么缩小实际的范围。先从这个题目的性质开始。一个满足题目要求的合法的数字显然是形如 $11111222222223333 \cdots 8899999 $ 的数,这时候,就有一个套路的想法(第一个想到的人太神了,我根本想不到):可以把这个数拆成若干个只由连续的的 \(1\) 组成的数相加。

具体的,例如一个数 \(1111222334\),它可以有如下变换:

\[1111222334=1111111111+0000111111+0000000111+0000000001 \]

那么,我们就可以把任意一个合法的数分割成若干个形如 \(0000 \cdots 111\) 的数相加,为了方便我们称一个形如\(000 \cdots 111\)的数为 \(S_k\),其中,\(k\) 表示有从低位开始的连续的 \(1\) 的个数。例如,我们将数 \(000111111111\) 称为 \(S_9\)

这样做有一个好处:一个数最多只会由 \(9\) 个这样的数组成。这时,一个暴力的想法出现了:因为只能选 \(9\)\(S_k\),我们就可以跑一个 \(O(n^9)\) 的暴力。当然,这样打一点分的得不到。

但这样给了我们一点思路,观察以下暴力的过程,相当于在进行一个完全背包:总共有 \(n\) 个物品,背包容量为 \(9\),第 \(k\) 物品的价值为 \(S_k\),体积为 \(1\)。可以做一个背包的转移。

但是,\(n\) 实在是太大了,这样做依然不能完全 AC。

我们尝试减少物品数量。实际上,由于同余运算的性质:\((x+y) \mod n = ( ( x \mod n )+(y \mod n) ) \mod n\) ,我们并不关心 \(S_k\) 具体是什么样子,而是关心它在模 \(P\) 下的余数,我们可以预处理出所有可能的 \(S_k\) 在模 \(P\) 情况下的余数,通过这些余数,我们就可以知道一个合法的数是否可以被 \(P\) 整除。

那么,我们将看到,实际上有很多的 \(S_k\) 的余数相同,意味着它们对答案的贡献是相同的。我们可以将相同余数的 \(S_k\) 归为一个同余类,然后通过组合数学求出每一个同余类对答案的贡献。题目中的 \(P\) 不到 \(500\),意味着我们实际的物品数最多也就是 \(500\),相比于之前的 \(10^{18}\),这少了可不止一点。

接下来具体讨论怎么DP:

我们记 某一个\(S_k\) 在模 \(P\) 的意义下的余数为 \(i\),选了多少个 \(S_k\) 记为 \(j\),余数为 \(i\)\(S_k\) 的个数记为 \(g_i\)

那么可以设状态数组为 \(dp[i][j][s]\) ,表示考虑到余数 \(i\),目前的余数和在模 \(P\) 后的结果为 \(j\)。当前选了 \(s\)\(S_k\)

对于余数 \(i\),我们假设从其同余类中选了 \(t\)\(S_k\),其中 \(s+t \leq 9\)

那么就有:

\[dp[i+1][ (j+t \times i) \mod P ][s+t]+=\sum \binom {g_i+t-1}{t} \times dp[i][j][s] \]

其中的 $ \dbinom {g_i+t-1}{t} $ 表示从 \(g_i\) 个不同的 \(S_k\) 中可重复的选 \(t\) 个数的方案数,这是一个经典的结论。

由于 \(t\) 很小,我们直接用公式 \(\dbinom {g_i+t-1}{t}=\dfrac {\amalg_{x=0}^{t-1} (g_i+x)}{t!}\) 计算出所有的 \(\dbinom {g_i+t-1}{t}\),存在数组 \(cc[i][t]\) 中。

还剩下最后一个小问题:怎么计算 \(g_i\)

对于 \(n \leq P\),我们可以直接暴力求解 \(g_i\)。枚举每一个 \(S_k\),进行统计。

对于 \(n > P\),对于现在的 \(S_k\),设它模 \(P\) 的余数 \(r_k\)\(i\),那么下一个数 \(S_{k+1}=S_{k} \times 10+1\),它模 \(P\) 的余数 \(r_{k+1}\)\((r_{k} \times 10+1) \mod P\),如果一对 \((S_{k1},S_{k2})\),满足 \(r_{k1}==r_{k2}\),那么有 \(r_{k1} \times 10 +1==r_{k2} \times 10 +1\),得 \((r_{k1} \times 10 +1) \equiv (r_{k2} \times 10 +1) \pmod{P}\),最终有 $ S_{k1+1} \equiv S_{k2+1} \pmod{P} $。不断递推下去,可以发现 \(r_k\) 是在不断的循环,

那么就可以求出循环节起始位置和长度,通过数学计算快速求出 \(g_i\)

还有一些细节,详见代码,算法复杂度为 \(O(9^{2} P^{2})\)

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=999911659;
const int N=511;
const int M=10;

inline int read()
{
	int f=1,x=0; char ch=getchar();
	while(!isdigit(ch)) { if(ch=='-') f=-1; ch=getchar(); }
	while(isdigit(ch))  { x=x*10+ch-'0';    ch=getchar(); }
	return f*x;
}

int n,P;
int g[N],nR;
int dp[N][N][M];
int cc[N][9];

int pa[N];
inline void calc_g()
{
    if(n<=P)
    {
        int r=0;
        for(int i=1;i<=n;i++)
          r=(r*10+1)%P,g[r]++;
        nR=r;
        return ;
    }

    int r=1%P,len=0,st=0;
    for(int i=1;i<=P+1;i++)
    {
        if(pa[r]) { st=pa[r];len=i-st;break; }
        pa[r]=i,g[r]++,r=(r*10+1)%P;
    }
    for(int i=0;i<P;i++)
      if(pa[i] && pa[i]>=st)
      {
          g[i]+=(n-pa[i])/len%mod;
          g[i]%=mod;
          if( (pa[i]-st+1)%len==(n-st+1)%len )
            nR=i;
      }
}

inline int FastPow(int x,int k)
{
    int tot=1;x%=mod;
    while(k)
    {
        if(k&1) tot=(tot*x)%mod;
        x=(x*x)%mod;
        k>>=1;
    }
    return tot;
}

int fact[N],infact[N];
inline void GetFact()
{
    fact[0]=1;
    for(int i=1;i<N;i++)
     fact[i]=fact[i-1]*i%mod;
    infact[N-1]=FastPow(fact[N-1],mod-2);
    for(int i=N-2;i>=0;i--)
     infact[i]=infact[i+1]*(i+1)%mod;
}

signed main()
{
    // freopen("P2481.in","r",stdin);
	// freopen("P2481.out","w",stdout);
    n=read(),P=read();
    calc_g();
    GetFact();
    for(int i=0;i<P;i++)
    {
        cc[i][0]=1;
        for(int t=1;t<9;t++)
        {
            cc[i][t]=1;
            for(int j=0;j<t;j++)
              cc[i][t]=cc[i][t]*( (g[i]+j)%mod )%mod;
            cc[i][t]=cc[i][t]*infact[t]%mod;
        }
    }

    dp[0][nR][0]=1;
    for(int i=0;i<P;i++)
     for(int j=0;j<P;j++)
      for(int k=0;k<9;k++)
       for(int t=0;t<9-k;t++)
         dp[i+1][(j+t*i)%P][k+t]=(dp[i+1][(j+t*i)%P][k+t]+dp[i][j][k]*cc[i][t]%mod)%mod;
    int ans=0;
    for(int i=0;i<9;i++)
      ans=(ans+dp[P][0][i])%mod;
    printf("%lld",ans);
	return 0;
}

posted @ 2025-09-02 20:33  南北天球  阅读(10)  评论(0)    收藏  举报