题目大意:给你一个区间[l,r],让你求出这个区间内满足数位中众数为d的数的个数。
题解:一道披着数位dp皮的组合计数题。
如果用常规数位dp的思路去维护,不难发现状态很难维护,因此我们要考虑换一种思路。
考虑数位dp枚举当前位时,当前位不是数位的上限的情况。
eg:假设数位上限为123456789,当前枚举到第七位,之前的数位为123455(即123455XXX),
即前面数位有1个1,1个2,1个3,1个4,2个5,后面数任意取能组成众数为d的情况。
这个可以通过指数型生成函数(用来求可重集的排列方案数)求出来,具体细节参考代码。
具体的解决方法:
假设当前枚举数位为pos(即后面还有pos位可以填)
先枚举d在pos中的出现个数,至少大于之前数位中除d之外的最大出现次数(mx+1)减去之前d出现的次数,至多是pos(位数限制)。
这样d在pos中的个数就确定了,问题就转换成了已知总个数为pos,0的出现次数为[L,R]....d的出现次数为[m,m]...9的出现次数为[L,R],求从中选pos个的排列数,这是一个比较经典的可重集的排列问题,可以用指数型母函数解决。
注意i(!=d)的出现次数最少为0,最大应该为min(pos-m,m+cnt[d]-cnt[i]-1)。
因为要保证位数的限制,以及d是众数的条件(即总个数m+cnt[d]应该大于之前i出现的次数cnt[i])。
但是对于这种情况我们直接维护是会超时的,因此我们需要考虑优化一下。
考虑1个1,2个2和2个1,1个2的情况对于d的影响是相同的,所以我们可以考虑记忆化一下,定义状态为其他数(不等于d)的次数排序后的排列顺序和d的个数,以及当前枚举的位置相连,因此需要通过map和vector来实现记忆化。
而对于其他情况,继续dfs维护就行了。
代码实现:
#include <bits/stdc++.h> #define PI atan(1.0)*4 #define E 2.718281828 #define rp(i,s,t) for (register int i = (s); i <= (t); i++) #define RP(i,t,s) for (register int i = (t); i >= (s); i--) #define ll long long #define ull unsigned long long #define mst(a,b) memset(a,b,sizeof(a)) #define lson l,m,rt<<1 #define rson m+1,r,rt<<1|1 #define pii pair<int,int> #define pb push_back #define debug puts("ac") #define INF 0x3f3f3f3f #define LINF 0x3f3f3f3f3f3f3f3f using namespace std; int cnt[10],d; int a[20],num; map<vector<int>,ll> mp; long double g[20],f[20]; ll fac[20]; void init(){//预处理阶乘 fac[0]=1; for(int i=1;i<20;i++) fac[i]=fac[i-1]*i; } ll calc(int pos){//计算1~pos位置符合条件的数的个数 int mx=0;//维护除d外的最大出现次数 for(int i=0;i<=9;i++) if(i!=d) mx=max(mx,cnt[i]); int l=max(mx+1-cnt[d],0);//d的最少个数需要满足比除d外的最大出现次数至少多一个的条件 int r=pos;//最多的个数是位数pos ll ans=0; for(int m=l;m<=r;m++){//枚举d的个数 memset(f,0,sizeof(f)); f[0]=1; for(int i=0;i<=9;i++){//指数型母函数求解满足d出现次数为m,总出现次数为pos的排列个数 memset(g,0,sizeof g); int st=(i==d)?m:0; int ed=(i==d)?m:min(pos-m,m+cnt[d]-cnt[i]-1); //既要满足m+cnt[d]大于cnt[i]的条件,又要满足位数的限制 for(int j=0;j<=pos;j++) for(int k=st;j+k<=pos&&k<=ed;k++) g[j+k]+=f[j]*1.0/fac[k]; memcpy(f,g,sizeof(f)); } ans+=f[pos]*fac[pos]+0.5;//精度损失 } return ans; } ll dfs(int pos,int limit,int lead){ if(pos==0){//枚举到最后一位直接判断就行了 int mx=cnt[d]; int tot=0; for(int i=0;i<=9;i++) if(i!=d) mx=max(mx,cnt[i]); for(int i=0;i<=9;i++) if(cnt[i]==mx) tot++; return mx==cnt[d]&&tot==1; } ll ans=0; if(!limit&&!lead){//当前位之前没有前导0且没有被限制 vector<int> v;//记忆化存储状态 //因为当d为3时,取1次1,2次2和1次2,2次1时没有区别的 //状态和最后排序后次数的排列顺序以及d的个数和当前位置有关 for(int i=0;i<=9;i++) if(i!=d&&cnt[i]) v.push_back(cnt[i]); sort(v.begin(),v.end()); v.push_back(cnt[d]); v.push_back(pos); if(mp.find(v)!=mp.end()) return mp[v];//当前状态之前已经被访问过,直接返回 return mp[v]=calc(pos);//否则记忆化一下 } int up=limit?a[pos]:9; for(int i=0;i<=up;i++){//其余情况,维护正常的数位dp求就行了。 if(!lead||i!=0) cnt[i]++;//排除掉有前导0且当前位为0的情况。 ans+=dfs(pos-1,limit&i==a[pos],lead&&i==0); if(!lead||i!=0) cnt[i]--;//不要忘记清除掉之前的影响 } return ans; } ll solve(ll x){ num=0; memset(cnt,0,sizeof(cnt)); while(x) a[++num]=x%10,x/=10; return dfs(num,1,1); } int main(){ init(); int T;scanf("%d",&T); while(T--){ ll l,r;scanf("%lld%lld%d",&l,&r,&d); printf("%lld\n",solve(r)-solve(l-1)); } return 0; }