Percursos
/
Julia
Julia
/
Exercícios
/
Matriz do Exercism
Matriz do Exercism

Matriz do Exercism

Exercício de aprendizagem

Introdução

Muito atrás, no Conceito Vectors, notámos que "os arrays podem ter um tamanho arbitrário (sujeitos apenas às limitações de memória do teu hardware) e podem ter um número arbitrário de dimensões."

Desde então, temos ignorado em grande parte os arrays com mais de uma dimensão, só para manter as coisas simples. Esta decisão fará mais sentido se tentares ler os documentos de referência da Julia, na sua plena complexidade.

No entanto, os arrays de dimensões superiores são muito, muito importantes na computação científica, por isso precisamos de os compreender.

Nomenclatura: Seguindo séculos de precedente matemático, referimo-nos aos arrays 1-D como Vectors e aos arrays 2-D como Matrices.

Os exemplos neste documento serão, na sua maioria, matrizes. Trabalhar com 3 ou mais dimensões é sintaticamente quase idêntico, mas a saída é difícil de ler e confusa (num ecrã 2-D).

Construir arrays

Já criámos muitos vetores colocando uma lista separada por vírgulas entre parênteses retos. Também se pode usar o ponto e vírgula como separador.

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

Se usarmos um espaço (ou um tabulador) como separador, o resultado é diferente.

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

A Julia define agora isto como uma Matrix 1×3 (noutros contextos, chamar-lhe-íamos row vector).

Em geral, os espaços juntam coisas na horizontal e os pontos e vírgulas (ou as mudanças de linha) juntam coisas na vertical.

A referência a "coisas" é deliberadamente vaga, porque a Julia tentará trabalhar com aquilo que lhe deres.

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

Há as funções hcat() e vcat() que fazem o mesmo, deixando mais explícito que se trata de concatenações horizontais e verticais. A generalização para dimensões superiores é a função 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

Escrever matrizes explícitas faz-se comodamente por ordem row-major, porque isso encaixa na intuição humana (é mais fácil de olhar para elas, nas culturas com texto horizontal):

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

No entanto, tem em conta que a Julia (tal como o Fortran, o R e o Matlab, mas ao contrário do C/C++ ou do NumPy) armazena os arrays de N dimensões por ordem column-major, e isto pode fazer uma enorme diferença de desempenho se fizeres um ciclo sobre os elementos. Ajuda a cache da tua CPU para que ela te ajude!

# 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

O exemplo acima pega nos inteiros 1 a 6 e preenche com eles uma Matrix 2×3, coluna a coluna.

Há várias funções utilitárias para construir tipos comuns de array (uniformes ou aleatórios).

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

Indexação

Para um array 2-D, usamos geralmente dois índices pela ordem [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

O último exemplo é talvez surpreendente: um índice único não é um erro e devolve um único elemento.

A explicação remonta ao comentário sobre a ordem column-major: a Julia desce a coluna 1, depois a coluna 2, até encontrar o 3.º elemento na memória.

Tem cuidado: isto tem algumas utilidades ao escrever bibliotecas de uso geral, mas é mais provável que seja confuso!

Surge uma questão semelhante quando consultamos o tamanho de um array. length() dá o número total de elementos e size() dá um tuplo de ndims() elementos com o comprimento de cada dimensão.

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

Indexar com intervalos e arrays

Podemos copiar facilmente uma submatriz.

No exemplo abaixo, a função reshape() força um array às dimensões especificadas, preenchendo coluna a coluna, e depois fazemos uma fatia dele.

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

Um : sozinho significa "copiar tudo nesta dimensão".

Usa vetores para linhas/colunas não consecutivas:

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

Aplicar funções a um array

Já vimos anteriormente funções de agregação como sum() e maximum() aplicadas a coleções 1-D, onde operam sobre todos os elementos e devolvem um resultado escalar.

Isto também funciona em dimensões superiores. No entanto, podemos querer aplicar a função a apenas uma dimensão, por exemplo somando para baixo ou para o lado, para devolver um array com uma singleton dimension de tamanho 1.

Para isso, existe um argumento de palavra-chave opcional, 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

O valor de dims é a dimensão que se torna singleton, portanto dims=1 => 1×3 e dims=2 => 2×1 nos exemplos acima.

Por extensão, os arrays de dimensões superiores podem reduzir várias dimensões, se dims for definido como um array ou um intervalo (mais uma vez, compreender a saída pode exigir alguma reflexão!).

Escrever funções que têm em conta as dimensões

O argumento de palavra-chave dims é comum em funções incorporadas como sum(), mas como escrevemos algo equivalente no nosso próprio código?

Uma boa resposta é usar funções de ordem superior como reduce(), e isto será abordado com algum detalhe num Conceito mais adiante.

Em alternativa, a Julia disponibiliza várias funções que nos permitem tratar arrays de N dimensões como se fossem vetores aninhados de vetores.

Para as matrizes, eachrow() e eachcol() são convenientes, mas a função mais geral é eachslice(), que consegue lidar com dimensões arbitrárias.

# 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]

O tipo é um pouco assustador, mas só porque isto é uma vista sobre o array original, que nos permite trabalhar sobre ele sem copiar. Os arrays da Julia podem ter potencialmente terabytes de tamanho, por isso copiar pode ser um pesadelo de desempenho!

Uma vista como esta pode ser usada para fazer ciclos, para broadcasting, ou para todas as outras operações que vimos no programa do curso até agora.

Além disso, as compreensões podem ser poderosas e versáteis quando trabalham com arrays. Foram mencionados casos simples no conceito Loops, mas haverá uma discussão mais desenvolvida num conceito posterior.

Broadcasting em múltiplas dimensões

Discutimos o caso 1-D no conceito 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

O operador com ponto .- trata o 0.5 singleton como equivalente a [0.5, 0.5, 0.5], fazendo broadcasting dele para corresponder às dimensões, e depois faz a subtração elemento a elemento.

Estender isto a dimensões superiores é, na verdade, apenas mais do mesmo.

Por exemplo, ao fazer broadcasting da multiplicação de um vetor linha 2x1 [1.0 1.5 2.0] pela matriz 2x3 [1 2 3; 4 5 6], é equivalente a fazer broadcasting de [1.0 1.5 2.0] para [1.0 1.5 2.0; 1.0 1.5 2.0], com a multiplicação depois feita elemento a elemento.

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

Nota: São as dimensões não singleton que devem coincidir em tamanho (por exemplo, 2x3 Matrix .* 2x1 Matrix) para o broadcasting.

Além disso, uma função pode ser aplicada por broadcasting a cada elemento de uma Matrix exatamente como se faz com um 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

Podes sentir, ao início, que o potencial de confusão do programador cresce exponencialmente com o número de dimensões, mas a prática ajuda muito. Além disso, a familiaridade com os arrays do NumPy transfere-se bastante bem para a Julia, apesar da sintaxe diferente.

Instruções

Uma matriz de pontos é uma imagem bidimensional composta por pontos e espaço em branco. Os pontos formam a imagem pretendida sobre o fundo de espaço em branco.

Uma imagem de matriz de pontos pode ser armazenada e manipulada em memória como uma Matrix bidimensional. No que se segue, o "espaço em branco" nas matrizes será representado por 0 e os "pontos" serão diferentes de zero.

1. Define a Matrix do logótipo do Exercism

A Matrix é a seguinte, com 0 como espaço em branco e 1 como ponto:

[
    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;
]

Se a "desenhares", fica assim:

  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. Define funções que fazem o logótipo franzir a boca

Define as funções frown!() e frown(), que recebem a Matrix do logótipo do Exercism. Devolve uma Matrix com a boca sorridente transformada numa boca franzida.

A Matrix resultante ficaria assim:

  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. Monta uma parede de autocolantes

Define a função stickerwall(), que recebe a matriz do Exercism como parâmetro de entrada. Devolve uma Matrix da matriz de pontos que se desenha assim:

  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. Substitui os pontos pelo número de píxeis de cada coluna

Não estamos limitados a usar apenas 1 como ponto, por isso podemos codificar outras informações úteis, se quisermos. Define a função colpixelcount(), que recebe qualquer matriz de pontos cujos pontos valham 1 como parâmetro de entrada. Devolve uma matriz de pontos do mesmo tamanho, em que os pontos de cada coluna passam a ser o número de pontos dessa coluna.

Tendo a Matrix do logótipo do Exercism como parâmetro de entrada, o resultado é uma Matrix como se segue:

[
    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. Desenhar uma matriz de pontos

Mais do que falar de desenhar as nossas criações, gostaríamos de o conseguir fazer de facto, para as vermos com facilidade. Define a função render(), que recebe uma matriz de pontos como parâmetro de entrada. Devolve uma string em que os pontos são representados por 'X', os 0 por ' ', e cada linha é ligada por um carácter de nova linha.

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  "

Quando impresso, isto deve desenhar a Matrix do Exercism conforme esperado.

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

Isto também deve funcionar com "pontos" diferentes de 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
Editar via GitHub A ligação abre numa nova janela ou separador
Julia Exercism

Estás pronto para começar Matriz do Exercism?

Inscreve-te no Exercism para aprenderes e dominares Julia com 35 conceitos128 exercícios, e mentoria humana real, tudo grátis.