数位dp小结

数位dp

对,你没有看错,我最近才会数位dp,我真是太菜了

引入

数位dp显著特点是按数位dp,也就是说按个位十位百位。通常解决 位与位之间有限制,或与位上的数相关的问题。还有个特点就是输入少

通常问题格式为:在区间\([l,r]\)内有多少个满足限制的数,\(l,r<=10^{18}\)

显然的结论为:区间\([l,r]\)的答案为\((区间[1,r]-区间[1,l-1])\)

问题转化为求区间\([1,x]\)的答案(然鹅并没有什么卵用)

求答案的方法即为按数位去dp计算答案。

……

\(f[i]\)表示在第\(i\)位时的答案,从\(f[i-1]\)转移而来

好像没什么好讲的

讲一下细节:

1.实现使用记忆化搜索会更简易

2.一般从高位开始记忆化搜索,方便处理超出上界的情况

例如:求\([1,21]\)的数,十位不能取大于\(2\)的数,如果十位是\(2\),个位不能取大于\(1\)的数,否则个位取啥都行。

实现时用\(limit\)记录\(0或1\)表示到当前状态之前数位有没有触碰上界

3.看题目对前导\(0\)有没有什么限制,不一定每个题目都有

给个不伪的伪代码理解一下:(仅供理解)

//f[x][y]为前x位,状态为y(随题目改变)的答案

int dfs(int x,int y,int st,int limit){
  
	if(到底了)返回值;//if(!x) return 1;
  
	if(已经记忆化过并且没有上限限制)返回当前状态值
    
	int maxx=9;				//当前数位取值上限
	long long sum=0;		//记录状态答案
	if(limit==1)maxx=a[x];	//如果有上界,数位上限缩小
  
	for(枚举数位值)
		if(不符合限制)continue;  
		if(前导0有特殊影响)sum+=dfs(x-1,更新状态,1,0);
		else sum+=dfs(x-1,更新状态,0,是否还触碰上界);
    
  
	if(!limit&&!st) f[x][y]=sum;	//如果有限制,说明前x位还有限制,不记忆化
	return sum;	
}

int solve(long long x){
	memset(f,-1,sizeof(f));len=0;
	while(x)a[++len]=x%10,x/=10;	//拆位
	return dfs(len,-2,1,1);
}

要点:理解清\(limit\)\(st\)限制

还是要结合题目才行


P2657[SCOI2009]windy数

简单的数位dp练手题

自己按模板打打看

注意判前导\(0\)

代码:相当于代码版的模板了

#include<bits/stdc++.h>
#define maxn 20
using namespace std;
long long a[maxn],n,m,len;
long long f[maxn][maxn];
int dfs(int x,int y,int st,int limit){
	if(!x)return 1;		//到底了
	if(f[x][y]!=-1&&limit==0)return f[x][y];	//记忆化过了且没有限制
	int maxx=9;long long sum=0;
	if(limit==1)maxx=a[x];	//更新数位值上限
	for(int i=0;i<=maxx;i++){
		if(abs(y-i)<2)continue;	//不符合限制
		if(st==1&&i==0)sum+=dfs(x-1,-2,1,0);	//前导0处理
		else sum+=dfs(x-1,i,0,limit&&i==a[x]);	//正常处理
	}
	if(!limit&&!st) f[x][y]=sum;	//没有限制记忆化
	return sum;
}
int solve(long long x){
	memset(f,-1,sizeof(f));len=0;
	while(x)a[++len]=x%10,x/=10;
	return dfs(len,-2,1,1);
}
int main(){
	scanf("%lld%lld",&n,&m);
	printf("%d\n",solve(m)-solve(n-1));
	return 0;
}

P4127[AHOI2009]同类分布

题意:给出两个数\(a,b\),求出\(a,b\)中各位数字之和能整除原数的数的个数。

不是位与位的限制了,而是原数与数位的限制了

我们设\(f[x][y][val]\)表示dp到前\(x\)位,此时数位和为\(y\),数的值为\(val\)的答案

但很明显这是不可行的,\(val\)高达\(10^{18}\) 时空爆炸!

我们观察,数位和只有\([1,18*9]\) ​种可能。

那么我们可以考虑枚举数位和\(sum\),那么\(val\)变为在模\(sum\)意义下的数的值,\(val\)的范围便只有\(162\)这么大了,便可以数位dp了

边界为当 \(x=0\)时,当前数位和\(y==sum\),数的值\(val=0(mod\ sum)\)\(1\) 否则为 \(0\)

long long dfs(int x,long long y,long long val,int limit,int mod){
	if(!x&&y==0)return 0;			//特判0
	if(!x) return val==0&&y==mod?1:0;	//边界
	if(!limit&&f[x][y][val]!=-1)return f[x][y][val];	//记忆化
	int maxx=9;long long res=0;
	if(limit) maxx=a[x];
	for(int i=0;i<=maxx;i++){
		res+=dfs(x-1,y+i,(val*10+i)%mod,(limit&&i==maxx)?1:0,mod);
	}
	if(!limit)f[x][y][val]=res;
	return res;
}
long long solve(long long x){
	int len=0;long long ans=0;
	while(x)a[++len]=x%10,x/=10;	
	for(int i=1;i<=9*len;i++){	//枚举数位和
		memset(f,-1,sizeof(f));
		ans+=dfs(len,0,0,1,i);
	}
	return ans;
}

CF55DBeautiful numbers

看大佬博客吧,我太懒了

here T1,另强推并%%%

放个代码

#include<bits/stdc++.h>
#define maxn 51
using namespace std;
int a[2530];
long long f[20][2530][50],l,r;
int Map[2530];
long long dfs(int x,int y,int lcm,bool limit){
	if(!x)return y%lcm==0;
	if(f[x][y][Map[lcm]]!=-1&&!limit)return f[x][y][Map[lcm]];
	int maxx=9;long long sum=0;
	if(limit==1)maxx=a[x];
	for(int i=0;i<=maxx;i++){
		sum+=dfs(x-1,(y*10+i)%2520,lcm*max(i,1)/__gcd(lcm,max(i,1)),limit&&i==maxx);
	}
	if(!limit) f[x][y][Map[lcm]]=sum;
	return sum;
}
long long solve(long long x){
	int len=0;
	if(!x)a[len=1]=0;
	while(x)a[++len]=x%10,x/=10;
	return dfs(len,0,1,1);
}
int main(){
	memset(f,-1,sizeof(f));
	int T,tot=0;
	scanf("%d",&T);
	for(int i=1;i<=2520;i++)if(2520%i==0)Map[i]=++tot;
	while(T--){
		scanf("%lld%lld",&l,&r);
		printf("%lld\n",solve(r)-solve(l-1));
	}
	return 0;
}

P4884 多少个1?

To be continue……

posted @ 2020-09-10 20:25  Hastieyua  阅读(171)  评论(0编辑  收藏  举报