_________<<算法竞赛入门训练指南》
矩阵(matrix)是一个由数字排列成的矩形,例如
A=[a11,a12,a13] [1 2 3]
[a21,a22,a23]= [4 5 6]
是一个23的矩阵,其中i=1,2,j=1,2,3。第i行第j列的元素用aij表示。一般用大写字母表示矩阵,小写字母表示它的元素。A的转置A^T通过翻转A的行和列得到。例如,对于上面的A,小写字母表示它的元素。A的转置A的t次方通过翻转A的行和列得到,例如,对于上面的A,有
A^T=[1 4]
[2 5]
[3 6]
矩阵的运算。加法和减法简单的定义为逐个元素进行加减。注意,两个矩阵的行数,列数分别相等时,加减法才有定义。矩阵的乘法比较复杂,当A的列数等于B的行数,则可以定义乘法C=A
B。如果A是mn矩阵,B是np矩阵,那么C是一个m*p矩阵,其中Cij满足
Cik= a i j b j k \sum{aijbjk} aijbjk
根据这个定义容易得到一个O(N^3)时间的矩阵乘法。显然有更快的,但在算法竞赛中,立方时间的矩阵乘法已经足够。
如果你觉得矩阵乘法很奇怪,想一想下面这个线性方程组吧。
a11x1+a12x2+…a1nxn=b1
a21x1+a22x2+…a2nxn=b2

an1x1+an2x2+…annxn=bn
也就是
[a11 a12… a1k]=b1
[a21 a22… a2k]=b2

[an1 an2… ann]=bn

   这3个矩阵分别为A,x,和b(注意x和b都是n*1的矩阵,也称列向量) ,则线性方程组可以简单的写成Ax=b。这样是不是很方便呢?建议读者仔细研究一下这个Ax=b。
  它就意味着A把要给列向量x变成了另外一个列向量b。也就是说,矩阵A描述的是一个线性变换,它把向量x变换到了向量b。在算法竞赛中,只要遇到“把一个向量v变成另一个向量v',并且把v'的每一个分量都是v各个分量的线性组合”的情况,就可以考虑用矩阵乘法来描述这个关系。这里的线性组合是指每个元素乘以要给常数后相加。比如
  2x+3y-z就是x,y,z的线性组合,但5xy和x^2-3y都不是。矩阵乘法最重要的性质就是满足结合律,即(AB)C=A(BC),因此和幂取模一样,A^n也可以用倍增法加速。
  逆矩阵。在数论部分我们曾证明,在模n的缩系中,每个元素a都存在唯一的一个逆向a^-1使得a*a^-1=a^-1*a=1(我们还介绍了用扩展欧几里得算法设计这个a^-1的方法)。类似地,连n阶方阵(即n*n矩阵)的集合中,也存在一些特殊的矩阵A存在唯一逆元A^-1使得A*A^-1=A^-1*A=In,其中In是n阶单位矩阵(identity matrix)
  In=[1 0 ... 0]
       [0 1 ... 0]
       [0 0 ....1]
   我们暂且不管怎样的矩阵是可逆的,如果已知矩阵A是可逆的,并且逆矩阵为A^-1,那么方程组Ax=b是很容易求解的,两边左乘A^-1得到x=A^-1b。这说明方程组是有唯一解。
   “矩阵可逆”有一个常用的充分必要条件:所有行向量是线性无关的。什么叫线性无关呢?就是任意一个向量都不是其他向量的线性组合。比如3个向量(1,2,3),(1,3,5),(1,1,1)不是线性无关的。因为(1,1,1)=2*(1,2,3)-(1,3,5)。而(1,0,0),(0,1,0和(0,0,1)就是线性无关的。
   这个条件和“矩阵可逆”等价的严格证明超出了本书的范围,但其中的思想还是很直观的。比如:
   [1 2 1][x]     [10]
   [1 3 5][y]=   [9]  等价于
   [1 1 1][z]=   [8]
 [x+2y+z=10]             
 [x+3y+5z=9]
 [x+y+z=8]

为什么上面的方程组无解?因为(1,1,1)是(1,2,1)和(1,3,5)的线性组合,所以得到了两个左边是x+y+z的方程,并且右边不同(一个等于11,一个等于8),所以无解(我们称这两个方程为矛盾方程)。就算是等号右边恰好相同,也就说明两个方程重复了(其中一个是多余方程),相当于两个方程3个未知数,于是又无穷多解。注意我们用方程x+y+z=11代替了原来的x+3y+5z=9(而不是添加一个新的方程)。这是因为由x+2y+z=10和x+y+z=11可以推导出x+3y+5z=9,这是因为由x+2y+z=10和x+y+z=11可以推导出x+3y+5z=9.这样的变换保持了方程组的解集不变,称为同解变换。
顺便说一句,虽然逆矩阵看上去很吸引人,但实际上在多数情况下,我们都可以避开矩阵求逆。事实上,算法竞赛中极少会用到矩阵求逆的过程。因此本书不加以讨论(尽管求逆的过程几乎就是一个高斯消元)
高斯消元计算举例。理论知识已经介绍得比较充分了,还是求解线性方程组了吧!在中学时,我们学过高斯消元(gauss elimination)。在算法领域,这个方法仍然可以用来求解线性方程组。简单起见,先假设系数矩阵A是一个n阶可逆矩阵。例如:
2x+y-z=8 (L1)
-3x-y+2z=-11 (L2)
-2x+y+2z=-3 (L3)
写成矩阵的形式为
[2 1 -1 ] [8]
[-3 -1 2] [11]
[-2 1 2 ] [-3]
这俄格矩阵称为增广矩阵(argumented matrix),其中最后一列并不是系数矩阵,而是等式右边的常数列。
从整体上来讲,我们从上到下依次处理每一行,处理完第i行后,让Aii非0,而Aji均为0,过程如下。
[2 1 -1] [8]
[-3 -1 2] [-11]
[-2 1 2] [-3]
等价于
[-3 -1 2] [-11]
[ 1/3 1/3] [2/3]
[ 5/3 2/3] [13/3]
等价于
[-3 -1 2] [-11]
[ 5/3 2/3] [13/3]
[ 1/5] [-1/5]
其中最后一个增广矩阵的系数部分是上三角形(0用空白表示),而且主对角元素aii均非0,该矩阵对应的方程组为
{-3x-y+2z=-11
5y/3+2z/3=13/3
z/5=-1/5
}
直接可得z=-1,把z的值代入第二个方程得y=3;把y和z的值代入第一个方程得x=2。
这是一个比较直观的例子,求解过程还需要细化。在消化过程中,假设正在处理第i行,则首先需要找一个r>i且绝对值最大的ari,然后交换第r行和第i行。因为每行对应一个方程,交换两个方程的位置不会对解产生任何影响,但可以提高数值稳定性。当A可逆时,可以保证交换后aii一定不等于0,这种方法称为列主元法。
接下来进行所谓的“加减消元”。比如在下面的例子中,要用第一个方程(-3,1,2,-11)消去第二个方程(2,1,-1,8)的第一列,方法是把第二个方程中的每个数都减去第一行对应元素的-2/3倍
[2 1 -1] [8]
[-3 -1 2] [-11]
[-2 1 2] [-3]
等价于
[-3 -1 2][-11]
[2 1 -1][8]
[-2 1 2][-3]
等价于
[-3 -1 2] [11]
[ 1/3 1/3] [2/3]
一般情况下,如果要用第i个方程消去第k个方程的第j列,那么第k行的所有元素A[k][j]都应该减去A[i][j]的A[k][i]/A[i][i]倍。
下一个过程是回代。现在A已经是一个上三角矩阵了,即第1,2…行的最左边非0元素分别在第1,2,3列。这样,最后一行实际上已经告诉我们xn的值了;接下来像前面说的那样不停的回代计算,最终会得到每个变量的唯一解。
typedef double Matrix[Maxn][Maxn];
//要求系数矩阵可逆
//这列的A是增光矩阵
//运行结束后A[i][n]是第i个未知数的值
void Gause_elimination(Matrix A,int n)
{
int i,j,k,r;
//消元过程
for (int i=0;i<n;i++)
{
//选一行r并与第i行交换
r=i;
for (int j=i+1;j<n;j++)
if (fabs(A[j][i])>fabs(A[r][i])) r=j;
if (r!=i) for (int j=0;j<=n;j++)
swap(A[r][j],A[i][j]);
//与第i+1~n行进行消元
for (int k=i+1;k<=n;k++)
{
double f=A[k][i]/A[i][i];//为了让A{i][i]=0,第i行所乘以的倍数
for (int j=i;j<=n;j++)
A[k][j]-=f*A[i][j];
}

   } 
   //回代过程
   for (int i=n-1;i>=0;i--)
     for (int j=i+1;j<n;j++)
	   A[i][n]-=A[j][n]*A[i][j];
  A[i][n]/=A[i][i];

}
}
消元中的精度问题。
需要注意的是上述代码的消元部分中有一个中间变量f,保存的是为了使A[i][k]=0,第i行所乘以的倍数。注意,f的类型是double,有可能会损失精度,解决这个问题,
有一种方法是不使用中间变量,但前提是A[k][i]不会过早地被破坏,所以应写成按照列编号从大到小的顺序消元,代码如下。

for (int j=n;j>=i;j–)//必须逆序枚举
for (int k=i+1;k<n;k++)
A[k][j]-=A[k][i]/A[i][i]*A[i][j];
当然,对于大多数题目,前面的逐行消元法已经可以满足要求,但对于精度要求很高的题目,不妨采用上面的方法。
高斯-约当消元法。在消元过程中,还可以把系数矩阵变成对应对角阵而非上三角阵,从而省略回代过程(因为第i行只有A[i][i]和A[i][n]两项非0,所以直接可得x[i]=A[i][n]/A[i][i])。这个方法
称为高斯—亚当消元法(Gauss-Jordan elimination),运算量比高斯消元略大,但代码更简单(因为少了回调过程)。
A不可逆的情形。上述讨论和代码都只针对A可逆的情况。当A不可逆时,会引入一些复杂性,下面我们继续讨论。
一般情况下,消元过程的目标是在解不变的情况下把线性方程组的系数矩阵A变成阶梯矩阵(Row Echelon Form,REF),也就是说,除了第一行外,每一行最左边的非0元素在上一行最左边非0元素的右边(即前者的列编号严格大于后者的列编号),比如:
[1 2 -1 1 3][1]=[_1 2 -1 1 3][1]
[2 4 -1 2 4][2]=[0 0 _1 -1 -2][0]
[1 2 0 0 2][3]=[0 0 0 0 _1][2]
[0 0 -2 2 4][0]=[0 0 0 0 0][0]
注意,那些A矩阵全为0的行所对应的方程要么是多余方程(它们总是成立),要么是矛盾方程(整个方程组无解),应放在所有非全0行的下方。
上面的矩阵是怎么“回代”呢?把方程三的x5=2代入方程二的x3-x4-2x5=0只能得到x3-x4=4,无法得到方程x1+2x2=a,其中a的值取决于x4的具体数值。如果知道了x2的值,就可以算出x1.换句话说,这个方程组有两个自由变量(free variable)x2和x4,这两个
变量本身可以取任何值,而且只要这两个变量的取值确定了,所有其他变量(也就是每行最左边的非0元素所对应的那些变量)得取值确定了,所有其他变量(也就是每行最左边的非0元素所对应的那些变量)的取值也能确定。换句话说,设消元后矩阵有r个非0行,
且不存在矛盾方程(对应于系数部分全为0,但常数部分不为0的行),则一共有r个非自由变量,即有界变量(bound variable).这个r称为系数矩阵的秩(rank)。另外,不难发现,自由变量集的选择并不是唯一的。比如在刚刚举的例子中,也可以选x1和x3为自由变量。
但不管怎么选,自由变量得数量不会改变。