數位 dp

Brilliant11001發表於2024-08-31

\(\texttt{0x00}\) 簡介

數位 dp 解決的是與數字有關的一類計數問題,在求解過程中常把一個數字的每一位都拆開來看,比如十進位制下就是把千位、百位、十位、個位上的數字都拆開來看,其他進位制類比十進位制。

數位 dp 的問題一般比較顯眼,有幾個常見形式:

  1. 要求統計滿足一定條件的數的數量(即,最終目的為計數);

  2. 這些條件經過轉化後可以使用「數位」的思想去理解和判斷;

  3. 輸入會提供一個數字區間(有時也只提供上界)來作為統計的限制;

  4. 上界很大(比如 \(10^{18}\)),暴力列舉驗證會超時。

(from OI Wiki)

在數位 dp 的實現上,我通常採用的是記憶化搜尋,這樣寫不僅容易,而且易於擴充,還可以當板子來背,這已經是 dp 中少見的了。

\(\texttt{0x01}\) 例題

P2657 [SCOI2009] windy 數

題目大意

\([l, r]\) 內有多少個數十進位制表示下所有的相鄰數位數值之差大於等於 \(2\)

思路

考慮從最高位開始填數,在記憶化搜尋時記錄 \(pos\) 表示當前填到第幾位,\(pre\_num\) 表示上一個位置填的數是什麼,\(limit\) 記錄前面放的數是否頂上界,\(zero\) 記錄當前這位之前是否是前導零。

先把上界的每一位摳出來,那麼當搜尋放第 \(i\) 位時,要先確定這一位能放什麼數,若前面都是貼著上界放的,那麼這一位最多隻能放 \(num_{pos}\),否則就不受限制。

然後在列舉第 \(i\) 位放什麼時還要滿足相鄰數位數值之差大於等於 \(2\) 的限制,這個很好轉移。

當然,若是前導零的話還要特別注意,因為這時 \(0\sim 9\) 都可以放,而如果沒有考慮到這一點,最高位就只能至少放 \(2\) 了。

在加記憶化時還要注意,若出現了頂上界或前導零的情況是不能記憶化的(當然你也可以多開兩維來額外存,不過我覺得沒什麼必要,這個時候直接暴力搜尋就行了,反正也費不了多少時間)。

\(\texttt{Code:}\)

#include <cmath>
#include <vector>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 15;

int l, r;
int f[N][N];
vector<int> num;

int dfs(int pos, int pre_num, bool limit, bool zero) {
    if(pos < 0) return 1; //邊界,若放完了最後一位就返回 1,因為我們一直是按要求放的,所以此時也是一種情況
    if(!limit && pre_num >= 0 && f[pos][pre_num] != -1) return f[pos][pre_num]; //記憶化
    int mx = (limit ? num[pos] : 9); //計算上界
    int res = 0;
    for(int i = 0; i <= mx; i++) {
        if(abs(i - pre_num) < 2) continue;
        if(!i && zero) //特判前導零的情況,這時 prenum 設為 -2 確保下一位不受任何限制
            res += dfs(pos - 1, -2, limit && (i == num[pos]), 1);
        else 
            res += dfs(pos - 1, i, limit && (i == num[pos]), 0);
    }
    if(!limit && !zero) f[pos][pre_num] = res;
    return res;
}

int calc(int x) {
    num.clear();
    int tmp = x;
    //先把上界的每一位摳出來
    while(tmp) {
        num.push_back(tmp % 10);
        tmp /= 10;
    }
    //初始化
    memset(f, -1, sizeof f);
    return dfs(num.size() - 1, -2, 1, 1);
}

int main() {
    scanf("%d%d", &l, &r);
    //數位 dp 通常都有這種類似字首和的形式
    printf("%d\n", calc(r) - calc(l - 1));
    return 0;
}

P2602 [ZJOI2010] 數字計數

題目大意

\([l, r]\) 中的數在十進位制表示下 \(0\sim 9\) 各個數碼分別出現了多少次。

思路

對每個數碼分開計算,還是拆成 \(r\) 減去 \(l - 1\) 的形式。

在記憶化搜尋時記錄一下當前考慮的數碼 \(d\) 填了多少次,在所有位填完後再計算即可。

後面都只放主要程式碼了,因為真的很板子。

\(\texttt{Code:}\)

ll dfs(int pos, ll cnt, bool limit, bool zero, int d) {
    if(pos < 0) return cnt;
    if(!limit && !zero && f[pos][cnt] != -1) return f[pos][cnt];
    int mx = (limit ? num[pos] : 9);
    ll res = 0;
    for(int i = 0; i <= mx; i++)
        res += dfs(pos - 1, cnt + ((!zero || i) && (i == d)), limit && (i == num[pos]), zero && (!i), d);
    if(!limit && !zero) f[pos][cnt] = res;
    return res;
}

Digit Sum

題目大意

\([1, N]\) 中有多少個數在十進位制表示下數碼和是 \(D\) 的倍數。

資料範圍:\(1\le N\le 10^{10000},1\le D\le 100\)

思路

很明顯的數位 dp。

首先把上界 \(N\) 的每一位摳出來,然後進行填數,個人喜歡從最高位開始填。

加上記憶化,設 \(f(pos, r)\) 表示在沒有頂上界和前導零的情況下,當前填到了第 \(pos\) 位,餘數為 \(r\) 的數的個數。

然後在搜尋過程中記一下當前數位和 \(\bmod p\) 等於多少,再簡單轉移一下即可,詳細註釋在程式碼中。

這裡再講一下數位 dp 如何分析時間複雜度。

注意到狀態數為 \(D\cdot\lg N\),每次轉移時最多列舉 \(10\) 個可填的數,所以時間複雜度為 \(O(D\cdot \lg N)\),可以透過此題。

注意!由於最後要 \(-1\),所以為防止減為負數要先加上模數再取模。

\(\texttt{Code:}\)

ll dfs(int pos, int r, bool limit, bool zero) {
    if(pos < 0) return (r == 0);
    if(!limit && !zero && f[pos][r] != -1) return f[pos][r];
    int mx = (limit ? num[pos] : 9);
    ll res = 0;
    for(int i = 0; i <= mx; i++)
        res = (res + dfs(pos - 1, (r + i) % d, limit && (i == num[pos]), zero && (!i))) % mod;
    if(!limit && !zero) f[pos][r] = res;
    return res;
}

P4127 [AHOI2009] 同類分佈

題目大意

求出 \([l, r]\) 中各位數字之和能整除原數的數的個數。

思路

若是要求整除的數是同一個數,那就和上一題一樣,但若除的數都不一樣該怎麼辦?

那我們就換一種思路,直接列舉數位和,然後在搜尋時每填一個數就相應地減去,同時記錄一下餘數,其他的引數照搬即可。

由於最多有 \(18\) 位,所以要列舉 \(1\sim 162\)

\(\texttt{Code:}\)

ll dfs(int pos, int sum, int r, bool limit, bool zero) {
    if(sum < 0) return 0;
    if(pos < 0) return !sum && !r;
    if(!limit && !zero && f[pos][sum][r] != -1) return f[pos][sum][r];
    int mx = (limit ? num[pos] : 9);
    ll res = 0;
    for(ll i = 0; i <= mx; i++)
        res += dfs(pos - 1, sum - i, (r * 10 + i) % d, limit && (i == num[pos]), zero && (!i));
    if(!limit && !zero) f[pos][sum][r] = res;
    return res;
}

擴充:

數位 dp 一般會與 Lucas 定理一起食用,畢竟 Lucas 定理就是逐位求組合數。

習題:

P8764 [藍橋杯 2021 國 BC] 二進位制問題
P6218 [USACO06NOV] Round Numbers S
P4124 [CQOI2016] 手機號碼
P4317 花神的數論題
P7976 「Stoi2033」園遊會
P3413 SAC#1 - 萌數
P3286 [SCOI2014] 方伯伯的商場之旅
P2481 [SDOI2010] 程式碼拍賣會

相關文章