分治与递归的艺术:从乘法到幂集的思维跃迁

在算法的宇宙中,递归与分治如同量子纠缠——看似独立的问题,背后藏着相同的思维骨架。今日,我们将揭开两道经典题目(递归乘法 vs 幂集)的神秘面纱,看它们如何用‘分而治之’的哲学,一个实现高效运算,一个穷尽无限组合。无需代码,只需逻辑,带你体验算法设计的极致美感。

问题一:递归乘法——算术的“细胞分裂”
递归乘法。 写一个递归函数,不使用 * 运算符, 实现两个正整数的相乘。可以使用加号、减号、位移,但要吝啬一些。

题目本质
在禁止使用乘法运算符(*)的约束下,仅通过加法、减法和位移,实现两正整数相乘。核心挑战是用时间复杂度逼近传统乘法($O(1)$),而非暴力累加($O(n)$)。

算法策略:二进制分解与分治

  1. 分治思想:将乘法 $A×B$ 分解为更小的子问题:

    • 若 $B$ 为偶数:$A×B = (A × \frac{B}{2}) × 2$ → 转化为 $A$ 左移(<< 1

    • 若 $B$ 为奇数:$A×B = A × (B-1) + A$ → 转化为加法与子问题

  2. 二进制优化

    • 将 $B$ 视为二进制数(如 $5 = 101_2$),则 $A×B = A×\sum_{i}2^{i} = \sum_{i} (A << i)$

    • 通过右移 $B$ 逐位判断(B & 1),累加左移后的 $A$,时间复杂度 $O(\log B)$。

示例解析

  • $A=3, B=4$($4=100_2$):
    $3<<2 = 12$(仅需1次位移,优于 $3+3+3+3$ 的3次加法)。

  • $A=1, B=10$($10=1010_2$):
    $1<<3 + 1<<1 = 8+2=10$(2次位移+1次加法)。

关键洞见:将线性累加转化为对数级操作,本质是利用二进制表达的分治特性

题目程序:

#include <stdio.h>  // 包含标准输入输出头文件,提供printf和scanf等函数

/**
 * 递归乘法函数
 * @param A 被乘数
 * @param B 乘数
 * @return A和B的乘积
 */
int multiply(int A, int B) {
    // 基础情况1:乘数为0时乘积为0(乘法零律)
    if (B == 0) {
        return 0;
    }
    // 基础情况2:乘数为1时乘积为被乘数(乘法单位元)
    if (B == 1) {
        return A;
    }
    
    // 递归情况1:乘数为偶数时
    if (B % 2 == 0) {  // 通过模运算判断奇偶性
        // 分治策略:A*B = (A*(B/2)) * 2
        int half = multiply(A, B >> 1);  // 递归计算子问题:B右移1位实现除2
        return half << 1;  // 结果左移1位实现乘以2
    } 
    // 递归情况2:乘数为奇数时
    else {
        // 分治策略:A*B = A*(B-1) + A
        return multiply(A, B - 1) + A;  // 递归计算A*(B-1)后加A
    }
}

int main() {
    int num1, num2;  // 存储用户输入的两个整数
    printf("请输入两个正整数(空格分隔):");  // 提示输入
    scanf("%d %d", &num1, &num2);  // 读取用户输入的值
    
    // 输入验证:确保两个数均为正整数
    if (num1 <= 0 || num2 <= 0) {
        printf("错误:必须输入正整数!\n");  // 错误提示
        return 1;  // 返回非零值表示程序异常终止
    }
    
    int result = multiply(num1, num2);  // 调用递归乘法函数计算乘积
    printf("乘积:%d\n", result);  // 输出计算结果
    return 0;  // 程序正常终止
}

输出结果:


问题二:幂集——集合的“生命游戏"
幂集。编写一种方法,返回某集合的所有子集。集合中 不包含重复的元素。

说明:解集不能包含重复的子集。

题目本质
生成不含重复元素的集合的所有子集(如 $[1,2,3]$ 的幂集含 $2^3=8$ 个子集)。核心挑战是避免重复且高效枚举指数级组合

算法策略:回溯与位映射

  1. 分治思想:对元素 $x_i$,划分两个平行宇宙:

    • 包含 $x_i$ 的子集:将 $x_i$ 加入所有现有子集

    • 不包含 $x_i$ 的子集:保留现有子集

    text

    幂集([1,2,3]) = {[1] + 幂集([2,3])} ∪ {幂集([2,3])}  
  2. 位运算优化

    • 用 $n$ 位二进制数表示子集($1$ 选/$0$ 不选),如 $010_2 → [2]$。

    • 遍历 $0$ 到 $2^n-1$,根据位模式构造子集,时间复杂度 $O(n·2^n)$。

示例解析

  • $nums=[1,2,3]$:
    $0→000_2→[]$,$1→001_2→[3]$,$2→010_2→[2]$,$3→011_2→[2,3]$,...,$7→111_2→[1,2,3]$。

关键洞见:幂集是组合数学中的分形结构——每个子集都是原问题的微观镜像。

题目程序:

#include <stdio.h>   // 标准输入输出库
#include <stdlib.h>  // 内存分配和释放库

/**
 * 生成集合的所有子集(幂集)
 * @param nums 输入集合数组
 * @param numsSize 集合元素数量
 * @param returnSize 返回子集总数
 * @param returnColumnSizes 返回每个子集的大小数组
 * @return 二维数组指针,包含所有子集
 */
int** subsets(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
    // 计算子集总数:2的numsSize次方
    *returnSize = 1 << numsSize;  // 左移操作实现2的幂次计算
    
    // 分配子集大小记录数组内存
    *returnColumnSizes = (int*)malloc(*returnSize * sizeof(int));
    
    // 分配子集结果数组内存(二维数组)
    int** result = (int**)malloc(*returnSize * sizeof(int*));
    
    // 遍历所有可能的子集(0 到 2^numsSize - 1)
    for (int i = 0; i < *returnSize; i++) {
        // 计算当前子集大小(二进制中1的个数)
        int count = 0;
        for (int j = 0; j < numsSize; j++) {
            if (i & (1 << j)) {  // 检查第j位是否为1
                count++;  // 统计1的个数
            }
        }
        
        // 记录当前子集大小
        (*returnColumnSizes)[i] = count;
        
        // 分配当前子集内存
        result[i] = (int*)malloc(count * sizeof(int));
        
        // 填充当前子集元素
        int index = 0;
        for (int j = 0; j < numsSize; j++) {
            if (i & (1 << j)) {  // 检查第j位是否为1
                // 若为1,将对应元素加入子集
                result[i][index++] = nums[j];
            }
        }
    }
    
    return result;  // 返回所有子集
}

/**
 * 打印所有子集
 * @param result 子集二维数组
 * @param returnSize 子集总数
 * @param returnColumnSizes 每个子集的大小数组
 */
void printSubsets(int** result, int returnSize, int* returnColumnSizes) {
    printf("[");  // 开始输出
    for (int i = 0; i < returnSize; i++) {
        printf("\n  [");  // 子集开始
        for (int j = 0; j < returnColumnSizes[i]; j++) {
            printf("%d", result[i][j]);  // 输出元素
            if (j < returnColumnSizes[i] - 1) {
                printf(", ");  // 元素分隔符
            }
        }
        printf("]");  // 子集结束
        if (i < returnSize - 1) {
            printf(",");  // 子集分隔符
        }
    }
    printf("\n]\n");  // 结束输出
}

int main() {
    // 示例集合 [1, 2, 3]
    int nums[] = {1, 2, 3};
    int numsSize = sizeof(nums) / sizeof(nums[0]);  // 计算集合大小
    
    int returnSize;  // 子集总数
    int* returnColumnSizes;  // 子集大小数组
    
    // 生成所有子集
    int** result = subsets(nums, numsSize, &returnSize, &returnColumnSizes);
    
    // 打印结果
    printf("集合: [1, 2, 3]\n");
    printf("幂集(所有子集):\n");
    printSubsets(result, returnSize, returnColumnSizes);
    
    // 释放内存
    for (int i = 0; i < returnSize; i++) {
        free(result[i]);  // 释放每个子集
    }
    free(result);  // 释放子集数组
    free(returnColumnSizes);  // 释放子集大小数组
    
    return 0;  // 程序正常退出
}

输出结果:

 


对比分析:分治的两种面孔
维度递归乘法幂集
问题类型算术运算(数值计算)组合枚举(集合操作)
分治核心二进制分解(乘数 $B$)元素选择/排除(集合元素 $x_i$)
时间复杂度$O(\log B)$(位移次数)$O(n·2^n)$(子集数量×元素数)
空间复杂度$O(1)$(迭代)或 $O(\log B)$(递归栈)$O(n·2^n)$(存储所有子集)
操作约束仅用加/减/位移无重复子集
分治粒度比特级(位运算)元素级(集合操作)

对比图示

分治策略对比树  
│  
├── 递归乘法:垂直分解(按比特位)  
│   ├── 位0:选择是否累加 (A<<0)  
│   ├── 位1:选择是否累加 (A<<1)  
│   └── ...  
│  
└── 幂集:水平分解(按元素)  
    ├── 元素1:选择/不选 → 分支1  
    ├── 元素2:选择/不选 → 分支2  
    └── ...  

灵魂总结:分治的本质与哲学
  1. 统一性

    • 递归乘法通过二进制分治将线性问题对数化;

    • 幂集通过决策树分治将组合问题结构化。

    • 二者皆将问题分解为独立子问题(位决策/元素决策),合并子解得最终解。

  2. 差异本源

    • 乘法:子问题间无依赖(位独立),可迭代实现。

    • 幂集:子问题叠加(后序元素依赖前序选择),需回溯/递归保存状态。

  3. 思维升华

    "分治是算法师的望远镜——将庞然大物拆解为星辰;递归是显微镜——在每一粒星辰中窥见宇宙全貌。乘法与幂集,一个在数轴上跳跃,一个在集合中繁衍,却共享同一套分治基因。"

递归与分治,是程序员手中的双刃剑:

  • 斩开递归乘法的数值枷锁,它化作位移的闪电

  • 劈开幂集的组合迷雾,它变为决策的星河
    无论面对算术的严谨还是组合的混沌,记住:分解的勇气与合并的智慧,才是算法之美的根源

明日预告:《动态规划:从斐波那契到星际航行的最优路径》—— 看如何用“记忆化”跨越时空!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

司铭鸿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值