题目入口: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;
}