首页 > 其他分享 >两个和最大的区间(线段树+单调栈+dp)

两个和最大的区间(线段树+单调栈+dp)

时间:2022-09-27 00:12:07浏览次数:44  
标签:最大 子段 int 线段 dp 区间 sum 单调

  胜哥投喂的一道面试题
  题意:有一个环形数组\(a\),找出两个不重叠的子串,是的这两个区间内所有的数加起来的和最大。
  数据范围: \(1 \leq n \leq 10 ^ 5, \left| a_i \right| \leq 10 ^ 9\)
  思路:看到环形数组而且要选取一段长度为不超过\(n\)的区间,所以不难想到要断环为链来处理。而想要快速的求出区间和,就需要借助前缀和来实现\(O\left(1\right)\)的计算出区间和是多少。这样我们的预处理就可以写出来了。

    int n; std::cin >> n;
    std::vector<i64> a(n * 2 + 1), sum(n * 2 + 1);
    for (int i = 1; i <= n; i++) {
        std::cin >> a[i];
        a[i + n] = a[i];
    }

    for (int i = 1; i <= n * 2; i++) {
        sum[i] = sum[i - 1] + a[i];
        b[i] = a[i];
    }

  接下来我们就需要考虑区间和最大的问题,连续的一段区间最大和有两种方式来实现:
    ①直接\(dp\),考虑以\(i\)结尾的一段连续的区间,\(dp[i]\)表示这个区间的最大和是多少,那么不难写出状态转移方程\(dp[i] = \max\left(a[i], dp[i - 1] + a[i]\right)\),这个表示的意思就是如果当前的值就是最大的,那就不需要从上一个状态转移过来,不然的话就在前一次的状态基础上加上当前的值,时间复杂度是\(O(n)\)。
    ②因为求一段连续区间\([l, r]\)的和可以转化成\(\sum\limits_{i = l - 1}^{r}a[i] \Rightarrow \left(sum_{r} - sum_{l - 1}\right)\),那么要求出区间和的最大值,就需要让\(sum_{l - 1}\)的值最小,那么就需要去动态的维护出来\(\min\{sum\}\),所以就需要去考虑一个具有单调性的数据结构单调栈来维护出之前的最小值,时间复杂度\(O\left(n\log{n}\right)\)
  对于本题而言,因为我们有一个额外的限制条件,区间长度不能超过\(n\),所以不能单纯的用\(dp\)来对最大子段和来求解,考虑用单调栈+dp来求解.单调栈存下前\(n\)个区间的最小前缀和,\(dp[i]\)来表示以\(i\)结尾的最大子段和,状态转移方程为:\(dp[i] = \min\left(a[i], sum[i] - sum[stk[top]]\right)\)因为每一次单调栈栈顶的值都是要给下一次状态转移的方程用的,所以应该先状态转移求出以\(i\)结尾的最大子段和,再去维护出\([i - n, i]\)之间的最小前缀和。

    for (int i = 1; i <= n * 2; i++) {
        dp[i] = std::max(sum[i] - sum[stk[top]], dp[i]);
        while(top && (sum[stk[top]] >= sum[i] || stk[top] - 1 <= i - n)) top--;
        if (!top) stk[++top] = i;
    }

  这样就求出了最大子段和,但是这个做法对于本题来说是不可取的,因为不可以单纯的认为最大子段和+次大子段和就是最后的答案,如果一段区间的数是...xx..xxx这样的(xxx是我们要拿走的),这两个区间的和不一定是最大的,但是最后他俩加起来的和却是最大的,这就需要最后遍历一遍所有的长度为\(n\)的区间将所有的情况都考虑进去。基于此需要同时存下左右区间的端点,而右端点就是\(i\),所以只需要在\(dp\)数组维护最大子段和的同时存下来区间的左端点,就可以了。

    std::vector<std::pair<i64, int>> dp(n * 2 + 1);
    for (int i = 1; i <= n * 2; i++) {
        dp[i].first = std::max(sum[i] - sum[stk[top]], a[i]);
        dp[i].second = stk[top] + 1;
        while(top && (sum[stk[top]] >= sum[i] || stk[top] - 1 <= i - n)) top--;
        if (!top) stk[++top] = i; 
        // 不加if判断的话,就会出现新加入的元素比原栈顶的元素大,不符合单调栈的性质
    }

  做完了上面的操作后,就是求答案的过程了。因为在遍历所有区间的时候,\(dp[i]\)只能知道长度为\(n\)的区间的某一段xxx另外一段xxx就需要剩下的\([r - n + 1, l - 1]\)或者\([r + 1, l + n - 1]\)区间去找,这个时候就不好再用原先的\(dp\)了.因为线段树可以维护出某一段区间的最大子段和,那么可以一开始就将\(a[2 * n]\)数组建树.

    i64 b[N];

    struct Info {
        int l, r;
        i64 lmax, rmax, mx, sum;
    } tr[N << 2];

    Info operator + (const Info& x, const Info& y) {
        Info c;
        c.l = x.l, c.r = y.r;
        c.sum = x.sum + y.sum;
        c.lmax = std::max(x.lmax, x.sum + y.lmax);
        c.rmax = std::max(y.rmax, x.rmax + y.sum);
        c.mx = std::max({x.mx, x.rmax + y.lmax, y.mx});
        return c;
    }

    void build(int u, int l, int r) {
        tr[u] = {l, r};
        if (l == r) return void(tr[u].lmax = tr[u].rmax = tr[u].mx = tr[u].sum = b[l]);
        int mid = (l + r) >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        tr[u] = tr[u << 1] + tr[u << 1 | 1];
    }

    i64 query(int u, int l, int r) {
        if (tr[u].l >= l && tr[u].r <= r) return tr[u].mx;
        int mid = tr[u].l + tr[u].r >> 1;
        i64 ans = -1E18;
        if (mid >= l) ans = std::max(ans, query(u << 1, l, r));
        if (mid < r) ans = std::max(ans, query(u << 1 | 1, l, r));
        return ans;
    }

  在查找第二段区间最大的时候,直接在线段树上去查找就好了,这样就找出了两个不重叠的区间,使得和最大.

    i64 res = -1E18;
    build(1, 1, n * 2);
    for (int i = 1; i <= n * 2; i++) {
        int l = dp[i].second, r = i;
        i64 ans = dp[i].first, ret = -1E18;
        if (r <= n) {
            ret = query(1, r + 1, l + n - 1);
        } else {
            ret = query(1, r - n + 1, l - 1);
        }
        res = std::max(res, ans + ret);
    }
    std::cout << res << "\n";

标签:最大,子段,int,线段,dp,区间,sum,单调
From: https://www.cnblogs.com/Haven-/p/16733058.html

相关文章