HDU 6796 X number:数位DP+指数型母函数

X number:数位DP,指数型母函数

题目

如果一个数字的所有数位中,数字 d d d出现的次数最多且是唯一最多,则该数字数于 d d d类。

给定区间 [ l , r ] [l,r] [l,r],求该区间内属于 d d d类的数字的个数

1 ≤ l ≤ r ≤ 1 0 18 ,   0 ≤ d ≤ 9 1\le l\le r\le 10^{18},\ 0\le d\le 9 1lr1018, 0d9

题解

采用数位DP的思想,当计算到某一数位且没有限制时,直接采用指数型母函数来计算可行方案数。

首先通过dfs()对每一数位进行枚举:

当有前导零或者有限制时,使用a[10]数组存储枚举过程的选择情况,如果递归到了最后一位,就通过check()函数检查当前的枚举情况是否合理;

当没有限制并且没有前导零时,那么剩余的数位的方案数可以直接通过cal()指数型母函数计算出来

指数型母函数计算过程

首先统计除了数字 d d d以外数字的最大个数,记为mx

int mx = 0;  // 除了数字d以外数字的最大个数
for (int i = 0; i <= 9; i++) {
    if (i != d) mx = max(mx, a[i]);
}

考虑后pos位中,数字 d d d的取值范围:

数字 d d d至少要比目前已出现的数字中最多的那一个大1,至多就是把pos位全部填d

在之后的选择过程中,其他数字的选择次数都要小于数字 d d d的选择次数,以保证数字 d d d是最多的且是唯一的

int MI = max(mx + 1, a[d]) - a[d];  // 数字d最少的个数
int MX = pos;  // d最多的个数

确定了后pos d d d的范围后,枚举即可

for (int m = MI; m <= MX; m++) {  // 枚举后面pos个位置用多少个d
    memset(f, 0, sizeof f);
    f[0] = 1;
    for (int i = 0; i <= 9; i++) {  // 指数型母函数求解
        memset(g, 0, sizeof g);
        int sta = i == d ? m : 0;  // d至少选m个
        int end = i == d ? m : min(pos - m, m + a[d] - a[i] - 1);  //选其他数字不能超过m个,也不能超过位数限制
        for (int j = 0; j <= pos; j++) {
            for (int k = sta; k <= end && j + k <= pos; k++) {
                g[j + k] += 1.0 * f[j] / fac[k];
            }
        }
        memcpy(f, g, sizeof f);
    }
    tot += f[pos] * fac[pos] + 0.5;  // 精度损失
}
记忆化优化

记忆化的时候只需要考虑选了不是 d d d的数字的个数,而不用关心具体选了哪些数字。比如$d=$1时,选择1个2、2个3与1个3、2个2计算出来的结果是一样的,不会影响1的摆法。

将选了非 d d d数字的个数排序后,再插入选 d d d的个数,再插入当前是第pos位,就完成了当前状态的表示。用mapvector即可

vector<int> v;
for (int i = 0; i <= 9; i++) {  // 记忆化状态
    if (i != d && a[i]) {
        v.push_back(a[i]);
    }
}
sort(v.begin(), v.end());
v.push_back(a[d]);
v.push_back(pos);
if (mp.find(v) != mp.end()) {
    return mp[v];
}
return mp[v] = cal(pos);

代码

int d;  // 要求的类别(0~9)
int x[20];  // 存储数字的每一位
int a[10];  // 存储0-9的选择个数
ll fac[20];  // 阶乘,用于指数型母函数
map<vector<int>, ll> mp;  // 记忆化
long double g[20], f[20];
void init()
{
    fac[0] = 1;
    for (int i = 1; i <= 18; i++) {
        fac[i] = fac[i - 1] * i;
    }
}
inline bool check()
{
    for (int i = 0; i <= 9; i++) {
        if (i != d && a[i] >= a[d]) return 0;
    }
    return 1;
}
ll cal(int pos)
{
    int mx = 0;  // 除了数字d以外数字的最大个数
    for (int i = 0; i <= 9; i++) {
        if (i != d) mx = max(mx, a[i]);
    }
    ll tot = 0;
    int MI = max(mx + 1, a[d]) - a[d];  // d最少的个数
    int MX = pos;  // d最多的个数
    for (int m = MI; m <= MX; m++) {  // 枚举后面pos个位置用多少个d
        memset(f, 0, sizeof f);
        f[0] = 1;
        for (int i = 0; i <= 9; i++) {  // 指数型母函数求解
            memset(g, 0, sizeof g);
            int sta = i == d ? m : 0;  // d至少选m个
            int end = i == d ? m : min(pos - m, m + a[d] - a[i] - 1);  //选其他数字不能超过m个
            for (int j = 0; j <= pos; j++) {
                for (int k = sta; k <= end && j + k <= pos; k++) {
                    g[j + k] += 1.0 * f[j] / fac[k];
                }
            }
            memcpy(f, g, sizeof f);
        }
        tot += f[pos] * fac[pos] + 0.5;  // 精度损失
    }
    return tot;
}
ll dfs(int pos, int lim, int zero)
{
    ll sum = 0;
    if (pos == 0) {  // 检查当前填充方法是否可行
        return check();
    }
    if (!lim && zero) {  // 没有限制,并且没有前导零,直接计算
        vector<int> v;
        for (int i = 0; i <= 9; i++) {  // 记忆化状态
            if (i != d && a[i]) {
                v.push_back(a[i]);
            }
        }
        sort(v.begin(), v.end());
        v.push_back(a[d]);
        v.push_back(pos);
        if (mp.find(v) != mp.end()) {
            return mp[v];
        }
        return mp[v] = cal(pos);
    }
    int up = lim ? x[pos] : 9;  // 当前位最大能取的数字
    for (int i = 0; i <= up; i++) {
        if (zero || i != 0) {  //是前导零时,0的个数不用加
            a[i]++;
        }
        sum += dfs(pos - 1, lim && i == up, zero || i);
        if (zero || i != 0) {
            a[i]--;
        }
    }
    return sum;
}
ll solve(ll num)
{
    int cnt = 0;  // 位数
    while (num) {
        x[++cnt] = num % 10;
        num /= 10;
    }
    return dfs(cnt, 1, 0);
}
int main()
{
    ios::sync_with_stdio(0), cin.tie(0);
    init();
    int _;
    cin >> _;
    while (_--) {
        ll l, r;
        cin >> l >> r >> d;
        cout << solve(r) - solve(l - 1) << endl;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值