6.15训练总结数位dp

6.15训练总结

数位dp

数位dp一般来说就是一个计数,主要是针对数位来计算的,跟数本身的大小无关。而且跟区间一样非常好判断,特别明显。一般有几个特点。

  • 数据范围一般非常鬼畜,从 1 ≤ n ≤ 1 e 9 1 \leq n \leq 1e9 1n1e9 或者 1 ≤ n ≤ 1 e 30 1 \leq n \leq 1 e30 1n1e30 可谓是层出不穷
  • 一般都是计数,与数位有关。

那我们由浅入深,先讲一下模版HDU3555吧。

例题

HDU3555 Bomb

原题链接

题目大意就是 1 1 1 N N N 中包含连续子串 49 49 49,有多少个。

我们就可以开这样的 dp 数组,大概如下。

  • d p i , 0 dp_{i,0} dpi,0,不含 49 49 49
  • d p i , 1 dp_{i,1} dpi,1 ,不含 49 49 49 但最高位为 9 9 9
  • d p i , 2 dp_{i,2} dpi,2,包含 49 49 49

大概什么思路呢?先以 N = 123456 N=123456 N=123456 为例,我们

因为要一位一位地判断,总得有个顺序吧。那是从高到低,还是从低到高。我们想一想,他的 N N N 的限制如果从低到高还能判断吗?你后面太大,前面卡了怎么办?所以,顺序就是从高到低了。好,我们来模拟一下。 N = 123456 N=123456 N=123456,我们最高位是0时没有限制,随便搞。百万位为 1 1 1 时,有限制,怎么办?找下一位,然后重复这个过程:找没有限制的,全部搞完,有限制了,下一位继续做

这是数位dp写法

#include<bits/stdc++.h>
long long dp[50][3];
void init()
{
    memset(dp,0,sizeof(dp));
    dp[0][0]=1;
    for(int i=1;i<50;i++)
	{
        dp[i][0]=dp[i-1][0]*10-dp[i-1][1];//!要减去一个第i-1位为9的情况,因为如果第i位为4的话就会构成49。
        dp[i][1]=dp[i-1][0];//!只让第i位为9,则就等于1x(以前不包含49的所有情况)。
        dp[i][2]=dp[i-1][2]*10+dp[i-1][1];//!以前包含的49则第i位有10中可能,加上以前没有包含49但是第i-1位为9的情况。
    }
}
long long solve(long long n)
{
    int bit[50],len=0;
    while(n)//!存储n的每一位
	{
        bit[++len]=n%10;
        n/=10;
    }
    bit[len+1]=0;
    long long ans=0;
    bool flag=false;
    for(int i=len;i;i--)//!从高位开始取,这里从len和len-1开始都是一样的。因为都不会改变ans的值
	{
        ans+=bit[i]*dp[i-1][2];//!加上i-1位所有包含49的情况。第i位可以填1到num[i]的数据。
        if(flag)//!如果前面出现了49的情况,则后面的数据可以直接加上了。
		{
            ans+=bit[i]*dp[i-1][0];
        }
        else
		{
            if(bit[i]>4)//!如果没有前面没有出现49的情况,并且第i位大于4
			{
                ans+=dp[i-1][1];//!则加上第i位为4,第i-1位为9的所有情况

            }
            if(bit[i+1]==4&&bit[i]==9)//!如果出现49的情况则改变标记。
			{
                flag=true;
            }
        }
    }
    if(flag)
	{
        ans++;
    }
    return ans;
}
int main()
{
    init();
    int T;
    scanf("%d",&T);
    while(T--)
	{
        long long n;
        scanf("%lld",&n);
        printf("%lld\n",solve(n));
    }
}

下面是记忆化搜索写法

#include<bits/stdc++.h>
using namespace std;
int digit[30];
long long dp[30][3];
int solve(long long x)
{
    memset(digit,0,sizeof(digit));
    int cnt=0;
    while(x)
    {
        digit[++cnt]=x%10;
        x/=10;
    }
    return cnt;
}
//!pos表示当前的位数
//!status中0表示不含49,1表示上一位是4,2表示含49
//!limit表示当前位是否受限制
long long solve(int pos,int status,int limit)
{
    if(pos<=0)
        return status==2;
    if(!limit&&dp[pos][status]!=-1)
        return dp[pos][status];//!这里就是说比如257,那么当你计算过了0到99之后,100到199就可以直接调用了,而2这一位是有限制的,所以不能直接调用现成的,要到下面重新计算一下
    long long ans=0;
    int num=limit?digit[pos]:9;
    for(int i=0;i<=num; i++)
    {
        int nstatus=status;//!相当于else s=status;
        if(status==0&&i==4)//!高位不含49,并且末尾不是4,现在末尾添加的是4返回状态1
            nstatus=1;
        else if(status==1&&i!=4&&i!=9)//!高位不含49,且末尾是4,现在末尾添加的不是4,返回状态0
            nstatus=0;
        else if(status==1&&i==9)//!高位不含49,且末尾是4,现在末尾添加9,返回状态2
            nstatus=2;
            ans+=solve(pos-1,nstatus,limit&&i==num);
    }
    if(!limit)
        dp[pos][status]=ans;
    return ans;
}
int main()
{
    int T;
    long long n;
    ios::sync_with_stdio(false);
    cin>>T;
    while(T--)
    {
        memset(dp,-1,sizeof(dp));
        cin>>n;
        int len=solve(n);
        long long sum=solve(len,0,1);
        cout<<sum<<endl;
    }
    return 0;
}

windy数

原题链接

题目大意不必多说,思路和上一题类似。

上代码。

#include<bits/stdc++.h>
using namespace std;
long long dp[15][15],ans;//!dp[i][j]表示搜到第i位,前一位是j,的limit方案和。
int a[15],len;
long long L,R;
long long dfs(int pos,int pre,int st,int limit)//!pos当前位置,pre前一位数,st判断前面是否全是0,limit最高位限制 
{
	if(pos>len) 
		return 1;//!搜完了 
	if(!limit&&dp[pos][pre]!=-1) 
		return dp[pos][pre];//!没有最高位限制,已经搜过了
	long long ret=0;
	int res=limit?a[len-pos+1]:9;//!当前位最大数字 
	for(int i=0;i<=res;i++)//!从0枚举到最大数字 
	{
		if(abs(i-pre)<2) 
			continue;//!不符合题意,继续 
		if(st&&i==0) 
			ret+=dfs(pos+1,-2,1,limit&&i==res);//!如果有前导0,下一位随意 
		else 
			ret+=dfs(pos+1,i,0,limit&&i==res);//!如果没有前导0,继续按部就班地搜 
	}
	if(!limit&&!st) 
		dp[pos][pre]=ret;//!没有最高位限制且没有前导0时记录结果 
	return ret;
}
void part(long long x)
{
	len=0;
	while(x) 
	{
		a[++len]=x%10;
		x/=10;
	}
	memset(dp,-1,sizeof dp);
	ans=dfs(1,-2,1,1);
}
int main()
{
    scanf("%lld%lld",&L,&R);
    part(L-1);
	long long minn=ans;
	part(R);  
	long long maxn=ans;
	printf("%lld",maxn-minn);
	return 0;
}

SAC#1 - 萌数

原题链接

[ l , r ] [l,r] [l,r] 内有多少个合法的正整数。一个数是合法的,当且仅当这个数字内部存在长度至少为 2 2 2 的回文子串。答案对 1 e 9 + 7 1e9+7 1e9+7​ 取模。

这题还是数位dp,比较模板,但是呢它让我们求带有回文子串的数字,有点多,嗯。那怎么办呢?当然就是正难则反。去求不包含的不就好了吗?OK,这题就搞定了。其余细节我放代码注释里了,自己去看。

#include<bits/stdc++.h>
#define Mod 1000000007
using namespace std;
int s1[1005],s2[1005];
int st[1005];
long long f[1005][15][15][2][2][2];
string sx,sy;
int len;
//! pre2当前位置的第前两位 pre1当前位置的第前一位
//! zero前导零  
//! pos当前位置
//! limit最高为限制
//! flag如果flag为1 则当前这个数是萌的 否则它为0 
//! len记录当前字符串的长度 
long long dfs (int pre2,int pre1,int pos,int limit,int zero,int flag)
{
    if(pos>len)//!位置都排完了
		return flag;
    long long ret=0;
    if(f[pos][pre1][pre2][limit][zero][flag]!=-1)//!位置排完,无前导0
		return f[pos][pre1][pre2][limit][zero][flag]%Mod;
    int limit=limit?st[pos]:9;//!最大上限
    for(int i=0;i<=limit;i++)
    {
        if(flag&&pre1!=-2)//!如果这个数是萌的 无需再判断了
			ret=(ret%Mod+dfs(pre1,(zero&&!i)?-2:i,pos+1,limit&&i==limit,(zero&&!i)?1:0,1)%Mod)%Mod;
        else
        {
			//!这里其实可以细分成aa和aba这两种,其他的都是复制过来的。
            if(pre1==i&&pre1!=-2||pre2==i&&pre2!=-2&&pre1!=-2)
                ret=(ret%Mod+dfs(pre1,(zero&&!i)?-2:i,pos+1,limit&&i==limit,(zero&&!i)?1:0,1)%Mod)%Mod;//1
            else
                ret=(ret%Mod+dfs(pre1,(zero&&!i)?-2:i,pos+1,limit&&i==limit,(zero&&!i)?1:0,0)%Mod)%Mod;//0 
        }
    }

    f[pos][pre1][pre2][limit][zero][flag]=(ret%Mod);//!存下状态
    return ret%Mod;
}
long long solve (int le,int flag)
{
    memset(st,0,sizeof(st));
    memset(f,-1,sizeof(f));
    len=le;
    if(flag==1)
    {
        for(int i=1;i<=len;i++)
            st[i]=s2[i];
    }
    else
    {
        for(int i=1;i<=len;i++)
			st[i]=s1[i];
    }
    return dfs(-2,-2,1,1,1,0)%Mod;
}
int main()
{ 
    cin>>sx>>sy;//!这里数据太大了,只能用字符串
    int len1=sx.length();
    for(int i=0;i<len1;i++)
    {
        s1[i+1]=sx[i]-'0';
    }
    int len2=sy.length();
    for(int i=0;i<len2;i++)
    {
        s2[i+1]=sy[i]-'0';
    }
    s1[len1]--;
    long long ans1=solve(len1,0);//!正难则反
    long long ans2=solve(len2,1);
    printf("%long longd",((ans2-ans1)%Mod+Mod)%Mod);//!一定要记得取模
    return 0;
}
储能表

原题链接

还行,普通的数位dp,不做过多的解释。

#include<bits/stdc++.h>
using namespace std;
const long long int N=70;
long long int f[N][2][2][2],g[N][2][2][2];
long long int n,m,k,T,p;
int main(){
	scanf("%lld",&T);
	while(T--)
	{
		memset(f,0,sizeof(f));
		memset(g,0,sizeof(g));
		scanf("%lld%lld%lld%lld",&n,&m,&k,&p);
		g[62][1][1][1]=1;
		for(int i=61;i>=1;i--){
			long long int x=(n>>(i-1))&1,y=(m>>(i-1))&1,z=(k>>(i-1))&1;
			for(long long int a=0;a<2;a++){
				for(long long int b=0;b<2;b++){
					for(long long int c=0;c<2;c++){
						if(f[i+1][a][b][c] || g[i+1][a][b][c]){
							for(long long int xx=0;xx<2;xx++){ 
								for(long long int yy=0;yy<2;yy++){
									long long int zz=xx^yy;
									if((a && x<xx)||(b && y<yy)||(c && z>zz)){ 
										continue;
									}
									long long int aa=(a && x==xx),bb=(b && y==yy),cc=(c && z==zz); 
									g[i][aa][bb][cc]+=g[i+1][a][b][c];
									g[i][aa][bb][cc]%=p;
									f[i][aa][bb][cc]+=f[i+1][a][b][c]+(zz-z+p)%p*((1ll<<(i-1))%p)%p*g[i+1][a][b][c]%p;
									f[i][aa][bb][cc]%=p;
								}
							}
						}
					}
				}
			}
		}
		printf("%lld\n",f[1][0][0][0]);
	}
}
花神的数论题

原题链接

这题简而言之求 1 1 1 n n n 中每个数的二进制下的 1 1 1 的个数的累乘。

那看到二进制我们很容易想到直接二进制拆分后直接套数位dp的模板,就是我标注的部分,好,上代码。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e7+7;
int len;
ll n; 
int a[55];
ll dp[55][55];
ll dfs(int p,int st,int limit)//!树形dp模板
{
	if(p>len) 
		return max(st,1);
	if(dp[p][st]!=-1&&!limit) 
		return dp[p][st];
	ll ret=1;
	int res;
	if(limit) 
		res=a[len-p+1];
	else 
		res=1;
	for(int i=0;i<=res;i++)
		(ret*=dfs(p+1,i==1?st+1:st,limit&&(i==res)))%=mod;
	if(!limit) 
		dp[p][st]=ret;
	return ret;
}
int main()
{
	scanf("%lld",&n);
	while(n)
	{
		a[++len]=n&1;
		n>>=1;
	}
	memset(dp,-1,sizeof dp);
	printf("%lld\n",dfs(1,0,1));
	return 0;
}

UVA12517 Digit Sum

这题的dfs与之前的非常类似,就是限制改一下,前几位就是枚举数字。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int l,r,dp[50][50],a[50],siz;
void dfs()
{
	for(int i=1;i<=20;++i)
	{
		for(int j=0;j<=9;++j)
		{
			for(int k=0;k<=9;++k)
			{
				dp[i][j]+=dp[i-1][k];
			}
			int s=1;
			for(int k=1;k<=i-1;++k)
			{
				s*=10;
			}
			dp[i][j]+=s*j;
		}
	}
}
void add(int x)
{
	memset(a,0,sizeof(a));
	siz=0;
	while(x>0)
	{
		a[++siz]=x%10;
		x/=10;
	}
}
int query(int x)
{
	add(x);
	int ans=0,getsum=0;
	for(int i=siz;i>=1;--i)
	{
		for(int j=0;j<a[i];++j)
		{
			ans+=dp[i][j];
			int p=1;
			for(int k=1;k<i;++k)
			{
				p*=10;
			}
			ans+=(p*getsum);
		}
		getsum+=a[i];
	}
	ans+=getsum;
	return ans;
}
signed main()
{
	int T;
	cin>>T;
	dfs();
	while(T--)
	{
		cin>>l>>r;
		if(l==0&&r==0)
			break;
		cout<<query(r)-query(l-1)<<"\n";
	}
	return 0;
}
Palindromic Numbers

题目大意:给你 i , j i,j ij 求两个数之间回文数的个数,注意前导零;

思路如下

  1. 既然是求 i , j i,j ij 之间的个数,还是求 f j − f j − 1 f_{j}-f_{j-1} fjfj1​​;
  2. 利用 d p i dp_{i} dpi 表示所有 i i i​ 位的数有多少回文数。但是还有一个问题,如何保存呢,分奇数位和偶数位两种情况:偶数位如果保证回文,要求对称,如果是 i i i 位数,前 i / 2 i/2 i/2 位要对称于后 i / 2 i/2 i/2 位,如四位数,前 2 2 2 位数字有 10 10 10 99 99 99 共有 90 90 90 种情况,所以四位数共有 90 90 90 个回文数;奇数位相当于在少一位的偶数位中间插入一个数,个数可以是 0 0 0 9 9 9 10 10 10 个数任意一个;例如五位数,中间不考虑就是有 90 90 90 种情况,这时考虑中间数有 10 10 10 种,所以共有 90 ∗ 10 90*10 9010​ 种情况;
  3. 分步算答案
#include<bits/stdc++.h> 
#define MAXN 10000005
#define Mod 10001
using namespace std;
int dight[40],tmp[40];
long long dp[40][100][100];
long long dfs(int start,int pos,int s,bool limit)
{
    if(pos<0)
        return s;
    if(!limit&&dp[pos][s][start]!=-1)
        return dp[pos][s][start];
    int end;
    long long ret=0;
    if(limit)
        end=dight[pos];
    else
        end=9;
    for(int d=0; d<=end; ++d)
    {
        tmp[pos]=d;
        if(start==pos&&d==0)
            ret+=dfs(start-1,pos-1,s,limit&&d==end);
        else if(s&&pos<(start+1)/2)
            ret+=dfs(start,pos-1,tmp[start-pos]==d,limit&&d==end);
        else
            ret+=dfs(start,pos-1,s,limit&&d==end);
    }
    if(!limit)
        dp[pos][s][start]=ret;
    return ret;
}
long long solve(long long a)
{
    memset(dight,0,sizeof(dight));
    int cnt=0;
    while(a!=0)
    {
        dight[cnt++]=a%10;
        a/=10;
    }
    return dfs(cnt-1,cnt-1,1,1);
}
int main()
{
    memset(dp,-1,sizeof(dp));
    int t,cnt=1;
    scanf("%d",&t);
    while(t--)
    {
        long long x,y;
        scanf("%lld%lld",&x,&y);
        if(x>y)
            swap(x,y);
        printf("Case %d: %lld\n",cnt++,solve(y)-solve(x-1));
    }
    return 0;
}
Magic Numbers

题目大意

给定 m , d , l , r m,d,l,r m,d,l,r ,求符合以下条件的数 x x x​ 的个数:

  • l ≤ x ≤ r l \leq x \leq r lxr
  • x x x 的偶数位是 d d d ,奇数位不是 d d d​ 。
  • m ∣ x m∣x mx

传三个参数 k , x , p k,x,p k,x,p 进入 dfs,分别表示枚举到第 k k k 位,当前数模 m m m 的余数,以及这一位填的数有没有限制,再记忆化存下即可

看似很简单,感觉不到紫题的难度,但是,还有很多注意的点。

  1. d d d 填的不能超过 m a x n maxn maxn
  2. a a a d f s dfs dfs 均从高往低处搜,避免因为限制卡掉。
  3. 数位dp是使用了前缀和的思想,但是这道题目可能会退位,所以毒瘤的高精度不可避免了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod=1e9+7;
int m,d,len,a[2005];
ll f[2005][2005];
char l[2005],r[2005];
ll dfs(int k,int x,int p)
{
    if(k>len)
        return x?0:1;
    if(!p&&f[k][x]!=-1)
        return f[k][x];
    int y=p?a[k]:9;
    ll res=0;
    if(k&1)
    {
        for(int i=0;i<=y;i++)
        {
            if(i==d)
                continue;
            res=(res+dfs(k+1,(x*10+i)%m,p&&(i==y)))%mod; 
        }
    }
    else
    {
        if(d<=y)
            res=(res+dfs(k+1,(x*10+d)%m,p&&(d==y)))%mod;
    }
    if(!p)
        f[k][x]=res;
    return res;
}
ll divide(char *s)
{
    len=strlen(s+1);
    for(int i=1;i<=len;i++)
        a[i]=s[i]-'0';
    return dfs(1,0,1);
}
bool check(char *s)
{
    len=strlen(s+1);
    int x=0;
    for(int i=1;i<=len;i++)
    {
        int y=s[i]-'0';
        x=(x*10+y)%m;
        if(i&1)
        {
            if(y==d)
                return false;
        }
        else
        {
            if(y!=d)
                return false;
        }
    }
    return !x;
}
int main()
{
	memset(f,-1,sizeof(f));
    scanf("%d%d%s%s",&m,&d,l+1,r+1);
    printf("%lld\n",(divide(r)-divide(l)+check(l)+mod)%mod);
    return 0;
}
  • 20
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值