需要支持多种操作的线段树该如何确定运算顺序?
先來看一道最簡單的加乘標記:
\(\huge\text{點我看題}\)
本題需要我們進行加法,乘法的在線修改以及查詢?nèi)∧:蟮慕Y果。因為加法和乘法對于取模運算來說是不受限制的,即可以隨時在操作過程中進行取模操作。
對于在線修改,我第一個想到的是lazytag(延遲標記)。求出某區(qū)間內(nèi)的值保存在懶標內(nèi),幾乎可以達到
\(O(N\log N)\)的時間復雜度。
因為本題需要支持加法和乘法操作,因此我們使用兩個懶標,分別存儲加法和乘法后的數(shù)值,pushdown時按照某種先后順序下放即可。
經(jīng)過分析,我們有以下兩種選擇:
但是這樣的話,更新操作并不方便,并且計算過程中會出現(xiàn)小數(shù)而出現(xiàn)精度誤差,因此這種操作是不優(yōu)的。
這樣操作的話,不會出現(xiàn)精度誤差,故我們選擇乘法優(yōu)先。
總代碼如下
#include<bits/stdc++.h> #define MAXN 100005 #define mid ((l+r)>>1) using namespace std;int n,m,mod,flag,x,y,z; long long a[MAXN];struct tree {long long v,mul,add;//數(shù)據(jù),乘法懶標,加法懶標 }t[4*MAXN];void build(int root,int l,int r) {t[root].add=0;t[root].mul=1;//初始化懶標if (l==r) t[root].v=a[l];else{build(root<<1,l,mid);build(root<<1|1,mid+1,r);t[root].v=t[root<<1].v+t[root<<1|1].v;}t[root].v%=mod;return; }//初始化建樹void pushdown(int root,int l,int r)//標記下放 {t[root<<1].v=(t[root<<1].v*t[root].mul+t[root].add*(mid-l+1))%mod;t[root<<1|1].v=(t[root<<1|1].v*t[root].mul+t[root].add*(r-mid))%mod;//更新值t[root<<1].add=(t[root<<1].add*t[root].mul+t[root].add)%mod;//左兒子的加法標記t[root<<1|1].add=(t[root<<1|1].add*t[root].mul+t[root].add)%mod;//右兒子的加法標記t[root<<1].mul=(t[root<<1].mul*t[root].mul)%mod;//左兒子的乘法標記t[root<<1|1].mul=(t[root<<1|1].mul*t[root].mul)%mod;//右兒子的乘法標記t[root].add=0;t[root].mul=1;//清空標記 }void addition(int root,int now_l,int now_r,int l,int r,long long k) {if(l>now_r||r<now_l) return;//無重疊部分if(l<=now_l&&r>=now_r)//部分重疊{t[root].add=(t[root].add+k)%mod;//修改加法標記t[root].v=(t[root].v+k*(now_r-now_l+1))%mod;//修改當前點return;}pushdown(root,now_l,now_r);int Mid=(now_l+now_r)>>1;addition(root<<1,now_l,Mid,l,r,k);addition(root<<1|1,Mid+1,now_r,l,r,k);//二分進行加法操作t[root].v=(t[root<<1].v+t[root<<1|1].v)%mod;return; }void multiplication(int root,int now_l,int now_r,int l,int r,long long k) {if(l>now_r||r<now_l) return;//無重疊部分if(l<=now_l&&r>=now_r)//部分重疊{t[root].v=(t[root].v*k)%mod;//修改當前點t[root].add=(t[root].add*k)%mod;//修改加法標記t[root].mul=(t[root].mul*k)%mod;//修改乘法標記return;}pushdown(root,now_l,now_r);int Mid=(now_l+now_r)>>1;multiplication(root<<1,now_l,Mid,l,r,k);multiplication(root<<1|1,Mid+1,now_r,l,r,k);//二分進行乘法操作t[root].v=(t[root<<1].v+t[root<<1|1].v)%mod;return; }long long query(int root,int now_l,int now_r,int l,int r) {if(l>now_r||r<now_l) return 0;//無重疊部分if(l<=now_l&&r>=now_r) return t[root].v;//部分重疊pushdown(root,now_l,now_r);int Mid=(now_l+now_r)>>1;return (query(root<<1,now_l,Mid,l,r)+query(root<<1|1,Mid+1,now_r,l,r))%mod; }template<class T> inline void read(T &re) {re=0;T sign=1;char tmp;while((tmp=getchar())&&(tmp<'0'||tmp>'9')) if(tmp=='-') sign=-1;re=tmp-'0';while((tmp=getchar())&&(tmp>='0'&&tmp<='9')) re=re*10+(tmp-'0');re*=sign; }int main() {read(n);read(m);read(mod);for(register int i=1;i<=n;i++) read(a[i]);build(1,1,n);for(register int i=1;i<=m;i++){read(flag);if(flag==1) {read(x);read(y);read(z);multiplication(1,1,n,x,y,z);}else if(flag==2){read(x);read(y);read(z);addition(1,1,n,x,y,z);}else if(flag==3){read(x);read(y);printf("%lld\n",query(1,1,n,x,y));}}return 0; }根據(jù) @初學C++的本間芽衣子 的建議,本文有了下面的擴展內(nèi)容:
\[\text{hdu4578}\]
There are n integers, a1,a2,…, an. The initial values of them are 0. There are four kinds of operations.
Operation 1: Add c to each number between a x and a y inclusive. In other words, do transformation a k<---a k+c, k=x,x+1,…,y.
Operation 2: Multiply c to each number between a x and a y inclusive. In other words, do transformation a k<---a k×c, k = x,x+1,…,y.
Operation 3: Change the numbers between a x and a y to c, inclusive. In other words, do transformation a k<---c, k = x,x+1,…,y.
Operation 4: Get the sum of p power among the numbers between a x and a y inclusive. In other words, get the result of a xp+a x+1p+…+a yp.
大意:
對于一個區(qū)間有4個操作:
與上題類似,本題的本質(zhì)是線段樹的區(qū)間更新和求和。但是求和時要返回的是區(qū)間各元素的和,平方和或立方和
顯然,我們肯定不能遍歷子節(jié)點求和,會T到飛起
考慮到我們只用維護到最多立方和,因此想到儲存三個標記——加法(lazy1),乘法(lazy2),賦值(lazy3)
然后,我們需要想出一種方法完成上述的幾個操作,如下:
一次方:區(qū)間每個數(shù)都加\(c\) --→ 加\(len*c\)。
- 平方:\((a+c)^2\) = \(a^2\)+\(2ac\)+\(c^2\)。所以區(qū)間每個數(shù)都加\(c\)之后的平方和=\(p2\)+\(2*p1*c\)+\(len*c^2\)
立方:\((a+c)^3\)=\(a^3\)+\(3a^2c\)+\(3ac^2\)+\(c^3\)。所以區(qū)間每個數(shù)都加c之后的立方和 =\(p3\)+\(3*p2*c\)+\(3*p1*c^2\)+\(len*c^3\)。
\((ac)^n\)=\(a^n*c^n\);
- 一次方:\(p1*c\)。
- 平方:\(p2*c^2\)
- 立方:\(p3*c^3\)
- 一次方:\(len*c\)
- 平方:\(len*c^2\)
- 立方:\(len*c^3\)
本題到這里都很好想,然而最重要的部分是——
多個lazy同時存在該如何處理?
首先是賦值。如果先進行加法或乘法操作再進行賦值,那么之前的加法乘法操作沒有任何意義。
于是我們考慮給lazy3賦值的同時清空lazy1和lazy2,這樣的話如果lazy1!=0 或 lazy2>1所代表的加法/乘法運算一定在賦值操作之后。這樣我們就可以放心的讓lazy3第一個PushDown
同樣的,先加后乘還是先乘后加?
\((a+b)*c=ac+bc\)
\(a*c+b=ac+b\)
差距在最后的部分,也就是將lazy1向子區(qū)間更新時該加\(b*c\)還是\(b\)的問題。
當我們進行乘操作時判斷一下lazy1是否不為0,如果true則代表之前已經(jīng)有進行加法運算,那么應該將lazy1乘以c
到這里,本題就完成了
總結:線段樹的標記下放永遠都是最惡心的東西,這玩意兒沒有一個定論,只能靠自己推。多做題背背順序也行……
以后如果還有題會在這兒更新的
轉載于:https://www.cnblogs.com/tqr06/p/10486283.html
總結
以上是生活随笔為你收集整理的需要支持多种操作的线段树该如何确定运算顺序?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 剖析Hadoop和Spark的Shuff
- 下一篇: day62 中间件