Codeforces Round 977 (Div. 2) E. Digital Village(树形背包 kruskal重构树 凸函数 闵可夫斯基和(min,+)差分优化)

题目

n点m边的图,有p个点需要上网,可以在一些点上装服务器,

上网点x到服务器y的延迟,是一条x到y路径上的边权的最大值,

最优延迟,是所有x到y的路径里,使最大值最小的那个延迟

对于k=1,2,...,n,确定恰好装k个服务器时,p个点的最小总延迟之和

easy:n=400

hard:n=5000

extreme:n=2e5

思路来源

lzz、zeeman、jiangly代码

闵可夫斯基和与凸函数优化

闵可夫斯基和学习笔记 - HappyJaPhy - 博客园

【学习笔记】Max 卷积 & 闵可夫斯基和 - APJifengc - 博客园

题解

因为是边权最大值,不难想到按边权从小到大排序维护

先考虑easy & hard,

如果我对于每个连通块维护装1,2,...,x个服务器时,能满足连通块内所有上网点的最小总延迟之和

那么kruskal求最小生成树,连接一条生成树边时,

计u是左连通块的祖先,v是右连通块的祖先,w是这条边的权值,

只有以下可能:

①没有上网点需要跨过w这条边,

左连通块装x个服务器,右连通块装y个服务器,

总共装x+y个,转移就是树形背包的合并

②有上网点需要跨过w这条边,因为w是当前考虑到的最大边,

所以只能是其中一个连通块里没有装服务器,

全跑到另一个连通块里用服务器,那么讨论一下是u没有还是v没有

直接暴力每次开n大小的vector时复杂度是O(n^3)的,

但是用树形背包的trick,开大小是连通块大小的vector,复杂度就是O(n^2)的了,

可以参考https://leetcode.cn/circle/discuss/t7l62c/

当然easy还有一种做法是,先floyd求出上网点x到服务器y的最大边的最小值dis[x][y]

然后类似prim,由f(i)去推f(i+1),每次新找一个最优的服务器当上网点,

只是这种做法无法扩展到O(n^2)

然后考虑extreme,树形背包转移是经典的凸函数优化的式子,实际就是在维护凸函数

可以参考上面提到的博客,

简单来说,就是两个凸包的闵可夫斯基和(min/max,+)卷积还是凸包

形如h[i][z]=min(f[u][x]+g[v][y]+F(u,v),x+y=z)这种式子,

在f、g、F是凸函数的前提下,可以通过差分+排序优化复杂度

本题中可以感性理解下凸函数,随着服务器的增多,每次能优化的边权越来越少

然后考虑做法,先在kruskal求生成树的过程中,把kruskal重构树建出来,

在kruskal重构树上dp,

dp[x]表示(x所在连通块里本来没有服务器)如果有装一个服务器的机会,装在x这个连通块里,使得x里的所有上网点,不再需要通过x的父亲fa[x]对应的原图的边进到另一个连通块里时,可以节省的最大权值之和是多少

设kruskal重构树上点x有直连儿子y1、y2

假设y1内子树所有点都在y1处集结(已经通过y1及子树内的边联通),

如果有一个服务器在y1所在的连通块里,

就能阻止y1子树内所有点通过点x进入兄弟y2子树,

也就是x这条边权实际不需要连,也就是能省去y1->x增量的贡献

递归往y1的子树里考虑,放的这一个服务器,

可以满足既在y1连通块里,也在y1直连儿子z1的连通块里,

这样就能省去z1->y1增量的贡献,

在z1和z2两个儿子中,取dp值二者更大的那个儿子显然更优

所以先往下搜求得儿子的结果,再回溯的时候由父亲决策选哪个儿子

递归到底实际是一个叶子,也就是原图上的点

所以放一个服务器时,影响的是一条权值和最大的链,

将dp值更大的儿子看成是重儿子,可以将树解构成若干条重链,

那么将kruskal重构树树链剖分后,按权值链降序贪心即可

每一条链都对应了加一个服务器时,可以省去的代价

hard代码(树形背包)

#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<map>
#include<set>
using namespace std;
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define per(i,a,b) for(int i=(a);i>=(b);--i)
typedef long long ll;
typedef double db;
typedef pair<int,int> P;
#define fi first
#define se second
#define pb push_back
#define dbg(x) cerr<<(#x)<<":"<<x<<" ";
#define dbg2(x) cerr<<(#x)<<":"<<x<<endl;
#define SZ(a) (int)(a.size())
#define sci(a) scanf("%d",&(a))
#define pt(a) printf("%d",a);
#define pte(a) printf("%d\n",a)
#define ptlle(a) printf("%lld\n",a)
#define debug(...) fprintf(stderr, __VA_ARGS__)
typedef long long ll;
const ll INF=1e15;
const int N=5e3+10;
int t,n,m,p,v,par[N],ans[N],sz[N],now;
vector<ll>f[N],dp[N];
bool ok[N];
struct edge{
    int u,v,w;
}e[N];
int find(int x){
    return par[x]==x?x:par[x]=find(par[x]);
}
bool operator<(edge a,edge b){
    return a.w<b.w;
}
int main(){
    sci(t);
    while(t--){
        sci(n),sci(m),sci(p);
        rep(i,1,n){
            par[i]=i;
            sz[i]=0;
            dp[i].resize(2,0);
        }
        rep(i,1,p){
            sci(v);
            ok[v]=1;
            sz[v]=1;
            dp[v][1]=0;
            dp[v][0]=INF;
        }
        //now=p;
        //per(i,n,now)ans[i]=0;
        rep(i,1,m){
            scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
        }
        sort(e+1,e+m+1);
        rep(i,1,m){
            int u=find(e[i].u),v=find(e[i].v),w=e[i].w;
            if(u==v)continue;
            vector<ll>tmp(sz[u]+sz[v]+1,INF);
            rep(x,0,sz[u]){
                rep(y,0,sz[v]){
                    tmp[x+y]=min(tmp[x+y],dp[u][x]+dp[v][y]);
                    if(!x && !y)continue;
                    if(!x)tmp[y]=min(tmp[y],dp[v][y]+1ll*sz[u]*w);
                    if(!y)tmp[x]=min(tmp[x],dp[u][x]+1ll*sz[v]*w);
                }
            }
            dp[u]=tmp;
            sz[u]+=sz[v];
            par[v]=u;
        }
        int f=find(1);
        rep(i,1,n){
            printf("%lld%c",i>=p?0:dp[f][i]," \n"[i==n]);
        }
    }
    return 0;
}

/*
dp[i][j]
*/

extreme代码(kruskal重构树 凸函数优化 差分 贪心)

//#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<map>
#include<set>
using namespace std;
#define rep(i,a,b) for(int i=(a);i<=(b);++i)
#define per(i,a,b) for(int i=(a);i>=(b);--i)
typedef long long ll;
typedef double db;
typedef pair<int,int> P;
#define fi first
#define se second
#define pb push_back
#define dbg(x) cerr<<(#x)<<":"<<x<<" ";
#define dbg2(x) cerr<<(#x)<<":"<<x<<endl;
#define SZ(a) (int)(a.size())
#define sci(a) scanf("%d",&(a))
#define pt(a) printf("%d",a);
#define pte(a) printf("%d\n",a)
#define ptlle(a) printf("%lld\n",a)
#define debug(...) fprintf(stderr, __VA_ARGS__)
const int N=2e5+10,M=4e5+10;
const ll INF=1e15;
int t,n,m,p,v,c,par[M],sz[M],W[M];
ll ans,dp[M];
vector<ll>f[M];
struct edge{
    int u,v,w;
}e[N];
int find(int x){
    return par[x]==x?x:par[x]=find(par[x]);
}
bool operator<(edge a,edge b){
    return a.w<b.w;
}
void dfs(int u,int fa){
    dp[u]=0;
    for(auto &v:f[u]){
        dfs(v,u);
        dp[u]=max(dp[u],dp[v]);
    }
    for(auto &v:f[u]){
        if(dp[u]==dp[v]){
            dp[v]=0;
            break;
        }
    }
    if(u<c){
        dp[u]+=1ll*sz[u]*(W[fa]-W[u]);
    }
}
int main(){
    sci(t);
    while(t--){
        sci(n),sci(m),sci(p);
        rep(i,1,n){
            par[i]=i;
            sz[i]=W[i]=0;
        }
        rep(i,1,p){
            sci(v);
            sz[v]=1;
        }
        rep(i,1,m){
            scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
        }
        sort(e+1,e+m+1);
        c=n;
        rep(i,1,m){
            int u=find(e[i].u),v=find(e[i].v),w=e[i].w;
            if(u==v)continue;
            W[++c]=w;sz[c]=0;
            par[u]=par[v]=par[c]=c;
            f[c].pb(u);
            f[c].pb(v);
            sz[c]=sz[u]+sz[v];
        }
        dfs(c,0);
        ll ans=1ll*p*W[c];
        sort(dp+1,dp+c+1,greater<ll>());
        rep(i,1,n){
            ans-=dp[i];
            printf("%lld%c",ans," \n"[i==n]);
        }
        rep(i,1,c){
            f[i].clear();
        }
    }
    return 0;
}
/*
设kruskal重构树上点x有直连儿子y1、y2

假设y1内子树所有点都在y1处集结(已经通过y1及子树内的边联通),
如果有一个宽带在y1所在的连通块里,
就能阻止y1子树内所有点通过点x进入兄弟y2子树,
也就是x这条边权实际不需要连,也就是能省去y1->x增量的贡献

递归往下考虑,放的这一个宽带,
可以满足既在y1连通块里,也在y1直连儿子z1的连通块里,
这样就能省去z1->y1增量的贡献,取二者更大的那个显然更优,

递归到底实际是一个叶子,也就是原图上的点
所以放一个宽带最优影响的是一条权值和最大的重链
那么将kruskal重构树树链剖分后,按权值链降序贪心即可

由于实际转移是一个树形背包的形式,函数也都是凸函数,
所以闵可夫斯基和(min,+)卷积可以被拆成差分,即f(i+1)可以由f(i)得到
*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值