数位dp 模板加例题

概念:所谓数位”dp“,是指对数字的”位“进行的与计数有关的DP。一个数一个位,十位,百位,千位等,数的每一位就是数位。数位DP用来解决与数字操作有关的问题。例如数位之和的问题。特定数字问题等。这些问题的特征就是给定的区间不能超级大,不用用暴力的方法逐个检查,必须接近O(log2n) 复杂度的算法。解题的思路是用DP对”数位“进行操作,记录已经算过的区间状态,用在后续计算中,快速进行大范围的筛选。

实现方法:1.递推实现 2.用记忆法搜索实现。

一.递推的实现:

    我们来看一到例题:统计[0,n]内不含4的数字个数(直接上代码)

  

#include<cstdio>
#include<algorithm>
#include<set>
#include<iostream>
using namespace std;
const int maxn=2e5+5;
const int M=15;  
int dp[M][M];  //dp[i][j]表示i位数字第一个数是j是符合条件的数字数量;
int bit[M];    //bit[i]存第i位数字;
void init()
{
  dp[0][0]=1;
  for(int i=1;i<=M;i++)
  {
    for(int j=0;j<10;j++)
    {
      for(int k=0;k<10;k++)
      {
        if(j!=4) dp[i][j]+=dp[i-1][k];
      }
    }
  }
}
//计算[0,n]区间内满足条件数字的个数;
int solve(int len)
{
  int ans=0;
  for(int i=len;i>=1;i--)  //从高位到地位处理;
  {
    for(int j=0;j<bit[i];j++)
       if(j!=4) ans+=dp[i][j];
     if(bit[i]==4) {ans--;break;}  //i位开头是4的都不行;
  }
  return ans;
}
int ok(int x)  //简单的一个求bit数组以及n的位数;
{
  int len=0;
  while(x)
  {
    bit[++len]=x%10;
    x/=10;
  }
  return solve(len);
}
int main()
{
  int n;
  init();  //预计算dp[][];
  cin>>n;
  printf("%d
",ok(n)+1); //不要忘记0也是符合条件;
  return 0;
}

  二.记忆化搜索:

 记忆化搜索的思路其实是在递归程序dfs中搜索所以可能的情况,但是遇到已经算过的记录在dp[ ]中的结果就可以直接使用,减少了重复计算。然后记忆化搜索是有模板(这让我这种dp菜鸡有一丝丝动力)

const int M=15;  
int dp[M][sta];  //sta对应不同转态;
int bit[M];    
int dfs(int pos,/*sta变量*/,int lead/*前导零*/,int lim/*数位上界变量*/)
{
  if(pos==-1) return 1; //判断是否枚举到最低位;或者有些题目用于剪枝;

  if(!lim&&dp[pos][sta]!=-1) return dp[pos][sta];

  int up=lim?bit[pos]:9;
  
  int ans=0; //计数;
  for(int i=0;i<=up;i++)
  {
    if()...
    else if()...
    //不同题目的要求不同;
    //这里是记忆化搜索最灵活的部分要具体题目分析;

    ans+=dfs(pos-1,/*sta状态转移*/,lead&&i==0,lim&&i==bit[pos]);
  }
  if(!lim) dp[pos][sta]=ans;
  return ans;
}
int solve(int x)
{
  int  len=0;
  while(x)
    {
      bit[len++]=x%10;
      x/=10;
    }
    return dfs(len-1,/*从最高位开始枚举sta状态*/,/*一系列状态*/,1);
}
int main()
{
  int l,r;
  while(~scanf("%d%d",&n,&m))
  {
    memset(dp,-1,sizeof dp); //dp数组初始化,这个也可以放在while外面;
    printf("%d
",solve(r)-solve(l-1));
  }
  return 0;
}

 看了模板我们来看几题例题:

 1.Hdu 2089 题意是统计[n,m]内不含4且没有“62”的数:

 代码如下:

#include<algorithm>
#include<cstring>
#include<cstdio>
#include<iostream>
#include<string>
using namespace std;
typedef unsigned long long ull;
const int maxn=4e2+5;
const int Inf=0x3f3f3f3f;
int dp[15][15];
int bit[15];
int dfs(int pos,int pre,int sta,int lim) //pre记录前一个数字;
{
    if(pos<=-1) return 1;  
    if(!lim&&dp[pos][sta]!=-1) return dp[pos][sta];
    int up=lim?bit[pos]:9;
    int ans=0;
    for(int i=0;i<=up;i++)
    {
        if(pre==6&&i==2) continue; //如果前一个数字是六现在补上的是2那么跳过循环;
        if(i==4) continue;      
        ans+=dfs(pos-1,i,i==6,lim&&i==bit[pos]);
    }
    if(!lim)  dp[pos][sta]=ans;
    return ans;
}
int solve(int x)
{
    int pos=0;
    while(x)
    {
        bit[pos++]=x%10;
        x/=10;
    }
  return dfs(pos-1,0,0,1);
}
int main()
{   
    int l,r;
    memset(dp,-1,sizeof dp);
    while(~scanf("%d%d",&l,&r)&&r+l)
    {
        printf("%d
",solve(r)-solve(l-1));
    }
   return 0;
}

  2.Hdu 3652 题意是求[1,n]中能被13整除且包含“13”的数。这题较上题多一个整除的判断处理需要多开一维来记录余数:

 代码如下:

#include<algorithm>
#include<cstring>
#include<cstdio>
#include<iostream>
#include<string>
using namespace std;
typedef unsigned long long ull;
const int maxn=1e5+5;
int dp[15][15][3];
//dp[i][j][k]
//i:数位
//j:余数
//k:3种操作状况,0:末尾不是1 , 1:末尾是 1 ,2:含有13

int bit[15];
int dfs(int pos,int mod,int have,int lim)
{
    if(pos<=0) return mod==0&&have==2;    //枚举完成返回合理的数字即 能被13整除且包含“13”;
    if(!lim&&dp[pos][mod][have]!=-1) return dp[pos][mod][have];
    int up=lim?bit[pos]:9;
    int mod_x,have_x;
    int ans=0;
    for(int i=0;i<=up;i++)
    {
        mod_x=(mod*10+i)%13;  //其实是模拟除法;
        have_x=have;
        if(have==0&&i==1)    //末尾数字是不是1现加入1,末尾为1;
           have_x=1;
        if(have==1&&i!=1)    //末尾是1 现加入的不为1 末尾不是1;
          have_x=0;
        if(have==1&&i==3)    //包含“13” 标记为2;
           have_x=2;
        ans+=dfs(pos-1,mod_x,have_x,lim&&i==bit[pos]);   
    }
    if(!lim) dp[pos][mod][have]=ans;
    return ans;
}
int solve(int x)
{
    int len=0;
    while(x)
    {
        bit[++len]=x%10;
        x/=10;
    }
    return dfs(len,0,0,1);
}
int main()
{
    int n; 
    memset(dp,-1,sizeof dp);
    while(~scanf("%d",&n)) cout<<solve(n)<<endl;
    return 0;
}

这两题是比较基础的数位dp,主要用于熟悉记忆化搜索的模板(hdu 3555类似于HDU2089 大家可以试试)

比较难的有 hdu 6148,hdu 4507大家可以挑战挑战。

个人感觉数位dp比一般的dp更容易理解,大家多花点时间效果是很明显的。

原文地址:https://www.cnblogs.com/zpj61/p/13435924.html