题解 P1028 【数的计算】
Vel_
2019-01-25 20:44:15
## Code $O(n)$
### Force 25% $O(n^n)$
递归是过不了的。。与Fibonacci相似。可以分析递归复杂度。
$(n) = \sum_{i=1}^{n/2}f(i)+o(1)$
求上界的话,放缩一下:
$f(n) = nf(n-1)+o(1)$
显然,递推一下就有($n!=O(n^n)$),比指数还吓人。。
$f(n) = O(n^n)$
```c++
#include <bits/stdc++.h>
using namespace std;
int f(int n){
if(n == 1) return 1;
int result = 0;
for(int i = 1; i <= n/2; ++i) //通项
result += f(i);
return result + 1; //算上自己
}
int main(){
int n; cin>>n;
cout<<f(n);
}
```
### Force+记忆剪枝 $O(n^2)+O(n)$
```c++
#include <bits/stdc++.h>
using namespace std;
int f_remember[1005];
int f(int n){
if(n == 1) return 1;
if(f_remember[n]) return f_remember[n];
int result = 0;
for(int i = 1; i <= n/2; ++i)
result += f(i);
return f_remember[n] = result + 1; //记忆
}
int main(){
int n; cin>>n;
cout<<f(n);
}
```
### 迭代/传播 $O(n^2)+O(n)$
> 此方法相当于记忆剪枝的迭代版改写。
状态方程:(先求小,再求大)
$f(n) = \sum_{i=1}^{n/2}f(i), f(1)=1$
注意到从递推式的基本项可以向上**传播**。
```c++
#include <bits/stdc++.h>
using namespace std;
int f[1005];
int main(){
int n; cin>>n;
f[1] = 1;
for(int i = 2; i <= n; ++i){
for(int j = 1; j <= i/2; ++j) //通项
f[i] += f[j];
f[i] ++; //算上自己
}
cout<<f[n];
}
```
### 递推优化 $O(n)+O(0.5n)$
注意到,
$f(n) = \sum_{i=1}^{n/2}f(i)+o(1),\quad f(n-1) = \sum_{i=1}^{(n-1)/2}f(i)+o(1)$
> 显然每一步的**求和**中,**存在大量的重复计算**。
递推公式可以优化为:
$f(n) = SUM(n/2)+1,\quad SUM(i) = SUM(i-1)+f(i)$
这时内部求和的$O(n)$变为分摊常数。同样可以采用递归或迭代方法。
#### 记忆剪枝 $O(n)+O(n)$
```c++
#include <bits/stdc++.h>
using namespace std;
int f[1005], sum[505];
int F(int i); int Sum(int i);
int main(){
int n; cin>>n;
f[1] = sum[1] = 1;
cout<<F(n);
}
int F(int i){
if(f[i]) return f[i]; //剪枝
return f[i] = Sum(i/2) + 1; //利用赋值,记忆
}
int Sum(int i){
if(sum[i]) return sum[i]; //剪枝
return sum[i] = Sum(i - 1) + F(i); //利用赋值,记忆
}
```
#### 迭代传播 $O(n)+O(n)$
```c++
#include <bits/stdc++.h>
using namespace std;
int f[1005], sum[505];
int main(){
int n; cin>>n;
f[1] = 1;
for(int i = 2; i <= n; ++i){
sum[i/2] = sum[i/2 - 1] + f[i/2];
f[i] = sum[i/2] + 1;
}
cout<<f[n];
}
```
#### 就地传播 $O(n)+O(0.5n)$
> 前面两种的空间准确来讲其实是$O(n)+O(1.5n)$。通过就地可以进行空间优化。
>
> 理论上任何传播算法都可以做到就地,因为每一状态都只与**紧邻的前一状态**有关。
>
> 由于本题的**<font color=red>两个</font>**传播公式**相互耦合**,同时实现两个状态的就地传播是**不可能的**。
>
> > 可以理解为:当`i`取值较大时,`sum`的某一项`sum[i]`可以传播到`f`相对很远的两项`f[2i],f[2i+1]`,为了之后计算`sum[2i]`和`sum[2i+1]`,`f`的两项必须被动态缓存起来。可以预见,最多的缓存项可以达到$n/4$的规模。
>
> 但是,`sum`和`f`**分别的就地传播**,两种方案都可行。如下图所示:
![](https://i.imgur.com/FYFPuoj.png)
再关注一下递推公式:
$f(n) = SUM(n/2)+1,\quad SUM(i) = SUM(i-1)+f(i)$
于是我们可以由此写就地传播辣~~
##### **sum就地**: $O(n)+O(1.0n)$
```c++
#include <bits/stdc++.h>
using namespace std;
int f[1005], sum;
int main(){
int n; cin>>n;
for(int i = 1; i <= n; i++){
f[i] = sum + 1;
if(i%2) sum += f[(i+1)/2];
}
cout<< f[n];
}
```
##### **f就地**: $O(n)+O(0.5n)$
```c++
#include <bits/stdc++.h>
using namespace std;
int f, sum[505];
int main(){
int n; cin>>n;
for(int i = 1; i <= n/2; i++){
f = sum[i/2] + 1;
sum[i] = f + sum[i - 1];
}
cout<< sum[n/2] + 1;
}
```
#### 就地+队列 $O(0.5n)+O(0.25n)$
我们之前已经提到,`f`可以用动态缓存的方式记录。并且,每一个`f[i]`的调用都是先进先出的,因此可以采用队列。。
> `sum`只要求到`n/2`就可以求出`f[n]`,`sum`只要求到`n/4`就可以求出`f[n/2]`。
>
> 因此,`sum`求到`n/4`(此时积累了`n/4`的缓存),然后卸载缓存到`f[n/2]`求得`sum[n/2]`,即可求出`f[n]`。如图:
![Imgur](https://i.imgur.com/Ypo0faS.png)
```c++
#include <bits/stdc++.h>
using namespace std;
queue<int> f; int sum = 0; //sum[0] = 0
int main(){
int n; cin>>n;
int i;
f.push(1); //initialize f as sum[1]
for(i = 1; i <= (n >> 2); i++){
sum += f.front(); f.pop(); //reserve f in queue buffer
f.push(sum+1); f.push(sum+1);
}
for( ; i <= (n >> 1); i++){
sum += f.front(); f.pop(); //clear the buffer(maybe 1 element left sometime)
}
cout<< sum + 1;
}
```