【学习笔记】代数
线性方程组与矩阵
定义
线性方程组指的是形如
的方程组。其中我们肯定很熟悉二元一次方程组的解法,而这也属于线性方程组。
其中数字 叫做第 个方程的常数项。特别的,如果所有 ,那么则称这个方程组是齐次的。这个方程组就是上个方程组相对应的齐次方程组,或称上个方程组的诱导组。而未知数的系数正好可以排成一个长方形的表。
叫做一个 的矩阵。如果 ,则称作是 阶方阵。可以用 或大写字母(通常用 )简示。第 行,第 列的元素是 ,读作 (zfy 注)。自然地,我们使用 表示上面矩阵的第 行。而对于某一列,我们使用中括号,即 ,来表示。当矩阵是一个方阵时,我们称元素 组成了该矩阵的主对角线。如果除了主对角线,其他元素都是 ,那么则称该方阵为对角矩阵,有时记作:
当一个对角矩阵满足 ,那么就称该方阵为纯量矩阵,记作 。 称作单位矩阵,通常记作 ,当矩阵阶数确定时可简记为 。(个人常把它记为 )
同时,我们定义最开始的方程组的增广矩阵为 。即在上面那个矩阵后面加一列 来表示常系数。为清楚起见,我们用竖线将 两部分分开。
假如我们用数字 代表未知数 ,原方程组的所有式子全部变为恒等式,那么就称 个数组成的有序组 为原方程组的一个解,而 称为解的第 个分量。这时也称有序组 满足原方程组的每一个方程。没有任何解的方程组叫做不相容的,有解的方程组叫做相容的,如果只有唯一解,就叫做确定的,解的个数多于一个的方程组叫做不定的。
前置推论
解这个方程之前,我们先来学习几个前置推论。
假设我们有一个与最开始的方程组未知数个数和方程个数相同的线性方程组,称作方程组 。如果 是 中除 之外的所有方程不变,而第 个方程交换位置,则称 是由 经过 I 型初等变换 得到的。
如果 中除第 个之外的所有方程保持不变,而第 个方程变为:
则称变换后的方程组 是由 经过 II 型初等变换 得到的。其中, 是一个常数。这个变换可以理解为行向量 加上数乘 后的行向量 后得到的。
如果两个线性方程组同时是不相容的,或者同时是相容的且有相同的解,那么就称这两个线性方程组是等价的。两个等价的方程组 和 记作: 。注意到该等价满足自反性,即 ;还满足对称性,即 ;也满足传递性,即 。
通过上面的定义,我们可以证明等价性的充分条件了。
定理 :如果一个线性方程组是由另一个线性方程组进行多次初等变换得到,则这两个方程组等价。
证明:
通过传递性可知,我们只需要证明方程组 是由方程组 经过初等变换得到,并且方程组 与 等价即可。
首先我们不难证明这两个初等变换是可逆的,即 I/II 型初等变换可以再变换一次变换回来。那么如果证明 的所有解满足 即可。
对于 I 型初等变换,我们不难发现交换方程式对于方程组没有本质改变,之前的解依然满足变换后的方程组。
对于 II 型初等变换,我们可以直接把那个改变了的式子拆出来然后将 中的式子带入 不难发现直接满足。
最后说明一个方程的不相容性代表另一个方程的不相容性,这个用反证法和上面过程差不多就证完了。
高斯消元
终于进入主题了。
高斯消元的思想就是通过施行一系列的初等变换,将给定的方程组转化为较简单的形式。
首先需要钦定在系数 中至少有一个不为 。如果没有那么这个方法就退化了一阶。如果有我们就可以先使用 I 型初等变换把这个系数所对应的方程移动到第一个,然后使用 次 II 型初等变换把其它方程中所有第 列的系数全部变为 。这时这个方法退化一阶,直接求解下一层,直到一层中只留下了一个方程为止。结束变换后的方程组是阶梯状的,于是我们把这种方程组称作是阶梯形的。
定理 :任意线性方程组都等价于一个阶梯形的方程组。
这个结论可以直接由上面的推导证明。
定理 :任意矩阵可以由初等变换变化成阶梯形。
可以把任意矩阵当做是某线性方程组的增广矩阵。
求解这个阶梯形方程组就容易多了,但是还是有许多情况要讨论。
首先从相容性问题入手,如果这个阶梯形方程组包含一个 且 ,那么这个方程组一定不相容。我们可以证明如果阶梯形方程组不包含这种方程就一定相容。因为这样的话有效行数 小于等于未知数个数 ,而我们如果钦定其中的 个未知数是自由变量的话,那么这些变量随意取的情况下,剩下的 个未知变量如果组成严格阶梯形线性方程组,那么一定有唯一解,换句话说,如果 ,这个方程组相容但不确定,如果 ,这个方程组相容且确定,如果 ,这个方程组不相容。
对于相容但不确定的线性方程组,我们可以倒序求解每个值,如果遇到一个消元完之后还有两个及其以上参数的方程,那么就把这个靠后的几个未知数随机赋值(懒人可以直接赋成 ),然后继续倒序求解。
讲了这么多数学推导,读者应该都了解高斯消元的主题过程及其正确性了,那么,我们来讲讲 OI 中如何高斯消元。
最简单的实现是直接按上面的模拟,除法就直接使用 double
就行了。
玄学的是,每次找最大的值拿来消元精度会更高。如果有取模要求就取逆元就行了。
更加优美的一个实现方法是,每次直接暴力把这列除其本身外的所有数全部消至 ,这样剩下的矩阵就是一个类对角矩阵,这样更方便于求解每个未知数的值。
模板:P3389
#include<bits/stdc++.h>
using namespace std;
#define db long double
const db eps=1e-8;
db a[105][105];
int n;
bool gauss(){
for(int i=1;i<=n;i++){
int maxpos=i;
for(int j=i+1;j<=n;j++)if(fabs(a[j][i])>fabs(a[maxpos][i]))maxpos=j;//若不加fabs,最大值有可能为0,寄
if(maxpos!=i)swap(a[i],a[maxpos]);
if(fabs(a[i][i])<eps)return 0;
for(int j=1;j<=n;j++){
if(j^i){
db p=a[j][i]/a[i][i];
for(int k=i;k<=n+1;k++)a[j][k]-=p*a[i][k];
}
}
}
return 1;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n+1;j++){
cin>>a[i][j];
}
}
if(gauss()){
for(int i=1;i<=n;i++)cout<<fixed<<setprecision(2)<<a[i][n+1]/a[i][i]<<"\n";
}else cout<<"No Solution\n";
return 0;
}
这道题数据较弱,实际上上面的代码并不完善。因为有些时候我们需要判断这个方程是无穷解还是无解,所以如果出现这一列全为 ,但是下一行移到的位置及其以下都是 ,并且原一列相应位置不为 ,那么就错过了一列可消元的一列,从而造成答案有误。
因此我们维护可消元行的上下限就行了。如果出现了 真正的全 0 列
,那么再记录为无解 / 无穷解,并维护上下限。
最后我们用我们之前推出来无解的条件(即存在 )判断即可。
对应模板:P2455
#include<bits/stdc++.h>
using namespace std;
#define db long double
const db eps=1e-10;
db a[105][105];
int n;
int gauss(){
bool o=0;
for(int i=1,r=1;i<=n&&r<=n;i++,r++){
int maxpos=i;
for(int j=i+1;j<=n;j++)if(fabs(a[j][r])>fabs(a[maxpos][r]))maxpos=j;//若不加fabs 最大值有可能为 0,寄
if(maxpos!=i)swap(a[i],a[maxpos]);
if(fabs(a[i][r])<eps){
o=1;
i--;
continue;
}
for(int j=1;j<=n;j++){
if(j^i){
db p=a[j][r]/a[i][r];
for(int k=r;k<=n+1;k++)a[j][k]-=p*a[i][k];
}
}
}
if(!o)return 1;
for(int i=1;i<=n;i++){
o=0;
for(int j=1;j<=n&&!o;j++)o=fabs(a[i][j])>eps;//,cout<<a[i][j]<<" ";
// cout<<a[i][n+1];
// cout<<"\n";
if(!o&&fabs(a[i][n+1])>eps)return -1;
}
return 0;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n+1;j++){
cin>>a[i][j];
}
}
int op=gauss();
if(op==1){
for(int i=1;i<=n;i++)cout<<"x"<<i<<"="<<fixed<<setprecision(2)<<(fabs(a[i][n+1]/a[i][i])<eps?0:a[i][n+1]/a[i][i])<<"\n";
}else cout<<op;
return 0;
}
高斯消元的应用之后再说。
行列式定义
行列式()是一个函数定义,取值是一个标量。
在《代数学引论》中定义为了 用于求解线性方程组的一般公式所需要的量
。
对于一个 阶方阵 ,其 阶行列式写作 或者 ,定义为:
其中我们定义 是一个 的任何一个排列(全排列)。而 指排列 中的逆序对个数。
定义一个排列 的奇偶性就是 的奇偶性,即当 为偶数时,排列 是偶排列,当 为奇数时,排列 是奇排列。此时有性质:对于一个 阶排列的所有排列情况,奇排列和偶排列各占一半。
定义在排列 交换其中的两个元素的操作(其他元素不变)叫对换。显然,一次对换会改变一个排列的奇偶性。任意一个排列可以通过若干次对换变成一个元素升序排列的自然排列,对换次数奇偶性与原排列的奇偶性相同。
行列式性质
下面给出几条可能会用到的性质:
性质 :将矩阵的任意 行 / 列交换,行列式取反。
这个相当于是把之前所有的用于行列式计算的排列全部进行一次对换,那么排列奇偶性和 奇偶性都取反,那么行列式的值也取反。
性质 :进行一次矩阵转置,行列式不变。
矩阵转置指的是行列互换后的矩阵,对于方阵可以理解为对于主对角线对称一次。
显然,转置之后不影响所有的排列选择,故行列式不变。
之后对于所有对于行都满足的结论均可以用性质 直接证明列的情况。
性质 :行列式某行(列)的所有元素等比例变化(即都乘上一个数 ),则行列式也等比例变换。
把行列式式子拆成定义形式,把那个乘的 提出来即可。
性质 :如果行列式对应矩阵 中有一行是由两个数加起来的,那么把两个数拆出来,剩余数不变的两个矩阵 , 满足 。
同样拆成定义式子,然后乘法分配律一下就证完了。
性质 :如果行列式对应矩阵 中有一行与另一行成比例,那么 。
好像可以直接拆,但是好像需要较为复杂的计算。这里有一个较为巧妙的证明。
我们可以首先将这成比例的两行交换,根据性质 ,有 。
然后根据性质 ,我们可以消掉这个 ,得到 ,而我们有 ,所以我们又有 ,从而得到这个矩阵的行列式为 ,乘上 得到 。
性质 :把一个矩阵的某一行乘上一个数加到另一行上,行列式不变。
这个其实已经非常显然了。性质 和性质 结合起来看就行了。
行列式求值
讲了这么多了,该开始讲行列式求值了。
首先我们为了简化行列式计算量,最好的方式就是添 ,因为 可以让很多排列的贡献都变成 。我们不由得想到高斯消元,因为高斯消元可以让一个矩阵变成阶梯矩阵,对于方阵,此时有贡献的只有主对角线上的元素积。
根据性质 ,高斯消元所涉及的 II 型初等变换对行列式的值没有影响。根据性质 ,高斯消元涉及的 I 型初等变换会对行列式的值取反。
那么直接模拟高斯消元的过程,记录一个正负性变量,每交换一次就改变一次这个变量就行了。最后直接计算主对角线元素积即可。如果中间出现了任何一列扫描到全 ,根据高斯消元的定理 ,行列式的值就是 。
但是行列式是一个整数值,不能使用 double
求,需要逆元。
恰巧,模数不一定是质数,那么我们就要使用另一种方式高斯消元了。
考虑我们的目的实际上是每次对于两行,将某一行一列的值通过两者互消的方式变为 。
那么我们使用辗转相除法,这个方法保证除完一定有一个值为 ,并另一个值非 。我们可以每次选取两个非 行进行辗转相除法消元,然后把最后非 行直接移到原主对角线上列的对应行(即第 行)。这样我们能保证开始时第 行一定有值,那么我们每次以第 行值为除数,第 行值为 为终止条件进行辗转相除即可。这时我们一定能保证最后的非 值一定在正在被枚举到的行上,将这两行交换即可。重复以上过程,就能达到高斯消元最后的阶梯矩阵形式。
这样乍一看复杂度不对啊,不是 吗?但是我们每次消元目标是直接消成 ,那么要消成 的数有 个,每一次消掉一个数的代价是 ,所以求行列式的时间复杂度实际上是 。
模板:P7112
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,mod;
int a[605][605];
int gauss(){
int res=0,syb=1;
for(int i=1;i<=n;i++){
int maxpos=i;
for(int j=i+1;j<=n;j++)if(a[j][i]>a[maxpos][i])maxpos=j;
if(a[maxpos][i]==0)return 0;
if(maxpos^i)swap(a[i],a[maxpos]),syb=-syb;
for(int j=i+1;j<=n;j++){
while(a[i][i]){
int div=a[j][i]/a[i][i];
for(int k=i;k<=n;k++)a[j][k]=(a[j][k]+(mod-(div*a[i][k])%mod))%mod;
swap(a[j],a[i]);
syb=-syb;
}
swap(a[j],a[i]);
syb=-syb;
}
}
res=syb;
for(int i=1;i<=n;i++)res=res*a[i][i]%mod;
return (res+mod)%mod;
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
cin>>n>>mod;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>a[i][j],a[i][j]%=mod;
cout<<gauss();
return 0;
}
行列式的应用同样后面再说。
向量空间
首先补一下前面跳过的知识:设 是任意两个集合。任取 ,给定顺序的元素对 ,叫做一个有序对,这时两个有序对 相等当且仅当 且 。
全体有序对 的集合
叫做两个集合 的笛卡尔积。
相应的,我们定义笛卡尔幂就是一个集合多次与自己作笛卡尔积后的结果。
那么我们设 是一个固定的自然数, 上长为 的行向量空间指集合 (其元素称为行向量或简称向量),连同向量的加法运算以及纯量(实数)与向量的乘法运算。相信大家已经知道矩阵的各种运算了,此处由于向量等于说是一个 的矩阵,所以此处就不提向量的运算了,但是记住向量和矩阵都满足乘法分配律就行了。
除了长为 的行向量空间外,也存在高为 的列向量空间。列向量的表达形式在定义一节中已经提到。
设 是向量空间 中的向量, 是纯量,则向量 叫做向量 的带有系数 的线性组合。举个例子:
不难发现两个线性组合 如果所组合的向量一致就一定有 也是线性组合。于是我们有结论,由给定向量组 的所有线性组合构成的集合 具有性质:
特别的,零向量(全零向量)永远包含在 中。
通常用符号 表示,并通常称为向量组 的线性包(或简称为包)。包是在该向量组上张成的(或生成的)。
定义单位行向量 即为:
其满足在行向量空间 中满足 。单位列向量同,不过通常以 表示。
如果可以找到 个不全为 的数 能使空间 的向量组 满足
(右边是零向量),则称该向量组线性相关,并称上式为非平凡的。如果 ,则该向量组线性无关。
设 是 中的一个非零线性包,向量组 称为 的基,当且仅当该向量组线性无关,且其生成的线性包与 重合,即 。
从基和线性包的定义推出,每一个向量 都可以唯一地写成 ,系数 称作 对于基 的坐标。
不难发现 就是 的一组基,特别的,我们称这组基为标准基,但这不是 空间中的唯一一组基,比如这组基全部乘上 也是 空间的一组基。
引理:设 是 中以 为基的线性包,而 并且线性无关,那么 。
证明:拆出 的线性组合式,再列出线性无关性质式:
考虑使用反证法。设 ,将线性组合式带入上式,有
有线性方程组
由于 ,在讲高斯消元时有定理可以证明该方程有非全 解,那么 向量组线性相关,矛盾。
定理 : 中的所有非零线性包 都至少有一组有限基。
证明:首先,除零向量外的任意一个向量组成的向量组都是线性无关的。设我们现在已经找到了一个线性无关的向量组 。若 不等于 ,那么就一定有 不是 的线性组合。根据线性组合的性质, 也不是 的任意一个子组合的线性组合。那么 不能抵消任何一个原向量组的任何一个线性组合,那么 也是线性无关的。这样对于向量组的扩张是无穷的,直到 等于 。我们称此时的线性无关组是极大的,此时这个向量组就是 的一组有限基。
定理 : 中的任意非零线性包 的所有基元素个数 都相等,且 。
证明:假如说 存在两个不同的基,设元素个数分别是 ,那么根据引理,我们可以知道 且 ,则有 。而 的标准基长度为 ,所以其内元素个数一定小于等于最大基长度(标准基长度) 。
我们记 是线性包 的维数,记为 或简记为 。其实 就是极大线性无关子向量组的元素个数。
向量组的秩的定义就是对于一个向量组 ,其张成的线性包的最大维数。于是有:
当 时(零线性包),有 。
定义线性包的和是:
特别地,当 时,称这是线性包的直和。
一个矩阵的秩就是每行组成的行向量的向量组的秩。
矩阵运算
较为简单,乘法与加法就不讲了。
设 的维数为 , 的一组基为 , 是 上的一个线性变换,则有:
化为矩阵就是 。
矩阵 就是线性变换 在这组基下的矩阵表现形式。
显然 确定时, 确定,所以 能唯一确定一个 。
所以有些时候 dp 和 ds 题是线性变换的时候就可以使用矩阵维护。
对矩阵乘法进行推广:
定义广义矩阵乘法是对于矩阵 ,形如:
的式子。我们将其称作是 的矩阵乘法。
当 满足以下条件时,该广义矩阵乘法满足结合律:
- 具有交换律。
- 具有交换律和结合律。
- 对 有分配率,即 。
常见的矩阵乘法形式有 。
矩阵求逆
定义一个矩阵 的逆矩阵就是满足 的矩阵,用 表示。
如何求一个矩阵的逆矩阵呢?
定义一个初等矩阵就是描述我们之前提到的高斯消元的两个初等变换。举个例子,对于 ,交换第 行的 I 类初等变换对应的初等矩阵就是
对于 ,将第 行乘上 加到第 行的初等矩阵是
假设初等矩阵我们用 表示,那么如果我们能将原矩阵 变换成 ,那么就有 ,从而我们推出 。
那么由于每次变换相当于左乘一个初等矩阵。由于 ,所以我们可以把 和 拼到一起进行高斯消元,将原矩阵消成一个对角矩阵之后再除以对应系数从而消成单位矩阵,这时右侧就是矩阵求逆之后的结果。
判断矩阵是否可逆就直接消元时判断该矩阵能不能消成一个对角矩阵就行了。
在具体的实现中,我们可以每消完一行就除去当前行对应的值,可以省去一次求逆元的过程。
模板:P4783
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=1e9+7;
int ksm(int a,int b){
if(b==0)return 1;
if(b==1)return a;
return (b&1?a:1)*ksm(a*a%mod,b/2)%mod;
}
int a[401][801],n;
bool gauss(){
for(int i=1;i<=n;i++){
int maxpos=i;
for(int j=i+1;j<=n;j++)if(a[j][i]>a[maxpos][i])maxpos=j;
if(maxpos^i)swap(a[i],a[maxpos]);
if(a[i][i]==0)return 1;
int Ii=ksm(a[i][i],mod-2);
for(int j=1;j<=n;j++){
if(j^i){
int p=a[j][i]*Ii%mod;
for(int k=i;k<=n*2;k++){
a[j][k]=(a[j][k]+mod-p*a[i][k]%mod)%mod;
}
}
}
for(int k=i;k<=n*2;k++)a[i][k]=a[i][k]*Ii%mod;
}
return 0;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>a[i][j];
for(int i=1;i<=n;i++)a[i][i+n]=1;
if(gauss()){
cout<<"No Solution";
return 0;
}
for(int i=1;i<=n;i++){
for(int j=n+1;j<=n*2;j++){
cout<<a[i][j]<<" ";
}
cout<<"\n";
}
return 0;
}
注意有些地方是 n*2
而不是 n
。
线性基
普通的线性基就是我们之前在向量空间一节定义的基,这个基可以解决很多关于线性方程组的问题。
如何构造一组线性基?最简单的方法就是高斯消元出极大线性无关组。
线性基通常分为两类,如果向量定义在 空间内,则该线性基称为异或线性基。如果向量定义在 空间内,则称该线性基为实数线性基。
异或线性基
我们已经了解了一种求异或线性基的方式:高斯消元。把原来的 个数理解成一个 的矩阵,消成类对角矩阵之后,剩余有 的位置就是一组极大线性无关组,而它求最大时的贡献就是所有 的位置组成起来的数。那么我们就能解决最大最小和排名 大问题,时间复杂度是 。
考虑优化。我们使用贪心来构造一组神秘的按位首组的基。这组基的性质就是可能不是从原来的数中抽取的极大线性无关组,但是构造出来的一定是一个上三角矩阵。这样就和高斯消元后的结果本质相同,询问时也贪心地填满是 的位置就行了。插入时,我们可以直接从大到小枚举基内元素,如果枚举到一位有数,那么我们将目前数异或上当前位上的数来消位以继续枚举下一位,而如果枚举到一位没有数,那么直接将目前数插入到该位置上。时间复杂度是 ,通过询问的贪心可以知道正确性显然。
贪心法不能直接解决第 大的问题,需要进行几步转化。我们已经知道贪心法构造出一组线性基的矩阵大小是 的。所以我们可以对这个矩阵进行暴力高斯消元消成对角矩阵,然后就能对 进行二进制拆分,像高斯消元那样做了。时间复杂度 。
具体实现非常简单,这里给出两个版本的线性基。
模板:P3812
贪心版实现:
#include<bits/stdc++.h>
using namespace std;
#define int long long
struct lbase{
int num[52];
void ins(int p){
for(int i=log2(p)+1;i>=0;i--){
if(!(p>>i))continue;
if(num[i])p^=num[i];
else{
num[i]=p;
break;
}
}
return;
}
int qu(){
int res=0;
for(int i=50;i>=0;i--){
if((res>>i)&1)continue;
res^=num[i];
}
return res;
}
}T;
int n,x;
signed main(){
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>x,T.ins(x);
cout<<T.qu();
return 0;
}
高斯消元版实现:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int a[51],n;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
int yyy=0;
for(int i=1,r=50;i<=n&&r>=0;yyy++,i++,r--){
int j;
for(j=i;j<=n;j++)if(a[j]&(1ll<<r))break;
if(j<=n&&j^i)swap(a[j],a[i]);//注意j<=n这个条件易漏!
if(!(a[i]&(1ll<<r))){yyy--;i--;continue;}
for(j=1;j<=n;j++)if(j^i&&(a[j]&(1ll<<r)))a[j]^=a[i];
}
int ans=0;
for(int i=1;i<=yyy;i++)ans^=a[i];
cout<<ans;
return 0;
}
实数线性基
对于实数线性基,我们就不能像之前一样贪心构造一组基了。考虑高斯消元,每次交换 ,我们就能构造出一组线性基。
模板:P3265 [JLOI2015] 装备购买
这道题是要求我们找到一个特定价值最小的一组基。根据基的求法,我们每次找到一个非 且花费最小的物品来消元即可。为减小常数,我们还可以初始时按花费先排个序。时间复杂度 。
#include<bits/stdc++.h>
using namespace std;
int rk[501],val[501],v[501];
int n,m;
long double ray[501][501];
long double* ra[501];
const long double eps=1e-5;
bool cmp(const int &a,const int &b){
return val[a]<val[b];
}
int main(){
ios::sync_with_stdio(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
rk[i]=i;
for(int j=1;j<=m;j++)cin>>ray[i][j];
}
for(int i=1;i<=n;i++)cin>>val[i];
sort(rk+1,rk+n+1,cmp);
for(int i=1;i<=n;i++)v[i]=val[rk[i]],ra[i]=ray[rk[i]];
int l=1;
for(int i=1;i<=m&&l<=n;i++,l++){
int pos;
for(pos=l;pos<=n;pos++)if(fabs(ra[pos][i])>eps)break;
if(pos>n){l--;continue;}
for(int j=pos;j>=l+1;j--)swap(ra[j],ra[j-1]),swap(v[j],v[j-1]);
for(int j=l+1;j<=n;j++){
long double p=ra[j][i]/ra[l][i];
for(int k=i;k<=m;k++)ra[j][k]-=ra[l][k]*p;
}
}
cout<<l-1<<" ";
int ans=0;
for(int i=1;i<l;i++)ans+=v[i];
cout<<ans;
return 0;
}
线性基合并
高斯消元类型的线性基合并就是暴力重构两个基,复杂度 ,如果是异或线性基的话可以压位,复杂度 。
贪心的异或线性基合并就是把一个线性基里的元素插入到另一个里面,复杂度 。
void join(const lbase &p){
for(int i=50;i>=0;i--)ins(p[i]);
return;
}
线性基求交
这个应该是线性基板块最难的部分了。
可能只有异或线性基支持这个操作。
设 是两组线性基,其生成空间为 ,那么这两个线性空间的交满足 ,那么则称 是线性基 的交。
根据异或线性基线性组合的性质,我们发现任意两个 内的元素都满足其线性组合在 空间内,所以 是一个线性包。
设我们存在一个线性基 ,满足以下条件:
接下来我们证明 就是线性基 和 的交。
不难发现我们可以证明 。
假设存在 ,根据定义, 的基既在 中又在 中,那么其组合起来的空间 一定同时属于 和 。矛盾。
假设存在 ,由于 ,那么一定有 的一个线性组合是 ,把这些 分成两组,一组 ,另一组 ,而 ,所以若 的一组存在,那么可以得到 中的一个元素异或上一个 的线性组合的结果在 中,因此推出 是非零线性包,矛盾。
那么如何求 呢?
考虑如果 ,那么 一定有用。但是我们需要只留下属于 的贡献,所以设 ,而 ,。所以我们每次把 (即 )加入到 中。
如果不属于,那么一定在遍历基中,出现未插入过的位数,那么就把那个当前数插入到这个位数之中。这里还是像插入那样每次拿基内元素去消首位。
现在我们最麻烦的地方就是考虑如何维护 。考虑把维护 转化成维护 。模拟插入的过程可以发现我们每次尝试异或一个数时都会有一部分可能异或到插入的 ,这个时候我们就需要更新这个位置的贡献。显然,这个位置存了 和之前前面它异或过的东西的异或和,那么这个位置的贡献就是它前面异或过的东西的异或和。贡献的初值就是之前 的线性基每一位上的值。
模板:UOJ698 [候选队互测2022] 枪打出头鸟
本文作者:xingyu_xuan
本文链接:https://www.cnblogs.com/xingyuxuan/p/-/algebra
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步