Как придумал себе (проблему) задачу и вспомнил школьный курс алгебры

от автора

Одной из функций приложения, над которым я работал, было показывать набор случайных цифр. Стало интересно получится ли найти правило по которому можно соотнести каждый такой набор с целым числом. Так я смогу получать число из ГПСЧ, а все итоговые строки будут получаться с одинаковой долей вероятности.

Первое, что должно прийти в голову — это преобразовывать число в строку без изменений. То есть, например, 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())) } 

Строгих требований к приложению не было, как и конкретных сроков сдачи. Смог позволить себе подойти творчески к решению банальных задач. Заодно вспомнил школьный курс алгебры. Теперь на каждое целое число в определённом диапазоне, которые берутся из ГПСЧ с равной долей вероятности, можно получить все необходимые комбинации цифр. Итоговая функция удовлетворяет поставленным требованиям.


ссылка на оригинал статьи https://habr.com/ru/articles/730508/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *