学习笔记:数位DP

引入

数位dp,顾名思义,对数的每一位进行dp,具体我们来看题分析解决。

原理

传送门luoguP2657windy数
此题就是一道非常标准的数位dp的题目,限制相邻位上的数字,要统计范围内的所有满足的数,如果暴力枚举肯定不够优美,那么我们对于每一位进行逐位逐位的确定,那么就能降低大量的复杂度。

查询l~r区间比较麻烦,我们可以求出0~r区间和0~l-1区间,相减即可。
我们用a来表示每一位上的数,然后就进行dfs,其中:
t表示当前所在的位数,pre表示上一位数,st表示是否前面全是0(若前面全是0,那么该位不受2的限制),limit表示是否受最高位限制(比如r=432,若最高位是1,那么下面位数可以任意取,若最高位是4,那么下面位数只能取到3,2)。带入进去判定即可,我们发现,若r=432时,第一次搜到了132,下一次搜到232,那么已经搜过,直接返回,即记忆化搜索。这样我们可以节约大量时间。
代码实现:

#include<bits/stdc++.h>
using namespace std;
#define ll long long

ll dp[15][15];
int a[15],len;

ll dfs(int t,int pre,bool st,bool limit)
{
	if(t==0) return 1;//搜索结束
	if(limit==0 && dp[t][pre]!=0) return dp[t][pre];//记忆化搜索,没有限制的情况下
	ll sum=0;int res=limit?a[t]:9;//是否首高位限制,否则0~9都可以
	for(int i=0;i<=res;i++)
	{
		if(abs(i-pre)<2) continue;
		if(st==1 && i==0) sum+=dfs(t-1,-2,1,limit&&i==res);//如果全是0,下一位没有限制
		else sum+=dfs(t-1,i,0,limit&&i==res); 
	} 
	if(limit==0 && st==0) dp[t][pre]=sum;
	return sum;
}

ll solve(int x)
{
	len=0;
	while(x)
		a[++len]=x%10,x/=10;
	memset(dp,0,sizeof(dp));
	return dfs(len,-2,1,1);//分别表示当前位置,上一位置,是否之前位置全是0,是否有最高位限制
	//由于在最高位,这一位没有受至少差2的限制,但会受最高位限制 
}

int main()
{
	int l,r;
	scanf("%d%d",&l,&r);
	cout<<solve(r)-solve(l-1);
	return 0;
}

再来一道例题传送门luoguP2602数字计数
首先要开long long
我们还是记忆化搜索,枚举每一种数字0~9,搜索是否是这个数,是就统计加1,我们要特别注意0的情况,如果是前导0,那么就不能统计,由于每个数字其实差不多,那么我们可以记下没有限制的dp值,下次搜到直接return。

代码实现:

#include<bits/stdc++.h>
using namespace std;
#define int long long

const int N=100;
int dp[N][N],a[N],len;

int dfs(int t,bool st,bool limit,int sum,int k)
{
	int ans=0;
	if(t==0) return sum;//搜完了,返回总数
	if(limit==0 && st==0 && dp[t][sum]!=-1) return dp[t][sum];
	int res=limit?a[t]:9;
	for(int i=0;i<=res;i++)
	{
		if(st&&!i)
			ans+=dfs(t-1,1,i==res&&limit,sum,k);
		else
			ans+=dfs(t-1,0,i==res&&limit,sum+(i==k),k);
	}
	if(limit==0 && st==0) dp[t][sum]=ans;
	return ans;
}

int solve(int x,int k)
{
	len=0;
	memset(dp,-1,sizeof(dp));//多次使用,初始化
	while(x) a[++len]=x%10,x/=10;
	return dfs(len,1,1,0,k);//前导0,最高位限制,统计的数量,统计哪一个数
}

signed main()
{
	int n,m;
	scanf("%lld%lld",&n,&m);
	for(int i=0;i<=9;i++)
		cout<<solve(m,i)-solve(n-1,i)<<" ";
	return 0;
}

再再来一道例题传送门luoguP4999烦人的作业,
该题和上题差不多,只需统计后的个数乘上这个数,就是该数字总的和,加到一起就ok了。
核心代码:

			ans=((ans+i*(solve(m,i)-solve(n-1,i))+mod)%mod+mod)%mod;

注意可能会出现负的,那么就多模几下。

再再再来一道例题传送门luoguP4317花神的数论题。
根据数据,最多有250,即50位,那么就可以枚举有多少个1,然后去搜索填这么多个1,由于是连乘,可以使用快速幂来解决。
代码实现:

#include<bits/stdc++.h>
using namespace std;
#define int long long

const int N=55,mod=1e7+7;
int dp[N][2][N][N],a[N],len;

int fpow(int u,int v)
{
	int ans=1;
	while(v!=0)
	{
		if(v&1) ans=(ans*u)%mod;
		u=u*u%mod;
		v>>=1;
	}
	return ans;
}

int dfs(int t,int limit,int sum,int k)
{
	if(t==0) return sum==k;
	if(dp[t][limit][sum][k]!=-1) return dp[t][limit][sum][k];
	int res=limit?a[t]:1;
	int ans=0;
	for(int i=0;i<=res;i++)
		ans=ans+dfs(t-1,limit&&i==res,sum+(i==1),k);
	return dp[t][limit][sum][k]=ans;
}

int solve(int x)
{
	while(x) a[++len]=x&1,x>>=1;
	int ans=1;
	for(int i=1;i<=len;i++)
	{
		memset(dp,-1,sizeof(dp));
		ans=(ans*fpow(i,dfs(len,1,0,i)))%mod;
	}
	return ans;
}

signed main()
{
	int n;
	scanf("%lld",&n);
	cout<<solve(n);
	return 0;
}

今天的题有点多嘿嘿

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值