权值线段树
一、 核心共性模型:权值线段树 (Value-Domain Segment Tree)
这两道题的底层核心都是权值线段树。
普通的线段树是以“数组的下标”建树,维护的是某个区间内的信息;而权值线段树是以“元素的值域”建树。
- 节点维护信息:
cnt:当前数值区间内有多少个元素。sum:当前数值区间内所有元素的总和。
- 杀手锏功能 (queryTopK):能够在动态增删元素的情况下,以极快的对数时间复杂度查找当前集合中最大的 $K$ 个元素的总和。
- 游走逻辑:查询时优先看右子树(因为右子树代表更大的数值)。如果右子树的
cnt$\ge K$,直接向右递归;如果不足 $K$,则全盘接收右子树的sum,并向左子树继续索要剩下的 $K - cnt_{right}$ 个元素。
- 游走逻辑:查询时优先看右子树(因为右子树代表更大的数值)。如果右子树的
二、 2026深圳D:分段贪心与动态候选池
1. 题面抽象
给定一个序列,其中包含普通数值元素和特殊的“乘法/增益操作符”。特殊操作符可以与前面出现的若干个元素产生倍增效果。需要在从左到右遍历的过程中,动态决定将哪些数值分配给操作符,以求得全局最大收益。
2. 核心思路
- 按操作符分段:代码将整个序列以“特殊操作符 (W)”为界限划分成多个段进行处理。
- 动态候选池:把前面已经遍历过、暂时未被操作符消耗掉的元素全部扔进权值线段树中,作为“候选池”。
- 枚举与 Top-K 贪心:
每遇到一个操作符,我们面临一个分配问题:是把当前段新出现的数值与操作符结合,还是从历史候选池中选?
做法是:枚举分配给当前段的结合次数 $i$,那么剩下的 $cnt - i$ 次结合机会就去历史候选池中找最大的,即直接调用线段树的queryTopK(cnt - i)。将两者收益相加,取最大值即为局部最优。 - 离散化避坑:因为数值可能很大,直接建权值线段树会爆内存,所以代码中先用了
sort和unique对所有数值进行了离散化压缩,映射到[1, n]的范围内再建树。
点击查看代码
vector<int> h;
int get_id(int x) {
return lower_bound(h.begin(), h.end(), x) - h.begin();
}
struct Tag {
int cnt = 0;
int sum = 0;
void apply(const Tag &t) & {}
};
struct Info {
int cnt = 0;
int sum = 0;
void apply(const Tag &t) & {
cnt += t.cnt;
sum += t.sum;
}
};
Info operator+(const Info &a, const Info &b) {
Info res;
res.cnt = a.cnt + b.cnt;
res.sum = a.sum + b.sum;
return res;
}
struct SegmentTree {
vector<Info> info;
vector<Tag> tag;
int n;
SegmentTree(int size) {
n = size;
info.assign(4 * (n + 5), Info{});
tag.assign(4 * (n + 5), Tag{});
build(1, 0, n);
}
void build(int p, int l, int r) {
if (r - l == 1) {
info[p] = Info();
return;
}
int m = (l + r) / 2;
build(2 * p, l, m);
build(2 * p + 1, m, r);
pull(p);
}
void pull(int p) {
info[p] = info[2 * p] + info[2 * p + 1];
}
void applyNode(int p, const Tag &v) {
info[p].apply(v);
tag[p].apply(v);
}
void push(int p) {
applyNode(2 * p, tag[p]);
applyNode(2 * p + 1, tag[p]);
tag[p] = Tag();
}
void modify(int p, int l, int r, int x, const Info &v) {
if (r - l == 1) {
info[p] = v;
return;
}
int m = (l + r) / 2;
push(p);
if (x < m) {
modify(2 * p, l, m, x, v);
} else {
modify(2 * p + 1, m, r, x, v);
}
pull(p);
}
Info rangeQuery(int p, int l, int r, int x, int y) {
if (l >= y || r <= x) {
return Info();
}
if (l >= x && r <= y) {
return info[p];
}
int m = (l + r) / 2;
push(p);
return rangeQuery(2 * p, l, m, x, y) + rangeQuery(2 * p + 1, m, r, x, y);
}
int queryTopK(int k) {
return queryTopK(1, 0, n, k);
}
int queryTopK(int p, int l, int r, int k) {
if (k <= 0) return 0;
if (info[p].cnt <= k) {
return info[p].sum;
}
if (r - l == 1) {
return k * h[l];
}
int m = (l + r) / 2;
push(p);
int right_cnt = info[2 * p + 1].cnt;
if (right_cnt >= k) {
return queryTopK(2 * p + 1, m, r, k);
} else {
return info[2 * p + 1].sum + queryTopK(2 * p, l, m, k - right_cnt);
}
}
void modify(int x, const Info &v) {
modify(1, 0, n, x, v);
}
Info rangeQuery(int x, int y) {
return rangeQuery(1, 0, n, x, y);
}
};
struct Node {
char op;
int x;
};
void solve() {
int n;
cin >> n;
SegmentTree seg(n + 1);
vector<Node> a(n + 1);
h.clear();
for (int i = 1; i <= n; i++) {
cin >> a[i].op;
if (a[i].op == 'F') {
cin >> a[i].x;
h.push_back(a[i].x);
}
}
sort(h.begin(), h.end());
h.erase(unique(h.begin(), h.end()), h.end());
int cnt = 0;
int ans = 0;
for (int l = 1; l <= n; l++) {
auto [op, x] = a[l];
if (op == 'W') {
cnt++;
int j = l + 1;
while (j <= n && a[j].op != 'W') {
// 处理
j++;
}
vector<int> V;
for (int k = l + 1; k < j; k++) {
// cout << a[k].x << endl;
V.push_back(a[k].x * (j - k));
}
sort(V.begin(), V.end(), greater<int>());
for (int i = 1; i < V.size(); i++) {
// cout << V[i] << endl;
V[i] += V[i - 1];
}
V.insert(V.begin(), 0);
int len = V.size() - 1ll;
int mx = -1e18;
for (int i = 0; i <= min(len, cnt); i++) {
int A = V[i];
int B = seg.queryTopK(cnt - i) * (j - l);
mx = max(A + B, mx);
}
ans += mx;
for (int k = l + 1; k < j; k++) {
auto id = get_id(a[k].x);
auto ww = seg.rangeQuery(id, id + 1);
ww.cnt++;
ww.sum += a[k].x;
seg.modify(id, ww);
}
l = j - 1;
} else {
auto id = get_id(a[l].x);
auto ww = seg.rangeQuery(id, id + 1);
ww.cnt++;
ww.sum += a[l].x;
seg.modify(id, ww);
}
}
cout << ans << endl;
}
三、 F_Egoism:多类别集合的动态增益与边界修正
1. 题面抽象
动态维护一个包含两种类型元素的集合(类型 1 为普通,类型 2 带增益),支持单点修改(同时修改元素的值和类型)。类型 2 的元素可以提供一种“让集合内其他元素额外计算一次”的增益效果。求每次修改后,集合能产生的最大总和。
2. 核心思路
- 基础池与增益池分离:
首先,无论元素是啥类型,它自己的基础值一定会加到总和中。
其次,如果有 $K$ 个类型 2 的元素,相当于获得了 $K$ 次“白嫖”额外元素值的机会。根据贪心原则,这 $K$ 次机会当然要用在全场最大的 $K$ 个元素上(调用线段树的queryTopK(K))。 - 数据结构协同:
- 权值线段树:无视类型,动态维护全场所有元素的
cnt和sum,专门用来秒查 Top-K 增益。 - std::multiset:开两个 set,专门用来分类维护类型 1 和类型 2 元素的最值,为后续的“极端情况修正”做准备。
- 权值线段树:无视类型,动态维护全场所有元素的
- 极值边界修正 (思维难点/Corner Case):
贪心策略“直接取前 $K$ 大”在一种极端情况下会翻车:如果全场前 $K$ 大的元素全都是类型 2 怎么办?
通常逻辑中,增益元素不能无限对自己或者互相套娃。如果类型 2 的最小值(最弱的增益)甚至都大于类型 1 的最大值(最强的普通底子),说明我们的 Top-K 贪心“过度透支”了不合法的增益关系。- 修正逻辑:必须强行把那个最弱的类型 2 踢出增益位,换上最强的类型 1。即
ans = sum + bonus - min_2 + max_1。
- 修正逻辑:必须强行把那个最弱的类型 2 踢出增益位,换上最强的类型 1。即
点击查看代码
template<class Info, class Tag>
struct LazySegmentTree {
int n;
std::vector<Info> info;
std::vector<Tag> tag;
LazySegmentTree() : n(0) {}
LazySegmentTree(int n_, Info v_ = Info()) {
init(n_, v_);
}
template<class T>
LazySegmentTree(std::vector<T> init_) {
init(init_);
}
void init(int n_, Info v_ = Info()) {
init(std::vector(n_, v_));
}
template<class T>
void init(std::vector<T> init_) {
n = init_.size();
info.assign(4 << std::__lg(n), Info());
tag.assign(4 << std::__lg(n), Tag());
std::function<void(int, int, int)> build = [&](int p, int l, int r) {
if (r - l == 1) {
info[p] = init_[l];
return;
}
int m = (l + r) / 2;
build(2 * p, l, m);
build(2 * p + 1, m, r);
pull(p);
};
build(1, 0, n);
}
void pull(int p) {
info[p] = info[2 * p] + info[2 * p + 1];
}
void apply(int p, const Tag &v) {
info[p].apply(v);
tag[p].apply(v);
}
void push(int p) {
apply(2 * p, tag[p]);
apply(2 * p + 1, tag[p]);
tag[p] = Tag();
}
void modify(int p, int l, int r, int x, const Info &v) {
if (r - l == 1) {
info[p] = v;
return;
}
int m = (l + r) / 2;
push(p);
if (x < m) {
modify(2 * p, l, m, x, v);
} else {
modify(2 * p + 1, m, r, x, v);
}
pull(p);
}
void modify(int p, const Info &v) {
modify(1, 0, n, p, v);
}
void rangeApply(int p, int l, int r, int x, int y, const Tag &v) {
if (l >= y || r <= x) {
return;
}
if (l >= x && r <= y) {
apply(p, v);
return;
}
int m = (l + r) / 2;
push(p);
rangeApply(2 * p, l, m, x, y, v);
rangeApply(2 * p + 1, m, r, x, y, v);
pull(p);
}
void rangeApply(int l, int r, const Tag &v) {
return rangeApply(1, 0, n, l, r, v);
}
//x y 是要查的 l r 是扫到哪了
Info rangeQuery(int p, int l, int r, int x, int y) {
//超出区间
if (l >= y || r <= x) {
return Info();
}
//在区间中
if (l >= x && r <= y) {
return info[p];
}
int m = (l + r) / 2;
push(p);
return rangeQuery(2 * p, l, m, x, y) + rangeQuery(2 * p + 1, m, r, x, y);
}
Info rangeQuery(int l, int r) {
return rangeQuery(1, 0, n, l, r);
}
int queryTopK(int k) {
return queryTopK(1, 0, n, k);
}
int queryTopK(int p, int l, int r, int k) {
if(k <= 0) return 0;
if(info[p].cnt <= k){
return info[p].sum;
}
if(r - l == 1){
return k * l;
}
int m = (l + r)/2;
push(p);
int right_cnt = info[2*p + 1].cnt;
if(right_cnt >= k){
return queryTopK(2*p + 1,m,r,k);
}
else{
return info[2*p + 1].sum + queryTopK(2*p,l,m,k - right_cnt);
}
}
};
struct Tag {
int cnt = 0;
int sum = 0;
void apply(const Tag &t) & {
// x = std::max(x, t.x);
}
};
struct Info {
int cnt = 0;
int sum = 0;
void apply(const Tag &t) & {
// x = std::max(x, t.x);
cnt += t.cnt;
sum += t.sum;
}
};
Info operator+(const Info &a, const Info &b) {
Info res;
res.cnt = a.cnt + b.cnt;
res.sum = a.sum + b.sum;
return res;
}
const int N = 1e6 + 10;
void solve() {
int n;int q;
int k = 0;
cin >> n >> q;
LazySegmentTree<Info,Tag> seg(N);
vector<int> a(n+1),b(n+1);
int sum = 0;
multiset<int> ty[3];
for(int i=1;i<=n;i++){
cin >> a[i] >> b[i];
sum += a[i];
ty[b[i]].insert(a[i]);
Tag t = {1,a[i]};
seg.rangeApply(a[i],a[i] + 1,t);
if(b[i] == 2) k ++ ;
}
while(q -- ){
int i;
cin >> i;
int na,nb;
cin >> na >> nb;
auto it = ty[b[i]].find(a[i]);
ty[b[i]].erase(it);
ty[nb].insert(na);
sum -= a[i];
sum += na;
if(b[i] == 1 && nb == 2) k ++;
else if(b[i] == 2 && nb == 1) k -- ;
seg.rangeApply(a[i],a[i] + 1,{-1,-a[i]});
seg.rangeApply(na,na+1,{1,na});
a[i] = na;
b[i] = nb;
int ans ;
if(k == 0){
ans = sum;
}
else if(k == n){
ans = sum + seg.queryTopK(n-1);
}
else{
int bonus = seg.queryTopK(k);
int max_1 = *ty[1].rbegin();
int min_2 = *ty[2].begin();
ans = sum + bonus;
if(min_2 > max_1){
ans = sum + bonus - min_2 + max_1;
}
}
cout << ans << endl;
}
}
💡 终极总结
当你遇到“动态增删元素”且需要“频繁计算最大/最小的若干个元素之和”,或者“拥有若干次翻倍/增益机会”的题目时:
- 首选结构:权值线段树(如果值域过大记得先离散化)。
- 核心函数:手写
queryTopK递归查右补左。 - 防坑指南:警惕贪心失效的边界,用
multiset辅助维护最值,必要时进行“强行替换”的逻辑修正。

浙公网安备 33010602011771号