【算法】Matrix - Tree 矩阵树定理 & 题目总结

  最近集中学习了一下矩阵树定理,自己其实还是没有太明白原理(证明)类的东西,但想在这里总结一下应用中的一些细节,矩阵树定理的一些引申等等。

  首先,矩阵树定理用于求解一个图上的生成树个数。实现方式是:\(A\)为邻接矩阵,\(D\)为度数矩阵,则基尔霍夫(Kirchhoff)矩阵即为:\(K = D - A\)。具体实现中,记 \(a\) 为Kirchhoff矩阵,则若存在 \(E(u, v)\) ,则\(a[u][u] ++, a[v][v] ++, a[u][v] --, a[v][u] --\) 。即\(a[i][i]\) 为 \(i\) 点的度数,\(a[i][j]\) 为 \(i, j\)之间边的条数的相反数。

  这样构成的矩阵的行列式的值,就为生成树的个数。而求解行列式的快速方法为使用高斯消元进行消元消处上三角矩阵,则有对角线上的值的乘积 = 行列式的值。一般而言求解生成树个数的题目数量会非常庞大,需要取模处理。取模处理中,不能出现小数,于是使用辗转相除法:(其中因为消的是行列式,所以与消方程有所不同。交换两行行列式的值变号,且消元只能将一行的数 * k 之后加到别的行上。)

int Gauss()
{
    int ans = 1;
    for(int i = 1; i < tot; i ++)
    {
        for(int j = i + 1; j < tot; j ++)
            while(f[j][i])
            {
                int t = f[i][i] / f[j][i];
                for(int k = i; k < tot; k ++)
                    f[i][k] = (f[i][k] - t * f[j][k] + mod) % mod;
                swap(f[i], f[j]);
                ans = - ans;
            }
        ans = (ans * f[i][i]) % mod;
    }
    return (ans + mod) % mod;
}

  变元矩阵树定理:求所有生成树的总边积的和。和矩阵树的求法相同,不过行列式中 \(a[i][i]\) 记录的是总的边权和,\(a[i][j]\) 记录 \(i, j\) 之间边权的相反数。

  以下为几道题目:

    1.HEOI2015 小Z的房间     2.SHOI2016 黑暗前的幻想乡

    3.SDOI2014 重建         4.JSOI2008 最小生成树计数

  1.HEOI2015 小Z的房间(妥妥的模板题一个)

#include <bits/stdc++.h>
using namespace std;
#define maxn 90
#define int long long
#define mod 1000000000
int n, m, f[maxn][maxn];
int tot, Map[maxn][maxn];

int read()
{
    int x = 0, k = 1;
    char c;
    c = getchar();
    while(c < ‘0‘ || c > ‘9‘) { if(c == ‘-‘) k = -1; c = getchar(); }
    while(c >= ‘0‘ && c <= ‘9‘) x = x * 10 + c - ‘0‘, c = getchar();
    return x * k;
}

void add(int x, int y)
{
    if(x > y) return;
    f[x][x] ++, f[y][y] ++;
    f[x][y] --, f[y][x] --;
}

int Gauss()
{
    int ans = 1;
    for(int i = 1; i < tot; i ++)
    {
        for(int j = i + 1; j < tot; j ++)
            while(f[j][i])
            {
                int t = f[i][i] / f[j][i];
                for(int k = i; k < tot; k ++)
                    f[i][k] = (f[i][k] - t * f[j][k] + mod) % mod;
                swap(f[i], f[j]);
                ans = - ans;
            }
        ans = (ans * f[i][i]) % mod;
    }
    return (ans + mod) % mod;
}

signed main()
{
    n = read(), m = read();
    for(int i = 1; i <= n; i ++)
    {
        char c;
        for(int j = 1; j <= m; j ++)
        {
            cin >> c;
            if(c == ‘.‘) Map[i][j] = ++ tot;
        }
    }
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
        {
            int tem, u;
            if(!(u = Map[i][j])) continue;
            if(tem = Map[i - 1][j]) add(u, tem);
            if(tem = Map[i + 1][j]) add(u, tem);
            if(tem = Map[i][j - 1]) add(u, tem);
            if(tem = Map[i][j + 1]) add(u, tem);
        }
    printf("%lld\n", Gauss());
    return 0;
}

  2.SHOI2016黑暗前的幻想乡

  容斥+矩阵树定理。与模板的不同之处在于每一家公司都要参与修建,则合法方案数 = 总的方案数 - 有一个公司未修建的方案数 + 有两个公司未修建的方案数……暴力重构矩阵求解即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int mod = 1000000007;
int n;
ll g[20][20];
vector<pair<int , int > > q[20];

int read()
{
    int x = 0, k = 1;
    char c;
    c = getchar();
    while(c < ‘0‘ || c > ‘9‘) { if(c == ‘-‘) k = -1; c = getchar(); }
    while(c >= ‘0‘ && c <= ‘9‘) x = x * 10 + c - ‘0‘, c = getchar();
    return x * k;
}

int Gauss()
{
    ll ans = 1;
    for(int i = 1; i < n; i ++)
    {
        for(int j = i + 1; j < n; j ++)
            while(g[j][i])
            {
                ll t = g[i][i] / g[j][i];
                for(int k = i; k < n; k ++)
                    g[i][k] = (g[i][k] - g[j][k] * t) % mod;
                swap(g[i], g[j]);
                ans = -ans;
            }
        ans = (ans * g[i][i]) % mod;
        if(!ans) return 0;
    }
    return (ans + mod) % mod;
}

int main()
{
    n = read();
    for(int i = 1; i < n; i ++)
    {
        int m = read();
        for(int j = 1; j <= m; j ++)
        {
            int x = read(), y = read();
            q[i].push_back(make_pair(x, y));
        }
    }
    int ans = 0, CNST = 1 << (n - 1);
    for(int i = 0; i < CNST; i ++)
    {
        int cnt = 0; memset(g, 0, sizeof(g));
        for(int j = 1; j < n; j ++)
            if(i & (1 << (j - 1)))
            {
                for(int k = 0; k < q[j].size(); k ++)
                {
                    int x = q[j][k].first, y = q[j][k].second;
                    g[x][x] ++, g[y][y] ++;
                    g[x][y] --, g[y][x] --;
                }
                cnt ++;
            }
        if((n - cnt) & 1) ans = (ans + Gauss()) % mod;
        else ans = (ans - Gauss() + mod) % mod;
    }
    printf("%d\n", ans);
    return 0;
}

  3.SDOI2014重建

  化式子 + 变元矩阵树定理。将概率的式子写出来变形即可得到矩阵树定理求 \(\prod \frac{p(u, v)}{1 - p(u, v)}\)

#include <bits/stdc++.h>
using namespace std;
#define maxn 100
#define db double
#define eps 0.000001
int n;
db ans = 1.0, a[maxn][maxn];

db Gauss(int n)
{
    db ans = 1.0;
    for(int i = 1; i <= n; i ++)
    {
        for(int j = i + 1; j <= n; j ++)
        {
            int t = i;
            if(fabs(a[j][i]) > fabs(a[t][i])) t = j;
            if(t != i) swap(a[t], a[i]), ans = -ans;
        }
        for(int j = i + 1; j <= n; j ++)
        {
            db t = a[j][i] / a[i][i];
            for(int k = i; k <= n; k ++)
                a[j][k] -= t * a[i][k];
        }
        ans *= a[i][i];
    }
    return fabs(ans);
}

int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
            scanf("%lf", &a[i][j]);
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
        {
            db t = fabs(1.0 - a[i][j]) < eps ? eps : (1.0 - a[i][j]);
            if(i < j) ans *= t;
            a[i][j] = a[i][j] / t;
        }
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
            if(i != j) { a[i][i] += a[i][j], a[i][j] = -a[i][j]; }
    printf("%lf\n", Gauss(n - 1) * ans);
    return 0;
} 

  4.JSOI2008最小生成树计数

  这题虽然最早年,然而也最强啊……个人认为这位博主解释得很好了 Z-Y-Y-S的博客

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
#define maxn 200
#define mod 31011
int n, m, ans = 1, tmp[maxn];
int sum, fa[maxn], set[maxn];
int a[maxn][maxn];

struct edge
{
    int u, v, w;
}E[maxn * 20], e[maxn * 20];

int read()
{
    int x = 0, k = 1;
    char c;
    c = getchar();
    while(c < ‘0‘ || c > ‘9‘) { if(c == ‘-‘) k = -1; c = getchar(); }
    while(c >= ‘0‘ && c <= ‘9‘) x = x * 10 + c - ‘0‘, c = getchar();
    return x * k;
}

bool cmp(edge a, edge b) { return a.w < b.w; }

int find(int x) { return set[x] == x ? x : set[x] = find(set[x]); }
int find2(int x) { return fa[x] == x ? x : fa[x] = find2(fa[x]); }

int Gauss(int n)
{
    int ans = 1;
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
            a[i][j] = (a[i][j] + mod) % mod;
    for(int i = 1; i <= n; i ++)
    {
        for(int j = i + 1; j <= n; j ++)
            while(a[j][i])
            {
                int t = a[i][i] / a[j][i];
                for(int k = i; k <= n; k ++)
                    a[i][k] = (a[i][k] - 1ll * t * a[j][k] % mod + mod) % mod;
                swap(a[i], a[j]); ans = - ans;
            }
        ans = 1ll * ans * a[i][i] % mod;
    }
    return (ans + mod) % mod;
}

void Cal(int S, int T)
{
    int cnt = 0;
    for(int i = S; i <= T; i ++)
    {
        e[i] = E[i];
        int p = find(e[i].u), q = find(e[i].v);
        e[i].u = p, e[i].v = q;
        if(p == q) continue;
        tmp[++ cnt] = p, tmp[++ cnt] = q;
    }
    sort(tmp + 1, tmp + 1 + cnt);
    cnt = unique(tmp + 1, tmp + cnt + 1) - tmp - 1;
    memset(a, 0, sizeof(a));
    for(int i = 1; i <= cnt; i ++) fa[i] = i;
    for(int i = S; i <= T; i ++)
    {
        if(e[i].u == e[i].v) continue;
        int p = find(e[i].u), q = find(e[i].v);
        if(p != q) -- sum, set[p] = q;
        int u = lower_bound(tmp + 1, tmp + cnt + 1, e[i].u) - tmp;
        int v = lower_bound(tmp + 1, tmp + cnt + 1, e[i].v) - tmp;
        a[u][u] ++, a[v][v] ++;
        a[u][v] --, a[v][u] --;
        p = find2(u), q = find2(v);
        if(p != q) fa[p] = q;
    }
    for(int i = 2; i <= cnt; i ++)
        if(find2(i) != find2(i - 1))
        {
            int p = find2(i), q = find2(i - 1);
            a[p][p] ++, a[q][q] ++;
            a[p][q] --, a[q][p] --;
            fa[p] = q;
        }
    ans = 1ll * ans * Gauss(cnt - 1) % mod;
}

int main()
{
    n = read(), m = read();
    for(int i = 1; i <= m; i ++)
        E[i].u = read(), E[i].v = read(), E[i].w = read();
    sort(E + 1, E + 1 + m, cmp);
    for(int i = 1; i <= n; i ++) set[i] = i;
    sum = n;
    for(int i = 1, j; i <= m; i = j)
    {
        for(j = i; j <= m; j ++)
            if(E[j].w != E[i].w) break;
        if(j - i > 1) Cal(i, j - 1);
        else
        {
            int p = find(E[i].u), q = find(E[i].v);
            if(p != q) set[p] = q;
            sum --;
        }
    }
    if(sum > 1) printf("0");
    else printf("%d\n", ans);
    return 0;
} 

原文地址:https://www.cnblogs.com/twilight-sx/p/9064208.html

时间: 2024-10-14 10:37:26

【算法】Matrix - Tree 矩阵树定理 & 题目总结的相关文章

最小生成树计数(matrix tree矩阵树定理)

Matrix-Tree 定理是解决生成树计数问题最有力的武器之一.它首先于 1847 年被Kirchhoff 证明. 在介绍定理之前, 我们首先明确几个概念: 1. G 的度数矩阵 D[G]是一个 n*n 的矩阵, 并且满足: 当 i≠j 时,dij=0:当 i=j时, dij 等于 vi 的度数. 2. G 的邻接矩阵 A[G]也是一个 n*n 的矩阵, 并且满足: 如果 vi. vj 之间有边直接相连,则 aij=1,否则为 0. 我们定义 G 的 Kirchhoff 矩阵(也称为拉普拉斯算

@算法 - [email&#160;protected] matrix - tree 定理(矩阵树定理)

目录 @0 - 参考资料@ @0.5 - 你所需要了解的线性代数知识@ @1 - 定理主体@ @证明 part - [email protected] @证明 part - [email protected] @证明 part - [email protected] @证明 part - 4@ @2 - 一些简单的推广@ @3 - 例题与应用@ @0 - 参考资料@ MoebiusMeow 的讲解(超喜欢这个博主的!) 网上找的另外一篇讲解 @0.5 - 你所需要了解的线性代数知识@ 什么是矩阵

CSU 1805 Three Capitals(矩阵树定理+Best定理)

http://acm.csu.edu.cn/csuoj/problemset/problem?pid=1805 题意: A和B之间有a条边,A和G之间有b条边,B和G之间有c条边.现在从A点出发走遍所有的边,然后再回到A点,问一共有多少种方法. 思路: 16年湖南省赛题目,这道题目是求欧拉回路的个数,和生成树的计数有一定的联系. 首先给出神奇的Best定理,这是什么鬼定理,反正查不到什么有关该定理的文章... $ec(G)=t_s(G)\cdot deg(s)! \cdot \prod_{v\i

【bzoj2467】[中山市选2010]生成树 矩阵树定理

题目描述 有一种图形叫做五角形圈.一个五角形圈的中心有1个由n个顶点和n条边组成的圈.在中心的这个n边圈的每一条边同时也是某一个五角形的一条边,一共有n个不同的五角形.这些五角形只在五角形圈的中心的圈上有公共的顶点.如图0所示是一个4-五角形圈. 现在给定一个n五角形圈,你的任务就是求出n五角形圈的不同生成树的数目.还记得什么是图的生成树吗?一个图的生成树是保留原图的所有顶点以及顶点的数目减去一这么多条边,从而生成的一棵树. 注意:在给定的n五角形圈中所有顶点均视为不同的顶点. 输入 输入包含多

【bzoj4894】天赋 矩阵树定理

题目描述 小明有许多潜在的天赋,他希望学习这些天赋来变得更强.正如许多游戏中一样,小明也有n种潜在的天赋,但有一些天赋必须是要有前置天赋才能够学习得到的.也就是说,有一些天赋必须是要在学习了另一个天赋的条件下才能学习的.比如,要想学会"开炮",必须先学会"开枪".一项天赋可能有多个前置天赋,但只需习得其中一个就可以学习这一项天赋.上帝不想为难小明,于是小明天生就已经习得了1号天赋-----"打架".于是小明想知道学习完这n种天赋的方案数,答案对1

【Luogu】P3317重建(高斯消元+矩阵树定理)

题目链接 因为这个专门跑去学了矩阵树定理和高斯消元qwq 不过不是很懂.所以这里只放题解 玫葵之蝶的题解 某未知dalao的矩阵树定理 代码 #include<cstdio> #include<cstdlib> #include<cctype> #include<algorithm> #include<cstring> #include<cmath> #define eps 1e-8 #define maxn 100 using na

bzoj4031 [HEOI2015]小Z的房间——矩阵树定理

题目:https://www.lydsy.com/JudgeOnline/problem.php?id=4031 矩阵树定理的模板题(第一次的矩阵树定理~): 有点细节,放在注释里了. 代码如下: #include<iostream> #include<cstdio> #include<cstring> using namespace std; typedef long long ll; int const mod=1e9; int n,m,a[105][105],d[

luoguP3317 [SDOI2014]重建 变元矩阵树定理 + 概率

首先,我们需要求的是 $$\sum\limits_{Tree} \prod\limits_{E \in Tree} E(u, v) \prod\limits_{E \notin Tree} (1 - E(u, v))$$ 我们知道变元矩阵树定理 ---> 不知道请见此 我们自然希望要求和的事物只跟生成树的边有关 因此考虑把$\prod\limits_{E \notin Tree} (1 - E(u, v))$转化为$\prod\limits_{E} (1 - E(u, v)) * \frac{1

bzoj4596/luoguP4336 [SHOI2016]黑暗前的幻想乡(矩阵树定理,容斥)

bzoj4596/luoguP4336 [SHOI2016]黑暗前的幻想乡(矩阵树定理,容斥) bzoj Luogu 题解时间 看一看数据范围,求生成树个数毫无疑问直接上矩阵树定理. 但是要求每条边都属于不同公司就很难直接实现. 按套路上容斥: 如果直接将几个公司的修路列表加进矩阵里的话,求出来的是"只使用"这些边的生成树个数. 很明显上容斥之后就会直接变成"只使用"且"每个都被使用"的个数. 正好符合题目要求的生成树的n-1条边分属于n-1个公