P9640 [SNCPC2019] Digit Mode

P9640 [SNCPC2019] Digit Mode

很不常规的数位 DP。

下文记:

  • \(n\) 为原数,\(num\) 为实际填写的数。
  • \(m(num)\)\(num\) 各数位的众数。
  • \(c_i\) 为搜索过程中数码 \(i\) 填写的次数。
  • \(n\) 的最高位为 \(len\),最低位为 \(1\)

在 DFS 过程中计算 \(m(num)\),需要的上下文信息太多(比如整个 \(c\) 数组),状态数太大。

一个常用的技巧是“提前钦定”,即枚举 \(m(num)\) 的值 \(x\),以及该众数的出现次数 \(y\),看有多少个 \(num\) 满足条件(比如 P4127 同类分布 - 题解)。

在主函数中钦定,我们发现仍不好做。原因在于有前导零和当前位取值范围的限制。

既然如此,我们不妨挪到搜索过程中,在每个“既不是前导零,也没有取值范围限制”的数位上钦定。

具体来说,当前在填写数位 \(p\),若 !lim&&!zro,我们即可枚举众数 \(x\),再枚举其出现次数 \(y(\max\{1,c_x\}\le y\le c_x+p)\)

由于没有 limzro 的限制,所以每个数码的最多填写次数是固定的,更具体些:

\[limit_i=y-c_i-[i>x]\quad (i\ne m) \]

接下来我们要做的,是填充剩下的 \(p\) 位。

  • \(x\) 的次数已经固定为 \(y\),所以要占据 \(p\) 位中的 \(y-c_x\) 位。
  • 其他数的次数不固定,但必须 \(\le limit_i\)

第二条可以用类似多重背包的方式进行转移。记 \(f_{i,j}\) 为前 \(i\) 个数码,共占据了 \(j\) 个位置的方案数。则有转移(\(x\) 不参与,\(limit\) 只有 \(9\) 个位置):

\[f_{i,j}=\sum_{k=0}^{limit_i} f_{i-1,j-k}\times \binom{j}{k} \]

最终对答案的贡献为:

\[f_{9,p-(y-c_x)}\times \binom{p}{y-c_x} \]

下面是一个无关紧要的优化:

注意到一些同构,可令 \(F_{i,j}=\dfrac{f_{i,j}}{j!}\),则转移为:

\[F_{i,j}=\sum_{k=0}^{limit_i} \frac{F_{i-1,j-k}}{k!} \]

最终对答案的贡献为:

\[F_{9,p-(y-c_x)}\times \frac{p!}{(y-c_x)!} \]

这样来做,码量和常数都会小一些。

至于不满足 !lim&&!zro,就按正常的数位 DP 往下搜即可。

如果搜到了末尾,就暴力判定。

时间复杂度 \(O(B^4 N^3)\)

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define eb emplace_back
using namespace std;
const int N=55,P=1e9+7;
int t,a[N],len,c[10],fac[N],inv[N];
string s;
inline int qp(int x,int n){
	int a=1;
	while(n){
		if(n&1) a=a*x%P;
		x=x*x%P,n>>=1;
	}
	return a;
}
inline int bf(){
	int x=0,p=0;
	for(int i=9;~i;i--) if(c[i]>x) x=c[i],p=i;
	return p;
}
inline int calc(vector<int>& v,int s){//求方案数
	if(s<0) return 0;
	vector<int> f(s+1),g(s+1);
	f[0]=1;
	for(int i:v){
		g.clear();
		g.resize(s+1);
		for(int j=0;j<=s;j++){
			for(int k=0;k<=i&&j+k<=s;k++){
				(g[j+k]+=f[j]*inv[k])%=P;
			}
		}
		f=g; 
	}
	return f[s];
}
inline int dfs(int p,bool lim,bool zro){
	if(!p) return bf();
	int ans=0;
	if(!lim&&!zro){
		//枚举众数x及其出现次数y
		for(int x=1;x<10;x++){
			for(int y=max(1ll,c[x]);y<=c[x]+p;y++){
				vector<int> w;
				for(int k=0;k<x;k++) w.eb(y-c[k]);
				for(int k=x+1;k<10;k++) w.eb(y-c[k]-1);
				bool flg=1;
				for(int i:w) if(i<0){
					flg=0;
					break;
				}
				if(flg) ans+=fac[p]*inv[y-c[x]]%P*calc(w,p-y+c[x])%P*x;
			}
		}
	}else{
		int rig=lim?a[p]:9;
		for(int i=0;i<=rig;i++){
			bool tzro=zro&&!i;
			if(!tzro) c[i]++;
			ans+=dfs(p-1,lim&&i==rig,tzro);
			if(!tzro) c[i]--;
		}
	}
	return ans%P;
}
inline int solve(string& s){
	len=s.size();
	for(int i=0;i<len;i++) a[len-i]=s[i]-'0';
	return dfs(len,1,1);
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	fac[0]=inv[0]=1;
	for(int i=1;i<N;i++){//预处理阶乘及其逆元
		fac[i]=fac[i-1]*i%P;
		inv[i]=qp(fac[i],P-2);
	}
	cin>>t;
	while(t--){
		cin>>s;
		cout<<solve(s)<<"\n";
	}
	return 0;
}
posted @ 2025-11-08 08:30  Sinktank  阅读(11)  评论(0)    收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.