數位DP:
用來統計區間內符合條件的數的個數,通常區間範圍很大,並且條件通常與數的組成有關。
解題關鍵:對前綴的抽象分類。須符合兩個條件。
- 對於一個數anan-1……a2a1a0, 可以將前綴anan-1…ak+1歸類於狀態(sta,k), 使得具有相同狀態的前綴的解一致。
- 可以求解,在狀態(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函數的返回值都是浮點類型,所以取整的時候可能會出現精度損失。因此,以後對於整形的求指,還是自己實現吧!