数位 DP 做题记录
\(\color{green}{\texttt{Luogu P2602 [ZJOI2010]}}\)数字计数
\(\large\mathcal{Description}\)
给定两个正整数 \(a\) 和 \(b\),求在 \([a,b]\) 中的所有整数中,每个数码各出现了多少次。
\(1\le a \le b\le 10^{12}\).
\(\large\mathcal{Solution}\)
典型的数位 \(\texttt{dp}\).
状态设计:\(\texttt{f[n][lead][limit][dig][sum]}\) 表示位数从高到低做到第 \(\texttt{n}\) 位,前导 \(0\) 标志 \(\texttt{lead}\),最高位标记 \(\texttt{limit}\), 现在统计的数码是 \(\texttt{dig}\) , 现在已经有了 \(\texttt{sum}\) 个 \(\texttt{dig}\) 的方案数的情况下有多少个 \(\texttt{dig}\).
递推方程很好设。
for (reg int i = 0; i <= Max; ++ i)
if (i == 0 && lead) res += dfs(n - 1, 1, 0, dig, sum);
else res += dfs(n - 1, 0, limit && (i == Max), dig, sum + (i == dig));
然后目标是 \(\texttt{f[len][1][1][i][0]}\) , 其中 \(\texttt{len}\) 是总数位,\(\texttt{i}\) 为现在统计的数码。
然后就没有然后了。
\(\large\mathcal{Code}\)
#include <bits/stdc++.h>
#define reg register
using namespace std;
typedef long long LL;
typedef long double LDB;
LL l, r, f[15][2][2][10][15];
int len, a[15];
LL dfs(reg int n, reg bool lead, reg bool limit, reg int dig, reg int sum)
{
reg LL res = 0;
if (!n) return sum;
if (f[n][lead][limit][dig][sum] != -1) return f[n][lead][limit][dig][sum];
reg int Max = limit ? a[n] : 9;
for (reg int i = 0; i <= Max; ++ i)
if (i == 0 && lead) res += dfs(n - 1, 1, 0, dig, sum);
else res += dfs(n - 1, 0, limit && (i == Max), dig, sum + (i == dig));
return f[n][lead][limit][dig][sum] = res;
}
inline LL work(reg LL x, reg int dig)
{
len = 0;
while (x)
{
a[ ++ len] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(len, 1, 1, dig, 0);
}
int main()
{
scanf("%lld %lld", &l, &r);
for (reg int i = 0; i <= 9; ++ i) printf("%lld ", work(r, i) - work(l - 1, i));
return 0;
}
\(\color{green}{\texttt{Luogu P6218 [USACO06NOV]}} \texttt{Luogu Round Numbers S}\)
\(\large\mathcal{Description}\)
如果一个正整数的二进制表示中,\(0\) 的数目不小于 \(1\) 的数目,那么它就被称为「圆数」。
请你计算,区间 \([l,r]\) 中有多少个「圆数」。
\(1\le l, r\le 2 \times 10^9.\)
\(\large\mathcal{Solution}\)
感觉比上一题还简单。
我们平时做数位 \(\texttt{dp}\) 是十进制拆分,我们这题因为是考虑一个数的二进制的性质所以我们进行二进制拆分。
状态设计:\(\texttt{f[n][lead][limit][now + 35]}\) 表示做到第 \(\texttt{n}\) 位,前导 \(0\) 标志, \(\texttt{lead}\),最高位标记 \(\texttt{limit}\) , 然后 \(\texttt{now}\) 是 \(0\) 个数和 \(1\) 个数之差。之所以加上 \(35\) 是因为 \(\texttt{now}\) 本身有可能是负数。
转移显然。当前位是前导 \(0\) 的话转移到 \(\texttt{f[n - 1][...][...][now]}\), 若是 \(0\) 但不是前导 \(0\) 的话转移到 \(\texttt{f[n - 1][...][...][now + 1]}\), 否则转移到 \(\texttt{f[n - 1][...][...][now - 1]}.\)
然后目标是 \(\texttt{f[len][1][1][35]}\).
\(\large\mathcal{Code}\)
#include <bits/stdc++.h>
#define reg register
using namespace std;
typedef long long LL;
typedef long double LDB;
int l, r;
int len, a[35], f[35][2][2][70];
int dfs(reg int n, reg bool lead, reg bool limit, reg int now)
{
if (!n) return now >= 0;
if (f[n][lead][limit][now + 35] != -1) return f[n][lead][limit][now + 35];
reg int Max = (limit ? a[n] : 1), res = 0;
for (reg int i = 0; i <= Max; ++ i)
res += dfs(n - 1, lead && (i == 0), limit && (i == a[n]), now + ((lead && !i) ? 0 : (i == 0 ? 1 : -1)));
return f[n][lead][limit][now + 35] = res;
}
inline int work(reg int x)
{
len = 0;
while (x)
{
a[ ++ len] = x % 2;
x /= 2;
}
memset(f, -1, sizeof f);
return dfs(len, 1, 1, 0);
}
int main()
{
scanf("%d %d", &l, &r);
printf("%d\n", work(r) - work(l - 1));
return 0;
}
\(\color{green}{\texttt{Luogu P4317 }}\)花神的数论题
\(\large\mathcal{Description}\)
\(sum(i)\) 表示 \(i\) 二进制下 \(1\) 的个数。求 \(\prod_{i=1}^n sum(i)\).
\(1\le n\le 10^{15}.\)
\(\large\mathcal{Solution}\)
比较常规的数位 \(\texttt{dp}.\)
状态设计:\(\texttt{f[n][lead][limit][s]}\) 表示做到第 \(n\) 位,前导 \(0\) 标志 \(\texttt{lead}\), 最高位标志 \(\texttt{limit}\), 当前搜到的数的二进制表示下 \(1\) 的个数位 \(s\).
然后跟第二题一样,二进制拆分。
转移也非常显然。
reg int Max = limit ? a[n] : 1, ret = 1;
for (reg int i = 0; i <= Max; ++ i)
ret = 1ll * ret * dfs(n - 1, lead && (i == 0), limit && (i == a[n]), s + (i == 1)) % mod;
这个应该比较好懂。初始状态显然是 \(\texttt{f[len][1][1][0]}\), 其中 \(\texttt{len}\) 是输入的数二进制拆分的位数。
\(\large\mathcal{Code}\)
#include <bits/stdc++.h>
#define reg register
using namespace std;
typedef long long LL;
typedef long double LDB;
const int mod = 10000007;
LL x;
int len, a[70], f[70][2][2][70];
LL dfs(reg int n, reg bool lead, reg bool limit, reg int s)
{
if (!n) return (s == 0 ? 1 : s);
if (f[n][lead][limit][s] != -1) return f[n][lead][limit][s];
reg int Max = limit ? a[n] : 1, ret = 1;
for (reg int i = 0; i <= Max; ++ i)
ret = 1ll * ret * dfs(n - 1, lead && (i == 0), limit && (i == a[n]), s + (i == 1)) % mod;
return f[n][lead][limit][s] = ret;
}
inline LL work(reg LL x)
{
len = 0;
while (x)
{
a[ ++ len] = x % 2;
x /= 2;
}
memset(f, -1, sizeof f);
return dfs(len, 1, 1, 0);
}
int main()
{
scanf("%lld", &x);
printf("%lld\n", work(x));
return 0;
}
\(\color{green}{\texttt{Luogu P4999 }}\)烦人的数学作业
\(\large\mathcal{Description}\)
给定 \(l, r\), 求 \(\sum_{i=l}^rsum(i)\). 其中 \(sum(i)\) 表示 \(i\) 的数位和。
\(1\le l, r\le 10^{18}.\)
\(\large\mathcal{Solution}\)
不讲,把第一题改一改就过了。
\(\large\mathcal{Code}\)
#include <bits/stdc++.h>
#define reg register
using namespace std;
typedef long long LL;
typedef long double LDB;
const int mod = 1000000007;
LL l, r, f[20][2][2][10][20];
int T, len, a[20];
LL dfs(reg int n, reg bool lead, reg bool limit, reg int dig, reg int sum)
{
reg LL res = 0;
if (!n) return sum;
if (f[n][lead][limit][dig][sum] != -1) return f[n][lead][limit][dig][sum];
reg int Max = limit ? a[n] : 9;
for (reg int i = 0; i <= Max; ++ i)
if (i == 0 && lead) res += dfs(n - 1, 1, 0, dig, sum);
else res += dfs(n - 1, 0, limit && (i == Max), dig, sum + (i == dig));
return f[n][lead][limit][dig][sum] = res;
}
inline LL work(reg LL x, reg int dig)
{
len = 0;
while (x)
{
a[ ++ len] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(len, 1, 1, dig, 0);
}
int main()
{
scanf("%d", &T);
while (T -- )
{
reg LL ans = 0;
scanf("%lld %lld", &l, &r);
for (reg int i = 0; i <= 9; ++ i)
{
reg LL cnt = work(r, i) - work(l - 1, i);
ans = (ans + 1ll * (cnt % mod) * i % mod) % mod;
}
printf("%lld\n", (ans % mod + mod) % mod);
}
return 0;
}
\(\color{green}{\texttt{Luogu P4127 [AHOI2009]}}\)同类分布
\(\large\mathcal{Description}\)
给出 \(l, r\),求出 \([l, r]\) 中各位数字之和能整除原数的数的个数。
\(1\le l, r\le 10^{18}.\)
\(\large\mathcal{Solution}\)
感觉灵活了不少。
首先我们能想到一个朴素的算法:把当前做到的位数 \(n\), 前导零标记,最高位标记,数字和 \(sum\),原数 \(num\) 统统记进状态,转移显然、
然后...好像做完了?个锤子。明显原数高达 \(10^{18}\), 无法记进状态。那怎么办呢?我们很容易想到的是我们把原数在 \(\bmod\) 数字和记进状态。但是考虑到数字和并非固定的,这样也不行。我们退而求其次,既然只能将原数 \(\bmod\) 一个固定的数记进状态,不妨枚举数字和。
转移一样转移,即:
reg int Max = limit ? a[n] : 9;
reg LL res = 0;
for (reg int i = 0; i <= Max; ++ i)
res += dfs(n - 1, lead && (i == 0), limit && (i == a[n]), sum + i, (num * 10 + i) % mod);
然后当 \(n = 0\) 时,你需要 \(sum=mod\ \&\ num\bmod mod = 0\) 才能返回 \(1\).
最后你发现前导 \(0\) 标记在这题完全没有作用,删掉即可。
\(\large\mathcal{Code}\)
#include <bits/stdc++.h>
#define reg register
using namespace std;
typedef long long LL;
typedef long double LDB;
LL l, r, f[20][2][180][180];
int len, a[20], mod;
LL dfs(reg int n, reg bool limit, reg int sum, reg int num)
{
if (sum > mod || sum + 9 * n < mod) return 0;
if (!n) return (sum == mod && num == 0) ? 1 : 0;
if (f[n][limit][sum][num] != -1) return f[n][limit][sum][num];
reg int Max = limit ? a[n] : 9;
reg LL res = 0;
for (reg int i = 0; i <= Max; ++ i)
res += dfs(n - 1, limit && (i == a[n]), sum + i, (num * 10 + i) % mod);
return f[n][limit][sum][num] = res;
}
inline LL work(reg LL x)
{
len = 0;
while (x)
{
a[ ++ len] = x % 10;
x /= 10;
}
reg LL res = 0;
for (mod = 1; mod <= 9 * len; ++ mod)
{
memset(f, -1, sizeof f);
res += dfs(len, 1, 0, 0);
}
return res;
}
int main()
{
cin >> l >> r;
cout << work(r) - work(l - 1) << endl;
return 0;
}
UPD ON 2021.9.15
\(\color{green}{\texttt{CF1036C } \color{black}{Classy\ Numbers}}\)
\(\large\mathcal{Description}\)
定义一个数字是“好数”,当且仅当它的十进制表示下有不超过 \(3\) 个数字 \(1 \sim 9\).
给定 \([l,r]\),问有多少个 \(x\) 使得 \(l \le x \le r\),且 \(x\) 是“好数”。
多测,测试组数 \(\le 10^4\), \(l, r\le 10^{18}\).
\(\large\mathcal{Solution}\)
一眼题。
设 \(\texttt{f[n][lead][limit][sum]}\) 表示位数从高到低做到第 \(\texttt{n}\) 位,前导 \(0\) 标志 \(\texttt{lead}\),最高位标记 \(\texttt{limit}\), 现在有 \(\texttt{sum}\) 个数 \(1\sim 9\).
转移显然。
int T, a[20];
LL f[20][2][2][20];
LL dfs(reg int n, reg int lead, reg int limit, reg int sum)
{
if (!n) return sum <= 3;
if (f[n][lead][limit][sum] != -1) return f[n][lead][limit][sum];
reg int Max = limit ? a[n] : 9;
reg LL ans = 0;
for (reg int i = 0; i <= Max; ++ i) ans += dfs(n - 1, lead && (i == 0), limit && (i == a[n]), sum + (i > 0));
return f[n][lead][limit][sum] = ans;
}
LL work(reg LL x)
{
reg int len = 0;
while (x)
{
a[ ++ len] = x % 10;
x /= 10;
}
memset(f, -1, sizeof f);
return dfs(len, 1, 1, 0);
}
int main()
{
scanf("%d", &T);
while (T -- )
{
reg LL l, r;
scanf("%lld %lld", &l, &r);
}
return 0;
}
\(\color{green}{\texttt{[ABC154E] } \color{black}{Almost\ Everywhere\ Zero}}\)
与上题几乎一致...
\(\large\mathcal{Code}\)
string n;
int K, a[200];
LL f[200][2][2][20];
LL dfs(reg int n, reg int lead, reg int limit, reg int sum)
{
if (!n) return sum == K;
if (sum > K) return 0;
if (f[n][lead][limit][sum] != -1) return f[n][lead][limit][sum];
reg int Max = limit ? a[n] : 9;
reg LL ans = 0;
for (reg int i = 0; i <= Max; ++ i) ans += dfs(n - 1, lead && (i == 0), limit && (i == a[n]), sum + (i > 0));
return f[n][lead][limit][sum] = ans;
}
int main()
{
cin >> n >> K;
reg int sz = n.size();
for (reg int i = sz - 1; i >= 0; -- i) a[sz - i] = n[i] - '0';
memset(f, -1, sizeof f);
cout << dfs(sz, 1, 1, 0);
return 0;
}