渡船很爽(ferry)
渡船很爽
题目描述
在 JOI 王国,有 \(n\) 个岛屿,编号从 \(1\) 到 \(n\)。每个岛屿都有一个不安全等级。岛屿 \(i\) 的不安全等级是 \(s_i\)。
在 JOI 王国,岛屿之间的船只是主要的交通方式。有 \(m\) 艘船,编号从 \(1\) 到 \(m\)。船 \(i\) 连接岛屿 \(u_i\) 和岛屿 \(v_i\)。我们可以在需要时运行船只。可以通过多次乘船从任何岛屿到达其他岛屿。
在 JOI 王国,有计划引入新的船只。我们可以选择任何一对岛屿来连接新引入的船只。
有一天,发生了一起事件。一艘停泊的船遭到了攻击。JOI 王国的总理 K 决定引入新的船只。他还要求 JOI 王国的船只满足以下安全条件。
- 当船停泊在岛屿 \(i\) 时,船上的保安人数必须大于或等于 \(s_i\)。
然而,由于雇佣保安很昂贵,我们希望最小化雇佣保安的人数。只要满足“可以通过多次乘船从任何岛屿到达其他岛屿”的条件,就可以废除当前运行的船只。
因此,我们将按如下方式运行船只。这里,\(k\) 是新引入的船只数量。
- 对于每艘新引入的船,我们选择它连接的两个岛屿。
- 我们选择若干(大于或等于 \(0\))船只,并废除它们。允许废除新引入的船只。
- 对于每艘船,我们将其停泊在它连接的两个岛屿之一。我们让若干保安登船。此外,必须满足以下条件。
条件:对于每对岛屿 \(u,v\),可以通过多次重复以下操作将乘客从岛屿 \(u\) 运输到岛屿 \(v\)。在此过程中,安全条件必须始终得到满足。
- 我们让乘客或保安登上停泊在乘客或保安所在岛屿的船。
- 我们让乘客或保安在船当前停泊的岛屿下船。
- 我们将船从当前停泊的岛屿移动到船连接的另一个岛屿。
由于预算有限,我们最多可以引入 \(q\) 艘新船。对于每个 \(k\ (0 \le k \le q)\),总理 K 想知道如果新引入的船只数量为 \(k\) 时,雇佣保安的最小可能人数。
编写一个程序,给定岛屿的信息、船只的航线以及我们可以引入的新船数量,计算每个 \(k\) 的雇佣保安的最小可能人数。
ps:省集题面太一坨了,搬的原题题面。
数据范围
- \(2\le n\le2\times10^5\);
- \(n-1\le m\le4\times10^5\);
- \(0\le q\le2\times10^5\);
- \(\forall i\in[1,n],1\le s_i\le10^9\);
- \(\forall i\in[1,m],1\le u_i<v_i\le n\);
- \(\forall x,y\in[1,n]\wedge x<y,(u_x,v_x)\neq(u_y,v_y)\);
- 可以通过多次乘船从任何岛屿到达其他岛屿(图连通);
- 给定值均为整数。
| 子任务编号 | 分值 | 特殊限制 |
|---|---|---|
| \(1\) | \(12\) | \(m=n-1\),\(q=0\),\(\forall i\in[1,n]\),\(s_i\le 2\),\(\forall i\in[1,m]\),\(u_i=i\),\(v_i=i+1\) |
| \(2\) | \(13\) | \(m=n-1\),\(q=0\),\(\forall i\in[1,m]\),\(u_i=i\),\(v_i=i+1\) |
| \(3\) | \(12\) | \(m=n-1\),\(q=0\) |
| \(4\) | \(13\) | \(q=0\) |
| \(5\) | \(8\) | \(n\le16\) |
| \(6\) | \(18\) | \(n\le3000\) |
| \(7\) | \(24\) | 无 |
时空限制
\(3\text{s}\),\(1024\text{Mb}\)。
做法
\(\text{sub1}\)
首先考虑一组肯定合法但不一定最优的解:每条与 \(s_i=2\) 相连的边的船初始都停靠在 \(s_i=2\) 的这座岛上,给 \(2\) 个保安。
这样合法的原因是任意一对相邻的点之间都可以随意运送乘客。但是这样很浪费。我们能不能在保安不够的时候,从别的船上抽调保安?
答案是肯定的。比如说下面这个图:

\((1,2)\) 之间的船初始时可以停在 \(2\) 号点,给 \(1\) 个保安;\((2,3)\) 之间的船初始时停在 \(3\) 号点,给 \(2\) 个保安。这样,如果想从 \(2\) 向 \(1\) 运送乘客,那就先让 \(3\) 号点的船去 \(2\) 号点,停靠后让其中 \(1\) 个保安去到 \((1,2)\) 之间的船上,然后船再从 \(2\) 去 \(1\)。随后让船和保安原路返回,又恢复原样。
于是我们想到,我们可以选择这条链上的一个点 \(x\) 使得 \(s_x=k\),让与它相连的边的船初始时都停靠在这个点并且给 \(2\) 个保安,剩下的所有点都至多有一条相连的边的船停靠在这个点(头上 \(2\) 个点没有),至于是哪条边,应该视这个点与 \(k\) 的位置关系而定。比如下图就是合法的:

\(\text{sub2}\)
有了 \(\text{sub1}\) 的做法,可以直接推广。我们选一个最大值,让与它相连的 \(2\) 条边的船全都停靠在这里,然后往两边推。
\(\text{sub3}\)
依旧推广,选出最大值作为根,往下推。我们注意到其实每个点对答案的贡献都是 \(s_i\times(deg_i-1)\),其中 \(deg_i\) 是点 \(i\) 的度数。当然,因为最大值作为根,所以最后答案为 \(\max\limits_{i=1}^n s_i+\sum\limits_{i=1}^{n}s_i\times(deg_i-1)\)。
\(\text{sub4}\)
依旧推广。考虑我们一定是要把图删成树。对 \(\text{sub3}\) 中式子变形:
于是我们可以把原图上的每条边的边权设为 \(s_{u_i}+s_{v_i}\),跑一遍最小生成树即可。
\(\text{sub5&6}\)
考虑 \(q\neq0\) 怎么做。从 \(k\) 到 \(k+1\) 时,存在一个最优方案使得只修改一条边。
首先跑出最小生成树。假设 \(x\) 点的 \(s_x\) 最小,我们以 \(x\) 为根。每次修改方案时,找到一条边 \((fa_u,u)\) 使得
最大。然后删去 \((fa_u,u)\),在 \(x\) 与 \(u\) 子树中 \(s\) 最小的点之间连一条边。
\(\text{sub7}\)
上个做法中找点、删边、连边之类的对树形态进行修改的操作十分不好维护,所以考虑倒着做。
依旧以 \(x\) 为根,容易想到最后(\(n-1\) 次操作后)的树一定是以 \(x\) 为根的菊花。我们每次需要删掉一条当前树中存在且原图不存在的边,然后加上一条当前不存在原图存在的边。
我们把 \(x\) 单独拎出来,剩下的部分一定是若干个与根相连的连通块(初始时因为是菊花所以是 \(n-1\) 个)。对于一个连通块,我们用一个小根堆维护这个连通块中所有的点连到别的连通块的、在原图中存在、当前不存在的边的边权。假设第 \(j\) 个连通块的堆顶为 \(minn_j\),与根相连的边的边权为 \(r_j\)。我们同时开一个小根堆维护所有的 \(minn-r\)。于是每次找到最小的 \(minn-r\),假设这是第 \(j\) 个连通块的,其中 \(minn_j\) 对应的边连向了连通块 \(j'\),那么我们删除 \(r_j\) 对应的边并连上 \(minn_j\) 对应的边,合并 \(j\) 和 \(j'\) 两个连通块。启发式合并即可。时间复杂度 \(O(n\log^2n+q)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,q;
int a[1000010],pos[1000010];
long long sum;
int minn=1e9,maxn=-1e9;
struct edge{
int u,v,id;
long long w;
}e[1000010];
bool operator < (edge e1,edge e2){
return e1.w>e2.w;
}
long long ans[1000010];
int vis[1000010];
priority_queue<edge> s[200010],S;
int fa[200010];
int getfa(int u){
if(fa[u]==u) return u;
return fa[u]=getfa(fa[u]);
}
int main(){
// freopen("ferry.in","r",stdin);
// freopen("ferry.out","w",stdout);
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
for(int i=1;i<=n;i++) cin>>a[i],minn=min(minn,a[i]),sum+=a[i],maxn=max(maxn,a[i]);
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
e[i]={x,y,0,a[x]+a[y]};
}
int root=0;
for(int i=1;i<=n;i++){
if(minn==a[i]){
root=i;
break;
}
}
for(int i=1;i<=n;i++){
if(i==root) continue;
ans[n-1]+=a[i]+a[root];
fa[i]=i;
}
int cnt=0;
for(int i=1;i<=m;i++){
if(root==e[i].u||root==e[i].v){
vis[e[i].u]=vis[e[i].v]=1;
cnt--;
continue;
}
e[i].id=e[i].u;
s[e[i].u].push(e[i]);
e[i].id=e[i].v;
s[e[i].v].push(e[i]);
}
cnt+=n-1;
for(int i=1;i<=n;i++){
pos[i]=i;
if(vis[i]) continue;
edge now=s[i].top();
now.w-=a[root]+a[i];
S.push(now);
}
for(int i=n-2;i>=cnt;i--) ans[i]=ans[n-1];
for(int i=1;i<=cnt;i++){
ans[cnt-i]=ans[cnt-i+1];
while(S.size()&&(getfa(S.top().u)==getfa(S.top().v)||s[S.top().id].empty())) S.pop();
if(!S.size()) continue;
edge now=S.top();
S.pop();
ans[cnt-i]+=now.w;
int x=getfa(now.u),y=getfa(now.v);
if(s[x].size()>s[y].size()){
while(!s[y].empty()) s[x].push(s[y].top()),s[y].pop();
fa[y]=x;
if(now.id==x) pos[x]=pos[y];
}
else{
while(!s[x].empty()) s[y].push(s[x].top()),s[x].pop();
fa[x]=y;
if(now.id==y) pos[y]=pos[x];
}
x=getfa(x);
while(s[x].size()&&getfa(s[x].top().u)==getfa(s[x].top().v)) s[x].pop();
if(!s[x].size()||vis[pos[x]]) continue;
now=s[x].top();
now.w-=a[root]+a[pos[x]];
now.id=x;
S.push(now);
}
for(int i=0;i<=q;i++){
if(i>=n-1) cout<<ans[n-1]-sum+maxn<<'\n';
else cout<<ans[i]-sum+maxn<<'\n';
}
}

浙公网安备 33010602011771号