生成树学习指南

前置芝士

Kruskal算法

先把边按照权值进行排序,用贪心的思想优先选取权值较小的边,并依次连接,若出现环则跳过此边(用并查集来判断是否存在环)继续搜,直到已经使用的边的数量比总点数少一即可。

[证明]

刚刚有提到:如果某个连通图属于最小生成树,那么所有从外部连接到该连通图的边中的一条最短的边必然属于最小生成树。所以不难发现,当最小生成树被拆分成彼此独立的若干个连通分量的时候,所有能够连接任意两个连通分量的边中的一条最短边必然属于最小生成树。

\(1≤N≤5000,1≤M≤2×10^5,1≤w_i≤10^4\)

[c++]

struct node{
    int u,v,w;
}e[200005];
int fa[5005],cnt,n,m,ans;
bool cmp(node x,node y){
    return x.w<y.w;
}
int find(int x){
    while(x!=fa[x]) x=fa[x]=fa[fa[x]];
    return x;
}
void Krusal(){
    sort(e,e+m,cmp);
    for(int i=1;i<=n;i++){
        fa[i]=i;
    }
    for(int i=0;i<m;i++){
        int x=find(e[i].u);
        int y=find(e[i].v);
        if(x==y) continue;
        fa[x]=y;
        ans+=e[i].w;
        if(++cnt==n-1){
            break;
        }
    }
}
void solve(){
 cin>>n>>m;
    for(int i=0;i<m;i++){
       cin>>e[i].u>>e[i].v>>e[i].w;  
         }
        Krusal();
        if(cnt!=n-1) cout<<"orz"<<endl;//不能连通输出orz
        else  cout<<ans<<endl;
}

Prim算法

Prim和最短路中的dijkstra很像,Prim的思想是将任意节点作为根,再找出与之相邻的所有边(用一遍循环即可),再将新节点更新并以此节点作为根继续搜,维护一个数组:dis,作用为已用点到未用点的最短距离。

[证明]

Prim算法之所以是正确的,主要基于一个判断:对于任意一个顶点v,连接到该顶点的所有边中的一条最短边(v, vj)必然属于最小生成树(即任意一个属于最小生成树的连通子图,从外部连接到该连通子图的所有边中的一条最短边必然属于最小生成树)

Prim在稠密图中比Kruskal优,在稀疏图中比Kruskal劣。Prim是以更新过的节点的连边找最小值,Kruskal是直接将边排序。

#define inf 0x3f3f3f3f
#define maxN 5005
#define maxM 200005
struct edge{
    int v,w,next;
}e[maxM<<1];
int head[maxN],dis[maxN],cnt,n,m,tot,now=1,ans;
bool vis[maxN];
void add(int u,int v,int w){
e[++cnt].v=v;//终点
e[cnt].w=w;//权值
e[cnt].next=head[u];//与cnt条边同起点的下一条边的位置
head[u]=cnt;//保存的是以u为起点的所有边中编号最大的那个
}
void init(){
    int u,v,w;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
  cin>>u>>v>>w;
        add(u,v,w);
        add(v,u,w);
    }   
}
int prim(){
    for(int i=2;i<=n;i++){
        dis[i]=inf;
    }
    for(int i=head[1];i;i=e[i].next){
        dis[e[i].v]=min(dis[e[i].v],e[i].w);
    }
    while(++tot<n){
        int minn=inf;
        vis[now]=1;
    for(int i=1;i<=n;i++){
        if(!vis[i]&&minn>dis[i]){
                minn=dis[i];
                now=i;
        }
    }
    if(minn==inf) return -1;  
    ans+=minn;//加上这个点到集合S的距离
    for(int i=head[now];i;i=e[i].next){
        int v=e[i].v;
        if(dis[v]>e[i].w && !vis[v]){
            dis[v]=e[i].w;
        }
    }
    }
    return ans;
}
void solve(){
    init();
    int res=prim();
    if(res==-1) cout<<"orz"<<endl;
    else printf("%d",res);
}

花费最小代价,使标记节点互相不连通

[problem desrciption]

现在有 \(N\) 个城市,其中 \(K\) 个被敌方军团占领了,\(N\) 个城市间有 \(N-1\) 条公路相连,破坏其中某条公路的代价是已知的,现在,告诉你 \(K\) 个敌方军团所在的城市,以及所有公路破坏的代价,请你算出花费最少的代价将这 \(K\) 个地方军团互相隔离开,以便第二步逐个击破敌人。

[input]

第一行包含两个正整数 \(N\)\(K\)

第二行包含 \(K\) 个整数,表示哪个城市被敌军占领。

接下来 \(N-1\) 行,每行包含三个正整数 \(a,b,c\),表示从 \(a\) 城市到 \(b\) 城市有一条公路,以及破坏的代价 \(c\)。城市的编号从 \(0\) 开始。

[output]

输出一行一个整数,表示最少花费的代价。

[样例]

样例输入

5 3
1 2 4
1 0 4
1 3 8
2 1 1
2 4 3

样例输出

4

\(2\le N\le10^5\)\(2\le K\le N\)\(1\le c\le 10^6\)

[solved]

(1)给定一个树,在将一些边删去后让标记的节点互相不可到达的前提下,使花费最少。

(2)反向思考,在保证标记的节点不互通的前提下,尽可能构造出最大花费的森林

(3)答案=总边权-最大花费

posted @ 2023-10-10 14:01  White_Sheep  阅读(13)  评论(0编辑  收藏  举报