数位DP
所谓数位DP,就是把一个数按照k进制数的每一个位展开,在把每一位的信息作为状态来推出答案。
比如:12345这个数在十进制下可拆为1、2、3、4、5,个位可以推十位,十位可以推百位,以此类推。推状态的方法有很多,一般来说第一反应就是像普通dp一样从前往后推,但是由于思维难度较高而且写起来难度较大,所以推荐递归写法,也就是记忆话搜索的写法。
以HDU2089 不要62为例。
统计l,r区间内不含62,4的数字个数。
l~r区间符合条件的数字没有普遍的规律,状态数目过多不好做DP。但是1~x一段连续的区间在枚举数字时有大量重复状出现,利用记忆化搜索做DP。
一、首先将问题拆分成两部分,也就是1~r区间内符合条件数字的数目减去1~(l-1)区间内符合条件数字的数目。
ans=solve(r)-solve(l-1);
二、将输入的边界x转化为k进制存入数组备用。
int solve(int x) { int pos=0; while(x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,-1,0,true); }
三、安位枚举所有的数字,过程中发现状态已经搜索过,就可以直接返回答案(记忆化)
int dfs(int pos,int pre,int sta,bool limit)//pos当前数位,pre上一个数字,sta:pre是否为6,limit代表是否有至少有一个高位小于数位限制,如果小于的话,后面所有数位均无限制条件。 { if(pos==-1)return 1; //搜索到最后一位时,就说明找到了一个符合条件数 if(!limit&&dp[pos][sta]!=-1)return dp[pos][sta];//非limit表示后面的数位无特殊限制,这里产生了大量重复子问题,而limit条件下后面每一位都认为是特殊的,不会被重复枚举,记忆化无用。 int up=limit?a[pos]:9; //limit条件下对当前位增加特殊限制 int temp=0; //计数 for(int i=0; i<=up; i++) { if(i==4)continue; if(pre==6&&i==2)continue; //跳过非法状态。 temp+=dfs(pos-1,i,i==6,limit&&i==a[pos]); //记忆化递归 } if(!limit)dp[pos][sta]=temp; return temp; }
整体代码
#include<bits/stdc++.h> using namespace std; int dp[20][2],a[20]; int n,m; int dfs(int pos,int pre,int sta,bool limit) { if(pos==-1)return 1; if(!limit&&dp[pos][sta]!=-1)return dp[pos][sta]; int up=limit?a[pos]:9; int temp=0; for(int i=0; i<=up; i++) { if(i==4)continue; if(pre==6&&i==2)continue; temp+=dfs(pos-1,i,i==6,limit&&i==a[pos]); } if(!limit)dp[pos][sta]=temp; return temp; } int solve(int x) { int pos=0; while(x) { a[pos++]=x%10; x/=10; } return dfs(pos-1,-1,0,true); } int main() { int l,r; while(~scanf("%d%d",&l,&r)&&l+r) { memset(dp,-1,sizeof(dp)); printf("%d\n",solve(r)-solve(l-1)); } return 0; }