【笔记】数位 DP
数位 \(DP\)
基本概念
- 经典的数位 \(DP\) 是要求统计符合限制的数字的个数。
- 可以通过记录决定了前多少位以及大小关系来 \(DP\)。
- 或者 \(DP\) 处理不同位数的数的个数,然后 \(DP\) 统计。
- 善用不同进制来处理,一般问题都是 \(10\) 进制和二进制的数位 \(dp\)。
经典的数位 \(DP\) 是要求统计符合限制的数字的个数。
一般的形式是:求区间 \([n,m]\) 满足限制 \(f(1)\)、\(f(2)\) 、 \(f(3)\) 等等的数字的数量是多少。条件 \(f(i)\) 一般与数的大小无关,而与数的组成有关。
数位 \(DP\) 的部分一般都是很套路的,但是有些题目在数位 \(DP\) 外面套了一个
华丽的外衣,有时我们难以看出来。
【\(B-number\)】
【\(Description\)】
统计区间 \([1,n]\) 中含有 \(13\) 且模 \(13\) 为 \(0\) 的数字有多少个。
\(n \leq 10 ^ 9\)
【\(Solution\)】
暴力的去枚举每一个数然后去计算必然太慢。
我们先来考虑一个更简单的形式。
统计区间 \([1,n]\) 中含有 \(3\) 的数字有多少个。
我们先想一种特殊情况,\(n=100 \dots 0\),也就是只要第一位不填 \(1\) 后面每一位怎么填都可以,也就是把上界给消掉了。
\(n=x_1 \ \ x_2 \ \ x_3 \ \ x_4 \dots \ x_{Total}\) 。 \(x_i\) 为 \(n\) 的从高到低第 \(i\) 位是多少。\(Total\) 是总的位数。
如果我们考虑从高到低位不断填数 \(y_1 \ \ y_2 \dots\)。那么问题其实就是问有多少填数的方案,一要满足上限的限制(对应区间 \([1,n]\) ),二要满足题目的其他限制。
这样其实就比 \([1,n]\) 看起来更能 \(DP\) 了。
假设到了第 \(k\) 位 \(y_k \not = x_k\),则 \(k\) 位之后就没有上限的限制了,情况就简化了。
\(X = 3212121\)
\(Y = 3239\)
如果前面 \(y\) 中没有出现 \(3\) :那么假如我们可以求出来,\(f[k][0]\) 表示 \(k\) 位之后没有上限限制(随意填),但是必须填个 \(3\) (前面没有出现),有多少种填数的方案。
如果前面 \(y\) 中出现了 \(3\):那么假如我们可以求出来,\(f[k][1]\) 表示k位之后没有上限限制(随意填),没有必须出现 \(3\) 的限制(前面出现过了),有多少种填数的方案。
首先我们可以枚举到哪一位 \(y_k \not = x_k\),然后再枚举这一位是多少,把对应的 \(f\) 加起来就是答案了,一共需要加位数 \(\times 10\) 次。这是不大的。
- 而 \(f\) 数组总大小也很小, 位数\(\times 2\)
- 边界 \(f[total+1][0] = 0\), \(f[total+1][1]=1\),转移复杂度 \(O(10)\)
- 总复杂度 \(O(\log_{10}(n) \times 10 ^ 2)\)
回归原题 :
- 枚举哪一位不同没什么变化吧,跟原先一样枚举就好了。
- 就是 \(f\) 数组要变,因为约束条件更多了,所以状态的维数要增加。
- 设\(f[i][have][state][k]\),转移的话同样还是枚举这一位是填什么即可。
- 前面是否已经出现 \(13\) \((have)\)
- 上一位是否是 \(1\) \((state)\)
- 前面的那些数 \(\bmod \ \ 13\) 等于多少 \((k)\)
记忆化搜索来实现 :
其实,我们刚刚那个 \(DP\) 的过程自然是思路很清晰,但是一般实现我们不
那么写。因为毕竟还是好多个 \(for\) 循环,预处理一套循环,算答案一套循
环,记忆话搜索的话则是要什么算什么,会好写很多。
实际上数位 \(DP\) 往往都是用记忆化搜索的方式来实现,就是求什么调用什么,调用完了,记下来,下次就不用重新算了。
【\(Code\)】
【数字计数】
【\(Description\)】
给定两个正整数 \(a\) 和 \(b\),求在 \([a,b]\) 中的所有整数中,每个数码各出现了多少次。
\(1 \leq a , b \leq 10 ^ {12}\)
【\(Solution\)】
从高位向低位搜索 + 套模板。
用记忆化搜索做,每次记录一下个数即可。
【\(Code\)】
#include<cstdio>
#include<cstring>
#define LL long long
using namespace std;
LL l,r;
LL f[20][20][3][3];
int s[100];
int cnt = 0;
LL dfs(int id,int sum,int num,int limit,int zero)
{
if(!id) return sum;
if(~f[id][sum][limit][zero] && ! limit ) return f[id][sum][limit][zero];
int up = limit ? s[id] : 9;
LL ans = 0;
for(int i = 0;i <= up;i ++) {
if(zero && (i == 0)) ans += dfs(id - 1,sum,num,0,1);
else ans += dfs(id - 1,sum + (i == num),num,limit && (i == up),0);
}
if(!limit) f[id][sum][limit][zero] = ans;
return ans;
}
LL work(int num,LL r)
{
memset(f,-1,sizeof f);
cnt = 0;
for(;r;r /= 10) s[++ cnt] = r % 10;
return dfs(cnt,0,num,1,1);
}
signed main(){
scanf("%lld%lld",&l,&r);
for(int i = 0;i <= 9;i ++) printf("%lld ",work(i,r) - work(i,l - 1));
}
【\(windy\) 数】
【\(Description\)】
不含前导零且相邻两个数字之差至少为 \(2\) 的正整数被称为 \(windy\) 数。\(windy\) 想知道,在 \(a\) 和 \(b\) 之间,包括 \(a\) 和 \(b\) ,总共有多少个 \(windy\) 数?
【\(Solution\)】
套模板/cy
【\(Code\)】
#include <bits/stdc++.h>
#define LL long long
using namespace std;
LL l,r;
int f[70][70];
int s[70];
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
LL dfs(int id,int last,int limit,int zero)
{
if(!id) return 1;
if(~f[id][last] && !limit && !zero) return f[id][last];
int up = limit ? s[id] : 9;
LL ans = 0;
for(int i = 0;i <= up;i ++) {
if(abs(i - last) < 2) continue;
if(zero && (i == 0)) ans += dfs(id - 1,-2,limit && (i == up),1);
else ans += dfs(id - 1,i,limit && (i == up),0);
}
if(!limit && !zero) f[id][last] = ans;
return ans;
}
LL work(LL d)
{
int cnt = 0;
memset(f,-1,sizeof f);
for(;d;d /= 10) s[++ cnt] = d % 10;
return dfs(cnt,-2,1,1);
}
signed main()
{
scanf("%lld%lld",&l,&r);
printf("%lld",work(r) - work(l - 1));
}
【花神的数论题】
【\(Description\)】
设 \(sum(i)\) 表示 \(i\) 的二进制中 \(1\) 的个数。给出一个正整数 \(N\) ,求 \(\prod _{i = 1} ^ n sum(i)\) ,也就是 \(sum(i)\) ~ \(sum(n)\) 的乘积,得到的答案对 \(10 ^ 7 + 7\) 取模。
对于 \(100 \%\) 的数据,\(1 \leq n \leq 10 ^{15}\)
【\(Solution\)】
虽然输入的 \(10\) 进制数,但是本质有影响的是二进制形态啊!
怎么来转换一下,求 \(1\) ~ \(n\) 中每个数的一的个数总相乘之积,首先感觉到,每个数都会有唯一对应的 \(1\) 的个数,且一的个数的取值不到 \(60\),因为 \(n\) 最大 \(10^{15}\), 那么就想,如果枚举 \(1\) 的个数 \(k\) ,计算有多少个数含有 \(k\) 个 \(1\)。
(因为数位 \(dp\) 就是来做,有多少满足的数,且不关注数的大小)这样就
转化为数位 \(dp\) 的模型了另外,发现含有 \(k\) 个 \(1\) 的数个数可能非常多,快速幂搞一搞啦。
这题的关键就是发现一的个数的情况比较少可以枚举再转化为另一种情
况计算其实,这题本质就是转化一下,注意在模型难以建立的情况下,
通过转化,可以使题目可做。
【\(Code\)】
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int mod = 1e7 + 7;
LL n;
int cnt = 0;
int s[100];
LL dp[60][60][60][2];
LL sum[60];
inline int read()
{
int s = 0, f = 0;char ch = getchar();
while (!isdigit(ch)) f |= ch == '-', ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
LL dfs(int id,int tot,int num,int limit)
{
if(!id) return num == tot;
if(dp[id][tot][num][limit] != -1) return dp[id][tot][num][limit];
int up = limit ? s[id] : 1;//s[] 的每一位都是 0 / 1 ,如果没到边界
LL mult = 0;
for(int i = 0;i <= up;i ++) mult += dfs(id - 1,tot + (i == 1),num,limit && (i == up));
return dp[id][tot][num][limit] = mult;
}
LL qsm(LL x,LL y)
{
LL mul = 1;
LL base = x;
for(;y;y >>= 1){
if(y & 1) mul = mul * base % mod;
base = base * base % mod;
}
return mul % mod;
}
signed main()
{
scanf("%lld",&n);
for(;n;n >>= 1) s[++ cnt] = n & 1;
for(int i = 1;i <= 50;i ++) {
memset(dp,-1,sizeof dp);
sum[i] = dfs(cnt,0,i,1);
}
LL ans = 1;
for(int i = 1;i <= 50;i ++) ans = ans * qsm(i,sum[i]) % mod;
printf("%lld",ans);
return 0;
}
总结 & 经验
- 注意很多时候带进去是 \(n=0\) 要特殊处理。
- 还有一般问 \([m,n]\),我们求 \([1,n]-[1,m-1]\) 但是有的时候 \(m\) 为 \(0\) 就炸了。然后一道题 \(\color{red}{WA}\) 一个小时。。。。。。正常。。。。。
- 求所有包含 \(49\) 的数,其实就是(总数-所有不包含 \(49\) 的数)。前者的化需要有两维限制,一个是上一位是什么,一个是 之前有没有 \(49\)。但是后者只需要记一个上一位是什么。就能好写一些。
- 一般问题的数位 \(dp\) 部分,都是套路,但是这并不代表它外面“华丽的外衣”和与其他算法结合的的部分也是无脑的。 要看出它是考数位 \(dp\), ,要看出问题怎么变化一下就是数位 \(dp\)了。
- \(dp\) 初始化 \(\text{memset}\) 要置为 \(-1\)。不能置为 \(0\) !!!!!!因为有很多时候 \(dp\) 值就应该是 \(0\),然后我们如果误以为是因为之前没有计算,从新计算的话,就会 \(TLE\)。
- 既然是记忆化搜索,那就可以剪枝!!!!可行性剪枝!!
- 注意 \(windy\) 数的情况,有时前导 \(0\) 也需要记的!!!
- \(10\) 进制数位 \(dp\) 的基本最简单的形式。
- 记忆化搜索处理数位 \(dp\)的代码实现,数位 \(dp\) 一般都用记忆化搜索来做。
- 考察思维的数位 \(dp\) 往往会和其他如枚举算法结合,或作为原问题的子问题。
- 除了十进制,二进制的数位 \(dp\) 也是常见的,此外 \(K\) 进制的也是可以的。