数位DP小记

1.基础

1.1. 问题

数位 DP 解决的一般都是和数字相关的计数问题,常见的有 \(l \sim r\) 中有多少数符合某个关于数位的条件。

对于这种问题,我们都是先用前缀和转化成小于等于某个数的问题。

下面以 P2602 [ZJOI2010] 数字计数 为模板题。

1.2 记忆化搜索

我们先枚举每个数码。

我们考虑设一个状态 \((i,j,0/1,0/1)\) 表示当前处理到了第 \(i\) 位,已经填了 \(j\) 个当前数码,有无前导 \(0\),是否贴着上界。

我们发现,有前导 0 或者贴着上界的状态其实只会搜索到 1 次,所以我们只用记录 \(f(i,j)\) 表示 \((i,j,0,0)\) 状态的答案来进行记忆化搜素。

首先,如果 \(i = 0\),我们直接返回 \(j\)

其次,如果已经计算过了,我们返回答案。

否则,我们根据是否贴着上界算一下这一位的范围,然后枚举每个数码。

枚举的过程中,我们需要特判 0 和前导 0 的情况。

得出答案后在记忆化并返回即可。

时间复杂度是位数乘以进制。

#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;

int tmp, num[N] = {0};//表示当前处理的数字
long long f[N][N] = {{0}};//f[i][j] 表示处理到 i 已经有了 j 个 tmp,且没有前导 0 和最高位限制

long long dfs(int pos, int sum, bool hd, bool lim) {
	long long ans = 0ll;
	if (pos == 0)
		return sum;
	if (!hd && !lim && f[pos][sum] != -1)
		return f[pos][sum];
	int up = (lim ? num[pos] : 9);
	for (int i = 0; i <= up; i++) {
		if (i == 0 && hd)
			ans += dfs(pos - 1, sum, hd, lim && i == up);
		else if (i == tmp)
			ans += dfs(pos - 1, sum + 1, false, lim && i == up);
		else
			ans += dfs(pos - 1, sum, false, lim && i == up);
	}
	if (!hd && !lim)
		f[pos][sum] = ans;
	return ans;
}

long long slv(long long n) {
	int len = 0;
	while (n > 0)
		num[++len] = n % 10, n /= 10;
	memset(f, -1, sizeof f);
	return dfs(len, 0, true, true);
}

int main() {
	long long l, r;
	cin >> l >> r;
	for (int i = 0; i <= 9; i++) {
		tmp = i;
		cout << slv(r) - slv(l - 1) << " ";
	}
	return 0;
} 

1.3 适合多测的方法

我们从另一个角度看,每次我们相当于需要处理的就是一个数划分成的若干区间。关键就是上界。

所以我们可以直接最开始计算 \(f(i,j)\) 表示从低到高确定了 \(i\) 位,\(j\) 的总个数。

然后对于一个数,我们从高到低处理,枚举每一位用 dp 值计算出答案即可。

这样我们只用一遍 dp 就可以解决问题了。

本质就是我们对于每个数单独考虑,枚举其符合了多少上界的条件

#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;

long long pw[N] = {0}, f[N] = {0};
long long cnt[N] = {0};

void DP() {
	pw[0] = 1;
	for (int i = 1; i < N; i++) {
		f[i] = f[i - 1] * 10 + pw[i - 1];
		pw[i] = 10 * pw[i - 1];
	}
}

int num[N] = {0};
void slv(long long n, int op) {
	int len = 0;
	while (n > 0)
		num[++len] = n % 10, n /= 10;
	for (int i = len; i >= 1; i--) {//前面的都是上界
		for (int j = 0; j < 10; j++)
			cnt[j] += f[i - 1] * num[i] * op;
		for (int j = 0; j < num[i]; j++)
			cnt[j] += pw[i - 1] * op;
		long long tot = 0;
		for (int j = i - 1; j >= 1; j--)
			tot = tot * 10ll + num[j];//最高位单独处理
		cnt[num[i]] += (tot + 1) * op;
		cnt[0] -= pw[i - 1] * op;//处理前导0
	}
}

int main() {
	long long l, r;
	cin >> l >> r;
	DP();
	slv(r, 1), slv(l - 1, -1);
	for (int i = 0; i < 10; i++)
		cout << cnt[i] << " ";
	return 0;
} 

2.应用

差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!
差分不要忘了取模!!!!!!!!!!!

P4798 [CEOI2015 Day1] 卡尔文球锦标赛

我们采用数位 dp 第二种方法的思想,我们枚举与给定的前 \(i\) 位都相同的,这时候我们只需要知道 \(f(i,j)\) 表示第一个数的范围是 \(1 \sim j\),长为 i 的填法即可。这个可以很轻松的转移。

所以我们可以在 \(O(n^2)\) 内解决这个问题,时间复杂度 \(O(n^2)\),注意需要优化空间。

提交记录

Acwing311 月之谜

枚举数字和,设 \(f(p,s,r)\) 表示当前处理到第 \(p\) 位,数字和是 \(s\),对目标数字和的余数是 \(r\)

数字和的范围不大,最多 90,所以时间复杂度足够。终止条件是 \(r=0\)\(s\) 等于目标数字和。

提交记录

HDU2089 不要62

\(f(i,0/1)\) 表示处理到第 \(i\) 位,上一位是不是 6 即可。

提交记录

HDU4507 恨7不成妻

这道题求的是平方和,所以我们需要记录三个值:平方和,总和,个数。

然后转移就是将完全平方拆开即可。记得差分时要取模!

提交记录

P4317 花神的数论题

记录 \(f(i,j)\) 表示到 \(i\),数字和为 \(j\) 的贡献,将加法改成乘法即可。

提交记录

P6669 [清华集训2016] 组合数问题

经典问题。我们知道 Lucas 定理,所以这道题相当于将 \(i,j\) 堪称 \(k\) 进制后有多少 \(i\) 存在某一位小于 \(j\)

为了方便,我们计算所有 \(i\) 每一位大于等于 \(j\) 的方案数。\(f(i,0/1,0/1)\) 表示考虑到了第 \(i\) 为,两个数是否到达上界。

记得多测清空 num 数组。

提交记录

CF1670F Jee, You See?

显然是 dp 题,但是我们要思考按照什么来 dp。

我们发现对于总和的限制和数位 dp 很像,所以我们按照总和来 dp。

\(f(i,j,0/1)\) 表示从高到底确定到了第 \(i\) 位,进位了 \(j\),是否是上界。

我们发现现在我们需要知道这一位上有多少个数是 \(1\) 以及下一位给了多少进位。同时我们发现只用枚举一个就可以根据 \(j\) 算出另一个,所以我们只用枚举一个。

符合第三个条件就只用判断这一位上是 \(1\) 的个数的奇偶性即可。

注意这道题不存在前导 0,所以我们必须三个维度都要记忆化,否则会超时。

提交记录

P3464 [POI2007] WAG-Quaternary Balance

显然一个不会被选择超过两次。

啊其实这个观察屁用没有。

我们先转成四进制,然后 \(f(i,0/1)\) 表示第 \(i\) 位是选的比原来多一,还是原来的。转移时如果前一位比原来多一,只能减,否则之只能加。求出最小值的同时记录一下答案即可。

貌似没法构造。

提交记录

CF628D Magic Numbers

额过于板子了,甚至不用处理前导 0。

提交记录

posted @ 2024-08-30 18:11  rlc202204  阅读(14)  评论(0编辑  收藏  举报