【学习笔记】浅谈短小可爱的左偏树(可并堆)
文章目錄
- 左偏樹(shù)
- 左偏樹(shù)的合并(merge)操作
- 例題
- 羅馬游戲
- [Apio2012]dispatching
- [JLOI2015]城池攻占
- [Baltic2004]sequence
左偏樹(shù)
左偏樹(shù)是一個(gè)堆,而且是一個(gè)可并堆,所以一定有權(quán)值的限制
以小根堆為例,那么必須滿(mǎn)足節(jié)點(diǎn)權(quán)值小于左右兒子權(quán)值,即val[i]<val[lson[i]] val[i]<val[rson[i]]
為了維護(hù)左偏樹(shù)的時(shí)間復(fù)雜度,就要設(shè)計(jì)一些左偏樹(shù)獨(dú)有的工具,定義
- 外節(jié)點(diǎn):滿(mǎn)足左右兒子至少有一個(gè)沒(méi)有的點(diǎn)
- 距離dis[i]:點(diǎn)iii到離它最近的一個(gè)外節(jié)點(diǎn)的距離;特別的,空節(jié)點(diǎn)的距離為?1-1?1,外節(jié)點(diǎn)本身距離為000
左偏樹(shù),樹(shù)如其名,向左偏的樹(shù)
也就是說(shuō)左邊要“重一點(diǎn)”,但這個(gè)重不是常見(jiàn)的子樹(shù)大小或者高度,而是我們定義的dis
- 對(duì)于左偏樹(shù)的每個(gè)點(diǎn)iii,滿(mǎn)足dis[lson[i]]>=dis[rson[i]]
完全有可能,左兒子只有一個(gè)點(diǎn),右兒子是一條鏈:不管是左兒子的一個(gè)點(diǎn)還是右兒子一條鏈上的點(diǎn)都符合外節(jié)點(diǎn)定義,所以dis都是000
考慮由下向上更新距離數(shù)組,即dis[i]=min(dis[lson[i]],dis[rson[i]])+1
由于左偏樹(shù)的性質(zhì),右兒子距離一定是小的,所以左偏樹(shù)的更新直接就是從右兒子處更新
- dis[i]=dis[rson[i]]+1
左偏樹(shù)特殊距離的定義,就保證了左偏樹(shù)的時(shí)間復(fù)雜度
- 節(jié)點(diǎn)數(shù)為nnn的樹(shù),樹(shù)根的dis≤log?(n+1)?1dis\le \log(n+1)-1dis≤log(n+1)?1
一個(gè)點(diǎn)的距離大于xxx,意味著x?1x-1x?1層內(nèi)都是滿(mǎn)二叉樹(shù)
左偏樹(shù)的合并(merge)操作
左偏樹(shù)最重要的,或者說(shuō)特色,就是它的合并操作
有了以上的理論基礎(chǔ),結(jié)合fhq-treap的返回根合并操作,左偏樹(shù)的合并還是很好寫(xiě)的
struct node { int l, r, val, dis; } t[maxn]; int merge( int x, int y ) {if( ! x or ! y ) return x + y;if( t[x].val > t[y].val ) swap( x, y );t[x].r = merge( t[x].r, y );if( t[t[x].l].dis < t[t[x].r].dis ) swap( t[x].l, t[x].r );t[x].dis = t[t[x].r].dis + 1;return x; } int lson[maxn], rson[maxn], val[maxn], dis[maxn]; int merge( int x, int y ) {if( ! x or ! y ) return x + y;if( val[x] < val[y] ) swap( x, y );rson[x] = merge( rson[x], y );if( dis[lson[x]] < dis[rson[x]] ) swap( lson[x], rson[x] );dis[x] = dis[rson[x]] + 1;return x; }例題
羅馬游戲
luogu 2713
這就是比較板的題了,初始時(shí),把每個(gè)士兵當(dāng)成一個(gè)左偏樹(shù),合并就是兩個(gè)士兵的左偏樹(shù)合并
殺士兵,就是去掉左偏樹(shù)的根節(jié)點(diǎn),直接合并左右兒子即可
但是怎么找士兵所在的兵團(tuán)呢?——并查集!
#include <cstdio> #include <iostream> using namespace std; #define maxn 1000005 struct node { int l, r, val, dis; } t[maxn]; bool vis[maxn]; int f[maxn]; int n, m;int find( int x ) { return x == f[x] ? x : f[x] = find( f[x] ); }int merge( int x, int y ) {if( ! x or ! y ) return x + y;if( t[x].val > t[y].val ) swap( x, y );t[x].r = merge( t[x].r, y );if( t[t[x].l].dis < t[t[x].r].dis ) swap( t[x].l, t[x].r );t[x].dis = t[t[x].r].dis + 1;return x; }int main() {scanf( "%d", &n );t[0].dis = -1;for( int i = 1;i <= n;i ++ ) {scanf( "%d", &t[i].val );f[i] = i;}scanf( "%d", &m );char opt[5]; int i, j;while( m -- ) {scanf( "%s", opt );if( opt[0] == 'M' ) {scanf( "%d %d", &i, &j );if( vis[i] or vis[j] ) continue;i = find( i ), j = find( j );if( i ^ j ) f[j] = f[i] = merge( i, j );}else {scanf( "%d", &i );if( vis[i] ) printf( "0\n" );else {i = find( i );vis[i] = 1;printf( "%d\n", t[i].val );f[i] = f[t[i].l] = f[t[i].r] = merge( t[i].l, t[i].r );/*就算殺了根節(jié)點(diǎn)代表的這個(gè)士兵也要更新士兵的并查集集合因?yàn)榭赡苡行┦勘邕^(guò)了根節(jié)點(diǎn)的左右兒子直接并查集是接在根節(jié)點(diǎn)并查集的下面的find的時(shí)候是會(huì)跳到根節(jié)點(diǎn)的并查集點(diǎn)的*/}}}return 0; }[Apio2012]dispatching
BZOJ 2809
dfs樹(shù)從下往上合并
每次丟掉最大的CiC_iCi?即可
同樣別忘了并查集
#include <cstdio> #include <vector> #include <iostream> using namespace std; #define maxn 100005 #define int long long vector < int > G[maxn]; int lson[maxn], rson[maxn], dis[maxn], f[maxn], L[maxn], C[maxn]; int n, m, root; long long ans;int find( int x ) { return f[x] == x ? x : f[x] = find( f[x] ); }int merge( int x, int y ) {if( ! x or ! y ) return x + y;if( C[x] < C[y] ) swap( x, y );rson[x] = merge( rson[x], y );if( dis[lson[x]] < dis[rson[x]] ) swap( lson[x], rson[x] );dis[x] = dis[rson[x]] + 1;return x; }pair < int, int > dfs( int u ) {int cnt = 1, sum = C[u];for( auto v : G[u] ) {pair < int, int > son = dfs( v );cnt += son.first, sum += son.second;int x = find( u ), y = find( v );root = f[u] = f[v] = merge( x, y );while( sum > m ) {sum -= C[root], cnt --;f[root] = f[lson[root]] = f[rson[root]] = merge( lson[root], rson[root] );root = f[root];}ans = max( ans, L[u] * cnt );}ans = max( ans, L[u] * cnt );return make_pair( cnt, sum ); }signed main() {scanf( "%lld %lld", &n, &m );for( int i = 1, B;i <= n;i ++ ) {scanf( "%lld %lld %lld", &B, &C[i], &L[i] );G[B].push_back( i );f[i] = i;}dis[0] = -1;dfs( 1 );printf( "%lld\n", ans );return 0; }[JLOI2015]城池攻占
luogu 3261
初始時(shí),將每一座城市當(dāng)成一個(gè)左偏樹(shù),并把在該城市的士兵在左偏樹(shù)上完成合并
與上一題一樣的,從下往上合并左偏樹(shù),把所有兒子的騎士聚集到當(dāng)前根節(jié)點(diǎn)城市
把戰(zhàn)斗力不足的扔掉,所以需要小根堆
利用初末城市的深度差,巧妙計(jì)算出每個(gè)騎士能攻占的城市數(shù)量
至于城市對(duì)騎士的戰(zhàn)斗力影響,一樣的整體懶標(biāo)記,碰到了再下傳
#include <cstdio> #include <vector> #include <iostream> using namespace std; #define int long long #define maxn 300005 vector < int > G[maxn]; struct node { int lson, rson, dis, val, add, mul; }t[maxn]; int root[maxn], h[maxn], a[maxn], v[maxn], c[maxn]; int ans1[maxn], ans2[maxn], dep[maxn]; int n, m;void modify( int x, int mul, int add ) {if( ! x ) return;t[x].val *= mul;t[x].val += add;t[x].mul *= mul;t[x].add *= mul;t[x].add += add; }void pushdown( int x ) { if( ! x ) return;modify( t[x].lson, t[x].mul, t[x].add );modify( t[x].rson, t[x].mul, t[x].add );t[x].mul = 1;t[x].add = 0; }int merge( int x, int y ) {if( ! x or ! y ) return x + y;pushdown( x );pushdown( y );if( t[x].val > t[y].val ) swap( x, y );t[x].rson = merge( t[x].rson, y );if( t[t[x].lson].dis < t[t[x].rson].dis ) swap( t[x].lson, t[x].rson );t[x].dis = t[t[x].rson].dis + 1;return x; }void dfs( int u, int fa ) {dep[u] = dep[fa] + 1;for( auto v : G[u] ) {dfs( v, u );root[u] = merge( root[u], root[v] );//將所有從子樹(shù)的管轄城市中存活到u城市的騎士進(jìn)行合并 }while( root[u] and t[root[u]].val < h[u] ) {//從最小殺傷力騎士開(kāi)始 將所有在u城市死亡的騎士剔除 并記錄最后答案 pushdown( root[u] );ans1[u] ++;ans2[root[u]] = dep[c[root[u]]] - dep[u];//騎士能攻占的數(shù)量巧妙利用初末深度差 最開(kāi)始的城市深度-u的深度 root[u] = merge( t[root[u]].lson, t[root[u]].rson ); }if( a[u] ) modify( root[u], v[u], 0 );else modify( root[u], 1, v[u] ); }signed main() {scanf( "%lld %lld", &n, &m );for( int i = 1;i <= n;i ++ ) scanf( "%lld", &h[i] );for( int i = 2, f;i <= n;i ++ ) {scanf( "%lld %lld %lld", &f, &a[i], &v[i] );G[f].push_back( i );}t[0].dis = -1;//在每一座城市都建立一個(gè)左偏樹(shù) for( int i = 1;i <= m;i ++ ) {scanf( "%lld %lld", &t[i].val, &c[i] );root[c[i]] = merge( root[c[i]], i );//將在同一座城市的騎士在左偏樹(shù)上體現(xiàn)合并 //root[i]:在城市i的左偏樹(shù)的最小殺傷力騎士的編號(hào) }dfs( 1, 0 );//搜城 從下往上爬 while( root[1] ) {/*最后在總城市根1的騎士答案還未計(jì)算 暴力彈出所有存活的騎士 攻占的數(shù)量就是初始城市到1號(hào)超級(jí)城市一路經(jīng)過(guò)的城市也就是深度*/ ans2[root[1]] = dep[c[root[1]]];root[1] = merge( t[root[1]].lson, t[root[1]].rson );}for( int i = 1;i <= n;i ++ ) printf( "%lld\n", ans1[i] );for( int i = 1;i <= m;i ++ ) printf( "%lld\n", ans2[i] );return 0; }[Baltic2004]sequence
BZOJ 1367
給定nnn,以及長(zhǎng)度為nnn的整數(shù)序列a1,...,ana_1,...,a_na1?,...,an?,你需要構(gòu)造一個(gè)嚴(yán)格上升的長(zhǎng)度為nnn整數(shù)序列t1,...,tnt_1,...,t_nt1?,...,tn?,定義R=∣t1?a1∣+...+∣ti?ai∣+...+∣tn?an∣R=|t_1-a_1|+...+|t_i-a_i|+...+|t_n-a_n|R=∣t1??a1?∣+...+∣ti??ai?∣+...+∣tn??an?∣,求最小的RRR
n≤106n\le 10^6n≤106
假設(shè)是構(gòu)造不下降序列
考慮兩種特殊的序列
- a1≤a2≤...≤ana_1\le a_2\le ...\le a_na1?≤a2?≤...≤an?,直接讓ti=ait_i=a_iti?=ai?,最后答案就是000
- a1≥a2≥...≥ana_1\ge a_2\ge ...\ge a_na1?≥a2?≥...≥an?,直接讓t1=amidt_1=a_{mid}t1?=amid?(序列的中位數(shù))
現(xiàn)在考慮一般形式的aaa
將aaa序列劃分成這兩種特殊序列的若干段
考慮1...i1...i1...i的答案為w1,...wiw_1,...w_iw1?,...wi?,現(xiàn)在考慮i+1i+1i+1,先讓wi+1=ai+1w_{i+1}=a_{i+1}wi+1?=ai+1?
然后就要根據(jù)第二種特殊序列,進(jìn)行合并,如果wi≥wi+1w_i\ge w_{i+1}wi?≥wi+1?,就合并后wi=wi+1=w_i=w_{i+1}=wi?=wi+1?=中位數(shù),直到wj<wi+1w_j<w_{i+1}wj?<wi+1?
這個(gè)中位數(shù)是參與合并的www中的中位數(shù),不是整體的中位數(shù)
用左偏樹(shù)維護(hù)一個(gè)sizsizsiz大小即可
#include <cstdio> #include <iostream> using namespace std; #define maxn 1000005 struct node {int lson, rson, val, siz, dis; }t[maxn]; int n, cnt; int root[maxn], L[maxn], R[maxn], a[maxn];int merge( int x, int y ) {if( ! x or ! y ) return x + y;if( t[x].val < t[y].val ) swap( x, y );t[x].rson = merge( t[x].rson, y );t[x].siz = t[t[x].lson].siz + t[t[x].rson].siz + 1;if( t[t[x].lson].dis < t[t[x].rson].dis )swap( t[x].lson, t[x].rson );t[x].dis = t[t[x].rson].dis + 1;return x; }long long Fabs( long long x ) {return x < 0 ? -x : x; }int NewNode( int x ) {t[++ cnt].val = x;t[cnt].siz = 1;t[cnt].lson = t[cnt].rson = t[cnt].dis = 0;return cnt; }int main() {scanf( "%d", &n );for( int i = 1;i <= n;i ++ )scanf( "%d", &a[i] ), a[i] -= i;t[0].dis = -1;int ip = 0;for( int i = 1;i <= n;i ++ ) {++ ip;L[ip] = R[ip] = i;root[ip] = NewNode( a[i] );while( ip > 1 and t[root[ip - 1]].val > t[root[ip]].val ) {root[ip - 1] = merge( root[ip - 1], root[ip] );R[ip - 1] = R[ip];ip --;while( t[root[ip]].siz * 2 > R[ip] - L[ip] + 2 )root[ip] = merge( t[root[ip]].lson, t[root[ip]].rson );}}long long ans = 0;while( ip ) {int val = t[root[ip]].val;for( int i = L[ip];i <= R[ip];i ++ )ans += Fabs( 1ll * val - a[i] );ip --;}printf( "%lld\n", ans );return 0; }總結(jié)
以上是生活随笔為你收集整理的【学习笔记】浅谈短小可爱的左偏树(可并堆)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: mp3随身听十大品牌排行榜
- 下一篇: 加强版[BZOJ#3483] SGU50