数位dp(从小白到大白)

经典问题模型

  • 给定一个闭区间 [L,R],求区间中满足某条件的数的个数

例题

  • 题目大意:给定一个区间 ,求其中,将数位拆分后,相邻两个数字相差至少为2的数的个数。

问题分析

  1. 首先,利用差分的思想,我们把求区间 [L,R] 中满足条件的数的个数看成求区间 [0,R] 中满足条件的数的个数减去区间 [0,L-1] 中满足某条件的个数,即 ans r _r r - ans l − 1 _{l-1} l1
  2. 其次考虑时间复杂度:用循环的时间复杂度:O( n l g n nlgn nlgn);而用 dfs 去枚举每一位的值,则时间复杂度为:O(n)。
    要知道搜索时是可以直接按照满足条件的情况下进行 dfs,会有很大程度上的剪枝的,所以我们毫不犹豫的选择了 dfs 的写法。
  3. 如何用 dfs 来写?

数位dp

  • 数位dp其实就是用dfs加状态加记忆化搜索的方法实现一个dp

思路分析

  • 假设 n 的每一位分别为 a[i]
  • 分析一下每一位:
  • 最高位:[0,a[i]]
  • 次高位:当最高位取a[i]时,次高位只能取 [0,a[i-1]]
        当最高位不取a[i]时,次高位可以取 [0,9]
  • 非最高位:只要其高位有一个没取a[i],该位就可以任取 [0,9]

状态设置

  • dfs设置三个状态:x,st,op,
  • 设最低位为第 1 位,dfs从高位到低位
  • x:判断第x位要选什么数
  • st:前缀状态,通常为上一位置选择的数。由于最高位没有上一位,为方便起见,有时我们设:当 st = 10时,暂时未出现最高位。
  • op:
  • 当op=1,其所有高位均选择了最大值,第x位只能选 [0,a[x]]
  • 当op=0,其高位有一位没选择最大值,第x位可选 [0,9]

当没有任何状态限制时,根据题意我们写出了以下代码,刚好遍历了0-n的所有数,无一重复,也无一越界。
当我们需要有状态限制时,加入一个注释掉的check函数判断限制条件。成功遍历到最后一位则该数可以,计数。

void dfs(int x,int st,int op){
    
    
	if(x==0){
    
    
		ans++;
		return;
	}
	int up=op?a[x]:9;
	for(int i=0;i<=up;i++){
    
    
	    //if(check(i,st)==0)continue;
		dfs(x+1,i,op&(i==up));
	}
}
int main(){
    
    
	dfs(1,-1,1);

这是一个O(n)时间复杂度的算法,易超时,需要优化
如何优化???记忆化搜索

记忆化搜索优化

  • 状态{x,st,op}的dfs分支会不止走一次!!!
    但是往后dfs过程得到的总答案数是相等的

  • 例如上面例题中:
    假设n为5位数,且最高位为5,则12—和22—在dfs均走到了dfs(3,2,0)这个状态,且该状态往后的所有dfs分支均一致,没有必要重新计算一遍。

  • 即:当我们走完dfs(x,st,op)这个状态的时候,我们记录一个f[x][st][op]往后走得到的答案总数,则下一次走到{x,st,op}的时候不需要再次计算。

  • 由于op=1的{x,st,op}分支一定只有一个,因此我们不记录。只用一个数组 f[x][st] 来记录 {x,st,0} 的分支

  • 由于第一位数没有限制条件,我们令st=11时表示暂时未出现第一位数,因为abs(11-i)>=2恒成立

例题的完整代码如下

扫描二维码关注公众号,回复: 13121298 查看本文章
#include<bits/stdc++.h>
using namespace std;

vector<int>vec;

long long f[70][15];

long long dfs(int x,int st,int op) {
    
    
	if(op==0&&f[x][st]!=-1)return f[x][st];
	if(x==0)return 1;
	
	int up=op?vec[x]:9;
	long long ret=0;
	for(int i=0;i<=up;i++){
    
    
		if(abs(st-i)<2)continue;
		if(st==11&&i==0)ret+=dfs(x-1,11,op&(i==up));
		else ret+=dfs(x-1,i,op&(i==up));
	}
	if(op==0)f[x][st]=ret;
	return ret;
}
long long run(long long n) {
    
    
	memset(f,-1,sizeof(f));
	vec.clear();
	vec.push_back(-1);
	while(n) {
    
    
		vec.push_back(n%10);
		n=n/10;
	}
	return dfs(vec.size()-1,11,1);
}
int main() {
    
    
	long long l,r;
	cin>>l>>r;
	cout<<run(r)-run(l-1)<<endl;
	return 0;
}

做些例题强化一下

以下是以state前缀状态的不同进行简单的分类
上面的那道例题属于第一类
题目大部分来自洛谷:洛谷链接

分类1:数位之间相互限制,计算满足某条件的数的个数

例题1-1

  • 题目描述:不要62。求区间 [l,r] 中数位不出现62的数的个数。例如:621,1362均出现62,1263没出现62。
  • 问题分析:前缀状态st存储上一个数,st=6且i=2则该数出现62,婷婷!成功遍历到最后一位则该数符合题意,计数。
    记忆化: f[x][st]存储状态{x,st}往后的所有符合条件的数的个数。举个栗子:当op=0时,123–和213–往后符合条件的数的个数一样,没必要再次计算。
vector<int>vec;
long long f[30][30];

int dfs(int x,int st,int op){
    
     
    if(op==0&&f[x][st]!=-1)return f[x][st];
    if(x==0)return 1;
    
	int up=op?vec[x]:9;
	long long ret=0;
	for(int i=0;i<=up;i++){
    
    
	    if(st==6&&i==2)continue;
	    ret+=dfs(x-1,i,op&(i==up));
	}
	if(op==0)f[x][st]=ret;
	return ret;
} 

long long run(int n){
    
    
	memset(f,-1,sizeof(f));
	vec.clear();
	vec.push_back(-1);
	while(n){
    
    
		vec.push_back(n%10);
		n/=10;
	}
	return dfs(vec.size()-1,10,1);
} 

int main(){
    
    
	long long l,r;
	cin>>l>>r;
	cout<<run(r)-run(l-1);
	return 0;
}

例题1-2

  • 题目描述:非连续数。求区间 [l,r] 的每个数中,其数位不存在相邻数相同的数的个数。例如:123,121符合题意,112,122不符合题意

例题1-3

  • 题目描述:圆数计数。求区间 [l,r] 的每个数中,其二进制下的数位里,0的个数不小于1的个数的数的个数。
  • 问题分析:以二进制进行数位拆分预处理,前缀状态设置两个,分别为到当前位记录的0的数量st0和1的数量st1。dfs完所有位后判断一下st0和st1是否符合题意即可,符合则计数。
    记忆化:当op=0时,110–和100–往后符合条件的数的个数是一样的,没必要重复计算。设f[x][st0][st1]为该st0,st1状态往后符合条件的数的个数。ps:需要注意的是不要把前导0算进了st0里了
#include<bits/stdc++.h>
using namespace std;

vector<int>vec;

long long f[70][100][100];

long long dfs(int x,int st0,int st1,int op) {
    
    
	if(op==0&&f[x][st0][st1]!=-1)return f[x][st0][st1];
	if(x==0){
    
    
		if(st0>=st1)return 1;
		else return 0;
	}

	int up=op?vec[x]:1;
	long long ret=0;
	for(int i=0; i<=up; i++) {
    
    
		if(st1==0){
    
    
			if(i==0)ret+=dfs(x-1,st0,st1,op&(i==up));
			else ret+=dfs(x-1,st0,st1+1,op&(i==up));
		}
		else {
    
    
			if(i==0)ret+=dfs(x-1,st0+1,st1,op&(i==up));
			else ret+=dfs(x-1,st0,st1+1,op&(i==up));
		}
	}
	if(op==0)f[x][st0][st1]=ret;
	return ret;
}
long long run(long long n) {
    
    
	memset(f,-1,sizeof(f));
	vec.clear();
	vec.push_back(-1);
	while(n) {
    
    
		vec.push_back(n%2);
		n=n/2;
	}
	return dfs(vec.size()-1,0,0,1);
}
int main() {
    
    
	long long l,r;
	cin>>l>>r;
	cout<<run(r)-run(l-1)<<endl;
	return 0;
}

例题1-4

  • 问题描述:回文串

例题1-5

  • 问题描述:B进制下的子串和

分类2:数位之间无限制,计算所有数的某特征

例题2-1

  • 题目描述:数字和。求区间 [l,r]中每个数的数字和的和,答案mod 1e9+7。例如:123的数字和是1+2+3=6
  • 问题分析:前缀状态st存储到当前位数的所有数字之和(123-,此时st=6),dfs到最后一位则返回st。
    记忆化:当op=0时,12–和21–往后走的所有分支数的数字和都是一样的,因为所有的分支的数字和都加上6也就是st。f[x][st] 记忆化存储状态{x,st}为从x往后的所有分支的数字和。
#include<bits/stdc++.h>
using namespace std;

vector<int>vec;
long long f[30][1000],mod=1e9+7;

long long dfs(int x,int st,int op) {
    
    
	if(op==0&&f[x][st]!=-1)return f[x][st];
	if(x==0)return st;

	int up=op?vec[x]:9;
	long long ret=0;
	for(int i=0; i<=up; i++) {
    
    
		ret=(ret+dfs(x-1,st+i,op&(i==up)))%mod;
	}
	if(op==0)f[x][st]=ret;
	return ret;
}
long long run(long long n) {
    
    
	memset(f,-1,sizeof(f));
	vec.clear();
	vec.push_back(-1);
	while(n) {
    
    
		vec.push_back(n%10);
		n=n/10;
	}
	return dfs(vec.size()-1,0,1);
}
int main() {
    
    
	int T;
	cin>>T;
	while(T--) {
    
    
		long long l,r;
		cin>>l>>r;
		cout<<((run(r)-run(l-1))%mod+mod)%mod<<endl;
	}
	return 0;
}

例题2-2

  • 题目描述:求区间 [1,n] 的每个数中,其二进制下1的数目的乘积,答案mod 1e7+7。例如:n=4,ans=1x1x2x1=2
  • 问题分析:以二进制进行数位拆分预处理,前缀状态st存储到当前位记录的1的数目,dfs完所有位后判断乘上st即可,要注意的是0要特判。
    记忆化: 当op=0,110–和100–往后的所有数的st之积是一样的,没必要重复计算。设f[x][st]为状态{x,st}往后的所有数的st之积。
#include<bits/stdc++.h>
using namespace std;

vector<int>vec;

long long f[100][100],mod=1e7+7;

long long dfs(int x,int st,int op) {
    
    
	if(op==0&&f[x][st]!=-1)return f[x][st];
	if(x==0){
    
    
		if(st!=0)return st;
		else return 1;
	}
	
	int up=op?vec[x]:1;
	long long ret=1;
	for(int i=0;i<=up;i++){
    
    
		if(i==1)ret=(ret*dfs(x-1,st+1,op&(i==up)))%mod;
		else ret=(ret*dfs(x-1,st,op&(i==up)))%mod;
	}
	if(op==0)f[x][st]=ret;
	return ret;
}
long long run(long long n) {
    
    
	memset(f,-1,sizeof(f));
	vec.clear();
	vec.push_back(-1);
	while(n) {
    
    
		vec.push_back(n%2);
		n=n/2;
	}
	return dfs(vec.size()-1,0,1);
}
int main() {
    
    
	long long n;
	cin>>n;
	cout<<run(n);
	return 0;
}

例题2-3

  • 题目描述:数字计数。求区间 [l,r] 的每个数中,每个数字各出现了多少次。例如:[12-15],1出现4次,2,3,4,5出现1次
  • 问题分析:做完前三题,思路应该清晰了许多,前缀状态st自然要存储到当前位下,0-9各出现了多少次。那前缀状态st要存储10个数值?那记忆化搜索的状态设置怎么办?不不不,这不好。换个思路,我们可以分别计算0-9在每个数中出现了多少次,每次数位dp只去计算一个数字在所有数中出现的次数。啊,那这不是有手就行吗?
    前缀状态st存储到当前位下某数字key出现的次数,dfs完所有位后返回st计数即可
    记忆化: 设f[x][st]该状态往后的所有数中key出现的总次数。
  • 但要注意的是在计算0在所有数中出现的总次数时,前导0的0不算。(但数字0的0出现次数为1)。我们单独处理一下0的情况,用一个形参bool0存储数字是否已出现首非0,即数字的第一位(bool0=0表示未出现)。由于状态{x,st,0}和状态{x,st,1}最后的结果肯定不一样,因此我们记忆化搜索时需要多存储一个状态,即存储状态{x,st,bool0}。如果是求区间 [0,n] 的0出现的次数,还需特判一下单独的数字0的情况(这道题不用)。为了方便代码书写,我们将数字0和其他数字的合并一下,其他数字的bool1恒为0就好
#include<bits/stdc++.h>
using namespace std;

vector<int>vec;

long long f[100][100][3];

long long dfs(int x,int st,int key,int bool1,int op) {
    
    
	if(op==0&&f[x][st][bool1]!=-1)return f[x][st][bool1];
	if(x==0)return st;

	int up=op?vec[x]:9;
	long long ret=0;
	for(int i=0; i<=up; i++) {
    
    
		if(key==0) {
    
    
			if(i==0) {
    
    
				if(bool1==0)ret+=dfs(x-1,st,key,bool1,op&(i==up));
				else ret+=dfs(x-1,st+1,key,bool1,op&(i==up));
			} else {
    
    
				ret+=dfs(x-1,st,key,1,op&(i==up));
			}
		} else {
    
    
			if(i==key)ret+=dfs(x-1,st+1,key,bool1,op&(i==up));
			else ret+=dfs(x-1,st,key,bool1,op&(i==up));
		}

	}
	if(op==0)f[x][st][bool1]=ret;
	return ret;
}
long long run(long long n,int key) {
    
    
	memset(f,-1,sizeof(f));
	vec.clear();
	vec.push_back(-1);
	while(n) {
    
    
		vec.push_back(n%10);
		n=n/10;
	}
	return dfs(vec.size()-1,0,key,0,1);
}
int main() {
    
    
	long long l,r;
	cin>>l>>r;
	for(int i=0; i<=9; i++) {
    
    
		cout<<run(r,i)-run(l-1,i)<<" ";
	}
	return 0;
}

例题2-4

  • 题目描述:给定A,B;
    ∑ i = 0 A \sum_{i=0}^A i=0A ∑ j = 0 B \sum_{j=0}^B j=0B ( l o g 2 ( i + j ) + 1 ) (log2(i+j)+1) (log2(i+j)+1)(i&j==0) ,且 i,j 不同时为0
    0<=A,B<=1e9
  • 问题分析:以前是一维的数位,现在是二维的数位。但其实道理一样。考虑存储的状态,观察这个 log2(i+j)+1 和1&j == 0,很容易想到二进制,由于 i&j = 0,在二进制下 i+j 必然不进位,则 log2(i+j)+1的值就取决于 i+j 的二进制下最高位1所在的位置。那么dfs的状态就来了!!!
    很显然,二进制下的数位dp。
    状态{x,st1,st2,fir}为 i 的第x+1位取st1,j 的第x+1位取st2,fir为最高位1所在位置。限制条件为第x位的 i 和 j 不同时取1。dfs到尽头则该数可以,返回fir。
    记忆化:懂的都懂
#include<bits/stdc++.h>
using namespace std;

vector<int>vec1,vec2;
long long mod=1e9+7;
long long f[100][2][2][100];

long long dfs(int x,int st1,int op1,int st2,int op2,int fir){
    
    
	if(op1==0&&op2==0&&f[x][st1][st2][fir]!=-1)return f[x][st1][st2][fir];
	if(x==0)return fir;
	
    long long ret=0;
    int up1=op1?vec1[x]:1;
    int up2=op2?vec2[x]:1;
    for(int i=0;i<=up1;i++){
    
                         
    	for(int j=0;j<=up2;j++){
    
    
    		if(i+j==2)continue;
    		if(fir==0&&i^j!=0)fir=x;
    		ret+=dfs(x-1,i,op1&(i==up1),j,op2&(i==up2),fir);
		}
	}
	if(op1==0&&op2==0)f[x][st1][st2][fir]=ret;
	return ret;
}
long long run(long long A,long long B){
    
    
	vec1.push_back(-1);
	vec2.push_back(-1);
	memset(f,-1,sizeof(f));
	
	while(A||B){
    
    
		vec1.push_back(A%2);
		vec2.push_back(B%2);
		A=A/2;
		B=B/2;
	}
	cout<<vec1.size()-1;
	return dfs(vec1.size()-1,1,1,1,1,0);
}
int main(){
    
    
	int A,B;
	cin>>A>>B;
	cout<<run(A,B);
	return 0;
}

例题2-5

猜你喜欢

转载自blog.csdn.net/weixin_43602607/article/details/115137280
今日推荐