数位DP详解

数位dp一般针对统计某个区间符合一个或多个条件的数的数量,因为算法名字叫数位dp,所以我们需要对数位进行枚举

思路

考虑,传统做法,例如统计[L, R]之间有多少个数,该数至少含有一个1,传统做法,直接暴力,算出[0, R], [0, L - 1], 类似于前缀和一样,然后相减即可。但是复杂度无疑来到了o(N²), 还带有一个很大的常数。R一旦到1e6左右就寄了。
而数位dp提供了一种新思路,利用记忆化搜索(dp的一种实现方式),枚举数位的方式,减少了很多复杂状态的重复计算。

举个例子:
例如现在是算[0, 114519]
已经枚举完了11,现在是11XXXX,这个时候第一个X等于1,2,3的时候,种类数相同,因为后面的3个X可以任意枚举0-9(等于4的时候就不行,后面的第二个x就有5的上限的限制),这就是重复状态,数位dp,可以将这重复状态的计算给去掉。

还有一个特殊的限制是前导0签到爆0,例如4231,我们在记忆化dfs的时候,会出现00XX的情况,因为需要枚举0,0可能在中间(属于合法状态),但是如果前面全是0的这种情况(有些题目是合法的,有些题目不合法,根据题目而来)。所以我们需要维护以下几种东西:

1、pos 当前位置pos
2、limit 当前数位pos是否到达枚举上限
3、lead 前导0是否存在
4、others 其他题目给出的限制条件

模板

该算法模板大部分基本一致,dfs功底扎实的一下就能看懂!

int f[pos][limit][lead]
int dfs(int pos, ..., bool limit, bool lead)
{
    if (pos == cnt)//枚举到最后一位
        return 1;//不一定是1 有时候是题目限制
    int ans = 0;
    if (f != -1) return f;//该状态已经计算过了
    for (int v = 0; v <= (limit ? A[pos] : 9); v++)//根据limit判断枚举上限
    {
        //limit 和 lead 与当前位置的限制关系都是 &&
        ans += dfs(pos + 1, limit && v == A[pos], lead && v == 0);
    }
    return f = ans;
}

int fx(int x)
{
    if (x == 0)return 0;
    cnt = 0;
    memset(A, 0, sizeof(A));
    memset(dp, -1, sizeof(dp)); //初始化为-1 判断是否存在该状态 某些状态0也算 所以不能赋值为0
    while(x) A[cnt++] = x % 10, x /= 10;
    reverse(A, A + cnt);//反转一下 还正着从左往右数 理解前导0
    return dfs(0, true, true);//第一位有限制 true 前缀都看为0 为true
}

例题

不要62

传送门
Des
数位不能存在62连起来,也不能含有4。
Solution
注意:这里它是个车牌号,前导0是合法的,所以我们不用判断前导0,多维护一个信息,上一位是几,如果last = 6, pos = 2,就跳过不用枚举,pos = 4的情况也跳过就行。
Code

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int cnt, A[10];
int f[12][20][2];//pos last limit
int dfs(int pos, int last, int limit)
{
    if(pos == cnt) return 1;
    if(f[pos][last][limit] != -1) return f[pos][last][limit];
    int ans = 0;
    for(int v = 0; v <= (limit ? A[pos] : 9); ++v)
    {
        if((last == 6 && v == 2) || v == 4) continue;
        ans += dfs(pos + 1, v, limit && A[pos] == v);
    }
    return f[pos][last][limit] = ans;
}
int fx(int x)
{
    cnt = 0;
    memset(A, 0, sizeof A);
    memset(f, -1, sizeof f);
    while(x) A[cnt++] = x % 10,  x /= 10;
    reverse(A, A + cnt);
    return dfs(0, 11, true);//11表示上一位不存在
}
signed main()
{
    ios::sync_with_stdio(false), cin.tie(0);
    int L, R;
    while(cin >> L >> R, L || R)
    {
        cout << fx(R) - fx(L - 1) << "\n";
    }
    return 0;
}

数位小孩

传送门
Des
限制:
相邻数位和是否是素数,
至少含有一个1,
没有前导0,
Soluiton
素数和好说,类似于上一题,记录一个last(上一位),判断last + pos是否为素数就行,
至少含有一个1,这个我们不能因为当前pos不是1,而直接return,因为后面的位可能可以枚举1,所有用flag变量,记录当前的数位是否有1,直到最后枚举完所有数位,返回flag(这也是为什么,return 那里不一定是1,也有可能是限制条件)
Code

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
#define int long long//记得LL
using namespace std;
int cnt, A[20];
int f[20][20][2][2][2];
int vis[40];
int dfs(int pos, int last, bool limit, bool lead, bool flag)
{
    if(pos == cnt) return flag;
    if(f[pos][last][limit][lead][flag] != -1) return f[pos][last][limit][lead][flag];
    int ans = 0;
    for(int v = 0; v <= (limit ? A[pos] : 9); ++v)
    {
        if(lead)
            ans += dfs(pos + 1, v, limit && v == A[pos], lead && v == 0, flag || v == 1);
        else if(vis[v + last])
            ans += dfs(pos + 1, v, limit && v == A[pos], lead && v == 0, flag || v == 1);
    }
    return f[pos][last][limit][lead][flag] = ans;
}
int fx(int x)
{
    cnt = 0;
    memset(A, 0, sizeof A);
    memset(f, -1, sizeof f);
    while(x) A[cnt++] = x % 10, x /= 10;
    reverse(A, A + cnt);
    return dfs(0, 11, true, true, false);
}
signed main()
{
    vis[2] =  1;
    vis[3] =  1;
    vis[5] =  1;
    vis[7] =  1;
    vis[11] =  1;
    vis[13] =  1;
    vis[17] =  1;
    int L, R;
    cin >> L >> R;
    cout << fx(R) - fx(L - 1) << "\n";
    return 0;
}

数字计数

传送门
Des
出现0 - 9 的每个数位的次数
Solution
一下子统计出来,不太好搞,一次找一个数位即可。
Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#define int long long
using namespace std;
int cnt, A[20];
int dight;
int f[20][20][2][2];//pos cntd lead limit
int dfs(int pos, int cntd, bool lead, bool limit)
{
    if(pos == cnt) return cntd;
    if(f[pos][cntd][lead][limit] != -1) return f[pos][cntd][lead][limit];
    int ans = 0;
    for(int v = 0; v <= (limit ? A[pos] : 9); ++v)
    {
        //不能合起来,计算0的时候 v == dight 0会多算
        if(lead && v == 0)
            ans += dfs(pos + 1, cntd, true, limit && v == A[pos]);
        else
            ans += dfs(pos + 1, cntd + (v == dight), false, limit && v == A[pos]);
    }
    return f[pos][cntd][lead][limit] = ans;
}
int fx(int x)
{
    cnt = 0;
    memset(A, 0, sizeof A);
    memset(f, -1, sizeof f);
    while(x) A[cnt++] = x % 10, x /= 10;
    reverse(A, A + cnt);
    return dfs(0, 0, true, true);
}
signed main()
{
    int L, R;
    cin >> L >> R;
    for(int i = 0; i <= 9; ++i)
    {
        dight = i;
        cout << fx(R) - fx(L - 1) << " ";
    }
    return 0;
}

完结撒花。
参考一下两边博客,写了一些自己理解的其他东西。
[博客1](https://zhuanlan.zhihu.com/p/348851463)
[博客2](https://zhuanlan.zhihu.com/p/465885658)

posted @ 2022-08-29 15:37  std&ice  阅读(311)  评论(0编辑  收藏  举报