HDU3555 Bomb 数位DP经典题

来源:互联网 发布:福利软件 编辑:程序博客网 时间:2024/05/22 20:55

传送门:HDU3555

题意:1到n区间内有多少个数字含有‘49’。

题解:典型数位DP的题目,这次做个题的收获是学到了通用的递推解数位DP的方法。

递推解法:

//dp[i][j]:长度为i的数的第j种状态
//dp[i][0]:长度为i但是不包含49的方案数
//dp[i][1]:长度为i且不含49但是以9开头的数字的方案数
//dp[i][2]:长度为i且包含49的方案数

(转)状态转移如下
dp[i][0] = dp[i-1][0] * 10 - dp[i-1][1];  // not include 49  如果不含49且,在前面可以填上0-9 但是要减去dp[i-1][1] 因为4会和9构成49
dp[i][1] = dp[i-1][0];  // not include 49 but starts with 9  这个直接在不含49的数上填个9就行了
dp[i][2] = dp[i-1][2] * 10 + dp[i-1][1]; // include 49  已经含有49的数可以填0-9,或者9开头的填4

接着就是从高位开始统计

在统计到某一位的时候,加上 dp[i-1][2] * digit[i] 是显然对的,因为这一位可以填 0 - (digit[i]-1)
若这一位之前已经有49,那么加上 dp[i-1][0] * digit[i] 也是显然对的。
若这一位之前没有49,但是digit[i]比4大,那么当这一位填4的时候,就得加上dp[i-1][1]


以上转自:http://blog.csdn.net/u012860063/article/details/46820639

递推做的话要预处理出DP数组来,用到的就是上面的三个状态转移方程。

代码:

#include<stdio.h>#include<iostream>#include<string.h>#include<math.h>#include<algorithm>#include<queue>#include<stack>#include<set>#include<vector>#include<map>#define ll long long#define pi acos(-1)#define inf 0x3f3f3f3fusing namespace std;typedef pair<int,int>P;//dp[i][j]:长度为i的数的第j种状态  //dp[i][0]:长度为i但是不包含49的方案数  //dp[i][1]:长度为i且不含49但是以9开头的数字的方案数  //dp[i][2]:长度为i且包含49的方案数 int c[25];ll dp[25][3];void init(){memset(dp,0,sizeof(dp));dp[0][0]=1;for(int i=1;i<=20;i++){dp[i][0]=dp[i-1][0]*10-dp[i-1][1];dp[i][1]=dp[i-1][0];dp[i][2]=dp[i-1][1]+dp[i-1][2]*10;}}ll solve(int len,ll n){ll ans=0;bool flag=false;for(int i=len;i>=1;i--){ans+=dp[i-1][2]*c[i];if(flag)ans+=dp[i-1][0]*c[i];if(!flag&&c[i]>4)ans+=dp[i-1][1];if(c[i+1]==4&&c[i]==9)flag=true;}return ans;}int main(){int t;ll n;init();scanf("%d",&t);while(t--){scanf("%lld",&n);n++;//n++是因为本题要求[1,n]含49的数字的个数,而递推过程求得是[1,n)含49的数字的个数。int len=0;memset(c,0,sizeof(c));while(n){c[++len]=n%10;n/=10;}printf("%lld\n",solve(len,n));}}
然后就是经典的记忆化搜索了,我用的是原来从网上扒的一个模板。

代码:

#include<stdio.h>#include<iostream>#include<string.h>#include<math.h>#include<algorithm>#include<queue>#include<stack>#include<set>#include<vector>#include<map>#define ll long long#define pi acos(-1)#define inf 0x3f3f3f3fusing namespace std;typedef pair<int,int>P;//    pos    = 当前处理的位置(一般从高位到低位)//    pre    = 上一个位的数字(更高的那一位)//    status = 要达到的状态,如果为1则可以认为找到了答案,到时候用来返回,//            给计数器+1。//    limit  = 是否受限,也即当前处理这位能否随便取值。如567,当前处理6这位,//            如果前面取的是4,则当前这位可以取0-9。如果前面取的5,那么当前//            这位就不能随便取,不然会超出这个数的范围,所以如果前面取5的//            话此时的limit=1,也就是说当前只可以取0-6。////    用DP数组保存这三个状态是因为往后转移的时候会遇到很多重复的情况。ll DP[25][10][2];int DIG[25];ll dfs(int pos,int pre,int status,int limit){    //已结搜到尽头,返回"是否找到了答案"这个状态。    if(pos < 1)    return status;    //DP里保存的是完整的,也即不受限的答案,所以如果满足的话,可以直接返回。    if(!limit && DP[pos][pre][status] != -1)    return DP[pos][pre][status];    int end = limit ? DIG[pos] : 9;    ll ret = 0;    //往下搜的状态表示的很巧妙,status用||是因为如果前面找到了答案那么后面    //还有没有答案都无所谓了。而limti用&&是因为只有前面受限、当前受限才能    //推出下一步也受限,比如567,如果是46X的情况,虽然6已经到尽头,但是后面的    //个位仍然可以随便取,因为百位没受限,所以如果个位要受限,那么前面必须是56。    //    //这里用"不要49"一题来做例子。    for(int i = 0;i <= end;i ++)    ret += dfs(pos - 1,i,status || (pre == 4 && i == 9),limit && (i == end));    //DP里保存完整的、取到尽头的数据    if(!limit)    DP[pos][pre][status] = ret;    return ret;}int main(){int t;ll n;scanf("%d",&t);while(t--){memset(DP,-1,sizeof(DP));scanf("%lld",&n);int len=0;while(n){DIG[++len]=n%10;n/=10;}printf("%lld\n",dfs(len,0,0,1));}}

注意一下用long long就好了。

0 0