數位DP codeforces55(D)-Beautiful numbers

數位DP:

用來統計區間內符合條件的數的個數,通常區間範圍很大,並且條件通常與數的組成有關。

解題關鍵:對前綴的抽象分類。須符合兩個條件。

  1. 對於一個數anan-1……a2a1a0, 可以將前綴anan-1…ak+1歸類於狀態(sta,k), 使得具有相同狀態的前綴的解一致。
  2. 可以求解,在狀態(sta,k)下,dkdk-1…d1d0中符合條件的數字的個數。

實現方法是記憶化搜索。dp[k][sta]存儲狀態爲sta,剩餘k+1位時的解。

 

實現小技巧:

上界限制:limit  -> 對當前位的取值有上界限制,如213,若前兩位取21,則對個位取值上界產生影響

前導0:     lead -> 如果前面的位數都是0,會對後面的計數產生影響

 

注意點:這類問題的檢錯相對方便許多,因爲可以直接輸入數字查看其計算結果。所以可以直接對範圍兩個邊界進行輸出檢查,如左側直接輸出檢查[-1, 19]。右側則還要考慮會不會溢出。最後再檢查一下中間特殊點(如果有的話)。如果都沒問題,那麼出錯概率就小很多了。

 

 


附上例題:

比較典型的例題就是hdu2089-不要62

題意:
給定整數對n, m(0 < n ≤ m ≤ 1000000),求區間[n,m]中不含4和62的數。

思路:
這題就是典型的數位dp了,顯然對於尾數不爲6的任意前綴,給定剩餘位數,其解是相同的。例如,23xxx和32xxx中符合條件的數是相同的。同理,尾數爲6的前綴的解也是相同的。

代碼:

// 數位DP
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <map>
#include <set>
using namespace std;
const int MAX_POS = 7;
const int INF_ = 0x3f3f3f3f;
int dp[MAX_POS+1][2];           // dp[i][1] 當第i+1位爲6時前i符合條件的個數。
int num[MAX_POS+1];             // 將數字按位放置到數組中
void init()
{
    memset(dp, -1, sizeof(dp));
}

// 當前面位已知,求0到pos位符合條件數的個數
// 三種情況:
//          1. 0位到pos位都可以取任意數         存於dp[pos][0]
//          2. 前一位pre是6, pos位選值有影響    存於dp[pos][1]
//          3. 前面幾位都是取到最高位(limit=true), 因此0位到pos位的取值受影響 -> 影響當前pos位取值, 並將影響遞歸下去;

int dfs(int pos, int pre, bool limit)
{
    if (pos == -1) return 1;
    // 記憶化
    if (!limit && dp[pos][pre == 6] != -1) return dp[pos][pre == 6];
    int up = limit ? num[pos] : 9;
    int tmp = 0;
    for (int i = 0; i <= up; i++)
    {
        if (i == 4) continue;
        if (pre == 6 && i == 2) continue;
        tmp += dfs(pos-1, i, limit && i == up);
    }
    if (!limit) dp[pos][pre == 6] = tmp;
    return tmp;
}

int solve(int n)
{
    int pos = 0;
    while (n > 0)
    {
        num[pos++] = n % 10;
        n /= 10;
    }
    return dfs(pos-1, -1, 1);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, m;
    init();
    while (cin >> n >> m)
    {
        if (n == 0 && m == 0) break;
        cout << solve(m) - solve(n-1) << endl;
    }
}

 

 


稍微難一點的題目codeforces55(D)-Beautiful numbers

 

題意:
求區間[li,ri]中可以整除自身所有非0數字的數的數目。(1 ≤ li ≤ ri ≤ 9*10^18)

 

分析:
還是一樣的思路,對前綴進行抽象分類。即給定怎樣的前綴,他們的解是一樣的。

首先可以對整除自身所有非0數字進行分析,這句話的意思就是說要求整除這些非0數的最小公倍數。而其實1到9可以組成的公倍數的數量只有48個。其中最大的最小公倍數是2520(2^3*3^2*5*7)。

所謂整除就是取mod爲0,而取mod有個常用的性質,就是分配率。即anan-1…a2a1 mod p = (an...ak+1 * 10^k mod p + ak-1...a1 mod p) mod p。於是我們就可以將前綴與後綴分開,從而對前綴進行分類了。具有相同最小公倍數,並且對所有最小公倍數求mod的值相同的前綴可以歸於一類。這時狀態數量=19 * 48 * 48 * 2520。而對所有最小公倍數求模值相同等價於對他們的公倍數求模相同( (A mod p1p2) mod p1 = A mod p1)。而2520就是所有最小公倍數的公倍數,所以我們可以只記錄其前綴對2520取模的值。這時狀態數量就變成19 * 48 * 2520了。

 

代碼:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <map>
#include <set>
using namespace std;

/*
即求可以整除其自身所有位數的最小公倍數的數字數目。
位數的最小公倍數的數量爲 (4*3*2*2), 最大的最小公倍數爲(2^3*3^2*5*7)
所以可以根據前綴最小公倍數分爲48個狀態
若我們已知給定前綴,mod48個狀態所得各個取值的數字個數,則給定前綴,我們可以快速求出答案。
需48*48*2520個狀態
(A mod p1p2) mod p1 = A mod p1
只需記錄(mod 2520)即可知道其mod其他最小公倍數的值, 只需48*2520個狀態
剩餘pos+1個bit, (mod (2520) == sta)的前綴的解
dp[pos][sta_id][2520]
*/

const long long MAX_POS = 19;                   // 最多佔用19個bit
const long long MAX_STA = 48;                   // 最小公倍數數量
const long long MAX_VAL = 2520;                 // 最大的最小公倍數
long long dp[MAX_POS][MAX_STA][MAX_VAL];
long long num[MAX_POS];
long long sta_vals[MAX_STA];
long long pow_10[MAX_POS];

long long myPow(long long base, long long e)
{
    long long ans = 1;
    for (int i = 0; i < e; i++) ans *= base;
    return ans;
}

void init()
{
    memset(dp, -1, sizeof(dp));
    long long cnt = 0;
    for (long long i = 0; i <= 3; i++)
        for (long long j = 0; j <= 2; j++)
            for (long long p = 0; p <= 1; p++)
                for (long long q = 0; q <= 1; q++)
                {
                    sta_vals[cnt++] = myPow(2, i) * myPow(3, j) * myPow(5, p) * myPow(7, q);
                }
    for (long long i = 0; i < MAX_POS; i++)
    {
        pow_10[i] = myPow(10, i);
        // pow_10[i] = pow(ten, i);
        // cout << "10^"<<i<<" "<< pow_10[i] << endl;
        // pow返回值是浮點數,會有精度損失
    }
}

long long getStaId(long long sta)
{
    for (long long i = 0; i < MAX_STA; i++)
        if (sta == sta_vals[i]) return i;
}

long long getLCM(long long a, long long digit)
{
    if (digit == 0) return a;
    long long tmp = a;
    long long prime_num[] = {2, 3, 5, 7};
    for (long long i = 0; i < 4; i++)
    {
        while (tmp % prime_num[i] == 0 && digit % prime_num[i] == 0)
        {
            tmp /= prime_num[i];
            digit /= prime_num[i];
        }
    }
    return a*digit;
}

// 當前位數, 狀態(前綴數字的最小公倍數), mod_val(mod 2520的值)
long long dfs(long long pos, long long sta, long long mod_val, bool limit)
{
    if (pos == -1)
    {
        if (mod_val % sta == 0) return 1;
        else return 0;
    }
    long long sta_id = getStaId(sta);
    if (!limit && dp[pos][sta_id][mod_val] != -1) return dp[pos][sta_id][mod_val];
    long long up = limit ? num[pos] : 9;
    long long tmp = 0;
    for (long long i = 0; i <= up; i++)
    {
        long long new_mod_val = ((i * pow_10[pos]) % MAX_VAL + mod_val) % MAX_VAL;
        long long new_sta = getLCM(sta, i);
        tmp += dfs(pos - 1, new_sta, new_mod_val, limit && (i == up));
    }
    if (!limit) dp[pos][sta_id][mod_val] = tmp;
    return tmp;
}

long long solve(long long n)
{
    long long pos = 0;
    while (n > 0)
    {
        num[pos++] = n % 10;
        n /= 10;
    }
    return dfs(pos-1, 1, 0, true);
}


int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    init();
    long long t;
    cin >> t;
    while (t-->0)
    {
        long long le, ri;
        cin >> le >> ri;
        cout << solve(ri)-solve(le-1) << endl;
    }
}

 

在這題我遇到了一個坑。我在本機的輸出和在codeforces上的輸出不一致。多虧codeforces有測試樣例輸出,我才發現了在codeforces上整形pow函數會有精度損失。左圖是本機計算pow(10,i)的結果,右圖是codeforces上的結果。原因是pow函數的返回值都是浮點類型,所以取整的時候可能會出現精度損失。因此,以後對於整形的求指,還是自己實現吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章