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.
然后类似的,我们可以推断,如果把行列式中的某一行乘以某个数再加到另一行去,行列式的值也不会改变.
然后我们就可以用普通的高斯消元去求解啦~~~~
是不是很简单?
最后对角线的乘积即为行列的值,根据两个定义你都可以推断出来.