GESP 拆分题------从暴力到数学的三层优化之路
目录
-
题目背景与原题呈现
-
问题抽象与核心分析
-
解法一:暴力深搜(DFS + 回溯)
-
解法二:动态规划(DP)优化
-
解法三:数学规律最优解
-
三种解法对比与总结
-
完整代码与提交说明
一、题目背景与原题呈现
这是一道 GESP 七级算法真题,核心考察「整数拆分求最大乘积」问题,也是竞赛中经典的优化类题目。
原题描述
小 A 想将正整数 n 拆分成若干个正整数之和,并最大化拆分后的正整数之积。你需要求出拆分后正整数之积的最大值对 10^9 取模的结果。
形式化地说,n的拆分是满足 $$a_1 + a_2 + ... + a_k = n 的若干个正整数 a_1,a_2,...,a_k(其中 1 < k < n),你需要求所有拆分中 a_1 * a_2 * ... *a_k 的最大值对 10^9 取模的结果。
输入输出
-
输入:第一行一个正整数t(数据组数);接下来 t行,每行一个正整数 n。
-
输出:每组数据输出一行,为拆分后乘积最大值对 10^9 取模的结果。
样例
|-----------|----------------|
| 输入 | 输出 |
| 3 5 8 100 | 6 18 755407364 |
二、问题抽象与核心分析
问题抽象
给定正整数 n,将其拆分为若干正整数之和 a_1+a_2+...+a_k=n,求 a_1×a_2×...×a_k 的最大值(结果对 10^9 取模)。
核心矛盾
-
直接枚举所有拆分组合(暴力法)会随着 n 增大,组合数指数级增长,无法通过大数据用例;
-
直接找数学规律需要一定的数学推导能力,容易出错;
-
动态规划作为中间优化方案,兼顾了直观性和效率,是从暴力到最优解的过渡桥梁。
三、解法一:暴力深搜(DFS + 回溯)
思路分析
这是拿到题目时的「第一反应」:既然要找所有拆分组合,那直接用深搜 + 回溯枚举所有合法拆分,计算每个组合的乘积,记录最大值即可。
关键实现细节
-
去重优化:通过限制拆分数字非递减(下一个数≥上一个数),避免重复枚举同一拆分的不同排列(如 2+3 和 3+2 视为同一组合);
-
取模处理:每次乘积计算后对 10^9 取模,避免数值溢出;
-
剪枝优化:剩余和即使全拆为 1 也无法凑够 n 时,直接回溯。
完整代码
cpp
#include <iostream>
#include <algorithm>
using namespace std;
const int MOD = 1000000000;
long long maxP;
int n;
void dfs(int sum, long long mul, int last) {
if (sum == n) {
maxP = max(maxP, mul);
return;
}
for (int i = last; i <= n - sum; ++i) {
dfs(sum + i, (mul * i) % MOD, i);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
cin >> n;
maxP = 0;
dfs(0, 1, 1);
cout << maxP % MOD << "\n";
}
return 0;
}
优缺点与得分情况
-
✅ 优点:思路直接,容易理解,无需复杂数学推导;
-
❌ 缺点:时间复杂度为指数级,仅能通过 n≤20 左右的小数据,n=100 时会直接超时;
-
得分情况:仅能通过前 2~3 个小数据点,得分率极低。
四、解法二:动态规划(DP)优化
思路分析
暴力法超时的核心问题是重复计算了大量子问题,因此我们可以用动态规划预处理每个数的最优解,将时间复杂度从指数级降到线性级。
状态定义
-
f[i]:将 i 拆分为若干正整数之和时,乘积的最大值(对 10^9 取模);
-
lnf[i]:f[i] 的自然对数,用于比较乘积大小(避免直接计算大数溢出)。
状态转移方程
-
对于每个 i,最优拆分只有两种可能:拆出一个 2,或拆出一个 3(推导见下文);
-
转移:
-
拆 2:lnf[i+2] = lnf[i] + ln2,f[i+2] = (2×f[i])%mod;
-
拆 3:lnf[i+3] = lnf[i] + ln3,f[i+3] = (3×f[i])%mod;
-
-
每次选择对数更大的方案(对数单调递增,对数越大乘积越大)。
完整代码
cpp
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 1e6 + 5;
const int mod = 1e9;
const double ln2 = log(2);
const double ln3 = log(3);
int f[N];
double lnf[N];
int main() {
f[0] = f[1] = 1;
for (int i = 0; i < N; ++i) {
if (i + 2 < N && lnf[i] + ln2 > lnf[i + 2]) {
lnf[i + 2] = lnf[i] + ln2;
f[i + 2] = 2LL * f[i] % mod;
}
if (i + 3 < N && lnf[i] + ln3 > lnf[i + 3]) {
lnf[i + 3] = lnf[i] + ln3;
f[i + 3] = 3LL * f[i] % mod;
}
}
int t;
scanf("%d", &t);
while (t--) {
int n;
scanf("%d", &n);
printf("%d\n", f[n]);
}
return 0;
}
优缺点与得分情况
-
✅ 优点:时间复杂度 O(n),可处理 n≤10^6 的数据,比暴力法效率大幅提升;
-
❌ 缺点:需要预处理数组,空间复杂度 O(n),且依赖「拆 2 或拆 3」的隐含规律;
-
得分情况:可通过所有 n≤10^6 的测试用例,在本题中可拿满分。
五、解法三:数学规律最优解
核心规律推导
通过数学分析,我们可以得出「整数拆分求最大乘积」的黄金法则:
-
优先拆成 3:3 的乘积效率最高(3*3>2*2*2 ,3+3=6 乘积为 9,2+2+2=6 乘积为 8);
-
余数处理:
-
若 n%3=0:全拆为 3,乘积为 3^{n/3};
-
若 n%3=1:拆为两个 2 和若干 3(3+1=2+2,2×2>3×1),乘积为 4×3^{(n-4)/3};
-
若 n%3=2:拆为一个 2 和若干 3,乘积为 2×3^{n/3};
-
-
特殊情况:n=1 乘积为 1,n=2 乘积为 2,n=3 乘积为 3。
关键实现细节
-
快速幂:用快速幂计算 3^k%mod,时间复杂度 O(log k),避免循环乘法超时;
-
取模处理:每次乘法后对 10^9 取模,防止溢出。
完整代码
cpp
#include <iostream>
using namespace std;
const int MOD = 1000000000;
long long qpow(long long a, long long b) {
long long res = 1;
while (b > 0) {
if (b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
if (n == 1) cout << "1\n";
else if (n == 2) cout << "2\n";
else if (n == 3) cout << "3\n";
else {
long long ans;
if (n % 3 == 0) ans = qpow(3, n / 3);
else if (n % 3 == 1) ans = qpow(3, (n - 4) / 3) * 4 % MOD;
else ans = qpow(3, n / 3) * 2 % MOD;
cout << ans << "\n";
}
}
return 0;
}
优缺点与得分情况
-
✅ 优点:时间复杂度 O(log n),空间复杂度 O(1),可处理 n≤10^{18} 的数据,效率拉满;
-
❌ 缺点:需要推导数学规律,对数学能力有一定要求;
-
得分情况:可通过所有测试用例,是本题的最优解。
六、三种解法对比与总结
|------|-------------|------------|------------|--------------|-------------|
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 | 优点 | 缺点 |
| 暴力深搜 | 指数级 O(2^n) | O(n) (递归栈) | n≤20 | 思路直观,无需推导 | 大数据超时,得分率低 |
| 动态规划 | O(n) | O(n) | n≤10^6 | 线性时间,可通过本题数据 | 需预处理,依赖隐含规律 |
| 数学规律 | O(log n) | O(1) | n≤10^{18} | 效率最高,无额外空间 | 需推导数学规律 |
核心优化思路
这道题的优化路径,正是算法竞赛中常见的「暴力→优化→最优解」的经典路线:
-
暴力法是「起点」,帮助我们理解问题的本质;
-
动态规划是「过渡」,通过预处理子问题,将指数级复杂度降到线性级;
-
数学规律是「终点」,通过数学推导找到最优解,将复杂度降到对数级。
七、完整代码与提交说明
提交建议
-
本题在竞赛场景下,优先使用数学规律解法,效率最高且无额外空间消耗;
-
若对数学推导不熟悉,可使用动态规划解法,在本题中同样可拿满分;
-
暴力法仅适合理解问题,不建议提交。