数位DP套路模板

数位DP

【介绍】

显然,是一种DP。

再显然,是一种关于每一位数字之间关系(性质)的DP


【题目特征】

题目描述一般是求【L,R】范围内满足各位数字间存在某种规律的数有几个

数位DP的题目数据范围往往很大,动不动就是1e18,但是由于是数位DP只跟位数有关,所以也就18位。


【算法设计】

使用记忆化搜索进行DP因为会出现重复出现的状态,使用记忆化搜索可以减少重复的搜索,减少复杂度。

一.记忆化搜索的过程

​ 从起点向下搜索,搜索的途中累加每一位的答案,最后在起点得到答案。

二. 区间转化为[0,X]

​ 对于[L,R]的区间问题,一般我们可以根据前缀和相减转化为[0,R] - [0,L-1]的问题。

三.dfs的参数

pos, 当前找到了第几位
limit,因为问题已经转化为了小于X的所有正整数中符合条件的个数,我们从高位枚举,一旦当前位小于X,那么后面数字可以随便选。limit就是判断当前位是否可以任意选。

递归中limit的传递

1.当前没有限制limit=0,那么显然后面应该都没限制,是0

2.当前有限制limit=1;

​ 1.如果当前位选的数小于X对应位数字,那么后面应该是没限制0

​ 2.如果当前位选的数恰好等于X对应位数字,这个分支相当于是紧贴着X的每一位(前面枚举到的位选的和X一样),那么后面还要限制数字大小,limit为1

综上:我们可以总结出limit的转移方程 limit = limit&&j==X[i] (X[i]是当前位能枚举得最大值, j是当前正在枚举得数)

3.**pre,**有些题目的性质是和前几位数字有关,那么也可以加上pre1…pre2

4.**zero,**判断是否有前导0,比如所有位相同的数包括000333(实质是333)前面都是0的话取1,否则取0

递归中zero前导零的传递

1.前一位是前导0

​ 1.当前位是0,那么后面还是zero还是1

​ 2.当前位不是0,那么后面zero=0

2.前一位不是前导0,那么不管当前位是不是0,后面zero都是1

综上:我们可以总结出zero的转移方程 zero = zero&&(!j) (X[i]是当前位能枚举得最大值, j是当前正在枚举得数)

四.记忆化搜索的记忆化

​ 我们可以用一个dp数组来记录已经确认的状态的值,下标用来表示状态,等到后面搜到相同状态时不用递归到最底层直接可以拿来复用。

五.模板
ll dfs(int pos,int pre,int st,……,int lead,int limit)//记忆化搜索
{
    if(pos>len) return st;//剪枝
    if((dp[pos][pre][st]……[……]!=-1&&(!limit)&&(!lead))) return dp[pos][pre][st]……[……];//相同状态,也可以吧limit和lead也放入dp数组中(多开两个维度)
    ll ret=0;//暂时记录当前方案数
    int res=limit?a[len-pos+1]:9;//res当前位能取到的最大值,limit的作用
    for(int i=0;i<=res;i++)//搜索每个可取的数
    {
        //有前导0并且当前位也是前导0
        if((!i)&&lead) ret+=dfs(……,……,……,i==res&&limit);
        //有前导0但当前位不是前导0,当前位就是最高位
        else if(i&&lead) ret+=dfs(……,……,……,i==res&&limit); 
        else if(根据题意而定的判断) ret+=dfs(……,……,……,i==res&&limit);
    }
    if(!limit&&!lead) dp[pos][pre][st]……[……]=ret;//没前导零,后面无限制,当前状态方案数记录
    return ret;
}
ll part(ll x)//把数按位拆分
{
    len=0;
    while(x) a[++len]=x%10,x/=10;
    memset(dp,-1,sizeof dp);//初始化-1(因为有可能某些情况下的方案数是0)
    return dfs(……,……,……,……);//进入记搜
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%lld%lld",&l,&r);
        if(l) printf("%lld",part(r)-part(l-1));//[l,r](l!=0)
        else printf("%lld",part(r)-part(l));//从0开始要特判
    }
    return 0;
}

【例题】

一般的数位DP套路题都是一个数满足什么性质,而这题是两个数满足一个性质就是与起来是0。那么我们类比一下,原来限制单个数后面取值的limit现在要有两个,而当前搜索的位置因为两个数位置总是同步的,所以只需要一个。因为两个数&起来等于0,那么他们相加是不进位的,又取了log,答案就是最高位的位置加1,所以每一位的贡献就是这位是最高位的数量乘以这位的位置+1.

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


#define mem(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define ll long long
#define ull unsigned long long
#define fi first
#define se second
#define endl '\n'
#define PI acos(-1.0)
#define lcm(a,b) a/gcd(a,b)*b
#define INF 0x3f3f3f3f3f3f3f3f
#define debug(a) cerr<<#a<<"="<<a<<endl;
#define Adebug(a,i) cerr<<#a<<"["<<i<<"]="<<a[i]<<endl;
#define int long long
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define vi vector<int>
#define vpii vector<PII>
#define pb push_back
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
#define lb(s) ((s) & (-s))
#define mk(s, t) make_pair(s, t)


inline void wt(ll x){cout << x;}
inline void wtl(ll x){cout << x << endl;}
inline void wtb(ll x){cout << x << ' ';}
template <typename T> bool ckmax(T &x, T y){return x < y ? x = y, true : false;}
template <typename T> bool ckmin(T &x, T y){return x > y ? x = y, true : false;}
int qmi(int a, int k, int p){int res = 1;while (k){if (k & 1) res = (ll)res * a % p;a = (ll)a * a % p;k >>= 1;}return res;}
int qpow(int a,int b){int res = 1;while(b){if(b&1) res *= a;b>>=1;a*=a;}return res;}
int mo(int x,int p){return x = ((x%p)+p)%p;}
int gcd(int a,int b){return b?gcd(b,a%b):a;}
const int maxn = 1e6+7;
const int mod = 1e9+7;
int dx[] = {0,0,1,-1}, dy[] = {1,-1,0,0};

int T,N,M,K;
int num1[33],num2[33],dp[33][2][2];
int st[maxn];
int bit[33];
int L, R, ans;

int dfs(int now, int limit1, int limit2, int zero){
	if(now == -1) return 1;//枚举完了
	int &x = dp[now][limit1][limit2];
	if(~x) return x;//已经搜过了
	if ((!limit1)&&(!limit2)&&!zero) return x = bit[now+1];//没有限制,后面就是全部的组合
	int end1 = limit1?num1[now]:1;
	int end2 = limit2?num2[now]:1;
	x = 0;
	int temp = 0, cnt = 0;
	rep(i,0,end1){
		rep(j,0,end2){
			if(i&j) continue;//不符合&=0
			if(zero&&(i|j)){
				temp = dfs(now-1,limit1&&(i==end1),limit2&&(j==end2),0);
				x = (x+temp)%mod;//为上一层累加
				cnt = (cnt+temp)%mod;//当前位是最高位
			}
			else{
				x = (x+dfs(now-1,limit1&&(i==end1),limit2&&(j==end2),zero&&(!(i|j))))%mod;//为上一层累加
			}
		}
	}
	ans=(ans+cnt*(now+1))%mod;
	return x;
}
void part(int X, int Y){
	int len = -1;//数字放在【0,len】中
	while(X || Y) {
		num1[++len] = X%2, X/=2;
		num2[len] = Y%2, Y/=2;
	}
	ans = 0;
	mem(dp,-1);
	dfs(len,1,1,1);
	wtl(ans);
}
void solve(){
	cin >> L >> R;
	part(L,R);

}
signed main()
{

	bit[0] = 1;
	rep(i,1,32) bit[i] = bit[i-1]*3%mod;
	cin >> T;
	while(T--) solve();
  return (0-0); //<3
} 

这题与上题不同,只有一个数,我们可以用前缀和相减来求,但是数字太大有5000位,需要用到字符串来存储,对字符串求减一涉及到借位,可能有些麻烦(其实也并不麻烦)。这里提供另一种思路,就是判断L这个数是否满足即可。

参数分析:

  1. 满足f(x)= x(mod M) 我们可以构造g(x)= f(x)- x,那么条件变成g(x)= 0 (mod M)
  2. 计算f(x)时,为了避免重复计算贡献,规定当前数字只会跟前面的数字产生贡献,那么我们只需要记录每一位数字的前缀和pre,然后当前数字的贡献就是j * pre
#include<bits/stdc++.h>
using namespace std;


#define mem(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define ull unsigned long long
#define fi first
#define se second
#define endl '\n'
#define PI acos(-1.0)
#define lcm(a,b) a/gcd(a,b)*b
#define INF 0x3f3f3f3f3f3f3f3f
#define debug(a) cerr<<#a<<"="<<a<<endl;
#define Adebug(a,i) cerr<<#a<<"["<<i<<"]="<<a[i]<<endl;
#define rep(i, a, b) for(int i = (a); i <= (b); i ++)
#define per(i, a, b) for(int i = (a); i >= (b); i --)
#define vi vector<int>
#define vpii vector<PII>
#define pb push_back
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
#define lb(s) ((s) & (-s))
#define mk(s, t) make_pair(s, t)


inline void wt(int x){cout << x;}
inline void wtl(int x){cout << x << endl;}
inline void wtb(int x){cout << x << ' ';}
template <typename T> bool ckmax(T &x, T y){return x < y ? x = y, true : false;}
template <typename T> bool ckmin(T &x, T y){return x > y ? x = y, true : false;}
int qpow(int a,int b){int res = 1;while(b){if(b&1) res *= a;b>>=1;a*=a;}return res;}
int mo(int x,int p){return x = ((x%p)+p)%p;}
int gcd(int a,int b){return b?gcd(b,a%b):a;}
const int maxn = 1e6+7;
const int mod = 1e9+7;

int T,N,M,K;
int bit[5009],A[5009],dp[5009][61][61][2],id,st[5009][61][61][2];
char R[5009],L[5009];
int f;
int dfs(int now,int pre,int fx, int limit){
	long long ans = 0;
	if(now==(N+1)){
		f = (fx==0);//判断X是否可行
		return f;
	}
	if (st[now][pre][fx][limit]==id) return dp[now][pre][fx][limit];//这样可以不用初始化dp数组,等于当前id说明是本组数据,不同组的模数是不同的,所以同一个状态的值可能不同
	st[now][pre][fx][limit] = id;
	int lim = limit?A[now]:9;
	rep(i,0,lim){
		int dif = mo((fx+i*pre-i*bit[N-now]),M);
		ans = ans+dfs(now+1,(pre+i)%M,dif,limit&&(i==A[now]));
	}
	ans = (ans+mod)%mod;
	dp[now][pre][fx][limit] = ans;
	return ans;
}
int part(char X[]){
	//printf("!---%s\n",X+1);
	++id;//标记测试数据组别
	N = strlen(X+1);	
	rep(i,1,N) A[i] = X[i]-'0';
	return dfs(1,0,0,1);	
}
void solve(){
	scanf("%s %s %d",L+1,R+1,&M);
	N = strlen(R+1);
    bit[0] = 1;
	rep(i,1,N){
		bit[i] = bit[i-1]*10%M;
	}
	int ans = part(R);
	f = 0;
	ans-=part(L);
	//debug(f);
	ans+=f;//f是判断L是否有效,有效的话就为1(前缀和相减时多减了1加回来)
	wtl(mo(ans,mod));

}
signed main()
{
	cin >> T;
	while(T--) solve();
    return (0-0); //<3
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值