一开始的思路
从条件(1)可知,所有的元素都是从set(n)
集合中得到的
从条件(2)可知,设最近添加的数为lastAdd
,则新添加的数的范围为 ,其中除法下取整(不超过一半)
从条件(3)推测,该规则要一直进行下去,可以使用递归操作
第一个问题:如何添加数?
第一个问题:“在 n 的左边添加一个自然数 i”,怎么将这句话表示成式子?
举一个例子,,得到
可以归纳:
取n的位数
第二个问题:如何将这个规则一直进行下去?如何退出(终止条件)?
我们已经知道新添加的数的范围为 ,相当于知道了所有可以新添加的选择
对于每一种选择,我们按照上面的公式算出新的数然后添加到set(n)
中,并把新添加的选择作为最近添加的数,放到下层递归
def halfSet(lastAdd, lastNum):
for i : 每种选择(范围):
newNumber = 代入公式
set.push(newNumber) // 添加新的数
halfSet(i, newNumber) // 最近添加的是i,然后进入下一层递归
end
end
递归需要出口,即终止条件,什么时候跳出递归呢?
我们发现,当for
循环不满足条件而终止时,递归其实就结束了,因此我们不需要添加新的终止条件,for
循环的范围就是条件。
实际上,根据题意,我们只需要给出最终半数集的元素个数即可,不需要确定半数集中的元素。
我们可以通过递归直接计算元素个数
def halfSet(lastAdd):
int cnt = 1; // cnt表示半数集元素个数,初始化为1(至少有一个元素)
for i : 每种选择(范围):
cnt += halfSet(i); // 选择 i 添加,并将层层递归的结果返回给计数器
end
end
代码
#include <iostream>
using std::cin;
using std::cout;
int halfSet(int lastAdd) {
int cnt = 1;
for (int i = 1; i <= lastAdd / 2; ++i) {
// 将递归的结果累加起来,就是半数集的元素个数
// 类似于求树的节点数 = 左子树节点数 + 右子树节点数 + 根节点
// 再递归求左子树的左子树、右子树……和右子树的左子树、右子树……
cnt += halfSet(i);
}
return cnt;
}
int main()
{
int n;
cin >> n;
cout << halfSet(n);
}
改进
运行上面的代码时发现,一旦数据量变大,如达到 的量级时,程序运行很慢,这涉及到了递归时的重复子问题。
造成重复的原因在于,如果我们之前算出了 n 在左侧添加数的所有可能,那么之后比 2n 大的数在遍历选择的时候,
还会计算 n在左侧添加数的所有可能,这是没有必要的。
例如:如果我们之前就算出“4……”在左侧添加数的情况有① “14……”,② “24……”,③ “124……”
那么之后遇到情况“48……”要计算在左侧添加数的情况,就是① “148……”, ② “248……”, 和③ “1248……”
完全没有必要再算三遍,直接让计数器 += 3
即可
结论:程序想要进入某一个递归前,我们先检查是否有必要进入这个递归,我们要对递归树进行剪枝,去掉不需要的分支入口
常用的方法是记忆化,利用一个数组存储计算过的数,如果出现相同的计算结果,那么不用进入递归,直接处理计数器即可
同时发现:保存数字小的计数结果,可以直接算出数字大的情况的结果
如:计算出“1”的计数结果为0,“2”的为1,“3”的1,“4”的为3(14、24、124三种情况)……
那么当计算“16”的计数情况时,需要从 1 选择到 8,选择1的时候,0种情况,cnt = 1
(自身);选择4的时候,3种情况,cnt = 4
可以一开始将数字小的情况保存到数组,后面数字大的很多递归结果就直接通过数组调用
代码
#include <iostream>
#include <vector>
using std::cin;
using std::cout;
using std::vector;
// memo存储某种选择已经添加过的元素个数,初始时所有元素都设置为0表示还未添加
vector<int> memo;
int halfSet(int lastAdd) {
// 如果lastAdd这种选择已经计算过添加的元素有多少个的话
// 直接返回,让计数器累加
if (memo[lastAdd] != 0) {
return memo[lastAdd];
}
int cnt = 1; // 本身也是半数集的一个元素,半数集不为空
for (int i = 1; i <= lastAdd / 2; ++i) {
cnt += halfSet(i);
}
// 存储在memo中以供后续使用
memo[lastAdd] = cnt;
return cnt;
}
int main()
{
int n;
cin >> n;
// 将memo的大小调整为n + 1,用0填充
memo.resize(n + 1, 0);
cout << halfSet(n);
}