数位统计DP入门

数位统计DP

数位统计DP是一种有关数字的限制问题,一般问题形式类似于给定若干限制条件,求满足条件的第K小的数是多少,或者是询问区间[L,R]内有多少满足要求的数字,对于这种类型的题,我们一般是先使用动态规划进行预处理,再运用类似与倍增优化DP的拼凑思想拼出最后的答案,亦或者是试填法
在这类题中,我们动态规划的状态设计一般是第一维是第i位数字,也就是阶段,后面几个维度一般是题目给的限制条件诞生的维度。
在做题过程中,这类型的题数据范围一般较大,但我们动态规划的算法复杂度大约为O(logkN),至于拼凑与试填法的复杂度也是对数级别。这里的题很多与数学问题有所交叉,可以留意一下

做题套路

  1. 第K大,遇到这种问题的一般步骤是:动态规划预处理,再使用试填法,这个过程从高位考虑到低位,考虑当前位有多少种选择,填了当前位置之后有多少种方案(排名),类似于平衡树求排名,我们从小到大枚举每一位的可能性,当我们可以填i的时候,如果填i之后的方案数不够K,那么i就不可以填,此时还要令K-=填i的方案数,注意一定得减,当找到的第一个方案数大于k的,这个方案就是我们应该选择的方案。这个的原理我们可以把整个所有的可能性看成一颗搜索树,每一个节点上都存着它有多少个叶子节点(对应方案数),我们要在这棵搜索树里找到从树根到排名为K的叶子节点的路径就是我们的答案,就与平衡树,权值线段树类似
  2. 求区间[L,R],这里因为是关于数的统计问题,是无后效性的,于是我们可以将其转换为前缀和运算,同样的,先进行动态规划预处理,用某些标志性的点进行拼凑出答案,当然我们也可以使用试填法的思想,只不过这个变成了统计方案而已

注意事项

  1. 时刻注意前导零的处理
  2. 强调不重不漏性
  3. 注意到动态规划的维度等等不要冗余

实战

例题1:装饰围栏

题目描述:

N 块长方形的木板,长度分别为 1,2,,N,宽度都是1

现在要用这 N 块木板组成一个宽度为 N 的围栏,满足在围栏中,每块木板两侧的木板要么都比它高,要么都比它低。

也就是说,围栏中的木板是高低交错的。

我们称“两侧比它低的木板”处于高位,“两侧比它高的木板”处于低位。

显然,有很多种构建围栏的方案。

每个方案可以写作一个长度为 N 的序列,序列中的各元素是木板的长度。

把这些序列按照字典序排序,如下图所示,就是 N=4 时,所有满足条件的围栏按照木板长度的字典序排序后的结果。

现在给定整数 C,求排名为 C 的围栏中,各木板的长度从左到右依次是多少。
1N20,
0<C<263

分析

很明显,N很小,C很大,这是一个第K大问题,我们按照套路来处理

  1. 设计动态规划:由于N很小,我们设计一个与N相关的状态,又因为这个涉及到高低排名,于是:设f[i,j,k]表示用i块木板,其中最左边木板的排名为j,身处k位(0低1高)的方案数,注意这里我们采用了类似排名的描述,而不是像长度为j这样的绝对性描述,这是因为在1N中,排名为j和长度为j是等价的,且运用排名会使得我们状态转移非常轻松
  2. 进行动态规划预处理:有状态转移方程:

f[i,j,0]=p=ji1f[i1,p,1]f[i,j,1]=q=1j1f[i1,q,0]

  1. 进行拼凑,具体步骤为:
    (1). 假设我们已经填好了i1块木板,记上一块木板的长度为last,位于k位,现在我们来考虑第i块木板
    (2). 将k改为k1即可得到当前木板的状态,然后我们从小到大枚举当前木板的实际长度和相对排名,尝试着使用这一个方案进行下一步
    (3).设我们枚举的实际长度为len,相对排名为j,若f[ni+1,j,k]<C,令C=Cf[ni+1,j,k],继续尝试更大的j,否则这个j就是我们第i位的答案,进行下一位的枚举,直到最后就得出了答案,当然,最初的计算我们就直接判断k=0/1谁可以就可以了

Code

#define int long long
int t,f[25][25][2],n,m,vis[25];
void init(){
	f[1][1][0]=f[1][1][1]=1;
	for(int i=2;i<=20;i++)
		for(int j=1;j<=i;j++){
			for(int k=1;k<j;k++)f[i][j][1]+=f[i-1][k][0];
			for(int k=j;k<i;k++)f[i][j][0]+=f[i-1][k][1];
		}
}//DP预处理
signed main(){
	init();
	scanf("%lld",&t);
	while(t--){
		scanf("%lld%lld",&n,&m);
		memset(vis,0,sizeof vis);
		int last,k;
		for(int j=1;j<=n;j++){
			if(f[n][j][1]>=m){
				last=j;
				k=1;
				break;
			}
			else m-=f[n][j][1];
			if(f[n][j][0]>=m){
				last=j;
				k=0;
				break;
			}
			else m-=f[n][j][0];
		}//枚举第一位的情况
		vis[last]=1;
		printf("%lld",last);
		for(int i=2;i<=n;i++){
			k^=1;
			int j=0;
			for(int len=1;len<=n;len++){
				if(vis[len])continue;
				j++;
				if(k==0&&len<last||k==1&&len>last){
					if(f[n-i+1][j][k]>=m){
						last=len;break;
					}
					else m-=f[n-i+1][j][k];
				}
			}
			vis[last]=1;
			printf(" %lld",last);
		}
		puts("");
	}
	return 0;
}

例题2:圆形数字

题目描述:

定义圆形数字如下:

把一个十进制数转换为一个无符号二进制数,若该二进制数中 0 的个数大于或等于 1 的个数,则它就是一个圆形数字。

现在给定两个正整数 a 和 b,请问在区间 [a,b] 内有多少个圆形数字。

分析

下面简称圆数
对于这道题,由于其与二进制有关,且要命的是这个二进制数不能含有前导零,且统计与0有关,这就逼迫我们不得不不记录前导零
那么对于这道题DP的状态就很明显了,设f[i,j]表示有i位的数里有j位是0(不含前导零)的数的数量
不难发现,这个数很容易抽象成:给定i个数,需要从中选出j个强制变成0,并且不能选择第一个数(为开头的1),很明显这就是一个组合数:f[i,j]=Ci1j,值得说的是,我们之所以以0来作为状态而不是1,是因为0相对于1更方便统计,因为组合数可以直接算,这里就不说了,当然也可以借Cnm=Cn1m+Cn1m1这个性质计算
那么我们下面讨论如何求[1,a]中圆数的数量
考虑将a也拆成二进制,假设有tot位(1tot),那么对于二进制表示下位数不足tot的部分就非常容易统计了
这部分的贡献一共是

i=1tot1j=i2i1f[i,j]

很快可以算出来,然后我们考虑第tot位该怎么算
我们设a被二进制分解为atotatot1a1(从高到低)
下面我们按从高到低的顺序进行拼凑
详细地说,我们设当前已经拼到了第i位,第toti+1一共有b个0,当前累计的答案为ans,那么我们就可以分类讨论了

  1. ai=1,此时如果我们拼的数这一位取0,那么这个数后面无论再怎么取都一定不会超过a了,于是我们就可以把取0这一部分的答案累加上,写成公式是:

ans=ans+k=tot2b1i1f[i,k]

  1. ai=0,令b=b+1
    一路统计下来我们就可以得到答案

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int num[55],cnt,tot,c[55][55],n,m,a,b;
#define f(i,j) c[i-1][j]
int solve(int a){
	if(a==0)return 0; 
	tot=0;
	while(a){
		num[++tot]=a&1;
		a>>=1;
	}
	int ans=0;
	for(int i=1;i<tot;i++){
		int k=(i+1)/2;
		for(int j=k;j<=i;j++){
			ans+=f(i,j);
		}
	}
	int b=0;
	for(int i=tot-1;i>0;--i){
		if(num[i]==1){
			for(int p=(tot+1)/2-b-1;p<=i;p++)ans+=f(i,p);//加上这一位填0的收益 
		}
		else b++;
	}
	if(b>=(tot+1)/2)ans++;
	return ans;
}
int main(){
	for(int i=0;i<=30;i++)c[i][0]=1;
	for(int i=1;i<=30;i++){
		for(int j=1;j<=i;j++){
			c[i][j]=c[i-1][j-1]+c[i-1][j];
		}
	}
	scanf("%d%d",&a,&b);
	printf("%d\n",-(solve(a-1)-solve(b)));
} 

例题3:计数问题

这个题直接来一个数学做法

具体看我做题时的手稿:

在这里插入图片描述

至少我认为这还是比较好懂的,我们只需要把问题转换为前缀和来做就可以了,顺带的,19可以像我手稿上的直接做,但是0的时候注意前导零,具体看代码

int cnt(int n,int num) {
    int ans=0,i=1,qd0=0;//前导零
    while(i<=n){
        int l=n/i,r=n%i;
        ans+=(l+9-num)/10*i;
        if(l%10==num)ans+=r+1;
        if(!num)qd0+=i;
        i*=10;
    }
    if(!num)qd0-=1;
    return ans-qd0;
}
int main(){
    int a,b;
    while(~scanf("%d%d",&a,&b)&&a&&b) {
        if(a>b)a^=b,b^=a,a^=b;//装逼必备,把式子展开你就懂了
        for(int i=0;i<=9;++i) 
            printf("%d ",cnt(b,i)-cnt(a-1,i));
        puts("");
    }
    return 0;
}
posted @   spdarkle  阅读(80)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示