【Coel.学习笔记】【图论终章!】朱刘算法与 Prufer 编码

结束啦!接下来就到数据结构了~

朱刘算法

有向图上的最小生成树被称为最小树形图。树形图具有两个特点:不存在环,且除了根节点外每个点的入度都为 \(1\)
用于求解最小树形图的算法被称为朱刘算法,由朱永津和刘振宏在 1965 年首次提出而得名。后来 Jack Edmonds 也独立发现了这个算法,所以又叫做 Edmonds 算法。

朱刘算法的基本过程如下:

  1. 对于每个点(根除外),找到边权最小的入边。
  2. 判断刚才选择的边是否会组成一个环。若不存在环,则算法结束。
  3. 若存在环则对所有环缩点。对于环内部的边直接删除;终点在环内的边则把权值变为原有权值减去环内边的权值;其他边不变。
  4. 重复上述操作,直到算法结束。

每次缩点后点数至少会减少 \(1\),所以迭代次数为 \(O(m)\);每次用 tarjan 算法缩点和找环的时间复杂度为 \(O(n)\),因此总的时间复杂度为 \(O(nm)\)。Tarjan 提出了一个时间复杂度为 \(O(m+n\log n)\) 的优化算法,由于朴素的 \(O(nm)\) 算法已经足够日常使用(而且朱刘算法非常少见),此处不再赘述。

代码如下:
洛谷传送门

// Problem: P4716 【模板】最小树形图
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4716
// Memory Limit: 250 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)

#include <algorithm>
#include <cstring>
#include <iomanip>
#include <iostream>

using namespace std;

const int maxn = 150;
const int inf = 0x3f3f3f3f;

int n, m, r;
bool f[maxn][maxn];
int dis[maxn][maxn], bdis[maxn][maxn];
int pre[maxn], bpre[maxn];
int dfn[maxn], low[maxn], stk[maxn], tot, top;
int bel[maxn], idx;
bool vis[maxn], jud[maxn];

void dfs(int u) {
    jud[u] = true;
    for (int i = 1; i <= n; i++)
        if (dis[u][i] != inf && !jud[i]) dfs(i);
}

bool check() { //判断图是否连通
    dfs(r);
    for (int i = 1; i <= n; i++)
        if (!jud[i]) return false;
    return true;
}

void tarjan(int u) {
    dfn[u] = low[u] = ++tot;
    stk[++top] = u, vis[u] = true;
    int v = pre[u];
    if (!dfn[v]) {
        tarjan(v);
        low[u] = min(low[u], low[v]);
    } else if (vis[v])
        low[u] = min(low[u], dfn[v]);
    if (low[u] == dfn[u]) {
        idx++;
        do {
            v = stk[top--];
            vis[v] = false;
            bel[v] = idx;
        } while (v != u);
    }
}

int solve() {
    int res = 0;
    while (true) {
        for (int i = 1; i <= n; i++) { //初始化每个点的前继
            pre[i] = i;
            for (int j = 1; j <= n; j++)
                if (dis[pre[i]][i] > dis[j][i]) pre[i] = j;
        }
        memset(dfn, 0, sizeof(dfn));
        tot = idx = 0;
        for (int i = 1; i <= n; i++)  //找缩点
            if (!dfn[i]) tarjan(i);
        if (idx == n) {  //强连通分量数等于点数,意味着无环
            for (int i = 2; i <= n; i++) res += dis[pre[i]][i];
            break;
        }
        for (int i = 1; i <= n; i++) { //统计答案,注意省略根节点
            if (i == r) continue;
            if (bel[pre[i]] == bel[i]) res += dis[pre[i]][i];
        }

        for (int i = 1; i <= idx; i++)
            for (int j = 1; j <= idx; j++) bdis[i][j] = inf;
        for (int i = 1; i <= n; i++)  //备份图
            for (int j = 1; j <= n; j++)
                if (dis[i][j] < inf && bel[i] != bel[j]) {
                    int u = bel[i], v = bel[j];
                    if (bel[pre[j]] == bel[j])
                        bdis[u][v] =
                            min(bdis[u][v], dis[i][j] - dis[pre[j]][j]);
                    else
                        bdis[u][v] = min(bdis[u][v], dis[i][j]);
                }
        n = idx;
        memcpy(dis, bdis, sizeof(dis));  //用备份图更新原图
        r = bel[r];  //根换成强连通分量
    }
    return res;
}

int main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    memset(dis, 0x3f, sizeof(dis));
    cin >> n >> m >> r;
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        if (u != v) dis[u][v] = min(dis[u][v], w); //点数很少,用邻接矩阵更方便
        /*注意这里要特判重边和自环*/
    }
    if (!check())
        cout << -1;
    else
        cout << solve();
    return 0;
}

Prufer 编码

Prufer 序列可以将一个带标号 \(n\) 个结点的树用 \(1\)\(n\) 中的 \(n-2\) 个整数表示,相当于完全图生成树与数列的双射。
给定一个无根树,每次找到一个编号最小的叶子结点。输出和该结点相连的父节点,并删除叶子节点。如此反复,直到只剩两个结点,这样就构造出了一个Prufer 编码。
如下图,无根树的 Prufer 编码就是 \(3,5,4,5\)
image

使用堆可以非常方便地以 \(O(n\log n)\) 的时间复杂度实现 Prufer 编码与树相互转换,但使用一个指针代替堆寻找最小值,可以实现 \(O(n)\) 的算法。

代码如下:
洛谷传送门

// Problem: P6086 【模板】Prufer 序列
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P6086
// Memory Limit: 500 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)

#include <iostream>

#define int long long

const int maxn = 5e6 + 10;

using namespace std;

int n, m;
int f[maxn], d[maxn], p[maxn];

int solve_prufer() {
    long long res = 0;
    for (int i = 1; i < n; i++) {
        cin >> f[i];
        d[f[i]]++;
    }
    for (int i = 1, j = 1; i <= n - 2; i++, j++) {
        while (d[j]) j++;
        p[i] = f[j];
        while (i <= n - 2 && --d[p[i]] == 0 && p[i] < j)
            p[i + 1] = f[p[i]], i++;
    }
    for (int i = 1; i <= n - 2; i++) res ^= 1LL * i * p[i];
    return res;
}

int solve_tree() {
    long long res = 0;
    for (int i = 1; i <= n - 2; i++) {
        cin >> p[i];
        d[p[i]]++;
    }
    p[n - 1] = n;
    for (int i = 1, j = 1; i < n; i++, j++) {
        while (d[j]) j++;
        f[j] = p[i];
        while (i < n && --d[p[i]] == 0 && p[i] < j) f[p[i]] = p[i + 1], i++;
    }
    for (int i = 1; i < n; i++) res ^= 1LL * i * f[i];
    return res;
}

signed main(void) {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    if (m == 1)
        cout << solve_prufer();
    else
        cout << solve_tree();
    return 0;
}
posted @ 2022-07-21 21:36  秋泉こあい  阅读(46)  评论(1编辑  收藏  举报