小韦老师@NOIP普及组-2001-数的计算
题目:
描述
我们要求找出具有下列性质数的个数(包含输入的自然数 n):
先输入一个自然数 n ( n<=1000),然后对此自然数按照如下方法进行处理:
-
不作任何处理;
-
在它的左边加上一个自然数,但该自然数不能超过原数的一半;
-
加上数后,继续按此规则进行处理,直到不能再加自然数为止。
输入
输入一个自然数。
输出
输出满足条件的数的个数。
输入样例1
6
输出样例1
6
提示
输入样例1 的 6 个数分别是:
6
16
26
126
36
136
来源
NOIP 普及组 2001年 第一题
题解:
破题
对于自然数 n,按照以下的方法进行出处理:
1.不作任何处理;
2.在它的左边加上一个自然数,但该自然数不能超过原数的一半;
3.加上数后,继续按此规则进行处理,直到不能再加自然数为止。
这里的此规则是指,这个新的数要将刚刚得到的数作为处理的对象再进行处理,
例如 n = 6:
1.先得到的数是 6
2.枚举 1~3(6 的一半,即为 3)
-
枚举 1,可以放在 6 的左边变成 16,1 已经不能再枚举小于 1 的一半的数
-
枚举 2,可以放在 6 的左边变成 26;2 还可以枚举 1,所以放在 26 的前面变成 126,现在 1 已经不可以再枚举
-
枚举 3,可以放在 6 的左边变成 36;3 还可以枚举 1,所以放在 26 的前面变成 136,现在 1 已经不可以再枚举
所以,得到了 6 个数分别是:6,16,26,126,36,136
方法一:递归
思路:
整体思路:
对于一个自然数 n 而言,得到它自己这 1 个数后,然后枚举 1~n/2 得到 n/2 个数,然后再对枚举 1~n/2 的每个数进行同样规则的枚举,所以原问题被分解成结构与原问题一致,但是规模变小的子问题,因此可以用递归。
具体步骤:
1.定义 n,并输入 n。
2.定义一个 cnt,作为计数器,用于记录结果。
3.调用自定义函数完成计算过程:
// 函数功能:完成计算过程
// 参数:n
// 返回值:无
f(n);
4.输出结果 f(n)。
5…实现递归函数 f(n):
void f(int n) {
cnt++; // 进来就加 1 (第一次进来的时候是加 n 自己)
for (int i = 1; i <= n/2; i++) { // 枚举 1~n/2
f(i); // 调用递归函数
}
}
完整代码:
#include <bits/stdc++.h>
using namespace std;
int cnt = 0; // 计数器,用于记录结果
void f(int n) {
cnt++; // 进来就加 1 (第一次进来的时候是加 n 自己)
for (int i = 1; i <= n/2; i++) { // 枚举 1~n/2
f(i); // 调用递归函数
}
}
int main() {
// 定义 n,并输入 n
int n;
cin >> n;
// 函数功能:完成计算过程
// 参数:n
// 返回值:无
f(n); // 调用自定义函数完成计算过程
// 输出结果
cout << cnt;
return 0;
}
方法二:递推
思路:
整体思路:
从 n = 6 的例子中,我们可以发现,如果用 f[n] 表示自然数为 n 时,按照以上规则可以得到的数的数量(也即题目所求),则递推式为:
f[n] = 1 + f[1] + f[2] + f[3] +……+f[n/2](加 1 是加上自己)
边界条件为:
f[1] = 1
f[1]~f[n/2] 为 1~n/2 在 n 的左边时能得到的数的数量。
例如:n = 6 时,
f[2] = f[1] + 1 = 2
f[3] = f[1] + 1 = 2
f[4] = f[1] + f[2] + 1 = 4
f[5] = f[1] + f[2] + 1 = 4
f[6] = f[1] + f[2] + f[3] + 1 = 1 + 2 + 2 + 1 = 6
具体步骤:
1.定义数组 f,用来存储对应的自然数能得到的数的数量
const int N = 1e3 + 10;
int f[N];
2.定义 n,并且输入 n
3.处理边界条件, n = 1 时 f[1] 等于 1。
4.枚举 2~n,每次先把自己加上,然后根据递推式计算:
for (int i = 2; i <= n; i++) { // 枚举 1~n
f[i] = 1; // 先把 i 自己加上
// 枚举 1~i/2,f[i] = f[1] + f[2] + f[3] + ……+ f[i/2]
for (int j = 1; j <= i/2; j++) {
f[i] += f[j];
}
}
5.输出结果 f[n]。
完整代码:
#include <bits/stdc++.h>
using namespace std;
// 定义数组 f,用来存储对应的自然数能得到的数的数量
const int N = 1e3 + 10;
int f[N];
int main() {
// 定义 n,并且输入 n
int n;
cin >> n;
f[1] = 1; // 边界条件, n = 1 时等于 1
for (int i = 2; i <= n; i++) { // 枚举 2~n
f[i] = 1; // 先把 i 自己加上
// 枚举 1~i/2,f[i] = f[1] + f[2] + f[3] + ……+ f[i/2]
for (int j = 1; j <= i/2; j++) {
f[i] += f[j];
}
}
// 输出结果
cout << f[n];
return 0;
}
方法三:动态规划
思路:
整体思路:
用 f(n) 来表示自然数为 n 时的结果
我们来观察以下的例子:
n = 1,f(n) = 1
n = 2,f(n) = 2
n = 3,f(n) = 2
n = 4,f(n) = 4
n = 5,f(n) = 4
n = 6,f(n) = 6
n = 7,f(n) = 6
n = 8,f(n) = 10
n = 9,f(n) = 10
通过观察,我们可以发现第 1 个规律,当 n 为奇数时,f(n) = f(n-1)(n > 1)
而 n 为偶数时有什么规律呢?我们以 n = 8 来观察:
1 18 28 128 38 138 48 148 248 1248
对于前面的 1 18 28 128 38 138 而言,跟 n = 7 的情况是一样的:
1 18 28 128 38 138
1 17 27 127 37 137
而对于后面的 48 148 248 1248 则和 n = 4 的情况是一样的:
48 148 248 1248
4 14 24 124
所以我们得到第 2 个规律:
当 n 为偶数时,f(n) = f(n-1) + f(n/2)
具体步骤:
1.定义数组 f,用来存储对应的自然数能得到的数的数量
const int N = 1e3 + 10;
int f[N];
2.定义 n,并且输入 n。
3.处理边界条件 f[1] = 1。
4.枚举 2~n,若是计数项,则等于前一项的值;若是偶数项,则等于前一项加上自己的一半的那一
项的值。
for (int i = 2; i <= n; i++) {
if (i % 2 != 0) { // 若是奇数项,则等于 f[i-1]
f[i] = f[i-1];
} else { // 若是偶数项,则等于 f[i-1] + f[i/2]
f[i] = f[i-1] + f[i/2];
}
}
5.输出结果。
完整代码:
#include <bits/stdc++.h>
using namespace std;
// 定义数组 f,用来存储对应的自然数能得到的数的数量
const int N = 1e3 + 10;
int f[N];
int main() {
// 定义 n,并且输入 n
int n;
cin >> n;
// 处理边界条件
f[1] = 1;
// 枚举 2~n
for (int i = 2; i <= n; i++) {
if (i % 2 != 0) { // 若是奇数项,则等于 f[i-1]
f[i] = f[i-1];
} else { // 若是偶数项,则等于 f[i-1] + f[i/2]
f[i] = f[i-1] + f[i/2];
}
}
// 输出结果
cout << f[n];
return 0;
}
思考:
1°这三种方法你更喜欢哪一种方法?请说明理由
2°这三种的方法的优缺点各是什么?
3°这三种方法有什么相同点,有什么不同点?
4°你能这个题收获些什么?