一.最小生成树
前言
- 在一个含有 n n n 个节点的无向图中,找到一棵含有 n n n 个节点的树,且该树边权总和最小
kruskal 算法学习
- 算法描述:kruskal 算法,基于贪心。对所有边从小到大排序,并依次使用小的边去将图上的点连接起来(构成环则不连),总时间复杂度: O ( m l o g m ) O(mlogm) O(mlogm)
- 性质:
- 边权和最小
- 就是最大的边最小。‘
模板代码
int find(int x) {
if(x==fa[x])return x;
else return fa[x]=find(fa[x]);
}
int cmp(ppp x,ppp y) {
return x.c>y.c;
}
int main() {
int n,m,u,v,c;
cin>>n>>m;
for(int i=1; i<=m; i++) {
cin>>u>>v>>c;
add(u,v,c);
}
for(int i=1; i<=n; i++)fa[i]=i;
sort(e+1,e+k+1,cmp);
int cnt=1,dissum=0;
for(int i=1; i<=k; i++) {
int p1=find(e[i].u),p2=find(e[i].v);
if(p1!=p2){
fa[p1]=p2;
cnt++;
dissum+=e[i].c;
if(cnt>=n)break;
}
}
}
prim 算法学习
- 算法描述: 开始时任意选择一个点作为起点,每次操作:从剩下未选择的点构成的点集中,找到一个与已选择点距离最小的点 v v v,将 v v v 加入已选择的点集中,并添加边。总时间复杂度: O ( n 2 ) O(n^2) O(n2)
模板代码:
void prim(){
for(int i=1;i<=n;i++)dis[i]=inf;
dis[1]=0;
int ans=0;
for(int i=1;i<=n;i++){
int minn=inf,pos=0;
for(int j=1;j<=n;j++){
if(vis[j]==0&&dis[j]<minn){
minn=dis[j];
pos=j;
}
}
vis[pos]=1;
ans+=minn;
for(int j=1;j<=n;j++){
if(vis[j])continue;
if(dis[j]>dist(pos,j))dis[j]=dist(pos,j);
}
}
}
例题
例题1:求最近距离最远的 d i s ( s , t ) dis(s,t) dis(s,t)
- 题目描述: 无向连通图中有 n 个点,求最近距离最远的 d i s ( s , t ) dis(s,t) dis(s,t)
- 问题分析: 无向连通图中建一个最大生成树,求最大生成树的直径即可
例题2:k 条一线道路
- 题目描述: n 个点,m 条道路,每条道路可以选择建设一线道路,花费 c i c_i ci;或者建设二线道路,花费 d i d_i di ,且保证 c i > = d i c_i>=d_i ci>=di 。要求从 m m m 条道路中选择 n − 1 n-1 n−1 条道路,使得所有点均互相可达,且至少选择 k k k 条一线道路。求选择所有道路的最大花费最小为多少。
- 问题分析: 将每条道路拆分成两条路,花费分别为 c i , d i c_i,d_i ci,di 。先用 kruskal 从 c c c 中选择 k k k 条一线道路,再对剩余的 c c c 以及 d d d 中选择 n − 1 − k n-1-k n−1−k 条即可。注意:同一条道路只能选一次。
核心代码:
void kruskal(){
int maxans=-1e9;
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m;i++){
int p1=find(e1[i].u),p2=find(e1[i].v);
if(p1!=p2){
fa[p1]=p2;
cnt++;
ans[++top].x=e1[i].id;
ans[top].y=1;
vis[e1[i].id]=1;
maxans=max(maxans,e1[i].w);
}
if(cnt==k+1)break;
}
for(int i=1;i<=m;i++){
if(vis[e2[i].id])continue;
int p1=find(e2[i].u),p2=find(e2[i].v);
if(p1!=p2){
fa[p1]=p2;
cnt++;
ans[++top].x=e2[i].id;
ans[top].y=2;
maxans=max(maxans,e2[i].w);
}
if(cnt==n)break;
}
cout<<maxans<<endl;
for(int i=1;i<=top;i++)cout<<ans[i].x<<" "<<ans[i].y<<endl;
}
例题3:一条 最 大 值 最 小 值 \frac{最大值}{最小值} 最小值最大值最小的道路
- 题目描述: 给定一个 n 个点,m 条边,有边权 w i w_i wi 的图。找到 s s s 到 t t t 中, 最 大 值 最 小 值 \frac{最大值}{最小值} 最小值最大值最小的路径,求得该比值。 n < = 500 , w i < = 30000 , m < = 5000 n<=500,w_i<=30000,m<=5000 n<=500,wi<=30000,m<=5000
- 问题分析: 排个序,枚举最小边 i i i,kruskal 求出 s , t s,t s,t 有通路的最小边 j j j,check 一下比值即可。
核心代码:
void kruskal(){
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++)fa[j]=j;
for(int j=i;j<=m;j++){
int p1=find(e[j].u),p2=find(e[j].v);
if(p1!=p2)fa[p1]=p2;
if(find(s)==find(t)){
check(e[j].w,e[i].w);
break;
}
}
}
if(ansfz==1e9)cout<<"IMPOSSIBLE";
else if(ansfm==1)cout<<ansfz;
else cout<<ansfz<<"/"<<ansfm;
}
例题4:一棵完全图中找到一棵最小生成树
- 题目描述: 一个平面图上有 n n n 个顶点,每个点的坐标分别为 ( x i , y i ) (x_i,y_i) (xi,yi),求一棵最小生成树,输出最小边权和。 n < = 5000 n<=5000 n<=5000
- 问题分析: prim 板子,时间复杂度 O ( n 2 ) O(n^2) O(n2)。【注意】用 kruskal 需要 O ( n 2 l o g n ) O(n^2logn) O(n2logn),prim 的优越性体现出来了。
例题5:双限制
- 题目描述: n n n 个点,每个点都有高度 h i h_i hi。 m m m 条无向边 ( u , v , w ) (u,v,w) (u,v,w) 。从 1 1 1 号结点出发,每次不能走到当前高度高的点,但是可以回到已走过的点(瞬移)。求经过最多个结点的前提下走的最小距离为多少。
- 问题分析: 最多经过结点是固定的,可以先用 d f s dfs dfs ,并打上标记(表示未来要生成一个覆盖所有标记点的生成树)。再使用 kruskal 求最小生成树,以边的终点高度为第一关键字,以边权为第二关键字排序。这样就保证了所遍历的点一定是从高到低遍历的。(不会出现多个起点的情况)
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=1e6+10;
struct E {
int u,v,w,next;
} e[M*2];
int vex[N],tot,n,m,p[N],vis[N],h[N];
void add(int u,int v,int w) {
tot++;
e[tot].u=u;
e[tot].v=v;
e[tot].w=w;
e[tot].next=vex[u];
vex[u]=tot;
}
int cmp(E x,E y) {
if(h[x.v]==h[y.v])return x.w<y.w;
else return h[x.v]>h[y.v];
}
int find(int x) {
if(x==p[x])return x;
else return p[x]=find(p[x]);
}
void dfs(int u) {
if(vis[u])return;
vis[u]=1;
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
dfs(v);
}
}
void kruskal() {
long long cnt=0,ans=0,cnt2=1;
for(int i=1; i<=n; i++) {
if(vis[i]==1)cnt++;
}
sort(e+1,e+tot+1,cmp);
for(int i=1; i<=tot; i++) {
if(!vis[e[i].u]||!vis[e[i].v])continue;
int p1=find(e[i].u),p2=find(e[i].v);
if(p1!=p2) {
p[p1]=p2;
ans+=e[i].w;
if(cnt2==cnt)break;
}
}
cout<<cnt<<" "<<ans;
}
int main() {
int u,v,w;
cin>>n>>m;
for(int i=1; i<=n; i++)p[i]=i;
for(int i=1; i<=n; i++)cin>>h[i];
for(int i=1; i<=m; i++) {
cin>>u>>v>>w;
if(h[u]>h[v])add(u,v,w);
else if(h[u]<h[v])add(v,u,w);
else {
add(u,v,w);
add(v,u,w);
}
}
dfs(1);
kruskal();
}
二.kruskal重构树
算法学习
构造方法:
- 最小生成树的边,作为重构树上的非叶子结点
- 最小生成树的点,作为重构树上的叶子结点
- 当 u , v u,v u,v 连接时,寻找 u u u 所在的树的根 r o o t u root_u rootu,与 v v v 所在的树的根 r o o t v root_v rootv,(这个可以用并查集实现),新增结点 n o d e node node,添加边 ( u , r o o t ) , ( v , r o o t ) (u,root),(v,root) (u,root),(v,root)。
性质:
- 原树上两点之间的边权最大值为 l c a lca lca 的权值
模板代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e5+10;
struct E {
int u,v,w,next;
} e[N*4],e2[M];
int vex[N*2],b[N*2],tot,f[N*2][25],p[N*2];
int node=0;
void add(int u,int v) {
tot++;
e[tot].u=u;
e[tot].v=v;
e[tot].next=vex[u];
vex[u]=tot;
}
int cmp(E x,E y) {
return x.w<y.w;
}
void dfs(int u,int fa) {
f[u][0]=fa;
for(int i=1; i<=20; i++)f[u][i]=f[f[u][i-1]][i-1];
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
if(v==fa)continue;
dfs(v,u);
}
}
int find(int x){
if(x==p[x])return x;
else return p[x]=find(p[x]);
}
int main() {
int n,m,u,v,w;
cin>>n>>m;
for(int i=1; i<=n; i++)p[i]=i;
for(int i=1; i<=m; i++) {
cin>>u>>v>>w;
e2[++tot].u=u;
e2[tot].v=v;
e2[tot].w=w;
}
sort(e2+1,e2+m+1,cmp);
node=n;
tot=0;
for(int i=1; i<=m; i++) {
int p1=find(e2[i].u),p2=find(e2[i].v);
if(p1!=p2) {
node++;
add(p1,node),add(node,p1);
add(p2,node),add(node,p2);
p[p1]=p[p2]=p[node]=node;
b[node]=e2[i].w;
}
}
dfs(node,0);
b[0]=2e9;
}
例题
例题1:kruskal重构树+倍增
- 题目描述: n 个结点 m 条边的一个图,点有点权 a i a_i ai,边有边权 w w w。开始时有价值 k k k。当经过一个点 i i i 后,可以获取它的值 a i a_i ai 即 k + = a i k+=a_i k+=ai,但不可重复获取。当 k k k 大于边权时,才可以走对应的边。 q q q 次询问,每次询问给定起点 s s s 和初始价值 k k k ,求最多可以得到多少的价值。 n , m , q < = 1 e 5 , a i < = 1 e 4 , w i < = 1 e 9 n,m,q<=1e5,a_i<=1e4,w_i<=1e9 n,m,q<=1e5,ai<=1e4,wi<=1e9
- 问题分析:
- 建造 kruskal 重构树,树上任意点的点权为以其为根的树的所有叶子的权值和。对于起点 u u u,不停的让 u u u 往上跳,直到不能跳为止,那么对应的点的点权 + k +k +k 即为答案。
- 优化跳的过程:倍增往上跳,要注意
- 不是直接一次倍增 w [ f [ x ] [ i ] ] < = a [ f [ x ] [ i ] ] + k w[f[x][i]]<=a[f[x][i]]+k w[f[x][i]]<=a[f[x][i]]+k,因为其还不一定能经过 x − > f [ x ] [ i ] x->f[x][i] x−>f[x][i] 之间的所有点。
- 而是多次倍增,每次都以起点 t e m p = x temp=x temp=x 为最大价值,倍增 w [ f [ x ] [ i ] ] < = a [ t e m p ] + k w[f[x][i]]<=a[temp]+k w[f[x][i]]<=a[temp]+k
倍增部分的代码:
int jump(int x,int k) {
while(x!=node) {
int temp=x;
for(int i=20; i>=0; i--) {
if(b[f[x][i]]<=k+a[temp])x=f[x][i];
}
if(x==temp)break;
}
return k+a[x];
}
二.次小生成树
算法学习
定义: 次小生成树,顾名思义就是第二小的生成树,有严格与非严格之分
定理: 如果存在次小生成树,则次小生成树与最小生成树一定只有一条边的差异
算法思路: 求出最小生成树,枚举没有使用过的每一条边
(
u
,
v
,
w
)
(u,v,w)
(u,v,w),来替换原树上的一条边。由于增加一条边必然成环,那么只需删去原最小生成树上
(
u
,
v
)
(u,v)
(u,v) 路径的最大边即可。对于非严格最小生成树:只需删去最大边即可;对于严格最小生成树,若最大边与
w
w
w 相同,则删除次大边,否则删除最大边。PS:以上的增边和删边不用真的增删
算法实现: 由于最小生成树是树型结构,那么路径
(
u
,
v
)
=
(
u
,
r
o
o
t
)
+
(
v
,
r
o
o
t
)
(u,v)=(u,root)+(v,root)
(u,v)=(u,root)+(v,root),因此我们可以倍增求最大值和次大值。
模板代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N=6e5+10,M=6e5+10;
struct E {
int u,v,w,next;
} e[M],e2[M];
int cmp(E x,E y) {
return x.w<y.w;
}
int vex[N],f[N][25],Max[N][25][2],p[N],tot,vis[N],dep[N];
long long sum=0,ans=0;
void add(int u,int v,int w) {
tot++;
e[tot].u=u;
e[tot].v=v;
e[tot].w=w;
e[tot].next=vex[u];
vex[u]=tot;
}
int find(int x) {
if(x==p[x])return x;
else return p[x]=find(p[x]);
}
void update(int &Max0,int &Max1,int temp0,int temp1) { //更新最大值Max0和次大值Max1
if(Max0>temp0) {
Max1=max(Max1,temp0);
} else if(Max0<temp0){
Max1=max(Max0,temp1);
Max0=temp0;
}else{
Max1=max(Max1,temp1);
}
}
void dfs(int u,int fa,int w) {
f[u][0]=fa;
Max[u][0][0]=w;
Max[u][0][1]=-1;
dep[u]=dep[fa]+1;
for(int i=1; i<=20; i++) {
f[u][i]=f[f[u][i-1]][i-1];
Max[u][i][0]=Max[u][i-1][0];
Max[u][i][1]=Max[u][i-1][1];
update(Max[u][i][0],Max[u][i][1],Max[f[u][i-1]][i-1][0],Max[f[u][i-1]][i-1][1]);
}
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
if(v==fa)continue;
dfs(v,u,e[i].w);
}
}
void lca(int x,int y,int w) {
if(dep[x]<dep[y])swap(x,y);
int Max0=-1,Max1=-1;
for(int i=20; i>=0; i--) {
if(dep[f[x][i]]>=dep[y]) {
update(Max0,Max1,Max[x][i][0],Max[x][i][1]);
x=f[x][i];
}
}
if(x!=y) {
for(int i=20; i>=0; i--) {
if(f[x][i]==f[y][i])continue;
update(Max0,Max1,Max[x][i][0],Max[x][i][1]);
update(Max0,Max1,Max[y][i][0],Max[y][i][1]);
x=f[x][i];
y=f[y][i];
}
update(Max0,Max1,Max[x][0][0],Max[x][0][1]);
update(Max0,Max1,Max[y][0][0],Max[y][0][1]);
}
if(w==Max0) {
if(Max1!=-1)ans=min(ans,sum+w-Max1);
} else ans=min(ans,sum+w-Max0);
}
int main() {
int n,m,u,v,w;
cin>>n>>m;
for(int i=1; i<=m; i++) {
cin>>u>>v>>w;
e2[i]= {u,v,w};
}
for(int i=1; i<=n; i++)p[i]=i;
sort(e2+1,e2+m+1,cmp);
for(int i=1; i<=m; i++) {
int p1=find(e2[i].u),p2=find(e2[i].v);
if(p1!=p2) {
p[p1]=p2;
add(e2[i].u,e2[i].v,e2[i].w);
add(e2[i].v,e2[i].u,e2[i].w);
vis[i]=1;
sum+=e2[i].w;
}
}
dfs(1,0,0);
ans=1e16;
for(int i=1; i<=m; i++) {
if(vis[i]||e2[i].v==e2[i].u)continue;
lca(e2[i].u,e2[i].v,e2[i].w);
}
cout<<ans;
return 0;
}