论线段树 tag 的优先级

线段树在多种区间修改时要使用多个 lazy_tag,在 push_down 的时候将会出现多个 tag 更新的次序问题,在以往题解中,被称为 tag 的优先级。

事实上,很多题解在 P3373【模板】线段树 2 一题中,对于加、乘两种操作优先级的解释为:乘法优先级高于加法。但是,“乘法优先级高于加法”中的“优先级”是数学表达式中的,不应当混为一谈。很多题解构造除了一种正确的更新方式,但未阐明其它更新方式的错误之处。本文将探讨“更新的优先级”的本质。

约定

  • 设有 \(n\) 个区间修改操作,分别采用 \(tag_i\) 进行维护。
  • 定义:优先级为 \(tag_i\) 执行的顺序,其执行的序数越前,优先级越高;反之,优先级越低。
  • 定义:\(tag\) 间的二元关系符 \(>\) 表示其优先级较高,\(<\) 表示其优先级较低。

1 观察

1.1 更新方式

我们发现,\(tag\) 只会在两处被修改,一个是 add_tag 中,一个是 push_down 中。为了讨论方便,这里放下 P3373【模板】线段树 2 相应代码,以便讨论。

class Segment_Tree
{// Turn: add[l,r], mu[l,r]; Query: sum[l,r]
    private:
        static const int M = (N << 2);
        ll t[M],tags[M],tagm[M];
        inline int ls(int p) { return p << 1; }
        inline int rs(int p) { return p << 1 | 1; }
        inline void push_up(int p) {
            t[p] = (t[ls(p)] + t[rs(p)]) % mod;
        }
        inline void add_tag_s(int p,int pl,int pr,ll v) {
            tags[p] = (tags[p] + v) % mod,
            t[p] = (t[p] + v * (pr - pl + 1ll) % mod) % mod;
        }
        inline void add_tag_m(int p,int pl,int pr,ll v) {
            tagm[p] = tagm[p] * v % mod,t[p] = t[p] * v % mod;
            tags[p] = tags[p] * v % mod;
        }
        inline void push_down(int p,int pl,int pr)
        {
            if(tagm[p] != 1)
            {
                int mid = (pl + pr) >> 1;
                add_tag_m(ls(p),pl,mid,tagm[p]);
                add_tag_m(rs(p),mid + 1,pr,tagm[p]);
                tagm[p] = 1;
            }
            if(tags[p] != 0)
            {
                int mid = (pl + pr) >> 1;
                add_tag_s(ls(p),pl,mid,tags[p]);
                add_tag_s(rs(p),mid + 1,pr,tags[p]);
                tags[p] = 0;
            }
        }
    public:
        void tadd(int l,int r,ll v,int p,int pl,int pr)
        {
            if(l <= pl && pr <= r)
            {
                add_tag_s(p,pl,pr,v);
                return;
            }
            int mid = (pl + pr) >> 1; push_down(p,pl,pr);
            if(l <= mid) tadd(l,r,v,ls(p),pl,mid);
            if(mid < r) tadd(l,r,v,rs(p),mid + 1,pr);
            push_up(p);
        }
        void tmu(int l,int r,ll v,int p,int pl,int pr)
        {
            if(l <= pl && pr <= r)
            {
                add_tag_m(p,pl,pr,v);
                return;
            }
            int mid = (pl + pr) >> 1; push_down(p,pl,pr);
            if(l <= mid) tmu(l,r,v,ls(p),pl,mid);
            if(mid < r) tmu(l,r,v,rs(p),mid + 1,pr);
            push_up(p);
        }
        ll qsum(int l,int r,int p,int pl,int pr)
        {
            if(l <= pl && pr <= r) return t[p];
            int mid = (pl + pr) >> 1; ll ans = 0; push_down(p,pl,pr);
            if(l <= mid) ans = (ans + qsum(l,r,ls(p),pl,mid)) % mod;
            if(mid < r) ans = (ans + qsum(l,r,rs(p),mid + 1,pr)) % mod;
            return ans;
        }
} T;

以这题为例,我们发现能对 \(tag\) 造成影响的函数无非两种,一个是 add_tag,一个是 push_down

  • add_tag,一个 \(tag\) 对几个 \(tag\) 进行更新
  • push_down,一个 \(tag\) 对前面 push_down 过的 \(tag\) 进行更新。

第二条较难理解,例如 \(base \times 6 + 2\),按先乘后加的顺序,则 \(+2\) 是建立在 \(\times 6\) 的基础上。难以理解的是,加为什么更新了乘?这是因为 \(base \times 6 + 2\) 等价于 \(base \times (6 + \frac2{base})\),相当于 tagmul += 2.0 / base。联系下文仔细体会。

1.2 影响关系

P3373【模板】线段树 2 中的题解提到,加法操作的 \(tag\) 对乘法操作没有影响,但是这对吗?

所有操作间会相互影响。

结合 1.1 可知,加法对乘法的影响。
在 1.1 中,我们已然提到 push_down 时 的 \(tag\) 会对前面 push_down 过的 \(tag\) 进行更新,其实,这个更新没有直接更新 \(tag\) 的值,而是对整个 \(tag\) 集体造成的影响造成影响。有点绕,\(tag\) 其核心作用就是对子节点的 \(tag\)\(tree\) 值造成影响,而 1.1 中加法的影响就作用在 push_down 中,影响了子节点的 \(tag\)\(tree\) 的值,其实也是影响了本层的乘法的 \(tag\)

这个过程需要读者仔细体会,用心理解。

2 分析

设按优先级排序后为:\(tag_{p_1} > tag_{p_2} > ... > tag_{p_{n - 1}} > tag_{p_n}\)

  • push_down:对于 \(tag_{p_i}\),对 \(tag_{p_j}(1 \le j < i)\) 进行更新。
  • add_tag:对于 \(tag_{p_i}\),对 \(tag_{p_j}(i < j \le n)\) 进行更新。

第二条是因为:

  1. 每一个 \(tag\) 都要对其它所有 \(tag\) 造成影响,这样 add_tag 补充了 push_down 时没有进行的更新。
  2. 如果一个 \(tag_{p_j}\)\(tag_{p_i}\) 两次更新(被 push_downadd_tag 更新),那么更新会叠加导致错误。

所以我们对于任意优先级排列得到了一个正确的更新的方案。
所以,所谓的“优先级”是不存在的,你可以使用任意顺序更新,但是要按照相应方案。

3 应用

3.1 本题

为什么本题使用先乘后加?
并不是因为优先级,而是因为先加后乘不好维护。
如果使用先加后乘,大概是这样的代码:

inline void add_tag_s(int p,int pl,int pr,ll v) {
    tags[p] = (tags[p] + v) % mod,
    t[p] = (t[p] + v * (pr - pl + 1ll) % mod) % mod;
    tagm[p] = (tagm[p] + v / t[p]) % mod;
}
inline void add_tag_m(int p,int pl,int pr,ll v) {
    tagm[p] = tagm[p] * v % mod,t[p] = t[p] * v % mod;
    //tags[p] = tags[p] * v % mod; 不需要这一行
}
inline void push_down(int p,int pl,int pr)
{//调换顺序
    if(tags[p] != 0)
    {
        int mid = (pl + pr) >> 1;
        add_tag_s(ls(p),pl,mid,tags[p]);
        add_tag_s(rs(p),mid + 1,pr,tags[p]);
        tags[p] = 0;
    }
    if(tagm[p] != 1)
    {
        int mid = (pl + pr) >> 1;
        add_tag_m(ls(p),pl,mid,tagm[p]);
        add_tag_m(rs(p),mid + 1,pr,tagm[p]);
        tagm[p] = 1;
    }
}

注意高亮一行,这里更新会用到逆元,而且还要知道原本的值,难以实现,复杂度也不好保证。

3.2 赋值 + 加

有些人常说赋值优先级高于加法。
如果我们设加法优先级高于赋值,则在加法的 add_tag 中,将赋值的 \(tag\) 相应累加;在 push_down 中,先 push_down 加法的 \(tag\) 即可。

例题:P1253 扶苏的问题

代码(先赋值后加,放这里作比较,可以跳过,先加后赋值的在后面,差别主要在高亮部分):

#include <bits/stdc++.h>
using namespace std;

#define int long long

namespace Fast_read
{
	//快读略。
}
using namespace Fast_read;

#define cin fin

const int N = 1e6+5;

int n,q; 
int a[N];

const int S = N<<2;
const int INF = -(1e18)-5;
int root;
struct Node
{
	int v,ls,rs;
	void init()
	{
		v = ls = rs = 0;
	}
};
Node tr[S];
int tag1[S],tag2[S];
int& ls(int x) { return tr[x].ls; }
int& rs(int x) { return tr[x].rs; }
stack<int> Tree_stack;
void init_tree()
{
	for(int i = S-1; i >= 1; i--)
		Tree_stack.push(i);
	for(int i = S-1; i >= 1; i--)
		tag1[i] = INF;
}
int New()
{
	if(Tree_stack.empty())
	{
		cout << "Tree Stack Empty. \n";
		cout << "Memory Limit Error. \n";
		exit(0);
	}
	int t = Tree_stack.top();
	Tree_stack.pop();
	return t;
}
void Del(int x) { tr[x].init(); Tree_stack.push(x); }

void add_tag1(int p,int x)
{
	tr[p].v = x;
	tag1[p] = x;
	tag2[p] = 0;
}
void add_tag2(int p,int l,int r,int x)
{
	tr[p].v += x;
	tag2[p] += x;
}
void push_down(int p,int l,int r)
{
	if(tag1[p] != INF)
	{
		add_tag1(ls(p),tag1[p]);
		add_tag1(rs(p),tag1[p]);
		tag1[p] = INF;
	}
	if(tag2[p])
	{
		int mid = (l+r) >> 1;
		if(l <= mid) add_tag2(ls(p),l,mid,tag2[p]);
		if(mid < r) add_tag2(rs(p),mid+1,r,tag2[p]);
		tag2[p] = 0;
	}
}
void push_up(int p)
{
	tr[p].v = max(tr[ls(p)].v,tr[rs(p)].v);
}

int build(int p,int pl,int pr)
{
	if(!p) p = New();
	if(pl == pr)
	{
		tr[p].v = a[pl];
		return p;
	}
	int mid = (pl+pr) >> 1;
	if(pl <= mid) tr[p].ls = build(ls(p),pl,mid);
	if(mid < pr) tr[p].rs = build(rs(p),mid+1,pr);
	push_up(p);
	return p;
}
void build()
{
	root = build(root,1,n);
}
void turn(int l,int r,int v,int p,int pl,int pr)
{
	if(l <= pl && pr <= r)
	{
		add_tag1(p,v);
		return;
	}
	push_down(p,pl,pr);
	int mid = (pl+pr) >> 1;
	if(l <= mid) turn(l,r,v,ls(p),pl,mid);
	if(mid < r) turn(l,r,v,rs(p),mid+1,pr);
	push_up(p);
}
void turn(int l,int r,int v)
{
	turn(l,r,v,1,1,n);
}
void add(int l,int r,int v,int p,int pl,int pr)
{
	if(l <= pl && pr <= r)
	{
		add_tag2(p,pl,pr,v);
		return;
	}
	push_down(p,pl,pr);
	int mid = (pl+pr) >> 1;
	if(l <= mid) add(l,r,v,ls(p),pl,mid);
	if(mid < r) add(l,r,v,rs(p),mid+1,pr);
	push_up(p);
}
void add(int l,int r,int v)
{
	add(l,r,v,1,1,n);
}
int query(int l,int r,int p,int pl,int pr)
{
	if(l <= pl && pr <= r) return tr[p].v;
	push_down(p,pl,pr);
	int mid = (pl+pr) >> 1;
	int maxn = INF;
	if(l <= mid) maxn = max(maxn,query(l,r,ls(p),pl,mid));
	if(mid < r) maxn = max(maxn,query(l,r,rs(p),mid+1,pr));
	return maxn;
}
int query(int l,int r)
{
	return query(l,r,1,1,n);
}

signed main()
{
	cin >> n >> q;
	for(int i = 1; i <= n; i++)
		cin >> a[i];
	
	init_tree();
	build();
	
	int op,l,r,x;
	while(q--)
	{
		cin >> op;
		if(op == 1) cin >> l >> r >> x,turn(l,r,x);
		if(op == 2) cin >> l >> r >> x,add(l,r,x);
		if(op == 3) cin >> l >> r,printf("%lld\n",query(l,r));
	}
    return 0;
}

代码(先加后赋值):

#include <bits/stdc++.h>
using namespace std;

#define int long long

namespace Fast_read
{
	//快读略。
}
using namespace Fast_read;

#define cin fin

const int N = 1e6+5;

int n,q; 
int a[N];

const int S = N<<2;
const int INF = -(1e18)-5;
int root;
struct Node { int v = 0,ls = 0,rs = 0; void init() { v = ls = rs = 0; }};
Node tr[S]; int tag1[S],tag2[S];
inline int& ls(int x) { return tr[x].ls; }
inline int& rs(int x) { return tr[x].rs; }
stack<int> Tree_stack;
inline void init_tree()
{
	for(int i = S-1; i >= 1; i--)
		Tree_stack.push(i);
	for(int i = S-1; i >= 1; i--)
		tag1[i] = INF;
}
inline int New()
{
//	if(Tree_stack.empty())
//	{
//		cout << "Tree Stack Empty. \n";
//		cout << "Memory Limit Error. \n";
//		exit(0);
//	}
	int t = Tree_stack.top();
	Tree_stack.pop();
	return t;
}
inline void Del(int x) { tr[x].init(); Tree_stack.push(x); }

inline void add_tag1(int p,int x)
{
	tr[p].v = x;
	tag1[p] = x;
	//tag2[p] = 0;
}
inline void add_tag2(int p,int l,int r,int x)
{
	tr[p].v += x;
	tag2[p] += x;
	if(tag1[p] != INF) tag1[p] += x;
}
inline void push_down(int p,int l,int r)
{
	if(tag2[p])
	{
		int mid = (l+r) >> 1;
		if(l <= mid) add_tag2(ls(p),l,mid,tag2[p]);
		if(mid < r) add_tag2(rs(p),mid+1,r,tag2[p]);
		tag2[p] = 0;
	}
	if(tag1[p] != INF)
	{
		add_tag1(ls(p),tag1[p]);
		add_tag1(rs(p),tag1[p]);
		tag1[p] = INF;
	}
}
inline void push_up(int p) {
	tr[p].v = max(tr[ls(p)].v,tr[rs(p)].v);
}

int build(int p,int pl,int pr)
{
	if(!p) p = New();
	if(pl == pr)
	{
		tr[p].v = a[pl];
		return p;
	}
	int mid = (pl+pr) >> 1;
	if(pl <= mid) tr[p].ls = build(ls(p),pl,mid);
	if(mid < pr) tr[p].rs = build(rs(p),mid+1,pr);
	push_up(p);
	return p;
}
void turn(int l,int r,int v,int p,int pl,int pr)
{
	if(l <= pl && pr <= r)
	{
		add_tag1(p,v);
		return;
	}
	push_down(p,pl,pr);
	int mid = (pl+pr) >> 1;
	if(l <= mid) turn(l,r,v,ls(p),pl,mid);
	if(mid < r) turn(l,r,v,rs(p),mid+1,pr);
	push_up(p);
}
void add(int l,int r,int v,int p,int pl,int pr)
{
	if(l <= pl && pr <= r)
	{
		add_tag2(p,pl,pr,v);
		return;
	}
	push_down(p,pl,pr);
	int mid = (pl+pr) >> 1;
	if(l <= mid) add(l,r,v,ls(p),pl,mid);
	if(mid < r) add(l,r,v,rs(p),mid+1,pr);
	push_up(p);
}
int query(int l,int r,int p,int pl,int pr)
{
	if(l <= pl && pr <= r) return tr[p].v;
	push_down(p,pl,pr);
	int mid = (pl+pr) >> 1;
	int maxn = INF;
	if(l <= mid) maxn = max(maxn,query(l,r,ls(p),pl,mid));
	if(mid < r) maxn = max(maxn,query(l,r,rs(p),mid+1,pr));
	return maxn;
}

signed main()
{
//	freopen("P1253_2.in","r",stdin);
//	freopen("P1253_2.txt","w",stdout);
	 
	cin >> n >> q;
	for(int i = 1; i <= n; i++)
		cin >> a[i];
	
	init_tree();
	root = build(root,1,n);
	
	int op,l,r,x;
	while(q--)
	{
		cin >> op;
		if(op == 1) cin >> l >> r >> x,turn(l,r,x,1,1,n);
		if(op == 2) cin >> l >> r >> x,add(l,r,x,1,1,n);
		if(op == 3) cin >> l >> r,printf("%lld\n",query(l,r,1,1,n));
	}
    return 0;
}

代码是以前的代码改的,码风有些丑陋。

类似题(本篇文章起源于作者被这题整雾了):P2572,留给读者思考。

4 总结

在使用多种操作的 \(tag\) 时,要考虑每个 \(tag\) 对其它 \(tag\) 的影响,自行排列合适的优先级序列。

add_tag 中,应当更新且只更新优先级比本身小的 \(tag\)
push_down 中,应当按照优先级序列进行更新。

这也启发着我们一定要深入思考算法的原理本质,而不只是停留在表面,一知半解、感觉对就对。

谢谢大家,留个赞再走呗 qwq。

posted @ 2026-03-03 22:48  cshur  阅读(2)  评论(0)    收藏  举报