关于数位DP的学习
---恢复内容开始---
因为最近做比赛经常会出现数位DP,便尝试着去学学看数位DP。
先给出两篇论文的链接:
然后也是寻找了很多大牛的博客,学习了很多(但是没学会囧。),现在先总结一下已经学到的东西
“在信息学竞赛中,有这样一类问题:求给定区间中,满足给定条件的某个D 进制数或
此类数的数量。所求的限定条件往往与数位有关,例如数位之和、指定数码个数、数的大小
顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,我们就需要利
用数位的性质,设计log(n)级别复杂度的算法。解决这类问题最基本的思想就是“逐位确定”
的方法。下面就让我们通过几道例题来具体了解一下这类问题及其思考方法。”——刘聪
事实上,为什么会想到用数位DP来做,就是因为限定条件往往和数位有关,而仔细地朴素的暴力方法中,所做的重复的工作太多。这样的条件会使得DP(记忆化搜索)有用武之地。
目前我所接触到的大多数的题,都是可以通过记录某些值(比如数位等)来减少重复的运算。当然,因为此类题的特殊性,可以编写check函数确定代码的正确性。
再偷用某个大牛的一句话:其实数位DP(或者说所有记忆化搜索)都是可以看做通过搜索来填满状态的值。
首先先想想数位DP的运行模式
如果我们要统计[0,54321]中满足某个条件的个数,需要将其拆分为
[00000,09999][10000,19999],[20000,29999],[30000,39999],[40000,49999],
[50000,50999],[51000,51999],[52000,52999],[53000,53999],
[54000,54099],[54100,54199],[54200,54299],
[54300,54309],[54310,54319],
[54320,54321]
为什么要这么分呢?随便举个例子,如果我们统计过了[0000,9999]中的满足条件(或者其他各种不满足条件的状态)的个数,那么分别在加上前缀,就可以判断出有多少个满足条件的个数。目的是为了将大的区间划分为小的区间进行求解。
因此,总结一句话,数位DP减少的运算量为:前面几位固定,后面几位可以任意取的个数统计。
比如分析一道简单题:HDU 3652,通过我这个渣渣的错误历程来分析一些细节上的问题
先贴错误代码
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 int dp[20][20][20]; 6 int num[20]; 7 int dfs(int pos,int mod,int pre,int stat,int limit){ 8 if(pos==0) return mod==0&&stat; 9 if(!limit&&dp[pos][mod][pre]!=-1) return dp[pos][mod][pre]; 10 int ans=0; 11 int end=limit?num[pos]:9; 12 for(int i=0;i<=end;i++){ 13 int nmod=(mod*10+i)%13; 14 int nstat=(pre==1&&i==3)||stat; 15 ans+=dfs(pos-1,nmod,i,nstat,limit&&i==end); 16 } 17 if(!limit) dp[pos][mod][pre]=ans; 18 return ans; 19 } 20 int cal(int x){ 21 int cnt=0; 22 memset(num,0,sizeof(num)); 23 while(x){ 24 num[++cnt]=x%10; 25 x/=10; 26 } 27 return dfs(cnt,0,0,0,1); 28 } 29 int main() 30 { 31 int i,j; 32 int n; 33 memset(dp,-1,sizeof(dp)); 34 while(scanf("%d",&n)!=EOF){ 35 int ans=cal(n); 36 printf("%d\n",ans); 37 } 38 return 0; 39 }
我选取的记录参数有三个:pos当前处理位,mod前缀和余数,还有前一位的数字pre
但是运算结果却始终会小于等于正确的答案,为什么呢?
想了想,发现其实是因为参数含义的问题。
分析一下,如果我将pre作为一个关键参数记录下来,其实我并不能区分我记录的是后面几位能不能随机取的个数。
即当下一次搜索到pos,mod,pre的时候,不能确定前面是否有13,或者以前搜索的DP[pos][mod][pre]中的数的个数是否有13。
因此应该把记录的参数改为pos,mod,stat(表示为记录状态,0为不含13,1为只含前一位为1,2为前缀含有13)。
因此得到下面的AC代码
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; int dp[20][20][3]; int num[20]; int dfs(int pos,int mod,int stat,int limit){ if(pos==0) return mod==0&&stat==2; if(!limit&&dp[pos][mod][stat]!=-1) return dp[pos][mod][stat]; int ans=0; int end=limit?num[pos]:9; for(int i=0;i<=end;i++){ int nmod=(mod*10+i)%13; int nstat; if(stat==1&&i==3||stat==2) nstat=2; else if(i==1) nstat=1; else nstat=0; ans+=dfs(pos-1,nmod,nstat,limit&&i==end); } if(!limit) dp[pos][mod][stat]=ans; return ans; } int cal(int x){ int cnt=0; memset(num,0,sizeof(num)); while(x){ num[++cnt]=x%10; x/=10; } return dfs(cnt,0,0,1); } int main() { int i,j; int n; memset(dp,-1,sizeof(dp)); while(scanf("%d",&n)!=EOF){ int ans=cal(n); printf("%d\n",ans); } return 0; }
好了讲完了