Y
K
N
U
F

数位DP |【题解】 P13085 [SCOI2009] windy 数(加强版)

芝士:

数位 dp。

什么是数位 dp?

数位 dp 是指把一个数字按位数拆开。并按位数统计答案的一种 dp。为了做到不重不漏,一般从高位到低位统计。

如果这样说不太明了,不妨直接放到题目中理解。

自认为还行的讲解:

以本题举例,假设 \(a = 1\)\(b = 10\)。我们很容易统计出答案,分别是 \(1\)\(2\)\(3\)\(4\)\(5\)\(6\)\(7\)\(8\)\(9\)。如果扩展到 \(b=120\) 呢?注意到它具有很多完整的 \(10\),那么我们有没有什么办法利用起来之前得到的结果来减少运算呢?

显然可以!不难想到按位数将 \(120\) 打散成一些部分。如 \([1,10]\)\([1,100]\)。但是考虑到每一位的前一位也影响统计,我们不妨将前一位也作为打散的标准来。于是变成了另一些部分,如 \([1,10]\)\([11,20]\)\([21,30]\)\([1,100]\)\([101,200]\)。如此一来,便可以很容易的利用之前得到的结果了。

结合代码来看,所谓结合了上一位也就是给记忆化数组多开了一维作为一个状态。这样,数位 dp 的大体思路就得到了。但仍有细节需要注意,我们结合代码讲解。

int dig[20];//拆完的数
lint dp[20][10];//记忆化数组

lint dfs(int pos, int last, bool lim, bool lead/*这两个就是细节了*/) {
	if(!pos) return 1;
	if(!lead and !lim and dp[pos][last] != -1) return dp[pos][last];//上文所说的再利用
	lint res=0; const int up = lim ? dig[pos] : 9;
	for(int i = 0; i <= up; i ++)
		if(abs(i - last) >= 2) {//题目要求
    		if(lead and !i) res+=dfs(pos-1, -100, lim and i==up, 1);
    		else res += dfs(pos-1, i, lim and i==up, 0);
        }
	return !lim and !lead ? dp[pos][last] = res : res;
}

注意到以上代码中有两个前面没有提到过的东西,\(lim\)\(lead\),我们分别来说。

\(lim\) 用来判断这个区间的完整性,从而判断可否拿来记忆,可否利用之前记忆的结果以及搜索的上界是什么。考虑这样一个情形,假如你要计算 \([1,13]\),你要处理 \([1,10]\),但你如何区分你处理的不是 \([11,13]\) 呢?于是 \(lim\) 有用了。再考虑一个情景,假如你已经处理了 \([101,200]\) 的结果并将其记录,而你现在要运算的是 \([1,2122]\),你看到了一个 \([1101,1122]\),你知道不应该直接返回 \([101,200]\) 的结果,于是 \(lim\) 就又有用了。

\(lead\) 用来判断前导零的存在,有着判断可否向下一位传递结果和可否利用之前记忆的结果的作用。前导零是一个数最前面的 \(0\)。如 \(011\) 有前导零而 \(11\) 没有。可以将有前导零的数看做不完整的数字,它前面还要有东西的。所以它的作用就显然了(吧)。

如果还不明白的话,希望代码会有帮助。

完整代码:

// code by 樓影沫瞬_Hz17
#include <bits/extc++.h>
using namespace std;
using lint = long long;

int dig[20];
lint dp[20][10];

lint dfs(int pos, int last, bool lim, bool lead) {
	if(!pos) return 1;
//已经搜到了第零位,说明返回后是第一位,故返回1
	if(!lead and !lim and dp[pos][last] != -1) return dp[pos][last];
//前两个条件表示我要搜完整的区间,后一个表示我搜过这个区间
	lint res=0; const int up = lim ? dig[pos] : 9;//有限制的话当然不能搜完所有了
	for(int i = 0; i <= up; i ++)
		if(abs(i - last) >= 2) {//题目条件
    		if(lead and !i) res += dfs(pos-1, -100/*意思是还没完呢,一串0*/, lim and i==up/*只有既到上界又有限制才能把限制下传*/, 1/*有前导0*/);
    		else res += dfs(pos-1, i/*last*/, lim and i==up, 0);//同理
        }
	return !lim and !lead ? dp[pos][last] = res : res;//记忆化
}

lint work(lint num) {
	int t = 0;//位数
	do dig[++t] = num % 10;//拆数
    while(num /= 10);
	return dfs(t, -100, 1, 1);//从上往下搜
}

int main(){
    memset(dp, -1, sizeof dp);
	lint A, B;
	cin >> A >> B;
//简单的容斥
	cout << work(B) - work(A-1) << endl;
	return 0;
}
posted @ 2025-07-09 19:56  樓影沫瞬_17Hz  阅读(30)  评论(0)    收藏  举报