数位DP
本节比较难理解,解析会比较繁琐一些,需要大家耐心的看一看,抱歉啦~
这类题的解题思路比较统一,有一个大致的思路,就是设置一个函数,传入参数x,返回的是[0, x]符合题意所有情况的结果个数;函数内的操作需要根据题意进行分析,但是也有一定的模板可以套。
函数内的操作有一套标准的体系,下面是详细的图例解析:
模板代码:
int dp(int x) {
if (!x) return 0; // 有的题目0也符合题意
vector<int> nums;
while (x) nums.push_back(x % 10), x /= 10; // 并不一定所有题目都是求十进制每位上的数字
int res = 0; // 存储最终结果
int last = 0; // 一般情况是存储上一位数字
for (int i = nums.size(); i >= 0; i--) {
int x = nums[i]; // 存储从最高到最低位遍历到的数字
... // 有关题意得操作
if (!i) res++; // 如果到最后一位特判,当然if中也可能会有其他条件
}
return res;
}
注:我没有写求组合数的初始化函数模板,是因为这个在不同题目中设定都不一样,不太好统一。
接下来我们通过几道例题来更清楚地理解这些模板。
1081.度的数量
题目解析:这一题中和我们刚刚所说的每一位这个概念有些细微的差别,刚刚是十进制的,这里是b进制的每一位;经过我们刚刚模板的分析,此时我们只需要将注意力放在“有关题意操作”这方面就可以了,也就是对每一位上的b进制数x做文章即可。
开始分析之前先要明白题目要求我们计算的是什么?分析可得,是x在b进制下允许有多少个1?
每一位上的数字遍历的时候都设为x,x在b进制中分三种情况:
1.x > 1 则表示这个位置既可以放1又可以放0;因此看看k - last > 0 ? 放1 : 不放。
注:这里很重要,很多题解没有讲清楚为什么0和1都行?因为我们函数结果返回的是[0, x]中有多少个符合题意的结果,而并非只考虑x一个数,比它小的结果也要加上!
2.x = 1 则last++即可。
3.x = 0 上面解析图中我们左分支里0的情况,使用组合数即可。
代码 + 注释:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 35;
int n, m, k, b;
int f[N][N]; // f[i][j] 表示在[0, i]中放j个1的方案数
void init() { // 初始化所有组合数
for (int i = 0; i < N; i++) {
for (int j = 0; j <= i; j++) {
if (!j) f[i][j] = 1;
else f[i][j] = f[i - 1][j - 1] + f[i - 1][j];
}
}
}
int dp(int x) {
if (!x) return 0; // 本题中0不符合情况
vector<int> nums;
while (x) nums.push_back(x % b), x /= b;
int res = 0, last = 0; // last表示已经放了1的个数
for (int i = nums.size() - 1; i >= 0; i--) {
int x = nums[i];
if (x) { // x > 0 才能有分直的情况,不然只能是0
res += f[i][k - last]; // x = 0的情况:第i位是0,在[0, i-1]中选择k - last个1
if (x > 1) { // x > 1的情况:0 和 1 都行
if (k - last > 0) res += f[i][k - last - 1]; // 如果有剩余量供我们填1,就填上
break; // 填完就break,因为 x > 1 是肯定不符合题意的,因此循环无法进行下去了,到这里结束
}
else { // x = 1的情况
last++;
if (k - last < 0) break; // 如果 1 的数量超过题目所给的范围,则不合法,直接break
}
}
if (!i && k == last) res++;
}
return res;
}
int main() {
scanf("%d%d%d%d", &n, &m, &k, &b);
init();
printf("%d\n", dp(m) - dp(n - 1));
return 0;
}
1082.数字游戏
题目解析:按照给的模板写就行,注意遍历顺序是从最高位到最低位遍历(倒序遍历);上一题代码的last存储的是b进制中1的个数,而本题则存储的是上一位的数字,便于判断不降数;内部操作还是对每一位上的数字进行判断,一种是0~a-1、另一种是a,然后再分支......
代码 + 注释:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 15;
int n, m;
int f[N][N]; // f[i][j] 表示一共有i位、最高位是j且符合条件的数量
void init() {
for (int i = 0; i <= 9; i++) f[1][i] = 1;
for (int i = 2; i < N; i++) {
for (int j = 0; j <= 9; j++) {
for (int k = j; k <= 9; k++) {
f[i][j] += f[i - 1][k];
}
}
}
}
int dp(int n) {
if (!n) return 1;
vector<int> nums;
while (n) nums.push_back(n % 10), n /= 10;
int res = 0, last = 0; // last 表示上一位的数值
for (int i = nums.size() - 1; i >= 0; i--) {
int x = nums[i];
for (int j = last; j < x; j++) {
res += f[i + 1][j];
}
if (x < last) break; // 不符合不降数的题意
last = x;
if (!i) res++;
}
return res;
}
int main() {
init();
while (scanf("%d%d", &n, &m) != -1) {
printf("%d\n", dp(m) - dp(n - 1));
}
return 0;
}
1083.Windy数
题目解析:本题与之前不同的是有前导零的特殊情况需要特判。
代码 + 注释:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 20;
int n, m;
int f[N][N]; // f[i][j] 表示的是有i位数、最高位是j的数字的个数
void init() {
for (int i = 0; i < 10; i++) f[1][i] = 1;
for (int i = 2; i < N; i++) {
for (int j = 0; j < 10; j++) {
for (int k = 0; k < 10; k++) {
if (abs(k - j) >= 2) {
f[i][j] += f[i - 1][k];
}
}
}
}
}
int dp(int x) {
if (!x) return 0;
vector<int> nums;
while (x) nums.push_back(x % 10), x /= 10;
int res = 0, last = -2;
// 处理数字长度为nums.size()的
for (int i = nums.size() - 1; i >= 0; i--) {
int x = nums[i];
// 处理左分支: [0, x)
for (int j = i == nums.size() - 1; j < x; j++) { // 首位不能是0
if (abs(j - last) >= 2) res += f[i + 1][j];
}
// 处理右分支: x
if (abs(last - x) >= 2) last = x;
else break;
if (!i) res++; // 最右叶子节点的特判
}
// 处理长度小于nums.size()的
for (int i = 1; i < nums.size(); i++) { // 处理前导零问题,因为类似于000025这种也算是不降数
for (int j = 1; j < 10; j++) {
res += f[i][j];
}
}
return res;
}
int main() {
init();
scanf("%d%d", &n, &m);
printf("%d\n", dp(m) - dp(n - 1));
return 0;
}
1084.数字游戏II
题目解析:和数字游戏差不太多,但是有一点是需要说明的,就是下面代码中打上“***”的语句,详细说一下为什么要这样转换:
int sum; // 表示[nums.size() - 1, i + 1]所有位置上的数字之和
int j; // 就是代码中的j:nums[i]这个位置上的所有可能取值,遍历小于x的所有数字,对应模板中的左分支
int last; // 和代码中的一样:[nums.size() - 1, i]所有位置上的数字之和
根据题意我们能够得出下面的等式:
sum = last + j --- 1;
sum % mod == 0 --- 2;
由1, 2可推出(last + j) % mod == 0
又因为此时余数为0;因此前一层状态表示为最高位是j,余数为0 - last
代码 + 注释:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 35, M = 110;
int a, b, mod;
int f[N][10][M]; // f[i][j][k] 表示的是一共有i位、第i位是j且对mod取余结果为k的数字个数
int get(int x) { // 对负数取余的技巧
return ((x % mod) + mod) % mod;
}
void init() {
memset(f, 0, sizeof f);
for (int i = 0; i < 10; i++) f[1][i][i % mod]++;
for (int i = 2; i < N; i++) {
for (int j = 0; j < 10; j++) {
for (int k = 0; k < mod; k++) {
for (int q = 0; q < 10; q++) {
f[i][j][k] += f[i - 1][q][get(k - j)]; // ***
}
}
}
}
}
int dp(int n) {
if (!n) return 1;
vector<int> nums;
while (n) nums.push_back(n % 10), n /= 10;
int res = 0, last = 0; // last 表示[0, i]中所有数字的总和
for (int i = nums.size() - 1; i >= 0; i--) {
int x = nums[i];
// 左分支
for (int j = 0; j < x; j++) {
res += f[i + 1][j][get(-last)];
}
last += x;
// 右分支
if (!i && last % mod == 0) res++;
}
return res;
}
int main() {
while (scanf("%d%d%d", &a, &b, &mod) != -1) {
init();
printf("%d\n", dp(b) - dp(a - 1));
}
return 0;
}
1085.不要62
经过前面的训练,这题看起来就非常简单了,因此直接上代码吧 ^ ^
代码 + 注释:
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 10;
int n, m;
int f[N][10]; // f[i][j] 表示一共有i位、最高位是j且符合题意的数字
void init() {
for (int i = 0; i < 10; i++) {
if (i == 4) continue;
f[1][i] = 1;
}
for (int i = 2; i < N; i++) {
for (int j = 0; j < 10; j++) {
if (j != 4) {
for (int k = 0; k < 10; k++) {
if (k == 4 || (j == 6 && k == 2)) continue;
f[i][j] += f[i - 1][k];
}
}
}
}
}
int dp(int n) {
if (!n) return 1;
vector<int> nums;
while (n) nums.push_back(n % 10), n /= 10;
int res = 0, last = 0;
for (int i = nums.size() - 1; i >= 0; i--) {
int x = nums[i];
// 左分支
for (int j = 0; j < x; j++) {
if (j == 4 || (last == 6 && j == 2)) continue;
res += f[i + 1][j];
}
if (x == 4 || (last == 6 && x == 2)) break;
last = x;
// 右分支
if (!i) res++;
}
return res;
}
int main() {
init();
while (scanf("%d%d", &n, &m)) {
if (!n || !m) break;
printf("%d\n", dp(m) - dp(n - 1));
}
return 0;
}
1086.恨7不成妻
这个题贼难,思路绕的地方很多,后面我专门写一篇来讲吧,详细一些。
代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 55, P = 1e9 + 7;
int T;
LL n, m;
int power7[N], power9[N];
struct F {
// s0 = t
// s1 = A1 + A2 + A3 + ... + At
// s2 = A1² + A2² + ... + At²
int s0, s1, s2;
} f[N][10][7][7]; // f[i][j][a][b] 表示数字有i位、最高位是j、这个数字 % 7 == a、每一位数字和 % 7 == b的所有数字平方和
int mod(LL x, int y) { // 使得余数必大于0
return (x % y + y) % y;
}
void init() {
// 只有一位的数字
for (int i = 0; i < 10; i++) {
if (i == 7) continue;
auto &v = f[1][i][i % 7][i % 7];
v.s0++, v.s1 += i, v.s2 += i * i;
}
// 根据状态转移推导其它的结果
LL power = 10;
for (int i = 2; i < N; i++, power *= 10) {
for (int j = 0; j < 10; j++) {
if (j == 7) continue;
for (int a = 0; a < 7; a++) {
for (int b = 0; b < 7; b++) {
for (int k = 0; k < 10; k++) {
if (k == 7) continue;
auto &v0 = f[i][j][a][b], &v1 = f[i - 1][k][mod(a - j * power, 7)][mod(b - j, 7)];
v0.s0 = mod(v0.s0 + v1.s0, P);
v0.s1 = mod(v0.s1 + v1.s1 + j * (power % P) % P * v1.s0, P);
v0.s2 = mod(v0.s2 + j * j * (power % P) % P * (power % P) % P * v1.s0 + 2 * j * (power % P) % P * v1.s1 + v1.s2, P);
}
}
}
}
}
power7[0] = power9[0] = 1;
for (int i = 1; i < N; i++) {
power7[i] = 10 * power7[i - 1] % 7;
power9[i] = 10ll * power9[i - 1] % P;
}
}
F get(int i, int j, int a, int b)
{
int s0 = 0, s1 = 0, s2 = 0;
for (int x = 0; x < 7; x ++ )
for (int y = 0; y < 7; y ++ )
if (x != a && y != b)
{
auto v = f[i][j][x][y];
s0 = (s0 + v.s0) % P;
s1 = (s1 + v.s1) % P;
s2 = (s2 + v.s2) % P;
}
return {s0, s1, s2};
}
int dp(LL n)
{
if (!n) return 0;
LL backup_n = n % P;
vector<int> nums;
while (n) nums.push_back(n % 10), n /= 10;
int res = 0;
LL last_a = 0, last_b = 0;
for (int i = nums.size() - 1; i >= 0; i -- )
{
int x = nums[i];
for (int j = 0; j < x; j ++ )
{
if (j == 7) continue;
int a = mod(-last_a * power7[i + 1], 7);
int b = mod(-last_b, 7);
auto v = get(i + 1, j, a, b);
res = mod(
res +
(last_a % P) * (last_a % P) % P * power9[i + 1] % P * power9[i + 1] % P * v.s0 % P +
v.s2 +
2 * last_a % P * power9[i + 1] % P * v.s1,
P);
}
if (x == 7) break;
last_a = last_a * 10 + x;
last_b += x;
if (!i && last_a % 7 && last_b % 7) res = (res + backup_n * backup_n) % P;
}
return res;
}
int main() {
scanf("%d", &T);
init();
while (T--) {
scanf("%lld%lld", &n, &m);
printf("%d\n", mod(dp(m) - dp(n - 1), P));
}
return 0;
}