2-3 半数集问题(算法设计与分析)

一开始的思路

从条件(1)可知,所有的元素都是从set(n)集合中得到的

从条件(2)可知,设最近添加的数为lastAdd,则新添加的数的范围为 [1,\ lastAdd\ /\ 2],其中除法下取整(不超过一半)

从条件(3)推测,该规则要一直进行下去,可以使用递归操作

第一个问题:如何添加数?

第一个问题:“在 n 的左边添加一个自然数 i”,怎么将这句话表示成式子?

举一个例子,n = 26,\ i=1,得到 n^\prime=126=i\times100+n=i\times 10^2+n

可以归纳:


n^\prime=i\times 10^{\rm digit(n)}+n,\quad {\rm digit(n)}: 取n的位数
 

第二个问题:如何将这个规则一直进行下去?如何退出(终止条件)?

我们已经知道新添加的数的范围为 [1,\ lastAdd\ /\ 2],相当于知道了所有可以新添加的选择

对于每一种选择,我们按照上面的公式算出新的数然后添加到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);
}

改进

运行上面的代码时发现,一旦数据量变大,如达到 10^3的量级时,程序运行很慢,这涉及到了递归时的重复子问题。

造成重复的原因在于,如果我们之前算出了 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);
}

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值