数学总结 3(线性代数、行列式)
零、目录
- 线性基
- 线性空间
- 行列式
- 矩阵树定理
- 矩阵求逆
一、线性基
线性基是数据结构啊,可是既然它在线性代数题单里,那它就是线性代数吧。
事实上确实可以用接近于线性代数的思维理解。
DS 滚出数学。
引入
考虑如下的问题:
给定 \(n\) 个整数(数字可能重复),求在这些数中选取任意个,使得他们的异或和最大。
$ 1 \leq n \leq 50, 0 \leq S_i < 2 ^ {50} $。
线性基是一种擅长处理异或问题的数据结构。
首先考虑异或的本质:不进位加法。
考虑把一个数(例如 \(5\))写成二进制形式:\((0101)_2\)。
然后把它写成向量形式:\(\{0, 1, 0, 1\}\)。
然后两个数 \(x,y\) 异或,相当于它们每一位做不进位加法,有相当于它们的向量相加,但是每一维都在 \(\bmod 2\) 意义下。
回到本题:
相当于现在有 \(n\) 个向量 \(v_1, v_2, \dots, v_n\),需要给每个向量赋值 \(a_i\) 为 \(0\) 或 \(1\),计算 \(a_1 v_1 + a_2 v_2 + \dots + a_n v_n\) 的所有可能结果和字典序最大的结果。
线性基的思想就是找到一组基,使得它能表示出的向量与 \(\{ v_1, v_2, \dots, v_n \}\) 能表示出的向量集合是一样的。
举一个特殊例子:已知 \(\{x,y\}\),它能表示出的有 \(\{0, x, y, x \oplus y\}\)。然而 \(\{x, x \oplus y\}\) 也恰好表示出这个集合,因此它是一组合法的基。
也就是 \(a_i \oplus a_j\) 之后,这个序列能表示的集合不变,此时我们不妨称这样的操作为一次等价变换。
线性基就是不断通过等价变换,让这些向量变得尽量简单或有规律。
如何让它变得有规律?线性基通过限制基的最高位互不相同解决了这件事情。
具体地,假设现在基中已有元素 \(10010\) 和 \(01001\),现在要插入 \(11101\)。
由于当前插入的元素做任意次等价变换,表示的集合依然不变,所以可以把它和基中的元素异或。
- 第一轮:最高位已经在基内,执行 \(11101 \oplus 10010 = 01111\)。
- 第二轮:最高位已经在基内,执行 \(01111 \oplus 01001 = 00110\)。
- 第三轮:最高位不在基内,直接把 \(00110\) 插入线性基即可。
因此我们在执行插入操作的时候只关心最高位是否出现。
简单操作 & Code
学会了思路之后代码是好写的。
记录 base[i] 表示以 \(i\) 为最高位的基底元素。
按照之前的插入流程模拟即可,时空复杂度均为 \(O(\log n)\)。
设位数为 \(m\),若位数很多则复杂度为 \(O(\frac{m^2}{\omega})\),因为要遍历每一位,还要异或,可以用 bitset 优化。
bool chk(long long x, int i) { return (x >> i) & 1; }
void insert(long long x) {
for (int i = 50; i >= 0; i--) {
if (!chk(x, i)) continue;
if (!base[i]) return base[i] = x, void();
x ^= base[i];
}
}
其实这个过程很像高斯消元,类似于消成上三角形式,也就是把对角线下面全部异或为 \(0\)。
求异或可得最大元素
类似于 Trie 树上贪心二分,从高往低能取 \(1\) 就取 \(1\)。
long long querymx() {
long long res = 0;
for (int i = 50; i >= 0; i--) res = max(res, res ^ base[i]);
return res;
}
求 \(x\) 是否可以被表示为异或和
也是贪心,尽量异或掉多的位。
如果最后被消成 \(0\) 说明可以被表示。
bool querycan(long long x) {
for (int i = 50; i >= 0; i--) x = min(x, x ^ base[i]);
return (x == 0ll);
}
例 1:HDU3949 XOR
给定 \(n\) 个正整数 \(a_1, a_2, \dots, a_n\),求这些数异或表示出的数的集合中,第 \(k\) 小的是多少(不能选空集)。
事实上和模板几乎一样。
还是考虑从高位到低位贪心确定,首先如果基内有 \(c\) 个元素,那么它们可以组合出 \(2^c\) 种数。
那么我们只要考虑 \(k\) 和 \(2^{c-1}\) 的关系就知道这一位是 \(0\) 还是 \(1\) 了。
具体地,设到当前确定的数为 \(x\),到了第 \(u\) 位,接下来还有 \(c\) 个基要处理(因为有一些位的基可能不存在所以要记录 \(c\))。
如果 \(x\) 的第 \(u\) 位已经是 \(1\),那么选上这一位的基底会让 \(x\) 更小,不选会更大,根据 \(k\) 属于哪边递归求解。
这样写代码真的很丑……但有一种更优雅的做法可以让代码变得较为简单。
考虑按照之前的方法求出的基如何再简化:相当于拿高位的基再和低位的基异或,把高位基消掉尽可能多低位的 \(1\)。
说着挺绕的,但事实上按照高斯消元的思维模式很好理解:
10011
01110
00110
00001
这样的一组基,我们把上面的不断异或掉下面的,就会得到:
10010
01000
00110
00001
这样转化有什么用?
这样每一组基的最高位,在矩阵上的位置设为 \((x,y)\),则 \((x,y)\) 上方一定都是 \(0\),因为它之前的基一定可以通过异或它把这一位消掉。
相当于少一层分讨,因为不需要特判 \(x\) 的第 \(u\) 位是否是 \(1\) 了,选了一定更大,不选一定更小。
因此将询问 \(k\) 的二进制表示为 \(1\) 的那些基底异或起来就是答案。
因为之前 \(mid = 2^{c-1}\),所以要把全 \(0\) 行删掉再给 \(k\) 做二进制分解,也就是删掉 base 中为 \(0\) 的元素。
由于不能选空集,还需要特判线性基中的元素能否构成 \(0\)。
这是简单的,可以用 map 维护,但是更省事的方法是:如果一个元素插入线性基失败,说明它可以被表示为之前元素的异或和,因此一定有一种方案使得选出的元素异或和为 \(0\)。
Code.
例 2:洛谷 P4151 WC2011 最大XOR和路径
好神秘的题目。
看到异或和最大可以联想到 Trie 树?不对啊这题怎么 Trie 都不是很正经。
但这正是线性基擅长处理的问题类型。
发掘异或的性质:对于这张图上的环,我们一定可以任意决定它选或不选。
为什么?考虑先从起点 \(1\) 开始搜一下 dfs 树,则非树边都是返祖边,且它和一部分树边构成环。
如果我们需要选一个环,可以考虑从 \(1\) 开始,先到达环的顶部,然后沿着树边走到环底部,再通过返祖边绕回来,最后回到起点 \(1\),此时产生的异或和就是环异或和。
但是需要 \(1\) 走到 \(n\),那就是 dfs 树上 \(1 \to n\) 的异或和了。
求异或最大值,只需要把所有返祖边构成的环扔进线性基,查询 \(dis_n\) 即树上异或和能构成的最大数。
环的数量等于返祖边数量,是 \(O(m)\) 级别的;线性基也就多一个 \(\log\),可以通过。
Code.
例 3:CF251D Two Sets
已知一个序列 \(a_1,a_2, \dots, a_n\),把它分成两个集合 \(S_1,S_2\),使得两个集合内元素异或和之和尽量大,在使得这个值最大的前提下让 \(S_1\) 内的异或和尽量小,求具体的分配集合方案。
考虑异或对答案有什么具体的影响。
首先全集 \(a\) 是已知的,所以全集异或和等于 \(S_1,S_2\) 异或和也是已知的,我们记它为 \(sum\)。
若 \(sum\) 第 \(i\) 位为 \(1\),则无论如何分配 \(S_1,S_2\) 这一位一定至少有一个 \(1\),所以它对答案是没有影响的。
若 \(sum\) 第 \(i\) 位为 \(0\),则这一位在 \(S_1,S_2\) 中可能是 \(0,0\) 也可能是 \(1,1\)。
下文称这两种分别为“\(0\) 位”和“\(1\) 位”,考虑把无影响的 \(1\) 位和有影响的 \(0\) 位分开计算。
但是 \(0,1\) 位内部是有权值大小顺序的。
综上我们构造出这样一种合法的转化:
设高位为“前面”,低位为“后面”。
则我们把所有 \(0\) 位按照从高到低的顺序放在前面,所有 \(1\) 位按照从高到低的顺序放在后面。
这样转化可以保证位之间的优先贪心顺序,而且能转化为更加便于贪心的形式。
至于构造方案,只需要记录线性基内每一个元素由原序列哪些元素异或得到即可。
Code.
例 4:CF895C Square Subsets
给定长度为 \(n\) 的序列 \(a\),求有多少种选非空集合的方式可以使得选出的集合内,元素之积是完全平方数。
简单题。
容易想到给每个 \(a_i\) 质因数分解,观察到值域 \(\leq 70\),所以质因子数量很少。
然后完全平方数相当于每个指数幂指数都为偶数,所以考虑记录 \(\bmod 2\) 意义下的指数。
乘积相当于指数相加,体现在 \(\bmod 2\) 意义下就是异或,求有多少种选集合方法使得异或和位 \(0\),这很容易想到用线性基维护。
若有 \(c\) 个元素插入失败,则答案显然为 \(2^c - 1\)。因为插入失败的元素随便选,一定有唯一的线性基元素选或不选的状态和它对应;要 \(-1\) 因为不能选空集。
例 5:洛谷 P3292 SCOI2016 幸运数字
给定一棵 \(n\) 个点的树,点有点权 \(\lt 2^{60}\),\(q\) 次询问路径 \((u,v)\) 上任选点权,异或得到最大的权值。
看到选择元素,使得异或和最大自然想到线性基。
然后这是路径,考虑和主席树一样可持久化……可持久化线性基?开始玄幻了。
如果记录每个点到根的线性基呢?但线性基不能执行删除操作,也做不了。
……
既然不能删除那就直接合并。
暴力遍历路径上每个点然后扔进线性基。这显然不对。
能否通过记录一些额外的信息使得查询速度更快?例如多维护一些线性基。
那得先考虑如何合并两个线性基?显然直接暴力把一个线性基中的元素插入另一个线性基即可。
然后我们做一个类似于求 LCA 的事情:倍增。
对于每个点,我们存储它往上跳 \([0,2^k)\) 步到达点的集合的线性基,这样空间复杂度 \(O(n \log n \log V)\)。
查询就直接暴力跳倍增即可,这样总时间复杂度是 \(O((n + q) \log n \log^2 V)\)。
看上去有点悬。
复杂度瓶颈在哪里?在于询问的次数 \(q\) 比较大。
但异或有一个很优美的性质:“对消”——\(x \oplus x = 0\)。
因此这类似于 RMQ 问题,对于 RMQ 的查询 \([l,r]\),只需要拿出 \([l,l+2^k)\) 和 \((r-2^k,r]\) 的倍增结果 \(O(1)\) 合并即可。
那么对于 \((u,lca)\) 这条路径也一样,做一个类似于 ST 表的操作就可以 \(O(\log^2 V)\) 合并出答案,另外一边 \((v,lca)\) 同理。
这样总复杂度就降到了 \(O(n \log n \log^2 V + q \log^2 V)\)。
Code.
例 6:洛谷 P4570 BJWC2011 元素
给定若干个二元组 \((a_i, b_i)\),要求选出一个集合 \(S\)。满足 \(S\) 的 \(b_i\) 之和最大,且满足不存在 \(S\) 的一个非空子集 \(S'\) 使得 \(S'\) 内 \(a_i\) 的异或和为 \(0\)。
顺便提一下,不存在异或和为 \(0\) 称为线性无关。
贪心地,对于一个相同的 \(a\) 我们肯定取最大的 \(b\)。
然后异或和为 \(0\) 联想到使用线性基。
能否 DP,问题来了这题无论怎样设计状态都不像可以优化的样子。
考虑一个集合 \(S\),它内部 \(a_i\) 异或和为 \(0\) 且不存在一个非空子集 \(S'\) 满足 \(S'\) 的异或和为 \(0\)。
那么只要从 \(S\) 中任意删除一个元素就可以满足条件。
贪心地,肯定删除最小的元素。
把这个结论反过来做,就是把二元组按照 \(b\) 排序,能加入线性基就加入。
类似于 MST 的 Kruskal 求解过程,只要加入不会形成环就加入。
例 7:洛谷 CF1100F Ivan and Burgers
给定一个长度为 \(n\) 的序列 \(a\),\(q\) 次询问 \([l,r]\) 内选数的最大异或和。
\(n, q \leq 5 \times 10^5, a_i \leq 10^6\)。
3s,512 MB
很简单啊,直接 RMQ 就做完了。
然后太冲动了直接 Coding 所导致的忘记算空间了。
ST 表再内套一个线性基直接 \(O(n \log n \log V)\),空间复杂度起飞了,算了下大概 700 多 MB 的样子。
于是交上去喜提 MLE On Test #1,老实了。
正解完全没有半点头绪,于是开始卡空间,然后发现这玩意儿不能开 short……
但是注意到如果可以时间换空间那就很强了,因为 \(5 \times 10^8\) 空间开不下但可以跑得过。
去问了一下,发现原来是我没学过猫树分治 (lll¬ω¬)。
可能和网上的版本有些差别?因为没有仔细学,了解思想之后直接现推了。
ST 表能解决的问题猫树都能解决,不同于 ST 表的倍增,猫树的基本思想是分治,大致可以理解为是 ST 表和线段树的结合。
由于结合了 ST 表,它一般不支持修改操作,但可以通过离线等方式快速处理询问,再拼接成答案。
具体地,它的分治树长得和线段树几乎一样,对于一次询问 \([l,r]\),我们需要寻找一个树上区间 \([L,R]\),使得 \(L \leq l \leq mid \lt mid + 1 \leq r \leq R\)。
然后对于树上的每个区间 \([L,R]\),考虑记录它的前后缀答案,对于一次询问只需要合并 \([L,mid]\) 和 \([mid+1,R]\) 两个区间的后缀、前缀答案。
对于这题,考虑把问题离线下来,先找到 \([l,r]\) 对应的 \([L,R]\) 把询问插入这个区间,然后对每一个区间计算前后缀的线性基。
由于猫树深度是 \(O(\log n)\) 的,所以这样做的复杂度是 \(O(n \log n \log V)\),\(\log V\) 是线性基插入元素复杂度。
查询可以直接合并两个线性基,做到 \(O(\log^2 V)\)。
感觉写的会非常抽象……因为没有看过猫树的正统写法,随手胡的代码。
事实上并不抽象,甚至几乎一遍过。
Code.
二、线性空间
不是,学这玩意儿到底有啥用啊。
定义 & 引入

OI-Wiki 虽然定义得严谨,但是看上去好神秘啊。
并不严谨但比较通俗地说,之前线性基元素通过异或能表示的数的集合就是一个线性空间。
二维空间、三维空间实际上都是都是线性空间。即 \((x,y)\) 向量可以做加法、数乘运算,得到二维空间、三维空间。
之前的线性基事实上就是 \(k\) 维的基,类似于二三维空间,但运算在 \(\bmod 2\) 意义下进行。
类似地,可以把 \(\bmod 2\) 推广到 \(\bmod m\),即 \(m\) 进制数,每一位 \(\in [0,m)\)。
对于一组基 \(\{ v_1, v_2, \dots, v_k \}\),也可以通过和之前二进制类似的方法求出 \(a_1v_1 + a_2v_2 + \dots + a_kv_k\) 的最大值(不断消低位)。
相关概念
线性无关、线性相关
如果 \(a_1v_1 + a_2v_2 + \dots + a_kv_k = 0\) 的解只有 \(a_1=a_2=\dots=a_k=0\),即零解,则我们称其为线性无关的,所以基一定是线性无关的。
若有 \(v_i\) 能被其它 \(v\) 表示出,则称其为线性相关的。
极大线性无关组、秩
换一个角度理解线性相关这件事情。回顾在向线性基内插入元素的流程,如果一个元素始终没有找到对应的 base 插入,那么它就一定可以被表示为之前插入的元素的异或和,此时若强行把它扔进线性空间,那么就成了线性相关。
考虑不断删除这些可以被其他元素表示的元素,直到线性空间变得线性无关,我们称其为极大线性无关组。
显然,某种意义上线性基就是一个极大线性无关组,它的大小就是秩,相当于是“维数”。
三、行列式
这个又有什么道理要学它啊。
定义
对于一个 $ n \times n $ 的矩阵 $ A $:
行列式的值就是:
其中 \(p\) 是 \(1\) 到 \(n\) 的排列,\(\tau(p)\) 表示 \(p\) 的逆序对数量。
也就是枚举所有的排列 \(p\),然后把第 \(k\) 行第 \(p_k\) 列的所有元素乘起来,根据 \(p\) 的逆序对数量决定系数是 \(-1\) 还是 \(1\)。
性质
性质 1
矩阵任意一行或任意一列 \(\times t\),则行列式的值 \(\times t\)。
根据值的定义式,一定有且仅有一个 \(a_{k,p_k}\) 被 \(\times t\),所以最后行列式的值 \(\times t\)。
性质 2
交换矩阵任意两行或任意两列,行列式的值取反。
假设交换第 \(i\) 行和第 \(j\) 行,对于交换列推导类似。
相当于是交换排列中的 \(p_i, p_j\)。
对于后面乘积部分是不会影响的,因为元素只发生了交换,没有修改。
所以考虑 \(\tau(p)\),即逆序对奇偶性的改变。
首先对于 \(i,j\) 本身的交换,逆序对一定恰好变化 \(1\)。
然后考虑 \(i,j\) 与其它数构成的逆序对,只有在 \((i,j)\) 区间内的数可能会影响,推导可以得到它们的变化量为偶数。
因此逆序对奇偶性一定变化,行列式的值取反。
性质 3
若矩阵存在任意两行相同,或任意两列相同,则行列式的值为 \(0\)。
性质 2 的推论,交换后为相反数,但是又相同,所以是 \(0\)。
性质 4
若矩阵存在任意两行或任意两列,所有元素成比例,则行列式的值为 \(0\)。
性质 3 结合性质 1,对其中一行或一列进行等比例扩大或缩小,就可以得到两行或两列相同了。
性质 5
若矩阵 \(A\) 的第 \(i\) 行(列)等于 \(m\) 个矩阵 \(B_1,B_2, \dots, B_m\) 第 \(i\) 行(列)之和,其它位置都相等,那么 \(|A| = \sum\limits_{i=1}^{m} |B_i|\)。
考虑 \(m=2\),有 \(|A| = |B_1| + |B_2|\),再推广到 \(m \gt 2\) 个矩阵的情况。
按照定义式把第 \(i\) 行单独提出来,然后再分配律合并 \(B_1,B_2\) 的式子就可以得到 \(A\) 的式子。
性质 6
若把矩阵某一行所有元素 \(\times t\) 之后再加到另一行,则行列式的值不变;列同理。
假设是 \(A\) 矩阵第 \(i\) 行的 \(\times t\) 之后加到第 \(j\) 行,那么:
根据上一条性质,构造另一个矩阵 \(B\),使得第 \(j\) 行是 \(A\) 矩阵第 \(i\) 行的元素集体 \(\times t\) 的结果,其它行都一样。
那么相当于是求证 \(|A|=|A|+|B|\)。
事实上 \(|B|\) 显然为 \(0\),因为 \(B\) 中第 \(i\) 行和第 \(j\) 成比例,根据性质 4 它的值为 \(0\)。
性质 7
定义余子式:设矩阵 \(A\) 删掉第 \(i\) 行第 \(j\) 列之后的矩阵是 \(B_{i,j}\),称其为余子式。
定义代数余子式:\(C_{i,j} = -1^{i+j} B_{i,j}\)。
对于矩阵 \(A\) 任意一行或任意一列,计算所有元素与它对应代数余子式之积的和,得到的结果等于行列式的值。
假定矩阵是 \(n \times n\) 的,我们对矩阵 \(A\) 的第 \(i\) 行进行计算,列同理。
考虑把 \(A\) 拆成 \(n\) 个矩阵 \(M(1),M(2),\dots,M(n)\),使得每个矩阵除了第 \(i\) 行都和 \(A\) 一样,且满足第 \(j\) 个矩阵 \(M(j)\) 的第 \(i\) 行除了 \(M(j)_{i,j} = A_{i,j}\),其它都为 \(0\)。
显然根据性质 \(5\),矩阵 \(A\) 行列式的值等于 \(\sum\limits_{i=1}^{n} |M(i)|\),所以我们可以分别对每个 \(M(i)\) 计算贡献。
考虑其中第 \(j\) 个矩阵的行列式有何变化。
根据行列式的计算公式:\(|A| = \sum_{p} (-1)^{\tau(p)} \prod_{k=1}^{n} a_{k,p_k}\),由于 \(a\) 的第 \(i\) 行只有 \(a_{i,j}\) 有值,其它都为 \(0\),所以只有 \(p_i=j\) 的时候有贡献。
相当于强制 \(p_i=j\)。
强制 \(p_i=j\) 之后就要计算前面 \(-1\) 次幂的取值了。
我们只计算和 \(i\) 这一位相关的逆序对数量。
假设我们交换 \(p_a,p_b\),钦定 \(a \lt b\),开始分类讨论:
- 若 \(a, b \lt i\) 或 \(a, b \gt i\):
- 显然不会影响到 \(\tau\),因此 \(\Delta = 0\)。
- 若 \(p_a, p_b \lt p_i\) 或 \(p_a, p_b \gt p_i\):
- 此时 \(a,b\) 分居两边,但同高同低。
- 也不会影响到 \(\tau\),因此 \(\Delta = 0\)。
- 若 \(p_a \lt p_i \lt p_b\):
- 并不难发现 \(\tau\) 会被影响,\(p_i\) 前面多一个比它大的,后面多一个比它小的。
- 因此 \(\Delta = 2\)。
- 若 \(p_a \gt p_i \gt p_b\):
- 和上一个同理,\(\Delta = -2\)。
因此关于 \(i\) 的逆序对奇偶性不会变化。
问题是知道这玩意儿没啥用,我们真正要求的是整个排列的奇偶性。
但这东西启发我们构造一种特殊排列,满足 \(p_i=j\) 的同时能快速计算出逆序对奇偶性。
- 若 \(i=j\):
- 这没什么好说的,直接构造 \(\{1,2,\dots,i,\dots,n\}\),逆序对为 \(0\)。
- 若 \(i \lt j\):
- 构造排列 \(\{1,2,\dots,j,i,i+1,i+2,\dots,j-1,j+1,\dots,n\}\)。
- 此时 \([i+1,j]\) 区间的数都比 \(p_i\) 小,而如果去掉 \(p_i\),整体是单增的。
- 因此逆序对数为 \(j-i\)。
- 若 \(i \gt j\):
- 和之前同理,可以得到逆序对数为 \(i-j\)。
得出结论:逆序对数为 \(|i-j|\)。
我们只关注奇偶性,所以 \(|i-j| \equiv i+j \pmod 2\)。
则有:
行列式的求值
一个简单的方法是:直接按照定义式求值,需要枚举全排列,再枚举每一位,所以复杂度为 \(O(n! \times n)\)。
另一种方法类似于高斯消元,先把矩阵转为上三角,再转化为对角矩阵,在消元的过程中根据行列式的性质顺便更新行列式的值。
显然对角矩阵的行列式的值就是对角线乘积。
消成上三角矩阵
利用性质 1、2、6,可以很容易地用高斯消元求解行列式的值,由于取模操作,套一个逆元就行了。
这对吗?这不对!模数不一定为质数,所以逆元可能不存在。
考虑求 \(\gcd\) 的过程:辗转相除法。它最后递归的终止条件是 \(b=0\),此时返回 \(a\)。
这正是我们所需要的:把一个位置消成 \(0\),保留另一个位置。
与之不同的,我们现在是把两行的元素同时进行辗转相除。
消成对角矩阵
在思考如何消成对角矩阵之前先冷静分析一下。
现在对于 \(a_{i,j},i \lt j\) 的位置显然都已经是 \(0\) 了,因此只要有 \(p_i = j, i \lt j\) 那么整个式子乘上 \(0\) 结果就是 \(0\)。
那么就相当于限制 \(p_i \leq i\),但是排列又不能重复填数,所以一定在 \(p_i = i\) 的时候才有值。
因此只有对角线信息有用。
那么根本不需要消成对角矩阵,直接计算即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 605;
int n, mod, a[N][N];
long long ans;
inline void exchange(int x, int y) { for (int i = 1; i <= n; i++) swap(a[x][i], a[y][i]); ans *= -1; }
int main() {
scanf("%d%d", &n, &mod), ans = 1ll;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++) {
while (a[i][i]) {
int t = a[j][i] / a[i][i];
for (int k = i; k <= n; k++) a[j][k] = ( (a[j][k] - a[i][k] * 1ll * t) % mod + mod) % mod;
exchange(i, j);
}
exchange(i, j);
}
ans = (ans + mod) % mod;
for (int i = 1; i <= n; i++) ans = (ans * 1ll * a[i][i]) % mod;
printf("%lld\n", ans);
return 0;
}
矩阵树定理
矩阵树定理一般被用来求解无权图生成树数量,通过构造矩阵把图论问题转化为行列式求值问题。
构造矩阵
度数矩阵 \(D\)
- 对于无向图:定义 \(D_{i,i} = deg_i\),即度数;对于 \(i \not= j\) 定义 \(D_{i,j} = 0\)。
- 对于有向图:定义 \(D^{in} = in_i\),即入度;\(D^{out} = out_i\),即出度;对于 \(i \not= j\) 则 \(D^{in}_{i,j} = D^{out}_{i,j} = 0\)。
邻接矩阵 \(A\)
定义 \(A_{i,j}\) 表示图中 \(i,j\) 两点间有多少条边。
Laplace 矩阵 \(L\)(又名 Kirchhoff 矩阵)
- 对于无向图:定义 \(L_{i,j} = D_{i,j} - A_{i,j}\)。
- 对于有向图:定义 \(L^{in} = D^{in}_{i,j} - A_{i,j}\),即入度 \(L\) 矩阵;\(L^{out} = D^{out}_{i,j} - A_{i,j}\),即出度 \(L\) 矩阵。
子矩阵 \(R\)
为下文行文方便,令 \(R(M,i)\) 表示把 \(M\) 矩阵删除第 \(i\) 行第 \(i\) 列得到的矩阵。
定理
无向图
- 对于无向图:
- 它的生成树数量为:\(|R(L, rt)|\)。
- 其中 \(rt\) 为任意点。
- 对于有向图:
- 它的叶向生成树数量为:\(|R(L^{in}, rt)|\)。
- 它的根向生成树数量为:\(|R(L^{out}, rt)|\)。
- 其中 \(rt\) 表示树根。
证明略。
模板题
题目定义一个生成树的权值为边权乘积。
矩阵树定理并不能处理带权问题,但是可以转化:把一条边权为 \(w\) 的边转化为 \(w\) 条无权边。
然后就可以套用矩阵树定理了。
本题要求以一为根的叶向树,所以直接把点编号 \(-1\) 即可,做行列式求值的时候求 \([1,n-1]\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 605, mod = 1e9 + 7;
int n, m, T;
long long a[N][N], ans;
inline void exchange(int x, int y) { for (int i = 1; i <= n; i++) swap(a[x][i], a[y][i]); ans *= -1; }
void solve() {
ans = 1ll;
for (int i = 1; i < n; i++)
for (int j = i + 1; j < n; j++) {
while (a[i][i]) {
int t = a[j][i] / a[i][i];
for (int k = i; k < n; k++) a[j][k] = ( (a[j][k] - a[i][k] * 1ll * t) % mod + mod) % mod;
exchange(i, j);
}
exchange(i, j);
}
ans = (ans + mod) % mod;
for (int i = 1; i < n; i++) ans = (ans * 1ll * a[i][i]) % mod;
printf("%lld\n", ans);
}
int main() {
scanf("%d%d%d", &n, &m, &T);
for (int i = 1, u, v, w; i <= m; i++) {
scanf("%d%d%d", &u, &v, &w), u--, v--;
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; // 叶向树,用 in
}
for (int i = 1; i < n; i++)
for (int j = 1; j < n; j++) a[i][j] = (a[i][j] % mod + mod) % mod;
solve();
return 0;
}
四、矩阵求逆
对于 \(A\) 求一个矩阵,使得它和 \(A\) 做矩阵乘法得到单位矩阵。
在 \(A\) 的右边拼一个单位矩阵,然后对左边的 \(A\) 做高斯消元,使得它变成单位矩阵。
此时右边的单位矩阵就变成了逆矩阵。
#include <bits/stdc++.h>
using namespace std;
const int N = 405, mod = 1e9 + 7;
int qmi(int a, int k) {
int res = 1;
while (k) {
if (k & 1) res = res * 1ll * a % mod;
a = a * 1ll * a % mod, k >>= 1;
}
return res;
}
int n, m, a[N][N << 1];
void solve() {
for (int i = 1; i <= n; i++) {
int t = i;
for (int j = i + 1; j <= n; j++)
if (a[t][i] < a[j][i]) t = j;
for (int j = 1; j <= m; j++) swap(a[t][j], a[i][j]);
if (a[i][i] == 0) puts("No Solution"), exit(0);
int inv = qmi(a[i][i], mod - 2);
for (int j = i; j <= m; j++) a[i][j] = (a[i][j] * 1ll * inv) % mod;
for (int j = i + 1; j <= n; j++) {
int p = a[j][i];
for (int k = i; k <= m; k++) a[j][k] = (a[j][k] - (p * 1ll * a[i][k] % mod) + mod) % mod;
}
}
for (int i = n; i >= 1; i--)
for (int j = i + 1; j <= n; j++) {
int p = a[i][j];
for (int k = i + 1; k <= m; k++) a[i][k] = (a[i][k] - (p * 1ll * a[j][k] % mod) + mod) % mod;
}
for (int i = 1; i <= n; i++, puts(""))
for (int j = 1; j <= n; j++) printf("%d ", a[i][j + n]);
}
int main() {
scanf("%d", &n), m = n << 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i++) a[i][i + n] = 1;
solve();
return 0;
}

浙公网安备 33010602011771号