动态规划数位dp

概述

数位动态规划(数位DP)主要用于解决“在区间[l,r]这个范围内,满足某种约束的数字的数量、总和、平方“这一类问题。

这种问题一般有两种解法,记忆化搜索和迭代法,本文主要讲解记忆化搜索。

本文总结了数位dpdfs搜索函数的几种常见形参,及对于数字的约束形参之间转换,后有几道例题,用于更加系统化、套路化攻克这一类题目。

 例题

例题1烦人的数学作业 ​​​​​​

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

typedef long long ll;
const int mod=1e9+7,N=20;
int a[N];
int f[N][9*18+5];

ll dfs(int pos,bool limit,int sum)
{
	if(!pos)
		return sum; 
		
	if(!limit && (~f[pos][sum]))
		return f[pos][sum];
		
	int up=limit ? a[pos]:9;
	ll res=0;
	for(int i=0;i<=up;i++)
		res=(res+dfs(pos-1,limit&&i==up,sum+i))%mod;
	
	if(!limit)
		f[pos][sum]=res;
	
	return res;
}


ll solve(ll x)
{
	int cnt=0;
	while(x>0)
	{
		a[++cnt]=x%10;
		x/=10;
	}
	return dfs(cnt,true,0);
	
}


int t;
int main()
{
	cin>>t;
	
	while(t--)
	{
		memset(f,-1,sizeof f);
		ll l,r;cin>>l>>r;
		ll ans=(solve(r)-solve(l-1)+mod)%mod;
		if(ans<0)
			ans+=mod;
		cout<<ans<<"\n";
	}
	return 0;
}

还有一题与之类类似活页码

看完上面可以练一下熟悉熟悉

//首先我们先来看一下有哪些状态,sum数字之和,当前位置
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

typedef long long ll;
const int N=45*9+7;
ll f[10][N];
int a[10];

ll dfs(int pos,bool limit,int sum)
{
	if(!pos)
		return sum;
	
	auto &v=f[pos][sum];
	if(!limit&&~v)
		return v;
		
	int up=limit?a[pos]:9;
	ll res=0;
	for(int i=0;i<=up;i++)
	{
		res=res+dfs(pos-1,limit&&i==up,sum+i);
	}
	if(!limit)
		v=res;
	return res;
}

ll solve(ll x)
{
	int len=0;
	while(x>0)
	{
		a[++len]=x%10;
		x/=10;
	}
	return dfs(len,true,0);
}

int main()
{
	memset(f,-1,sizeof f);
	ll l;
	cin>>l;
	cout<<solve(l);
	return 0;
}

例题2数字计数 

思路

首先我们需要注意的一点是,对于数码0,我们需要考虑一下前导为0的情况,例如00120 , 0 这个数字出现的次数为1,而并非3。

因此我们的dfs形参里面需要一个参数lead0表示有没有前导零

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long ll;
int a[12],ans[12];
ll f[12][12][2];//最后一位表示是否含有前导0
int digit;

ll dfs(int pos,bool limit,bool lead0,int cnt)
{
    if(!pos)
        return cnt;
        
    auto &v=f[pos][cnt][digit!=0];//如果这个数字不等于0,那么不可能成为前导0
    if(!limit&&!lead0&&~v)//取反操作,表示不等于-1,即说明已经搜过他了就没必要再继续搞他
        return v;
    
    int up=limit?a[pos]:9;//当前数字是否处于最大位上,如果处于,那么后一位就只能达到他输入的那位,而不能随便取取到9
        
    int res=0;
    for(int i=0;i<=up;i++)
    {
        int tmp=cnt+(i==digit);
        if(lead0&&i==0&&digit==0)
            tmp=0;
         res+=dfs(pos-1,limit&&i==up,lead0&&i==0,tmp);
    }
    if(!limit&&!lead0)
        v=res;//这里联系到上面的return v那句话,用v来保存了答案,然后下一层循环如果循环到他了就直接返回了,形成记忆化,减少代码运行时间
    return res;
}

void solve(ll x,int f)
{
    int len=0;
    while(x>0)
    {
        a[++len]=x%10;
        x/=10;
    }
    for(int i=0;i<=9;i++)
    {
        digit=i;
        ans[i]+=f*dfs(len,true,true,0);
    }
    
}

int main()
{
	ll n,m;
        memset(f,-1,sizeof f);
        //memset(a,0,sizeof a);
        //memset(ans,0,sizeof ans);
        //if(m<n)
        //	swap(n,m);//有多组数据一定要记得初始化
        solve(m,1);solve(n-1,-1);
        for(int i=0;i<=9;i++)
        {
            cout<<ans[i]<<" ";
        }
    return 0;
}

例题3windy 数

#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=10,inf=1e9;
int a[N];
int f[N][N];

int dfs(int pos,bool limit,bool lead0,int last)
{
	if(!pos)
		return 1;
	if(!limit&&last!=inf&&~f[pos][last])
		return f[pos][last];
	
	int up=limit?a[pos]:9;
	int res=0;
	for(int i=0;i<=up;i++)
	{
		if(lead0)
			res=res+dfs(pos-1,limit&&i==up,lead0&&i==0,i==0?last:i);
		else
			if(abs(last-i)>=2)
			res=res+dfs(pos-1,limit&&i==up,false,i);
	}
	if(!limit&&last!=inf)
		f[pos][last]=res;
	return res;
}


int solve(int x)
{
	int len=0;
	while(x>0)
	{
		a[++len]=x%10;
		x/=10;
	}
	return dfs(len,true,true,inf);
}

int main()
{
	memset(f,-1,sizeof f);
	int l,r;
	cin>>l>>r;
	cout<<solve(r)-solve(l-1);
	return 0;
}

例题4花神的数论题

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

typedef long long LL;
const int N=60,mod=10000007;
int a[60];
LL f[N][N];

LL dfs(int pos,bool limit,int cnt)//只需要限制不超过最大位数
{
	if(!pos)
		return max(cnt,1);//因为是乘法,所以我们得考虑到如果不满足的时候,等于0,会把所有答案都变成0
	if(!limit && ~f[pos][cnt])
		return f[pos][cnt];
		
	LL res=1;
	int up=limit?a[pos]:1;
	for(int i=0;i<=up;i++)
	{
		res=res*dfs(pos-1,limit &&(i==up),cnt+(i==1))%mod;//只有等于1的时候才相加,这样写省去一个if
	}
	if(!limit)
		f[pos][cnt]=res;
	return res;
}


LL solve(LL x)
{
	int len=0;
	while(x>0)
	{
		a[++len]=x%2;
		x/=2;
	}
	return dfs(len,true,0);
}

int main()
{
	memset(f,-1,sizeof f);
	LL n;
	cin>>n;
	cout<<solve(n);
	return 0;
}

 例题5Round Numbers S

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=32;
int a[N];
int f[N][N][N];

int dfs(int pos,bool limit,bool lead0,int num0,int num1)
{
	if(!pos)
		return num0>=num1;
	auto &v=f[pos][num0][num1];
	if(!limit&&~v&&!lead0)
		return v;
	int up=limit?a[pos]:1;
	int res=0;
	for(int i=0;i<=up;i++)
	{
		bool next0=lead0&&i==0;
		int s0=next0?0:num0+(i==0);
		int s1=next0?0:num1+(i==1);
		res=res+dfs(pos-1,limit&&i==up,next0,s0,s1);	
	}
	if(!limit)
		v=res;
	return res;	
}


int solve(int x)
{
	int len=0;
	while(x>0)
	{
		a[++len]=x%2;
		x/=2;
	}
	return dfs(len,true,true,0,0);
}


int main()
{
	memset(f,-1,sizeof f);
	int l,r;
	cin>>l>>r;
	cout<<solve(r)-solve(l-1);
	return 0;
}

例题6手机号码

//我们先确定一下参数pos表示当前位置,last1上一位,last2上两位,same表示是否相等,have4是否出现过4,have8是否出现国8
//f[pos][last1][last2][same][have4][have8]


#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

typedef long long ll;
const int N=12;

int a[N];
ll f[N][N][N][2][2][2];
int len;
//dfs需要的参数同样有pos当前位置,limit限制不能超过当前位的最大值,last1上一位,last2上两位,same表示是否相等,have4是否出现过4,have8是否出现国8
ll dfs(int pos,bool limit,int last1,int last2,bool same,bool have4,bool have8)
{
	if(!pos)
		return same&&((have4&&have8)?0:1);//have4和have8同时成立那就不满足,返回0
	auto &v=f[pos][last1][last2][same][have4][have8];
	
	if(!limit&&~v)//记忆化返回减少运行时间,
		return v;
		
	int up=limit?a[pos]:9;
	ll res=0;
	int falst=pos==len?1:0;//首位必须从1开始,就是不能含有前导0,长度等于当前传入的位相等的时候就是最高位
	for(int i=falst;i<=up;i++)
	{
		bool tmp=same||(last1==i&&last2==i);//用same是因为后续要把tmp传给same,后面是same的定义
		if(i==4&&!have8)//have4的定义不能直接用have4
			res+=dfs(pos-1,limit&&i==up,i,last1,tmp,true,false);
			//下一位的上一位就是现在取的这位数,所以传i,依次类推
		else if(!have4&&i==8)
			res+=dfs(pos-1,limit&&i==up,i,last1,tmp,false,true);
		else if(i!=4&&i!=8)
			res+=dfs(pos-1,limit&&i==up,i,last1,tmp,have4,have8);
			//还需要判断一下,因为如果i不是4也不是8也要递归一下,后面可能会出现是4或8,不加可能会漏数
	}
	if(!limit)
		v=res;
	return res;
		
}

ll solve(ll x)
{
	if(x<1e10)
		return 0;//记得特判一下不合法号码
	len=0;
	while(x>0)
	{
		a[++len]=x%10;
		x/=10;
	}
	return dfs(len,true,10,10,false,false,false);
	//填大于9或者小于0的都可以,不要跟后面的数字重了
	
}

int main()
{
	memset(f,-1,sizeof f);
	ll l,r;
	cin>>l>>r;
	cout<<solve(r)-solve(l-1);
	return 0;
}

例题7Magic Numbers

//我们先来定义一些参数,r表示余数,当余数为0的时候我们就可以判断该数字满足条件
//我们定义odd来判断当前位是奇数还是偶数

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

typedef long long ll;
const int N=2005,mod=1e9+7;

int a[N],len;
ll f[N][N];
int d,m;


ll dfs(int pos,bool limit,int r)
{
	if(!pos)
		return r==0;
	if(!limit&&~f[pos][r])
		return f[pos][r];
	
	int up=limit?a[pos]:9;
	ll res=0;
	bool t=(len+1-pos)&1;//举个例子,1234,pos刚开始是等于4,代表第一位,使用我们这里还需要反转一下第几位
	if(t)
	{//奇数位置,不能让他等于d,
		for(int i=0;i<=up;i++)
		{
			if(i==d)
				continue;
			res=(res+dfs(pos-1,limit&&i==up,(r*10+i)%m))%mod;
		}
	}
	else//偶数位置必须让他等于d
	{
		if(d<=up)//减轻代码量,如果一个数的最高位都还没有d大的话,必然就不是
			res=(res+dfs(pos-1,limit&&d==up,(r*10+d)%m))%mod;
	}
	if(!limit)
		f[pos][r]=res;
	return res;
}

ll solve(string x)
{
	len=x.size();
	for(int i=0;i<len;i++)
		a[len-i]=x[i]-'0';
	return dfs(len,true,0);
}

bool cheak(string x)
{
	int r=0;
	len=x.size();
	for(int i=0;i<len;i++)
		a[i+1]=x[i]-'0';
	for(int i=1;i<=len;i++)
	{
		if(i&1)//是奇数位
		{
			if(d==a[i])
				return	false;
		}
		else
		{
			if(d!=a[i])
				return false;
		}
		r=(r*10+a[i])%m;//每一位都取余数防止数字过大
	}
	return r==0;//如果余数等于0,那就说明满足条件了,返回1
}

int main()
{
	memset(f,-1,sizeof f);
	string l,r;
	cin>>m>>d>>l>>r;
	cout<<(solve(r)-solve(l)+cheak(l)+mod)%mod;//数据范围太大
}

例题8Salazar Slytherin's Locket

//定义参数,st表示是奇数还是偶数,lead0表示有没有前导0的情况
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

typedef long long ll;
const int N=61;
int b,a[N];
ll f[11][N][(1<<11)];//f[pos][st]当前位置

ll dfs(int pos,bool limit,bool lead0,int st)
{
	if(!pos)
		return (st==0&&!lead0) ?1:0;
	auto &v=f[b][pos][st];
	if(!limit&&!lead0&&~v)
		return v;
	ll res=0;
	int up=limit?a[pos]:b-1;
	for(int i=0;i<=up;i++)
	{
		
		int tmp=(lead0&&i==0)?0:st^(1<<i);//这里&&的优先级高于?:
		//如果每个数都出现偶数次那取异或的结果就是0,否则就为1,对应边界判断是否为0
		res+=dfs(pos-1,limit&&i==up,lead0&&i==0,tmp);
	}
	if(!limit&&!lead0)
		v=res;
	return res;
	
}

ll solve(ll x)
{
	int len=0;
	while(x>0)
	{
		a[++len]=x%b;
		x/=b;
	}
	return dfs(len,true,true,0);
}

int main()
{
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	
	int t;cin>>t;
	memset(f,-1,sizeof f);	//如果 每次都初始化t很大的时候就会超时,所以我们需要在f上多开一维,开10层,比起t最大到10的三次方更划算
	while(t--)
	{
		
		ll l,r;cin>>b>>l>>r;
		cout<<solve(r)-solve(l-1)<<"\n";
	}
	return 0;
}

近期较忙,后续会给出详解,敬请期待

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值