动态规划:数位DP

数位DP是一种针对于数的每一位进行转移的动态规划。一般题目的特点更偏向于范围大( O ( n ) O(n) O(n)会超时)、数位计数。

通常会求 [ l , r ] [l,r] [l,r]内的所有数字中符合数位特点的数有多少个。此时我们将问题拆解成 [ 1 , l − 1 ] [1,l-1] [1,l1] [ 1 , r ] [1,r] [1,r],针对两个区间进行答案的记忆化搜索,然后通过二者答案差值计算符合要求的个数。

HDU2089 - 不要62

题目链接

给定一个区间 [ n , m ] [n,m] [n,m],问区间内不包含 4 4 4 62 62 62(连在一起)的数的个数。

将问题拆解成 [ 1 , n − 1 ] [1,n-1] [1,n1] [ 1 , m ] [1,m] [1,m],令 a n s ( x ) ans(x) ans(x)为区间 [ 1 , x ] [1,x] [1,x]中不包含 4 4 4 62 62 62的数字的个数,那么最后的结果是 a n s ( m ) − a n s ( n − 1 ) ans(m)-ans(n-1) ans(m)ans(n1)

现在问题转化成了如何求解 a n s ( 1 , x ) ans(1,x) ans(1,x)

我们首先定义dp数组。我们在转移的过程中要注意的事情有三个:当前是第几位、上一位是什么(用于判断62,上一位如果是6,那么这一位不可以是2),当前转移的前几位是否与x的前几位相同(如果相同,那么这一位的上限是x在这一位里的数值,否则上限是9)。

初始化时将所有元素初始化成0。

于是:dp[i][j][0/1]表示当前转移状态是第 i i i位,上一位是 j j j,这一位之前与x是否相同的状态(是则为1,否则为0)。

我们先记录x的每一位的数是多少。接下来进入记忆化搜索。

  1. 如果我们已经判断完了最后一位,也就是当前位已经超过了我们存储数位的范围,那么说明我们已经确定了一个数字,那么我们直接返回1。
  2. 如果当前位置 p o s pos pos、上一个位置数是 j j j、且这一位之前与x全部相同的状态 l i m lim lim的情况之前已经搜索过了,根据记忆化搜索的原则,直接返回我们之前搜过的结果。
  3. 当前两种情况都不符合时,此时就需要计算当前情况下的dp值。我们枚举这一位所有可能的、合法的数值,然后递归枚举下一位。最后将每一位的答案求和,作为当前所求状态的dp值,并将这个值返回。

所谓可能的、合法的数值,要从以下情况考虑:

  1. 如果前面的位和x的位完全相同,那么这一位的上限取决于x的这一位是多少。如果前面存在一位与x不同,那么这一位可以0~9任意取。

  2. 题目中的一些特征的限定。比如说这道题目,对于任意一位,在任意一种情况下都不能选择 4 4 4,同时如果上一位时 6 6 6的话,那么这一位不能选择 2 2 2

Code:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

LL n, m;
LL a[22];
LL dp[22][11][2];

LL DP(LL pos, bool las, bool lim) {
	if (!pos) return 1;
	if (dp[pos][las][lim]) return dp[pos][las][lim];
	LL ans = 0;
	for (LL i = 0; i <= (lim ? a[pos] : 9); ++i) {
		if (i == 4) continue;
		if (las && (i == 2)) continue;
		ans += DP(pos - 1, (i == 6), (lim and (i == a[pos])));
	}
	return dp[pos][las][lim] = ans;
}

LL sol(LL x) {
	LL dig = 0;
	memset(a, 0, sizeof(a));
	while (x) {
		a[++dig] = x % 10;
		x /= 10;
	}
	memset(dp, 0, sizeof(dp));
	return DP(dig, 0, 1);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0); 
	while (1) {
		cin >> n >> m;
		if (!n and !m) break;
		cout << sol(m) - sol(n - 1) << "\n";
	}
	return 0;
}

LuoguP2602 - [ZJOI2010] 数字计数

题目链接

题目大意:给定区间 [ l , r ] [l,r] [l,r],求 0 0 0~ 9 9 9的每一个数在区间中的数的数位上出现的次数。

a n s ( i , x ) ans(i,x) ans(i,x)表示区间 [ 1 , x ] [1,x] [1,x]中每一个数中的每一个数位里 i i i出现的次数。那么本题的答案为 a n s ( r ) − a n s ( l − 1 ) ans(r)-ans(l-1) ans(r)ans(l1)

现在求 a n s ( i , x ) ans(i,x) ans(i,x)

我们还按照刚刚拿到题目的思路来思考这道题目。我们使用记忆化搜索来求解。

考虑如何设计dp。我们令dp[i][0/1][j][0/1]表示在第 i i i个数位,这一位之前是否和x的每一位都相同(0/1),到这一位之前所求的数出现的次数 j j j,当前是否是前导0。

dp数组初始化全部为0。

先将x按位拆分成数组形式,然后进行记忆化搜索。对于当前状态,设定四个参数:当前是第pos位;这一位之前是否和x的每一位都相同,用bool变量lim表示,是则lim=1,否则lim=0;所求的数出现的次数sum;当前是否是前导0,用bool变量zero表示,若是则zero=1,否则zero=0。步骤如下:

  1. 如果所有位都已经走过,即pos=0,则返回当前所求的数出现的次数sum。
  2. 如果当前状态已经在之前搜索过,即dp[pos][lim][sum][zero]>0,则直接返回这个dp值。
  3. 根据lim的状态设立当前位置的数的遍历上限,如果lim是1,则选取a[pos](a是x按位拆成的数组),否则取9。对于遍历的每一个数,如果当前状态是1而且这个数=a[pos],那么下一个状态也一定lim=1,否则lim=0;如果当前属于前导零范围,即zero=1,且当前遍历是0,则下一个状态一定zero=1,否则zero=0。如果当前遍历的数是我这一轮正在查询的数,那么下一个状态所求数出现的次数为sum+1。
  4. 将所有遍历得到的结果求和,作为当前状态的dp值,并将这个值返回。

code:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;

LL A, B, dig;
LL dp[13][2][13][2];
LL a[22];

LL DP(LL x, LL pos, LL lim, LL sum, LL zero) {
	if (pos == 0) return sum;
	if (dp[pos][lim][sum][zero]) return dp[pos][lim][sum][zero];
	LL ans = 0;
	for (int i = 0; i <= (lim ? a[pos] : 9); ++i) {
		ans += DP(x, pos - 1, (lim and a[pos] == i), sum + ((!zero or i > 0) and (i == x)), (zero and !i));
	}
	return dp[pos][lim][sum][zero] = ans;
}

LL solve(LL d, LL x) {
	memset(dp, 0, sizeof(dp));
	dig = 0;
	LL xx = x;
	while (xx) {
		a[++dig] = xx % 10; xx /= 10;
	}
	return DP(d, dig, true, 0, true); 
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0); 
	cin >> A >> B;
	for (LL i = 0; i <= 9; ++i) {
		cout << solve(i, B) - solve(i, A - 1) << " "; 
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值