数位DP专题

文章介绍了数位动态规划(DP)的基本思想、方法,包括记忆化搜索策略,以及如何处理搜索参数如非零数个数、状态转移规则和剪枝技巧。通过实例分析了CF1036c、ATabc208e、LGP4999和LGP4317等题目,展示了如何运用数位DP解决实际问题。
摘要由CSDN通过智能技术生成

DP是一种建立模型,划分出阶段性状态,寻找转移规律的思想,如果硬要拿模板套未免有些舍本求末。但是数位DP还是有一些普遍规律,总结如下:

1.数位DP是求 F ( x ) F(x) F(x)的问题,有点类似于数论函数,其中的 x x x代表从 1... x 1...x 1...x的整数集合 X X X。返回的值代表在 X X X中满足某个条件的整数个数,或者满足条件的整数函数值。

2.数位DP通常采用的方法是记忆化搜索,其中搜索的参数往往有(不一定都要):
2.1搜索到当前的位置 p p p
2.2当前位是否可以从0搜索到9,还是只能搜索到给定的 x x x p p p上的那一位
2.3前面是否搜索了非0数(因为某些条件下如00…03不是一个正确的数)
2.4搜索中记录的状态

3.搜索的方法是将 x x x视为一个数字序列,从0位开始到n-1位,当搜到n位(即越界时)检查状态,看是否满足条件然后返回。

列举几道题

CF 1036c

here
算是比较简单的入门题
这个题的条件是不超过3个非0数,所以我们可以记录一个nz(not zero),代表非0数的个数
cap代表这一位是否“顶住” x x x,比如本来这位是3,如果取了2,那么下一位就能取0-9.但是如果这位是3,前面的搜索都是“顶住” x x x,那么下一位还是不能随心所欲去取数字。
这个题只有满足 n z ≤ 3 nz \le3 nz3才会继续搜下去,所以可以剪掉那些 n z > 3 nz \gt 3 nz>3的部分。
有一个隐患是init函数中的写法需要每次memset一遍,如果dp记录的状态太大容易TLE

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>
#include <cstring>
#include <climits>
#include <cstdlib>
#include <cmath>
#include <map>
#include <set>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>
#define LT(x) (x * 2)
#define RT(x) (x * 2 + 1)

using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef vector<int> vi;

ll mod = ll(1e9 + 7);
int T;
ll f[20][2][4];
int a[20];
int n;


ll get(int pos, int cap, int nz) {
    if (pos == n) {
        return 1;
    }
    if (f[pos][cap][nz] != -1) 
        return f[pos][cap][nz];
    ll& res = f[pos][cap][nz];
    res = 0;
    int nmax = cap ? a[pos] : 9;
    for (int i = 0; i <= nmax; ++i) {
        int ncap = i < a[pos] ? 0 : cap;
        int nnz = i == 0 ? nz : nz + 1;
        if (nnz <= 3) {
            res += get(pos + 1, ncap, nnz);
        }
    }
    return res;
}

void init(ll x) {
    n = 0;
    int temp[20] = { 0 };
    while (x) {
        temp[n++] = x % 10;
        x /= 10;
    }
    for (int i = 0; i < n; ++i)
        a[i] = temp[n - i - 1];
    memset(f, 0xff, sizeof(f));
}


int main() {
    //freopen("in.txt", "r", stdin);
    scanf("%d", &T);
    while (T--) {
        ll l, r;
        scanf("%lld%lld", &l, &r);
        l--;
        init(l);
        ll al = get(0, 1, 0);
        init(r);
        ll ar = get(0, 1, 0);
        printf("%lld\n", ar - al);
    }
    
    return 0;
}

AT abc208e

here
要难一点了,这个题的难点在于状态不好搞,乘积怎么表示。
但实际上小于9的自然数乘积并不多,可以用map离散记录。
这个题的第二个trick是乘积0的处理, 如果前面位不是全为0,那么如果填了0乘积就为0,否则乘积按1来计算
最后在剪枝的时候不要把乘积超过k的状态剪掉,因为后面还可能会变成0.这里的处理是如果超过k,那么把状态置为k+1。最后结算的时候检查乘积是否超过k即可。

#include <cstring>
#include <climits>
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <map>

using namespace std;
typedef pair<int, int> pii;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef vector<int> vi;

char s[20];
int a[20];
int n;
ll k;
map<ll, ll> f[20][2][2];


ll get(int pos, int cap, int lead, ll val) {
    if(pos == n) {
        // printf("%d %d %d %lld %lld\n", pos, cap, lead, val, 1);
        if (val <= k)
            return 1ll;
        else
            return 0;
    } 
    if(f[pos][cap][lead].count(val)) {
        return f[pos][cap][lead][val];
    }
    ll &res = f[pos][cap][lead][val];
    int ncap = a[pos] == 0 ? cap : 0;
    int nval = lead ? 0 : 1;
    res += get(pos + 1, ncap, lead, nval);
    int nmax = cap ? a[pos]: 9;
    for(int i = 1; i <= nmax; ++ i) {
        ncap = i < a[pos] ? 0 : cap;
        nval = val * i > k ? k + 1 : val * i;
        res += get(pos + 1, ncap, 1, nval);
    }
    //printf("%d %d %d %lld %lld\n", pos, cap, lead, val, res);
    return res;
}


int main() {
    //freopen("in.txt", "r", stdin);
    scanf("%s", s);
    n = strlen(s);
    for(int i = 0; i < n; ++ i) 
        a[i] = s[i] - '0';
    scanf("%lld", &k);
    ll ret = get(0, 1, 0, 1);
    printf("%lld\n", ret - 1);
    return 0;
}

LG P4999

烦人的数学作业
入门题,记录搜到当前的和作为状态

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>
#include <cstring>
#include <climits>
#include <cstdlib>
#include <cmath>
#include <map>
#include <set>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>
#define LT(x) (x * 2)
#define RT(x) (x * 2 + 1)

using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef vector<int> vi;

ll mod = ll(1e9 + 7);
int n;
int a[20];
ll dp[20][2][180];


void init(ll x) {
    int temp[20] = {};
    n = 0;
    while (x) {
        temp[n++] = x % 10;
        x /= 10;
    }
    for (int i = 0; i < n; ++i) {
        a[i] = temp[n - 1 - i];
    }
    memset(dp, 0xff, sizeof(dp));
}

ll dfs(int pos, int cap, ll cur) {
    if (dp[pos][cap][cur] != -1)
        return dp[pos][cap][cur];
    if (pos == n) {
        return cur;
    }
    ll& res = dp[pos][cap][cur];
    res = 0;
    int nmax = cap ? a[pos] : 9;
    for (int i = 0; i <= nmax; ++i) {
        int ncap = i < a[pos] ? 0 : cap;
        res += dfs(pos + 1, ncap, cur + i);
        res %= mod;
    }
    return res;
}



int main() {
    //freopen("in.txt", "r", stdin);
    int T;
    scanf("%d", &T);
    while (T--) {
        ll l, r;
        scanf("%lld%lld", &l, &r);
        l--;
        init(l);
        ll a0 = dfs(0, 1, 0);
        init(r);
        ll a1 = dfs(0, 1, 0);
        ll a = a1 - a0;
        if (a < 0) a += mod;
        printf("%lld\n", a);
    }
    return 0;
}

LG P4317

花神的数论题
和上一题几乎一样,只是最后求的是乘积

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <string>
#include <cstring>
#include <climits>
#include <cstdlib>
#include <cmath>
#include <map>
#include <set>
#include <vector>
#include <queue>
#include <unordered_map>
#include <algorithm>
#define LT(x) (x * 2)
#define RT(x) (x * 2 + 1)

using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef vector<int> vi;

int n;
ll x;
ll mod = (ll)(10000007);
ll dp[64][2][64];
int a[64];


ll dfs(int pos, int cap, int cnt) {
    if (dp[pos][cap][cnt] != -1)
        return dp[pos][cap][cnt];
    if (pos == n) {
        if (cnt == 0) return 1;
        else return cnt;
    }
    ll& res = dp[pos][cap][cnt];
    res = 1;
    int nmax = cap ? a[pos] : 1;
    for (int i = 0; i <= nmax; ++i) {
        int ncap = i < a[pos] ? 0 : cap;
        res *= dfs(pos + 1, ncap, cnt + i);
        res %= mod;
    }
    return res;
}



int main() {
    //freopen("in.txt", "r", stdin);
    memset(dp, 0xff, sizeof(dp));
    scanf("%lld", &x);
    int temp[64] = {};
    while (x) {
        temp[n++] = x % 2;
        x /= 2;
    }
    for (int i = 0; i < n; ++i)
        a[i] = temp[n - 1 - i];
    ll ans = dfs(0, 1, 0);
    printf("%lld\n", ans);
    return 0;
}


待续

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值