数位DP 学习笔记

概述

数位DP解决的是有关数字每一位的问题。一般形式为统计一个区间内满足一些限制的数的数量。

例题

HDU3555

首先把数字拆位:

for(maxn=0;temp1;)n[++maxn]=temp1%10,temp1/=10;

数位DP可以采用记搜的方式转移。反向统计不含 \(49\) 的个数,设 \(f_{pos,p}\)\(pos\) 表示当前的位,\(p\) 表示上一位是否为 \(4\)。转移方程 \(f_{pos,p}=\sum_{i,\neg p\vee i\ne9}f_{pos-1,i}\)

对于上界需要一个布尔值 \(lim\),表示当前位是否能取到上界。\(lim\) 为真仅当上一位 \(lim\) 为真并且这一位取到上界。若 \(lim\) 为真,\(i\) 不能超过当前位的值。

最后加上记忆化即可。

#include<bits/stdc++.h>
using namespace std;
int t,n[25],maxn;
long long temp,temp1,f[25][2];
long long dfs(int pos,bool lim,bool p){
  if(f[pos][p]!=-1&&!lim)return f[pos][p];
  if(!pos)return 1;
  long long ans=0;
  for(int i=0;i<=(lim?n[pos]:9);i++){
    if(p&&i==9)continue;
    ans+=dfs(pos-1,lim&&i==n[pos],i==4);
  }
  if(!lim)f[pos][p]=ans;
  return ans;
}
int main(){
  memset(f,-1,sizeof(f)),cin>>t;
  while(t--){
    cin>>temp,temp1=temp;
    for(maxn=0;temp1;)n[++maxn]=temp1%10,temp1/=10;
    cout<<temp-dfs(maxn,1,0)+1<<'\n';
  }
  return 0;
}

注意这里 \(f\) 仅初始化了一次,因为这里 DP 数组可以复用,在多测时降低了复杂度。dp 数组复用需要满足数组的意义不变。因此要从低位向高位存数,并且 \(lim\) 为真时不记忆化。不过当没有多测或者 \(lim\) 为真的状态很多,可以选择将 \(lim\) 记忆化。

P2602

首先差分,转化为两个从一开始的询问。

注意到这题中,前导 \(0\) 不能够统计入答案,需要多一维表示前导 \(0\)。设 \(f_{pos,cnt,flag}\)\(pos\) 表示当前的位,\(cnt\) 表示当前出现了多少个目标数字。\(flag\) 表示当前位是否属于前导 \(0\)

转移时将后继状态求和,边界是 \(pos\)\(0\) 时返回 \(cnt\)

#include<bits/stdc++.h>
using namespace std;
long long t1,t2,f[17][17][2];
int maxa,maxb,a[17],b[17];
long long dfs(int pos,int cnt,bool lim,bool f0,int a[],int d){
  long long ans=0;
  if(!pos)return cnt;
  if(f[pos][cnt][f0]!=-1&&!lim)return f[pos][cnt][f0];
  for(int i=0;i<=(lim?a[pos]:9);i++)ans+=dfs(pos-1,cnt+(i==d&&!(f0&&!i)),lim&&i==a[pos],f0&&!i,a,d);
  if(!lim)f[pos][cnt][f0]=ans;
  return ans;
}
int main(){
  cin>>t1>>t2,t1--;
  while(t1)a[++maxa]=t1%10,t1/=10;
  while(t2)b[++maxb]=t2%10,t2/=10;
  for(int i=0;i<=9;i++)memset(f,-1,sizeof(f)),cout<<dfs(maxb,0,1,1,b,i)-dfs(maxa,0,1,1,a,i)<<' ';
  return 0;
}

[[动态规划]]

posted @ 2024-03-01 09:21  lgh_2009  阅读(8)  评论(0)    收藏  举报