题解:P9341 [JOIST 2023] 警卫 / Security Guard
ForgetOIDuck 讲了一次,省集讲了一次,zr 又讲了一次,终于学明白了这个火题。
首先我们先直观体验一下这个操作移动方式上在干什么,下文中的 \(a\) 等价于题中的 \(s\)。
假设有一次移动是形如 \(x \to u \to y\),那么显然在 \(x \to u\) 的航线上,船上至少要有 \(\max(a_x, a_y)\) 个警卫。同时我们还发现,如果能把警卫带下船,那么我们直接携带尽量多的警卫下船是不劣的;到达 \(u\) 后需要换一艘船,但是我们还需要留下至少 \(a_u\) 个人在船上。
如下图,图中的黑色举行表示一个节点,矩形的高度表示每个节点的 \(a\);圆圈表示警卫,我们不妨给每个警卫分配一个高度,则每个警卫可以自由左右滑动,但是会被左右 \(\ge\) 其高度的位置被卡住。因此一次 \(1 \to n\) 的移动可以看作,每次航行的时候都携带恰好 \(\max\{a\}\) 个警卫在身上,然后每个节点都会留下一些警卫,按照这种方式行走直到到达 \(n\)。

则图中的圆圈个数就是最少需要的警卫数量。
上图为方便展示仅画了链的情况。树的情况和链的情况几乎相同,仅仅把区间换成了连通块。容易类比发现这依然是满足题意的。
接下来考虑 \(k=0\)。当图中存在环的时候,我们需要关闭一些边。容易发现断掉环上 \(\max(a_u + a_v)\) 最大的边 \((u, v)\) 在不影响连通性的同时不会让答案变大。不断重复这个断开环边的过程,容易把原图调整成一棵生成树。
至此归纳到了图为树的情况。接下来考虑对于一棵树,求出答案。则答案分为两部分:第一部分是对于一条边 \((u, v)\),被完全卡在 \(u, v\) 之间的警卫数量 \(\min(a_u, a_v)\),即图中所有红色圆圈;第二部分是其他的警卫,这一部分可以把贡献按照值域拆开,第 \(i\) 层的警卫数量是只保留点权 \(<i\) 的点的导出子图的连通块数量,因为每个警卫会被 \(a_u\ge i\) 的点 \(u\) 挡住,所以一个警卫只能在保留点权 \(<i\) 的点后的导出子图上的一个连通块上自由移动。记 \(f(x)\) 表示点权 \(\color{red}\le \color{black}x\) 后的点形成的联通块数目,则答案是 \(\displaystyle{\sum_{(u, v) \isin E} \min(a_u, a_v) + \sum_{x = \min\{a\}}^{\max{a} - 1} f(x)}\)。
注意到 \(f(x)\) 是一个森林的联通块数目的形式,施加点边容斥。点数是 \(\displaystyle{\sum_{i = 1}^n} [a_i \le x]\),边数是 \(\displaystyle{\sum_{(u, v) \le E} [\max(a_u, a_v) \le x]}\)。又因为 \(\displaystyle{\sum_{x = \min\{a\}}^{\max\{a\}-1} [t \le x]} = \max\{a\} - t\),所以
仅第一项和树的具体形态有关,因此对于一般图 \(k=0\) 的情况,以 \(a_u + a_v\) 作为一条边的边权,求解最小生成树即可。
再思考 \(k > 0\) 的情况:可以新加入一些边。假如我加入的边数没有限制,则最优情况的最小生成树一定是以 \(a\) 最小的点为根的菊花,记这个点权最小的点为 \(p\),此时 \(k = n-1\),更大的 \(k\) 都是没有用的。假设我们新加入的和 \(p\) 相连的边成为新边,那么当 \(k\) 减少,\(p\) 的新边数量也要求减少。这是一个经典问题最小度限制生成树,下面简要说明一下做法。
首先先求出最终状态下的最小生成树,上文已经说明是以 \(p\) 为根的菊花。注意下文中的所有连通性未特殊说明,均指仅考虑原图上的边意义下的连通性。则我们维护一个集合 \(S\) 表示目前未与 \(p\) 联通的连通块集合,则 \(S\) 中的每个连通块都含有一条到 \(p\) 的新边。
现在让 \(k\) 减少 \(1\),则我们要删除一条 \(p\) 为端点的新边,则我们需要选择一个未与 \(p\) 联通的连通块,将其与 \(p\) 相连的新边断开,随后加入这个联通块到连通块外一点的原图中的边。由 Boruvka 的思想,我们一定会取该连通块最小的出边。而假设 \(u\) 所在的连通块最小出边边权为 \(w\),则断开边 \((u, p)\) 的代价等于 \(w - (a_u + a_p)\),则我们一定会选择一个 \(w - (a_u + a_p)\) 最小的连通块。由最小生成树的凸性,这个算法必然是正确的。
最后的问题就在于如何在支持连通块合并的同时支持找连通块的最小出边。我们对每个连通块开一个堆,存有这个连通块的出边的一个超集,为了方便我们让这个堆的堆顶一定是合法的出边。在连边的时候我们将两个堆启发式合并,然后一直 pop 直到堆顶合法。对于求全局 \(w - (a_u + a_p)\) 最小的连通块,可以使用一个 std::set 进行维护,每次修改一个连通块的信息后先删除旧的 \(w - (a_u + a_p)\),修改后再插入新的 \(w - (a_u + a_p)\)。
至此问题得到解决,时间复杂度为 \(O(n \log^2 n)\),瓶颈在于堆的启发式合并。由于我维护的是与 \(p\) 不联通的连通块,所以代码实现和大部分题解代码略有不同。
#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;
}

浙公网安备 33010602011771号