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 1≤l≤r≤1018, 0≤d≤9
题解
采用数位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
位,就完成了当前状态的表示。用map
套vector
即可
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;
}
}