时间限制:10000ms
单点时限:1000ms
内存限制:256MB
描述
给定两个N × N的稀疏矩阵A和B,其中矩阵A有P个元素非0,矩阵B有Q个元素非0。请计算两个矩阵的乘积C = A × B并且输出C中所有非0的元素。
输入
第一行包含三个整数N, P, Q
以下P行每行三个整数i, j, k表示A矩阵的一个非0元素:Aij = k
以下Q行每行三个整数i, j, k表示B矩阵的一个非0元素:Bij = k
对于80%的数据,1 ≤ N, P, Q ≤ 200
对于100%的数据, 1 ≤ N, P, Q ≤ 2000, 1 ≤ i, j ≤ N, 0 ≤ k ≤ 100
输出
输出若干行,按先行后列的顺序输出矩阵C的每一个非0元素
每行三个整数i, j, k表示C矩阵的一个非0元素:Cij = k
样例输入
2 2 4
1 1 1
2 2 1
1 1 1
1 2 2
2 1 3
2 2 4
样例输出
1 1 1
1 2 2
2 1 3
2 2 4
题目分析:
稀疏矩阵:在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵。
对于此题来说,如果直接进行暴力运算,将每一个元素都计算一次,肯定会超时,因为做了很多无用功,我们都知道数值0乘任何数都等于0,而在稀疏矩阵中,数值为0的元素的个数是非常多的。那么这里我们肯定都会想到一种优化方法,就是元素值为0的元素,我们不去计算它,我们只计算非零元素。
如果我们人为计算,那肯定一眼就能看出来,哪些位置需要计算,哪些位置不需要计算,但是电脑不知道,电脑需要我们去告诉它,哪儿需要计算,哪儿可以不用计算。对于这道题,我有两个思路,如果大家还有其他更好的思路,欢迎留言补充,不足之处望大家指正出来,不甚感激。下面我给定两个四阶的稀疏矩阵A和B来进行说明。
A B
思路1
注:为了方便说明,下面涉及到的所有下标,都是从1开始,不从0开始
生成同阶的辅助数组,来记录矩阵中连续0的个数。对于矩阵A,我们需要记录每一行中,从每一次的 第一个0元素位置开始,连续0的个数,如果这个位置非0,我们就记为0,有一个0元素就记为1,有连续两个0,就记为2…以此类推。对于矩阵B,跟矩阵A类似,不过在B中不需要记录每一行中连续0的个数,而是记录每一列中连续0的个数。假设二维数组help_A为跟矩阵A同阶的辅助数组,二维数组help_B为跟矩阵B同阶的辅助数组。
以矩阵A的第一行元素为例:[0,1,0,0]
我们能够看出A[1][1]=0,A[1]2[]=1,A[1][3]=0,A[1][4]=0。在help_A中,我们将每一个位置一一对应起来,即help_A[]1[1]=1,help_A[1][2]=0,help_A[1][3]=2,
注意,我们在记录连续0的个数时,如果只有当前元素为0,那么我们直接记录为1,例如A[1]2[]=1,对应于help_A[]1[1]=1,当出现连续0的情况时,
例如A[1][3]=0****,A[1][4]=0,此处第一个出现0元素的位置为**(1,3),并且(1,4)**也是0,这里就有连续两个0,我们只需要把连续0的个数记录在第一个0元素出现的位置,其他0元素的位置设为0,即,help_A[1][3]=2,help_A[1][4]=0,(可以理解为把其他位置的0全部搬运到了第一个出现0元素的位置上计数,那么其他位置的0元素就没了,所有不需要计数),对于矩阵A,生成的help_A为: help_A
对于矩阵B,我们不是记录行的连续0,而是记录列,类似于help_A;以B的第四列来说,B[1][4]=0,B[2][4]=0,B[3][4]=0,B[4][4]=2,对应于help_B,help_B[1][4]=3,help_B[2][4]=0,help_B[2][3]=0,help_B[2][4]=0,依次类推,生成的help_B为:
help_B
那么我们如何使用辅助表呢,其实很好用,当我们做矩阵乘法时,我们先查询这两张辅助表,我们以矩阵A的第一行为例:我们用 i 记录行,j 记录列,初始化为 i = 1, j = 1(指向A的第一个元素A[1][1]),在开始计算前,我们查询 help_A[i][j](这里为help_A[0][0]=1>0),意思就是说,当前位置记录了0元素的个数,有 help_A[0][0]=1个,也就是从当前第 j 个位置开始,往后数help_A[i][j]个元素都是0,所以我们跳过为0的元素,不进行计算,一直到下一个非0元素的位置,即 j = j+help_A[i][j]=1+1=2,此时我们在矩阵A中的位置为 A[1][2](这个位置的元素非0),对于矩阵B的用法类似(查询help_B表)。运用这一种方法,其实我们可以同时查询这两张辅助表,因为当我们单独计算矩阵A的某一行乘B的某一列时,其实就是两个长度相等的一维数组元素对应相乘再相加,例如A的第一行[0,1,0,0],B的第一列[0,1,0,1],我们以 index(A的列标,B的行标) 记录一维数组中的位置,假设 index=3,我们通过查表 help_A[1][3]=2,help_B[3][1]=1。可以得到 index=index + max(help_A[1][3],help_B[3][1]),如果 index 没有越界,就说明我们得到了下一个可以计算的非0元素位置, Index 之所以要加两张表中最大的那一个,是因为元素都是对应相乘的,就比如[0,1,0,0]和[0,1,0,1],虽然在后一个数组中,第四位是非0的,但是在第一个数组中,第三,第四位都是0,所以相乘结果仍为0。我也不知道说清楚没有。下面上代码:
c++代码:
#include<iostream>
using namespace std;
int *help1,*help2;
//求矩阵1第m行乘矩阵第n列的结果
//k为矩阵的阶数
int matrixsum(int mat1[],int mat2[],int m,int n,int k){
int sum=0;
for(int r=0;r<k;r++){
if(help1[m*k+r]>0||help2[r*k+n]>0){//如果在这个位置出现了连续0的记录
//下标往后推进到下一个不为0的位置(通过加上 max{help1,help2})
r=r+(help1[m*k+r]>help2[r*k+n]?help1[m*k+r]:help2[r*k+n]);
}
if(r<k){//没有越界
sum+=mat1[m*k+r]*mat2[r*k+n];
}
}
return sum;
}
//n为矩阵阶数
void f(int *mat1,int *mat2,int n){
int *C=new int[n*n]();//存储相乘结果
for(int i=0;i<n;i++){
if(help1[i*n]!=n){//如果该行元素不是全为0
for(int j=0;j<n;j++){
if(help2[i*n+j]!=n){//如果该列元素不是全为0
C[i*n+j]=matrixsum(mat1,mat2,i,j,n);
}else{//该列元素全为0,不需要计算 ,计算下一列
continue;
}
}
}else{//该行元素全为0,不需要计算 ,计算下一行
continue;
}
}
//输出矩阵C
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(C[i*n+j]!=0){
cout<<i+1<<" "<<j+1<<" "<<C[i*n+j]<<endl;
}
}
}
delete []C;
}
//向右统计连续0的个数 (矩阵A的每一行都需要统计)
void cntr_0(int mat[],int help[],int N){
for(int i=0;i<N;i++){
int cnt=0;
int x=0;
bool flag=true;//标记是否为第一次找到0
for(int j=0;j<N;j++){
if(mat[i*N+j]!=0){//当前元素不为0
help[i*N+x]=cnt;//记录当前位置的0的个数
x=j; //更新下一个应该记录0元素个数的位置
cnt=0;//记录一个位置后,重置为0 ,开始记录下一个位置
flag=true;//记录完一次后,重置为true,方便开始下一次新的记录
}else{//当前元素为0
cnt++;//计数加1
if(flag){//第一次找到0
x=j;//保留当前第一个找到0的位置(这里是记录连续0的位置)
flag=false;//标记为false,也就是不需要找第一个为0的位置了(已经找到)
if(j==N-1){//如果已经找到了当前行的最后一个元素
help[i*N+x]=cnt;//将连续0的个数记录下来,就是当前位置
break;
}
}
if(j==N-1){//当前不是第一次找到0,并且已经是最后一个元素了
help[i*N+x]=cnt;//将连续0的个数记录下来(记录在第一个不为0的位置上)
break;
}
//如果不是第一次找到0,且没到该行的最后一个元素,继续循环寻找下一个位置是否为0
}
}
}
}
//向下统计连续0的个数 (矩阵B的每一列都需要统计)
//跟统计行的连续0的个数,基本上一致
void cntd_0(int mat[],int help[],int N){
for(int i=0;i<N;i++){//控制列
int cnt=0;
int x=0;
bool flag=true;//标记是否为第一次找到0
for(int j=0;j<N;j++){//控制行
if(mat[j*N+i]!=0){
help[x*N+i]=cnt;
x=j;
cnt=0;
flag=true;
}else{
cnt++;
if(flag){//第一次找到0
x=j;
flag=false;
if(j==N-1){
help[x*N+i]=cnt;
break;
}
}
if(j==N-1){
help[x*N+i]=cnt;
break;
}
}
}
}
}
int main(){
//此程序中都是以一维数组来存储的矩阵,
//然后存取是二维的( A[i][j]=A[i*N+j] )
//所涉及到的下标都是从0开始
int N,P,Q;
cin>>N>>P>>Q;
int *A=new int[N*N]();
int *B=new int[N*N]();
help1=new int[N*N]();//辅助矩阵
help2=new int[N*N]();//辅助矩阵
for(int r=0;r<P;r++){
int i,j,k;
cin>>i>>j>>k;//A(ij)=k;
A[(i-1)*N+j-1]=k;
}
for(int r=0;r<Q;r++){
int i,j,k;
cin>>i>>j>>k;//B(ij)=k;
B[(i-1)*N+j-1]=k;
}
cntr_0(A,help1,N); //生成辅助矩阵1
cntd_0(B,help2,N); //生成辅助矩阵2
f(A,B,N);
delete []A,B,help1,help2;
return 0;
}
思路2
注:思路二中涉及到的所有下标是从0开始的
思路1的代码能够提交通过,但是需要开辟两个额外的辅助数组,是一种典型的用空间换时间的解法,并且在生成辅助表的时候,也需要耗费一定时间。而思路2的解法,不需要开辟空间来单独存储矩阵A和矩阵B,我们只需要记录矩阵中有值的元素的行标、列标和值,再计算时,直接计算有值的部分就行了。下面开始思路讲解。
用c++提供的类或结构体(我用的类),来记录矩阵非0元素的信息(行标,列标,值),除了行标、列标和值外,我还定义了一个用来记录距离的变量d,这里我定义为rec类。
class rec{//记录矩阵有值点的下标和值
public:
//x为行标,y为列标,z为(x,y)处的值,
//d记录矩阵A同一行中,第一个有值的位置和第二个有值 的位置差
//或者是记录矩阵B同一列中,第一个有值的位置和第二个有值 的位置差
int x,y,z,d;
rec(int x1=-1,int y1=-1,int z1=-1,int d1=-1){
x=x1;
y=y1;
z=z1;
d=d1;
}
void setValue(int x1,int y1,int z1){
x=x1;
y=y1;
z=z1;
}
void print(){
cout<<"["<<x<<","<<y<<","<<z<<","<<d<<"]"<<endl;
}
};
然后定义两个对象数组,分别用来记录A,B中的非0元素的信息。
rec *A= new rec[P+1];
rec *B=new rec[Q+1];
保险起见,我们先对两个对象数组按照不同的排序规则进行排序,使他们规范化。对于矩阵A,我们按照行标升序排列,如果行标相等,按照列标升序,对于矩阵B,我们按照列标升序,如果列标相等,按照行标升序。
//A的排序规则
bool cmp1(rec a,rec b){//按行标升序
if(a.x==b.x){//行相等,按列升序
return a.y<b.y;
}
return a.x<b.x;
}
//B的排序规则
bool cmp2(rec a,rec b){ //按列标升序
if(a.y==b.y){//列相等,按行升序
return a.x<b.x;
}
return a.y<b.y;
}
排序完成后,我们遍历A数组(矩阵A),找到所有在同一行中的非0元素,在找的同时,我们更新 d( d 为记录矩阵A(B)的同一行(列)中,两个有值位置的位置差),例如矩阵A的第二行[2,3,0,0],2的信息存储在A[1] {x=1,y=0,z=2,d=-1 }中,(因为A的第一行有一个非0元素,这个位置为A[0]),3这个位置的信息存储在A[2] {x=1,y=1,z=3,d=-1 }中,那么我们可以很明显的看见,A[1]和A[2]的列标 y 差了1(只看同一行中),那么我们将这个列标的差记录在后一个元素A[2]中,即A[2].d=A[2].y-A[1].y=1,此时A[2] {x=1,y=1,z=3,d=1 },这样我们通过A[2]中的d,就可以确定A[1]的位置。当A数组的某一行的非0元素全部找到后,我们就可以去遍历B数组(矩阵B),找到B中在同一列中的所有非0元素(同时更新 B中的d)。
当A的一行和B的一列全部找好并且 d 也更新完毕,我们就可以进行计算了。以A的第二行[2,3,0,0]和B的第一列[0,1,0,1]为例:
A[1]={x=1,y=0,z=2,d=-1}(在同一行中A[1]的前面没有非0元素了,所以 A[1].d= - 1),A[2] ={x=1,y=1,z=3,d=1 }(同一行中,A[2]的前面还有非0元素A[1]。所以A[2].d=A[2].y-A[1].y=1);
B[0]={x=1,y=0,z=1,d=-1},B[1]={x=3,y=0,z=1,d=2}(B[1].d=B[1].x-B[0].x=2)
为了方便演示,我们将A的第二行,和B的第一列放在两个一维数组中:
A的第二行
B的第一列
当开始计算时,我们从后往前算,也就是如下图所示:
假设我们定义col=A[2].y=1;row=B[1].x=3,通过上述图片,我们很明显的看出来,此时的两个点,并不处于同一个位置上(col != row),也就是不能进行计算,并且col < row,这就意味着 col 这个点后面的值,都不用去计算(因为0乘任何元素都为0)。通过查询B[1].d=2,我们可以知道,在B[1]的前面还有满足条件的非0元素B[0],所以我们通过row=row-B[1].d=3-2=1,可以得到B的新的计算位置row,如图所示:
此时col=row=1,可以进行计算,即A[2].z*B[0].z=3*1=3,计算完毕后,我们通过查询A[2].d=1和B[0].d= - 1,发现A[2]前面还有非0元素,B[0]前面没有了,由于0乘任何元素都为0,所以在row这个位置往前的值都不需要计算了,退出计算。下面上代码:
C++代码:
#include<iostream>
#include<algorithm>
using namespace std;
class rec{//记录矩阵有值点的下标和值
public:
//x为行标,y为列标,z为(x,y)处的值,
//d记录前一个矩阵同一行中,第一个有值的位置和第二个有值 的位置差
//或者是记录后一个矩阵同一列中,第一个有值的位置和第二个有值 的位置差
int x,y,z,d;
rec(int x1=-1,int y1=-1,int z1=-1,int d1=-1){
x=x1;
y=y1;
z=z1;
d=d1;
}
void setValue(int x1,int y1,int z1){
x=x1;
y=y1;
z=z1;
}
void print(){
cout<<"["<<x<<","<<y<<","<<z<<","<<d<<"]"<<endl;
}
};
bool cmp1(rec a,rec b){//按行标升序
if(a.x==b.x){//行相等,按列升序
return a.y<b.y;
}
return a.x<b.x;
}
bool cmp2(rec a,rec b){ //按列标升序
if(a.y==b.y){//列相等,按行升序
return a.x<b.x;
}
return a.y<b.y;
}
//从后往前找
int matrixsum(rec *A,rec *B,int index1,int index2,int n){
int sum=0;
int col=A[index1].y;//该行最右边的有值元素的列
int row=B[index2].x;//该列最下边的有值元素的行
while(true){
if(col<row){//说明这两个位置的元素,不是对应的
if(B[index2].d!=-1){//说明该列还有不为0的元素
row=row-B[index2].d;//计算这一列中 当前位置的上一个不为0元素的位置
index2=index2-1;//更新当前元素的下标 (B的下标往前移动一次)
}else{//说明该列没有非0元素了,就不用计算了
break;
}
}else if(row<col){//说明这两个位置的元素,不是对应的
if(A[index1].d!=-1){//说明该行还有不为0的元素
col=col-A[index1].d;//计算这一行中 当前位置的上一个不为0元素的位置
index1=index1-1;//更新当前元素的下标 (A的下标往前移动一次)
}else{//说明该行没有非0元素了,就不用计算了
break;
}
}else{//需要计算 两个位置的元素对应
sum+=A[index1].z*B[index2].z;
if(A[index1].d==-1||B[index2].d==-1){//只要这一行或者这一列 ,当前位置的前面元素都为0,退出计算
break;
}else{//否则,这一行和这一列前面还有非0元素,还应该继续计算
col=col-A[index1].d;//计算这一行中 当前位置的上一个不为0元素的位置
row=row-B[index2].d;//计算这一列中 当前位置的上一个不为0元素的位置
index1=index1-1;//将A的下标往前移动一步(指向 该行中 前一个非0元素)
index2=index2-1;//将B的下标往前移动一步(指向 该列中 前一个非0元素)
}
}
}
return sum;
}
void f(rec *A,rec *B,int p,int q,int n){
//存储结果这里也可以直接用对象数组存储有值的结果
int *C=new int[n*n]();
int x1=A[0].x;
bool flag1=true;
for(int i=0;i<p;i++){
if(A[i].x!=x1){//当行标不相等时
flag1=true;//当开始新行时,要重新更新flag1
int y1=B[0].y;
bool flag2=true;
for(int j=0;j<q;j++){
if(B[j].y!=y1){//当列标不相等时
flag2=true; //当开始新列时,要重新更新flag2
C[x1*n+y1]=matrixsum(A,B,i-1,j-1,n);
y1=B[j].y;//更新y1的值(开始新的一列)
j=j-1;//回退一步,因为马上要+1了
}else{
if(flag2){
flag2=false;
}else{
B[j].d=B[j].x-B[j-1].x;
}
}
}
x1=A[i].x;//更新x1的值(开始新的一行)
i=i-1;//回退一步,因为马上要+1了
}else{
if(flag1){
flag1=false;
}else{
A[i].d=A[i].y-A[i-1].y;
}
}
}
//输出C
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(C[i*n+j]!=0){
cout<<i+1<<" "<<j+1<<" "<<C[i*n+j]<<endl;
}
}
}
delete []C;
}
//A按行标从小到大排序,B按列标从小到大排序
int main(){
int N,P,Q;
cin>>N>>P>>Q;
rec *A= new rec[P+1];
rec *B=new rec[Q+1];
for(int r=0;r<P;r++){
int i,j,k;
cin>>i>>j>>k;//A(ij)=k;
A[r].setValue(i-1,j-1,k);
}
for(int r=0;r<Q;r++){
int i,j,k;
cin>>i>>j>>k;//B(ij)=k;
B[r].setValue(i-1,j-1,k);
}
sort(A,A+P,cmp1);
sort(B,B+Q,cmp2);
f(A,B,P+1,Q+1,N);
delete []A,B;
return 0;
}
以上两种解法,都在hihoCoder中测试通过,且第二种解法效率优于第一种。