Одной из функций приложения, над которым я работал, было показывать набор случайных цифр. Стало интересно получится ли найти правило по которому можно соотнести каждый такой набор с целым числом. Так я смогу получать число из ГПСЧ, а все итоговые строки будут получаться с одинаковой долей вероятности.
Первое, что должно прийти в голову - это преобразовывать число в строку без изменений. То есть, например, 123
в "123"
. Сразу бросится в глаза, что нужно как-то разделять "42"
и "042"
, и подобные. Поэтому вариант без изменений числа не может подойти.
Для демо версии приложения я использовал алгоритм с использованием условного оператора. Допустим ожидается что итоговая строка будет не больше двух символов. Значит двузначные числа можно преобразовывать в строку без изменений. Ведь 42
преобразуется в "42"
и никак иначе. А если генерировать числа не от 0
, а от -10
, то можно вручную разделить их на числа с нулём в начале и без.
Пример кода на Kotlin:
/**
* -10 -> "0"
* -9 -> "1"
* -1 -> "9"
* 0 -> "00"
* 9 -> "09"
* 99 -> "99"
*/
fun getFormattedString(number: Int): String {
require(number in -10..99)
return if (number < 0) {
(number + 10).toString()
} else {
"%02d".format(number)
}
}
То есть, например, 42
преобразуется в "42"
, 9
в "09"
, а -9
в "1"
.
Очевидно такая реализация слишком неповоротлива для модернизации под решение задачи в общем случае. Во время экстраполяции этой идеи на числа больших разрядов я обратил внимание на закономерность - строки состоящие из нулей соотносятся с числом которое из себя представляет сумму десяток в степени.
10^n + 10^(n-1) + ... + 10^2 + 10^1
То есть, например для трёхзначных чисел будет справедливо что -100
преобразится в "00"
, или -110
в "0"
. Эти частные случаи суммы ряда можно вычислить и использовать как точку опоры вместо строгих условий вроде number < 0
. Это должно работать с разными длинами строк, а значит и разными разрядами чисел.
Я взял число, которое находится между суммами ряда. Прибавил к нему сумму ряда и получил число которое я ожидаю увидеть в строке. Теперь не надо выделять часть чисел в положительную сторону, одно правило должно работать в общем случае. А степень ряда равна длине итоговой строки (слева добавляются нули если не хватает).
Допустим ожидаемая длина равна 3
. Тогда будет справедливо следующее:
-1110 -> " 0" -> -1110 + 10^3 + 10^2 + 10^1
-1106 -> " 4" -> -1106 + 10^3 + 10^2 + 10^1
-1100 -> " 00" -> -1100 + 10^3 + 10^2
-1058 -> " 42" -> -1058 + 10^3 + 10^2
-1000 -> "000" -> -1000 + 10^3
-958 -> "042" -> -958 + 10^3
-1 -> "999" -> -1 + 10^3
Как я говорил ранее мы имеем частный случай суммы ряда вроде a * r + a * r^2 + ... + a * r^(n-1) + a * r^n
. Где коэффициент a равен 1
, r равен 10
, а n
равен заданной длине искомой строки. Допустим у нас есть функция getSumOfSeries
, которая вычисляет такую сумму. При ближайшем рассмотрении видно что дело придётся иметь не просто с суммами рядов, а с разностью сумм рядов. То есть, например 1100
- это результат разности суммы ряда где n
равно 3
и суммы ряда где n
равно 1
. А разность 1110 - 110
(тут n
равны 3
и 2
соответственно), полученная по такому же принципу, будет равна 1000
.
1100 = 1110 - 10 = (10^3 + 10^2 + 10^1) - 10^1
1000 = 1110 - 110 = (10^3 + 10^2 + 10^1) - (10^2 + 10^1)
Так я придумал функцию, которая в цикле проверяет к какой разности сумм рядов относится сгенерированное число. И формирует из него строку, используя полученные в результате поиска значения коэффициентов:
fun getFormattedString(number: Int, length: Int): String {
val sum = getSumOfSeries(n = length)
for (i in 1..length) {
if (number < getSumOfSeries(n = i) - sum) {
val formatted = number + sum - getSumOfSeries(n = i - 1)
return "%${length}s".format("%0${i}d".format(formatted.toInt()))
}
}
error("Impossible!")
}
Сразу в голову не пришло что раз мы работаем с разностью сумм, то ту самую разность можно получать сразу. Но сейчас мы вычисляем сумму ряда в одном месте, а затем получаем ещё в двух местах результат разности с другими суммами рядов. Ведь мы знаем формулу по которой вычисляется сумма ряда. Теперь можно представить ряд a*r^n + a*r^(n - 1) + ... + a*r^(m + 1) + a*r^m
как разность суммы ряда со степенью n
и ряда со степенью m
.
gSOS = getSumOfSeries
10^n + 10^(n-1) + ... + 10^(m+1) + 10^m + 10^(m-1) + ... + 10^1 = gSOS(n)
| |
10^m + 10^(m-1) + ... + 10^1 = gSOS(m)
|
10^n + 10^(n-1) + ... + 10^(m+1) | = gSOS(n) - gSOS(m)
Функция вычисляющая частный случай суммы ряда от 1
до n
теперь вычисляет общий случай суммы ряда от n
до m
:
fun getSumOfSeries(
a: Int = 1,
r: Double = 10.0,
m: Int = 0,
n: Int,
): Double {
require(m >= 0)
require(n >= m)
return r * a * (r.pow(m) - r.pow(n)) / (1.0 - r)
}
А функцию формирующую строку можно сократить избавившись от получения разности сумм вручную:
fun getFormattedString(number: Int, length: Int): String {
for (i in 1..length) {
if (number < -getSumOfSeries(n = length, m = i)) {
val formatted = number + getSumOfSeries(n = length, m = i - 1)
return "%${length}s".format("%0${i}d".format(formatted.toInt()))
}
}
error("Impossible!")
}
Ещё лучше, но для вычисления всё ещё используются управляющие конструкции вроде цикла и условия. Я обратил внимание на то, что коэффициент, который мы ищем в цикле, есть степень суммы ряда, на которую мы опираемся.
10^n + 10^(n-1) + ... + 10^2 + 10^1 = (1 - 10^n) / (1 - 10) = Sn
n = log10(1 - Sn * (1 - 10) / 10)
Зная сумму ряда, мы можем вычислить степень ряда:
fun getPowerOfSeries(
a: Int = 1,
r: Double = 10.0,
sum: Double
): Double {
return kotlin.math.log(1 - sum * (1.0 - r) / (a * r), r)
}
Если рассматривать числа между суммами ряда как значения суммы некоторого ряда десяток в степенях, то, вычисляя степень такого ряда, мы будем получать нецелое число как раз между теми которые нам нужны.
-1110 -> " 0" -> m = log10(1 - (1110 - 1110) * (-9) / 10) = 0
-1106 -> " 4" -> m = log10(1 - (1110 - 1106) * (-9) / 10) = 0
-1100 -> " 00" -> m = log10(1 - (1110 - 1100) * (-9) / 10) = 1
-1058 -> " 42" -> m = log10(1 - (1110 - 1058) * (-9) / 10) = 1
-1000 -> "000" -> m = log10(1 - (1110 - 1000) * (-9) / 10) = 2
-958 -> "042" -> m = log10(1 - (1110 - 958) * (-9) / 10) = 2
-1 -> "999" -> m = log10(1 - (1110 - 1) * (-9) / 10) = 2
Используя только целую часть мы будем получать как раз необходимый нам коэффициент:
fun getFormattedString(number: Int, length: Int): String {
val m = getPowerOfSeries(sum = getSumOfSeries(n = length) + number).toInt()
val formatted = getSumOfGeometricSeries(n = length, m = m) + number
return "%${length}s".format("%0${m + 1}d".format(formatted.toInt()))
}
Теперь мы избавились от управляющих конструкций. Но теперь кажется что мы получаем случайные числа отрицательными только по историческим причинам. Оказалось что с положительными числами предложенный алгоритм работает ещё лучше. Получать степень ряда можно просто из заданного числа без предварительной обработки (ранее мы складывали его с суммой ряда). И если вычесть этого числа сумму ряда по полученной степени то мы получим искомое число, которое остаётся отформатировать по той же полученной степени.
gSOS = getSumOfSeries
gPOS = getPowerOfSeries
0 -> 0 - gSOS(gPOS( 0)) -> 0 - gSOS(0) -> 0 - 0 -> " 0"
4 -> 4 - gSOS(gPOS( 4)) -> 4 - gSOS(0) -> 4 - 0 -> " 4"
10 -> 10 - gSOS(gPOS( 10)) -> 10 - gSOS(1) -> 10 - 10 -> " 00"
58 -> 58 - gSOS(gPOS( 58)) -> 58 - gSOS(1) -> 58 - 10 -> " 42"
110 -> 110 - gSOS(gPOS( 110)) -> 110 - gSOS(2) -> 110 - 110 -> "000"
168 -> 168 - gSOS(gPOS( 168)) -> 168 - gSOS(2) -> 168 - 110 -> "042"
1109 -> 1109 - gSOS(gPOS(1109)) -> 1109 - gSOS(1) -> 1109 - 110 -> "999"
Главное не забыть проверить данные на входе. Заданное число должно быть в диапазоне от 0
(включительно) до суммы ряда по степени равной длине целевой строки (не включительно). Так же можно ограничить ожидаемую длину от 1
до 9
. Таким образом заданное число ожидается минимум 0
, а максимум 1_111_111_109
. Что впритык влезает в положительный диапазон знаковых целочисленных 32х разрядных (для длины 10
уже не влезет).
fun Int.formatted(length: Int): String {
require(length in 1..9)
require(this in 0 until getSumOfSeries(n = length).toInt())
val n = getPowerOfSeries(sum = toDouble()).toInt()
val formatted = this - getSumOfSeries(n = n)
return "%${length}s".format("%0${n + 1}d".format(formatted.toInt()))
}
Строгих требований к приложению не было, как и конкретных сроков сдачи. Смог позволить себе подойти творчески к решению банальных задач. Заодно вспомнил школьный курс алгебры. Теперь на каждое целое число в определённом диапазоне, которые берутся из ГПСЧ с равной долей вероятности, можно получить все необходимые комбинации цифр. Итоговая функция удовлетворяет поставленным требованиям.
Комментарии (13)
sophist
20.04.2023 14:50+1А если генерировать числа не от
0
, а от-10
, то можно вручную разделить их на числа с нулём в начале и без.А если генерировать числа от
10^(series_length)
, то можно просто игнорировать лидирующую единицу и в ус не дуть.lorc
20.04.2023 14:50Не, если я правильно понял что автор хотел сделать, то так не получится. Потому что код должен уметь генерировать "0", "1", "00", "01", "001", "000" и все в том же духе. А у вас - получится только 000 и 001.
sophist
20.04.2023 14:50Отнюдь. Чтобы генерировать строки разной длины, надо просто варьировать параметр series_length.
lorc
20.04.2023 14:50Таки "нюдь".
надо просто варьировать параметр series_length.
Ну это простой и очевидный вариант Можно вообще просто в цикле генерировать цифры и не париться. А вы попробуйте напишите функцию которая одно случайное число, отображает в эту самую последовательность цифр переменной длины. Да еще и так, чтобы распределение сего отображения было равномерным. Я так понимаю, это и было целью данной работы.
lorc
Извините, но не совсем понятно какую задачу вы пытаетесь решить.
Вам нужно отобразить M случайных цифр, при том что само M тоже случайно и меньше заданного N?
И вы решили найти функцию (в математическом смысле) которая отображает число X в эту самую последовательность цифр?
kepocnhh Автор
Вы правильно поняли. Это статья про поиск той самой функции. В приложении требовалось выводить последовательности от 0 до 999, но я хотел посмотреть насколько сильно это можно абстрагировать. Почти все методы которые первыми приходят в голову не справляются с нулями слева (01, 001 и тд), поэтому так увлёкся.
lorc
Ну вам большой плюс что вы выразили это в виде алгебраической формулы, конечно. Но вообще математики не гнушаются использовать if/switch при записи функций. Функция Дирихле, как тому пример.
Кстати, интересно почему у вас получилось 1_111_111_109, а не 1_111_111_110?
Для случая с тремя цифрами:
У нас :
1000 комбинаций 000-999
100 комбинаций 00-99
10 "комбинаций" 0-9
(и еще 1 "комбинация" для пустой строки).
Если пустую строку не учитывать, то получается 1110 возможных комбинаций. А вы одну где-то потеряли.
kepocnhh Автор
"заданное число ожидается минимум
0
, а максимум1_111_111_109
" речь об ожидаемом числе которое принимает функция, не о количестве комбинаций. То есть комбинаций 1_111_111_110, это верно.