吉司機線段樹小記
勢能線段樹的一種,相信如果理解了勢能線段樹的基本思想那這東西也不難理解了。
問題的引入——區間取 \(\min\) 區間求和
你需要維護一個序列,支援以下兩種操作:
- 區間取 \(\min\)
- 區間求和
在剛學線段樹的時候我們就知道,這東西一般的線段樹+懶標記是解決不了的,因為在下放標記的時候無法直接計算此次修改對和的貢獻。
怎麼辦呢?這下就要請出我們的吉司機線段樹了。
吉司機線段樹大概就是對線段樹上每個節點維護一個最大值 \(mx\)(注意是最大值,可以簡單記憶為維護的值與操作相反)和嚴格次大值 \(smx\)(注意,必須是嚴格次大值,否則會出錯),以及最大值個數 \(c\),如果不存在嚴格次大值可以設為 \(-\infty\)
吉司機線段樹的過程如下:
-
假設我們要對於區間 \([l,r]\) 中的數對 \(x\) 取 \(\min\),我們首先將 \([l,r]\) 拆分成線段樹上若干個區間 \([l_i,r_i]\)
-
對於每個 \([l_i,r_i]\):
-
如果它的最大值 \(\le x\),那麼顯然此次操作沒有任何效果,直接
return
即可。 -
如果 \(x\) 小於最大值 \(mx\),但大於(注意,這裡必須是嚴格大於,否則的話也會出問題)嚴格次大值 \(smx\),那麼顯然有且只有 \(c\) 個最大值會變為 \(x\),我們可以簡單維護一個標記 \(tg\) 表示這段區間內最大值會增加 \(tg\)
-
如果 \(x\) 小於等於嚴格次大值 \(smx\),這個就沒有什麼優美的方法了,直接暴力遞迴左右子區間即可。
-
吉司機線段樹複雜度是 \(n\log n\) 的,證明如下:
- 我們記一個節點的容為這段區間內不同數的個數,那麼顯然在不斷取 \(\min\) 的過程中容只可能越來越小。而如果對於某個區間如果我們對其進行暴力遞迴,那麼原來是最大值和次大值的位置上的數必然會變為同一個值,區間的容建一,而所有區間的長度之和是 \(n\log n\) 級別的,因此所有區間容之和也是 \(n\log n\)
程式碼大致長這樣:
void pushup(int k){
s[k].mx=max(s[k<<1].mx,s[k<<1|1].mx);
s[k].sum=s[k<<1].sum+s[k<<1|1].sum;
if(s[k<<1].mx>s[k<<1|1].mx){
s[k].smx=max(s[k<<1|1].mx,s[k<<1].smx);
s[k].c=s[k<<1].c;
} else if(s[k<<1].mx<s[k<<1|1].mx){
s[k].smx=max(s[k<<1].mx,s[k<<1|1].smx);
s[k].c=s[k<<1|1].c;
} else {
s[k].smx=max(s[k<<1].smx,s[k<<1|1].smx);
s[k].c=s[k<<1].c+s[k<<1|1].c;
}
}
void build(int k,int l,int r){
s[k].l=l;s[k].r=r;if(l==r) return s[k].mx=a[l],s[k].smx=INF,s[k].c=1,void();
int mid=l+r>>1;build(k<<1,l,mid);build(k<<1|1,mid+1,r);pushup(k);
}
void pushtag(int k,ll v){s[k].sum+=1ll*v*s[k].c;s[k].tag+=v;s[k].mx+=v;}
void pushdown(int k){
if(s[k].tag){
bool tmp=s[k<<1|1].mx>=s[k<<1].mx;
if(s[k<<1].mx>=s[k<<1|1].mx) pushtag(k<<1,s[k].tag);
if(tmp) pushtag(k<<1|1,s[k].tag);s[k].tag=0;
}
}
void modify(int k,int l,int r,int v){
if(l>r||v>=s[k].mx) return;
if(l<=s[k].l&&s[k].r<=r){
if(v>s[k].smx) return pushtag(k,v-s[k].mx),void();
else{
int mid=(pushdown(k),l+r>>1);
return modify(k<<1,l,mid,v),modify(k<<1|1,mid+1,r,v),pushup(k),void();
}
} int mid=(pushdown(k),s[k].l+s[k].r>>1);
if(r<=mid) modify(k<<1,l,r,v);
else if(l>mid) modify(k<<1|1,l,r,v);
else modify(k<<1,l,mid,v),modify(k<<1|1,mid+1,r,v);
pushup(k);
}
ll query(int k,int l,int r){
if(l<=s[k].l&&s[k].r<=r) return s[k].sum;
int mid=(pushdown(k),s[k].l+s[k].r>>1);
if(r<=mid) return query(k<<1,l,r);
else if(l>mid) return query(k<<1|1,l,r);
else return query(k<<1,l,mid)+query(k<<1|1,mid+1,r);
}
吉司機線段樹的一些變種
帶區間加的線段樹
其實也比較好辦,額外加一個懶標記 \(stag\) 表示除了最大值之外的其他數都要增加 \(stag\),區間加 \(v\) 時令對應區間的 \(tg\) 和 \(stag\) 都加 \(v\),區間取 \(\min\) 時只會影響 \(tg\),不會影響 \(stag\) 的值。下推標記就和普通吉司機樹一樣正常推即可。具體可見 CF1290E 的程式碼。
根據 UOJ #228 的經驗,區間整體加一個數不影響它的容,因此區間加操作最多改變 \(\log n\) 個區間的容,因此複雜度還是 \(n\log n\) 的。
同時取 \(\min/\max\) 的吉司機線段樹
再維護一個最小值、次小值、最小值標記即可,注意特判最大值等於最小值的情況。
複雜度依舊 \(n\log n\)。
求一個點被取最小值的次數
額外維護一個標記 \(ctag\) 表示被取最小值的次數,對於上面情況中的第二種(\(smx<x<mx\)),直接令 \(ctag\) 加一即可,下推 \(ctag\) 時就對於左右兒子中最大值等於該區間原來的最大值的區間把 \(ctag\) 傳給他們即可。
查詢時就一路將 \(ctag\) 推到葉子節點處,輸出對應葉子節點的 \(ctag\) 即可。
例題:
1. HDU 5306 Gorgeous Sequence
mol ban tea,沒啥好說的,練練熟練度吧
2. CF1290E Cartesian Tree
首先關於笛卡爾樹子樹的大小,有一個結論:記 \(r_i\) 為在 \(i\) 後面第一個大於 \(a_i\) 的位置,\(l_i\) 為在 \(i\) 前面第一個小於 \(a_i\) 的位置,那麼以 \(i\) 為根的子樹大小為 \(r_i-l_i+1\)。
因此所有子樹之和的大小自然就是 \(\sum\limits_{i=1}^nr_i-l_i+1\)
故我們只需求出 \(\sum\limits_{i=1}^nl_i\) 和 \(\sum\limits_{i=1}^nr_i\) 即可。
考慮怎樣維護這個東西,當我們加入一個數 \(a_p=x\) 時候,會對 \(r_i\) 產生以下的影響:
- 對於 \(i>p\),\(r_i\leftarrow r_i+1\),因為下標整體向右移了一格
- 對於 \(i<p\),\(r_i\leftarrow\min(r_i,p')\),其中 \(p'\) 為 \(a_p\) 此時在序列中的位置
- 對於 \(i=p\),\(r_i\leftarrow x+1\)
於是我們需要支援區間加、區間取 \(\min\),單點賦值,全域性求和,吉司機線段樹即可。
至於怎樣求 \(\sum l_i\),其實只需把序列反轉一下,按照 \(\sum r_i\) 的套路維護即可,最後真正的 \(\sum l_i\) 等於 \(i(i+1)\) 減去求出的 \(\sum r_i\)
時間複雜度 \(n\log n\)。
3. CF855F Nagini
講個笑話,星期三我們機房幾個(吊打我的)人組隊打 CF 855 的 virtual,而我在學吉司機線段樹,然後上網一搜,剛好搜到這個題……
kyl:啊,這場竟然是以哈利波特為主題的
跑題了跑題了。
首先開兩個 set
\(ban1,ban2\) 分別維護不存在正數、不存在負數的集合編號的集合,當我們加入一個正數時,我們只需將 \(ban1\) 中編號在 \([l,r)\) 中的數刪除,加入一個負數也同理,如果一個數同時不在 \(ban1,ban2\) 中就證明它的權值非零了。
顯然,對於一個權值非零的集合,它的權值等於正數的最小值減去負數的最大值,於是考慮建立兩棵線段樹維護正數最小值和負數最大值,需要支援區間取 \(\min\)(或者區間取 \(\max\)),將某個位置設為合法,查詢所有合法的位置上數的和,吉司機線段樹一波帶走,時間複雜度線性對數。
4. UOJ 515 【UR #19】前進四
這題為什麼 UOJ 上這麼多差評啊 qwq,感覺除了套路一點其他還好罷/ts
首先將詢問離線並建出時間軸,然後從右往左列舉位置,對於每個位置記錄每個數出現的時間(顯然是一個區間),線上段樹上對其取 \(\min\),那麼對於一個詢問而言,它的答案就是對應詢問時間的位置被取 \(\min\) 的次數,直接用求一個點被取最小值的次數的套路即可搞定,時間複雜度 \(n\log n\)。