ACM_数位DP

引言

数位DP: 与记数有关的一种动态规划, 一般题目是 : 求0 ~ n 之间有多少个符合….条件的数, 或者l ~ r 之间有多少个符合条件的数, 第二种一般来说可以转化到第一种[0, r] - [0, l) = [l, r] 这类问题通常会和取模, 记数, 数字和, 等运算联系在一起; 
本文将以: 
1.数位DP的所用定理(其实就一个同模) 
2.数位DP的状态, 状态转移, 初始化 
3.数位DP的统计过程(简单解释一下程序的运算顺序, 每一步是做什么的) 
4.例题 
的形式介绍数位DP

为了介绍方便会用一个例题来解释数位DP(这个例子没有用到下面所讲的同模的定理) 
题目链接 : http://acm.hdu.edu.cn/showproblem.php?pid=2089 
这里写图片描述
题目大意就是问你从l 到 r 有多少个不含62和4的数

所用定理:

 因为数位DP很多和取模有关, 并且和所在数字的位置有关, 所以有个很重要的取模定理(我发现有些人会用这个定理, 但是从来没有去想过这个定理为什么可以)
 定理: 如果(a * 10 + b) % x = y 那么(a % x * 10 + b) % x = y;
 就是说一个数a对于一个数b取模, 它可以一位一位的算;
 举个例子, 求7456 % 9 = ?
 7 % 9 = 7
 (7 * 10 + 4) % 9 = 2
 (2 * 10 + 5) % 9 = 7
 (7 * 10 + 6) % 9 = 4;
 所以7456 % 9 = 4;
 证明:
 令 a = k1 * x + b1; b = k2 * x + b2;
 那么左边的(a * 10 + b) % x = y 就可以写成 (k1 * x * 10 + b1 * 10 + k2 * x + b2) % x 
 右边的 (a % x * 10 + b) % x = y 就可以写成((k1 * x + b1) % x * 10 + k2 * x + b2) % x
 即 ((k1 * x * 10 + b1 * 10) % x + (k2 * x + b2) % x) % x
 即 (k1 * x * 10 + b1 * 10 + k2 * x + b2) % x  等于右边
 用了一个同模定理 a * b % c = a % c * b % c 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

数位DP的状态, 状态转移, 初始化 :

大部分数位DP的状态都会有一个状态 : 这个数是几位数, 比如dp[3]一般表示的是后面3位随便填的意思, 指的就是0 ~ 1000的范围的数(当然可能还有其他的限制, dp的维度可能不止一维) 对于上面介绍的例题, 我的状态是这样的 dp[i][j] 表示 i 位数, 并且最高位是j 的不含62和4的 数字有多少个(现在想想 好像一维也可以?) 
状态转移 : dp[i][j] = dp[i-1][0~9] 表示 i 位数最高位是 j = i-1位数, 最高位是0 ~ 9 的情况和, 如果j = 6 低位不能为2 如果j = 4, dp[i][j] = 0 
初始化 : 数位DP的初始化dp[0]一般不好理解 我也只是模糊的感觉, 如果觉得难理解的话, 可以多写一点 初始化dp[1]也可以 其他位置的初始化成0, 如果开全局的话也可以不用memset 因为全局一出来就初始化成0了 
如下图就是对于上面例题的状态 状态转移 和初始化 
这里写图片描述

数位DP的统计过程 :

数位DP的统计是按照从高位到低位依次数下来 举个例子更容易明白, 比如我们统计 324 那么我们先把324 + 1 = 325(为什么+1 看后面) 
1算 [0, 100) 
2算[100, 200) 
3算[200, 300) 
4算[300, 310) 
5算[310, 320) 
6算[320, 321) 
7算[321, 322) 
8算[321, 323) 
9算[323, 324) 
10算[324, 325) 
注意括号开闭, 你就知道为什么要 + 1了 
比如我们对于上面的例题 算21(举个小一点的例子) 
[0, 10) = 9 
[10, 20) = 9 
[20, 21) = 1 
[21, 22) = 1; 
所以有 20个符合条件的数 
再对于 626(因为有62, 同理有4的情况) 
1算[0, 100) 
2算[100, 200) 
… 
6算[500, 600) 
7算[600, 610) 
8算[610, 620) 
9算[620, 621) 但是因为前面已经有 62了, 所以接下来的都不符合情况, 不加入结果 
到此就完成了[0, x] 中符合条件的数的统计 
这里写图片描述
对于求[l, r] 中符合条件的数 只需要求出[0, r] 和 [0, l-1] 然后相减就可以了 
代码如下:

//
//  Created by fkjs on 2015-12-03
//  Copyright (c) 2015 fkjs. All rights reserved.
//
//#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <set>
#include <vector>

using namespace std;

typedef long long int ll;
const int INF = 0x3f3f3f3f;

int dp[11][11];
int n, m;

int getnum(int x) {
    int digit[10];
    int k = 0;
    while(x) {
        digit[++k] = x % 10;
        x /= 10;
    }
    digit[k+1] = 0;
    int ret = 0;
    for(int i = k; i; --i) {
        for(int j = 0; j < digit[i]; ++j)
            if(j - 4 && !(digit[i+1] == 6 && j == 2))
                ret += dp[i][j];
        if(digit[i] == 4 || (digit[i+1] == 6 && digit[i] == 2)) break;
    }
    return ret;
}

int main(void) {
    dp[0][0] = 1;
    for(int i = 1; i <= 7; ++i) {
        for(int j = 0; j <= 9; ++j) {
            if(j == 4) continue;
            for(int k = 0; k <= 9; ++k) {
                if(j == 6 && k == 2) continue;
                if(k == 4) continue;
                dp[i][j] += dp[i-1][k];
            }
        }
    }
    while(scanf("%d%d", &n, &m) == 2 && (n || m)) {
        printf("%d\n", getnum(m+1) - getnum(n));
    }
    return EXIT_SUCCESS;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

例题:

题目链接 : http://acm.hdu.edu.cn/showproblem.php?pid=3652 
这里写图片描述
(这个题就要用到上面所说的定理了) 
题目大意: 问你[0, n]中有多少个数满足 包含13 且能被13整除 
具体思路 : 明显的数位DP, 
首先有个表示几位数的 占一维 
包含13 只需要一维就可以解决, 这一维只需要3个大小, 分别代表, 没13且第一位不是3, 没13 但第一位是3, 有13 
能被13整除, 也只需要一维, 只需要13个大小, 表示的这一类数中模13 等于多少就可以了 
所以一共三维 这里写图片描述

dp中的第二维中0代表 : 没13且第一位不是3 
1代表 : 没13 但第一位是3 
2代表 : 有13

状态转移 : dp[i][0][k] += dp[i-1][0][(k + u) * 10 % 13]( 0 <= u <= 9 && u != 3) 
dp[i][0][k] += dp[i-1][1][(k+u) * 10 % 13] (0 <= u <= 9 && u != 1 && u!= 3) 
表示的是第 i 位 没有13 且第一位不是3 可以由第 i-1 位 没有13且第一位不是3 转移而来(最高位只要不是3) 也可以由 第一位是3转移而来(只要最高时不是3 和1 ) 
dp[i][1][k] += dp[i-1][0][(k+3) * 10 % 13] 
dp[i][1][k] += dp[i-1][1][(k+3) * 10 % 13] 
最高位是3就行 
dp[i][2][k] += dp[i-1][2][(k + u) * 10 % 13]( 0 <= u <= 9) (反正已经有13了, 高位随便是什么都行) 
dp[i][2][k] += dp[i-1][1][(k+1) * 10 % 13] (最高位填1, 后面的加上最高位是3 就行了)

具体代码如下: (我用的是递归的方式, 其实递推也可以)

#include <cstdio>
#include <cstring>

const int N = 15;

long long dp[N][3][N];
int digit[N];

long long Pow(int a, int b) {
    long long ret = 1;
    for(int i = 0; i < b; ++i) ret *= a;
    return ret;
}

long long dfs(int pos, int type, int mod, int lim) {
    long long ret = dp[pos][type][mod];
    if(!lim && ret) return ret;
    if(pos == 0) return type == 0 && mod == 0;
    ret = 0;
    int g = lim? digit[pos] - 1 : 9;
    if(type == 0) {
        for(int k = 0; k <= g; ++k) {
            if(k != 3) ret += dfs(pos-1, 0, (mod + k) * 10 % 13, 0);
            if(k - 3 && k - 1) ret += dfs(pos-1, 1, (mod + k) * 10 % 13, 0);
        }
    } else if(type == 1) {
        if(!lim || (lim && digit[pos] >= 3)) {
            ret += dfs(pos-1, 0, (mod + 3) * 10 % 13, 0);
            ret += dfs(pos-1, 1, (mod + 3) * 10 % 13, 0);
        }
    } else if(type == 2) {
        for(int k = 0; k <= g; ++k) {
            ret += dfs(pos-1, 2, (mod + k) * 10 % 13, 0);
            if(k == 1) ret += dfs(pos-1, 1, (mod + k) * 10 % 13, 0);
        }
    }
    if(!lim) dp[pos][type][mod] = ret;
    return ret;
}

long long cal(long long x) {
    int len = 0;
    for(int i = 0; x / Pow(10, i); ++i) digit[++len] = x / Pow(10, i) % 10;
    digit[len+1] = 0;
    long long ret = 0;
    long long sto = 0;
    for(int i = len; i; --i) {
        ret += dfs(i, 2, sto * 10 % 13, 1);
        if(digit[i+1] == 1 && digit[i] != 3) ret += dfs(i, 1, sto * 10 % 13, 1);
        if(digit[i+1] == 1 && digit[i] == 3) {
            if(i != 1) ret += (x - (sto * 10 + digit[i]) * Pow(10, i-1)) / 13 + 1;
            break;
        }
        sto = sto * 10 + digit[i];
    }
    return ret;
}

int main() {
    memset(dp, 0, sizeof dp);
    long long n;
    while(scanf("%I64d", &n) == 1) {
        printf("%I64d\n", cal(n+1));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

数位DP还有很多类型 有些甚至不能用[l, r] = [0, r] - [0, l-1]这种方法, 不过很少 而且数位DP更多的可能会和其他的一些方法放在一起出题, 感觉只要会了平时遇到的数位DP还是能解开的

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值