1. 程式人生 > 其它 >NOI2005 維護數列 題解 (洛谷 P2042) splay

NOI2005 維護數列 題解 (洛谷 P2042) splay

前言

語文老師佈置了隨筆的作業,要求每週兩篇,題材字數不限。
我對著我貧瘠的人生沉思了一會兒,決定用題解矇混過關。

正文

link

區間翻轉,一眼 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;
}