前言
本篇博客会较为详细地讲一下我个人对三角矩阵压缩存储公式的理解,希望能给后面的朋友们带来一些帮助。
等差数列的求和公式
由于三角矩阵的压缩存储公式是依靠求和公式来推导的,所以得先补一下等差数列的求和公式。
求和公式一:
其中n是整个数列的项数,
是数列的首项,d是数列的公差(递增数列公差为正数,递减数列公差为负数)。
求和公式二:
其中n为整个数列的项数,
是数列的首项,是数列的末项。下面主要用到这个公式二。
上三角矩阵压缩储存公式的推导
首先我们知道,压缩储存上三角矩阵,本质上就是将矩阵的上三角块的元素“展开”成一条长的数列存在数组里。问题就在于,我们如何根据原矩阵里元素的行号和列号得到压缩后数组里对应的下标?
我们可以这样考虑:对于一个上三角块里第i行第j列的元素,它在数组里的下标就等于(在原矩阵中)他前面i-1行的元素数量 + (原矩阵中)他所在行的他前面的元素数量,以下面这个矩阵为例,
在数组里的位置就应该是它前面两行元素的数量5+4=9,再加上所在行它前面的元素数量1(即是),最终结果10即是在数组中的位置(当然,转换成物理下标的话还需要-1)。那么问题又来了,我们如何才能知道前面1到i-1行的元素数量?这个时候就要用到我们的等差数列求和公式了,我们可以从上到下地将每行的元素数量看成一个数列,对于上图的矩阵来说,这个数列就是5 4 3 2 1,项数为5,首项为5,末项为1,直接套进求和公式二,假设矩阵的维度n为5(意为5*5的矩阵),就可以得到
,这就是压缩后数组大小的公式,其中n是矩阵的维度。但这与我们要求1到i-1行的元素数量有什么关系?确实没什么关系,上面这段是我补充的内容,但是理解了求整个数组大小的过程对后面有帮助,下面回到正题。
1到i-1行的元素数量我们可以可以看成是一个数列,这个数列的首项是n(这个n也是矩阵的维度),末项是第i-1行的元素数量是n-i+2(找行号与该行的元素数量的规律可得),数列的项数是i-1,套进求和公式二里就可以得到
,这样就解决了求1到i-1行元素数量的问题。注意,这里数列的末项是指的上一行的元素数量,是i-1行的元素数量。那么就到第二个问题,如何知道在所在行,它前面的元素数量?
这个简单,找一下行号和列号之间的规律就知道是j-i(列数减行数),那么我们最终的公式就出来了:
在数组里的位置=
嗯?好像与一些数据结构教材上给的公式对不上?这是因为我们的上图的矩阵元素是默认从
开始的,所以上面那个最终公式我写的是在数组里的位置,而不是在数组里的下标。一般我们的矩阵行号列号都是从0开始算的,也就是(对上图矩阵而言)矩阵里第一个元素是最后一个元素是,那这样上面推导过程中出现的数组就变成是以n为首项,n-i+1为末项,项数为i的数列,套进求和公式二就可以得到,再加上该元素所在行,它前面的元素数量j-i,可得最终的公式:在数组里的下标=
这个公式应该是和大部分数据结构教材给的公式一致的。
推导过程总结
因为推导过程写得比较乱,这里再总结一下,理清思路。
对于求元素在压缩后数组里下标的问题,我们可以转换思路,变成求
前面有几个元素的问题;求
前面有几个元素的问题,我们又可以拆成求前面i-1行的元素数量的问题1和求在所在行,前面的元素数量的问题2;对问题1,我们将矩阵每行的元素数量从上到下视为一个等差数列,利用等差数列的求和公式二求解。
对问题2,我们可以去找一下其中行列号的规律来解,也就是j-i,列数减行数。
推导过程中要注意的地方有三个:
- 数列的末项是对来说的i-1行,而不是所在的第i行。
- 对第x行来说,元素的数量就等于n-x+1,其中n是原矩阵的维度,x是指逻辑上的顺序,从1开始。下面从0开始和从1开始的矩阵数列末项会不同也是因为x的取值不同所导致,公式不同也是。
- 矩阵行列号从0还是从1开始的问题,从0开始的数列是一个首项为n,末项为n-i+1,项数为i的数列;从1开始的数列是一个首项为n,末项为n-i+2,项数为i-1的数列
最终公式是:
在数组里的下标=,其中i为行号j为列号,n为原矩阵维度
代码实现
毕竟标题都用了练习二字,不打代码说不过去,于是怒写一个cpp:
#include <iostream>
#define dimSize 5
using namespace std;
typedef struct TriangularMatrix
{
int *data;
int dim;
TriangularMatrix() : data(NULL), dim(-1) {}
} Mat;
int getArraySize(Mat *mat)
{
int dim = mat->dim;
int sz = dim * (dim + 1) / 2 + 1; //+1 for saving constant C
return sz;
}
Mat *createMat()
{
Mat *mat = new Mat;
mat->dim = dimSize;
mat->data = new int[getArraySize(mat)]{0};
return mat;
}
void destroyMat(Mat *&mat)
{
delete[] mat->data;
delete mat;
mat = NULL;
}
bool checkIndex(Mat *mat, int col, int row)
{
int n = mat->dim;
if (col > n || row > n)
return false;
return true;
}
void printArr(Mat *mat)
{
int sz = getArraySize(mat);
cout << "TMat:[";
for (int i = 0; i < sz; i++)
{
cout << mat->data[i];
if (i != sz - 1)
cout << " ";
}
cout << "] " << sz << endl;
}
void initTMat(Mat *mat, int *data)
{
int n = mat->dim;
int k = 0;
int tmp;
for (int i = 0; i < n; i++) //row
{
for (int j = i; j < n; j++) //column
{
if (j >= i)
{
tmp = i * n + j;
mat->data[k++] = data[tmp];
}
}
}
mat->data[k] = data[(n - 1) + 1]; //constant C
}
int getValue(Mat *mat, int col, int row) // col and row are logical indices (based-1)
{
if (!checkIndex(mat, col, row))
return INT_MIN;
int n = mat->dim;
--col; //logical index to physical index
--row; //ditto
if (col >= row)
return mat->data[row * (2 * n - row + 1) / 2 + col - row];
else //if col < row
return mat->data[getArraySize(mat) - 1];
}
void printTMat(Mat *mat)
{
int n = mat->dim;
for (int r = 1; r <= n; r++) //row
{
for (int c = 1; c <= n; c++) //column
cout << getValue(mat, c, r) << " ";
cout << endl;
}
}
int main()
{
Mat *mat = createMat();
int arr[25]{
0, 1, 2, 3, 4,
0, 5, 6, 7, 8,
0, 0, 9, 10, 11,
0, 0, 0, 12, 13,
0, 0, 0, 0, 14};
initTMat(mat, arr);
printArr(mat);
printTMat(mat);
destroyMat(mat);
return 0;
}
在重点地方都给了英文的注释,这里再补充几点:
- 开数组的时候给多了一个空间是用来储存常数,因为对于上三角矩阵来说,下三角块就是一个常数,也需要一个数组空间来存。
- initTMat里用了上三角矩阵的性质来用if筛选元素存进数组里;第二个for的初始化语句int j = i是灵光一闪想到的优化循环次数的条件;最后一句取下三角常数的语句是随便挑的,顺带考虑了一下普适性用了维度作为下标,如果取一个特定值的话可能在更大维度的矩阵上会取到上三角的值。
- getValue的参数行号和列号是逻辑的,也就是从1开始的,代码里用自减转换为物理下标。
本文到此结束。
来源:CSDN
作者:Melonl
链接:https://blog.csdn.net/u014799564/article/details/103960121