谈数位DP

在了解数位dp之前,先来看一个问题:

  例1.求a~b中不包含49的数的个数. 0 < a、b < 2*10^9

注意到n的数据范围非常大,暴力求解是不可能的,考虑dp,如果直接记录下数字,数组会开不起,该怎么办呢?要用到数位dp.

 数位dp一般应用于:

求出在给定区间[A,B]内,符合条件P(i)的数i的个数.

条件P(i)一般与数的大小无关,而与 数的组成 有关.

这样,我们就要考虑一些特殊的记录方法来做这道题.一般来说,要保存给定数的每个位置的数.然后要记录的状态为当前操作数的位数,剩下的就是根据题目的需要来记录.可以发现,数位dp的题做法一般都差不多,只是定义状态的不同罢了.

下面开始针对例题进行分析:

我们要求[a,b]不包含49的数的个数,可以想到利用前缀和来做,具体来说,就是[a,b] = [0,b] - [0,a),(")"是不包括a),我们先求出给定a,b的每个位置的数,保存在数组s中,例如a = 109,那么a[1] = 9,a[2] = 0,a[3] = 1.然后开始dp,我们可以选择记忆化搜索或者是递推,前一种相对于第二种而言简单和较为容易理解一些,所以我们选择记忆化搜索.那么需要记录些什么呢?首先长度是一定要记录的,然后记录当前的数位是否为4,这样就便于在记忆化搜索中得到答案.

然后进行记忆化搜索,记录上一位是否为4和枚举这一位,如果没有限制的话很好办,直接枚举就可以了,但是这样可能会超空间,因此我们每次都必须要判断是否有最大的限制,这里不是很好说,看代码更容易理解:

typedef long long ll;

ll dp[30][2], shu[20];

ll dfs(int st, int state, int limit) {
    if (st  < 1) return 1;
    if (!limit && dp[st][state] != -1) return dp[st][state];
    // 为什么要返回呢?可以画图理解当我们搜到3XXX时
    // 程序运行到1XXX时就已经把3XXX之后的搜索完了,记忆化也是这个用意.
    int up= limit ? shu[st] : 9; // 根据进制写最大值
    ll res = 0;
    for (int i = 0 ; i <= up ; ++ i) {
        if (state && i == 9) continue;
        res += dfs(st - 1, i == 4, limit && i == up);
        //只有之前有限制现在的达到了上限才能构成限制
    }
    if (!limit) dp[st][state] = res;
    return res;
    //如果有限制,那么就不能记忆化,否则记忆的是个错误的数.
}

ll cal(int x) {
    memset(shu, 0, sizeof(shu));
    int k = 0;
    while (x) {
        shu[++k] = x % 10;  //保存a,b的数
        x /= 10;
    }
    return dfs(k, false, true);
}

void solve() {
    ll l, r;
    memset(dp, -1, sizeof(dp));
    scanf("%lld%lld", &l, &r);
    printf("%lld\n", cal(r) - cal(l - 1));
}

再来看一道题:例题2.求a~b中不包含62和4的数的个数. 0 < a、b < 2*10^9
题目HDU2089 不要62

分析:和上一题一样,只需要再判断一下4是否出现和上一位是否为6即可.

ll dp[30][2], shu[30];
ll dfs(int st, int state, int limit) {
    if (st < 1) return 1; // 返回状态
    if (!limit && dp[st][state] != -1) return dp[st][state];
    int up = limit ? shu[st] : 9;
    ll res = 0;
    for (int i = 0 ; i <= up ; ++ i) {
        if (i == 4 || (state && i == 2)) continue;
        res += dfs(st-1, i == 6, limit && i == up);
    }
    if (!limit) dp[st][state] = res;
    return res;
}
ll cal(ll x) {
    int k = 0;
    while(x) {
        shu[++k] = x % 10;
        x /= 10;
    }
    return dfs(k, 0, 1);
}
void solve() {
    ll l, r;
    while(~scanf("%lld%lld", &l, &r)) {
        if (l + r == 0) break;
        if (l > r) swap(l, r);
        printf("%lld\n", cal(r) - cal(l-1));
    }
}

数位dp模板(…根据实际情况来填):

typedef long long ll;
ll dp[len][..][..], shu[20];  // 第一维一般都是位数 

ll dfs(int st, ..., int limit) {
	// st 指的是当前位
    if (st < 1) return ...;
    if (!limit && dp[st][..][..] != -1) return dp[st][..][..];  
    //dp数组的内容应和dfs调用参数的内容相同,除了是否达到上限
    int up = limit ? shu[st] : 9;  // 注意一些题目中的进制
    ll res = 0;
    for (int i = 0 ; i <= up ; ++ i) {
        ...;  // 写对应的判定条件
        res += dfs(st - 1, ..., limit && i == up);
    }
    if (!limit) dp[st][..][..] = res;
    return res;
}

ll cal(ll x) {
    int k = 0;
    while (x) {
        shu[++k] = x % 10;
        x /= 10;
    }
    return dfs(k, ..., 1);
}

void solve() {
	memset(dp, -1, sizeof(dp));
    ll l, r;
    scanf("%lld%lld", &l, &r);   //有些题目其实并不需要用到long long
    printf("%lld\n", solve(r) - solve(l - 1)); //只有满足区间减法才能用
}

总结:

1.如果题目中出现求满足区间[l,r]的符合…性质的数的个数,考虑使用数位dp.

2.思考一下:如果我们只能从前往后一位位枚举当前的数位,要做出这道题,我们需要知道哪些量?利用这些来补充到dfs的调用参数中.

3.套用模板.

你可能感兴趣的:(数位dp)