论进制类型的数位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 j1
由此得状态转移方程 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=dpi1,j+dpi1,j1
聪明的你应该已经看出来了,这不是递推求组合数吗?
对于有 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][Kcnt] 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!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值