题解:P9341 [JOIST 2023] 警卫 / Security Guard
更好的阅读体验
ForgetOIDuck 讲了一次,省集讲了一次,zr 又讲了一次,终于学明白了这个火题。
首先我们先直观体验一下这个操作移动方式上在干什么,下文中的
假设有一次移动是形如
如下图,图中的黑色矩形表示一个节点,矩形的高度表示每个节点的
则图中的圆圈个数就是最少需要的警卫数量。
上图为方便展示仅画了链的情况。树的情况和链的情况几乎相同,仅仅把区间换成了连通块。容易类比发现这依然是满足题意的。
接下来考虑
至此归纳到了图为树的情况。接下来考虑对于一棵树,求出答案。则答案分为两部分:第一部分是对于一条边
注意到
仅第一项和树的具体形态有关,因此对于一般图
再思考
首先先求出最终状态下的最小生成树,上文已经说明是以
现在让
最后的问题就在于如何在支持连通块合并的同时支持找连通块的最小出边。我们对每个连通块开一个堆,存有这个连通块的出边的一个超集,为了方便我们让这个堆的堆顶一定是合法的出边。在连边的时候我们将两个堆启发式合并,然后一直 pop 直到堆顶合法。对于求全局 std::set 进行维护,每次修改一个连通块的信息后先删除旧的
至此问题得到解决,时间复杂度为
#include<bits/stdc++.h>
#define endl '\n'
#define N 400006
using namespace std;
using i64=long long;
using pil=pair<int,i64>;
using pli=pair<i64,int>;
int n,m,q,mx,a[N],fa[N];
i64 sum,now,ans[N];
vector<pil> G[N];
priority_queue<pli,vector<pli>,greater<pli> > h[N];
set<pli> S;
int find(int k) {return fa[k]==k?k:fa[k]=find(fa[k]);}
inline void merge(int x,int y) // y -> x
{
if(h[y].size()>h[x].size())swap(h[x],h[y]);
while(h[y].size())h[x].push(h[y].top()),h[y].pop();
}
main()
{
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),fa[i]=i,mx=max(mx,a[i]),sum+=a[i];
for(int i=1,u,v;i<=m;i++)
{
scanf("%d%d",&u,&v);
G[u].push_back({v,a[u]+a[v]}),G[v].push_back({u,a[u]+a[v]});
}
int mn=2e9,pos=0;
for(int i=1;i<=n;i++)if(a[i]<mn)mn=a[i],pos=i;
for(int i=1;i<=n;i++)
{
fa[i]=i;
if(i!=pos)
{
for(auto j:G[i])h[i].push({j.second,j.first});
S.insert({h[i].top().first-(a[pos]+a[i]),i}),now+=a[pos]+a[i];
}
}
for(int i=n-1;~i;i--)
{
ans[i]=now; if(!i)break;
int u=S.begin()->second,v=h[u].top().second;
now+=S.begin()->first,S.erase({h[u].top().first-(a[pos]+a[u]),u});
if(find(v)!=find(pos))
{
v=find(v),S.erase({h[v].top().first-(a[pos]+a[v]),v});
merge(v,u),fa[u]=v;
while(h[v].size())
{
int w=h[v].top().second;
if(find(w)==find(v))h[v].pop(); else break;
}
S.insert({h[v].top().first-(a[pos]+a[v]),v});
} else fa[u]=find(v);
}
for(int i=0;i<=q;i++)printf("%lld\n",ans[min(i,n-1)]+mx-sum);
return 0;
}