Треки
/
Julia
Julia
/
Вправи
/
Матриця Exercism
Матриця Exercism

Матриця Exercism

Навчальна вправа

Вступ

Давно, ще в концепції Vectors, ми зауважили, що «масиви можуть бути довільного розміру (обмежені лише памʼяттю обладнання) і мати довільно багато вимірів.»

Відтоді ми здебільшого оминали масиви з більш ніж одним виміром, просто щоб не ускладнювати. Це рішення стане зрозумілішим, якщо спробувати прочитати довідкову документацію Julia в усій її складності.

Однак багатовимірні масиви дуже, дуже важливі в наукових обчисленнях, тож нам треба їх розуміти.

Номенклатура: Наслідуючи багатовікову математичну традицію, ми називаємо одновимірні масиви Vectors, а двовимірні - Matrices.

Приклади в цьому документі здебільшого стосуватимуться матриць. Робота з трьома і більше вимірами синтаксично майже ідентична, але результат важко й заплутано читати (на двовимірному екрані).

Створення масивів

Ми вже створювали багато векторів, записуючи список, розділений комами, у квадратних дужках. Розділювачем можуть бути також крапки з комою.

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> w = [1; 2; 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v == w
true

Якщо використати пробіл (або табуляцію) як розділювач, результат буде іншим.

julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

Тепер Julia визначає це як матрицю 1×3 (в інших контекстах ми назвали б її row vector).

Загалом пробіли зʼєднують елементи горизонтально, а крапки з комою (або символи нового рядка) - вертикально.

Згадка про «речі» навмисно неконкретна, бо Julia намагається працювати з тим, що їй передають.

julia> [v 2v]
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> [v; 2v]
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

Є функції hcat() і vcat(), які роблять те саме, лише чіткіше показуючи, що це горизонтальна та вертикальна конкатенація. Багатовимірним узагальненням цього є функція cat().

jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
 1  2
 2  4
 3  6

julia> vcat(v, 2v)
6-element Vector{Int64}:
 1
 2
 3
 2
 4
 6

Явно вводити матриці зручно в порядку заповнення рядками, бо це відповідає людській інтуїції (легше дивитися, принаймні в культурах із горизонтальним текстом):

julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

Однак варто знати, що Julia (як Fortran, R і Matlab, але на відміну від C/C++ чи NumPy) зберігає N-вимірні масиви в порядку стовпців, і це може дати величезну різницю в швидкодії, якщо проходити циклом по елементах. Допоможімо кешу процесора, щоб він допоміг нам!

# put these integers in 2 rows and 3 columns

julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
 1  3  5
 2  4  6

У наведеному прикладі беруться цілі числа від 1 до 6, і ними по стовпцях заповнюється матриця 2×3.

Є різні допоміжні функції для створення поширених типів масиву (однорідних або випадкових).

julia> zeros(2, 3)  # see also ones()
2×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> falses(2, 2)  # booleans, see also trues()
2×2 BitMatrix:
 0  0
 0  0

julia> rand(Float32, 2, 3)  # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
 0.768823  0.169633  0.632565
 0.388451  0.109176  0.850381

Індексація

Для двовимірного масиву ми зазвичай використовуємо два індекси в порядку [row, col].

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m[1, 2] # row 1, col 2
2

# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]

julia> m[3]
2

Останній приклад, мабуть, дивує: один індекс - це не помилка, і він повертає єдиний елемент.

Пояснення повертає нас до згадки про порядок стовпців: Julia рухається вниз по 1-му стовпцю, потім по 2-му, доки не знайде третій елемент у памʼяті.

Будьмо уважні: це іноді стає в пригоді, коли ми пишемо бібліотеки загального призначення, але частіше тільки заплутує!

Схожа проблема виникає, коли ми запитуємо розмір масиву. length() дає загальну кількість елементів, а size() - кортеж із ndims() елементів, у якому записано довжину кожного виміру.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> length(m)
6

julia> size(m)  # 2 rows, 3 cols
(2, 3)

julia> ndims(m)  # how many dimensions? Like `m |> size |> length`
2

Індексація діапазонами і масивами

Ми легко можемо скопіювати підматрицю.

У наведеному нижче прикладі функція reshape() приводить масив до заданих розмірів, заповнюючи його по стовпцях, а потім ми беремо з нього зріз.

julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
 1  5   9
 2  6  10
 3  7  11
 4  8  12

julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
 2  6
 3  7
 4  8

# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
 2  6  10
 3  7  11
 4  8  12

Сам символ : означає «скопіювати все в цьому вимірі».

Для несуцільних рядків чи стовпців використовуймо вектори:

julia> m12[[1, 3], :]  # rows 1 and 3
2×3 Matrix{Int64}:
 1  5   9
 3  7  11

Застосування функцій до масиву

Раніше ми бачили агрегувальні функції, як-от sum() і maximum(), застосовані до одновимірних колекцій, де вони працюють з усіма елементами й повертають скалярний результат.

Це працює і в більшій кількості вимірів. Однак іноді ми хочемо застосувати функцію лише до одного виміру, наприклад підсумувати вниз по стовпцях або впоперек по рядках, щоб отримати масив із singleton dimension розміру 1.

Для цього є необовʼязковий ключовий аргумент dims.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> sum(m)  # sum everything
21

julia> sum(m; dims=1)  # sum down
1×3 Matrix{Int64}:
 5  7  9

julia> sum(m; dims=2)  # sum across
2×1 Matrix{Int64}:
  6
 15

# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
 3
 6

Значення dims - це вимір, який стає одиничним, тож у наведених прикладах dims=1 => 1×3, а dims=2 => 2×1.

Аналогічно багатовимірні масиви можуть згортати кілька вимірів, якщо dims задати як масив або діапазон (і знову, щоб зрозуміти результат, доведеться подумати!).

Написання функцій, що враховують виміри

Ключовий аргумент dims часто трапляється у вбудованих функціях, як-от sum(), але як написати щось подібне у власному коді?

Один із добрих підходів - використати функції вищого порядку, як-от reduce(), і докладніше про це йтиметься в одній із наступних концепцій.

Або ж Julia надає кілька функцій, які дозволяють працювати з N-вимірними масивами так, ніби це вкладені вектори векторів.

Для матриць зручні eachrow() і eachcol(), але загальніша функція - eachslice(), яка працює з довільними вимірами.

# m is as in the previous examples

julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
 [1, 2, 3]
 [4, 5, 6]

julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
 [1, 4]
 [2, 5]
 [3, 6]

Цей тип трохи лякає, але лише тому, що це подання на вихідний масив, яке дозволяє працювати з ним без копіювання. Масиви Julia можуть сягати терабайтів, тож копіювання може стати справжнім кошмаром для швидкодії!

Таке подання можна використовувати для циклів, трансляції чи будь-яких інших операцій, які ми бачили в силабусі досі.

Крім того, генератори бувають потужними й універсальними, коли на вхід подано масив. Прості випадки згадувалися в концепції Loops, але ширше обговорення буде в одній із наступних концепцій.

Трансляція в кількох вимірах

Одновимірний випадок ми розглядали в концепції Vector Operations.

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v .- 0.5
3-element Vector{Float64}:
 0.7
 1.0
 1.2

Оператор із крапкою .- вважає одиничне 0.5 рівнозначним [0.5, 0.5, 0.5], застосовуючи broadcasting для узгодження розмірів, а потім виконує віднімання поелементно.

Поширення цього на більшу кількість вимірів - це насправді те саме, лише більше.

Наприклад, коли ми транслюємо множення рядкового вектора розміру 2x1 [1.0 1.5 2.0] на матрицю розміру 2x3 [1 2 3; 4 5 6], це еквівалентно трансляції [1.0 1.5 2.0] до [1.0 1.5 2.0; 1.0 1.5 2.0] з подальшим поелементним множенням.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> m .* [1.0 1.5 2.0]  # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0]  # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
 1.0  3.0   6.0
 4.0  7.5  12.0

julia> m ./ [1, 2]  # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

julia> m ./ [1 1 1; 2 2 2]  # 2x3 elementwise division
2×3 Matrix{Float64}:
 1.0  2.0  3.0
 2.0  2.5  3.0

Примітка: для трансляції розміри мають збігатися за неодиничними вимірами (наприклад, 2x3 Matrix .* 2x1 Matrix).

Крім того, функцію можна транслювати на кожен елемент Matrix точно так само, як це робиться з Vector.

julia> m
2×3 Matrix{Int64}:
 1  2  3
 4  5  6

julia> (x -> x^2).(m)  # broadcast a function to all elements
2×3 Matrix{Int64}:
  1   4   9
 16  25  36

Спершу може здатися, що можливість заплутатися зростає експоненційно з кількістю вимірів, але практика дуже допомагає. До того ж знайомство з масивами NumPy добре переноситься на Julia, попри інший синтаксис.

Вказівки

Точкова матриця - це двовимірне зображення, що складається з точок і порожнього простору. Точки утворюють задумане зображення на тлі порожнього простору.

Точкове зображення можна зберігати в памʼяті та працювати з ним як із двовимірним Matrix. Далі «порожній простір» у матрицях будемо позначати як 0, а «точки» будуть ненульовими.

1. Визначити Matrix із логотипом Exercism

Matrix має такий вигляд, де 0 - це порожній простір, а 1 - точка:

[
    0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0;
    0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
    0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0;
    0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0;
    0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0;
    0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
    0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
]

Якщо ми «відрендеримо» його, він матиме такий вигляд:

  XX          XX  
 X              X 
 X   X      X   X 
 X  X X    X X  X 
 X              X 
X                X
 X    X    X    X 
 X     X  X     X 
 X      XX      X 
 X              X 
  XX          XX

2. Визначити функції, які змушують логотип насупитися

Визначте функції frown!() і frown(), які приймають Matrix із логотипом Exercism. Поверніть Matrix, у якому усміхнений рот замінено на насуплений.

Отриманий Matrix рендерився б так:

  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X      XX      X
 X     X  X     X
 X    X    X    X
 X              X
  XX          XX

3. Зібрати стіну наліпок

Визначте функцію stickerwall(), яка приймає матрицю Exercism як вхідні дані. Поверніть Matrix точкової матриці, який рендериться так:

  XX          XX    XX          XX
 X              X  X              X
 X   X      X   X  X   X      X   X
 X  X X    X X  X  X  X X    X X  X
 X              X  X              X
X                XX                X
 X    X    X    X  X      XX      X
 X     X  X     X  X     X  X     X
 X      XX      X  X    X    X    X
 X              X  X              X
  XX          XX    XX          XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  XX          XX    XX          XX
 X              X  X              X
 X   X      X   X  X   X      X   X
 X  X X    X X  X  X  X X    X X  X
 X              X  X              X
X                XX                X
 X      XX      X  X    X    X    X
 X     X  X     X  X     X  X     X
 X    X    X    X  X      XX      X
 X              X  X              X
  XX          XX    XX          XX

4. Замінити точки на кількість пікселів у стовпці

Ми не обмежені використанням лише 1 як точки, тож за бажання можемо закодувати й іншу корисну інформацію. Визначте функцію colpixelcount(), яка приймає будь-яку точкову матрицю з 1 як точки. Поверніть точкову матрицю того самого розміру, де точки в кожному стовпці дорівнюють кількості точок у цьому стовпці.

Якщо подати на вхід Matrix із логотипом Exercism, на виході отримаємо такий Matrix:

[
    0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    0 8 0 0 0 1 0 0 0 0 0 0 1 0 0 0 8 0;
    0 8 0 0 1 0 2 0 0 0 0 2 0 1 0 0 8 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
    0 8 0 0 0 0 2 0 0 0 0 2 0 0 0 0 8 0;
    0 8 0 0 0 0 0 1 0 0 1 0 0 0 0 0 8 0;
    0 8 0 0 0 0 0 0 1 1 0 0 0 0 0 0 8 0;
    0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
    0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
]

5. Відрендерити точкову матрицю

Замість того щоб лише говорити про рендеринг наших творінь, нам хотілося б уміти робити це насправді, щоб було зручно дивитися. Визначте функцію render(), яка приймає точкову матрицю як вхідні дані. Поверніть рядок тексту (англ. string), де точки відрендерено як 'X', 0 - як ' ', а рядки зʼєднано символами нового рядка.

julia> render(E)
"  XX          XX  \n X              X \n X   X      X   X \n X  X X    X X  X \n X              X \nX                X\n X    X    X    X \n X     X  X     X \n X      XX      X \n X              X \n  XX          XX  "

Під час друку це має відрендерити матрицю Exercism так, як очікується.

julia> print(render(E))
  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X    X    X    X
 X     X  X     X
 X      XX      X
 X              X
  XX          XX

Це також має працювати з «точками», відмінними від 1:

julia> print(render(colpixelcount(E)))
  XX          XX
 X              X
 X   X      X   X
 X  X X    X X  X
 X              X
X                X
 X    X    X    X
 X     X  X     X
 X      XX      X
 X              X
  XX          XX
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Julia Exercism

Час розпочати Матриця Exercism?

Зареєструйтеся на Exercism, щоб вивчати й опановувати Julia, а також 35 концепцій128 вправ та справжнє наставництво від людей, і все це безкоштовно.