線段樹合併 & 權值線段樹 & 一些題目
%%%
權值線段樹的一些操作
int getrank(int x){ return query_num(root,1,maxdata,1,x)+1; } int getpre(int x){//查詢前驅 int tmp=query_num(root,1,maxdata,1,x); return query_kth(root,1,maxdata,tmp); } int getnxt(int x){//查詢後繼 int tmp=query_num(root,1,maxdata,1,x); return query_kth(root,1,maxdata,tmp+1); }
A雨天的尾巴
樹上差分,每次操作都單點修改,操作完之後dfs合併線段樹。
每個點都開一個權值線段樹,維護出現次數最多的權值和出現次數最多的權值的出現次數。
B bzoj4636蒟蒻的序列
區間修改(但又不是完全的區間修改),如果按照提意來去跑葉子節點每次詢問的時間複雜度會到\(O(n\log n)\)。考慮線段樹每個節點都維護一個懶標,表示所表示區間內所有數的權值,線段樹dfs查詢。
update if(L<=l && r<=R){ tr[rt].lazytag=max(tr[rt].lazytag,k); return; } query w=max(w,tr[rt].lazytag); if(!rt){ return 1ll*(R-L+1)*w; }
C Promotion Counting
這個應該是最裸的板子。樹上每一個節點開一個權值線段樹維護桶,在樹上dfs合併。統計答案的時候每個點都掃一遍,在其對應的線段樹上查詢比\(val_i\)大的數的個數
所以dfs序+分塊完全可以做到
D 永無鄉
用並查集維護連通塊,每一個連通塊內建一個權值線段樹維護桶。點之間連邊就對應所在連通塊線段樹的合併,然後查詢排名就是板子
E 魔法少女Ljj
沒啥難的,就是上面幾個板子的綜合
但是Chano很鬱悶,你能幫幫他嗎
F大根堆
考慮樹形dp。設\(f[u][val]\)為以u為根節點的樹中選出的最大值不超過\(val\)的最多的節點,用線段樹來維護。
則有
f[u][val]+=f[v][val]//v為u的子節點,此操作對應線段樹合併
f[u][val[u]]=max(f[u][val[u]],f[u][val[u]-1]+1);
而且不僅是\(f[u][val[u]]\)要更新,所有小於\(f[u][val[u]-1]+1\)的\(f[u][val]\)都要更新,由於\(f\)本身的是單調的,所以考慮二分查詢並且區間更新
涉及到區間更新所以我打了lazy,但是這樣可能會開出很多沒有用的點來,整一個回收棧,把合併後沒有用的點的編號扔到棧裡,開新點的時候取出棧裡的編號即可(注意扔的時候清空點的資訊)
最後答案為\(f[1][maxdata]\)
void dfs(int u,int fa){
for(int i=head[u];i;i=e[i].next){
int v=e[i].to;
if(v==fa)continue;
dfs(v,u);
root[u]=merge(root[u],root[v],1,maxdata);
}
int tmp=query(root[u],1,maxdata,val[u]-1)+1;
if(tmp<=query(root[u],1,maxdata,val[u]))return;
int l=val[u],r=maxdata,ans=0;
while(l<=r){
int mid=l+r>>1;
if(query(root[u],1,maxdata,mid)<tmp){
ans=mid;l=mid+1;
}
else r=mid-1;
}
update(root[u],1,maxdata,val[u],ans,1);
}
G 領導集團問題LuoguP4577
和 F 大根堆一樣,就是變成了小根堆
H 大融合LuoguP4219
以題面上的圖為例,查詢<3,8>,先把<3,8>斷開,3所在的聯通塊內有3個點,8所在的聯通塊內有2個點,從3所在的聯通快內任意一個點出發到8所在的聯通塊內的任意一點,需要經過<3,8>的路徑有2*3條,所以查詢結果應為6
按照這個思路有一個很顯然的做法,從u開始dfs,遇到<u,v>跳過,算出u所在聯通塊內的點數,v同理。\(40pts\)
離線處理,把所有的邊都連上,處理出dfs序來,對於每一個聯通塊開一個權值線段樹來維護當前狀態下\(dfn[i]\)出現的次數(有沒有出現)。
查詢
如果x是y的父親,則答案為 聯通塊內\(dfn[y]--outdfn[y]\)出現總次數(y一側的點數)與所在聯通塊內的總點數減去y一側的點數 (x一側的點數)的乘積,這樣就不用dfs以斷開<x,y>
更新
並查集維護聯通塊再線段樹合併即可
G 基站選址\(LuoguP2605\)
樸素方程
f[i][j]第j個基站建在i最小費用,cost[i][j]第i~j個村莊之間沒有被基站i,j覆蓋的村莊所需的賠償費用
f[i][j]=min(f[k][j-1]+cost[k][i])+c[i] k=[j-1,i)
當我們推導 \(i\) 時,我們只考慮了它和前面的基站產生的影響,這時對於最後一個基站我們不會考慮它和之後的村莊產生的影響,則我們可以在最後增加一個村莊
,保證它必定被作為基站(無建設費用)且不對前面產生影響,這樣就不會有遺漏的了
++n,++k,w[n]=d[n]=Inf;
第二維只與上一次有關,可以滾掉,在最外層列舉j即可
f[i]=min(f[k]+cost[k][i])+c[i] k=[j-1,i)
難點在於求出 \(cost\)
維護兩個陣列, \(st_i\) 點 \(i\) 的左邊界(建基站能覆蓋到\(i\)的最靠左的點), \(ed_i\) 點\(i\) 的右邊界,二分查詢即可
對於一個點 \(i\) 如果 \(ed_i\) 沒有建基站,那麼 \(i\) 就要賠償 賠償總費用為 \(1--st_i-1\)賠償的費用加上 \(w_i\) 用線段樹區間更新
如果在 \(ed_i\)處建基站,賠償費用就為\(1--st_i-1\)賠償的費用 不用更新
狀態初值 \(i=1\) 第一個基站建在哪個位置
可能會有多個點的有邊界為j,用鄰接表維護
int sum=0;//cost[1][j-1];
for(int j=1;j<=n;++j){
f[j]=sum+c[j];
計算如果不在j建基站需要賠償的費用
for(int p=G.head[j];p;p=G.e[p].next){
int v=G.e[p].to;sum+=w[v];
}
}
ans=f[n];
線段樹中維護 \(min(f[p]+cost[p][j]),\) \(j\) 為當前決策的點
轉移
for(int j=1;j<=n;++j){
if(j-1>=i-1)
f[j]=S.query(root,1,n,i-1,j-1)+c[j];
else f[j]=c[j];
計算如果不在j建基站需要賠償的費用
for(int p=G.head[j];p;p=G.e[p].next){
int v=G.e[p].to;
if(st[v]>1){
S.update(root,1,n,1,st[v]-1,w[v]);
}
}
}
由於把第二維滾掉了,所以在列舉到新的 \(i\) 時要重新建樹把原來的狀態覆蓋掉
for(int i=1;i<=k;++i){
if(i==1){//初值
···
}
else{
S.build(root,1,n);
···
轉移
}
ans=min(ans,f[n]);
}