数位DP

数位DP

数位dp是一种计数用的dp,在每个数位上dp,一般就是要统计一个区间[l,r]内满足一些条件数的个数 。通常数据范围很大,10^18及以上

大部分可以直接套板子

typedef long long ll;
int a[20];
ll dp[20][state];//用于记忆化搜索,不同题目状态不同
ll dfs(int pos,/*数位*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
    //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
    if(pos==-1) return 1;
	/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */
   
    //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
    if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
    int up=limit?a[pos]:9;//根据limit判断枚举的上界up(例如234,第一位是2的时候,第二位枚举的上界是3,否则为9) 
    ll ans=0;
    //开始计数
    for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了
    {
        if() ...
        else if()...
        ans+=dfs(pos-1,/*状态转移*/,lead && (i==0),limit && (i==a[pos]) ) //最后两个变量传参都是这样写的
    }
    if(!limit && !lead) dp[pos][state]=ans;
    /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
    return ans;
}
ll solve(ll x)
{
    int pos=0;
    while(x)//把数位都分解出来
    {
        a[++pos]=x%10;
        x/=10;
    }
    return dfs(pos/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int main()
{
    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri))
    {
        //初始化dp数组为-1,这里还有更加优美的优化,后面讲
        printf("%lld\n",solve(ri)-solve(le-1));
    }
} 

windy 数

Description

不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy 想知道,在 a 和 b 之间,包括 a 和 b ,总共有多少个 windy 数?

Solution

•int dfs ( int pos, int pre, bool lead, bool lim)只需记录前一个数pre即可

#include <iostream>
#include <cmath>
#include <cstdio>
#include <cstring>
using namespace std;
int l,r,a[20],f[20][20];

int dfs(int pos,int pre,bool lead,bool lim) {
	if(pos<=0)return 1;
	if(!lead && !lim && f[pos][pre]!=-1) return f[pos][pre];
	int up=lim?a[pos]:9,tmp=0;
	for(int i=0;i<=up;i++)
		if(abs(pre-i)>=2 || lead)
			tmp+=dfs(pos-1,i,lead&&(i==0),lim&&(i==up));
	if(!lim && !lead) f[pos][pre]=tmp;
	return tmp;
}

int solve(int x) {
	memset(a,0,sizeof(a));
	int pos=0;
	while(x) {
		a[++pos]=x%10;
		x/=10;
	}
	return dfs(pos,0,1,1);
}

int main() {
	scanf("%d%d",&l,&r);
	memset(f,-1,sizeof(f));
	printf("%d",solve(r)-solve(l-1));
	return 0;
}

数字游戏

Description

不降数:满足从左到右各位数字成小于等于的关系;指定一个整数闭区间 [a,b]内有多少个不降数。

Solution

同上

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;

LL l, r, a[20], f[20][20];

inline LL dfs(int pos, int pre, bool lead, bool lim) {
    if (!pos)
        return 1;
    if (!lead && !lim && f[pos][pre] != -1)
        return f[pos][pre];
    LL tmp = 0, up = lim ? a[pos] : 9;
    for (int i = 0; i <= up; i++)
        if (pre <= i)
            tmp += dfs(pos - 1, i, lead && (i == 0), lim && (i == up));
    if (!lead && !lim)
        f[pos][pre] = tmp;
    return tmp;
}

inline LL solve(LL x) {
    memset(f, -1, sizeof(f));
    int len = 0;
    while (x) {
        a[++len] = x % 10;
        x /= 10;
    }
    return dfs(len, 0, 1, 1);
}

int main() {
    while (~scanf("%lld%lld", &l, &r)) {
        printf("%lld\n", solve(r) - solve(l - 1));
    }
    return 0;
}

数字计数

Description

给定两个正整数 a 和 b,求在 [a,b],每个数码(digit)各出现了多少次 1≤a≤b≤10^12

Solution

dfs(int pos,int sum,bool lead,bool lim) sum记录 这一位某数码出现几次

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;

LL l,r,a[15],cur,f[15][15];

inline LL dfs(int pos,int sum,bool lead,bool lim) {
	if(!pos) return sum;
	if(!lead&&!lim&&f[pos][sum]!=-1) return f[pos][sum];
	LL tmp=0,up=lim?a[pos]:9;
	for(int i=0;i<=up;i++)
		tmp+=dfs(pos-1,sum+( (!lead&&!i&&!cur)||(cur&&i==cur)),lead&&(i==0),lim&&(i==up));
	if(!lead&&!lim) f[pos][sum]=tmp;
	return tmp;	
}

inline LL solve(LL x){
	memset(f,-1,sizeof(f));
	int len=0;
	while(x){
		a[++len]=x%10;
		x/=10;
	}
	return dfs(len,0,1,1);
}

int main() {
	scanf("%lld%lld",&l,&r);
	for(cur=0;cur<=9;cur++)
		printf("%lld ",solve(r)-solve(l-1));
	return 0;
}

圆数

#include <queue>
#include <cmath>
#include <cstdio>
#include <vector>
#include <cstring>
#include <utility>
#include <iostream>
#include <algorithm>

using namespace std;
const int N=35;
typedef long long LL;
LL l,r,a[N],f[N][N][N];//pos,几个0,几个1 
inline LL dfs(int pos,LL sum0,LL sum1,bool lead,bool lim) {
	if(!pos) return lead||sum0>=sum1;//
	if(!lead&&!lim&&f[pos][sum0][sum1]!=-1) return f[pos][sum0][sum1];
	LL tmp=0,up=lim?a[pos]:1;//是1不是9
	for(int i=0;i<=up;i++) 
		if(lead&&!i) tmp+=dfs(pos-1,0,0,1,lim&&(i==up));
		else tmp+=dfs(pos-1,sum0+(i==0),sum1+(i==1),0,lim&&(i==up));
	if(!lead&&!lim) f[pos][sum0][sum1]=tmp;
	return tmp;
}

inline LL solve(LL x) {
	memset(f,-1,sizeof f);
	int len=0;
	while(x) {
		a[++len]=x&1;//
		x>>=1;//
	}
	return dfs(len,0,0,1,1);
}
int main() {
	scanf("%lld%lld",&l,&r);
	if(l>r) swap(l,r);
	printf("%lld\n",solve(r)-solve(l-1));		
	return 0;
}


手机号码

Description

号码中要出现至少 3 个相邻的相同数字;号码中不能同时出现 8 和 4。号码必须同时包含两个特征才满足条件。手机号码一定是 11 位数,前不含前导的 0。统计出 [L,R] 区间内所有满足条件的号码数量。L 和 R 也是 11 位的手机号码。

Solution

my solution

记录

dfs(int pos,int pre,int cnt,int you,int right,bool lim)

pre 上一个,cnt有几个连续的了(初始值为1),you=1,有4,you=2有8,right满不满足连续三个 ,

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;

LL l,r,len,a[15],f[15][10][10][5][5];

inline LL dfs(int pos,int pre,int cnt,int you,int right,bool lim) {
    if(!pos) return right;
    if(!lim && f[pos][pre][cnt][you][right]!=-1) return f[pos][pre][cnt][you][right];
    LL tmp=0;int up=lim?a[pos]:9,down=(pos==len?1:0);
    for(int i=down;i<=up;i++){
        int k=(i==pre?cnt+1:1);
        if(you==1&&i==8)continue;
        if(you==2&&i==4)continue;
        tmp+=dfs(pos-1,i,k,(i==8||i==4?(i==4?1:2):you),right||(k>2),lim&&(i==up));
    }
	if(!lim) f[pos][pre][cnt][you][right]=tmp;
	return tmp;
}

LL solve(LL x){
    memset(f,-1,sizeof(f));
    memset(a,0,sizeof(a));
    len=0;
    while(x){
        a[++len]=x%10;
        x/=10;
    }
    if(len!=11) return 0;
    return dfs(len,-1,1,0,0,1);
}

int main() {
    scanf("%lld%lld",&l,&r);
    printf("%lld ",solve(r)-solve(l-1));
    return 0;
}

正解:

dfs(int pos,int pre1,int pre2,bool right,int you4,int you8,int lim)

pre1 上一个,pre2上上个,right满不满足连续三个 ,有没有4,8,

注意solve分解的时候判断len==11

小trick:如何避开第一位上的前导0?

答: 枚举第一位上的数字,从下一位开始搜索

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;

LL l,r,a[15],f[15][10][10][2][2][2][2];

inline LL dfs(int pos,int pre1,int pre2,bool right,int you4,int you8,int lim){
	if(you4&&you8) return 0;
	if(pos<=0) return right?1:0; 
	if(!lim&&f[pos][pre1][pre2][right][you4][you8][lim]!=-1) return f[pos][pre1][pre2][right][you4][you8][lim];
	int up=lim?a[pos]:9,sum=0;
	for(int i=0;i<=up;i++){
		sum+=dfs(pos-1,i,pre1,right||((i==pre1)&&(i==pre2)),you4||(i==4),you8||(i==8),lim&&(i==up));
	}
	return f[pos][pre1][pre2][right][you4][you8][lim]=sum;
}
LL solve(LL x){
	memset(f,-1,sizeof(f));
	int len=0;
	while(x){
		a[++len]=x%10;
		x/=10;
	}
	if(len!=11) return 0;
	LL ans=0;
	for(int i=1;i<=a[len];i++)
		ans+=dfs(len-1,i,0,0,i==4,i==8,i==a[len]);
	return ans;
}

int main() {
	scanf("%lld%lld",&l,&r);
	printf("%lld ",solve(r)-solve(l-1));
	return 0;
}

同类分布

Description

​ 求出 [a,b] 中各位数字之和能整除原数的数的个数

Solution

记录pos,sum, (数字各位上的数的和),st(原数),limit

转移:dfs(pos+1,sum+i,st∗10+i,i==res&&limit)

结束返回:搜完之后如果st%sum=0就返回1否则返回0

现在关键的问题是:st可达到1e18显然是不能作为dp转移的下标直接记录的

所以我们考虑取模

我们最理想的模数当然是把每次搜到最后得到的数字各个位数之和

但是我们发现在这个过程中sum是发生变化的

所以我们就应该以一个定值作为模数

那好,我们虽然不知道最后各位之和的结果,我们枚举总可以吧

我们只需要枚举所有的各位数字之和作为模数

最后判断sum和枚举的mod相等并且 st%sum==0的数就是符合题意的答案

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;

LL a,b,f[20][200][200];
int bit[20],MOD;

inline long long read()
{
	long long x=0;char ch=getchar();
	while(!isdigit(ch)) ch=getchar();
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x;
}

LL dfs(int pos,int sum,int sta,bool lim){
	if(pos==0){
		if(sta==0&&MOD==sum)return 1;
		else return 0;
	}
	if(sum>MOD)return 0;
	if(!lim&&f[pos][sum][sta]!=-1)return f[pos][sum][sta];
	int up=lim ? bit[pos]:9;	
	LL tmp=0;
	for(int i=0;i<=up;i++) 
		tmp+=dfs(pos-1,sum+i,(sta*10+i)%MOD,lim&&i==up);
	if(!lim) f[pos][sum][sta]=tmp;
	return tmp;
}

LL solve(LL x) {
	int len=0,mod=0;
	while(x) {
		bit[++len]=x%10;
		mod+=bit[len];
		x/=10;
	}
	LL res=0;
	for(MOD=1;MOD<=9*len;MOD++) {
		memset(f,-1,sizeof(f));
		res+=dfs(len,0,0,1);
	}
	return res;
}

int main() {
	a=read();b=read();
	printf("%lld",solve(b)-solve(a-1));
	return 0;
}

萌数

Description

只有满足“存在长度至少为 2 的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。从 L 到 R 的所有整数中有多少个萌数?

Solution

dfs(int pos,int pre1,int pre2,bool right,bool lead,bool lim)

记录前一个,前前一个,是否满足题意,

满足条件形如 aa || aba

自己写的只有10pts QwQ

#include <cstdio>
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
typedef long long LL;
const int mod=1000000007;

LL c[1005],d[1005],posa,posb,f[1005][11][11][2][2][2][2],ans;
string a,b;

inline LL dfs1(int pos,int pre1,int pre2,bool right,bool lead,bool lim) {
	if(pos==0) return right?1:0;
	if(!lead&&!lim&&f[pos][pre1][pre2][right][lead][lim][0]!=-1) return f[pos][pre1][pre2][right][lead][lim][1];
	LL tmp=0;int up=lim?c[pos]:9;
	for(int i=0;i<=up;i++){
		if(right&&pre1!=-1) tmp=(tmp+dfs1(pos-1,(lead&&!i)?-1:i,pre1,1,lead&&(i==0),lim&&(i==up)))%mod;
		else {
			if((pre1==i&&pre1!=-1) || (pre2==i&&pre2!=-1&&pre1!=-1))
				tmp=(tmp+dfs1(pos-1,(lead&&!i)?-1:i,pre1,1,lead&&(i==0),lim&&(i==up))%mod)%mod;
			else tmp=(tmp+dfs1(pos-1,(lead&&!i)?-1:i,pre1,0,lead&&(i==0),lim&&(i==up))%mod)%mod;
		}
	}
	if(!lead&&!lim) f[pos][pre1][pre2][right][lead][lim][0]=tmp;
	return tmp;
}

inline LL dfs2(int pos,int pre1,int pre2,bool right,bool lead,bool lim) {
	if(pos==0) return right?1:0;
	if(!lead&&!lim&&f[pos][pre1][pre2][right][lead][lim][1]!=-1) return f[pos][pre1][pre2][right][lead][lim][1];
	LL tmp=0;int up=lim?d[pos]:9;
	for(int i=0;i<=up;i++){
		if(right&&pre1!=-1) tmp=(tmp+dfs2(pos-1,(lead&&!i)?-1:i,pre1,1,lead&&(i==0),lim&&(i==up)))%mod;
		else {
			if((pre1==i&&pre1!=-1) || (pre2==i&&pre2!=-1&&pre1!=-1))
				tmp=(tmp+dfs2(pos-1,(lead&&!i)?-1:i,pre1,1,lead&&(i==0),lim&&(i==up))%mod)%mod;
			else tmp=(tmp+dfs2(pos-1,(lead&&!i)?-1:i,pre1,0,lead&&(i==0),lim&&(i==up))%mod)%mod;
		}
	}
	if(!lead&&!lim)	f[pos][pre1][pre2][right][lead][lim][1]=tmp;
	return tmp;
}

int main() {
//	freopen("in.txt","r",stdin);
//	freopen("baoli.out","w",stdout);
	memset(f,-1,sizeof(f));
	cin>>a;
	int lena=a.length()-1;
	if(a[lena]=='0')a[lena]='9',a[lena-1]=a[lena-1]-1;
	else a[lena]=a[lena]-1;
	lena++;
	while(lena--) 
		c[++posa]=a[lena]-'0';
	ans=dfs1(posa,-1,-1,0,1,1);
	memset(f,-1,sizeof(f));
	cin>>b;
	int lenb=b.length();
	while(lenb--) 
		d[++posb]=b[lenb]-'0';
	printf("%lld\n",dfs2(posb,-1,-1,0,1,1)-ans);
	return 0;
} 

piao的大佬的

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#define p 1000000007
using namespace std;
typedef long long LL;

int len,cnt,a[1005];
char l[1005],r[1005];
LL  f[1005][15][15][2][2][2];

LL dfs(int pos,int pre1,int pre2,bool right,bool lead,bool lim){
	if(!pos) return right?1:0;
	if(f[pos][pre1][pre2][right][lead][lim]!=-1) return f[pos][pre1][pre2][right][lead][lim];
	LL tmp=0,up=lim?a[pos]:9;
	for(int i=0;i<=up;i++){
		if(right) tmp=(tmp+dfs(pos-1,i,pre1,1,0,lim&&(i==up)))%p;
		else {
			if(lead) {
				if(!i) tmp=(tmp+dfs(pos-1,11,11,0,1,0))%p;
				else tmp=(tmp+dfs(pos-1,i,11,0,0,lim&&(i==up)))%p;
			}
			else tmp=(tmp+dfs(pos-1,i,pre1,right||(i==pre1)||(i==pre2),0,lim&&(i==up)))%p;
		}
	}
	return f[pos][pre1][pre2][right][lead][lim]=tmp;
}


LL solve(char *s){
	len=strlen(s+1);
	memset(f,-1,sizeof(f));
	for(int i=1;i<=len;i++) a[i]=s[i]-'0';
	if(a[len]==0)len--;
	return dfs(len,11,11,0,1,1);
}

int main() {
	scanf("%s%s",l+1,r+1);
	len=strlen(r+1),reverse(r+1,r+1+len);
	len=strlen(l+1),reverse(l+1,l+1+len);
	l[++cnt]--;
	while(l[cnt]-'0'==-1)l[cnt++]=9+'0',l[cnt]--;
	printf("%lld",(solve(r)-solve(l)+p)%p);
	return 0;
}

储能表

Description

Solution

hdu4507

P3286 [SCOI2014]方伯伯的商场之旅

P3303 [SDOI2013]淘金

【UER #4】被粉碎的数字

UOJ 86 mx的组合数

Description

Solution

posted @ 2020-09-06 11:20  ke_xin  阅读(104)  评论(0编辑  收藏  举报