1. 程式人生 > >數位DP入門專題禮包已經出現~

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