2021上海icpc H kruskal重构树

题意:

有一副有 n n n个节点, m m m条边的图。有一个人起始在 x x x节点,它能在 x x x节点赚 a [ x ] a[x] a[x]的钱,但这个钱只能赚一次,不能再赚。它可以去别的节点再赚别的节点的钱,但当这个人需要经过道路 e d g e i = ( u , v ) edge_{i=}(u,v) edgei=(u,v)时,他当前有的钱不能小于过路费 w i w_{i} wi(不需要交过路费,只需要拥有这么多钱就可以了)。现在有 q q q次询问,每次给出起始点 x x x和初始钱财 k k k,问在这个条件下最多能赚多少钱?

Solution:

按照题意,我们在 x x x点,肯定是贪心的去赚钱,哪里能去我就去哪里,因为这没有损失,一定对赚钱有利,那么我们利用优先队列,加入可以考虑去的点,按需要的过路费小优先排序,哪里能去我们就去哪里,去到某个地方我们就把这个地方能考虑去的加入优先队列。走回头路是不需要花费的,所以这样的算法可行,但可惜时间复杂度是 O ( q n ) O(qn) O(qn)的。

这里引入一种数据结构: K r u s k a l Kruskal Kruskal重构树

对于任意一副 n n n个节点, m m m条边的图,可以按照如下规则重构这张图:

1.按边权排序所有边

2.按顺序加入边,如果两个端点目前联通,那么跳过,否则把这两个端点属于的集合的根连向一个新的虚点,虚点的权值是这条边的权值,只有虚点有权值,原图中就有地点我们叫实点

在这里插入图片描述

例如这张图,按照边权从小到大的排序,可以得到这样的边序列

( 1 , 3 ) , ( 2 , 3 ) , ( 1 , 3 ) , ( 3 , 4 ) (1,3),(2,3),(1,3),(3,4) (1,3),(2,3),(1,3),(3,4)

按顺序加入边,得到这幅图(圈内的数字是点编号,圈外数字是点权值,这棵树是倒的)

在这里插入图片描述

由于我们按照从小到大的顺序加入边,于是在原图联通的部分彼此到达的最大权值一定不超过他们的 l c a lca lca的权值

按照这个规则构建了原图的 K r u s k a l Kruskal Kruskal重构树之后。我们可以这样考虑这个问题,以这个图为例子,假设构造完的 K r u s k a l Kruskal Kruskal重构树就是这棵树

如果我们在点 4 4 4,我们只需要 4 4 4的钱财就可以到达他的父亲,由于边是从小到大加入的,到达了某个虚点我们就一定能到达这个虚点的子树内的所有点,于是,只要某个虚点的钱,就可以获得这个虚点为根的子树的所有收益,在这里,到达 4 4 4就可以获得点 1 , 2 , 3 1,2,3 1,2,3点的收益了

更一般地考虑,假设我们在 x x x点,我们只需要一步一步向跟爬就可以了,于是我们只需要检查我们我们当前地钱够不够到达我们的父亲,这样一路往上跳,直到到达一个我们不能在往上跳的祖宗时,我们获得最大收益,是目前点的子树收益和,可是这样的时间复杂度还是 O ( q n ) O(qn) O(qn)的。

万幸的是我们在树上操作,我们不必每次只跳到自己的父亲,可以倍增跳到 2 k 2^k 2k级祖先,如何保证跳跃合法就是最后一个问题了,设 m a x 1 [ u ] [ i ] max1[u][i] max1[u][i]是从 u u u一步一步跳跃到 u u u 2 i 2^i 2i祖先时,(需要的过路费-已获得的利润)的最大值,那么如果 m a x 1 [ u ] [ i ] ≤ k max1[u][i]\leq k max1[u][i]k,我们就能跳跃,利用倍增,跳到 2 k 2^k 2k祖先的最大值就是 u u u 2 k − 1 2^{k-1} 2k1的最大值和 2 k − 1 2^{k-1} 2k1 2 k 2^k 2k的最大值的最大值。边界情况是到达父亲的值,此时获得的利润显然是 u u u子树的利润,于是

m a x 1 [ u ] [ 0 ] = v a l [ f a [ u ] [ 0 ] ] − s u m [ u ]   m a x 1 [ u ] [ i ] = m a x ( m a x 1 [ u ] [ i − 1 ] , m a x [ f a [ u ] [ i − 1 ] ] [ i − 1 ] ) max1[u][0]=val[fa[u][0]]-sum[u]\\~max1[u][i]=max(max1[u][i-1],max[fa[u][i-1]][i-1]) max1[u][0]=val[fa[u][0]]sum[u] max1[u][i]=max(max1[u][i1],max[fa[u][i1]][i1])

然后倍增跳跃即可,这样每次查询的复杂度就是 O ( l o g n ) O(logn) O(logn)了,总复杂度 O ( m l o g m + q l o g n + n ) O(mlogm+qlogn+n) O(mlogm+qlogn+n),即 O ( n l o g n ) O(nlogn) O(nlogn)级别的

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

using ll=long long;
const int N=100005,inf=0x3fffffff;
const long long INF=0x3f3f3f3f3f3f,mod=998244353;

struct krus_way
{
    int u,v,w;
}eedge[N];

struct way
{
    int to,next;
}edge[N<<2];
int cntt,head[N<<1];

void add(int u,int v)
{
    edge[++cntt].to=v;
    edge[cntt].next=head[u];
    head[u]=cntt;
}

ll sum[N<<1];
int n,m,q,a[N],f[N<<1],fa[N<<1][21],cnt,max1[N<<1][21],val[N<<1];

int find(int k)
{
    if(f[k]==k) return k;
    return f[k]=find(f[k]);
}

void dfs1(int u,int fa)
{
    //这里的u<=n原因是,重构树节点最多有n+m个,a数组只有n大小,并且只有原点才有收益初始值
    if(u<=n) sum[u]=a[u];
    //也可以把a数组开大一倍,u<=n就不用写了
    for(int i=1;i<=20;i++) ::fa[u][i]=::fa[::fa[u][i-1]][i-1];
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        if(v==fa) continue;
        dfs1(v,u);
        sum[u]+=sum[v];
    }
}
//max1=need-get
void dfs2(int u,int fa)
{
    max1[u][0]=val[::fa[u][0]]-sum[u];
    for(int i=1;i<=20;i++) max1[u][i]=max(max1[u][i-1],max1[::fa[u][i-1]][i-1]);
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].to;
        if(v==fa) continue;
        dfs2(v,u);
    }
}

void KruskalRebuild()
{
    sort(eedge+1,eedge+1+m,[&](const krus_way &x,const krus_way &y){
        return x.w<y.w;
    });
    cnt=n;
    //最多有m条边,每条边最多新增一个虚点,最后最多有n+m个点
    for(int i=1;i<=n+m;i++) f[i]=i;
    for(int i=1;i<=m;i++)
    {
        auto& [u,v,w]=eedge[i];
        int pu=find(u),pv=find(v);
        if(pu==pv) continue;
        f[pu]=f[pv]=fa[pu][0]=fa[pv][0]=++cnt;
        val[cnt]=w;
        add(pu,f[pu]); add(f[pu],pu);
        add(pv,f[pv]); add(f[pv],pv);
    }
    dfs1(cnt,0); dfs2(cnt,0);
}

ll solve(int x,int k)
{
    for(int i=20;i>=0;i--)
        if(fa[x][i]&&max1[x][i]<=k) x=fa[x][i];
    return sum[x];
}

int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    {
        auto& [u,v,w]=eedge[i];
        scanf("%d%d%d",&u,&v,&w);
    }
    KruskalRebuild();
    while(q--)
    {
        int x,k; scanf("%d%d",&x,&k);
        printf("%lld\n",k+solve(x,k));
    }
    return 0;
  {
        int x,k; scanf("%d%d",&x,&k);
        printf("%lld\n",k+solve(x,k));
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值