Kruskal
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,tot=0,k=0;//n端点总数,m边数,tot记录最终答案,k已经连接了多少边
int fat[200010];//记录集体老大
struct node
{
int from,to,dis;//结构体储存边
}edge[200010];
bool cmp(const node &a,const node &b)//sort排序(当然你也可以快排)
{
return a.dis<b.dis;
}
int father(int x)//找集体老大,并查集的一部分
{
if(fat[x]!=x)
return father(fat[x]);
else return x;
}
void unionn(int x,int y)//加入团体,并查集的一部分
{
fat[father(y)]=father(x);
}
int main()
{
scanf("%d%d",&n,&m);//输入点数,边数
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&edge[i].from,&edge[i].to,&edge[i].dis);//输入边的信息
}
for(int i=1;i<=n;i++) fat[i]=i;//自己最开始就是自己的老大 (初始化)
sort(edge+1,edge+1+m,cmp);//按权值排序(kruskal的体现)
for(int i=1;i<=m;i++)//从小到大遍历
{
if(k==n-1) break;//n个点需要n-1条边连接
if(father(edge[i].from)!=father(edge[i].to))//假如不在一个团体
{
unionn(edge[i].from,edge[i].to);//加入
tot+=edge[i].dis;//记录边权
k++;//已连接边数+1
}
}
printf("%d",tot);
return 0;
}
Kruskal的一个性质:由Kruskal构建出的最小生成树里面的各个n个顶点之间的路径,其各个顶点路径所经过边的最大值在原图中最小,即Kruskal构建的路径就是在原图里面所要求的2个顶点之间经过最长边最短的路径,同样的如果构建的是最大生成树则反之。
Prime
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=2e5+5;
const int inf=1e9+7;
int n,m;
struct node{
int to,val;
node(int x,int y):to(x),val(y){}
};
int d[maxn],vis[maxn];
vector<node>vec[maxn];
bool operator<(node a,node b){
return a.val>b.val;
}
void prime(){
priority_queue<node,vector<node>,less<node>>q;
q.push(node(1,0));
int ans=0;
int cnt=0;
while(q.size()&&cnt<n){
node now=q.top();
q.pop();
//cout<<now.val<<"\n";
int from=now.to,val=now.val;
if(vis[from]){
continue;
}
else{
cnt++;
ans+=val;
vis[from]=1;
for(int i=0;i<vec[from].size();i++){
node tmp=vec[from][i];
if(!vis[tmp.to]){
q.push(node(tmp.to,tmp.val));
}
}
}
}
if(cnt==n)cout<<ans<<"\n";
else cout<<"impossible"<<"\n";
}
signed main(){
cin>>n>>m;
while(m--){
int x,y,z;
cin>>x>>y>>z;
vec[x].push_back(node(y,z));
vec[y].push_back(node(x,z));
}
prime();
}
次小生成树
首先对于次小生成树,我们可以枚举各个不在原先最小生成树当中的边,设其为关键边x,然后先加入边x作为第一条边,再求x作为第一条边的最小生成树,枚举完后取其中的最小值就是次小生成树
对于各次枚举x,通过分析可以得出如果边y不是原先的最小生成树的边,也不是当前枚举的关键边x,那么一定不会构成x所构成的最小生成树(原先按顺序轮不到你加入的边,现在多了一条边占位置更加加不进去了),所以x对应的最小生成树就在原先的最小生成树的边集和新加入的x这条边当中,可以发现现在形成了一个树当中多了一条边构成了一个环,那么要求这个边集对应的非严格最小生成树就相当于把这个集合当中除去关键边(因为要加入的就是关键边)的最大边去掉,剩下的就是x对应最小生成树。但是如果是求严格最小生成树,那么去掉这个环里面的最大边后,可能生成的新树跟原来的最小生成树一样大,那么是不行的,所以还要求出这个环里面的严格次大边,当替换最大边产生的新树跟原来的最小生成树一样大时就替换严格次大边
根据上面的结论,我们每次枚举的时候只需要在原来mst的基础上加入关键边,减去关键边两个端点的路径上最长的那条边的权值即可。这个问题我们可以通过一些经典方法来解决,比方说树剖、倍增、Link/Cut Tree
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
struct Edge
{
int a, b, w;
bool used;
bool operator< (const Edge &t) const
{
return w < t.w;
}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];//log2(1e5)=16 d1最大边 d2次大边
void add(int a,int b,int c)
{
e[idx] = b,ne[idx] = h[a],w[idx] = c,h[a] = idx++;
}
int find(int x)
{
if(x!=p[x])p[x]= find(p[x]);
return p[x];
}
LL kruskal()
{
for (int i = 1; i <= n; i ++ ) p[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; i ++ )
{
int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
if (a != b)
{
p[a] = b;
res += w;
edge[i].used = true;
}
}
return res;
}
void build()
{
memset(h,-1,sizeof h);
for(int i = 0;i<m;i++)
{
if(edge[i].used)
{
int a = edge[i].a,b = edge[i].b,w = edge[i].w;
add(a,b,w),add(b,a,w);
}
}
}
void bfs()
{
memset(depth,0x3f,sizeof depth);
depth[0] = 0,depth[1] = 1;//哨兵0 根节点1
queue<int> q;
q.push(1);
while(q.size())
{
int t = q.front();
q.pop();//日常漏
for(int i = h[t];~i;i=ne[i])
{
int j = e[i];
// j没有被遍历过
if(depth[j]>depth[t]+1)
{
depth[j] = depth[t]+1;
q.push(j);
fa[j][0] = t;
d1[j][0] = w[i],d2[j][0] = -INF;
for(int k = 1;k<=16;k++)
{
/*
→ →
o---o---o
j anc
d1[i,k-1],d2[i,k-1] d1[anc,k-1],d2[anc,k-1]
*/
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
//初始化d1[j][k]和d2[j][k]
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; u ++ )
{
int d = distance[u];
// 更新最大值d1和次大值d2
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
// 严格次大值
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
// lca求出a, b之间的最大边权与次大边权
int lca(int a,int b,int w)
{
static int distance[N * 2];
int cnt = 0;
// a和b中取深度更深的作为a先跳
if (depth[a] < depth[b]) swap(a, b);
for (int k = 16; k >= 0; k -- )
// 如果a 跳2^k后的深度比b深度大 则a继续跳
// 直到两者深度相同 depth[a] == depth[b]
if (depth[fa[a][k]] >= depth[b])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
a = fa[a][k];
}
// 如果a和b深度相同 但此时不是同一个点 两个同时继续向上跳
if (a != b)
{
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
distance[cnt ++ ] = d1[b][k];
distance[cnt ++ ] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
// 此时a和b到lca下同一层 所以还要各跳1步=跳2^0步
distance[cnt ++ ] = d1[a][0];
distance[cnt ++ ] = d1[b][0];
}
// 找a,b两点距离的最大值dist1和次大值dist2
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++ )
{
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2) dist2 = d;
}
// ⭐ dist1和dist2是a和b之间的最大边权和次大边权 所以可以用w替换而仍然保持生成树(包含所有节点)
// 因为加入w这条边 原来的树会形成环
// 删除环中边权最大的边(如果最大的边和加入的边相等,那么删去次大边)。
// 如果w>这条路的最大边 w替换dist1
if (w > dist1) return w - dist1;
// 否则w==dist1 w替换dist2
if (w > dist2) return w - dist2;
// 不加这个return INF也是可以的
// ⭐因为非树边w的值域是一定≥dist1 否则在之前kruskal求最小生成树的时候把w替换dist1连接a和b就得到一个更小的生成树了 矛盾
// 所以最坏情况是w==dist1
// return INF;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
// kruskal建最小树(把用到的边标记)
LL sum = kruskal();
// 对标记的边建图
build();
bfs();
LL res = 1e18;
//从前往后枚举非树边
for (int i = 0; i < m; i ++ )
if (!edge[i].used)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
// lca(a,b,w) 返回用w替换w[i] 的差值 = w-w[i]
res = min(res, sum + lca(a, b, w));
}
printf("%lld\n", res);
return 0;
}