论进制类型的数位dp:胎教级教学
1前言
阅读本文需要有一定的dp基础,建议阅读我主页关于数位dp的内容
建议搭配如下例题阅读
P8764 [蓝桥杯 2021 国 BC] 二进制问题
[一本通提高数位动态规划]数字游戏
2例题1
(1)问题
问题如下图
可以发现,和传统数位dp不同的是,本题是基于二进制来dp的
(2)状态设置
我们发现,二进制数的性质很好
可以表示为一棵二叉树(就是01Trie,不会的点这里)
我们把这棵树画出来
注:本图片来自《信息学奥赛一本通》,如有侵权请联系作者删除
我们就设要求解的数为
13
13
13
图片里加粗的线就表示着
13
13
13这个数
显然的,我们求解的范围是不大于
13
13
13的(所有包含的点已在图中用阴影标出)
对于
13
13
13本身,我们可以特判求解
我们可以先看看标有阴影的节点
很容易发现,这些节点可划分若干棵子树(在图中用不同颜色的阴影标出)
而且这些子树还有一些性质
一,都为
13
13
13这条链连接的左子树(求解范围不大于
13
13
13,就显然是左子树)
二,这些子树具有自相似性(即大的子树嵌套着小的子树)
有了这些性质,我们就可以设置状态
d
p
i
,
j
dp_{i,j}
dpi,j为
i
i
i层子树选取
j
j
j个
1
1
1的方案数
属性显然为COUNT(数位dp大致是相同的)
(3)状态转移
对于状态
d
p
i
,
j
dp_{i,j}
dpi,j
我们考虑新插入一位为
0
0
0,
1
1
1的两种情况,如果是
0
0
0,那么这个节点的祖先节点
1
1
1的个数显然为
j
j
j个,如果为
1
1
1,它的祖先节点中
1
1
1的个数则为
j
−
1
j-1
j−1个
由此得状态转移方程为
d
p
i
,
j
=
d
p
i
−
1
,
j
+
d
p
i
−
1
,
j
−
1
dp_{i,j} = dp_{i-1,j}+dp_{i-1,j-1}
dpi,j=dpi−1,j+dpi−1,j−1
聪明的你应该已经看出来了,这不是递推求组合数吗?
对于有
k
k
k个
1
1
1的
h
h
h位数(基于二进制)的数量为
C
h
k
C^{k}_{h}
Chk
这个和递推求组合数可以互相推,会一个你就全会了,所以快来读我的文章吧
广告:作者关于组合数的博客,你值得拥有
(4)利用状态,求解问题
我们按位分解,举的例子还为
13
13
13,假设这一位是第
i
i
i位,值为
1
1
1,左子树就可以取到
答案加上
d
p
[
i
]
[
K
−
c
n
t
]
dp[i][K-cnt]
dp[i][K−cnt](
K
K
K代表求有
K
K
K个
1
1
1的数的个数,意思同题面中的
K
K
K,
c
n
t
cnt
cnt代表遍历过的1的个数)
如果是
0
0
0,它本身就在左子树,这就取不到了,答案不用动就行
最后别忘了加上这个数本身----如果情况合法的话 (这个好像能HACK洛谷部分题解?)
(5)例题1的代码
为了让大家看得舒服,我把代码单独放在这里
给几点提示:
1.为了和组合数的程序同意,也为了严谨,树的层数从0开始
2.十年OI一场空,不开longlong见祖宗
3.
1
0
18
10^{18}
1018大概是60个二进制位
代码在这里(c++)
#include<bits/stdc++.h>
using namespace std;
long long a,k;
long long n,s[114514];
long long dp[1145][1145];
long long ans = 0;
void init(){
for(long long i = 0;i<=60;i++){
for(long long j = 0;j<=i;j++){
if(j!=0){
dp[i][j] = dp[i-1][j]+dp[i-1][j-1];
}else{
dp[i][j] = 1;
}
}
}
return;
}
void solve(){
long long hhh = a;
while(hhh){
s[++n] = hhh%2;
hhh>>=1;
}
long long cnt = 0;
for(long long i = n;i>=1;i--){
if(s[i]&1){
if(k>=cnt){
ans+=dp[i-1][k-cnt];
}
cnt++;
}
}
if(cnt==k){
ans++;
}
return;
}
int main(){
cin>>a>>k;
init();
solve();
cout<<ans;
return 0;
}
3例题2
(1)问题
问题如下图
看着跟上一个没啥关系…
(2)用dp的思想来转化问题
我们尝试用数学语言来表示
对于每个答案
a
n
s
ans
ans
都有
a
n
s
=
b
h
1
+
b
h
2
.
.
.
.
.
.
+
b
h
k
ans = b^{h_{1}}+b^{h_{2}}......+b^{h_{k}}
ans=bh1+bh2......+bhk
其中对于每个
h
i
h_{i}
hi没有任何一个
h
j
h_{j}
hj与其相等(显然
j
j
j不等于
i
i
i)
这种表示形式很像
b
b
b进制分解,但是所有
b
h
b^{h}
bh,系数都为
1
1
1
这种数有什么性质呢,显然将其转化为
b
b
b进制数,就只包含
0
,
1
0,1
0,1(因为
k
k
k不一定等于数
x
x
x在二进制的位数,所以会有
0
0
0的存在,但是因为系数为
0
0
0,没在上面体现)
01
01
01序列…那不就和上一题一样了
建一棵Trie数(此时为
b
b
b叉树)将非0非1的点都删除后,剩下的还是
01
01
01Trie!
所以,我们状态转移 复制粘贴 上面的代码,这就是用dp的方法来做dp题
上一题就如同求好的状态,现在我们就相当于在写状态转移方程
(3)从想法到实现
现在,大致的思路有了,但是具体实现还要考虑
上文说过,我们的目的是求出一个
01
01
01序列,可以在二进制上求解
假设边界上的数本身合法,全是
01
01
01,直接
b
b
b进制分解
那如果出现了大于
1
1
1的数呢,此时已经多了一位,后面的所有,无论什么数,都可以取做
1
1
1,我们找到大于
1
1
1的数,直接将这一位和后面都取
1
1
1就可以了
这种COUNT型问题一般都具有前缀和性质,我们对左边界
−
1
-1
−1和右边界分别求解,相减就好了
但是,两边都要求解的话,我们一般习惯使用函数,减少码量
问题来了,一个
01
01
01序列无法作为函数的返回值,为了方便,我们把
01
01
01序列状压
其实就是化为二进制数…和原数没有直接的数量关系
在求解的时候,也可以把状压dp那一套位运算搬过来用
(4)例题2的代码
会了上一题,这一题也不难
直接上代码(c++)
#include<bits/stdc++.h>
using namespace std;
long long x,y,k,b;
long long n,s[114514];
long long dp[1145][1145];
long long ans = 0;
void init(){
for(long long i = 0;i<=60;i++){
for(long long j = 0;j<=i;j++){
if(j!=0){
dp[i][j] = dp[i-1][j]+dp[i-1][j-1];
}else{
dp[i][j] = 1;
}
}
}
return;
}
long long get(long long x){
long long res = 0;
long long g[114514],idx = 0,hhh = x;
while(hhh){
g[++idx] = hhh%b;
hhh/=b;
}
long long st[114514];
for(long long i = idx;i>=1;i--){
if(g[i]>1){
for(long long j = i;j>=1;j--){
st[j] = 1;
}
break;
}else{
st[i] = g[i];
}
}
long long sum = 0;
for(long long i = idx;i>=1;i--){
sum<<=1;
sum+=st[i];
}
return sum;
}
long long solve(long long a){
n = 0,ans = 0;
long long hhh = a;
while(hhh){
s[++n] = hhh%2;
hhh>>=1;
}
long long cnt = 0;
for(long long i = n;i>=1;i--){
if(s[i]&1){
if(k>=cnt){
ans+=dp[i-1][k-cnt];
}
cnt++;
}
}
if(cnt==k){
ans++;
}
return ans;
}
int main(){
cin>>x>>y>>k>>b;
init();
x = get(x);
y = get(y);
long long AC = solve(y)-solve(x-1);
cout<<AC;
return 0;
}
4后记
本文有两道例题,篇幅长是不可避免的
读了本文,相信你对数位dp的理解一定会大有进步
本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准CSDN森林古猿1!