数位dp(从小白到大白)

本文深入探讨了数位动态规划(DP)的概念,通过一系列例题展示了如何利用DP解决区间内满足特定条件的数的个数问题。内容包括相邻数位差至少为2的数、数位不出现特定数字、数位无相邻相同数字、二进制下数位中0的个数不小于1的个数等,并提供了模板代码和记忆化搜索优化。此外,还讨论了与模运算、数位整除相关的DP问题以及多次查询的处理策略。
摘要由CSDN通过智能技术生成

数位dp的学习

经典问题模型

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

例题学习

  • 题目描述: 给定一个区间 ,求其中,将数位拆分后,相邻两个数字相差至少为2的数的个数
  • 问题分析:
  • 利用差分的思想,把求区间 [L,R] 中满足条件的数的个数看成求区间 [ 0 , R ] [0,R] [0,R] 中满足条件的数的个数减去区间 [ 0 , L − 1 ] [0,L-1] [0,L1] 中满足某条件的个数,即 a n s r − a n s l − 1 ans_r - ans_{l-1} ansransl1
  • 其次考虑时间复杂度:用循环的时间复杂度: O ( n l g n ) O(nlgn) O(nlgn);而用 dfs 去枚举每一位的值,则时间复杂度为:O(n)。 要知道搜索时是可以直接按照满足条件的情况下进行 dfs,会有很大程度上的剪枝的,所以我们毫不犹豫的选择了 dfs 的写法。
  • 如何用 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函数判断限制条件。成功遍历到最后一位则该数可以,计数。

数位dp模板

模板如下

#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++){
		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);
}
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);
}

字符串版

  • 如果 L,R 的数字很大,那么要通过字符串形式进行数位 dp ,但是我们不好计算 L-1。不妨计算 [ 0 , R ] − [ 0 , L ] + c h e c k ( L ) [0,R]-[0,L]+check(L) [0,R][0,L]+check(L)。如果传参的话记得传指针,别直接传字符串。例如:传参 string &

记忆化搜索优化

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

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

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

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

前导0的影响

  • 对于出现了前导 0 和未出现前导 0 ,其状态 x , s t {x,st} x,st 往后的分支可能是不一样的,要注意下。

例题的完整代码如下

#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;
}

例题汇总

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

例题1:不出现 62 个数的个数

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

例题2:不存在相邻数相同的数

  • 题目描述: 求区间 [l,r] 的每个数中,其数位不存在相邻数相同的数的个数。
  • 问题分析:前缀状态 st 存储上一个数,i==st,婷婷!成功遍历到最后一位则该数符合题意,计数。
  • 记忆化: f [ x ] [ s t ] f[x][st] f[x][st] 存储状态 {x,st} 往后的所有符合条件的数的个数。举个栗子:当op=0 时,123–和213–往后符合条件的数的个数一样,没必要再次计算。
  • 注意: 未出现首非 0 元素之前,st = 10
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==i)continue;
	    if(i==0&&st==10)ret+=dfs(x-1,10,op&(i==up));
	    else ret+=dfs(x-1,i,op&(i==up));
	}
	if(op==0)f[x][st]=ret;
	return ret;
} 
void solve(){
    dfs(vec.size()-1,10,1);
}

例题3:二进制下的数位中,0的个数不小于1的个数的数的个数

  • 题目描述: 求区间 [ l , r ] [l,r] [l,r] 的每个数中,其二进制下的数位里,0的个数不小于1的个数的数的个数。
  • 问题分析: 以二进制进行数位拆分预处理,前缀状态设置两个,分别为到当前位记录的 0 的数量 st0 和 1 的数量 st1 。dfs 完所有位后判断一下 st0 和 st1 是否符合题意即可,符合则计数。
  • 记忆化:当op=0时,110–和100–往后符合条件的数的个数是一样的,没必要重复计算。设 f [ x ] [ s t 0 ] [ s t 1 ] f[x][st0][st1] f[x][st0][st1] 为该 st0,st1 状态往后符合条件的数的个数。
  • 注意:不要把前导 0 算进了 st0 里了
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;
}
void solve(){
    dfs(vec.size()-1,0,0,1);
}

例题4:数位中非 0 数不超过 3 个的数的个数

  • 题目描述: 求区间 [ l , r ] [l,r] [l,r] 中,数位中非 0 数不超过 3 个的数的个数
  • 问题分析: st 记录非 0 个数即可。
long long dfs(int x,int st,int op){
	if(op==0&&f[x][st]!=-1)return f[x][st];
	if(st>3)return 0;
	if(x==0)return 1;
	int up=op?vec[x]:9;
	long long ret=0;
	for(int i=0;i<=up;i++){
	    if(i==0)ret+=dfs(x-1,st,op&(i==up)); 
		else ret+=dfs(x-1,st+1,op&(i==up));
	}
	if(op==0)f[x][st]=ret;
	return ret;.m  
}

例题5:数位中不同数字不超过 k 个的数的和(状压)

  • 题目描述: 求区间 [ l , r ] [l,r] [l,r] 中,数位中不同数字不超过 k 个的数的和。 r < = 1 e 18 , k < = 10 r<=1e18,k<=10 r<=1e18,k<=10
  • 问题分析: 状压记录状态,用 st 的第 i 位表示数字 i 出现情况,然后记忆化即可。

数位之间无限制,统计所有数的某特征(该特征可以边枚举第 x 位的值边算)

例题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 ] [ s t ] f[x][st] f[x][st] 记忆化存储状态 {x,st} 为从 x 往后的所有分支的数字和。
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;
}

例题2:二进制下 1 的个数的乘积

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

例题3:0-9 在每个数的数位中一共出现多少次

  • 题目描述: 求区间 [ l , r ] [l,r] [l,r] 的每个数中,每个数字各出现了多少次。例如:[12-15],1出现4次,2,3,4,5出现1次
  • 问题分析: 前缀状态 st 自然要存储到当前位下,0-9各出现了多少次?那前缀状态st要存储10个数值?那记忆化搜索的状态设置怎么办?换个思路,我们可以分别计算 0-9 在数位中一共出现多少次,每次数位 dp 只去计算一个数字在所有数中出现的次数。前缀状态 st 存储到当前位下某数字 key 出现的次数, d f s dfs dfs 完所有位后返回 st 计数即可
  • 记忆化: f [ x ] [ s t ] f[x][st] f[x][st] 该状态往后的所有数中 key 出现的总次数。
  • 注意: 计算 0 的时候,前导 0 不要算进去了
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;
}

例题4: ∑ 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](二维)

  • 题目描述: 给定 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
  • 问题分析: 从一维数位到二维数位。考虑存储的状态,对于 l o g 2 ( i + j ) + 1 log2(i+j)+1 log2(i+j)+1 和[1&j==0],很容易想到二进制,由于 i&j = 0,在二进制下 i+j 必然不进位,则 log2(i+j)+1的值就取决于 i+j 的二进制下最高位1所在的位置。那么 dfs 的状态就来了!!!并且同时要保证 i,j 二进制下的每一位都不同。
  • 很显然,二进制下的数位dp。状态{x,st1,st2,fir}为 i 的第 x 位取 st1,j 的第 x 位取 st2,fir 为最高位 1 所在位置。限制条件为第 x 位的 i 和 j 不同时取1。 d f s dfs 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;
}

与 m|x 的值有关的数位 dp

套路

  • 预处理 g [ i ] [ j ] = 1 0 i ( m o d j ) g[i][j]=10^i\pmod j g[i][j]=10i(modj),这样枚举数位的时候,就可以快速计算出 ( m o d m ) \pmod m (modm) 下的值,从而记忆化 dp 。

例题1: ∑ x = L R [ x ≡ f ( x ) ( m o d m ) ] \sum_{x=L}^R [x\equiv f(x)\pmod m] x=LR[xf(x)(modm)] f ( x ) f(x) f(x) 为 x 的两两数位乘积和

  • 题目描述: 求区间 [ L , R ] [L,R] [L,R] 中,满足 x ≡ f ( x ) ( m o d m ) x\equiv f(x)\pmod m xf(x)(modm) 的 x 的个数,答案 ( m o d 1 e 9 + 7 ) \pmod {1e9+7} (mod1e9+7) 10 < = L < = R < = 1 0 5000 , 2 < = m < = 60 。 10<=L<=R<=10^{5000},2<=m<=60。 10<=L<=R<=105000,2<=m<=60
  • 问题分析: x ≡ f ( x ) ( m o d m ) < = > 0 ≡ f ( x ) − x ( m o d m ) x\equiv f(x)\pmod m<=> 0\equiv f(x)-x\pmod m xf(x)(modm)<=>0f(x)x(modm)。设 r e t s u m = f ( x ) − x ( m o d m ) retsum=f(x)-x\pmod m retsum=f(x)x(modm) g [ i ] [ j ] g[i][j] g[i][j] 1 0 j ( m o d i ) 10^j\pmod i 10j(modi),记录到第 x 位的数位和 s u m ( m o d m ) sum\pmod m sum(modm) ,这样枚举第 x 位的值时,就可以快速计算 r e t s u m retsum retsum s u m sum sum 。记忆化 f [ x ] [ s u m ] [ r e t s u m ] f[x][sum][retsum] f[x][sum][retsum] 即可,思考发现其之后的分支是相同的。考虑到 L,R 值很大,需要用字符串形式存储。
#include <bits/stdc++.h>
using namespace std;

int T, n, m,f[5100][61][61],g[5100][61],mod=1e9+7;
string L,R;
int dfs(int x,int op,int sum,int retsum,string &s){
	if(op==0&&f[x][sum][retsum]!=-1)return f[x][sum][retsum];
	if(x==s.size()){
		if(retsum==0)return 1;
		else return 0;
	}
	int up=op?s[x]-'0':9;
	int ret=0;
	for(  int i=0;i<=up;i++){
		ret=(ret+dfs(x+1,op&(i==up),(sum+i)%m,((retsum+sum*i-i*g[s.size()-x-1][m])%m+m)%m,s))%mod;
	}
	if(op==0)f[x][sum][retsum]=ret;
	return ret;
}
int check(string &s){
	int sum=0,retsum=0;
	for(  int i=0;i<s.size();i++){
		retsum=(retsum+sum*(s[i]-'0')-(s[i]-'0')*g[s.size()-i-1][m])%m;
		sum=(sum+s[i]-'0')%m;
	}
	if(retsum==0)return 1;
	else return 0;
}
int solve(string &s){
	for( int i=0;i<=s.size();i++){
		for(  int j=0;j<=m;j++){
			for(  int k=0;k<=m;k++){
				f[i][j][k]=-1;
			}
		}
	}
	return dfs(0,1,0,0,s);
}
int main()
{
    for(int i=2;i<=60;i++){
    	g[0][i]=1;
    	for(int j=1;j<=5000;j++){
    		g[j][i]=g[j-1][i]*10%i;
		}
	}
   	cin>>L>>R>>m;
    cout<<((solve(R)-solve(L)+check(L))%mod+mod)%mod<<endl;
}

例题2:m|x 且 x 的偶数位为 d,奇数位不为 d 的数的个数

  • 题目描述: 求区间 [ l , r ] [l,r] [l,r] 中,m|x 且 x 的偶数位为 d,奇数位不为 d 的数 x 的个数。 r < = 1 0 2000 , 1 < = m < = 2000 , 0 < = d < = 9 r<=10^{2000},1<=m<=2000,0<=d<=9 r<=1020001<=m<=20000<=d<=9
  • 问题分析: 直接根据套路枚举数位即可,记录 f [ x ] [ s t ] f[x][st] f[x][st] 为到第 x 位时,前面的数对 m 的取余为 st 。
#include <bits/stdc++.h>
using namespace std;

int m,d,f[2100][2100],g[2100],mod=1e9+7;
string L,R;
int dfs(int x,int op,int st,string &s) {
	if(op==0&&f[x][st]!=-1)return f[x][st];
	if(x==s.size()) {
		if(st==0)return 1;
		else return 0;
	}
	int up=op?s[x]-'0':9,ret=0;
	if(x%2==0){
		for(int i=0;i<=up;i++){
			if(i!=d)ret=(ret+dfs(x+1,op&(i==up),(st+i*g[s.size()-x-1])%m,s))%mod;
		}
	}
	else{
		if(d<=up)ret=(ret+dfs(x+1,op&(d==up),(st+d*g[s.size()-x-1])%m,s))%mod;
	}
	if(op==0)f[x][st]=ret;
	return ret;
}
int check(string &s) {
	long long value=0;
	for(int i=0; i<s.size(); i++) {
		if(i%2==0) {
			if(s[i]-'0'==d)return 0;
		} else {
			if(s[i]-'0'!=d)return 0;
		}
		value=(value*10+s[i]-'0')%m;
	}
	if(value==0)return 1;
	else return 0;
}
int solve(string &s) {
	for(int i=0; i<=s.size(); i++) {
		for(int j=0; j<=m; j++) {
			f[i][j]=-1;
		}
	}
	return dfs(0,1,0,s);
}
int main() {
	cin>>m>>d;
	g[0]=1%m;
	for(int i=1; i<=2000; i++)g[i]=g[i-1]*10%m;
	cin>>L>>R;
	cout<<((solve(R)-solve(L)+check(L))%mod+mod)%mod;
	return 0;
}

例题3:x 的数位均整除 x 的数的个数

  • 题目描述: 求区间 [ l , r ] [l,r] [l,r] 中,数 x 的数位均整除 x 的数的个数。 r < = 1 e 18 r<=1e18 r<=1e18
  • 问题分析: 假设数 x 只出现 {a1,a2,a3},那么 x 就需要能整除 lcm(a1,a2,a3) ,而这个过程我们是可以轻松计算的。不妨我们枚举 x 出现的数字,然后根据整除这些数字的最大公约数来进行数位 dp 。

多次查询

思路(下面例子与思路没有直接关系,仅多次查询)

  • 在出现样例较多,状态也多的情况时。不管是初始化 f 为 -1,还是数位 dp 的部分,其时间复杂度都超时。
  • 思考我们记录的状态 {x,st,op=0} 表示的意思:其表示数位中第 1 位到第 x 位在没有任何上届限制的情况下。在多次查询,且查询问题一样的情况下,是不需要任何初始化的,因为上次求得的 f 数组,下次还是能用。

例题1:数位中每个数字都出现偶数次的数的个数(状压)

例题链接

  • 题目描述: q 次询问,求区间 [ l , r ] [l,r] [l,r] 中,b 进制下数位中每个数字都出现偶数次的数的个数。 r < = 1 0 18 , q < = 1 e 5 , 2 < = b < = 10 r<=10^{18},q<=1e5,2<=b<=10 r<=1018q<=1e52<=b<=10
  • 问题分析: 状压记录状态,用 st 的第 i 位表示数字 i 的出现次数的奇偶性,然后记忆化即可。注意要处理前导 0 。到第 x 位,且前面状态为 st ,该位是否能取最大之后的分支是不一样的,出现前导 0 和未出现前导 0 的之后的分支耶是不一样的。
#include <bits/stdc++.h>
using namespace std;
vector<int>vec;

long long f[80][1025][11],b;
long long dfs(int x,int st,int op,int key) {
	if(op==0&&key==1&&f[x][st][b]!=-1)return f[x][st][b];
	if(x==0){
		if(st==0&&key==1)return 1;
		else return 0;
	}
	int up=op?vec[x]:b-1;
	long long ret=0;
	for(int i=0;i<=up;i++){
		if(key==0&&i==0)ret+=dfs(x-1,st,op&(i==up),0);
		else ret+=dfs(x-1,st^(1<<i),op&(i==up),1);
	}
	if(op==0&&key==1)f[x][st][b]=ret;
	return ret;
}
long long run(long long n) {
	if(n==0)return 0;
	vec.clear();
	vec.push_back(-1);
	while(n) {
		vec.push_back(n%b);
		n=n/b;
	}
	return dfs(vec.size()-1,0,1,0);
}
int main(){
	memset(f,-1,sizeof(f));
	long long T,l,r;
	cin>>T;
	while(T--){
		cin>>b>>l>>r;
		cout<<run(r)-run(l-1)<<endl;
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值