c++ 左偏樹簡析 猴王例題講解
題目:猴王 Monkey King
題目描述
很久很久以前,在一個廣闊的森林裡,住著n只好斗的猴子。起初,它們各幹各的,互相之間也不瞭解。但是這並不能避免猴子們之間的爭吵,當然,這隻存在於兩個陌生猴子之間。當兩隻猴子爭論時,它們都會請自己最強壯的朋友來代表自己進行決鬥。顯然,決鬥之後,這兩隻猴子以及它們的朋友就互相瞭解了,這些猴子之間將再也不會發生爭論了,即使它們曾經發生過沖突。
假設每一隻猴子都有一個強壯值,每次決鬥後都會減少一半(比如10會變成5,5會變成2.5)。並且我們假設每隻猴子都很瞭解自己。就是說,當它屬於所有朋友中最強壯的一個時,它自己會站出來,走向決鬥場。
輸入
輸入分為兩部分。
第一部分,第一行有一個整數n(n<=100000),代表猴子總數。
接下來的n行,每行一個數表示每隻猴子的強壯值(小於等於32768)。
第二部分,第一行有一個整數m(m<=100000),表示有m次衝突會發生。
接下來的m行,每行包含兩個數x和y,代表第x個猴子和第y個猴子之間發生衝突。
輸出
輸出每次決鬥後在它們所有朋友中的最大強壯值。資料保證所有猴子決鬥前彼此不認識。
樣例輸入
5
20
16
10
10
4
4
2 3
3 4
3 5
1 5
樣例輸出
8
5
5
10
解析
首先,看到題目我們首先會想到,並查集和堆,因為我們每一次都要從一群猴子中找出最強壯的,暴力搜一遍顯然不行,因此要用到堆,然後兩隻猴子打架後需要將兩群猴子合併,因此要用到並查集,但是同時寫這兩種資料結構太麻煩,所以要藉助一種既具有查詢功能又具有合併功能的資料結構–左偏樹。
下面簡單講一講左偏樹,首先定義幾個概念,
1.外結點:左子樹或右子數為空的結點即無左子樹或右子樹。
2.結點i的距離:結點i到後代最近的外結點的所經過的邊數。
3.左偏性質:一棵左偏樹中任意結點的左子結點距離大於右子節點距離。
上圖
圖中外結點有18 20 19 24 15 30 28 11 21 16 42 50 33 26 27。
藍色數字為該結點的距離。
由第二,三條概念得一個結點的距離一定是它右子結點(如果有)的距離+1,因為左子結點距離大於等於右子結點距離,結點距離又要求最近。
左偏樹基本操作(大根為例):
1.合併操作(合併兩棵左偏樹):將兩棵樹中較大的根結點,作為合併後的樹的根結點,將根較小的樹與根較大的樹的右子結點繼續合併操作,直到無子結點。合併後如果不滿足左偏樹性質,即左子結點距離小於右子結點距離則維護,即交換指標。
2.插入(加入一個結點到樹中):易得單個結點也屬於左偏樹,因此可以將單個結點視為一棵左偏樹進行一次合併操作。
3.刪除操作(刪除根結點):合併左右兩棵子樹即可。
可以說實際上,左偏樹就只有一種操作,合併。
上圖(小頂樹,跟我不一樣):
虛擬碼:
Merge(a,b)//a與b都是大頂堆
{
If(a==null) return b;
If(b==null) return a;
If(key(a)<key(b)) swap(a,b);
a.rchild=Merge(a.rchild,b);
If(dis[a.rchild]>dis[a.lchild]) swap(a.lchild,a.rchild);
dis[a]=dis[a.rchild]+1;
Return a;
}
關於左偏樹的講解就到這兒結束,認真看過之後,猴王這道題目幾乎就成了左偏樹的裸題,只需要一個模板再加一點兒想象力。
請看程式碼講解(靜態)。
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<algorithm>
using namespace std;
int scan(){
char c=getchar();
int x=0;
while(c>'9'||c<'0')
c=getchar();
while(c>='0'&&c<='9')
x=x*10+c-'0',
c=getchar();
return x;
} //讀入優化
struct node{
int dis; //結點距離
int fa; //父親結點
int rchild,lchild; //左右子結點
int key; //強壯值
}; //一隻猴子的屬性結構體
int n,m;
node monkey[100010]; //100000只猴子
int find(int x){
if(monkey[x].fa!=x)
monkey[x].fa=find(monkey[x].fa);
return monkey[x].fa;
} //找到編號為x的猴子所屬群最強壯的猴子(編號),並壓縮路徑
//壓縮路徑可能寫錯了(歡迎指點)
int merge(int a,int b){ //a,b均為猴子編號
if(a==0) return b;
if(b==0) return a; //達到最底層
if(monkey[a].key<monkey[b].key) swap(a,b);//維護大頂樹的性質,強行讓猴子 a 強壯
monkey[a].rchild=merge(monkey[a].rchild,b);//遞迴合併操作
//似乎沒有更新父節點
if(monkey[monkey[a].rchild].dis>monkey[monkey[a].lchild].dis)
swap(monkey[a].rchild,monkey[a].lchild);//不滿足左偏樹性質,維護,交換指標
monkey[a].dis=monkey[monkey[a].rchild].dis+1; //更新距離
return a; //返回最強壯猴子編號
} //核心操作,合併兩群猴子
int main(){
int i,j,k,x,y;
n=scan();
for(i=1;i<=n;i++)
monkey[i].key=scan(),
monkey[i].fa=i; //猴子資料讀入
m=scan();
for(i=1;i<=m;i++){
x=scan();
y=scan(); //x,y號猴子要決鬥
j=find(x);k=find(y); //尋找各自最強壯的猴子朋友 j,k
monkey[j].key/=2.0;
monkey[k].key/=2.0; //戰鬥力減半
int fa1=merge(monkey[j].lchild,monkey[j].rchild);
int fa2=merge(monkey[k].lchild,monkey[k].rchild);
//刪除操作,先把決鬥的猴子各自從自己的猴群中刪除(最強壯的猴子是根結點),之後各自合併自己的左右子猴群,例如 fa1 是猴子 j 的猴群再次合併後的根結點, fa2 則是 k 的。(合併操作將返回最強壯的猴子編號)
monkey[j].lchild=monkey[j].rchild=monkey[k].lchild=monkey[k].rchild=0;//猴子 j 和 k 逐出猴群,子結點清空為0
int t=merge(fa1,fa2);
//合併參戰猴群,無 j 和 k 猴子
int p=merge(j,k);
//合併 j 和 k 猴子
int final_fa=merge(t,p);
//重新納入 j 和 k 猴子,final_fa為最終最強壯的猴子,即兩棵左偏樹在經過一番操作後形成的新左偏樹的根結點。
monkey[fa1].fa=monkey[fa2].fa=monkey[t].fa=final_fa;
monkey[j].fa=monkey[k].fa=monkey[p].fa=monkey[final_fa].fa=final_fa; //更新可能是猴王的猴子的父結點
printf("%d\n",monkey[final_fa].key);//輸出猴王的強壯值
//下一次戰鬥 迴圈
}
}
以上是全部內容,歡迎指點,結束--