数位dp 入门

数位dp几篇很不错的文章:

传送门:

https://blog.csdn.net/wust_zzwh/article/details/52100392

https://www.cnblogs.com/Rlemon/p/3418448.html

https://www.cnblogs.com/zbtrs/p/6106783.html

我们接下来直接讲几个例题:

例题一:

HDU 2089 不要62

杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。
杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
不吉利的数字为所有含有4或62的号码。例如:
62315 73418 88914
都属于不吉利号码。但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。

 

 

Input

输入的都是整数对n、m(0<n≤m<1000000),如果遇到都是0的整数对,则输入结束。

 

 

Output

对于每个整数对,输出一个不含有不吉利数字的统计个数,该数值占一行位置。

 

 

Sample Input

 

1 100 0 0

 

 

Sample Output

 

80

 

 

Author

qianneng

思路:

赤裸裸的模板题。

数位dp,我们定义一个数组dp[pos][is6],代表访问到第pos位,第pos-1位上的数字是否为6的满足条件的数字的个数。

代码:

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define MAXN 1e12+1
typedef long long ll;
using namespace std;
ll a,b;
ll pos[20];
ll dp[20][2];
ll dfs(ll i,bool is6,bool limit)
{
    if(i==0)
        return 1;
    if(!limit&&dp[i][is6]!=-1)
        return dp[i][is6];
    ll k=limit?pos[i]:9;
    ll ans=0;
    for(ll j=0;j<=k;j++)
    {
        if(is6&&j==2)
            continue ;
        if(j==4)
            continue ;
        ans+=dfs(i-1,j==6,limit&&j==pos[i]);
    }
    if(!limit)
        dp[i][is6]=ans;
    return ans;
}
ll solve(ll z)
{
    int j=1;
    while (z)
    {
        pos[j++]=z%10;
        z=z/10;
    }
    return dfs(j-1,false,true);
}
int main()
{
    memset(dp,-1,sizeof(dp));//这个题目有多组输入,因为对于每个数,该数是否满足题目条件是一定的,所以我们计算下一组时可以用上一组所算出来的结果
    while (~scanf("%lld%lld",&a,&b)&&a+b)
        printf("%lld\n",solve(b)-solve(a-1));
}

例题二:

2018 ACM 国际大学生程序设计竞赛上海大都会赛重现赛 J:

链接:https://www.nowcoder.com/acm/contest/163/J
来源:牛客网
 

NIBGNAUK is an odd boy and his taste is strange as well. It seems to him that a positive integer number is beautiful if and only if it is divisible by the sum of its digits.

We will not argue with this and just count the quantity of beautiful numbers from 1 to N.

输入描述:

The first line of the input is T(1≤ T ≤ 100), which stands for the number of test cases you need to solve.
Each test case contains a line with a positive integer N (1 ≤ N ≤ 1012).

输出描述:

For each test case, print the case number and the quantity of beautiful numbers in [1, N].

 

示例1

输入

复制

2
10
18

输出

复制

Case 1: 10
Case 2: 12

题目大意:

给出一个数,要求找出1-n满足:本身能够整除自身各位数之和,的数字的数量。

思路:

当看到n的范围是1e12时,就知道不可能是用暴力来求解的。这道题用到数位dp来做,我们假设一个数组为dp[pos][sum][ret]代表访问到第pos位,前面pos-1位的数字之和为sum,对某个数mod取模结果为ret时,满足条件的数字的个数。这个地方的mod实际上就是各个位上数字相加之和,只不过我们不知道,必须一一枚举出来。

举个例子:我们假设有个数字为126,这个数字是满足题目要求的,那么,我们如何知道这个数字是满足题目要求的呢?只有我们定为mod为9时,进行dfs后才可以知道这个数字是满足要求的,mod为其余的值时我们无法判断126是否为符合要求的数字。

代码:

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define MAXN 1e12+1
typedef long long ll;
using namespace std;
ll a[20];
ll dp[20][110][110];
ll t,n,mod;
ll dfs(ll pos,ll sum,ll ret,bool limit)
{
    if(pos==0)//最后一位也访问完成了
        return (sum==mod&&!ret);//返回是0还是1取决于该数字位数之和是否等于mod和该数字%mod是否为0(题目条件),如果这两个要求都满足,返回1,否则。返回0
    if(!limit&&dp[pos][sum][ret]!=-1)//当前状态在前面的时候已经算出来过了,直接返回
        return dp[pos][sum][ret];
    ll op=limit?a[pos]:9;//模板
    ll ans=0;
    for(ll i=0;i<=op;i++)
    {
        if(sum+i>mod)
            break ;
        ans+=dfs(pos-1,sum+i,(ret*10+i)%mod,limit&&i==a[pos]);
    }
    if(!limit)
        dp[pos][sum][ret]=ans;
    return ans;
}
ll solve(ll z)//这个函数求的是1到z范围内满足条件的数字的个数
{
    ll k=1;
    while (z)//将数字z的所有位上的数字放到a数组中去
    {
        a[k++]=z%10;
        z=z/10;
    }
    ll ans=0;
    for(ll i=1;i<=9*(k-1);i++)//这个地方我们必须枚举所有可能的mod(位数上的数字之和),如果是三位,那么枚举范围就是从1到9*3,然后将算出的结果累加
    {
        mod=i;
        memset(dp,-1,sizeof(dp));
        ans+=dfs(k-1,0,0,true);//对于每次dfs,求出来的是位数上的数字之和等于mod的满足题目要求的数字的个数
    }
    return ans;
}
int main()
{
    cin>>t;
    ll o=1;
    while (t--)
    {
        cin>>n;
        cout<<"Case "<<o++<<": "<<solve(n)<<endl;
    }
}

例题三:

题目大意:

给定一个范围,让你找出该范围内的数字满足数位和能被10整除的数字的个数。

思路:

定义一个数组dp[pos][sum]代表数字第pos位,前pos-1位的数位和为sum的满足条件的数字个数,只要这个状态确定了,接下来就是模板了。注意,这个题目存在特殊情况,当最后的sum为0的时候,0%10为0,这是不对的,需要特殊判断。

代码:

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define MAXN 1e12+1
typedef long long ll;
using namespace std;
ll a[20];
ll dp[20][200];
ll t,n,m,mod;
ll dfs(ll pos,ll sum,bool lead,bool limit)//lead是判断前导零的
{
    if(pos==0)
    {
        if(lead)
            return 0;
        else
            return !(sum%10);
    }
    if(!limit&&dp[pos][sum]!=-1)
        return dp[pos][sum];
    ll op=limit?a[pos]:9;
    ll ans=0;
    for(ll i=0;i<=op;i++)
        ans+=dfs(pos-1,sum+i,lead&&i==0,limit&&i==a[pos]);
    if(!limit)
        dp[pos][sum]=ans;
    return ans;
}
ll solve(ll z)//这个函数求的是0到z范围内满足条件的数字的个数
{
    ll k=1;
    while (z)//将数字z的所有位上的数字放到a数组中去
    {
        a[k++]=z%10;
        z=z/10;
    }
    return dfs(k-1,0,true,true);
}
int main()
{
    cin>>t;
    ll o=1;
    memset(dp,-1,sizeof(dp));//这个地方可以放在外面初始化,因为对于一个数,其数位之和能否被10整除是确定的。
    while (t--)
    {
        cin>>n>>m;
        cout<<solve(m)-solve(n-1)<<endl;
    }
}

例题四:

HDU 4734

题意:

题目给了个f(x)的定义:F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1,Ai是十进制数位,然后给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。

思路:

常规想:这个f(x)计算就和数位计算是一样的,就是加了权值,所以dp[pos][sum],这状态是基本的。a是题目给定的,f(a)是变化的不过f(a)最大好像是4600的样子。如果要memset优化就要加一维存f(a)的不同取值,那就是dp[10][4600][4600],这显然不合法。

这个时候就要用减法了,dp[pos][sum],sum不是存当前枚举的数的前缀和(加权的),而是枚举到当前pos位,后面还需要凑sum的权值和的个数,

也就是说初始的是时候sum是f(a),枚举一位就减去这一位在计算f(i)的权值,那么最后枚举完所有位 sum>=0时就是满足的,后面的位数凑足sum位就可以了。

仔细想想这个状态是与f(a)无关的(新手似乎很难理解),一个状态只有在sum>=0时才满足,如果我们按常规的思想求f(i)的话,那么最后sum>=f(a)才是满足的条件。

代码:

​
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define MAXN 1e12+1
typedef long long ll;
using namespace std;
ll t;
ll a,b;
ll a1[12];
ll ren;
ll dp[12][20000];
ll f(ll z)//计算f(x)
{
    ll ans=0;
    ll k=0;
    while (z)
    {
        ans+=pow(2,k)*(z%10);
        z=z/10;
        k++;
    }
    return ans;
}
ll dfs(ll pos,ll sum,bool limit)
{
    if(pos==-1)
        return (sum>=0);
    if(sum<0)//这个地方相当于一个剪枝,如果还没有到达这一条线上的数字的最后一位,sum已经小于0了,直接返回0,这一条线上的数字肯定不满足条件
        return 0;
    if(!limit&&dp[pos][sum]!=-1)
        return dp[pos][sum];
    ll op=limit?a1[pos]:9;
    ll ans=0;
    for(ll i=0;i<=op;i++)
        ans+=dfs(pos-1,sum-(i*pow(2,pos)),limit&&i==a1[pos]);
    if(!limit)
        dp[pos][sum]=ans;
    return ans;
}
ll solve(ll z)
{
    ll k=0;
    while (z)
    {
        a1[k++]=z%10;
        z=z/10;
    }
    return dfs(k-1,ren,true);
}
int main()
{
    cin>>t;
    ll o=1;
    memset(dp,-1,sizeof(dp));//这个地方必须运用memset优化
    while (t--)
    {
        cin>>a>>b;
        ren=f(a);
        cout<<"Case #"<<o++<<": "<<solve(b)<<endl;
    }
}

​

例题五:

POJ 3252

题意:

给定一个范围,让你找出该范围内的数字的二进制中0的数量不少于1的数量的数字的个数。

思路:

这题的约束就是一个数的二进制中0的数量要不能少于1的数量,我们假设dp[pos][ans]代表前pos-1位,0的数量减去1的数量为ans的满足条件的数字的个数。中间某个pos位上ans可能为负数(这不一定是非法的,因为我还没枚举完嘛,只要最终的ans>=0才能判合法,中途某个pos就不一定了),这里比较好处理,二进制最小就-32,直接加上32,把32当0用(不能直接用0,因为如果用0的话,有可能出现负数,数组下标没有负数,所以用32代表0,小于32的就是负数,大于32的就是正数)。

注意,这个题存在前导零问题。对于什么是前导零,我们就这个题目来说,对于这个题目,如果我们枚举到的一个数的二进制为0000000101,那么第一个1前面的0都是没有用的,如果我们没有判断前导零,那么前面这些0都会被算到结果中去,显然错误,所以我们需要判断前导零,对于有些题目,就不会存在前导零问题,例如上面的例题一,如果我们枚举到一个数为001,这个数其实是1,但是这个数前面的0不会影响最终结果,这就不会存在前导零问题。

代码:

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define MAXN 1e12+1
typedef long long ll;
using namespace std;
ll a,b;
ll a1[33];//a1数组存的是一个数的二进制的各个位上的值,因为二进制最多不超过32为,所以说开33大小的数组就可以了
ll dp[35][66];//dp[pos][ans]代表数字二进制前pos-1位,0-1差值为ans的满足条件的数字的个数,这个地方我们以32为分界点,比32大的视为大于0的,比32小的视为小于0的,这个地方不能用0作为分界点,因为数组下标没有小于0的
ll dfs(ll pos,ll ans,bool lead,bool limit)//lead判断前导零
{
    if(pos==0)
        return ans>=32;
    if(!limit&&!lead&&dp[pos][ans]!=-1)
        return dp[pos][ans];
    ll op=limit?a1[pos]:1;
    ll sum=0;
    for(ll i=0;i<=op;i++)
    {
        if(lead&&i==0)
            sum+=dfs(pos-1,ans,true,limit&&i==a1[pos]);
        else
            sum+=dfs(pos-1,ans+(i==0?1:-1),false,limit&&i==a1[pos]);
    }
    if(!limit&&!lead)
        dp[pos][ans]=sum;
    return sum;
}
ll solve(ll z)
{
    ll k=1;
    while (z)//提取一个数的二进制的各个位数上的值的方法
    {
        a1[k++]=z&1;
        z=z>>1;
    }
    return dfs(k-1,32,true,true);
}
int main()
{
    memset(dp,-1,sizeof(dp));
    while(~scanf("%lld%lld",&a,&b))
        cout<<solve(b)-solve(a-1)<<endl;
}

例题六:

HDU 3709

题意:

给定区间[a,b],求区间内平衡数的个数,平衡数的定义:例如4139,我们以3为中心,左边4*2+1*1=9等于右边9*1=9,我们称这样的数为平衡数。

思路:

这题就是要枚举中轴,然后数位dp.

我们假设dp[pos][k][sum]代表枚举到数字前pos-1位,我们假设的平衡位置为k,前pos-1位数字乘以距离的和为sum满足条件的数字的个数,最后如果sum等于0,说明是平衡数。注意,对于每次枚举平衡位置时,我们都会进行dfs,每进行dfs一次dfs时,全为0的这次都会满足,所以最后要减去多余的全为0的这次。

还有个小技巧是当sum<0时就可以直接return了,可以加速。因为sum的变化是先加后减的,如果sum一旦小于0,sum后面的变化肯定是减的,只会越来越小,肯定不符合条件,直接返回。

代码:

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
typedef long long ll;
using namespace std;
ll t;
ll x,y;
ll dp[20][20][2000];
ll a[20];
ll dfs(ll pos,ll k,ll sum,bool limit)
{
    if(pos==0)
        return !sum;
    if(sum<0)//这个地方是加速过程,因为sum的变化是先加后减的,如果sum一旦小于0,sum后面的变化肯定是减的,只会越来越小,肯定不符合条件,直接返回
        return 0;
    if(!limit&&dp[pos][k][sum]!=-1)
        return dp[pos][k][sum];
    ll ans=0;
    ll op=limit?a[pos]:9;
    for(ll i=0;i<=op;i++)
        ans+=dfs(pos-1,k,sum+(pos-k)*i,limit&&i==a[pos]);
    if(!limit)
        dp[pos][k][sum]=ans;
    return ans;
}
ll solve(ll z)
{
    ll k=1;
    while (z)
    {
        a[k++]=z%10;
        z=z/10;
    }
    ll ans=0;
    for(ll i=1;i<k;i++)
        ans+=dfs(k-1,i,0,true);
    return ans-(k-1);
}
int main()
{
    cin>>t;
    while (t--)
    {
        cin>>x>>y;
        memset(dp,-1,sizeof(dp));
        cout<<solve(y)-solve(x-1)<<endl;
    }
}

另一种解法:

这种解法无法过oj,提示内存超限,个人感觉时间复杂度也很高,不过答案是正确的。

思路:

还是枚举平衡位置,然后数位dp,只不过这次dp[pos][sum1][sum2]代表数字的前pos-1位,在平衡位置左边的数字和为sum1,在平衡位置右边的数字和为sum2的满足条件的数字的个数。最后如果sum1等于sum2,说明是平衡数。

代码:

#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define mod 1000000007
typedef long long ll;
using namespace std;
ll a[20];
ll dp[20][2000][2000];
ll t,x,y;
ll dfs(ll pos,ll f,ll s,ll sum1,ll sum2,bool limit)
{
    if(pos==0)
        return sum1==sum2;
    if(!limit&&dp[pos][sum1][sum2]!=-1)
        return dp[pos][sum1][sum2];
    ll op=limit?a[pos]:9;
    ll ans=0;
    for(ll i=0;i<=op;i++)
    {
        if(pos>f)//当前枚举的数字位数在平衡位置的左边
            ans+=dfs(pos-1,f,s,sum1+((pos-f)*i),sum2,limit&&i==a[pos]);
        else if(pos==f)//当前枚举的数字位数在平衡位置处
            ans+=dfs(pos-1,f,1,sum1,sum2,limit&&i==a[pos]);
        else//当前枚举的数字位数在平衡位置的右边
            ans+=dfs(pos-1,f,s+1,sum1,sum2+(i*s),limit&&i==a[pos]);
    }
    if(!limit)
        dp[pos][sum1][sum2]=ans;
    return ans;
}
ll solve(ll z)
{
    ll k=1;
    while (z)
    {
        a[k++]=z%10;
        z=z/10;
    }
    ll ans=0;
    for(ll i=k-1;i>=1;i--)
    {
        memset(dp,-1,sizeof(dp));//这个地方无法使用memset优化
        ans+=dfs(k-1,i,i,0,0,true);
    }
    return ans-(k-1);
}
int main()
{
    cin>>t;
    while (t--)
    {
        cin>>x>>y;
        cout<<solve(y)-solve(x-1)<<endl;
    }
}

例题七:

 HDU - 6156

题意:

求L~R所有的数的l~r进制的f(x,k进制), 如果x是回文串f(x,k进制) = k, 否则等于1,累加f

思路:

数位dp,我们定义状态:dp[pos][cnt][cary][ishuiwen],代表当前访问到pos位,最长回文串的长度为cnt,cary进制,是否为回文串的满足是回文串的数字的个数。

我们通过数位dp求出L到R范围内所有满足条件的数字的个数(假设为count个),然后乘cary,累加,此时(R-L+1)-count就代表L到R范围内不是回文串的数字的个数,根据题目可得,此时f函数返回的是1,累加上就可以了。

注意,此题有前导零问题,需要处理。

我们以12344321为例,此时cnt等于7,刚开始cnt,pos都为7,

情况一:当pos大于(cnt+1)/2时,说明pos位还在整个数字串的前半部分,此时是否为回文无法判断,ishuiwen默认为true,

情况二:当pos小于(cnt+1)/2时,此时我们判断pos位上的数字和cnt-pos位上的数字是否相等,如果相等,ishuiwen为true,然后判断下一位,否则(此时,整个数字串肯定不是回文串),ishuiwen为false。

代码:

#include<algorithm>
#include<iostream>
#include<limits.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
#include<cstdlib>
#include<cstring>
#include<cassert>
#include<string>
#include<cstdio>
#include<bitset>
#include<vector>
#include<cmath>
#include<ctime>
#include<stack>
#include<queue>
#include<deque>
#include<list>
#include<set>
#define MAXN 1000001
#define mod 1000000007
typedef long long ll;
using namespace std;
ll dp[40][40][50][2];
int num[40];
int temp[40];//存储当前遍历的这条线上的数字
int t;
ll L,R;
int l,r;
ll dfs(int pos,int cnt,int cary,bool ishuiwen,bool limit)
{
    if(pos==-1)
        return ishuiwen;
    if(!limit&&dp[pos][cnt][cary][ishuiwen]!=-1)
        return dp[pos][cnt][cary][ishuiwen];
    int m=limit?num[pos]:cary-1;//这个地方是cary-1,不是9,因为我们习惯了10进制了,这个地方是cary进制
    ll ans=0;
    for(int i=0;i<=m;i++)
    {
        temp[pos]=i;//储存起来,后面需要判断是否为回文
        if(pos==cnt&&!i)//这个if语句是用来判断前导零的,直到i不等于0时,说明我们访问的数字才开始有意义,此时最大的可能的回文串的长度边短了(因为前面的0都不算),所以cnt要--
            ans+=dfs(pos-1,cnt-1,cary,ishuiwen,limit&&i==num[pos]);
        else if(ishuiwen&&pos<(cnt+1)/2)//情况二
            ans+=dfs(pos-1,cnt,cary,i==temp[cnt-pos],limit&&i==num[pos]);
        else//情况一
            ans+=dfs(pos-1,cnt,cary,ishuiwen,limit&&i==num[pos]);
    }
    if(!limit)
        dp[pos][cnt][cary][ishuiwen]=ans;
    return ans;
}
ll solve(ll z,int cary)
{
    int k=0;
    while (z)//这个地方是套路,求一个十进制数的cary进制的各个位上的数字
    {
        num[k++]=z%cary;
        z=z/cary;
    }
    return dfs(k-1,k-1,cary,true,true);
}
int main()
{
    scanf("%d",&t);
    int h=1;
    memset(dp,-1,sizeof(dp));
    while (t--)
    {
        ll sum=0;
        scanf("%lld%lld%d%d",&L,&R,&l,&r);
        for(int i=l;i<=r;i++)
        {
            sum+=(solve(R,i)-solve(L-1,i))*i;
            sum+=R-solve(R,i)-(L-1-solve(L-1,i));//剩下的就是f函数返回的是1的字符串
        }
        printf("Case #%d: %lld\n",h++,sum);
    }
}

收获:

知道了如何求十进制数的其它进制的各个位上的数字,相当于一个模板。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值