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

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


  1. lorc
    20.04.2023 14:50
    +3

    Извините, но не совсем понятно какую задачу вы пытаетесь решить.

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

    Вам нужно отобразить M случайных цифр, при том что само M тоже случайно и меньше заданного N?

    И вы решили найти функцию (в математическом смысле) которая отображает число X в эту самую последовательность цифр?


    1. kepocnhh Автор
      20.04.2023 14:50

      Вы правильно поняли. Это статья про поиск той самой функции. В приложении требовалось выводить последовательности от 0 до 999, но я хотел посмотреть насколько сильно это можно абстрагировать. Почти все методы которые первыми приходят в голову не справляются с нулями слева (01, 001 и тд), поэтому так увлёкся.


      1. lorc
        20.04.2023 14:50

        Ну вам большой плюс что вы выразили это в виде алгебраической формулы, конечно. Но вообще математики не гнушаются использовать if/switch при записи функций. Функция Дирихле, как тому пример.

        Кстати, интересно почему у вас получилось 1_111_111_109, а не 1_111_111_110?

        Для случая с тремя цифрами:

        У нас :

        1000 комбинаций 000-999

        100 комбинаций 00-99

        10 "комбинаций" 0-9

        (и еще 1 "комбинация" для пустой строки).

        Если пустую строку не учитывать, то получается 1110 возможных комбинаций. А вы одну где-то потеряли.


        1. kepocnhh Автор
          20.04.2023 14:50

          "заданное число ожидается минимум 0, а максимум 1_111_111_109" речь об ожидаемом числе которое принимает функция, не о количестве комбинаций. То есть комбинаций 1_111_111_110, это верно.


  1. sophist
    20.04.2023 14:50
    +1

    А если генерировать числа не от 0, а от -10, то можно вручную разделить их на числа с нулём в начале и без.

    А если генерировать числа от 10^(series_length), то можно просто игнорировать лидирующую единицу и в ус не дуть.


    1. lorc
      20.04.2023 14:50

      Не, если я правильно понял что автор хотел сделать, то так не получится. Потому что код должен уметь генерировать "0", "1", "00", "01", "001", "000" и все в том же духе. А у вас - получится только 000 и 001.


      1. sophist
        20.04.2023 14:50

        Отнюдь. Чтобы генерировать строки разной длины, надо просто варьировать параметр series_length.


        1. lorc
          20.04.2023 14:50

          Таки "нюдь".

          надо просто варьировать параметр series_length.

          Ну это простой и очевидный вариант Можно вообще просто в цикле генерировать цифры и не париться. А вы попробуйте напишите функцию которая одно случайное число, отображает в эту самую последовательность цифр переменной длины. Да еще и так, чтобы распределение сего отображения было равномерным. Я так понимаю, это и было целью данной работы.


        1. kepocnhh Автор
          20.04.2023 14:50

          @lorc правильно подметил что основная проблема в нулях слева. Вы уточните как именно применить ваш метод чтобы получать последовательности вроде 01, 001 и тд.


  1. Naf2000
    20.04.2023 14:50

    Имхо, алгебры я тут не увидел. Только арифметику


    1. kepocnhh Автор
      20.04.2023 14:50

      Вспомнил как проходили ряды в школе на уроках алгебры. Поэтому такое название статьи.


  1. khash_math
    20.04.2023 14:50

    Мне тоже непонятно, что за задача.


    1. kepocnhh Автор
      20.04.2023 14:50

      Выше @lorc спросил. Можете присоединиться к обсуждению.