zoukankan      html  css  js  c++  java
  • 线性代数(不完全)与矩阵合集

    杂谈

    一.前言

    ​ 我,不喜欢线性代数。这是前前后后断断续续写的,语言和逻辑不清见谅。(下次还敢

    二.线性代数与矩阵

    ​ 大概是矩阵吧,一个关于线性的诡异东西。线性代数拆开,就变成了代数和线性。代数很好理解,无非就是解一些方程,或者带一些数进去,而关于所有的元都要求次数为1,即为线性。

    1.矩阵的定义

    ​ 在二维上一堆数排在一起。吧。就是矩形其实,只不过由一些数拼凑而成。很神秘,大小为 (n*m) 的矩阵有 n 行 m 列。按照我的理解,矩阵中的数字在运用中多半是代表着系数。

    2.矩阵的加减法

    ​ 前提是两个矩阵大小相等,假设结果矩阵为 (res) ,有 (res_{ij}=A_{ij}+B_{ij}) ,减法同理,还算科学。

    3.矩阵的乘法

    ​ 前提不是大小相等,而是……(A) 的大小为 (n*m), B 的大小为 (m*p) ,(res) 的大小为 (n*p) 有点像把中间的 (m) 给消掉了。(祖玛吗这)乘法其实是我觉得特别魔幻的一个部分,第一次看到就会觉得头大的那种。下面给出柿子。

    [Res_{i,j}=sumlimits_{k=1}^{m} A_{i,k}*B_{k,j} ]

    (其中 i 范围为 n ,j 为 p,这也能解释res的大小,同时意识到 k 在两个矩阵中都有,范围自然是 m)

    ​ 多提一嘴这个其实是计算几何中点积的形式,在 m 维中,虽然我不知到有什么特别的意义,但是也许可以加强记忆?

    ​ 顺便这个和 (Floyd) 的柿子长得很像,于是也有一类最短路题是关于这个的。

    4.系数矩阵

    ​ 假如我们有一些线性方程,他们共同组成了一个方程组。例如:

    [egin{align} 2a+3b&=6\ 7b+c&=8\ 3a+4c&=11 end{align} ]

    可能会有解,也可能会没有吧。乱写的。

    ​ 但是这个方程组可以写成一个很奇妙的形式。

    [2~~~~3~~~~0~~~~~~~~~~~~~~a~~~~~~~~~6\ 0~~~~7~~~~1~~~~~~~~~~~~~~b~~~~~~~~~8\ 3~~~~0~~~~4~~~~~~~~~~~~~~c~~~~~~~11\ ]

    实际上左边的矩阵乘上中间矩阵等于右边矩阵所代表的形式就是上面的方程组(可以手玩),换而言之一个可行的中间矩阵就可以当作一组解。

    ​ 理论上讲,一般 n 个元需要 n 个线性方程才解的出来,这 n 个方程用上述方法会整出一个 (n*n) 的矩阵,它叫做系数矩阵。

    5.增广矩阵

    ​ 就是将方程右边的数值和系数矩阵拼在一起。叫做增广矩阵。还是上面的例子。

    [2~~~~3~~~~0~~~~~~~~~6\ 0~~~~7~~~~1~~~~~~~~~8\ 3~~~~0~~~~4~~~~~~~~11\ ]

    6.初等行变换

    ​ 顾名思义,是关于行的操作。一共有三种操作。下文的 k 不能等于 0,但是可以取负。

    • 将两行交换
    • 一行加上另一行的 k 倍
    • 一行乘上 k

    初等行变换会多次运用于之后的讲解中。按照我的理解,初等行变换并不会改变这个矩阵,按照百度的说法,若是两个矩阵可以通过行变换相互转化,那么称它们是等价的。在做题中一般可以使求解简化,且变化时答案和矩阵所对应的值与关系不会有太大改变。

    7.左乘一个矩阵

    ​ 其实我是想要说对一个矩阵实行初等行变换等于左乘一个矩阵。(左乘指将这个矩阵放在左边做乘法,因为矩阵的乘法不满足交换律所以有左右之分)。

    ​ 换而言之,所有的初等行变换可以通过左乘一个矩阵来实现相同的效果。

    8.高斯消元

    ​ 一个基于增广矩阵和初等行变换的未知数求解算法,有时间就单开一篇。

    9.矩阵加速

    ​ 常见于各种DP题中,比如最经典的斐波拉契数列,它的递推式满足

    [F_i=F_{i-1}+F_{i-2} ]

    于是可以转化为

    [({F_{i-1}}~~~~{F_{i-2}})*(_{1 ~0}^{1~1})=(F_{i}~~~~F_{i-1})\ ]

    可以细细感悟其中个过程,一般只要递推式是单纯的线性几项加减,都可以转化为矩阵形态,可以发现,一般乘上的是一个只有常数的矩阵,如果不需要中间的值,就可以把很长的柿子写出来,可以矩阵满足乘法结合律,拿矩阵做一个快速幂芜湖起飞~

    10.特殊的矩阵

    ​ 对角线矩阵:只有主对角线有值的矩阵

    ​ 上三角矩阵:只有上半部分有值(包括主对角线)的矩阵。

    三.矩阵的逆

    ​ 关于这个问题,涉及到一小段数学史。依我之拙见,数学一开始只有在整数域中的加法。需要反向求解加法的时候就依附于加法创造了减法(加上一个数的相反数)。而乘法也是由于加法的次数过多而产生的。

    ​ 但是思考到乘法的逆运算除法的时候,也是添加了一个定义,用小学的话来说叫做“除以一个数等于乘上这个数的倒数”。而这个“倒数”( (frac{1}{x}) ),就是整数域中的逆。

    ​ 而后数学的范围在不断扩大,有小数,实数,复数,有理数,无理数等,他们都是不同的数域,纵观之下,会发现其中的逆与一个叫作“元”的东西有着密不可分的联系。

    ​ 此处批注:一般都只会讨论方阵的逆

    ​ 首先思考元的定义。在实数域中,元是“1”,也就是常说的“单位1”。但是元之所以为元是为什么呢?我曾记得有一个表情包是这样的:

    (1*0=0) 的原因到底是 1 乘上任何数都是那个数,还是 0 乘任何数都是 0 呢?

    虽然比较扯淡,但是这句话也确实点出了一些元的性质:对于任何一个数域中的数 (x),都有元 (e) 使得 (x*e=x) ,当然这里的乘号根据在这个域中的定义是不同的,意会就好。当然的,不是每一种数域都有自己的元,幸运的是,矩阵有着自己的元。

    ​ 矩阵的元叫做单位矩阵。通常记作 (I) ,它的形式是一个主对角线为 1 的方阵。其余的数为0,显然满足元的定义。

    ​ 逆,元,逆元。

    ​ 这三者从名字上就有种深刻的联系,说到逆元,肯定首先会先想到取模运算中,做除法需要求逆元。而这样做的根据就是:在除法中,也叫做求这个数的逆,除一个数,等价于乘上这个数的逆元。

    逆元

    ​ 对于一个元素 (A) 若有 (B) 使得 (A*B=e) 那么称 B 为 A 的逆元。在某些特殊的数域中,这种关系是相互的,即 A 和 B 互为逆元。在方阵中,这种关系是相互的。此处给出简单证明。

    已知 (AB=I) , (BC=I) 求证 (A=C)

    (ABC=(AB)C=A(BC)=A=C)

    矩阵的除法

    ​ 有了上面的铺垫(此处特指 二.6 和 二.7 和 二.8)之后我们试图求一个矩阵 A 的逆元。换而言之我们需要一个矩阵 B 使得 (BA=I) ,为了好讲,B 在左边。那么又可以知道大部分矩阵都可以通过初等行变换成为 I.

    ​ 具体的方式为像高斯约旦消元一般消成对角线矩阵,最后再乘成1就好。

    ​ 若是消不成对角线矩阵,证明有一列全为 0 ,就求不了逆,相当于除数为0.

    ​ 由于初等行变换等于左乘一个矩阵,所以这个等效矩阵就是 B.此时有(BA=I).但是其实 B 不太好表示,但是没有关系,在进行行变换的时候同时对一个单位矩阵做相同的矩阵就好,最后就会求得B。下面给出代码(有取模)

    矩阵求逆

    struct Matrix{
    	int size;
    	int num[lst][lst];
    	inline void init(){size=0;memset(num,0,sizeof(num));}
    	inline Matrix makeI(int x){
    		Matrix ans;
    		ans.init();
    		ans.size=x;
    		M_for(i,0,size-1)ans.num[i][i]=1;
    		return ans;
    	}
    	inline void ins(int x){
    		init();
    		size=x;M_for(i,0,x-1)M_for(j,0,x-1)num[i][j]=read();
    	}
    	inline void pr(){
    		if(size==-1)printf("No Solution");
    		else 
    			M_for(i,0,size-1){
    				M_for(j,0,size-1)printf("%d ",num[i][j]);
    				printf("
    ");
    			}
    	}
    };
    Matrix QN(Matrix x){
    	Matrix ans=x.makeI(x.size);
    	M_for(i,0,x.size-1){
    		int k=x.num[i][i],p=i;
    		M_for(j,i+1,x.size-1)if(x.num[j][i]>k)k=x.num[j][i],p=j;
    		if(!k){ans.size=-1;return ans;}
    		swap(x.num[i],x.num[p]);
    		swap(ans.num[i],ans.num[p]);
    		int alf=ksm(x.num[i][i],mod-2);
    		M_for(j,0,x.size-1)if(j!=i){
    			int g=(1LL*x.num[j][i]*alf)%mod;
    			M_for(q,0,x.size-1){
    				x.num[j][q]=((1LL*x.num[j][q]-1LL*x.num[i][q]*g)%mod+mod)%mod,
    				ans.num[j][q]=((1LL*ans.num[j][q]-1LL*ans.num[i][q]*g)%mod+mod)%mod; 
    			}
    		}
    		M_for(j,0,x.size-1)x.num[i][j]=(1LL*x.num[i][j]*alf)%mod,
    		ans.num[i][j]=(1LL*ans.num[i][j]*alf)%mod;
    	}
    	return ans;
    }
    

    四.期望与高斯消元

    ​ 期望DP其实一直是一个令人头秃的问题,大部分的都是倒着推转移方程式就能做。但是有一些问题,他们的状态转移不满足DP所代表的 DAG ,也就是会互相影响。此时会发现每一个要求的期望是一个未知数,然后从其他的状态转移过来也都是未知数,且一般都是线性转移求和。

    ​ 于是可以对于每一个状态列方程。下面结合着例题来讲。

    到 t 的路程期望

    暂时没找到例题?但是可以有转移柿子 (Eu=sum_j^{u o v}p_j(E_v+len_j)),这个柿子很感性,看到就觉的是对的,但是并没有那么的简单,如果每个都想当然会把很多东西都弄错。下面给出从期望定义开始的严格证明。

    [egin{align} E_u&=sum_i^{u o t}p_iw_i\ &=sum_j^{u o v}sum_i^{v o t}p_ip_j(w_i+len_j)\ &=sum_j^{u o v}p_jsum_i^{v o t}(p_iw_i+p_ilen_j)\ &=sum_j^{u o v}p_j(E_v+sum_i^{v o t}p_ilen_j)\ &=sum_j^{u o v}p_jE_v+sum_i^{v o t}p_isum_j^{u o v}p_jlen_j\ &=sum_j^{u o v}p_j(E_v+len_j) end{align} ]

    迷惑理解:因为所有走到 u 的必然会经过与它相连的边,所以上面的证明有一步拆分,所以就从与 u 相连的点转移,可以类比最短路?随便口胡的。

    到 t 的异或和路径期望

    XOR和路径

    ​ 这道题如果想当然的和前面一样转移就会是错误的,不信从定义开始推,会在某个地方卡住。如果用一句话总结的话就是期望的异或不是异或的期望。

    ​ 既然直接上期望不行,那我们转而去求概率,因为期望一般为概率的倒数。就拿这道题来说,虽然不知道最后的期望是多少,但是考虑到异或之间每一位不相互影响,于是可以求出每一位上为 1 或者为 0 的概率。最后再求和就好。

    ​ 具体而言,记每一个点的出度为 (deg) ,(w(i,u,v)) 表示(u o v) 这条边的二进制第 i 位的值。(f_{k,i}) 表示到 k 的时候第 i 位为 1 的概率(为 0 的概率可以用1减),于是有。

    [egin{align} f_{k,i}&=frac{1}{deg_i}(sum_{w(k,i,j)=0}f_{k,j}+sum_{w(k,i,j)=1}1-f_{k,j})\ f_{k,i}deg_i&=sum_{w(k,i,j)=0}f_{k,j}+sum_{w(k,i,j)=1}1-f_{k,j}\ f_{k,i}deg_i-sum_{w(k,i,j)=0}f_{k,j}+sum_{w(k,i,j)=1}f_{k,j}&=sum_{w(k,i,j)=1}1 end{align} ]

    于是方程就搞好了,对于二进制每一位高斯消元最后汇总就可。给出代码。

    const int lst=105;
    struct Matrix{
    	int size;
    	double num[lst][lst];
    	inline void gs(){
    		int loc=1;
    		for(int i=1;i<=size;++i){
    			int p=loc;
    			for(int j=loc+1;j<=size;++j)if(mabs(num[j][i])>mabs(num[p][i]))p=j;
    			if(check(num[p][i]))continue;
    			swap(num[loc],num[p]);
    			for(int j=1;j<=size;++j)if(j!=loc){
    				double gate=1.0*num[j][i]/num[loc][i];
    				for(int k=1;k<=size+1;++k)num[j][k]-=num[loc][k]*gate;
    			}
    			loc++;
    		}
    	}
    	inline void init(){size=0;memset(num,0,sizeof(num));}
    }S;
    int to[10005],fr[10005],w[10005],deg[lst],MX;
    double ans;
    inline void solve(int x){
    	S.init();
    	S.size=n;
    	for(int i=1;i<=n;++i)S.num[i][i]=deg[i];
    	for(int i=1,u,v;i<=m;++i){
    		u=fr[i],v=to[i];
    		if(w[i]&x){
    			S.num[u][v]++,S.num[u][n+1]++;
    			if(u^v)S.num[v][u]++,S.num[v][n+1]++;
    		}
    		else{
    			S.num[u][v]--;
    			if(u^v)S.num[v][u]--;
    		}
    	}
    }
    int main(){
    	n=read();m=read();
    	for(int i=1;i<=m;++i){
    		fr[i]=read();to[i]=read();
    		w[i]=read();
    		deg[to[i]]++;
    		if(fr[i]^to[i])deg[fr[i]]++;
    		MX=max(MX,w[i]);
    	}
    	for(int i=1;i<=MX;i<<=1){
    		solve(i);
    		memset(S.num[n],0,sizeof(S.num[n]));
    		S.num[n][n]=1;
    		S.gs();
    		ans+=1.0*i*S.num[1][n+1]/S.num[1][1];
    	}
    	printf("%.3lf",ans);
    	return 0;
    }
    

    某个点的期望经过次数

    赶小猪

    先讲感性做法,求出每个点的一直不爆炸的期望经过次数乘上爆炸概率。

    [f_{u}=sum_{(u,v)in E}frac{1}{deg_v}f_v(1-P) ]

    这个只是一个大概式子,但是在实际问题之中,会有出发点一开始为 1 ,终点无法对其他造成贡献等等

    在代码注释里面细讲

    const int MAXN=305;
    int n,m,q,p;
    int fr[MAXN*MAXN],to[MAXN*MAXN],deg[MAXN];
    double S[MAXN][MAXN],P;
    int main(){
    	n=read();m=read();p=read();q=read();
    	P=1.0*p/q;
    	for(int i=1;i<=m;++i)deg[(fr[i]=read())]++,deg[(to[i]=read())]++;//记录出度
    	for(int i=1;i<=m;++i)
    		S[fr[i]][to[i]]+=(1.0-P)/deg[to[i]],S[to[i]][fr[i]]+=(1.0-P)/deg[fr[i]];//系数
    	for(int i=1;i<=n;++i)S[i][i]=-1.0;//起点一开始会有一个多余的+1,移项当常数
    	S[1][n+1]=-1.0;//移项常数变-1
    	for(int i=1,loc=1;i<=n;++i){
    		double k=S[loc][i];
    		int p=loc;
    		for(int j=loc+1;j<=n;++j)if(mabs(S[j][i])>mabs(k))k=S[j][i],p=j;//高斯约旦消元
    		if(check(S[p][i]))continue;
    		swap(S[loc],S[p]);
    		for(int j=1;j<=n;++j)if(loc!=j){
    			double g=1.0*S[j][i]/S[loc][i];
    			for(int k=1;k<=n+1;++k)S[j][k]-=S[loc][k]*g;
    		}
    		loc++;
    	}
    	for(int i=1;i<=n;++i)printf("%.9lf
    ",S[i][n+1]/S[i][i]*P);
    	return 0;
    }
    

    但是!正确性不太显然。此处给出另一个正确性显然的做法。

    ​ 设 (f_{i,k,j}) 为由k走 i 步到 j 的期望经过次数。易得(A为邻接矩阵)

    [f_{i,u,j}=sum_{(u,j)in E} f_{i-1,u,v}*A_{v,j}*(1-P)\ herefore f_i=A^i ]

    然后要对所有的 i 的 (f_{i}) ,求和,i可以到正无穷。发现它收敛,等比数列求和,需要做一个求逆。

    然后下面给出感性做法的证明(感谢黄金之路和Clorf神犇的推导)

    (E _{u}=sumdfrac{E _{v}}{mathrm{deg} _{v}})

    (f _{k,u})(k) 条边正好到达 (u) 的概率。

    (f _{k+1,v}=sum _{u=1} ^{n}f _{k,u} imes A_{v,u})

    (sum _{k=0} ^{infty} f _{k,u})(1sim n) 的路径经过 (u) 的期望次数

    [E _{u}=sum _{i} ^{1 o n} p_{i}w _{i}=sum _{k=0} ^{infty} f _{k,u}=f _{0,u}+sum _{(v,u)} A _{u,v}sum _{k=0} ^{infty}f_{k,v}=f _{0,u}+sum _{(v,u)}A _{u,v}E _{v}=f _{0,u}+sum dfrac{E _{v}}{mathrm{deg} _{v}}=(u=1)+sum _{(u,v)}dfrac{E _{v}}{mathrm{deg} _{v}}​ ]

    (sum _{i,1 o ^{k} u } ^{1 o n} p_{i}=f _{k,u})

    (1 ightleftarrows 2 ightleftarrows 3)

    (f _{0}=(1,0,0))

    (f _{1}=(0,1,0))

    (f _{2}=(0.5,0,0.5))

    (f _{3}=(0,0.5,0))

    (f _{4}=(0.25,0,0.25))

    [A=left[egin{matrix} 0 & 0.5 & 0\ 1 & 0& 0\ 0 & 0.5 & 0\ end{matrix} ight] ]

    (A _{i,j}) 代表 (j o i) 的概率。

    Clorf 强强

    某条边的期望经过次数

    HNOI2013 游走

    ​ 这个题就是如此,求出期望经过次数分配就好。边的经过次数由两边的点经过次数所决定。

    [E_{u,v}=frac{f_u}{deg_u}+frac{f_v}{deg_v} ]

    注意到终点不能再出。直接上代码,内附注释。

    double S[MAXN][MAXN],ans[505],f[125005],res;
    bool vis[MAXN][MAXN];
    int main(){
    	n=read();m=read();
    	for(int i=1,x,y;i<=m;++i){
    		x=read();y=read();
    		vis[x][y]=1;
    		if(y!=n)S[x][y]=1;//终点不出
    		if(x!=n)S[y][x]=1;
    		degree[x]++;degree[y]++;
    	}
    	for(int i=1;i<=n;++i)for(int j=1;j<=n;++j)if(vis[i][j]){
    		if(i!=n)S[j][i]/=degree[i];
    		if(j!=n)S[i][j]/=degree[j];
    	}
    	for(int i=1;i<=n;++i)S[i][i]=-1;//同上一篇
    	S[1][n+1]=-1;
    	int loc=1;
    	for(int i=1;i<=n;++i){
    		double k=S[loc][i];
    		int p=loc;
    		for(int j=loc+1;j<=n;++j)if(S[j][i]>k)k=S[j][i],p=j;
    		if(check(k)){continue;}
    		swap(S[loc],S[p]);
    		for(int j=1;j<=n;++j)if(i!=j){
    			double g=1.0*S[j][i]/S[loc][i];
    			for(int k=1;k<=n+1;++k)S[j][k]-=g*S[loc][k];
    		}
    		loc++;
    	}
    	for(int i=1;i<=n;++i)ans[i]=S[i][n+1]/S[i][i];
    	ans[n]=0;//终点不出
    	for(int i=1;i<=n;++i)for(int j=1;j<=n;++j){
    		if(vis[i][j])
    			f[++tot]=1.0*ans[i]/degree[i]+1.0*ans[j]/degree[j];//边的经过次数
    	}
    	sort(f+1,f+tot+1);
    	for(int i=1;i<=tot;++i)res+=f[i]*(tot-i+1);
    	printf("%.3lf",res);
    	return 0;
    }
    
  • 相关阅读:
    this指针是什么?
    C++多态实现原理
    面试题2:实现Singleton模式
    面试题1:赋值运算符函数
    1.Two Sum
    朴素贝叶斯
    K近邻法
    感知机
    数据类型、用户交互、赋值运算
    蓝牙技术概述
  • 原文地址:https://www.cnblogs.com/clockwhite/p/13675526.html
Copyright © 2011-2022 走看看