数位DP套路模板

Posted XINNNNNNNYU

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数位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 << ' ';beautiful number  数位dp

数位DP套路模板

数位DP套路模板

数位DP模板

数位DP模板

算法笔记--数位dp