左偏树可并堆
我不是故意学它的 OJ上一道主席树例题然后我一直想不出来,终于读懂题了,发现这个根本不是一个主席树,搜了一下叫做可并堆。
瞬间mmp 学了一下可并堆还是比较实用的东西比STL优先队列要快好多好多。
转载吧 不想写概念,转一个我素未谋面却感觉很熟悉的人 他的名字好像叫做花。
左偏树,一种可以合并的堆状结构,支持insert/remove/merge等操作。稳定的时间复杂度在Θ(logn)的级别。
对于一个左偏树中的节点,需要维护的值有dist和value。一般维护的东西当然也可以有懒标记
其中value不必多说,dist记录这个节点到它子树里面最近的空节点的距离,空节点距离为0。
定义空节点是左偏树中的一个节点,如果它的左子树或右子树为空,则称它是一个空结点。
性质:
- 一个节点的value必定(或小于)左、右儿子的value (堆性质)
- 一个节点的左儿子的dist不小于右儿子的dist (左偏性质)
- 一个节点的距离始终等于右儿子+1
推论:任何时候,节点数为n的左偏树,距离最大为log(n+1)−1
Proof:
对于一棵距离为定值k的树,点数最少时,一定是一棵满二叉树。这是显然的。
因为对于每个节点,如果想要有最少的儿子,那么起码要做到左儿子的数量等于右儿子的数量。
那么对于他的逆命题也是成立的——“若一棵左偏树的距离为k,则这棵左偏树至少有2k+1−1个节点.
n≥2k+1−1 log2(n+1)≥k+1 log2(n+1)−1≥k 证毕。
删除操作 插入操作 合并操作 其实都是同一个函数 即 合并函数->merge
inline int merge(int a,int b) { if(!a||!b)return a+b; if(sum[a]>sum[b]||(sum[a]==sum[b]&&a>b))swap(a,b); rs[a]=merge(rs[a],b); if(dis[rs[a]]>dis[ls[a]])swap(ls[a],rs[a]); dis[a]=dis[rs[a]]+1; return a; }
我曾经有疑问是指为什么两颗左偏树合并时优先合并右边节点,是这样的因为左边节点默认已满如果直接先合并左边可能成了一棵又偏树了,所以是先合并右边,然后发现右边比左边还满的时候交换即可。
这道题傻逼一个典型的 左偏树的应用 加上一个并查集即可解决这个问题 分析一波复杂度吧 可能合并n-1次 每次合并都是loga+logb
所以总复杂度是nlogn 比优先队列快不知道哪里去了,优先队列做这道题是一个 搞一发启发式合并每个点最多合并logn次每次都是logn的复杂度
总复杂度是一个nlog^2n的 非常难受。。。
#include<iostream> #include<queue> #include<iomanip> #include<cctype> #include<cstdio> #include<deque> #include<utility> #include<cmath> #include<ctime> #include<cstring> #include<string> #include<cstdlib> #include<vector> #include<algorithm> #include<stack> #include<map> #include<set> #include<bitset> #define max(x,y) ((x)>(y)?(x):(y)) #define min(x,y) ((x)>(y)?(y):(x)) #define INF 10000000000000ll #define ll long long using namespace std; char buf[1<<15],*fs,*ft; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline int read() { int x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(int x) { x<0?putchar('-'),x=-x:0; int num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar('\n');return; } const int MAXN=100010; int n,m; int ls[MAXN],rs[MAXN],vis[MAXN],root[MAXN]; int sum[MAXN],f[MAXN],dis[MAXN]; inline int getfather(int x){return f[x]==x?x:f[x]=getfather(f[x]);} inline int merge(int a,int b) { if(!a||!b)return a+b; if(sum[a]>sum[b]||(sum[a]==sum[b]&&a>b))swap(a,b); rs[a]=merge(rs[a],b); if(dis[rs[a]]>dis[ls[a]])swap(ls[a],rs[a]); dis[a]=dis[rs[a]]+1; return a; } int main() { //freopen("1.in","r",stdin); //freopen("1.out","w",stdout); n=read();m=read(); for(int i=1;i<=n;++i)f[i]=i,root[i]=i,sum[i]=read(); for(int i=1;i<=m;++i) { int p,x,y; p=read(); if(p==1) { x=read();y=read(); if(vis[x]||vis[y])continue; int xx=getfather(x); int yy=getfather(y); if(xx==yy)continue; root[xx]=merge(root[xx],root[yy]); f[yy]=xx; } else { x=read(); if(vis[x]){puts("-1");continue;} int xx=getfather(x); put(sum[root[xx]]); vis[root[xx]]=1; root[xx]=merge(ls[root[xx]],rs[root[xx]]); } } return 0; }
打错了字母调错了好久尴尬。。
卡我这么久的题目竟是道水题我算是服了,这道题其实就是求出以某个点为根的子树中薪水能雇佣的最多的忍者*当前点的领导力。
直接以master进行dfs 可以然后沿途加上一些点然后一旦超过了薪水就把最大的踢掉,用左偏树复杂度nlogn
#include<iostream> #include<queue> #include<iomanip> #include<cctype> #include<cstdio> #include<deque> #include<utility> #include<cmath> #include<ctime> #include<cstring> #include<string> #include<cstdlib> #include<vector> #include<algorithm> #include<stack> #include<map> #include<set> #include<bitset> #define max(x,y) ((x)>(y)?(x):(y)) #define min(x,y) ((x)>(y)?(y):(x)) #define INF 10000000000000ll #define ll long long #define l(x) t[x].l #define r(x) t[x].r #define sum(x) t[x].sum #define v(x) t[x].v using namespace std; char buf[1<<15],*fs,*ft; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline int read() { int x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?putchar('-'),x=-x:0; int num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar('\n');return; } const int MAXN=100010; int n,m,len; int lin[MAXN],nex[MAXN],ver[MAXN]; int ls[MAXN],rs[MAXN],dis[MAXN]; ll sum[MAXN],ans,v[MAXN],w[MAXN],sz[MAXN]; inline void add(int x,int y) { ver[++len]=y; nex[len]=lin[x]; lin[x]=len; } inline int merge(int a,int b) { if(!a||!b)return a+b; if(w[a]<w[b])swap(a,b); rs[a]=merge(rs[a],b); if(dis[ls[a]]<dis[rs[a]])swap(ls[a],rs[a]); dis[a]=dis[rs[a]]+1; return a; } inline int dfs(int x) { int a,b; a=x;sum[x]=w[x];sz[x]=1; for(int i=lin[x];i;i=nex[i]) { int tn=ver[i]; b=dfs(tn); a=merge(a,b); sum[x]+=sum[tn];sz[x]+=sz[tn]; } while(sum[x]>m) { sum[x]-=w[a];--sz[x]; a=merge(ls[a],rs[a]); } ans=max(ans,v[x]*sz[x]); return a; } int main() { //freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;++i) { int x; x=read(); if(x)add(x,i); w[i]=read();v[i]=read(); } dfs(1); put(ans); return 0; }
这道题算是一道感觉比较难的题目了其实也比较难写,首先我们直接每个骑士暴力运算,然后发现是一棵树 可以拓扑排序,然后拓扑一下发现可以直接O(m)我们还要维护一下当前到这个城池的骑士之中攻击力最低的然后不断pop 关键是如何搞提升攻击力的东西把我卡住了,感觉怎么都不能写,翻了一下题解发现了懒标记这个东西实在是太妙了,直接懒标记标记第一个骑士该下传的时候下传即可。好题。
#include<iostream> #include<queue> #include<iomanip> #include<cctype> #include<cstdio> #include<deque> #include<utility> #include<cmath> #include<ctime> #include<cstring> #include<string> #include<cstdlib> #include<vector> #include<algorithm> #include<stack> #include<map> #include<set> #include<bitset> #define max(x,y) ((x)>(y)?(x):(y)) #define min(x,y) ((x)>(y)?(y):(x)) #define INF 10000000000000ll #define ll long long using namespace std; char buf[1<<15],*fs,*ft; inline char getc() { return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++; } inline ll read() { ll x=0,f=1;char ch=getc(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();} return x*f; } inline void put(ll x) { x<0?putchar('-'),x=-x:0; ll num=0;char ch[50]; while(x)ch[++num]=x%10+'0',x/=10; num==0?putchar('0'):0; while(num)putchar(ch[num--]); putchar('\n');return; } //建立n个左偏树 const ll MAXN=300010; ll n,m,len,l,r; ll nex[MAXN],ru[MAXN],op[MAXN],tag[MAXN]; ll w[MAXN],ans[MAXN],cnt[MAXN],sz[MAXN];//w为每座防御塔的血量 //s->skewing n 歪斜 勇士们到达该防御塔的偏移量 ans 统计勇士的答案 //cnt统计防御塔的答案 采用topsort q单指队列 ll root[MAXN],ls[MAXN],rs[MAXN],q[MAXN],dis[MAXN]; ll sum[MAXN],add[MAXN],mul[MAXN],v[MAXN]; inline void modify(ll x,ll a,ll b,ll c) { if(!x)return; sum[x]*=a;sum[x]+=b;tag[x]+=c; mul[x]*=a;add[x]*=a;add[x]+=b;cnt[x]+=c; } inline void pushdown(ll x) { modify(ls[x],mul[x],add[x],tag[x]); modify(rs[x],mul[x],add[x],tag[x]); tag[x]=0;mul[x]=1;add[x]=0;return; } inline ll merge(ll a,ll b) { if(!a||!b)return a|b; if(tag[a])pushdown(a); if(tag[b])pushdown(b); if(sum[a]>sum[b])swap(a,b); rs[a]=merge(rs[a],b); if(dis[ls[a]]<dis[rs[a]])swap(ls[a],rs[a]); dis[a]=dis[rs[a]]+1; return a; } inline void topsort() { while(l++<r) { ll te=q[l]; while(sum[root[te]]<w[te]&&sz[te]) { if(tag[root[te]])pushdown(root[te]); root[te]=merge(ls[root[te]],rs[root[te]]); ++ans[te];--sz[te]; } if(op[te])modify(root[te],v[te],0,1); else modify(root[te],1,v[te],1); //if(te==3)cout<<v[te]<<' '<<sum[root[te]]<<endl; root[nex[te]]=merge(root[nex[te]],root[te]); sz[nex[te]]+=sz[te];--ru[nex[te]];sz[te]=0; if(!ru[nex[te]])q[++r]=nex[te]; } return; } int main() { //freopen("1.in","r",stdin); //freopen("1.out","w",stdout); n=read();m=read(); for(ll i=1;i<=n;++i)w[i]=read(); for(ll i=2;i<=n;++i) { nex[i]=read(); ++ru[nex[i]];op[i]=read();v[i]=read(); } for(ll i=1;i<=m;++i) { sum[i]=read();ll x=read(); mul[i]=1;++sz[x]; root[x]=merge(root[x],i); } for(ll i=1;i<=n;++i)if(ru[i]==0)q[++r]=i; topsort(); while(sz[0]) { if(tag[root[0]])pushdown(root[0]); root[0]=merge(ls[root[0]],rs[root[0]]); --sz[0]; } for(ll i=1;i<=n;++i)put(ans[i]); for(ll i=1;i<=m;++i)put(cnt[i]); return 0; }
但是注意以后发现空间足够的话把long long全开上我就是以为该开的long long 应该都开了所以没管结果一直wa。
注意我这种拓扑写法最后要判断root[0]这是获得全部胜利的骑士所在的集合。
撒花~