今日来整递归与阶乘,开始
目一:阶乘之和的阶乘表示判定
算法分析:
-
问题核心:
给定一组正整数,计算其阶乘之和是否为另一个数的阶乘。例如,判断是否存在整数 mm 使得 ∑iai!=m!∑iai!=m!。 -
涉及算法:
-
阶乘计算与累加:需计算每个输入数的阶乘并求和。
-
阶乘查找匹配:在有限的阶乘值范围内查找是否存在与和相等的阶乘。
-
数学优化:利用阶乘的快速增长特性缩小搜索范围。
-
-
关键步骤:
-
计算阶乘和:遍历输入数组,计算每个数的阶乘并累加总和 SS。
-
预生成阶乘表:预先计算可能的 m!m!(如 m≤20m≤20,因 20!≈2.43×101820!≈2.43×1018 接近64位整数上限)。
-
二分查找匹配:在预生成的阶乘表中查找是否存在 m!=Sm!=S。
-
-
复杂度分析:
-
时间复杂度:O(n+k)O(n+k),其中 nn 为输入数组长度,kk 为预生成阶乘表长度(通常 k≤20k≤20)。
-
空间复杂度:O(k)O(k),存储预生成阶乘表。
-
-
数学验证:
-
阶乘函数 m!m! 增长极快,实际可能匹配的 mm 范围很小。
-
例:若 S=10S=10,查表可知 3!=63!=6、4!=244!=24,无解;若 S=120S=120,则 5!=1205!=120,存在解。
-
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
/* 常量定义 */
#define MAX_FACTORIAL_INDEX 20 ///< 最大支持的阶乘数索引(对应20!)
#define MAX_INPUT_NUMBERS 100 ///< 单次最大输入数字数量
/**
* @brief 预先生成的阶乘查询表
* @note 索引0~20对应0!~20!的值
* @note 20! = 2432902008176640000 (接近uint64_t最大值)
*/
const uint64_t FACTORIAL_TABLE[MAX_FACTORIAL_INDEX + 1] = {
1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880,
3628800, 39916800, 479001600, 6227020800, 87178291200,
1307674368000, 20922789888000, 355687428096000,
6402373705728000, 121645100408832000, 2432902008176640000
};
/**
* @brief 计算单个数字的阶乘(查表法)
* @param num 输入数字(必须满足 0 <= num <= 20)
* @param[out] result 阶乘计算结果指针
* @return true 计算成功
* @return false 输入非法(输出结果不可信)
* @warning 输入超过20将导致整数溢出
*/
bool calculate_factorial(uint8_t num, uint64_t* result) {
// 输入参数有效性验证
if (num > MAX_FACTORIAL_INDEX) {
fprintf(stderr, "[错误] 输入数字%d超过最大支持值%d\n", num, MAX_FACTORIAL_INDEX);
return false;
}
*result = FACTORIAL_TABLE[num];
return true;
}
/**
* @brief 验证数值是否为某个整数的阶乘
* @param sum 待验证的数值
* @param[out] m 匹配到的阶乘数(仅在返回true时有效)
* @return true 存在满足 sum = m! 的整数m
* @note 特殊处理0!和1!的情况
*/
bool is_factorial_number(uint64_t sum, uint8_t* m) {
// 处理边界情况(0!和1!都等于1)
if (sum == 0 || sum == 1) {
*m = (sum == 0) ? 0 : 1;
return true;
}
// 遍历阶乘表查找匹配项(从2!开始)
for (uint8_t i = 2; i <= MAX_FACTORIAL_INDEX; ++i) {
if (FACTORIAL_TABLE[i] == sum) {
*m = i;
return true;
}
}
return false;
}
/**
* @brief 核心验证流程:计算输入数字的阶乘和并进行验证
* @param numbers 输入数字数组(元素范围0-20)
* @param count 数组有效长度
* @note 包含溢出保护机制
*/
void verify_factorial_sum(const uint8_t* numbers, size_t count) {
uint64_t total_sum = 0; ///< 累计阶乘和
// 逐项计算阶乘并累加
for (size_t i = 0; i < count; ++i) {
uint64_t fact = 0;
if (!calculate_factorial(numbers[i], &fact)) {
return; // 遇到错误直接终止
}
// 溢出保护检查(避免无符号整数回绕)
if (total_sum > UINT64_MAX - fact) {
fprintf(stderr, "[错误] 阶乘和超过64位无符号整数范围\n");
return;
}
total_sum += fact;
}
// 验证结果并输出
uint8_t m = 0;
if (is_factorial_number(total_sum, &m)) {
printf("√ 验证通过:sum = %lu = %u!\n", total_sum, m);
} else {
printf("× 验证未通过:sum = %lu 不是任何整数的阶乘\n", total_sum);
}
}
/* 输入处理模块 */
/**
* @brief 解析命令行参数为数字数组
* @param argc 参数总数
* @param argv 参数值数组
* @param[out] numbers 输出数字缓冲区
* @param[out] count 有效数字数量
* @return true 解析成功
* @note 输入格式示例:./program 2 3 5
*/
bool parse_command_line(int argc, char *argv[], uint8_t *numbers, size_t *count) {
if (argc < 2) {
fprintf(stderr, "用法: %s <数字1> <数字2> ... <数字N>\n", argv[0]);
fprintf(stderr, "示例: %s 2 3 4\n", argv[0]);
return false;
}
*count = 0;
// 遍历所有参数(跳过程序名)
for (int i = 1; i < argc && *count < MAX_INPUT_NUMBERS; i++) {
char *endptr;
long num = strtol(argv[i], &endptr, 10); // 十进制转换
// 验证转换完整性及范围有效性
if (*endptr != '\0' || num < 0 || num > MAX_FACTORIAL_INDEX) {
fprintf(stderr, "[错误] 无效输入: %s (必须是0-20的整数)\n", argv[i]);
return false;
}
numbers[(*count)++] = (uint8_t)num;
}
return true;
}
/**
* @brief 从标准输入读取并解析数字
* @param[out] numbers 输出数字缓冲区
* @param[out] count 有效数字数量
* @return true 读取到有效输入
* @note 支持空格分隔的数字输入,例如:"2 3 5 7"
*/
bool read_from_stdin(uint8_t *numbers, size_t *count) {
printf("请输入要计算的数字(0-20),用空格分隔(最多%d个),按回车结束:\n", MAX_INPUT_NUMBERS);
char input[1024]; // 输入缓冲区
if (fgets(input, sizeof(input), stdin) == NULL) {
return false;
}
// 使用strtok分割输入字符串
char *token = strtok(input, " \t\n"); // 支持空格/TAB分隔
*count = 0;
while (token != NULL && *count < MAX_INPUT_NUMBERS) {
char *endptr;
long num = strtol(token, &endptr, 10);
// 忽略无效输入项
if (*endptr != '\0' || num < 0 || num > MAX_FACTORIAL_INDEX) {
fprintf(stderr, "[错误] 忽略无效输入: %s (必须是0-20的整数)\n", token);
} else {
numbers[(*count)++] = (uint8_t)num;
}
token = strtok(NULL, " \t\n");
}
return (*count > 0); // 至少需要1个有效输入
}
/**
* @brief 程序主入口
* @param argc 命令行参数个数
* @param argv 命令行参数数组
* @note 支持两种输入模式:
* 1. 命令行参数模式:./program 2 3 5
* 2. 交互式输入模式:运行后按提示输入
*/
int main(int argc, char *argv[]) {
uint8_t numbers[MAX_INPUT_NUMBERS]; // 输入数字缓冲区
size_t count = 0; // 有效数字数量
// 输入模式选择逻辑
if (argc > 1) {
// 模式1:命令行参数输入
if (!parse_command_line(argc, argv, numbers, &count)) {
return EXIT_FAILURE;
}
} else {
// 模式2:标准输入交互模式
if (!read_from_stdin(numbers, &count)) {
fprintf(stderr, "未提供有效输入\n");
fprintf(stderr, "用法: %s <数字1> <数字2> ... <数字N>\n", argv[0]);
fprintf(stderr, "或直接运行程序并按照提示输入数字\n");
return EXIT_FAILURE;
}
}
// 执行核心验证流程
verify_factorial_sum(numbers, count);
return EXIT_SUCCESS;
}
结果如下:
题目二:三柱汉诺塔问题
算法分析:
-
问题核心:
将 NN 个圆盘从起始柱移动到目标柱,每次只能移动一个圆盘,且大盘不能叠在小盘上。 -
涉及算法:
-
递归算法:经典解法基于分治思想。
-
迭代算法:可通过栈模拟递归过程。
-
最小步数计算:总步数为 2N−12N−1。
-
-
递归步骤:
-
将 N−1N−1 个盘子从起始柱通过目标柱移动到中间柱。
-
将第 NN 个盘子从起始柱直接移动到目标柱。
-
将 N−1N−1 个盘子从中间柱通过起始柱移动到目标柱。
-
-
复杂度分析:
-
时间复杂度:O(2N)O(2N),因每一步移动需递归分解。
-
空间复杂度:O(N)O(N),递归调用栈深度为 NN。
-
-
数学验证:
-
总移动次数公式:2N−12N−1,可通过归纳法证明。
-
例:N=3N=3 时,最少需 77 步。
-
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>
#include <ctype.h>
/* 配置常量 */
#define MAX_DISKS 20 // 安全阈值
#define MIN_DISKS 1
#define STEP_LOG_SIZE (1 << MAX_DISKS) // 精确计算所需空间
/* 移动步骤记录 */
typedef struct {
char from;
char to;
} Move;
Move step_log[STEP_LOG_SIZE];
int step_count = 0;
/**
* @brief 强化输入验证
* @param input 输入字符串
* @return 验证后的圆盘数量,-1表示无效
*/
int validate_input(const char* input) {
char* endptr;
long num = strtol(input, &endptr, 10);
/* 新增:检查空白输入 */
if (endptr == input) {
fprintf(stderr, "[错误] 未检测到有效输入\n");
return -1;
}
/* 检查:允许末尾空格但拒绝中间非数字字符 */
while (*endptr != '\0') {
if (!isspace((unsigned char)*endptr)) {
fprintf(stderr, "[错误] 包含非法字符'%c'\n", *endptr);
return -1;
}
endptr++;
}
if (num < MIN_DISKS || num > MAX_DISKS) {
fprintf(stderr, "[错误] 输入必须在%d-%d之间\n", MIN_DISKS, MAX_DISKS);
return -1;
}
return (int)num;
}
/**
* @brief 经典递归解法
*/
void hanoi(int n, char src, char tar, char aux) {
if (n == 1) {
step_log[step_count++] = (Move){src, tar};
return;
}
hanoi(n-1, src, aux, tar);
step_log[step_count++] = (Move){src, tar};
hanoi(n-1, aux, tar, src);
}
/**
* @brief 解决方案入口
*/
bool solve_hanoi(int n) {
/* 双重验证确保安全 */
if (n < MIN_DISKS || n > MAX_DISKS) {
fprintf(stderr, "[系统错误] 参数%d超出验证范围\n", n);
return false;
}
printf("\n【汉诺塔解决方案】\n");
printf("• 圆盘数量: %d\n• 最少需要步数: %d\n", n, (1 << n) - 1);
step_count = 0; // 重置计数器
hanoi(n, 'A', 'C', 'B');
return true;
}
/**
* @brief 智能输出控制
*/
void print_steps(bool verbose) {
if (step_count == 0) {
printf("未生成任何移动步骤\n");
return;
}
printf("共需%d步移动:\n", step_count);
if (verbose) {
for (int i = 0; i < step_count; ++i) {
printf("[%04d] %c → %c\n", i+1, step_log[i].from, step_log[i].to);
}
} else {
printf("(完整步骤已保存到hanoi_steps.txt)\n");
FILE* fp = fopen("hanoi_steps.txt", "w");
if (fp) {
for (int i = 0; i < step_count; ++i) {
fprintf(fp, "%04d: %c → %c\n", i+1, step_log[i].from, step_log[i].to);
}
fclose(fp);
} else {
perror("无法创建输出文件");
}
}
}
int main() {
char input[32];
printf("请输入圆盘数量(%d-%d): ", MIN_DISKS, MAX_DISKS);
if (!fgets(input, sizeof(input), stdin)) {
fprintf(stderr, "[错误] 读取输入失败\n");
return EXIT_FAILURE;
}
int n = validate_input(input);
if (n == -1) return EXIT_FAILURE;
if (!solve_hanoi(n)) return EXIT_FAILURE;
/* 自动判断输出模式:n<=5直接显示,n>5存文件 */
print_steps(n <= 5 || MAX_DISKS <= 5);
return EXIT_SUCCESS;
}
结果如下:
三,两题对比
对比维度 | 题目一(阶乘和) | 题目二(汉诺塔) | 相同点 |
---|---|---|---|
算法类型 | 数学计算 + 查找算法 | 递归/分治算法 | 均需循环或递归控制流程 |
核心操作 | 阶乘计算、二分查找 | 递归移动圆盘 | 依赖逻辑分解问题 |
数学模型 | 阶乘函数 m!m! 的单调性 | 递推公式 T(N)=2T(N−1)+1T(N)=2T(N−1)+1 | 均基于数学规律 |
关键变量 | 阶乘和 SS、预生成阶乘表 | 圆盘数量 NN、移动步数 | 需维护中间状态 |
边界条件 | S=0S=0 或 S=1S=1 时的特殊处理 | N=1N=1 时的直接移动 | 需处理最小规模问题 |
输入规模影响 | 阶乘和可能超出数值范围(需大数处理) | 圆盘数量 NN 过大会导致递归深度爆炸 | 大规模输入时均需优化 |
时间复杂度 | O(n+k)O(n+k)(线性) | O(2N)O(2N)(指数级) | 复杂度差异显著 |
实际应用场景 | 密码学(特殊数值模式识别) | 算法教学、递归思想训练 | 均体现基础算法的重要性 |
验证方法 | 查表验证、数学归纳法 | 数学归纳法、移动步骤模拟 | 需通过数学证明确保正确性 |
总结
-
题目一:
-
核心是数学计算与查找,需结合阶乘的单调性和数值范围特性优化查找效率。
-
关键挑战:处理大数溢出(如使用高精度计算库)。
-
-
题目二:
-
核心是递归思想的应用,通过分治策略将复杂问题分解为子问题。
-
关键挑战:理解递归调用栈的运作及避免栈溢出(对大规模 NN 需改用迭代)。
-
-
共同要求:
-
对问题数学本质的深刻理解
-
算法设计的逻辑严密性
-
边界条件与异常输入的鲁棒性处理
-