洛谷 - P1552 - 派遣 - 左偏树 - 并查集

首先把这个树建出来,然后每一次操作,只能选中一棵子树。对于树根,他的领导力水平是确定的,然后他更新答案的情况就是把他子树内薪水最少的若干个弄出来。
问题在于怎么知道一棵子树内薪水最少的若干个分别是谁。
考虑到原本就是从别的博客过来的,先天知道了这是左偏树。
那么就是每次合并若干个忍者吗?

首先一开始每个忍者可以自己派遣自己出去,贡献就是1x自己的领导力水平。
然后逐个合并每个忍者和自己的直接领导,合并之后就保证了自己的直接领导一定有被选中!新的贡献就是领导的领导力水平x目前堆最小的若干个元素?

很容易发现,薪水较高的忍者会很快被淘汰掉。
所以可以设想一个最大堆,每次两棵左偏树维护的还有整棵树的薪水和。
当两棵左偏树合并时,直到他们的薪水和低于限制,把堆根弹出并更新薪水和。

好,那就动手吧。
需要注意的是,要从叶子节点向上合并。


出了一些问题,的确应该是从叶子节点向上合并,但是应该是当这个节点出度为0的时候才统计。
合并可并堆自然不用说,把两组接在一起就可以了。在Pop的时候可能会把领导节点丢失掉,但是领导节点所在的块是依然存在的。
所以一开始高于薪水的就直接不建立左偏树,只建立并查集。
每次合并,就把当前忍者所在的并查集根对应的左偏树和领导的并查集根对应的左偏树合并(当领导左偏树存在时,要是不存在则只是直接把并查集根指向领导的根)
一个块的领导力水平和总的薪水和由并查集来确定,Pop的时候其中一棵子树接在另一棵子树上,并查集的根应该指向这个新的根,同时把领导力水平覆盖过去,薪水和为原薪水和减去Pop的节点。


终于过了啊!关键点是在于合并左偏树前维护并查集的整体信息,然后Pop节点时把并查集的信息下传。


#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int solve();

int main() {
#ifdef Yinku
    freopen("Yinku.in","r",stdin);
#endif // Yinku
    solve();
}

int n,m;

const int MAXN=100005;
int tot,v[MAXN],l[MAXN],r[MAXN],d[MAXN],f[MAXN];
int sum[MAXN],siz[MAXN];
//编号为x的左偏树的薪水总和为sum[x]
int lead[MAXN];
//编号为x的左偏树的领导(不一定是树根)的领导力是lead[x]

int outd[MAXN];
//出度

int ld[MAXN];

void show() {
    printf("---\n");
    printf("ii=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",i);
    }
    printf("\n");

    printf("vv=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",v[i]);
    }
    printf("\n");

    printf("su=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",sum[i]);
    }
    printf("\n");

    printf("le=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",lead[i]);
    }
    printf("\n");

    printf("ld=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",ld[i]);
    }
    printf("\n");

    printf("ls=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",l[i]);
    }
    printf("\n");

    printf("rs=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",r[i]);
    }
    printf("\n");

    printf("si=");
    for(int i=1; i<=n; i++) {
        printf("%3d ",siz[i]);
    }
    printf("\n---\n");
}



//这其实是一片左偏树森林
struct Leftist_Tree {
    int Merge(int x,int y) {
        //将两棵左偏树,返回他们的新根
        //当其中一棵是空树,直接返回另一棵
        if(!x||!y)
            return x+y;

       // printf("Merge %d %d\n",x,y);
        //show();

        //此处符号决定是最小堆还是最大堆
        //标记是不是更改过领导为y
        int haveswap=0;
        if(v[x]<v[y]||(v[x]==v[y]&&x>y)){
            swap(x,y);
        }
        //维持x不比y小,相等则把大的那个作为根
        r[x]=Merge(r[x],y);
        //保证左子树比右子树高

        //维持并查集的性质
        f[r[x]]=x;
        if(d[l[x]]<d[r[x]])
            swap(l[x],r[x]);
        d[x]=d[r[x]]+1;

        //show();
        //printf("New root=%d\n",x);
        return x;
    }
    void Init(int n) {
        //使用v[]中的值初始化一片左偏树-并查集森林
        tot=n;
        for(int i=1; i<=n; i++) {
            l[i]=r[i]=d[i]=0;
            //不合要求的一开始就不建左偏树
            if(v[i]<=m){
                sum[i]=v[i];
                siz[i]=1;
            }
            else{
                v[i]=0;
                sum[i]=0;
                siz[i]=0;
            }
            //只建立并查集
            f[i]=i;
        }
    }

    int Get_root(int x){
        //查找编号为x的节点所在的左偏树的根的序号,不需要可以删除
        int r=x;
        while(f[r]!=r)
            r=f[r];
        //路径压缩,直接指向他们的根
        int k;
        while(f[x]!=r){
            k=f[x];
            f[x]=r;
            x=k;
        }
        return r;
    }
    int Pop(int x) {
        //将两个子树合并,相当于删除了堆顶
        //删除一个元素,要把新的树根的薪水和更新
        //其次要把新树根的领导力也更新

        //把一棵树的堆顶删除
        int rt=Merge(l[x],r[x]);
        if(v[x]){
            sum[rt]=sum[x]-v[x];
            //本身一开始v[x]就没进来的,不需要减去siz
            siz[rt]=siz[x]-1;
        }
        lead[rt]=lead[x];
        f[x]=f[l[x]]=f[r[x]]=f[rt]=rt;
        return rt;
    }
}lt;

void Merge2(int p1,int p2){
    if(p1==p2)
        exit(-1);
    int newsum=sum[p1]+sum[p2];
    int newsiz=siz[p1]+siz[p2];
    int newlead=lead[p1];
    sum[p1]=sum[p2]=newsum;
    siz[p1]=siz[p2]=newsiz;
    lead[p1]=lead[p2]=newlead;
}

queue<int> pq;

ll ans=0;

void Update_Ans(int x){
    //编号为x的左偏树需要更新答案
    int fx=lt.Get_root(x);
    //现在这个忍者的根在fx
    ll TMP=1ll*siz[fx]*lead[fx];
    if(TMP>ans){
        ans=TMP;
    }
    return;
}

int solve() {
    scanf("%d%d",&n,&m);
    memset(outd,0,sizeof(outd));
    for(int i=1; i<=n; i++) {
        scanf("%d%d%d",&ld[i],&v[i],&lead[i]);
        if(v[i]<=m) {
            ans=max(ans,(ll)lead[i]);
        }
        outd[ld[i]]++;
    }

    lt.Init(n);
    //show();

    for(int i=1;i<=n;i++){
        if(outd[i]==0)
            pq.push(i);
    }

    while(!pq.empty()){
        int i=pq.front();
        if(ld[i]==0)
            break;
        pq.pop();
        outd[ld[i]]--;


        //合并他和他的领导,找到两个并查集合并
        int p1=lt.Get_root(ld[i]);
        int p2=lt.Get_root(i);
        //if(p1==p2)
            //exit(-1);
        //合并,新的根是x
        Merge2(p1,p2);
        //show();
        //cout<<"!!!\n\n\n"<<endl;
        int x=lt.Merge(p1,p2);
        //show();
        //合并完之后sum超过了m,要弹出一些节点,根据定义不会出错
        while(sum[x]>m) {
            x=lt.Pop(x);
        }

        //cout<<"AP!"<<endl;
        //show();

        if(outd[ld[i]]==0){
            //某个忍者和他所有的下属合并完成,更新他
            Update_Ans(ld[i]);
            pq.push(ld[i]);
        }
    }
    printf("%lld\n",ans);
    return 0;
}

posted @ 2019-06-01 11:06  韵意  阅读(142)  评论(0编辑  收藏  举报