数位DP详细解析
1.定义与原理
2.例题一:
题目
思路
我们做数位
-
1.对于求一段数中满足条件的数的个数,可以用前缀和的方法完成,即
; -
2.在想思路时,可以把问题转换成 树 的形式,对每个步骤分情况讨论,下面拿这道题来举例子:
首先分析样例,把
总结规律,得出结论,问题转化为:从
那么我们结合这张图具体分析:
我们令一个
比如我们对
然后我们分出两种情况:小于
比如这个数为
-
1.当取
到 时,又分为取非 和取 两种情况。当取的数不是 时,意味着我们将在剩下的 个数中填 个 ,总方案数为 ;否则,说明已经填入了一个 ,总方案数为 。 -
2.当取的数为
时,说明这一位已经被固定,于是我们继续用同样思路推下一位即可。
最后,当我们推到
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
// #define int long long
using namespace std;
#define N 1010
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()
int l, r, k, b;
int c[N][N];
void init () {
For (i, 0, N - 1) {
For (j, 0, i) {
if (j == 0) c[i][j] = 1;
else c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
}
} //初始化组合数
}
int dp (int n) {
if (n == 0) return 0; //特判边界
vector <int> nums;
while (n) nums.push_back (n % b), n /= b; //将n转化成b进制数
int res = 0, lst = 0; //前面是模板
//res是答案,lst是计算右分支时的前缀信息(已经填好的数中1的个数)
for (int i = nums.size () - 1; i >= 0; i --) { //倒序循环,因为进制转换时存的数是倒序的
int x = nums[i];
if (x) { //只有x>0时才有左右分支
res += c[i][k - lst]; //首先肯定可以填0,,就在剩下i位中填k-lst个数
if (x > 1) { //如果可以取1,那就假设取的就是1
if (k - lst - 1 >= 0) res += c[i][k - lst - 1];
//还需取1的个数减1,记得判断一下是否还能再取
//因为对于右边分支取的就是x本身(x>1),所以不合法,直接break!
break;
} else { //当x==1时,只能取0,所以交给下一位,下一位可使用的1的个数会少1,体现在代码上是last+1
lst ++;
if (lst > k) break; //如果已经填了k个1,就退出
}
}
if (i == 0 && lst == k) { //最右侧分支上的方案
res ++;
} //如果算到最后一位且已经填了k个1,就退出
}
// cout << endl;
return res;
}
int main () {
IOS;
init ();
cin >> l >> r >> k >> b;
cout << dp (r) - dp (l - 1) << endl; //前缀和思想
return 0;
}
3.例题2:数字游戏
题目
思路
思路和上题一样,只是我们在处理左边分支时方法不一样。
本题让我们求
我们发现,当最高位取值为
所以,预处理的状态转移方程为
然后在数位
然后判断,如果已经出现了降序,就退出,最后处理一下右边分支即可。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
//#define int long long
using namespace std;
#define N 15
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()
int a, b, f[N][N];
//f[i][j]表示以j为最高位的i位数的数的个数
void init () {
For (i, 0, 9) f[1][i] = 1;
For (i, 2, N - 1)
For (j, 0, 9)
For (k, j, 9)
f[i][j] += f[i - 1][k];
}
int dp (int n) {
if (n == 0) return 1;
vector <int> nums;
while (n) nums.push_back (n % 10), n /= 10;
int res = 0, lst = 0;
//----------------------------
for (int i = nums.size () - 1; i >= 0; i --) {
int x = nums[i];
For (j, lst, x - 1)
res += f[i + 1][j];
if (x < lst) break;
lst = x;
if (i == 0) res ++;
}
return res;
}
int main () {
IOS;
init ();
while (cin >> a >> b) {
cout << dp (b) - dp (a - 1) << endl;
}
return 0;
}
4.例题3:Windy数
题目
思路
还是一样的在预处理时很容易推出公式:在满足相邻两位之差至少为
然后分别处理左边分支和右边分支:
-
1.左边:首先要特判一下,如果是最高位就从1开始枚举,否则从0开始枚举;然后枚举到当前位减1,每次判断如果合法,就将答案加上
-
2.右边:首先判断能不能往下做分支,就是说当前位是否比上一位至少大2。如果是的话,就将
更新,否则说明没有分支,直接退出循环。
然后如果已经算到最后一位,就将答案加1即可。
最后,因为这个数不能带前导
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cmath>
//#define int long long
using namespace std;
#define N 15
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()
int a, b, f[N][N];
//f[i][j]表示以j为最高位的i位数的数的个数
void init () {
For (i, 0, 9) f[1][i] = 1;
For (i, 2, N - 1)
For (j, 0, 9)
For (k, 0, 9)
if (abs (j - k) >= 2)
f[i][j] += f[i - 1][k];
return ;
}
int dp (int n) {
if (n == 0) return 0;
vector <int> nums;
while (n) nums.push_back (n % 10), n /= 10;
int res = 0, lst = -2; //lst记录上一位数字,初始值需比0~9的任意数字之差>=2
//----------------------------
for (int i = nums.size () - 1; i >= 0; i --) {
int x = nums[i];
for (int j = i == nums.size () - 1; j < x; j ++)
if (abs (j - lst) >= 2)
res += f[i + 1][j];
if (abs (x - lst) >= 2) lst = x;
else break;
if (i == 0) res ++;
}
//特殊处理有前导0的数
For (i, 1, nums.size () - 1)
For (j, 1, 9)
res += f[i][j];
return res;
}
int main () {
IOS;
init ();
cin >> a >> b;
cout << dp (b) - dp (a - 1) << endl;
return 0;
}
5.例题4:数字游戏II
题目
思路
还是一样的思路,我们着重讲预处理的方法:
我们用
每当我们取下一位时,假如我们取的数为
所以状态转移方程为
然后数位
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cmath>
//#define int long long
using namespace std;
#define N 15
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()
int a, b, p, f[N][N][110];
//f[i][j]表示以j为最高位的i位数的数的个数
int mod (int x, int y) {
return (x % y + y) % y;
}//得到整数余数
void init () {
For (i, 0, 9) f[1][i][i % p] ++;
for (int i = 2; i < N; i ++)
for (int j = 0; j <= 9; j ++)
for (int k = 0; k < p; k ++)
for (int x = 0; x <= 9; x ++)
f[i][j][k] += f[i - 1][x][mod (k - j, p)];
}
int dp (int n) {
if (n == 0) return 1;
vector <int> nums;
while (n) nums.push_back (n % 10), n /= 10;
int res = 0, lst = 0; //lst记录前面所有数字之和
//----------------------------
for (int i = nums.size () - 1; i >= 0; i --) {
int x = nums[i];
for (int j = 0; j < x; j ++)
res += f[i + 1][j][mod (-lst, p)];
//第三维解释:因为前面数之和(lst)加上后面数之和 mod p=0,所以后面数之和为 (-lst mod p)
lst += x;
if (i == 0 && lst % p == 0) res ++;
}
return res;
}
int main () {
IOS;
while (cin >> a >> b >> p) {
init ();
cout << dp (b) - dp (a - 1) << endl;
}
return 0;
}
6.例题5:不要62
题目
思路
我们用
然后再正常跑一遍数位
需要注意的就是需要随时判断一下当前位是否为
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
//#define int long long
using namespace std;
#define N 110
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(0),cin.tie(),cout.tie()
int l, r, f[N][N];
//f[i][j]表示以j为最高位的i位数的满足条件的数的个数
void init () {
for (int i = 0; i <= 9; i ++)
if (i != 4)
f[1][i] = 1;
for (int i = 2; i < N; i ++) {
for (int j = 0; j <= 9; j ++) {
if (j == 4) continue;
for (int k = 0; k <= 9; k ++) {
if (k == 4 || (j == 6 && k == 2)) continue;
f[i][j] += f[i - 1][k];
}
}
}
}
int dp (int n) {
if (!n) return 1;
vector <int> nums;
while (n) nums.push_back (n % 10), n /= 10;
int res = 0, lst = 0; //lst记录上一位的数值,只要不为6即可,用于判断是否组成62
//----------------------------
for (int i = nums.size () - 1; i >= 0; i --) {
int x = nums[i];
for (int j = 0; j < x; j ++) {
if (j == 4 || (lst == 6 && j == 2)) continue;
res += f[i + 1][j];
}
if (x == 4 || (lst == 6 && x == 2)) break;
lst = x;
if (!i) res ++;
}
return res;
}
int main () {
IOS;
init ();
while (cin >> l >> r, l || r) {
cout << dp (r) - dp (l - 1) << endl;
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!