“北京的士”上線: 100 多家巡遊計程車實現網約化運營
前言
以下純屬口胡
正文
記得陣列開四倍。
普通線段樹
演算法思想為從樹根開始,往下遞迴。
定義為:
struct Node{
int l,r/*左右兒子*/,val/*權值*/,tag/*懶惰標記*/;
}e[4000001];
建樹
很簡單,由根節點遞歸向下建樹,搜到葉子節點後,回溯更改區間值,程式碼為。
void build(int x,int l,int r) { e[x].l=l,e[x].r=r; if (l==r) {//搜到葉子節點 e[x].val=val[l]; return; } int mid=l+r>>1; build(x*2,l,mid); build(x*2+1,mid+1,r); //左右遞迴建樹 e[x].val=e[x*2].val+e[x*2+1].val;//回溯賦值 }
修改
單點:很簡單,相信大家也都會(而且也基本用不到),就不提了。
區間:這裡就需要提到一個東西了,\(Lazy\;tag\)。
這是個啥呢,舉個例子,你在區間修改時,如果從根節點開始修改,並且線段樹的深度還特別大,那麼就需要遞迴到葉子節點,複雜度可想而知。
而 \(Lazy\;tag\) 呢,就是在修改時,給當前節點打上標記,如果要查詢,就把標記下傳,達到優化的效果。
下傳函式的寫法為:
void pushdown(int x) { if (e[x].tag) { e[x*2].val+=e[x].tag*(e[x*2].r-e[x*2].l+1); e[x*2+1].val+=e[x].tag*(e[x*2+1].r-e[x*2+1].l+1); e[x*2].tag+=e[x].tag; e[x*2+1].tag+=e[x].tag; e[x].tag=0; } }
得出了這個,修改函式的寫法也就十分簡單了,如下:
void updata(int x,int l,int r,int pos) { if (l<=e[x].l and r>=e[x].r) { e[x].val+=pos*(e[x].r-e[x].l+1); e[x].tag+=pos; return; } pushdown(x); int mid=e[x].l+e[x].r>>1; if (l<=mid) updata(x*2,l,r,pos); if (r>mid) updata(x*2+1,l,r,pos); e[x].val=e[x*2].val+e[x*2+1].val; }
查詢
單點:依舊不提。
區間:和修改函式相似,依舊採用 \(Lazy\;tag\) 優化,程式碼如下:
int ask(int x,int l,int r) {
if (l<=e[x].l and r>=e[x].r) return e[x].val;
pushdown(x);
int mid=e[x].l+e[x].r>>1;
int ans=0;
if (l<=mid) ans+=ask(x*2,l,r);
if (r>mid) ans+=ask(x*2+1,l,r);
return ans;
}
線段樹1 AC Code:
#include <iostream>
#define int long long
using namespace std;
int n,k,val[1000001];
struct Node{
int l,r,val,tag;
}e[4000001];
void build(int x,int l,int r) {
e[x].l=l,e[x].r=r;
if (l==r) {
e[x].val=val[l];
return;
}
int mid=l+r>>1;
build(x*2,l,mid);
build(x*2+1,mid+1,r);
e[x].val=e[x*2].val+e[x*2+1].val;
}
void pushdown(int x) {
if (e[x].tag) {
e[x*2].val+=e[x].tag*(e[x*2].r-e[x*2].l+1);
e[x*2+1].val+=e[x].tag*(e[x*2+1].r-e[x*2+1].l+1);
e[x*2].tag+=e[x].tag;
e[x*2+1].tag+=e[x].tag;
e[x].tag=0;
}
}
void updata(int x,int l,int r,int pos) {
if (l<=e[x].l and r>=e[x].r) {
e[x].val+=pos*(e[x].r-e[x].l+1);
e[x].tag+=pos;
return;
}
pushdown(x);
int mid=e[x].l+e[x].r>>1;
if (l<=mid) updata(x*2,l,r,pos);
if (r>mid) updata(x*2+1,l,r,pos);
e[x].val=e[x*2].val+e[x*2+1].val;
}
int ask(int x,int l,int r) {
if (l<=e[x].l and r>=e[x].r) return e[x].val;
pushdown(x);
int mid=e[x].l+e[x].r>>1;
int ans=0;
if (l<=mid) ans+=ask(x*2,l,r);
if (r>mid) ans+=ask(x*2+1,l,r);
return ans;
}
signed main() {
cin>>n>>k;
for (int i=1;i<=n;i++) cin>>val[i];
build(1,1,n);
for (int i=1,opt,x,y,tmp;i<=k;i++) {
cin>>opt;
if (opt==1) {
cin>>x>>y>>tmp;
updata(1,x,y,tmp);
} else {
cin>>x>>y;
cout<<ask(1,x,y)<<endl;
}
}
}
zkw 線段樹
本文重點來了,先放一下線段樹1 AC 程式碼:
#include <iostream>
#include <cstdio>
#define MAXN 200005
#define int long long
using namespace std;
int n,N=1,m;
int tree[MAXN<<2],add[MAXN<<2];
void build() {
for (;N<=n+1;N<<=1);
for (int i=N+1;i<=N+n;i++) scanf("%d",tree+i);
for (int i=N-1;i>=1;i--) tree[i]=tree[i<<1]+tree[i<<1|1];
}
void updata(int l,int r,int k) {
int lnum=0,rnum=0,nnum=1;
for (l=N+l-1,r=N+r+1;l^r^1;l>>=1,r>>=1,nnum<<=1) {
tree[l]+=k*lnum,tree[r]+=k*rnum;
if (~l&1) add[l^1]+=k,tree[l^1]+=k*nnum,lnum+=nnum;
if (r&1) add[r^1]+=k,tree[r^1]+=k*nnum,rnum+=nnum;
}
for (;l;l>>=1,r>>=1) tree[l]+=k*lnum,tree[r]+=k*rnum;
}
int ask(int l,int r) {
int lnum=0,rnum=0,nnum=1;
int ans=0;
for (l=N+l-1,r=N+r+1;l^r^1;l>>=1,r>>=1,nnum<<=1) {
if (add[l]) ans+=add[l]*lnum;
if (add[r]) ans+=add[r]*rnum;
if (~l&1) ans+=tree[l^1],lnum+=nnum;
if (r&1) ans+=tree[r^1],rnum+=nnum;
}
for (;l;l>>=1,r>>=1) ans+=add[l]*lnum+add[r]*rnum;
return ans;
}
void work() {
cin>>n>>m;
build();
for (int i=1,opt,x,y,k;i<=m;i++) {
cin>>opt>>x>>y;
if (opt==1) {
cin>>k;
updata(x,y,k);
} else if (opt==2) {
cout<<ask(x,y)<<endl;
}
}
}
signed main() {
work();
}
是不是發現短了很多。
其實,不止是長度變短了,並且速度也快了不少,和普通線段樹對比如下。
普通線段樹:
zkw 線段樹:
為什麼會這樣呢?
因為 zkw 線段樹和普通線段樹的演算法不一樣(廢話),普通線段樹是從根節點到葉子結點,而 zkw 線段樹是從葉子節點到根節點。
???
從葉子節點到根節點?那不是樹狀陣列嗎?
啊,其實不然,zkw 線段樹相較於樹狀陣列的的操作可以更多一點(所以複雜度也相應變高了)。
建樹
從葉子結點往上建樹,咋建呢?
很簡單,我們需要一個變數 \(N\),這個變數是記錄葉子結點的第一位的編號減一的位置,而要求得它我們就需要一個神奇的 for
迴圈,如下:
for (;N<=n+1;N<<=1);
這個 for
的意思是隻要 \(N\) 的編號不到葉子結點(因為葉子結點為 \(n\) 的滿二叉樹,其節點數為 \(2\times n-1\)),就倍增下去。
接著到了輸入資料(沒錯 zkw 建樹需要輸入資料),直接把資料存到葉子結點即可,接著往上推,直到推到根節點,程式碼如下:
void build() {
for (;N<=n+1;N<<=1);
for (int i=N+1;i<=N+n;i++) scanf("%lld",tree+i);
for (int i=N-1;i>=1;i--) tree[i]=tree[i<<1]+tree[i<<1|1];
}
修改
單點:不提。
區間:這裡需要連個指標:\(l\) 和 \(r\) ,他們代表的是左區間減一和右區間加一,從下往上推,直到推到父節點相等停止。
是不是覺得少了什麼,沒錯 \(Lazy\;tag\),其實 zkw 線段樹的 \(Lazy\;tag\) 和不同線段樹還不一樣,他才用了標記永久化的思想,就是讓它一直懶下去,然後如果你位運算好一點的話,就可以寫出程式碼了:
void updata(int l,int r,int k) {
int lnum=0/*l 遍歷到的節點個數*/,rnum=0/*r 遍歷到的節點個數*/,nnum=1/*區間節點個數*/;
for (l=N+l-1,r=N+r+1;l^r^1;l>>=1,r>>=1,nnum<<=1) {
tree[l]+=k*lnum,tree[r]+=k*rnum;
if (~l&1) add[l^1]+=k,tree[l^1]+=k*nnum,lnum+=nnum;
if (r&1) add[r^1]+=k,tree[r^1]+=k*nnum,rnum+=nnum;
}
for (;l;l>>=1,r>>=1) tree[l]+=k*lnum,tree[r]+=k*rnum;
}
看到這一大坨位運算是不是很蒙,別急我來解釋一下。
l^r^1
:什麼意思呢很簡單就是判斷 \(l\) 和 \(r\) 的父節點是否相同,是返回 \(0\),否返回 \(1\)。
~l&1
和 r&1
:判斷 \(l\) 和 \(r\) 是否為根節點。
最後一句:其實就是如果 \(l\) 和 \(r\) 都不為 \(1\) 時,對根節點的更改操作。
查詢
單點:不提。
區間:和上面類似,也需要 \(l\)、\(r\) 和 \(Lazy\;tag\),大致操作類似,只不過需要多一句:
if (add[l]) ans+=add[l]*lnum;
if (add[r]) ans+=add[r]*rnum;
(好吧是兩句)
這兩句也就是判斷是否有懶惰標記,整體程式碼如下:
int ask(int l,int r) {
int lnum=0,rnum=0,nnum=1;
int ans=0;
for (l=N+l-1,r=N+r+1;l^r^1;l>>=1,r>>=1,nnum<<=1) {
if (add[l]) ans+=add[l]*lnum;
if (add[r]) ans+=add[r]*rnum;
if (~l&1) ans+=tree[l^1],lnum+=nnum;
if (r&1) ans+=tree[r^1],rnum+=nnum;
}
for (;l;l>>=1,r>>=1) ans+=add[l]*lnum+add[r]*rnum;
return ans;
}
\(finally\)
zkw 線段樹還是很有用的(畢竟線段樹那常數大的呀),但是弊端很明顯,不能處理優先順序問題,如 p3373。
就這樣,byebye。