DP专题-学习笔记:数位 DP

一些 update

update 2021/2/23:最近作者发现数位 DP 的 f 数组初始化有问题,导致代码出现了根本性错误(原理不变),现在已经纠正,对各位读者造成的困扰深表歉意。

1. 概述

数位 DP,是一种 DP (废话),专门用于数字统计类问题。

这种问题首次接触可能会有些难理解,但是练过几道题之后就会掌握套路了。

2. 例题

[SCOI2009] windy 数

这是数位 DP 的基础题,我个人认为比别的题目更好入门。

2.1 表达形式

数位 DP 的题目通常都是这样表达的:

问:在 [l,r] 范围内,满足条件 P(x) 的数 x 有几个?

比如例题:

问:在 [a,b] 范围内,满足条件 2 的数 x 有几个?

2.2 修改问题

数位 DP 的问题一般都满足 区间可减性

比如说例题,无论 a,b 怎么变,Windy 数始终是 Windy 数,非 Windy 数始终是非 Windy 数。

于是乎,我们就可以使用差分将询问变为 f(b)f(a1)

其中 f(x) 表示在 [1,x] 内有几个满足条件的数。

代码里面需要特别对 a=0 特判!

2.3 设计 DP

既然是数位 DP,那么肯定跟数位有关。

那么首先我们要拆分数字。假设拆分在 a 数字里面,ai 表示 从低到高第 i(这是为了与代码匹配)。cnt 为位数。

数位 DP 有两种写法:记忆化搜索与直接递推。

记忆化搜索便于理解,直接递推代码简洁。

本文章采用记忆化搜索讲解。

首先设计 DFS 函数:(LL 即为 long long)

LL dfs(int pos, int las, ..., int zero, int limit)

pos 表示当前搜到第几位,las 表示上一位数字是什么(Windy 数需要上一位数字),zero 表示是否为前导 0,limit 表示这一位有没有最高位限制。

... 是表示有的题目可能需要一些别的。

先撇下 zerolimit 不管,我们继续设计 DFS 函数。

那么搜索思路显然:我们只需要枚举第 pos 位上的数字,满足题目要求就继续往下搜,而采用 f[pos][las] 来记忆化。

那么这个 zerolimit 呢?

zero 是前导零,limit 表示限制。

因为前导零是不计入数字限制,所以特别要注意前导零不能对数字统计造成干扰,于是我们需要注意前导零的限制,在记忆化搜索的时候对于有前导零限制的数我们不能记忆化。

limit 表示限制,这个限制是干什么用的呢?

考虑以下数据:

0-47382,现在已经填了 473??

那么第四位我们要怎么枚举呢?

如果我们直接枚举 0-9,那么在枚举到 9 的时候就会发现:4739? 这个数字不在范围内。于是我们需要引入 limit 做出限制。

怎么处理?

2.4 特别处理

首先看 zero 的处理。

zero 的判定条件:当上一位 zero 为真且当前位为 0 时,接下来的 zero 为真。

解释:

上一位 zero 为真表示前面所有位都是 0,而这一位也是 0,于是乎数字为 00..0??...?,那么 zero 接下来为真,告诉后面的数字这一位的 0 是前导零。

再看 limit 的处理。

limit 的判定条件:当上一位 limit 为真且当前位为最高位时,接下来的 limit 为真。

解释:

上一位 limit 为真表示前面都是最高位,这一位也是最高位,于是乎从最前面到这一位都是最高位,那么 limit 接下来为真,告诉后面的数字这一位是最高位。

需要特别注意的是,zero=0limit=0 的时候不能记忆化!(当然你可以加两个状态表示 zerolimit,这样可以)

2.5 书写代码

在搜索时我们按照一般的搜索套路,特别注意对 zerolimit 的处理。

这里先简单画一下流程图。以例题为例。

这是本人第一次使用 Markdown 画流程图,如果画的不好请各位大佬海涵,有更好的建议请在评论区提出或者私信提出,本人将会采纳,表示感谢!

Syntax error in textmermaid version 10.9.0

那么有了这个流程图,代码应该很好写了。

代码如下:

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

typedef long long LL;
int l, r, f[20][10], a[20], cnt;

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
	while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
	return sum * fh;
}
int Abs(int x) {return (x < 0) ? -x : x;}

int dfs(int pos, int las, int zero, int limit)
{
	if (pos == 0) return 1;
	if (!zero && !limit && f[pos][las] != -1) return f[pos][las];
	int t = limit ? a[pos] : 9, ans = 0;
	for (int i = 0; i <= t; ++i)
	{
		if (zero && i == 0) ans += dfs(pos - 1, -2, zero, i == a[pos] && limit);
		else if (Abs(i - las) >= 2) ans += dfs(pos - 1, i, 0, i == a[pos] && limit);
	}
	if (!zero && !limit) f[pos][las] = ans;
	return ans;
}

int Get(int k)
{
	cnt = 0; memset(a, 0, sizeof(a));
	memset(f, -1, sizeof(f));
	while (k) {a[++cnt] = k % 10; k /= 10;}
	return dfs(cnt, -2, 1, 1);
}

int main()
{
	l = read(), r = read();
	printf("%d\n", Get(r) - Get(l - 1));
	return 0;
}

这里再放一个万能板子,供大家参考:

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

//sth.

int read()
{
	int sum = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
	while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
	return sum * fh;
}

int dfs(int pos, ..., int zero, int limit)
{
	if (pos == 0) return 1;
	if (!zero && !limit && f[...] != -1) return f[...];
	int t = limit ? a[pos] : 9, ans = 0;
	for (int i = 0; i <= t; ++i)
	{
		if (zero && i == 0) ans += dfs(pos - 1, ..., zero, i == a[pos] && limit);
		else if (由题目条件决定) ans += dfs(pos - 1, ..., 0, i == a[pos] && limit);
		//else ...
	}
	if (!zero && !limit) f[...] = ans;
	return ans;
}

int Get(int k)
{
	cnt = 0; memset(a, 0, sizeof(a));
	memset(f, -1, sizeof(f));
	while (k) {a[++cnt] = k % 10; k /= 10;}
	return dfs(cnt, ..., 1, 1);
}

int main()
{
	l = read(), r = read();
	if (l != 0) printf("%d\n", Get(r) - Get(l - 1));
	else printf("%d\n", Get(r));
	return 0;
}

这里对代码做一个统一说明与解释:

  1. 例题代码的 -2 是为什么?
    处理方便,不需要特判。
  2. 注意板子里面要特判一下 l!=0

这里再给一个板子,这个板子是将 zerolimit 也加入记忆化的状态里面的。

int f[...][2][2];

int dfs(int pos, ..., int zero, int limit)
{
	if (pos == 0) return 1;
	if (f[...][zero][limit] != -1) return f[...][zero][limit];
	int t = limit ? a[pos] : 9, ans = 0;
	for (int i = 0; i <= t; ++i)
	{
		if (zero && i == 0) ans += dfs(pos - 1, ..., zero, i == a[pos] && limit);
		else if (由题目条件决定) ans += dfs(pos - 1, ..., 0, i == a[pos] && limit);
		//else ...
	}
	f[...][zero][limit] = ans;
	return ans;
}

3. 练习题

练习题传送门:DP专题-专项训练:概率/期望 DP + 数位 DP

posted @   Plozia  阅读(102)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示