数位DP入门专题礼包已经出现~

题目入口:AFei的炼金术修行之数位DP

题目难度排序:D<F<A<B<E<C<H<G

哇,这套题我做了三次,终于给AK了,陆陆续续差不多做了一个月吧,碰到类似的题已经能够很快地想到思路了,就很苏福~~

 

何谓数位DP

数位DP,顾名思义,dp的对象是数字,并且这个数字的特点是在于数位上。啥意思呢,比如想找出【0,1e18】内不包含62的数字,什么叫"包含62"呢,就比如12362,9862976这样的数字,但632这样的就不行~~显然这些数字的特点在数位上,会有连续的两位,第一位是6,第二位是2。“不包含62”就是说不要这样的数字嘛~~~给定一个数字,让判断是不是这种数很简单,暴力即可过。但就像上面的要找【0,1e18】有多少个数不包含62呢?甚至给定任意区间【L,R】,最多可以到2000位,这怎么处理呢?

这就用到数位DP了~~

先看一下数位DP通常怎么写~

int solve(int pos, int pre, bool limit)
{
    if(pos == -1)  return 1;

    int& d = dp[pos][pre];
    if(d && !limit)    return d;
    int ret = 0;
    int n = limit ? dig[pos] : 9;

    for(int i = 0; i <= n; ++ i)
    {
        if(pre == 6 && i == 2)
            continue;
        ret += solve(pos-1, i, limit && i == dig[pos]);
    }
    if(!limit)
        d = ret;
    return ret;
}

分析一下上面的代码,一般来说,数位DP一次只能求[0,x]内的满足条件的数,而要得到[L,R]的就用[0,R]-[0,L-1]即可~数位DP一般是按数位从高到低进行递归,然后加上记忆化。用dig[]来保存x的各个数位,pos是位置,表示当前已经到了第i位。pre是上一个位置的数字,这个变量对于不同的题是不一样的,总之是用来描述数位特点的。limit为true的时候表示遍历到当前位,前面的数字是不是和x的前几位完全一样——这影响到当前位可以是哪些数字:如果前面都一样,由于我们求的范围是[0,n],那么这一位枚举的数字是不能超过x的这一位,即dig[pos]的,如果前面已经有比x小的数了,那么这一位完全可以说0,1,2...9中的任意数字~~~也就是这一句: int n = limit ? dig[pos] : 9;下面的循环枚举这一位数字。新的递归里,limit为true的条件是之前limit==true并且当前位i==dig[pos]。dp数组只用来记录limit==false的情况——这很容易理解,毕竟当limit为true的时候,x会影响到dp结果,而limit为true的数字其实特别少,对复杂度的影响可以忽略不计,所以字计limit==false的情况~~

 

专题题解

上面说的可能有些抽象,那就看看专题的代码吧,虽然都是入门题,但也有难度区别吧,我就按照自己做的时候的感觉分三个难度容易、一般、困难吧。

 

A题 HDU 4734

难度一般,不推荐第一次接触数位DP就做它,因为它有两个限制条件,一个是区间限制[0,B],还有一个是f(A)的限制。对于B的限制,无须再提了,不懂的可以自己再理解一下下~~~对于A的限制,这里使用减法,令所有数位得到的f值不超过sum,所以遍历到pos位,pos位枚举到i,那么后面的f值就不能超过sum-(i<<pos)了,这样就完成了递推。

#include<bits/stdc++.h>
#define ll long long
#define rgt register int

using namespace std;
int bit[12];
int dp[12][5000];
int getBit(int x) // 将x按位存入bit里,并返回长度
{
    int cnt = 0;
    while(x)
    {
        bit[cnt ++] = x%10;
        x /= 10;
    }
    return cnt;
}
int getF(const int& x)
{
    int p = 1;
    int cnt = getBit(x);
    int sum = 0;
    for(int i = 0; i < cnt; ++ i)
    {
        sum += p*bit[i];
        p <<= 1;
    }
    return sum;
}

//当前遍历到第pos位,剩余位的F值不能超过sum,limit==true表示前面的位和bit[]的一样
int dfs(int pos, int sum, bool limit)
{
    if(pos < 0 || sum < 0)  return 0;
    if(dp[pos][sum] && !limit)  return dp[pos][sum];
    if(pos == 0)    return 1 + min(sum, limit ? bit[0] : 9);

    int n = (limit ? bit[pos] : 9) + 1;

    int ret = 0;
    //枚举每一位
    for(int i = 0; i < n; ++ i)
        ret += dfs(pos-1, sum-(i<<pos), limit&&(i==bit[pos]));

    if(!limit)  dp[pos][sum] = ret;
    return ret;
}

int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    int T, A, B;
    scanf("%d", &T);
    for(rgt _case = 1; _case <= T; ++ _case)
    {
        scanf("%d%d", &A, &B);
        int sum = getF(A);
        int cnt = getBit(B);
        int ans = dfs(cnt-1, sum, true);
        printf("Case #%d: %d\n", _case, ans);
    }

    return 0;
}

B题 URAL 1057

难度一般,这一题实际上比较难理解的是k个不同的b的幂如何转化为数位。那么我们先简化一下,如果令b=2,一个数如何转化为若干个不同的2的幂,很容易想到转化为2进制即可,比如3转化为11(2),那么很容易看到3==2^0+2^1。那么对于任意b呢?同样的道理,转化为b进制即可。但要注意的是不能出现多个b的幂,也就是说要找的数字转化为b进制后数位上只能是0或1,即要么没有2^i,要么只能有1个2^i,然后k就好控制了,k个1嘛~~~

// [x, y]之间有多少个数能是k个不同的b的幂的和
#include<iostream>
#include<cstdio>
#define ll
using namespace std;

int mi[65] = {1}; //mi[i]表示pow(b, i)
int d[35];
int getD(int x, int b)//将x转化为b进制数
{
    if(!x)  return d[0] = 0, 1;
    int cnt = 0;
    while(x)
    {
        d[cnt ++] = x % b;
        x /= b;
    }
    return cnt;
}
int dp[35][22]; // dp[i][j]表示当前在第i位,后面还可以由j个1
int solve(int pos, int k, bool limit)
{
    if(k == 0)      return 1;
    if(pos == -1)   return 0;
    int& tmp = dp[pos][k];
    if(tmp && !limit)           return tmp;
    int ret = 0;
    if(limit && !d[pos])
        ret = solve(pos-1, k, limit);
    else ret = solve(pos-1, k-1, limit && d[pos] == 1) + solve(pos-1, k, false);
    if(!limit)
        tmp = ret;
    return ret;
}
int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    int x, y, k, b;
    scanf("%d%d%d%d", &x, &y, &k, &b);
    int len = getD(x-1, b);
    x = solve(len-1, k, true);
    len = getD(y, b);
    y = solve(len-1, k, true);
    printf("%d\n", y-x);
    return 0;
}

C题 CodeForces 55D

 难度困难,这一题我辗转了好久。一开始对如何确定一个数对各数位取模都为0,暴力是不可能暴力的,1e18的规模啊~~最初看题是毫无想法,直到做完E题才有点思路,然后de了好久bug~~以下是我的两个思路,第一个被证明是错误的。

根据E题,我从高位开始对mod取模,到下一位,给余数*10+当前位,再次进行取模,到最后如果余数为0,那么这个数就是mod的倍数(这个很容易理解的吧,毕竟你手算除法就是这样的啊~~)

那么第一种思路是:对于每一位,不仅将余数*10+当前位,还要将mod=lcm(mod, 当前位),lcm是求最小公倍数的函数,这样到最后,mod就是各位的最小公倍数了,然而,我发现一个问题,当x%3%12==0,x%12不一定等于0,比如x=341, 3014,这样的例子比比皆是。想了一下,这其实也很容易理解的的吧,x%12==0表示x是12的倍数,x%3%12==0表示x是3的倍数,后者怎么可能代表前者呢,我脑子是卡了粑粑了吧~~

第二种思路是:当x%12==0,那么x%3%4==0一定成立,并且(x%12)%3%4 == x%3%4是一定成立的~~~先考虑所有位的最小公倍数最大是多少——lcm(1,2,3,4,5,6,7,8,9)=2520,由第一种思路我们可以知道,位位取模的时候模数不能改变,那么我就固定模数为2520,所有位都对2520取模,取模的同时,我们还记录各位的lcm是多少,到最后再用余数对所有位的lcm取模即可~

注意一点,第三维不能直接存各数位的lcm,20*2521*2521,这数字有点大了啊,实际上各位的lcm是离散的,需要离散化一下~~

#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long

using namespace std;
int dig[20];

ll dp[20][2521][50];
const int mod = 2520;
int gcd(const int& a, const int& b)         {   return b == 0 ? a : gcd(b, a%b);}
inline int lcm(const int& a, const int& b)  {   return a * b / gcd(a, b);       }

//int a[50]; // 存最小公倍数
int b[7561];// 存这个数在a数组中对应的索引

ll solve(int pos, int re, int dalao, bool limit)
{
    if(pos == -1)
    {
        return !(re%dalao);
    }

    ll& d = dp[pos][re][b[dalao]];
    if(d != -1 && !limit)   return d;

    ll ret = 0;
    int n = limit ? dig[pos] : 9;

    for(int i = 0; i <= n; ++ i)
    {
        ret += solve(pos-1, (re*10+i)%mod, i ? lcm(dalao, i) : dalao, limit && i==dig[pos]);
    }

    if(!limit)  d = ret;

    return ret;
}

ll getDig(ll x)
{
    int cnt = 0;
    if(!x) ++ cnt, dig[0] = 0;
    else while(x)
    {
        dig[cnt ++] = x % 10;
        x /= 10;
    }

    return solve(cnt-1, 0, 1, 1);
}

void init()
{
    memset(dp, -1, sizeof dp);
    memset(b, -1, sizeof b);
    int cnt = 0;

    for(int i = 1; i < 513; ++ i)
    {
        int res = 1;
        for(int j = 0; j < 9; ++ j)
            if(i & (1<<j))
                res = lcm(res, j+1);
        if(b[res] == -1)
        {
            b[res] = cnt ++;
//            a[cnt ++] = res;
        }
    }
}

int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    int T;
    ll l, r;
    cin >> T;

    init();
    while(T --)
    {
        cin >> l >> r;
        cout << getDig(r)-getDig(l-1) << endl;
    }

    return 0;
}

D题 HDU 2089

难度简单,真水题没错了,推荐第一个做。求一个范围内不包含4和62的数字有多少个,1e7的规模,直接暴力稍微剪一下枝貌似都能过(我猜的),但不推荐暴力做,毕竟是数位dp专题~~这题没啥好说的,直接看代码吧~

#include<bits/stdc++.h>
using namespace std;

int bit[10];
int getBit(int x)
{
    if(!x)  return bit[0] = 0, 1;
    int cnt = 0;
    while(x)
    {
        bit[cnt ++] = x%10;
        x /= 10;
    }
    return cnt;
}

int dp[12][10];//dp[i][j]表示第i位的前一位是j的情况有多少
int solve(int pos, int pre, bool limit)
{
    if(pos == -1)  return 1;

    int& d = dp[pos][pre];
    if(d && !limit)    return d;
    int ret = 0;
    int n = limit ? bit[pos] : 9;

    for(int i = 0; i <= n; ++ i)
    {
        if(i == 4 || pre == 6 && i == 2)
            continue;
        ret += solve(pos-1, i, limit && i == bit[pos]);
    }
    if(!limit)
        d = ret;
    return ret;
}
int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    int n, m;
    while(scanf("%d%d", &n, &m), n || m)
    {
        int len = getBit(n-1);
        n = solve(len-1, 0, true);
        len = getBit(m);
        m = solve(len-1, 0, true);
        printf("%d\n", m-n);
    }
    return 0;
}

E题 HDU - 3652

难度一般。B数,大家心里都有的啦~~如何包含13就不说了,那么13的倍数怎么弄的?想一下,手算竖式除法是怎么算的?按位来的!没错,从最高位开始,按位步步取模,每到新的一位,对之前的余数*10+新的一位,作为新的被除数,再次取模,取到最后就是整个数字对mod的余数了,代码如下~~

#include<iostream>
#include<cstdio>
using namespace std;

int d[12];
int getDigit(int x)
{
    if(!x)  return d[0] = 0, 1;
    int cnt = 0;
    while(x)
    {
        d[cnt ++] = x%10;
        x /= 10;
    }
    return cnt;
}

int dp[12][14][10][2];
// dp[i][j][k]当前在第i位,并且前面数字对13取模余数是j,并且前面一位是k,有多少满足条件的数。最后一位表示13有没有出现过
int solve(int pos, int sy, int pre, bool hav13, bool limit)
{
//    printf("(%d, %d, %d, %d, %d)\n", pos, sy, pre, hav13, limit);
    if(pos == -1)   return hav13 && !sy;
    int& p = dp[pos][sy][pre][hav13];
    if(p && !limit)   return p;

    int n = limit ? d[pos] : 9;
    int ret = 0;
    for(int i = 0; i <= n; ++ i)
        ret += solve(pos-1, (sy*10+i)%13, i, hav13 || (pre==1 && i==3), limit && i==d[pos]);
    if(!limit)
        p = ret;
    return ret;
}
int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    int n;
    while(~scanf("%d", &n))
    {
        int cnt = getDigit(n);
        printf("%d\n", solve(cnt-1, 0, 0, false, true));
    }
    return 0;
}

F题 POJ 3252

难度简单,这题没啥好说的,只需要按位递推的时候记录一下0和1的数量差就行了,需要注意的是前导0,第一位只能是1~~~

#include<iostream>
#include<cstdio>
using namespace std;

int d[33];
int getDigit(int x)
{
    if(!x)  return d[0] = 0, 1;
    int cnt = 0;
    while(x)
    {
        d[cnt ++] = x&1;
        x >>= 1;
    }

    return cnt;
}

int dp[33][65][2];
int solve(int pos, int c, bool hav1, bool limit)//c表示(0的数量)-(1的数量),hav1表示pos前面是否有1
{
    if(pos == -1)   return c >= 0;

    int& p = dp[pos][c+32][hav1];
    if(p && !limit) return p;

    int ret = 0, n = limit ? d[pos] : 1;
    for(int i = 0; i <= n; ++ i)
    {
        int t = c;
        if(hav1)
        {
            if(i)   -- t;
            else    ++ t;
        }
        else
            t -= i;
        ret += solve(pos-1, t, hav1 || i, limit && i==d[pos]);
    }

    if(!limit)  p = ret;
    return ret;
}
int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    int L, R;
    scanf("%d%d", &L, &R);

    int cnt = getDigit(L-1);
    L = solve(cnt-1, 0, false, true);

    cnt = getDigit(R);
    R = solve(cnt-1, 0, false, true);

    printf("%d\n", R-L);

//    printf("%d %d\n", L, R);
    return 0;
}

G题 HDU - 3709

难度困难。这题我也做了好久,主要是因为实在没思路啊~~~对于一个数,是不是平衡数除了要递推数位,还要枚举轴的位置——这可就难为我了~~如果轴位置是固定的多好啊,是固定的多好啊,固定的多好啊,好啊~~~既然轴不固定,那么我们手动给它固定住不就好了吗?在solve()函数里,我不考虑轴的位置变化,也就是说,让轴的位置是一个常数,那么递推起来就简单了啊!问题是那轴怎么办呢?既然solve里面不能改变轴的值,那么我们就枚举每一个轴的位置都求一次,然后求和不就行了吗?——一个非0平衡数的轴必定是确定的,也就是说不可能会出现重复,唯一会重复的数字是0,枚举任意轴,0都是平衡数,0重复次数为枚举轴的次数-1,最后减掉即可~~~

#include<cstdio>
#include<cmath>
#include<iostream>
#include<vector>
#include<cstring>
#include<stack>
#define ll long long

using namespace std;
int dig[20];    // 数位
ll dp[20][20][1500];// dp[i][j][k]表示轴位置在i的,当前算到了j位,合数字矩为k的平衡数个数

int getDig(ll x)
{
    int cnt = 0;
    if(!x)
        dig[cnt ++] = 0;
    while(x)
    {
        dig[cnt ++] = x%10;
        x /= 10;
    }
    return cnt;
}

bool isBanlancedNum(const ll& x)
{
    int len = getDig(x);
    bool flag = false;

    int sum = 0;
    for(int i = 0; i < len; ++ i)
    {
        sum = 0;
        for(int j = 0; j < len; ++ j)
            sum += (i-j)*dig[j];
        if(!sum)
        {
            flag = true;
            break;
        }
    }
    return flag;
}

ll solve(int pivot, int pos, int sum, bool limit)//轴的位置pivot,当前算到了pos位,合数字矩为sum,limit
{
    if(sum < 0)      return 0;
    if(pos == -1)    return sum ? 0 : 1;

    ll &d = dp[pivot][pos][sum];
    if(d!=-1 && !limit)
        return d;

    ll ret = 0;
    int n = limit ? dig[pos] : 9;

    for(int i = 0; i <= n; ++ i)
        ret += solve(pivot, pos-1, sum+(pos-pivot)*i, limit && i==dig[pos]);

    if(!limit)
        d = ret;
    return ret;
}

int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    memset(dp, -1, sizeof dp);

    int T;
    ll L, R;
    cin >> T;

    while(T --)
    {
        cin >> L >> R;

        int lenL = getDig(L);
        ll ansL = 0, ansR = 0;

        for(int i = 0; i < lenL; ++ i)//枚举轴
            ansL += solve(i, lenL-1, 0, true);
        ansL -= lenL-1;// 一般平衡数只有1个轴,但0除外,0长度为len时有len个轴,所以会重复算了len-1次

        int lenR = getDig(R);
        for(int i = 0; i < lenR; ++ i)
            ansR += solve(i, lenR-1, 0, true);
        ansR -= lenR-1;

        cout << ansR - ansL + isBanlancedNum(L) << endl;
    }

//    for(int i = 0; i < 1000; ++ i)
//        if(isBanlancedNum(i))
//            cout << i << endl;
    return 0;
}

 

H题 CodeForces 628D

难度困难。这一题实际上并不是特别难,之所以设为困难是因为我这一题卡了好久。首先,我没看到,给定[l,R],L和R长度一样。当时还撒fufu地考虑前导0 。。。其次,最重要的是:dp数组要初始化为-1,这一题我是在G题之前做的,除了G、H这两题,前面我所有代码里dp都是默认初始化为0的,但是问题是计数用的dp,dp[i]==0的i是存在的,甚至为dp[i]==0 的 i 还很多,这记忆化的意义就大大削减,就可能导致TLE。前面所有题都没有卡时间,然而这一题卡了,我T到心态爆炸。所以dp数组还是得初始化为-1啊~~~具体算法不是很难,这套题做到这儿的时候,这一题已经不是问题了~

// k-magic,m的倍数
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;
using ll = long long;
const int mod = 1e9 + 7;
int dig[2010];
int m, k;
inline void add(ll& a, ll b){a = a+b >= mod ? a+b-mod : a+b;}
ll dp[2010][2010]; // dp[i][j]表示当前在第i位,前面对m余数为j,有多少个数满足条件

int getDig(char* p) // 将以字符串形式存储的数字p,存入dig里,并返回长度
{
    int l = strlen(p);
    for(int i = 0; i < l; ++ i)
    {
        dig[i] = p[i] - '0';
    }
    return l;
}

ll solve(int pos, int re, int l, bool limit)
{// 位置,余数
    if(pos == l)   return re == 0;

    ll& d = dp[pos][re];
    if(d != -1 && !limit) return  d;

    int n = limit ? dig[pos] : 9;
    ll ret = 0;
    for(int i = 0; i <= n; ++ i)
    {
        if((pos&1) && i!=k)   continue;
        if(!(pos&1) && i==k)  continue;
        add(ret, solve(pos+1, (re*10+i)%m, l, limit && (i==dig[pos])));
    }
    if(!limit)  d = ret;
    return ret;
}
bool isK_magic(char* p)
{
    int l = strlen(p);
    int re = 0;
    for(int i = 0; i < l; ++ i)
    {
        if(p[i]-'0'!=k && (i&1))   return false;
        if(p[i]-'0'==k && !(i&1))  return false;
        re = (re*10 + p[i]-'0') % m;
    }
    return re == 0;
}
int main()
{
    #ifdef AFei
    freopen("in.c", "r", stdin);
    #endif // AFei

    memset(dp, -1, sizeof dp);
    char a[2010], b[2010];

    scanf("%d%d", &m, &k);
    scanf("%s%s", a, b);

    int cnt = getDig(a);
    ll l = solve(0, 0, cnt, true) - isK_magic(a);
    cnt = getDig(b);
    ll r = solve(0, 0, cnt, true);

    printf("%lld\n", (r-l+mod)%mod);
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值