数位dp学习笔记

数位dp

一、思想

将一个数字按照数位拆开,接着关注每一位数字。

在信息学题目中,有关数位dp问题通常具有以下特征:

  • 可以通过数位思想设计动态规划算法。
  • 大部分数位dp用以解决“给定区间内符合一个条件的数的个数”一类计数问题。
  • 上界很大,枚举会超时。

数位dp通常可以用计数问题的技巧如 [ l , r ] = [ 0 , r ] − [ 0 , l − 1 ] [l,r]=[0,r]-[0,l-1] [l,r]=[0,r][0,l1] 等。

数位中,无论是1~3位、10~12位,还是1000~1002位,性质在大多数题目中都相同。

所以通常将一些数据处理在一个通用的数组内,然后利用这个数组加加减减乘乘除除求解答案。

求解答案的过程可以采用记忆化搜索,或循环dp,具体视具体题目的实现难度而定。

接下来看几道题目。

二、例题

例1:P2602 [ZJOI2010] 数字计数

给定两个正整数 l l l r r r ,求在 [ l , r ] [l,r] [l,r] 中的所有整数中,数码0~9各出现了多少次。

1 ≤ l ≤ r ≤ 1 0 12 1\le l\le r\le 10^{12} 1lr1012

此题可以通过 [ 0 , r ] − [ 0 , l − 1 ] [0,r]-[0,l-1] [0,r][0,l1] 求解,接下来记录思路。

要注意,第 i i i 位的位权是 1 0 i − 1 10^{i-1} 10i1 i i i 位数共有 1 0 i 10^i 10i 个。

为了方便书写,规定以下记号:

  • g ( n , k ) g(n,k) g(n,k) 表示 n n n 1 1 1 ~ k k k 位所构成的数。

  • a i a_i ai 表示数字 a a a 从低到高的第 i i i 位数字。

  • 发现在一个 i i i 位数中,每个数码的出现次数相同,记为 f i f_i fi 。显然有 f i = 10 f i − 1 + 1 0 i − 1 f_i=10f_{i-1}+10^{i-1} fi=10fi1+10i1

不难想到,答案可以表示为“1位数中每个数码的出现次数+2位数中……+ log ⁡ 10 n \log_{10}n log10n 位数中每个数码的出现次数”。

问题在于 i i i 位数中每个数码的出现次数。

分两类情况讨论:

  • i i i 位数不取上界:

    0 ∼ a i − 1 0\sim a_i-1 0ai1 a i a_i ai 种取法。带来两种贡献: i − 1 i-1 i1 位数拿满共 f i − 1 × a i f_{i-1}\times a_i fi1×ai 次、取的每个数各出现了 1 0 i − 1 10^{i-1} 10i1 次。

  • i i i 位数取上界:

    上界出现了 g ( 原数 , i − 1 ) g(原数,i-1) g(原数,i1) 次。

问题看似解决,实际上写完代码会发现0的计数多了许多。

原因很简单,在情况一的第二种贡献中0作为前导零被计入了答案。

于是减去0的计数。

目前有一个问题,当 a i = 0 a_i=0 ai=0 ,直观上来说会多减去0的计数,但是过了,判掉就WA,不知为甚。

此题精华在于 f f f

代码见文末。

例2:

Code

例1:

#include<cstdio>
#define ll long long
using namespace std;
const int N=15;
ll l,r,p10[N],ans[2][N],f[N],a[N];
void init() {
	p10[0]=1;
	for(int i=1;i<=14;i++) {
		p10[i]=p10[i-1]*10;
	}
	for(int i=1;i<=14;i++) {
		f[i]=10*f[i-1]+p10[i-1];
	}
}
void solve(ll x,int kd) {
	ll xx=x;
	int len=0;
	while(x) {
		a[++len]=x%10;
		x/=10;
	}
	for(int i=len;i>=1;i--) {
		for(int j=0;j<=9;j++) {
			ans[kd][j]+=f[i-1]*a[i];
		}
		for(int j=0;j<a[i];j++) {
			ans[kd][j]+=p10[i-1];
		}
		xx-=a[i]*p10[i-1];
		ans[kd][a[i]]+=xx+1;
//		if(a[i]!=0)
		ans[kd][0]-=p10[i-1];
	}
}
int main() {
	init();
	scanf("%lld%lld",&l,&r);
	solve(l-1,0),solve(r,1);
	for(int i=0;i<=9;i++) {
		printf("%lld ",ans[1][i]-ans[0][i]);
	}
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值