線段樹
by yyb
Type1 維護特殊信息
1.【洛谷1438】無聊的數列
維護一個數列,兩種操作
1.給一段區間加上一個等差數列
2.單點詢問值
維護等差數列
不難發現,等差數列可以寫成\(ad+b\)的形式
因為具有可加性
所以維護一下這個類似於斜率的東西
每次下放的時候把數列拆分成兩段,\(d\)值公差不變
而變化的只有后面的常數項
至於如何只在一段區間內維護等差數列
相當於在當前\([l,n]\)位置維護這一段公差為\(d\)的等差數列
再在\([r+1,n]\)維護一個負公差就行了
這題單點詢問其實是簡化了,完全可以維護區間詢問
2.【BZOJ2243】染色
給定一棵樹
兩種操作
1.把一段路徑染上一種顏色
2.詢問路徑上顏色段的個數
維護顏色段
想想怎么把顏色拼起來
假設原來兩段是\(111222112233\)和\(3332221124\)
假設兩段原來分別含有\(k1,k2\)段
拼在一起后,答案為\(k1+k2\)或者\(k1+k2-1\)
如果答案減一的話,相當於左端的最右端和右段的最左端顏色相同
所以要維護的是當前段的顏色段個數,左端點顏色和右端點顏色
3.【BZOJ1018】堵塞的交通
維護一個2×n的方格,一開始所有邊都是不連通的
兩個操作
1.修改一條邊的連通性
2.詢問兩點的連通性
維護連通性
對於區間的這一段單元格
我們在意的只有四個端點點之間的連通性
所以維護六個\(bool\)類型
分別維護端點之間兩兩的連通性
當區間大小只有\(1\)的時候,不能夠僅僅只維護連通性
還要維護邊是否存在,
所以區間大小為\(1\)時,額外維護\(4\)個\(bool\)類型,分別表示四條邊是否聯通
考慮如何向上合並
既然維護了聯通性,此時只需要拿出當前左右兩個區間的四個點進行討論
左端點的左上和左下,右端點的右上和右下,
分別討論經過中間點時是否聯通,從而可以合並,得到大區間四個端點的聯通性
從而可以維護答案
4.【BZOJ1558】等差數列
給你一個數列,兩個操作
1.給一段區間加上一個等差數列
2.將一段區間分解為最少個數的等差數列,輸出等差數列的個數
線段樹維護區間\(dp\)
可以說這道題已經非常毒瘤了
怎么考慮詢問操作?
如果直接將一段數分解為等差數列?
太麻煩了。。。。
考慮相鄰的數做差,
這樣等差數列變為了一段連續的相等區間
考慮怎么維護分解一段區間為最少數量的等差數列
事實上,等差數列的第一項不一定要和后面的相等,所以合並的時候要額外考慮
所以,設\(s[0/1/2/3]\)分別表示左右端點是否計算入內
同時維護最左端和最右端的值\(l,r\)
如果沒有計算入內,則此時左右端點作為一個等差數列的開頭
如果計算入內,則是一樣的計算,考慮連續區間
合並的代碼如下:
struct Data{int s[4],l,r;};
Data operator+(Data x,Data y)
{
Data c;c.l=x.l,c.r=y.r;
c.s[0]=x.s[2]+y.s[1]-(x.r==y.l);
c.s[0]=min(c.s[0],x.s[0]+y.s[1]);
c.s[0]=min(c.s[0],x.s[2]+y.s[0]);
c.s[1]=x.s[3]+y.s[1]-(x.r==y.l);
c.s[1]=min(c.s[1],x.s[1]+y.s[1]);
c.s[1]=min(c.s[1],x.s[3]+y.s[0]);
c.s[2]=x.s[2]+y.s[3]-(x.r==y.l);
c.s[2]=min(c.s[2],x.s[2]+y.s[2]);
c.s[2]=min(c.s[2],x.s[0]+y.s[3]);
c.s[3]=x.s[3]+y.s[3]-(x.r==y.l);
c.s[3]=min(c.s[3],x.s[3]+y.s[2]);
c.s[3]=min(c.s[3],x.s[1]+y.s[3]);
return c;
}
以\(s[0]\)舉例,\(s[0]\)表示的是左右端點都不選
轉移如下:
1.可以直接合並左邊選右端點,右邊選左端點。如果兩者的差值相同,則可以將原來的等差數列合並為一個
2.左邊兩側都不選,左邊的右端點作為一個等差數列的首項,右邊就要選擇左端點
3.左邊選右端點,右邊的左端點作為一個等差數列的首項,所以右端點兩邊都不選
其他的\(s[1/2/3]\)轉移同理
至於區間的加法,不過是對查分數組造成兩個單點修改,以及一個區間修改的影響
仔細考慮清楚就可以
總結1
這一類問題主要在於如何維護一些信息
一般的做法在於找到對應的信息的維護方式
比如對於一次函數,可以直接加和,所以維護斜率和截距
對於維護顏色段,我們發現關鍵在於左段的右端點和右段的左端點,所以維護左右端點
對於最大子段和,我們考慮貪心是怎么做的,所以維護最大左右子段和
對於維護連通性,我們找到聯通性之間的關系,利用分類討論來向上合並
......
總之,對於這一類維護特殊信息的問題
我們要維護的就是要求的信息,
以及向上合並時需要的信息
這一類問題的難度就在於怎么合並,
只要想清楚了怎么合並,這類問題都非常好解決
Type2 線段樹維護特殊操作
1.【BZOJ3211】花神游歷各國
維護一個數列,兩個操作
1.區間開根
2.區間求和
很容易知道區間開根的操作次數不會很多,
\(10^{12}\)的數據的操作次數在\(6\)次左右
而\(\sqrt 1=1,\sqrt 0=0\)
所以維護區間最小值\(min\)
對於區間開根,暴力下方,如果\(min<=1\)可以直接\(return\)
2.【BZOJ4869】相逢是問候
維護一個區間,兩個操作
1.將區間[l,r]所有數變為C^(ai)
2.求區間[l,r]mod p 的和
我們根據歐拉定理
知道反復進行若干遍操作之后,答案不會再變
所以提前預處理出所有的\(\varphi\)值
同時記錄區間最少的操作次數,如果最小操作次數達到了上限,直接\(return\)
3.【UOJ228基礎數據結構練習題】
維護一個區間,三種操作
1.區間加法
2.區間開根
3.區間求和
這是前面那題的升級版
如果再是單純的維護區間最小值顯然不合理了
我們來看看怎么開根?
如果區間所有值都相等怎么辦?
顯然可以直接開根
如果\(max-sqrt(max)=min-sqrt(min)\)怎么辦?
此時意味着雖然開根出來的值不同,但是減去的值相同
舉個例子,比如\(8,9\)
開根后是\(2,3\)
雖然值不同,但是差相同
所以,我們把開根換成區間減法
當出現上述兩種情況時下放減法標記即可
區間開根代碼的實現
void Modify_Sqrt(int now,int l,int r,int L,int R)
{
if(L<=l&&r<=R)
{
ll a=sqrt(t[now].mx),b=sqrt(t[now].mn);
if(t[now].mx==t[now].mn){puttag(now,l,r,a-t[now].mx);return;}
if(t[now].mx-a==t[now].mn-b){puttag(now,l,r,a-t[now].mx);return;}
}
pushdown(now,l,r);
int mid=(l+r)>>1;
if(L<=mid)Modify_Sqrt(lson,l,mid,L,R);
if(R>mid)Modify_Sqrt(rson,mid+1,r,L,R);
t[now].v=t[lson].v+t[rson].v;
t[now].mx=max(t[lson].mx,t[rson].mx);
t[now].mn=min(t[lson].mn,t[rson].mn);
}
總結2
這類題目的重點在於這些特殊操作的處理
此時的思考的主要方向已經不是線段樹如何使用了
而是想清楚當前操作具有的特殊性質
再來相應地在線段樹上維護所需要的東西
Type3 作為輔助的數據結構
1.【BZOJ4552】排序
給定一個初始數列
進行若干操作
每次給定[l,r]
將這段區間進行升序或者降序的排序
最后詢問第Q個位置上的數
輔助二分
顯然無法直接維護排序后的結果
但是,如果是\(01\)序列,我們是可以直接維護排序后的結果的
那么,二分一個答案
將所有大於答案的數賦值為\(1\),其他的賦值為\(0\)
每次維護\(01\)序列的排序
檢查目標位置是\(0/1\)來繼續二分
2.【CF833B】The Bakery
將一個長度為n的序列分為k段,使得總價值最大
一段區間的價值表示為區間內不同數字的個數
n<=35000,k<=50
輔助\(dp\)
一個很簡單的暴力\(dp\)
設\(f[i][j]\)表示前\(i\)個數分為\(j\)段的最大總價值
轉移很簡單\(f[i][j]=max(f[k][j-1]+Calc(k+1,i))\)
其中\(Calc\)是題目給定的價值
但是這樣復雜度顯然是假的
我們重新看看這個轉移
如果我們按照第二維來枚舉,
那么,相當於從頭開始插入每一個數
方程不變,還是\(f[i][j]=max(f[k][j-1]+Calc(k+1,i))\)
考慮如何計算\(Calc\)
其實,每一次都相當於把當前\(i\)位置,以及這個數上一次出現的位置\(lst\)之間的轉移值全部加一了
所以,在線段樹上面維護區間加法
同時,每次增加分段的數量的時候,重構線段樹
節點的值就是上一維的\(dp\)值
這樣就可以利用線段樹來優化(進行)\(dp\)啦
3.【CF903G】Yet Another Maxflow Problem
一張圖分為兩部分,左右都有n個節點,
Ai->Ai+1連邊,Bi->Bi+1連邊,容量給出
有m對Ai->Bj有邊,容量給出
兩種操作
1.修改某條Ai->Ai+1的邊的容量
2.詢問從A1到Bn的最大流
n,m<=100000,流量<=10^9
這算輔助什么???簡易版本網絡流???
將最大流的詢問轉換為最小割
假設\(A\)側割掉了\(A_i->A_{i+1}\),\(B\)側割掉了\(B_j->B_{j+1}\)
那么,\(Ans=A_iA_{i+1}+B_j+B_{j+1}+A_xB_y(x<=i,y>j)\)
所以,對於\(A\)側,我們如果枚舉割掉哪一條邊
我們在\(B\)側都要找到對應的\(j\)使得答案最小
同樣的,因為改變的只有\(A\)側的流量,因此無論怎么修改,在\(B\)側選擇的\(j\)是不會變化的
這個時候我們就比較明朗了
現在的問題,考慮如何求解對於每個\(i\),最優的\(B\)
我們只需要掃一遍\(A\),一邊在\(B\)中插入對應的值,求出區間最小值就行了
然后,把剩下的所有的值加上\(Ai\)后重構一棵線段樹
修改就是單點修改
每次詢問相當於查找全局最小值
4.【POJ1151】Atlantis
平面內有若干矩形
求面積和
輔助掃描線
這是掃描線的模板題
當然,重點不在掃描線,在於線段樹
所以這里只是大致提一下
(當然啦,掃描線也要去學一下啦)
總結3
線段樹不僅僅可以出裸題(雖然就算出裸題我也不一定會做)
可是可以和各種各樣的東西結合起來的
然后?然后我就不認識它是線段樹了
這種類型的題目不應該從線段樹入手了
而是從其他算法入手,發現可以使用線段樹來進行優化
這個時候才可以美滋滋的用上線段樹啦
Type4 線段樹一些很interesting的食用方法
1.【BZOJ3531】旅行
給出一棵樹,每個節點都有一個顏色和一個權值
4個操作:
1.修改單點顏色
2.修改單點權值
3.詢問路徑上某種顏色的權值和
4.詢問路徑上某種顏色的權值最大值
顏色數、節點數、詢問數<=100000
線段樹動態開點
看題目的意思,我們顯然要對於每種顏色維護一棵線段樹
當時顯然是開不下的
那么,我們考慮動態開點
發現每棵線段樹顯然是不滿的
所以,對於每個線段樹的節點,額外存下左右節點
在修改的時候,如果訪問到的當前節點為空
則直接新建一個節點,否則繼續訪問就行了
唯一的缺點的是,如果一個點上的權值被刪除,這個點並不能夠回收
但是這不影響我們對於動態開點的需求
動態開點修改代碼
void Modify(int &now,int l,int r,int pos,int w)
{
if(!now)now=++tot;
if(l==r){t[now].v=t[now].ma=w;return;}
int mid=(l+r)>>1;
if(pos<=mid)Modify(t[now].ls,l,mid,pos,w);
else Modify(t[now].rs,mid+1,r,pos,w);
t[now].v=t[t[now].ls].v+t[t[now].rs].v;
t[now].ma=max(t[t[now].ls].ma,t[t[now].rs].ma);
}
2.【BZOJ4653】區間
給的是UOJ的鏈接,UOJ的Hack數據比較強,建議在UOJ提交
從n個線段中選擇M個線段
使得他們至少都包含一個相同的位置
定義一個方案的花費是M個線段的長度的最大值減最小值
求最小花費,如果無解輸出-1
N<=500000,M<=200000,0≤li≤ri≤10^9
標記永久化
貪心的想一想,把區間按照長度排序,依次加入到區間中
如果把當前的線段插入進去后
這個區間的最大覆蓋值超過了\(M\)
就把所有不需要的線段全部彈掉
然后計算貢獻。
這里需要做的就是反復的區間加法
所以我們沒有必要每次都把標記下放
可以直接把標記打在這個點上面
每次訪問到這個點的時候直接加上這個標記值然后向上更新就好啦
代碼復雜度和常數一下就降下去了了
3.【CF817F】MEX Queries
維護一個01串,一開始全部都是0
3種操作
1.把一個區間都變為1
2.把一個區間都變為0
3.把一個區間的所有數字翻轉過來
每次操作完成之后詢問區間最小的0的位置
l,r<=10^18
線段樹上二分
這題不離散一下空間卡得我一愣一愣的
先不考慮空間的問題,直接用線段樹維護一下,
放區間覆蓋標記和區間翻轉標記,之前已經講過這兩個標記要怎么放,不再提了
其實可以直接動態開點,每次最多產生點\(60\)個左右(然而事實上是\(120\)個)
但是如果直接這么打了發現您就\(MLE/RE\)了
因為產生的點其實最多是\(120\)個,因為在操作過程中需要下放標記
如果左右子樹不存在的話必須新建,導致空間多了一倍
再加上要開\(long long\),發現空間開不下
所以要離散化,首先\(1\)要在離散數組里面,然后所有的\(l,r,l+1,r+1\)也必須在里面(為什么?自己思考一下)
最后是如何計算答案,
在線段樹上面二分
如果左子樹不滿,那么答案在左子樹,否則答案在右子樹
如果當前這個子樹都不存在,當然直接返回最左端就行了
4.【BZOJ2957】樓房重建
給定數軸上的1~n位置
每次單點修改一個位置的高度
每次修改完之后詢問從原點能夠看到幾個位置
這應該算什么?用整棵子樹來更新當前位置的神奇操作???
對於整個區間維護最大斜率以及只考慮這個區間的答案
考慮如何向上合並。
首先左半段的答案是一定存在的
所以,現在的問題就是右半段能夠貢獻的答案
如果右半段的最大斜率小於左半段的最大斜率,則不存在貢獻
否則,如果右半段分為右左和右右兩段
如果右左的最大值大於了左半段的斜率,直接加上右右段的貢獻
然后遞歸除了右左段
否則,直接遞歸處理右右段
直接說有點說不清,這題需要自己好好思考一下
5.【BZOJ2733】永無鄉
題目有點小復雜,直接放題面啦
永無鄉包含 n座島,編號從 1 到 n ,每座島都有自己的獨一無二的重要度,按照重要度可以將這 n 座島排名,名次用 1 到 n 來表示。某些島之間由巨大的橋連接,通過橋可以從一個島到達另一個島。如果從島 a 出發經過若干座(含 0 座)橋可以 到達島 b ,則稱島 a 和島 b 是連通的。
現在有兩種操作:
B x y 表示在島 x 與島 y 之間修建一座新橋。
Q x k 表示詢問當前與島 x 連通的所有島中第 k 重要的是哪座島,即所有與島 x 連通的島中重要度排名第 k 小的島是哪座,請你輸出那個島的編號。
線段樹合並
線段樹合並是一個很有趣的姿勢
前置技能:動態開點線段樹
具體實現:每次合並兩棵線段樹的時候,假設叫做\(t1,t2\),其中要把\(t2\)合並進\(t1\)中
假設當前位置\(t1\)沒有節點,則直接把\(t2\)的這個位置給\(t1\)(直接接上去就好啦)
如果\(t2\)這個位置沒有節點,那么直接\(return\)
否則,兩個位置都有節點,把兩個節點的信息合並,然后遞歸合並左右子樹
簡單的代碼如下:
void MergeNode(int &r1,int r2)
{
if(!r1){r1=r2;return;}
if(!r2)return;
t[r1].v+=t[r2].v;
MergeNode(t[r1].ls,t[r2].ls);
MergeNode(t[r1].rs,t[r2].rs);
}
回到這道題目
對於每一個聯通快維護一個值域線段樹
每次在線段樹上二分一下第\(K\)大就好了
每次修橋相當於合並兩棵線段樹
用並查集維護一下聯通快就可以啦,多簡單
總結4
這些食用方法都很有用
包括但不限於:卡常、卡空間、降低編程困難度 等等等
而具體怎么用?
那就要靈活的看題目而定了