1. 程式人生 > >c++ 左偏樹簡析 猴王例題講解

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);//輸出猴王的強壯值

        //下一次戰鬥 迴圈
    }
}
  以上是全部內容,歡迎指點,結束--