数位DP模板
来源:互联网 发布:linux service 配置 编辑:程序博客网 时间:2024/06/09 19:36
数位DP问题
数位DP指一类问题:给定正整数区间[s, e],问符合条件的数一共有多少个。例如hdu2089以及hdu3555等。
其中,hdu2089的条件为数字中不含4且不含62,hdu3555的条件为数字中包含49。一般而言,这类条件中至少有一个是与整个数的数值无关的,而是与每一位的数字有关。例如上面2道题的条件就与整个数的数值无关。
因此这类问题实际上是数位有关的问题,另一方面,这类问题通常使用DP来加速(因为暴力法几乎肯定超时)。所以称为数位DP。
对于这类问题,大量的解题报告实际上总结出了一个模板,按照这个模板,可以很容易的抓住问题的关键部分以及组织实施代码。
首先对问题做一个变形,原问题很容易变为:问[0, N]区间内符合条件的数一共有多少个。求解的对象只剩下一个,就是N。
数位DP简单思路
以hdu2089为例,令Ai为长度为i的符合条件的数的个数,Bi为长度为i的首位为2的符合条件的数的个数,Ci为长度为i的首位不为2的符合条件的数的个数。很容易写出DP方程。
求出A、B、C以后,就能根据一定的规则进行推导求解。例如考虑N=3812,这是一个4位数,但不能直接采用
关于N=3812的答案,我们应该这样考虑。假设千位取0,则后3位可以任意取,因此需要将
这个思路原则上是可以求解的,但是比较繁琐。
数位DP模板
数位问题是采用DP思想求解的。原则上,DP既可以使用迭代实现,也可以使用递归实现。这一点在数位DP中也是一样的。但是,这里介绍的数位DP的模板是采用递归实现的。这个模板本质上就是能DP的就DP,不能DP的就搜索。
整个模板如下:
typedef long long llt;//这是因为数位问题的结果一般比较大,直接使用longlongllt D[POS][STATUS]; //DP数组,第一维代表数的长度,其他维由具体问题决定int Dig[POS]; //这个是N分解出的每一位数字llt dfs(int pos, status, bool lead, bool limit){ if ( -1 == pos ) 根据status返回结果,不是0就是1; if ( !lead && !limit && -1 != D[pos][status] ) return D[pos][status]; int last = limit ? Dig[pos] : 9; llt ans = 0; for(int i=0;i<=last;++i){ ans += dfs(pos-1,newStatus,newLead,limit&&i==last); } return lead || limit ? ans : D[pos][status] = ans;}
首先解释一下参数:
llt dfs(int pos, status, bool lead, bool limit)
第一个参数代表数位,个位用0表示,十位用1表示……当然个位也可以用1表示,看个人习惯。最后一个参数表示是否受限。例如当
最重要的参数就是
if ( -1 == pos ) 根据status返回结果,不是0就是1; if ( !lead && !limit && -1 != D[pos][status] ) return D[pos][status];
第一个
第二个
另外,return的那句话显然也是DP递归实现的一般写法。
int last = limit ? Dig[pos] : 9;
这句话很简单,确定搜索终点。如果不受限就是9,否则就是当前数位上的数字。以
llt ans = 0; for(int i=0;i<=last;++i){ ans += dfs(pos-1,newStatus,newLead,limit&&i==last); }
这一段就是递归搜索。其中last是刚才确定的。而递归调用中,前后2个参数总是这样写,而
有了dfs函数以后,我们还需要一个小小的函数,用于分解N的各位数字,以及第一次调用递归函数。这个小函数就不专门列出了。看例题。
模板的使用
hdu2089
/* 含有4或者62的数是不吉利的 问[s,e]之间吉利的数字有多少个*/#include <stdio.h>#include <string.h>#include <algorithm>using namespace std;typedef long long llt;//Dij表示长度为i的前一位为j的满足条件的数llt D[7][10];int Dig[100];//这里只需要用1个量表示状态,pre表示当前位的前一位数字//这里不需要lead参数,因为条件与0无关llt dfs(int pos,int pre,bool limit){ if ( -1 == pos ) return 1;//能够搜索到这里,说明前面的每一个条件都满足,返回1个 //如果没有限制,而且前面已经求出D,则直接返回 if ( !limit && -1 != D[pos][pre] ) return D[pos][pre]; int last = limit ? Dig[pos] : 9; llt ans = 0; //递归搜索 for(int i=0;i<=last;++i){ if ( 4==i || (2==i&&6==pre) ) continue; ans += dfs(pos-1,i,limit&&(i==Dig[pos])); } return limit ? ans : D[pos][pre] = ans;}//求[0,n]满足条件的数有多少个llt ddp(int n){ int k = 0; while(n){ Dig[k++] = n % 10; n /= 10; } return dfs(k-1,0,true);}int main(){ //freopen("1.txt","r",stdin); memset(D,-1,sizeof(D)); int a,b; while( scanf("%d%d",&a,&b),a+b ){ printf("%lld\n",ddp(b)-ddp(a-1)); } return 0;}
hdu3555
/* 问[s,e]之间含有49的数字有多少个*/#include <stdio.h>#include <string.h>#include <algorithm>using namespace std;typedef long long llt;//Dijk表示长度为i的前一位为j的满足条件的数,k表示前面的数是否已经合法llt D[20][10][2];int Dig[100];//这道题的状态就需要使用2个量表示,相应的DP数组也是三维的//pre表示前一位数字//valid表示是否已经出现过49llt dfs(int pos,int pre,int valid,bool limit){ if ( -1 == pos ) return valid;//能够搜索到这里,只看valid即可 //如果没有限制,而且前面已经求出D,则直接返回 if ( !limit && -1 != D[pos][pre][valid] ) return D[pos][pre][valid]; int last = limit ? Dig[pos] : 9; llt ans = 0; //递归搜索 for(int i=0;i<=last;++i){ ans += dfs(pos-1,i,valid||(4==pre&&9==i),limit&&(i==Dig[pos])); } return limit ? and : D[pos][pre][valid]=ans;}//求[0,n]满足条件的数有多少个llt ddp(llt n){ int k = 0; while(n){ Dig[k++] = n % 10; n /= 10; } return dfs(k-1,0,0,true);}int main(){ //freopen("1.txt","r",stdin); memset(D,-1,sizeof(D)); int nofkase; scanf("%d",&nofkase); llt n; while(nofkase--){ scanf("%I64d",&n); printf("%I64d\n",ddp(n)); } return 0;}
POJ3252
下面来看一个要考虑前导零的例子。这个题目的条件显然是与0有关的。
/* 表示成二进制以后,0比1多认为是合法的 问[s,e]之间有多少个数是合法的*/#include <stdio.h>#include <string.h>#include <algorithm>using namespace std;typedef long long llt;//Dij:i表示长度,j表示0与1的数量之差加32llt D[40][70];int Dig[100];//pre表示0比1多多少个,由于可能是负数,为了做数组索引索引加32做一个平移//lead表示是否前导0//limit表示是否要限制上界llt dfs(int pos,int pre,bool lead,bool limit){ //检查一下,是否合法 if ( -1 == pos ) return pre >= 32; //如果没有限制,而且前面已经求出D,则直接返回 if ( !limit && !lead && -1 != D[pos][pre] ) return D[pos][pre]; int last = limit ? Dig[pos] : 1; llt ans = 0; //递归搜索 for(int i=0;i<=last;++i){ if ( lead && 0==i ) ans += dfs(pos-1,pre,true,limit&&(i==Dig[pos])); else ans += dfs(pos-1,pre+(0==i?1:-1),false,limit&&(i==Dig[pos])); } return limit||lead?ans:D[pos][pre]=ans;}//求[1,n]满足条件的数有多少个llt ddp(llt n){ int k = 0; while(n){ Dig[k++] = n & 1; n >>= 1; } llt tmp = dfs(k-1,32,true,true); return tmp;}int main(){ //freopen("1.txt","r",stdin); memset(D,-1,sizeof(D)); llt a,b; while(2 == scanf("%I64d%I64d",&a,&b)){ printf("%I64d\n",ddp(b)-ddp(a-1)); } return 0;}
BZOJ1026
再看一个与0有关的题目。这个题目的题面明确提到了“不含前导零”,提示需要
/* 条件为相邻两个数字之差至少为2的正整数 问[s,e]之间符合条件的数*/#include <stdio.h>#include <string.h>#include <algorithm>using namespace std;typedef long long llt;//Di:i表示长度,j表示最高位llt D[15][11];int Dig[100];//pos表示当前处理的数位,直到0//pre表示前一位的情况//lead表示是否为前导0//limit表示是否要限制上界llt dfs(int pos,int pre,bool lead,bool limit){ //检查一下,是否合法,本题搜到这里肯定合法 if ( -1 == pos ) return 1; //如果没有限制,而且前面已经求出D,则直接返回 if ( !lead && !limit && -1 != D[pos][pre] ) return D[pos][pre]; int last = limit ? Dig[pos] : 9; llt ans = 0; //递归搜索 for(int i=0;i<=last;++i){ if ( lead ){ if ( 0 == i ) ans += dfs(pos-1,0,true,limit&&i==last); else ans += dfs(pos-1,i,false,limit&&i==last); }else if( pre + 2 <= i || i <= pre - 2 ){ ans += dfs(pos-1,i,false,limit&&i==last); } } return lead||limit?ans:D[pos][pre]=ans;}//求[1,n]满足条件的数有多少个llt ddp(llt n){ int k = 0; while(n){ Dig[k++] = n % 10; n /= 10; } llt tmp = dfs(k-1,0,true,true); return tmp;}int main(){ //freopen("1.txt","r",stdin); memset(D,-1,sizeof(D)); llt a,b; for(;2==scanf("%lld%lld",&a,&b);){ printf("%lld\n",ddp(b)-ddp(a-1)); } return 0;}
hdu3652
再看一个不光是跟数位有关而且跟数值有关的例子。在数位DP下,数值有关的条件也可以归结于数位相关的。如果是题目是纯数值条件的,那也许就是数论的题目,而不是数位DP的题目。
在这道题中,状态就是用3个量表示的,因此DP数组是四维的。
/* 条件为含有13并能被13整除的数 问[s,e]之间有多少个数是符合条件的*/#include <stdio.h>#include <string.h>#include <algorithm>using namespace std;typedef long long llt;//Dijk:i表示长度,j表示前一位,k表示余数,w表示是否出现过13llt D[40][10][13][2];int Dig[100];//pos表示当前处理的数位,直到0//pre表示之前的位//left表示当前对13的余数//valid为是否出现过13//limit表示是否要限制上界llt dfs(int pos,int pre,int left,bool valid,bool limit){ //检查一下,是否合法 if ( -1 == pos ) return valid&&0==left?1:0; //如果没有限制,而且前面已经求出D,则直接返回 if ( !limit && -1 != D[pos][pre][left][valid] ) return D[pos][pre][left][valid]; int last = limit ? Dig[pos] : 9; llt ans = 0; //递归搜索 for(int i=0;i<=last;++i){ ans += dfs(pos-1,i,(left*10+i)%13,valid||(1==pre&&3==i),limit&&i==last); } return limit?ans:D[pos][pre][left][valid]=ans;}//求[1,n]满足条件的数有多少个llt ddp(llt n){ int k = 0; while(n){ Dig[k++] = n % 10; n /= 10; } llt tmp = dfs(k-1,0,0,false,true); return tmp;}int main(){ //freopen("1.txt","r",stdin); memset(D,-1,sizeof(D)); llt a; while(1 == scanf("%I64d",&a)){ printf("%I64d\n",ddp(a)); } return 0;}
再来看一些状态上更加复杂一点的例子,可能需要用到状态压缩等。当然,这也是DP中常用的技巧。
CF55D
这个题目应该很容易归结于数位DP,因为除数是各个位上的数字,显然与数位有关。被除数是整个数,也就是数值相关。总的来说,是数位问题。
只需考虑数字2~9即可,数字0、1不必考虑。然后注意到2~9的最小公倍数是2520,就可以把余数做一个有效的记录。
/* 如果一个数能够被自己各个位上的数字整除,就是美丽的 问[s,e]之间美丽的数字有多少个*/#include <stdio.h>#include <string.h>#include <algorithm>using namespace std;typedef long long llt;//Dijk:i表示长度,j表示已经出现过的数字,k表示当前的数字值llt D[20][1<<8][2600];int Dig[100];//pos表示当前处理的数位,直到0//pre表示之前已经出现过的数字,用二进制表示表示//sum表示当前的数字值//limit表示搜索的上界llt dfs(int pos,int pre,int sum,bool limit){ //检查一下,sum是否能整除所有出现过的数位 if ( -1 == pos ) { for(int i=2;i<=9;++i){ if ( (pre&(1<<(i-2))) && (0!=sum%i) ){ return 0; } } return 1; } //如果没有限制,而且前面已经求出D,则直接返回 if ( !limit && -1 != D[pos][pre][sum] ) return D[pos][pre][sum]; int last = limit ? Dig[pos] : 9; llt ans = 0; //递归搜索 for(int i=0;i<=last;++i){ ans += dfs(pos-1,i<2?pre:pre|(1<<(i-2)),(sum*10+i)%2520,limit&&(i==Dig[pos])); } return limit?ans:D[pos][pre][sum]=ans;}//求[1,n]满足条件的数有多少个llt ddp(llt n){ int k = 0; while(n){ Dig[k++] = n % 10; n /= 10; } return dfs(k-1,0,0,true);}int main(){ //freopen("1.txt","r",stdin); memset(D,-1,sizeof(D)); int nofkase; scanf("%d",&nofkase); llt a,b; while(nofkase--){ scanf("%I64d%I64d",&a,&b); printf("%I64d\n",ddp(b)-ddp(a-1)); } return 0;}
SPOJ 10606 BALNUM
这个题目显然是需要
/* 条件为每一个奇数数字有偶数个,每一个偶数数字有奇数个 问[s,e]之间符合条件的数*/#include <stdio.h>#include <string.h>#include <algorithm>using namespace std;typedef long long llt;typedef unsigned long long ull;int const POW_OF_3[] = { 1,3,9,27,81,81*3,81*9,81*27,81*81,81*81*3};//Dijk:i表示长度,j表示对应数字的出现情况,使用3进制表示,0表示没有,1表示奇数,2表示偶数llt D[22][59100];int Dig[100];//pos表示当前处理的数位,直到0//pre表示之前的情况//lead表示是否为前导0//limit表示是否要限制上界llt dfs(int pos,int pre,bool lead,bool limit){ //检查一下,是否合法 if ( -1 == pos ) { //pre%3如果是0就不必讨论,否则i为奇数时余数必须是2,i为偶数时余数必须是1 for(int i=0;pre;++i,pre/=3){ if ( ( pre % 3 ) && ( ( pre % 3 ) & 1 ) == ( i & 1 ) ){ return 0; } } return 1; } //如果没有限制,而且前面已经求出D,则直接返回 if ( !lead && !limit && -1 != D[pos][pre] ) return D[pos][pre]; int last = limit ? Dig[pos] : 9; llt ans = 0; //递归搜索 int tmp = 0, npre; for(int i=0;i<=last;++i){ if ( lead && 0 == i ) ans += dfs(pos-1,pre,true,limit&&i==Dig[pos]); else { npre = pre; tmp = npre / POW_OF_3[i] % 3; npre -= tmp * POW_OF_3[i]; if ( 2 == tmp ) tmp = 1; else tmp += 1; npre += tmp * POW_OF_3[i]; ans += dfs(pos-1,npre,false,limit&&i==Dig[pos]); } } return lead||limit?ans:D[pos][pre]=ans;}//求[1,n]满足条件的数有多少个llt ddp(ull n){ int k = 0; while(n){ Dig[k++] = n % 10; n /= 10; } llt tmp = dfs(k-1,0,true,true); return tmp;}int main(){ //freopen("1.txt","r",stdin); memset(D,-1,sizeof(D)); int kase; scanf("%d",&kase); ull a,b; while(kase--){ scanf("%llu%llu",&a,&b); printf("%lld\n",ddp(b)-ddp(a-1)); } return 0;}
最后看两个可以转为数位DP的例子。这两个题目通过找规律配合一些数组什么的也都是可以做的。
51NOD 1009
这个题目问数字1出现的次数,可以通过找规律然后直接统计计算得出。这里把它转成数位DP问题,即回答以下9个数位问题:包含1个1的数有多少个,包含2个1的数有多少个……
当然,这个做法速度上应该有点影响。
/* N以内1的数量是多少个 求出[1,N]区间内包含1个1的数有多少个 2个1的数有多少个…… 最后累加。 数位DP*/#include <string.h>#include <iostream>using namespace std;typedef long long llt;llt D[12][11][11];int Dig[30];int K;llt dfs(int pos,int sum,bool limit){ if ( -1 == pos ) return sum==K; if ( !limit && -1 != D[pos][sum][K] ) return D[pos][sum][K]; llt last = limit ? Dig[pos] : 9; llt ans = 0; for(int i=0;i<=last;++i){ ans += dfs(pos-1,sum+(1==i),limit&&i==last); } return limit ? ans : D[pos][sum][K] = ans;}llt ddp(llt n){ int k = 0; while( n ){ Dig[k++] = n % 10; n /= 10; } llt ans = 0; for(int i=1;i<=k;++i) K=i,ans+=i*dfs(k-1,0,true); return ans;}int main(){ memset(D,-1,sizeof(D)); llt n; cin >> n; cout<<ddp(n)<<endl; return 0;}
51NOD 1042
这个题目要求统计0~9的数量,上一题实际上是统计1的数量。所以,在上一题的基础上改改就行,传个参数。
/* [s,e]区间内0-9数字出现的次数 统计含1个1的数有几个,2个1的数有几个……*/#include <string.h>#include <iostream>using namespace std;typedef long long llt;//dijkw,i表示数位,j表示数字,k表示已有的数量,w表示目标数量llt D[20][10][20][20];int Dig[30];int K,Digit;llt dfs(int pos,int cnt,bool lead,bool limit){ if ( -1 == pos ) return cnt==K; if ( !lead && !limit && -1 != D[pos][Digit][cnt][K] ) return D[pos][Digit][cnt][K]; llt last = limit ? Dig[pos] : 9; llt ans = 0; for(int i=0;i<=last;++i){ if ( lead && 0==i ) ans += dfs(pos-1,0,true,limit&&i==last); else ans += dfs(pos-1,cnt+(Digit==i),false,limit&&i==last); } return lead || limit ? ans : D[pos][Digit][cnt][K] = ans;}llt proc(llt n){ int k = 0; while( n ){ Dig[k++] = n % 10; n /= 10; } llt ans = 0; for(int i=1;i<=k;++i) K=i,ans+=i*dfs(k-1,0,true,true); return ans;}int main(){ memset(D,-1,sizeof(D)); llt a,b; cin >> a >> b; for(Digit=0;Digit<10;++Digit){ cout<<proc(b)-proc(a-1)<<endl; } return 0;}
总结
数位DP模板是一个很好的模板。因为它首先给定了代码组织实施的框架,这里面就包含了状态转移的实现;更重要的是这个框架将DP中另一个核心问题“状态的设计”单独抽了出来,对每一个问题只需要单独考虑状态即可,很容易抓住重点去进行考虑和实现。
在此基础上进行一定的修改,还有可能完成更复杂的跟数位有关的问题。
- 数位dp模板
- 数位DP模板
- 数位DP模板
- 数位DP模板
- 【自用模板】数位dp
- 数位DP模板
- 数位DP模板
- 数位DP模板详解
- 数位DP模板
- 数位DP模板
- 数位DP模板
- 数位DP模板
- 数位DP(模板)
- 数位DP模板
- 数位DP模板
- 数位DP模板
- 【数位DP模板】
- 数位dp模板
- Python纪实:必需却又恐怖的monster --- mysql 数据库
- asp.net 伪删除
- mysql读写分离
- 【cpp】引用、指针、const
- 机器语言的理解
- 数位DP模板
- Java基础之GC回收
- map相关问题及匿名函数的问题
- Android 调用系统相机拍照、录像
- java处理异常的标准格式
- HTTP和HTTPS的区别
- [GO] mac cannot find package "fmt"
- ajax请求简单封装
- 计划-8/6:各种同步控制工具的使用