数位DP经典例题

例题1:度的数量

题意

求给定区间\([X,Y]\)中满足下列条件的整数个数:这个数恰好等于\(K\)个互不相等的\(B\)的整数次幂之和。

例如,设\(X=15, Y=20, K=2, B=2\),则有且仅有下列三个数满足题意:

  • \(17 = 2^{4} + 2^{0}\)
  • \(18 = 2^{4} + 2^{1}\)
  • \(20 = 2^{4} + 2^{2}\)

题目链接:https://www.acwing.com/problem/content/1083/

数据范围

\(1 \leq X \leq Y \leq 2^{31} − 1\)
\(1 \leq K \leq 20\)
\(2 \leq B \leq 10\)

思路

首先,按照数位DP惯用套路,将问题转化为\(g(Y) - g(X-1)\),其中\(g(x)\)表示前\(x\)个数中满足要求的数字个数。

然后,我们可以将\(x\)转化为\(B\)进制,这样通过统计\(1\)的个数就能快速判断是否满足要求。

\(f(i,j)\)表示如果正在枚举第\(i\)位且已经有\(j\)\(1\),答案有多少个(有limit的单算)。则\(f(i,j) = \sum\limits_{t=0}^{up} f(i - 1,j+t)\)

由于记忆化搜索版数位DP时间复杂度较高,因此考虑剪枝:在枚举的过程中,如果数量超过了\(K\),则不继续搜索。

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 35;

int l, r, k, b;
int f[N][N];
//f(i, j)表示如果正在枚举第i位且已经有j个b的整数次幂,答案有多少个
//最终答案为f(p - 1, 0)
int a[N];

int dfs(int p, int num, bool limit) //limit为1表示当前位上有限制,为0表示无限制
{
    if(p < 0) return num == k; //枚举完最后一位,判断是否为答案
    if(!limit && f[p][num] != -1) return f[p][num];
    int up = limit ? min(a[p], 1) : 1; //当前位的选择上限
    int ans = 0;
    for(int i = 0; i <= up; i ++) {
        if(num + i > k) continue;
        ans += dfs(p - 1, num + i, limit && i == a[p]);
    }
    if(!limit) f[p][num] = ans;
    return ans;
}

int solve(int x)
{
    int p = 0;
    while(x) {
        a[p ++] = x % b;
        x /= b;
    }
    memset(f, -1, sizeof f);
    return dfs(p - 1, 0, 1);
}

int main()
{
    cin >> l >> r >> k >> b;
    printf("%d\n", solve(r) - solve(l - 1));
    return 0;
}

例题2:数字游戏

题意

某人命名了一种不降数,这种数字必须满足从左到右各位数字呈非下降关系,如\(123, 446\)

现在大家决定玩一个游戏,指定一个整数闭区间\([a,b]\),问这个区间内有多少个不降数。

题目链接:https://www.acwing.com/problem/content/1084/

数据范围

\(1 \leq a \leq b \leq 2^{31}−1\)

思路

在搜索的时候记录一下上一位是多少即可。

\(f(i,j)\)表示如果正在枚举第\(i\)位且上一位是\(j\),答案有多少个(有limit的单算)。则\(f(i,j) = \sum\limits_{t=j+1}^{up} f(i - 1,t)\)

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 15;

int l, r;
int f[N][N];
int a[N];

int dfs(int p, int pre, bool limit)
{
    if(p < 0) return 1;
    if(!limit && f[p][pre] != -1) return f[p][pre];
    int up = limit ? a[p] : 9;
    int ans = 0;
    for(int i = pre; i <= up; i ++) {
        ans += dfs(p - 1, i, limit && i == a[p]);
    }
    if(!limit) f[p][pre] = ans;
    return ans;
}

int solve(int x)
{
    int p = 0;
    while(x) {
        a[p ++] = x % 10;
        x /= 10;
    }
    memset(f, -1, sizeof f);
    return dfs(p - 1, 0, 1);
}

int main()
{
    while(~scanf("%d%d", &l, &r)) printf("%d\n", solve(r) - solve(l - 1));
    return 0;
}

例题3:Windy数

题意

Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为\(2\)的正整数被称为 Windy 数。

Windy 想知道,在\(A\)\(B\)之间,包括\(A\)\(B\),总共有多少个 Windy 数?

题目链接:https://www.acwing.com/problem/content/1085/

数据范围

\(1 \leq A \leq B \leq 2 \times 10^9\)

思路

这道题需要考虑前导零。考虑前导零的意思就是,前导零部分不需要满足题目中的限制条件。

与limit处理方法类似,是否是前导零用一个标记lead表示,然后单独去讨论前导零的情况即可。

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 15;

char L[N], R[N];
int a[N];
ll f[N][N];

ll dfs(int p, int pre, bool lead, bool limit)
{
    if(p < 0) return 1;
    if(!limit && !lead && f[p][pre] != -1) return f[p][pre];
    int up = limit ? a[p] : 9;
    ll ans = 0;
    for(int i = 0; i <= up; i ++) {
        if(lead) { //如果为前导零
            if(!i) ans += dfs(p - 1, 0, 1, limit && i == a[p]); //如果当前填0
            else ans += dfs(p - 1, i, 0, limit && i == a[p]); //如果当前不填0
        }
        else { //不是前导零
            if(abs(i - pre) >= 2) ans += dfs(p - 1, i, 0, limit && i == a[p]);
        }
    }
    if(!limit && !lead) f[p][pre] = ans;
    return ans;
}

ll solve(string s)
{
    int p = 0;
    int len = s.size();
    for(int i = len - 1; i >= 0; i --) a[p ++] = s[i] - '0';
    memset(f, -1, sizeof f);
    return dfs(p - 1, 0, 1, 1);
}

bool check(string s)
{
    int len = s.size();
    for(int i = 1; i < len; i ++) {
        int a = s[i] - '0', b = s[i - 1] - '0';
        if(abs(a - b) < 2) return false;
    }
    return true;
}

int main()
{
    cin >> L >> R;
    printf("%lld", solve(R) - solve(L) + check(L));
    return 0;
}

例题4:数字游戏II

题意

某人又命名了一种取模数,这种数字必须满足各位数字之和\(\mod N\)\(0\)

现在大家又要玩游戏了,指定一个整数闭区间\([a.b]\),问这个区间内有多少个取模数。

题目链接:https://www.acwing.com/problem/content/1086/

数据范围

\(1 \leq a,b \leq 2^{31}−1\)
\(1 \leq N < 100\)

思路

这道题就是就是经典的计数DP拓展到了数位DP。

\(f(i,j)\)表示当前枚举到第\(i\)位且模数为\(j\)的情况下,方案数为多少。\(f(i, j) = \sum\limits_{t=0}^{up} f(i-1, (j+t)\% N)\)

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 15, M = 110;

int l, r, n;
int a[N];
int f[N][M];

int dfs(int p, int num, bool limit)
{
    if(p < 0) return !num;
    if(!limit && f[p][num] != -1) return f[p][num];
    int up = limit ? a[p] : 9;
    int ans = 0;
    for(int i = 0; i <= up; i ++) {
        ans += dfs(p - 1, (num + i) % n, limit && i == a[p]);
    }
    if(!limit) f[p][num] = ans;
    return ans;
}

int solve(int x)
{
    int p = 0;
    while(x) {
        a[p ++] = x % 10;
        x /= 10;
    }
    memset(f, -1, sizeof f);
    return dfs(p - 1, 0, 1);
}

int main()
{
    while(cin >> l >> r >> n) printf("%d\n", solve(r) - solve(l - 1));
    return 0;
}

例题5:不要62

题意

\([L, R]\)之间有多少数字里面没有\(4\)\(62\)

题目链接:https://www.acwing.com/problem/content/1087/

数据范围

\(1 \leq L \leq R \leq 10^9\)

思路

首先看没有\(4\)的情况,我们只需要在枚举当前位时,把\(4\)去掉即可。

对于没有\(62\)的情况,我们记录一个pre,如果pre为\(6\)且当前位为\(4\),则continue

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 15;

int l, r;
int a[N];
int f[N][N];

int dfs(int p, int pre, bool limit)
{
    if(p < 0) return 1;
    if(!limit && f[p][pre] != -1) return f[p][pre];
    int up = limit ? a[p] : 9;
    int ans = 0;
    for(int i = 0; i <= up; i ++) {
        if(i == 4) continue;
        if(pre == 6 && i == 2) continue;
        ans += dfs(p - 1, i, limit && i == a[p]);
    }
    if(!limit) f[p][pre] = ans;
    return ans;
}

int solve(int x)
{
    int p = 0;
    while(x) {
        a[p ++] = x % 10;
        x /= 10;
    }
    memset(f, -1, sizeof f);
    return dfs(p - 1, 0, 1);
}

int main()
{
    while(cin >> l >> r) {
        if(!l && !r) break;
        cout << solve(r) - solve(l - 1) << endl;
    }
    return 0;
}

例题6:计数问题

题意

给定两个整数\(a\)\(b\),求\(a\)\(b\)之间的所有数字中\(0 \sim 9\)的出现次数。

题目链接:https://www.acwing.com/problem/content/340/

数据范围

\(0 < a, b < 10^8\)

思路

这道题需要统计\(10\)个量,因此我们可以跑\(10\)次数位DP,分别统计每个数字的出现个数。

由于这道题前导零是不算在内的,因此需要考虑前导零的情况。

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 15;

int l, r;
int a[N];
int f[N][N];

int dfs(int p, int num, int x, bool lead, bool limit)
{
    if(p < 0) return num;
    if(!limit && !lead && f[p][num] != -1) return f[p][num];
    int up = limit ? a[p] : 9;
    int ans = 0;
    for(int i = 0; i <= up; i ++) {
        if(lead) {
            if(!i) ans += dfs(p - 1, num, x, lead, limit && i == a[p]);
            else ans += dfs(p - 1, num + (x == i), x, 0, limit && i == a[p]);
        }
        else ans += dfs(p - 1, num + (x == i), x, lead, limit && i == a[p]);
    }
    if(!limit && !lead) f[p][num] = ans;
    return ans;
}

int solve(int x, int t)
{
    int p = 0;
    while(x) {
        a[p ++] = x % 10;
        x /= 10;
    }
    memset(f, -1, sizeof f);
    return dfs(p - 1, 0, t, 1, 1);
}

int main()
{
    while(cin >> l >> r) {
        if(!l && !r) continue;
        if(l > r) swap(l, r);
        for(int i = 0; i < 10; i ++) {
            printf("%d ", solve(r, i) - solve(l - 1, i));
        }
        printf("\n");
    }
    return 0;
}
posted @ 2022-09-27 11:37  pbc的成长之路  阅读(150)  评论(0编辑  收藏  举报