数位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;
}