NOI2005 維護數列 題解 (洛谷 P2042) splay
前言
語文老師佈置了隨筆的作業,要求每週兩篇,題材字數不限。
我對著我貧瘠的人生沉思了一會兒,決定用題解矇混過關。
正文
區間翻轉,一眼 splay。
其實我平衡樹只會 splay
定眼一看全是區間操作,慣例把區間字首旋到根,把區間字尾旋到根的右兒子,然後對著它的左兒子快樂操作就好了。
#define work ch[ch[rt][1]][0]
並沒有什麼用,但是可以簡化程式碼
因為要找區間前驅後繼,所以要在頭尾插入哨兵節點以防越界。
fa[2]=rt=1,ch[1][1]=num=2;
插好了
插入了頭節點,所以後面所有的 posi 都應該 +1。
然後依次分析每個操作。
1.區間插入
在第 posi 個數之後插入一段序列,所以區間字首就是 kth(posi),字尾是 kth(posi+1)。(kth 即求第 k 個數,返回節點編號)
區間建樹採用遞迴。
用中序遍歷的順序處理,還可以省略中轉陣列,邊讀入邊建樹。
點選檢視程式碼
int build(int l,int r,int f){ if(l>r) return 0; int mid=(l+r)>>1,u=++num; ch[u][0]=build(l,mid-1,u); val[u]=read(),fa[u]=f; ch[u][1]=build(mid+1,r,u); pushup(u); return u; } void insert(){ work=build(1,m,ch[rt][1]); pushup(ch[rt][1]),pushup(rt); } //在 main 函式中: n=read()+1,m=read(); //n 即為 posi(變數名只是個代號qwq),m為區間長度 splay(kth(n)),splay(kth(n+1),rt); insert();
對初始序列的建樹也可以視作上面這樣的區間建樹。
2~5.區間修改、查詢
這些操作針對的均是從 posi 開始的 tot 個數,即區間 [posi,posi+tot)。
所以它的字首為 kth(posi-1),字尾為 kth(posi+tot)。
對於區間刪除,我們只需要斷掉根的右兒子與它左兒子的聯絡即可。
對於區間推平,我們將區間中每個節點的區間和修改為區間大小與修改值的積。
區間翻轉,相當於交換區間中每個節點的左右兒子。我們先交換當前節點的左右兒子。
注意到這兩個操作均需要下傳標記,所以開兩個懶標記陣列,chg[] 記錄區間被賦的值,tag[] 記錄區間是否翻轉。
注意到
任何時刻數列中任何一個數字均在 [-10^3, 10^3] 內
於是我們可以通過給 chg[] 賦一個不在此範圍內的值 inf 來表示此節點未被賦值。
對於區間求和,直接輸出區間和即可。
不要忘記將哨兵節點的 chg[] 也賦為 inf。
由於需要求和,哨兵節點的 val[] 應為 0。
點選檢視程式碼
void change(int x,int k){
chg[x]=val[x]=k,tag[x]=0;
sum[x]=siz[x]*k;
}
void turn(int x){
swap(ch[x][0],ch[x][1]);
tag[x]^=1;
}
void pushdown(int x){
if(chg[x]!=inf){
change(ch[x][0],chg[x]);
change(ch[x][1],chg[x]);
tag[x]=0,chg[x]=inf;
}
if(tag[x]){
turn(ch[x][0]);
turn(ch[x][1]);
tag[x]=0;
}
}
//在 main 函式中:
blabla..//初始化
for(char op[10];q--;){
scanf(" %s",op);
blabla..//操作6
n=read()+1,m=read();
blabla..//操作1
splay(kth(n-1)),splay(kth(n+m),rt);
switch(op[0]){
case 'G':printf("%d\n",sum[work]);continue;
//switch 不能匹配 continue,這裡繼續迴圈、進行下一個操作
case 'D':work=0;break;
case 'R':turn(work);break;
case 'M':change(work,read());
}
pushup(ch[rt][1]),pushup(rt);//不要忘記在區間修改後上傳
}
6.最大子段和
對於靜態區間,這個問題有 O(n) 的簡單 dp,當然這與此題無關。
下面簡述一下適用於本題的帶修改 O(nlogn) 做法。
對於每段區間,記錄這一區間的最大子段和 ms[],最大字首和 ls[],最大字尾和 rs[]。
對於單節點區間,顯然易得,三者均為 max(0,val[])。
其他節點歸併求解。
最大子段和對應子段有三種情況,完全位於左或右兒子,或橫跨兩兒子。
最大字首和對應字首有兩種情況,完全位於左兒子,或包含左兒子、延伸至右兒子。最大字尾和同理。
在 pushup 函式中一併處理即可。
點選檢視程式碼
void pushup(int x){
int l=ch[x][0],r=ch[x][1];
siz[x]=siz[l]+siz[r]+1;
sum[x]=sum[l]+val[x]+sum[r];
ls[x]=max(ls[l],sum[l]+val[x]+ls[r]);
rs[x]=max(rs[r],sum[r]+val[x]+rs[l]);
ms[x]=max(max(ms[l],ms[r]),rs[l]+val[x]+ls[r]);
//由於三者均非負,所以可以簡化掉一些具體的分類,僅討論這幾種
//若該節點無左或右兒子,則 l 或 r 為 0,對應的值為 0,在取 max 時不會造成影響
//所以對於葉子節點也無需特判
}
//在 main 函式中:
if(op[2]=='X'){printf("%d\n",ms[rt]);continue;}
到這裡,我們寫完了一份程式碼。
提交後可以獲得
90pts WA on #3
的好成績。
定眼一看報錯資訊:
Wrong Answer.wrong answer On line 79 column 1, read 0, expected -.
一個負數答案被求解為 0。
回顧上面求最大子段和的過程,可以發現,我們在處理每個區間時,可以選擇不選,將答案記為 0。
這樣,當區間內的數全部為負時,所得答案為 0。
如果要求最大子段和必須選數,則需要特判一下這種情況。
這裡提供一種特判思路wtcl想不到其他的方法:
記錄區間最大值 mx[],若最大值為負,則區間全部為負,此時顯然最大子段和即為這個值。
否則輸出求得的最大子段和即可。
注意,前面將哨兵節點的 val[] 設為了 0,在記錄 mx[] 時不能記錄它們的 val[]。
點選檢視程式碼
//在 pushup 函式中:
mx[x]=max(x>2?val[x]:-inf,max(mx[l],mx[r]));
//在 main 函式中:
mx[0]=-inf;//防止缺少兒子的節點 mx[] 更新出錯
blabla..
if(op[2]=='X'){printf("%d\n",mx[rt]<0?mx[rt]:ms[rt]);continue;}
再次提交:
90pts MLE on #8
emm……
再仔細讀題發現:
任何時刻數列中最多含有 5 * 10^5 個數,
插入的數字總數不超過 4 * 10^6。
我們可以通過回收節點來減小陣列大小。
在刪除一段區間時,遍歷對應的所有節點,將它們的編號放入一個棧方便。
在新建節點時,若棧中有節點編號閒置,則使用。
因為這一編號是使用過的,所以需要清空標記等資訊否則會獲得 10pts AC on #1 的好成績。(事實上大部分資訊會在 pushup 中更新,這裡初始化懶標記即可)
點選檢視程式碼
void recycle(int u){
if(!u) return;
recycle(ch[u][0]);
stk[++tp]=u;
recycle(ch[u][1]);
}
int build(int l,int r,int f){
if(l>r) return 0;
int mid=(l+r)>>1,u=tp?stk[tp--]:++num;
ch[u][0]=build(l,mid-1,u);
val[u]=read(),fa[u]=f,chg[u]=inf,tag[u]=0;
ch[u][1]=build(mid+1,r,u);
pushup(u);
return u;
}
//在 main 函式中:
case 'D':recycle(work);work=0;break;
到這裡即可 AC 本題。
最後貼一些關鍵程式碼:
點選檢視程式碼
//省略標頭檔案
#define work ch[ch[rt][1]][0]
//省略快讀
const int N=5e5+5,inf=1e4;
//省略所開變數
void recycle(int x){
if(!x) return;
recycle(ch[x][0]);
stk[++tp]=x;
recycle(ch[x][1]);
}
void pushup(int x){
int l=ch[x][0],r=ch[x][1];
siz[x]=siz[l]+siz[r]+1;
sum[x]=sum[l]+val[x]+sum[r];
ls[x]=max(ls[l],sum[l]+val[x]+ls[r]);
rs[x]=max(rs[r],sum[r]+val[x]+rs[l]);
ms[x]=max(max(ms[l],ms[r]),rs[l]+val[x]+ls[r]);
mx[x]=max(x>2?val[x]:-inf,max(mx[l],mx[r]));
}
void change(int x,int k){
chg[x]=mx[x]=val[x]=k,tag[x]=0;
sum[x]=siz[x]*k;
ls[x]=rs[x]=ms[x]=max(0,sum[x]);
}
void turn(int x){
swap(ch[x][0],ch[x][1]);
swap(ls[x],rs[x]);
tag[x]^=1;
}
void pushdown(int x){
if(chg[x]!=inf){
change(ch[x][0],chg[x]);
change(ch[x][1],chg[x]);
tag[x]=0,chg[x]=inf;
}
if(tag[x]){
turn(ch[x][0]);
turn(ch[x][1]);
tag[x]=0;
}
}
//省略旋轉操作
int build(int l,int r,int f){
if(l>r) return 0;
int mid=(l+r)>>1,x=tp?stk[tp--]:++num;
ch[x][0]=build(l,mid-1,x);
val[x]=read(),fa[x]=f,chg[x]=inf,tag[x]=0;
ch[x][1]=build(mid+1,r,x);
pushup(x);
return x;
}
void insert(){
work=build(1,m,ch[rt][1]);
pushup(ch[rt][1]),pushup(rt);
}
int kth(int x){
for(int u=rt,s;;){
pushdown(u);
//所有操作都需要先求第 k 個數將其上旋,所以只在這裡 pushdown 即可
s=siz[ch[u][0]];
if(x<=s) u=ch[u][0];
else{
x-=s+1;
if(!x) return u;
u=ch[u][1];
}
}
}
int main(){
fa[2]=rt=1,ch[1][1]=num=2,chg[1]=chg[2]=inf,mx[0]=-inf;
m=read(),q=read();insert();
for(char op[10];q--;){
scanf(" %s",op);
if(op[2]=='X'){printf("%d\n",mx[rt]<0?mx[rt]:ms[rt]);continue;}
n=read()+1,m=read();
if(op[0]=='I'){
splay(kth(n)),splay(kth(n+1),rt);
insert();
continue;
}
splay(kth(n-1)),splay(kth(n+m),rt);
switch(op[0]){
case 'G':printf("%d\n",sum[work]);continue;
case 'D':recycle(work);work=0;break;
case 'R':turn(work);break;
case 'M':change(work,read());
}
pushup(ch[rt][1]),pushup(rt);
}
return 0;
}