ACM 数位DP

来源:互联网 发布:如何用记事本编写java 编辑:程序博客网 时间:2024/06/14 21:07

转载自 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还是能解开的

原创粉丝点击