• 数位dp 从基础到解题


    概念:

    数位dp是一种计数用的dp,一般就是要统计一个区间[le,ri]内满足一些条件数的个数。所谓数位dp,字面意思就是在数位上进行dp咯。数位还算是比较好听的名字,数位的含义:一个数有个位、十位、百位、千
    位......数的每一位就是数位啦!
    之所以要引入数位的概念完全就是为了dp。数位dp的实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后记忆化就可以了。
    ( 引自:原文 )

    数位使得我们有了可以dp的状态,在一定程度上数位之间是不影响的,大部分情况我们只需要判断,前面的位数有没有达到上界。
    一般情况数位dp的状态设置成为 $$dp[i][state]$$ 其中state根据题目的需求不同,呈现出的意义也不同。
    一般情况下数位dp我们使用记忆化搜索来dp,因为递推很难表示跟判断上界,如果不嫌麻烦,递推当然也是可以的。

    数位dp的上界lim

    数位dp的上界指的是前面的所有位数都贴着题目给定的最大值,如果有一个没有贴着最大值,那么接下来的数若没有特殊要求的话就可以随便取,因为之前的高位小了,那不管接下来的低位怎么取都会比上界小

    模板:

    #include<bits/stdc++.h>
    void rea();
    using namespace std;
    int T, f[][][];
    ll dfs(int pos, ll state, int lim)
    /*pos是位数 state是根据题目而定的状态 lim表示当前位数有没有靠着上界*/
    {
        if(/*边界*/) return /*题意要求的值*/
        /*这里是需要注意的地方*/ 
        if(!lim && ~f[pos][state]) return f[pos][state]; /*记忆化搜索的精髓这里*/
        int up = (lim?bit[pos]:9);/*是不是贴着上界*/ 
        ll num = 0;
        for(R int i = 0; i <= up; ++i)
            num += dfs(pos-1, state+i*k, (lim?i==up:0));/*搜索下去*/
        /*这里是需要注意的地方*/
        if(!lim) f[pos][x][res] = num;
        return num;
    }
    ll query(ll x)
    {
        if(x < 0) return 0;
        ll ans = 0; len = 0;
        for(; x; x/=10) bit[++len] = x%10;//得到位数,有的时候可能是k进制注意一下 
        return dfs(len, 0, 1); //求答案 
    }
    int main()
    {
        using IO::max;using IO::min;
        memset(f, -1, sizeof f);
        rea(T);
        while(T --> 0)
        {
            rea(a), rea(b);
            printf("%lld
    ", query(b)-query(a-1));
        }
        return 0;
    }
    

    模板中标记的需要注意的地方数位dp比较值得提的两个地方,下面会进行讨论

    不过让我们先看看数位dp的优化

    数位dp的优化(数位dp的复杂度保障)

    接下来新手可能会有点难理解。
    数位dp,我们进行的都是对某一个或者一段区间的数字的操作。
    而数字呈现的是他的"自身属性","自身属性"的意思就是他本身的属性可以与题目的多组询问的问题无关。
    举个例子: POJ 3252

    给定T组询问,每次询问给定区间[l, r], 问区间内有多少个数转换成为二进制之后0的数目要不能少于1的数目

    可以发现,一个数转换成二进制之后0的数目少不少于1的数目就是数字的"自身属性",这个属性是不会随着询问的l和r的变化而变化的
    所以我们就有个这样的优化

    memset(f, 0, sizeof f)操作放在程序开始,多组数据询问之前

        memset(f, -1, sizeof f);
        rea(T);
        while(T --> 0)
        {
        	solve();
        }
    

    可以这样操作的原因就是数字的"自身属性"是通用的,之前求到的答案在后面也可以使用

    注意:万一有的题目设出状态之后发现同一个状态的答案会随着询问的不同变化怎么办

    看看这题:HDU 4734

    题目给定了f(x)表示(A_n*2^{n-1}+A_{n-1}*2^{n-2}......A_2*2^1+A_1*2^0) 其中A表示x的十进制位数
    然后给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。
    f(x)的最大值是5000左右,位数是18

    看到这题会设什么状态的?
    f[i][num]这是最基本的,其中i表示位数第i位,num表示f()值。
    不知道这样直接做过不过的了
    但如果我们要用memset优化就会发现,题目给定的f(a)会变,也就是说我们之前存下来的f[i][num]在后面很多时候用不上
    如果我们非要用的话必须要增加一维变成f[20][5000][5000],这样显然是不行的。
    现在就需要我们自己去找这题特殊的的"自身属性"。
    yy一下可以发现我们现在的值距离f(a)的值还有多少这个属性是固定的
    于是可以设f[i][num]表示i位前i位数,num表示当前值距离f(a)还有多少,然后这题我们就可以做了。

    总之有的时候如果T有点大导致有可能每次询问重新记录记忆化数组,就需要我们根据不同题目特有的自身属性,来设dp数组,这个多做做题能体会到了。

    模板需要注意的地方

    if(!lim && ~f[pos][state]) return f[pos][state];
    if(!lim) f[pos][x][state] = num;

    这个稍微yy一下应该也很好理解,既然是记忆化,肯定需要记录通用的东西,lim表示的就是有没有靠着上界
    如果靠着上界,会发现下面求答案的时候超过上界的部分dfs不到,也就不能用之前求出来的f值
    而下面也是一个道理,必须现在没有限制,当前求到的值才能作为通用的f值存入f数组

    接下来就是做题环节了

    POJ 3252

    HDU 4734

    HDU 3709

    HDU 4507

    清华集训2016-组合数问题 这篇博主写了题解

    谢谢您的阅览Orz

  • 相关阅读:
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
    小D课堂
  • 原文地址:https://www.cnblogs.com/heanda/p/12348084.html
Copyright © 2020-2023  润新知