题解:洛谷 P15800 选数

【题目来源】

洛谷:P15800 [GESP202603 六级] 选数 - 洛谷

【题目描述】

给定两个包含 \(n\) 个整数的数组 \(a=[a_1,\dots,a_n]\)\(b=[b_1,\dots,b_n]\)。你需要指定若干下标 \(p_1\lt \cdots\lt p_k\)\(1\leq k\leq n\))使得以下条件成立:

  • \(1\leq p_i\leq n\)\(1\leq i\leq k\));
  • \(p_{i+1}\geq p_i+b_{p_i}\)\(1\leq i< k\))。

你需要在满足以上条件的前提下最大化 \(\sum_{i=1}^k a_{p_k}\),也即最大化数组 \(a\) 对应下标的整数之和。

【输入】

第一行,一个正整数 \(n\),表示数组长度。

第二行,\(n\) 个正整数 \(a_1,a_2,\dots,a_n\),表示数组 \(a\)

第三行,\(n\) 个正整数 \(b_1,b_2,\dots,b_n\),表示数组 \(b\)

【输出】

一行,一个整数,表示在满足下标条件的前提下,数组 \(a\) 对应下标的整数之和的最大值。

【输入样例】

4
1 2 3 4
3 3 1 1

【输出样例】

7

【核心思想】

  1. 问题分析:给定两个数组 \(a\)\(b\),需要选择若干下标 \(p_1 < p_2 < \cdots < p_k\),满足 \(p_{i+1} \geq p_i + b_{p_i}\)(即选择下标 \(i\) 后,至少间隔 \(b_i\) 个位置才能选下一个),最大化 \(\sum_{i=1}^k a_{p_i}\)。这是一个带间隔约束的最大子序列和问题,类似于加权活动选择问题。

  2. 算法选择

    • 线性DPdp[i] 表示到时间 \(i\) 为止能获得的最大收益(不包含在时间 \(i\) 开始的任务)
    • 两种决策:在每个时间点 \(i\),可以选择执行活动 \(i\) 或不执行
    • 时间轴DP:将问题转化为在时间轴上的决策,利用 \(dp[i]\) 记录到时间 \(i\) 的最优解
  3. 关键步骤

    • 初始化dp[i] 初始为 0,表示到时间 \(i\) 的收益为 0
    • 状态转移:遍历每个时间点 \(i\)
      • 执行活动 \(i\):收益为 dp[i] + a[i],更新答案 ans。若 \(i + b[i] \leq n\),则更新 dp[i + b[i]] = max(dp[i + b[i]], dp[i] + a[i])
      • 不执行活动:收益延续到下一个时间点,dp[i + 1] = max(dp[i + 1], dp[i])
    • 输出答案ans 记录所有可能的最大值
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),只需遍历一次时间轴
    • 空间复杂度:\(O(N)\),DP数组大小
  5. 时间轴DP的核心思想

    • 状态定义技巧dp[i] 表示到时间 \(i\) 为止的最大收益,但不包含在时间 \(i\) 开始的任务,避免重复计算
    • 两种决策统一处理:在每个时间点同时考虑"执行"和"不执行"两种选择
    • 延迟更新:执行任务 \(i\) 的收益在 \(i + b[i]\) 时刻才生效,体现间隔约束
    • 答案实时维护:在遍历过程中实时更新 ans,避免最后遍历数组
    • 适用于带时间/位置间隔约束的序列选择问题

【解题思路】

【算法标签】

普及 #线性DP-一维

【代码详解】

// 40分版本
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 100005;

int n, a[N], b[N], ans;
// dp[i] 表示以第 i 个活动结尾时,能获得的最大价值
int dp[N];
int len = 0;

signed main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    for (int i = 1; i <= n; i++)
    {
        cin >> b[i];
    }

    for (int i = 1; i <= n; i++)
    {
        // 初始化:只选择第 i 个活动
        dp[i] = a[i];
        // 考虑在 i 之前的活动 j
        for (int j = 1; j < i; j++)
        {
            // 检查活动 j 结束后是否可以安排活动 i
            if (i >= j + b[j])
            {
                // 如果可以,则尝试从活动 j 转移到活动 i
                dp[i] = max(dp[i], dp[j] + a[i]);
            }
        }
    }

    // 找到所有 dp[i] 中的最大值
    for (int i = 1; i <= n; i++)
    {
        ans = max(ans, dp[i]);
    }

    cout << ans << endl;
    return 0;
}
// 100分版本
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 100005;

int n, a[N], b[N];
// dp[i]: 到时间i为止能获得的最大收益(不包含在时间i开始的任务)
int dp[N];

signed main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    for (int i = 1; i <= n; i++)
    {
        cin >> b[i];
    }

    int ans = 0;

    for (int i = 1; i <= n; i++)
    {
        // 情况1: 在时间i开始执行任务i
        // 收益 = 到时间i为止的最大收益 + 任务i的收益
        ans = max(ans, dp[i] + a[i]);

        // 如果执行任务i
        int finish_time = i + b[i];
        if (finish_time <= n)
        {
            // 在任务i结束时更新收益
            dp[finish_time] = max(dp[finish_time], dp[i] + a[i]);
        }

        // 情况2: 在时间i不执行任何任务
        // 收益延续到下一个时间点
        dp[i + 1] = max(dp[i + 1], dp[i]);
    }

    // 注意:最终答案不一定是dp[n],因为可能在n时刻之前就已经得到了最大收益
    // 我们已经在循环中通过ans记录了所有可能的最大值
    cout << ans << endl;

    return 0;
}

【运行结果】

4
1 2 3 4
3 3 1 1
7
posted @ 2026-06-15 09:50  团爸讲算法  阅读(21)  评论(0)    收藏  举报