数位DP详细解析

1.定义与原理

image

2.例题一:

题目

Acwing 1081. 度的数量

思路

我们做数位 DP 时,一般有如下两个技巧方便做题,理清思路:

  • 1.对于求一段数中满足条件的数的个数,可以用前缀和的方法完成,即 ans=dp(r)dp(l1)

  • 2.在想思路时,可以把问题转换成 的形式,对每个步骤分情况讨论,下面拿这道题来举例子:

首先分析样例,把 1520 中的所有数转化为二进制:

15=1111,16=10000,17=10001,18=10010,19=10011,20=10100

总结规律,得出结论,问题转化为:从 lr 中所有数的 b 进制中恰好有 k1 的数的个数。

image

那么我们结合这张图具体分析:

我们令一个 N 位数,其每一位为 a(n1),a(n2)...a0,然后我们把每一位竖着写好,在从高到低对每一位分情况讨论。

比如我们对 a(n1) 讨论,因为这是个 N 进制数,所以其每一位都小于 N

然后我们分出两种情况:小于 a(n1) 和等于 a(n1)

比如这个数为 76543210,我们取第一位时可以取 06 之间的任何数,也可以取 7

  • 1.当取 06 时,又分为取非 1 和取 1 两种情况。当取的数不是 1 时,意味着我们将在剩下的 N1 个数中填 k1,总方案数为 CN1k;否则,说明已经填入了一个 1,总方案数为 CN1k1

  • 2.当取的数为 7 时,说明这一位已经被固定,于是我们继续用同样思路推下一位即可。

最后,当我们推到 a0 时,同样分两种情况,但此时已经不能再往下分支了,所以最后的答案就是图中所有左边的分支和 a0 这一块。

代码

#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:数字游戏

题目

AcWing 1082. 数字游戏

思路

思路和上题一样,只是我们在处理左边分支时方法不一样。

本题让我们求 1n 中的不降数。我们不妨用 f[i][j] 表示以 j 为最高位的 i 位数中有多少种取值方案。

我们发现,当最高位取值为 j 时,其下一位是从 j9 中的任意数字。如果下一位确定了,我们就把第 i 位抹掉,继续往下走。

所以,预处理的状态转移方程为 f[i][j]=f[i][j]+f[i1][k](k>=j)

然后在数位 DP 的过程中,我们首先加上左分支的方案数。为了保证不降序,我们从上一位 lst 开始枚举,当选完这一位数后,剩下要填的数的个数为 i0+1 个,所以每次答案加上 f[i+1][j]

然后判断,如果已经出现了降序,就退出,最后处理一下右边分支即可。

代码

#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数

题目

Acwing 1083. Windy数

思路

还是一样的在预处理时很容易推出公式:在满足相邻两位之差至少为 2 时,有公式 f[i][j]+=f[i1][k]

然后分别处理左边分支和右边分支:

  • 1.左边:首先要特判一下,如果是最高位就从1开始枚举,否则从0开始枚举;然后枚举到当前位减1,每次判断如果合法,就将答案加上 f[i+1][j]

  • 2.右边:首先判断能不能往下做分支,就是说当前位是否比上一位至少大2。如果是的话,就将 lst 更新,否则说明没有分支,直接退出循环。

然后如果已经算到最后一位,就将答案加1即可。

最后,因为这个数不能带前导 0,还要再处理一遍带前导 0 的数。

代码

#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

题目

Acwing 1084. 数字游戏 II

思路

还是一样的思路,我们着重讲预处理的方法:

我们用 f[i][j][k] 表示所有以 j 为最高位的所有数字和能整除 ki 位数的数量。

每当我们取下一位时,假如我们取的数为 x,那么位数少1,最高位为 x,其数字和取余后余数为 (kj)modp

所以状态转移方程为 f[i][j][k]+=f[i1][x][mod(kj,p)];

然后数位 DP 的过程还是差不多,稍微变通一下就可以了。

代码

#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

题目

Acwing 1085. 不要62

思路

我们用 f[i][j] 表示以 j 为最高位的 i 位数的满足条件的数的个数。首先常规操作处理完一位数后,对于每个满足条件的方案,有公式 f[i][j]+=f[i1][k]

然后再正常跑一遍数位 DP 即可。

需要注意的就是需要随时判断一下当前位是否为 4,或当前位与上一位是否为 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;
}
posted @   linbaicheng2022  阅读(399)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示