----------当数论遇上DP,算法世界里的魔法与几何奇迹!
想象一下,你手里有一个数字 `n`,现在你要做两件神奇的事情:
1️⃣ 把它拆成若干个完美数(比如6、28、496…),要求拆得越少越好——就像用最少的魔法宝石拼出完整的能量阵!
2️⃣ 把它变成一座数字金字塔,每一层的数字像滚雪球一样增长,最终计算第 `n` 层的数字之和——仿佛在数学宇宙里搭建通天塔!
今天,我们就来探索这两道融合“数论+动态规划/递推”的奇妙题目,看看数学如何与算法碰撞出智慧的火花!🔥
🔍 题目一:完美数分割
问题描述
给定正整数 `n`,将其拆分成若干个“完美数”(即等于其真因数之和的数,如6=1+2+3)之和,求最少需要多少个完美数**。若无法拆分,返回 `-1`。
🧠 算法分析
1. 完美数有哪些?
- 已知的完美数并不多(6, 28, 496, 8128…),在 `n ≤ 1e5` 范围内,可用的完美数很少。
2. 动态规划思路
- 定义 `dp[i]` 表示数字 `i` 最少需要多少个完美数组成。
- 初始化 `dp[0] = 0`,其余 `dp[i] = ∞`(表示不可达)。
- 遍历所有完美数 `p`,更新 `dp[i] = min(dp[i], dp[i - p] + 1)`。
3. 关键点
- 由于完美数很少,可以预处理所有可能的 `p`,再跑完全背包DP。
- 若 `dp[n]` 仍为 `∞`,说明无法拆分,返回 `-1`。
💡 思考
这道题的关键在于“完美数的稀疏性”,使得DP的计算量大幅降低。
如果 `n` 很大(比如 `1e18`),可能需要数学方法优化,但本题 `n ≤ 1e5`,DP足够高效。
#include <stdio.h> // 标准输入输出库,用于printf和scanf
#include <stdlib.h> // 标准库,用于动态内存分配(malloc和free)
#include <limits.h> // 定义整数类型的大小限制(如INT_MAX)
int main() {
int n; // 定义变量n,用于存储用户输入的正整数
printf("Enter a positive integer: "); // 提示用户输入一个正整数
scanf("%d", &n); // 读取用户输入的正整数,并存入变量n中
// 预定义的完美数列表(在1e5以内的),已知的完美数较少
int perfect_numbers[] = {6, 28, 496, 8128}; // 完美数数组
int p_count = 4; // 完美数的个数,即数组长度
// 检查是否存在可用的完美数(小于等于n),避免无效计算
int has_valid = 0; // 标记是否存在可用的完美数,初始为0(不存在)
for (int i = 0; i < p_count; i++) { // 遍历所有完美数
if (perfect_numbers[i] <= n) { // 如果当前完美数 <= n
has_valid = 1; // 标记为存在可用完美数
break; // 找到即退出循环
}
}
if (!has_valid) { // 如果没有可用的完美数(即所有完美数都 > n)
printf("This number cannot be split!.\n");
printf("-1\n"); // 直接输出-1,表示无法拆分
return 0; // 程序结束
}
// 动态规划数组初始化(0-1背包问题)
int *dp = (int *)malloc((n + 1) * sizeof(int)); // 分配动态数组dp,大小为n+1
for (int i = 0; i <= n; i++) { // 初始化dp数组
dp[i] = INT_MAX; // 初始值为INT_MAX(表示不可达)
}
dp[0] = 0; // dp[0]初始化为0,表示和为0时不需要任何完美数
// 0-1背包动态规划处理每个完美数
for (int k = 0; k < p_count; k++) { // 遍历所有完美数
int p = perfect_numbers[k]; // 当前完美数
if (p > n) continue; // 如果当前完美数 > n,跳过
for (int i = n; i >= p; i--) { // 反向遍历,确保每个数只选一次(0-1背包)
if (dp[i - p] != INT_MAX && dp[i] > dp[i - p] + 1) { // 如果i-p可达且更优
dp[i] = dp[i - p] + 1; // 更新dp[i]为更小的值
}
}
}
// 输出结果
if (dp[n] != INT_MAX) { // 如果dp[n]不是初始值(即可达)
printf("The minimum number of perfect numbers required for output: %d\n", dp[n]); // 输出最少需要的完美数个数
} else { // 否则(即无法拆分)
printf("This number cannot be split!.\n");
printf("-1\n"); // 输出-1
}
free(dp); // 释放动态分配的dp数组内存
return 0; // 程序正常结束
}
输出结果:
🔍 题目二:数字金字塔
📜 问题描述
给定 `n`,构建数字金字塔:
- 第 `i` 层有 `i` 个数字,每个数字 = 左上方数字 + 右上方数字(若不存在则视为0)。
- 求第 `n` 层的所有数字之和。
第1层:1
第2层:1 1
第3层:1 2 1
第4层:1 3 3 1
...
```
(没错,这其实就是“杨辉三角”的变种!)
🧠 算法分析
1. 观察规律
每一层的数字实际上是**组合数**(二项式系数),即 `C(i-1, k)`。
第 `n` 层的数字和 = `2^(n-1)`(因为每一行的和是前一行和的2倍)。
2. 递推计算
直接计算 `2^(n-1)` 即可,但要注意 `n` 可能很大(如 `n=1000`,`2^999` 是个超大数)。
如果题目要求取模,可以用快速幂优化;若不取模,可能需要大数计算。
#include <stdio.h> // 标准输入输出头文件,提供printf和scanf等函数
#include <stdlib.h> // 标准库头文件,提供malloc和free等内存管理函数
// 函数声明
void printPascalTriangle(int n); // 声明打印杨辉三角的函数
unsigned long long pyramidSum(int n); // 声明计算第n层和的函数
int main() {
int n; // 定义整型变量n,用于存储用户输入的层数
printf("Enter the number of layers (n): "); // 提示用户输入层数
scanf("%d", &n); // 读取用户输入的整数并存储到n中
if (n <= 0) { // 检查输入的合法性,n必须为正整数
printf("Invalid input! n must be positive.\n"); // 非法输入提示
return 1; // 非正常退出程序
}
printf("\nDigital Pyramid:\n"); // 打印金字塔标题
printPascalTriangle(n); // 调用函数打印杨辉三角
unsigned long long sum = pyramidSum(n); // 调用函数计算第n层和
printf("\nSum of numbers in layer %d: %llu\n", n, sum); // 打印计算结果
return 0; // 程序正常退出
}
// 打印杨辉三角(数字金字塔)的函数定义
void printPascalTriangle(int n) {
// 动态分配二维数组:创建n行的指针数组
unsigned long long **triangle = (unsigned long long **)malloc(n * sizeof(unsigned long long *));
// 为每一行分配内存:第i行有i+1个元素
for (int i = 0; i < n; i++) {
triangle[i] = (unsigned long long *)malloc((i + 1) * sizeof(unsigned long long));
}
// 初始化第一层
triangle[0][0] = 1; // 杨辉三角顶端元素为1
// 填充剩余层
for (int i = 1; i < n; i++) { // 从第二层开始填充
triangle[i][0] = 1; // 每层第一个数字总是1
triangle[i][i] = 1; // 每层最后一个数字总是1
// 计算中间数字:每个数字等于上方两个数字之和
for (int j = 1; j < i; j++) {
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
}
}
// 打印金字塔
for (int i = 0; i < n; i++) { // 遍历每一层
// 打印前导空格(实现金字塔的居中效果)
for (int space = 0; space < n - i - 1; space++) {
printf(" "); // 每层前打印2*(n-i-1)个空格
}
// 打印当前层数字
for (int j = 0; j <= i; j++) {
printf("%4llu", triangle[i][j]); // 每个数字占4位宽度
}
printf("\n"); // 每层打印完后换行
}
// 释放内存:先释放每一行的内存,再释放指针数组
for (int i = 0; i < n; i++) {
free(triangle[i]); // 释放第i行的内存
}
free(triangle); // 释放指针数组的内存
}
// 计算第n层数字和的函数定义
unsigned long long pyramidSum(int n) {
// 第n层和等于2的(n-1)次方
// 使用位运算快速计算:1左移(n-1)位
return 1ULL << (n - 1); // ULL表示unsigned long long类型
}
输出结果:
💡 思考
这道题的关键在于**发现杨辉三角的规律**,避免暴力计算。
如果题目改成**求第 `n` 层第 `k` 个数字**,可以直接用组合数公式 `C(n-1, k-1)` 计算。
题目 | 核心算法 | 数学依赖 | 计算复杂度 | 关键技巧 |
---|---|---|---|---|
完美数分割 | 动态规划(完全背包) | 完美数的性质 | O(n * k),k是完美数个数 | 预处理完美数,DP优化 |
数字金字塔 | 数学递推(组合数) | 杨辉三角规律 | O(1) 或 O(log n)(快速幂) | 观察规律,避免暴力 |
🎯 总结
- 完美数分割 考察的是“动态规划+数论”的结合,关键在于如何利用完美数的稀疏性优化DP。
- 数字金字塔:则更偏向“数学推导”,发现规律后可以瞬间降维打击。
- 共同点:两道题都展示了数学如何优化算法,避免暴力计算!
数学与算法的结合,就像魔法与科技的交融——“完美数分割“让我们学会如何用最少的‘数字宝石’拼出目标,而“数字金字塔”则揭示了数字背后的几何之美。
如果你喜欢这种数学+DP的奇妙组合,不妨挑战:
如果完美数可以重复使用,如何优化DP?
如果金字塔的递推规则改变(如乘法而非加法),和会如何变化?*
欢迎在评论区分享你的思考!🚀 下期我们将探索更疯狂的“数论+图论”题目,敬请期待!"
✨ 互动彩蛋
"你能找到比6、28、496更大的完美数吗?在评论区写下你的发现!🔍"