数位dp


title: 数位dp

数位dp

文中提到的引用均来自巨佬

  • 数位dp,一般是求一段区间内满足给定条件的数的个数,数位:顾名思义,就是按照一个数的位数进行dp,比如一个三位数,就按照百位、十位、个位进行。
  • 但是我感觉,有些带记忆的dfs内味了

数位dp实质还是一种暴力枚举的方法
对于区间 [l,r] 求满足条件的数的个数,最简单的暴力做法如下:

for(int i = l;  l <= r; i++){
	if(fair(i))	ans++;
}

但是这样如果l 是1而r是1e9,这种暴力的做法就直接没了,而数位dp由于是按照位数进行枚举每一种可能,这样就降低了每一次判断的复杂度(个人理解)其次就是记忆化搜索(这个是最重要的一点),使枚举的数字都满足了dp的性质 满足递推公式 dp[pos][j] = sum(dp[pos - 1][k]) j是指某个状态下(这个可以先略过,重点是公式)

这是我引用的数位dp枚举的一个小过程 说好的自己写呢?

新的枚举:控制上界枚举,从最高位开始往下枚举,例如:ri=213,那么我们从百位开始枚举:百位可能的情况有0,1,2(觉得这里枚举0有问题的继续看)
然后每一位枚举都不能让枚举的这个数超过上界213(下界就是0或者1,这个次要),当百位枚举了1,那么十位枚举就是从0到9,因为百位1已经比上界2小了,后面数位枚举什么都不可能超过上界。所以问题就在于:当高位枚举刚好达到上界是,那么紧接着的一位枚举就有上界限制了。具体的这里如果百位枚举了2,那么十位的枚举情况就是0到1,如果前两位枚举了21,最后一位之是0到3(这一点正好对于代码模板里的一个变量limit 专门用来判断枚举范围)。最后一个问题:最高位枚举0:百位枚举0,相当于此时我枚举的这个数最多是两位数,如果十位继续枚举0,那么我枚举的就是以为数咯,因为我们要枚举的是小于等于ri的所以数,当然不能少了位数比ri小的咯!(这样枚举是为了无遗漏的枚举,不过可能会带来一个问题,就是前导零的问题,模板里用lead变量表示,不过这个不是每个题目都是会有影响的,可能前导零不会影响我们计数,具体要看题目)

上面的枚举只规定了上限,即区间的上边界,那么我们该如果去求得 [l,r] 区间满足条件的个数呢?我们上面求的既然是 [1,r] 那么我们再求一遍 [1 , l -1],这样用 num( [1,r] ) - num([1, l - 1]) 就可以求出 [l, r] 的满足条件的数量

这里我直接引用一个数位dp的模板(套路),把里面一些我自己的理解说一说

typedef long long ll;
int a[20];
ll dp[20][state];//不同题目状态不同
ll dfs(int pos,/*state变量*/,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;这个的例子前面用213讲过了
    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]) //最后两个变量传参都是这样写的
        /*这里还算比较灵活,不过做几个题就觉得这里也是套路了
        大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
        去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
        要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
        前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
    }
    //计算完,记录状态
    if(!limit && !lead) dp[pos][state]=ans;
    /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
    return ans;
}
ll solve(ll x)
{
    int pos=0;
    while(x)//把数位都分解出来
    {
        a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行
        x/=10;
    }
    return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int main()
{
    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri))
    {
        //初始化dp数组为-1,这里还有更加优美的优化,后面讲
        printf("%lld\n",solve(ri)-solve(le-1));
    }
}

对于

  • 1、 limit && i==a[pos]
    因为我们要判断当前位数前一位是不是到达上界了,只有&后面的条件是不可以的
    比如 213 如果 百位枚举1,十位枚举1,那么要枚举个位的时候,十位到达上界了吗?显然没有,因为十位可以枚举到10,所以我们要往前判断两步,一步是枚举上一位数是不是与上边界的相同位的数字相等,以及上上位数是不是处于那一位的边界状态。

  • 2、在减枝和记忆化中的 if(!limit) 首先看 return dp[pos][sta] 如果,由于dp[pos][sta]是当前状态和当前位数下所有满足条件的和,比如213 ,dp[1][sta]是后两位所有满足条件的和,也就是说十位的枚举是用0 - 9里面找满足的数字,但是如果没有前面的限制条件,我们在已知百位枚举到2的时候,我们十位可以枚举 0 -9里面满足的数吗?显然不能因为由于前面百位的上界的限制,肯定不能枚举0 -9,所以直接返回会返回比正确答案多的数量,同理在记忆化的时候,由于它枚举的总和小于我们规定的dp[pos][sta]的值,如果这时候记忆化了,就导致dp数组的值要比正确的值小,在后面直接取记忆化的值时,会造成错误

这句引用的话也说的很好

limit为true的数并不多,一个个枚举不会很浪费时间,所以我们记录下! limit的状态解决了不少子问题重叠。

例题:

hdu 2089 不要62

要求数位上不能有4,且62不能连续出现,这样枚举的时候,遇见4直接跳过,遇到6先记下来,然后在枚举下一位的时候,如果有2就跳过,这里开两个状态记录分别记录,dp[pos][sta] pos是当前位数,sta是上一位是不是6 (0不是,1是)

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <stack>
#include <queue>
#include <map>
#include <vector>

using namespace std;    

typedef long long ll;

const int MAXN = 1e6 + 7;
const int Mod = 1e9 + 7;

int a[20];
int dp[20][2];
//第二维 0是第 i 位前一位不为6时满足条件的个数,1则是前一位为6时满足条件的个数

int dfs(int pos , int pre , int sta, bool limit)
{
    //递归终点,到终点的数一定是满足条件的,所以直接计数+1
    if(pos == -1)   return 1;
    if(!limit && dp[pos][sta] != -1)    return dp[pos][sta];
    int up = limit? a[pos] : 9;
    int  tmp = 0;
    for(int i = 0; i <= up; i++){
        if(i == 4)  continue;
        if(pre == 6 && i == 2)  continue;
        tmp += dfs(pos - 1 , i , i == 6 , limit && i == a[pos]);
    }
    if(!limit)  dp[pos][sta] = tmp;
    return tmp;
}

int Solve(int x)
{
    int pos = 0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    int ans = dfs(pos - 1 , -1 ,0 ,true);
    return ans;
}

int main()
{
    int l,r;
    while(scanf("%d%d",&l,&r) && l + r){
        memset(dp, - 1, sizeof dp);
        printf("%d\n", Solve(r) - Solve(l - 1)); 
    }
    return 0;
}

数位dp的优化:

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的个数。

这个题,如果你选择最朴素的数位dp的做法的话,因为输入一次a的值,就要重新清空一次dp数组,由于这个题时间卡的很死,所以把memset放在循环里, 会时间超限
或者按照这种思路但是内存就爆了

如果要memset优化就要加一维存f(a)的不同取值,那就是dp[10][4600][4600],这显然不合法。,所以这里优

思路:

  • 那我们就可以选择利用减法,每一步都减去当前位置的权值,那么当走到最后一位的时候,如果sum >= 0 ,说明f(a) >= f(i)的,相反如果sum在某一位的时候 < 0就说明一定f(i) < f(a),就不用继续向下找了,起到了剪枝的作用

所以这样dp状态就和f(a)没有关系了,那么我们只需要在循环外memset一次就可以了
因为我们的终点状态是跟f(a)没有关系的,这样就算我们遇到了上次循环用到的dp数组空间,那么也没有关系,因为上一次的dp[pos][rsum]难道和这一次到这个状态的值不一样吗?显然是一样的,如过没有遇到上一次的状态,那我们也可以当作清空了dp数组,这样来说,清一次dp数组就够了.

TLE 代码

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <stack>
#include <queue>
#include <map>
#include <vector>

using namespace std;    

typedef long long ll;

const int MAXN = 1e6 + 7;
const int Mod = 1e9 + 7;
const int Sum = 5000;

int a[20] , p[11];
int dp[20][Sum];
int cmp;

int dfs(int pos , int sum , bool limit)
{
    //递归终点,到终点的数一定是满足条件的,所以直接计数+1
    if(pos == -1)   return  sum <= cmp;
    if(!limit && dp[pos][sum] != -1)    return dp[pos][sum];
    int up = limit? a[pos] : 9;
    int  tmp = 0;
    for(int i = 0; i <= up; i++){
        tmp += dfs(pos - 1 ,sum + i * p[pos], limit && i == a[pos]);
    }
    if(!limit)  dp[pos][sum] = tmp;
    return tmp;
}

int Solve(int x)
{
    int pos = 0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    int ans = dfs(pos - 1, 0,true);
    return ans;
}

int f(int x)
{
    int l = 0,fsum = 0;
    while(x){
        int d = x % 10;
        x  = x / 10;
        fsum  = fsum + d * p[l];
        l++;
    }
    return fsum;
}

int main()
{
    int a,b,t;
    scanf("%d",&t);
    p[0] = 1;
    for(int i = 1; i <= 10; i++)    p[i]  = p[i - 1] * 2;
    //memset(dp, - 1, sizeof dp);
    int Case = 0;
    while(t--){
        memset(dp, - 1, sizeof dp);
        Case++;
        scanf("%d%d",&a,&b);
        cmp  = f(a);
        printf("Case #%d: %d\n",Case, Solve(b) ); 
    }
    return 0;
}

ac代码

    #include <iostream>
    #include <algorithm>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <string>
    #include <stack>
    #include <queue>
    #include <map>
    #include <vector>

    using namespace std;    

    typedef long long ll;

    const int MAXN = 1e6 + 7;
    const int Mod = 1e9 + 7;
    const int Sum = 5000;

    int a[20] , p[11];
    int dp[20][Sum];
    int cmp;

    int dfs(int pos , int sum ,bool lead ,  bool limit)
    {
        //递归终点,到终点的数一定是满足条件的,所以直接计数+1
        if(pos == -1) return sum >= 0;
        if(sum < 0)   return 0;//剪枝
        if(!limit && dp[pos][sum] != -1)    return dp[pos][sum];
        int up = limit? a[pos] : 9;
        int  tmp = 0 ,num1;
        for(int i = 0; i <= up; i++){
            if( lead && !i )   num1 = sum;
            else    num1 = sum - i * p[pos];
            tmp += dfs(pos - 1 ,num1,lead && i == 0, limit && i == a[pos]);
        }
        if(!limit)  dp[pos][sum] = tmp;
        return tmp;
    }

    int Solve(int x)
    {
        int pos = 0;
        while(x){
            a[pos++] = x % 10;
            x /= 10;
        }
        int ans = dfs(pos - 1, cmp, true , true);
        //前导0 , 上界
        return ans;
    }

    int f(int x)
    {
        int l = 0,fsum = 0;
        while(x){
            int d = x % 10;
            x  = x / 10;
            fsum  = fsum + d * p[l];
            l++;
        }
        return fsum;
    }

    int main()
    {
        int a,b,t;
        scanf("%d",&t);
        p[0] = 1;
        for(int i = 1; i <= 10; i++)    p[i]  = p[i - 1] * 2;
        memset(dp, - 1, sizeof dp);
        int Case = 0;
        while(t--){
            //memset(dp, - 1, sizeof dp);
            Case++;
            scanf("%d%d",&a,&b);
            cmp  = f(a);
            printf("Case #%d: %d\n",Case, Solve(b) ); 
        }
        return 0;
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值