數位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;
}
難度困難,這一題我輾轉了好久。一開始對如何確定一個數對各數位取模都為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;
}
難度困難。這一題實際上並不是特別難,之所以設為困難是因為我這一題卡了好久。首先,我沒看到,給定[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;
}