【数位DP】

来源:互联网 发布:9377皇图进阶数据 编辑:程序博客网 时间:2024/06/08 02:50

【总览】

通过将数拆分成一位位的进行dp(记忆化搜索),状态最基本的有:位置($pos$),最高位限制($limit$),前导零($lead$),前一位($pre$)等等,通常需要的状态视题目而定。

记忆化搜索的数组$dp$由多维构成,每一位都是一种状态的因素。

不要62

数位$dp$入门题,$dp$数组为$dp[pos][pre][limit]$($limit$可以通过更改代码去掉这一维)。

【code】

#include<iostream>#include<cstdio>#include<cstring>#include<string>#include<algorithm>#include<cstdlib>#include<cmath>using namespace std;const int N = 10;int as[N], bs[N], lena, lenb;long long dp[N][N][2];int a, b;inline int read(){    int i = 0, f = 1; char ch = getchar();    for(; (ch < '0' || ch > '9') && ch != '-'; ch = getchar());    if(ch == '-') f = -1, ch = getchar();    for(; ch >= '0' && ch <= '9'; ch = getchar())        i = (i << 3) + (i << 1) + (ch - '0');    return i * f;}inline void wr(long long x){    if(x < 0)putchar('-'), x = -x;    if(x > 9) wr(x / 10);    putchar(x%10 + '0');}inline long long dfs(int pos, int last, bool limit, int *s, int l){    if(pos == l + 1) return 1;    if(dp[pos][last][limit] != -1) return dp[pos][last][limit];    int high = limit ? s[pos] : 9;    long long ret = 0;    for(int i = 0; i <= high; i++){        if(last == 6 && i == 2) continue;        if(i == 4) continue;        ret += dfs(pos + 1, i, limit && (i == high), s, l);    }    dp[pos][last][limit] = ret;    return ret;}int main(){    while(1){        a = read(), b = read();        if(!a && !b) break;        //----------------------------------        memset(dp, -1, sizeof dp);        memset(as, 0, sizeof as);        lena = 0;        if(a > b) swap(a, b);        int tmp;        tmp = a - 1;        while(tmp){            as[++lena] = tmp % 10;            tmp /= 10;        }        reverse(as + 1, as + lena + 1);        long long r1 = dfs(1, 0, 1, as, lena);        //----------------------------------                memset(dp, -1, sizeof dp);        memset(bs, 0, sizeof bs);        lenb = 0;        tmp = b;        while(tmp){            bs[++lenb] = tmp % 10;            tmp /= 10;        }        reverse(bs + 1, bs + lenb + 1);        long long r2 = dfs(1, 0, 1, bs, lenb);        //-------------------------------------        wr(r2 - r1), putchar('\n');    }}
View Code

 windy数

同样是入门级别的题,$dp$中加入前导零这一维(亦可更改代码去掉),当当前位可作为前导零时,有两种选择:

  • 继续前导零
  • 填上大于$0$的数,以后的数不能再使用“前导”$0$(普通$0$可以用)

若不能在作为前导$0$,则必须满足差值大于等于$2$的要求。

【code】

#include<iostream>#include<cstdio>#include<cstdlib>#include<cstring>#include<string>#include<algorithm>#include<vector>using namespace std;const int N = 15;int dp[N][N][2][2]; //位置,前一个数,最高位限制int as[N], bs[N], lena, lenb, a, b;inline int dfs(int pos, int last, bool limit, bool lead, int *s, int l){    if(pos == l + 1) return 1;    if(dp[pos][last][limit][lead] != -1) return dp[pos][last][limit][lead];    int high = limit ? s[pos] : 9, ret = 0;    for(int i = 0; i <= high; i++){        if(!lead){            if(i != 0)ret += dfs(pos + 1, i, limit && i == high, 1, s, l);            else ret += dfs(pos + 1, i, limit && i == high, 0, s, l);        }        else if(abs(i - last) >= 2)            ret += dfs(pos + 1, i, limit && i == high, 1, s, l);    }    dp[pos][last][limit][lead] = ret;     return ret;}    int main(){    scanf("%d%d", &a, &b);    if(a > b) swap(a, b);    //------------------------    memset(dp, -1, sizeof dp);    int tmp = a - 1;    while(tmp) as[++lena] = tmp % 10, tmp /= 10;    reverse(as + 1, as + lena + 1);    int r1 = dfs(1, 0, 1, 0, as, lena);        memset(dp, -1, sizeof dp);    tmp = b;    while(tmp) bs[++lenb] = tmp % 10, tmp /= 10;    reverse(bs + 1, bs + lenb + 1);    int r2 = dfs(1, 0, 1, 0, bs, lenb);    cout<<r2 - r1<<endl;    return 0;}
View Code

Beautiful Numbers

 学到了。本题难点在于离散压空间。若正常的进行$dp$,那么数组大小早就爆了。

  现在考虑这样一个问题:对于$1-9$的数而言,最小公倍数为2520.

  数学推到知$a \equiv b (mod p) \Leftrightarrow p | a - b$

  现在设最后的数为$x$,所有数位的最小公倍数为$gcd$:

  $$x \% gcd = 0$$

  $$ 2520 \% gcd = 0$$

  $$\Rightarrow n2520 \% gcd = 0 (n \in N)$$

  $$ \Rightarrow x \equiv n2520 (mod  gcd)$$

  $$ \Rightarrow gcd | (x - n2520)$$

  $$ \Rightarrow (x \% 2520) \% gcd = 0$$

 于是,只要这个数$mod 2520$后能整除$gcd$,那么符合要求。所以结果这一维缩小到了$2520+$。

 再看看当前最小公倍数这一维,因为$1-9$组成的最小公倍数最多只有$48$种,所以可以将$gcd$用$hash[]$进行离散,将这一维降到$50-$。

【code】

#include<iostream>#include<cstdio>#include<cstdlib>#include<cstring>#include<string>#include<algorithm>#include<vector>#include<cmath>using namespace std;const int N = 20;long long dp[20][2560][50];  //位置,模后和,最小公倍数哈希值long long x, y;int num[N], len, hash[2560];inline long long read(){    long long i = 0, f = 1; char ch = getchar();    for(; (ch < '0' || ch > '9') && ch != '-'; ch = getchar());    if(ch == '-') f = -1, ch = getchar();    for(; ch >= '0' && ch <= '9'; ch = getchar())        i = (i << 3) + (i << 1) + (ch - '0');    return i * f;}inline void init(){    int cnt = 0;    for(int i = 1; i <= 2520; i++)        if(2520 % i == 0) hash[i] = ++cnt;}inline int gcd(int a, int b){    return b ? gcd(b, a % b) : a;}inline int LCM(int a, int b){    return a / gcd(a, b)* b;}inline long long dfs(int pos, int value, int lcm, bool limit, int *s, int l){    if(pos == l + 1) return value % lcm == 0 ? 1 : 0;    if(!limit && dp[pos][value][hash[lcm]] != -1) return dp[pos][value][hash[lcm]];    int high = limit ? s[pos] : 9;    long long ret = 0;    for(int i = 0; i <= high; i++)        ret += dfs(pos + 1, (value * 10 + i) % 2520, i ? LCM(lcm, i) : lcm, limit && (i == high), s, l);    if(!limit) dp[pos][value][hash[lcm]] = ret;    return ret;}inline long long get(long long p){    memset(dp, -1, sizeof dp);    if(p < 0) return 0;    len = 0;    while(p){        num[++len] = p % 10;        p /= 10;    }    reverse(num + 1, num + len + 1);    return dfs(1, 0, 1, 1, num, len);}int main(){    x = read(), y = read();        init();    printf("%I64d\n", get(y) - get(x - 1));//    for(int i = 1; i <= lenx; i++) cout<<x[i];cout<<endl;//    for(int j = 1; j <= leny; j++) cout<<y[j];cout<<endl;    return 0;}
View Code

 【HDU - 3652】B-Number

题意大概是求$1-n$中满足出现子串"$13$"且能被$13$整除的数的个数。

可以直接求同时满足两种的:记录$pos$, $pre$, $mod$, $limit$, $sat$,表示位置,前一位,当前得到的除以$13$的余数,最高位限制,和是否满足了出现"$13$"的要求。

如果$pre = 1 并且 i = 3$或者$sat$已经为$true$,那么以后的转移都为$true$,否则以后为$false$知道满足。当位数已满时判断$mod=0并且sat = true$。

【code】

#include<iostream>#include<cstring>#include<string>#include<cstdio>#include<cstdlib>#include<algorithm>#include<vector>#include<cmath>using namespace std;const int N = 15;int dp[N][10][15][2][2];int n, num[N], len;inline int read(){    int i = 0, f = 1; char ch = getchar();    for(; (ch < '0' || ch > '9') && ch != '-'; ch = getchar());    if(ch == '-') f = -1, ch = getchar();    for(; ch >= '0' && ch <= '9'; ch = getchar())        i = (i << 3) + (i << 1) + (ch - '0');    return i * f;}inline void wr(int x){    if(x < 0) putchar('-'), x = -x;    if(x > 9) wr(x / 10);    putchar(x%10+'0');}inline int dfs(int pos, int pre, int mod, bool limit, bool sat){    if(pos == len + 1) return (mod == 0 && sat) ? 1 : 0;    if(dp[pos][pre][mod][limit][sat] != -1) return dp[pos][pre][mod][limit][sat];    int high = limit ? num[pos] : 9, ret = 0;    for(int i = 0; i <= high; i++){        if((pre == 1 && i == 3) || sat) ret += dfs(pos + 1, i, (mod * 10 + i) % 13, limit && (i == high), 1);        else ret += dfs(pos + 1, i, (mod * 10 + i) % 13, limit && (i == high), 0);    }    dp[pos][pre][mod][limit][sat] = ret;    return ret;}inline int calc(int x){    len = 0;    memset(dp, -1, sizeof dp);    while(x) num[++len] = x % 10, x /= 10;    reverse(num + 1, num + len + 1);    return dfs(1, 0, 0, 1, 0);}int main(){    while(scanf("%d", &n) != EOF){        wr(calc(n));        putchar('\n');    }    return 0;}
View Code
原创粉丝点击