稠密图(prim算法):
题目:
稠密图的定义:
对于稠密图,边的数量接近于顶点数量的平方,即 m≈O(n^2)。在这种情况下,邻接矩阵表示法非常高效,可以快速访问和更新边权重。Prim算法在稠密图中的时间复杂度为 O(n2),适合处理这种边数较多的情况。
Prim算法的基本思想:
- 初始化:将每个点的距离初始化为正无穷,并将第一个点的距离初始化为0。使用一个数组
st
来记录每个点是否已经在最小生成树集合中。 - 迭代构建最小生成树:
- 在每一步中,找到一个不在集合中的、离集合距离最近的点
t
,将t
加入到集合中,并用t
来更新其他点到集合的距离。 - 如果当前选择的点的距离仍然是正无穷,说明最小生成树不存在,因为有些点无法连接到集合中。
- 在每一步中,找到一个不在集合中的、离集合距离最近的点
- 更新距离:每次找到新的点
t
后,用t
更新其他未在集合中的点的距离,距离更新的依据是通过t
到这些点的边的权重。
变量说明:
n
:图的顶点数。m
:图的边数。g[N][N]
:邻接矩阵表示的图,其中g[i][j]
表示顶点i
到顶点j
的边权重。dist[N]
:记录每个点到最小生成树集合的最小距离。st[N]
:记录每个点是否在最小生成树集合中。
代码流程
- 初始化图的邻接矩阵:将所有边权重初始化为正无穷(使用
0x3f3f3f3f
表示)。 - 读入图的边信息:根据输入更新邻接矩阵
g
。 - Prim算法核心部分:
- 初始化距离数组
dist
和集合标记数组st
。 - 使用双重循环构建最小生成树:每次找到一个离集合最近的点
t
,将其加入集合,并更新其他点到集合的距离。
- 初始化距离数组
- 输出结果:如果构建的最小生成树的总权重为正无穷,则输出 "impossible",否则输出最小生成树的总权重。
代码:
// 与朴素版Dijkstra()算法相似;
// 初始化每个点的距离为正无穷;
// 找到t:不在集合中离集合距离最近的那个点;
// 用t来更新其他点到集合的距离;
#include <iostream>
#include <cstring>
using namespace std;
int n,m;
const int N=510;
const int INF=0x3f3f3f3f;
int g[N][N]; // 稠密图;
int dist[N];
bool st[N]; // 判断是否在集合中;
int prim()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0; // 第一个点必须在集合中;
int res=0; // 最小生成树的距离之和;
for(int i=1;i<=n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
{
if(st[j]==0&&(t==-1||dist[j]<dist[t])){
t=j;
}
}
st[t]=true;
if(dist[t]==INF) return INF; // 如果除了第一个点外的到这个集合距离是正无穷,说明最小生成树不存在;
res+=dist[t];
for(int j=1;j<=n;j++){
dist[j]=min(dist[j],g[t][j]);// 与朴素版Dijkstra不同: g[t][j]就是到集合的距离;
// 不要求是到起点的距离;
}
}
return res;
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b]=g[b][a]=min(g[a][b],c);// 防止重边取最小的;
}
int t=prim();
if(t>INF/2) cout<<"impossible";
else cout<<t;
}
稀疏图(kruskal算法):
题目:
稀疏图的定义:
对于稀疏图,边的数量远小于顶点数的平方,因此Kruskal算法的效率更高。排序操作和并查集的使用使得Kruskal算法的时间复杂度为 O(mlogm+nα(n)),其中 α(n) 是阿克曼函数的反函数,近似为常数。相比于Prim算法在稀疏图中可能的 O(n2) 时间复杂度,Kruskal算法更高效。
Kruskal算法的基本思想:
- 排序:将所有边按权重从小到大排序。
- 初始化并查集:用并查集(Disjoint Set Union,DSU)来管理集合,用来判断两个顶点是否属于同一个连通分量。
- 逐边检查:按权重从小到大依次检查每条边,如果这条边连接的两个顶点属于不同的集合,则将它们合并,并将这条边加入最小生成树。
- 终止条件:当加入的边数等于 n−1 时,算法结束,因为最小生成树已经包含 n−1 条边。
变量说明:
n
:图的顶点数。m
:图的边数。p[N]
:并查集数组,用于管理集合。Edge
:结构体,表示图中的一条边,包含两个顶点a
和b
以及权重c
。
代码流程:
- 初始化并查集:每个顶点自成一个集合。
- 边排序:按权重从小到大对所有边排序。
- 边检查和合并:逐边检查并用并查集管理连通性。
- 输出结果:如果成功构建最小生成树,输出其总权重,否则输出 "impossible"。
代码:
// krusal()算法: O(nlogn); 排序O(nlogn)+并查集(O(n));
// 将边按照从小到大进行排序;
// 如果a点和b点不在一个集合中,就将a,b加入集合;
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
const int N=2e5+10; const int INF=0x3f3f3f3f;
int p[N];
struct Edge
{
int a,b,c;
}edge[N];
bool cmp(struct Edge x,struct Edge y)
{
return x.c<y.c;
}
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int krusal()
{
for(int i=1;i<=n;i++) p[i]=i;
sort(edge,edge+m,cmp);
int res=0,cnt=0;
for(int i=0;i<m;i++)
{
auto t=edge[i];
int a=t.a;
int b=t.b;
int c=t.c;
int x=find(a); int y=find(b);
if(x!=y)
{
p[x]=y;
res+=c;
cnt++;
}
}
if(cnt!=n-1) return INF;
else return res;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++) cin>>edge[i].a>>edge[i].b>>edge[i].c;
int t=krusal();
if(t>INF/2) cout<<"impossible";
else cout<<t;
return 0;
}
两种结合(稀疏图中部分稠密):
题目:
分析:
观察可知这道题目要求的是最小生成树,由于是多组询问,所以需要分类讨论,令点集S的长度为cnt,当cnt<sqrt(n),利用prim算法,当cnt>=sqrt(n)时利用kruskal算法。还有一个问题,利用什么来存图?可以利用map<int,int>mp[N]来存图。假设我们有 N
个节点,且每个节点都有一个与其他节点的关联度信息,我们可以使用 map<int, int> mp[N]
来存储这些信息。其中,mp[i]
存储节点 i
与其他节点的关联度。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct Node{
int s,e,v;
}e[N]; // 定义边结构体,存储边的起点s、终点e和权重v。
int f[N]; // 并查集父节点数组。
int a[N]; // 查询中的节点数组。
bool vis[N]; // 节点访问标志。
map<int,int> w[N]; // 邻接表,存储边的权重。
int dis[N]; // Prim算法中的距离数组。
bool cmp(struct Node A,struct Node B)
{
return A.v<B.v; // 比较函数,用于边权重的排序。
}
int find(int x){
if(f[x]!=x) f[x]=find(f[x]); // 并查集查找函数。
return f[x];
}
bool merge(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return 0; // 如果两个节点已经在同一集合中,则返回false。
f[y]=x;
return 1;
}
int main(){
int n,m,q;
scanf("%d%d%d",&n,&m,&q); // 输入节点数、边数和查询数。
for(int i=1;i<=m;i++){
scanf("%d%d%d",&e[i].s,&e[i].e,&e[i].v);
if(w[e[i].s].count(e[i].e))
{
w[e[i].s][e[i].e]=min(w[e[i].s][e[i].e],e[i].v);
w[e[i].e][e[i].s]=min(w[e[i].e][e[i].s],e[i].v);
}
else
{
w[e[i].s][e[i].e]=e[i].v;
w[e[i].e][e[i].s]=e[i].v;
}
}
sort(e+1,e+m+1,cmp); // 按照边的权重从小到大排序。
while(q--){
int k;
scanf("%d",&k);
if(k<=sqrt(n)){
// 使用Prim算法
for(int i=1;i<=k;i++){
scanf("%d",&a[i]);
dis[i]=1e9+7;
vis[a[i]]=0;
}
long long res=0;
int now=1;
dis[1]=0; vis[a[1]]=1;
for(int i=1;i<k;i++){
for(int j=1;j<=k;j++){
if(vis[a[j]])continue;
if(w[a[now]].count(a[j])){
dis[j]=min(dis[j],w[a[now]][a[j]]);
}
}
long long temp=1e9+7;
int nxt=-1;
for(int j=1;j<=k;j++){
if(vis[a[j]])continue;
if(dis[j]<temp){
temp=dis[j];
nxt=j;
}
}
if(nxt==-1){
res=-1;
break;
}
res+=temp;
vis[a[nxt]]=1;
now=nxt;
}
printf("%lld\n",res);
for(int i=1;i<=k;i++){
vis[a[i]]=0;
}
} else {
// 使用Kruskal算法
long long res=0;
for(int i=1;i<=k;i++){
scanf("%d",&a[i]);
f[a[i]]=a[i];
vis[a[i]]=1;
}
for(int i=1;i<=m;i++){
if(vis[e[i].s]&&vis[e[i].e]){
if(merge(e[i].s,e[i].e))
res+=e[i].v;
}
}
for(int i=1;i<=k;i++){
if(find(a[i])!=find(a[1])){
res=-1;
break;
}
}
printf("%lld\n",res);
for(int i=1;i<=k;i++){
vis[a[i]]=0;
}
}
}
return 0;
}
详细步骤:
- 输入处理:程序首先读取节点数、边数和查询数,然后读取每条边的信息,存储在边数组
e
和邻接表w
中。 - 边排序:对所有边按照权重从小到大排序,以便后续Kruskal算法使用。
- 查询处理:
- 小规模查询:使用Prim算法
- 初始化距离数组
dis
和访问标志数组vis
。 - 从任意一个节点开始(这里从
a[1]
),逐步将最近的未访问节点加入集合,并更新其他节点的最短距离。 - 重复直到所有节点都被访问或无法再找到未访问节点。
- 初始化距离数组
- 大规模查询:使用Kruskal算法
- 初始化并查集数组
f
和访问标志数组vis
。 - 遍历所有边,尝试将每条边加入最小生成树,如果两个节点不在同一集合中,则合并它们的集合并累加边的权重。
- 检查所有节点是否在同一集合中,如果有节点不在同一集合中,说明这些节点无法构成连通图,返回-1。
- 初始化并查集数组
- 小规模查询:使用Prim算法
- 输出结果:输出每个查询的结果。
注意事项:
- 代码中使用
scanf
和printf
进行输入输出以提高效率。 sort
函数用于边的排序,排序后方便Kruskal算法逐一处理边。- 使用并查集(Disjoint Set Union,DSU)实现Kruskal算法中的合并和查找操作。
- 稠密图使用Prim算法,稀疏图使用Kruskal算法,两者结合可以处理不同规模的查询。