Trilhas
/
Julia
Julia
/
Exercícios
/
Diário de bordo do capitão
Diário de bordo do capitão

Diário de bordo do capitão

Exercício de aprendizagem

Introdução

Muitos programas precisam de valores (aparentemente) aleatórios para simular eventos do mundo real.

Exemplos comuns e familiares incluem:

  • Um lançamento de moeda: um valor aleatório de ('H', 'T').
  • O lançamento de um dado: um número inteiro aleatório de 1 a 6.
  • Embaralhar um baralho de cartas: uma ordenação aleatória de uma lista de cartas.

Gerar valores verdadeiramente aleatórios com um computador é um desafio técnico surpreendentemente difícil, então você pode ver esses resultados sendo chamados de "pseudorrandômicos".

Importante: este Conceito não cobre números aleatórios criptograficamente seguros, que são um desafio muito mais difícil.

No entanto, bibliotecas bem projetadas como o módulo Random da biblioteca padrão do Julia são rápidas, flexíveis e produzem resultados amplamente bons o suficiente para a maioria das aplicações em modelagem, simulação e jogos.

O Julia divide a funcionalidade de aleatoriedade em vários lugares:

  • Apenas algumas funções básicas, mas muito versáteis, em Base, que estão sempre disponíveis.
  • Uma variedade maior de opções no módulo Random.
  • Funcionalidades mais especializadas em pacotes que precisam ser instalados antes do uso (e que não estão disponíveis no Exercism).

Random faz parte da biblioteca padrão e provavelmente já vem pré-instalado, mas você vai precisar adicionar using Random no topo do seu programa para trazer o conteúdo dele para o namespace.

A função rand()

O que essa função faz depende dos argumentos que você passa para ela. Existem muitas opções.

Sem argumentos, ela gera um número de ponto flutuante entre 0 (inclusive) e 1. Essa é uma distribuição uniform, com todos os valores igualmente prováveis, como discutido na seção Trabalhando com Distribuições, abaixo.

Um único argumento inteiro gera um vetor desse comprimento.

julia> rand()
0.10261774967264703

julia> rand(5)
5-element Vector{Float64}:
 0.24134501977563894
 0.5664193284851202
 0.9804412082089355
 0.6229551330613335
 0.47589221741904664

Para um intervalo diferente, basta deslocar e escalar o resultado de forma adequada.

O exemplo abaixo usa broadcasting para a subtração, abordado no Conceito de Operações com Vetores. O .- simplesmente aplica essa aritmética a cada elemento do vetor.

# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
 -0.5303906759076336
  0.9635682226775855
 -0.048823697086981754
  0.465842804648374
  0.9880834344780736

Com um tipo como único argumento, rand vai usar typemin e typemax como limites. Provavelmente não é isso que você quer!

Para números inteiros aleatórios, podemos fornecer um intervalo e, opcionalmente, quantos valores gerar.

julia> rand(Int64)
-9159538335234594326 # not very useful

julia> rand(1:10, 5)
5-element Vector{Int64}:
 1
 1
 1
 4
 7

No exemplo rand(1:10, 5) acima, repare que há valores repetidos (por coincidência), porque cada escolha é independente. Isso é "amostragem com reposição", discutida em mais detalhes abaixo.

Para valores de ponto flutuante em um intervalo, você geralmente vai precisar especificar um tamanho de passo. Caso contrário, o passo assume o valor padrão 1,0, que raramente é útil.

julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
 3.19
 2.53
 3.14
 3.13

Como alternativa, forneça um array ou uma tupla, e rand vai retornar uma entrada aleatória:

julia> rand([4, 9, 16, 25])
16

# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)

# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
 1
  "name"

Amostragem com ou sem reposição

Imagine que temos um saco contendo 3 bolas vermelhas e 4 bolas verdes, e retiramos uma bola aleatoriamente do saco. Para pegar uma segunda bola, há duas possibilidades:

  1. Devolver a primeira bola ao saco e sacudir tudo bem antes de tirar outra. O número de bolas agora é o mesmo de antes (7), e a proporção entre vermelhas e verdes também é a mesma.
  2. Colocar a primeira bola sobre a mesa antes de tirar uma segunda. Agora há apenas 6 bolas no saco, e a proporção vermelho:verde depende da cor da primeira bola.

O cenário 1 é com reposição, o cenário 2 é sem, e eles dão resultados diferentes.

Para simular amostragem sem reposição no Julia, há algumas opções.

A mais simples (e, no Exercism, a única opção) é usar Random.shuffle() para colocar as entradas em ordem aleatória e depois usar os primeiros n elementos. Isso funciona bem para problemas pequenos, mas pode não escalar bem para coleções grandes: shuffle precisa gerar o array completo, mesmo que você queira só uma pequena fração dele.

Para fazer amostragem com reposição "do jeito certo", instale o pacote StatsBase.jl. Ele fornece a função sample() com uma gama completa de opções.

Podemos razoavelmente esperar que uma funcionalidade semelhante seja adicionada ao Random em uma versão futura, para que passe a fazer parte da biblioteca padrão (os exemplos de código neste documento foram testados com Julia 1.11).

Trabalhando com Distribuições

Até agora, nos concentramos em casos em que todos os resultados são igualmente prováveis. Por exemplo, rand(1:100) tem a mesma probabilidade de dar qualquer número inteiro de 1 a 100.

Muitas situações do mundo real são bem menos simples do que isso. Por isso, os estatísticos criaram uma grande variedade de distributions para descrever resultados "do mundo real" matematicamente.

Distribuições uniformes

A função rand() descrita acima é usada quando todas as probabilidades são iguais. Isso é chamado de distribuição uniform.

Distribuição gaussiana

Também chamada de distribuição "normal" ou curva "em forma de sino", é uma forma muito comum de descrever imprecisão em valores medidos.

Por exemplo, suponha que a fábrica onde você trabalha acabou de comprar 10.000 parafusos que deveriam ser idênticos. Você quer configurar o robô da fábrica para lidar com eles, então pesa uma amostra de 100 e descobre que eles têm um peso médio (ou mean) de 4,731g. É extremamente improvável que isso signifique que todos pesem exatamente 4,731g. Talvez você descubra que os valores variam de 4,627 a 4,794g, mas se concentram em torno de 4,731g.

Essa é a Gaussian distribution, na qual as probabilidades atingem o pico na média e caem simetricamente dos dois lados (daí "em forma de sino"). Para simular isso em software, precisamos de alguma forma de especificar a largura da curva (normalmente, parafusos caros se concentram mais perto da média do que parafusos baratos!).

Por convenção, isso é feito com o standard deviation: valores pequenos para uma curva pontuda e estreita, valores grandes para uma curva baixa e larga. Os matemáticos adoram letras gregas, então usamos μ ("mi") para representar a média e σ ("sigma") para representar o desvio padrão. Assim, se você ler que "95% dos valores estão dentro de 2σ de μ" ou que "o bóson de Higgs foi detectado com confiança de 5 sigmas", esses comentários têm a ver com o desvio padrão.

Haverá mais a dizer sobre isso no Conceito Statistics.

A função randn()

Abreviação de "random normal", ela é parecida com a variante de ponto flutuante de rand(), exceto que os valores são distribuídos como uma gaussiana com média 0 e desvio padrão 1.

Novamente, você pode querer escalar a saída bruta de randn para o desvio padrão e deslocá-la para a média. O exemplo abaixo converte para média 30 e desvio padrão 5.

julia> raw = randn(5)
5-element Vector{Float64}:
  3.0762588867281475
  1.5101100620253902
 -0.5914858221637778
  0.684175554069735
 -0.8416433926114673

julia> raw * 5 .+ 30
5-element Vector{Float64}:
 45.38129443364074
 37.55055031012695
 27.04257088918111
 33.420877770348675
 25.791783036942665

É difícil perceber, olhando para a saída, que a saída bruta se concentra mais perto de zero do que numa distribuição uniforme. Se você duvidar, gere 1000 ou mais e faça um gráfico para deixar isso mais óbvio.

O módulo Random

Esse módulo contém o próximo nível de funcionalidade, omitido de Base para ajudar a minimizar o tamanho da configuração padrão do Julia.

Random complementa rand e randn de Base com versões mutantes, rand! e randn!.

Uma adição útil é randstring, que gera uma string de determinado comprimento. Por padrão, ela usa letras maiúsculas e minúsculas mais os dígitos de 0 a 9, mas outras escolhas podem ser especificadas.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

Além disso, existe a função bitrand para gerar um BitArray aleatório de comprimento especificado.

julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
 1
 1
 0
 0
 1

Embaralhamentos e permutações

Para embaralhar aleatoriamente as entradas de um Vector, temos shuffle; e também shuffle! para modificar o vetor de entrada no lugar.

julia> v = ['A', '1', '2', 'J', 'Q', 'K'];

julia> shuffle(v)
6-element Vector{Char}:
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)

# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
 '2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
 'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
 'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
 '1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)

Às vezes é útil ter os índices embaralhados em vez disso. Para isso, use randperm(n), em que n é o comprimento da sequência.

julia> randperm(6)
6-element Vector{Int64}:
 6
 2
 4
 1
 3
 5

Na prática, o exemplo acima dá os mesmos resultados que shuffle(1:6).

Instruções

Mary é uma grande fã da série de TV Star Trek: The Next Generation. Ela costuma jogar RPG de mesa, em que ela e os amigos fingem ser a tripulação da Starship Enterprise. A personagem de Mary é o Capitão Picard, o que significa que ela precisa manter o diário de bordo do capitão. Ela adora a parte criativa do jogo, mas não gosta de gerar dados aleatórios na hora.

Ajude Mary criando geradores aleatórios para dados que costumam aparecer no diário de bordo do capitão.

1. Gere um planeta aleatório

A Starship Enterprise encontra muitos planetas em suas viagens. Os planetas do universo de Star Trek são divididos em categorias com base em suas propriedades. Por exemplo, a Terra é um planeta classe M. Todas as classes planetárias possíveis são: D, H, J, K, L, M, N, R, T e Y.

Implemente a função random_planet. Ela deve retornar uma das classes planetárias de forma aleatória.

julia> random_planet_class()
'K'

2. Gere um número de registro aleatório de nave estelar

A Enterprise (número de registro NCC-1701) não é a única nave estelar voando por aí! Quando ela se encontra com outra nave estelar, Mary precisa registrar o número de registro dessa nave.

Os números de registro começam com o prefixo "NCC-" e depois usam um número de 1000 a 9999 (inclusive).

Implemente a função random_ship_registry_number, que retorna um número de registro aleatório de nave estelar.

julia> random_ship_registry_number()
"NCC-1947"

3. Gere uma data estelar aleatória

De que serve um diário de bordo se ele não inclui datas?

Uma data estelar é um número de ponto flutuante. As aventuras da Starship Enterprise na primeira temporada de The Next Generation acontecem entre as datas estelares 41000.0 e 42000.0. O "4" representa o século 24, e o "1", a primeira temporada.

Implemente a função random_stardate, que retorna um número de ponto flutuante entre 41000.0 (inclusive) e 42000.0.

julia> random_stardate()
41458.15721310934

4. Gere uma data estelar arredondada

Houve reclamações sobre haver pontos decimais demais nas datas estelares, então Mary decide arredondar os registros para uma casa decimal.

Embora isso pudesse ser feito arredondando valores de random_stardate(), você percebe que existe uma abordagem muito mais simples.

Implemente a função random_stardate_v2, que retorna um número de ponto flutuante entre 41000.0 e 42000.0 (inclusive), com uma única casa decimal.

julia> random_stardate_v2()
41732.6

5. Escolha algumas naves estelares aleatórias de uma lista.

Uma das versões do jogo envolve interagir com outras naves estelares, e Mary precisa de uma seleção.

Existe uma lista completa de naves estelares possíveis, mas só algumas são necessárias em cada partida, e elas devem ser escolhidas aleatoriamente.

Implemente a função pick_starships(starships, number_needed), que recebe um vetor de números de registro únicos de naves estelares e retorna um vetor de comprimento number_needed. Os valores do resultado devem ser retirados de starships, sem duplicatas.

julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
 "NCC-1228"
 "NCC-5011"

5a. Opcionalmente, pense em outros tipos de argumento.

Restringimos o argumento starships a ser um vetor. Como você implementaria pick_starships() para funcionar com outros iteráveis, como tuplas ou conjuntos?

Não há testes para esta parte.

Editar via GitHub O link abre em uma nova janela ou aba
Julia Exercism

Tudo pronto para começar Diário de bordo do capitão?

Crie sua conta no Exercism para aprender e dominar Julia com 35 conceitos128 exercícios e mentoria humana de verdade, tudo de graça.