杭电多校第三场X Number——数位dp+指数型母函数

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6796

题目大意:给你一个区间[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;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值