You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
7.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## $HDU-4734-F(x)$
[题目传送门](http://acm.hdu.edu.cn/showproblem.php?pid=4734)
### 一、题目描述
题目给了个$F(x)$的定义:$$\large F(x) = A_n * 2^{n-1} + A_{n-1} * 2^{n-2} + ... + A_2 * 2^1 + A_1 * 2^0$$$A_i$是十进制数位,然后给出$a、b,$求区间$[0,b]$内满足$f(i)<=f(a)$的$i$的个数。其中 $0 <= A,B < 10^9$
### 二、计算题目中的$F$函数
```c++
#include <bits/stdc++.h>
using namespace std;
/**
12345-->f(12345)=1*2^4+2*2^3+3*2^2+4*2^1+5*2^0
f(1234) =1*2^3+2*2^2+3*2^1+4*2^0
2*f(1234) =1*2^4+2*2^3+3*2^2+4*2^1
所以形成递归表示式
*/
//利用递归计算出F(x)
int f(int x) {
if (x == 0) return 0;
return f(x / 10) * 2 + x % 10;
}
//利用循环进行计算
int f2(int x) {
int res = 0;
int i = 0;
while (x) {
res += x % 10 * (1 << i);
i++;
x /= 10;
}
return res;
}
int main() {
cout << f(12345) << endl;
cout << f2(12345) << endl;
//极大值是1e9-1=99999999
cout << f2(1e9 - 1) << endl;
//输出4599
return 0;
}
```
### 三、暴力怎么做?
用例解析:
以 $5$ $100$ 为例:即$A=5,B=100$
$F(A)=5*1=5$
也就是让我们求一下,在$F(0)$~$F(100)$之间,有多少个$F(x)<=5$
撸起袖子就是干!
```c++
#include <bits/stdc++.h>
using namespace std;
int F(int x) {
if (x == 0) return 0;
return F(x / 10) * 2 + x % 10;
}
int fa;
int main() {
int T;
int num = 1;
cin >> T;
while (T--) {
int x, y;
cin >> x >> y;
fa = F(x);
int cnt = 0;
for (int i = 0; i <= y; i++)
if (F(i) <= fa) cnt++;
printf("Case #%d: %d\n", num++, cnt);
}
return 0;
}
```
毫无疑问,如此暴力,只能是$TLE$,看了一下时间限制,要求$500ms$,应该是一个数位$DP$问题。
### 四、正常的数位$DP$思路
#### 1、思考暴力$dfs$怎么做:
`int dfs(int pos,int sum,bool limit)`
* $pos$:我在哪里
* $sum$:我这个分身,走到现在,按$F(x)$的计算办法,已经拼接出的加权前缀和是多少,之所以要携带这个,是因为走完全程时需要判断总的$sum$是不是小于$F(A)$
* $limit$:是不是贴上界,之所以需要知道这个,是因为每位都需要上一位告诉自己,自己及以后各位是不是能取满,还是只能取一部分
**返回值**$int$类型的$dfs$,思路就是 **向后思考** :如果我站在$pos$位置,并且前面的人告诉了我:以前有多少$sum$前缀和,是不是贴上界,
然后,我的任务就是计算,在给定的条件下,后续的枚举所有可能数字中,有多少符合条件的数字。
根据这个返回值定义,所以整体的结果就是:$dfs(al,0,true)$
#### 2、记忆化
根据我们的经验,$500ms$直接暴力$dfs$应该也不可能过的了,必须考虑是不是**存在重复计算**,能不能记忆化。简单想一想,肯定是可以记忆化的:到达哪种状态后,我不管你前面怎么到达我这里,一旦我的状态确定,那么我的后续所有可能中,符合条件的数字个数是一定的,我计算一次,想办法记下来,下回就可以重复利用。
那么按什么来记忆呢?根据经验,$limit$一般不记,只记忆$!limit$的状态结果,所以$dp[pos][sum]$的状态记忆数组定义就出来了。
代码也就很简单了:
```c++
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>
using namespace std;
const int N = 32;
const int M = 1024 * 10 + 10;
int f[N][M];
int a[N];
int fa;
int F(int x) {
if (x == 0) return 0;
return F(x / 10) * 2 + x % 10;
}
int dfs(int pos, int sum, bool limit) {
if (pos == 0) return sum <= fa;
if (sum > fa) return 0;
if (!limit && ~f[pos][sum]) return f[pos][sum];
int ans = 0;
int up = limit ? a[pos] : 9;
for (int i = 0; i <= up; i++)
ans += dfs(pos - 1, sum + i * (1 << (pos - 1)), limit && i == a[pos]);
if (!limit) f[pos][sum] = ans;
return ans;
}
inline int calc(int x) {
memset(f, -1, sizeof f);
int al = 0;
while (x) a[++al] = x % 10, x /= 10;
return dfs(al, 0, true);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
int cnt = 1;
cin >> T;
while (T--) {
int x, y;
cin >> x >> y;
fa = F(x);
printf("Case #%d: %d\n", cnt++, calc(y));
}
return 0;
}
```
但很不幸运,<font color='red' size=4><b>Time Limit Exceeded</b></font>
### 五、释放减法的力量
[大神原文](https://www.cnblogs.com/sparkyen/p/11347119.html)
<font color='red' size=4><b>某大神写到:</b></font>刚开始一眼看到这题,算了一下 $F(999999999) = 4599$,也不是很大,感觉挺简单的,想着$dp[pos][sum]$不就完事了:$sum$记录从$len$(**最高位**)到$pos$位上累计的符合条件数字个数;
写出来交了一发,$TLE$,感觉就有点奇怪,翻了翻网上的题解,说是不能是$dp[pos][sum]$,要是$dp[pos][F(A) - sum]$才行;
也就是说,新定义$dp[pos][comp]$$comp$代表剩下的从第$pos$位到第$1$位,累加起来的权重,不能超过$comp$
**这是为什么呢?原因其实很简单:**
我们定义$dp[pos][sum]$的话,显然我们这 <font color='red' size=4><b> $T$组数据</b></font>……
每组数据只要$B$有变化,由于$sum$是从$B$最高位$(al)$**往下**累加权重,它跟$B$有密切关系($B$的长度$al$即为最高位),那么$dp$数组就必须要重新$memset(dp,-1,sizeof(dp))$,这样才不会出错;
那么我们如果是$dp[pos][comp]$呢,$comp$代表从第$1$位到第$pos$位最多还能累加起多少权重,那么它就和$B$没有关系,我们就不需要在输入每组数据后都重新将$dp$数组全部重置为$-1$。
```c++
#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>
using namespace std;
const int N = 32; //数位的长度上限
const int M = 1024 * 10 + 10; //剩余的数大小(状态集合按这个划分)
int f[N][M]; //结果DP数组
int a[N]; //数位拆分出来的数组
int fa;
//利用递归计算出
int F(int x) {
if (x == 0) return 0;
return F(x / 10) * 2 + x % 10;
}
/**
* @param pos 数位位置
* @param st 剩余对比值,初始值是F(x)
* @param limit 是不是贴上界
* @return
*/
int dfs(int pos, int st, bool limit) {
if (pos == 0) return st >= 0; //如果到达最后并且有剩余表示f(i)<= f(x),计数++
if (st < 0) return 0; //中途或最后一旦发生小于0情况剪枝
if (!limit && ~f[pos][st]) return f[pos][st];
int ans = 0;
int up = limit ? a[pos] : 9;
for (int i = 0; i <= up; i++)
//st = st - i * 2^ ( p-1 )
ans += dfs(pos - 1, st - i * (1 << (pos - 1)), limit && i == a[pos]);
if (!limit) f[pos][st] = ans;
return ans;
}
inline int calc(int x) {
int al = 0;
while (x) a[++al] = x % 10, x /= 10;
return dfs(al, fa, true);
}
int main() {
//不加78MS加了31MS效果还是很明显的
ios::sync_with_stdio(false);
cin.tie(0);
int T;
int cnt = 1;
cin >> T; //这个数值太BT了最大10000次
//优化的写法
memset(f, -1, sizeof f);
while (T--) {
int x, y;
cin >> x >> y;
fa = F(x); //计算出F(x)值,这是一个固定值
printf("Case #%d: %d\n", cnt++, calc(y));
}
return 0;
}
```