數位DP小記

rlc202204發表於2024-08-30

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 就可以解決問題了。


相關文章