1. 程式人生 > >【學習筆記】數位DP

【學習筆記】數位DP

題目

求區間[l,r]內,滿足某條件的數的個數。這個條件一般與數的組成有關,如不包括4,非0位不超過3個,等等。

解法

  1. 答案滿足字首性質,可以用[1,r]的答案減去[1,l-1]的答案。在求解[1,x]時,遍歷每一位數,對每一位數及之後的位求取答案。
  2. 在滿足條件的數字很少且方便搜尋時,可以打一個表然後直接upper_bound - lower_bound.
Codeforces 1036C. Classy Numbers

定義好看數字為含有不超過3個非0數字位的數字,1e4個詢問,每次問兩個數(1e18)之間好看數字的個數。

//經過很長時間的優化後 狀態表示

solve(upper,need)表示[0,upper]內有多少個非0位不超過need個的數字,之所以不用陣列是因為upper會很大,存不下。 初始狀態upper=0need=0solve0 狀態轉移

solve(upper,need)=solve(10k11,need)+(upper[0]1)solve(10k11,need1
)+solve(upper%10k1,need1)
其中k為表示upper的位數,upper[0]表示upper的最高位數字。 式子第一項表示最高位為0時,第二項表示最高位小於upper最高位,最後一項代表最高位不變。 狀態優化:可以發現,演算法會重複計算非常多次全為9的upper,所以首先打一個表,table[i][j]=solve(10i+11,j),之後狀態轉移方程變為 solve(upper,need)=table[k1][need]+(upper[0]1)table[k1][need1]+solve(upper%10k1,need1) 複雜度:打表複雜度O(80),當計算solve時,狀態數O(k),狀態轉移O(1),總複雜度O(log n)

實現細節:

  1. 額外預處理一個10的冪次表,就可以通過upper_bound來獲得位數。
  2. 獲得位數後再查表一次求商,就可以獲得最高位的值。
  3. 遞迴時不能按位數每次減一,因為會有連續0的情況,只需要統計一次。
const int NEED = 3;
ll table[22][4]={1,1,1,1};
vector<ll> power10(1,1);

ll solve(ll upper, int need)
{
    if(!bit || !need) return 1;
    int bit = upper_bound(power10.begin(), power10.end(), upper) - power10.begin();

    return table[bit - 1][need] +
           (upper / power10[bit - 1] - 1) * table[bit - 1][need - 1] +
           solve(upper % power10[bit - 1], need - 1);
}
int main(void)
{
    for(int i = 1; i <= 18; i++)
        power10.push_back(power10.back() * 10);
    for(int i = 1; i <= 19; i++)    
    {
        table[i][0] = 1;
        for(int j = 1; j <= 3; j++)
            table[i][j] = table[i - 1][j] + 9 * table[i - 1][j - 1];
    }
    int T = read();
    while(T--)
    {
        ll lef = read(), rig = read();
        printf("%I64d\n", solve(rig, NEED) - solve(lef - 1, NEED) );
    }

    return 0;
}
小結

字首性質一定要利用好,但本題中是[0,r]-[0,l-1],0與1要分清楚。 數位dp的狀態表示也像普通dp那樣,直接以答案的形式表示。 因為狀態數過多,無法用陣列表示時,可以整體仍然遞迴,部分記憶化或打表以加快速度。