Loading

数位dp杂记

突然想对数位dp做个总结,奈何有些做过的题目忘记题号了,待发现的时候会加上,所以本文会随时更新

数位dp,顾名思义,就是对数字的每一位进行dp,但是它的本质就是记忆化搜索

所以首先我们要把数字拆开:

fx(int,solve)(int num){
	set(dig,0,dig,1);high=0;
	while(num){
		dig[++high]=num%10;
		num/=10;
	}
	return dp(...);
}

这种dp通常与数字的大小无关,而与状态数和数字位数有关


例如我们要找一个区间 \([l,r]\) 中不含数字 \(4\) 的数的个数

很容易想到枚举每一位,之后对每一位进行讨论

但是状态数是 \(9^n\) 的,需要优化

我们很容易想到:假设现在考虑到了第 \(np\) 位,并且前 \(np-1\) 位都没有 \(4\)

那么这一位不管选多少(除 \(4\) 外),都是剩下 \(n-np\) 位,并且它们的dp结果相同

我们可以用一个数组 \(\text{dpv}[np]\) 来记录剩下 \(n-np\) 位的方法数

\(\text{dpv}\) 数组经常用来记录某个状态下得到的结果,以防止搜索重复状态,不同情况下可能要多开几维

在对上界或是前导 \(0\) 进行处理的时候,我们可以多传几个参数来记录当前情况,以方便用来判断

具体情况具体分析


[HDU2089] 不要62

题意简介:

求出给定区间中满足:\(6\)\(2\) 不能相邻且不含 \(4\) 的数的个数

分析:

超级简单的入门题

对于不含 \(4\) 我们可以在枚举的时候判断

\(6\)\(2\) 不能相邻的情况我们可以用变量 \(pre\) 记录上一位选的数,之后如果 pre==6&&i==2 就跳过,统计即可

#define fx(l,n) inline l n
fx(int,dp)(int np,int pre,bool limit){
    if(!np) return 1;
    if(!limit&&dpv[np][pre==6]) return dpv[np][pre==6];
    int al=limit?digit[np]:9,sum=0;
    for(int i=0;i<=al;i++){
        if(i==4||(pre==6&&i==2)) continue;
        sum+=dp(np-1,i,limit&&i==al);
    }
    if(!limit) dpv[np][pre==6]=sum;
    return sum;
}

[ZJOI2010] 数字计数

题意简介:

求出在给定区间的所有正整数中,每个数码出现的次数

分析:

对于一个整数 \(\overline{ABCD\cdots Z}\) 我们可以将其分开统计,即先统计 \(\overline{A000\cdots0}\) 再统计 \(\overline{B00\cdots0}\) 以此类推,直到最后

对于 \(n\) 位数 \(\overline{A00\cdots0}\) 不难发现就是将区间 \([000\cdots0,999\cdots9]\) 每个数码出现的次数统计起来,乘 \(A\),最后加上 \(A\) 出现的次数即可

之后我们将统计的前导 \(0\) 减掉,顺次统计

#define fx(l,n) inline l n
#define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
fx(void,solve)(int num){
	int cp=num;
	set(digit,0,int,50);high=0;
	set(ans,0,int,N);
	while(num){
		digit[++high]=num%10;
		num/=10;
	}
	for(R i=high;i>0;i--){
		for(int o=0;o<=9;o++) ans[o]+=pz[i-1]*digit[i];
		for(int o=0;o<digit[i];o++) ans[o]+=pow[i-1];
		ans[digit[i]]+=cp%pow[i-1]+1;
		ans[0]-=pow[i-1];
	}
}

[SCOI2009] windy 数

题意简介:

求区间 \([l,r]\) 内相邻两个数字之差至少为 \(2\) 的正整数

分析:

也是一个比较裸的题目,但是本题前导 \(0\) 会影响答案,所以我们要增加一个前导 \(0\) 标记

之后就是原样枚举,注意判断的时候要取绝对值

#define fx(l,n) inline l n
fx(int,dp)(int np,int pre,bool pz,bool limit){
	if(!np) return 1;
	if(!limit&&!pz&&dpv[np][pre]) return dpv[np][pre];
	int al=limit?dig[np]:9,sum=0;
	for(int i=0;i<=al;i++){
		if((llabs(pre-i)<2)&&!pz) continue;
		int po=dp(np-1,i,pz&&!i,limit&&(i==al));
		sum+=po;
	}
	if(!limit&&!pz) dpv[np][pre]=sum;
	return sum;
}

花神的数论题

题意简介:

\(\text{sum}(i)\) 表示 \(i\) 的二进制中 \(1\) 的个数,求:

\[\begin{align*} \prod\limits^N_{i=1}\text{sum}(i) \end{align*} \]

分析:

\(\text{num}(i)\)\(1\sim N\) 中二进制中 \(1\) 的个数等于 \(i\) 的数的个数

即:\(\prod\limits^N_{i=1}\text{sum}(i)=\prod\limits_{i=1}i^{\text{num}(i)}\)

数位dp即可,代码较垃圾就不放了

众所周知在 \(n\) 位二进制中,二进制中 \(1\) 的个数等于 \(i\) 的数的个数共有 \(\dbinom{n}{i}\)

所以我们可以将递归转为枚举位

for(int i=50,o;i!=-1;i--){
	for(o=50;o;o--) num[o]+=num[o-1];
	if((n>>i)&1){
		num[high]+=1;
		high+=1;
	}
}
num[high]+=1;
for(int i=1;i<=50;i++) (ans*=pow(i,num[i]))%=mod;

Beautiful numbers

题意简介:

求出区间 \([l,r]\) 内能被它每个数位上的数整除的数

分析:

首先我们知道 \(1\sim10\) 的最小公倍数是 \(2520\),于是我们对于每层递归进行计算后 \(\%2520\) 即可

我们还需统计当前已经选择的数的最小公倍数,这可以预处理直接拿来用

之后我们在第 \(0\) 位判断余数是否整除,如果是 \(0\) 就统计

暴力数位dp即可

之后会TLE与MLE双丰收

考虑如何优化,我们用来进行数位dp的数组有两维,即现在位数和当前递归层的模数已经不可优化了,考虑优化第三维

我们发现有效的状态当且仅当在第 \(0\) 位的时候余数模最小公倍数为 \(0\)

而这个最小公倍数一定是 \(2520\) 的一个因数

而不管我们选择了哪些数,只要它们的最小公倍数相同,它们就一定是同一种状态

我们成功的将第三维的大小优化至 \(50\)

预处理 \(2520\) 的每一个因数,之后进行数位dp即可

fx(int,dp)(int np,int amn,int q,bool lmt){
	if(!np) return amn%q==0;
	if(!lmt&&dpv[amn][mp[q]][np]!=-1) return dpv[amn][mp[q]][np];
	int al=lmt?dig[np]:9,sum=0;
	for(int i=0;i<=al;i++){
		sum+=dp(np-1,(amn*10+i)%2520,i?(q*i/__gcd(q,i)):q,lmt&&i==al);
	}
	if(!lmt) dpv[amn][mp[q]][np]=sum;
	return sum;
}
signed main(){
	set(dpv,-1,dpv,1);
	for(int i=1;i<=2520;i++) if(2520%i==0) mp[i]=++ys;
}
posted @ 2021-02-04 11:02  zythonc  阅读(96)  评论(0编辑  收藏  举报