1. 程式人生 > 遊戲 >rogue-lite《遊靈》開啟搶先體驗 首發特惠促銷

rogue-lite《遊靈》開啟搶先體驗 首發特惠促銷

LCT學習筆記

前言

老呂又講了LCT,據他說特別簡單,於是就強行灌輸

引入

  • 維護一棵樹,維護以下的操作:
    • 鏈上求和
    • 鏈上求最值
    • 鏈上修改
    • 子樹修改
    • 子樹求和

可能你第一眼想的是樹鏈剖分,的確,這都是樹鏈剖分的基本操作。

但是如果再增加一些操作呢

    • 換根

    • 斷開樹上的一條邊

    • 連線兩個點,保證連線後仍是一棵樹

線段樹就不好做了,於是我們的 LCT 就出場了。

簡介

LCT,全稱 Link-Cut Tree,一種動態樹,用來解決動態的樹上問題。說它是樹也不大準確,它維護的其實是一個森林。據我不可信的猜測,這個名字可能是由於這個資料結構特有的特色來命名的,也就是 Link,Cut,支援樹上的刪邊,加邊。這一點是普通線段樹沒法做到的,LCT的 access 也是他的一大特色,也是常用的一個函式。(個人感覺)

構造

我們在學習樹鏈剖分的時候,就知道,將鏈進行剖分,主要有三種形式:

  1. 重鏈剖分。只要是按照子樹大小進行剖分,就是把兒子數最多的兒子當做重兒子,重兒子連成的鏈叫做重鏈。
  2. 長鏈剖分。並不是很常見,我也不到了解。
  3. 實鏈剖分。將樹上的鏈分成虛實兩種,一個點最多隻有一個孩子作為實孩子。連線實孩子的稱為實邊,實邊組成的鏈稱為實鏈。

我們在 LCT 中就是才用的實鏈剖分,其中,實孩子是不固定的,它可以通過我們的修改而發生改變,我想,這也是 LCT 的一個動態,當然,其主要的動態還是動態刪邊和加邊。因此,我們需要選用更靈活的資料結構。

維護一條鏈,理論上 FHQ-Treap 和 Splay 都是可以的,但是 FHQ-Treap 要比 Splay 多一個 \(\log\)

,而且網路上的題解大部分都寫的是 Splay,因此,這裡推薦 Splay 的寫法,不會 Splay 的可以去學習一下,因為這是非常重要的一部分。因為我也沒寫過FHQ-Teap的

我們在之前說過,一個點頂多只有一個實孩子,也就是說一條實鏈上,每個節點的深度在原樹中都是不同的,因此,我們把深度作為關鍵字用 Splay 維護,對於一個節點,它的左兒子的深度要比它小,右兒子的深度要比它大。

這裡補充一下兩個概念:

原樹:也就是我們對其進行剖分的樹。在我們實現的時候,原樹是 不儲存 的,只是為了方便我們理解。

輔助樹:也就是一棵splay,或者說一些 Splay。

  • 它維護的是原樹中的一條實鏈,在程式中真正操作的都是輔助樹。中序遍歷這些點的時候,其對應的就是原樹中的一條鏈。

  • 在 LCT 中每棵 Splay 的根節點的指向 原樹這條鏈 的鏈頂的父親節點(即鏈最頂端的點的父親節點)。主要的特點在於兒子認父親,而父親不認兒子,對應原樹的一條 虛邊

基礎操作

我們先造一顆樹。這是一棵原樹。

我們選擇一些邊作為虛邊,選擇一些邊作為實邊。

然後,讓我們畫出輔助樹。

我們找出其中的 Splay,大概就是這個亞子。

瞭解完這些之後,我們開始今天的重點。

變數宣告

我習慣用將變數放到結構體裡。

  • tree.ch[0/1] 左右兒子
  • f[N] 父親
  • tree.sum 路徑權值和
  • tree.val 點權
  • tree.laz[N] 翻轉標記

主要的函式:

  • link(x,y)連線兩個點

  • cut(x,y):斷開兩個點間的邊

  • access(x):把 \(x\) 點下面的實邊斷開,並把 \(x\) 點一路向上邊到樹的根

  • makeroot(x):把 \(x\) 點變為樹的根

  • find(x):查詢 \(x\) 所在樹的根

  • isroot(x):判斷 \(x\) 是否是輔助樹的根

  • split(x,y) : 提取出 \(x,y\) 間的路徑

  • update(x,y) : 修改 \(x\) 的點權為 \(y\)

當然還有 rotatesplaypushuppushdown ,不過這些都是線段樹或 Splay 的基本操作,就不詳細展開了。

accsee

斷開當前點連的實鏈,到根節點連一條實鏈。

\(x\) 點伸展到splay的根,再把它的右子樹連到 \(t\)\(t\) 的初值為 0,也就了與下一層的實鏈斷開了,然後 \(t\) 更新為 \(x\),而 \(x\) 更新為 \(x\) 的父親,繼續向上連線。因為我們現在的連線,父親認兒子,兒子認父親,一直到根,也就到根連線了一條實鏈。

假設我們 \(access(9)\) ,我們的圖就變成了這樣。原諒我不會製作動圖,沒有詳細的變化過程。

void access(int p) 
{
	int t=0;//因為當前點是這條鏈的最後一個點,旋轉到根之後右邊的點就是當前點之後的點,也就是要斷開的點
	while(p)
	{
		splay(p);//把 p 伸展到根節點, 
		rson(p)=t;//不斷讓父親向它連邊,也就是連上了實邊 
		t=p;
		p=f[p];
		push_up(p);
	}
}

makeroot

作用:把x點變為所在原樹的根。

方法:首先的把 \(x\)\(access\) 到根,把 \(x\) 點到根就變成了一個 Splay,然後把 \(x\) 伸展到根。由於 \(x\) 點是輔助樹在原樹中最下面的點,所以這時其它的點都在 \(x\) 的左子樹上,只要把左子樹變成右子樹,\(x\) 也就變成了根。

我們上面 \(accsee(9)\) ,不妨就繼續讓 \(9\) 變成根。先 Splay 一下。

void makeroot(int p)//是當前點變成原樹裡的根節點 
{
	access(p);//到根節點連實鏈,也就是一顆 splay
	splay(p);//將當前點轉到根節點
	tree[p].laz^=1//由於 x 點是最後一個,當前為根節點時所有的點都在他的左邊,^一下讓所有的點都在他右邊,就變成了根了
}

findtoot

作用:查詢原樹的根

我們想一下,在輔助樹中,怎麼才能找到原樹的根呢?

我們發現,位於最頂部的 Splay,它的最左邊的孩子為原樹的根,因為我們要保證 Splay 的形態,先要保證它的中序遍歷和原樹一致。

方法:首先把 \(x\)\(access\) 到原樹的根,並把它 Splay 到輔助樹的根,這時原樹的根就是 \(x\) 左子樹中最左側的點。

在借用上面的 \(access(9)\)\(Spaly(9)\)

int find(int x)//找原樹的根 
{
	access(x);//x到根建一顆splay
	splay(x);//將 x 伸展到根節點
	while(lson(x)) push_down(x),x=lson(x);//因為原樹根節點肯定就是中序遍歷的第一個點,也就是最頂上的
	return x;// splay的最左邊的兒子,一直找左兒子就行了 
}

split

作用:提取出 \(x,y\) 間的路徑

我們再 \(makeroot(9)\) ,圖在前面,就不放了,我們 \(access(10)\)\(Splay(10)\)

void split(int x, int y) {
    makeroot(x);//首先把x置為根節點 
    access(y);//生成一顆 Splay
    splay(y);
    //y維護的就是x - y 路徑上的資訊 
}

作用:把 \(x\) 點和 \(y\) 點之間連一條邊
方法:把 \(x\) 點變成所在原樹的根,然後把 \(x\) 點的父親變成 \(y\) 就可以了。

比如說加一條連向 \(9\) 的邊。

void link(int x,int y)//連邊
{
	makeroot(x);//使p變成根節點
	f[x]=y;//x變成y的父親,也就是連了邊
}

cut

作用:把 \(x\) 點和 \(y\) 點之間的邊刪掉
方法:把 \(x\) 點變成所在原樹的根,然後把 \(y\)\(access\) 到根,Splay \(y\) 到輔助樹的根,然後斷開y與它左孩子間的邊。由於 \(x\) 是原樹的根,\(y\) 是樹中的一點,所以就 \(y\) 點通過 \(access\)\(x\) 點連到一個輔助樹中時,\(x\) 點一定是它們所在實鏈的鏈頂。而 \(y\) splay到輔助樹的根時,如果 \(x\),\(y\) 間有一條邊,則 \(x\)一定是 \(y\) 的左孩子。

比如說刪去 \(8\to 9\) 這條邊。

void cut(int x,int y)//刪邊
{
	makeroot(x);//x變成根節點
	access(y);//y通向 x 減了一個實鏈,也就是一顆 splay,因為 x,y之間有邊,所以這顆splay 裡面只有兩個點
	splay(y);//將 y 轉到頂部 
	if(lson(y)!=x ||rson(x)) return;//兩者之間本來就沒有邊
	f[x]=0;//刪去原來連邊的資訊 
	lson(y)=0;
	push_up(x);
}

isroot

作用:判斷是否是splay的根
方法:splay的根結點的父親並不認這個孩子。
注意:原樹的根的父親點是 \(0\)

bool isroot(int x)//判斷當前點是否是實鏈的根節點
{//當前點是根節點因為這它認父親,父親不認兒子 
	return lson(f[x])!=x && rson(f[x])!=x; 
}

下面的部分都是基礎操作,Splay 有個地方有點不一樣,可以看見。

pushup

void push_up(int p)
{
	tree[p].sum=tree[lson(p)].sum^tree[rson(p)].sum^a[p];
    or
    tree[p].sum=tree[lson(p)].sum+tree[rson(p)].sum+a[p];
}

pushdown

void ff(int p) 
{
	swap(lson(p),rson(p));
	tree[p].laz^=1;
}
void push_down(int p)
{
	if(!tree[p].laz) return;
	if(lson(p)) ff(lson(p));
	if(rson(p)) ff(rson(p));
	tree[p].laz=0;
}

rotate

void rotate(int x,int op)
{
	int y=f[x];
	if(!isroot(y))
		tree[f[y]].ch[rson(f[y])==y]=x;//原先父親節點與其父親節點的邊斷開,連上現在的這個點 
	f[x]=f[y];//兒子節點的爸爸換成爺爺 
	if(tree[x].ch[op])//兒子節點op兒子有的話,改變他的父親為父親 
		f[tree[x].ch[op]]=y;
	tree[y].ch[!op]=tree[x].ch[op];//父親的兒子變成兒子的兒子 
	f[y]=x;//父親的父親變成兒子 
	tree[x].ch[op]=y;//兒子的對應兒子變成父親
	push_up(y); 
	//注:註釋裡的父親,兒子,爺爺,都表示沒變化之前的稱謂 
}

spaly

這裡將一下和普通 Splay 的一點區別,就是我們先用棧將我們接下來要旋轉的點儲存下來,然後一起 pushdown 。這樣就不用邊旋轉邊 pushdown。

int sta[M],top;//為了將懶惰標記一氣兒下傳 
void splay(int x)
{
	sta[++top]=x;
	for(int i=x;!isroot(i);i=f[i]) sta[++top]=f[i];
	while(top) push_down(sta[top--]);//splay之前先將要旋轉的鏈上的懶惰標記全部下穿,免去了邊旋轉邊下傳的麻煩
	while(!isroot(x))//當前點不是根 
	{
		if(!isroot(f[x]))//父親也不是根 
		{	
			if((rson(f[x])==x)^(rson(f[f[x]])==f[x]))//不在一邊 
				rotate(x,lson(f[x])==x);//旋轉當前節點 
			else
				rotate(f[x],lson(f[f[x]])==f[x]);//鏈的情況,旋轉父親節點才能改變形態,旋轉父親節點 
		}
		rotate(x,lson(f[x])==x);
	}
	push_up(x);
}

習題

後面的沒做,做了有時間再補程式碼。

參考資料

oi_wiki

flashhu大佬的部落格

親學長的部落格

老師的課件

本欲起身離紅塵,奈何影子落人間。