数位 DP

数位 dp 大多使用高位计算的时候使用低位计算后的结果,从而做到优化效率

1|0[ZJOI2010] 数字计数

1|1题目描述

给定两个正整数 ab,求在 [a,b] 中的所有整数中,每个数码各出现了多少次。

  • 保证 1ab1012

1|2求解策略

1|0第一种方法 - 递推法

定义 dpii 位数中,每种数字出现的次数,这里我们每种数字出现的次数都是相同的,随便用一个数字即可

  • 一位数 0 ~ 9,每种数字只有 dp1=1
  • 两位数 00 ~ 99,每种数字有 dp2=20 个,这里是 00 而不是 0,实际上是不合法的,但是先按照统一处理,到后面会减去
  • 三位数 000 ~ 999,每种数字有 dp3=300

那么 dp[] 有两种计算方法,分别是递推和数学组合

  • dpi = dpi110+10i1。以数字 2 为例,计算 dp2 的时候,2 在个位上出现了 dpi110=dp110=10 次,即 2,12,22,...,92。那么十位出现了 10i1=10 次,即 21,22,23,...29。以此类推即可
  • dpi=i10i10,从 i0 递增到 i9,所有的字符共出现了 i10i 次,0 ~ 9 每个数字出现了 i10i10

那么考虑如何实现,我们以 0 ~ 324 为例,设 cnt 为答案,numii 位上的数字:

  • 对于普通情况,也就是符合 00 ~ 99 的情况,先拆分成 000 ~ 099100 ~ 199200 ~ 299,这些的后两位是符合普通情况的,我们直接使用 cnt0..9=dpi1num[i] 统计
  • 对于最高位,我们需要特殊判断,首先看 0 ~ numi1 的这些数字,他们都对应着所有的 00 ~ 9910i1 个数字,所以对于这些最高位,都有 cnt0..numi1+=10i1。对于 3 来说,对应的只有 00 ~ 2425 个,特殊处理即可,最后我们要把最高位为 0 的处理掉,也就是 cnt0=10i1
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 15, mod = 1e9 + 7; int dp[N], ten[N], num[N]; void init(){ ten[0] = 1; for(int i = 1; i <= N; i++){ dp[i] = i * ten[i - 1]; ten[i] = ten[i - 1] * 10; } } vector<int> make(int x){ int len = 0; while(x){ num[++len] = x % 10, x /= 10; } vector<int> cnt(N); for(int i = len; i >= 1; i--){ for(int j = 0; j <= 9; j++){ cnt[j] += num[i] * dp[i - 1]; } for(int j = 0; j < num[i]; j++){ cnt[j] += ten[i - 1]; } int num2 = 0; for(int j = i - 1; j >= 1; j--) num2 = num2 * 10 + num[j]; cnt[num[i]] += num2 + 1; cnt[0] -= ten[i - 1]; } return cnt; } signed main() { std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); int a, b; cin >> a >> b; init(); vector<int> ans1 = make(a - 1), ans2 = make(b); for(int i = 0; i <= 9; i++){ cout << ans2[i] - ans1[i] << ' '; } return 0; }

1|0第二种方法 - 记忆化搜索

定义 dppos,sum,lead,limit

  • dppos,sum 表示最后 pos 位的范围是 00..0 ~ 99..9,前面 2 的个数为 sum2 的总个数,例如 dp1,3 表示区间 2220 ~ 22292 的个数为 31
  • lead 表示是否有前导零
  • limit 表示是否有最高位限制,即若计算最高位 3 开始的数字,有数位限制
#include <bits/stdc++.h> #define int long long using namespace std; const int N = 15, mod = 1e9 + 7; int dp[N][N][2][2]; int now, num[N]; int dfs(int pos, int sum, int lead, int limit){ int ans = 0; if(pos == 0) return sum; if(dp[pos][sum][lead][limit] != -1) return dp[pos][sum][lead][limit]; int to = limit ? num[pos] : 9; for(int i = 0; i <= to; i++){ if(i == 0 && lead) ans += dfs(pos - 1, sum, true, limit && i == to); else if(i == now) ans += dfs(pos - 1, sum + 1, false, limit && i == to); else if(i != now) ans += dfs(pos - 1, sum, false, limit && i == to); } dp[pos][sum][lead][limit] = ans; return ans; } int solve(int x){ int len = 0; while(x){ num[++len] = x % 10; x /= 10; } memset(dp, -1, sizeof dp); return dfs(len, 0, true, true); } signed main() { std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); int a, b; cin >> a >> b; for(int i = 0; i <= 9; i++){ now = i; cout << solve(b) - solve(a - 1) << ' '; } return 0; }

2|0求出n之前的所有数满足位数和整除当前数

见题:E - Digit Sum Divisible (atcoder.jp)

  P4127 [AHOI2009] 同类分布 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

考虑数位动规,设方程 dp[i][j][k][l] 为状态:
i:搜到了第 i 位(倒着枚举,也就是指 i 到最高位都填完了)。

j: 表示当前的数位总和

k: 表示当前的数位总和模上我们枚举的数位和

l: 是否 i 前面的位(包括 i)都填满了。这里的填满指填的与原数 n 相同。例如 114514 就是在 n=119198 时第五到最高位的填满。

那么状态转移方程就是:对于l=0,也就是当前位前面的没有被填满,那么我们在这个位置可以枚举到9

fi,0,k,l=t=09fi1,j+t,(10k+t)modm,0。表示枚举第 i 位填的数为 t。那么因为前面存在某一位没填满,那么后面的位 09 都是可以填的。因此 t 的范围为 [0,9]

fi,1,k,l=t=0pfi1,j+t,(10k+t)modm,[t==p],其中 p 表示给定的 n 的第 i 位,[t=p] 表示 t 是否等于 p(真为 1,假为 0)。表示枚举的第 i 位为 t。那么因为前面每一位都填满了,那么这一位肯定不能超过 n 原来的这一位,所以枚举到 p.

 

#include <bits/stdc++.h> #define int long long using namespace std; const int N = 1e6 + 10, mod = 1e9 + 7; int dp[20][300][300][2], dep[20]; int dfs(int u, int s, int x, int k, int lim) { if (u == 0) return s == k && x == 0; if (dp[u][s][x][lim] != -1) return dp[u][s][x][lim]; int ed = lim ? dep[u] : 9; dp[u][s][x][lim] = 0; for (int i = 0; i <= ed; i++) dp[u][s][x][lim] += dfs(u - 1, s + i, (x * 10 + i) % k, k, lim && (i == ed)); return dp[u][s][x][lim]; } signed main() { std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); int a, b; cin >> a >> b; int len = 0; a--; while (a) dep[++len] = a % 10, a /= 10; int res = 0; for (int i = 1; i <= 9 * 18; i++) { memset(dp, -1, sizeof dp); res += dfs(len, 0, 0, i, 1); } len = 0; while (b) dep[++len] = b % 10, b /= 10; int ans = 0; for (int i = 1; i <= 9 * 18; i++) { memset(dp, -1, sizeof dp); ans += dfs(len, 0, 0, i, 1); } cout << ans - res; return 0; }

3|0[SCOI2009] windy 数

题目描述

不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy 想知道,在 ab 之间,包括 ab ,总共有多少个 windy 数?

对于全部的测试点,保证 1ab2×109

思路: 还是用计数类似统计,高位的可以通过低位计算过的结果来计算。定义 dpposlast 为当前长度为 pos,上一位是 last,那么显然的: dp1,0=9 代表区间 [00,09],此时有 9 个符合的数, dp1,2 = 7 代表区间 [20,29] 中有 7 个数字符合,dp2,1=50 代表区间 [100199] 中有 50 个符合的数...
代码:

#include <bits/stdc++.h> #define int long long using namespace std; const int N = 15, mod = 1e9 + 7; int dp[N][N][2][2]; int num[N], now; int dfs(int pos, int last, int lead, int limit){ if(pos == 0) return 1; if(last>=0 && dp[pos][last][lead][limit] != -1) return dp[pos][last][lead][limit]; int ans = 0, to = limit ? num[pos] : 9; for(int i = 0; i <= to; i++){ if(abs(i - last) < 2) continue; if(i == 0 && lead) ans += dfs(pos - 1, -2, true, limit && i == to); // 前导0的存在导致后面任意一个数都符合题意 else if(abs(i - last) >= 2) ans += dfs(pos - 1, i, false, limit && i == to); } dp[pos][last][lead][limit] = ans; return ans; } int solve(int x){ int len = 0; while(x){ num[++len] = x % 10; x /= 10; } memset(dp, -1, sizeof dp); return dfs(len, -2, true, true); } signed main() { std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); int a, b; cin >> a >> b; cout << solve(b) - solve(a - 1); return 0; }

__EOF__

本文作者Sakurajimamai
本文链接https://www.cnblogs.com/o-Sakurajimamai-o/p/18313002.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   o-Sakurajimamai-o  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
历史上的今天:
2023-07-20 树状数组
2023-07-20 #885
-- --
点击右上角即可分享
微信分享提示