题解:学而思编程 训练计划

【题目来源】

训练计划

【题目描述】

小九是个天赋异禀的孩子。尤其在信息学方面,学习不满一年,他的成绩便已远远超出了学校里久居机房的高年级前辈们。天赋固然重要,但小九知道,要早日实现他的真正目标,努力必不可少。为此,小九制定了一项训练计划。

他的训练计划共包括 \(N\) 个习题,编号 \(1∼N\),第 \(i\) 个习题能为小九带来收获值 \(g_i\)​。迫不及待的小九,今天就开始执行自己的训练计划了。他会以每天一题的速度,按顺序做题。

当然,小九并不想让自己的训练过程过于枯燥,因此对于任意连续编号的习题而言,小九至多只会选择其中的KK题。

现在,小九想提前知道,当他完成此项训练计划时,他所选择的习题的收获值的乘积,最大会是多少?

【输入】

第一行,两个整数 \(N\)\(K\),意义如上。

第二行,\(N\) 个用空格分隔的整数,其中第 \(i\) 个整数表示相应编号习题的收获值gigi​。

【输出】

一个整数,表示所选习题的最大收获值乘积。输出计算结果对 \(10^9+7\) 取模后的数值即可。

【输入样例】

4 2
2 5 3 5

【输出样例】

50

【核心思想】

  1. 问题分析:给定 \(N\) 个习题的收获值 \(g_i\),需要选择若干习题使得乘积最大,约束是任意连续编号的习题中至多选择 \(K\) 题。等价于将序列划分为若干段,每段长度不超过 \(K\),每段内选择所有元素,最大化各段乘积之和。关键观察是:由于收获值均为正整数,每段内全选的乘积最大,问题转化为将序列划分为长度不超过 \(K\) 的段,最大化各段乘积之和

  2. 算法选择

    • AC 版本:单调队列优化 DP + 对数比较:定义 \(d[i]\) 为前 \(i\) 个元素的最优划分,利用对数将高精度乘积比较转化为浮点数加法,配合单调队列优化转移
    • 30 分版本:高精度 DP\(dp[i][j]\) 表示前 \(i\) 个数分成 \(j\) 段的最大乘积,用高精度计算但复杂度较高
  3. 关键步骤(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\) 取模
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),单调队列每个元素最多入队出队一次
    • 空间复杂度:\(O(N)\),存储前缀和、DP 状态和单调队列
  5. 单调队列优化 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
posted @ 2026-07-02 22:45  团爸讲算法  阅读(2)  评论(0)    收藏  举报