20260629 - 最小生成树 总结
我怎么又提前下班了!老师你快加题啊!
今天提前下班有点早啊。我咋才做了 \(80\) 分钟就下班了。
敢问 xrc 现在每天都在干什么,倒着抢首 A 制造集体恐慌吗。(
老师老师你在还剩 \(50\) 分钟的时候加一个防 AK 的 2400 是和一位啊,不是哥们现在已经有四个人 AK 了好嘛。。。
最小生成树分两种算法,一个是 Kruskal 算法,一个是 Prim 算法。其中 Kruskal 算法较为常用,通常适用于稀疏图(\(n\) 与 \(m\) 同阶),时间复杂度 \(O(m \log m)\),瓶颈在于排序;Prim 算法则与之相反,其适用于稠密图(\(n^2\) 与 \(m\) 同阶),时间复杂度 \(O(n^2)\)。
我绝不会告诉你我总是把 Prim 算法打成 Prime,绝不是因为我喜欢质数,绝对不是!
具体的做法在这里不是很想展开讲了,反正老师今天也没讲课我在这里写写写写啥啊。
所以直接讲题吧。
A - 最小生成树
Tag:最小生成树,Kruskal 算法。
最小生成树板子,没啥好说的,随便扔个 Kruskal 上去做一下就行了。
时间复杂度:\(O(m \log m)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e5+5;
struct lines{int x,y,w;}ln[N];
int n,m,fa[N],Ans,cnt;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read(),m=read();
Ans=0,cnt=0;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)
ln[i].x=read(),ln[i].y=read(),ln[i].w=read();
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y,Ans+=w,cnt++;
}
if(cnt==n-1)cout<<Ans<<"\n";
else cout<<"orz\n";
return 0;
}
B - 繁忙的都市
Tag:最小生成树,Kruskal 算法。
题面叭叭叭了一坨废话,最后就是让你建一个树出来使其最大边权最小,简单,还是板子 Kruskal,把前面那题代码贺过来再稍微改改就行了。
时间复杂度:\(O(m \log m)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e5+5;
struct lines{int x,y,w;}ln[N];
int n,m,fa[N],Ans;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read(),m=read();
Ans=0;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++)
ln[i].x=read(),ln[i].y=read(),ln[i].w=read();
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y,Ans=max(Ans,w);
}
cout<<n-1<<" "<<Ans<<"\n";
return 0;
}
C - Building Roads S
Tag:最小生成树,Kruskal 算法,Prim 算法。
罚时原因:
- 贺了前一题的代码然后没改数组大小,喜提 RE。(
这个题目说给你一堆坐标上的点,并且它们之间已经有某些点之间连了边了,问你至少再加长度总和为多少的边能使整个连通。
那你把并查集初始化那里稍微改改,一个 Kruskal 板子就过掉了。
应该也能用 Prim 做,也许会快一点吧,但是我太懒了不想再敲一个板子就没写了。
时间复杂度:\(O({n^2} \log {n^2})\),用 Prim 的话是 \(O(n^2)\) 的。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e6+5;
struct lines{LL x,y;double w;}ln[N];
LL n,m,a[N],b[N],fa[N];double Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
double Dist(int x,int y){
double res=(a[x]-a[y])*(a[x]-a[y])+(b[x]-b[y])*(b[x]-b[y]);
return sqrt(res);
}
int main(){
n=read(),m=read();
Ans=0;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=n;i++)
a[i]=read(),b[i]=read();
for(int i=1;i<=m;i++){
LL x=read(),y=read();
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y;
}
m=0;
for(int i=1;i<n;i++)
for(int j=i+1;j<=n;j++)
if(FF(i)!=FF(j))ln[++m]={i,j,Dist(i,j)};
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
int xx=x,yy=y;
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y,Ans+=w;
}
printf("%.2lf\n",Ans);
return 0;
}
D - 买礼物
Tag:最小生成树,Kruskal 算法,Prim 算法。
罚时原因:
- 没注意到如果 \(K_{i,j} = 0\) 表示之间没有连边。
- 把 \(K_{i,j} = 0\) 的边删掉了导致图可能不连通,应该把 \(K_{i,j} = 0\) 视作 \(K_{i,j} = A\)。
发现题目这个说法,其实就是要你在这个完全图中选一棵树出来,答案就是这棵树的所有边权之和加上 \(A\),因为你还需要一个起点。那么这就是板子最小生成树了,就不多说了,我实现采用的是 Kruskal 别问我为什么。
主要是注意对 \(K_{i,j}\) 边权的处理,因为题目有两种特殊情况,当 \(K_{i,j} = 0\) 或 \(K_{i,j} > A\) 时都应该让 \(K_{i,j} = A\) 确保完全图结构正确。
时间复杂度:用 Kruskal 的话是 \(O({n^2} \log {n^2})\) 的,用 Prim 是 \(O(n^2)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e5+5;
struct lines{int x,y,w;}ln[N];
int A,n,m,fa[N],Ans,cnt;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
A=read(),n=read();
Ans=0;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int x=read();
if(i>=j)continue;
if(x>A||!x)x=A;
ln[++m]={i,j,x};
}
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y,Ans+=w;
}
cout<<Ans+A<<"\n";
return 0;
}
E - Built?
Tag:最小生成树,Kruskal 算法,重构边,建图。
全场最有思维含金量的一题——!没有之一,毕竟其他题目都太显然了呵。
发现直接建图去做是会时空双炸的,咋办,我们只能想点法子优化建图。
注意到建图的代价是 \(\min(|x_1 - x_2|,|y_1 - y_2|)\),注意这里是 \(\min\),由于我们要跑的就是最小生成树,那么这个 \(\min\) 的操作其实可以在建树的过程中体现出来。
于是我们成功将一条边拆成了两条边,额,这是不是加大难度?并非,你观察 \(|x_1 - x_2|\),那最优的建图当然就是对两个差值最小的 \(x_1\) 和 \(x_2\) 建了,于是我们对所有点按照 \(x\) 值升序排序,相邻两个建边即可。\(y\) 值同理。
于是最后你就收获了 \(2(n-1)\) 条真正有效的边,拿去跑 Kruskal 就行了。
时间复杂度:\(O(n \log n)\),有一个 \(6\) 左右的常数。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e5+5;
struct node{int x,y,id;}a[N];
struct lines{LL x,y,w;}ln[N];
LL n,m,fa[N],Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmpx(node d1,node d2){return d1.x<d2.x;}
bool cmpy(node d1,node d2){return d1.y<d2.y;}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read();
for(int i=1;i<=n;i++)
a[i].x=read(),a[i].y=read(),a[i].id=i;
sort(a+1,a+n+1,cmpx);
for(int i=1;i<n;i++)
ln[++m]={a[i].id,a[i+1].id,a[i+1].x-a[i].x};
sort(a+1,a+n+1,cmpy);
for(int i=1;i<n;i++)
ln[++m]={a[i].id,a[i+1].id,a[i+1].y-a[i].y};
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y,Ans+=w;
}cout<<Ans<<"\n";
return 0;
}
F - Sum of Maximum Weights
Tag:最小生成树,Kruskal 算法。
你考虑为什么对于一条边的边权 \(w\) 能有 \(f(i,j) = w\),这说明在加入边权为 \(w\) 的边之前,也就是在只有边权 \(<w-1\) 的边之前,\(i,j\) 是不连通的。
欸那不是好做了,还是一个 Kruskal,然后多维护点东西,维护一下每个集合的大小 \(sz_i\)。你在加入一条边 \(u \to v\) 时,答案增多 \(sz_u \times sz_v \times w\),求和即可。
时间复杂度:\(O(n \log n)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e5+5;
struct lines{LL x,y,w;}ln[N];
LL n,fa[N],sz[N],Ans;
vector<int> g[N];
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read();
for(int i=1;i<n;i++)
ln[i].x=read(),ln[i].y=read(),ln[i].w=read();
for(int i=1;i<=n;i++)fa[i]=i,sz[i]=1;
sort(ln+1,ln+n,cmp);
for(int i=1;i<n;i++){
auto [x,y,w]=ln[i];
x=FF(x),y=FF(y);
if(x==y)continue;//按理来说不存在
Ans+=sz[x]*sz[y]*w;
fa[x]=y,sz[y]+=sz[x],sz[x]=0;
}
cout<<Ans<<"\n";
return 0;
}
G - Choose Two and Eat One
Tag:最小生成树,Kruskal 算法,Prim 算法,快速幂。
显然就是一个板子啊,除了需要写个快速幂以外毫无难度啊,Kruskal 和 Prim 都能解决,我还是用的 Kruskal。
所以凭啥放 G 啊,它配吗它配吗它配吗,不是,F 都比它难啊(无语。
时间复杂度:\(O({n^2} \log {n^2})\),用 Prim 就是 \(O(n^2)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 3e5+5;
struct lines{LL x,y,w;}ln[N];
LL n,m,MOD,a[N],fa[N],Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
LL QP(LL x,LL y){
LL as=1;
while(y){
if(y&1)as=as*x%MOD;
x=x*x%MOD,y>>=1;
}return as;
}
bool cmp(lines l1,lines l2){return l1.w>l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read(),MOD=read(),m=0;
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<n;i++)
for(int j=i+1;j<=n;j++){
LL val=QP(a[i],a[j])+QP(a[j],a[i]);
ln[++m]={i,j,val%MOD};
}
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y,Ans+=w;
}
cout<<Ans<<"\n";
return 0;
}
H - Shichikuji and Power Grid
Tag:最小生成树,Kruskal 算法,Prim 算法。
实现有点恶心,但应该是我的做法问题,emm 因为还能加虚点做 Prim,好像简单一些欸。
以下讲解我的做法。
首先把所有位置都当作发电机求一个答案出来。然后对边排序,还是正常跑 Kruskal,但是此时并查集还需要维护一个东西,就是当前集合中作为发电机的最优代价(取 \(\min\)),这个在维护并查集的时候同步更新即可。
那么对于一条边,绝对不是只要不连通就连边了,因为全连通不一定最优,所以还需要判断一下。
考虑当前让 \(x\) 和 \(y\) 两个集合连边(边权为 \(w\)),\(val\) 是发电机代价数组,那么连边时,对答案的影响就是 \(ans \gets ans - \max(val_x,val_y) + w\)。欸,为什么是 \(\max\) 呀?因为我们为了最优肯定是保留 \(\min\),所以删去 \(\max\)。
于是连边的时候再额外判断一下是否 \(\max(val_x,val_y) \ge w\) 即可。
至于输出方案,这个随便维护一下就行。
时间复杂度:\(O({n^2} \log {n^2})\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2005;
const int M = 4e6+5;
struct node{LL x,y;}a[N];
struct lines{LL x,y,w;}ln[M];
LL n,m,c[N],k[N];
LL fa[N],val[N],id[N],Ans,cnt;
vector<node> lian;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read();
for(int i=1;i<=n;i++)
a[i].x=read(),a[i].y=read();
for(int i=1;i<=n;i++)c[i]=read();
for(int i=1;i<=n;i++)k[i]=read();
for(int i=1;i<n;i++)
for(int j=i+1;j<=n;j++){
LL dis=abs(a[i].x-a[j].x)+abs(a[i].y-a[j].y);
ln[++m]={i,j,dis*(k[i]+k[j])};
}
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=n;i++)
fa[i]=i,val[i]=c[i],id[i]=i,Ans+=c[i];
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
int xx=x,yy=y;
x=FF(x),y=FF(y);
if(x==y)continue;
LL tt=max(val[x],val[y]);
if(tt>=w){
fa[x]=y,Ans=Ans-tt+w;
lian.pb({xx,yy});
id[y]=(val[y]<val[x]?id[y]:id[x]);
val[y]=min(val[y],val[x]);
}
}
cout<<Ans<<"\n";
for(int i=1;i<=n;i++)if(FF(i)==i)cnt++;
cout<<cnt<<"\n";
for(int i=1;i<=n;i++)
if(FF(i)==i)cout<<id[i]<<" ";cout<<"\n";
cout<<lian.size()<<"\n";
for(auto [x,y]:lian)cout<<x<<" "<<y<<"\n";
return 0;
}
I - MinOr Tree
Tag:最小生成树,Kruskal 算法,二进制,拆位。
根据二进制的特性,如果保证了第 \(i\) 位为 \(0\),那么即使往后的 \(i-1,i-2,\dots,0\) 都为 \(1\),也比第 \(i\) 位为 \(1\) 优。
于是倒着枚举二进制位,考虑所有边权在当前位为 \(0\) 的边,加在一起跑 Kruskal 判断是否能全图连通。如果能,那么这个位置就可以赋值为 \(0\);如果不能,那么这个位置就只能含泪赋 \(1\) 了。注意每次还要更新当前还能用的边,比如上一步铁定了上一位是 \(0\),那么接下来就只能考虑上一位是 \(0\) 的那些边了。
时间复杂度:\(O(n \log n \log V)\),事实上不需要跑 Kruskal(见双倍经验)因此时间复杂度可以做到更优 \(O(n \log V)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e5+5;
struct lines{int x,y,w;}ln[N],tmp[N];
int T,n,m,m2,fa[N],Ans;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w<l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
bool K_build(){
for(int i=1;i<=n;i++)fa[i]=i;
sort(tmp+1,tmp+m2+1,cmp);
for(int i=1;i<=m2;i++){
auto [x,y,w]=tmp[i];
x=FF(x),y=FF(y);
if(x!=y)fa[x]=y;
}
for(int i=1;i<=n;i++)
if(FF(i)!=FF(1))return 0;
return 1;
}
int main(){
T=read();
while(T--){
n=read(),m=read(),Ans=0;
for(int i=1;i<=m;i++)
ln[i].x=read(),ln[i].y=read(),ln[i].w=read();
for(int i=29;i>=0;i--){
m2=0;
for(int x=1;x<=m;x++)
if(!((ln[x].w>>i)&1))tmp[++m2]=ln[x];
if(K_build()){
m=m2;
for(int x=1;x<=m;x++)ln[x]=tmp[x];
}else Ans+=(1<<i);
}cout<<Ans<<"\n";
}
return 0;
}
J - 货车运输
Tag:最小生成树,Kruskal 算法,重构树,倍增,LCA。
罚时原因:
- 误以为图一定是连通的,事实上可能不连通。
你首先贪心一下,把每个连通子图的最大生成树搞出来,对,用 Kruskal 算法,把树重构一下。这个贪心显然是对的,也很好证。
然后你发现在树上求就方便多了啊,搞个 LCA 和类似 LCA 的倍增数组维护一下边权 \(\min\) 值,最后求 LCA 的同时求一下经过的所有边的边权的 \(\min\) 值即可得到答案。
记得判一下 \(x\) 和 \(y\) 不连通的情况,以及最后得到的图是森林而不是一棵完整的树!!
时间复杂度:\(O(m \log m + n \log n + Q \log n)\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e5+5;
struct lines{int x,y,w;}ln[N];
int n,m,fa[N],Q;
int dep[N],f[N][20],mnv[N][20];
vector<pii> g[N];
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(lines l1,lines l2){return l1.w>l2.w;}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
void DFS_init(int u,int Fa){
dep[u]=dep[Fa]+1;
for(int i=0;f[u][i];i++)
f[u][i+1]=f[f[u][i]][i],
mnv[u][i+1]=min(mnv[u][i],mnv[f[u][i]][i]);
for(auto [v,w]:g[u])if(v^Fa)
f[v][0]=u,mnv[v][0]=w,DFS_init(v,u);return;
}
int sol(int x,int y){
if(FF(x)!=FF(y))return -1;
if(dep[x]<dep[y])swap(x,y);
int ans=1e6;
for(int i=19;i>=0;i--)
if(dep[f[x][i]]>=dep[y])
ans=min(ans,mnv[x][i]),x=f[x][i];
if(x==y)return ans;
for(int i=19;i>=0;i--)
if(f[x][i]!=f[y][i])
ans=min(ans,mnv[x][i]),
ans=min(ans,mnv[y][i]),
x=f[x][i],y=f[y][i];
ans=min(ans,mnv[x][0]),ans=min(ans,mnv[y][0]);
return ans;
}
int main(){
n=read(),m=read();
for(int i=1;i<=m;i++)
ln[i].x=read(),ln[i].y=read(),ln[i].w=read();
sort(ln+1,ln+m+1,cmp);
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++){
auto [x,y,w]=ln[i];
int xx=x,yy=y;x=FF(x),y=FF(y);
if(x!=y)fa[x]=y,g[xx].pb({yy,w}),g[yy].pb({xx,w});
}
for(int i=1;i<=n;i++)
if(!f[i][0])DFS_init(i,0);
Q=read();
while(Q--){
int x=read(),y=read();
cout<<sol(x,y)<<"\n";
}
return 0;
}
K - Spanning Tree Queries
这里有一个口胡版本,正确做法版本在分割线之后。
老师在最后一个小时加了一个 2400,好家伙,这显然不是我能做的东西啊,尝试了一下发现很难,于是滚回来把总结补到了 J,好家伙,滚回去胡了一下,胡出来一个看起来很真的做法,哦↗哦↘哦↗哦↘哦↗哦↘哦↘哦↗
首先你考虑一个边权 \(w\) 只有在 \(x \le w\) 和 \(x > w\)(等号放哪边都无所谓)两个是不同的情况,那么把所有 \(m\) 条边的 \(w\) 扔到一起,得到一堆分割线和一堆区间。在这一个区间中,只要知道其中的一个答案,其他的就完全可以通过偏移一定数值求出来,是 \(O(1)\) 的。
那么得到的区间数量是 \(O(m)\) 级别的,对这每个区间的任意一个点,比如左端点、右端点或者中间值,做一次 Kruskal 求出答案,一个区间就取样一次,把答案求出来就行。一定要存下来。注意这里的区间包括最后面的无限延长区间,也需要取个样算一下。
然后接着你再处理整个 \(k\) 次查询,此时你根据当前这次询问的 \(x\) 值找到其对应的区间,拿到其区间样本的答案,然后偏移计算就能得到答案了。
你不觉得很真吗 .jpg
时间复杂度,我算一下,大概是 \(O(m^2 \log m + k \log m)\) 的,都不到 \(10^8\) 啊,题目给 \(4\) 秒干嘛?还是说假了?
时间不够了懒得写代码了,等老师讲完题验证一下做法正确性再写代码。
Tag:最小生成树,Kruskal 算法,交点,区间划分,预处理,偏移量,二分查找。
其实我的口胡真了 \(80\%\),但我还是决定来再重新写一下。
你去考虑把与 \(X\) 的绝对值这个图像画到二维平面上,大概是一个“V”形的图,那么任意两个 \(w\) 所形成的图像一定是有且仅有一个交点的,这个交点就是 \(\frac{w_1 + w_2}{2}\)。当然,你还需要考虑图像与 \(x\) 轴的交点,这个交点就是 \(w\) 啦。
然后你看这些交点,一共有 \(O(m^2)\) 个交点,你把所有交点排序一看,哦,一个区间内的整个 \(m\) 条边它的顺序其实是确定的。怎么说呢,就是你不需要在每次给出 \(X\) 时对 \(m\) 条边去排序了,它顺序已经排好了。你可以把这个 \(O(m^2)\) 种情况预处理排序一下,这样复杂度就是 \(O(m^3 \log m)\)。
但是这样还是过不去啊,你也不支持对于每次询问跑一个 \(O(\alpha(n) m)\) 的并查集,还是会 T,怎么办,我能不能提前把并查集的答案也求出来啊?当然可以啦,但是求出来之后一个区间内的答案可是不一样的,咋办?一个一个预处理肯定不行,但我们发现它其实是一个等差数列,也就是我们只需要考虑相邻两个的差值即可。也就是说等价于,你在区间里取一个样本做一次 Kruskal,然后同步算出差值(偏移量),就可以 \(O(1)\) 得到这个区间内其他值的答案了。
于是预处理的时间复杂度还是 \(O(m^3 \log m)\)(因为还是排序所消耗的时间更多,并查集可以理解为带来了一层 \(2\) 的常数),但是处理 \(k\) 个查询的时候,你只需要在 \(O(m^2)\) 个区间中找到当前 \(X\) 所属区间,然后根据样本答案和偏移量算出当前答案了。
实现上还是有点细节的要注意一下,我调了十个世纪。
时间复杂度:\(O(m^3 \log m + k \log {m^2})\)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int M2 = 1e5+5;
struct lines{LL u,v,w;}ln[305],tmp[305];
struct tmpans{LL res,bas;}ans[M2];
LL n,m,dx[M2],cnt,X,fa[55];
LL p,k,A,B,C,Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmpln(lines l1,lines l2){return l1.w<l2.w;}
bool cmpx(lines l1,lines l2){
if(abs(l1.w-X)==abs(l2.w-X))return l1.w>l2.w;
return abs(l1.w-X)<abs(l2.w-X);
}
int FF(int u){return (fa[u]==u?u:fa[u]=FF(fa[u]));}
int main(){
n=read(),m=read();
for(int i=1;i<=m;i++)
ln[i].u=read(),ln[i].v=read(),ln[i].w=read();
sort(ln+1,ln+m+1,cmpln);
dx[++cnt]=0;
for(int i=1;i<=m;i++){
dx[++cnt]=ln[i].w+1;
for(int j=1;j<=m;j++)
dx[++cnt]=(ln[i].w+ln[j].w+1)/2;
}
sort(dx+1,dx+cnt+1);
cnt=unique(dx+1,dx+cnt+1)-dx-1;
for(int tt=1;tt<=cnt;tt++){
X=dx[tt];//取出临时 x 求值
for(int i=1;i<=m;i++)tmp[i]=ln[i];
sort(tmp+1,tmp+m+1,cmpx);
for(int i=1;i<=n;i++)fa[i]=i;
LL res=0,bas=0;
for(int i=1;i<=m;i++){
auto [x,y,w]=tmp[i];
x=FF(x),y=FF(y);
if(x==y)continue;//无法连边
if(w<X)res-=w,bas++;
else res+=w,bas--;
//变动基础值和变化值
fa[x]=y;//连边
}ans[tt]={res,bas};//记录答案
}
p=read(),k=read();
A=read(),B=read(),C=read();
X=0,Ans=0;
for(int i=1;i<=k;i++){
if(i<=p)X=read();
else X=(X*A+B)%C;
int id=upper_bound(dx+1,dx+cnt+1,X)-dx-1;
LL nowans=ans[id].res+ans[id].bas*X;
Ans^=nowans;
}
cout<<Ans<<"\n";
return 0;
}
总结
感觉最小生成树的运用还是很广的,不过前面的题好简单啊。
最后一题好难啊不愧是 2400 啊。不过质量确实高的 /qiang,这个思维很巧。
我不管,反正总结好吃。

浙公网安备 33010602011771号