数位dp
前言:为什么会想到学习数位dp,是因为群友问了一个蓝桥杯国赛的题要用数位dp解,然后闲的无聊就来学习数位dp了。
- 数位dp的尝试方式大都不算特殊,绝大多是都是线性展开的,类似从左到右的尝试。
- 数位dp是在数组的每一位上展开而已。
- 不同的题目有不同的限制,数位dp解题的核心在于:可能性整理 + 排列组合相关的知识。
对于数位操作题型的探索
统计各位数字都不同的数字个数
题目大意:给你一个整数 ,统计并返回各位数字都不同的数字的个数,其中\(0 \le x \lt 10^n\)。
【解题】:核心思路:统计1 - n 位数字对结果的贡献。
不难想到 i = 1 时, 需要求的结果时10 以内个位数字不同的数字个数为 10。
当 i > 1 时,由于限定该数字必须为 i 位,所以第一位不能是 0 ,共 9 种选法, 由于第一位选择了一个数组后面还有 10 - 1 = 9 种可以选, 同理往后可选择的方案依次减一。
code:
class Solution {
public:
int countNumbersWithUniqueDigits(int n)
{
if(n == 0) return 1;
int ret = 9, sum = 10;
for(int k = 2, s = 9; k <= n; k++, s--)
{
ret = ret * s;
sum += ret;
}
return sum;
}
};
最大数字为 N 的数字组合
题目大意: 给定一个按非递减顺序排列的数字数组。你可以用任意次数来写的数字。例如,如果 digits = ['1','3','5'],我们可以写数字,如 '13', '551', 和 '1351315'。
返回 可以生成的小于或等于给定整数n 的正整数的个数。
【解题】:发现对于每一位数字的选择可以分为三种情况:
- 不选当前位数字,即组成一个位数小于 n 的数字。
- 当前位数字选择一个小于 n 对应位置的数字。
- 当前位数字选择一个等于 n 对应位置的数字。
用 dfs 搜索每一位可以填的数字,对于1,2这两种情况,后续可以填写任意数字(因为此时已经注定要小于等于 n 了),对于情况三,则需要填写小于等于 n 对应位置的数字的数。
code:
class Solution {
public:
// ret :可选数字集合
// len :当前选择数字的位数
// offset :辅助提取 n 的第 len 位
// free :前一个数字的选择,0 表示前一个数字与n的相同,1表示小于对应数字
// fix :前面是否选择过数字
int f(int n, vector<int> & ret, int len, int offset, int free, int fix)
{
if(len == 0)
{
// return fix;
// 如果到最后前面一个数字没有选择返回0
return fix == 1 ? 1 : 0;
}
// 提取当前位的数字
int cur = (n / offset) % 10;
int ans = 0;
if(fix == 0)
{
// 一直不选数字
ans += f(n, ret, len - 1, offset / 10, 1, 0);
}
if(free == 1)
{
// 当前位 以即后面的位置可以随便选
ans += ret.size() * f(n, ret, len - 1, offset / 10, 1, 1);
}
else
{
for(int i = 0; i < ret.size(); i++)
{
if(ret[i] < cur)
{
ans += f(n, ret, len - 1, offset / 10, 1, 1);
}
else if(ret[i] == cur)
{
ans += f(n, ret, len - 1, offset / 10, 0, 1);
}
else
{
break;
}
}
}
return ans;
}
int atMostNGivenDigitSet(vector<string>& digits, int n)
{
vector<int> ret;
for(auto ch : digits) ret.push_back(ch[0] - '0');
int tmp = n / 10;
int len = 1;
int offset = 1;
while(tmp)
{
tmp /= 10;
len++;
offset *= 10;
}
return f(n, ret, len, offset, 0, 0);
}
};
这段代码的速度已经击败100%的用户了,但是还是有可以优化的地方,我们先把这段代码的递归展开图画出来。

我们发现其实有很对的递归展开是无效的,换句话说我们其实可以一开始就算出来没有必要进行展开,下面对代码进行一点小优化:
code:
class Solution {
public:
// ret :可选数字集合
// len :当前选择数字的位数
// offset :辅助提取 n 的第 len 位
// free :前一个数字的选择,0 表示前一个数字与n的相同,1表示小于对应数字
// fix :前面是否选择过数字
int f(int n, vector<int> & ret, vector<int> & cnt, int len, int offset)
{
if(len == 0)
{
// n 本身
return 1;
}
// 提取当前位的数字
int cur = (n / offset) % 10;
int ans = 0;
for(int i = 0; i < ret.size(); i++)
{
if(ret[i] < cur) ans += cnt[len - 1]; // 减去无效递归
else if(ret[i] == cur) ans += f(n, ret, cnt, len - 1, offset / 10);
else break;
}
return ans;
}
int atMostNGivenDigitSet(vector<string>& digits, int n)
{
int m = digits.size();
vector<int> ret;
for(auto ch : digits) ret.push_back(ch[0] - '0');
int tmp = n / 10;
int len = 1;
int offset = 1;
while(tmp)
{
tmp /= 10;
len++;
offset *= 10;
}
vector<int> cnt(len + 1); // cnt[i] 表示 i 的前缀已经确定且比n小时后面可选数字的总数
int ans = 0;
cnt[0] = 1;
for(int i = 1, k = m; i < len; i++, k *= m)
{
cnt[i] = k;
ans += cnt[i]; // 表示前缀不选的组合数(减去无效递归)
}
return ans + f(n, ret, cnt, len, offset);
}
};
统计整数数目
题目大意:给你两个数字字符串 num1 和 num2 ,以及两个整数 max_sum 和 min_sum 。如果一个整数 x 满足以下条件,我们称它是一个好整数:
- \(num1 \le x \le num2\)
- \(min\_sum \le digit_sum(x) \le max\_sum\)
返回答案mod \(10^9 + 7\)的结果。
【解题】:num的返回很大超过了longlong的范围,因此我们要是从 0 开始枚举的话不但时间会超,而且我们也存不下,但是 min_sum 的范围有很小,这题的解法还是和上面的题一样,选择每一位的数。
统计(0, num2)的总数 cnt1,(0, num1 - 1)的总数 cnt2,用 cnt1 - cnt2 即是答案。
但是 num 给的是字符串,num1 - 1 需要我们单独设计字符串的减法,可以直接统计 (0, num1) 的数量cnt2,最后单独检查num1是否符合题意即可。
class Solution
{
public:
const static int N = 410, M = 25;
const int MOD = 1e9 + 7;
int dp[M][N][2];
static int mins, maxs, len;
void init()
{
for (int i = 0; i < M; i++)
for (int j = 0; j < N; j++)
for (int k = 0; k < 2; k++)
dp[i][j][k] = -1;
}
// pos :当前需要填的位置
// sum :前缀的累加和
// free:后续位置是否可以自由填写
int f(int pos, int sum, int free, string num)
{
// 可行性剪枝
if (sum > maxs) return 0;
if (sum + (len - pos) * 9 < mins) return 0;
if (pos == len) return 1;
if (dp[pos][sum][free] != -1) return dp[pos][sum][free]; // 记忆化搜索
int ans = 0;
int cur = num[pos] - '0';
if (free == 1)
{
for (int i = 0; i <= 9; i++)
{
ans = (ans + f(pos + 1, sum + i, 1, num)) % MOD;
}
}
else
{
for (int i = 0; i <= 9; i++)
{
if (i < cur) ans = (ans + f(pos + 1, sum + i, 1, num)) % MOD;
else if (i == cur) ans = (ans + f(pos + 1, sum + i, 0, num)) % MOD;
else break;
}
}
return dp[pos][sum][free] = ans;
}
int check(string num)
{
int tmp = 0;
for (auto ch : num)
{
tmp += ch - '0';
}
return (tmp >= mins && tmp <= maxs);
}
int count(string num1, string num2, int min_sum, int max_sum)
{
mins = min_sum, maxs = max_sum;
vector<int> ret(M);
len = num2.size();
for (int i = 0; i < len; i++) ret[i] = num2[i] - '0';
init();
int cnt1 = f(0, 0, 0, num2);
len = num1.size();
for (int i = 0; i < len; i++) ret[i] = num1[i] - '0';
init();
int cnt2 = f(0, 0, 0, num1);
return ((cnt1 - cnt2 + check(num1)) % MOD + MOD) % MOD;
}
};
int Solution::mins = 0;
int Solution::maxs = 0;
int Solution::len = 0;
统计特殊整形
如果一个正整数每一个数位都是 互不相同 的,我们称它是 特殊整数 。
给你一个正整数n,请你返回区间[1, n]之间特殊整数的数目。
【解题】:本题类似于 1.1 不同的是上界 n ,本题把 n 任意化了。
我们把答案分为三部分:
- 位数小于 n 的位数;
- 位数等于 n 的位数,最高位小于 n 的最高位;
- 位数等于 n 的位数,最高位等于 n 的最高位;
不难发现,前两种情况其实不需要递归展开,我们可以直接算出来:
- 对于位数小于 n 的位数的情况:9 * 9 * 8 * 7 * 6 ... 解法和 1.1 一样;
- 对于位数等于 n 的位数,最高位小于 n 的最高位;
需要递归展开的仅仅是最后一种情况。
code:
class Solution {
public:
// pos :当前要填的位,权值高的优先,这里是从高位往低位填
// offset:用于提取num的pos位
// status:状态压缩,它的第 i 个比特位表示 i 这个数字是否被用过
int f(int num, vector<int>& cnt, int pos, int offset, int status)
{
if (pos == 0)
{
// 此时所选的数字恰好等于n
return 1;
}
int first = (num / offset) % 10;
int ans = 0;
for (int i = 0; i < first; i++)
{
if (((1 << i) & status) == 0) ans += cnt[pos - 1]; // 直接结算
}
if (((1 << first) & status) == 0) ans += f(num, cnt, pos - 1, offset / 10, (1 << first) ^ status);
return ans;
}
int countSpecialNumbers(int n)
{
int tmp = n / 10;
int len = 1;
int offset = 1;
while (tmp)
{
tmp /= 10;
len++;
offset *= 10;
}
int ans = 0;
// 位数少于 n 的位数
if (len >= 2)
{
ans = 9;
int ret = 9;
for (int a = 2, b = 9; a < len; a++, b--)
{
ret *= b;
ans += ret;
}
}
// cnt[i] : 前面len - i 位已经确定了, 还剩 i 位没有确定,要求前缀len - i 不为 0,的剩余方案数
vector<int> cnt(len + 1);
cnt[0] = 1;
for (int k = 10 - len + 1, i = 1; i < len; i++, k++) cnt[i] = cnt[i - 1] * k;
// 位数等于 n 的位数,最高位小于 n 的最高位
int first = n / offset;
ans += (first - 1) * cnt[len - 1];
// 递归展开:位数等于 n 的位数,最高位等于 n 的最高位
ans += f(n, cnt, len - 1, offset / 10, (1 << first));
return ans;
}
};
至少有 1 位重复的数字
这题完全是上面那题的翻版,至少有一位重复出现的数字的反面就是没有重复数字出现。用n - 上面的答案就行。
code:
class Solution {
public:
// pos :当前要填的位,权值高的优先,这里是从高位往低位填
// offset:用于提取num的pos位
// status:状态压缩,它的第 i 个比特位表示 i 这个数字是否被用过
int f(int num, vector<int>& cnt, int pos, int offset, int status)
{
if (pos == 0)
{
// 此时所选的数字恰好等于n
return 1;
}
int first = (num / offset) % 10;
int ans = 0;
for (int i = 0; i < first; i++)
{
if (((1 << i) & status) == 0) ans += cnt[pos - 1]; // 直接结算
}
if (((1 << first) & status) == 0) ans += f(num, cnt, pos - 1, offset / 10, (1 << first) ^ status);
return ans;
}
int numDupDigitsAtMostN(int n)
{
int tmp = n / 10;
int len = 1;
int offset = 1;
while (tmp)
{
tmp /= 10;
len++;
offset *= 10;
}
int ans = 0;
// 位数少于 n 的位数
if (len >= 2)
{
ans = 9;
int ret = 9;
for (int a = 2, b = 9; a < len; a++, b--)
{
ret *= b;
ans += ret;
}
}
// cnt[i] : 前面len - i 位已经确定了, 还剩 i 位没有确定,要求前缀len - i 不为 0,的剩余方案数
vector<int> cnt(len + 1);
cnt[0] = 1;
for (int k = 10 - len + 1, i = 1; i < len; i++, k++) cnt[i] = cnt[i - 1] * k;
// 位数等于 n 的位数,最高位小于 n 的最高位
int first = n / offset;
ans += (first - 1) * cnt[len - 1];
// 递归展开:位数等于 n 的位数,最高位等于 n 的最高位
ans += f(n, cnt, len - 1, offset / 10, (1 << first));
return n - ans;
}
};
windy数
题目大意:不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy 想知道,在 a 和 b 之间,包括 a 和 b ,总共有多少个 windy 数?
保证 1≤a≤b≤2×109。
【解题】:
统计(a - b)区间内的windy数,其实使用(0 - b) 之间的windy数减去0 - a之间的windy数,这题的数据范围不大,没必要之前的一个题一样,用(0 - b)- (0 - a)然后单独判断a这个数是否合法,读入a的时候直接让a--,计算(0 - b) - (0 - a - 1)之间的windy数即可。
- 保证数的大小小于 传入的num,像前面几题一样分为位数少于num的位数;位数等于num的位数但是当前位和num当前位的数字一样;位数等于num 的位数,但是当前位的数字和num当前位的数字一样。
- 这次在选择数字的时候多了一个限制,当前选择的数字不能和前面数字的差值小于2,因此我们在传入参数的时候多传入一维pre表示前一个参数我们选择的情况。
code:
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <cmath>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define endl '\n'
const int N = 15;
int a, b;
int dp[N][N][N];
void init()
{
for(int i = 0; i < N; i++)
{
for(int j = 0; j < N; j++)
{
for(int k = 0; k < N; k++)
{
dp[i][j][k] = -1;
}
}
}
}
// num : 0 - num 的windy数
// offset :用于提取 num 的第 len 位
// len :从右到左第 len 位
// free:当前数字是否可以随便选择
int f(int num, int offset, int pre, int len, int free)
{
if(num == 0) return 1;
if(len == 0)
{
// 不同管pre,假设前面什么都没选就是0
return 1;
}
if(dp[pre][len][free] != -1) return dp[pre][len][free];
int ans = 0;
int cur = num / offset % 10;
if(free == 0)
{
// 当前数字不能随便选择
if(pre == 10)
{
// 前面没有选择过数字,此时就是num本身
ans += f(num, offset / 10, 10, len - 1, 1); // 接着不选
for(int i = 1; i < cur; i++)
{
ans += f(num, offset / 10, i, len - 1, 1);
}
ans += f(num, offset / 10, cur, len - 1, 0);
}
else
{
// 前面选择过数字
for(int i = 0; i < cur; i++)
{
if(abs(i - pre) >= 2)
{
ans += f(num, offset / 10, i, len - 1, 1);
}
}
if(abs(cur - pre) >= 2) ans += f(num, offset / 10, cur, len - 1, 0);
}
}
else // 可以随便选择
{
if(pre == 10)
{
// 前面还是没有选择过数字
ans += f(num, offset / 10, 10, len - 1, 1); // 接着不选
for(int i = 1; i <= 9; i++)
{
ans += f(num, offset / 10, i, len - 1, 1);
}
}
else
{
// 前面选择过数字
for(int i = 0; i <= 9; i++)
{
if(abs(i - pre) >= 2)
{
ans += f(num, offset / 10, i, len - 1, 1);
}
}
}
}
return dp[pre][len][free] = ans;
}
void solve()
{
cin >> a >> b;
a--;
int len = 1;
int offset = 1;
int tmp = b / 10;
while(tmp)
{
tmp /= 10;
len++;
offset *= 10;
}
init();
// 由于前面什么都没选初始pre传入10,上一位和num一致free传入0表示不能随便选。
int cnt1 = f(b, offset, 10, len, 0);
len = 1;
offset = 1;
tmp = a / 10;
while(tmp)
{
tmp /= 10;
len++;
offset *= 10;
}
init();
int cnt2 = f(a, offset, 10, len, 0);
cout << cnt1 - cnt2 << endl;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while(T--)
{
solve();
}
return 0;
}
萌数
题目大意:“存在长度至少为 2 的回文子串”的数是萌的,想知道从 l 到 r 的所有整数中有多少个萌数。只需要输出答案对 1000000007(109+7)的余数。
对于全部的数据,n≤1000,l<r。
【解题】:
● 首先这题的 n 很长,longlong存不下,我们也没必要单独设计高精度减法,思路和前面的一样统计(0 - r) - (0 - l) 的萌数,单独判断a是否是萌数就行。
● 萌数的判断:存在长度至少为 2 的回文子串,我们不好单独判断这个条件,可以用总数减去它的反面:对于位置 i 和 i - 1, i - 2 位置的数都不同。
● 然后这题就变成了求(0 - num)之间 满足条件 当前选择的数和i - 1, i - 2 位置的数都不同的数的个数。
● 最后涉及到减法,即用num - 上面算出来的结果,由于num可能还是很大,我们采用秦九韶算法变还原边取模。
code:
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define endl '\n'
const int N = 15;
const int MOD = 1e9 + 7;
string l, r;
int dp[1010][N][N][2];
void init()
{
for(int i = 0; i <= 1000; i++)
{
for(int j = 0; j < N; j++)
{
for(int k = 0; k < N; k++)
{
for(int m = 0; m < 2; m++)
{
dp[i][j][k][m] = -1;
}
}
}
}
}
int f(string num, int pos, int pp, int p, int free)
{
if(pos == num.size())
{
return 1;
}
if(dp[pos][pp][p][free] != -1) return dp[pos][pp][p][free];
int ans = 0;
int cur = num[pos] - '0';
if(free == 0) // 当前数字不能随便选择
{
if(p == 10) // 前面没有选择过数字
{
// num 本身
ans += f(num, pos + 1, 10, 10, 1); // 继续不选
for(int i = 1; i < cur; i++)
{
ans += f(num, pos + 1, p, i, 1);
ans %= MOD;
}
ans += f(num, pos + 1, p, cur, 0);
ans %= MOD;
}
else
{
// 前面选择过数字
for(int i = 0; i <= cur; i++)
{
if(i != pp && i != p)
{
if(i != cur) ans += f(num, pos + 1, p, i, 1);
else ans += f(num, pos + 1, p, cur, 0);
ans %= MOD;
}
}
}
}
else // 当前位可以自由选择
{
if(p == 10) // 前面没有选择过数字
{
ans += f(num, pos + 1, 10, 10, 1); // 继续不选
ans %= MOD;
for(int i = 1; i <= 9; i++)
{
ans += f(num, pos + 1, p, i, 1);
ans %= MOD;
}
}
else // 前面选择过数字
{
for(int i = 0; i <= 9; i++)
{
if(i != pp && i != p)
{
ans += f(num, pos + 1, p, i, 1);
ans %= MOD;
}
}
}
}
return dp[pos][pp][p][free] = ans;
}
int calc(string s)
{
if(s[0] == '0') return 0;
int n = s.size();
LL sum = 0;
LL base = 1;
for(int i = n - 1; i >= 0; i--)
{
sum = (sum + base * (s[i] - '0')) % MOD;
base = (base * 10) % MOD;
}
sum = (sum + 1) % MOD;
int ret = f(s, 0, 10, 10, 0);
return ((sum - ret) % MOD + MOD) % MOD;
}
int check(string s)
{
if(s.size() == 1) return 0;
if(s[0] == s[1]) return 1;
for(int i = 2; i < s.size(); i++)
{
if(s[i] == s[i - 1] || s[i] == s[i - 2]) return 1;
}
return 0;
}
void solve()
{
cin >> l >> r;
int len = l.size();
init();
int cnt1 = calc(r);
len = r.size();
init();
int cnt2 = calc(l);
cout << ((cnt1 - cnt2 + check(l)) % MOD + MOD) % MOD;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while(T--)
{
solve();
}
return 0;
}
不含连续 1 的非负整数
题目大意:给定一个正整数,请你统计在范围的非负整数中,有多少个整数的二进制表示中不存在 连续的 1
【解题】:
看下面代码吧。
code1:
class Solution {
public:
int findIntegers(int n)
{
// cnt[i]:第 i 位填1 0都可以的情况下一共有多少种情况。
vector<int> cnt(35);
cnt[0] = 1; cnt[1] = 2;
// 当前位填 0 下一位可以填0 1 -> cnt[i - 1]
// 当前位填 1 下一位必须填0 -> cnt[i - 2]
for(int i = 2; i <= 30; i++) cnt[i] = cnt[i - 1] + cnt[i - 2];
int ans = 0;
for(int i = 30; i >= -1; i--)
{
if(i == -1)
{
// num 本身就合法
ans++;
break;
}
if((n & (1 << i)) != 0)
{
ans += cnt[i];
if((n & (1 << (i + 1))) != 0) break; // 如果上一位还是 1,就直接结算,后续不合法.
}
}
return ans;
}
};
code2:
class Solution {
public:
int f(vector<int> & cnt, int pos, int num)
{
if(pos == -1)
{
// num 本身
return 1;
}
int ans = 0;
if((num & (1 << pos)) != 0)
{
ans += cnt[pos]; // 结算后面的情况
if((num & (1 << (pos + 1))) == 0) ans += f(cnt, pos - 1, num); // 只有前一位不是1的情况才往后递归
}
else ans += f(cnt, pos - 1, num); // 该位是 0 二进制中只能填 0 ,接着往后递归。
return ans;
}
int findIntegers(int n)
{
// cnt[i]:第 i 位填1 0都可以的情况下一共有多少种情况。
vector<int> cnt(35);
cnt[0] = 1; cnt[1] = 2;
// 当前位填 0 下一位可以填0 1 -> cnt[i - 1]
// 当前位填 1 下一位必须填0 -> cnt[i - 2]
for(int i = 2; i <= 30; i++) cnt[i] = cnt[i - 1] + cnt[i - 2];
return f(cnt, 30, n);
}
};
数字计数
题目大意:给定两个正整数 a 和 b,求在 [a,b] 中的所有整数中,每个数码(digit)各出现了多少次。
- 对于 100% 的数据,保证 1≤a≤b≤1012。
【解题】:
code:我们可以把答案拆分成每一位上是 d 出现合法数字的个数,用(1,b) - (1 - a - 1) 就可以得到最后的结果。
难度在于如何合理的讨论每种情况,还有对特殊情况 0 的处理,看下面代码。
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define endl '\n'
const int N = 2e5 + 10;
LL a, b;
// 统计 1 - num 中数字d出现的次数
LL count(LL num, int d)
{
// 情况1 : d != 0 d = 5
// 30583
// cur < d
// 3058x
// 0 ~ 3057 0 ~ 0
// cur > d
// 305x3
// 0 ~ 304 0 ~ 9
// 305 0 ~ 9
// cur == d
// 30x83
// 0 ~ 29 0 ~ 99
// 30 0 ~ 9
// 情况2 : d == 0
// 30583
// cur != 0
// 3058x
// 0001 ~ 3057 0 ~ 9
// 3058 0 ~ 9
// cur == 0
// 3x583
// 1 ~ 2 0 ~ 999
// 3 0 ~ 583
LL tmp = num;
LL ans = 0;
for(LL left = 1, right = 1, cur; tmp != 0; tmp /= 10, right *= 10)
{
left = tmp / 10; cur = tmp % 10;
if(d == 0) left--; // 如果 d == 0,要求前缀从 1 开始,前去0的情况
ans += left * right; // 当cur 前面的数字小于num时,后面可以任意选择 0 ~ right
if(cur > d)
{
// 当cur > d 时前缀和num相同时,后面仍然可以自由选择 0 ~ right
ans += right;
}
else if(cur == d)
{
// 当cur == d时,前缀和num相同,后面只能选择 0 ~ num % right
ans += num % right + 1;
}
}
return ans;
}
void solve()
{
cin >> a >> b;
for(int i = 0; i <= 9; i++) cout << count(b, i) - count(a - 1, i) << " ";
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while(T--)
{
solve();
}
return 0;
}
总结
通过这 9 道题下来可以发现数位dp其实就是分类讨论的搜索题,这类题通常是给定一个条件,然后让你统计[l, r]范围内满足条件的种数,它的解法是一种从左到右的尝试,用[1, r] 的情况数 - [1, l - 1]的情况数,将从[l, r]的枚举 + 检测行为简化成了从 num 的最高位开始尝试填写满足要求的数,只要把情况列清楚相信不会是难题。
它之所以叫数位dp,或许是因为出现了重复子问题的调用,对其采用了记忆化数组的优化,而记忆化数组又通常可以改写成有底到顶的动态规划。
昨天复习 dp 的时候突然想到为什么线性的 dp 的解法是从最后一步划分情况,因为它本身就是从记忆化搜索该来的,从顶到底逐步拆解出子问题,到最后递归出口回归当然可以变成从已知的子问题出发,从底到顶逐步堆积成最终结果。
最后来看一下蓝桥杯的这道国赛题:
给定一个正整数 N。你可以对 N 的任意一位数字执行任意次以下 2 种操作:
- 将该位数字加 1。如果该位数字已经是 9,加 1 之后变成 0。
- 将该位数字减 1。如果该位数字已经是 0,减 1 之后变成 9。
你现在总共可以执行 1 号操作不超过 A 次,2 号操作不超过 B 次。
请问你最大可以将 N 变成多少?
对于 100% 的数据, 1≤N≤1017;0≤A,B≤100
【解题】:这题其实考察的不是数位dp,看题解写的 dp 其实就是暴力枚举所有情况,和搜索是一样的,而且搜索有很多很好写的剪枝操作,时间空间优于 dp 的暴力转移。
code:
#include <iostream>
#include <vector>
#include <unordered_map>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
#define endl '\n'
const int N = 2e5 + 10;
string ans;
int n;
void dfs(int pos, int a, int b, string s)
{
if (pos == n)
{
if (s > ans)
{
ans = s;
}
return;
}
int up = '9' - s[pos], down = s[pos] - '0' + 1;
if (up <= a || down <= b)
{
// 如果可以变成9,就都尝试一下
s[pos] = '9';
if(up <= a) dfs(pos + 1, a - up, b, s);
if(down <= b) dfs(pos + 1, a, b - down, s);
}
else
{
// 两种操作都不能使该位置变为9,把操作1用完
s[pos] += a;
dfs(pos + 1, 0, b, s);
}
}
void solve()
{
string s; int a, b; cin >> s >> a >> b;
n = s.size();
dfs(0, a, b, s);
cout << ans << endl;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int T = 1;
// cin >> T;
while (T--)
{
solve();
}
return 0;
}
浙公网安备 33010602011771号