• dp:数位dp


    方法一:

    对每一位数字为某一个值时的情况分步计算

    难度:低

    实用性:低,仅在要求判断数位中是否出现某个数字时可用

     

    例如求数字中没有连续62且没有4的情况

    456小的数,可以这么考虑,

    4          5             6

    4        5           (0~6)

    4        (0~4)        (0~9)

    (0~3)   (0~9)        (0~9)

    先计算百位为0,1,2,3时能有多少个数满足(0~399)

    然后计算十位为0,1,2,3,4时能有多少个数满足(000~049)

    最后计算十位为0~6时能有多少个数满足(000~006)

    这里说明一下0~49等价于400~449

    假如这些数字的前面出现了4或62,则这些数字不计

    没有出现,则前面的数字的值没有影响

     

    讲解一下实际操作

    dp[][],第一维表示位数(个,十,百等),第二维表示该位上的数字是几

    先赋初值,显然个位上除4以外每一个的值都为1

     

    然后状态转移方程为

    dp[i][j]=dp[i-1][0]+ dp[i-1][1]+…+dp[i-1][9]

    然后要加入62和4的判断

    FOR(i,2,6)

           {

                  FOR(j,0,9)

                  {

                         if(j==4) continue;

                         FOR(k,0,9)

                         {

                                if(k==4||(j==6&&k==2)) continue;

                                dp[i][j]+=dp[i-1][k];

                         }

                  }

           }     

    其中j的位数在k的左边,用于判断62是否存在

     

    初始化是比较简单的,但在求解过程中中有很多易错点

    先把读入的数字每一位都排出来

           while(k)

           {

                  a[++cnt]=k%10;

                  k/=10;

           }

           a[cnt+1]=0;

    这里一定要保证数组最大位的前一位为0,因为后续判断中会用到这一位

    首先要从高位到低位

    比如456,我们先算了0~399,由于有4,后面的数字都是非法的

    所以高位的数字决定低位的数字是否有效

    所以出现这些数字时即可结束循环

     

    假如百位上是3,我们要计算0~299,所以我们实际上计算了

    0~99

    100~199

    200~299

    也就是比3小的数字,因此循环的范围为0~(a[i]-1)

    这个计算方式在计算最后一位时是会少算一个数字的

    比如123,我们实际上算了0~122,所以要算的数字还要自增1

    ROF(i,cnt,1)

           {

                  FOR(j,0,a[i]-1)

                  {

                         if((a[i+1]==4&&j==9)) continue;

                         ans+=dp[i][j];

                  }

                  if((a[i+1]==4&&a[i]==9)) break;

           }

    以上方法是非常基础的,书写易出错且用处非常有限

     

     

    方法二:

    记忆化搜索dp

    难度:高

    实用性:高,几乎一切情况

     

    这里先补充一下dp的认知

    以数塔为例

    如果你枚举所有的状态,会出现很多非常相似的状态,它们之间只有很小的区别,而且你会进行大量的重复运算,如果找到某个规律能够利用所有被计算过的状态,并运用到将要进行的运算中,效率将会大大提升。

    然而数塔的情况非常单调,当你推理状态时,状态仅用一个一维数组就能完全表示。但即便如此,我们依旧可以进行记忆化搜索。

    记忆化搜索即———在判断过该状态后,下次再遇到时,直接借用过去的结果。

     

    代码:

    int dfs(int x,int y) 

           if(dp[x][y]) 

           return dp[x][y];

           if(x>n||y>n) return 0;

           dp[x][y]=max(dfs(x+1,y),dfs(x+1,y+1))+a[x][y]; 

           return dp[x][y]; 

    }

    那么dp[1][1]就是我们需要的答案,而且其运算顺序与常规的状态方程写法一致。

    因此,我们可以发现,dp和记忆化搜索有着极其密切的联系,如果目前看不懂数位dp的搜索,不如先用记忆化搜索重写以前的dp题加深理解。

     

    以下给出记忆化搜索的数位dp的一个实例

    ll dfs(int pos,int sta,int limit)

    //3到4四个参数,为当前位,状态(可能用两个数表示状态),和上限

    {

           //cout<<pos<<" "<<sta<<" "<<f[sta]<<endl;

           if(pos==0)//到最后一位数字,判断该数字是否满足条件

           {

                  return f[sta]==1;

           }

           if(!limit&&dp[pos][sta]!=-1)

      //在位数相同,状态相同时,直接借用过去结果,limit是是否达到上限值,比如上次算出的是1000到1999,与现在2000到2999刚好同一情况,但上限(初始输入)小于2999,所以不能借用

           {

                  return dp[pos][sta];

           }     

           int up=limit?a[pos]:9,s,g=sta,k=sta;

           ll ans=0;

           FOR(i,0,up)

           {

                  if(i==0)

                  {

                         if(sta!=0)

                         {

                                s=g%3;

                                g/=3;

                                if(s==0||s==1) k=sta+t[i];

                                else k=sta-t[i];

                         }

                  }

                  else

                  {

                         s=g%3;

                         g/=3;

                         if(s==0||s==1) k=sta+t[i];

                         else k=sta-t[i];

                  }

                  ans+=dfs(pos-1,k,limit&&i==up);

           }

           if(!limit)//没达到上限时,记录该状态

                  dp[pos][sta]=ans;

           return ans;

    }

    ll solve(ll x)

    {

        int pos=0;

        while(x)

        {

            a[++pos]=x%10;

            x/=10;

        }

        a[pos+1]=0;

        return dfs(pos,0,1);

    }

    题目对应SPOJ BALNUM ,实际做题按需求改写,没有什么泛用性,但结构基本一致。

    顺带先讲一下这道题,题目要求在l和r中有多少BALNUM,BALNUM是指,数位中的奇数出现偶数次,偶数出现奇数次,没有出现的不考虑,如1,111,11111,22,2222都是BALNUM。

    用三进制表示0到9的状态,0为没出现,1为奇数次,2为偶数次。

    由于只有1和2加入计算,所以可以通过以下二进制处理出所有符合条件的状态数字

    注:t[j]为3的j次方

    FOR(i,1,pow(2,10)-1)

           {

                  int l=i,ret=0;

                  FOR(j,0,9)

                  {

                         if(l&1) ret+=((j%2)==1?2*t[j]:t[j]);

                         l>>=1;

                  }

                  //cout<<i<<" "<<ret<<endl;

                  f[ret]=1;

           }

     就不放完整代码了,用时为0ms

  • 相关阅读:
    一起写框架-MVC框架的概述(一)
    一起写框架-说在前面的话(一)
    一起写框架-Ioc内核容器的实现-对象的调用-@Bean注解注入容器的对象(十二)
    一起写框架-Ioc内核容器的实现-对象的调用-方法注入容器的对象(十一)
    一起写框架-Ioc内核容器的实现-对象的调用-属性注入容器的对象(十)
    一起写框架-Ioc内核容器的实现-基础功能-组件注解支持自定义的对象名(九)
    一起写框架-控制反转(Ioc)概述(二)
    一起写框架-Ioc内核容器的实现-基础功能-容器对象名默认首字母小写(八)
    一起写框架-Ioc内核容器的实现-基础功能-ComponentScan支持组件注解限制(七)
    一起写框架-Ioc内核容器的实现-基础功能-ComponentScan支持多包扫描(六)
  • 原文地址:https://www.cnblogs.com/qq936584671/p/8987539.html
Copyright © 2020-2023  润新知