[題解]P2042 [NOI2005] 維護數列 - Splay解法

Sinktank發表於2024-06-23

P2042 [NOI2005] 維護數列

一道思路不難,但實現細節很多的平衡樹題,調了一天半終於做出來了w。


對於初始序列,我們可以直接構建一條鏈(畢竟一個一個呼叫插入函式也可能形成一條鏈)。題解有遞迴直接構建成一棵嚴格平衡的二叉樹的,這樣也可以,常數可能會小一點。

其中區間反轉就是裸的文藝平衡樹題解)。

區間修改&求和與文藝平衡樹的原理相同,只需要多加\(1\)個賦值的懶標記即可。

刪除都是區間操作。我們受文藝平衡樹的啟發,找到區間的左右端點\(l,r\),然後再找到\(l\)在中序遍歷中的前驅節點\(L\)\(r\)在中序遍歷中的後繼節點\(R\)splay(L,0),在splay(R,L),這樣\(R\)的左子樹就是要操作的區間\([l,r]\)了。刪除\([l,r]\)區間,直接斷開\(R\)與其左子樹的連線即可。

插入同樣是區間操作,我們可以把輸入的值連成一條鏈(或構建成一棵嚴格平衡的二叉樹),然後找我們要插入的位置是哪一個節點,設要插入的位置為\(pos\)之後,那麼找到的節點\(u\)就是中序遍歷第\(pos\)個節點。我們再尋找\(u\)的後繼節點,把這條連結到後繼它上面就可以了(別忘了無右兒子的情況)。

平衡樹維護最大子列和其實不難,但這道題有個很毒的坑點:子列不能為空,然而題面卻沒有給出!

每個節點\(u\)額外維護\(3\)個資訊:

  • \(lmax\):子樹\(u\)的中序遍歷的最大字首(可以為空)。
  • \(rmax\):子樹\(u\)的中序遍歷的最大字尾(可以為空)。
  • \(maxx\):子樹\(u\)的中序遍歷的最大子列和(不能為空)。

初始化(\(sum\)表示子樹權值和):
\(maxx(u)=v(u)\\lmax(u)=rmax(u)=\max(v(u),0)\)

轉移:

  • \(\color{green}lmax(u)=\max\{lmax(lc(u)),sum(lc(u))+v(u)+lmax(rc(u)),0\}\)
    (如果不選\(u\)就是\(lmax(lc(u))\),如果選\(u\)就是\(sum(lc(u))+v(u)+lmax(rc(u))\),根據定義可以為空,所以還要和\(0\)取一個\(\max\)
  • \(\color{darkcyan}rmax(u)=\max\{rmax(rc(u)),sum(rc(u))+v(u)+rmax(lc(u)),0\}\)
    (同理)
  • \(\color{mediumblue}maxx(u)=\max\{rmax(lc(u))+v(u)+lmax(rc(u)),maxx(lc(u)),maxx(rc(u))\}\)
    (如果不選\(u\)就直接等於左邊的答案/右邊的答案,如果選\(u\)就是左邊的字尾最大+\(u\)的權值+右邊的字首最大)

pushdown中的轉移(set_sum)見程式碼。

注意到轉移的第\(3\)個式子中,\(rmax(lc(u))+v(u)+lmax(rc(u))\)是選\(u\)的情況,所以前字尾可以為空。


其實就這些了,思路不難,不過程式碼實現有很多需要注意。如果遇到問題可以查一下:

  • 子列不能為空。
  • 兩個哨兵值要置為\(0\),否則計算\(sum\)可能會出問題。
  • 一定一定要pushdown,在查詢第\(k\)名、插入節點的過程中都要pushdown
  • 插入次數是\(4*10^6\),如果開這麼大的陣列會MLE。但題目保證任何時候平衡樹裡不超過\(5*10^5\)個元素。我們考慮把刪掉的空間重複利用:每刪除一個子樹,就把這個子樹的根節點入棧,為插入的節點分配空間時,先看棧中有沒有節點,如果有,則把該節點拿出來,把它的左右子節點(如果有)入棧。沒有必要刪除後一次性把刪掉的節點都入棧。
  • 區間翻轉需要lmaxrmax交換位置。
  • 和線段樹相似地,下放標記時對左兒子、右兒子操作,而非自己。拿計算\(sum\)來距離,pushdown(x)應該是更新左右孩子的\(sum\),而自己的\(sum\)應該在主函式中完成設定。其他同理。這應該作為寫pushdown的一個習慣。因為程式碼中,很有可能呼叫pushdown(x)後就去獲得\(x\)子節點的各種資訊(比如ins函式中pushdown(u)後就去獲取\(lc(rc(u))\)),所以必須保證子節點狀態得到更新。
點選檢視程式碼
#include<bits/stdc++.h>
#define int long long
#define N 500010
#define lc(x) tr[x].ch[0]
#define rc(x) tr[x].ch[1]
#define ch(x,y) tr[x].ch[y]
#define fa(x) tr[x].fa
#define v(x) tr[x].v
#define siz(x) tr[x].siz
#define tag(x) tr[x].tag
#define tag2(x) tr[x].tag2
#define sum(x) tr[x].sum
#define lmax(x) tr[x].lmax
#define rmax(x) tr[x].rmax
#define maxx(x) tr[x].maxx
using namespace std;
struct tree{
	int ch[2],siz,fa,v,tag2,sum;
	int lmax,rmax,maxx;
	bool tag;
	void init(){ch[0]=ch[1]=siz=fa=v=lmax=rmax=maxx=tag=0;tag2=LLONG_MIN;}
}tr[N];
int n,m,cnt,root,nodecnt;
stack<int> fr;
void init(int u){tr[u].init();}
int newnode(int v,int siz=1){
	int u;
	if(fr.empty()) u=++cnt;
	else{
		u=fr.top(),fr.pop();
		if(lc(u)) fr.push(lc(u));
		if(rc(u)) fr.push(rc(u));
	}
	init(u);
	sum(u)=v(u)=maxx(u)=v,siz(u)=siz;
	lmax(u)=rmax(u)=max(v,0ll);
	return u;
}
void update(int u){
	siz(u)=siz(lc(u))+siz(rc(u))+1;
	sum(u)=sum(lc(u))+v(u)+sum(rc(u));
	lmax(u)=max(max(lmax(lc(u)),sum(lc(u))+v(u)+lmax(rc(u))),0ll);
	rmax(u)=max(max(rmax(rc(u)),sum(rc(u))+v(u)+rmax(lc(u))),0ll);
	maxx(u)=rmax(lc(u))+v(u)+lmax(rc(u));
	if(lc(u)) maxx(u)=max(maxx(u),maxx(lc(u)));
	if(rc(u)) maxx(u)=max(maxx(u),maxx(rc(u)));
}
bool get(int u){return u==rc(fa(u));}
void rot(int x){
	int y=fa(x),z=fa(y);//保證y!=0
	bool dir=get(x),tdir=get(y);
	if(ch(x,!dir)) fa(ch(x,!dir))=y;
	ch(y,dir)=ch(x,!dir);
	ch(x,!dir)=y;
	fa(y)=x,fa(x)=z;
	if(z) ch(z,tdir)=x;
	update(y),update(x);
}
void splay(int x,int y){
	for(int f;(f=fa(x))!=y;rot(x))
		if(fa(f)!=y) rot(get(f)==get(x)?f:x);
	if(!y) root=x;
}
void set_sum(int x,int v){//用於pushdown和主函式
	tag2(x)=v,v(x)=v,sum(x)=v*siz(x);
	if(v>0) lmax(x)=rmax(x)=maxx(x)=sum(x);
	else lmax(x)=rmax(x)=0,maxx(x)=v;
}
//用於pushdown和主函式
void set_rev(int x){swap(lc(x),rc(x)),swap(lmax(x),rmax(x)),tag(x)^=1;}
void pushdown(int x){
	if(tag2(x)!=LLONG_MIN){
		if(lc(x)) set_sum(lc(x),tag2(x));
		if(rc(x)) set_sum(rc(x),tag2(x));
		tag(x)=0;//相當於一個小最佳化,如果已經區間賦值了,那麼翻轉不翻轉無所謂了
		tag2(x)=LLONG_MIN;
	}
	if(tag(x)){
		if(lc(x)) set_rev(lc(x));
		if(rc(x)) set_rev(rc(x));
		tag(x)=0;
	}
}
int find(int num){//找中序遍歷第num個是哪個節點
	int u=root;
	while(u){
		pushdown(u);//別忘了pushdown
		if(siz(lc(u))+1==num) break;
		else if(siz(lc(u))>=num) u=lc(u);
		else num-=siz(lc(u))+1,u=rc(u);//這兩句順序別反了
	}
	return u;
}
void ins(int num,int v){//在中序第num後插入子樹v
	int u=find(num+1);
	pushdown(u);//pushdown不要忘記
	int nex=rc(u);
	if(!nex) rc(u)=v,fa(v)=u,splay(u,0);
	else{
		while(lc(nex)){
			pushdown(nex);//一定記得寫pushdown
			nex=lc(nex);
		}
		pushdown(nex);//這個也不要忘
		lc(nex)=v,fa(v)=nex,splay(nex,0);
	}
}
void make_range(int& l,int& r){//取出[l,r]的區間,放在lc(r)中
	l=find(l),r=find(r+2);//本來應該是l-1,r+1,但因為有哨兵節點所以需要+1
	splay(l,0),splay(r,l);//lc(r)就是要操作的部分
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>m;
	newnode(0,1);//兩個哨兵的值都為0,否則sum計算會出問題
	for(int i=1;i<=n+1;i++){//連成一條鏈
		int v;
		if(i==n+1) v=0;
		else cin>>v;
		newnode(v);
		lc(cnt)=cnt-1;
		fa(cnt-1)=cnt;
		update(cnt);
	}
	root=nodecnt=n+2;//根節點不是1而是n+2
	while(m--){
		string op;
		cin>>op;
		if(op=="INSERT"){
			int pos,tot,x;
			cin>>pos>>tot;
			if(tot==0) continue;
			int cur,last;
			for(int i=1;i<=tot;i++){//把輸入連成一條鏈
				cin>>x;
				cur=newnode(x,i);
				if(i>1) fa(last)=cur,lc(cur)=last;
				update(cur);
				last=cur;
			}
			ins(pos,cur);//cur是這條鏈的根節點
			nodecnt+=tot;
		}else if(op=="DELETE"){
			int l,tot,r;
			cin>>l>>tot;
			r=l+tot-1;
			make_range(l,r);
			if(lc(r)) fa(lc(r))=0,fr.push(lc(r)),lc(r)=0;//需要將子樹根節點入棧
			nodecnt-=tot;
		}else if(op=="MAKE-SAME"){
			int l,tot,r,v;
			cin>>l>>tot>>v;
			r=l+tot-1;
			make_range(l,r);
			if(lc(r)) set_sum(lc(r),v);
		}else if(op=="REVERSE"){
			int l,tot,r;
			cin>>l>>tot;
			r=l+tot-1;
			make_range(l,r);
			if(lc(r)) set_rev(lc(r));
		}else if(op=="GET-SUM"){
			int l,tot,r;
			cin>>l>>tot;
			r=l+tot-1;
			make_range(l,r);
			cout<<sum(lc(r))<<"\n";
		}else if(op=="MAX-SUM"){
			int l=1,r=nodecnt-2;
			make_range(l,r);
			cout<<maxx(lc(r))<<"\n";
		}
	}
	return 0;
}

相關文章