题解:P6178 【模板】Matrix-Tree 定理
Blog
1. 基础概念
根据图论里的知识,我们已经知道了邻接矩阵
这里我们引入一个新的概念:度数矩阵
对于无向图而言,度数矩阵的定义为
而对于有向图而言,度数矩阵则分为入度矩阵和出度矩阵。具体而言,入度矩阵的定义为
而对于带权图,此处的度数定义为对应边的权值之和。
由此可以引出图的 Laplace 矩阵(拉普拉斯矩阵,又称 Kirchhoff 基尔霍夫矩阵):
- 对于无向图,Laplace 矩阵
L 定义为D - A 的值。 - 对于有向图:
- 入度 Laplace 矩阵
L^{\mathrm{in}} = D^{\mathrm{in}} - A 。 - 出度 Laplace 矩阵
L^{\mathrm{out}} = D^{\mathrm{out}} - A 。
- 入度 Laplace 矩阵
最后还有一个矩阵:图的关联矩阵
其中
2. Matrix-Tree 定理
定理
定理
此处“根向树形图”又称内向树。同理下文中“叶向树形图”又称外向树。
定理
3. Matrix-Tree 定理证明
容易发现定理
下文中,引理
引理
证明:
对分块矩阵的行列式\begin{vmatrix} I_m & B \\ A & I_n\end{vmatrix} 进行变换。具体而言,分别将第一行倍加到第二行、将第二行倍加到第一行:\begin{vmatrix} I_m & B \\ A & I_n\end{vmatrix} = \begin{vmatrix} I_m & B \\ O & I_n - AB\end{vmatrix} = \begin{vmatrix} I_m-BA & O \\ A & I_n\end{vmatrix} 将该主对角型行列式算出后得
|I_n - AB| = |I_m - BA| 。
引理
证明:
对于\lambda = 0 的情况,显然是成立的。而对于\lambda\ne 0 的情况,\lambda^{-1} 显然存在,因此有:\begin{aligned}\lambda^m|\lambda I_n - AB| &= \lambda^{n+m}|I_n - \lambda^{-1}AB| \\ &= \lambda^{n+m}|I_m - \lambda^{-1}BA| \\ &= \lambda^{n}|\lambda I_m - BA|\end{aligned} 其中第二个等号用到了上面引理
1 换元后的结论。
引理
证明:
注意到\lambda I_m-P 是形如下面的矩阵:\begin{pmatrix} \lambda-p_{1,1} & -p_{1, 2} & \cdots & -p_{1, m}\\ -p_{2, 1} & \lambda-p_{2,2} & \cdots & -p_{2, m}\\ \vdots & \vdots & \ddots & \vdots \\ -p_{m, 1} & -p_{m, 2} & \cdots & \lambda-p_{m, m}\end{pmatrix} 考虑暴力展开行列式时行顺排的过程,钦定乘积中
\lambda 的幂次为m-i ,那么就相当于钦定了m-i 个元素必须要在主对角线上,且乘积中选择\lambda 一项;其余元素无论在哪一列,乘积中一律选择-p 一项。那么可以得到式子:(-1)^ic_i=\sum_{\sigma \in S_{m}}\sum_{J_i}\prod_{j = 1}^m [j\notin J_i](-p_{j,\sigma(j)}) 其中
J_i 表示所有满足|J| = m-i 且所有元素均为排列\sigma 中的不动点的集合J 。
交换求和顺序并化简后可得式子:(-1)^ic_i=(-1)^{i}\sum_{J_i}\sum_{\sigma \in S_{m}}\prod_{j = 1}^m [j\notin J_i]p_{j,\sigma(j)} 注意到
\sum_{\sigma \in S_{m}}\prod_{j = 1}^m [j\notin J_i]p_{j,\sigma(j)} 部分很像行列式的暴力展开形式,于是可以继续化简为:(-1)^ic_i = (-1)^{i}\sum_{t \subseteq [m] \wedge |t|=m-i}\det P_{[m]\setminus t,[m]\setminus t} 得到结论:
c_i = \sum_{t\subseteq [m] \wedge |t|=m-i}\det P_{[m]\setminus t,[m]\setminus t} 即:
c_i = \sum_{S\subseteq [m] \wedge |S|=i}\det P_{S,S} 这个结论同样可以引入 Kronecker 符号更加严谨地进行证明,此处仅为简单解释一下原理。
引理
证明:
根据前文的引理2 ,有\lambda^m|\lambda I_n - AB| = \lambda^n|\lambda I_m - BA| ,移项后得:\lambda^{m-n}|\lambda I_n - AB| = |\lambda I_m - BA| 比较左右两侧
\lambda^{m-n} 的系数。容易发现等式左侧\lambda^{m-n} 的系数相当于要求|\lambda I_n - AB| 中\lambda 的幂次为0 ,因此等式左侧的系数为(-1)^n\det AB ;而等式右侧\lambda^{m-n} 的系数可以套用引理3 的结论,所以系数为(-1)^nc_n = (-1)^n\sum_{S\subseteq [m] \wedge |S|=i}\det P_{S,S} 。
显然等式左右两侧\lambda 对应幂次的系数相等,因此有:(-1)^n\det AB = (-1)^n\sum_{S\subseteq [m] \wedge |S|=n}\det (BA)_{S,S} 化简得:
\det AB = \sum_{S\subseteq [m] \wedge |S|=n}\det (BA)_{S,S} 因为留下的是主子式,所以有:
\det (BA)_{S,S} = \det{B_{S, [n]}A_{[n],S}} = \det{B_{S, [n]}} \times \det{A_{[n], S}}=\det{A_{[n], S}} \times \det{B_{S, [n]}} 因此得到结论:
\det AB = \sum_{S\subseteq [m] \wedge |S|=n}\det{A_{[n], S}} \times \det{B_{S, [n]}}
引理
证明很容易,直接考虑矩阵乘法的过程,模拟就能发现两个
根据这个结论我们还可以得出:
引理
证明:
首先我们可以将子图中所有的边权全部转化为1 。原因在于这两个行列式中的每一行都可以提出一个公因数\sqrt{w_i} ,且这些公因数的乘积等于树的权值w(T) 。
然后先对\det (M^{\mathrm{out}})^{\mathrm{T}} 进行分析。显然里面没有任何一列全部为0 。但是如果存在任意一行全部为0 的时候(即一个点没有出边),该行列式的值一定为0 ,此时一定无法组成根向树形图。但是我们无法得到其他信息了,只能得到结论:每个点的出边数为1 是形成根向树形图的必要不充分条件。
又因为当\det (M^{\mathrm{out}})^{\mathrm{T}}\ne 0 的时候,每个点的出边数必然为1 。所以我们只需要判断图中是否存在环即可。
再对\det (M^{\mathrm{out}}-M^{\mathrm{in}}) 进行分析。对每一行(每条边)进行考虑,只要这条边直接指向的不是根(根不在集合W 中),那么必然存在一个数是1 ,另一个数为-1 。我们考虑消掉这个-1 ,具体而言,假设当前的边是a\to b ,因为b\ne \text{root} ,所以必然存在另一条边b\to c 。此时第二条边所在行的第b 列必然为1 ,而第一条边所在行的第b 列为-1 ,利用行列式的倍加性质,可以使得第一条边所在行的第b 列变成0 ,而第c 列变成-1 。如此往复,直到“第二条边”直接指向根节点,第c 列就会变成0 了。此时这一行只有第a 列为1 。这个构造的过程很像不断往父亲处跳,直到跳到根节点的过程。
按照这种方法模拟下去,如果某一行被完全消成了0 ,就说明这张图存在环,行列式一定为0 。否则每一行只有起点所在列是1 ,其余全部为0 ,行列式显然为1 。
接下来就可以来证明 Matrix-Tree 定理了,我们以定理
其中
根据引理
接下来的定理
4. Cayley 公式
定理
证明:
原命题相当于给定了一张n 个点的无向完全图,求生成树个数。
考虑套用矩阵树定理,首先写出矩阵的形态:\begin{pmatrix} n-1 & -1 & \cdots & -1 \\ -1 & n-1 & \cdots & -1 \\ \vdots & \vdots & \ddots & \vdots\\ -1 & -1 & \cdots & n-1\end{pmatrix}_{n\times n} 把第一行第一列去掉,得到:
\begin{pmatrix} n-1 & -1 & \cdots & -1 \\ -1 & n-1 & \cdots & -1 \\ \vdots & \vdots & \ddots & \vdots\\ -1 & -1 & \cdots & n-1\end{pmatrix}_{(n-1)\times (n-1)} 现在我们只需要求出它的行列式即可。
这显然是行列式求值里“列和相等”的模型。具体而言,我们注意到每一列元素之和相等,于是把所有行全部利用倍加性质加到第一行上,得:\begin{vmatrix} 1 & 1 & \cdots & 1 \\ -1 & n-1 & \cdots & -1 \\ \vdots & \vdots & \ddots & \vdots\\ -1 & -1 & \cdots & n-1\end{vmatrix}_{(n-1)\times (n-1)} 接下来再用第一行倍加到其他行上,把
-1 项消掉,得:\begin{vmatrix} 1 & 1 & \cdots & 1 \\ 0 & n & \cdots & 0 \\ \vdots & \vdots & \ddots & \vdots\\ 0 & 0 & \cdots & n\end{vmatrix}_{(n-1)\times (n-1)} 这是一个主对角型行列式,只要将对角线上的元素乘起来就能得到行列式的值,即
n^{n-2} 。
5. BEST 定理
定理
根据这个定理,我们可以延伸出 BEST 定理的作用:求有向图中欧拉回路的个数。
需要注意的是,普通无向图中欧拉回路的计数是 NPC,无法在多项式时间复杂度内解决。
定理
其中,
证明:
将定理的形式转化为\deg(k)\times \mathrm{ec}(G) = t^{\mathrm{root}}(G, k)\times \deg(k)!\times \prod_{1\le i \le n, i\ne k} (\deg(i) - 1)! 。
等式右侧的意义为:从欧拉回路中选出每个点(点k 除外)最后走的那条边,这些边一定构成了一个以k 为根的根向树形图。然后点k 和其他点都随便钦定出边的顺序,按照每个点出边的顺序走就一定能构成欧拉回路。
等式左侧还有个\deg(k) 的原因是,循环同构的欧拉回路算作同一个欧拉回路,需要用除法去掉。例如1\to 2\to 1\to 3\to 1 和1\to 3\to 1\to 2\to 1 是同一个欧拉回路。
接下来证明除k 以外,其余点最后走的那条边一定构成根向树形图。
因为一共只有n-1 条边,所以我们只需要证明子图中不存在环即可。
考虑反证法,如果子图中存在环,那么想要构成欧拉回路,环上必然存在一条边,排在所谓的“最后一条边”后面,并且直接或者间接地指向k 。那么就说明了这些边并不是最后走的一条边,与题设矛盾,因此子图中不存在环。
注意这只是一个不严谨的证明,想要看严谨证明的可以参考 OIwiki 中的双射构造。
需要注意的是,如果我们不把循环同构的回路当做同一个欧拉回路,那么等式左侧就不要加那个
6. 例题
打星号的不是我自己想出来的题。
6.1 P6178 【模板】Matrix-Tree 定理
最模板的矩阵树定理,直接套用定理
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 305;
const ll mod = 1e9 + 7;
ll qpow(ll a, ll b)
{
ll res = 1;
while(b)
{
if(b & 1) res = (res * a) % mod;
b >>= 1;
a = (a * a) % mod;
}
return res;
}
ll Det(int n, ll a[N][N])
{
bool flag = 0; ll ans = 1;
for(int i = 1; i <= n; i++)
{
for(int j = i + 1; j <= n && a[i][i] == 0; j++)
if(a[j][i])
swap(a[i], a[j]), flag ^= 1;
for(int j = i + 1; j <= n; j++)
{
ll c = a[j][i] * qpow(a[i][i], mod - 2) % mod;
for(int k = i; k <= n; k++)
a[j][k] = ((a[j][k] - c * a[i][k]) % mod + mod) % mod;
}
ans = (ans * a[i][i]) % mod;
}
return (flag ? (mod - ans) % mod : ans);
}
int n, m, t;
ll a[N][N], b[N][N];
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> t;
for(int i = 1; i <= m; i++)
{
ll u, v, w;
cin >> u >> v >> w;
if(!t)
{
a[u][u] += w;
a[v][v] += w;
a[u][v] -= w;
a[v][u] -= w;
}
else
{
a[v][v] += w;
a[u][v] -= w;
}
}
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
b[i][j] = (a[i + 1][j + 1] % mod + mod) % mod;
cout << Det(n - 1, b);
return 0;
}
6.2 SP104 HIGH - Highways
同样是矩阵树定理无向图形式的模板题,但是因为这题没有模数,所以需要使用辗转相除法求行列式的值。时间复杂度依然是
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 15;
ll Det(ll n, ll a[N][N])
{
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i + 1; j <= n; j++)
{
while(a[j][i])
{
ll c = a[i][i] / a[j][i];
for(int k = i; k <= n; k++)
a[i][k] -= c * a[j][k];
Swap(i, j);
}
}
res = res * a[i][i];
}
return (flag ? -res : res);
}
ll n, m, a[N][N], b[N][N];
void solve()
{
cin >> n >> m;
memset(a, 0, sizeof(a));
while(m--)
{
ll u, v;
cin >> u >> v;
a[u][u]++; a[v][v]++;
a[u][v]--; a[v][u]--;
}
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
b[i][j] = a[i + 1][j + 1];
cout << Det(n - 1, b) << "\n";
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--) solve();
return 0;
}
6.3 P4111 [HEOI2015] 小 Z 的房间
题意转化为给定一张无向网格图(部分点被删掉),求生成树个数。
注意到数据范围很小,在满数据点下点数也只有
时间复杂度
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 105;
const ll mod = 1e9;
const int gox[] = {0, 0, 1, -1};
const int goy[] = {1, -1, 0, 0};
int n, m, a[N][N], id[N][N], cnt;
ll b[N][N];
bool legal(int x, int y)
{
return (1 <= x && x <= n && 1 <= y && y <= m);
}
ll Det(ll n, ll a[N][N])
{
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i + 1; j <= n; j++)
{
while(a[j][i])
{
ll c = a[i][i] / a[j][i];
for(int k = i; k <= n; k++)
a[i][k] = (a[i][k] - c * a[j][k]) % mod;
Swap(i, j);
}
}
res = (res * a[i][i]) % mod;
}
if(flag) res = -res;
return (res % mod + mod) % mod;
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
char x;
cin >> x;
if(x == '.') id[i][j] = ++cnt;
}
}
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
if(!id[i][j]) continue;
int u = id[i][j];
for(int k = 0; k < 4; k++)
{
int tx = i + gox[k], ty = j + goy[k];
if(!legal(tx, ty)) continue;
int v = id[tx][ty];
if(!v) continue;
if(u < v) continue;
a[u][u]++; a[v][v]++;
a[u][v]--; a[v][u]--;
}
}
}
for(int i = 1; i < cnt; i++)
for(int j = 1; j < cnt; j++)
b[i][j] = a[i + 1][j + 1];
cout << Det(cnt - 1, b);
return 0;
}
6.4 P3317 [SDOI2014] 重建
容易将“概率”转化为树边的权值,这样一棵树出现的概率即为权值之积,可以使用矩阵树定理求解。
但是本题并不是模板的矩阵树定理。对于非树边,依然要乘上它不出现在图中的概率。这里我们考虑转化权值,具体而言,令常数
一个细节是,概率
- 将
p = 1 的边缩点后用矩阵树定理做。这种做法可能细节更多,但是精度肯定能做到更高。 - 将树边的权值定为
\dfrac{p_{i, j}}{1-p_{i, j}+\mathrm{eps}} ,其中\mathrm{eps} 是一个很小的数。这样就可以避免除以0 的错误了。代码中采用的就是这种方式。
时间复杂度
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 55;
const ldb eps = 1e-10;
int n;
ldb g[N][N], a[N][N], b[N][N], ans = 1;
ldb Det(int n, ldb a[N][N])
{
bool flag = 0; ldb res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++)
{
if(abs(a[j][i]) > eps)
{
if(i ^ j) Swap(i, j);
break;
}
}
for(int j = i + 1; j <= n; j++)
{
ldb c = a[j][i] / a[i][i];
for(int k = i; k <= n; k++)
a[j][k] -= c * a[i][k];
}
res *= a[i][i];
}
return (flag ? -res : res);
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
{
scanf("%Lf", &g[i][j]);
if(i > j) continue;
a[i][i] += g[i][j] / (1 - g[i][j] + eps);
a[j][j] += g[i][j] / (1 - g[i][j] + eps);
a[i][j] -= g[i][j] / (1 - g[i][j] + eps);
a[j][i] -= g[i][j] / (1 - g[i][j] + eps);
ans *= (1 - g[i][j] + eps);
}
}
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
b[i][j] = a[i + 1][j + 1];
printf("%.10Lf", ans * Det(n - 1, b));
return 0;
}
6.5 P4208 [JSOI2008] 最小生成树计数
MST 经典套路题。保证边权的重数不超过
考虑 Kruskal 的过程,对于边权相等的边,显然我们可以以任意顺序尝试将其加入最小生成树。因此,我们可以假设当前的边权为
- 若边权为
w 的边(u,v) 两端已经连通,那么这条边必然不在最小生成树中。 - 否则
(u,v) 两端不连通,可能出现在最小生成树里,因此把(u, v) 加入图G 中。
这个过程可以开一个桶和并查集实现。
最后对图
时间复杂度
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 105;
const ll mod = 31011;
int n, m;
map<int, vector<pi> > tot;
ll a[N][N], b[N][N];
struct DSU{
int fa[N];
void init()
{
for(int i = 1; i <= n; i++) fa[i] = i;
}
int findf(int x)
{
if(fa[x] != x) fa[x] = findf(fa[x]);
return fa[x];
}
void combine(int x, int y)
{
int fx = findf(x), fy = findf(y);
fa[fx] = fy;
}
} dsu;
ll Det(ll n, ll a[N][N])
{
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i + 1; j <= n; j++)
{
while(a[j][i])
{
ll c = a[i][i] / a[j][i];
for(int k = i; k <= n; k++)
a[i][k] = (a[i][k] - a[j][k] * c) % mod;
Swap(i, j);
}
}
res = (res * a[i][i]) % mod;
}
if(flag) res = -res;
return (res % mod + mod) % mod;
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
while(m--)
{
ll u, v, w;
cin >> u >> v >> w;
tot[w].push_back({u, v});
}
dsu.init();
for(auto itm : tot)
{
for(auto eg : itm.se)
{
int u = dsu.findf(eg.fi), v = dsu.findf(eg.se);
if(u == v) continue;
a[u][u]++; a[v][v]++;
a[u][v]--; a[v][u]--;
}
for(auto eg : itm.se)
{
int u = dsu.findf(eg.fi), v = dsu.findf(eg.se);
if(u == v) continue;
dsu.combine(u, v);
}
}
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
b[i][j] = a[i + 1][j + 1];
cout << Det(n - 1, b);
return 0;
}
6.6 P2144 [FJOI2007] 轮状病毒
高精度能不能滚出 OI。
显然可以套用矩阵树定理,把 Laplace 矩阵写出来(
容易发现第一行第一列是最不规则的,所以可以选择去掉第一行第一列:
我们只需要求这个矩阵的行列式即可。
接下来就是比较机械的过程了。注意到这很像行列式模型里的“川型行列式”,只是左下角和右上角多了一个
首先对第一行展开,行列式的值为:
注意到其中的第一项就是川型行列式的形式。于是我们设
接下来对第二项的第一列展开:
最后对第三项的第一列展开:
于是答案即为:
其中
本题答案较大,需要写高精度。时间复杂度
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 105;
ll n;
struct Bigint{
string num;
Bigint () {
num = "0";
}
Bigint (ll x) {
num = "";
while(x)
{
num += (x % 10) + '0';
x /= 10;
}
}
int getdig(int id) const {
if(id >= num.length()) return 0;
return (num[id] - '0');
}
Bigint operator + (const Bigint & t) const{
Bigint tmp;
tmp.num = "";
ll jw = 0;
for(int i = 0; i < max(num.length(), t.num.length()) || jw > 0; i++)
{
jw += getdig(i) + t.getdig(i);
tmp.num += (jw % 10) + '0';
jw /= 10;
}
return tmp;
}
Bigint operator - (const Bigint & t) const{
Bigint tmp;
tmp.num = "";
ll jw = 0;
for(int i = 0; i < max(num.length(), t.num.length()); i++)
{
int upv = jw + getdig(i), dwv = t.getdig(i);
if(upv < dwv) jw = -1, tmp.num += '0' + upv + 10 - dwv;
else jw = 0, tmp.num += '0' + upv - dwv;
}
return tmp;
}
Bigint operator * (const ll x) const{
Bigint tmp;
tmp.num = "";
ll jw = 0;
for(int i = 0; i < num.length() || jw > 0; i++)
{
jw += getdig(i) * x;
tmp.num += '0' + (jw % 10);
jw /= 10;
}
return tmp;
}
void outp()
{
string tmp = num;
reverse(tmp.begin(), tmp.end());
cout << tmp;
}
} b[N];
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
b[1] = Bigint(3ll); b[2] = Bigint(8ll);
for(int i = 3; i <= n; i++) b[i] = (b[i - 1] * 3) - b[i - 2];
if(n == 1) cout << 1;
else if(n == 2) cout << 5;
else
{
Bigint res = (b[n - 1] * 3) - (b[n - 2] * 2) - Bigint(2ll);
res.outp();
}
return 0;
}
6.7 P4455 [CQOI2018] 社交网络
矩阵树定理求叶向树形图板子,注意是对入度 Laplace 矩阵求行列式。
时间复杂度
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 305;
const ll mod = 1e4 + 7;
ll n, m, a[N][N], b[N][N];
ll qpow(ll a, ll b)
{
ll res = 1;
while(b)
{
if(b & 1) res = (res * a) % mod;
b >>= 1;
a = (a * a) % mod;
}
return res;
}
ll Det(ll n, ll a[N][N])
{
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++)
{
if(a[j][i])
{
if(i ^ j) Swap(i, j);
break;
}
}
if(!a[i][i]) return 0;
for(int j = i + 1; j <= n; j++)
{
ll c = a[j][i] * qpow(a[i][i], mod - 2) % mod;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - c * a[i][k]) % mod;
}
res = (res * a[i][i]) % mod;
}
if(flag) res = -res;
return (res % mod + mod) % mod;
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
while(m--)
{
int u, v;
cin >> u >> v;
a[u][u]++; a[v][u]--;
}
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
b[i][j] = a[i + 1][j + 1];
cout << Det(n - 1, b);
return 0;
}
6.8 * P2109 [NOI2007] 生成树计数
这个题,有点深刻了。
很显然可以先用矩阵树定理来刻画问题,我们只需要求下列矩阵的任意一个
长得很像川型行列式对吧,掐头去尾行列式展开一下就好了。但是你很快就会发现
我们考虑将它化为主对角型行列式的形式。具体而言,把矩阵分为三部分:
- 第一部分:第
1\sim k 行。 - 第二部分:第
k + 1\sim n-k-1 行。 - 第三部分:第
n-k\sim n 行。
然后我们把第一部分与第二部分进行交换,得到如下的矩阵:
接下来是这个做法最神奇的部分,我们可以利用前
其原因在于,前
具体而言,假设第
那么我们令第一行乘上
此时非零的依然只有
按照这样的步骤进行下去,最后得到的矩阵即为:
因为求的是
注意到此时前
直接做的时间复杂度是
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 15;
const ll mod = 65521;
ll qpow(ll a, ll b)
{
ll res = 1;
while(b)
{
if(b & 1) res = (res * a) % mod;
b >>= 1;
a = (a * a) % mod;
}
return res;
}
ll Det(ll n, ll a[N][N])
{
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++)
{
if(a[j][i])
{
if(i ^ j) Swap(i, j);
break;
}
}
if(a[i][i] == 0) return 0;
for(int j = i + 1; j <= n; j++)
{
ll c = a[j][i] * qpow(a[i][i], mod - 2) % mod;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - c * a[i][k]) % mod;
}
res = (res * a[i][i]) % mod;
}
if(flag) res = -res;
return (res % mod + mod) % mod;
}
namespace Sol1 {
ll n, m, a[N][N], b[N][N];
void solve1(ll nnn, ll mmm)
{
n = nnn; m = mmm;
for(int i = 1; i <= n; i++)
{
for(int j = i + 1; j <= min(n, i + m); j++)
{
a[i][i]++; a[j][j]++;
a[i][j]--; a[j][i]--;
}
}
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
b[i][j] = a[i + 1][j + 1];
cout << Det(n - 1, b);
}
}
struct Matrix{
ll a[N][N];
Matrix () { memset(a, 0, sizeof(a)); }
Matrix operator * (const Matrix & t) const{
Matrix res;
for(int i = 0; i < N; i++)
for(int k = 0; k < N; k++)
for(int j = 0; j < N; j++)
res.a[i][j] = (res.a[i][j] + a[i][k] * t.a[k][j]) % mod;
return res;
}
};
Matrix Mqpow(Matrix a, ll b)
{
Matrix res;
for(int i = 0; i < N; i++) res.a[i][i] = 1;
while(b)
{
if(b & 1) res = res * a;
b >>= 1;
a = a * a;
}
return res;
}
namespace Sol2{
ll n, m;
ll a[N], b[N][N];
void getprod(ll (&a)[N], Matrix b)
{
ll res[N];
memset(res, 0, sizeof(res));
for(int i = 1; i <= 2 * m; i++)
for(int j = 1; j <= 2 * m; j++)
res[i] = (res[i] + a[j] * b.a[j - 1][i - 1]) % mod;
memcpy(a, res, sizeof(a));
}
void solve2(ll nnn, ll mmm)
{
n = nnn; m = mmm;
bool flag = (((n - 2 * m - 1) * (m + 1)) & 1); ll res = 1;
Matrix dp;
for(int i = 0; i < 2 * m; i++) dp.a[0][i] = -1;
dp.a[0][m - 1] = 2 * m;
for(int i = 0; i < 2 * m - 1; i++) dp.a[i + 1][i] = 1;
dp = Mqpow(dp, n - 2 * m - 1);
for(int i = 1, j = m; i <= m; i++, j++)
{
memset(a, 0, sizeof(a));
a[i] = j;
b[2 * m - i + 1][2 * m - i + 1] = j + 1;
for(int k = 1; k <= m + i; k++)
{
if(k == i) continue;
a[k] = -1;
b[2 * m - i + 1][2 * m - k + 1] = -1;
}
getprod(a, dp);
memcpy(b[i], a, sizeof(a));
}
res = Det(2 * m, b);
if(flag) res = -res;
cout << (res % mod + mod) % mod;
}
};
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
ll m, n;
cin >> m >> n;
if(n <= 2 * m + 1) Sol1::solve1(n, m);
else Sol2::solve2(n, m);
return 0;
}
6.9 * U659114 矩阵树定理?
本题是我在 u 群询问得到的做法,做法并不是我原创的,但是数据是我造的。
第一步是显然的,使用矩阵树定理,问题转化为如何求一个
我们考虑强化一下这个问题:如何求这个矩阵的伴随矩阵
根据伴随矩阵的公式
另外一种情况是
这说明我们只需要考虑
首先,线性代数里有一个经典的公式:
先从
我们把
同理,根据另一个式子
因此伴随矩阵的所有列向量都在
最后的问题在于,我们如何求出这个常数
时间复杂度
注意这个做法是依赖模数是质数的,因为最后一步需要求逆元。如果模数是合数,我暂时只能想到
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 1005;
const ll mod = 998244353;
ll ans[N], gx, gy, bel[N], A[N], B[N];
ll qpow(ll a, ll b)
{
ll res = 1;
while(b)
{
if(b & 1) res = (res * a) % mod;
b >>= 1;
a = (a * a) % mod;
}
return res;
}
ll inv(ll a)
{
return qpow(a, mod - 2);
}
void GaussX(ll n, const ll a_[N][N])
{
ll a[N][N];
memcpy(a, a_, sizeof(a));
ll rk = 0;
for(int i = 1; i <= n; i++)
{
for(int j = rk + 1; j <= n; j++)
{
if(a[j][i])
{
if((rk + 1) ^ j) swap(a[rk + 1], a[j]);
break;
}
}
if(a[rk + 1][i] == 0)
{
gy = i;
continue;
}
for(int j = rk + 2; j <= n; j++)
{
ll c = a[j][i] * inv(a[rk + 1][i]) % mod;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - c * a[rk + 1][k]) % mod;
}
if(a[rk + 1][i] != 0)
{
rk++;
bel[rk] = i;
}
}
if(rk < n - 1)
{
for(int i = 1; i <= n; i++) cout << "0 ";
exit(0);
}
gx = a[n][n + 1];
A[gy] = 1;
for(int i = n - 1; i >= 1; i--)
{
ll tmp = 0;
for(int j = bel[i] + 1; j <= n; j++) tmp = (tmp - a[i][j] * A[j]) % mod;
a[i][bel[i]] = (a[i][bel[i]] % mod + mod) % mod;
A[bel[i]] = (tmp * inv(a[i][bel[i]]) % mod + mod) % mod;
}
}
void GaussY(ll n, const ll a_[N][N])
{
ll a[N][N];
memcpy(a, a_, sizeof(a));
for(int i = 1; i <= n; i++)
for(int j = i + 1; j <= n; j++)
swap(a[i][j], a[j][i]);
ll rk = 0;
ll tmpgy = 0;
for(int i = 1; i <= n; i++)
{
for(int j = rk + 1; j <= n; j++)
{
if(a[j][i])
{
if((rk + 1) ^ j) swap(a[rk + 1], a[j]);
break;
}
}
if(a[rk + 1][i] == 0)
{
tmpgy = i;
continue;
}
for(int j = rk + 2; j <= n; j++)
{
ll c = a[j][i] * inv(a[rk + 1][i]) % mod;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - c * a[rk + 1][k]) % mod;
}
if(a[rk + 1][i] != 0)
{
rk++;
bel[rk] = i;
}
}
B[tmpgy] = 1;
for(int i = n - 1; i >= 1; i--)
{
ll tmp = 0;
for(int j = bel[i] + 1; j <= n; j++) tmp = (tmp - a[i][j] * B[j]) % mod;
a[i][bel[i]] = (a[i][bel[i]] % mod + mod) % mod;
B[bel[i]] = (tmp * inv(a[i][bel[i]]) % mod + mod) % mod;
}
}
ll Det(ll n, const ll a_[N][N])
{
ll a[N][N];
memcpy(a, a_, sizeof(a));
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++)
{
if(a[j][i])
{
if(i ^ j) Swap(i, j);
break;
}
}
if(a[i][i] == 0) continue;
for(int j = i + 1; j <= n; j++)
{
ll c = a[j][i] * inv(a[i][i]) % mod;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - c * a[i][k]) % mod;
}
res = (res * a[i][i]) % mod;
}
if(flag) res = -res;
return (res % mod + mod) % mod;
}
ll n, m, a[N][N], b[N][N], adj[N][N];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
ll u, v, w;
cin >> u >> v >> w;
a[u][u] += w;
a[u][v] -= w;
}
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
a[i][j] = (a[i][j] % mod + mod) % mod;
a[i][n + 1] = i;
}
GaussX(n, a);
GaussY(n, a);
for(int i = 1, icnt = 0; i <= n; i++)
{
if(i == gx) continue;
icnt++;
for(int j = 1, jcnt = 0; j <= n; j++)
{
if(j == gy) continue;
jcnt++;
b[icnt][jcnt] = a[i][j];
}
}
ll d = Det(n - 1, b);
if((gx ^ gy) & 1) d = -d;
d = (d % mod + mod) % mod;
ll dk = d * inv(A[gy] * B[gx] % mod) % mod;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
adj[i][j] = (A[i] * B[j] % mod * dk % mod);
for(int i = 1; i <= n; i++) cout << adj[i][i] << " ";
return 0;
}
6.10 P5807 【模板】BEST 定理 / Which Dreamed It
这就是上文中“循环同构不算同一个欧拉回路”的例子。直接套用 BEST 定理的公式计算即可。注意需要判断图中是否存在欧拉回路,并且有的点可能是孤立点。
时间复杂度
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 105, V = 1e6 + 5;
const ll mod = 1e6 + 3;
ll n, a[N][N], b[N][N], degin[N], degout[N], inv[V], f[V];
int low[N], dfn[N], tot, cnt, scc[N], stk[N], tp;
bitset<N> instk;
vector<int> g[N];
void init()
{
inv[1] = 1; f[0] = f[1] = 1;
for(int i = 2; i < mod; i++)
{
f[i] = (f[i - 1] * i) % mod;
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
}
void tarjan(int u)
{
low[u] = dfn[u] = ++tot;
stk[++tp] = u; instk[u] = 1;
for(auto v : g[u])
{
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(instk[v])
{
low[u] = min(low[u], dfn[v]);
}
}
if(low[u] == dfn[u])
{
int x;
++cnt;
do{
x = stk[tp--];
scc[x] = cnt;
instk[x] = 0;
} while(u != x);
}
}
bool check()
{
for(int i = 1; i <= n; i++)
if(degin[i] ^ degout[i])
return 0;
for(int i = 1; i <= n; i++)
if(!dfn[i])
tarjan(i);
for(int i = 1; i <= n; i++)
{
if(degout[i] == 0) continue;
if(scc[1] != scc[i]) return 0;
}
return 1;
}
ll Det(ll n, ll a[N][N])
{
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++)
{
if(a[j][i])
{
if(i ^ j) Swap(i, j);
break;
}
}
if(!a[i][i]) return 0;
a[i][i] = (a[i][i] % mod + mod) % mod;
for(int j = i + 1; j <= n; j++)
{
ll c = a[j][i] * inv[a[i][i]] % mod;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - c * a[i][k]) % mod;
}
res = (res * a[i][i]) % mod;
}
if(flag) res = -res;
return (res % mod + mod) % mod;
}
void solve()
{
memset(a, 0, sizeof(a));
memset(degin, 0, sizeof(degin));
memset(degout, 0, sizeof(degout));
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(scc, 0, sizeof(scc));
instk.reset();
tp = tot = cnt = 0;
cin >> n;
for(int i = 1; i <= n; i++) g[i].clear();
int smx = 0;
for(int i = 1; i <= n; i++)
{
int x;
cin >> x;
smx += x;
a[i][i] += x;
degout[i] = x;
if(x == 0) a[i][i] = 1, a[i][1] = -1;
while(x--)
{
int v;
cin >> v;
a[i][v]--;
degin[v]++;
g[i].push_back(v);
}
}
if(!check())
{
cout << "0\n";
return;
}
if(smx == 0)
{
cout << "1\n";
return;
}
for(int i = 1; i < n; i++)
for(int j = 1; j < n; j++)
b[i][j] = a[i + 1][j + 1];
ll res = Det(n - 1, b);
res = (res * f[degout[1]]) % mod;
for(int i = 2; i <= n; i++)
{
if(degout[i] == degin[i] && degout[i] == 0) continue;
res = (res * f[degout[i] - 1]) % mod;
}
cout << res << "\n";
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
init();
int t;
cin >> t;
while(t--) solve();
return 0;
}
6.11 P7531 [USACO21OPEN] Routing Schemes P
这有黑?这有黑?这有黑?这有黑?这有黑?
容易想到网络流里建立超级源点、超级汇点的思路,然后把超级汇点朝超级源点连一条边,那么问题就被转化为对有向图的欧拉回路计数,套用 BEST 定理即可解决。时间复杂度
注意因为欧拉回路中各路径的顺序并不会影响最后答案的计数,所以答案要除以
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 105;
const ll mod = 1e9 + 7;
int n, m, s, t;
ll a[N][N], b[N][N], f[N];
bitset<N> legal;
void init()
{
f[0] = 1;
for(int i = 1; i < N; i++)
f[i] = (f[i - 1] * i) % mod;
}
void add(int u, int v, int w)
{
a[u][u] += w;
a[u][v] -= w;
}
ll qpow(ll a, ll b)
{
ll res = 1;
while(b)
{
if(b & 1) res = (res * a) % mod;
b >>= 1;
a = (a * a) % mod;
}
return res;
}
ll Det(ll n, ll _a[N][N])
{
ll a[N][N];
memcpy(a, _a, sizeof(a));
bool flag = 0; ll res = 1;
auto Swap = [&] (int x, int y) -> void {
swap(a[x], a[y]); flag ^= 1;
};
for(int i = 1; i <= n; i++)
{
for(int j = i; j <= n; j++)
{
if(a[j][i])
{
if(i ^ j) Swap(i, j);
break;
}
}
for(int j = i + 1; j <= n; j++)
{
ll c = a[j][i] * qpow(a[i][i], mod - 2) % mod;
for(int k = i; k <= n; k++)
a[j][k] = (a[j][k] - c * a[i][k]) % mod;
}
res = (res * a[i][i]) % mod;
}
if(flag) res = -res;
return (res % mod + mod) % mod;
}
void solve()
{
cin >> n >> m;
s = n + 1; t = n + 2;
memset(a, 0, sizeof(a));
legal.reset();
int cnts = 0;
for(int i = 1; i <= n; i++)
{
char c;
cin >> c;
if(c == 'S') add(s, i, 1), cnts++;
else if(c == 'R') add(i, t, 1);
}
add(t, s, cnts);
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
{
char c;
cin >> c;
if(c - '0') add(i, j, 1);
}
}
for(int i = 1; i <= n + 2; i++)
if(a[i][i])
legal[i] = 1;
int tmpn = 0;
for(int i = 1, icnt = 0; i <= n + 2; i++)
{
if(!legal[i]) continue;
icnt++; tmpn++;
for(int j = 1, jcnt = 0; j <= n + 2; j++)
{
if(!legal[j]) continue;
jcnt++;
b[icnt][jcnt] = (a[i][j] % mod + mod) % mod;
}
}
ll res = Det(tmpn - 1, b);
for(int i = 1; i <= n + 2; i++)
if(a[i][i])
res = (res * f[a[i][i] - 1]) % mod;
res = (res * qpow(f[cnts], mod - 2)) % mod;
res = (res * qpow(f[cnts - 1], mod - 2)) % mod;
cout << res << "\n";
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
init();
int T;
cin >> T;
while(T--) solve();
return 0;
}
参考资料
- Amidst - 计数:Matrix-Tree 定理
- OI wiki - 矩阵树定理