数位dp学习笔记

本文详细介绍了数位dp在解决计数问题中的应用,如找出区间内不包含特定数字串(如62)的整数个数,以及如何通过数位状态转移计算B-number问题中的符合条件的数。通过实例和代码展示了如何构造dp数组并进行状态转移,适用于范围大、条件与数位结构有关的问题。
摘要由CSDN通过智能技术生成

前言

数位dp是一种计数用的dp,一般都是求再区间[l,r]之间满足条件的数的个数。一般像这种题,给出的数据范围都比较大,一个一个进行遍历显然不行,所以就用到了我们今天学习的数位dp,顾名思义就是在数位上进行dp。
数位的含义:个位、十位、百位等等,数的每一位就是就是数位。
数位dp的实质就是 换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后记忆化就可以了。

数位dp一般应用于:

(1)求出在给定区间[A,B]内,符合条件 P(i) 的数 i 的个数。

(2)条件P(i)一般与数的大小无关,而与数的数位组成有。
数位dp模板

不要62[HDU2089]

题目链接

题目描述

题意很简单,就是给定一个区间,找这个区间里,不含有“62”和“4”的数字的个数。
遇到都是0的整数对,则输入结束。
这道题目做的时候主要使用下面的思路来一步步进行。
1、首先,暴力枚举行不通,因此我们就想,能不能有一个函数count(x)表示从0到x的符合题意的数的个数。得到这个函数之后对于给定区间[n,m],我们就可以直接求count(m)-count(n)的值,直接就将答案算出来了。
2、利用数位dp记录下的值,我们可以在短时间内算出这个count。具体就是设一个数组dp[i][j]来表示i位数,最高位是j时的符合题意的个数。比如dp[1][2]=1、dp[1][4]=0
这道题关键的就是状态转移部分,状态转移的关键其实就是要将不符合题意的数字排除出要加的数字的行列。所以关键就是判断6和4两个部分。

if(j==4)
{
    dp[i][j]=0;
}
else if(j==6)
{
    for(int k=0; k<=9; k++)
    {
        if(k!=2)
        {
            dp[i][j]+=dp[i-1][k];
        }
    }
}
else
{
    for(int k=0; k<=9; k++)
    {
        dp[i][j]+=dp[i-1][k];
    }
}

代码

#include <iostream>
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll dp[8][10];//数位dp的dp数组一般都比较小
//这里要用ll,不然会超出范围
void init()//初始化数组
{
    int i,j,k;
    memset(dp,0,sizeof(dp));
    for(int j=0;j<=9;j++)//位数为1时情况很简单
    {
        if(j!=4)
            dp[1][j]=1;
    }
    for(i=2; i<=7; i++)//位数大于1,i表示位数
    {
        for(j=0; j<=9; j++)//j表示最高位的数字
        {
            if(j==4)//j等于4时dp为零
            {
                dp[i][j]=0;
            }
            else if(j==6)//j等于6时需要看一下下一位是不是2
            {
                for(k=0; k<=9; k++)
                {
                    if(k!=2)
                    {
                        dp[i][j]+=dp[i-1][k];
                    }
                }
            }
            else//j等于其他数时没有特殊情况,直接相加
            {
                for(k=0; k<=9; k++)
                {
                    dp[i][j]+=dp[i-1][k];
                }
            }
        }
    }
}

int a[20];
ll solve(int n)//求解的小于x符合题意的数的个数
{
    int len=0;//len代表位数,先将len求出来
    int i,j;
    while(n)
    {
        a[++len]=n%10;//ai代表着每位数
        n/=10;
    }
    a[len+1]=0;//终止
    ll ans=0;
    for(i=len; i>=1; i--)//倒序,从最高位到个位
    {
        for(j=0; j<a[i]; j++)
        {
            if(j==4||(a[i+1]==6&&j==2))//不要62和4
            {
                continue;
            }
            else
            {
                ans+=dp[i][j];
            }
        }
        if(a[i]==4)
            break;
        if(a[i+1]==6&&a[i]==2)
            break;
    }
    return ans;
}

int main()
{
    init();
    int n,m;
    while(scanf("%d%d",&m,&n)!=EOF)
    {
        if(m==0||n==0)
        {
            break;
        }
        ll ans=1ll*(solve(n+1)-solve(m));
        printf("%lld\n",ans);
    }
    return 0;
}

使用记忆化方法的代码

#include <bits/stdc++.h>
#define mem1(x) memset(x,-1,sizeof(x))
using namespace std;

typedef long long ll;
const int wei=100;
//ll x,y,k,b;
ll dp[35][70];
ll digit[wei];

ll dfs(ll pos,ll pre,ll sta,bool limit)
{
    if(pos==-1) return 1;
    if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];
    ll up=limit?digit[pos]:9;
    ll ans=0;

    for(ll i=0;i<=up;i++)
    {
        if(pre==6&&i==2)
            continue;
        if(i==4)
            continue;
        ans+=dfs(pos-1,i,i==6,limit && i==digit[pos]);
    }
    if(!limit) dp[pos][sta]=ans;
    return ans;
}

ll answer(ll x)
{
    ll pos=0;
    while(x)
    {
        digit[pos++]=x%10;
        x/=10;
    }
    return dfs(pos-1,-1,0,true);
}

int main()
{
    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri)&&(le||ri))
    {
        mem1(dp);
        printf("%lld\n",answer(ri)-answer(le-1));
    }
	return 0;
}

Amount of Degrees

题解链接
题面很简单,就是求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于 K 个互不相等的 B 的整数次幂之和。例如,设 X=15,Y=20,K=2,B=2,则有且仅有下列三个数满足题意:
17=24+20
18=24+21
20=24+22
解法我们就还是套用上面的那一套模板,不过要在模板之上进行一些变动。我们注意到满足题意的个数就是比当前数小的位数上有固定个1,其他位都是零的个数。所以我们其实就是在这个基础上去解题的。
首先我们知道如果不贴和上界,其实后面的低位直接就可以用组合数来求出。如果贴合上界,那我们就需要枚举最多两次来更新答案。

#include <bits/stdc++.h>
#define mem(x) memset(x,0,sizeof(x))
#define mem1(x) memset(x,-1,sizeof(x))
using namespace std;

typedef long long ll;
const int wei=100;
int dp[35][70];
int digit[wei];
int l,r,k,b;

void C_init()//预处理组合数
{
    for (int i=0;i<=35;i++)
    {
        dp[i][0]=1;
        for (int j=1;j<=i;j++)
            dp[i][j]=dp[i-1][j]+dp[i-1][j-1];
    }
}

int dfs(int pos,int lead,bool limit)//当前在第pos位,还需要取B进制上的lead个1,limit表示是否贴合上界
{
    if(!limit) return dp[pos][lead];//如果不贴合上界,即剩下i位可以随便填,用组合数即可直接计算方案数,从i位中选取j个1
    int up=limit?digit[pos]:b-1;//判断当前位能填的最大数
    int ans=0;
    for(int i=0; i<=min(up,1); i++)//枚举该位填0还是1
    {
        ans+=dfs(pos-1,lead-i,limit&&i==up);//更新答案
    }
    return ans;
}

int answer(int x)
{
    int pos=0;
    while(x)
    {
        digit[++pos]=x%b;//这里不能从0开始,因为要考虑到我们预处理的dp数组就是从1开始的,否则两者会不匹配。
        x/=b;
    }
    return dfs(pos,k,true);
}

int main()
{
    C_init();
    scanf("%d%d%d%d",&l,&r,&k,&b);
    printf("%d\n",answer(r)-answer(l-1));
    return 0;
}

B-number

题目链接
这道题的大意就是给定一个数n,让你在1~n的范围中找到符合下面要求的数的个数。要求是:数被13整除并且里面包含13。例如:13,2613都符合条件,2626,143不符合条件。题目包含多个输入。
这道题基本上就是模板题,直接就可以套用上面写的那一套模板,只不过在描述状态的时候要将状态描述正确,可以注意到题目中只有两个限制条件,那么肯定有两个参数来表示这两个状态。

代码

#include <bits/stdc++.h>
#define inf 0x7fffff
#define mem(x) memset(x,0,sizeof(x))
#define mem1(x) memset(x,-1,sizeof(x))
using namespace std;

typedef long long ll;

ll digit[15];
ll dp[15][15][3];

ll dfs(int pos, int mod, int have, bool limit)//前三个数pos表示位数,mod表示余数,have三个状态分别表示末尾是1、末尾不是1、含有13,limit表示上限
{
    int num,mod_x,have_x;
    ll ans;
    if (pos==-1)
        return mod==0&&have==2;
    if (!limit && dp[pos][mod][have]!=-1) //没有上限且被访问过
        return dp[pos][mod][have];
    ans = 0;
    num = limit?digit[pos]:9;//如果有上限,只能取到当前位数,如果没上限,可取到9
    for (int i=0; i<=num; i++)
    {
        mod_x = (mod*10+i)%13;//该位的每种情况对13取模
        have_x = have;
        if (have==0 && i==1)//末尾加1
            have_x=1;
        if (have==1 && i!=1)//末尾已经为1了
            have_x=0;
        if (have==1 && i==3)//末尾是1,现在加3
            have_x=2;
        ans+=dfs(pos-1, mod_x, have_x, limit&&i==num);//如果i==num,下一位能取的最大数就为s[pos-1],i!=num,下一位能取到9
    }
    if(!limit)
        dp[pos][mod][have] = ans;
    return ans;
}

ll answer(ll x)
{
    mem(digit);
    ll pos=0;
    while(x)
    {
        digit[pos++]=x%10;
        x/=10;
    }
    return dfs(pos-1,0,0,true);
}

int main()
{
    ll n;
    while(~scanf("%lld",&n))
    {
        mem1(dp);
        printf("%lld\n",1ll*(answer(n)));
    }
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值