ACM数位DP

介绍

数位DP往往都是给定一个闭区间 [l,r] ,求这个区间中满足某种条件的数的总数,这个区间可能很大,简单的暴力代码如下

int ans=0;
for(int i=l;i<=r;i++)
    if(check(i))
        ans++;

很容易发现,若区间长度超过 1e7,暴力枚举就会TLE了,而数位DP则可以解决这样的题型

数位DP实际上就是在数位上进行 dp

一般解法

数位DP就是换一种暴力枚举的方式,使得新的枚举方式符合dp的性质,然后预处理好即可

f(n) 表示 [0,n] 的所有满足条件的个数,那么对于 [l,r] 就可以用 [l,r]f( r )f (l−1) ,相当于前缀和思想

也就是说只要求出 f(n) 即可,那么数位DP关键的思想就是从树的角度来考虑,将数拆分成位,从高位到低位开始枚举

可以视 Nn 位数,那么拆分 N a n , a n − 1 , . . . , a 1 a_{n},a_{n-1},...,a_{1} an,an1,...,a1

然后开始分解建树,之后就可以预处理再求解 f(n)

经典例题

题目描述

定两个整数a和b,求[a,b]的所有数字中0∼9的出现次数

例如, a=1024,b=1032,则 a 和 b 之间共有 99 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中0出现10次,1出现10次,2出现7次,3 出现3次…

输入格式:

输入包含多组测试数据

每组测试数据占一行,包含两个整数a和b

当读入一行为0 0时,表示输入终止,且该行不作处理

输出格式:

每组数据输出一个结果,每个结果占一行

每个结果包含十个用空格隔开的数字,第一个数字表示0出现的次数,第二个数字表示1出现的次数,以此类推

数据范围:

0<a,b<1000000000

输入样例:

1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0

输出样例:

1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247

解题思路

需要预处理 f 数组,可以用 f[i,j,u] 表示 i 位,最高位为 j 的数拥有 u 的个数

那么如果 j 不等于 u 时,则 f[i][j][u]+=f[i−1][k][u],0≤k≤9 ,这个应该不难理解,因为这个状态就是由之前的状态得到的

而当 j 等于 u 时,那么同样也可以由之前的 9 个状态得到,为 f[i][j][u]+=f[i−1][k][u],0≤k≤9

注意,此时是还没有计算最高位的 u 个数的,因为最高位本身就为 u ,也是一种可能,所以需要加上,那么总共有 1 0 i − 1 10^{i-1} 10i1 多的数,所以增加的 u 的数量为 1 0 i − 1 10^{i-1} 10i1

初始状态就是 f[1][i][i]=1,到这,f 数组就初始化完了,接下来就是拆位分支的数位DP套路讨论了

题解代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+7;

int l,r;
int f[11][10][10];//预处理f数组,其中f[i][j][u]表示i位最高位为j的数拥有u的个数

void init(){
	for(int i=0;i<10;i++)
		f[1][i][i]=1;
	for(int i=2;i<11;i++)
		for(int j=0;j<10;j++)
			for(int u=0;u<10;u++){
		//判断j是否等于u
		if(j==u)f[i][j][u]+=pow(10,i-1);
		for(int k=0;k<10;k++)
			f[i][j][u]+=f[i-1][k][u];
	}
}
ll dp(int n,int u){
	//1~n,求u的出现次数
	if(!n)return u?0:1;//特判n是否为0.根据u的值确定返回值
	vector<int> nums;//存储分割后的位数
	while(n)nums.push_back(n%10),n/=10;
	int last=0;//last记录前面u出现的次数
	ll ans=0;//答案
	for(int i=nums.size()-1;i>=0;i--){
		int x=nums[i];
		//左边分支,0~x
		for(int j=(i==nums.size()-1);j<x;j++){
			//由于此题不能有前导0
			ans+=f[i+1][j][u];//注意这里i需要+1,因为我们i下标从0开始。而位数从1开始
		}
		//走左边分支,那么我们需要加上前面的个数。注意这里需要乘上x,因为左边分支有x中选择
		ans+=x*last*pow(10,i);
		if(x==u)last++;//记录last
		if(!i)ans+=last;//加上这个数本身含有的
	}
	//由于我们前面都是枚举n位数的,我们还需要统计所有0~n-1位数的方案数量
	//例如000011是不合法的,但11是合法的
	//这一步确实很容易忽略,没办法,数位DP就是这么难
	for(int i=1,len=nums.size();i<len;i++){
		for(int j=(i!=1);j<=9;j++){
			ans+=f[i][j][u];
		}
	}
	return ans;
}
int main(){
	init();
	while(cin>>l>>r&&(l||r)){
		if(l>r)swap(l,r);
		for(int i=0;i<=9;i++){
			cout<<dp(r,i)-dp(l-1,i);
			i==9?cout<<endl:cout<<" ";
		}
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花崽oyf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值