1. 程式人生 > >主席樹入門詳解一(學習筆記)(例題POJ-2104 求區間第k小)

主席樹入門詳解一(學習筆記)(例題POJ-2104 求區間第k小)

學習主席樹,在網上搜了很多教程(都好簡短啊,直接就是幾行字就上程式碼,看不懂啊有木有~~),最後才很艱難的學會了最基礎的部分。下面就是我在學習的過程中的產生的疑惑和解決的辦法。

學習主席樹需要的前置技能:線段樹。

參考資料

1. B站上的視訊講解(話說B站真的啥都有啊)

2.參考部落格

主席樹是啥??

話說主席樹這個名字的來歷還是挺好玩的 ,發明它的人叫黃嘉泰,名字的首字母呢就是(HJT),和我們的某位主席的名字簡寫是一樣的,所以就有了這個名字。

言歸正傳,那麼,主席樹到底是啥,主席樹就是可持久化的線段樹(也就是可以查詢歷史版本的線段樹),也叫函式式線段樹。具體點說就是你對某個線段樹進行更新,然後在後面的過程中,你還可以找到這個更新之前的版本的線段樹。說通俗點就是每一次更新,都會把舊的線段樹存起來,這樣以後用的時候就可以直接找啦。顯然直接建立一堆線段樹是顯然會爆記憶體的。但是呢我們發現,如果更新了一個點,那麼只有一條線上的節點會被更新

,也就是說,我們記錄下一個版本的線段樹的時候,可以共用前一個版本線段樹的大部分節點,這樣就可以節省記憶體啦。(巧妙啊)

如下圖,修改紅圈內的點,只會影響這一條線上的點。

對於一般的線段樹來說,如果父節點的編號是 i ,那麼他的兩個子節點的編號分別為 2 * i(左),  2 * i + 1(右),但是主席樹在這一點則有別於一般的線段樹,每一個父節點,他的兩個兒子節點的編號不一定滿足這個關係(因為我們上面所說的節點共用0.0)。

附上一個大家都用了的牛人的理解

所謂主席樹呢,就是對原來的數列[1..n]的每一個字首[1..i](1≤i≤n)建立一棵線段樹,線段樹的每一個節點存某個字首[1..i]中屬於區間[L..R]的數一共有多少個(比如根節點是[1..n],一共i個數,sum[root] = i;根節點的左兒子是[1..(L+R)/2],若不大於(L+R)/2的數有x個,那麼sum[root.left] = x)。若要查詢[i..j]中第k大數時,設某結點x,那麼x.sum[j] - x.sum[i - 1]就是[i..j]中在結點x內的數字總數。而對每一個字首都建一棵樹,會MLE,觀察到每個[1..i]和[1..i-1]只有一條路是不一樣的,那麼其他的結點只要用回前一棵樹的結點即可,時空複雜度為O(nlogn)。

主席樹可以幹啥??以及怎麼建立??(以例題闡述)

上面講過主席樹是一堆線段樹的集合,那麼只要是需要用很多線段樹來解決的問題,我們都可以用主席樹來解決。這樣說是不是太籠統了?舉個例子,我們可以用它來求區間的第k小(大)的值,也可以求區間內有多少種數字。主席樹的題是可以非常靈活的,難點就在於靈活的建樹,和如何建立利用線段樹。

最經典的例題當然是求區間第k小的題了,題目連結:POJ-2104

題目大意:有n個數,m個詢問。先給你n個數字,然後每個詢問會告訴你一組(l,r,k),意思是詢問區間【l,r】之間第k小的值。

看到這兒,我們可以先去想如果只有一個區間的話,這個問題可以怎麼解決。這個時候我們的線段樹

就可以閃亮登場啦!(如果有負數的話,可以整體加上最大的負數的絕對值,轉化成正數,最後不要忘了轉化回來就行)區間【l,r】所維護的資訊就是此區間所包含的數的個數(即大於等於 l  小於等於 r 的數的個數)。(葉子節點【l,l】記錄的資訊就是數字l的出現次數)。按照這個規則,一顆線段樹就建立好了。那麼我們應該怎麼去查詢呢。查詢的時候,如果左子樹所代表的區間包含數的個數大於等於k,就說明所要查詢的第k小的數在左子樹中,否則就在右子樹中,最終到達葉子節點的時候,輸出葉子節點即可。

查詢的程式碼如下(很簡單):

//l,r代表維護的區間,num代表當前區間所包含的數字的個數
int Query(int k,int cnt)
{
	if(tree[cnt].l==tree[cnt].r)
		return tree[cnt].l;
	if(tree[cnt<<1].num>=k)
		return Query(k,cnt<<1);
	else
		return Query(k-tree[cnt<<1].num,cnt<<1|1);
}

哈,那這樣的話,我們這一題的解法是不是就出來了呢?對每個區間建立一個線段樹,然後按照上面的方式求解。但這樣顯然是不行的,我們承受不了這麼大的時空複雜度。這個時候我們的主席樹就上場了,“線段樹,你退下吧,一切有我!”。(很多線段樹?想到了什麼,主席樹就是很多線段樹的集合啊。)

主席樹的各個節點都是同一結構的線段樹(相同的區間,相同的資訊)(因為是儲存了之前的歷史資訊0.0)。線段樹對一條線段,儲存的是這個數字區間的出現次數,所以是可以互相加減的。如果我們的主席樹是每次插入一個點來更新的,那麼第一個線段樹也就是第一個陣列成的線段樹,第 i 顆線段樹,就是前 i 個數組成的線段樹(按照上面講的線段樹的建樹方式,區間資訊是包含的數的個數)。那麼類比字首和的思想,我們怎麼表示區間【l,r】之間有多少個數呢?只要拿出 Tj 和 Ti-1,對每個節點相減就可以了。說的通俗一點,詢問 i~j 區間中,一個數字區間(a~b)的出現次數時,就是這些數字在 Tj 中出現的次數減去在 Ti-1 中出現的次數。(注意區分割槽間i~j  和  a~b哦)。

有的同學會說了,這樣也不行啊,你忽略了一個很重要的問題,就是數的範圍!題幹中給的數的範圍是 -1e9~1e9,線段樹怎麼可能開得下啊!(因為區間的大小是因為數的大小確定的)。對,這是一個很重要的問題,但是我們發現,數的個數是1e5個,是在我們能接受的範圍之內的。也就是說我們單單按照數字大小來建樹的話,會浪費掉非常多的空間。那該怎麼辦呢??為了解決這個問題,我們要引入一個高大上的方法,叫離散化(為啥叫這個名字我也不知道)。

離散化是啥?怎麼離散化?下面我就說一點自己的理解。我的理解是離散化是一種對映關係(Hash)。就拿這個題來說,我們可以把這n個數排序,然後把最小的對映成1,次小的對映成2......(注意去重)以此類推形成一種對映關係,然後我們就可以按照這種對映關係來建樹,這樣時空複雜度就在我們可以承受的範圍內了~。

在B站上學到的一種離散化的方法如下:(個人覺得挺好的)

首先我們讀入資料的時候順便把資料壓入到一個vector中

for(int i=1;i<=n;i++)
{
    scanf("%d",&a[i]);
    v.push_back(a[i]);
}

然後把vector排序去重(利用unique函式)

sort(v.begin(),v.end());
v.erase(unique(v.begin(),v.end()),v.end());

然後利用二分我們就可以愉快的得到對映的值辣

int getid(int x)
{
    return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}

這樣我們的思路就講完了,下面是具體實現。

一.單點更新

上程式碼,解釋見註釋!

void Update(int l,int r,int &x,int y,int pos)
//l,r代表當前區間  x代表當前更新的空樹  y代表x這個數所需要共用節點的
//上一版本的樹  pos代表當前更新的數 
{
    T[++cnt]=T[y],T[cnt].sum++,x=cnt;
    //建立樹的節點 
    if(l==r)
        return;
    int mid=(l+r)>>1;
    //判斷當前的數大小,來選擇更新左子樹還是右子樹 
    if(mid>=pos)
        Updata(l,mid,T[x].l,T[y].l,pos);
    else
        Updata(mid+1,r,T[x].r,T[y].r,pos);
}

二.建樹

建樹的時候不斷的進行單點更新即可

for(int i=1;i<=n;i++)
    Updata(1,n,root[i],root[i-1],getid(a[i]));
    //每一棵樹依賴的都是他的前一棵樹(也就是他的歷史版本)
	//root[i]存的是第i顆樹的根節點的座標 

三.查詢

上程式碼,解釋見註釋!

int Query(int l,int r,int x,int y,int k)
//l,r代表操作區間 x代表第l顆樹 y代表第r顆樹  k代表所求的第k小的數中的k 
{
    if(l==r)
        return l;
    //到達葉子節點  返回答案即可 
    int mid=(l+r)>>1;
    int sum=T[T[y].l].sum-T[T[x].l].sum;
    //判斷所求的數在左子樹還是右子樹 
    if(sum>=k)
        return Query(l,mid,T[x].l,T[y].l,k);
    else
        return Query(mid+1,r,T[x].r,T[y].r,k-sum);
        //注意理解從k到k-sum的變化 
}

四.完整程式碼

經過上面三步,大家應該已經能夠實現這個程式碼了


#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>

using namespace std;
const int MAXN=1e5+10;
struct Tree{
    int l,r,sum;
}T[MAXN*40];
vector<int> v;
int cnt,root[MAXN],a[MAXN];

void Init()
{
    cnt=0;
    T[cnt].l=0;T[cnt].r=0;T[cnt].sum=0;
    root[cnt]=0;
    v.clear();
}

int getid(int x)
{
    return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}

void Update(int l,int r,int &x,int y,int pos)
{
    T[++cnt]=T[y],T[cnt].sum++,x=cnt;
    if(l==r)
        return;
    int mid=(l+r)>>1;
    if(mid>=pos)
        Updata(l,mid,T[x].l,T[y].l,pos);
    else
        Updata(mid+1,r,T[x].r,T[y].r,pos);
}

int Query(int l,int r,int x,int y,int k)
{
    if(l==r)
        return l;
    int mid=(l+r)>>1;
    int sum=T[T[y].l].sum-T[T[x].l].sum;
    if(sum>=k)
        return Query(l,mid,T[x].l,T[y].l,k);
    else
        return Query(mid+1,r,T[x].r,T[y].r,k-sum);
}

int main()
{
    Init();
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        v.push_back(a[i]);
    }
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());
    for(int i=1;i<=n;i++)
        Updata(1,n,root[i],root[i-1],getid(a[i]));
    int l,r,k;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&l,&r,&k);
        printf("%d\n",v[Query(1,n,root[l-1],root[r],k)-1]);
    }
    return 0;
}

主席樹入門詳解二連結(講解基本用法 區間數字種數)

總結

主席樹的本質就是一堆線段樹的集合(也就是包含歷史版本的線段樹),所以需要一堆線段樹來解決的問題,就可以用我們的主席樹來解決了,主席樹與線段樹最大的區別就是主席樹的左右兒子的節點編號是不固定的。那麼我們在編寫程式碼的時候,傳入根節點的座標,然後再記錄左右兒子的座標,這樣我們的查詢,更新函式,都和普通的線段樹差不了多少,關鍵就是節點的公用關係,和線段樹在題目中的意義和用法

END~~

本篇學習筆記到此正式結束,以後學到了新的東西會繼續更新的(主席樹還有好多東西要學啊  啊啊~~~~)

相關推薦

主席入門學習筆記例題POJ-2104 區間k

學習主席樹,在網上搜了很多教程(都好簡短啊,直接就是幾行字就上程式碼,看不懂啊有木有~~),最後才很艱難的學會了最基礎的部分。下面就是我在學習的過程中的產生的疑惑和解決的辦法。 學習主席樹需要的前置技能:線段樹。 參考資料 1. B站上的視訊講解(話說B站真的啥都有啊)

主席入門學習筆記例題SPOJ

主席樹入門詳解一連結 Start~ 看了前一篇部落格,應該已經對最基礎的主席樹有了一個大概的掌握。主席樹的本質就是一堆線段樹的集合(也就是包含歷史版本的線段樹),所以需要用一堆線段樹來解決的問題,就可以用主席樹來解決。主席樹與線段樹最大的區別就是主席樹的左右兒子的節點

主席入門+題目推薦

主席樹學名可持久化線段樹,就是這個可持久化,衍生了多少資料結構 為什麼會有主席樹這個資料結構呢?它被髮明是用來解決什麼問題的呢? 給定n個數,m個操作,操作型別有在某個歷史版本下單點修改,輸出某個歷史版本下某個位置的值的值,n和m小於等於1e6 乍一看是不是一點頭緒也沒有。我們先來想想暴力怎麼

poj2104區間k,靜態主席入門模板

看了很久的主席樹,最後看https://blog.csdn.net/williamsun0122/article/details/77871278這篇終於看懂了 #include <stdio.h> #include<algorithm> using namespace s

落谷 P3834 可持久化線段 1主席區間k

設區間為l,r,用r版本減去l版本求出區間第k小,一個板子 #include<cstdio> #include<cmath> #include<cstring> #include<algorithm> using

區間k主席

區間第k小 題目描述 如題,給定NNN個正整數構成的序列,將對於指定的閉區間查詢其區間內的第KKK小值。 輸入格式 第一行包含兩個正整數NNN、MMM,分別表示序列的長度和查詢的個數。 第二行包含NN

POJ2104主席區間K

模板題,模板來自B站UESTCACM #include <iostream> #include <algorithm> #include <queue> #in

poj 2104主席區間k

區間 ++ cto ast http lan air algorithm while POJ - 2104 題意:求區間第k小 思路:無修改主席樹 AC代碼: #include "iostream" #include "iomanip" #include "string.

線段 入門

ear 接下來 數組 編譯器 一位 離散化 都是 並且 建立 概念(copy度娘): 線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。 使用線段樹可以快速的查找某一個節點在若幹條線段中出現的次數,時間復雜度為O

hdu 5919--Sequence II主席--區間不同數個數+區間k

positions minus -s ima date rst itl 主席樹 技術 題目鏈接 Problem Description Mr. Frog has an integer sequence of length n, which can be denot

HDU 5919 - Sequence II (2016CCPC長春) 主席 區間K+區間不同值個數

HDU 5919 題意:   動態處理一個序列的區間問題,對於一個給定序列,每次輸入區間的左端點和右端點,輸出這個區間中:每個數字第一次出現的位子留下, 輸出這些位子中最中間的那個,就是(len+1)/2那個。 思路:   主席樹操作,這裡的思路是從n到1開始建樹。其他就是主席樹查詢區間第K小,計算區

靜態區間K整體二分、主席

題目連結 題解 主席樹入門題 但是這裡給出整體二分解法 整體二分顧名思義是把所有操作放在一起二分 想想,如果求\([1-n]\)的第\(k\)小怎麼二分求得? 我們可以二分答案\(k\), \(O(n)\)統計有多少個數小於等於\(k\) 如果對於每個詢問都這麼搞,肯定不行 我們可以發現,如果

[機器學習入門] 李巨集毅機器學習筆記-1Learning Map 課程導覽圖

在此就不介紹機器學習的概念了。 Learning Map(學習導圖) PDF VIDEO 先來看一張李大大的總圖↓ 鑑於看起來不是很直觀,我“照虎

[機器學習入門] 李巨集毅機器學習筆記-5Classification- Probabilistic Generative Model;分類:概率生成模型

[機器學習] 李巨集毅機器學習筆記-5(Classification: Probabilistic Generative Model;分類:概率生成模型) Classification

[機器學習入門] 李巨集毅機器學習筆記-15 Unsupervised Learning: Word Embedding;無監督學習:詞嵌入

[機器學習入門] 李巨集毅機器學習筆記-15 (Unsupervised Learning: Word Embedding;無監督學習:詞嵌入) PDF VIDEO

[機器學習入門] 李巨集毅機器學習筆記-6 Classification: Logistic Regression;邏輯迴歸

[機器學習] 李巨集毅機器學習筆記-6 (Classification: Logistic Regression;Logistic迴歸) PDF VIDEO Three steps Step 1: Function Set

hdu2665 區間k主席or可持久化線段or函式式線段

題目大意:感覺題目表述得不明不白的,給一堆不知道我也不知道什麼資料範圍的數,然後給你M個區間,輸出每個區間的第k大的數(這裡出現嚴重的問題!!!) 題目說得kth bigger 難道不是第k大?結果我WA了一堆之後,翻了幾篇別人的部落格程式碼,結果發現別人

[機器學習入門] 李巨集毅機器學習筆記-14 Unsupervised Learning: Linear Dimension Reduction;無監督學習:線性降維

[機器學習入門] 李巨集毅機器學習筆記-14 (Unsupervised Learning: Linear Dimension Reduction;線性降維) PDF VI

動態區間k主席+線段狀陣列

靜態區間第k小問題,是給你一個序列,每次詢問序列中的一個區間中的第k小數,這個問題用普通的主席樹就可以解決。動態區間第k小問題就是在靜態的基礎上加上了修改操作,也就是每次除了詢問區間第k小之外,還可以修改序列中的某個數。因為這裡涉及到了修改操作,我們用只用主席樹

HDU 2665 Kth number主席靜態區間K題解

可持久化 unique algorithm using 主席樹 可持久化線段樹 long spa 靜態區 題意:問你區間第k大是誰 思路:主席樹就是可持久化線段樹,他是由多個歷史版本的權值線段樹(不是普通線段樹)組成的。 具體可以看q學姐的B站視頻 代碼: