您的位置:首页 > 其它

HDU 3555 Bomb 详解(数位DP入门题)

2016-10-25 00:30 525 查看


Bomb

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 131072/65536 K (Java/Others)

Total Submission(s): 16153    Accepted Submission(s): 5902


Problem Description

The counter-terrorists found a time bomb in the dust. But this time the terrorists improve on the time bomb. The number sequence of the time bomb counts from 1 to N. If the current number sequence includes the sub-sequence "49", the power of the blast would
add one point.

Now the counter-terrorist knows the number N. They want to know the final points of the power. Can you help them?

 

Input

The first line of input consists of an integer T (1 <= T <= 10000), indicating the number of test cases. For each test case, there will be an integer N (1 <= N <= 2^63-1) as the description.

The input terminates by end of file marker.

 

Output

For each test case, output an integer indicating the final points of the power.

 

Sample Input

3
1
50
500

 

Sample Output

0
1
15

HintFrom 1 to 500, the numbers that include the sub-sequence "49" are "49","149","249","349","449","490","491","492","493","494","495","496","497","498","499",
so the answer is 15.

 

Author

fatboy_cw@WHU

 

Source

2010 ACM-ICPC Multi-University
Training Contest(12)——Host by WHU

 

Recommend

zhouzeyong   |   We have carefully selected several similar problems for you:  3554 3556 3557 3558 3559 

  终于弄懂了第一道数位DP,有些开心的。。。这题照着别人用递推式写了出来,然后学dfs写法,网上看到三种版本自己写了两种,也大约弄清了dfs数位DP的原理把。。。

1,递推式写法:借鉴网上的思路:转自:http://www.2cto.com/kf/201408/324528.html

先说一下总体思路:
假设统计N=591时,那么按以下的顺序进行统计:
1~499 确定5这一位,统计的数比它小
500~589 确定9这一位 ,统计的数比它小
590 确定1这一位,统计的数比它小
最后判断一下自身是不是符合 即591
循环三次就把符合题意的数的总数全都求出来了,这就是本题的数位DP的奥妙之处.
再比如 1249
1~999
1000~1199
1200~1239
1240~1248
1249

dp[i][j] 表示长度为i的数(也就是有i位数)状态为j的数的总数有多少
本题状态有三种:
①dp[i][0]代表长度为i且不包含49的数有多少个
②dp[i][1]代表长度为i且不包含49且左边第一位(最高位)为9的数有多少个
③dp[i][2]代表长度为i且包含49的数有多少个
打表预处理,0<=i<=21(21位就够了),主要是处理状态的转移
dp[i][0]=dp[i-1][0]*10-dp[i-1][1]; //dp[i][0]高位随便加一个数字都可以,但是会出现49XXX的情况,要减去

dp[i][1]=dp[i-1][0]; //在不含49的情况下高位加9

dp[i][2]=dp[i-1][2]*10+dp[i-1][1]; //在含有49的情况下高位随便加一位或者不含49但高位是9,在前面最高位加上4就可以了
#include <cstdio>
#include <string.h>
#include <iostream>
using namespace std;
//dp[i][0]代表长度为i不含49的数
//dp[i][1]代表长度为i不含49且最高位为9的数
//dp[i][2]代表长度为i含有49的数
long long dp[22][3];
int bit[21];
long long n;
void init()  //最开始初始化时从右往左;从最低位往最高位走
{
dp[0][0]=1,dp[0][1]=0,dp[0][2]=0;
for(int i=1;i<=21;i++)
{
dp[i][0]=dp[i-1][0]*10-dp[i-1][1];//dp[i][0]高位随便加一个数字都可以,但是会出现49XXX的情况,要减去
dp[i][1]=dp[i-1][0];//在不含49的情况下高位加9
dp[i][2]=dp[i-1][2]*10+dp[i-1][1];//在含有49的情况下高位随便一位或者不含49但高位是9,在前面最高位加上4就可以了
}
}

long long cal(long long n)
{
int len=0;
while(n)
{
bit[++len]=n%10;
n/=10;
}
long long ans=0;
bit[len+1]=0;
bool has=false;
for(int i=len;i>=1;i--)//每次确定一位,从最左往右,最高位往最低位走
{
ans+=dp[i-1][2]*bit[i];//低位中含有49,高位随便一个1,2,3....bit[i]都可以,bit[i]是代表有几个数字,比如bit[i]=5,那么代表有五个数字,0,1,2,3,4,比5小。
if(!has)
{
if(bit[i]>4)
ans+=dp[i-1][1];//低位中高位是9,前面加上4就可以了
}
else
ans+=(dp[i-1][0])*bit[i];//如果有49,就随便选了,比如 495的时候,有490 491 492 493 494
//上面这句话困扰了我一天多的时间。为什么不写(d[i-1][0]+dp[i-1][2])*bit[i]呢,前面已经出现过49
//那么低位任意选择都可以,dp[i-1][0]是那些低位不出现49的,dp[i-1][2]是那些低位出现49的,按理说应该
//加上啊,BUT!!!清注意循环里面的第一句ans+=dp[i-1][2]*bit[i]; 前面已经加上了dp[i-1][2]低位有
//49的情况了,哎,欲哭无泪........
if(bit[i+1]==4&&bit[i]==9)
has=true;
}
if(has)
ans++;
return ans;
}

//以491为例,先求出所有比400小的数中有多少符合题意的,然后4这一位确定以后,再求所有比490小,再求出所有比491小
//i=3 求出数 049 149 249 349
//i=2 求出数 449
//i=1 求出数 490

//自身包含49 所以求出数491

int main()
{
init();
int t;cin>>t;
while(t--)
{
cin>>n;
cout<<cal(n)<<endl;
}
return 0;
}


2,间接先求出不含有49所有的数字,n减去这个数字。。。这个比较好写,也比较好想。。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 25;
long long dp[maxn][2]; //两个状态,一个是前面是4用1表示,反之用0表示
int bit[maxn];
long long dfs(int len, int pre, int limit)
{
if(len < 1) return 1; //如果递归到最后一位,说明这个数可以复合要求就+1,如果前面有49不可能到这里的直接被continue了
if(!limit && dp[len][pre] != -1) return dp[len][pre]; //这里一定要加!limit,下面有解释
int last = limit ? bit[len] : 9;
long long ans = 0;
for(int i = 0; i <= last; i++) //这里的i是指每一位上数字大小
{
if(pre && i == 9) continue; //如果有可以连在一起的49,直接pass掉
ans += dfs(len-1, i == 4, limit && (i == last)); //没有连在一起的后面可以随意+数字
}
if(!limit) dp[len][pre] = ans; //这里一定要!limit
return ans;
}
long long cal(long long n)
{
int k = 0;
while(n)
{
bit[++k] = n % 10;
n /= 10;
}
memset(dp, -1, sizeof(dp)); //不要忘记初始化
return dfs(k, 0, 1);
}
int main ()
{
long long t, n;
scanf("%I64d", &t);
while(t--)
{
scanf("%I64d", &n);
printf("%I64d\n", n-cal(n)+1);
}
return 0;
}

3,根据上面这个思想,加了一个数组进行优化
/*
一开始真的按照“不要49”这么来求的。写博客的时候看看别人博客吸收精华,下面是正着求得,z[i]数组真是神来之笔
本来我也行正着求但是不知道怎么表示找到49之后应该加什么,一个z[i]完美解决了找到49之后应该加多少
*/
#include<iostream>
#include<stdio.h>
#include<vector>
#include<algorithm>
#include<string.h>
#include<cstdio>
#define N 30
using namespace std;
long long t,n;
int g
;//用来存放数字的位数
long long dp
[2];//dp[i][j][k]表示当前剩余位数是i上一位数字是j且当前位上是否为4的状态的个数
long long z
={1};
long long dfs(int len,bool s,bool f)//s表示是不是当前位上是不是4
{
if(len==0)
return 0;
if(!f&&dp[len][s]>=0)
return dp[len][s];
int fmax=f?g[len]:9;
long long cur=0;
//cout<<"fmax="<<fmax<<endl;
for(int i=0;i<=fmax;i++)
{
if(s&&i==9)
{
cur+=f?n%z[len-1]+1:z[len-1];//当前位找到了,剩下的不用搜了,直接加上就行了,这里加的是剩下的所有位
}
else
cur+=dfs(len-1,i==4,f&&g[len]==i);
//cout<<"cur="<<cur<<endl;
}
//cout<<cur<<endl;
return f?cur:dp[len][s]=cur;
}
long long solve(long long x)
{
int len=0;
while(x)
{
g[++len]=x%10;
x/=10;
}
//for(int i=1;i<=len;i++)
// cout<<g[i];
//cout<<endl;
g[len+1]=0;
return dfs(len,false,true);
}
int main()
{
//freopen("in.txt","r",stdin);
for (int i=1;i<N;i++)
{
z[i]=z[i-1]*10;
}
scanf("%lld",&t);
memset(dp,-1,sizeof dp);
while(t--)
{
scanf("%lld",&n);
printf("%lld\n",solve(n));
}
return 0;
}

4,就是根据递推式的递推规则,写递归,这应该是最正版的dfs了把。。。
dp数组的后两维实际上只需要三个状态就能够分别。分别为到d[pos]前一位已经出现了49,用2表示;d[pos]前一位没有出现49,但是前一位为4,用1表示;d[pos]前一位没有出现49,而且前一位不为4,用0表示。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 25;
long long dp[maxn][3];
int bit[maxn];
long long dfs(int len, int pre, int limit)
{
if(len < 1) return pre == 2; //这里如果前面有49,那么一个数字可以使ans+1,如果递归到这里没有出现过49就不能+1
if(!limit && dp[len][pre] != -1) return dp[len][pre];
int last = limit ? bit[len] : 9;
long long ans = 0; //这里初始赋值=0,为了给下面dp数组记忆化,记录这个位置的这个状态后面有几个符合条件的
for(int i = 0; i <= last; i++)
{
if(pre == 0 || pre == 1 && i != 9) //不可能有49的,就一直往下找有49的
ans += dfs(len-1, i == 4, limit && (i == last));
else
ans += dfs(len-1, 2, limit && (i == last)); //一担出现过49 后面的pre就都是2了,所以这个dfs后面所有的数字都会使结果+1
//这样前面dfs递归到这里的时候 可以直接用了,这里后面的都被染色成了2,dp[len][2]记录当前个数
}
if(!limit) dp[len][pre] = ans;
return ans;
}
long long cal(long long n)
{
int k = 0;
while(n)
{
bit[++k] = n%10;
n /= 10;
}
memset(dp, -1, sizeof(dp));
return dfs(k, 0, 1);
}
int main()
{
long long t, n;
scanf("%lld" ,&t);
while(t--)
{
scanf("%lld", &n);
printf("%lld\n", cal(n));
}
return 0;
}

对!limit的解释:

相信读者还对这个有不少疑问,笔者认为有必要讲一下记忆化为什么是if(!limit)才行,大致就是说有无limit会出现状态冲突,举例:

约束:数位上不能出现连续的两个1(11、112、211都是不合法的)

假设就是[1,210]这个区间的个数

状态:dp[pos][pre]:当前枚举到pos位,前面一位枚举的是pre(更加前面的位已经合法了),的个数(我的pos从0开始)

先看错误的方法计数,就是不判limit就是直接记忆化




那么假设我们第一次枚举了百位是0,显然后面的枚举limit=false,也就是数位上0到9的枚举,然后当我十位枚举了1,此时考虑dp[0][1],就是枚举到个位,前一位是1的个数,显然dp[0][1]=9;(个位只有是1的时候是不满足的),这个状态记录下来,继续dfs,一直到百位枚举了2,十位枚举了1,显然此时递归到了pos=0,pre=1的层,而dp[0][1]的状态已经有了即dp[pos][pre]!=-1;此时程序直接return
dp[0][1]了,然而显然是错的,因为此时是有limit的个位只能枚举0,根本没有9个数,这就是状态冲突了。有lead的时候可能出现冲突,这只是两个最基本的不同的题目可能还要加限制,反正宗旨都是让dp状态唯一


对于这个错误说两点:一是limit为true的数并不多,一个个枚举不会很浪费时间,所以我们记录下! limit的状态解决了不少子问题重叠。第二,有人可能想到把dp状态改一下dp[pos][state][limit]就是分别记录不同limit下的个数,这种方法一般是对的,关于这个具体会讲,下面有题bzoj3209会用到这个。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: