\(AcWing\) \(3662\). 最大上升子序列和

一、题目描述

给定一个长度为 \(n\) 的整数序列 \(a_1,a_2,…,a_n\)。

请你选出一个该序列的 严格上升子序列要求所选子序列的各元素之和尽可能大

请问这个 最大值 是多少?

输入格式
第一行包含整数 \(n\)。

第二行包含 \(n\) 个整数 \(a_1,a_2,…,a_n\)。

输出格式
输出最大的上升子序列和。

数据范围
对于前三个测试点,\(1≤n≤4\)。

对于全部测试点,\(1≤n≤10^5,1≤a_i≤10^9\)。

输入样例1

2
100 40

输出样例1

100

输入样例2

4
1 9 7 10

输出样例2

20

样例解释
对于样例 \(1\),我们只选取 \(100\)。

对于样例 \(2\),我们选取 \(1,9,10\)。

二、前置知识

1. 离散化

2. 树状数组 or 线段树 (用于维护前缀的信息)

3. 最长上升子序列 AcWing 895. 最长上升子序列

三、分析

首先这道题在不考虑优化的情况下是一道最长上升子序列板子题,不会的先去看一下 这道题 ,由于本题数据范围过大,我们需要考虑如何优化,这也是本题的 难点 所在。

我们先从\(DP\)的角度讨论一下这题的朴素写法

AcWing 3662. 最大上升子序列和_树状数组

本题的状态转移方程见上图,显然,我们在执行状态方程的途中需要寻找某一个前缀的最大值,这使得我们每次执行方程都会遍历一遍前面的每一个数,这就导致了\(O(n^2)\)

由于我们每次寻找的\(f(k)_{max}\)是一个前缀中的最大值,我们很容易可以想到树状数组或者线段树,来动态的维护前缀的最大值吗,这样我们每次查询就可以从\(O(n)\)优化到\(O(logn)\)

那么我们如何来维护这样一个前缀呢?

比较容易想到的是将\(f(i)\)维护到下标\(i\),如下图所示

AcWing 3662. 最大上升子序列和_子序列_02

但是这样做在本题中是否行得通呢?

先说结论,本题不可以这样维护,由于题目明确规定,我们要找的子序列为单调上升子序列,所以必须满足一个条件\(a_i>a_k\),如果我们维护上面这样一个关系,虽然可以让我们找到前缀中最大的一个\(f(k)\),但是我们却无法保证\(a_i>a_k\)。

那我们如何维护呢?

我们考虑将\(f(i)\)维护到下标\(a_i\),如下图所示

AcWing 3662. 最大上升子序列和_离散化_03

如果我们采取这种维护策略,若\(a_2>a_i\),则我们在查询\(a_i\)的前缀时,就永远也无法找到\(f(2)\),这样就保证了\(a_i>a_k\)

但是,到这里还有最后一个问题,\(1≤ai≤109\),如果我们直接维护,那么需要开一个大小为\(10^9\)的数组才可以做到,这显然是不可以接受的,同时我们发现\(1≤n≤10^5\),这说明我们可以离散化一下,把每一个\(a_i\)按照相对位置关系,一一映射到\(1≤n≤10^5\)

AcWing 3662. 最大上升子序列和_离散化_04

离散化需要去重,由于本题的答案是取决于单调上升子序列并且子序列内部元素不能相等,所以去重不影响答案,我们需要的只是每个元素的相对位置不变,以及相对位置信息。

四、\(Code\)

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
typedef long long LL;      // 本题中 a 的上线是 10 ^ 9,故答案可能会超过 int 的上限
#define lowbit(x) (x & -x) // lowbit函数模板

int n, a[N];

int b[N], bl;  // 离散化数组
LL tr[N], res; // 树状数组,答案

void add(int x, LL c) {
    for (; x <= bl; x += lowbit(x)) tr[x] = max(tr[x], c);
}

// 查询x范围内,元素和最大值
LL query(int x) {
    LL res = 0;
    for (; x; x -= lowbit(x)) res = max(res, tr[x]);
    return res;
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", a + i);
        b[i] = a[i];
    }
    // 离散化
    sort(b, b + n);
    bl = unique(b, b + n) - b;

    for (int i = 0; i < n; i++) {                     // 枚举原始数组
        int k = lower_bound(b, b + bl, a[i]) - b + 1; // 二分查找a[i]离散化后的值,注意配合树状数组+1
        /*
        状态转移方程:f[i]=a[i]+max(f[j]) (0<=j<i,a[j]<a[i])
        f[i]: f
        a[i]: a[i]
        max(f[j]):query(k - 1) 表示k-1范围内元素和最大值
        */
        LL f = a[i] + query(k - 1);
        res = max(res, f); // 更新最大值
        add(k, f);         // 更新树状数组
    }
    printf("%lld\n", res);

    // 下面的代码也可以AC
    // printf("%lld\n", query[bl]);
    return 0;
}