Перейти к содержимому

Что такое матрица в программировании

  • автор:

Знакомство с матрицами

Разработчики нейросетей говорят, что все нейросети — это просто бесконечное перемножение матриц. Мы решили разобраться, что это за матрицы и как их перемножать, а для этого пришлось полезть в линейную алгебру. И это оказалось не так сложно, как мы думали:

  • Вектор — это просто группа из нескольких чисел, выстроенных в определённой последовательности. Например, рост и вес человека можно представить как вектор (172, 80). Ничего сложного.
  • У вектора может быть внутри сколько угодно чисел. Главное — чтобы мы договорились, что для нас значат эти числа, и не меняли их местами просто так, произвольно.
  • Векторы можно складывать, вычитать, умножать. Это чуть сложнее, чем с обычными числами.
  • У вектора есть понятие линейной зависимости. Грубо говоря — параллельны друг другу векторы или нет. От этого зависит, какие операции можно делать с этими векторами.

Вектор — это «кирпичик» линейной алгебры. На его основе мы переходим к понятию матрицы.

Что такое матрица

Если вектор — это строка с числами в определённом порядке, то матрица — это таблица с числами в определённом порядке. Как у любой таблицы, у матрицы есть столбцы и строки. В них сидят какие-то числа. Всё вместе — это математический объект, то есть в каких-то случаях всю эту таблицу можно рассматривать как единое целое и совершать с ним операции.

Матрицы принято обозначать большими буквами латинского алфавита вроде А, В, С, D и так далее.

Числа внутри матрицы называют элементами. Каждый элемент обозначается двумя цифрами: первая цифра указывает на строку, а вторая — на столбец. Это адрес числа внутри матрицы. Например, элемент А₂₃ означает, что нужное число находится во второй строке и третьем столбце. Нумерация элементов нужна для записи формул и устного объяснения того, где находится нужное число в матрице.

В матрице может находиться неограниченное количество строк, столбцов и элементов. Из-за этого матрицы бывают разных видов и могут обладать разными особенностями. Например, если в матрице совпадает число строк и столбцов, то такая матрица называется квадратной.

В этой статье и в следующих материалах мы будем рассматривать разные виды матрицы и постепенно изучим их особенности.

Общая схема матрицы Пример квадратной матрицы с пятью строками и столбцами

Простые операции с матрицами

Вынесение минуса за пределы матрицы. Если внутри матрицы у большинства элементов знак минус, то часто это мешает расчётам или приводит к ошибкам. Чтобы этого избежать, от минуса избавляются. Для этого нужно вынести минус за пределы матрицы и изменить знак всех элементов внутри самой матрицы.

И наоборот: если внутри матрицы у большинства элементов знак минус и перед матрицей стоит минус, то минус можно внести в матрицу.

Выносим минус за пределы матрицы и получаем вместо двадцати одного отрицательного элемента — четыре Общая схема матрицы

Умножение матрицы на число. Для умножения матрицы на число достаточно каждый элемент матрицы умножить на это число.

Пример умножения матрицы на число

Транспонирование матрицы. Это операция, которая позже нам понадобится для решения матричных уравнений. Для транспонирования мы берём известную матрицу, меняем в ней местами строки со столбцами и получаем новую матрицу. Как бы поставили матрицу набок.

⚠️ При этом в матрице запрещено в произвольном порядке менять элементы. Зато можно полностью менять местами строки или столбцы. Если мы поменяем местами первую и вторую строку, то это останется прежняя матрица.

Схема транспонирования матриц Пример транспонирования Матрицу можно перетасовывать, но это нужно делать по правилам

Сложение и вычитание матриц

Если в нескольких матрицах совпадает число строк и столбцов, то мы можем их складывать и вычитать. Для вычислений нам нужно поэлементно сложить или вычесть каждый элемент матриц: первый элемент первой матрицы складываем с первым элементом второй матрицы или вычитаем из него и так далее. В результате получаем новую матрицу.

Пример сложения двух прямоугольных матриц с тремя строками и двумя столбцами Пример вычитания двух матриц

Умножение матриц

Матрицы умножаются по принципу строка на столбец. Мы умножаем первую строку первой матрицы, на первый столбец второй матрицы, складываем результаты и получаем первый элемент новой матрицы. По аналогичной схеме вычисляем все остальные элементы. Звучит запутанно, поэтому идём по шагам:

Пошаговое умножение матрицы

  1. У нас есть две матрицы A и B. Их нужно перемножить, чтобы получить новую матрицу C.
  2. Размер матрицы A два на два: есть две строки и два столбца. Первая строка состоит из элементов А₁₁ и А₁₂; вторая — А₂₁ и А₂₂.
  3. У матрицы B такая же размерность: есть две строки и два столбца. Первая строка состоит из элементов B₁₁ и B₁₂; вторая — B₂₁ и B₂₂.
  4. У нас две одинаковые по размеру матрицы с двумя строками и столбцами. Это значит, что и матрица C будет размером два на два. Первая строка будет состоять из элементов C₁₁ и C₁₂; вторая — C₂₁ и C₂₂.
  5. Считаем элемент C₁₁. Умножаем первый элемент первой строки матрицы А (А₁₁) на первый элемент первого столбика матрицы B (B₁₁). Это первая часть, после которой ставим знак плюс. Вторая часть: умножаем второй элемент первой строчки матрицы А (А₁₂) на второй элемент первого столбика матрицы B (B₂₁). Складываем обе части и получаем первый элемент первой строки матрицы С (C₁₁).
  6. Считаем элемент C₁₂. Умножаем первый элемент первой строки матрицы А (А₁₁) на первый элемент второго столбика матрицы B (B₁₂). Это первая часть. Вторая часть: умножаем второй элемент первой строчки матрицы А (А₁₂) на второй элемент второго столбика матрицы B (B₂₂). Складываем части и получаем второй элемент первой строки матрицы С (C₁₂).
  7. Считаем элемент C₂₁. Умножаем первый элемент второй строки матрицы А (А₂₁) на первый элемент первого столбика матрицы B (B₁₁). Это первая часть. Вторая часть: умножаем второй элемент второй строки матрицы А (А₂₂) на второй элемент первого столбика матрицы B (B₂₁). Складываем части и получаем первый элемент второй строки матрицы С (C₂₁).
  8. Считаем элемент C₂₂. Умножаем первый элемент второй строки матрицы А (А₂₁) на первый элемент второго столбика матрицы B (B₁₂). Это первая часть. Вторая часть: умножаем второй элемент второй строки матрицы А (А₂₂) на второй элемент второго столбика матрицы B (B₂₂). Складываем части и получаем второй элемент второй строки матрицы С (C₂₂).

Если нам нужно найти матрицу в квадрате, то мы умножаем эту матрицу на саму себя. Если нужна матрица в кубе — умножаем её на саму себя три раза и так далее в зависимости от количества степеней. Если в одной из матриц все элементы нули, то она считается нулевой и после умножения на другую матрицу даёт нулевую матрицу — это как нуль умноженный на число всегда даёт нуль.

Формула умножения матриц Пример умножения квадратных матриц размерностью 2×2

Что дальше

В следующий раз продолжим знакомиться с базовыми понятиями, которые нам понадобятся для решения матричных уравнений. А на сегодня Нео свободен ��

Что такое матрица в программировании

Матрица представляет собой прямоугольную таблицу элементов, в качестве которых могут выступать числа, функции, символы, слова и так далее — при условии, что заданы определенные правила математических действий с этими элементами.

Примеры матриц:

, .

Матричный элемент, расположенный на пересечении i-ой строки и j-го столбца, записывается в виде ai j , а выражение A = || ai j || означает, что матрица A составлена из элементов ai j :

.

Матричная алгебра имеет обширные применения в различных отраслях знания – в математике, физике, информатике, экономике. Например, матрицы используется для решения систем алгебраических и дифференциальных уравнений, нахождения значений физических величин в квантовой теории, шифрования сообщений в Интернете.

Матрица обозначается одной из заглавных букв латинского алфавита, а набор ее элементов помещается в круглые скобки:

(1)

Представленная формулой (1) матрица A имеет m строк и n столбцов и называется m×n матрицей (“эм на эн матрицей”) или матрицей размера m×n. Строки матрицы нумеруются сверху вниз, а столбцы – слева направо.

Рис. 1. Порядок нумерации строк и столбцов матрицы.

Матричный элемент, расположенный на пересечении i-ой строки и j-го столбца, называется i,j-м элементом и записывается в виде ai j , а выражение A = || ai j || означает, что матрица A составлена из элементов ai j .

Матрица размера 1×n называется строчной или вектор-строкой.

Матрица размера n×1 называется столбцевой или вектор-столбцом. Для краткости вектор-строку и вектор-столбец обычно называют просто векторами.

Особую роль играют матрицы, у которых число строк совпадает с числом столбцов, то есть матрицы размера n×n. Такие матрицы называются квадратными При ссылке на квадратную матрицу достаточно указать ее порядок. Например, матрица третьего порядка имеет размер 3×3.

Квадратная матрица порядка 1 отождествляется с единственным ее элементом.

Матрицы. Понятие. Применение

Матрица – это набор чисел, записываемый в виде прямоугольной таблицы. Строки и столбцы матрицы можно считать векторами. Матрицы, у которых число строк равно числу столбцов, называют квадратными. Существует особая квадратная матрица I (Identity) – единичная. Она характеризуется тем, что ее диагональные элементы равны единице, а остальные — нулю: $$I_ = 1, i = j
\\
I_ = 0, i \neq j$$ В 3D-графике, матрицы используются для преобразования координат из одной системы координат в другую. Вообще, для преобразования базиса (базис — набор векторов, задающих координатные оси) в трехмерном пространстве вполне хватает матрицы 3х3, однако, для целей 3D-графики этого не всегда достаточно. В частности, перспективное проецирование удобно представить матрицей, но для этого необходима матрица 4х4. Также с этой целью вводятся т.н. однородные координаты, где трехмерной точке (x, y, z) соответствует набор четырех координат (xw, yw, zw, w).

Основные операции

Из операций, определенных над матрицами, мы рассмотрим умножение, транспонирование и, частично, обращение.

Умножение матриц

Определяется произведение матриц так:
Если Aij — элемент матрицы A, стоящий в i-ой строке и j-ом столбце, и C = AB, то $$C_ = \sum_A_ \cdot B_$$ т.е. элементы матрицы C получаются как скалярные произведения строк A на столбцы B. Отсюда видно, что при умножении матриц, необходимо, чтобы в первом сомножителе было столько же столбцов, сколько строк во втором.
Кое-кому также очевидно, что если существует произведение AB, то это еще не означает существования BA, не говоря уже об их равенстве.
В общем, $$AB \neq BA$$, т.е. произведение матриц не коммутативно. Зато оно ассоциативно. Проще говоря:
$$(AB)С = A(BC)$$ Умножение любой матрицы на единичную, дает исходную:
$$AI = A$$

Транспонирование матриц

Транспонирование обозначается как AT и возможно только для квадратных матриц. Если $$B=A^T$$, то
$$B_ = B_$$
Иными словами, строки и столбцы меняются местами.

Обращение

Обратная к A матрица обозначается как $$A^<-1>$$ и характеризуется тем, что при умножении A на нее, получается единичная матрица:
$$AA^ <-1>= I$$ Существует обратная к данной матрица далеко не всегда и процесс её нахождения далеко не тривиален. Однако, в 3D графике, напомню, матрицы используются для задания преобразований системы координат (базиса). Базис, заданный тремя взаимно перпендикулярными векторами единичной длины, называется ортонормированным. А у матрицы, задающей преобразование из одного ортонормированного базиса в другой, обратная матрица совпадает с транспонированной:
$$A^ <-1>= A^T$$ Матрицы, обладающие этим свойством, называются ортогональными.
В 3D-графике матрицы поворота ортогональны и их произведение тоже.

Применение матриц

Выше уже было сказано, что в 3D-графике матрицы используются для преобразования координат. С этой целью точка умножается (как однострочная матрица) на соответствующую матрицу преобразования. Например, для поворота точки относительно оси X и параллельного переноса, ее надо последовательно умножить на матрицу поворота и параллельного переноса (см. следующий раздел). Причем, т.к. произведение матриц не коммутативно, результат зависит от последовательности умножения:
$$PRT \neq PTR$$, т.к.
$$RT \neq TR,$$
где P — точка, R — матрица поворота, T — матрица переноса. Ассоциативность произведения матриц позволяет не умножать каждую точку на несколько матриц, а сперва перемножить матрицы преобразования и умножать точки уже на результирующую матрицу:
$$PRT = P(RT)$$

Основные матрицы

Здесь приведены наиболее часто используемые в 3D-графике матрицы. Параллельный перенос точки на вектор (x, y, z):

1 0 0 0
0 1 0 0
0 0 1 0
x y z 1

Вращение относительно оси X на угол a:

1 0 0 0
0 cos a sin a 0
0 -sin a cos a 0
0 0 0 1

Вращение относительно оси Y на угол a:

cos a 0 -sin a 0
0 1 0 0
sin a 0 cos a 0
0 0 0 1

Вращение относительно оси Z на угол a:

cos a sin a 0 0
-sin a cos a 0 0
0 0 1 0
0 0 0 1

Масштабирование по осям X, Y и Z на x, y и z соответственно:

z 0 0 0
0 y 0 0
0 0 x 0
0 0 0 1

Проективная матрица с вертикальным и горизонтальным углами обзора fovx и fovy соответственно:

ctg fovx/2 0 0 0
0 ctg fovy/2 0 0
0 0 Zf/(Zf – Zn) 0
0 0 -Zn*Zf/(Zf – Zn) 1

где Zn — ближняя z-плоскость. Zf — дальняя z-плоскость.

Полезно знать

В 3D-графике матрицы преобразования координат, в большинстве случаев,
являются композицией (произведением) матриц вращения, масштабирования и
переноса, и имеют следующую структуру:

Rx Ry Rz 0
Ux Uy Uz 0
Fx Fy Fz 0
Tx Ty Tz 1

где R — вектор, показывающий направление оси X новой (т.е. задаваемой данной матрицей) системы координат, в координатах исходной. U и F, соответственно, направления осей Y и Z. Модули этих векторов совпадают с масштабированием по соответствующим осям в новой системе. T — вектор переноса, совмещающего начала координат исходной и новой системы. Получается, что подматрица 3х3, состоящая из векторов R, U и F, задает вращение и масштабирование, а четвертая строка отвечает за перенос.

Функциональное программирование на примере работы с матрицами из теории линейной алгебры

В основе работы с матрицами (в данной статье мы будем рассматривать только двумерные матрицы) лежит мощная математическая теория из области линейной алгебры. Одно определение или действие следует из другого, одна функция вызывает другую. Поэтому для программной реализации функционала математических операций над матрицами функциональные языки подходят очень хорошо. В рамках данной статьи мы рассмотрим конкретные примеры на языке F# и дадим подробные комментарии, как это работает. Так как F# входит в семейство .NET, то полученный функционал можно без каким либо проблем использовать в других императивный языках, например C#.

Определение матрицы и реализация на F#

Матрицы являются базовой и важнейшей частью линейной алгебры. Матрицы часто используются в программировании, например в 3D-моделировании или гейм-девелопинге. Разумеется, велосипед уже давно изобретен и необходимые фреймворки для работы с матрицами уже готовы, и их можно и нужно использовать. Данная статья не ставит своей целью изобретение нового фреймворка, но показывает реализацию базовых математических операций для работы с матрицами в функциональном стиле с использованием языка программирования F#. По мере рассмотрения материала мы будем обращаться к математической теории матриц и смотреть, как ее можно реализовать в коде.

Для начала вспомним, что такое матрица? Теория говорит нам следующее

Прямоугольная таблица чисел, содержащая m строк и n столбцов, называется матрицей размера m x n

Матрицы, как правило, обозначаются прописными буквами латинского алфавита и записываются в виде

Для работы с матрицами в F# создадим запись, основанную на двумерной таблице, которую в дальнейшем будем расширять полезными методами для того, чтобы совершать необходимые математические операции над ней.

type Matrix = < values: int[,] >with // здесь будем добавлять методы end 

Добавим вспомогательный метод для инициализации записи двумерным массивом

static member ofArray2D (values: int [,]) =

Входным аргументом функции будет двумерный массив, а на ее выходе — запись типа Matrix. Ниже приведем пример инициализации записи.

let a = array2D [[1;0;2] [3;1;0]] let A = Matrix.ofArray2D a 

Две матрицы A=(aij) и B=(bij) называются равными, если они совпадают поэлементно, т.е. aij=bij для всех i=1,2. m и j=1,2. n

Для реализации этого правила будем использовать переопределенный оператор == и добавим пару полезных функций, которые также понадобятся нам в дальнейшем.

static member sizes matrix = let rows = matrix.values.[*,0].Length let cols = matrix.values.[0,*].Length (rows, cols) static member isEquallySized matrix1 matrix2 = let dim1 = Matrix.sizes matrix1 let dim2 = Matrix.sizes matrix2 (dim1 = dim2) static member (==) (matrix1, matrix2) = if not (Matrix.isEquallySized matrix1 matrix2) then false else not (matrix1.values |> Array2D.mapi (fun x y v -> if matrix2.values.[x, y] <> v then false else true) |> Seq.cast |> Seq.contains false) 

Давайте подробнее рассмотрим код выше. Как можно заметить, здесь есть три функции. Первая функция sizes возвращает размерность матрицы в виде кортежа. Так как мы работаем только с прямоугольными матрицами, то для получения количества строк мы берем полный срез первой колонки и возвращаем ее длину.

let rows = matrix.values.[*,0].Length 

Аналогичным способом работает определение количества колонок — получаем полный срез первой строки и возвращаем ее длину.

Следующая функция isEquallySized сравнивает размерность двух матриц и возвращает true если они равны. Для этого она использует уже готовую функцию sizes и просто сравнивает результаты.

Оператор == для поэлементного сравнения двух матриц кажется сложнее, но сейчас вы увидите, что он также простой.

Перед тем, как сравнивать две матрицы, сравним их размерность. Если они не равны, то нет дальше смысла проводить проверку, так как уже понятно, что и матрицы будут не равны.

if not (Matrix.isEquallySized matrix1 matrix2) then false 

Далее, на основе исходных матриц matrix1 и matrix2 мы формируем новую матрицу, заполненную true или false, в зависимости от того, совпадают ли соответствующие ячейки обеих матриц.

matrix1.values |> Array2D.mapi (fun x y v -> if matrix2.values.[x, y] <> v then false else true 

Функция Array2D.mapi перебирает все элементы matrix1 и передает в обработчик три параметра
x — индекс строки
y — индекс колонки
v — содержимое ячейки
Содержимое ячейки v мы сравниваем с соответствующей ячейкой matrix2 и если они равны, то пишем true, иначе — false.

Если есть хоть одна ячейка с элементом false, то это означает, что матрицы не равны между собой.

Так как Array2D не содержит в себе методов для фильтрации или поиска, то реализуем это сами. Для этого разложим матрицу в линейный список

|> Seq.cast

И найдем хоть одно несовпадение

|> Seq.contains false 

Функция Seq.contains вернет true если в разложенном списке будет найдено хоть одно значение false. Поэтому нам нужно инвертировать полученный результат, чтобы оператор == работал так, как мы хотим

else not (matrix1.values |> Array2D.mapi (fun x y v -> if matrix2.values.[x, y] <> v then false else true) |> Seq.cast |> Seq.contains false) 

Матрица O называется нулевой или нуль-матрицей, если все ее элементы равны нулю.

static member O rows cols = let array2d = Array2D.zeroCreate rows cols

Пример использования этой функции

let AO = Matrix.O 5 5 

Полагаю, что здесь нет ничего сложного, что требует пояснений, поэтому продолжаем.

Матрица, число строк которой равно числу столбцов и равно n, называется квадратной матрицей порядка n

Таким образом, квадратная матрица имеет вид.

В рамках этого правила мы создадим функцию, которая прямоугольную матрицу трансформирует в квадратную путем отсечения всех элементов, которые не попадают в квадрат.

static member toSquare matrix = // получаем размерность исходной матрицы let dim = Matrix.sizes matrix // получаем количество колонок let colCount: int = snd dim // получаем количество строк let rowCount: int = fst dim // находим размер минимальной стороны let length = System.Math.Min (colCount, rowCount) // создаем пустой квадратный массив с размерностью // равной наименешей стороне исходной матрицу let zero = Array2D.zeroCreate length length // копируем исходную матрицу в квадратную let square = zero |> Array2D.mapi (fun x y _ -> matrix.values.[x, y]) // позвращаем полученную матрицу

Комментариев в исходном коде поясняют принцип работы функции, поэтому продолжим.

Квадратная матрица называется треугольной, если все ее элементы ниже главной диагонали равны нулю, т.е. треугольная матрица имеет вид

Ниже приведен код функции, который преобразует исходную матрицу в треугольную. Но в нашей функции мы будем работать с прямоугольной матрицей, то есть она может быть не квадратной. Читатель легко может модифицировать код функции так, чтобы она возвращала квадратную треугольную матрицу, используя функцию, которую мы рассмотрели ранее.

static member T matrix = let values = matrix.values |> Array2D.mapi (fun x y v -> if y

Функция Array2D.mapi преобразовывает исходный двумерный массив в новый при помощи обработчика, который принимает три параметра

x — номер строки
y — номер колонки
v — содержимое ячейки

if y < x then 0 else v 

Здесь мы делаем проверку, находится ли элемент ниже главной диагонали и если да, то заполняем ячейку 0. В противном случае — исходным значение из входной матрицы.

Ниже приведен пример использования этой функции.

let a = array2D [[1;2;3] [4;5;6] [7;8;9] [10;11;12]] let A = Matrix.ofArray2D a let R = Matrix.triangular A printfn "origin = \n %A" A.values printfn "triangular = \n %A" R.values 

Получаем следующий результат

origin = [[1; 2; 3] [4; 5; 6] [7; 8; 9] [10; 11; 12]] triangular = [[1; 2; 3] [0; 5; 6] [0; 0; 9] [0; 0; 0]] 

Квадратная матрица называется диагональной, если все ее элементы, расположенные вне главной диагонали, равны нулю

static member D matrix = let diagonal = matrix.values |> Array2D.mapi (fun x y v -> if x <> y then 0 else v)

Эта функция очень похожа на предыдущую, отличается только условие проверки. Ниже пример использования

let a = array2D [[1;2;3] [4;5;6] [7;8;9] [10;11;12]] let A = Matrix.ofArray2D a let R = Matrix.D A printfn "origin = \n %A" A.values printfn "diagonal = \n %A" R.values 
origin = [[1; 2; 3] [4; 5; 6] [7; 8; 9] [10; 11; 12]] diagonal = [[1; 0; 0] [0; 5; 0] [0; 0; 9] [0; 0; 0]] 

Диагональная матрица является единичной и обозначается E, если все ее элементы, расположенные на главной диагонали, равны единице

Реализация такой матрицы на F# выглядит так

static member E rows cols = let array2d = Array2D.init rows cols (fun x y -> if x = y then 1 else 0)

Операции над матрицами при помощи F#

Над матрицами, как и над числами, можно производить ряд действий, причем некоторые из них аналогичны операциям над числами, а некоторые — специфические.

Суммой двух матриц Amn=(aij)и Bmn=(bij)одинаковых размеров называется матрица того же размера A+B=Cmn=(cij), элементы которой равны сумме элементов матриц A и B, расположенных на соответствующих местах

Пример, для заданных матриц A и B находим сумму A+B

Рассмотрим код для сложения двух матриц

static member (+) (matrix1, matrix2) = if Matrix.isEquallySized matrix1 matrix2 then let array2d = matrix1.values |> Array2D.mapi (fun x y v -> matrix2.values.[x, y] + v) < values = array2d >else failwith "matrix1 is not equal to matrix2" 

Перед тем, как складывать матрицы, нужно убедиться, что их размерность совпадает, в противном случае функция генерирует исключение. Ниже приведем пример использования данной функции

let a = array2D [[2;3] [1;-5] [0;6]] let A = Matrix.ofArray2D a let b = array2D [[-3;3] [1;7] [2;0]] let B = Matrix.ofArray2D b let R = A+B printfn "A+B =\n %A" R.values 
A+B = [[-1; 6] [2; 2] [2; 6]] 

Произведением матрицы A=(aij) на число k называется матрица kA=(kaij) такого же размера, что и матрица A, полученная умножением всех элементов матрицы A на число k

Пример, для заданной матрицы A находим матрицу 3A

static member (*) (value, matrix) = let array2d = matrix.values |> Array2D.mapi (fun _ _ v -> v * value)

Матрицу -A=(-1)*A будем называть противоположной матрице A. Из этого определения плавно переходим к следующему

Разностью матриц A и B одинаковых размеров называется сумма матрицы A и матрицы, противоположной к B

static member (-) (matrix1: Matrix, matrix2: Matrix) = if Matrix.isEquallySized matrix1 matrix2 then matrix1 + (-1)*matrix2 else failwith "matrix1 is not equal to matrix2" 

Две матрицы называются согласованными, если число столбцов первой равны числу строк второй

static member isMatched matrix1 matrix2 = let row1Count = matrix1.values.[0,*].Length let col2Count = matrix2.values.[*,0].Length row1Count = col2Count 

Проверка согласованности матриц требуется для их перемножения.

Произведением AB согласованных матриц Amn=(aij) и Bnp=(bjk) называется матрица Cmn=(cik), элемент cik которой вычисляется как сумма произведений элементов i-й строки матрицы A и соответствующих элементов k-го столбца матрицы B

Вычислить произведение матриц

Решение по определению произведения матриц

Рассмотрим код для умножения двух матриц

static member (*) (matrix1, (matrix2: Matrix)) = if Matrix.isMatched matrix1 matrix2 then let row1Count = matrix1.values.[*,0].Length let col2Count = matrix2.values.[0,*].Length let values = Array2D.zeroCreate row1Count col2Count for r in 0..row1Count-1 do for c in 0..col2Count-1 do let row = Array.toList matrix1.values.[r,*] let col = Array.toList matrix2.values.[*,c] let cell = List.fold2 (fun acc val1 val2 -> acc + (val1 * val2)) 0 row col values.[r,c] else failwith "matrix1 is not matched to matrix2" 

Давайте разберемся с кодом подробнее.
Перед умножением нужно убедиться, что матрицы являются согласованными

if Matrix.isMatched matrix1 matrix2 then

Итоговая матрица будет иметь размерность, в которой количество строк такое же, как у первой матрицы и количество столбцов такое же, как у второй матрицы

let row1Count = matrix1.values.[*,0].Length let col2Count = matrix2.values.[0,*].Length // формируем пустой двумерный массив для сохранения результатов умножения let values = Array2D.zeroCreate row1Count col2Count 

После этого мы последовательно перебираем все строки и все столбцы исходных матриц

for r in 0..row1Count-1 do for c in 0..col2Count-1 do let row = Array.toList matrix1.values.[r,*] let col = Array.toList matrix2.values.[*,c] 

Вычисляем итоговое значение каждой ячейки

let cell = List.fold2 (fun acc val1 val2 -> acc + (val1 * val2)) 0 row col 

Функция List.fold2 на вход получает два списка (строку и колонку) и передает в обработчик следующие параметры

acc — аккумулятор, содержащий результат предыдущего вычисления
val1 — содержимое ячейки из первого массива. В нашем случае это строка из первой матрицы
val2 — содержимое ячейки из второго массива, то есть колонки второй матрицы

Так как матрицы являются согласованными, то мы уверены, что у нас не произойдет выхода за пределы массивов.

Обработчик добавляет к аккумулятору произведение ячеек из строк и столбца и полученное значение будет передано следующей итерации. Таким образом конечным итогом работы функции List.fold2 будет итоговое значение произведений двух матриц. Остается только заполнить им предварительно созданную пустую матрицу

values.[r,c] 

Которая вернется как результат

Ниже приведем пример использования данной функции

let a = array2D [[1;0;2] [3;1;0]] let A = Matrix.ofArray2D a let b = array2D [[-1;0] [5;1] [-2;0]] let B = Matrix.ofArray2D b let R = A*B printfn "A*B =\n %A" R.values 
A1*B1 = [[-5; 0] [2; 1]] 

Если k ∈ N, то k-й степенью квадратной матрицы Aназывается произведение k матриц A

Рассмотрим код на F# для произведения матрицы в степень. Здесь будет использоваться хвостовая рекурсия для того, чтобы не переполнить стек при больших значениях степеней. Хвостовая рекурсия — это такая рекурсия, которая компилятором в итоге преобразуется в цикл. По возможности рекомендуется всегда использовать именно хвостовую рекурсию вместо обычной, но для этого нужно чтобы каждый кадр рекурсии возвращал итоговое вычисленное значение. Это значение обычно называется аккумулятором и передается в следующий кадр рекурсии. То есть, в отличие от обычной рекурсии, которая возвращает вычисленное значение вверх по стеку, хвостовая рекурсия передает вычисленное значение вниз по стеку. Каждый новый кадр рекурсии делает свои вычисления и добавляет их к уже ранее вычисленному значению, которое хранится в аккумуляторе. После того, как последний кадр рекурсии отработал, в аккумуляторе уже есть вычисленное значение, которое просто возвращается как результат.

Таким образом, компилятор может оптимизировать код и преобразовать его в обычный цикл.

 // возводим матрицу в степень // matrix - исходная матрица // value - значение степени static member (^^) (matrix, value) = // внутренняя функция, которая реализует хвостовую рекурсию // m - матрица // p = значение степени let inRecPow m p = // рекурсивная функция // acc - накопленный аккумулятор. имеет тип Matrix // p - значение степени для текущего кадра // с каждым кадром рекурсии это значение уменьшается на единицу let rec recPow acc p = // сравниваем текущую степень match p with | x when x > 0 -> // вычисляем новое значение аккумулятора // умножаем исходную матрицу на старый аккумулятор, то есть возводим в следующую степень let nextAcc = acc*m // рекурсивно вызываем функцию и передаем ей уменьшенное на единицу значение степени recPow nextAcc (x-1) // если степень достигла нуля, то возвращаем вычисленный аккумулятор | _ -> acc // создаем единичную матрицу, чтобы передать ее в качестве аккумулятор для вычисления степени let dim = Matrix.sizes matrix let colCount = snd dim let rowCount = fst dim let u = Matrix.E rowCount colCount // вызываем рекурсивную функцию и передаем ей единичную матрицу в качестве аккумулятора recPow u p // вызываем функцию, реализующую хвостовую рекурсию для получения результата let powMatrix = inRecPow matrix value // возвращаем итоговую матрицу

Код содержит подробные комментарии о том, как он работает. Требует небольшого пояснения, зачем используется единичная матрица? Она нужна для первого кадра рекурсии и служит в качестве базового значения аккумулятора, в котором будет накапливаться итоговый результат.
Ниже рассмотрим пример использования нашей функции

Вычислим следующее произведение

Где E — это единичная матрица. Так как мы не можем к матрице прибавить число, то мы должны прибавлять 3E.

// возвращает сумму матрицы и числа static member (+) (matrix, (value: int)) = let dim = Matrix.sizes matrix let r = fst dim let c = snd dim // создаем единичную матрицу let unit = Matrix.E r c // умножаем единичную матрицу на число и прибавляем к входной матрице value*unit + matrix 
let a = array2D [[1;0] [2;-1]] let A = Matrix.ofArray2D a let R = 2*(A^^3) - 4*(A^^2) + 3 printfn "2*A^3 - 4*A^2 + 3 =\n %A" R.values 
2*A5^3 - 4*A5^2 + 3 = [[1; 0] [4; -3]] 

Матрица A T , столбцы которой составлены из строк матрицы A с теми же номерами и тем же порядком следования элементов, называется транспонированной к матрице A

static member transpose matrix = let dim = Matrix.sizes matrix let rows = fst dim let cols = snd dim // создаем нулевую матрицу для вычисления результатов let tMatrix = Matrix.O cols rows // копируем в нее данные из исходной матрицы matrix.values |> Array2D.iteri(fun x y v -> tMatrix.values.[y, x]  
let a = array2D [[1;2;3] [4;5;6] [7;8;9] [10;11;12]] let A = Matrix.ofArray2D a let R6 = Matrix.T A printfn "origin = \n %A" A.values printfn "transpose = \n %A" R.values 
origin = [[1; 2; 3] [4; 5; 6] [7; 8; 9] [10; 11; 12]] transpose = [[1; 4; 7; 10] [2; 5; 8; 11] [3; 6; 9; 12]] 

Итоги

В этот статье мы рассмотрели примеры реализации и использования матриц из теории линейной алгебры. А также основных математических операций над ними, с использованием функционального подхода на языке F#. Я надеюсь, что читатель смог оценить ту гибкость, которую дают функциональные языки.

Полный исходный код модуля матриц, фрагменты которого были рассмотрены в рамках статьи, вы сможете найти на гитхабе.

  • Функциональное программирование
  • F#

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *