1. 程式人生 > 其它 >線段樹合併 & 權值線段樹 & 一些題目

線段樹合併 & 權值線段樹 & 一些題目

https://www.luogu.com.cn/blog/styx-ferryman/xian-duan-shu-ge-bing-zong-ru-men-dao-fang-qi?tdsourcetag=s_pcqq_aiomsg

%%%

權值線段樹的一些操作

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]);
}