草庐IT

稀疏矩阵 C/C++

Gowilli 2024-04-10 原文

前言

关于稀疏矩阵在计算机科学中的应用,数据结构课程可能会有所涉及,但是在各类信息学竞赛中确几乎不会出现。这是因为数据结构课程中描述的稀疏矩阵相关算法冗余难懂,使用了大量不必要的操作。而信息学竞赛中经常会用到压缩空间的技巧,这一思想可以潜移默化的转移来处理数据结构课程中遇到的稀疏矩阵相关的问题。本文另辟蹊径,不同于某些讲师和教材,从本质入手,提供稀疏矩阵相关的一些算法的实现。

引入

矩阵

矩阵(Matrix)是一个按照长方阵列排列的复数或实数集合

由定义不难得知,矩阵研究的是之间的关系。

在程序中表示方法

在C/C++语言中,可以使用二维数组来模拟矩阵。

局限性

但是如果当一个矩阵的行数和列数很大,例如说有一万行和一万列时候,我们需要考虑在程序中实现的可行性。

这里应该认识到,在C++默认的编译器设置中,堆区空间大小是有限的,如果我们开int类型的数组的话,数组的最大容量大约在 1 e 8 1e8 1e8 数量级,而这时这个数组已经要占用超过400MB的内存空间!
如果这个数组开在函数中例如main里面的话,栈区空间大小更小,只能开到 1 e 5 1e5 1e5 数量级。超过这个大小,程序将会出现段错误(运行错误)。

那么存储上面提到的一个巨大的矩阵的时候,我们大概需要 1 e 5 × 1 e 5 = 1 e 10 1e5\times1e5=1e10 1e5×1e5=1e10 的数组容量,这需要40GB的内存空间,任何程序都是难以承受的。

但是如果这个矩阵是稀疏矩阵的话,也许我们还有方法能将它存储到程序中去。

稀疏矩阵的定义

在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律时,则称该矩阵为稀疏矩阵 (sparse matrix)

简单地说,如果矩阵的元素个数非常多,但其中大多数都是 0 0 0 元素(后面会知道这个多数元素只要是相等的也满足)时候,这个矩阵就被称作是一个稀疏矩阵(sparse matrix)
那上面那个 1 e 5 × 1 e 5 1e5\times1e5 1e5×1e5 大小的矩阵为例,虽然其中有 1 e 5 × 1 e 5 = 1 e 10 1e5\times1e5=1e10 1e5×1e5=1e10 个元素,但如果其中只有几百个或者几十个元素是不为零的(与大小相差若干个数量级),那么这个矩阵就是稀疏矩阵。

稀疏矩阵的存储

这里还是遵循了数据结构课程上介绍的三元组法对稀疏矩阵进行存储

我们知道,对于每一个矩阵中的元素,我们在描述它的时候会说

i i i 行第 j j j 列的元素 a i j a_{ij} aij等于XXX ( a a a 表示矩阵)

所以一个元素的信息包括了它所在的行数、列数以及它自身的数值
我们可以使用一个三元组来将这三个值存储起来,在稀疏矩阵中,我们只需要存储非零元素的信息

程序中存储单个元素

利用C/C++中的结构体类型来存储
我们定义一个struct类型 S p a r s e M a t r i x SparseMatrix SparseMatrix
里面的成员int型变量有行下标row、列下标col和它的值val a r o w , c o l a_{row,col} arow,col

存储整个稀疏矩阵

为了表示整个稀疏矩阵,我们要将所有非零元struct S p a r s e M a t r i x SparseMatrix SparseMatrix存储起来。可以用数组来存,在开辟大小时候,可以预确定非零元的个数来开,这里使用vector来存储

有的情况下我们还需要存储原矩阵的行数和列数

创建和读取

在程序中可能要求给出的是一个以普通矩阵表示的矩阵,需要对其进行一系列操作,并将其以普通矩阵的形式输出

那么我们就需要在读入时将矩阵转化为稀疏矩阵并在输出时将稀疏矩阵的三元组形式以普通矩阵的形式进行输出

下面的操作均是在这个意义下进行的
这时候稀疏矩阵的信息应该包括原矩阵的行数和列数

读入稀疏矩阵

与普通二维数组形式的矩阵的读入类似,使用一个双重循环,分别在循环行数和列数中循环,读入一个元素,判断其是否为非零元,若为非零元,将其以及其所处的行数和列数加入到vector

输出稀疏矩阵

输出问题是稀疏矩阵相关问题的难点和核心所在,有些讲师之所以设计出晦涩的代码,是因为他们没有掌握输出方面的一些要点或者技巧,以至于使用复杂的方法来实现。

思路

关键就在于零元素的输出
我们首先要知道零元素所处的位置,而这可以通过相邻非零元之间的位置所确定。与此同时,我们也知道了零元素的数量,就可以利用循环来进行输出了。

确定零元的范围

那么如何确定零元素的范围呢?
我们可以通过两个变量lcollrow来追踪上一个已经输出的非零元的行数和列数,然后将当前即将要输出的非零元的位置与它们作比较
在相应位置填补上零元并输出当前非零元后更新lcollrow

注意

在实现过程中需要注意输出完最后一个非零元后其后面矩阵的零元也需要接着输出,这些可以通过查看下面的代码来了解

稀疏矩阵的转置

转置操作可以概括为将矩阵元素的行和列互换

在稀疏矩阵中,我们只需要考虑非零元的变化,其余零元无论其位置变化其值是始终不变的



对于上面两张图表示的算法,只能表示:

实际上,将以三元组存储的稀疏矩阵转置的操作只要三步

  • 将原矩阵的行数和列数互换
  • 遍历三元组,对每个三元组中的行数和列数互换
  • 将三元组重新排序

第三步需要做额外的说明,请看其存储顺序

三元组的存储顺序

回顾稀疏矩阵以普通矩阵输出的输出函数,发现我们是按顺序将其非零元遍历输出的。如果顺序是错乱的,零元的输出会出现问题。

这里的顺序指的就是在以普通矩阵的表示形式中每个元素在以行优先到列从小到大依次访问的先后次序

在读入时,它输入时的先后次序就是我们所定义的顺序,不需要额外操作

但如果在对三元组进行其他可能改变这个顺序的操作,需要对三元组进行重排

根据定义我们知道

  • 如果行数不同,行数小的元素顺序在前
  • 如果行数相同,列数小的元素顺序在前

于是我们可以重载结构体struct S p a r s e M a t r i x SparseMatrix SparseMatrix中的 < < < 运算符
具体实现也请参照下面代码

参考代码(C++)

这个代码实现了对矩阵的转置操作
Please Forgive me those comments were writen in English
可以从中学习稀疏矩阵的存储、转置、转化和输出

/*author:Gowilli
theme:How to store and transpose a sparse matrix
*/
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
// to store non-zero elements in original matrix
// they could be descripted as the form of row-col and their own value
struct SparseMatrix
{
    int row, col, val;
    /*how to compare two sparsematrix in tuple
    evidently,this could boil down to which one would appear first
    when print the matrix manually
    so just need to compare their row and column
    */
    bool operator<(SparseMatrix const &b) const
    {
        if (row != b.row)
            return row < b.row;
        else if (row == b.row)
        {
            return col < b.col;
        }
        return true;
    }
};
/*
 the first thing that matters is how to convert a matrix to a sparse one
 and how we could output the sparse matrix into ordinary matrix form
 we could use a vector to store the tuple
 we agree that all index start from one
and all the elements in vector should be sorted in ascending order
this is automatically implemented while in reading
but you have to sort them manually when you transpose it
*/
vector<SparseMatrix> v;

// convert to sparsematrix tuple form,two parameters should be the numbers of rows and columns
void read(int n, int m)
{
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            // the elements of original matrix
            int x;
            cin >> x;
            if (x) // if read a non-zero element we need to store it to the vector
            {
                v.push_back({i, j, x});
            }
        }
    }
}
// convert  sparsematrix in tuple form to ordinary matrix,two parameters should be the numbers of rows and columns
void print(int n, int m)
{
    int lrow = 1, lcol = 0;
    for (int i = 0; i < v.size(); i++)
    {
        int row = v[i].row, col = v[i].col, val = v[i].val;
        // need fill zero between last non-zero element printed and this one
        if (lrow < row)
        {
            for (int j = lcol + 1; j <= m; j++)
                cout << 0 << " ";
            cout << endl;
            lrow++;
            while (lrow < row)
            {
                for (int j = 1; j <= m; j++)
                    cout << 0 << " ";
                cout << endl;
                lrow++;
            }
            for (int j = 1; j < col; j++)
                cout << 0 << " ";
            cout << val << " ";
        }
        else if (lrow == row)
        {
            for (int j = lcol + 1; j < col; j++)
                cout << 0 << " ";
            cout << val << " ";
        }
        else
        {
            cout << "error! check your input!" << endl;
            return;
        }
        // update recorded printed postion
        lcol = col;
    }
    // fill from last elements to the end of matrix(rightbottom)
    if (lrow < n)
    {
        for (int j = lcol + 1; j <= m; j++)
            cout << 0 << " ";
        cout << endl;
        lrow++;
        while (lrow < n)
        {
            for (int j = 1; j <= m; j++)
                cout << 0 << " ";
            cout << endl;
            lrow++;
        }
        for (int j = 1; j <= m; j++)
            cout << 0 << " ";
    }
    else if (lrow == n)
    {
        for (int j = lcol + 1; j <= m; j++)
            cout << 0 << " ";
    }
    else
    {
        cout << "error! check your input!" << endl;
        return;
    }
}

// transpose the sparsematrix,paramterer should be passed in reference form
void transpose(int &n, int &m)
{
    swap(n, m);
    for (int i = 0; i < v.size(); i++)
    {
        swap(v[i].row, v[i].col);
    }
    sort(v.begin(), v.end());
}

// main function behind is to transpose a sparse matrix
// which is input in ordinary form and require to output in ordinary form
int main()
{
    int n, m;
    cin >> n >> m;
    read(n, m);
    transpose(n, m);
    print(n, m);
    return 0;
}

(END)
ps.For students in BUCT,you could pass 算法5-1:稀疏矩阵转置 on BUCTOJ using code given above,meanwhile I would remind you this problem is suck cuz it even could pass use a two dimensional array ,so do to many other problems about sparse matrix on BUCTOJ ,those test data are too weak .

有关稀疏矩阵 C/C++的更多相关文章

  1. 旋转矩阵的几何意义 - 2

    点向量坐标矩阵的几何意义介绍旋转矩阵的几何含义之前,先介绍一下点向量坐标矩阵的几何含义点:在一维空间下就是一个标量,如同一条直线上,以任意某一个位置为0点,以一定的尺度间隔为1,2,3...,相反方向为-1,-2,-3...;如此就形成了一维坐标系,这时候任何一个点都可以用一个数值表示,如点p1=5,即即从原点出发沿着x轴正方向移动5个尺度;点p2=-3,负方向移动3个尺度;     在一维坐标系上过原点做垂直于一维坐标系的直线,则形成了二维坐标系,此时描述一个点需要两个数值来表示点p3=(3,2),即从原点出发沿着x轴正方向移动3个尺度,在此基础上沿着y轴正方向移动两个尺度的位置就是点p3。

  2. 华为OD机试真题 C++ 实现【带传送阵的矩阵游离】【2023 Q2 | 200分】 - 2

            所有题目均有五种语言实现。C实现目录、C++实现目录、Python实现目录、Java实现目录、JavaScript实现目录题目n行m列的矩阵,每个位置上有一个元素你可以上下左右行走,代价是前后两个位置元素值差的绝对值.另外,你最多可以使用一次传送阵(只能从一个数跳到另外一个相同的数)求从走上角走到右下角最少需要多少时间。输入描述:第一行两个整数n,m,分别代表矩阵的行和列。后面n行,每行m个整数,分别代表矩阵中的元素。输出描述:一个整数,表示最少需要多少时间。

  3. 欧拉角表示的姿态矩阵(313和312转序) - 2

    一、习惯约定图片来自PSINS(高精度捷联惯导算法)PSINS工具箱入门与详解.pptx二、基本旋转矩阵绕x轴逆时钟旋转α\alphaα角度Rx(α)=[ 1000cos⁡αsin⁡α0−sin⁡αcos⁡α]R_x(\alpha)=\begin{bmatrix}\1&0&0\\0&\cos\alpha&\sin\alpha\\0&-\sin\alpha&\cos\alpha\end{bmatrix}Rx​(α)=​ 100​0cosα−sinα​0sinαcosα​​绕y轴逆时钟旋转α\alphaα角度Ry(α)=[ cos⁡α0−sin⁡α010sin⁡α0cos⁡α]R_y(\alpha

  4. 欧拉角、旋转矩阵及四元数 - 2

    欧拉角、旋转矩阵及四元数1.简介2.欧拉角2.1欧拉角定义2.2右手系和左手系2.3转换流程3.旋转矩阵4.四元数4.1四元数与欧拉角和旋转矩阵之间等效变换4.2测试Matlab代码5.总结1.简介常用姿态参数表达方式包括方向余弦矩阵、欧拉轴/角参数、欧拉角、四元数以及罗德里格参数等。高分辨率光学遥感卫星主要采用欧拉角与四元数对姿态参数进行描述。这里着重讲解欧拉角、旋转矩阵和四元数。2.欧拉角2.1欧拉角定义欧拉角是表征刚体旋转的一种方法之一,由莱昂哈德·欧拉引入的三个角度,用于描述刚体相对于固定坐标系的方向。在摄影测量、空间科学或其它技术领域,一般用一组(三个)欧拉角描述两个空间坐标之间的旋

  5. ruby - 如何修改矩阵(Ruby std-lib Matrix 类)? - 2

    我理解RubystdlibMatrix是不可修改的,也就是说,例如。m=Matrix.zero(3,4)不会写m[0,1]=7但我非常想做...我可以用笨拙的编程来做,比如defmodify_value_in_a_matrix(matrix,row,col,newval)ary=(0...m.row_size).map{|i|m.rowi}.map(&:to_a)ary[row][col]=newvalMatrix[*ary]end...或者作弊,比如Matrix.send:[]=,0,1,7但我想知道,这一定是人们一直遇到的问题。有没有一些标准的、习惯的方法可以做到这一点,而不必使用

  6. 线性代数让我想想:快速求三阶矩阵的逆矩阵 - 2

    快速求三阶矩阵的逆矩阵前言一般情况下,我们求解伴随矩阵是要注意符号问题和位置问题的(如下所示)A−1=1[  ][−[  ]−[  ]−[  ]  −[  ]]=A−1=1[  ][   M11−[M12]   M13−[M21]   M22−[M23]     M31−[M32]   M33]⊤\begin{aligned}&A^{-1}=\frac{1}{[\\]}\left[\begin{array}{cccccc}&-[\\]&\\-[\\]&&-[\\]\\\\&-[\\]&\\\end{array}\right]=\\\\&A^{-1}=\frac{1}{[\\]}\left[\b

  7. 相机校准—外参矩阵 - 2

    在本文中,我们将探讨摄影机的外参,并通过Python中的一个实践示例来加强我们的理解。相机外参摄像头可以位于世界任何地方,并且可以指向任何方向。我们想从摄像机的角度来观察世界上的物体,这种从世界坐标系到摄像机坐标系的转换被称为摄像机外参。那么,我们怎样才能找到相机外参呢?一旦我们弄清楚相机是如何变换的,我们就可以找到从世界坐标系到相机坐标系的基变换的变化。我们将详细探讨这个想法。具体来说,我们需要知道相机是如何定位的,以及它在世界空间中的位置,有两种转换可以帮助我们:有助于确定摄影机方向的旋转变换。有助于移动相机的平移变换。让我们详细看看每一个。旋转通过旋转改变坐标让我们看一下将点旋转一个角度

  8. ruby - Ruby 中的有限矩阵 - 2

    为什么Matrix类没有方法来编辑它的向量和组件?似乎矩阵中的所有内容都可以读取但不能写入。我错了吗?是否有一些类似于Matrix的第三方优雅类允许我删除行并有意地编辑它们?如果没有这样的类(class),请通知我——我将停止搜索。 最佳答案 Matrix类的设计者一定是不可变数据结构和函数式编程的爱好者。是的,你是对的。无论如何,总有一个简单的解决方案可以满足您的需求。使用Matrix它可以做的事情,然后,只需使用.to_a来获得一个真正的数组。>>Matrix.identity(2).to_a=>[[1,0],[0,1]]另见N

  9. ruby - 在 Ruby 中打印可读矩阵 - 2

    在Ruby中是否有内置的打印可读矩阵的方法?例如require'matrix'm1=Matrix[[1,2],[3,4]]printm1让它显示=>1234在REPL中代替:=>Matrix[[1,2][3,4]]matrix的Ruby文档让它看起来像应该显示的那样,但这不是我所看到的。我知道编写一个函数来执行此操作是微不足道的,但如果有“正确”的方法,我宁愿学习! 最佳答案 您可以将其转换为数组:m1.to_a.each{|r|putsr.inspect}=>[1,2][3,4]编辑:这是一个“无积分”版本:putsm1.to_a

  10. matlab中矩阵点乘和乘的区别(超级简单) - 2

    matlab中矩阵点乘和乘的区别MATLAB中,一、矩阵相乘:表示两个矩阵相乘。二、矩阵点乘:表示矩阵中对应位置的元素分别相乘。三、举例3.1矩阵相乘3.2矩阵点乘MATLAB中,一、矩阵相乘:表示两个矩阵相乘。前提条件:满足矩阵相乘的规则,即前矩阵的列数等于后矩阵的行数。二、矩阵点乘:表示矩阵中对应位置的元素分别相乘。前提条件:满足矩阵点乘的规则,即前后矩阵维度相同。三、举例3.1矩阵相乘Example1:A=[123;456]A=123456>>B=[1;2;3]B=123>>C=A*BC=1432这时如果用点乘就会报错Example2:>>A=[123;456;789]A=1234567

随机推荐