论线段树 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)\) 进行更新。
第二条是因为:
- 每一个 \(tag\) 都要对其它所有 \(tag\) 造成影响,这样
add_tag补充了push_down时没有进行的更新。 - 如果一个 \(tag_{p_j}\) 被 \(tag_{p_i}\) 两次更新(被
push_down和add_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。
本文来自博客园,作者:cshur,转载请注明原文链接:https://www.cnblogs.com/cshur/p/19665367。
cshur 转载请注明作者

浙公网安备 33010602011771号