Skip to article frontmatterSkip to article content

Matrices

The matrix has you...

Matrices (Arreglos Bidimensionales)

En el ámbito de la programación, una matriz se define como una estructura de datos que facilita el almacenamiento de un conjunto homogéneo de elementos, organizados en una disposición bidimensional de filas y columnas. En el lenguaje de programación C, esta abstracción se materializa mediante la implementación de arreglos bidimensionales (2D), los cuales pueden ser conceptualizados como arreglos cuyos elementos son, a su vez, otros arreglos.

Las matrices son fundamentales en numerosas aplicaciones: desde operaciones matemáticas básicas hasta algoritmos complejos de procesamiento de imágenes, simulaciones físicas, análisis de datos, representación de grafos, implementación de juegos como el tres en raya o ajedrez, y sistemas de coordenadas bidimensionales. Su comprensión es esencial para el desarrollo de software eficiente y estructurado.

Relación con el álgebra lineal

Las matrices en programación están íntimamente relacionadas con el concepto matemático de matriz del álgebra lineal. Esto permite aplicar directamente teoremas y algoritmos matemáticos en implementaciones de software, especialmente en campos como gráficos por computadora, machine learning, y simulaciones científicas.

Extensión a múltiples dimensiones

Técnicamente, no están limitadas a dos dimensiones. Podés tener arreglos tridimensionales (int cubo[3][4][5]) o de mayor dimensionalidad. Sin embargo, las aplicaciones prácticas se vuelven menos claras y la complejidad de manejo aumenta considerablemente. Todos los conceptos presentados aquí se extienden naturalmente a estas dimensiones superiores.

Declaración

La declaración de una matriz requiere la especificación del tipo de dato de sus elementos, un identificador único y las dimensiones correspondientes al número de filas y columnas.

Sintaxis

tipo_dato nombre_matriz[CANTIDAD_FILAS][CANTIDAD_COLUMNAS];

Ejemplo

int miMatriz[3][4]; // Matriz de 3 filas y 4 columnas
Representación de una matriz bidimensional en memoria. Los elementos se
almacenan de forma contigua siguiendo el orden row-major, donde cada fila se
almacena completa antes de pasar a la siguiente.

Figure 1:Representación de una matriz bidimensional en memoria. Los elementos se almacenan de forma contigua siguiendo el orden row-major, donde cada fila se almacena completa antes de pasar a la siguiente.

Inicialización

Podemos inicializar nuestras matrices, esencialmente, de dos formas diferentes, con un inicializador como con los arreglos, o con código.

Tres métodos de inicialización de matrices: con inicializador completo, con
declaración implícita de la primera dimensión, y programáticamente mediante
lazos.

Figure 2:Tres métodos de inicialización de matrices: con inicializador completo, con declaración implícita de la primera dimensión, y programáticamente mediante lazos.

Inicialización completa

Este proceso se realiza mediante el uso de llaves anidadas, donde cada conjunto de llaves interno corresponde a una fila de la matriz.

1
2
3
4
int matriz[2][3] = {
    {1, 2, 3},  // Fila 0
    {4, 5, 6}   // Fila 1
};

Inicialización con declaración implícita

En C, es posible omitir la primera dimensión (filas) durante la inicialización, pero todas las dimensiones subsecuentes deben ser especificadas explícitamente. Esto se debe a que el compilador necesita conocer el tamaño de cada “sub-arreglo” para calcular las posiciones de memoria.

1
2
3
4
5
// Válido: el compilador infiere 2 filas basándose en el inicializador.
int matriz[][3] = {
    {1, 2, 3},  // Fila 0
    {4, 5, 6}   // Fila 1
};

La forma int matriz[][] es inválida y no compilará, ya que el compilador no tendría forma de saber dónde termina una fila y empieza la siguiente.

Inicialización manual

Constituye un método más flexible y programático. El uso de macros en mayúsculas para las dimensiones (Regla 0x002Fh: Las constantes (const o #define) deben nombrarse en MAYUSCULAS_SNAKE_CASE) y de size_t para los índices (Regla 0x002Eh: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t) son buenas prácticas que mejoran la legibilidad y portabilidad.

#define FILAS 3
#define COLUMNAS 4

int matriz[FILAS][COLUMNAS];
for (size_t i = 0; i < FILAS; i++) {
    for (size_t j = 0; j < COLUMNAS; j++) {
        matriz[i][j] = i * 10 + j;
    }
}

Asignación de valores mediante lazo anidados

Acceso a los Elementos

El acceso a un elemento específico de la matriz se realiza mediante la especificación de sus índices de fila y columna, los cuales son de base cero.

Sintaxis

nombre_matriz[indice_fila][indice_columna];

Ejemplo de L-Value y R-Value

matriz[0][1] = 100; // Asigna 100 al elemento en la fila 0, columna 1.
int valor = matriz[2][3]; // Toma el valor del elemento en la fila 2, columna 3.

Patrones de Recorrido de Matrices

El procesamiento sistemático de todos los elementos de una matriz requiere el uso de lazos anidados. La comprensión de los diferentes patrones de acceso es crucial tanto para la corrección del algoritmo como para el rendimiento del programa.

Recorrido por Filas (Row-Major)

El patrón más común y eficiente es el recorrido por filas, donde se accede a todos los elementos de una fila antes de pasar a la siguiente. Este patrón aprovecha la localidad espacial y optimiza el uso de la memoria caché (Regla 0x0000h: La claridad y prolijidad son de máxima importancia).

1
2
3
4
5
6
7
8
// Lazo externo: filas (i)
for (size_t i = 0; i < FILAS; i++) {
    // Lazo interno: columnas (j)
    for (size_t j = 0; j < COLUMNAS; j++) {
        printf("%d ", matriz[i][j]);
    }
    printf("\n"); // Salto de línea al final de cada fila
}

Recorrido fila por fila - patrón recomendado

Recorrido por Columnas (Column-Major)

En ocasiones específicas, puede ser necesario procesar los elementos columna por columna. Este patrón es menos eficiente en términos de caché, pero puede ser requerido por la lógica del algoritmo.

// Lazo externo: columnas (j)
for (size_t j = 0; j < COLUMNAS; j++) {
    // Lazo interno: filas (i)
    for (size_t i = 0; i < FILAS; i++) {
        printf("%d ", matriz[i][j]);
    }
    printf("\n"); // Nueva línea al final de cada columna
}

Recorrido columna por columna

Comparación entre el recorrido por filas (row-major) y por columnas
(column-major). El recorrido por filas accede a elementos contiguos en memoria,
aprovechando la caché. El recorrido por columnas genera saltos en memoria,
causando más fallos de caché.

Figure 3:Comparación entre el recorrido por filas (row-major) y por columnas (column-major). El recorrido por filas accede a elementos contiguos en memoria, aprovechando la caché. El recorrido por columnas genera saltos en memoria, causando más fallos de caché.

Recorrido Diagonal

Para matrices cuadradas, es común necesitar acceder a las diagonales.

#define DIM 4
int matriz_cuadrada[DIM][DIM];

// Diagonal principal (i == j)
printf("Diagonal principal: ");
for (size_t i = 0; i < DIM; i++) {
    printf("%d ", matriz_cuadrada[i][i]);
}
printf("\n");

// Diagonal secundaria (i + j == DIM - 1)
printf("Diagonal secundaria: ");
for (size_t i = 0; i < DIM; i++) {
    printf("%d ", matriz_cuadrada[i][DIM - 1 - i]);
}
printf("\n");

Acceso a diagonal principal y secundaria

Las diagonales principal y secundaria en una matriz cuadrada. La diagonal
principal cumple la condición i == j, mientras que la secundaria cumple
i + j == DIM - 1.

Figure 4:Las diagonales principal y secundaria en una matriz cuadrada. La diagonal principal cumple la condición i == j, mientras que la secundaria cumple i + j == DIM - 1.

Pasando matrices a funciones (Método Clásico)

Al pasar una matriz como argumento a una función, el estándar de C requiere que se especifiquen explícitamente todas las dimensiones, a excepción de la primera. Esto es necesario para que el compilador pueda calcular el desplazamiento en memoria de cada elemento.

1
2
3
4
5
6
7
8
9
10
11
#define COLUMNAS 4

// Es crucial pasar las dimensiones para cumplir con la regla {ref}`0x0027h`.
void imprimir_matriz(int mat[][COLUMNAS], size_t filas, size_t columnas) {
    for (size_t i = 0; i < filas; i++) {
        for (size_t j = 0; j < columnas; j++) {
           printf("%d\t", mat[i][j]);
        }
        printf("\n");
    }
}

¿Acaso las columnas no están ya en el macro COLUMNAS? Para garantizar la consistencia y minimizar los efectos secundarios en las funciones que operan con matrices, es fundamental ser explícito con el contexto en el que deben trabajar.

En este caso, las columnas como argumento evita que nuestro código dependa de un valor que es esencialmente externo a la misma y aunque funciona perfectamente sin él, en el momento en que veamos memoria dinámica, el código no funcionará sin mayores cambios.

Cálculo de Desplazamiento de Memoria

Dicha información es indispensable para que el compilador pueda calcular correctamente el desplazamiento de memoria necesario para localizar cualquier elemento matriz[i][j], utilizando una fórmula análoga a:

direccionbase+(i×COLUMNAS+j)×sizeof(int)\text{direccionbase} + (i \times \text{COLUMNAS} + j) \times \text{sizeof(int)}

Pasando matrices a funciones (Método ALV)

La utilización de ALV’s facilita la creación de funciones genéricas capaces de operar sobre matrices de dimensiones arbitrarias.

// Correcto: filas y cols se conocen antes de que el compilador procese matriz[filas][cols]
void procesar_matriz(size_t filas, size_t cols, int matriz[filas][cols]) {
    printf("\nProcesando matriz de %zu x %zu\n", filas, cols);
    for (size_t i = 0; i < filas; i++) {
        for (size_t j = 0; j < cols; j++) {
            matriz[i][j] *= 2; // Ejemplo: duplicar cada valor
        }
    }
}

Matrices Multidimensionales

El lenguaje C no impone un límite de dos dimensiones para los arreglos; es posible declarar arreglos multidimensionales. Un arreglo tridimensional, por ejemplo, puede conceptualizarse como un cubo de datos.

Arreglo tridimensional representado como capas de matrices bidimensionales. Cada
capa contiene una matriz completa, y el acceso requiere tres índices: capa,
fila y columna.

Figure 5:Arreglo tridimensional representado como capas de matrices bidimensionales. Cada capa contiene una matriz completa, y el acceso requiere tres índices: capa, fila y columna.

// Arreglo tridimensional: 2 capas, 3 filas, y 4 columnas.
int cubo[2][3][4];

// Acceso a un elemento
cubo[1][0][2] = 99;

// Recorrido con tres lazos anidados
for (size_t i = 0; i < 2; i++) {       // Capas
    for (size_t j = 0; j < 3; j++) {   // Filas
        for (size_t k = 0; k < 4; k++) { // Columnas
            cubo[i][j][k] = i + j + k;
        }
    }
}

Declaración y recorrido de un arreglo 3D

Consideraciones de Rendimiento: Localidad de Caché

El método empleado para iterar sobre los elementos de una matriz posee un impacto significativo sobre el rendimiento computacional. Este fenómeno se atribuye a la arquitectura de memoria jerárquica de los procesadores modernos, los cuales utilizan una memoria caché de alta velocidad como intermediaria entre la CPU y la memoria principal (RAM).

Impacto del orden de acceso en el rendimiento. El acceso row-major aprovecha la
localidad espacial, cargando elementos contiguos en la caché. El acceso
column-major genera saltos que causan múltiples fallos de caché.

Figure 6:Impacto del orden de acceso en el rendimiento. El acceso row-major aprovecha la localidad espacial, cargando elementos contiguos en la caché. El acceso column-major genera saltos que causan múltiples fallos de caché.

Recorrido Óptimo (Cache-Friendly)

La iteración por filas en el lazo externo y por columnas en el interno produce un patrón de acceso secuencial a la memoria. Esto maximiza la probabilidad de aciertos de caché (cache hits), ya que al solicitar un elemento, un bloque contiguo de memoria que incluye los elementos subsiguientes de la misma fila es transferido a la caché.

1
2
3
4
5
6
// ALTO RENDIMIENTO: Aprovecha la localidad de datos espaciales.
for (size_t i = 0; i < FILAS; i++) {
    for (size_t j = 0; j < COLUMNAS; j++) {
        suma += matriz[i][j];
    }
}

Recorrido Ineficiente (Cache-Unfriendly)

La inversión de los lazos de iteración genera un patrón de acceso no contiguo a la memoria. Cada acceso salta a una dirección de memoria distante, lo que reduce la eficacia de la caché y provoca constantes fallos de caché (cache misses). Cada fallo obliga a la CPU a esperar la recuperación de datos desde la lenta memoria principal, degradando sustancialmente el rendimiento.

1
2
3
4
5
6
// BAJO RENDIMIENTO: Genera fallos de caché frecuentes.
for (size_t j = 0; j < COLUMNAS; j++) {
    for (size_t i = 0; i < FILAS; i++) {
        suma += matriz[i][j];
    }
}

Operaciones Matemáticas con Matrices

En el ámbito de la programación en CC y otras áreas de la computación, el manejo de matrices es fundamental. AA continuación, te presento los algoritmos y las expresiones matemáticas para las operaciones básicas entre matrices.

Operaciones básicas con matrices: suma, resta y transposición. Cada operación
requiere validar que las dimensiones sean compatibles antes de proceder.

Figure 7:Operaciones básicas con matrices: suma, resta y transposición. Cada operación requiere validar que las dimensiones sean compatibles antes de proceder.

Suma de Matrices

La suma de dos matrices, AA y BB, de las mismas dimensiones (m x n), da como resultado una matriz CC de la misma dimensión. Cada elemento de CC es la suma de los elementos correspondientes en AA y BB.

Expresión Matemática

Para dos matrices AA y BB de tamaño 𝑚×𝑛𝑚×𝑛, la matriz resultante CC se define como:

Ci,j=Ai,j+Bi,jC_{i,j} = A_{i,j} + B_{i,j}

donde 𝑖𝑖 representa la fila y 𝑗𝑗 la columna.

Expansión Matemática

Visualmente, la suma de dos matrices de 2x2 se vería así:

(A1,1A1,2A2,1A2,2)+(B1,1B1,2B2,1B2,2)=(A1,1+B1,1A1,2+B1,2A2,1+B2,1A2,2+B2,2)\begin{pmatrix} A_{1,1} & A_{1,2} \\ A_{2,1} & A_{2,2} \end{pmatrix} + \begin{pmatrix} B_{1,1} & B_{1,2} \\ B_{2,1} & B_{2,2} \end{pmatrix} = \begin{pmatrix} A_{1,1} + B_{1,1} & A_{1,2} + B_{1,2} \\ A_{2,1} + B_{2,1} & A_{2,2} + B_{2,2} \end{pmatrix}

Algoritmo en Pseudocódigo

El algoritmo recorre ambas matrices y suma los elementos en la misma posición.

1
2
3
4
5
6
7
8
9
10
11
12
FUNCIÓN sumar_matrices(A, B, m, n)
  // A y B son matrices de dimensión m x n
  CREAR matriz C de tamaño m x n

  PARA i DESDE 0 HASTA m - 1
    PARA j DESDE 0 HASTA n - 1
      C[i][j] = A[i][j] + B[i][j]
    FIN PARA
  FIN PARA

  RETORNAR C
FIN FUNCIÓN

Algoritmo para la suma de dos matrices AA y BB.


Resta de Matrices

De manera análoga a la suma, la resta de dos matrices AA y BB de idénticas dimensiones resulta en una matriz CC donde cada elemento es la diferencia de los elementos correspondientes.

Expresión Matemática

Para dos matrices AA y BB de tamaño 𝑚×𝑛𝑚×𝑛, la matriz resultante CC se define como:

Ci,j=Ai,jBi,jC_{i,j} = A_{i,j} - B_{i,j}

Algoritmo en Pseudocódigo

El procedimiento es idéntico al de la suma, pero se realiza una resta.

1
2
3
4
5
6
7
8
9
10
11
12
FUNCIÓN restar_matrices(A, B, m, n)
  // A y B son matrices de dimensión m x n
  CREAR matriz C de tamaño m x n

  PARA i DESDE 0 HASTA m - 1
    PARA j DESDE 0 HASTA n - 1
      C[i][j] = A[i][j] - B[i][j]
    FIN PARA
  FIN PARA

  RETORNAR C
FIN FUNCIÓN

Algoritmo para la resta de dos matrices AA y BB.


Multiplicación de Matrices

La multiplicación de una matriz AA de dimensión 𝑚×𝑝𝑚×𝑝 por una matriz BB de dimensión 𝑝×𝑛𝑝×𝑛 produce una matriz CC de dimensión 𝑚×𝑛𝑚×𝑛. Es crucial que el número de columnas de AA sea igual al número de filas de BB.

Proceso de multiplicación de matrices. Cada elemento C[i][j] se calcula como el
producto escalar de la fila i de A con la columna j de B, sumando los productos
elemento por elemento.

Figure 8:Proceso de multiplicación de matrices. Cada elemento C[i][j] se calcula como el producto escalar de la fila i de A con la columna j de B, sumando los productos elemento por elemento.

Expresión Matemática

El elemento (𝑖,𝑗)(𝑖,𝑗) de la matriz resultante CC se calcula como la suma de los productos de los elementos de la fila 𝑖 de AA por los elementos de la columna 𝑗 de BB.

Ci,j=k=1pAi,kBk,jC*{i,j} = \sum*{k=1}^{p} A*{i,k} \cdot B*{k,j}

Expansión Matemática

Cada elemento Ci,jC_{i,j} de la matriz resultante se calcula realizando el producto escalar del vector fila 𝑖𝑖 de la matriz AA con el vector columna 𝑗 de la matriz BB.

Dadas las matrices:

A=(A1,1A1,pAi,1Ai,pAm,1Am,p)B=(B1,1B1,jB1,nBp,1Bp,jBp,n)A = \begin{pmatrix} A*{1,1} & \cdots & A*{1,p} \\ \vdots & \ddots & \vdots \\ \color{blue}A*{i,1} & \color{blue}\cdots & \color{blue}A*{i,p} \\ \vdots & \ddots & \vdots \\ A*{m,1} & \cdots & A*{m,p} \end{pmatrix} \quad B = \begin{pmatrix} B*{1,1} & \cdots & \color{red}B*{1,j} & \cdots & B*{1,n} \\ \vdots & \ddots & \color{red}\vdots & \ddots & \vdots \\ B*{p,1} & \cdots & \color{red}B*{p,j} & \cdots & B*{p,n} \end{pmatrix}

El elemento Ci,jC_{i,j} se calcula como:

Ci,j=(Ai,1B1,j)+(Ai,2B2,j)++(Ai,pBp,j)=k=1pAi,kBk,jC*{i,j} = (\color{blue}A*{i,1} \cdot \color{red}B*{1,j}) + (\color{blue}A*{i,2} \cdot \color{red}B*{2,j}) + \cdots + (\color{blue}A*{i,p} \cdot \color{red}B*{p,j}) = \sum*{k=1}^{p} A*{i,k} \cdot B*{k,j}

Por ejemplo, para calcular el elemento C1,1C_{1,1} de una multiplicación de matrices de 2x2:

(A1,1A1,2A2,1A2,2)×(B1,1B1,2B2,1B2,2)=(C1,1C1,2C2,1C2,2)\begin{pmatrix} \color{blue}A*{1,1} & \color{blue}A*{1,2} \\ A*{2,1} & A*{2,2} \end{pmatrix} \times \begin{pmatrix} \color{red}B*{1,1} & B*{1,2} \\ \color{red}B*{2,1} & B*{2,2} \end{pmatrix} = \begin{pmatrix} C*{1,1} & C*{1,2} \\ C*{2,1} & C*{2,2} \end{pmatrix}

Donde C1,1=(A1,1B1,1)+(A1,2B2,1)C_{1,1} = (\color{blue}A_{1,1} \cdot \color{red}B_{1,1}) + (\color{blue}A_{1,2} \cdot \color{red}B_{2,1}).

Algoritmo en Pseudocódigo

Este algoritmo requiere tres lazos anidados para calcular el producto escalar de cada fila de AA con cada columna de BB.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FUNCIÓN multiplicar_matrices(A, B, m, p, n)
  // A es una matriz de m x p
  // B es una matriz de p x n
  CREAR matriz C de tamaño m x n

  PARA i DESDE 0 HASTA m - 1
    PARA j DESDE 0 HASTA n - 1
      suma = 0
      PARA k DESDE 0 HASTA p - 1
        suma = suma + (A[i][k] * B[k][j])
      FIN PARA
      C[i][j] = suma
    FIN PARA
  FIN PARA

  RETORNAR C
FIN FUNCIÓN

Algoritmo para la multiplicación de una matriz A (m x p) por una matriz B (p x n).

Cálculo de Determinantes

El determinante es un valor escalar que se puede calcular para toda matriz cuadrada. Este valor encapsula propiedades importantes de la matriz, como la invertibilidad. Se denota como det(A)det(A) o A|A|.

Definición Matemática

Para una matriz de 2x2, el cálculo es directo:

det(A)=abcd=adbc\det(A) = \begin{vmatrix} a & b \\ c & d \end{vmatrix} = ad - bc

Para matrices de mayor tamaño (nxn)(n x n), un método común es la expansión por cofactores. El determinante se calcula expandiendo a lo largo de una fila o columna. Usando la primera fila, la fórmula es:

det(A)=j=1n(1)1+jA1,jdet(M_1,j)\det(A) = \sum*{j=1}^{n} (-1)^{1+j} \cdot A*{1,j} \cdot \det(M\_{1,j})

Donde:

Algoritmo Recursivo (Basado en Cofactores)

Este método matemático se traduce de forma natural en un algoritmo recursivo. La idea es reducir el problema de un determinante nxnn x n al cálculo de varios determinantes (n1)x(n1)(n-1) x (n-1), hasta llegar al caso base de una matriz 2x2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
FUNCIÓN calcular_determinante(A, n)
  // A es una matriz cuadrada de dimensión n x n

  SI n == 1 ENTONCES
    RETORNAR A[0][0]
  FIN SI

  SI n == 2 ENTONCES
    RETORNAR (A[0][0] * A[1][1]) - (A[0][1] * A[1][0])
  FIN SI

  determinante_total = 0
  PARA j_actual DESDE 0 HASTA n - 1
    // 1. Crear la submatriz (menor) M
    CREAR submatriz M de tamaño (n-1) x (n-1)
    PARA i DESDE 1 HASTA n - 1
      col_sub = 0
      PARA j DESDE 0 HASTA n - 1
        SI j != j_actual ENTONCES
          M[i-1][col_sub] = A[i][j]
          col_sub = col_sub + 1
        FIN SI
      FIN PARA
    FIN PARA

    // 2. Calcular el signo del cofactor
    signo = (-1)^j_actual // o potencia( -1, j_actual)

    // 3. Suma recursiva
    sub_determinante = calcular_determinante(M, n-1)
    determinante_total = determinante_total + (signo * A[0][j_actual] * sub_determinante)
  FIN PARA

  RETORNAR determinante_total
FIN FUNCIÓN

Algoritmo recursivo para el cálculo del determinante.


Inversión de Matrices

La inversa de una matriz cuadrada AA, denotada como A1A^{-1}, es aquella matriz que al multiplicarla por AA da como resultado la matriz identidad II.

AA1=A1A=IA \cdot A^{-1} = A^{-1} \cdot A = I

Condiciones para la Inversión

Una matriz es invertible si y solo si cumple dos condiciones:

  1. Es una matriz cuadrada.

  2. Su determinante es distinto de cero. AA las matrices con determinante cero se las llama singulares y no tienen inversa.

Método de la Matriz Adjunta

Un método para encontrar la inversa se basa en el determinante y la matriz adjunta. La fórmula es:

A1=1det(A)adj(A)A^{-1} = \frac{1}{\det(A)} \cdot \text{adj}(A)

Donde adj(A)adj(A) es la matriz adjunta de AA, que se define como la transpuesta de la matriz de cofactores de AA.

Algoritmo (Basado en la Adjunta)

El algoritmo consiste en seguir los pasos de la fórmula matemática.

  1. Calcular el determinante: Si es cero, la matriz no es invertible.

  2. Calcular la matriz de cofactores: Para cada elemento Ai,jA_{i,j}, su cofactor es (1)i+jdet(Mi,j)(-1)^{i+j} \det(M_{i,j}).

  3. Calcular la matriz adjunta: Transponer la matriz de cofactores.

  4. Obtener la inversa: Multiplicar la matriz adjunta por el escalar 1/det(A)1 / \det(A).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
FUNCIÓN invertir_matriz(A, n)
  // 1. Calcular determinante
  determinante = calcular_determinante(A, n)
  SI determinante == 0 ENTONCES
    RETORNAR ERROR "La matriz es singular y no se puede invertir."
  FIN SI

  // 2. Calcular la matriz de cofactores
  CREAR matriz_cofactores de tamaño n x n
  PARA i DESDE 0 HASTA n - 1
    PARA j DESDE 0 HASTA n - 1
      // a. Crear la submatriz menor M(i,j)
      CREAR submatriz M de (n-1) x (n-1) omitiendo fila i y columna j de A

      // b. Calcular el signo y el determinante del menor
      signo = (-1)^(i+j)
      det_menor = calcular_determinante(M, n-1)

      matriz_cofactores[i][j] = signo * det_menor
    FIN PARA
  FIN PARA

  // 3. Calcular la matriz adjunta (transpuesta de la de cofactores)
  CREAR matriz_adjunta de tamaño n x n
  PARA i DESDE 0 HASTA n - 1
    PARA j DESDE 0 HASTA n - 1
      matriz_adjunta[j][i] = matriz_cofactores[i][j]
    FIN PARA
  FIN PARA

  // 4. Calcular la inversa dividiendo la adjunta por el determinante
  CREAR matriz_inversa de tamaño n x n
  factor_inversion = 1.0 / determinante
  PARA i DESDE 0 HASTA n - 1
    PARA j DESDE 0 HASTA n - 1
      matriz_inversa[i][j] = matriz_adjunta[i][j] * factor_inversion
    FIN PARA
  FIN PARA

  RETORNAR matriz_inversa
FIN FUNCIÓN

Algoritmo para la inversión de una matriz A.

Validación y Manejo de Errores

En aplicaciones robustas, es fundamental implementar validaciones para prevenir accesos fuera de límites y operaciones inválidas. Esto es especialmente crítico en C, donde no existe verificación automática de límites (Regla 0x0027h: Verificá siempre los límites de los arreglos antes de acceder a sus elementos).

Validación de dimensiones para operaciones con matrices. La suma y resta
requieren dimensiones idénticas, mientras que la multiplicación requiere que las
columnas de A sean igual a las filas de B.

Figure 9:Validación de dimensiones para operaciones con matrices. La suma y resta requieren dimensiones idénticas, mientras que la multiplicación requiere que las columnas de A sean igual a las filas de B.

Validación de Índices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdbool.h>

bool indice_valido(int fila, int columna,
                   size_t max_filas, size_t max_columnas) {
    return (fila >= 0 && fila < max_filas &&
            columna >= 0 && columna < max_columnas);
}

int acceso_seguro_matriz(int matriz[][MAX_COLUMNAS],
                        int fila, int columna,
                        int filas, int columnas) {
    if (!indice_valido(fila, columna, filas, columnas)) {
        fprintf(stderr, "Error: Índices fuera de límites (%d, %d)\n",
                fila, columna);
        return -1; // Valor de error
    }

    return matriz[fila][columna];
}

Función para validar acceso seguro a matriz

Validación de Operaciones

Para operaciones matemáticas entre matrices, debemos verificar la compatibilidad de dimensiones antes de proceder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef enum {
    MATRIZ_OK,
    MATRIZ_ERROR_DIMENSIONES,
    MATRIZ_ERROR_MEMORIA,
    MATRIZ_ERROR_SINGULAR
} resultado_matriz_t;

resultado_matriz_t validar_suma(int filas_a, int columnas_a,
                               int filas_b, int columnas_b) {
    if (filas_a != filas_b || columnas_a != columnas_b) {
        return MATRIZ_ERROR_DIMENSIONES;
    }
    return MATRIZ_OK;
}

resultado_matriz_t validar_multiplicacion(int filas_a, int columnas_a,
                                        int filas_b, int columnas_b) {
    if (columnas_a != filas_b) {
        return MATRIZ_ERROR_DIMENSIONES;
    }
    return MATRIZ_OK;
}

Validación para operaciones con matrices

Mejores Prácticas y Optimizaciones

Uso de Macros para Dimensiones

Utilizá siempre macros para definir las dimensiones de tus matrices, siguiendo la regla de estilo Regla 0x002Fh: Las constantes (const o #define) deben nombrarse en MAYUSCULAS_SNAKE_CASE. Esto facilita el mantenimiento y la modificación del código.

#define MAX_FILAS 100
#define MAX_COLUMNAS 100

int matriz[MAX_FILAS][MAX_COLUMNAS];

Definición de dimensiones con macros

Optimización de Acceso a Memoria

Para matrices grandes, considerá el orden de acceso para optimizar el uso de la caché. El patrón row-major es generalmente más eficiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Preferible - acceso secuencial por filas
for (size_t i = 0; i < FILAS; i++) {
    for (size_t j = 0; j < COLUMNAS; j++) {
        procesar_elemento(matriz[i][j]);
    }
}

// Menos eficiente - acceso por columnas
// Usar solo cuando sea necesario por la lógica del algoritmo
for (size_t j = 0; j < COLUMNAS; j++) {
    for (size_t i = 0; i < FILAS; i++) {
        procesar_elemento(matriz[i][j]);
    }
}

Optimización para el acceso a memoria

Funciones Auxiliares

Creá funciones auxiliares para operaciones comunes, siguiendo la regla de claridad Regla 0x0000h: La claridad y prolijidad son de máxima importancia:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void imprimir_matriz(int matriz[][MAX_COLS], size_t filas, size_t columnas) {
    for (size_t i = 0; i < filas; i++) {
        for (size_t j = 0; j < columnas; j++) {
            printf("%4d ", matriz[i][j]);
        }
        printf("\n");
    }
}

void inicializar_con_ceros(int matriz[][MAX_COLS], size_t filas, size_t columnas) {
    for (size_t i = 0; i < filas; i++) {
        for (size_t j = 0; j < columnas; j++) {
            matriz[i][j] = 0;
        }
    }
}

bool son_matrices_iguales(int a[][MAX_COLS], int b[][MAX_COLS],
                         int filas, int columnas) {
    for (size_t i = 0; i < filas; i++) {
        for (size_t j = 0; j < columnas; j++) {
            if (a[i][j] != b[i][j]) {
                return false;
            }
        }
    }
    return true;
}

Funciones auxiliares para matrices

Glosario

memoria caché

Una memoria caché (del francés cacher, “esconder”) es un componente de hardware o software que almacena datos para que las futuras solicitudes de esos datos puedan ser atendidas más rápidamente. Se trata de una memoria auxiliar, de alta velocidad y menor capacidad, situada entre la unidad central de procesamiento (CPU) y la memoria de acceso aleatorio (RAM).

El objetivo principal de una caché es acelerar el acceso a los datos que se utilizan con mayor frecuencia. Cuando la CPU necesita leer o escribir datos, primero busca en la caché. Si los datos se encuentran allí (lo que se conoce como un acierto de caché o cache hit), se accede a ellos de forma casi inmediata, evitando el acceso mucho más lento a la memoria principal. Si los datos no están en la caché (fallo de caché o cache miss), se deben recuperar de la RAM y, por lo general, se copian en la caché para futuros accesos.

Existen diferentes niveles de caché (L1, L2, L3), que se diferencian por su tamaño, velocidad y proximidad a los núcleos de la CPU. La caché L1 es la más pequeña y rápida, mientras que la L3 es la más grande y lenta de las tres.

¿Pero por que no todo es memoria caché? La relación costo capacidad. Las memorias mas cercanas al procesador y las mas rápidas, son las mas caras, tengan en cuenta que una computadora moderna tiene algunos kilobytes de memoria L1 y unos pocos megabytes en L3.

localidad espacial

Un principio fundamental en el diseño de sistemas de memoria que establece que si un programa accede a una ubicación de memoria, es muy probable que también acceda a ubicaciones cercanas en un futuro próximo. Este principio es especialmente relevante para las matrices almacenadas en row-major order, donde los elementos de una fila son adyacentes en memoria. Al acceder secuencialmente por filas, se aprovecha esta localidad y se optimiza el uso de la caché.

localidad temporal

Principio que indica que si un programa accede a una ubicación de memoria, es probable que vuelva a acceder a la misma ubicación en un futuro cercano. En el contexto de matrices, esto se aprovecha cuando se realizan múltiples operaciones sobre los mismos elementos o cuando se recorren matrices varias veces con diferentes propósitos.

row-major order

Método de almacenamiento de matrices en memoria donde los elementos se disponen fila por fila de forma consecutiva. En una matriz de 3×4, los elementos se almacenan como: [matriz[0][0], matriz[0][1], matriz[0][2], matriz[0][3], matriz[1][0], ...]. Este es el orden usado por C, C++, Python (NumPy) y Java, entre otros.

column-major order

Método alternativo de almacenamiento donde los elementos se almacenan columna por columna. Usado por lenguajes como Fortran y MATLAB. En una matriz de 3×4, el orden sería: [matriz[0][0], matriz[1][0], matriz[2][0], matriz[0][1], ...]. Es importante conocer este concepto al interoperar con código de otros lenguajes.

matriz singular

Una matriz cuadrada cuyo determinante es igual a cero. Las matrices singulares no tienen inversa y representan transformaciones que “colapsan” el espacio, reduciendo su dimensionalidad. En términos geométricos, una matriz singular proyecta vectores de dimensión n en un subespacio de menor dimensión.

determinante

Un valor escalar que se puede calcular para cualquier matriz cuadrada. Proporciona información importante sobre las propiedades de la matriz: si es cero, la matriz es singular; si es positivo o negativo, indica orientación; y su magnitud representa el factor de escalamiento del volumen en transformaciones lineales.

matriz identidad

Una matriz cuadrada especial donde todos los elementos de la diagonal principal son 1 y todos los demás elementos son 0. Actúa como el elemento neutro en la multiplicación de matrices: A × I = I × A = A. Es fundamental en operaciones como la inversión de matrices.

Ejercicios

#include <stdio.h>

#define FILAS 3
#define COLUMNAS 3

int main() {
  int mi_matriz[FILAS][COLUMNAS] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

  printf("Contenido de la matriz:\n");
  for (size_t i = 0; i < FILAS; i++) {
    for (size_t j = 0; j < COLUMNAS; j++) {
      printf("%d\t", mi_matriz[i][j]);
    }
    printf("\n"); // Salto de línea al final de cada fila
  }

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

#define DIM 3

// La función recibe la matriz y su dimensión
int sumar_diagonal_principal(int matriz[][DIM], int dimension) {
  int suma = 0;
  for (int i = 0; i < dimension; i++) {
    suma = suma + matriz[i][i];
    // Accedemos solo a los elementos donde fila == columna
  }
  return suma;
}

int main() {
  int matriz_cuadrada[DIM][DIM] = {{10, 2, 3}, {4, 20, 6}, {7, 8, 30}};

  int suma = sumar_diagonal_principal(matriz_cuadrada, DIM);
  printf("La suma de la diagonal principal es: %d\n", suma);

  return 0;
}