P11260
题目大意
有
请对于每个
题解
首先需要注意到关键边不一定不成环,所以我们得到的不一定是一个生成树,也有可能有一些仅由关键边围成的环。
接下来,让我们考虑一个贪心做法:我们先求出
具体地,枚举一条未被加入过的关键边,将其两个端点在生成树上的路径取出,如果路径上不全为关键边,可以选择一条边权最大的非关键边删掉并将此边加入生成树,否则这条边即使被选上也不会加入生成树。
直接暴力(连树上倍增都不需要)做,用 Kruskal 算法求解最小生成树可以做到
参考代码
这是我的 AC 记录。
:::success[参考代码]
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
long long i,j,n,m,k,u[1100000],v[1100000],w[1100000],ok[1100000],num[1100000],*mp[11000],*mpv[11000],fa[11000],fv[11000],f[11000],deep[11000],fl[100],ans[100],det,wh,tt;
long long root(long long a)
{
if(f[a]==0)
return a;
else
return f[a]=root(f[a]);
}
bool in(long long a,long long b)
{
a=root(a);
b=root(b);
if(a==b)
return false;
f[a]=b;
return true;
}
void make_tree()
{
long long i;
for(i=1;i<=n;i++)
num[i]=0;
for(i=1;i<=m;i++)
if(ok[i]==1)
{
num[u[i]]++;
num[v[i]]++;
}
for(i=1;i<=n;i++)
{
mp[i]=new long long[num[i]+1];
mpv[i]=new long long[num[i]+1];
mp[i][0]=0;
}
for(i=1;i<=m;i++)
if(ok[i]==1)
{
mp[u[i]][0]++;
mp[u[i]][mp[u[i]][0]]=v[i];
mpv[u[i]][mp[u[i]][0]]=i;
mp[v[i]][0]++;
mp[v[i]][mp[v[i]][0]]=u[i];
mpv[v[i]][mp[v[i]][0]]=i;
}
}
void delete_tree()
{
long long i;
for(i=1;i<=n;i++)
{
delete [] mp[i];
delete [] mpv[i];
}
}
void dfs(long long x)
{
long long i;
deep[x]=deep[fa[x]]+1;
for(i=1;i<=mp[x][0];i++)
if(mp[x][i]!=fa[x])
{
fa[mp[x][i]]=x;
fv[mp[x][i]]=mpv[x][i];
dfs(mp[x][i]);
}
}
long long get(long long x,long long y)
{
long long s=0;
while(x!=y)
if(deep[x]>=deep[y])
{
if(w[fv[x]]>s)
s=w[fv[x]];
x=fa[x];
}
else
{
if(w[fv[y]]>s)
s=w[fv[y]];
y=fa[y];
}
return s;
}
void del(long long x,long long y)
{
long long s=-1,ww=0;
while(x!=y)
if(deep[x]>=deep[y])
{
if(w[fv[x]]>s)
{
s=w[fv[x]];
ww=fv[x];
}
x=fa[x];
}
else
{
if(w[fv[y]]>s)
{
s=w[fv[y]];
ww=fv[y];
}
y=fa[y];
}
ok[ww]=0;
}
main()
{
scanf("%lld%lld%lld",&n,&m,&k);
for(i=1;i<=m;i++)
scanf("%lld%lld%lld",&u[i],&v[i],&w[i]);
for(i=1;i<=m-k;i++)
num[i]=i+k;
sort(num+1,num+m-k+1,[](long long a,long long b)->bool{return w[a]<w[b]||w[a]==w[b]&&a<b;});
for(i=1;i<=m-k;i++)
if(in(u[num[i]],v[num[i]]))
{
ans[0]=ans[0]+w[num[i]];
ok[num[i]]=1;
}
for(i=1;i<=k;i++)
{
make_tree();
dfs(1);
det=1000000001;
wh=0;
for(j=1;j<=k;j++)
if(fl[j]==0)
{
tt=w[j]-get(u[j],v[j]);
if(tt<det)
{
det=tt;
wh=j;
}
}
ans[i]=ans[i-1]+det;
del(u[wh],v[wh]);
ok[wh]=1;
fl[wh]=1;
w[wh]=0;
delete_tree();
}
for(i=0;i<=k;i++)
printf("%lld\n",ans[i]);
}
:::
好像少了点什么?
正确性证明去哪里了?
为了证明上述贪心做法的正确性,我们引入本题的 wqs 二分做法。
wqs 二分
首先我们需要使得答案是一颗生成树,于是我们需要把关键边连出的每个连通块都先求一次最小生成树,不在最小生成树上的关键边一定不会出现在生成树中。
这是因为,如果不在最小生成树上的关键边出现在了生成树,那么就说明这条关键边两端点在最小生成树上的路径不全在生成树上,因为树上不会有环。
根据最小生成树的性质,这条关键边两端点在最小生成树上的路径中的每条边权值都不大于这条边,并且我们一定可以找到一条路径上的边来代替这条关键边使得我们的生成树的权值和最小,所以不在最小生成树上的关键边不可能出现在生成树中。
于是将不在最小生成树上的关键边最后考虑,现在我们的关键边不成环,答案就一定是一棵生成树。
考虑将每条关键边的权值增加
:::warning[提示]
这里出现的
观察我们在最小生成树中选择的关键边,不难发现,如果在
因为如果原图中还有边权和更小的选择恰好
我们注意到一点,
我们只要对于每个
如果你真的要写这个做法,有几个细节需要注意:
- 可以在忽略关键边的前提下先求出最小生成树,只保留在最小生成树上的非关键边,可以把边数从
O(m) 条压缩到O(n) 条(这里认为我们O(k)<O(n) )。这样复杂度才会是O(nk\log V) ,才可以过。 - 答案可能是非完全凸的,也就是说,可能有一些
i 二分不出答案,使用各种方式都可以处理此问题。(比如在最后再将线性变化的数值填入答案数组中) - 记得把不在最小生成树上的关键边加入答案,显然这些关键边可以将边权从小到大排序,然后就只需要执行两个凸包的合并。
代码没写。
好像又少了点什么?
正确性证明又去哪里了?(而且这个做法乍看之下也和上面的贪心没有联系)
接下来我们来证明 wqs 二分是对的。
证明
一般情况下,我们只需要证明答案是凸的(比如在本题中,只需证明答案关于关键边边数
但是当你真的试图开始证明答案是凸的,你很可能会遇到麻烦,于是我们考虑放弃定式思维。我们可以直接证明 wqs 二分是对的,通过 wqs 二分的正确性反推出答案的凸性。(因为最后合并生成树上的关键边和不在生成树上的关键边时也要用到答案的凸性,所以还是需要证明答案是凸的)
考虑
现在我们考虑将
首先在这里插入一个引理:字典序最小的生成树是最小生成树,并且最小生成树(按边权排序后)是字典序最小的生成树,这里的字典序是按照边权为唯一关键字排序的。粗略的证明就是 Kruskal 算法,因为 Kruskal 算法求出的会是一个字典序最小的生成树。
:::info[更严格的证明]
细心的人可能已经发现:Kruskal 算法能求出字典序最小的生成树,但是这里的“字典序”是按照边权为第一关键字,和一个随机顺序(比如边的编号)为第二关键字排序的,并非按照边权为唯一关键字排序的。
所以我们放弃考虑 Kruskal 算法,去寻找一个更有效的数学建模。
我们考虑用拟阵进行建模。别被吓跑了,接下来我会说人话的。
我们实际上就是需要在一个连通图上选出一些边,使得这些边不形成环。如果我们选择了尽可能多的边,那么就可以得到一个最小生成树。
我们依次检查拟阵的三个性质:
- 空集合法,这是显然的。
- 在一个合法的集合中选取一个子集仍然合法,这也是显然的。
- 对于一个合法的集合
A 和一个合法的集合B ,如果集合A 的大小大于集合B 的大小,那么一定存在一个元素s 属于A ,使得s 不属于B ,并且把s 加入B 以后得到的新集合仍然合法,这并不是很显然,所以我们接下来考虑先证明此性质。
考虑一个无环的边集
不难发现
于是我们成功将问题建模成了拟阵,对于拟阵模型,有一个经典结论:字典序最小的极大合法集合是所有极大合法集合中总和最小的,所有极大合法集合中总和最小(按权值排序后)就是字典序最小的极大合法集合。(极大合法集合就是不为任意一个合法集合的真子集的合法集合)
要证明这个经典结论,需要先证明所有极大合法集合的元素个数相同。
这点比较容易,设
于是接下来取字典序最小的极大合法集合为
考虑取出
所以除了字典序最小的极大合法集合,其他的极大合法集合权值都不可能最小,所以字典序最小的极大合法集合权值就是最小的。
我们就成功证明了上述结论。
:::
有了这个结论,我们就可以不去观察生成树的边权和而是去观察生成树的字典序。
考虑我们正要选上第一条关键边的时刻,此时我们正要全责一条关键边
考虑将
这只能是因为
于是我们把
我们发现实际上不只有第一条关键边加入时的情形是这样,每条关键边加入时都是类似的。
值得注意的是,在同一个时刻,可能有多条关键边同时加入,因为这些边之间互相独立,我们可以控制这些边部分加入来得到想要的关键边数,因为此时关键边和对应被替换的非关键边权值相同。
所以 wqs 二分是有效的,因为我们二分的序列是单调的,也就是可二分的。又由于 wqs 二分只能在答案凸的时候有效,所以我们的答案也只能是凸的。
现在,wqs 二分的正确性证完了,然后呢?
贪心的正确性去哪里了?
其实我们上面已经证过了:将
唯一的区别在于贪心时我们也考虑了不在生成树上的边,由于答案是凸的,我们实际上就是在贪心的过程中做了凸包的合并,这也是对的。
所以贪心的正确性也就证完了。
::::success[Bonus:关于时间复杂度的优化]{open}
首先需要注意,这一部分专注于理论复杂度的优化,我并没有实际去写代码。
如果有人得出了比我更优的复杂度也欢迎在评论区踢我。
以下默认所有关键边都在生成树上,可以用最小生成树算法快速剔除不在生成树上的关键边,并用凸包合并将两部分的答案合并起来,这些的复杂度都不会超过
:::warning[提示]
以下分析复杂度时默认
你也可以理解为我们在复杂度里写的
:::
(图片上展示的是本题另一篇题解的第一条评论)
我们要做到优于这个复杂度,也就是要优于
首先观察我们的暴力贪心的复杂度:
首先很容易发现一个瓶颈:我们在最开始需要求解一次最小生成树。
常见的算法的复杂度都超过了我们要求的复杂度:Kruskal 算法即便使用基数排序,复杂度也只能达到
是时候使用科技了:我们可以使用随机化线性时间最小生成树算法,也就是 KKT 算法,论文贴在这里,感兴趣的人可以去了解一下。在本文中,我们不会详细介绍 KKT 算法的算法流程和复杂度证明等,我们只把这个算法当做黑盒来用。
一些你需要知道的信息:KKT 算法的空间复杂度为
我们直接把 KKT 用到暴力贪心上,于是得到期望复杂度为
很明显,我们每次需要断一条边,加一条边并保证图还是一棵数,查询两点间边权的最大值,这明显就是动态树问题,直接用 LCT 就可以把期望复杂度做到
现在怎么才能继续优化呢?考虑到动态树问题非常困难,我们考虑此题中动态树问题的特殊性质。
我们发现,我们的问题无法离线下来做,因为每次加哪条边,删哪条边只有做完了当前需要做的查询操作才能得知。但是经过观察,我们可以求出我们所有操作中需要删除的边集。
考虑将所有的关键边的边权都设为
考虑将关键边全部去除,再将剩余的
考虑到我们只会删除在查找过程中能找到的边权最大的边,于是我们可以把除了这
接下来需要冷静思考,不要想歪然后试图去解决经典问题。
:::error[想歪了的后果]
这一部分是无法将复杂度优化达标的。
我们容易发现此题中,我们一共需要
接下来我们需要解决一个经典问题:查询树上路径最大值,允许离线。
在我的认知范围内,如果
关于
Update:
fydj 得出了复杂度为
然而这样做以后期望复杂度为
:::
我们考虑使用 Kruskal 重构树来进行树上路径权值的最大值。
首先一开始我们需要先花费不超过
接下来对于每一次删边加边操作,其实就是将树上的两个点之间的权值最大的边删除,然后再在两个点之间连接一条权为
我们知道,两点之间路径的权值最大值就是 Kruskal 重构树上两点最近公共祖先的权值,问题转化为线性预处理,常数时间求解两点的最近公共祖先。
这是一个经典问题,容易将其转化为树的欧拉序上的 RMQ 问题,并使用加减 1RMQ(或基于状压的线性 RMQ 算法)达成目标。
由于我们只有
最后总的期望时间复杂度就是
这个做法的空间复杂度为
::::
:::info[后记]
「人の正義もどうせ人の数だけあるんだろう」
秘密
:::