数位dp

8.29冯巨讲完没有写,10.15被叫上去讲课,于是花了一个上午看了一下。


数位dp

数位dp用于解决一类问题:求给定区间中,满足给定条件的某个 \(D\) 进制数或此类数的数量。给定条件往往与数位有关,例如数位之和、指定数码个数、数的大小顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,由于数的位数是 \(\log(n)\) 级别,我们就需要利用数位的性质,设计 \(\log(n)\) 级别复杂度的算法。解决这类问题最基本的思想就是“逐位确定”的方法。

大多情况下,题目要求 \([l,r]\) 中符合要求的数的个数,

数位dp名义上是dp,实则不好用递推来解决,通常会使用记忆化搜索。

大多情况下,题目要求 \([l,r]\) 中符合要求的数的个数,直接写搜索非常不好写,既有上界又有下界。所以一般都会差分一下,求 \(solve(r)-solve(l-1)\),这样就只有上界,好写的多。

用记忆化搜索非常好写,首先写一个爆搜:

int dfs(int now/*当前在哪一位*/,/*为满足题目条件记录的有用值*/,bool limit/*是否顶到上界*/){
	if(now==0)return ;//满足题目要求返回1,否则返回0
	int res=0,end=limit?a[now]:9;//循环这一位,如果顶到上界就只能循环到a[now]
	for(int i=0;i<=end;i++){
		if()continue;//不满足题目条件的跳过
		res+=dfs(now-1,/*记录有用值*/,limit&&i==end/*是否顶到上界*/);
	}
	return res;
}

显然复杂度是 \(O(ans)\),使用记忆化搜索:

int dfs(int now,,bool limit){
	if(now==0)return ;
    
    if(!limit&&f[now][...])return f[now][...];
    
	int res=0,end=limit?a[now]:9;
	for(int i=0;i<=end;i++){
		if()continue;
		res+=dfs(now-1,,limit&&i==end);
	}
    
    if(!limit)f[now][...]=res;
    
	return res;
}

其中 \(f[now][...]\),代表的是不受限制,第 \(now\) 位,在 \(...\) 状态下的方案总数。

也就是说顶到上界的情况不能这样算。\(...\) 比较灵活,可以根据题目条件或你记录的有用值来设。

由于顶到上界或者受到限制的情况很少,所以这样的时间复杂度就是 \(f\) 的状态数。

不理解就先看题


例题

不要62

题目要求 \([l,r]\) 中不出现 \(62\)\(4\) 的数的个数。

于是我们要求 \([0,n]\) 中不出现 \(62\)\(4\) 的数的个数,对于 \(4\) ,我们只要搜索时不搜 \(4\) 即可,但对于 \(2\),我们需要知道这一位前面是不是 \(6\),所以搜索的第二个值就记录前一位是不是 \(6\) 就好了。

\(f[now]\) 就是第 \(now\) 位时的方案数。

看一下代码就懂了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
inline int read(){
	int p=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
	return p*f;
}

int f[20];
int a[20],an;
int dfs(int now,bool is6,bool limit){
	if(now==0)return 1;
	if(!limit&&!is6&&f[now])return f[now];
	int res=0,end=limit?a[now]:9;
	for(int i=0;i<=end;i++){
		if(i==4||(is6&&i==2))continue;
		res+=dfs(now-1,i==6,limit&&i==end);
	}
	if(!limit&&!is6)f[now]=res;
	return res;
}
int solve(int x){
	an=0;
	while(x){
		a[++an]=x%10;
		x/=10;
	}
	return dfs(an,false,true);
}
signed main(){
	int n,m;
	n=in,m=in;
	while(n||m){
		cout<<solve(m)-solve(n-1)<<endl;
		n=in,m=in;
	}
	return 0;
}

windy 数

和上面差不多,记录的变成了前一个数是多少。

这题要注意前导 \(0\) 的情况,所以还要记录前面是否全是 \(0\)

当前面全是 \(0\) 的时候,当前位不能受前一位的影响,所以把前一位随便设一个不会影响的值就好了,比如 11 或者114515也行

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
inline int read(){
	int p=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
	return p*f;
}

int a[20],an;
int f[15][2][20];
int dfs(int now,int last,bool all0,bool limit){
	if(now==0)return 1;
	if(!limit&&f[last][all0][now])return f[last][all0][now];
	int res=0,end=limit?a[now]:9;
	for(int i=0;i<=end;i++){
		if(abs(i-last)<2)continue;
		res+=dfs(now-1,(all0&&i==0)?11:i,all0&&i==0,limit&&i==end);
	}
	if(!limit)f[last][all0][now]=res;
	return res;
}

int solve(int x){
	an=0;
	while(x){
		a[++an]=x%10;
		x/=10;
	}
	return dfs(an,11,1,1);
}
signed main(){
	int n,m;
	n=in,m=in;
	cout<<solve(m)-solve(n-1);
	return 0;
}


Amount of Degrees

这题有些需要思考的地方。

一个数要等于 \(K\) 个互不相等的 \(B\) 的整数次幂之和,那么在 \(B\) 进制下,它的表示中一定只有 \(0,1\) 且恰好有 \(K\)\(1\),于是可以记录已经有多少个 \(1\) 然后按套路做。

注意这里的上界会有一定变化,将上界化为 \(B\) 进制后,找到他从高到低第一个大于 \(1\) 的位置,将这一位及更低位变成 \(1\) 就是这里的上界,想一下就知道这样很对。

上述做法:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
inline int read(){
	int p=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
	return p*f;
}
int k,b;
int a[50],an;
int f[50][20];
int dfs(int now,int had,bool limit){
	if(now==0)return had==0;
	if(!limit&&f[now][had])return f[now][had];
	int res=0,end=limit?a[now]:1;
	
	for(int i=0;i<=end;i++){
		if(!had&&i==1)continue;
		res+=dfs(now-1,had-(i==1),limit&&i==end);
	}
	if(!limit)f[now][had]=res;
	return res;
}

int solve(int x){
	an=0;
	while(x){
		a[++an]=x%b;
		x/=b;
	}
	for(int i=an;i>=1;i--){
		if(a[i]>1)
			while(i)
				a[i--]=1;
	}
	return dfs(an,k,1);
}

signed main(){
	int n,m;
	n=in,m=in,k=in,b=in;
	cout<<solve(m)-solve(n-1);
	return 0;
}

但对于数位dp,还有一种很有用的东西能够帮助你思考,就是模拟一颗完全二叉树,

像这样,每层代表一位,每条路径代表一棵树,在这棵树上思考。

我们发现,从顶点向 \(n\) 走,一旦遇到 \(1\),就代表左子树内都可以被统计,因为每层代表每位,所以我们只需要在 \(x\) 层中有 \(y\)\(1\) 的数量,发现其实就是组合数 \(\binom x y\),也就是说预处理出组合数之后就可以线性做,这样每次统计左子树的答案,会漏掉 \(n\) 自己,特殊处理一下即可。

上述做法:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
inline int read(){
	int p=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
	return p*f;
}
int k,b;
int a[32],an;

int f[32][32];
void getf(){
	f[0][0]=1;
	for(int i=1;i<=31;i++)
		for(int j=1;j<=i;j++)
			f[i][j]=f[i-1][j-1]+f[i-1][j];
}

int solve(int x){
	an=0;
	while(x){
		a[++an]=x%b;
		x/=b;
	}
	for(int i=an;i>=1;i--){
		if(a[i]>1)
			while(i)
				a[i--]=1;
	}
	int res=0,temp=0;
	for(int i=an;i>=1;i--)
		if(a[i]==1)res+=f[i][k-temp+1],temp++;
	if(temp==k)res++;
	return res;
}
signed main(){
	int n,m;
	n=in,m=in,k=in,b=in;
	getf();
	cout<<solve(m)-solve(n-1);
	return 0;
}


可以找国集队爷刘聪的《浅谈数位类统计问题》来看。


8.29 笔记:

冯巨推题

https://blog.csdn.net/sslz_fsy/article/details/88618061
https://blog.csdn.net/sslz_fsy/article/details/87367688
https://blog.csdn.net/sslz_fsy/article/details/82284529
https://blog.csdn.net/sslz_fsy/article/details/95241942

posted @ 2021-10-27 08:58  llmmkk  阅读(39)  评论(0)    收藏  举报