jx day1 总结
虽然做了预习,但依然感觉有点难
主要内容也就是高斯消元,行列式,矩阵树定理,矩阵求逆,线性基
我还比较菜,会慢慢填坑
先说说高斯消元
高斯消元,基本也就是搞多元方程组,如2元方程组,就是按照我们怎么解2元方程组方程组的方法用程序实现
如何求解有 m m m个方程的 n n n元方程组呢?
首先,矩阵一共有 m m m行,每一行表示一个方程;前 n n n个数,表示每个项的系数,之后那个数表示这个方程的答案,所以是 m ∗ ( n + 1 ) m*(n+1) m∗(n+1)大小的
学过小学数学的人都知道解方程组有加减消元和代入消元两种
高斯消元也是差不对的,一共有三中操作
- 用一个非零的数乘上某一行
- 把一行的若干倍加到另一行上
- 交换两行的位置
先举一个3元方程组的例子
{ x + 2 y − z = − 6 2 x + y − 3 z = − 9 − x − y + 2 z = 7 \begin{cases} x+2y-z=-6\\ 2x+y-3z=-9\\ -x-y+2z=7 \end{cases} ⎩⎪⎨⎪⎧x+2y−z=−62x+y−3z=−9−x−y+2z=7
处理出的矩阵就是
[ 1 2 − 1 − 6 2 1 − 3 − 9 − 1 − 1 2 7 ] \begin{bmatrix}1&2&-1&-6\\\ 2&1&-3&-9\\\ -1&-1&2&7 \end{bmatrix} ⎣⎡1 2 −121−1−1−32−6−97⎦⎤
我们可以通过以上的三中操作使得上面的矩阵变成如下形态:
[ 1 2 − 1 − 6 0 1 1 1 0 0 1 3 ] \begin{bmatrix}1&2&-1&-6\\\ 0&1&1&1\\\ 0&0&1&3 \end{bmatrix} ⎣⎡1 0 0210−111−613⎦⎤
我们发现这个矩阵在左上—右下对角线以上的元素都是非零的,而其他都为0
我们称这种式子为上三角矩阵
这种事字相当的好求,直接倒推出答案即可
于是就做完了,还是挺好理解的
时间复杂度 O ( n 3 ) O(n^3) O(n3)
double ans[N], a[N][N] ;
int n ;
signed main(){
scanf("%d", &n) ;
rep(i, 1, n)
rep(j, 1, n + 1)
scanf("%lf", &a[i][j]) ; // 读入方程
rep(i, 1, n) {
int r = i ;
rep(j, i + 1, n)
if (fabs(a[r][i]) < fabs(a[j][i]))
r = j ; // 找到绝对值最大的一行,并且交换,保证精度
if (fabs(a[r][i]) < eps) PRINT("No Solution") ;
if (i != r) swap(a[i], a[r]) ; // 交换两行
double div = a[i][i] ;
rep(j, i, n + 1) a[i][j] /= div ; // 先对该行中的所有元素消去div倍数
rep(j, i + 1, n) {
div = a[j][i] ; // 对之后的方程进行加减消元
rep(k, i, n + 1) a[j][k] -= a[i][k] * div ;
}
} // 到这个时候矩阵已经变成上三角矩阵
ans[n] = a[n][n + 1] ;
per(i, n - 1, 1) {
ans[i] = a[i][n + 1] ;
rep(j, i + 1, n) ans[i] -= a[i][j] * ans[j] ; // 减去之前的答案
}
rep(i, 1, n) printf("%.2lf\n", ans[i]) ;
return 0 ;
}
趁热打铁A一个模板题 球形空间产生器
我们所求的是一个 n n n维的球的球心坐标 o [ 1... n ] o[1...n] o[1...n],则 ∀ i ∈ [ 1 , n + 1 ] , ∀i∈[1,n+1], ∀i∈[1,n+1],都满足 ∑ j = 1 n ( o j − x i j ) 2 = R 2 \sum\limits_{j=1}^n(o_j-x_{ij})^2=R^2 j=1∑n(oj−xij)2=R2, R R R为该球半径
这个式子无从下手,我们只好去化简一下
把左右两个式子相减,得到 ∑ j = 1 n [ ( o j − x i j ) 2 − ( o j − x i + 1 j ) 2 ] = 0 \sum\limits_{j=1}^n[(o_j-x_{ij})^2-(o_j-x_{i+1j})^2]=0 j=1∑n[(oj−xij)2−(oj−xi+1j)2]=0
继续化简, ∑ j = 1 n 2 ∗ o j ∗ ( x i + 1 j − x i j ) = ∑ j = 1 n x i j 2 − x i + 1 j 2 \sum\limits_{j=1}^n2*o_j*(x_{i+1j}-x_{ij})=\sum\limits_{j=1}^n{x_{ij}}^2-{x_{i+1j}}^2 j=1∑n2∗oj∗(xi+1j−xij)=j=1∑nxij2−xi+1j2
这样就可做了
等式左边我们直接作为 n n n个 n n n元方程,右边就是每个方程所对应的常数。
用高消求出n个方程组的解就行了。
友情提醒:注意行末不能有空格
int n ;
double a[N][N], b[N][N] ;
signed main() {
scanf("%d", &n) ;
rep(i, 1, n + 1) rep(j, 1, n) scanf("%lf", &b[i][j]) ;
rep(i, 1, n)
rep(j, 1, n) {
a[i][j] = 2 * (b[i][j] - b[i + 1][j]) ;
a[i][n + 1] += (b[i][j] * b[i][j] - b[i + 1][j] * b[i + 1][j]) ;
} // 处理处n个方程
rep(i, 1, n) { // 高斯消元的板子
int r = i ;
rep(j, i + 1, n) if (fabs(a[j][i]) > fabs(a[r][i])) r = j ;
if (r != i) rep(j, i, n + 1) swap(a[r][j], a[i][j]) ;
rep(k, i + 1, n) {
double t = a[k][i] / a[i][i] ;
rep(j, i, n + 1) a[k][j] -= a[i][j] * t ;
}
}
per(i, n, 1) {
rep(j, i + 1, n) a[i][n + 1] -= a[j][n + 1] * a[i][j] ;
a[i][n + 1] /= a[i][i] ;
}
rep(i, 1, n) printf("%.3lf ", a[i][n + 1]) ; // 直接输答案
}
高斯消元常常用来解决有后效性的 d p dp dp问题(大部分都是概率期望 d p dp dp),常见的为图上随机游走和博弈问题
之后我们拿两个举个例子
第一题 [HNOI2013]游走
很显然这是一道期望dp题
我们需要知道经过每条边的期望值,这样就能够贪心的选择边了
但是如何算每条边的期望值?
很显然,每条边是否能经过由他的两个端点决定
由于每个点等概率的走向连接他的边,所以一条边的期望也就是这个点的期望值除该点的度
那么该边经过的期望值就是两端点到这条边的期望值之和
显然我们就把边期望转化到点期望
于是这个问题就有想法了
f [ x ] f[x] f[x]表示到达 x x x点的期望值, d e g [ x ] deg[x] deg[x]表示 x x x的度,假设与 x x x相连的点为 y 1 , y 2 , . . . y k y_1,y_2,...y_k y1,y2,...yk,则 f [ x ] = ∑ i = 1 k f [ y i ] n u m i f[x]=\sum\limits_{i=1}^k\dfrac{f[y_i]}{num_i} f[x]=i=1∑knumif[yi]
每个点都有一个方程,用高效去解就行了
注意两个细节
由于"游走"是从点 1 1 1开始,则计算点 1 1 1期望时实际期望应该是原期望 + 1 +1 +1
若有点和 n n n相连,那么在计算期望时是不需要将其算入的,因为到了点 n n n的时候是不会继续"游走"了
基本就解决了
我才不会告诉你我数组开小了RE了4次!!!
double eps = 1e-8 ;
int fr[N * N], to[N * N], num[N] ;
vi e[N] ;
double q[N * N], f[N][N] ;
int n, m ;
signed main() {
scanf("%d%d", &n, &m) ;
rep(i, 1, m) {
int a, b ; scanf("%d%d", &a, &b) ;
e[a].pb(b) ; e[b].pb(a) ;
num[a]++, num[b]++ ;
fr[i] = a ; to[i] = b ;
}
f[1][n] = 1 ;
rep(i, 1, n - 1) {
f[i][i] = 1 ;
rep(j, 0, siz(e[i]) - 1) {
int v = e[i][j] ;
if (v != n) f[i][v] = -1.0 / num[v] ;
}
}
rep(i, 1, n - 1) {
int r = i ; double s = f[i][i] ;
rep(j, i + 1, n - 1)
if (fabs(s - 1.0) - fabs(f[j][i] - 1.0) >= eps) r = j, s = f[j][i] ;
if (r != i)
rep(j, 1, n) swap(f[i][j], f[r][j]) ;
per(j, n, i) f[i][j] /= f[i][i] ;
rep(j, 1, n - 1) {
if (i != j)
per(k, n, i) f[j][k] -= f[j][i] * f[i][k] ;
}
}
rep(i, 1, m) {
if (fr[i] != n) q[i] += f[fr[i]][n] * (1.0 / num[fr[i]]) ;
if (to[i] != n) q[i] += f[to[i]][n] * (1.0 / num[to[i]]) ;
}
sort(q + 1, q + m + 1) ;
double ans = 0.0 ;
rep(i, 1, m) ans += q[i] * ((m - i + 1) * 1.0) ;
printf("%.3lf\n", ans) ;
return 0 ;
}
之后还是概率dp题 赶小猪(驱逐猪猡)
一开始在 1 1 1有个炸弹,在每个点有 p q \frac{p}{q} qp的概率的爆炸,如果不爆炸则等概率地走向相邻的点,问在每个点爆炸的概率
设 f u f_u fu表示走到 u u u的期望次数, d e g u deg_u degu表示 u u u的度,初始 f 1 = 1 f_1=1 f1=1
因为必须不爆炸才向下走,所以有 f u = ∑ ( u , v ) ∈ E 1 d v ( 1 − p q ) f v f_u=\sum\limits_{(u,v)∈E}\dfrac{1}{d_v}(1-\dfrac{p}{q})f_v fu=(u,v)∈E∑dv1(1−qp)fv
在每个点爆炸的概率就是期望走到这个点的次数*爆炸的概率
用高消求方程即可
注意此题卡精度!!!出题人毒瘤。。。 eps考到1e-15就差不多了
int n, m, p, q ;
double gl ;
int num[N], e[N][N] ;
double ans[N], g[N][N] ;
signed main() {
scanf("%d%d%d%d", &n, &m, &p, &q) ; gl = 1.0 * p / q ;
rep(rd, 1, m) {
int a, b ; scanf("%d%d", &a, &b) ;
e[a][b] = e[b][a] = 1 ;
num[a]++ ; num[b]++ ;
}
rep(i, 1, n) g[i][i] = 1 ;
rep(i, 1, n)
rep(j, 1, n)
if (e[i][j])
g[i][j] += -(1.0 - gl) / num[j] ;
g[1][n + 1] = 1 ;
rep(i, 1, n) {
int r = i ;
rep(j, i, n) if (fabs(g[r][i]) >= fabs(g[j][i])) r = i ;
if (r != i)
rep(j, i, n + 1) swap(g[i][j], g[r][j]) ;
rep(j, i + 1, n)
if (fabs(g[j][i]) >= eps) {
double t = g[j][i] / g[i][i] ;
rep(k, i, n + 1) g[j][k] -= t * g[i][k] ;
}
}
per(i, n, 1) {
rep(j, i + 1, n) g[i][n + 1] -= ans[j] * g[i][j] ;
ans[i] = g[i][n + 1] / g[i][i] ;
}
rep(i, 1, n) printf("%.9lf\n", ans[i] * gl) ;
return 0 ;
}
感觉说的差不多了, 放个例题
Electric resistance
也是个高消的题目
之后我们说说高斯消元的应用,LU分解
以上暴力方法并不能挖掘出矩阵的什么性质,我们来搞事情
我们设 A = L U A=LU A=LU,称为 A A A的 L U LU LU分解,其中 L L L是一个单位下三角矩阵(对角线元素为单位元1), U U U是一个上三角矩阵,那么可以得到 y = A x = L U x y=Ax=LUx y=Ax=LUx。
如果设向量 z = U x z=Ux z=Ux,那么我们就得到了两个新的等式: L z = y Lz=y Lz=y和 U x = z Ux=z Ux=z。
因为 U U U是一个上三角矩阵,所以如果能求出 z z z那么我们可以很快地求出 x x x。
因为 L L L是一个单位下三角矩阵,所以我们可以更快地求出来 z z z。
也就是说,如果得到 A A A的 L U LU LU分解,那么我们可以在 O ( n 2 ) O(n^2) O(n2)的时间内对于任何一个向量 y y y都求出向量 x x x。
现在问题就是我们如何求出一个矩阵的 L U LU LU分解。
幸运的是,求解 L U LU LU分解的方法和之前的高斯消元是一样的。
在做完求解线性方程组的高斯消元之后, A A A数组的对角线及对角线上就是 U U U矩阵,对角线下就是 L L L矩阵,而消元之后 y y y数组的内容实际上就是向量 z z z。
预告:这东西将在之后的内容中发挥作用
之后我们来说一说矩阵的行列式
一个 n ∗ n n*n n∗n的方阵 A A A的行列式为 d e t ( A ) det(A) det(A)(也记做 ∣ A ∣ |A| ∣A∣)
例如二维矩阵 A = ( a c b d ) A= \begin{pmatrix}a&c\\b&d\end{pmatrix} A=(abcd) 的行列式 d e t ( A ) = a d − b c det(A)=ad-bc det(A)=ad−bc
把n*n的行列式中的元素 a i j a_{ij} aij所在的第 i i i行和第 j j j列划分出去时,留下的n-1阶行列式叫做 a i j a_{ij} aij的余子式,记 A i j = ( − 1 ) i + j M i j A_{ij}=(-1)^{i+j}M_{ij} Aij=(−1)i+jMij,叫做元素 a i j a_{ij} aij的代数余子阵
听着云里雾里? 举个栗子呗
设 D = ∣ a 11 a 12 a 13 a 14 a 21 a 22 a 23 a 24 a 31 a 32 a 33 a 34 a 41 a 42 a 43 a 44 ∣ D = \begin{vmatrix}a_{11}&a_{12}&a_{13}&a_{14}\\ a_{21}&a_{22}&a_{23}&a_{24}\\ a_{31}&a_{32}&a_{33}&a_{34}\\ a_{41}&a_{42}&a_{43}&a_{44}\\ \end{vmatrix} D=∣∣∣∣∣∣∣∣a11a21a31a41a12a22a32a42a13a23a33a43a14a24a34a44∣∣∣∣∣∣∣∣
则 M 23 = ∣ a 11 a 12 a 14 a 31 a 32 a 34 a 41 a 42 a 44 ∣ M_{23}= \begin{vmatrix} a_{11}&a_{12}&a_{14}\\ a_{31}&a_{32}&a_{34}\\ a_{41}&a_{42}&a_{44}\\ \end{vmatrix} M23=∣∣∣∣∣∣a11a31a41a12a32a42a14a34a44∣∣∣∣∣∣
A 23 = ( − 1 ) 2 + 3 M 23 = − M 23 A_{23}=(-1)^{2+3}M_{23}=-M_{23} A23=(−1)2+3M23=−M23
之后我们隆重推出行列式的定义:一个
n
n
n阶矩阵的行列式等于其任意一行和列的元素与对应的代数余子式乘积之和,即
d e t ( A ) = a i 1 A i 1 + . . . + a i n A i n = ∑ j = 1 n a i j ( − 1 ) i + j d e t ( A i j ) det(A)=a_{i1}A_{i1}+...+a_{in}A_{in}=\sum\limits_{j=1}^na_{ij}(-1)^{i+j}det(A_{ij}) det(A)=ai1Ai1+...+ainAin=j=1∑naij(−1)i+jdet(Aij)
很显然递归调用
如何求解一个矩阵 A A A的行列式 d e t ( A ) det(A) det(A)?
一种朴素的朴素的方法,直接按照定义计算,时间复杂度 O ( n ! ) O(n!) O(n!)
可否优化?
给出一个定理:如果 A = X Y A=XY A=XY,其中 A , X , Y A,X,Y A,X,Y都是方阵,那么 d e l ( A ) = d e l ( X ) ∗ d e l ( Y ) del(A)=del(X)*del(Y) del(A)=del(X)∗del(Y)
显然,(上/下)三角矩阵的行列式就是它对角线上所有元素的乘积
那么,对 A A A高斯消元求出矩阵的行列式只能为 1 1 1或 − 1 -1 −1,而且可以 O ( n ) O(n) O(n)求出 L U P LUP LUP分解然后对角线乘起来就可以了
另一种更简单的证明用到了一个性质:任意一行乘以一个常数加到另一行,矩阵的行列式不变。
接下来是矩阵树定理
问题:给你一张无向图图,求生成树个数。
设 E E E为图 G G G的邻接矩阵(矩阵中元素代表该边的个数,可有重边和自环), D D D为图 G G G的度数矩阵(对角线处元素为点的度数,其他地方为0),令 M = E − D M=E-D M=E−D,那么 M M M去掉任何一行一列之后的行列式的值就是该图的生成树个数。
时间复杂度 O ( n 3 ) O(n^3) O(n3)。
给个裸题 [HEOI2015]小Z的房间
很明显就是矩阵树定理的裸题
因为边权都为1,建出矩阵用高消求行列式就搞定了
ll ans = 1 ;
int h[N][N], vis[N] ;
ll f[N][N] ;
char s[N] ;
int n, m ;
int id(int x, int y) {
return (x - 1) * m + y ;
}
signed main(){
scanf("%d%d", &n, &m) ;
rep(i, 1, n) {
scanf("%s", s + 1) ;
rep(j, 1, m) {
if (s[j] == '.') h[i][j] = 1 ;
else vis[id(i, j)] = 1 ;
}
}
rep(i, 1, n)
rep(j, 1, m)
if (h[i][j]){
int deg = 0 ;
if (h[i][j - 1]) f[id(i, j)][id(i, j - 1)] = -1, deg++ ;
if (h[i][j + 1]) f[id(i, j)][id(i, j + 1)] = -1, deg++ ;
if (h[i + 1][j]) f[id(i, j)][id(i + 1, j)] = -1, deg++ ;
if (h[i - 1][j]) f[id(i, j)][id(i - 1, j)] = -1, deg++ ;
f[id(i, j)][id(i, j)] = deg ;
}
n = n * m - 1 ;
rep(i, 1, n)
if (!vis[i]) {
rep(j, i + 1, n)
if (!vis[j]) {
while (f[j][i]) {
ll r = f[i][i] / f[j][i] ;
rep(k, 1, n) {
f[i][k] = (f[i][k] - f[j][k] * r % MOD + MOD) % MOD ;
swap(f[i][k], f[j][k]) ;
}
ans = (MOD - ans) % MOD ;
}
}
if (!f[i][i]) print(0) ;
ans = (ans * f[i][i] + MOD) % MOD ;
}
printf("%lld\n", ans) ;
return 0 ;
}
矩阵求逆先咕了,听说SY和HH都秒了tql
先讲线性基吧
问题就是简化一个集合 S S S成为最小集 R R R,使得 S S S中的异或集与 T T T的相同
也就是说, R R R内元素随意异或能得出的数的集合和 S S S内元素随意异或能得出的数的集合相同
怎么解决?
我们通过加数方法实现
如果某个数 x x x能够被当前集合 R R R中的子集的异或表示,那我们就不需要 x x x,否则就要加入
那么如何判定他能够被 R R R中的子集表示?
把x拆成一个向量 [ a 0 a 1 . . . a k − 1 ] \begin{bmatrix} a_0&a_1&...&a_{k-1}\end{bmatrix} [a0a1...ak−1],则 x = ∑ i = 0 k − 1 a i ∗ y i x=\sum\limits_{i=0}^{k-1}a_i*y^i x=i=0∑k−1ai∗yi,其中 k k k是位数,在模 y y y意义下。
那么,求 x x x能被表示成 S S S的某个子集的异或和就等价于求 x x x能被表示成 S S S中数所对应的向量在模2意义下的线性组合。
这个东东可以用高消去做
对于新加入的数 x x x,如果它能被表示成 R R R,就代表它能被 R R R消成0。
我们把向量反过来写,把高位系数写在左边,低位系数写在右边,对所有书做高斯消元。
最终矩阵会被消成一个上三角矩阵,剩下的不为0的向量就是 R R R,而且每个向量(数)的最左边的非零数(最高位)都不相同。
因为矩阵行秩=列秩,所以 R R R最多有 k k k个元素。
模2意义下的矩阵操作可以压位做。
记 r i r_i ri为最高位为i的线性基中的数。
每次新加进来一个数之后从最高位开始消。
消不了就加入 r r r中。
插入方法如下
rep(i, 0, n - 1) {
ll tmp = a[i] ;
per(k, 50, 0)
if (tmp & (1ll << k)) {
if (!r[k]) r[k] = tmp ;
tmp ^= r[k] ;
}
}
查询:
per(i, 50, 0) if (ans < (ans ^ r[i])) ans ^= r[i] ;
printf("%lld\n", ans) ;
代码很短,线性基NB!!!
看一道简单题 [BJWC2011]元素
很显然,直接贪心的选择,能不能放用线性基去判定就行了
int n ;
ll ans ;
pll r[N] ;
ll p[N] ;
bool add(ll x) {
per(i, 63, 0)
if (x & (1ll << i)) {
if (p[i]) x ^= p[i] ;
else {
p[i] = x ;
return true ;
}
}
return false ;
}
bool cmp(pll a, pll b) {
return a.fi > b.fi ;
}
signed main(){
scanf("%d", &n) ;
rep(i, 1, n) scanf("%lld%lld", &r[i].se, &r[i].fi) ;
sort(r + 1, r + n + 1, cmp) ;
rep(i, 1, n)
if (add(r[i].se))
ans += r[i].fi ;
printf("%lld\n", ans) ;
return 0 ;
}
还有一些题/题解待填坑、
最小生成树计数 洛谷过目前97
[FJOI2007]轮状病毒 过不想写
bzoj4128 Matrix 没懂
bzoj3168 钙铁锌硒维生素 不会
bzoj4568 [SCOI2016]幸运数字 不会
bzoj4184 shallot