左偏树
简介
定义
左偏树类似一个堆的结构,支持堆的所有普通操作,同时还支持合并,所以是一种可并堆。左偏树的所有操作时间复杂度均为\(logn\)级别。
性质
考虑两个堆的合并操作,我们可以递归处理。以小根堆为例。
int merge(int x,int y) {
if (!x) return y;
if (!y) return x;
if (t[x].val>t[y].val) swap(x,y);
t[x].r=merge(t[x].r,y);
t[t[x].r].father=x;
}
注意到这个方法的复杂度主要取决于链上没有左儿子或右儿子的最高点的深度。考虑优化算法。我们定义\(dis\)为当前节点往下多少个节点会得到一个节点,它的某个儿子是空的,可以连上去。所以我们只要保证\(dis\)的大小即可。
考虑\(dis=0\)的深度最浅的节点,由\(dis\)的定义可知,此节点上方是一个完全二叉树。因此若树的大小为\(n\),那么有\(dis\le log_2n\)。复杂度可得到保证。每次合并完后,我们总是令左边的\(dis\)大于等于右边的\(dis\)。
int merge(int x,int y) {
if (!x) return y;
if (!y) return x;
if (t[x].x<t[y].x) swap(x,y);
t[x].ch[1]=merge(t[x].ch[1],y);
if (t[t[x].ch[1]].dis>t[t[x].ch[0]].dis) swap(t[x].ch[0],t[x].ch[1]);
if (t[x].ch[1]) t[x].dis=t[t[x].ch[1]].dis+1; else t[x].dis=0;
return x;
}
例题
hdu1512 Monkey King
题意
开始有很多猴子,互相都不认识。每次会有两只猴子打架。若两只猴子认识则输出-1,否则两只猴子分别从自己认识的所有猴子中找出最强的两只打架,之后力量值减半,两群猴子互相认识。求每次打完架后两群猴子中力量值最大的。
做法
需要支持一个大根堆的合并和查询元素是否属于同一集合。左偏树模板题。其中\(pop\)仅需断掉当前堆顶与两个儿子的连边,并把两个儿子\(merge\)一下。由于\(merge\)操作复杂度为\(log_2n\),所以可以依赖其实现其他操作,复杂度为\(log_2n\)。有点像Link-Cut Tree中的access操作。
代码
#include<cstdio>
#include<cctype>
#include<algorithm>
#include<cstdlib>
using namespace std;
int read() {
int x=0,f=1;
char c=getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=-1; else if (c==-1) exit(0);
for (;isdigit(c);c=getchar()) {
if (c==-1) exit(0);
x=x*10+c-'0';
}
return x*f;
}
const int maxn=1e5+10;
struct node {
int ch[2],dis,x;
} t[maxn];
int f[maxn];
int find(int x) {
return f[x]==x?f[x]:f[x]=find(f[x]);
}
int merge(int x,int y) {
if (!x) return y;
if (!y) return x;
if (t[x].x<t[y].x) swap(x,y);
t[x].ch[1]=merge(t[x].ch[1],y);
f[t[x].ch[1]]=x;
if (t[t[x].ch[1]].dis>t[t[x].ch[0]].dis) swap(t[x].ch[0],t[x].ch[1]);
if (t[x].ch[1]) t[x].dis=t[t[x].ch[1]].dis+1; else t[x].dis=0;
return x;
}
int pop(int x) {
int l=t[x].ch[0],r=t[x].ch[1];
f[l]=l,f[r]=r;
t[x].ch[0]=t[x].ch[1]=t[x].dis=0;
return merge(l,r);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("test.in","r",stdin);
#endif
int n;
while (n=read()) {
for (int i=1;i<=n;++i) {
f[i]=i;
int x=read();
t[i].x=x;
t[i].ch[0]=t[i].ch[1]=t[i].dis=0;
}
int m=read();
while (m--) {
int x=read(),y=read();
int fx=find(x),fy=find(y);
if (fx==fy) puts("-1"); else {
t[fx].x>>=1;
t[fy].x>>=1;
int kx=merge(pop(fx),fx);
int ky=merge(pop(fy),fy);
printf("%d\n",t[merge(kx,ky)].x);
}
}
}
}
bzoj1975 魔法猪学院
题意
给出一个图,求一个数\(k\)使得图中由\(1\)到\(n\)的前\(k\)短路总和小于等于给定的\(energy\)。
做法
一开始没有理解题目,觉得很麻烦。我们需要做的就是按顺序求前\(k\)短路。这里可以采用A*算法。我们设一个估价函数:\(F(x)=g(x)+h(x)\).
其中\(F(x)\)表示当前路径的估价,\(g(x)\)为到达当前点需要的代价,\(h(x)\)为从当前点到终点的代价。这里的\(h(x)\)其实可以直接算出来,通过从终点一次bfs。
需要用一个小根堆,键值为其估价,每次取出堆顶进行更新。这样可以保证每次走到终点都是找到一条不同的路径,且在剩余路径中最短,即第\(k\)短路。
这个题其实跟左偏树没什么关系,练练手而已。结果卡了一个下午,RE了半版,最后是把堆数组开到\(1.8*10^6\)过掉了。代码写的比较乱。
代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=5e3+10;
const int maxm=2e5+100;
const int maxs=1.8e6+10;
const double inf=1e100+10;
int q[maxn<<2],ql,qr,ans=0,n,m;
bool inq[maxn];
double d[maxn],energy;
int tt=0;
struct node {
double exp,to;
int u;
bool operator > (const node a) const {
return exp>a.exp;
}
node () {}
node (int u,double exp,double to):u(u),exp(exp),to(to) {}
};
struct point {
int ch[2],dis;
node p;
point () {}
point (node p):p(p) {ch[0]=ch[1]=dis=0;}
} t[maxs];
int root=0,cnt=0;
int merge(int &x,int &y) {
if (!x) return y;
if (!y) return x;
if (t[x].p>t[y].p) swap(x,y);
t[x].ch[1]=merge(t[x].ch[1],y);
if (t[t[x].ch[0]].dis<t[t[x].ch[1]].dis) swap(t[x].ch[0],t[x].ch[1]);
if (t[x].ch[1]) t[x].dis=t[t[x].ch[1]].dis+1; else t[x].dis=0;
return x;
}
void push(node x) {
++cnt;
t[++tt]=point(x);
int tmp=tt;
if (!root) root=tmp; else
merge(root,tmp);
}
node top() {
return t[root].p;
}
void pop() {
--cnt;
int l=t[root].ch[0],r=t[root].ch[1];
t[root].ch[0]=t[root].ch[1]=0;
root=merge(l,r);
}
struct graph {
graph ():tot(0) {}
struct edge {
int v,next;
double w;
} e[maxm];
int h[maxn],tot;
void add(int u,int v,double w) {
e[++tot].v=v;e[tot].next=h[u];e[tot].w=w;
h[u]=tot;
}
void spfa(int s) {
for (int i=1;i<=n;++i) d[i]=inf;
memset(inq,false,sizeof inq);
q[ql=qr=1]=s;
d[s]=0;
inq[s]=true;
while (ql<=qr) {
int u=q[ql++];
for (int i=h[u];i;i=e[i].next) {
int v=e[i].v;
double w=e[i].w;
if (d[v]>d[u]+w) {
d[v]=d[u]+w;
if (!inq[v]) q[++qr]=v,inq[v]=true;
}
}
inq[u]=false;
}
}
void Astar() {
node a(1,d[1],0);
push(a);
while (cnt) {
//print();
node x=top();
pop();
int u=x.u;
double to=x.to;
for (int i=h[u];i;i=e[i].next) {
int v=e[i].v;
double w=e[i].w;
double f=to+w+d[v];
node a(v,f,to+w);
push(a);
}
if (u==n) {
energy-=to;
if (energy<0) return; else ++ans;
}
}
}
} a,b;
int main() {
#ifndef ONLINE_JUDGE
freopen("test.in","r",stdin);
freopen("mag.out","w",stdout);
#endif
double w;
scanf("%d%d%lf",&n,&m,&energy);
for (int i=1;i<=m;++i) {
int u,v;
scanf("%d%d%lf",&u,&v,&w);
a.add(v,u,w);
b.add(u,v,w);
}
a.spfa(n);
b.Astar();
printf("%d\n",ans);
}
bzoj1455 罗马游戏
题目
罗马皇帝很喜欢玩杀人游戏。 他的军队里面有n个人,每个人都是一个独立的团。最近举行了一次平面几何测试,每个人都得到了一个分数。 皇帝很喜欢平面几何,他对那些得分很低的人嗤之以鼻。他决定玩这样一个游戏。 它可以发两种命令: 1. Merger(i, j)。把i所在的团和j所在的团合并成一个团。如果i, j有一个人是死人,那么就忽略该命令。 2. Kill(i)。把i所在的团里面得分最低的人杀死。如果i这个人已经死了,这条命令就忽略。 皇帝希望他每发布一条kill命令,下面的将军就把被杀的人的分数报上来。(如果这条命令被忽略,那么就报0分)
做法
同模板题。这里改成了hzwer的写法,比我自己之前的写法快一倍。估计是struct的常数问题。
代码
#include<cstdio>
#include<cctype>
#include<algorithm>
using namespace std;
int read() {
int x=0,f=1;
char c=getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=-1;
for (;isdigit(c);c=getchar()) x=x*10+c-'0';
return x*f;
}
const int maxn=1e6+10;
int d[maxn],v[maxn],f[maxn],l[maxn],r[maxn];
bool dead[maxn];
int find(int x) {
return f[x]==x?f[x]:f[x]=find(f[x]);
}
int merge(int x,int y) {
if (!x) return y;
if (!y) return x;
if (v[x]>v[y]) swap(x,y);
r[x]=merge(r[x],y);
if (d[l[x]]<d[r[x]]) swap(l[x],r[x]);
d[x]=d[r[x]]+1;
return x;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("test.in","r",stdin);
#endif
d[0]=-1;
int n=read();
for (int i=1;i<=n;++i) v[i]=read(),f[i]=i;
int m=read();
while (m--) {
static char o[3];
scanf("%s",o);
if (o[0]=='M') {
int x=read(),y=read();
if (dead[x] || dead[y]) continue;
int fx=find(x),fy=find(y);
if (fx!=fy) {
int k=merge(fx,fy);
f[fx]=f[fy]=k;
}
} else if (o[0]=='K') {
int x=read();
if (dead[x]) puts("0"); else {
int fx=find(x);
printf("%d\n",v[fx]);
dead[fx]=true;
f[fx]=merge(l[fx],r[fx]);
f[f[fx]]=f[fx];
}
}
}
}