1 条题解
-
0
题目分析
题目要求选择不超过 (m) 个连续子段,使得子段和最大。核心思路是每次选择当前剩余序列中的最大和连续子段,将其取反(避免重复选择),重复 (m) 次后累加结果。线段树用于高效维护区间最大/最小子段和、前缀/后缀和等信息,支持区间取反操作。
解题思路
- 线段树维护区间信息:每个节点存储区间和、最大/最小子段和、最大/最小前缀和、最大/最小后缀和。
- 区间取反操作:通过标记实现区间取反,交换最大/最小相关值并取反。
- 贪心选择:每次查询当前最大和子段,累加结果后将该子段取反,重复 (m) 次(或直到最大和非正)。
代码实现(带注释)
#include <cstdio> #include <iostream> #include <algorithm> using namespace std; #define LL long long const int N = 1e6 + 10; int n, m, a[N]; LL ans; // 线段树节点结构 struct node { LL s; // 区间和 LL mx, lx, rx; // 最大子段和、最大前缀和、最大后缀和 LL mn, ln, rn; // 最小子段和、最小前缀和、最小后缀和 bool tag; // 取反标记 } t[N * 4]; // 合并两个节点信息 node operator+(node x, node y) { node z; z.tag = 0; z.s = x.s + y.s; // 最大子段和:左区间最大、右区间最大、左后缀+右前缀 z.mx = max(max(x.mx, y.mx), x.rx + y.lx); // 最大前缀和:左前缀或左区间和+右前缀 z.lx = max(x.lx, x.s + y.lx); // 最大后缀和:右后缀或右区间和+左后缀 z.rx = max(y.rx, y.s + x.rx); // 最小子段和:左区间最小、右区间最小、左后缀+右前缀 z.mn = min(min(x.mn, y.mn), x.rn + y.ln); // 最小前缀和:左前缀或左区间和+右前缀 z.ln = min(x.ln, x.s + y.ln); // 最小后缀和:右后缀或右区间和+左后缀 z.rn = min(y.rn, y.s + x.rn); return z; } // 构建线段树 void build(int x, int l, int r) { if (l == r) { // 叶子节点初始化:所有值等于a[l] t[x].s = t[x].mn = t[x].mx = t[x].ln = t[x].lx = t[x].rn = t[x].rx = a[l]; t[x].tag = 0; return; } int mid = (l + r) / 2; build(x * 2, l, mid); build(x * 2 + 1, mid + 1, r); t[x] = t[x * 2] + t[x * 2 + 1]; // 合并左右子节点 } // 下传取反标记 void downtag(int x, int l, int r) { if (t[x].tag == 0) return; // 非叶子节点,下传标记到子节点 if (l != r) { t[x * 2].tag ^= 1; t[x * 2 + 1].tag ^= 1; } // 取反操作:交换最大/最小相关值并取反 t[x].s = -t[x].s; swap(t[x].mn, t[x].mx); swap(t[x].ln, t[x].lx); swap(t[x].rn, t[x].rx); t[x].mn = -t[x].mn; t[x].mx = -t[x].mx; t[x].ln = -t[x].ln; t[x].rn = -t[x].rn; t[x].lx = -t[x].lx; t[x].rx = -t[x].rx; t[x].tag = 0; // 清除标记 } // 区间取反操作 void change(int x, int l, int r, int L, int R) { downtag(x, l, r); // 先下传标记 if (L > r || l > R || L > R) return; if (L <= l && r <= R) { t[x].tag ^= 1; // 标记取反 downtag(x, l, r); // 执行取反 return; } int mid = (l + r) / 2; change(x * 2, l, mid, L, R); change(x * 2 + 1, mid + 1, r, L, R); t[x] = t[x * 2] + t[x * 2 + 1]; // 合并子节点信息 } // 查询最大前缀和的右端点 int query1(int x, int l, int r) { if (l == r) return l; downtag(x, l, r); int mid = (l + r) / 2; downtag(x * 2, l, mid); downtag(x * 2 + 1, mid + 1, r); // 最大前缀和来自右子节点的前缀 if (t[x].lx == t[x * 2].s + t[x * 2 + 1].lx) return query1(x * 2 + 1, mid + 1, r); return query1(x * 2, l, mid); // 来自左子节点的前缀 } // 查询最大后缀和的左端点 int query2(int x, int l, int r) { if (l == r) return l; downtag(x, l, r); int mid = (l + r) / 2; downtag(x * 2, l, mid); downtag(x * 2 + 1, mid + 1, r); // 最大后缀和来自左子节点的后缀 if (t[x].rx == t[x * 2 + 1].s + t[x * 2].rx) return query2(x * 2, l, mid); return query2(x * 2 + 1, mid + 1, r); // 来自右子节点的后缀 } // 查询最大子段和的区间 pair<int, int> query(int x, int l, int r) { if (t[x].s == t[x].mx) return {l, r}; // 整个区间是最大子段 downtag(x, l, r); int mid = (l + r) / 2; downtag(x * 2, l, mid); downtag(x * 2 + 1, mid + 1, r); if (t[x * 2].mx == t[x].mx) return query(x * 2, l, mid); // 最大子段在左区间 if (t[x * 2 + 1].mx == t[x].mx) return query(x * 2 + 1, mid + 1, r); // 最大子段在右区间 // 最大子段跨左右区间:左区间的最大后缀 + 右区间的最大前缀 return {query2(x * 2, l, mid), query1(x * 2 + 1, mid + 1, r)}; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); build(1, 1, n); // 构建线段树 for (int i = 1; i <= m; i++) { if (t[1].mx <= 0) break; // 无正和子段,停止选择 auto tmp = query(1, 1, n); // 查询当前最大子段 ans += t[1].mx; // 累加结果 change(1, 1, n, tmp.first, tmp.second); // 取反该子段 } printf("%lld", ans); return 0; }代码解释
- 线段树节点:存储区间和、最大/最小子段和、前缀/后缀和及取反标记。
- 合并操作:合并左右子节点信息,计算当前节点的最大/最小相关值。
- 区间取反:通过标记实现高效取反,交换最大/最小相关值并取反。
- 贪心选择:每次选最大和子段,累加后取反该子段,重复 (m) 次。
- 1
信息
- ID
- 5655
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者