记录今天在Acwing学习的几道数位Dp题目,整理了思路,方便以后的复习:
1.度的数量
题目描述
求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于 K 个互不相等的 B 的整数次幂之和。
例如,设 X=15,Y=20,K=2,B=2,则有且仅有下列三个数满足题意:
样例
输入
15 20
2
2
输出
3
数位DP
K个互不相同的B的整数次幂,等价于求数字num在B进制表示下是否是由K个1且其他位全是0组成,可用数位Dp来做。
对于数字num,存放其每一位上的数字(B进制表示下),从高位向低位枚举,假设枚举到第i位,第i位上的数字是x,那么分以下几种情况讨论:
-
1.x == 0
那么第i
位只能是0
,后面数位上现在都不能确定,只能继续向后看. -
2.x == 1
这里第i
位可以分成两种情况:- 1.第
i
位放0
,那么后面的数位上可以放k-last个1
,res += f[i][k-last]; - 2.第
i
位放1
,那么后面数位上的情况不能用组合数计算,因为要保证答案中的数字比原数字要小
- 1.第
-
3.x > 1
同样第i
位分成两种情况- 1.第
i
位放0
,那么后面的数位上可以放k-last个1
,res += f[i][k-last]; - 2.第
i
位放1
,那么后面的数位上可以放k-last-1个1
,res += f[i][k-last-1];
- 1.第
C++ 代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 34;
int f[N][N]; //组合数
int k,b;
int l,r;
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 n){
int res = 0, last = 0;
if(!n) return res;
vector<int> a;
while(n) a.push_back(n%b),n/=b;
int len = a.size()-1;
for(int i = len; i>=0; --i){
int x = a[i];
if(x){
res += f[i][k-last]; //第i位放0,后i-1位放k-last个1(0~i-1位,所以第i位后面有i位)
if(x>1){
//第i位放1,后i-1位放k-last-1个1
if(k-last-1>=0) res += f[i][k-last-1];
break; //这一位已经不合法了,所以上一步结束后直接break
}else{
//如果x为1,考虑到组成的数必须比原数字小,所以这里不能用组合数计算
last++;
if(last>k) break; //1如果放完了,直接break
}
}
if(!i && last == k) res ++; //树的最右侧的分支
}
return res;
}
int main()
{
init(); //预处理出组合数
cin>>l>>r>>k>>b;
cout<<dp(r) - dp(l-1);
return 0;
}
2.windy数
题目描述
Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为 2 的正整数被称为 Windy 数。
Windy 想知道,在 A 和 B 之间,包括 A 和 B,总共有多少个 Windy 数?
数据范围
样例
输入
1 10
输入
9
数位Dp
假设我们当前枚举到第i
位(设共有n
位),且第i
位上的数字为x
,那么对于答案中第i
位数字j
来说,有两类:
-
1.0~x-1 (如果第i位是最高位,这里是1~x-1)
括号中提到的1~x-1后面会解释 ,我们用last
记录上一位的数字,然后枚举j
,如果abs(j-last) >= 2
就累加答案,res += f[i+1][j]
; -
2.x
不需要枚举j
,last = x
,再枚举之后的数位即可
上述做完之后,由于上面的答案都是n
位的,对于数位个数低于n
的,再累加到答案中就行了
f数组的处理
f[i][j]
表示一共有i
位,且最高位数字为j
的满足windy数定义的数的个数
状态转移: 因为第i
位是j
已经确定,考虑第i-1
位,设第i-1
位数字为k
,那么根据windy数定义只要abs(k-j) >= 2
就可以转移过来
关于前导0
上面提到了枚举的第i
位是最高位,那么不能填0
,这里解释一下,如果我们填0
,那么答案就会加上f[i+1][0],,举这样一个例子,
对于数字13
,他是满足windy数定义的,那么加上前导0之后的013
就不会被f[3][0]
加进去,原因就是abs(0-1)<2
,这样就导致答案漏掉。
C++ 代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 11;
int f[N][N]; //f[i][j] 表示一共有i位,且最高为是数字j的满足wingy数定义的数的个数
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 = 0; k<=9; k++)
if(abs(k-j)>=2) f[i][j] += f[i-1][k];
}
int dp(int n){
int res = 0,last = -10;
vector<int> a;
while(n)a.push_back(n%10),n/=10;
int len = a.size()-1;
//答案是a.size()位的
for(int i =len; i>=0; --i){
int x = a[i];
for(int j = (i==len); j<x; ++j) //最高位从1开始
if(abs(j-last)>=2) res += f[i+1][j];
if(abs(x-last)<2) break; //不满足定义,直接break
last = x;
if(!i) res ++;
}
//答案小于a.size()位的
for(int i = 1; i<=len; i++)
for(int j = 1; j<=9; j++)
res += f[i][j];
return res;
}
int main()
{
init();
int l,r;
cin>>l>>r;
cout<<dp(r)-dp(l-1);
return 0;
}
3.数字游戏
题目描述
科协里最近很流行数字游戏。
某人命名了一种不降数,这种数字必须满足从左到右各位数字呈非下降关系,如 123,446。
现在大家决定玩一个游戏,指定一个整数闭区间 [a,b],问这个区间内有多少个不降数。
样例
输入
1 9
1 19
输出
9
18
数据范围
数位DP
按照数位DP分析步骤: 假设我们当前枚举到第i位,且第i位上的数字是x,那么现在对于答案的第i位数字j来说,可以填两类数字:
-
1.j取0~x-1
那么res += f[i+1][j]; -
2.j取x
last记录x,再枚举下一位
f数组
f[i][j] 表示一共有i位,且最高位数字为j的不降数的个数
状态转移: 因为最高位已经固定,所以假设第i-1位是k,根据不降数定义k>=j,所以
C++ 代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 12;
int f[N][N]; //f[i][j] 表示最高位是j且一共有i位的不降数的个数
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]; //f[i][j] = f[i-1][j] + f[i-1][j+1] + f[i-1][j+2] +...+ f[i-1][9];
}
}
int dp(int n){
if(!n) return 1;
int res = 0,last = 0;
vector<int> a;
while(n) a.push_back(n%10),n/=10;
int len = a.size()-1;
for(int i = len; i>=0; --i){ //从高到低枚举
int x = a[i];
for(int j = last; j<x; j++)//last表示上一位数字大小,当前枚举的数字不能比last小(不降数定义)
res += f[i+1][j]; //(0~i位,一共有i+1位)
if(x<last) break; //若小,则break
last = x; //更新last
if(!i) res++;
}
return res;
}
int main()
{
init();
int l,r;
while(cin>>l>>r) cout<<dp(r) - dp(l-1)<<endl;;
return 0;
}
4.数字游戏Ⅱ
题目描述
由于科协里最近真的很流行数字游戏。
某人又命名了一种取模数,这种数字必须满足各位数字之和为 0。
现在大家又要玩游戏了,指定一个整数闭区间 [a.b],问这个区间内有多少个取模数。
数据范围
样例
输入
1 19 9
输出
2
数位DP
在几道数位Dp题目练习过后,这类题目重点在于找到左边那一类如何直接计算
对于这一题来说,假设我们当前枚举到的第i位,且第i位上的数字是x,那么对于答案中的第i位数字j来说,可以填两类数:
-
1.0~x-1
我们用last
表示到当前为止,前面数位上的数字之和,对此,当前第i
位数字为j
,前面数字之和为last
,那么
后i
位(包括j
这一位)数字之和sum
与last
的关系就是(last+sum)%N == 0
,那么sum%N
的结果等价于(-last)%N
,
所以res += f[i+1][j][(-last%N)];
(后面会提到f数组的处理) -
2.x
如果j
填x
,那么不用枚举了,last += x
,再枚举下一位即可
f数组的处理
f[i][j][k]
表示一共有i
位,且最高位数字是j
,且所有位数字和模N
结果为k
的数的个数
状态转移: 因为第i
位已经是j
,且所有数字之和模N
为k
,所以我们考虑第i-1
位,假设第i-1
位数字是x
,由于j
已经知道,
那么剩下的i-1位数字之和模N的结果就是(k-j)%N
,那么状态转移方程就是:
C++ 代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 12, M = 102;
int f[N][10][M]; //f[i][j][k] 表示一共有i位,且最高位数字是j,且所有位数字和模p位k的数字个数
int p;
int mod(int u,int v){
return (u%v+v)%v; //c++的%会得到负数,需要做一个偏移
}
void init(){
memset(f,0,sizeof f); //多组测试数据,f数组要清空
for(int i = 0; i<=9; i++) f[1][i][i%p]++;
for(int i = 2; i<N; i++)
for(int j = 0; j<=9; j++)
for(int k = 0; k<p; k++)
for(int x = 0; x<=9; x++)
f[i][j][k] += f[i-1][x][mod(k-j,p)];
}
int dp(int n){
if(!n) return 1;
int res = 0,last = 0;
vector<int> a;
while(n) a.push_back(n%10),n/=10;
int len = a.size()-1;
for(int i = len; i>=0; --i){
int x =a[i];
for(int j = 0; j<x; j++) //第i位放0~x-1
res += f[i+1][j][mod(-last,p)]; //0~i位,所以一共有i+1位
last += x;
if(!i && last % p == 0) res ++;
}
return res;
}
int main()
{
int l,r;
while(cin>>l>>r>>p){
init();
cout<<dp(r)-dp(l-1)<<endl;
}
return 0;
}
网友评论