hihoCoder #1675 稀疏矩阵乘积

时间限制: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]=2help_A[1][4]=0,(可以理解为把其他位置的0全部搬运到了第一个出现0元素的位置上计数,那么其他位置的0元素就没了,所有不需要计数),对于矩阵A,生成的help_A为:在这里插入图片描述                  help_A
  对于矩阵B,我们不是记录行的连续0,而是记录列,类似于help_A;以B的第四列来说,B[1][4]=0B[2][4]=0B[3][4]=0B[4][4]=2,对应于help_B,help_B[1][4]=3,help_B[2][4]=0help_B[2][3]=0help_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中测试通过,且第二种解法效率优于第一种。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值