洛谷 P4716:【模板】最小树形图 ← 朱-刘算法(链式前向星)

【题目来源】
https://www.luogu.com.cn/problem/P4716

【题目描述】
给定包含 n 个结点, m 条有向边的一个图。试求一棵以结点 r 为根的最小树形图,并输出最小树形图每条边的权值之和,如果没有以 r 为根的最小树形图,输出 −1。

【输入格式】
第一行包含三个整数 n,m,r,意义同题目所述。
接下来 m 行,每行包含三个整数 u,v,w,表示图中存在一条从 u 指向 v 的权值为 w 的有向边。

【输出格式】
如果原图中存在以 r 为根的最小树形图,就输出最小树形图每条边的权值之和,否则输出 −1。

【输入样例一】
4 6 1
1 2 3
1 3 1
4 1 2
4 2 2
3 2 1
3 4 1

【输出样例一】
3

【输入样例二】
4 6 2
1 2 3
1 3 1
4 1 2
4 2 2
3 2 1
3 4 1

【输出样例二】
-1

【数据范围】
对于所有数据,1≤u,v≤n≤100, 1≤m≤10^4, 1≤w≤10^6。

【算法分析】
● 最小树形图(Minimum Arborescence)就是"有向图 + 指定根"版本的最小生成树。即在一张带权有向图里,找一棵++以根 root 为起点、能到达所有点、总权最小++ 的有向树。
● 最小树形图的经典解法是朱-刘算法(Chu-Liu/Edmonds)。朱‑刘算法,得名于中国科学院朱永津、刘振宏两位研究员。1965 年,二人联名发表论文,提出了世界上首个求解 "最小树形图" 的多项式算法,该算法也由此以两位研究者的姓氏合称命名。
● 链式前向星:https://blog.csdn.net/hnjzsyjyj/article/details/139369904

【算法代码】

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=1e2+5;
const int M=1e4+5;
const LL INF=0x3f3f3f3f;

struct Edge {
    int u,v;
    LL w;
} ed[M];

LL in[N];     //点的最小入边权值
int pre[N];   //点的最小入边的起点编号
int id[N];    //缩点后超级点编号
int st[N];
int h[N],e[M],ne[M],idx;
LL val[M];

void add(int a,int b,int w) {
    val[idx]=w,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

LL zhuliu(int root,int n,int m) {
    LL res=0;
    while(1) {
        idx=0;
        memset(h,-1,sizeof h);
        for(int i=0; i<m; i++) {
            int u=ed[i].u, v=ed[i].v;
            LL w=ed[i].w;
            add(u,v,w);
        }

        //1.求每个非根点的最小入边
        for(int i=1; i<=n; i++) {
            in[i]=INF, pre[i]=-1;
        }
        for(int u=1; u<=n; u++) {
            for(int i=h[u]; i!=-1; i=ne[i]) {
                int t=e[i];
                LL w=val[i];
                if(u!=t && w<in[t]) {
                    in[t]=w, pre[t]=u;
                }
            }
        }

        for(int i=1; i<=n; i++) { //无解
            if(i==root) continue;
            if(in[i]==INF) return -1;
        }

        //2.寻找环
        memset(id,0,sizeof id);
        memset(st,0,sizeof st);
        int cnt=0;
        in[root]=0;
        for(int i=1; i<=n; i++) {
            res+=in[i];
            int cur=i;
            while(st[cur]!=i && !id[cur] && cur!=root) {
                st[cur]=i, cur=pre[cur];
            }
            if(cur!=root && !id[cur]) {
                cnt++;
                for(int j=pre[cur]; j!=cur; j=pre[j]) {
                    id[j]=cnt;
                }
                id[cur]=cnt;
            }
        }
        if(cnt==0) break; //无环

        //不属于环的点分配超级点编号
        for(int i=1; i<=n; i++) {
            if(!id[i]) id[i]=++cnt;
        }

        //3.缩点。更新 w'=w-in[v]
        int nm=0; //缩点后新图的边总数
        for(int i=0; i<m; i++) {
            int u=ed[i].u, v=ed[i].v;
            LL w=ed[i].w;
            if(id[u]!=id[v]) {
                ed[nm].u=id[u];
                ed[nm].v=id[v];
                ed[nm].w=w-in[v];
                nm++;
            }
        }
        m=nm;
        n=cnt;
        root=id[root];
    }
    return res;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n,m,r;
    cin>>n>>m>>r;
    for(int i=0; i<m; i++) {
        cin>>ed[i].u>>ed[i].v>>ed[i].w;
    }
    cout<<zhuliu(r,n,m)<<endl;

    return 0;
}


/*
in:
4 6 1
1 2 3
1 3 1
4 1 2
4 2 2
3 2 1
3 4 1

out:
3
*/

【参考文献】

https://blog.csdn.net/hnjzsyjyj/article/details/139369904

https://www.luogu.com.cn/problem/solution/P4716

https://www.cnblogs.com/mogeko/p/10952944.html

https://blog.csdn.net/liufengwei1/article/details/115527549