题意:一个整数 (AnAn-1An-2 … A2A1), 定义 F(x)=An * 2n-1+An-1 * 2n-2+… +A2 * 2+A1 * 1,求[0..B]内有多少数使得F(x)<= F(A)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4734
思路:数位dp 都是泪~~~
比赛的时候,dp状态是dp[pos][pre],pre表示当前枚举到pre为多少,判断pre<=fa。但这样设计状态的一个问题是对于不同的A,dp[][]表示的状态不同,所以每个T都有memset dp数组,并且重新计算,然后就超时了哭……
后来的后来,
dp状态设计成dp[pos][remain],remain表示还剩多少fA可以分配,判断remain>=0。这样对于不同的A便不影响他的状态表示。所以不用每次T都重新计算dp[][],大大缩短了时间。
总结一下,在设计数位DP状态时最好把每次询问的变量作为初始值而不是状态转移、判定时依赖的量(比如第一个状态判定条件是pre<=fa,就依赖于当前fa,这样就不好),这样才可以最大限度的发挥记忆化的作用。
ac的代码:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define maxn 30
#define LL int
LL digit[maxn];
LL dp[maxn][20000];
LL ans;
LL dfs(LL pos, LL remain, bool doing){
if(pos == -1) return remain >= 0;
if(remain < 0) return 0;
if(!doing && dp[pos][remain]!=-1) return dp[pos][remain];
LL ans = 0,nremain;
LL end = doing ? digit[pos] : 9;
for(LL i = 0;i <= end;i++){
nremain = remain - i *(1 << pos);
ans += dfs(pos - 1, nremain , doing && i==end);
}
if(!doing) dp[pos][remain] = ans;
return ans;
}
LL calc(LL x){
LL pos = 0;
while(x){
digit[pos++] = x %10;
x /= 10;
}
return dfs(pos - 1, ans , 1);
}
LL f(LL x){
LL sum = 0;
LL carr = 1;
while(x){
sum += (x%10) * carr;
carr *= 2;
x /= 10;
}
return sum;
}
LL a,b;
int main()
{
int T;
scanf("%d",&T);
memset(dp,-1,sizeof(dp));
for(int ncase = 1;ncase <= T; ncase ++){
scanf("%d %d",&a,&b);
ans = f(a);
printf("Case #%d: %d\n",ncase,calc(b));
}
return 0;
}
TLE的代码:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define maxn 30
#define LL int
LL digit[maxn];
LL dp[maxn][20000];
LL ans;
LL dfs(LL pos, LL pre, bool doing){
if(pos == -1) return pre <= ans;
if(pre > ans) return 0;
if(!doing && dp[pos][pre]!=-1) return dp[pos][pre];
LL ans = 0,npre;
LL end = doing ? digit[pos] : 9;
for(LL i = 0;i <= end; i++){
npre = pre + i *(1 << pos);
ans += dfs(pos - 1, npre , doing && i==end);
}
if(!doing) dp[pos][pre] = ans;
return ans;
}
LL calc(LL x){
memset(dp,-1,sizeof(dp));
LL pos = 0;
while(x){
digit[pos++] = x %10;
x /= 10;
}
return dfs(pos - 1, 0 , 1);
}
LL f(LL x){
LL sum = 0;
LL carr = 1;
while(x){
sum += x%10 * carr;
carr *= 2;
x /= 10;
}
return sum;
}
LL a,b;
int main()
{
int T;
scanf("%d",&T);
//memset(dp,-1,sizeof(dp));
for(int ncase = 1;ncase <= T; ncase ++){
scanf("%d %d",&a,&b);
ans = f(a);
//printf("ans = %I64d\n",ans);
printf("Case #%d: %d\n",ncase,calc(b));
}
return 0;
}
原来这份TLE的代码也是可以A的,
剪枝到位就行,可惜因为重复计算的次数太多,导致记忆化的优势没有完全显示出来,
我们可以考虑到转化中,每个二进制的个数最大为9,当我们算到最大值的时候,如果都小于fa,就可以直接进位,少了很多不必要的进位
代码:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define maxn 10
#define LL int
LL digit[maxn];
LL dp[maxn][5000];
LL ans;
int c[10];
LL dfs(LL pos, LL pre, bool doing){
if(pos == -1) return pre <= ans;
if(!doing && dp[pos][pre]!=-1) return dp[pos][pre];
LL sum = 0,npre;
LL end = doing ? digit[pos] : 9;
for(LL i = 0;i <= end; i++){
npre = pre + i *(1<<pos);
if(!doing && npre + 9 *( (1<<pos) - 1) <= ans) sum += c[pos];
else if(npre > ans) continue;
else{
sum += dfs(pos - 1, npre , doing && i == end);
}
}
if(!doing) dp[pos][pre] = sum;
return sum;
}
LL calc(LL x){
memset(dp,-1,sizeof(dp));
LL pos = 0;
while(x){
digit[pos++] = x %10;
x /= 10;
}
return dfs(pos - 1, 0 , 1);
}
LL a,b;
int main()
{
int T,carr;
c[0]=1;
for(int i=1;i<10;i++) c[i]=c[i-1]*10;
scanf("%d",&T);
for(int ncase = 1;ncase <= T; ncase ++){
scanf("%d %d",&a,&b);
ans = 0;
carr = 1;
while(a){
ans += (a%10) * carr;
carr *= 2;
a /= 10;
}
printf("Case #%d: %d\n",ncase,calc(b));
}
return 0;
}