高斯消元
引入
与解二元方程组的方法类似,n元线性方程组有3个基本的操作:
1、将两个方程交换位置
2、将一个方程乘k
3、将一个方程a减去另一个方程b
显然这三个操作对答案不会有任何影响,那么用这三种操作进行消元(类似解二元一次方程组)的过程,我们称之为高斯消元。
高斯消元
高斯消元具体的步骤如下,对于一个i方程,我们取第i个元对1~M的方程(M是方程条数)进行消元,枚举完所有的方程后:
1、如果有解:剩下有n个方程都满足ax=b的形式(其实就是一个主对角线矩阵)
2、如果无解:剩下的方程中还有自由元,或不足n个方程满足ax=b的形式
这个变换的过程也可以看成是一个矩阵乘法,不过这个显然不是很常用。
细节
对于一个方程,可能出现第i个未知数的指数为0的情况,所以我们就要往下找一个第i个未知数不为零且绝对值最大的情况,为什么不能往上面找,因为上面的方程已有固定元,为什么要绝对值最大呢,因为消元的形式是相除,所以绝对值最大可以防止溢出,当只有整数时,消元时我们可以用直接乘上另一个数的方法以保证它们都是整数。
时间复杂度:O(MN^2)约为O(N^3),空间复杂度:O(MN)约为(N^2)。
CODE:
#include <algorithm> #include <iostream> #include <cstring> #include <cstdio> #include <vector> #include <cmath> #define sc1(p1) scanf("%d", &p1) #define sc2(p1, p2) scanf("%d %d", &p1, &p2) #define sc3(p1, p2, p3) scanf("%d %d %d", &p1, &p2, &p3) #define pr(p1) printf("%d\n", p1) using namespace std; const int maxn = 20; const int oo = 2100000000; const double eps = 1e-6; double a[maxn][maxn]; double b[maxn][maxn]; double ans; bool gauss(int n, int m) { if(m<n) return false; for (int i=1; i<=n; i++) { int id=i; for (int j=i+1; j<=n; j++) if(abs(b[j][i])>abs(b[id][i])) id=j; if(abs(b[id][i])<eps) return false; for (int j=i; j<=n+1; j++) swap(b[i][j], b[id][j]); for (int j=1; j<=n; j++) { if(i==j) continue; double t=b[j][i]/b[i][i]; for (int k=1; k<=n+1; k++) b[j][k]-=b[i][k]*t; } } return true; } int main() { //freopen(".in", "r", stdin); //freopen(".out", "w", stdout); int n; sc1(n); for (int i=0; i<=n; i++) for (int j=1; j<=n; j++) scanf("%lf", &a[i][j]); for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) { b[i][j]=2*(a[i][j]-a[i-1][j]); double t1=a[i][j]*a[i][j]; double t2=a[i-1][j]*a[i-1][j]; double t3=t1-t2; b[i][n+1]+=t3; } if(!gauss(n, n)) { pr(-1); return 0; } for (int i=1; i<=n; i++) { printf("%.3lf ", b[i][n+1]/b[i][i]); } return 0; }
—————————————————分割线—————————————————
同余方程
实际上就是将系数%上mod,然后跑一遍高斯消元就可以了。
—————————————————分割线—————————————————
行列式
按照行列式的定义去计算很麻烦,我们需要用更好的方法。
行列式有以下的一些性质:
1、交换任意两行,结果是之前的相反数。
2、让一行全部乘以k,结果也乘以k。
3、让一行全部乘以k再加到另外一行上,结果不变。
这几个性质很像高斯消元的矩阵变换。
我们只要对行列式进行高斯消元,按照行列式的规则计算答案的变化即可。
最后剩下主对角线矩阵时,由于只有一种选择方式不是0,行列式的结果为主对角线上每一个数乘起来。
—————————————————分割线—————————————————
线性基
将若干个向量排在一起变成矩阵,高斯消元成上三角矩阵,余下的就是这些向量的线性基。
用消元得到的线性基线性组合,可以得到之前所有的向量,和它们的任意线性组合(乘k,加减)。
换句话说,如果我们有m个n维向量,经过高斯消元得到线性基之后,我们只需要n个n维向量就可以表示原来的所有向量。
如果m很大而n很小,那么就可以极大地优化程序。
举个栗子:一个有解二元一次方程组的解,这一个二元组就是一组线性基。
线性基的拟陈性质
假如给你n个向量,每一个向量有一个权值。
让你选出其中一些向量,使得任意一个向量不会是其他向量的线性组合。
怎样选才能使选出来的权值之和最大(或最小)?
即选出一些向量让它们组成一组线性基。
由于线性基有拟阵的性质(意思就是可以贪心),我们可以这样解决这个问题:
把所有向量按照权值从大到小排序,一个一个放入线性基,如果能放入线性基,就把它的权值加到答案里,否则就不加。
拟阵的定义和性质比较复杂, 这里不给出证明。只要知道这个贪心算法就可以了。
动态维护线性基
在大部分线性基问题中,向量都不是一次性全部给出的。
如果每次加入一个向量,我们怎么维护线性基呢?
假设我们用一个矩阵A来储存已经计算好的线性基,矩阵A已经是上三角矩阵了。
我们试着把新的向量放进来。i从1开始枚举,
如果A[i][i] =0,而且新的向量第i个数不是0,那么直接把新的向量放到第i行就行了。
如果A[i][i]!=0,那么我们用原来的第i行来给新向量消元。
这样一路枚举过来,直到有位置放下新向量或者枚举到n为止。
由于A是上三角矩阵,每消元一次新向量的第i个数就会变成0,如果有位置放下,那么A还是上三角矩阵。如果没有位置放下,那么最后新向量会变为全0。
CODE:
#include <algorithm> #include <iostream> #include <cstring> #include <cstdio> #include <vector> #include <cmath> #define sc1(p1) scanf("%d", &p1) #define sc2(p1, p2) scanf("%lld %lld", &p1, &p2) #define sc3(p1, p2, p3) scanf("%d %d %d", &p1, &p2, &p3) #define pr(p1) printf("%lld\n", p1) using namespace std; const int maxn = 1100; const int oo = 2100000000; const int eps = 1e-6; struct po{ long long s, v; }a[maxn]; bool _sort(po xx, po yy) { return xx.v>yy.v; } int b[maxn]; long long bit[maxn]; int main() { //freopen(".in", "r", stdin); //freopen(".out", "w", stdout); int n; sc1(n); long long ans=0; bit[0]=1; for (int i=1; i<=63; i++) bit[i]=bit[i-1]<<1; for (int i=1; i<=n; i++) sc2(a[i].s, a[i].v); sort(a+1, a+n+1, _sort); for (int i=1; i<=n; i++) { for (int j=63; j>=0; j--) if(a[i].s&bit[j]) { long long t=1<<j; if(!b[j]) { b[j]=i; break; } a[i].s^=a[b[j]].s; } if(a[i].s) ans+=a[i].v; } pr(ans); return 0; }
练习
BZOJ1013
HDU3359
同余方程
POJ2947
异或方程组:
BZOJ 1923
POJ 1830
行列式:
HDU5852
线性基:
BZOJ2460
3105
Codechef XORSUB