[BZOJ]4031: [HEOI2015]小Z的房间(Matrix-Tree定理模板+欧几里得+高斯消元)

4031: [HEOI2015]小Z的房间
  • 题意就是求无向图的生成树个数。点数在100以内.

  • Matrix-Tree定理的模板啊.

  • 然后就有了下面这份代码,注意,这里是不带模数的,用实数去储存,但当数比较大后就会有精度,所以要用更高级的欧几里得算法.

  • 用Matrix-Tree定理要注意两点:

    • 得到基尔霍夫矩阵之后要取个绝对值!最后答案也是!!
    • 记得把\(n*n\)矩阵大小剪成\((n-1)*(n-1)\)!!
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>

#define F(i, a, b) for (int i = a; i <= b; i ++)
#define N 20

using namespace std;

char ch[N][N]; int n, m, cnt, D[N][N], A[N][N], B[N][N];
struct node { long double a[N]; } C[N];

void R(int &x) {
    char c = getchar(); x = 0; int t = 1;
    for (; !isdigit(c); c = getchar()) t = (c == '-' ? - 1 : t);
    for (; isdigit(c); x = (x << 3) + (x << 1) + c - '0', c = getchar()); x *= t;
}

void Kirchhoff() {
    F(i, 1, n)
        F(j, 1, m)
            if (ch[i][j] == '.')
            {
                B[i][j] = ++ cnt;
                if (ch[i - 1][j] == '.')
                {
                    A[B[i - 1][j]][cnt] = A[cnt][B[i - 1][j]] = 1;
                    D[B[i - 1][j]][B[i - 1][j]] ++;
                    D[cnt][cnt] ++;
                }
                if (ch[i][j - 1] == '.')
                {
                    A[B[i][j - 1]][cnt] = A[cnt][B[i][j - 1]] = 1;
                    D[B[i][j - 1]][B[i][j - 1]] ++;
                    D[cnt][cnt] ++;
                }
            }
    
    F(i, 1, cnt)
        F(j, 1, cnt)
            C[i].a[j] = abs(D[i][j] - A[i][j]);
}

void Solve() {
    int k = 1; long double ans = 1;
    
    cnt -- ;

    F(i, 1, cnt)
    {
        int now = 0;
        F(j, k, cnt)
            if (C[j].a[i]) { now = j; break; }

        if (now)
        {
            ans = ans * (- 1);
    
            swap(C[k], C[now]);
            
            F(j, now + 1, cnt)
                if (C[j].a[i])
                {
                    long double v = (long double) C[j].a[i] / C[now].a[i];
                    F(p, 1, cnt)
                        C[j].a[p] = C[j].a[p] - C[now].a[p] * v;
                }
            
            k ++;
        }
        
        ans = ans * C[i].a[i];
    }
    
    printf("%0.Lf\n", abs(ans));
}

int main() {
    R(n), R(m);
    F(i, 1, n)
        scanf("%s", ch[i] + 1);

    Kirchhoff();
    
    Solve();
}
  • 于是,我们假定!模数是一个被磨烂的质数.

  • 那么就可以上逆元!!!

  • 很好,于是就有了下面这份假代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>

#define ll long long
#define F(i, a, b) for (ll i = a; i <= b; i ++)
#define N 1010

const int mo = 1e9 + 7;

using namespace std;

char ch[N][N]; ll n, m, cnt, D[N][N], A[N][N], B[N][N];
struct node { ll a[N]; } C[N];

void R(ll &x) {
    char c = getchar(); x = 0; ll t = 1;
    for (; !isdigit(c); c = getchar()) t = (c == '-' ? - 1 : t);
    for (; isdigit(c); x = (x << 3) + (x << 1) + c - '0', c = getchar()); x *= t;
}

ll ksm(ll x, ll y) {
    ll ans = 1;
    for (; y; y >>= 1, x = (1LL * x * x) % mo)
        if (y & 1) ans = (1LL * ans * x) % mo;
    return ans;
}

void Kirchhoff() {
    F(i, 1, n)
        F(j, 1, m)
            if (ch[i][j] == '.')
            {
                B[i][j] = ++ cnt;
                if (ch[i - 1][j] == '.')
                {
                    A[B[i - 1][j]][cnt] = A[cnt][B[i - 1][j]] = 1;
                    D[B[i - 1][j]][B[i - 1][j]] ++;
                    D[cnt][cnt] ++;
                }
                if (ch[i][j - 1] == '.')
                {
                    A[B[i][j - 1]][cnt] = A[cnt][B[i][j - 1]] = 1;
                    D[B[i][j - 1]][B[i][j - 1]] ++;
                    D[cnt][cnt] ++;
                }
            }
    
    F(i, 1, cnt)
        F(j, 1, cnt)
            C[i].a[j] = D[i][j] - A[i][j];
}

void Solve() {
    ll k = 1, ans = 1;
    
    cnt -- ;

    F(i, 1, cnt)
    {
        ll now = 0;
        F(j, k, cnt)
            if (C[j].a[i]) { now = j; break; }

        if (now)
        {
            swap(C[k], C[now]);
            
            F(j, now + 1, cnt)
                if (C[j].a[i])
                {
                    ll v = (1LL * C[j].a[i] * ksm(C[now].a[i], mo - 2)) % mo;
                    F(p, 1, cnt)
                        C[j].a[p] = (C[j].a[p] - 1LL * C[now].a[p] * v) % mo;
                }
            
            k ++;
        }
        
        ans = (1LL * ans * C[i].a[i]) % mo;
    }

    ans = (ans + mo) % mo;
    printf("%lld\n", ans);
}

int main() {
    R(n), R(m);
    F(i, 1, n)
        scanf("%s", ch[i] + 1);

    Kirchhoff();
    
    Solve();
}
  • 注意到上面的代码中,我并没有选择交换两行.

  • 根据行列式的性质,所以什么都不需要做...

  • 但是如果要用一般的高斯消元,我们选择交换两行,则一定要mark一下,最后判断一下是否要减一下ans。

  • 像这样:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>

#define ll long long
#define F(i, a, b) for (ll i = a; i <= b; i ++)
#define N 1010

const int mo = 1e9 + 7;

using namespace std;

char ch[N][N]; ll n, m, cnt, D[N][N], A[N][N], B[N][N];
struct node { ll a[N]; } C[N];

void R(ll &x) {
    char c = getchar(); x = 0; ll t = 1;
    for (; !isdigit(c); c = getchar()) t = (c == '-' ? - 1 : t);
    for (; isdigit(c); x = (x << 3) + (x << 1) + c - '0', c = getchar()); x *= t;
}

ll ksm(ll x, ll y) {
    ll ans = 1;
    for (; y; y >>= 1, x = (1LL * x * x) % mo)
        if (y & 1) ans = (1LL * ans * x) % mo;
    return ans;
}

void Kirchhoff() {
    F(i, 1, n)
        F(j, 1, m)
            if (ch[i][j] == '.')
            {
                B[i][j] = ++ cnt;
                if (ch[i - 1][j] == '.')
                {
                    A[B[i - 1][j]][cnt] = A[cnt][B[i - 1][j]] = 1;
                    D[B[i - 1][j]][B[i - 1][j]] ++;
                    D[cnt][cnt] ++;
                }
                if (ch[i][j - 1] == '.')
                {
                    A[B[i][j - 1]][cnt] = A[cnt][B[i][j - 1]] = 1;
                    D[B[i][j - 1]][B[i][j - 1]] ++;
                    D[cnt][cnt] ++;
                }
            }
    
    F(i, 1, cnt)
        F(j, 1, cnt)
            C[i].a[j] = D[i][j] - A[i][j];
}

void Solve() {
    ll ans = 1, mak = 0;
    
    cnt -- ;

    F(i, 1, cnt)
    {
        int num = i;
        F(j, i + 1, cnt)
            if (abs(C[j].a[i]) > abs(C[num].a[i])) num = j;
        if (num ^ i) swap(C[i], C[num]), mak ^= 1;
        F(j, i + 1, cnt)
            if (C[j].a[i])
            {
                ll v = (1LL * C[j].a[i] * ksm(C[i].a[i], mo - 2)) % mo;
                F(p, 1, cnt)
                    C[j].a[p] = (C[j].a[p] - 1LL * C[i].a[p] * v) % mo;
            }
        
        ans = (1LL * ans * C[i].a[i]) % mo;
    }

    ans = (ans + mo) % mo;
    printf("%lld\n", mak == 1 ? mo - ans : ans);
}

int main() {
    R(n), R(m);
    F(i, 1, n)
        scanf("%s", ch[i] + 1);

    Kirchhoff();
    
    Solve();
}
  • 然后我们就开始搞这个恶心的质数了...

  • 丧心病狂出题人,偏要来个10^9的模数,不过没关系,我们有欧几里得大神的思想...

  • 例如我要使对应数值为\((a,b)\)\(b\)\(0\),则我们可以使\(b\)所在行+\(a\)所在行\(*b/a\)使得,\((a,b)\rightarrow (a,b\%a)\),交换两行,然后做相同操作.

  • 直至\(a,b\)中出现\(0\)为止.

  • 这其实就是运用了辗转相除的思想,因为不能直接等量替换,所以我们用时间换技巧.

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>

#define ll long long
#define F(i, a, b) for (ll i = a; i <= b; i ++)
#define N 1010

const int mo = 1e9;

using namespace std;

char ch[N][N]; ll n, m, cnt, D[N][N], A[N][N], B[N][N];
struct node { ll a[N]; } C[N];

void R(ll &x) {
    char c = getchar(); x = 0; ll t = 1;
    for (; !isdigit(c); c = getchar()) t = (c == '-' ? - 1 : t);
    for (; isdigit(c); x = (x << 3) + (x << 1) + c - '0', c = getchar()); x *= t;
}

void Kirchhoff() {
    F(i, 1, n)
        F(j, 1, m)
            if (ch[i][j] == '.')
            {
                B[i][j] = ++ cnt;
                if (ch[i - 1][j] == '.')
                {
                    A[B[i - 1][j]][cnt] = A[cnt][B[i - 1][j]] = 1;
                    D[B[i - 1][j]][B[i - 1][j]] ++;
                    D[cnt][cnt] ++;
                }
                if (ch[i][j - 1] == '.')
                {
                    A[B[i][j - 1]][cnt] = A[cnt][B[i][j - 1]] = 1;
                    D[B[i][j - 1]][B[i][j - 1]] ++;
                    D[cnt][cnt] ++;
                }
            }
    
    F(i, 1, cnt)
        F(j, 1, cnt)
            C[i].a[j] = D[i][j] - A[i][j];
}

void Solve() {
    ll ans = 1, mak = 0;
    
    cnt -- ;

    F(i, 1, cnt)
    {
        int num = i;
        F(j, i + 1, cnt)
            if (abs(C[j].a[i]) > abs(C[num].a[i])) num = j;
        if (num ^ i) swap(C[i], C[num]), mak ^= 1;
        F(j, i + 1, cnt)
            while (C[j].a[i]) {
                ll tmp = C[j].a[i] / C[i].a[i];
                F(k, 1, cnt)
                    C[j].a[k] = (C[j].a[k] + mo - tmp * C[i].a[k] % mo) % mo;
                if (C[j].a[i] == 0) break;
                mak ^= 1;
                swap(C[j], C[i]);
            }
        
        ans = (1LL * ans * C[i].a[i]) % mo;
    }

    ans = (ans + mo) % mo;
    printf("%lld\n", mak == 1 ? mo - ans : ans);
}

int main() {
    R(n), R(m);
    F(i, 1, n)
        scanf("%s", ch[i] + 1);

    Kirchhoff();
    
    Solve();
}
  • 至此,这道SB题终于被我们搞定了.

  • 总结一下,这题用到了线性代数的基本知识,行列式以及Matrix-tree定理.

  • 其中,关于Matrix-Tree定理的介绍有很多,这里就不详细讲.

  • 这里详细讲讲行列式的相关性质.

  • 在构造基尔霍夫矩阵时,定理告诉我们,只需把\(C[G]=D[G]-A[G]\)即可.

  • 其中\(D\)矩阵称为度数矩阵,即只有当\(i=j\)时,\(D[i][j] = f(v_i)|f表示度数\)\(A\)矩阵则是边矩阵,即两个点之间有边相连时,\(A_{ij}=1\),否则也为\(0\)

  • 然后再根据\(Matrix-tree\)定理,我们知道,最终的生成树个数就是\(C\)矩阵的某一个余子式的值.

  • 要求解行列式的值,首先我们需要知道其的两个定义:

  • \[\det(A)=\sum_{j=1}^n a_{ij}*(-1)^{i+j}*M_{ij}\]其中\(M_{ij}\)是余子式。
  • \[\det(A)=\sum_{(p_1p_2...p_n)}(-1)^{\tau(p_1p_2...p_n)}a_{1,p_1}a_{2,p_2}...a_{n,p_n}\]其中\(\tau(p_1p_2...p_n) = \tau_2+\tau_3+...+\tau_n\)\(\tau_i\)表示排列中前\(i\)个数有多少个数大于\(p_i\)

  • 啊?为什么同一种东西会有两种定义?其实你会发现,根据第二种定义,完全可以推导出第一种定义,只不过表达方式的不同罢了.

  • 因为我们要求行列式的值,所以我们可以直接根据定义去求,根据第一个定义,不太好下手...

  • 根据第二个定义,很好下手,但是这是\(n!\)级别的复杂度,不可取.

  • 于是我们需要一些性质:

  • 一个比较显然的性质是,交换逆序对当中的两个不同位置,逆序对的个数奇偶性会发生改变.

  • 然后我们还需要知道,有\(det(A)=det(A^T)\),对于这个的证明,网上有一大部分是不屑于证,一大部分是说根据定义,然而,一个东西怎么会有两种定义,只能说根据一种定义去推出另外一种等价的定义吧。

  • 所以让我们尝试着证明一下,\(\det(A)=\det(A^T)\),我们记\(b_{ij}=a_{ji}\)

  • 因为\[\det(A^T)=\sum_{(p_1p_2...p_n)}(-1)^{\tau(p_1p_2...p_n)}b_{1,p_1}b_{2,p_2}...b_{n,p_n}\]

  • 所以我们只需证\[\det(A)=\sum_{(p_1p_2...p_n)}(-1)^{\tau(p_1p_2...p_n)}a_{p_1,1}a_{p_2,2}...a_{p_n.n}\]即可.

  • 我们称\(1,2,...n\)这样的排列为标准排列,那么对于\(det(A^T)\)而言,其行排列为标准排列,对于\(\det(A)\)而言,其纵排列为标准排列.

  • 所以我们有,\[\tau(p_1...p_i...p_j...p_n)=-\tau(p_1...p_j...p_i...p_n)=\tau(1...j...i..n)+\tau(p_1...p_j...p_i...p_n)\]

  • 然后我们就发现了,虽然交换一个排列的两个数,其逆序对个数的奇偶性会发生改变,但对于行排列和纵排列的逆序对个数之和的奇偶性是没有变的.

  • 所以我们总可以通过对换,使得纵排列\(p_1...p_j...p_i...p_n\)变为标准排列,\(1,2...i...j...n\)变为某个新的排列,记此新排列为\(q_1,q_2,....q_n\),则\[(-1)^{\tau(p_1,p_2...p_n)}a_{1,p_1}a_{2,p_2}...a_{n,p_n}=(-1)^{\tau(q_1,q_2...q_n)}a_{q_1,1}a_{q_2,2}...a_{q_n,n}\]

  • 综上,命题得证.

  • 根据这个性质,我们可以发现,交换一个行列式中的两行或两列交换,行列值的值会变为其的相反数.

  • 那么我们还可以推出,如果在行列式中有两个一模一样的行或列,那么行列式的值就是\(0\)

  • 虽然上面这个性质貌似没什么用,但却恰好能验证我们上面的结论.

  • 继续推,我们会发现,行列式是可以分割的.

  • 即有类似下面的性质:
    \[\begin{align} A&=\begin{vmatrix} {a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ {\vdots}&{\vdots}&{\ddots}&{\vdots}\\ {b_{i1}+c_{i1}}&{b_{i2}+c_{i2}}&{\cdots}&{b_{in}+c_{in}}\\ {\vdots}&{\vdots}&{\ddots}&{\vdots}\\ {a_{n1}}&{a_{n2}}&{\cdots}&{a_{nn}}\\ \end{vmatrix}\\ &=\begin{vmatrix} {a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ {\vdots}&{\vdots}&{\ddots}&{\vdots}\\ {b_{i1}}&{b_{i2}}&{\cdots}&{b_{in}}\\ {\vdots}&{\vdots}&{\ddots}&{\vdots}\\ {a_{n1}}&{a_{n2}}&{\cdots}&{a_{nn}}\\ \end{vmatrix}+\begin{vmatrix} {a_{11}}&{a_{12}}&{\cdots}&{a_{1n}}\\ {\vdots}&{\vdots}&{\ddots}&{\vdots}\\ {c_{i1}}&{c_{i2}}&{\cdots}&{c_{in}}\\ {\vdots}&{\vdots}&{\ddots}&{\vdots}\\ {a_{n1}}&{a_{n2}}&{\cdots}&{a_{nn}}\\ \end{vmatrix}\\ &=\hat A+\dot A \end{align} \]

  • 这个的证明实际上根据第二个定义就可以了.

  • 因为有了这个重要的性质,所以我们可以推断出,即如果有两个行列式,然后我把行列式中的某一行直接加到另一行去,行列式的值不会改变.

  • 证明:因为行列式可割嘛,所以我们可以把行列式新增的这一行分割成一个新的行列式,这个新割的行列式有两个相同的行,所以值为0.

  • 然后类似的,我们可以推断,如果把行列式中的某一行乘以某个数再加到另一行去,行列式的值也不会改变.

  • 然后我们就可以用普通的高斯消元去求解啦~~~~

  • 是不是很简单?

  • 最后对角线的乘积即为行列的值,根据两个定义你都可以推断出来.

猜你喜欢

转载自www.cnblogs.com/Pro-king/p/9383440.html