数学总结 3(线性代数、行列式)

零、目录

  1. 线性基
  2. 线性空间
  3. 行列式
    • 矩阵树定理
  4. 矩阵求逆

一、线性基

线性基是数据结构啊,可是既然它在线性代数题单里,那它就是线性代数吧
事实上确实可以用接近于线性代数的思维理解。
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.


二、线性空间

不是,学这玩意儿到底有啥用啊。

定义 & 引入

C0E7806191A747B770A02D7047DB8671.png

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 $:

\[A = \begin{bmatrix} a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\ a_{2,1} & a_{2,2} & \cdots & a_{2,n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{k,1} & a_{k,2} & \cdots & a_{k,n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n,1} & a_{n,2} & \cdots & a_{n,n} \end{bmatrix} \]

行列式的值就是:

\[|A| = \sum_{p} (-1)^{\tau(p)} \prod_{k=1}^{n} a_{k,p_k} \]

其中 \(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\)
则有:

\[|A| = \sum_{p} (-1)^{\tau(p)} \prod_{k=1}^{n} a_{k,p_k} \]

\[=\sum\limits_{j=1}^{n} a_{i,j} \times (-1)^{i+j} \sum_{p} (-1)^{\tau(p)} \prod_{k=1,k \not= i}^{n} a_{k,p_k} \]

\[=\sum\limits_{j=1}^{n} a_{i,j} \times C_{i,j} \]

行列式的求值

一个简单的方法是:直接按照定义式求值,需要枚举全排列,再枚举每一位,所以复杂度为 \(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;
}
posted @ 2025-05-07 21:09  Conan15  阅读(39)  评论(0)    收藏  举报  来源