题解:洛谷 P1018 乘积最大
【题目来源】
【题目描述】
今年是国际数学联盟确定的“2000——世界数学年”,又恰逢我国著名数学家华罗庚先生诞辰 90 周年。在华罗庚先生的家乡江苏金坛,组织了一场别开生面的数学智力竞赛的活动,你的一个好朋友 XZ 也有幸得以参加。活动中,主持人给所有参加活动的选手出了这样一道题目:
设有一个长度为 \(N\) 的数字串,要求选手使用 \(K\) 个乘号将它分成 \(K+1\) 个部分,找出一种分法,使得这 \(K+1\) 个部分的乘积能够为最大。
同时,为了帮助选手能够正确理解题意,主持人还举了如下的一个例子:
有一个数字串:\(312\),当 \(N=3,K=1\) 时会有以下两种分法:
- \(3 \times 12=36\)
- \(31 \times 2=62\)
这时,符合题目要求的结果是:\(31 \times 2 = 62\)。
现在,请你帮助你的好朋友 XZ 设计一个程序,求得正确的答案。
【输入】
程序的输入共有两行:
第一行共有 \(2\) 个自然数 \(N,K\)。
第二行是一个长度为 \(N\) 的数字串。
【输出】
结果显示在屏幕上,相对于输入,应输出所求得的最大乘积(一个自然数)。
【输入样例】
4 2
1231
【输出样例】
62
【核心思想】
-
问题分析:给定一个长度为 \(N\) 的数字串,要求在其中插入 \(K\) 个乘号,将数字串分成 \(K+1\) 个部分,使得这些部分的乘积最大。数字串长度 \(N\) 可达 40,结果可能非常大,远超 64 位整数范围,因此必须使用高精度计算。本质是区间 DP + 高精度,通过枚举最后一个乘号的位置进行状态转移。
-
算法选择:
- 区间 DP + 高精度:设 \(dp[i][j]\) 表示前 \(i\) 个数字中插入 \(j\) 个乘号所能得到的最大乘积。预处理 \(num[l][r]\) 为数字串第 \(l\) 到 \(r\) 位组成的整数(高精度)。转移时枚举最后一个乘号的位置 \(p\)(\(j \le p < i\)),则有 \(dp[i][j] = \max(dp[p][j-1] \times num[p+1][i])\)。
- DFS 搜索(非最优):代码中还给出了 DFS 枚举所有乘号位置的解法,但存在大量重复计算,仅适合小数据。最优解是 DP。
- 高精度实现:由于乘积可能极大(\(10^{40}\) 级别),需自定义高精度结构体,支持乘法、比较和输出。
-
关键步骤(基于 DP + 高精度):
- 读入与预处理:读入 \(N, K\) 和数字串 \(s\)(1-based)。预处理 \(num[i][j]\) 为子串 \(s[i..j]\) 对应的整数(高精度)。
- 初始化 DP:\(dp[i][0] = num[1][i]\)(不插入乘号时,前 \(i\) 个数字组成一个数)。
- DP 转移:枚举乘号个数 \(j\) 从 1 到 \(K\),枚举前 \(i\) 个数字(\(i \ge j+1\)),枚举最后一个乘号位置 \(p\)(\(j \le p \le i-1\)),更新 \(dp[i][j] = \max(dp[i][j], dp[p][j-1] \times num[p+1][i])\)。
- 输出答案:\(dp[N][K]\)。
-
时间/空间复杂度:
- 时间复杂度:\(O(K \cdot N^2 \cdot H)\),其中 \(H\) 为高精度乘法复杂度(位数可达 40),\(N \le 40\),\(K \le 6\)(原题范围较小),完全可行。
- 空间复杂度:\(O(N^2 \cdot H)\),存储所有子串的高精度值。
-
区间 DP + 高精度的核心思想:
- 最优子结构:前 \(i\) 个数字插入 \(j\) 个乘号的最大值,可由前 \(p\) 个数字插入 \(j-1\) 个乘号的最大值乘上剩余部分得到。枚举 \(p\) 即可覆盖所有分割方式。
- 乘号位置枚举:最后一个乘号的位置 \(p\) 是关键,它将问题分解为“前半部分的最优值”与“后半部分的数值”的乘积。
- 高精度必要性:\(N\) 最大 40,乘积可能达到 \(10^{40}\),远超
long long(约 \(9\times10^{18}\)),必须使用高精度。 - 适用场景:适用于数字串分割求最大乘积的问题,特别是结果超出基本整数类型的情况。
【算法标签】
普及+ #DFS-一维
【代码详解】
// 60分版本
#include <bits/stdc++.h>
using namespace std;
const int N = 45, K = 8; // N: 数字串最大长度,K: 乘号最大数量
char s1[N]; // s1: 存储输入的数字字符串(下标从1开始)
long long f[N][K]; // f[i][j]: 前i个数字中插入j个乘号能得到的最大乘积
long long a[N][N]; // a[i][j]: 数字串第i到第j位组成的整数值
int n, m; // n: 数字串长度,m: 乘号个数
int main()
{
cin >> n >> m; // 输入长度和乘号数
// 输入数字串,存储到s1[1] ~ s1[n]
for (int i = 1; i <= n; i++)
{
cin >> s1[i];
}
// 预处理所有子串对应的整数值
// a[i][j] 表示 s1[i..j] 组成的整数
for (int i = 1; i <= n; i++)
{
long long te = 0;
for (int j = i; j <= n; j++)
{
te = te * 10 + (s1[j] - '0'); // 逐步构造子串数值
a[i][j] = te;
}
}
// 初始化:不插入任何乘号时,前i个数字的乘积就是整个数字串 a[1][i]
for (int i = 1; i <= n; i++)
f[i][0] = a[1][i];
// 动态规划:逐步增加乘号数量
for (int j = 1; j <= m; j++) // j: 乘号个数
{
for (int i = j + 1; i <= n; i++) // i: 前i个数字(至少需要j+1个数字才能放j个乘号)
{
// 枚举最后一个乘号的位置k
// 最后一个乘号放在第k个数字之后,则前k个数字中有j-1个乘号
for (int k = j; k <= i - 1; k++)
{
// 状态转移:前k个数字插入j-1个乘号的最大值 × 剩余部分(k+1..i)组成的数
f[i][j] = max(f[i][j], f[k][j - 1] * a[k + 1][i]);
}
}
}
// 输出前n个数字插入m个乘号的最大乘积
cout << f[n][m] << endl;
return 0;
}
// 60分版本
#include <bits/stdc++.h>
using namespace std;
#define int long long // 使用 long long 类型来存储乘积,适合本题范围
int n, k, ans; // n: 数字串长度, k: 乘号个数, ans: 最大乘积
string s; // 存储输入的数字串
// 深度优先搜索枚举所有乘号位置
// x: 当前要分割的起始下标 (0-based)
// y: 剩余要放置的乘号个数
// z: 当前已经乘得的部分乘积
void dfs(int x, int y, int z)
{
if (y == 0) // 没有剩余乘号,处理最后一段
{
// 从 x 到末尾的子串转换为整数
int t = stoll(s.substr(x, n - x));
t *= z; // 乘上之前的累积乘积
ans = max(ans, t); // 更新最大值
return;
}
// 枚举当前这一段结束的位置 i (包含 i 这一位)
// 要保证剩余部分能放下 y 个乘号,所以 i + y < n
for (int i = x; i + y < n; i++)
{
// 取出当前段子串并转为整数
int t = stoll(s.substr(x, i - x + 1));
t *= z; // 累乘到当前乘积
dfs(i + 1, y - 1, t); // 递归处理后续部分
}
}
signed main()
{
cin >> n >> k >> s; // 输入长度、乘号数和数字串
dfs(0, k, 1); // 从第0位开始,放置k个乘号,初始乘积为1
cout << ans << endl; // 输出最大乘积
return 0;
}
// AC版本
#include <bits/stdc++.h>
using namespace std;
int n, k; // n: 数字串长度, k: 乘号个数
string s; // 存储输入的数字串
// 高精度大整数结构体
struct BIG
{
int len, num[1005]; // len: 位数, num[1..len] 存储数字, 低位在前
BIG() // 构造函数, 初始化为0
{
memset(num, 0, sizeof(num));
len = 1;
}
void set(int n) // 将整数转换为高精度数
{
memset(num, 0, sizeof(num));
len = 0;
while (n > 0)
{
num[++len] = n % 10;
n /= 10;
}
if (len == 0) // 如果 n=0, 则长度为1, 数字为0
{
len = 1;
}
}
void set(string s) // 将数字字符串转换为高精度数, 下标从1开始, 低位在前
{
memset(num, 0, sizeof(num));
len = s.size();
for (int i = 1; i <= len; i++)
{
num[i] = s[len - i] - '0'; // 倒序存储
}
}
void print() // 输出高精度数
{
for (int i = len; i >= 1; i--)
{
cout << num[i];
}
cout << endl;
}
};
BIG ans; // 存储最终最大乘积
// 高精度乘法运算符重载
BIG operator*(BIG a, BIG b)
{
BIG c;
c.len = a.len + b.len; // 乘积的最大可能位数
for (int i = 1; i <= a.len; i++)
for (int j = 1; j <= b.len; j++)
c.num[i + j - 1] += a.num[i] * b.num[j];
// 统一处理进位
for (int i = 1; i < c.len; i++)
{
c.num[i + 1] += c.num[i] / 10;
c.num[i] %= 10;
}
// 去除前导零
while (c.len > 1 && c.num[c.len] == 0)
c.len--;
return c;
}
// 高精度小于比较运算符, 用于更新最大值
bool operator<(BIG a, BIG b)
{
if (a.len != b.len) return a.len < b.len;
for (int i = a.len; i >= 1; i--)
if (a.num[i] != b.num[i])
return a.num[i] < b.num[i];
return false;
}
// 深度优先搜索枚举所有乘号位置
// x: 当前要分割的起始下标 (0-based)
// y: 剩余要放置的乘号个数
// z: 当前已经乘得的部分乘积 (高精度)
void dfs(int x, int y, BIG z)
{
if (y == 0) // 没有剩余乘号, 处理最后一段
{
BIG t;
t.set(s.substr(x, n - x)); // 从 x 到末尾的子串
t = t * z; // 乘上之前的累积乘积
if (ans < t) // 更新最大值
ans = t;
return;
}
// 枚举当前这一段结束的位置 i (包含 i 这一位)
// 要保证剩余部分能放下 y 个乘号, 所以 i + y < n
for (int i = x; i + y < n; i++)
{
BIG t;
t.set(s.substr(x, i - x + 1)); // 当前段子串
t = t * z; // 累乘
dfs(i + 1, y - 1, t); // 递归处理后续部分
}
}
int main()
{
cin >> n >> k >> s;
BIG one;
one.set(1); // 初始乘积为1
dfs(0, k, one); // 从第0位开始, 放置k个乘号
ans.print(); // 输出最大乘积
return 0;
}
【运行结果】
4 2
1231
62
浙公网安备 33010602011771号