一、引入
先來看一道題:CF786B Legacy。
題目大意:有 \(n\) 個點、\(q\) 次操作。每一種操作為以下三種類型中的一種:
-
操作一:連一條 \(u\to v\) 的有向邊,權值為 \(w\)。
-
操作二:對於所有 \(i\in [l,r]\) 連一條 \(u\to i\) 的有向邊,權值為 \(w\)。
-
操作三:對於所有 \(i\in [l,r]\) 連一條 \(i\to u\) 的有向邊,權值為 \(w\)。
求從點 \(s\) 到其他點的最短路。\(1\leq n,q\leq 10^5,1\leq w\leq 10^9\)。
考慮暴力建圖。顯然不能通過此題。
這時候就需要用線段樹優化建圖了。線段樹優化建圖就是利用線段樹,減少連邊數量,從而降低復雜度。
二、基本思想
先建一棵線段樹。假如現在我們要從 \(8\) 號點向區間 \([3,7]\) 的所有點連一條權值為 \(w\) 有向邊。
那么怎么連邊?把區間 \([3,7]\) 拆成 \([3,4]\)、\([5,6]\) 和 \([7,7]\) 然后分別連邊。
就這樣:(如下圖所示。其中黑色普通邊的邊權為 \(0\),粉色邊的邊權為 \(w\)。)
原來我們要連 \(5\) 條邊,現在只需要連 \(3\) 條邊,也就是 \(\lceil \log_2 7\rceil\) 條邊。
於是 \(\mathcal{O}(n)\) 的邊數就優化成了 \(\mathcal{O}(\log n)\)。
那么操作三用和操作二類似的方法連邊。從區間 \([3,7]\) 的所有點向 \(8\) 號點連一條權值為 \(w\) 有向邊:(其實就是邊反了個方向)
以上是操作二與操作三分開來考慮的情形,那么操作二與操作三相結合該怎么辦呢?
考慮建兩棵線段樹,第一棵只連自上而下的邊,第二棵只連自下而上的邊。方便起見,我們把第一棵樹稱作“出樹”,第二棵樹稱作“入樹”。
初始時自上而下或自下而上地在每個節點與它的父親之間連邊。由於兩棵線段樹的葉子節點實際上是同一個點,因此要在它們互相之間連邊權為 \(0\) 的邊。初始時是這樣的:
建樹部分的代碼:(代碼中的 \(K\) 是一個常數,根據數據范圍而定。建出樹和入樹也可以分別用兩個函數實現,這樣就用不到 \(K\) 了。)
void build(int p,int l,int r){ if(l==r){a[l]=p;return ;} //a: 記錄葉子節點的編號 add(p,p<<1,0),add(p,p<<1|1,0); //出樹(從 p 向 p 的左右兒子連一條邊權為 0 的邊) add(p<<1+K,p+K,0),add(p<<1|1+K,p+K,0); //入樹(從 p 的左右兒子向 p 連一條邊權為 0 的邊) build(p<<1,l,mid); build(p<<1|1,mid+1,r); } //主函數中: for(int i=1;i<=n;i++) add(a[i],a[i]+K,0),add(a[i]+K,a[i],0); //兩棵線段樹的葉子節點之間連邊
接下來:
- 對於操作一,就從入樹的葉子節點向出樹的葉子節點連邊。
- 對於操作二,就從入樹的葉子節點向出樹中的對應區間連邊。
- 對於操作三,就從入樹中的對應區間向出樹中的葉子節點連邊。
舉個栗子。比如現在我們要從 \(8\) 號點向區間 \([3,7]\) 的所有點連一條權值為 \(w\) 有向邊。那么就如圖所示連邊:(為了讓圖更清楚,圖中把入樹和出樹葉子節點之間相連的邊省略了。)
連邊部分的代碼:
void modify(int p,int l,int r,int lx,int rx,int v,int w){ if(l>=lx&&r<=rx){ //如果當前區間被涵蓋 if(opt==2) add(v+K,p,w); //對於操作二,就從入樹的葉子節點向出樹中的對應區間連邊。 else add(p+K,v,w); //對於操作三,就從入樹中的對應區間向出樹中的葉子節點連邊。 return; } int mid=(l+r)/2; if(lx<=mid) modify(p<<1,l,mid,lx,rx,v,w); if(rx>mid) modify(p<<1|1,mid+1,r,lx,rx,v,w); } //主函數中: for(int i=1;i<=m;i++){ scanf("%lld",&opt); if(opt==1) scanf("%lld%lld%lld",&x,&y,&z),add(a[x]+K,a[y],z); //對於操作一,就從入樹的葉子節點向出樹的葉子節點連邊。 else{ scanf("%lld%lld%lld%lld",&x,&l,&r,&w); modify(1,1,n,l,r,a[x],w); } }
三、代碼實現
CF786B Legacy 無注釋完整代碼:
#include<bits/stdc++.h> #define int long long using namespace std; const int N=3e6+5,K=5e5; int n,m,s,opt,x,y,z,l,r,w,a[N],cnt,hd[N],to[N],nxt[N],val[N],d[N]; bool v[N]; priority_queue<pair<int,int> >q; void add(int x,int y,int z){ to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt,val[cnt]=z; } void build(int p,int l,int r){ if(l==r){a[l]=p;return ;} int mid=(l+r)/2; add(p,p<<1,0),add(p,p<<1|1,0); add((p<<1)+K,p+K,0),add((p<<1|1)+K,p+K,0); build(p<<1,l,mid); build(p<<1|1,mid+1,r); } void modify(int p,int l,int r,int lx,int rx,int v,int w){ if(l>=lx&&r<=rx){ if(opt==2) add(v+K,p,w); else add(p+K,v,w); return; } int mid=(l+r)/2; if(lx<=mid) modify(p<<1,l,mid,lx,rx,v,w); if(rx>mid) modify(p<<1|1,mid+1,r,lx,rx,v,w); } void dij(int s){ memset(d,0x3f,sizeof(d)),d[s]=0; q.push(make_pair(0,s)); while(q.size()){ int x=q.top().second;q.pop(); if(v[x]) continue; v[x]=1; for(int i=hd[x];i;i=nxt[i]){ int y=to[i],z=val[i]; if(d[y]>d[x]+z) d[y]=d[x]+z,q.push(make_pair(-d[y],y)); } } } signed main(){ scanf("%lld%lld%lld",&n,&m,&s),build(1,1,n); for(int i=1;i<=n;i++) add(a[i],a[i]+K,0),add(a[i]+K,a[i],0); for(int i=1;i<=m;i++){ scanf("%lld",&opt); if(opt==1) scanf("%lld%lld%lld",&x,&y,&z),add(a[x]+K,a[y],z); else{ scanf("%lld%lld%lld%lld",&x,&l,&r,&w); modify(1,1,n,l,r,a[x],w); } } dij(a[s]+K); for(int i=1;i<=n;i++) printf("%lld%c",d[a[i]]!=0x3f3f3f3f3f3f3f3fll?d[a[i]]:-1,i==n?'\n':' '); return 0; }