简介
首先我们介绍一下,什么叫做序列的最大连续子段和。给你一个序列,序列中有正有负,问你只选其中连续的一段子串求合可以拿到的最大值是多少?那么对于这样的一个问题,首先我们分两种情况。
1、静态查询
所谓静态查询,也就是对序列中原本的数不进行修改,每次只会查询一个固定的区间。那么对于这种静态的问题,我们最容易想到的办法就是 O ( n 2 ) O(n^2) O(n2)枚举前后端点, O ( n ) O(n) O(n)求和,最终求解最大值,这样的复杂度是 O ( n 3 ) O(n^3) O(n3)。
第二种做法,我们预处理全部的数它们的前缀和,这样我们就可以例用前缀和的特性 O ( 1 ) O(1) O(1)转移。但是前后端点的枚举无法改变所以时间复杂度仍然为 O ( n 2 ) O(n^2) O(n2)。
第三种做法,动态规划求解。对于我们区间中累加的每个数,我们都进行一次预加法处理,也就是先把 a i a_i ai当作区间右端点,如果这样得到的区间和仍然大于 0 0 0,那么我们保留这个右端点,并且更新我们可以求到的最大值,如果我们加完之后这个区间变成负数了,那么我们就一定不选择这个点做为我们的右端点,因为你继续加点的过程,前面一定会有一段是负数,还不如不算进去。那么接下来我们就把 a i a_i ai当作一个左端点继续求解最大子段和的过程,最终可以在 O ( n ) O(n) O(n)的复杂度找到答案。
int calc(vector<int>& a) {
int maxi = INT_MIN, sum = 0;
for (int i = 0; i < m; ++i) {
if (sum > 0) sum += a[i];
else sum = a[i];
maxi = max(maxi, sum);
}
return maxi;
}
2、动态查询
如果我们存在对原有序列修改的操作那该如何处理呢?
首先如果是单点修改,我们还可能可以参考上面动态规划的做法,实现复杂度期望为 O ( q n ) O(qn) O(qn), q q q为查询次数,但是绝大部分时候这样的复杂度都是不允许的。那么就更别说区间修改了,如何做到 log n \log_n logn的查询呢?
这时候就要请上我们的区间处理能手线段树数据结构了。对于每一个线段树节点我们分别记录 4 4 4种数值。
l s u m lsum lsum代表区间 [ l , r ] [l, r] [l,r]中紧靠左端点的最大子段和。
r s u m rsum rsum代表区间 [ l , r ] [l,r] [l,r]中紧靠右端点的最大子端和。
s u m sum sum代表区间 [ l , r ] [l,r] [l,r]的区间和。
a n s ans ans代表区间 [ l , r ] [l,r] [l,r]的最大子段和。
那么我们就从建树说起如何维护这样的线段树节点把,对应着线段树中的 p u s h _ u p ( ) push\_up() push_up()操作。
对于当前节点,它的 l s u m lsum lsum有两种来源,一种是左子树的 l s u m lsum lsum,另外一种是左子树的 s u m sum sum加上右子树的 l s u m lsum lsum。
同理对于 r s u m rsum rsum一种是右子树的 r s u m rsum rsum,一种是右子树的 s u m sum sum加上左子树的 r s u m rsum rsum。
区间和 s u m sum sum等于左右子树相加。
a n s ans ans有三种解,一种是左子树的 a n s ans ans,一种是右子树的 a n s ans ans,一种是左子数的 r s u m rsum rsum加上右子树的 l s u m lsum lsum。
那么维护好这样的四个节点就可以在 log n \log_n logn的查询中找到区间最大子段和了。这里给出一题洛谷的带修改题目,因为区间修改是进行或操作,所以对于每个节点只能修改 30 ∗ n 30*n 30∗n次,更新操作记录一下减枝更快。
序列
#include <bits/stdc++.h>
using namespace std;
typedef long long ll; typedef unsigned long long ull; typedef long double ld;
inline ll read() { ll s = 0, w = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') w = -1; for (; isdigit(ch); ch = getchar()) s = (s << 1) + (s << 3) + (ch ^ 48); return s * w; }
inline void print(ll x, int op = 10) { if (!x) { putchar('0'); if (op) putchar(op); return; } char F[40]; ll tmp = x > 0 ? x : -x; if (x < 0)putchar('-'); int cnt = 0; while (tmp > 0) { F[cnt++] = tmp % 10 + '0'; tmp /= 10; } while (cnt > 0)putchar(F[--cnt]); if (op) putchar(op); }
const int N = 1e5 + 7;
int n, m;
struct Seg_tree {
#define mid (l + r >> 1)
#define lson rt << 1, l, mid
#define rson rt << 1 | 1, mid + 1, r
#define ls rt << 1
#define rs rt << 1 | 1
struct Node {
ll lsum, rsum, sum, ans;
/*
* 把左子树默认放左边,A是右子树
**/
Node operator + (const Node& A) const {
return { max(lsum,sum + A.lsum),max(A.rsum,A.sum + rsum),
sum + A.sum,max({ans,A.ans,rsum + A.lsum}) };
}
}tree[N << 2];
int a[N << 2];
void push_up(int rt) {
tree[rt] = tree[ls] + tree[rs];
a[rt] = a[ls] & a[rs];
}
void build(int rt, int l, int r) {
if (l == r) {
a[rt] = read();
tree[rt].lsum = tree[rt].rsum = tree[rt].ans = max(0, a[rt]);
tree[rt].sum = a[rt];
return;
}
build(lson);
build(rson);
push_up(rt);
}
void update(int rt, int l, int r, int L, int R, int k) {
if ((a[rt] | k) == a[rt]) return;
if (l == r) {
a[rt] |= k;
tree[rt].lsum = tree[rt].rsum = tree[rt].ans = max(0, a[rt]);
tree[rt].sum = a[rt];
return;
}
if (L <= mid) update(lson, L, R, k);
if (R > mid) update(rson, L, R, k);
push_up(rt);
}
Node query(int rt, int l, int r, int L, int R) {
if (l >= L and r <= R) {
return tree[rt];
}
Node res;
if (R <= mid) res = query(lson, L, R);
else if (L > mid) res = query(rson, L, R);
else res = query(lson, L, R) + query(rson, L, R);
return res;
}
}A;
void solve() {
n = read(), m = read();
A.build(1, 1, n);
int op, l, r;
while (m--) {
op = read(), l = read(), r = read();
if (op & 1)
print(A.query(1, 1, n, l, r).ans);
else
A.update(1, 1, n, l, r, read());
}
}
int main() {
//int T = read(); rep(_, 1, T)
{
solve();
}
return 0;
}
二维最大子矩阵
上面求解的都是一维的方案,那么对于二维的不带修改的子矩阵最大和,也可以例用一维最大子段和的思想进行求解。
这里借用一下只会写臭虫博主的图片。
那么我们每次的计算固定矩阵上边界,每次计算的时候吧列的值进行累加,对于这样找到的新的列求解一维的最大连续子段和就是我们的答案了。期望的时间复杂度是 O ( n 2 m ) O(n^2m) O(n2m)。
最强对手矩阵
按照上面的逻辑写代码只能拿到 70 70 70分,目的只是引出二维最大子矩阵的求解方案之一,因为看出来我们矩阵的期望是 O ( n 2 m ) O(n^2m) O(n2m),因为题目给出的 n m ≤ 2 ∗ 1 0 5 nm\le2*10^5 nm≤2∗105,举个最简单例子当 n = 2 ∗ 1 0 5 , m = 1 n=2*10^5,m=1 n=2∗105,m=1时,我们把 n 2 n^2 n2换成 m 2 m^2 m2复杂度优化十分明显,所以我们只要保证 n ≤ 2 ∗ 1 0 5 n\le\sqrt{2*10^5} n≤2∗105。就可以算到极限复杂度 O ( n n m ) O(n\sqrt{n}m) O(nnm),只要保证相对大小即可满分。
#include <bits/stdc++.h>
using namespace std;
const int N = 1000 + 7;
int n, m;
int calc(vector<int>& a) {
int maxi = INT_MIN, sum = 0;
for (int i = 0; i < m; ++i) {
if (sum > 0) sum += a[i];
else sum = a[i];
maxi = max(maxi, sum);
}
return maxi;
}
int main() {
scanf("%d %d", &n, &m);
vector<vector<int>> a(n, vector<int>(m, 0)), b(m, vector<int>(n, 0));
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
scanf("%d", &a[i][j]);
if (n <= m) {
int ans = INT_MIN;
for (int i = 0; i < n; ++i) {
vector<int> tmp(m, 0);
for (int j = i; j < n; ++j) {
for (int k = 0; k < m; ++k)
tmp[k] += a[j][k];
int maxi = calc(tmp);
ans = max(ans, maxi);
}
}
printf("%d\n", ans);
}
else {
swap(n, m);
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j)
b[i][j] = a[j][i];
int ans = INT_MIN;
for (int i = 0; i < n; ++i) {
vector<int> tmp(m, 0);
for (int j = i; j < n; ++j) {
for (int k = 0; k < m; ++k)
tmp[k] += b[j][k];
int maxi = calc(tmp);
ans = max(ans, maxi);
}
}
printf("%d\n", ans);
}
return 0;
}