ACM 数位DP

转载自 http://blog.csdn.net/fkjslee

引言

数位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 

数位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;
}

例题:

题目链接 : 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));
    }
}

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值