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(a-1)\)

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

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

2.3 设计 DP

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

那么首先我们要拆分数字。假设拆分在 \(a\) 数字里面,\(a_i\) 表示 从低到高第 \(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 画流程图,如果画的不好请各位大佬海涵,有更好的建议请在评论区提出或者私信提出,本人将会采纳,表示感谢!

flowchat st=>start: LL dfs(int pos, int las, int zero, int limit) e=>end: return ans; cond1=>condition: pos != 0 op1=>operation: return 1; op2=>operation: 取出最高位 t op3=>operation: 枚举 i from 0 to t cond2=>condition: i == 0 且 zero == true cond3=>condition: abs(i - las) >= 2 op4=>operation: ans+=dfs(pos - 1, i, 1, i == a[pos] && limit) op5=>operation: ans+=dfs(pos - 1, i, 0, i == a[pos] && limit) cond4=>condition: 枚举完了吗 cond5=>condition: 条件 zero != 0 && limit != 0 op6=>operation: 记忆化 f[pos][las] = ans op7=>operation: 返回答案 ans st->cond1 cond1(no)->op1 cond1(yes)->op2->op3->cond2 cond2(yes)->op4->cond4 cond2(no)->cond3 cond3(yes)->op5->cond4 cond3(no)->cond4 cond4(yes)->cond5 cond4(no)->op3 cond5(yes)->op6->op7 cond5(no)->op7

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

代码如下:

#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 @ 2022-04-14 21:43  Plozia  阅读(87)  评论(0编辑  收藏  举报