数位DP[记忆化搜索Solve Anything]

记忆化搜索写数位DP就是爽

优点:     1.代码短      2.细节比较少

总结了一下,数位DP也有模板的

int dfs记忆化搜索
(int 当前位数,int 记忆化搜索要保留的条件(一些特殊情况对答案有影响),int limit(前面有没有放满)){
    if(当前位数==0) 如果满足答案要求 return 1;否则return 0;
    if(!limit&&~f[当前位数][...]) return f[当前位数][...];
    int end=limit?a[当前位数]:9;下一位从0枚举到end,如果前面放满了就只能放到a[当前位数]    
    int ans=0;
    for(int i=0;i<=end;i++){
        ans+=dfs(当前位数-1,i是不是特殊情况,limit&&i==end);
        //需要满足前面都放满,又要满足这位放满
    }
    if(!limit) f[当前位数][...]=ans;
    return ans;
}

中间那个点是很灵活的,需要根据自己的需求来加

另外,f[i][...]是当前位数为i的所有情况,如果前面放满了,f[i][...]也就不是所有情况了

先来看看例题理解一下

不要62

求l-r不含62和4的所有个数

分析

我们枚举下一位时,如果是4可以直接跳过

但如果是2,且前面一位是6呢

我们发现我们需要记录前面一位是不是6

如果前面一位是6与枚举的下一位是2就跳过

#include<bits/stdc++.h>
using namespace std;
int a[30],l,r;
int f[30][2];
int dfs(int d,bool is6,bool limit){
	if(d==0) return 1;
	if(!limit&&~f[d][is6]) return f[d][is6];
	int cur=0,end=(limit?a[d]:9);
	for(int i=0;i<=end;i++){
		if((is6&&i==2)||i==4) continue;
		cur+=dfs(d-1,i==6,limit&&i==end);
	}
	if(!limit) f[d][is6]=cur;
	return cur;
}
int solve(int x){
	memset(a,0,sizeof(0));
	int cnt=0;
	if(!x) a[++cnt]=0;
	while(x){a[++cnt]=x%10;x/=10;}
	return dfs(cnt,0,1);
}
int main(){
    memset(f,-1,sizeof(f));
	cin>>l>>r;
	cout<<solve(r)-solve(l-1);
	return 0;
}

注意a数组一般这样设

原数 18734

a[1]=4,a[2]=3,a[3]=7,a[4]=8,a[5]=1 

Balenced Number

l-r之间,有多少个可以选一个支点,左右两边力矩和相等的数

如4139 3为支点,4*2+1*1=9*1

分析

我们分析发现,需要增添一个左右的差,当全部位放满之后,若差为0 return1

另外,我们枚举支点的位置,每一种位置对应有一些答案

全部为0的情况需要减去

代码

#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL dp[20][20][2000];//当前位,支点,左边比右边大多少
LL l,r,a[20];
LL dfs(LL cur,LL pos,LL sum,LL limit){
	if(cur==0) return sum==0;
	if(sum<0) return 0;//我们先把左边的加上,右边的来减,还没完就减不够了就return了
	if(!limit&&~dp[cur][pos][sum]) return dp[cur][pos][sum];
	LL ans=0,end=limit?a[cur]:9;
	for(LL i=0;i<=end;i++){
		ans+=dfs(cur-1,pos,sum+(cur-pos)*i,limit&&i==end);
	}
	if(!limit) dp[cur][pos][sum]=ans;
	return ans;
}
LL solve(LL x){
	LL cnt=0,ans=0;
	while(x){a[++cnt]=x%10;x/=10;}
	for(int i=1;i<=cnt;i++){//枚举支点
		ans+=dfs(cnt,i,0,1);
	}
	return ans-(cnt-1);
}
int main(){
	memset(dp,-1,sizeof(dp));
	scanf("%lld%lld",&l,&r);
	printf("%lld",solve(r)-solve(l-1));
	return 0;
} 

Beautiful Number 

如果一个数能够被其每个数位的数都整除,那么这个数就叫做美丽数。
给定一个区间 [x,y] ,计算该区间内有多少个美丽数。

分析

如果能全部整除,相当于一个数被它的全部数位的最小公倍数整除

另外,设这个数为x,最小公倍数为y

因为y|2520,2520为1-9的最小公倍数

若y|x,则y|(x%2520)

设ay=2520,by=x

x%2520=by-k*ay,y*(b-a*k)=x%2520所以y|(x%2520)

因此,我们需要保留一个mod,当到下一位时,mod=(mod*10+i)%2520

实验发现,数位DP带模之类的,都有一个mod(除以2520的余数)

并且这个mod的上限可以优化到252,这里不再赘述

现在的空间20*2520*2520 还需要优化

我们发现,lcm一定是2520的约数,因此我们可以离散化一下

发现2520的约数只有48个

因此空间优化到20*2520*50 OK

#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL f[20][2525][50];//位数,模2520是多少,前面最小公约数离散化之后
LL l,r; int a[20],hash[2525];
int gcd(int a,int b){return b?gcd(b,a%b):a;}
int LCM(int a,int b){return a/gcd(a,b)*b;}
LL dfs(int cur,int mod,int lcm,int limit){
	if(cur==0) return (mod%lcm)==0;
	LL &res=f[cur][mod][hash[lcm]];
	if(!limit&&~res) return res;
	LL ans=0; int end=limit?a[cur]:9;
	for(int i=0;i<=end;i++){
		ans+=dfs(cur-1,(mod*10+i)%2520,i?LCM(lcm,i):lcm,limit&&i==end);
	}
	if(!limit) res=ans;
	return ans;
}
LL solve(LL x){
	int cnt=0;
	while(x){a[++cnt]=x%10;x/=10;}
	return dfs(cnt,0,1,1);
} 
int main(){
	memset(f,-1,sizeof(f));
	int cnt=0;
	for(int i=1;i<=2520;i++)//离散化
		if(2520%i==0) hash[i]=++cnt;
	scanf("%lld%lld",&l,&r);
	printf("%lld",solve(r)-solve(l-1));
	return 0;
}

Windy数

不含前导零且自身相邻两个数字之差至少为 2 的正整数(1位或多位数)被称为 Windy 数。注:单个数字 1~9 都是 Windy 数。

分析

我们发现需要记录上一位是什么,代码也很容易写出

#include<bits/stdc++.h>
#define LL long long
using namespace std;
LL l,r,f[20][11];int a[20];
LL dfs(int cur,int last,int limit){
	if(cur==0) return 1;
	LL &res=f[cur][last];
	if(!limit&&last>=0&&~res) return res;//last小于0是指前面的都是0
	int end=limit?a[cur]:9; LL ans=0;
	for(int i=0;i<=end;i++){
		if(abs(i-last)<2) continue;
		ans+=dfs(cur-1,(!i&&last==-10)?-10:i,limit&&i==end);
	}
	if(last>=0&&!limit) res=ans;
	return ans;
}
LL solve(int x){
	int cnt=0;
	while(x){a[++cnt]=x%10;x/=10;}
	return dfs(cnt,-10,1);
}
int main(){
	memset(f,-1,sizeof(f));
	scanf("%lld%lld",&l,&r);
	printf("%lld",solve(r)-solve(l-1));
	return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FSYo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值