「REMAKE系列」数位dp篇
模板及技巧总结
数位DP
常前缀和思想来解决 \([l,r]\) 范围内满足 xxx 要求的数字个数。
经典数位DP
- 求 \([l,r]\) 之间不含前导零且相邻两个数字之差至少为 \(2\) 的正整数。P2657 [SCOI2009] windy 数
- 出现连续 \(3\) 个相同数且不同时出现 \(4,8\) 的 11 位数。[P4124 [CQOI2016]手机号码](#3)
- 满足一些简单条件没有前导零的数位dp。CF628D Magic Numbers
- 萌数---数位中含回文的数字,结论:存在某一位和上一位或上两位相同。P3413 SAC#1 - 萌数
- 求出 \([a,b]\) 中各位数字之和能整除原数的数的个数。P4127 [AHOI2009]同类分布
- 进制+状态压缩+数位dp。CF855E. Salazar Slytherin's Locket
- 卡常状态压缩+数位dp。CF401D Roman and Numbers
较难写的数位DP
- 给定 \(K(K\leq 10),L,R\) ,求 \([L,R]\) 之间最多不包含超过 \(K\) 个数码的数的和。CF1073E Segment Sum
- 数位DP+简单数论,一个数字 \(x\) 是美丽的,当且仅当 \(x\in\mathbb{Z^+}\) 并且对于 \(x\) 的每一个非零位上的数 \(y\),都有 \(y|x\)。CF55D Beautiful numbers
- 智慧暴力, 二进制上数位dp。2020ICPC济南 L. Bit Sequence
- 双线程数位dp,传统记搜好写。2020ICPC上海 C. Sum of Log
记搜模板
以求满足 存在 3 个连续数位单调递增的数字个数为例
ll dp[20][2][10][5];
ll dfs(int rem, int exist, int last, int inc) { // (剩余,存在满足要求的数,上一位数,当前递增个数)
ll& ans = dp[rem][exist][last][inc];
if (ans != -1) return ans;
if (!rem) return exist;
ans = 0;
for (int i = 0; i <= 9; i ++) {
int new_inc = (i > last) ? min(3, inc + 1) : 1;
ans += dfs(rem - 1, exist | (new_inc == 3), i, new_inc);
}
return ans;
}
ll solve(ll x) {
x ++; // ++ 之后方便处理
vector<int> d; // 存每一位
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d)); // 反转
int m = d.size();
ll ans = 0;
// 处理前导零
for (int i = 1; i <= m - 1; i ++) {
for (int j = 1; j <= 9; j++)
ans += dfs(i - 1, 0, j, 1); // 第 i 位确定,还剩 i - 1 位。
}
int rem = 0, exist = 0, inc = 0, last = 0; // 记录前面固定的数位的状态
for (int i = 0; i < m; i++) {
for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) { // 如果是最高位从 1 开始枚举
int new_inc = (j > last) ? min(3, inc + 1) : 1; // 更新下一次搜索的状态,和 dfs 里的其实很像
ans += dfs(m - i - 1, exist | (new_inc == 3), j, new_inc);
}
// 结束一轮枚举,更新固定数位的状态。
// 注意如果前面固定的数位已经不满足条件了,就需要 break;
inc = (d[i] > last) ? min(3, inc + 1) : 1;
last = d[i];
exist |= (inc == 3);
}
return ans;
}
计数
排列计数
- lucas定理 + 二叉树小根堆应用。P2606 [ZJOI2010]排列计数
习题
dls动态规划中级课
数数3
- 求满足 存在 3 个连续数位单调递增的数字个数
ll dp[20][2][10][5];
ll dfs(int rem, int exist, int last, int inc) {
ll& ans = dp[rem][exist][last][inc];
if (ans != -1) return ans;
if (!rem) return exist;
ans = 0;
for (int i = 0; i <= 9; i ++) {
int new_inc = (i > last) ? min(3, inc + 1) : 1;
ans += dfs(rem - 1, exist | (new_inc == 3), i, new_inc);
}
return ans;
}
ll solve(ll x) {
x ++;
vector<int> d;
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
int m = d.size();
ll ans = 0;
// 处理前导零
for (int i = 1; i <= m - 1; i ++) {
for (int j = 1; j <= 9; j++)
ans += dfs(i - 1, 0, j, 1);
}
int rem = 0, exist = 0, inc = 0, last = 0;
for (int i = 0; i < m; i++) {
for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) {
int new_inc = (j > last) ? min(3, inc + 1) : 1;
ans += dfs(m - i - 1, exist | (new_inc == 3), j, new_inc);
}
inc = (d[i] > last) ? min(3, inc + 1) : 1;
last = d[i];
exist |= (inc == 3);
}
return ans;
}
CF1560F2. Nearest Beautiful Number (hard version)
- 定义 k-美丽数 为数位中不同数位最多为 \(k\) 的数字。
- 求大于等于 \(n\) 的最小 k-美丽数。
这类问题核心:快速判断剩下的位是否能满足要求(new_cnt <= k
)
vector<int> d;
int n, k;
int vis[11]; // 多组数据清空 vis[],直接返回dfs不一定回溯
bool dfs(int x, bool larger, int cnt, int num) {
if (x == (int)d.size()) {
printf("%d\n", num);
return true;
}
else {
for (int i = larger ? 0 : d[x]; i <= 9; i ++) {
vis[i]++;
int new_cnt = cnt;
if (vis[i] == 1) new_cnt++;
if (new_cnt <= k && dfs(x + 1, larger | (i > d[x]), new_cnt, num * 10 + i))
return true;
vis[i]--;
}
}
return false;
}
void solve(int x) {
d.clear();
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
dfs(0, 0, 0, 0);
}
不同位大于等于 \(k\) 最小数字求法
这里的核心代码就是 new_cnt + (int)d.size() - x - 1 >= k
,还要减 1 是因为当前位的贡献已经计算在了 new_cnt 里面。
vector<int> d;
int n, k;
int vis[11];
bool dfs(int x, bool larger, int cnt, int num) {
if (x == (int)d.size()) {
printf("%d\n", num);
return true;
}
else {
for (int i = larger ? 0 : d[x]; i <= 9; i ++) {
vis[i]++;
int new_cnt = cnt;
if (vis[i] == 1) new_cnt++;
if (new_cnt + (int)d.size() - x - 1 >= k && dfs(x + 1, larger | (i > d[x]), new_cnt, num * 10 + i))
return true;
vis[i]--;
}
}
return false;
}
void solve(int x) {
d.clear();
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
while (1) {
if (dfs(0, 0, 0, 0)) break; // 当前位数搜不到就要扩大位数,否则 break
int m = d.size() + 1;
d.clear();
d.push_back(1); // 原来有 m 位,扩大为 m + 1 位,那么从最小的 1000000开始,有 m 个 0
for (int i = 0; i < m - 1; i ++) d.push_back(0);
}
}
数位背包入门——梦幻岛宝珠
思路
- 形如背包重量为 \(a * 2^x\) 的题目,可以用数位背包的方法来尝试解题
- 由高到低位考虑,\(f[i][j]\) 表示到第 \(i\) 为还剩 \(j\) 个 \(2^i\) 容量的最大价值。
- 由于重量的系数 和 物品个数限制,考虑到第 \(i\) 位时,后面的所有位加起来贡献到第 \(i\) 位的重量是有限的,所以直接和物品重量总数取
min
- 然后注意细节和转移,由于状态定义为还剩xxx时的最大价值,所以要用 剩的多反而背包价值更大 去更新剩的少的价值,代码中是后缀 max
const int N = 2010;
ll f[N], g[N];
int n, W;
// f[i][j] 到第 i 位,还剩 j 个容量的最大价值
vector<PII> item[35];
void solve() {
for (int i = 0; i <= 31; i ++) item[i].clear();
int sum = 0; // 重量总数
for (int i = 0; i < n; i ++) {
int w, v;
re(w), re(v);
int lev = __builtin_ctz(w);
w >>= lev;
item[lev].pb({w, v});
sum += w;
}
for (int i = 0; i <= sum; i++) f[i] = -2e18;
f[0] = 0;
for (int i = 30; i >=0 ; i--) {
for (int j = 0; j <= sum; j++) g[j] = f[j], f[j] = -2e18; // 滚动数组
int d = W >> i & 1;
for (int j = 0; j <= sum; j ++) { // 不选物品,i->i+1 背包状态转移,容量 * 2 + d
f[min(sum, 2 * j + d)] = max(f[min(sum, 2 * j + d)], g[j]);
}
// 选物品,背包转移, f[i][j] = max(f[i][j], f[i - 1][j + w] + v);
// w <= j <= sum, 所以需要记录 f[i][j] 为后缀 f[][] 最大值, 因为剩的更多价值更大,剩的少的价值应该被更新掉。
for (int j = sum - 1; j >= 0; j--)
f[j] = max(f[j], f[j + 1]);
for (auto p: item[i]) {
for (int j = 0; j <= sum - p.first; j++) {
f[j] = max(f[j], f[j + p.first] + p.second);
}
}
}
printf("%lld\n", f[0]);
}
2020-CCPC长春-D, Meaningless Sequence
- 有个小结论然后无脑做,有个二项式定理,\((1+c)^k = \Sigma_i^k(c^i * \binom{k}{i})\)
const int N = 3010, mod = 1e9 + 7;
ll power[N];
int main() {
// freopen("input.txt", "r", stdin);
// freopen("output.txt", "w", stdout);
string s;
cin >> s;
int c, n = s.size();
re(c);
power[0] = 1;
for (int i = 1; i <= N - 10; i++)
power[i] = power[i - 1] * (c + 1) % mod;
ll pre = 1, ans = 0;
for (int i = 0; i < n; i ++) {
if (s[i] == '0') continue;
ans = (ans + pre * power[n - i - 1] % mod) % mod;
pre = pre * c % mod;
}
ans += pre;
printf("%lld\n", ans % mod);
// fclose(stdin);
// fclose(stdout);
return 0;
}
2020-ICPC济南-L. Bit Sequence
思路
- 从二进制高位向低位进行 dp,因为 \(m\) 较小,只会影响低 7 位。
- 考虑搜索到第 8 位时,最终的结果只和 3 个东西有关。
- 低 7 位的状态
- 连着第 8 位的连续 1 的个数
- 第 8 位以后的 1 的奇偶性。
- 预处理一下低 7 位对应有效状态个数,小于等于 7 位搜索时,可以直接暴力做。
- 放上自己写的屎山代码和网上题解代码。
#include<bits/stdc++.h>
#define pb push_back
#define ALL(x) x.begin(), x.end()
#define popcount(x) (__builtin_popcountll(x) % 2)
#define SZ(x) ((int)x.size())
using namespace std;
typedef long long ll;
const int N = 110;
int a[N], m;
ll L;
int A, B, C, D, E, F, G, H; // E all yes, F all no
int _popcount(int x) {
int cnt = 0;
for (int i = 7; i >= 0; i--) {
cnt += x >> i & 1;
}
cnt &= 1;
return cnt;
}
void init() {
int lim = 255;
A = B = C = D = E = F = G = H = 0;
for (int i = 0; i <= lim; i++) {
bool same = _popcount(i) == a[0];
bool ok = 1;
for (int j = 0; j < m; j++) {
if ((a[j] == _popcount(i + j)) != same) ok = 0;
}
if (ok)
same ? G++ : H++;
}
for (int i = lim - m + 2; i <= lim; i++) {
bool same = popcount(i) == a[0], changed = 0;
bool ok = 1;
for (int j = 1; j < m && ok; j++) {
int t = _popcount(i + j);
if (t == a[j] && !same) {
if (!changed) {
if (i + j != lim + 1) ok = 0;
changed = 1, same ^= 1;
}
else ok = false;
}
else if (t != a[j] && same) {
if (!changed) {
if (i + j != lim + 1) ok = 0;
changed = 1, same ^= 1;
}
else ok = false;
}
}
if (ok && changed) {
if (popcount(i) == a[0]) A ++;
else B++;
}
}
for (int i = 0; i < lim - m + 2; i++) {
bool ok = 1, same = popcount(i) == a[0];
for (int j = 1; j < m && ok; j++) {
if ((popcount(i + j) == a[j]) != same) ok = 0;
}
if (ok)
same ? C ++ : D ++;
}
for (int i = 0; i <= 255; i++) {
bool ok = 1;
for (int j = 0; j < m && ok; j++) {
int x = i + j;
if (popcount(x) != a[j]) ok = 0;
}
E += ok;
}
for (int i = 0; i <= 255; i++) {
bool ok = 1;
for (int j = 0; j < m && ok; j++) {
int x = i + j;
if (popcount(x) == a[j]) ok = 0;
}
F += ok;
}
}
ll dp[63][2][63];
ll dfs(int rem, bool cnt, int inc) {
ll & ans = dp[rem][cnt][inc];
if (ans != -1) return ans;
ans = 0;
if (rem != 9) {
ans += dfs(rem - 1, cnt, 0);
ans += dfs(rem - 1, cnt ^ 1, inc + 1);
}
else {
// fill 1
bool new_cnt = cnt ^ 1;
int new_inc = inc + 1;
if (new_inc & 1) {
if (!new_cnt) ans += G;
else ans += H;
}
else if (new_inc % 2 == 0) {
if (!new_cnt) ans += A + C;
else ans += B + D;
}
// fill 0
if (!cnt)
ans += E;
else
ans += F;
}
return ans;
}
int DFS(int rem, ll val) {
if (!rem) {
for (int i = 0; i < m; i++) {
ll x = val + i;
if (popcount(x) != a[i]) return 0;
}
return 1;
}
int ans = 0;
ans += DFS(rem - 1, val);
ans += DFS(rem - 1, val + (1ll << (rem - 1)));
return ans;
}
ll solve() {
vector<int> d;
int head = 0;
for (int i = M; i >= 0; i--) {
if (L >> i & 1) {
head = i;
break;
}
}
for (int i = head; i >= 0; i--) {
d.pb(L >> i & 1);
}
int len = SZ(d);
ll ans = 0;
ll tot = 0;
for (int i = 1; i < len; i++) {
if (i <= 9)
ans += DFS(i - 1, 1ll << (i - 1));
else
ans += dfs(i - 1, 1, 1);
}
bool cnt = 0;
int inc = 0;
ll val = 0;
for (int i = 0; i < len; i++) {
for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
int rem = len - i - 1;
if (rem >= 9) {
ans += dfs(rem, cnt, 0);
}
else if (rem == 8) {
// fill 0
if (!cnt)
ans += E;
else
ans += F;
}
else {
ans += DFS(rem, val);
}
}
cnt ^= d[i];
d[i] ? inc ++ : inc = 0;
val |= ((1ll * d[i]) << (len - i - 1));
}
bool ok = 1;
for (int i = 0; i < m; i++) {
if (popcount(L + i) != a[i]) ok = 0;
}
ans += ok;
if (L) {
ok = 1;
for (int i = 0; i < m; i++) {
if (popcount(i) != a[i]) ok = 0;
}
ans += ok;
}
return ans;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(dp, -1, sizeof dp);
scanf("%d%lld", &m, &L);
for (int i = 0; i < m; i++)
scanf("%d", &a[i]);
init();
printf("%lld\n", solve());
}
return 0;
}
洛谷——「能力综合提升题单-数位计数DP」, Codeforces
P4999 烦人的数学作业
求 \([L,R]\) 数字的数位和
- 数位dp模板题
const int mod = 1e9 + 7;
int dp[20][200]; // 不能是 dp[rem][last]
int dfs(int rem, int sum) {
if (!rem) return sum;
if (dp[rem][sum] != -1) {
return dp[rem][sum];
}
int& ans = dp[rem][sum];
ans = 0;
for (int i = 0; i <= 9; i++)
ans = (ans + dfs(rem - 1, sum + i)) % mod;
return ans;
}
ll solve(ll x) {
vector<int> d;
x++;
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
int m = d.size();
ll ans = 0;
for (int i = 0; i <= m - 2; i ++) {
for (int j = 1; j <= 9; j++)
ans = (ans + dfs(i, j)) % mod;
}
int sum = 0;
for (int i = 0; i < m; i++) {
for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) {
ans = (ans + dfs(m - i - 1, sum + j)) % mod;
}
sum += d[i];
}
return ans;
}
int main() {
int T;
re(T);
memset(dp, -1, sizeof dp);
while (T--) {
ll l, r;
re(l), re(r);
printf("%lld\n", (solve(r) - solve(l - 1) + mod) % mod);
}
return 0;
}
P2606 [ZJOI2010]排列计数
int main() {
int n;
re(n), re(p);
get_fact();
for (int i = 1; i <= n; i++) s[i] = 1;
for (int i = n; i >= 2; i--) s[i >> 1] += s[i];
// C_{i - 1}^l * f[l] * f[r];
for (int i = 1; i <= n * 2 + 1; i++) dp[i] = 1;
for (int i = n; i >= 1; i--) {
dp[i] = lucas(s[i] - 1, s[2 * i], p) * dp[2 * i] % p * dp[2 * i + 1] % p;
}
printf("%lld\n", dp[1]);
return 0;
}
P2657 [SCOI2009] windy 数
- 搜索状态:\(dp[rem][last]\) ,剩余多少位以及上一位是多少
int dp[12][10];
int dfs(int rem, int last) {
int& ans = dp[rem][last];
if (rem == 0) return ans = 1;
if (ans != -1) return ans;
ans = 0;
for (int i = 0; i <= 9; i++) {
if (abs(i - last) < 2) continue;
ans += dfs(rem - 1, i);
}
return ans;
}
int solve(int x) {
x ++;
vector<int> d;
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
int m = d.size(), ans = 0;
for (int i = 1; i <= m - 1; i++) {
for (int j = 1; j <= 9; j++)
ans += dfs(i - 1, j);
}
int last = -2;
for (int i = 0; i < m; i++) {
for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) {
if (abs(j - last) < 2) continue;
ans += dfs(m - i - 1, j);
}
if (abs(d[i] - last) < 2) break; // 已经不合法就不能再搜了
last = d[i];
}
return ans;
}
P4124 [CQOI2016]手机号码
出现连续 \(3\) 个相同数且不同时出现 \(4,8\) 的 11 位数。
// rem, last, exist, state inc
ll dp[13][11][2][5][5];
int cal(int num, int s) {
if (num == 4) s |= 1;
else if (num == 8) s |= 2;
return s;
}
ll dfs(int rem, int last, bool exist, int state, int inc) {
if (!rem) return exist;
ll& ans = dp[rem][last][exist][state][inc];
if (ans != -1) return ans;
ans = 0;
if (rem + inc < 3 && !exist) return ans;
for (int i = 0; i <= 9; i++) {
int new_state = cal(i, state);
int new_inc = i == last ? min(3, inc + 1) : 1;
int new_exist = exist | (new_inc == 3);
if (new_state == 3) continue;
ans += dfs(rem - 1, i, new_exist, new_state, new_inc);
}
return ans ;
}
ll solve(ll x) {
x++;
vector<int> d;
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
int m = d.size();
ll ans = 0;
int last = -1, exist = 0, state = 0, inc = 0;
for (int i = 0; i < m; i++) {
if (state == 3) break;
for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
int new_state = cal(j, state);
if (new_state == 3) continue;
int new_inc = (j == last ? min(3, inc + 1) : 1);
int new_exist = exist | (new_inc == 3);
ans += dfs(m - i - 1, j, new_exist, new_state, new_inc);
}
inc = d[i] == last ? min(inc + 1, 3) : 1;
last = d[i];
state = cal(last, state);
exist |= (inc == 3);
}
return ans;
}
int main() {
ll l, r;
re(l), re(r);
memset(dp, -1, sizeof dp);
printf("%lld\n", solve(r) - solve(l - 1));
return 0;
}
CF628D Magic Numbers
int d, M;
// rem, R,
ll dp[2010][2010];
string l, r;
const int mod = 1e9 + 7;
ll dfs(int rem, int R, bool is_even) {
ll& ans = dp[rem][R];
if (ans != -1) return ans;
if (!rem) return ans = R == 0;
ans = 0;
is_even ^= 1;
for (int i = 0; i <= 9; i++) {
if (is_even && i != d) continue;
if (!is_even && i == d) continue;
int new_R = (R * 10 + i) % M;
ans += dfs(rem - 1, new_R, is_even);
ans %= mod;
}
return ans;
}
bool check(string s) {
int t = 0;
for (int i = 0; i < SZ(s); i++) {
if (i & 1 && (s[i] - '0') != d) return 0;
if (i % 2 == 0 && (s[i] - '0') == d) return 0;
t = (t * 10 + (s[i] - '0')) % M;
}
return t == 0;
}
ll solve(string s) {
vector<int> v;
for (int i = 0; i < SZ(s); i++) v.pb(s[i] - '0');
int m = v.size();
ll ans = 0;
int R = 0;
for (int i = 0; i < m; i++) {
for (int j = (i == 0 ? 1 : 0); j < v[i]; j++) {
int new_R = (R * 10 + j) % M;
if ((i % 2 == 0 && (j == d)) || (i & 1 && j != d)) continue;
ans += dfs(m - i - 1, new_R, i & 1);
ans %= mod;
}
if ((i % 2 == 0 && (v[i] == d)) || (i & 1 && v[i] != d)) break;
R = (R * 10 + v[i]) % M;
}
if (check(s)) ans++;
return ans;
}
int main() {
IOS;
string l, r;
memset(dp, -1, sizeof dp);
cin >> M >> d;
cin >> l >> r;
ll ans = (solve(r) - solve(l) + check(l) + mod) % mod;
cout << ans << endl;
return 0;
}
P3413 SAC#1 - 萌数
回文的存在条件:存在某一位和上一位或上两位相同
const int mod = 1e9 + 7;
// rem, last1, last2, exist
ll dp[1010][13][13][2];
bool check(string s) {
int last1 = 11, last2 = 11;
for (int i = 0; i < SZ(s); i++) {
int n = s[i] - '0';
if (n == last1 || n == last2) return 1;
last2 = last1, last1 = n;
}
return 0;
}
ll dfs(int rem, int last1, int last2, bool exist) {
ll& ans = dp[rem][last1][last2][exist];
if (ans != -1) return ans;
if (!rem) return ans = exist;
ans = 0;
for (int i = 0; i <= 9; i++) {
bool new_exist = exist | (i == last1 || i == last2);
ans += dfs(rem - 1, i, last1, new_exist);
ans %= mod;
}
return ans;
}
ll solve(string s) {
vector<int> d;
for (int i = 0; i < SZ(s); i++)
d.pb(s[i] - '0');
int m = SZ(d);
ll ans = 0;
for (int i = 1; i < m; i++) {
for (int j = 1; j <= 9; j++) {
ans += dfs(i - 1, j, 11, 0);
ans %= mod;
}
}
int last1 = 11, last2 = 11;
bool exist = 0;
for (int i = 0; i < m; i++) {
for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
bool new_exist = exist | (j == last1 || j == last2);
ans += dfs(m - i - 1, j, last1, new_exist);
ans %= mod;
}
exist = exist | (d[i] == last1 || d[i] == last2);
last2 = last1;
last1 = d[i];
}
ans += check(s);
ans %= mod;
return ans;
}
int main() {
// IOS;
string l, r;
cin >> l >> r;
memset(dp, -1, sizeof dp);
ll ans = solve(r) - solve(l) + check(l);
ans = (ans + mod) % mod;
cout << ans << endl;
return 0;
}
P4127 [AHOI2009]同类分布
求出 \([a,b]\) 中各位数字之和能整除原数的数的个数。
- 因为原数是不确定的,但原数和有范围 \(\leq 9\times len\),最多 \(180\) 。
- 所以枚举所有可能的原数和(模数),复杂度是够的。
const int M = 200;
// rem, R,
int mod;
ll dp[20][M + 2][M + 2];
vector<int> d;
ll dfs(int rem, int sum, int R) {
ll& ans = dp[rem][sum][R];
if (ans != -1) return ans;
if (!rem) return ans = (sum == mod && (R == 0));
ans = 0;
for (int i = 0; i <= 9; i++) {
int new_sum = sum + i;
int new_R = (R * 10 + i) % mod;
if (new_sum > mod) break;
ans += dfs(rem - 1, new_sum, new_R);
}
return ans;
}
ll solve2() {
ll ans = 0;
int m = d.size();
for (int i = 1; i < m; i++) {
for (int j = 1; j <= min(9, mod); j++) {
ans += dfs(i - 1, j, j % mod);
}
}
int sum = 0, R = 0;
ll val = 0;
for (int i = 0; i < m; i++) {
for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
int new_R = (R * 10 + j) % mod;
ans += dfs(m - i - 1, sum + j, new_R);
}
sum += d[i];
R = (R * 10 + d[i]) % mod;
if (sum > mod) break;
}
return ans;
}
ll solve1(ll x) {
d.clear();
x ++;
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
ll ans = 0;
for (mod = 1; mod <= SZ(d) * 9; mod ++) {
memset(dp, -1, sizeof dp);
ans += solve2();
}
return ans;
}
int main() {
ll l, r;
cin >> l >> r;
printf("%lld\n", solve1(r) - solve1(l - 1));
return 0;
}
CF1073E. Segment Sum
给定 \(K(K\leq 10),L,R\) ,求 \([L,R]\) 之间最多不包含超过 \(K\) 个数码的数的和。
思路
- 考虑按位贡献来计算。
- 设状态为 \(dp[rem][state]\) ,剩余 rem 位,已填数数码状态为 state 的和。
- 由于后续还可以对应多个数,所以还需要一个数组记录剩余 rem 位,已填数码状态为 state 的数字个数,故以下转移:
- \(dp_{rem,state} = \sum_j dp_{rem-1,new_state} + g_{rem-1,new_state} \times j \times 10^{rem-1}\)
- 实现细节:在最后一个 for 循环时,由于原位的数字并没有被计入贡献,所以需要记录每个数位后续有多少个数,因为是从高位到低位循环,而个数是从低位开始算,所以需要再循环一次。
const int mod = 998244353;
int K;
// rem, state;
ll dp[20][1030], g[20][1030];
ll mi[22], ki[20];
void add(ll& a, ll b) {
b %= mod;
a += b;
if (a >= mod) a -= mod;
}
ll dfs(int rem, int state) {
ll& ans = g[rem][state];
if (ans != -1) return ans;
if (!rem) {
dp[rem][state] = 0;
return ans = __builtin_popcount(state) <= K;
}
ans = 0;
dp[rem][state] = 0;
for (int i = 0; i <= 9; i++) {
int new_state = state | (1 << i);
dfs(rem - 1, new_state);
add(ans, g[rem - 1][new_state]);
add(dp[rem][state], dp[rem - 1][new_state] + mi[rem - 1] * i % mod * g[rem - 1][new_state] % mod);
}
return ans;
}
ll solve(ll x) {
ll t = x;
x ++;
vector<int> d;
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
int m = d.size();
ll ans = 0;
for (int i = 1; i < m; i++) {
for (int j = 1; j <= 9; j++) {
dfs(i - 1, 1 << j);
add(ans, dp[i - 1][1 << j] + mi[i - 1] * j % mod * g[i - 1][1 << j] % mod);
}
}
ll val = 0, state = 0;
vector<int> v;
ll sum = 0;
for (int i = 0; i < m; i++) {
ll g_cnt = 0;
for (int j = (i == 0 ? 1 : 0); j < d[i]; j ++) {
int new_state = state | (1 << j);
dfs(m - i - 1, new_state);
add(ans, dp[m - i - 1][new_state] + mi[m - i - 1] * j % mod * g[m - i - 1][new_state] % mod);
add(g_cnt, g[m - i - 1][new_state]);
}
state |= 1 << d[i];
if (i)
v.pb(g_cnt), add(sum, g_cnt);
}
for (int i = 0; i < SZ(v); i++) {
add(ans, d[i] * mi[m - i - 1] % mod * sum % mod);
sum = (sum - v[i] + mod) % mod;
}
return ans;
}
int main() {
ll l, r;
re(l), re(r);
re(K);
mi[0] = 1, ki[0] = 1;
for (int i = 1; i <= 19; i++) {
mi[i] = mi[i - 1] * 10 % mod;
}
memset(dp, -1, sizeof dp);
memset(g, -1, sizeof g);
printf("%lld\n", (solve(r) - solve(l - 1) + mod) % mod);
return 0;
}
CF55D Beautiful numbers
一个数字 \(x\) 是美丽的,当且仅当 \(x\in\mathbb{Z^+}\) 并且对于 \(x\) 的每一个非零位上的数 \(y\),都有 \(y|x\)。
思路
- 找出 1-9 的最大公约数,最大是 \(2520\) 。
- 套路式设计状态 \(dp[rem][R]\) 剩余 rem 位,模最大公约数的余数是 R。
- 发现没法计算,到了最终的时候我们需要 所有数码的最大公约数 能整除 数值,这里与 R 等价,故加入状态中。
- 直接开三维开不下,压缩最大公约数的因子个数,总共 \(48\) 个。
const int M = 2520;
ll dp[20][M + 1][50];
vector<int> alls;
int find(int x) {
return lower_bound(ALL(alls), x) - alls.begin();
}
ll gcd(ll a, ll b) {
return b ? gcd(b, a % b) : a;
}
ll _lcm(ll a, ll b) {
return a / gcd(a, b) * b;
}
ll dfs(int rem, int R, int idx) {
ll& ans = dp[rem][R][idx];
if (ans != -1) return ans;
if (!rem) return ans = R % alls[idx] == 0;
ans = 0;
for (int i = 0; i <= 9; i++) {
int new_R = (R * 10 + i) % M;
int new_idx = idx;
if (i)
new_idx = find(lcm(alls[idx], i));
ans += dfs(rem - 1, new_R, new_idx);
}
return ans;
}
ll solve(ll x) {
x ++;
vector<int> d;
while (x) {
d.pb(x % 10);
x /= 10;
}
reverse(ALL(d));
int m = d.size();
ll ans = 0;
for (int i = 1; i < m; i++) {
for (int j = 1; j <= 9; j++)
ans += dfs(i - 1, j, find(j));
}
int R = 0, idx = 0;
for (int i = 0; i < m; i++) {
for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
int new_R = (R * 10 + j) % M;
int new_idx = idx;
if (j)
new_idx = find(lcm(alls[idx], j));
ans += dfs(m - i - 1, new_R, new_idx);
}
R = (R * 10 + d[i]) % M;
if (d[i])
idx = find(lcm(alls[idx], d[i]));
}
return ans;
}
int main() {
int T;
re(T);
memset(dp, -1, sizeof dp);
for (int i = 0; i < (1 << 9); i++) {
int x = 1;
for (int j = 0; j < 9; j++) {
if (i >> j & 1) {
x = lcm(x, j + 1);
}
}
alls.pb(x);
}
sort(ALL(alls));
alls.erase(unique(ALL(alls)), alls.end());
while (T--) {
ll l, r;
re(l), re(r);
printf("%lld\n", solve(r) - solve(l - 1));
}
return 0;
}
CF855E. Salazar Slytherin's Locket
题意
求\(l...r\)之间转成\(b\)进制后,\(0,1,2...,b-2,b-1\)都出现偶数次的数的个数。
\(1\le q \le 10^5\)
\(2\le b \le 10\)
\(1\le l \le r \le 10^{18}\)
思路
- 压缩 B 进制下 B 个数的奇偶状态进行数位dp。
- 这个题需要将进制这一维压进去,否则每次 memset 会超时。
CF401D. Roman and Numbers
题意
将 \(n\,(n\leq 10^{18})\)的各位数字重新排列(不允许有前导零),求可以构造几个 \(\mod m\) 等于 \(0\) 的数字。
思路
- 读完题,一股浓烈的数位 DP 的味道。那么就开始非常套路地设计状态 \(dp_{rem,R,state}\) 表示剩余 \(rem\) 位,当前余数为 \(R\) ,剩余数的状态为 \(state\) 的合法数字个数。
- \(state\): 一个长度为 10 的 vector,下标 i 表示数字 i 剩余多少个数。
- 状态转移:枚举能转移到哪些数字 \(j\) ,则有
\[dp_{rem,R,state} = \sum_{j=0}^9 dp_{rem-1,new\_R, new\_state}
\]
- 解释转移变量:\(new\_R=(R\times10+j)\mod m\),\(new\_state_j = state_j-1\)。
- 然后就开始愉快的卡常数和复杂度,使用 map 会卡在 test 80,使用 unordered_map 并手写对 vector 的简单哈希函数可以通过本题 (3300ms~3800ms)。另外务必注意传参vector时加入引用。
区域赛真题
2020ICPC上海 C. Sum of Log
思路
- 先发现要求的式子,就是找 i, j 最高位的累加和。
- 然后可以进行数位DP,先把 i,j 填充位数一样方便搜索。
- 然后智慧数位dp。
#include<bits/stdc++.h>
#define pb push_back
#define ALL(x) x.begin(),x.end()
#define SZ(x) ((int)x.size())
typedef long long ll;
using namespace std;
const int mod = 1e9 + 7;
int X, Y;
ll dp[33][2][2][2];
vector<int> dx, dy;
const int M = 32;
ll dfs(int rem, bool lim1, bool lim2, bool top) {
ll& ans = dp[rem][lim1][lim2][top];
if (ans != -1) return ans;
ans = 0;
if (!rem) return ans = 1;
int up1 = lim1 ? dx[M - rem] : 1, up2 = lim2 ? dy[M - rem] : 1;
for (int i = 0; i <= up1; i++) {
for (int j = 0; j <= up2; j++) {
if (i & j) continue;
ll res = 1;
if (!top && (i || j)) res = rem;
ans += dfs(rem - 1, lim1 && i == up1, lim2 && j == up2, top || i || j) * res % mod; // 如果前面已经有 1 了,只需要数剩余位的个数,否则乘一个权值。
ans %= mod;
}
}
return ans;
}
ll solve() {
ll ans = 0;
dx.clear(), dy.clear();
for (int i = M - 1; i >= 0; i--) {
dx.pb(X >> i & 1), dy.pb(Y >> i & 1);
}
return dfs(M, 1, 1, 0);
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(dp, -1, sizeof dp);
scanf("%d%d", &X, &Y);
printf("%lld\n", (solve() - 1 + mod) % mod); // 计算了 0,0
}
return 0;
}