题目特征
数位DP题目一般都是求
[
l
,
r
]
[l,r]
[l,r]间满足某种情况的数有几个, 一般
l
,
r
l,r
l,r都比较大
或者直接就是算
[
1
,
N
]
[1,N]
[1,N]的
题目解法
数位DP虽然是DP,但是大部分题都能用一个相同的方法来解决
虽然很多题目能用递推写,但是递推难度较记忆化搜索较高,因此最好选用记忆化搜索来写题
模板
ll dfs(int pos, ... , bool lead, bool isMax) {//当前位pos, ...为省略条件, lead判前导零, isMax判前几位数是否选的都是最大值
if (!pos) return 1;//此处为越过一个数的最后一位(最小的一位), 如123, 越过3这里说明当前已经是123了,所以只有一个数
//有时候不一定都是返回1,看条件
if (!isMax && !lead && dp[pos][...][...] != -1) return dp[pos][...][...];//记忆化, 如果有直接返回
int up = isMax ? a[pos] : 9; //如果一直是最大, 当前位最多也就是a[pos], 超过了就大于这个数了
ll res = 0;
for (int i = 0; i <= up; i++) {
//按限制条件来
//如, 判是否有前导0
if (lead) res += dfs(pos - 1, ... , !i, isMax && i == a[pos]);
else res += dfs(pos - 1, ... , 0, isMax && i == a[pos]);
}
return isMax || lead ? res : dp[pos][...][...] = res;//如果有前导0或者是前几位都是最大, 直接返回
//否则dp[pos][...][...]记录值再返回
}
ll calc(ll x) {
int pos = 0;
while (x) a[++pos] = x % 10, x /= 10;//数字用数组表示
return dfs(pos, ... , 1, 1);
}
练习题目
最经典的也是最简单的入门题就是
1. HDU2089 不要62
题意简单,求 [ l , r ] [l, r] [l,r]内有多少数满足,不存在4并且不存在62连号
写起来也非常简单:
ll dfs(int pos, int pre, bool lead, bool isMax) {//当前位, 前一个数是什么, 前导0, 前几位都是最大
if (!pos) return 1;//如果到最后了,因为这里循环里面判了条件了, 所以最后只有合法情况, 那么就是1
//如果不合法了,显然返回0
if (!isMax && !lead && dp[pos][pre] != -1) return dp[pos][pre];
int up = isMax ? a[pos] : 9;
ll res = 0;
for (int i = 0; i <= up; i++) {
if (i == 4) continue;//有4不要
if (pre == 6 && i == 2) continue;//有62不要
if (lead) res += dfs(pos - 1, !i ? -1 : i, !i, isMax && i == a[pos]);//有前导0
else res += dfs(pos - 1, i, 0, isMax && i == a[pos]);//无前导0
}
return isMax || lead ? res : dp[pos][pre] = res;
}
ll calc(ll x) {
int pos = 0;
while (x) a[++pos] = x % 10, x /= 10;
return dfs(pos, -1, 1, 1);
}
2. [SCOI2009]windy数
求 [ l , r ] [l, r] [l,r]间有多少数满足相邻两个数字之差至少为2
比起上一题来说其实也没有难太多,也是简单题:
ll dfs(int pos, int pre, bool lead, bool isMax) {
if (!pos) return 1;//最后都是符合情况的
if (!isMax && !lead && dp[pos][pre] != -1) return dp[pos][pre];
int up = isMax ? a[pos] : 9;
ll res = 0;
for (int i = 0; i <= up; i++) {
//情况分两种
//一是有前导0, 即前面没有数, 那么可以直接往下搜
//二是没有前导0, 那么我们只有在前后两个数差大于等于2的时候才会去搜
if (lead) res += dfs(pos - 1, !i ? -1 : i, !i, isMax && i == a[pos]);
else if (abs(i - pre) >= 2) res += dfs(pos - 1, i, 0, isMax && i == a[pos]);
}
return isMax || lead ? res : dp[pos][pre] = res;
}
ll calc(ll x) {
int pos = 0;
while (x) a[++pos] = x % 10, x /= 10;
memset(dp, -1, sz(dp));//注意此处,为什么每次搜前都初始化呢
//不要62这题 初始化一次就可以, 那是因为不要4和不要62连号对于每一个数来说都是同样的限制条件
//但是这里不一样, 相邻两个数差为2这个限制条件对于每个数来说是不一样的
return dfs(pos, -1, 1, 1);
}
3. [CQOI2016]手机号码
求的条件为 至少3个相邻的数字相同, 不能同时出现8和4
这里稍微要复杂一点点,但是处理起来一样的:
ll dfs(int pos, int pre, int ppre, bool three, bool four, bool eight, bool lead, bool isMax) {
if (four && eight) return 0;//如果同时出现4和8,直接返回0
if (!pos) return three;//到达最后,如果有3连返回1,没有返回0
if (!isMax && pre != -1 && ppre != -1 && dp[pos][pre][ppre][three][four][eight] != -1) return dp[pos][pre][ppre][three][four][eight];
int up = isMax ? a[pos] : 9;
ll res = 0;
for (int i = 0; i <= up; i++) {
//一样的处理,分前导0和无前导0
if (!lead) res += dfs(pos - 1, i, pre, three || (i == pre && i == ppre), four || i == 4, eight || i == 8, 0, isMax && i == a[pos]);
else res += dfs(pos - 1, !i ? -1 : i, -1, 0, i == 4, i == 8, !i, isMax && i == a[pos]);
}
return isMax || pre == -1 || ppre == -1 ? res : dp[pos][pre][ppre][three][four][eight] = res;
}
ll calc(ll x) {
int pos = 0;
while (x) a[++pos] = x % 10, x /= 10;
return dfs(pos, -1, -1, 0, 0, 0, 1, 1);
}
4. 烦人的数学作业
求 [ l , r ] [l,r] [l,r]内每个数的数字和,如123这个数的数字和为1+2+3=6
这里我们单独统计每一个数字:
ll dfs(int pos, int dig, int cnt, bool lead, bool isMax) {//当前位, 选的数字, 前面出现几次了, 前导0, 最大
if (!pos) return cnt;//到达最后返回这个数在前面出现的次数
if (!isMax && dp[pos][cnt] != -1) return dp[pos][cnt];
int up = isMax ? a[pos] : 9;
ll res = 0;
for (int i = 0; i <= up; i++)//这里我当时写的时候简写了,照样分2种,有无前导0
res = (res + dfs(pos - 1, dig, cnt + (dig == i && (!lead || i)), lead && !i, isMax && i == a[pos])) % mod;
return isMax || lead ? sum : dp[pos][cnt] = sum;
}
ll calc(ll x, int dig) {
int pos = 0;
while (x) a[++pos] = x % 10, x /= 10;
memset(dp, -1, sz(dp));//条件对于每一个数dig来说都是不一样的,所以每次都初始化
return dfs(pos, dig, 0, 1, 1);
}
//最后main函数里面这样统计即可
ll ans = 0;
for (int dig = 1; dig <= 9; dig++) {//0不用算
ll x = (calc(qr, dig) - calc(ql, dig) + mod) % mod;
ans = (ans + x * dig % mod) % mod;
}
5. [ZJOI2010]数字计数
上一题的双倍经验,一样的
6. [HAOI2010]计数
最开始会想不到这是数位DP,思路比较神奇的题,我没用记忆化搜索来做