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

原文地址:https://www.cnblogs.com/qq936584671/p/8987539.html