题解:学而思编程 训练计划
【题目来源】
【题目描述】
小九是个天赋异禀的孩子。尤其在信息学方面,学习不满一年,他的成绩便已远远超出了学校里久居机房的高年级前辈们。天赋固然重要,但小九知道,要早日实现他的真正目标,努力必不可少。为此,小九制定了一项训练计划。
他的训练计划共包括 \(N\) 个习题,编号 \(1∼N\),第 \(i\) 个习题能为小九带来收获值 \(g_i\)。迫不及待的小九,今天就开始执行自己的训练计划了。他会以每天一题的速度,按顺序做题。
当然,小九并不想让自己的训练过程过于枯燥,因此对于任意连续编号的习题而言,小九至多只会选择其中的KK题。
现在,小九想提前知道,当他完成此项训练计划时,他所选择的习题的收获值的乘积,最大会是多少?
【输入】
第一行,两个整数 \(N\)、\(K\),意义如上。
第二行,\(N\) 个用空格分隔的整数,其中第 \(i\) 个整数表示相应编号习题的收获值gigi。
【输出】
一个整数,表示所选习题的最大收获值乘积。输出计算结果对 \(10^9+7\) 取模后的数值即可。
【输入样例】
4 2
2 5 3 5
【输出样例】
50
【核心思想】
-
问题分析:给定 \(N\) 个习题的收获值 \(g_i\),需要选择若干习题使得乘积最大,约束是任意连续编号的习题中至多选择 \(K\) 题。等价于将序列划分为若干段,每段长度不超过 \(K\),每段内选择所有元素,最大化各段乘积之和。关键观察是:由于收获值均为正整数,每段内全选的乘积最大,问题转化为将序列划分为长度不超过 \(K\) 的段,最大化各段乘积之和。
-
算法选择:
- AC 版本:单调队列优化 DP + 对数比较:定义 \(d[i]\) 为前 \(i\) 个元素的最优划分,利用对数将高精度乘积比较转化为浮点数加法,配合单调队列优化转移
- 30 分版本:高精度 DP:\(dp[i][j]\) 表示前 \(i\) 个数分成 \(j\) 段的最大乘积,用高精度计算但复杂度较高
-
关键步骤(AC 版本):
- 前缀和统计:
sum[i]记录前 \(i\) 个数中数字 \(1 \sim 9\) 各自的出现次数 - 对数预处理:\(\ln[i] = \log(i)\),用于比较乘积大小而不直接计算高精度乘积
- 单调队列优化 DP:
- \(d[i] = \text{sum}[i-1] - \text{sum}[q[st]] + d[q[st]]\):从队列头部最优位置转移
- 若 \(i - q[st] > K\),弹出队头(区间长度超限)
- 维护队列单调性:弹出队尾不优的状态,加入当前状态
- 答案计算:根据 \(d[n+1]\) 中各数字的出现次数,直接计算乘积并对 \(10^9+7\) 取模
- 前缀和统计:
-
时间/空间复杂度:
- 时间复杂度:\(O(N)\),单调队列每个元素最多入队出队一次
- 空间复杂度:\(O(N)\),存储前缀和、DP 状态和单调队列
-
单调队列优化 DP 的核心思想:
- 对数转化比较:乘积的比较通过对数转化为加法:\(\log(\prod a_i) = \sum \log(a_i)\),避免了高精度乘积的直接计算,将比较操作优化至 \(O(1)\)
- 前缀和表示乘积结构:用数字出现次数数组代替实际乘积值,\(sum[i] - sum[j]\) 表示区间 \((j, i]\) 中各数字的出现次数,乘积可通过次数快速重构
- 单调队列维护最优转移点:\(d[i]\) 的转移只依赖于前 \(K\) 个位置中的最优值,单调队列维护候选转移点,确保队首始终为最优,均摊 \(O(1)\) 获取最优转移
- 状态压缩:将对数比较所需的数字计数数组作为 DP 状态,而非存储实际乘积值,大幅降低了状态表示的复杂度
- 适用于带区间长度限制的序列划分、乘积最大化、高精度优化等动态规划场景
【算法标签】
单调队列
【代码详解】
// 30分版本
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
const int MOD = 1e9 + 7;
// ==================== 高精度封装 ====================
struct BIG
{
int len, num[1005]; // len: 数字的长度,num: 存储数字的数组(低位在num[1])
BIG() // 构造函数,初始化
{
memset(num, 0, sizeof(num));
len = 1; // 初始长度为1(表示数字0)
}
void set(int x) // 用整数设置BIG
{
memset(num, 0, sizeof(num));
len = 0;
while (x > 0) // 从低位到高位存储
{
num[++len] = x % 10;
x /= 10;
}
if (len == 0) len = 1; // 如果x=0,长度设为1
}
void set(string s) // 用字符串设置BIG
{
memset(num, 0, sizeof(num));
len = s.size();
for (int i = 1; i <= len; i++) // 反转存储
num[i] = s[len - i] - '0';
}
void print() // 输出BIG
{
for (int i = len; i >= 1; i--) // 从高位到低位输出
cout << num[i];
cout << endl;
}
string toS() // 转换为字符串
{
string t;
for (int i = len; i >= 1; i--)
t += to_string(num[i]);
return t;
}
};
// 高精度乘法(高精 × 单精)
BIG operator*(BIG a, int b)
{
BIG c;
c.len = a.len;
long long u = 0; // 进位,用long long防止溢出
for (int i = 1; i <= c.len; i++)
{
long long t = (long long)a.num[i] * b + u; // 计算乘积加进位
c.num[i] = t % 10; // 取个位
u = t / 10; // 计算进位
}
while (u > 0) // 处理剩余的进位
{
c.num[++c.len] = u % 10;
u /= 10;
}
return c;
}
// 高精度比较(用于max)
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; // 相等
}
bool operator>(BIG a, BIG b) { return b < a; }
bool operator<=(BIG a, BIG b) { return !(b < a); }
bool operator>=(BIG a, BIG b) { return !(a < b); }
// 高精度对单精度取模
int mod(BIG a, int div)
{
long long rem = 0; // 余数
for (int i = a.len; i >= 1; i--) // 从高位到低位
{
rem = (rem * 10 + a.num[i]) % div; // 模拟除法
}
return (int)rem;
}
// ==================== 高精度封装结束 ====================
int n, k;
int g[N];
BIG dp[N][N]; // dp[i][j]: 前i个数,恰好分成j段的最大乘积
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> g[i];
// 初始化:前0个题目,乘积为1
dp[0][0].set(1);
for (int i = 1; i <= n; i++) // 处理第i个数字
{
// 情况1:第 i 个题目不选
// 继承前面所有状态的最大值
dp[i][0] = dp[i-1][0];
for (int j = 1; j <= k; j++)
if (dp[i-1][j] > dp[i][0])
dp[i][0] = dp[i-1][j];
// 情况2:第 i 个题目选,作为新段的第一个
BIG temp = dp[i-1][0] * g[i]; // 新开一段
if (temp > dp[i][1])
dp[i][1] = temp;
// 情况3:第 i 个题目选,接在上一段后面
for (int j = 2; j <= k; j++) // 至少需要已经有1段才能接在后面
{
if (dp[i-1][j-1].len > 1 || dp[i-1][j-1].num[1] > 0) // 前一个状态不为0
{
BIG temp = dp[i-1][j-1] * g[i];
if (temp > dp[i][j])
dp[i][j] = temp;
}
}
}
// 在dp[n][0...k]中找最大值
BIG ans;
ans.set(0);
for (int j = 0; j <= k; j++)
if (dp[n][j] > ans)
ans = dp[n][j];
// 输出对 MOD 取模的结果
cout << mod(ans, MOD) << endl;
return 0;
}
// AC版本
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005, mod = 1e9 + 7; // 定义数组大小和模数
int n, k; // n: 数组长度,k: 最大间隔长度
int g[N]; // 存储原始数组
double ln[15]; // 存储1-9的自然对数
struct Node
{
int s[15]; // 记录每个数字(1-9)出现的次数
// 重载 - 运算符:对应位置相减
Node operator-(const Node& other) const
{
Node res;
for (int i = 0; i < 15; i++)
{
res.s[i] = s[i] - other.s[i];
}
return res;
}
// 重载 + 运算符:对应位置相加
Node operator+(const Node& other) const
{
Node res;
for (int i = 0; i < 15; i++)
{
res.s[i] = s[i] + other.s[i];
}
return res;
}
// 重载 <= 运算符:比较乘积大小(用对数)
bool operator<=(const Node& other) const
{
double v1 = 0, v2 = 0;
for (int i = 1; i <= 9; i++) // 比较数字1-9的乘积
{
v1 += s[i] * ln[i]; // 当前节点的对数总和
v2 += other.s[i] * ln[i]; // 另一个节点的对数总和
}
return v1 <= v2; // 如果v1 <= v2,返回true
}
} sum[N], d[N]; // sum: 前缀和数组,d: 动态规划数组
int q[N]; // 单调队列
int main()
{
cin >> n >> k; // 输入数组长度和最大间隔
for (int i = 1; i <= n; i++)
{
cin >> g[i]; // 输入数组元素
sum[i] = sum[i - 1]; // 复制前缀和
sum[i].s[g[i]]++; // 对应数字计数加1
}
for (int i = 1; i < 10; i++) // 计算1-9的自然对数
ln[i] = log(i * 1.0);
sum[n + 1] = sum[n]; // 扩展一个位置
int st = 1, ed = 1; // 单调队列的头尾指针
q[1] = 0; // 队列初始位置
for (int i = 1; i <= n + 1; i++)
{
// 计算d[i]:从q[st]到i-1的最小乘积划分
d[i] = sum[i - 1] - sum[q[st]] + d[q[st]];
if (i - q[st] > k) // 如果区间长度超过k,弹出队头
st++;
// 维护单调队列:保持d[q[j]] + sum[i] - sum[q[j]]的最小值
while (ed >= st && (d[q[ed]] + sum[i] - sum[q[ed]]) <= d[i])
ed--;
q[++ed] = i; // 将当前位置加入队列
}
long long ans = 1; // 最终答案
for (int i = 1; i <= 9; i++) // 计算最终乘积
{
for (int j = 1; j <= d[n + 1].s[i]; j++) // 对每个数字计算乘积
ans = (ans * i) % mod; // 取模乘法
}
cout << ans << endl; // 输出结果
return 0;
}
【运行结果】
4 2
2 5 3 5
50
浙公网安备 33010602011771号