1. 程式人生 > 其它 >吉司機線段樹小記

吉司機線段樹小記

勢能線段樹的一種,相信如果理解了勢能線段樹的基本思想那這東西也不難理解了。

問題的引入——區間取 \(\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\)

      ,然後令 \(tg\leftarrow tg+(x-mx)\) 即可,由於更新完之後最大值依然嚴格大於次大值,因此最大值個數不會發生變化。

    • 如果 \(x\) 小於等於嚴格次大值 \(smx\),這個就沒有什麼優美的方法了,直接暴力遞迴左右子區間即可。

吉司機線段樹複雜度是 \(n\log n\) 的,證明如下:

  • 我們記一個節點的為這段區間內不同數的個數,那麼顯然在不斷取 \(\min\) 的過程中容只可能越來越小。而如果對於某個區間如果我們對其進行暴力遞迴,那麼原來是最大值和次大值的位置上的數必然會變為同一個值,區間的容建一,而所有區間的長度之和是 \(n\log n\) 級別的,因此所有區間容之和也是 \(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\)