Треки
/
Julia
Julia
/
Вправи
/
Капітанський журнал
Капітанський журнал

Капітанський журнал

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

Вступ

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

До звичних і знайомих прикладів належать:

  • Підкидання монети: випадкове значення з ('H', 'T').
  • Кидок кубика: випадкове ціле число від 1 до 6.
  • Перемішування колоди карт: випадковий порядок масиву карт.

Генерування справді випадкових значень на компʼютері - напрочуд складна технічна задача, тож ці результати можуть називати «псевдовипадковими».

Важливо: ця концепція не охоплює криптографічно стійкі випадкові числа, які становлять значно складнішу задачу.

Утім, добре спроєктовані бібліотеки, як-от модуль Random зі стандартної бібліотеки Julia, швидкі, гнучкі й дають результати, цілком достатні для більшості застосунків у моделюванні, симуляції та іграх.

Julia розділяє функціональність випадкових чисел між кількома місцями:

  • Лише кілька базових, але дуже універсальних функцій у Base, які доступні завжди.
  • Ширший набір можливостей у модулі Random.
  • Спеціалізованіша функціональність у пакетах, які потрібно встановити перед використанням (і які недоступні на Exercism).

Random входить до складу стандартної бібліотеки й, імовірно, уже встановлений, але нам потрібно додати using Random на початку програми, щоб його вміст потрапив у простір імен.

Функція rand()

Що робить ця функція, залежить від аргументів, які ми їй передаємо. Варіантів дуже багато.

Без аргументів вона генерує число з плаваючою комою між 0 (включно) і 1. Це uniform-розподіл, де всі значення однаково ймовірні, як ми обговоримо в розділі «Робота з розподілами» нижче.

Один аргумент-ціле число генерує вектор такої довжини.

julia> rand()
0.10261774967264703

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

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

У наведеному нижче прикладі для віднімання використано трансляцію, про яку йдеться в концепції «Операції з векторами». Оператор .- просто застосовує цю арифметику до кожного елемента вектора.

# 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

Якщо єдиний аргумент - це тип, rand використає typemin і typemax як межі. Імовірно, це не те, що нам потрібно!

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

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

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

У прикладі rand(1:10, 5) вище звернімо увагу, що там (випадково) є повторювані значення, бо кожен вибір незалежний. Це «вибірка з поверненням», про яку докладніше йдеться нижче.

Для чисел з плаваючою комою в діапазоні зазвичай потрібно вказати крок. Інакше крок типово дорівнюватиме 1.0, що рідко буває корисним.

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

Або ж передаймо масив чи кортеж, і rand поверне випадковий елемент:

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"

Вибірка з поверненням і без нього

Уявімо, що в нас є торбина з 3 червоними та 4 зеленими кульками, і ми випадково витягуємо з неї одну кульку. Щоб дістати другу кульку, є два варіанти:

  1. Повернути першу кульку в торбину, добре все струсити й аж тоді витягнути наступну. Кульок тепер стільки ж, скільки було (7), і співвідношення червоних до зелених теж не змінилося.
  2. Покласти першу кульку на стіл і лише потім витягнути другу. Тепер у торбині лише 6 кульок, і співвідношення червоних і зелених залежить від кольору першої кульки.

Сценарій 1 - це вибірка з поверненням, сценарій 2 - без повернення, і вони дають різні результати.

Щоб змоделювати вибірку без повернення в Julia, є кілька варіантів.

Найпростіший (і в межах Exercism єдиний) - скористатися Random.shuffle(), щоб розташувати елементи у випадковому порядку, а потім узяти перші n елементів. Це годиться для невеликих задач, але може погано масштабуватися на великі колекції: shuffle має згенерувати весь масив, навіть якщо нам потрібна лише його мала частка.

Щоб робити вибірку з поверненням «як слід», установімо пакет StatsBase.jl. Він надає функцію sample() з повним набором можливостей.

Цілком можна сподіватися, що подібну функціональність додадуть до Random у майбутньому випуску й зроблять її частиною стандартної бібліотеки (приклади коду в цьому документі перевірено на Julia 1.11).

Робота з розподілами

Досі ми розглядали випадки, де всі результати рівноймовірні. Наприклад, rand(1:100) з однаковою ймовірністю дає будь-яке ціле число від 1 до 100.

Багато реальних ситуацій значно складніші за це. Тому статистики створили безліч distributions, щоб математично описати результати з «реального світу».

Рівномірні розподіли

Функцію rand(), описану вище, використовують, коли всі ймовірності рівні. Такий розподіл називають uniform.

Розподіл Гауса

Його також називають «нормальним» розподілом або «дзвоноподібною» кривою; це дуже поширений спосіб описати неточність виміряних значень.

Наприклад, уявімо, що завод, де ми працюємо, щойно купив 10 000 болтів, які мали б бути однаковими. Нам потрібно налаштувати заводського робота, щоб він із ними працював, тож ми зважуємо вибірку зі 100 болтів і бачимо, що їхня середня (або mean) вага становить 4,731 г. Це аж ніяк не означає, що всі вони важать рівно 4,731 г. Можливо, ми виявимо, що значення лежать у межах від 4,627 до 4,794 г, але гуртуються навколо 4,731 г.

Це Gaussian distribution, у якому ймовірності сягають піку на середньому значенні й симетрично спадають з обох боків (звідси й «дзвоноподібна» форма). Щоб змоделювати його програмно, нам потрібен спосіб задати ширину кривої (зазвичай дорогі болти гуртуються щільніше навколо середнього, ніж дешеві!).

За домовленістю для цього використовують standard deviation: малі значення для гострої, вузької кривої, великі - для низької, широкої. Математики люблять грецькі літери, тож середнє значення позначають μ («мю»), а стандартне відхилення - σ («сигма»). Тож якщо ми читаємо, що «95% значень лежать у межах 2σ від μ» або що «бозон Гігса виявлено з достовірністю 5 сигм», ці зауваження стосуються стандартного відхилення.

Докладніше про це йтиметься в концепції Statistics.

Функція randn()

Скорочення від «random normal»; ця функція подібна до варіанта rand() для чисел з плаваючою комою, але її значення розподілені за Гаусом із середнім 0 і стандартним відхиленням 1.

Знову ж таки, нам може знадобитися масштабувати необроблені вихідні дані randn під потрібне стандартне відхилення й зсунути їх під потрібне середнє. Приклад нижче приводить їх до середнього 30 і стандартного відхилення 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

Дивлячись на вихідні дані, важко помітити, що необроблені значення гуртуються ближче до нуля, ніж за рівномірного розподілу. Якщо сумніваємося, згенеруймо 1000 або більше значень і побудуймо їхній графік, щоб це стало очевидніше.

Модуль Random

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

Random доповнює rand і randn з Base версіями з мутацією: rand! і randn!.

Корисне доповнення - це randstring, яка генерує рядок тексту (англ. string) заданої довжини. Типово вона використовує великі та малі літери, а також цифри від 0 до 9, але можна вказати й інші набори символів.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

Крім того, є функція bitrand, яка генерує випадковий BitArray заданої довжини.

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

Перемішування й перестановки

Щоб випадково перемішати елементи Vector, є shuffle, а також shuffle!, яка змінює вхідний вектор на місці.

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)

Іноді замість цього корисно мати перемішані індекси. Для цього використовуймо randperm(n), де n - довжина послідовності.

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

По суті, приклад вище дає ті самі результати, що й shuffle(1:6).

Вказівки

Мері дуже любить телесеріал Star Trek: The Next Generation. Вона часто грає в настільні рольові ігри, де вони з друзями уявляють себе екіпажем Starship Enterprise. Серед її персонажів є капітан Пікар, а отже, вона має вести журнал капітана. Вона любить творчу частину гри, але не любить генерувати випадкові дані нашвидкуруч.

Допоможімо Мері, створивши генератори випадкових даних для того, що часто трапляється в журналі капітана.

1. Згенеруйте випадкову планету

У своїх подорожах Starship Enterprise зустрічає багато планет. Планети у всесвіті Star Trek поділяються на категорії за своїми властивостями. Наприклад, Земля належить до планет класу M. Усі можливі класи планет: D, H, J, K, L, M, N, R, T і Y.

Реалізуйте функцію random_planet. Вона має повертати один із класів планет випадковим чином.

julia> random_planet_class()
'K'

2. Згенеруйте випадковий реєстраційний номер зорельота

У космосі літає не лише Enterprise (реєстраційний номер NCC-1701)! Коли він зустрічається з іншим зорельотом, Мері потрібно занотувати реєстраційний номер цього зорельота.

Реєстраційні номери починаються з префікса «NCC-», після якого йде число від 1000 до 9999 (включно).

Реалізуйте функцію random_ship_registry_number, яка повертає випадковий реєстраційний номер зорельота.

julia> random_ship_registry_number()
"NCC-1947"

3. Згенеруйте випадкову зоряну дату

Яка користь із журналу, якщо в ньому немає дат?

Зоряна дата - це число з плаваючою комою. Пригоди Starship Enterprise з першого сезону The Next Generation відбуваються між зоряними датами 41000.0 і 42000.0. «4» означає 24 століття, а «1» - перший сезон.

Реалізуйте функцію random_stardate, яка повертає число з плаваючою комою між 41000.0 (включно) і 42000.0.

julia> random_stardate()
41458.15721310934

4. Згенеруйте округлену зоряну дату

Були скарги, що в зоряних датах забагато десяткових знаків, тож Мері вирішує округлювати записи до одного десяткового знака.

Хоча це можна було б зробити, округливши значення з random_stardate(), ми розуміємо, що є значно простіший підхід.

Реалізуйте функцію random_stardate_v2, яка повертає число з плаваючою комою між 41000.0 і 42000.0 (включно), з одним десятковим знаком.

julia> random_stardate_v2()
41732.6

5. Виберіть кілька випадкових зорельотів з масиву.

Одна з версій гри передбачає взаємодію з іншими зорельотами, і Мері потрібен якийсь вибір.

Існує повний масив можливих зорельотів, але в кожній грі потрібно лише кілька з них, і вибирати їх слід випадковим чином.

Реалізуйте функцію pick_starships(starships, number_needed), яка приймає вектор унікальних реєстраційних номерів зорельотів і повертає вектор довжиною number_needed. Значення результату мають братися з starships, без повторів.

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

5a. За бажанням поміркуйте про інші типи аргументів.

Ми обмежили аргумент starships так, щоб він був вектором. Як реалізувати pick_starships(), щоб вона працювала з іншими ітерованими обʼєктами, як-от кортежі чи множини?

Для цієї частини тестів немає.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Julia Exercism

Час розпочати Капітанський журнал?

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