数位DP通常是用来求解给你一个 [ l , r ] [l,r] [l,r]区间,求这个区间满足一定条件的数的个数。
个人认为数位DP最重要的两个点:
1、len==0时,即遍历完一个数后,返回什么?
2、dp方程,如何能确保DP的唯一性。
其余的都是套模板
1.Bomb
题目链接:点击此处
题意:求 [ 1 , n ] [1,n] [1,n]中含49的个数
数位DP往往用一个数组来记录 n n n中每一个位的值,这边用 d i g i t [ i ] digit[i] digit[i]表示从个位到最高位的第 i i i位为多少。
比如1234,则 d i g i t [ 1 ] = 4 , d i g i t [ 2 ] = 3 , d i g i t [ 3 ] = 2 , d i g i t [ 4 ] = 1 digit[1]=4,digit[2]=3,digit[3]=2,digit[4]=1 digit[1]=4,digit[2]=3,digit[3]=2,digit[4]=1
在这里,我们要知道,我们如果求到了[1,100]中49的个数,那么[101,200]中49的个数也知道了,同理,往下推,除了一个[401,500]中49的个数不同,其他都是相同的,原因是49可能出现在百位上是4,十位上是9的情况。那么这表示,遇到这种情况我们要特判。
其次,我们对于1234,我们容易通过一些状态得知[1001,1100]中含有49的个数,同理,对于[1101,1200]也是,但是对于[1201,1234]我们不能通过前面那2个得到,因为前面那两组求得的是间隔100的49的数量,但是这里间隔只有34,所以,我们要特殊处理。
从这里,我们也知道,对于上一位为4,这一位为9的情况,我们要特殊判断,对于我们遇到了相应位上有上限的情况(比如对于1234,千位为1,遇到了百位为2时,要开始特判),我们也要判断。
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN =2e3 + 5;
const int MAXM = 1e6 + 5;
const ll mod = 1e9 + 7;
int digit[25];
ll dp[25][2];
ll dfs(int len, bool if4, bool limit) {//求得是不满足条件的数目
if (len == 0)return 1;//例如对于1来讲,不满足条件,肯定就1这一个数,2也是,9也是,因为就一位怎么凑得齐49呢。
if (!limit && dp[len][if4])return dp[len][if4];//dp表示长度为len中,有无4的不符合条件的个数
ll cnt = 0, up = (limit ? digit[len] : 9);
for (int i = 0;i <= up;i++) {
if (if4 && i == 9)continue;
cnt += dfs(len - 1, i == 4, limit && i == up);
}
if (!limit)dp[len][if4] = cnt;
return cnt;
}
ll solve(ll num) {
int k = 0;
while (num) {
digit[++k] = num % 10;
num /= 10;
}
return dfs(k, false, true);
}
int main() {
int t;
cin >> t;
while (t--) {
ll n;
cin >> n;
cout << n + 1LL - solve(n) << endl;
}
}
2.不要62
题目链接:点击此处
题意:给你一个 [ l , r ] [l,r] [l,r]区间,其中不包含4和62的多少。
和上一题有相似之处,对于不包含62,我们只要模仿上一个就行,那么对于4呢?
因为我们在dfs枚举的时候是枚举长度为len的所有情况,如果枚举到4了,那么只要continue就行,就可以把4去掉。
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN =2e3 + 5;
const int MAXM = 1e6 + 5;
const ll mod = 1e9 + 7;
int digit[25];
ll dp[25][2];
ll dfs(int len, bool if6, bool limit) {
if (len == 0)return 1;
if (!limit && dp[len][if6])return dp[len][if6];
ll cnt = 0, up = (limit ? digit[len] : 9);
for (int i = 0;i <= up;i++) {
if (if6 && i == 2)continue;
if (i == 4)continue;
cnt += dfs(len - 1, i == 6, limit && i == up);
}
if (!limit)dp[len][if6] = cnt;
return cnt;
}
ll solve(ll num) {
int k = 0;
while (num) {
digit[++k] = num % 10;
num /= 10;
}
return dfs(k, false, true);
}
int main() {
ll n, m;
while (cin >> n >> m) {
if (n == 0 && m == 0)break;
cout << solve(m) - solve(n-1) << endl;
}
}
3.Windy 数
题目链接:点击此处
题目意思:
Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为 2的正整数被称为 Windy 数。
Windy 想知道,在 [ l , r ] [l,r] [l,r]之间,总共有多少个 Windy 数?
这次我们要得到的数是差大于2的那些数,用st表示上一层的值,这一层老规矩枚举到最大值,如果这一层的值与st的差小于2,continue。
特殊情况就是如果上一层是0,即st==11时,这一层我们选0,那么我们这一层递归的st要是11,而不能是i,因为这表示下一层的数我们还是可以随便选。
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN =2e3 + 5;
const int MAXM = 1e6 + 5;
const ll mod = 1e9 + 7;
int digit[25];
ll dp[25][15];
ll dfs(int len, int st, bool limit) {
if (len == 0)return 1;
if (!limit && dp[len][st])return dp[len][st];
int maxx = limit ? digit[len] : 9;
ll cnt = 0;
for (int i = 0;i <= maxx;i++) {
if (abs(st - i) < 2)continue;
if (st == 11 && i == 0) {
cnt += dfs(len - 1, 11, limit && i == maxx);
}
else {
cnt += dfs(len - 1, i, limit && i == maxx);
}
}
if (!limit)dp[len][st] = cnt;
return cnt;
}
ll solve(ll num) {
int k = 0;
while (num) {
digit[++k] = num % 10;
num /= 10;
}
return dfs(k, 11, true);
}
int main() {
ll n, m;
cin >> n >> m;
//cout << solve(m) << " " << solve(n - 1) << endl;
cout << solve(m) - solve(n-1) << endl;
}
4.数字计数
题目链接:点击此处
题意:找出 [ l , r ] [l,r] [l,r]区间中0到9的个数
因为是要讨论0到9,不妨从头0到0-9一个个讨论分析。
首先数位DP肯定要知道有limit,并且在长度为0时要返回一个数,但是要返回多少,这个不确定,为何?其实数位DP从len递归到0,做的是对一个数的分解,也就是说我们递归到0,其实就是分析清楚了一个数的情况。上面几个题为何返回1,是因为他们是符合要求的一个数,这题为何不返回1,因为这题记录的是0到9的个数,返回的应该是这个数中目标数的个数。例如1124,讨论1的个数的话,就返回2,讨论2的个数的话就返回1,讨论3的个数的话就返回0。
其次 d p [ i ] [ j ] dp[i][j] dp[i][j]表示 i i i位数,其中 l e n len len到 i i i位中含有 j j j个含有对应位。
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN =2e5 + 5;
const int MAXM = 1e6 + 5;
const ll mod = 1e9 + 7;
int arr[MAXN];
int digit[20];
ll dp[20][20];
ll ans[10] = { 0 };
ll dfs(int len, bool lead, bool limit, int tar, ll sum) {
if (len == 0)return sum;
if ( !limit && ~dp[len][sum])return dp[len][sum];
ll cnt = 0;
int maxx = limit ? digit[len] : 9;
for (int i = 0;i <= maxx;i++) {
cnt += dfs(len - 1, lead || i, limit && i == maxx, tar, sum + ((lead || i) && (tar == i)));
}
if (!limit && lead)dp[len][sum] = cnt;
return cnt;
}
ll solve(ll num,int tar) {
memset(dp, -1, sizeof dp);
int k = 0;
while (num) {
digit[++k] = num % 10;
num /= 10;
}
return dfs(k, false, true, tar, 0);
}
int main() {
ll n, m;
cin >> n >> m;
solve(m, 1);
solve(n, -1);
for (int i = 0;i <= 9;i++) {
cout <<solve(m,i)-solve(n-1,i) << " ";
}
cout << endl;
}
5.同类分布
题目链接:点击此处
给 [ l , r ] [l,r] [l,r],找出区间中的各位数字之和能整除原数的数的个数。
数位DP, d p [ l e n ] [ s u m ] [ s t ] dp[len][sum][st] dp[len][sum][st],其中 l e n len len表示我们剩下未遍历的位数, s u m sum sum表示我们已遍历的数总和, s t st st表示已遍历的数取模 m o d mod mod的结果。这样 d p [ l e n ] [ s u m ] [ s t ] dp[len][sum][st] dp[len][sum][st]表示的结果具有唯一性。即可以使用记忆化搜索。因为假如我们未处于 l i m i t limit limit状态,我们对于已经遍历的得到的 s u m sum sum和 s t st st,后序不管遍历得到哪一个数字, s u m sum sum和 s t st st都是一一对应着变化的。所以我们只要这3个值一确定,如果 d p dp dp有记录,那么就可以得到答案。
最后 l e n = 0 len=0 len=0时返回结果1的条件必须是 s u m = m o d 且 s t = 0 sum=mod 且st=0 sum=mod且st=0
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN = 2e5 + 5;
const int MAXM = 1e6 + 5;
ll n, m;
ll dp[20][200][200];
int digit[20];
ll mod = 0;
ll dfs(int len, ll sum, ll st, bool limit) {
if (len == 0) {
if (sum == mod)return st==0?1:0;
else return 0;
}
if (!limit&&~dp[len][sum][st])return dp[len][sum][st];
int res = limit?digit[len] : 9;
ll ret = 0;
for (int i = 0;i <= res;i++) {
ret += dfs(len - 1, sum + i, (10ll * st + i) % mod, i == res && limit);
}
if (!limit)dp[len][sum][st] = ret;
return ret;
}
ll solve(ll num) {
int k = 0;
while (num) {
digit[++k] = num % 10;
num /= 10;
}
ll ans = 0;
for (ll i = 1;i <= 9 * k;i++){
mod = i;
memset(dp, -1, sizeof dp);
ans += dfs(k, 0, 0, true);
}
return ans;
}
int main() {
cin >> n >> m;
cout << solve(m) - solve(n-1) << endl;
}
6.萌数
题目链接:点击此处
只有满足“存在长度至少为2的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。
现在想知道从l到r的所有整数中有多少个萌数。
由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。
整数的长度为1到1000。
这题考了一个思维,就是如果存在4位回文,那么必定存在2位回文。比如1001中,00必定是回文。
所以本题只要看遍历的那个数和前面2个数是否组成回文就行。
个人认为数位DP其实难点在于如何确定唯一条件。
本题 d p [ l e n ] [ p r e 1 ] [ p r e 2 ] [ h a v e ] dp[len][pre1][pre2][have] dp[len][pre1][pre2][have],首先 l e n len len表示剩余未访问的位数长度, p r e 1 pre1 pre1表示前面2位的值, p r e 2 pre2 pre2表示前面1位的值, h a v e have have表示到现在是否存在回文。这DP就已经有了唯一性了。因为 p r e 1 pre1 pre1和 p r e 2 pre2 pre2和 h a v e have have已经完全表示前面数的状态。
然后 l e n = 0 len=0 len=0时,如果 h a v e = 1 have=1 have=1返回1, h a v e = 0 have=0 have=0返回0.
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN = 2e5 + 5;
const int MAXM = 1e6 + 5;
ll n, m;
ll dp[1005][11][11][2];
int digit[1005];
ll mod = 1e9 + 7;;
ll dfs(int len,int pre1,int pre2,bool have, bool limit) {
if (len == 0) {
if (!have)return 0;
else return 1;
}
if (!limit && ~dp[len][pre1+1][pre2+1][have])return dp[len][pre1+1][pre2+1][have]%mod;
int maxx = limit ? digit[len] : 9;
ll res = 0;
for (int i = 0;i <= maxx;i++) {
bool f = false;
if (pre1 == i || pre2 == i) {
f = true;
}
if (pre1 == -1 && pre2 == -1 && i == 0)res += dfs(len - 1, -1, -1, 0, limit && i == maxx);
else res += dfs(len - 1, pre2, i, f || have, limit && i == maxx);
res %= mod;
}
if (!limit)dp[len][pre1+1][pre2+1][have] = res % mod;
return res%mod;
}
ll solve(string num) {
int len = num.length();
for (int i = len - 1;i >= 0;i--)digit[len - i] = num[i] - '0';
memset(dp, -1, sizeof dp);
return dfs(len, -1, -1, 0, 1);
}
int main() {
string a, b;
cin >> a >> b;
int pre1 = -1, pre2 = -1;
bool f = false;
for (int i = 0;i < a.length();i++) {
int u = a[i] - '0';
if (u == pre1 || u == pre2) { f = true;break; }
pre1 = pre2;
pre2 = u;
}
ll now = (solve(b) - solve(a) + mod) % mod;
if (f)now++;
now %= mod;
cout << now << endl;
}
7.Valley Numer
题目链接:点击此处
求1-r中无山峰的数的个数。
这题很明显的知道我们要用一个标记标记是否存在上坡,不然哪来的山峰。这里我们用 u p up up标记。
之后我们还要有个标记记录上坡后是否存在下坡。这里我们用 h a v e have have来标记。
当然我们比较肯定需要有前一个数字的信息。这里我们用 p r e pre pre记录。
那么DP就很容易写了(要确保唯一性), d p [ l e n ] [ p r e ] [ u p ] [ h a v e ] dp[len][pre][up][have] dp[len][pre][up][have],这里的 p r e , u p , h a v e pre,up,have pre,up,have已经确保了当前的状态的唯一性了,后面即不会表示多于等于2个不同的状态。
l e n = = 0 len==0 len==0时,如果 h a v e have have返回0,否则返回1。
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN = 2e5 + 5;
const int MAXM = 1e6 + 5;
ll n, m;
ll dp[105][11][2][2];
int digit[105];
ll mod = 1e9 + 7;;
ll dfs(int len,int pre,bool up,bool have, bool limit) {
if (len == 0) {
if (have)return 0;
else return 1;
}
if (!limit && ~dp[len][pre][up][have])return dp[len][pre][up][have];
ll ans = 0;
int maxx = limit ? digit[len] : 9;
for (int i = 0;i <= maxx;i++) {
if (pre == -1 && i == 0)ans += dfs(len - 1, -1, 0, 0, limit && i == maxx);
else if (pre == -1 && i != 0)ans += dfs(len - 1, i, 0, 0, limit && i == maxx);
else if (pre != -1) {
ans += dfs(len - 1, i, i > pre || up, have || (up && i < pre), limit && i == maxx);
}
ans %= mod;
}
if (!limit)dp[len][pre][up][have] = ans%mod;
return ans%mod;
}
ll solve(string num) {
int len = num.length();
for (int i = len - 1;i >= 0;i--)digit[len - i] = num[i] - '0';
memset(dp, -1, sizeof dp);
return dfs(len, -1,0, 0, 1);
}
int main() {
int t;
cin >> t;
while (t--) {
string a, b;
cin >> b;
ll res = (solve(b) -1)% mod;
cout << res << endl;
}
}
8. Beautiful numbers
题目链接:点击此处
求[l,r]中一个数能被组成该数的所有非零数整除个数。
很容易知道被一个数的组成数整除,就相当于一个数能被组成数的LCM整除。
那么我们现在想我们上文的2个重要状态。
一个是DP的唯一性,一个len=0时返回什么。
如何定义DP能够让DP唯一标识一个数呢。
d p [ l e n ] [ l c m ] [ s t ] dp[len][lcm][st] dp[len][lcm][st]表示剩余 l e n len len个,哪些遍历过的数的 L C M = l c m LCM=lcm LCM=lcm,一个数取余2520= s t st st。为何取余2520,因为1到9的 L C M LCM LCM为2520,我们一个数能被2520取余为0,那么肯定可以被lcm取余为0。我们这样取可以表示唯一状态而且空间利用小。
l e n = 0 len=0 len=0时,如果 s t % l c m = 0 st\%lcm=0 st%lcm=0,返回1,否则返回0。
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN = 2e5 + 5;
const int MAXM = 1e6 + 5;
ll n, m;
ll dp[20][50][2520];
ll digit[105];
ll mod = 0;
int cnt = 0;
inline ll read() {
ll X = 0, w = 1; char c = getchar();
while (c < '0' || c>'9') { if (c == '-') w = -1; c = getchar(); }
while (c >= '0' && c <= '9') X = X * 10 + c - '0', c = getchar();
return X * w;
}
int GCD(int a, int b) {
return b == 0 ? a : GCD(b, a % b);
}
int LCM(int a, int b) {
return a / GCD(a, b) * b;
}
int tong[2521] = { 0 };
void init() {
for (int i = 1;i <= 2520;i++) {
if (2520 % i == 0) { tong[i] = ++cnt; }
}
}
ll dfs(int len,int lcm,int st,bool limit) {
if (len == 0) {
return st % lcm == 0;
}
if (!limit && ~dp[len][tong[lcm]][st])return dp[len][tong[lcm]][st];
int maxx = limit ? digit[len] : 9;
ll ans = 0;
for (int i = 0;i <= maxx;i++) {
int L = (i == 0 ? lcm : LCM(lcm, i));
ans += dfs(len - 1, L, (st * 10 + i) %2520, limit && i == maxx);
}
if (!limit)dp[len][tong[lcm]][st] = ans;
return ans;
}
ll solve(ll num) {
int k = 0;
while (num) {
digit[++k] = num % 10;
num /= 10;
}
return dfs(k,1,0,1);
}
int main() {
ll t;
t=read();
init();
memset(dp, -1, sizeof dp);
while(t--){
ll a,b;
a = read(), b = read();
printf("%I64d\n", solve(b) - solve(a - 1));
}
}
9.Magic Number
题目链接:点击此处
这个对于不满足条件的直接continue就行,因为一旦不满足条件,后续不管怎么变都不满足条件。
对于存在前导0的话,我们不能记录dp值。
d p [ l e n ] [ i s e v e n ] [ s t ] dp[len][iseven][st] dp[len][iseven][st]表示了唯一性, l e n len len表示还剩下的位数, i s e v e n iseven iseven表示现在遍历的奇数位还是偶数位, s t st st表示前面所有位的组成数取余 m m m得到的结果。首先前导0肯定不会得到DP值,所以能得到DP的都是非前导0。然后我们不满足条件的 c o n t i u e contiue contiue掉了,所以要DP的值的话,前面肯定是满足条件的。所以,在前面满足条件的情况下,通过看这个数是不是偶数位以及前面数的取模和可以唯一确定DP状态。
l e n = 0 len=0 len=0时,如果 s t = 0 st=0 st=0返回1,否则返回0。
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iomanip>
#include<stdio.h>
#include<map>
#include<queue>
using namespace std;
#define Check(x) cout<<"x:"<<x<<" "
#define Min(x,y,z) min(x,min(z,y))
#define Max(x,y,z) max(x,max(z,y))
typedef long long ll;
const int MAXN = 2e5 + 5;
const int MAXM = 1e6 + 5;
int m, d;
ll dp[2005][2][2000];
ll digit[2005];
ll mod = 1e9+7;
ll dfs(int len,int iseven,int topzero,int st,bool limit) {
if (len == 0) {
if (st == 0)return 1;
else return 0;
}
if (!limit && !topzero&&~dp[len][iseven][st])return dp[len][iseven][st];
int maxx = limit ? digit[len] : 9;
ll ans = 0;
for (int i = 0;i <= maxx;i++) {
if (topzero && i == 0)ans += dfs(len - 1, 0, 1, 0, i == maxx && limit);
else if (topzero && i != 0) {
if (i == d)continue;
else {
ans += dfs(len - 1, 1, 0, i % m, i == maxx && limit);
}
}
else if (!topzero) {
if ((iseven && i != d) || (!iseven && i == d)) {
continue;
}
ans += dfs(len - 1, iseven ^ 1, 0, (st * 10 + i) % m, i == maxx && limit);
}
ans %= mod;
}
if (!limit&&!topzero)dp[len][iseven][st] = ans;
return ans;
}
ll solve(string num) {
int k = 0;
for (int i = num.length()-1;i >= 0;i--) {
digit[++k] = num[i]-'0';
}
return dfs(k,0,1,0,1);
}
int main() {
memset(dp, -1, sizeof dp);
cin >> m >> d;
string a, b;
cin >> a >> b;
ll ans = (solve(b)-solve(a)+mod)%mod;
int st = 0;
bool f = true;
for (int i = 0;i < a.length();i++) {
int now = a[i] - '0';
if (((i + 1) % 2 == 0 && now != d) || ((i + 1) % 2 == 1 && now == d)) { f = false;break; }
st = (st * 10 + now) % m;
}
if (st != 0)f = false;
if (f)ans++;
ans %= mod;
cout << ans << endl;
}