以前粗浅地学习过这个数据结构,但是学得太浅学了跟没学一样。现在打算在学一遍,也许现在也是学了跟没学一样qaq。
推荐博客:https://wenku.baidu.com/view/20e9ff18964bcf84b9d57ba1.html (论文)
https://blog.csdn.net/wang3312362136/article/details/80615874
https://blog.csdn.net/pengwill97/article/details/82874235 (代码很好)
可并堆顾名思义就是可以合并的堆,可以理解为可以合并两个优先队列的数据结构,可并堆代码好写时间复杂度也十分优秀(大佬说是可以被卡成O(n)的)是一个不错的数据结构。可并堆具有堆性质和左偏性质。常见的操作有 ①合并两个堆 ②查询某个数所在堆 ③查询/删除堆顶元素。
题目练习:
模板题:洛谷P3377 左偏树
代码是参考上面大佬的(读书人的事能叫...咳咳)。但是大佬有一点错误:在查找某个元素的所属堆的时候是用的暴力向上跳的方式,这样一步步跳会被卡到O(n),在洛谷提交会被卡最后一组数据。优化办法是查找的过程应该顺便路径压缩,并且要注意采用了路径压缩那么pop元素的时候pop掉的点要指向新的根 。
#include <bits/stdc++.h> using namespace std; const int nmax = 1e6 + 7; const int INF = 0x3f3f3f3f; struct node { int val, lc, rc, dis, fa; }tree[nmax]; int tot = 0; int n, m; void init(int x) { for (int i = 0; i <= x; ++i) { tree[i].lc = tree[i].rc = tree[i].dis = 0; tree[i].fa = i; //一开始每个元素都是堆顶 } } int merge(int x, int y) { if (x == 0) return y; if (y == 0) return x; if (tree[x].val > tree[y].val || (tree[x].val == tree[y].val && x > y) ) swap(x, y); //注意这里的合并优先级,像是优先队列的优先级 tree[x].rc = merge(tree[x].rc, y); tree[tree[x].rc].fa = x; if (tree[tree[x].rc].dis > tree[tree[x].lc].dis) swap(tree[x].rc, tree[x].lc); tree[x].dis = tree[x].rc == 0 ? 0 : tree[tree[x].rc].dis + 1; return x; } int findset(int x) { return tree[x].fa==x ? x : tree[x].fa=findset(tree[x].fa); //fa=x的才是堆顶 } //int findset(int x) { // while (tree[x].fa != x) { //fa=x的才是堆顶 // x = tree[x].fa; // } // return x; //} int add(int val, int x) { //往x堆新增元素val tree[tot].lc = tree[tot].rc = tree[tot].dis = 0; tree[tot++].val = val; return merge(tot - 1, x); } int del(int x) { int l = tree[x].lc, r = tree[x].rc; tree[x].fa = tree[x].lc = tree[x].rc = tree[x].dis = 0; tree[x].val = -INF; tree[l].fa = l, tree[r].fa = r; return tree[x].fa=merge(l, r); //加入了路径压缩,pop掉的点要指向新的根 } int build() { queue<int> q; for (int i = 1; i <= n; ++i) q.push(i); while (!q.empty()) { if (q.size() == 1) break; else { int x = q.front(); q.pop(); int y = q.front(); q.pop(); q.push(merge(x, y)); } } int finally = q.front(); q.pop(); return finally; } int main() { scanf("%d %d", &n, &m); init(n); for (int i = 1; i <= n; ++i) scanf("%d", &tree[i].val); int op, a, b; for (int i = 1; i <= m; ++i) { scanf("%d", &op); if (op == 1) { scanf("%d %d", &a, &b); int xx = findset(a), yy = findset(b); if (tree[a].val == -INF || tree[b].val == -INF || xx == yy) { continue; } else { merge(xx, yy); } } else { scanf("%d", &a); if (tree[a].val == -INF) { printf("-1\n"); } else { int tmp = findset(a); printf("%d\n", tree[tmp].val); del(tmp); } } } return 0; }
BZOJ 2809 dispatching
题意:给定n个点的树,每个点有代价b和价值c,选一个根x然后在以x为根的子树选尽量多的点且这些点代价总和小于等于m,得到的价值是c[x]*siz[x](x点价值乘上选的点总数)。
解法:这道题非常好,刚好用来练手可并堆。一个显然正确的做法是,在树上dfs,dfs到当前点x就计算以在x子树选的最大值,那么如果当前子树花费总和大于m的话当然是按照花费从大到小把点剔除掉,直至花费小于m就是答案。再思考发现这个过程自下往上是可以连续考虑的(这里的意思是儿子子树不用的点父亲子树也一定不会用到因为m是固定的!)。那么就可以搜索过程维护一个堆,然后堆自下往上合并,那么就要用到左偏树优化了。
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 7; const int INF = 0x3f3f3f3f; typedef long long LL; struct node { int val, lc, rc, dis, fa; }tree[N]; vector<int> G[N]; int tot = 0; int n, m,rt,a[N],b[N],c[N]; LL ans=0; void init(int x) { for (int i = 0; i <= x; ++i) { tree[i].lc = tree[i].rc = tree[i].dis = 0; tree[i].fa = i; //一开始每个元素都是堆顶 } } int merge(int x, int y) { if (x == 0) return y; if (y == 0) return x; if (tree[x].val < tree[y].val || (tree[x].val == tree[y].val && x < y) ) swap(x, y); //注意这里的合并优先级,像是优先队列的优先级 tree[x].rc = merge(tree[x].rc, y); tree[tree[x].rc].fa = x; if (tree[tree[x].rc].dis > tree[tree[x].lc].dis) swap(tree[x].rc, tree[x].lc); tree[x].dis = tree[x].rc == 0 ? 0 : tree[tree[x].rc].dis + 1; return x; } int findset(int x) { //路径压缩 return tree[x].fa==x ? x : tree[x].fa=findset(tree[x].fa); //fa=x的才是堆顶 } //int findset(int x) { // while (tree[x].fa != x) { //fa=x的才是堆顶 // x = tree[x].fa; // } // return x; //} int add(int val, int x) { //往x堆新增元素val tree[tot].lc = tree[tot].rc = tree[tot].dis = 0; tree[tot++].val = val; return merge(tot - 1, x); } int del(int x) { int l = tree[x].lc, r = tree[x].rc; tree[x].fa = tree[x].lc = tree[x].rc = tree[x].dis = 0; tree[x].val = -INF; tree[l].fa = l, tree[r].fa = r; return tree[x].fa=merge(l, r); //加入了路径压缩,pop掉的点要指向新的根 } int build() { queue<int> q; for (int i = 1; i <= n; ++i) q.push(i); while (!q.empty()) { if (q.size() == 1) break; else { int x = q.front(); q.pop(); int y = q.front(); q.pop(); q.push(merge(x, y)); } } int finally = q.front(); q.pop(); return finally; } LL sum[N]; int siz[N]; void dfs(int x) { sum[x]+=b[x]; siz[x]++; for (int i=0;i<G[x].size();i++) { int y=G[x][i]; dfs(y); sum[x]+=sum[y]; siz[x]+=siz[y]; int fx=findset(x),fy=findset(y); merge(fx,fy); } while (sum[x]>m) { int fx=findset(x); sum[x]-=tree[fx].val; del(fx); siz[x]--; } ans=max(ans,(LL)siz[x]*c[x]); } int main() { scanf("%d %d", &n, &m); for (int i=1;i<=n;i++) { scanf("%d%d%d",&a[i],&b[i],&c[i]); if (a[i]==0) rt=i; else G[a[i]].push_back(i); } init(n); for (int i = 1; i <= n; ++i) tree[i].val=b[i]; dfs(rt); cout<<ans<<endl; return 0; }
BZOJ 1367 sequence
题意:给定序列a1~an,求一个递增序列b1~bn,使得abs(a1-b1)+abs(a2-b2)+...abs(an-bn)最小。
解法:这是论文里的题目,看不懂的话这里https://www.cnblogs.com/cnyali-Tea/p/10386117.html有一篇更详细而且带图的题解。
这里简单说下就是:一开始把每个数a[i]都当作一个区间,每个区间的中位数w[i]=a[i],从左到右维护不下降答案序列,每次插入一个数a[i+1]。
比较w[i]和w[i+1],① 如果w[i]<w[i+1] ,答案合法不用管。②若w[i]>w[i+1],不合法,要合并w[i]所在区间和w[i+1]所在区间并更新合并区间的中位数。
这样不断比较序列最后两个区间的中位数w[i]直至合法,如此维护出来的就是不下降的合法答案。
那么为什么能用左偏树实现合并两个序列并且快速求出新序列的中位数呢?方法是用最大堆左偏树维护该序列前半段数字,这样左偏树的堆顶就是中位数,那么合并的时候因为根据上诉结论合并后新序列的中位数只会变小不会变大,所以就直接合并两个序列前半段的数,如果新序列数字个数多于半段数个数就pop出来就行了。
还有一个细节:上面说的还是不下降,但是答案要求的是上升的,这里有一个实用的小技巧把每个数a[i]变成a[i]-i即可(在其他类似要求上升也可以用这个技巧)。
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 7; const int INF = 0x3f3f3f3f; struct node { int val, lc, rc, dis, fa; }tree[N]; int tot = 0; int n, m; void init(int x) { for (int i = 0; i <= x; ++i) { tree[i].lc = tree[i].rc = tree[i].dis = 0; tree[i].fa = i; //一开始每个元素都是堆顶 } } int merge(int x, int y) { if (x == 0) return y; if (y == 0) return x; if (tree[x].val < tree[y].val || (tree[x].val == tree[y].val && x > y) ) swap(x, y); //注意这里的合并优先级,像是优先队列的优先级 tree[x].rc = merge(tree[x].rc, y); tree[tree[x].rc].fa = x; if (tree[tree[x].rc].dis > tree[tree[x].lc].dis) swap(tree[x].rc, tree[x].lc); tree[x].dis = tree[x].rc == 0 ? 0 : tree[tree[x].rc].dis + 1; return x; } int get(int x) { //路径压缩 return tree[x].fa==x ? x : tree[x].fa=get(tree[x].fa); //fa=x的才是堆顶 } //int findset(int x) { // while (tree[x].fa != x) { //fa=x的才是堆顶 // x = tree[x].fa; // } // return x; //} int add(int val, int x) { //往x堆新增元素val tree[tot].lc = tree[tot].rc = tree[tot].dis = 0; tree[tot++].val = val; return merge(tot - 1, x); } int del(int x) { int l = tree[x].lc, r = tree[x].rc; tree[x].fa = tree[x].lc = tree[x].rc = tree[x].dis = 0; tree[x].val = -INF; tree[l].fa = l, tree[r].fa = r; return tree[x].fa=merge(l, r); //加入了路径压缩,pop掉的点要指向新的根 } int build() { queue<int> q; for (int i = 1; i <= n; ++i) q.push(i); while (!q.empty()) { if (q.size() == 1) break; else { int x = q.front(); q.pop(); int y = q.front(); q.pop(); q.push(merge(x, y)); } } int finally = q.front(); q.pop(); return finally; } int l,r,a[N],siz[N],q[N],len[N]; int main() { scanf("%d", &n); init(n); for (int i=1;i<=n;i++) scanf("%d",&a[i]),a[i]-=i; l=1; r=0; for (int i=1;i<=n;i++) { q[++r]=i; len[r]=siz[r]=1; tree[i].val=a[i]; while (l<r && tree[get(q[r])].val<tree[get(q[r-1])].val) { merge(get(q[r]),get(q[r-1])); len[r-1]+=len[r]; siz[r-1]+=siz[r]; r--; while (len[r]/2+1<siz[r]) { del(get(q[r])); siz[r]--; } } } q[++r]=n+1; int num=0; long long ans=0; for (int i=1;i<r;i++) { for (int j=q[i];j<q[i+1];j++) { ++num; //printf("%d ",tree[get(q[i])].val+num); ans+=abs(tree[get(q[i])].val-a[j]); } } cout<<ans<<endl; return 0; }