【6】矩阵学习笔记

前言

矩阵在 OI 中运用广泛,是一个很重要的内容。正是因为我不会矩阵,所以 WC2024 时讲动态 DP 我没办法听,所以矩阵真的很重要。

由于我实力有限,这里只能介绍一些矩阵的基本应用。

矩阵定义

矩阵:将 n×m 个数排列成 nm的形式称为一个 n×m矩阵

矩阵一般用大写字母 A,B,C 来表示,矩阵里第 i 行第 j 列的元素用对应的小写字母 ai,j,bi,j,ci,j 来表示。

例如:在矩阵 A 中,a1,1=9,a3,2=5,a4,3=9

A=[998244353998]

方阵:行数与列数相等的矩阵称为方阵

例如:矩阵 B 为方阵。

B=[1234]

单位矩阵:对于方阵 I,如果主对角线每一个元素均为 1,其余元素为 0,这个矩阵被称为单位矩阵,记作 I

I2=[1001],I3=[100010001],I4=[1000010000100001]

在代码编写中,我们把矩阵定义为一个结构体,这样方便写在函数中作为参数或返回值。矩阵结构体包含矩阵的二维数组,有时包含矩阵的行数列数

struct matrix
{
   long long n,m;
   long long v[200][200];
};

矩阵运算

矩阵加减

对于两个 n×m 的矩阵,它们的为一个 n×m 矩阵,矩阵中各位元素等于对应元素之和

对于两个 n×m 的矩阵,它们的为一个 n×m 矩阵,矩阵中各位元素等于对应元素之差

例:

A=[998244353998],B=[142204190818]

A+B=[9+19+48+22+24+04+43+15+93+09+89+18+8]=[1013104484143171016]

AB=[919482224044315930989188]=[856040243180]

矩阵加减法满足实数加减法的性质,比如加法交换律

矩阵乘法

对于一个 p×q 的矩阵 A 和一个 q×r 的矩阵 B,它们的为一个 p×r 的矩阵 C。在这个矩阵中,ci,j 满足如下式子:

ci,j=k=1qai,kbk,j

例:

A=[1221],B=[130425]

AB=[1×1+2×41×3+2×21×0+2×52×1+1×42×3+1×22×0+1×5]=[9710685]

矩阵乘法可以简单记为第 i 行第 j 列的元素等于第一个矩阵i第二个矩阵j对应元素相乘的。即先横着看,再竖着看。

矩阵乘法满足结合律,但是不满足交换律,这一点需要特别注意。

单位矩阵 I 的性质:单位矩阵乘以任何一个矩阵(可以乘的),这个矩阵。单位矩阵在矩阵乘法中,就相当于实数乘法中的 1

IA=A

在代码编写中,我们按照矩阵乘法的定义求出每一个 ci,j。这个过程时间复杂度较高,为 O(n3),其中 n 为矩阵的边长(假设 n,m 同阶)。

代码中 n 为方阵边长。如果是矩阵,把循环中的 n 改为对应的 a.n,a.m,b.n,b.m 即可。

struct matrix mul(struct matrix a,struct matrix b)
{
	struct matrix c;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++)
	        c.v[i][j]=0;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++)
	        for(int k=1;k<=n;k++)
	            c.v[i][j]=c.v[i][j]+a.v[i][k]*b.v[k][j];
	return c;
}

矩阵的幂

对于一个方阵 A,记这个方阵的Ak,表示 kA 连乘的

特别的,有 A0=I,其中 I单位矩阵

我们都知道,可以用快速幂来求一个实数的 k 次方,那我们也可以用类似的思想来求矩阵的 k 次方,这就是矩阵快速幂

相较于快速幂模板,矩阵快速幂只需要把实数改为矩阵,把实数运算改为矩阵运算,并将 ans 的初始值设为单位矩阵 I 即可。

时间复杂度为 O(n3logn)

代码中 n 为方阵边长。

struct matrix power(struct matrix a,long long p)
{
	struct matrix ans,x=a;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++)
	        {
	        if(i!=j)ans.v[i][j]=0;
	        else ans.v[i][j]=1;
	        }
	while(p)
	   {
	   	if(p&1)ans=mul(ans,x);
	   	p>>=1;
	   	x=mul(x,x);
	   }
	return ans;
}

矩阵加速递推

矩阵可以用来表示递推关系。当用矩阵表示递推关系时,我们把状态写入一个 n×1 的矩阵,这个矩阵称为状态矩阵;用一个 n×n 的方阵来表示递推关系,这个方阵称为转移矩阵

由于矩阵乘法独特的计算方式,我们可以通过转移矩阵里各位置的值来确定状态转移时的系数。当我们转移不需要某一个值时,就把这一位设为 0

例如:用矩阵表示 fi=5fi13fi3

首先,我们先把状态写入一个 n×1 的矩阵,并在等式另一边写上递推一次后的状态矩阵。这个递推式直接出现的状态为 fi1,fi3,额外会用到的状态为 fi2。我们把这些写入矩阵,并在等式另一边写出每个元素递推一次后的矩阵。

[fifi1fi2]=[][fi1fi2fi3]

接下来,我们考虑构造中间的转移矩阵。设状态矩阵的第一行为 xyz,根据矩阵乘法的运算法则,得到如下式子:

fi=xfi1+yfi2+zfi3

因为等号左边均为 fi,我们把这个式子和题目所给的递推式进行对比:

fi=xfi1+yfi2+zfi3

fi=5fi13fi3=5fi1+0fi13fi3

观察系数不难发现 x=5,y=0,z=3。所以我们可以填出转移矩阵的第一行:

[fifi1fi2]=[503][fi1fi2fi3]

接下来,设状态矩阵的第二行为 xyz,根据矩阵乘法的运算法则,得到如下式子:

fi1=xfi1+yfi2+zfi3

很显然,x=1,y=0,z=0 时就有 fi1=fi1,所以我们可以填出转移矩阵的第二行:

[fifi1fi2]=[503100][fi1fi2fi3]

设状态矩阵的第三行为 xyz,根据矩阵乘法的运算法则,得到如下式子:

fi2=xfi1+yfi2+zfi3

同第二行,x=0,y=1,z=0 时就有 fi2=fi2,所以我们可以填出转移矩阵的第三行:

[fifi1fi2]=[503100010][fi1fi2fi3]

这样,我们就成功用矩阵表示了 fi=5fi13fi3 这个递推关系。

这么做的好处是,我们可以用矩阵快速幂在 O(logn) 时间内推出 fn 的值。我们可以简单推导一下:

[fnfn1fn2]=[503100010][fn1fn2fn3]=[503100010]2[fn2fn3fn4]==[503100010]n3[f3f2f1]

使用矩阵快速幂,求出 [503100010]n3,再乘以初始状态就能在状态矩阵中推出 fn。由于转移矩阵很小,矩阵乘法视为常数,最后总的时间复杂度为 O(logn)

高斯消元法

我们可以用矩阵表示方程组,例如这个例子:

{a1,1x1+a1,2x2++a1,nxn=b1a2,1x1+a2,2x2++a2,nxn=b2an,1x1+an,2x2++an,nxn=bn

我们只把各项未知数系数写进矩阵,就得到了这个方程的矩阵表示。例如上面这个方程组,就可以这样表示:

[a1,1a1,2a1,na2,1a2,2a2,nan,1an,2an,n]

有的时候我们把 bi 也写入这个矩阵,我们把这样的矩阵称为增广矩阵

[a1,1a1,2a1,nb1a2,1a2,2a2,nb2an,1an,2an,nbn]

我们可以使用高斯消元法来求出这个方程的解。高斯消元法具体步骤如下:

假设现在处理到第 i 行。

1:在第 in 行的第 i 列寻找绝对值最大值,并将绝对值最大值所在的行与第 i 行交换。这么做是为了避免在第 ixi 的系数为 0 导致的一些奇怪情况。由于前 i1已经处理了,化成了最终需要的形式,所以不能交换

2xi 的系数为 1,把第 i 行的每一个元素(包括 bi)除以 xi,i

3消元。我们希望第 i+1nxi系数0,所以我们用加减消元的方式进行消元,具体操作见代码。

根据第 3 步,从第 i+1 行开始,xi 的系数均为 0。所以最后矩阵一定是这个形式,我们称之为上三角形式

[1a1,2a1,n01a2,n001]

在这个矩阵中,直接从 xnx1 不断代入就可以求出每一个未知数的值。

时间复杂度为 O(n3)

代码中有一些循环的范围容易记错,需要特别注意。另外,如果某次找到的最大值为 0,那么证明这个方程组并不是唯一解,可能是无解或者无穷解。因为如果找到的最大值为 0,第 i 行的方程就不会被用上(不然你怎么化系数为 1)。根据初中数学,一个 n 元方程组必须至少有 n 个方程才有唯一解,所以这个方程组必定不是唯一解。

bool gauss()
{
	double div=0;
	for(int i=1;i<=n;i++)
	    {
	    long long mx=i;
	    for(int j=i+1;j<=n;j++)
	        if(fabs(a[j][i])>fabs(a[mx][i]))mx=j;
	    if(fabs(a[mx][i])<eps)return 0; 
	    if(i!=mx)
	       {
	       for(int j=1;j<=n;j++)swap(a[i][j],a[mx][j]);
	       swap(b[i],b[mx]);
	       }
	    div=a[i][i],b[i]/=div;
	    for(int j=1;j<=n;j++)a[i][j]/=div;
	    for(int j=i+1;j<=n;j++)
	        {
	        	div=a[j][i],b[j]-=b[i]*div;
	        	for(int k=i;k<=n;k++)a[j][k]-=a[i][k]*div;
			}
	    }
	ans[n]=b[n];
	for(int i=n-1;i>=1;i--)
	    {
	    ans[i]=b[i];
	    for(int j=n;j>i;j--)ans[i]-=a[i][j]*ans[j];
	    }
	return 1;
}

对于高斯消元判定方程组解的情况,具体请看例题 5

这是更具有普适性的高斯消元算法模板。

long long gauss()
{
	double div=0;
	for(int i=1;i<=n;i++)
	    {
	    long long mx=now;
	    for(int j=now+1;j<=n;j++)
	        if(fabs(a[j][i])>fabs(a[mx][i]))mx=j;
	    if(fabs(a[mx][i])<eps)continue; 
	    if(now!=mx)
	       {
	       for(int j=1;j<=n;j++)swap(a[now][j],a[mx][j]);
	       swap(b[now],b[mx]);
	       }
	    div=a[now][i],b[now]/=div;
	    for(int j=1;j<=n;j++)a[now][j]/=div;
	    for(int j=now+1;j<=n;j++)
	        {
	        	div=a[j][i],b[j]-=b[now]*div;
	        	for(int k=i;k<=n;k++)a[j][k]-=a[now][k]*div;
			}
		now++;
	    }
	if(now-1==n)
		{
		ans[n]=b[n];
		for(int i=n-1;i>=1;i--)
		    {
		    ans[i]=b[i];
		    for(int j=n;j>i;j--)ans[i]-=a[i][j]*ans[j];
		    }
		}
	else
	   {
	   	for(int i=now;i<=n;i++)
	   	    if(fabs(b[i])>=eps)return -1;
		return 1;
	   }
	return 0;
}

例题

例题 1

P3390 【模板】矩阵快速幂

矩阵快速幂模板题,不多赘述。

#include <bits/stdc++.h>
using namespace std;
struct matrix
{
	long long v[200][200];
}a;
long long n,k,mod=1000000007;
struct matrix mul(struct matrix a,struct matrix b)
{
	struct matrix c;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++)
	        c.v[i][j]=0;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++)
	        for(int k=1;k<=n;k++)
	            c.v[i][j]=(c.v[i][j]+a.v[i][k]*b.v[k][j])%mod;
	return c;
}

struct matrix power(struct matrix a,long long p)
{
	struct matrix ans,x=a;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++)
	        {
	        if(i!=j)ans.v[i][j]=0;
	        else ans.v[i][j]=1;
	        }
	while(p)
	   {
	   	if(p&1)ans=mul(ans,x);
	   	p>>=1;
	   	x=mul(x,x);
	   }
	return ans;
}

int main()
{
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=n;j++)
	        scanf("%lld",&a.v[i][j]);
    a=power(a,k);
	for(int i=1;i<=n;i++)
	    {
	    for(int j=1;j<=n;j++)
	        printf("%lld ",a.v[i][j]);
	    printf("\n");
	    }
	return 0;
}

例题 2

P1962 斐波那契数列

递推需要用到了量有 Fn1,Fn2,把它们写入状态矩阵。

[FiFi1]=[][Fi1Fi2]

对于每一个量,列出递推关系:

Fi=Fi1+Fi2

Fi1=Fi1

把这些递推关系逐行利用待定系数法写入矩阵,得到用矩阵表示的递推关系:

[FiFi1]=[1110][Fi1Fi2]

初始状态 [F2F1]=[11],使用矩阵快速幂递推 n2 次,就可以推出 Fn

注意 mul(power(a,n-2),ans) 不能写成 mul(ans,power(a,n-2)),因为矩阵乘法不满足交换律。也要注意 n=1 时的特判,否则快速幂会进入死循环。

#include <bits/stdc++.h>
using namespace std;
struct matrix
{
	long long v[200][200];
}a,ans;
long long n,k,mod=1000000007;
struct matrix mul(struct matrix a,struct matrix b)
{
	struct matrix c;
	for(int i=1;i<=2;i++)
	    for(int j=1;j<=2;j++)
	        c.v[i][j]=0;
	for(int i=1;i<=2;i++)
	    for(int j=1;j<=2;j++)
	        for(int k=1;k<=2;k++)
	            c.v[i][j]=(c.v[i][j]+a.v[i][k]*b.v[k][j])%mod;
	return c;
}

struct matrix power(struct matrix a,long long p)
{
	struct matrix ans,x=a;
	for(int i=1;i<=2;i++)
	    for(int j=1;j<=2;j++)
	        {
	        if(i!=j)ans.v[i][j]=0;
	        else ans.v[i][j]=1;
	        }
	while(p)
	   {
	   	if(p&1)ans=mul(ans,x);
	   	p>>=1;
	   	x=mul(x,x);
	   }
	return ans;
}

int main()
{
	scanf("%lld",&n);
    ans.v[1][1]=1,ans.v[2][1]=1;
	a.v[1][1]=1,a.v[1][2]=1,a.v[2][1]=1,a.v[2][2]=0;
    if(n>1)
        {
        ans=mul(power(a,n-2),ans);
    	printf("%lld",ans.v[1][1]%mod);
        }
    else printf("1\n");
	return 0;
}

例题 3

P1707 刷题比赛

我认为这题是变相大模拟。递推式比较复杂,梳理一下,用到的量有 ak,ak+1,bk,bk+1,ck,ck+1,k2,k,1,wk,zk11 个,把它们写入状态矩阵。

[ak+1ak+2(k+1)2k+11bk+1bk+2wkck+1ck+2zk]=[][akak+1k2k1bkbk+1wk1ckck+1zk1]

这里状态矩阵中没有使用 wk,zk,是因为 wk1,zk1 通过改变系数可以做到一样的效果。两种写法都可以。

对于每一个量,列出递推关系:

ak=ak

ak+2=pak+1+qak+bk+1+ck+1+rk2+tk+1

bk=bk

bk+2=ubk+1+vbk+ak+1+ck+1+wk1×w

ck=ck

ck+2=xck+1+yck+ak+1+bk+1+zk1×z+k+2

(k+1)2=k2+2k+1

k+1=k+1

1=1

wk=wk1×w

zk=zk1×z

把这些递推关系逐行利用待定系数法写入矩阵,得到用矩阵表示的递推关系:

[ak+1ak+2(k+1)2k+11bk+1bk+2wkck+1ck+2zk]=[01000000000qprt10100100012100000000011000000000010000000000001000001000vuw0100000000w0000000000001001012010yxz0000000000z][akak+1k2k1bkbk+1wk1ckck+1zk1]

根据题意,写出 k=1 时等式右边的初始状态。使用矩阵快速幂递推 n1 次,就可以推出 ak,bk,ck

注意,在递推中有常数时,要把这个常数也写入矩阵状态。因为递推时所有信息都得包含在转移矩阵中,常数也不例外。

由于模数很大,需要使用龟速乘。总体时间复杂度为 O(log2n)

#include <bits/stdc++.h>
using namespace std;
struct matrix
{
	long long v[200][200];
}a,ans;
long long n,k,p,q,r,t,u,v,w,x,y,z,mod;
void init()
{
	ans.v[1][1]=1,ans.v[2][1]=3,ans.v[3][1]=1,ans.v[4][1]=1,ans.v[5][1]=1,ans.v[6][1]=1;
	ans.v[7][1]=3,ans.v[8][1]=1,ans.v[9][1]=1,ans.v[10][1]=3,ans.v[11][1]=1;
    a.v[1][2]=1,a.v[2][1]=q,a.v[2][2]=p,a.v[2][3]=r,a.v[2][4]=t,a.v[2][5]=1,a.v[2][7]=1;
    a.v[2][10]=1,a.v[3][3]=1,a.v[3][4]=2,a.v[3][5]=1,a.v[4][4]=1,a.v[4][5]=1,a.v[5][5]=1;
    a.v[6][7]=1,a.v[7][2]=1,a.v[7][6]=v,a.v[7][7]=u,a.v[7][8]=w,a.v[7][10]=1,a.v[8][8]=w;
    a.v[9][10]=1,a.v[10][2]=1,a.v[10][4]=1,a.v[10][5]=2,a.v[10][7]=1,a.v[10][9]=y,a.v[10][10]=x;
    a.v[10][11]=z,a.v[11][11]=z;
}

long long slow(long long a,long long p)
{
	long long ans=0,x=a;
	while(p)
	   {
	   	if(p&1)ans=(ans+x)%mod;
	   	p>>=1;
	   	x=(x+x)%mod;
	   }
	return ans;
}

struct matrix mul(struct matrix a,struct matrix b)
{
	struct matrix c;
	for(int i=1;i<=11;i++)
	    for(int j=1;j<=11;j++)
	        c.v[i][j]=0;
	for(int i=1;i<=11;i++)
	    for(int j=1;j<=11;j++)
	        for(int k=1;k<=11;k++)
	            c.v[i][j]=(c.v[i][j]+slow(a.v[i][k],b.v[k][j]))%mod;
	return c;
}

struct matrix power(struct matrix a,long long p)
{
	struct matrix ans,x=a;
	for(int i=1;i<=11;i++)
	    for(int j=1;j<=11;j++)
	        {
	        if(i!=j)ans.v[i][j]=0;
	        else ans.v[i][j]=1;
	        }
	while(p)
	   {
	   	if(p&1)ans=mul(ans,x);
	   	p>>=1;
	   	x=mul(x,x);
	   }
	return ans;
}

int main()
{
	scanf("%lld%lld",&n,&mod);
	scanf("%lld%lld%lld%lld",&p,&q,&r,&t);
	scanf("%lld%lld%lld",&u,&v,&w);
	scanf("%lld%lld%lld",&x,&y,&z);
    init();
    ans=mul(power(a,n-1),ans);
	printf("nodgd %lld\nCiocio %lld\nNicole %lld\n",ans.v[1][1],ans.v[6][1],ans.v[9][1]);
	return 0;
}

例题 4

P3389 【模板】高斯消元法

高斯消元模板题,不多赘述。

#include <bits/stdc++.h>
using namespace std;
long long n;
double a[200][200],b[200],ans[200],eps=1e-10; 
bool gauss()
{
	double div=0;
	for(int i=1;i<=n;i++)
	    {
	    long long mx=i;
	    for(int j=i+1;j<=n;j++)
	        if(fabs(a[j][i])>fabs(a[mx][i]))mx=j;
	    if(fabs(a[mx][i])<eps)return 0; 
	    if(i!=mx)
	       {
	       for(int j=1;j<=n;j++)swap(a[i][j],a[mx][j]);
	       swap(b[i],b[mx]);
	       }
	    div=a[i][i],b[i]/=div;
	    for(int j=1;j<=n;j++)a[i][j]/=div;
	    for(int j=i+1;j<=n;j++)
	        {
	        	div=a[j][i],b[j]-=b[i]*div;
	        	for(int k=i;k<=n;k++)a[j][k]-=a[i][k]*div;
			}
	    }
	ans[n]=b[n];
	for(int i=n-1;i>=1;i--)
	    {
	    ans[i]=b[i];
	    for(int j=n;j>i;j--)ans[i]-=a[i][j]*ans[j];
	    }
	return 1;
}

int main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	    {
	    for(int j=1;j<=n;j++)
	        scanf("%lf",&a[i][j]);
	    scanf("%lf",&b[i]);
	    }
	if(gauss())
	   {
	   	for(int i=1;i<=n;i++)
	   	    printf("%.2lf\n",ans[i]);
	   }
	else printf("No Solution\n");
	return 0;
}

例题 5

P2455 [SDOI2006] 线性方程组

由于每次找到的最大值可能为 0,所以我们分别记录目前处理的未知数 xi 和目前用到第 now 行的方程。如果找到的绝对值最大值为 0,那么我们跳过这个元素,而不是跳过这一行。具体而言,就是把代码中用到 i 作为行数时直接换成 now,并在每一次成功操作完一行之后将 now 的值增加。

如果最后我们用到了所有方程,也就是 now=n+1,那么这个方程组有唯一解。注意这里是 n+1,因为 now 表示的是处理到,但是还没有处理。

否则,这个方程组必定不是唯一解。对于没有用到的方程 i 中未知数 xj 的系数 xi,j,要么整个第 j 列绝对值最大值为 0,要么被消元消成 0,要么被换走再消元消成 0,所以这个方程的未知数系数一定全为 0

我们遍历未被处理的第 i 个方程,根据上文,这个方程的未知数系数一定全为 0,所以等号左边一定为 0。如果 bi0,就是要求 00,这个方程组显然无解。否则,方程组有解,且由于没有用足 n 个方程,一定无法确定某些未知数,所以方程组有无穷解。

#include <bits/stdc++.h>
using namespace std;
long long n,now=1;
double a[200][200],b[200],ans[200],eps=1e-10; 
long long gauss()
{
	double div=0;
	for(int i=1;i<=n;i++)
	    {
	    long long mx=now;
	    for(int j=now+1;j<=n;j++)
	        if(fabs(a[j][i])>fabs(a[mx][i]))mx=j;
	    if(fabs(a[mx][i])<eps)continue; 
	    if(now!=mx)
	       {
	       for(int j=1;j<=n;j++)swap(a[now][j],a[mx][j]);
	       swap(b[now],b[mx]);
	       }
	    div=a[now][i],b[now]/=div;
	    for(int j=1;j<=n;j++)a[now][j]/=div;
	    for(int j=now+1;j<=n;j++)
	        {
	        	div=a[j][i],b[j]-=b[now]*div;
	        	for(int k=i;k<=n;k++)a[j][k]-=a[now][k]*div;
			}
		now++;
	    }
	if(now-1==n)
		{
		ans[n]=b[n];
		for(int i=n-1;i>=1;i--)
		    {
		    ans[i]=b[i];
		    for(int j=n;j>i;j--)ans[i]-=a[i][j]*ans[j];
		    }
		}
	else
	   {
	   	for(int i=now;i<=n;i++)
	   	    if(fabs(b[i])>=eps)return -1;
		return 1;
	   }
	return 0;
}

int main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	    {
	    for(int j=1;j<=n;j++)
	        scanf("%lf",&a[i][j]);
	    scanf("%lf",&b[i]);
	    }
	long long flag=gauss();
	if(flag==0)
	   {
	   	for(int i=1;i<=n;i++)
	   	    printf("x%d=%.2lf\n",i,ans[i]);
	   }
	else if(flag==1)printf("0\n");
	else printf("-1\n");
	return 0;
}

例题 6

P4035 [JSOI2008] 球形空间产生器

设这个球的球心坐标为 (x1,x2xn),根据球心到球面上任意一点距离都相等的,我们可以列出一些等量关系式,组成一个方程组。

以第一个坐标 (a1,1,a1,2x1,n) 和第二个坐标为 (a2,1,a2,2x2,n) 为例,有如下式子:

(a1,1x1)2+(a1,2x2)2++(a1,nxn)2=(a2,1x1)2+(a2,2x2)2++(a2,nxn)2

两边同时平方得:

(a1,1x1)2+(a1,2x2)2++(a1,nxn)2=(a2,1x1)2+(a2,2x2)2++(a2,nxn)2

把平方展开得:

a1,122a1,1x1+x12+a1,222a1,2x2+x22++a1,n22a1,nxn+xn2=a2,122a2,1x1+x12+a2,222a2,2x2+x22++a2,n22a2,nxn+xn2

移项合并得:

2(a1,1a2,1)x1+2(a1,2a2,2)x22(a1,na2,n)xn=a1,12+a1,22+a1,n2a2,12+a2,22+a2,n2

这样,就产生了一个关于 (x1,x2xn) 的方程,且未知数的系数和等号右边的数都已知。对于任意两个的点的坐标,我们都可以这样写出一个方程。只要我们写出至少 n 个这样的方程联立,就可以用高斯消元法求出每一个未知数,进而确定球心的坐标。

为了方便,对于第 i(i1) 个点,我们都利用这个点和第 i1 个点到球心的距离建立方程。这样我们就可以在输入时记录上一次输入的值与平方和,简便地建立方程。一共输入 n+1 个点,这样刚好建立了 n 个方程。

#include <bits/stdc++.h>
using namespace std;
long long n;
double a[200][200],b[200],ans[200],l[200],now[200],ls,ns,eps=1e-10; 
bool gauss()
{
	double div=0;
	for(int i=1;i<=n;i++)
	    {
	    long long mx=i;
	    for(int j=i+1;j<=n;j++)
	        if(fabs(a[j][i])>fabs(a[mx][i]))mx=j;
	    if(fabs(a[mx][i])<eps)return 0; 
	    if(i!=mx)
	       {
	       for(int j=1;j<=n;j++)swap(a[i][j],a[mx][j]);
	       swap(b[i],b[mx]);
	       }
	    div=a[i][i],b[i]/=div;
	    for(int j=1;j<=n;j++)a[i][j]/=div;
	    for(int j=i+1;j<=n;j++)
	        {
	        	div=a[j][i],b[j]-=b[i]*div;
	        	for(int k=i;k<=n;k++)a[j][k]-=a[i][k]*div;
			}
	    }
	ans[n]=b[n];
	for(int i=n-1;i>=1;i--)
	    {
	    ans[i]=b[i];
	    for(int j=n;j>i;j--)ans[i]-=a[i][j]*ans[j];
	    }
	return 1;
}

int main()
{
	scanf("%lld",&n);
	for(int i=1;i<=n+1;i++)
	    {
	    ns=0;
	    for(int j=1;j<=n;j++)
	        {
	        scanf("%lf",&now[j]);
	        ns+=now[j]*now[j];
	        if(i!=1)a[i-1][j]=2*(l[j]-now[j]);
	        }
	    if(i!=1)b[i-1]=ls-ns;
	    for(int j=1;j<=n;j++)l[j]=now[j];
	    ls=ns;
	    }
	gauss();
	for(int i=1;i<=n;i++)
	   	printf("%.3lf ",ans[i]);
	return 0;
}

例题 7

CF1344F Piet's Palette

我们发现,如果两项颜色相同,则把两项都删去,这很符合异或的性质。再结合后面一条两项颜色不同,将这两项替换为与这两种颜色不同的颜色,我们发现需要找到三个数 a,b,c 来代表三种颜色,并且满足 ab=c,ac=b,bc=a。当 a=1,b=2,c=3 时,刚好满足条件。自然,0 就代表了白色与空位。

接下来,我们考虑如何维护另外三种操作。每一个位置的颜色需要两个二进制位来存储,我们把每一个位置的两个二进制位拆成 aibi,其中 ai 表示低位,bi 表示高位。我们令 1 对应红色,2 对应黄色,3 对应蓝色。

对于 RY 操作,我们只需要把子序列内 aibi 交换,就把所有 R 变为 Y,所有 Y 变为 RB 和空位置不变。

修改前颜色 修改前编号 修改后编号 修改后颜色
R 01 10 Y
Y 10 01 R
B 11 11 B
. 00 00 .

对于 RB 操作,我们只需要把子序列内 bi 改为 aibi,就把所有 R 变为 B,所有 B 变为 RY 和空位置不变。

修改前颜色 修改前编号 修改后编号 修改后颜色
R 01 11 B
Y 10 10 Y
B 11 01 R
. 00 00 .

对于 YB 操作,我们只需要把子序列内 ai 改为 aibi,就把所有 Y 变为 B,所有 B 变为 YR 和空位置不变。

修改前颜色 修改前编号 修改后编号 修改后颜色
R 01 01 R
Y 10 11 B
B 11 10 Y
. 00 00 .

我们需要对于每个位置维护两个变量,saisbi,分别表示低位是 ai,bi,aibi 中的哪一个和高位是 ai,bi,aibi 中的哪一个。我们利用位运算,来记录构成这一位的答案中是否存在 aibi

对于每一个 mix 操作,把子序列中的低位和高位依次异或,结果要等于给出的混合结果对应的编号的低位和高位,我们就能得到两个等式。于是,我们就得到了一个异或方程组。直接使用高斯消元算法求解即可。

#include <bits/stdc++.h>
using namespace std;
long long n,k,now[4100],y[4100],sa[4100],sb[4100],cnt=0;
bool a[4100][4100],b[4100],ans[4100];
string op;
void ry()
{
	long long m=0,now=0;
	cin>>m;
	for(int i=1;i<=m;i++)
	    {
	    cin>>now;
	    swap(sa[now],sb[now]);
	    }
}

void rb()
{
	long long m=0,now=0;
	cin>>m;
	for(int i=1;i<=m;i++)
		{
		cin>>now;
		sb[now]=sa[now]^sb[now];
	    }
}

void yb()
{
	long long m=0,now=0;
	cin>>m;
	for(int i=1;i<=m;i++)
	    {
	    cin>>now;
	    sa[now]=sa[now]^sb[now];
	    }
}

void mix()
{
	long long m=0,id=0;
	char col=0;
	cin>>m;
	for(int i=1;i<=m;i++)cin>>now[i];
	cin>>col;
	if(col=='W')id=0;
	if(col=='R')id=1;
	if(col=='Y')id=2;
	if(col=='B')id=3;
	cnt++,b[cnt]=id&1;
	for(int i=1;i<=m;i++)
	    {
	    if(sa[now[i]]&1)a[cnt][now[i]]=1;
	    if(sa[now[i]]&2)a[cnt][now[i]+n]=1;
	    }
	cnt++,b[cnt]=(id>>1)&1;
	for(int i=1;i<=m;i++)
	    {
	    if(sb[now[i]]&1)a[cnt][now[i]]=1;
	    if(sb[now[i]]&2)a[cnt][now[i]+n]=1;
	    }
}

bool gauss()
{
	long long now=1,res=0;
    for(int i=1;i<=n*2;i++)
        {
        long long mx=now;
        for(int j=now;j<=cnt;j++)
            if(a[j][i]!=0)
               {
			   mx=j,res=max(res,mx);
			   break;
		       }
        if(a[mx][i]==0)continue;
        if(now!=mx)
           {
           for(int j=1;j<=n*2;j++)swap(a[now][j],a[mx][j]);
           swap(b[now],b[mx]);
           }
        for(int j=now+1;j<=cnt;j++)
            if(a[j][i]==1)
	            {
	                b[j]^=b[now];
	                for(int k=i;k<=n*2;k++)a[j][k]^=a[now][k];
	            }
	    y[now]=i;
	    now++;
		}
	for(int i=now;i<=cnt;i++)
	    if(b[i])return 0;
    return 1;
}

void print(long long p)
{
	if(ans[p]==0&&ans[p+n]==0)printf(".");
	if(ans[p]==1&&ans[p+n]==0)printf("R");
	if(ans[p]==0&&ans[p+n]==1)printf("Y");
	if(ans[p]==1&&ans[p+n]==1)printf("B");
}

int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++)sa[i]=1,sb[i]=2;
    for(int i=1;i<=k;i++)
        {
        	cin>>op;
        	if(op=="RY")ry();
        	else if(op=="RB")rb();
        	else if(op=="YB")yb();
        	else if(op=="mix")mix();
		}
	if(!gauss())printf("NO\n");
	else
	   {
	   	printf("YES\n");
	   	ans[y[n*2]]=b[n*2];
	   	for(int i=n*2-1;i>=1;i--)
	        {
	        ans[y[i]]=b[i];
	        for(int j=n*2;j>y[i];j--)
			    if(a[i][j])ans[y[i]]^=ans[j];
	        }
	    for(int i=1;i<=n;i++)print(i);
	    printf("\n");
	   }
	return 0;
}

后记

矩阵这个知识点,推导简洁,码量较小,是一个比较简单的知识点。当然,也有可能是我水平有限,没有学习矩阵较难的内容。

不过,确实只有推过莫比乌斯反演和写过高级数据结构,才会发现一个推导简洁,码量较小的知识点确实很难得。

posted @   w9095  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示