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

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

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(a[i],dp[i1]+a[i]),这个表示的意思就是如果当前的值就是最大的,那就不需要从上一个状态转移过来,不然的话就在前一次的状态基础上加上当前的值,时间复杂度是O(n)
    ②因为求一段连续区间[l,r]的和可以转化成i=l1ra[i](sumrsuml1),那么要求出区间和的最大值,就需要让suml1的值最小,那么就需要去动态的维护出来min{sum},所以就需要去考虑一个具有单调性的数据结构单调栈来维护出之前的最小值,时间复杂度O(nlogn)
  对于本题而言,因为我们有一个额外的限制条件,区间长度不能超过n,所以不能单纯的用dp来对最大子段和来求解,考虑用单调栈+dp来求解.单调栈存下前n个区间的最小前缀和,dp[i]来表示以i结尾的最大子段和,状态转移方程为:dp[i]=min(a[i],sum[i]sum[stk[top]])因为每一次单调栈栈顶的值都是要给下一次状态转移的方程用的,所以应该先状态转移求出以i结尾的最大子段和,再去维护出[in,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[std::max(stk[top], 0)], 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就需要剩下的[rn+1,l1]或者[r+1,l+n1]区间去找,这个时候就不好再用原先的dp了.因为线段树可以维护出某一段区间的最大子段和,那么可以一开始就将a[2n]数组建树.

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";

__EOF__

本文作者HoneyGrey
本文链接https://www.cnblogs.com/Haven-/p/16733058.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   浅渊  阅读(64)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示