数位 DP 学习笔记

数位统计 DP 是与数字相关的一类计数问题。在这类题目中,一般给定一些限制条件,求满足限制条件的第 K 小的数是多少,或者求在区间 [L,R] 内有多少个满足限制条件的数。

计数问题

给定两个正整数 ab,求在 [a,b] 中的所有整数中,09 中每个数各出现了多少次。

数据范围

对于 30% 的数据,保证 ab106

对于 100% 的数据,保证 1ab1012

思路

f[x,k] 表示 k1x 中出现的次数,那么原问题就可以转化为求 f[b,k]f[a1,k](0k9)

考虑当 x=abcdefg¯ 时。假设现在要让第 4 位为 k。那么就可以分两种情况来讨论。

1.前面三位填的数小于 abc,那么此时第 4 位填 k。后面的位数怎么填都可以满足题意,那么对答案的贡献就是 abc103

2.前面三位填的数等于 abc,那么此时如果 k=d,那么后面三位填的数就必须小于等于 efg ,对答案的贡献就是 efg+1(还有一个 000);如果此时 k<d,那么后面怎么填都可以,对答案的贡献就是 103;如果 k>d,则对答案没有贡献。

其他位置同理。当然填第一位时不考虑第一种情况。

当然,0 需要特殊考虑一下,0 只能从第二位开始填起,同时在情况二时,前面填的数不能为 0,因为本题中不考虑前导零。

code:

#include<cstdio>
#include<vector>
using namespace std;
#define int long long
int power(int a,int b){int res=1;while(b--) res*=a;return res;}
int get(vector<int> nums,int l,int r){int res=0;for(int i=l;i>=r;i--) res=res*10+nums[i];return res;}
int count(int n,int x)
{
	vector<int> nums;
	while(n)
	{
	    nums.push_back(n%10);
	    n/=10;
	}
	n=nums.size();
	int res=0;
	for(int i=x!=0?n-1:n-2;i>=0;i--)
	{
		if(i<n-1)
		{
			res+=get(nums,n-1,i+1)*power(10,i);
			if(x==0) res-=power(10,i);
		}
		if(nums[i]==x) res+=get(nums,i-1,0)+1;
		if(nums[i]>x) res+=power(10,i);
	}
	return res;
}
signed main()
{
	int a,b;
	scanf("%lld%lld",&a,&b);
	for(int i=0;i<10;i++) printf("%lld ",count(b,i)-count(a-1,i));
	puts("");
	return 0;
}

度的数量

求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于 K 个互不相等的 B 的整数次幂之和。

例如,设 X=15,Y=20,K=2,B=2,则有且仅有下列三个数满足题意:

17=24+20

18=24+21

20=24+22

数据范围

1XY2311

1K20

2B10

思路

分析一下题面,可以发现一个数 x 满足题意,当且仅当 x[X,Y],并且 xB 进制表示是一个 1 的个数为 K01 序列。

和上一题类似,先求出 [1,Y] 中满足题意的数的个数,再求出 [1,X] 中满足题意的数的个数。(数位 DP 常用技巧,利用前缀和的思想,将求区间内的数转化为两个区间内数相减)

f[N] 表示 1N 中满足题意的数的个数。。

NB 进制表示下的第 i 位为 a,前面的数已经填了 tot1,当:

a=0,那么构造的数中这一位只能为 0,后面也不能随便填,因为可能最终构造的数会大于 N。所以要交给下一位处理;

a=1,如果构造的数这一位填 0 时,后面怎么填剩下的 ktot1,最终的数都不会大于 N;对答案的贡献也就是 Ci1ktot;如果构造的数这一位填 1。那么就和第一种情况类似,交给下一位处理,同时也要 tot++

a>1,这一位填 0 和第二种情况一样,但是这一位填 1 时,后面怎么填还是在 [1,N] 中。对答案的贡献就是 Cx1ktot1。此时后面的位数也就没必要枚举了。因为枚举下一位的原因是无法保证后面怎么填都在范围内。此时已经保证了,就不需要枚举了。

a>1
通过上面这种关系可以发现,所有的情况构成一棵,如下图所示。

最终的答案就是所有叶子节点的方案相加。

code:

#include<cstdio>
#include<vector>
using namespace std;
const int N=35;//二进制最多有32位 
int x,y,k,b,f[N][N];
void init()
{
	for(int i=0;i<N;i++)
	    for(int j=0;j<=i;j++)
	        if(!j) f[i][j]=1;else f[i][j]=f[i-1][j-1]+f[i-1][j];
}
int query(int n)
{
	int res=0,tot=0;
	vector<int> nums;
	nums.clear();
	while(n){nums.push_back(n%b),n/=b;}
	for(int i=nums.size()-1;i>=0;i--)
	{
		if(nums[i])
		{
			res+=f[i][k-tot];
			if(nums[i]>1)
			{
				res+=f[i][k-tot-1];
				break;
			}
			tot++;
			if(tot>k) break;
		}
		if(!i&&tot==k) res++;//考虑深度最深的右叶子节点
	}
	return res;
}
int main()
{
	init();
	scanf("%d%d%d%d",&x,&y,&k,&b);
	printf("%d\n",query(y)-query(x-1));
	return 0;
}

数字游戏

定义一个数字为不下降数,当且仅当这个数从左到右各位数字呈非下降关系,如 123446

给定一个整数闭区间 [a,b],求这个区间内有多少个不降数。

数据范围

1ab2311

思路

还是将问题转化为区间差。

h[N] 表示 1N 中的不下降数的个数。分类讨论,画出树状关系图。

显然,当第 i 位填的数小于 ai 时,后面的数只要满足非严格单调递增即可。考虑预处理出这些方案数

f[i][j] 表示数位为 i,且最高位为 j 的不下降数的个数。易得状态转移方程:

f[i][j]=k=j9f[i1][k]

最后,别忘了特判 N 本身是否是一个非下降数。

code:

#include<cstdio>
#include<vector>
using namespace std;
const int N=15;
int a,b,f[N][N];
void init()
{
	for(int i=0;i<=9;i++) f[1][i]=1;
	for(int i=2;i<=10;i++)
	    for(int j=0;j<=9;j++)//可以优化,但没必要 
		    for(int k=j;k<=9;k++)
			    f[i][j]+=f[i-1][k];  
}
int query(int n)
{
	if(!n) return 1;//注意特判0 
	vector<int>nums;
	nums.clear();
	while(n) nums.push_back(n%10),n/=10;
	int res=0; 
	nums.push_back(0);
	for(int i=nums.size()-2;i>=0;i--)
	{
	    
		for(int j=nums[i+1];j<nums[i];j++) res+=f[i+1][j];
		if(i&&nums[i]>nums[i-1]) break; 
		if(!i) res++;//特判自身 
	}
	return res;
}
int main()
{
	init();
	while(scanf("%d%d",&a,&b)!=EOF) printf("%d\n",query(b)-query(a-1));
	return 0;
}

[SCOI2009] windy 数

不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。求 [a,b] 中 windy 数的数量。

数据范围

1ab2×109

思路

和上一道题类似。需要预处理出一个数组。

f[i][j] 表示一共有 i 位,最高位为 j 的 windy 数的个数。得到转移方程:

f[i][j]=0j2f[i1][k]+j+29f[i1][k]

但是需要注意,上一道题填 0000¯ 前导零对后面的数是没有影响,但是本题就不一样了,因为如果和上一题枚举方式相同,那么形如 03574¯ 这样的数字就不会被计算为 windy 数,所以需要特判一下位数小于 N 的数,由于最高位填的是 0,后面怎么填都不会超过 N。直接统计 i=1|n|1j=19f[i][j] 即可。

code:

#include<cstdio>
#include<vector>
using namespace std;
const int N=15;
int f[N][N],a,b;
int abs(int a){return a>0?a:-a;}
void init()
{
	for(int i=0;i<=9;i++) f[1][i]=1;//用到f[1][0] 时前面肯定不会是0
	for(int i=2;i<=10;i++)
	    for(int j=0;j<=9;j++)
		    for(int k=0;k<=9;k++)
			    if(abs(j-k)>=2) f[i][j]+=f[i-1][k]; 
}
int query(int n)
{
	int res=0;
	vector<int>nums;
	nums.clear();
	while(n) nums.push_back(n%10),n/=10;
	nums.push_back(-1);//因为最高位不能是0
	for(int i=nums.size()-2;i>=0;i--)
	{
		for(int j=0;j<nums[i];j++)
		    if(abs(j-nums[i+1])>=2) res+=f[i+1][j];
		if(abs(nums[i]-nums[i+1])<2) break;
		if(!i) res++;
	} 
	for(int i=1;i<nums.size()-1;i++)
	    for(int j=1;j<=9;j++) res+=f[i][j];
	return res;
}
int main()
{
	init();
	scanf("%d%d",&a,&b);
	printf("%d\n",query(b)-query(a-1));
	return 0;
}

数字游戏 II

定义一个数为取模数,当且仅当这个数字各位上之和 modN=0

给定 a,b,求在 [a,b] 内的取模数个数。

数据范围

1a,b2311,

1N<100

思路

f[i][j][k] 表示一共有 i 位,最高位为 j,各位上的数字之和模 Nk 的数字的个数。得到状态转移方程:

f[i][j][k]=t=09f[i1][t][(kj)%P]

假设前面位填的数字之和是 sum,当前位填的是 x,后面几位需要填的数字之和为 res

就有 (sum+x+res)modN=0,移项得 (res+x)(sum)(modN)

对答案的贡献就是 f[i][j][(sum)%N]

code:

#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
const int N=15;
const int M=110;
int f[N][N][M],P;
int mod(int a,int b){ return (a%b+b)%b;}//处理负数
void init()
{
    memset(f,0,sizeof(f));//多测不清空,爆零两行泪
	for(int i=0;i<=9;i++) f[1][i][i%P]=1;
	for(int i=2;i<=10;i++)
	    for(int j=0;j<=9;j++)
	        for(int k=0;k<P;k++)
	            for(int t=0;t<=9;t++)
	                f[i][j][k]+=f[i-1][t][mod(k-j,P)];
}
int query(int n)
{
	if(!n) return 1;
	int res=0;
	vector<int>nums;
	nums.clear();
	while(n) nums.push_back(n%10),n/=10;
	int sum=0;
	for(int i=nums.size()-1;i>=0;i--)
	{
		for(int j=0;j<nums[i];j++) 
		    res+=f[i+1][j][mod(-sum,P)];
		sum+=nums[i];
		if(!i&&sum%P==0) res++; 
	}
	return res;
}
int main()
{
	int a,b;
	while(scanf("%d%d%d",&a,&b,&P)!=EOF)
	{
		init();//模数不同,所以需要多次预处理
		printf("%d\n",query(b)-query(a-1)); 
	}
	return 0;
}

不要62

不吉利的数字为所有含有 462 的号码。例如:62315,73418,88914 都属于不吉利号码。但是,61152 虽然含有 62,但不是连号,所以不属于不吉利数字之列。

给出 n,m,求在 [n,m]含有不吉利的数的个数。

数据范围

1nm109

思路

定义 f[i][j] 表示位数为 i,最高位为 j 的不含有不吉利的数的的个数。得到状态转移方程:

f[i][j]=k=09f[i1][k](j4,k4,j10+k62)

其他的步骤就和上面差不多了。。。所以其实数位 DP 的套路基本上都一样。

code:

#include<vector>
#include<cstdio>
using namespace std;
const int N=15;
int f[N][N],a,b;
void init()
{
	for(int i=0;i<=9;i++)
	    if(i!=4) f[1][i]=1;
	for(int i=1;i<=10;i++)
	    for(int j=0;j<=9;j++)
	    {
	    	if(j==4) continue;
	    	for(int k=0;k<=9;k++)
	    	{
	    		if(k==4||j*10+k==62) continue;
	    		f[i][j]+=f[i-1][k];
			}
		}
}
int query(int n)
{
	if(!n) return 1;
	vector<int> nums;
	nums.clear();
	while(n) nums.push_back(n%10),n/=10;
	nums.push_back(114514);//不影响最高位 
	int res=0;
	for(int i=nums.size()-2;i>=0;i--)
	{
		for(int j=0;j<nums[i];j++)
	    {
	    	if(j==4||nums[i+1]*10+j==62) continue;
	    	res+=f[i+1][j];
		}
		if(nums[i+1]*10+nums[i]==62||nums[i]==4) break;
		if(!i) res++;
	}
	return res;
}
int main()
{
	init();
	while(scanf("%d%d",&a,&b),a||b) printf("%d\n",query(b)-query(a-1));
	return 0;
}

恨7不成妻

如果一个整数符合下面三个条件之一,那么我们就说这个整数和 7 有关:

1.整数中某一位是 7

2.整数的每一位加起来的和是 7 的整数倍;

3.这个整数是 7 的整数倍。

求一个区间内和 7 无关的整数的平方和。

数据范围

1T50,

1LR1018

思路

本题的限制条件特别多,对于第一个条件显然很容易限制,而对于后面两个条件,就有一些繁琐了。

f[i][j][a][b] 表示位数为 i,最高位的数为 j,这个数本身对 7 取模等于 a。这个数各个数位上之和对 7 取模等于 b 的所有数的平方和

那么所有可以转移的状态就是 f[i1][k][(aj10i1)%7][(bj)%7]

设这些转移的数分别为 A1,A2,,At

那么转移就是 f[i][j][a][b]=k=1t(j10i1+Ak)2

展开,得到 (j10i1)2t+2j10i1k=1tAk+k=1tAk2

同理,一次方和为:

k=1t(j10i1+Ak)=tj10i1+k=1tAk

所以,如果把 t 看成 Ak0 次方之和,f 中需要记录的就是 Ak0,1,2 次方。

然后就到了激动人心的填数环节。

设当前填的是第 i 位,前面填的数是 lasta=xyz¯,前面填的各个数位之和是 lastb=x+y+z。那么当前位 j 和前面的 a 以及 b 就需要满足:

1.(lasta10i+j10i1+a)mod70

2.(lastb+j+b)mod70

对两个式子移项,得到:

1.(j10i1+a)mod7(lasta10i)mod7

2.(j+b)mod7(lastb)mod7

最后就可以愉快地写代码了。

#include<cstdio>
#include<vector>
using namespace std;
#define int long long
const int N=25;
const int p=1e9+7;
struct node{
	int s0,s1,s2;
}f[N][10][7][7];
int l,r;
int p7[N],p9[N];//10^i 对7取模,对1e9+7取模 
int mod(int a,int b){return (a%b+b)%b;}//处理负数取模 
void init()
{
	p7[0]=p9[0]=1;
	for(int i=1;i<N;i++) p7[i]=p7[i-1]*10%7,p9[i]=p9[i-1]*10ll%p;
	for(int i=0;i<=9;i++)
	{
		if(i==7) continue;
		f[1][i][i%7][i%7].s0++;f[1][i][i%7][i%7].s1+=i;f[1][i][i%7][i%7].s2+=i*i;
	}
	for(int i=2;i<N;i++)
	    for(int j=0;j<=9;j++)
	    {
	    	if(j==7) continue;
	    	for(int a=0;a<7;a++)
	            for(int b=0;b<7;b++)
	            {
	                for(int k=0;k<=9;k++)
	                {
	                	if(k==7) continue;
	                	node &v1=f[i][j][a][b],&v2=f[i-1][k][mod(a-j*p7[i-1],7ll)][mod(b-j,7ll)];
	                	v1.s0=(v1.s0+v2.s0)%p;
	                	v1.s1=(v1.s1+p9[i-1]*v2.s0%p*j%p+v2.s1)%p;
	                	v1.s2=(v1.s2+j*p9[i-1]%p*j%p*p9[i-1]%p*v2.s0%p+2ll*j*p9[i-1]%p*v2.s1%p+v2.s2)%p;
					}
	            }
	                
		}
	        
}
node get(int i,int j,int a,int b)//注意这里是不等于a,b 
{
	node tmp;
	tmp.s0=tmp.s1=tmp.s2=0;
		for(int x=0;x<7;x++)
		{
			if(x==a) continue;
			for(int y=0;y<7;y++)
			{
				if(y==b) continue;
				tmp.s0=(tmp.s0+f[i][j][x][y].s0)%p;
				tmp.s1=(tmp.s1+f[i][j][x][y].s1)%p;
				tmp.s2=(tmp.s2+f[i][j][x][y].s2)%p;
			}
		}
	return tmp;
}
int query(int n)
{
	if(!n) return 0;
	vector<int> nums;
	nums.clear();
	while(n) nums.push_back(n%10),n/=10;
	int last_a=0,last_b=0,res=0;
	for(int i=nums.size()-1;i>=0;i--)
	{
		for(int j=0;j<nums[i];j++)
		{
		    if(j==7) continue;
			int a=mod(-last_a*p7[i+1],7ll);
			int b=mod(-last_b,7ll);
			node v=get(i+1,j,a,b);
			res=(res+(last_a%p)*(last_a%p)%p*p9[i+1]%p*p9[i+1]%p*v.s0%p+last_a%p*v.s1%p*p9[i+1]%p*2ll%p+v.s2)%p;//注意相乘顺序,不然很容易溢出
		}
		if(nums[i]==7) break;
		last_a=last_a*10ll+nums[i];
		last_b+=nums[i];
		if(!i&&last_a%7&&last_b%7)
		{
			last_a%=p;
			res=(res+last_a*last_a%p)%p;
		}
	}
	return res;
}
signed main()
{
	init();
	int T;scanf("%lld",&T);
	while(T--) scanf("%lld%lld",&l,&r),printf("%lld\n",mod(query(r)-query(l-1),p));
	return 0;
} 
posted @   曙诚  阅读(99)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示