这道题目要求对稀疏矩阵做快速转置运算。因此我们首先需要了解快速转置和普通转置的区别。
这两种转置方式的前两个步骤都是:输入;行列互换。
关键在于第三步:行列互换后如何再按“行优先”排序。
普通转置方法可以在这里通过排序的方法,按照行列互换后“行优先”,行相同时列优先这种规则对行列互换后的三元组表排序,之后再输出即可。但这种方法的缺陷在于即使是使用快排、堆排这种本身已经很快的排序算法,对这个问题处理的时间复杂度依然来到了nlogn,OJ不认可这样的时间复杂度,因此使用这种排序方法得到的代码,结果上是对的,但OJ会给出错误的评判。
例如下面这一份代码:
#include <stdio.h>
void HeapAdjust(int num[][2],int target,int n)
{
int temp=num[target][0],tempvalue=num[target][1];
for(int i=target*2;i<=n;i*=2)
{
if(i<n&&num[i][0]<num[i+1][0]) i++;
if(temp>=num[i][0]) break;
num[target][0]=num[i][0],num[target][1]=num[i][1];
target=i;
}
num[target][0]=temp,num[target][1]=tempvalue;
}
int main()
{
int n,m,k;
scanf("%d %d %d", &n,&m,&k);
int Data[k][2]; //使用二维数组存储三元表的内容,每一行的第一位会存储col*10+row,第二位存储value
for(int i=0;i<k;i++)
{
int row,col,value;
scanf("%d %d %d", &row,&col,&value);
Data[i][0]=col*10+row,Data[i][1]=value;
}
for(int i=k/2;i>=0;i--) HeapAdjust(Data,i,k-1);
for(int m=k-1;m>0;m--)
{
int temp1=Data[m][0],temp2=Data[m][1];
Data[m][0]=Data[0][0],Data[m][1]=Data[0][1];
Data[0][0]=temp1,Data[0][1]=temp2;
HeapAdjust(Data,0,m-1);
}
for(int i=0;i<k;i++) printf("%d %d %d\n",Data[i][0]/10,Data[i][0]%10,Data[i][1]);
return 0;
}
另一个值得注意的地方是:OJ要求元素个数最大是10000,因此最开始给存储三元组表的数组分配空间时不能低于这个值,否则会得到“超时”的评判,这有可能是因为OJ在输入数据时发现空间不够于是迟迟没有完成输入导致的超时结果。
以下是快速转置的方法及代码实现:
快速转置通过两个辅助数组Count和StartLocation实现了在一次遍历三元组表的过程中直接判断其每一个元素在新三元组表中的位置,由此它的时间复杂度达到了O(n)。在关于这个方法的不同介绍中对辅助数组有有不同的名字。(例如cpot,但坦白地说,对我这种英语不是很好的人来说,第一次看到这个名字还是有点搞不明白它的作用,因此我的代码里给它取了这个更直白的名字)
简单地说,Count数组记录了输入时每一列的元素个数,StartLocation数组在此基础上计算每一列的第一个非零元素在转置后的起始位置。由此在一次遍历输入三元组表的过程中,就能判断出各个元素在新三元组表中的位置并完成赋值操作。
#include <stdio.h>
int main()
{
int n, m, k;
scanf("%d %d %d", &n, &m, &k); //输入行数、列数、元素个数
int PreNum[10001][3], NewNum[10001][3], count[10001], StartLocation[10001];
for (int i = 1; i <= k; i++)
{
int row, col, value;
scanf("%d %d %d", &row, &col, &value);
PreNum[i][0] = row, PreNum[i][1] = col, PreNum[i][2] = value;
count[col]++; //记录输入每一列,也就是新表对应的每一行的元素个数
}
StartLocation[1] = 1; //第一列中第一个非零元素转置后的位置是1
for (int i = 2; i <= m; i++) //StartLocation数组存储的是每一列第一个非零元素的位置
StartLocation[i] = StartLocation[i - 1] + count[i - 1];
for (int i = 1; i <= k; i++)
{
int start = StartLocation[PreNum[i][1]]; //根据列数判断存放位置
NewNum[start][1] = PreNum[i][0];
NewNum[start][0] = PreNum[i][1];
NewNum[start][2] = PreNum[i][2];
StartLocation[PreNum[i][1]]++; //每一列每存放完一个元素,该列元素存放的起始位置要加1
}
for (int i = 1; i <= k; i++)
printf("%d %d %d\n", NewNum[i][0], NewNum[i][1], NewNum[i][2]);
return 0;
}
另外还有一种简单的思路:直接赋值法【这种思路和人的行为很类似】
也就是对于输入的数据,直接依次将输入的列为1的元素,为2的元素,直至为m的元素录入新的三元表组。由于输入时是按照行优先的,因此这样直接录入的顺序是正确的。
另一方面,由于只要发现不是该列的就立刻判断下一个元素,在k>>m的情况下这种方法的时间复杂度仍是O(n),但由于需要判断,因此花费的时间比上面的快速转置要多一点。
#include <stdio.h>
int main()
{
int n, m, k;
scanf("%d %d %d", &n, &m, &k);
int PreNum[10001][3], NewNum[10001][3];
for (int i = 1; i <= k; i++)
scanf("%d %d %d", &PreNum[i][0], &PreNum[i][1], &PreNum[i][2]);
for (int i = 1, count = 1; i <= m; i++)
{
for (int j = 1; j <= k; j++)
{
if (PreNum[j][1] == i)
{
NewNum[count][0] = PreNum[j][1];
NewNum[count][1] = PreNum[j][0];
NewNum[count][2] = PreNum[j][2];
count++;
}
}
if (count > k)
break;
}
for (int i = 1; i <= k; i++)
printf("%d %d %d\n", NewNum[i][0], NewNum[i][1], NewNum[i][2]);
return 0;
}
除此之外,还可以利用C++自带的sort函数在输入时直接对输入进行排序,这样的方法写出的代码很简洁但可能不是很容易懂。读者可参考CSDN上关于这道题的另外几篇文章,它们大多数都用到了sort函数。
欢迎讨论交流。