线段树
先射一發博客,雖說還沒做幾個題吧,主要是怕以後懶得寫惹。
以例題爲主。
P3919 【模板】可持久化線段樹 1(可持久化數組)
題意:
給你一個長度爲\(n\)的數組和\(m\)次操作,要求設計一個可以支持單點修改歷史版本與單點查詢歷史版本的數據結構。
思路:
大可以每次操作都開一次線段樹,高效簡單,不失爲一個比較優秀的寫法
考慮修改或查詢單點,每次修改或查詢都會訪問\(logn\)個線段樹節點,我們只需要新增這些被修改的節點保存信息就可以達到保存歷史版本的目的。由於需要新增少量節點保存信息(可持久化數據結構的重疊性),我們不能使用堆式儲存,只能用動態開點。
使用一個\(RT\)數組來保存每一個歷史版本的根節點,當操作爲修改單點時,新開節點複製\(logn\)個節點的信息並修改。查詢是直接在歷史版本的根節點遞歸處理即可,此時版本的節點就是所查詢歷史版本的根節點。
代碼:
#include <bits/stdc++.h>
#define lson tr[rt].ls
#define rson tr[rt].rs
using namespace std;
const int maxn=1001000;
int n,m,tot,a[maxn],RT[maxn];
struct EE{
int ls,rs,val;
}tr[maxn<<5];
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') w=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
s=(s<<1)+(s<<3)+(ch^48);
ch=getchar();
}
return s*w;
}
inline int build(int l,int r){
int rt=++tot;
if(l==r){
tr[rt].val=a[l];
return rt;
}
int mid=(l+r)>>1;
lson=build(l,mid);
rson=build(mid+1,r);
return rt;
}
inline int update(int rt,int L,int R,int pos,int val){
int dir=++tot;
tr[dir]=tr[rt];
if(L==R){
tr[dir].val=val;
return dir;
}
int mid=(L+R)>>1;
if(pos<=mid)tr[dir].ls=update(lson,L,mid,pos,val);
else tr[dir].rs=update(rson,mid+1,R,pos,val);
return dir;
}
inline int query(int rt,int L,int R,int pos){
if(L==R) return tr[rt].val;
int mid=(L+R)>>1;
if(pos<=mid) return query(lson,L,mid,pos);
else return query(rson,mid+1,R,pos);
}
int main(){
n=read();m=read();
for(int i=1;i<=n;++i) a[i]=read();
RT[0]=build(1,n);
for(int i=1;i<=m;++i){
int Van=read(),opt=read();
if(opt==1){
int loc=read(),val=read();
RT[i]=update(RT[Van],1,n,loc,val);
}else{
int loc=read();
printf("%d\n",query(RT[Van],1,n,loc));
RT[i]=RT[Van];
}
}
return 0;
}
P3834 【模板】可持久化線段樹 2(主席樹)
題意:
給你一個長度爲\(n\)的數組,要求設計一個可以查詢區間第\(k\)小的數據結構。
思路:
大可以每個位置都開一次線段樹,高效簡單,不失爲一個比較優秀的寫法
與上一個例題的思想大體相同,只不過我們這次把目光投向了可持久化權值線段樹——主 席 樹(某種意義上的)。
首先離散化。
開一個\(RT\)數組保存根節點,用\(RT[0]\)建一顆空樹,並從\(1\)到\(n\)開始掃,第\(i\)個位置由\(i-1\)個位置轉移過來並更新信息(即:\(a[i]\)的權值個數加一)。
於是我們發現第\(i\)個位置根節點所代表的權值線段樹實際上保存了\(1~i\)中所有權值的個數,而且隨着\(i\)的增加,其所維護的信息只增不減,所以我們能利用前綴和講區間的第\(k\)小查詢出來。
每次遞歸到一個節點,\(siz(rt[r].ls)-siz(rt[l-1].ls)\)就是這個區間內左值域之間的個數,如果說\(k\)比此數小,那麼需要向左遞歸,否則減去左值域的大小向右遞歸。
代碼:
#include <bits/stdc++.h>
using namespace std;
const int maxn=200100;
int n,m,tot,a[maxn],rt[maxn],ark[maxn];
struct EE{
int ls,rs,siz;
}tr[maxn<<5];
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') w=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
s=(s<<1)+(s<<3)+(ch^48);
ch=getchar();
}
return s*w;
}
int build(int l,int r){
int root=++tot;
if(l==r) return root;
int mid=(l+r)>>1;
tr[root].ls=build(l,mid);
tr[root].rs=build(mid+1,r);
return root;
}
int update(int val,int l,int r,int prert){
int dir=++tot;
tr[dir].ls=tr[prert].ls;
tr[dir].rs=tr[prert].rs;
tr[dir].siz=tr[prert].siz+1;
if(l==r) return dir;
int mid=(l+r)>>1;
if(val<=mid) tr[dir].ls=update(val,l,mid,tr[prert].ls);
else tr[dir].rs=update(val,mid+1,r,tr[prert].rs);
return dir;
}
int query(int yr,int zr,int l,int r,int k){
if(l==r) return l;
int vals=tr[tr[yr].ls].siz-tr[tr[zr].ls].siz;
int mid=(l+r)>>1;
if(k<=vals){
return query(tr[yr].ls,tr[zr].ls,l,mid,k);
}else{
return query(tr[yr].rs,tr[zr].rs,mid+1,r,k-vals);
}
}
int main(){
n=read();m=read();
for(int i=1;i<=n;++i) ark[i]=a[i]=read();
sort(ark+1,ark+n+1);
int parkx=unique(ark+1,ark+1+n)-1-ark;
for(int i=1;i<=n;++i) a[i]=lower_bound(ark+1,ark+1+parkx,a[i])-ark;
rt[0]=build(1,parkx);
for(int i=1;i<=n;++i) rt[i]=update(a[i],1,parkx,rt[i-1]);
for(int i=1;i<=m;++i){
int l=read(),r=read(),k=read();
int p=query(rt[r],rt[l-1],1,parkx,k);
printf("%d\n",ark[p]);
}
return 0;
}
P6242 【模板】線段樹 3
雖然和可持久化沒啥關係但某種意義上它也非常持——久——
題意:
給你一個長度爲\(n\)的數組,要求設計一個支持區間增加,區間取小(對每個\(l\le i \le r\),使\(a[i]=min(a[i],v)\)),區間最大值查詢,區間和查詢,區間歷史最大值查詢。
思路:
有個傻逼把它當成可持久化來寫了我不說是誰好吧
其他都好說,想想怎麼樣在較小時間內使\(a[i]=min(a[i],v)\)還有維護區間歷史最大。
於是乎。
struct EE{
int l,r,sum,cnt,se,maxa,maxb;
int add1,add2,add3,add4;
//区间和,区间最大,区间严格次大,区间历史最大,区间最大值个数
//最大值增加懒标记,非最大值增加懒标记,最大值懒标记最大懒标记,非最大值懒标记最大懒标记
}tr[maxn<<4];
設\(se\)爲某個區間的嚴格次大值,\(maxa\)爲區間的最大值,發現\(maxa<=v\)時不用繼續修改,\(se<v<maxa\)時只需要修改最大值(相當於都減去一個差值),\(v<se\)時需要繼續遞歸,於是有了\(cnt,se,maxa\)。
(一開始本來想只需要維護一個最大值安享晚年的,後來\(T\)到老家了)
那麼如何維護歷史最大?
其實只需要維護一下區間懶標記的最大值即可,設目前最大爲\(maxa\),歷史最大爲\(maxb\),距離上一次
下放的最大增加懶標記爲\(add3\),於是有\(maxb=max(maxb,maxa+add3)\)。但是因爲最大值與非最大值要分開來維護,所以有了\(add2,add4\)。
然後就剩下維護億點細節了,具體看代碼吧。
代碼:
#include <bits/stdc++.h>
#define int long long
#define lson (rt<<1)
#define rson (rt<<1|1)
using namespace std;
const int maxn=502000,inf=999999999999999;
int n,m,a[maxn];
struct EE{
int l,r,sum,cnt,se,maxa,maxb;
int add1,add2,add3,add4;
//区间和,区间最大,区间严格次大,区间历史最大,区间最大值个数
//最大值增加懒标记,非最大值增加懒标记,最大值懒标记最大懒标记,非最大值懒标记最大懒标记
}tr[maxn<<4];
inline int read(){
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-') w=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
s=(s<<1)+(s<<3)+(ch^48);
ch=getchar();
}
return s*w;
}
inline void pushup(int rt){
tr[rt].sum=tr[lson].sum+tr[rson].sum;
tr[rt].maxa=max(tr[lson].maxa,tr[rson].maxa);
tr[rt].maxb=max(tr[lson].maxb,tr[rson].maxb);
if(tr[lson].maxa==tr[rson].maxa){
tr[rt].cnt=tr[lson].cnt+tr[rson].cnt;
tr[rt].se=max(tr[lson].se,tr[rson].se);
}else if(tr[lson].maxa<tr[rson].maxa){
tr[rt].cnt=tr[rson].cnt;
tr[rt].se=max(tr[lson].maxa,tr[rson].se);
}else{
tr[rt].cnt=tr[lson].cnt;
tr[rt].se=max(tr[rson].maxa,tr[lson].se);
}
}
inline void modify(int k1,int k2,int k3,int k4,int rt){
tr[rt].sum+=k1*(tr[rt].cnt)+k2*(tr[rt].r-tr[rt].l+1-tr[rt].cnt);
tr[rt].maxb=max(tr[rt].maxb,tr[rt].maxa+k3);
// 维护历史最大值
tr[rt].add3=max(tr[rt].add3,tr[rt].add1+k3);
// 从上一次下传 add 到现在,区间加标记达到的最大值
tr[rt].add4=max(tr[rt].add4,tr[rt].add2+k4);
tr[rt].maxa+=k1;
tr[rt].add1+=k1;tr[rt].add2+=k2;
if(tr[rt].se!=-inf) tr[rt].se+=k2;
}
inline void pushdown(int rt){
int add1=tr[rt].add1,add2=tr[rt].add2,add3=tr[rt].add3,add4=tr[rt].add4;
int Max=max(tr[lson].maxa,tr[rson].maxa);
//需要根据此节点的最大值转移情况来下放标记
if(Max==tr[lson].maxa) modify(add1,add2,add3,add4,lson);
//最大值存在于左儿子区间
else modify(add2,add2,add4,add4,lson);
if(Max==tr[rson].maxa) modify(add1,add2,add3,add4,rson);
//最大值存在于右儿子区间
else modify(add2,add2,add4,add4,rson);
tr[rt].add1=tr[rt].add2=tr[rt].add3=tr[rt].add4=0;
}
inline void build(int rt,int l,int r){
tr[rt].l=l;
tr[rt].r=r;
if(l==r){
tr[rt].sum=a[l];
tr[rt].cnt=1;
tr[rt].maxa=a[l];
tr[rt].se=-inf;
tr[rt].maxb=a[l];
return;
}
int mid=(l+r)>>1;
build(lson,l,mid);
build(rson,mid+1,r);
pushup(rt);
}
inline int querymaxa(int rt,int l,int r){
if(l<=tr[rt].l&&tr[rt].r<=r) return tr[rt].maxa;
pushdown(rt);
int mid=(tr[rt].l+tr[rt].r)>>1;
int maxa=-inf;
if(l<=mid) maxa=max(maxa,querymaxa(lson,l,r));
if(r>mid) maxa=max(maxa,querymaxa(rson,l,r));
return maxa;
}
inline int querymaxb(int rt,int l,int r){
if(l<=tr[rt].l&&tr[rt].r<=r) return tr[rt].maxb;
pushdown(rt);
int mid=(tr[rt].l+tr[rt].r)>>1;
int maxb=-inf;
if(l<=mid) maxb=max(maxb,querymaxb(lson,l,r));
if(r>mid) maxb=max(maxb,querymaxb(rson,l,r));
return maxb;
}
inline int querysum(int rt,int l,int r){
if(l<=tr[rt].l&&tr[rt].r<=r) return tr[rt].sum;
pushdown(rt);
int mid=(tr[rt].l+tr[rt].r)>>1;
int sum=0;
if(l<=mid) sum+=querysum(lson,l,r);
if(r>mid) sum+=querysum(rson,l,r);
return sum;
}
inline void updateadd(int rt,int l,int r,int val){
if(l<=tr[rt].l&&tr[rt].r<=r){
modify(val,val,val,val,rt);
return;
}
pushdown(rt);
int mid=(tr[rt].l+tr[rt].r)>>1;
if(l<=mid) updateadd(lson,l,r,val);
if(r>mid) updateadd(rson,l,r,val);
pushup(rt);
}
inline void updatemin(int rt,int l,int r,int val){
if(tr[rt].maxa<=val) return;
if(l<=tr[rt].l&&tr[rt].r<=r&&tr[rt].se<val&&val<tr[rt].maxa){
modify(val-tr[rt].maxa,0,val-tr[rt].maxa,0,rt);
// 草草草这里需要注意区间范围啊!!!
return;
}
pushdown(rt);
int mid=(tr[rt].l+tr[rt].r)>>1;
if(l<=mid) updatemin(lson,l,r,val);
if(r>mid) updatemin(rson,l,r,val);
pushup(rt);
return;
}
signed main(){
n=read();m=read();
for(int i=1;i<=n;++i) a[i]=read();
build(1,1,n);
for(int i=1;i<=m;++i){
int opt=read(),l=read(),r=read();
if(opt==1){
int K=read();
updateadd(1,l,r,K);
}else if(opt==2){
int Van=read();
updatemin(1,l,r,Van);
}else if(opt==3){
printf("%lld\n",querysum(1,l,r));
}else if(opt==4){
printf("%lld\n",querymaxa(1,l,r));
}else if(opt==5){
printf("%lld\n",querymaxb(1,l,r));
}
}
return 0;
}
小記:關於可持久化線段樹的區間修改
我們考慮標記永久化,查詢時幹預數據而不是直接修改數據。
标记上下传和标记永久化的一大区别在于,标记上下传会引发修改结点路径上的点的更新,而标记永久化不会影响树上的点。
这点区别是非常有意义的:考虑一棵可持久化线段树,如果从某节点上传,则到根节点路径上的点都会被修改,而可持久化结构导致了某些结点的复用,这会引发多个版本的线段树更新,无法指定是哪一版本;同理,下传标记也会引发不必要的点的更新。因此,我们只能通过标记永久化对可持久化线段树进行修改。
修改時將整個節點打上標記即可,查詢時的代碼如下:
inline int query(int rt,int L,int R,int l,int r,int mk){
if(l<=L&&R<=r) return tr[rt].sum+tr[rt].mk*(R-L+1);
int mid=(L+R)>>1;
int sum=0;
if(l<=mid) sum+=query(lson,L,mid,l,r,mk+tr[rt].mk);
if(r>mid) sum+=query(rson,mid+1,R,l,r,mk+tr[rt].mk);
return sum;
}
後面可能會補。
參考資料:可持久化线段树 主席树 详解