权值线段树

一、 核心共性模型:权值线段树 (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)。将两者收益相加,取最大值即为局部最优。
  • 离散化避坑:因为数值可能很大,直接建权值线段树会爆内存,所以代码中先用了 sortunique 对所有数值进行了离散化压缩,映射到 [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))。
  • 数据结构协同
    • 权值线段树:无视类型,动态维护全场所有元素的 cntsum,专门用来秒查 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
点击查看代码
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;
	   }
}

💡 终极总结

当你遇到“动态增删元素”且需要“频繁计算最大/最小的若干个元素之和”,或者“拥有若干次翻倍/增益机会”的题目时:

  1. 首选结构:权值线段树(如果值域过大记得先离散化)。
  2. 核心函数:手写 queryTopK 递归查右补左。
  3. 防坑指南:警惕贪心失效的边界,用 multiset 辅助维护最值,必要时进行“强行替换”的逻辑修正。
posted @ 2026-05-15 20:41  r_123  阅读(13)  评论(0)    收藏  举报