数位dp(打牌),这是一个相当深刻并且具有意义的话题。在没看懂这个内容的时候完完全全就是一脸懵逼,现在依旧是一脸懵逼。你以为你会了,题目:不,你不会!!就像你可能以为博主已经掌握了这个算法。
欸,你错了,我压根就不会。
博主只是看(抄)完别人的代码,突有所悟。又厚颜无耻的出一期博客(瞎bb)!
好了!今天,在这里,我主要介绍的是dfs模式实现的数位打牌模式 ;先来个简单点的问题吧。
3
1
50
500
0
1
15
那么我们开始讲讲思路:
这个问题怎么搞呢,首先我们看到n的范围非常的大,根本没有办法一个一个判断。这时候开动我们的脑袋瓜子,那我们可不可几十个几十个,或者几百个几百个一次算呢?Of course!why not!,这,就是我们打牌数组的来历(dp数组的作用),举个栗子,比如1~500的范围中:1–100,101–200,201–300,都是有一样的个数的符合条件的数字的,因为他们的最高位都与49的4无关,所以这三个区段又与401-500之间的个数不同,综上我们的打牌数组只需要两个维度dp【位数】【最高位是不是4】。接下来就是按从高到低依次枚举就好了。
----哇,博主,看你这么多,我完全没懂啊!
----莫慌,先看代码,下面还有讲解。
#include
using namespace std;
const int Max = 99999;
const int Min = 0;
const int inf = 1e6;
const int mod = 1e9+7;
#define M 1000
#define N 1000
#define ll long long
#define swap(x,y) x^=y^=x^=y
ll dp[20][6];
int digit[20];
ll dfs(int pos,int pre,int sta,bool limit){ //以53421为例
if(pos==-1) return 1; //代表这种情况搜索结束,+1
if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta]; //如果没有达到上限比如搜索0--50000 的时候,第一位是0,1,2,3,4的时候没有限制,5的时候有
int up = limit?digit[pos]:9; //有限制的话选取下一位的值,如万位是0,1,2,3,4,千位可以是1-9,但是万位是5,千位不能超过3
ll sum(0);
for(int i=0;i<=up;i++){ //每一位进行枚举
if(pre==4&&i==9) continue; //符合条件的不搜了
sum += dfs(pos-1,i,i==4,limit&&i==digit[pos]); //不符合条件的加上,pre记录这一位的值,sta记录是否有可能成为49,最后一个表示是否有限制
}
if(!limit) dp[pos][sta] = sum; //没有限制将dp的数值存起来,以便调用
return sum; //返回值
}
ll solve(ll a){
int cnt = 0; //分解这个数
while(a>0){
digit[cnt++] = a%10;
a/=10;
}
ll ans = dfs(cnt-1,0,0,true); //对这个数进行dfs
return ans;
}
int main(){
#ifdef LOCAL
freopen("test.txt","r",stdin);
#endif
int T;
cin >> T;
memset(dp,-1,sizeof(dp));
while(T--){
ll a;
scanf("%I64d",&a);
ll ans = solve(a);
cout << a+1-ans << endl;
}
return 0;
}
dp数组的作用是什么呢,看这句dp【位数】【最高位是不是4】,dp【20】【2】,一维存储的是每一位(十百千等,这就是我们的存储阶级)中符合条件的个数,比如dp[2][0]存储的就是每一百个数中符合要求的数字数量,dp[2][1]则是400–500中符合要求的数量。为啥要额外计算最高位为4的情况呢,因为400开头,很明显有490-499这一段数是与其他阶段不同的,而且无论题目是49,还是62,道理都是一样的。
因为深搜的特性,在计算dp[4][0]和dp[4][1]的时候(即每一万个数的符合条件的个数)会一口气深搜到底,顺势就得到了4位数以下的数目,因为得到每一万位的时候需要每一千位的值,同样每一千位需要每一百位等等等等。经过存储之后的dp,再经由第15行的返回dp的值,就大大体现了记忆化搜索的好处。
我们这里以53421为例:我们的dfs实际计算过程是分别计算:1–50000,50000–53000,53000–53400,53400-53420,53420-53421.
首先是最高位5,进入循环pos=4,pre=0,sta = 0,limit = true;
第14行 不用判断(为什么是1呢,因为深搜是按次数搜索的,每一次深搜结果,代表有一种情况)
第15行 dp数组的值目前全都是-1,所以依旧跳过,
第16行 limit有限制,up = 5(这里我们不能让遍历的值超过这一位的数),
第18行 开始进行对这位数的数值进行遍历,分别是0,1,2,3,4,5
第19行 判断上一位是不是4,如果是4,并且这一位遍历的是9,那么跳过他,因为没有上一位,肯定不是。
第20行 汇总这一个sum值。
第22行 如果没有数字限制(这里的限制就是指有没有遍历到遍历的上限,这里就是5)则可以继续访问,比如这一位枚举了0(不是5,所以没有限制),则下一位就可以枚举到9,如果这一位枚举了5,则下一位就没法枚举到9,只能枚举到digit[3],也就是3了。所以无限制就意味着枚举肯定到了9,这个值就是这一个阶梯(十百千等)所含的值。赋值给dp数组,以便于记忆化搜索
后面的过程基本一样,我就不赘述,有兴趣可以自己模拟一下。
第45行为啥是"a+1-ans"呢?如果题目问50到500之间有多少个这种数怎么办呢?
1.a+1-ans的原因是因为在遍历的时候dfs的内容把0这个数也加了进去,所以ans是比正确答案多1的。
+1,-1抵消. 欸,完美,无懈可击!
2.如果,题目问的是50到500之间怎么办!根本不用盘他,开前缀和啊!,solve(50) 记录的是0–50之间的个数,solve(500) 记录的是0–500之间的个数,那么两者相减自然就是 (50,500](注意这里是开区间)的个数了。如果是【50,500】的话就是solve(500)-solve(50-1)。一样的。