Количество монотонных чисел

Представим, что увеличивающиеся числа — это числа, цифры которых, при прочтении слева на право, всегда не меньше предыдущих, например: 234559.

И наоборот, в понижающихся числах цифры слева на право не возрастают, например: 97732.

Вам не нужно быть новым Гауссом, чтобы понять, что все числа до ста относятся к одной из этих двух категорий.

Сейчас вам надо сделать функцию, которая подсчитывает количество таких чисел до 10 в н-ной степени (агрумент функции).

Total Below
1 1
10 10
100 100
475 1000
1675 10000
4954 100000
12952 1000000
#include <string>
#include <cmath>
using namespace std; 
unsigned long long total_inc_dec(unsigned int n) {
  string curr_num; unsigned long long total = 0; int isInc;
  for (int i = 0; i < pow(10, n); i++) {
    curr_num = to_string(i);
    if (int(curr_num.length()) <= 2) {++total;}
    else {
      for (int it = 0; it < int(curr_num.length()); it++) {
        if (it==0) {
          if (curr_num[it] < curr_num[it+1]) {isInc = 1;}
          if (curr_num[it] == curr_num[it+1]) {isInc = 0;}
          if (curr_num[it] > curr_num[it+1]) {isInc = -1;}
        }
        else {
          if (curr_num[it] < curr_num[it+1]) {
              switch(isInc) {
                case 1:
                    if (it == int(curr_num.length()) - 2) {
                        ++total;
                    }
                    break;
                case 0: 
                    isInc = 1;
                    if (it == int(curr_num.length()) - 2) {
                        ++total;
                    }
                    break;
                case -1: 
                    it = int(curr_num.length());
                    break;
              }
          }
          if (curr_num[it] == curr_num[it+1]) {
              if (it == int(curr_num.length()) - 2) {
                ++total;
              }
          }
          if (curr_num[it] > curr_num[it+1]) {
            switch(isInc) {
                case -1:
                    if (it == int(curr_num.length()) - 2) {
                        ++total;
                    }
                    break;
                case 0: 
                    isInc = -1;
                    if (it == int(curr_num.length()) - 2) {
                        ++total;
                    }
                    break;
                case 1: 
                    it = int(curr_num.length());
                    break;
            }
          }
        }
      }
    }
  }
  return total;
}

Сегодня почти в первый раз услышал слово "оптимизировать". Вообще не знаю, как это сделать.


Ответы (1 шт):

Автор решения: Stanislav Volodarskiy

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

Текущее решение делает много лишней работы. Перебираются и проверяются все числа. Это не нужно. Например, если число имеет вид 132xxxxxxx, то его и всех его соседей можно пропустить. Они точно не пройдут проверку.

Короткая дорога подсказана Harry в комментарии к вопросу. Видите числовую последовательность? Отыщите её в энциклопедии OEIS: https://oeis.org/search?q=1+10+100+475+1675+4954+12952. Точного совпадения не будет, но будет ссылка на последовательность без единицы в начале: A364342. Читаем статью, убеждаемся что это наша последовательность, находим формулу, программируем, готово.

Рекурсивное решение

Если не повезло и последовательность не нашлась, придётся решать задачу головой. Будем считать отдельно возрастающие и отдельно убывающие числа.

Возрастающие числа

Нужен опыт чтобы догадаться до an,d - количество возрастающих чисел из ровно n цифр и первая цифра не меньше d. Факты:

  • an,9 = 1 – только одно возрастающее число из n цифр начинается с девятки;
  • a1,d = 10 - d – количество возрастающих чисел из одной цифры легко сосчитать;
  • an,d = an,d+1 + an-1,d – это важная формула. Если число начинается с цифры строго больше d, то такие числа "считает" первое слагаемое. Если число начинается с цифры равной d, то такие числа "считает" второе слагаемое. В этой формуле n > 1 и d < 9.

Нас интересуют все возрастающие числа из не более чем n цифр. Обозначим их через an. Обратите внимание на один индекс.

an = 1 + Σ1≤j≤n aj,1 – надо сложить возрастающие числа всех подходящих длин и добавить единицу для нуля.

Код прямо повторяет формулы:

unsigned long long a(unsigned n, unsigned d) {
    if (d == 9) {
        return 1;
    }
    if (n == 1) {
        return 10 - d;
    }
    return a(n, d + 1) + a(n - 1, d);
}

unsigned long long a(unsigned n) {
    unsigned long long s = 1;
    for (unsigned j = 1; j <= n; ++j) {
        s += a(j, 1);
    }
    return s;
}

Убывающие числа

Схема та же: bn,d - количество убывающих цепочек цифр (не чисел - здесь это важно) из ровно n цифр и первая цифра не больше d.

  • bn,0 = 1 – только одна убывающая цепочка из n цифр начинается с нуля;
  • b1,d = d + 1 – количество убывающих цепочек из одной цифры легко сосчитать;
  • bn,d = bn,d-1 + bn-1,d – если число начинается с цифры строго меньше d, то такие числа "считает" первое слагаемое. Если число начинается с цифры равной d, то такие числа "считает" второе слагаемое. В этой формуле n > 1 и d > 0.

Нас интересуют все убывающие числа (на этот раз не цепочки, числа) из не более чем n цифр. Обозначим их через bn. Снова обратите внимание на один индекс.

bn = (Σ1≤j≤n bj,9) - (n - 1) – надо сложить возрастающие цепочки всех подходящих длин и вычесть цепочки, которые не являются числами. Таких цепочек n - 1: 00, 000, 0000, ...

unsigned long long b(unsigned n, unsigned d) {
    if (d == 0) {
        return 1;
    }
    if (n == 1) {
        return d + 1;
    }
    return b(n, d - 1) + b(n - 1, d);
}

unsigned long long b(unsigned n) {
    unsigned long long s = 0;
    for (unsigned j = 1; j <= n; ++j) {
        s += b(j, 9);
    }
    return s - (n - 1);
}

"Монотонные" числа

Количество "монотонных" чисел cn:

  • c0 = 1 – последовательности a и b не определены для n = 0, требуется отдельное определение;

  • сn = an + bn - 9·n - 1 – число "монотонных" чисел это число возрастающих чисел плюс число убывающих чисел минус те числа, которые мы сосчитали два раза. Два раза сосчитан ноль и числа в которых все цифры одинаковы: 333, 9999. Таких чисел 9·n.

unsigned long long c(unsigned n) {
    if (n == 0) {
        return 1;
    }
    return a(n) + b(n) - 9 * n - 1;
}

Динамическое программирование

Это решение на моём железе считает c25 = 236030401 за одну секунду. Это лучше чем было, но можно ещё лучше. Функции a(n, d) и b(n, d) многократно вызываются для одних и тех же аргументов. Заменим их на таблицы:

unsigned long long a(unsigned n) {
    std::vector<std::array<unsigned long long, 10>> a(n + 1);
    for (unsigned i = 1; i < 10; ++i) {
        a[1][i] = 10 - i;
    }
    for (unsigned j = 2; j <= n; ++j) {
        a[j][9] = 1;
        for (unsigned i = 8; i > 0; --i) {
            a[j][i] = a[j][i + 1] + a[j - 1][i];
        }
    }

    unsigned long long s = 1;
    for (unsigned j = 1; j <= n; ++j) {
        s += a[j][1];
    }
    return s;
}

unsigned long long b(unsigned n) {
    std::vector<std::array<unsigned long long, 10>> b(n + 1);
    for (unsigned i = 1; i < 10; ++i) {
        b[1][i] = i + 1;
    }
    for (unsigned j = 2; j <= n; ++j) {
        b[j][0] = 1;
        for (unsigned i = 1; i < 10; ++i) {
            b[j][i] = b[j][i - 1] + b[j - 1][i];
        }
    }

    unsigned long long s = 0;
    for (unsigned j = 1; j <= n; ++j) {
        s += b[j][9];
    }
    return s - (n - 1);
}

Этот код считает быстро и правильно для n ≤ 375. c375 = 17980269677772943001. Дальше переполняется unsigned long long. Расчёт занимает микросекунды, можно было бы успокоится. Но есть способ быстрее и с меньшим количеством кода.

Сочетания

Возрастающие числа

Возьмём любое возрастающее число из не более чем n цифр, дополним его нулями слева до n цифр ровно. Обозначим di, 0 ≤ i ≤ 9 - количество цифр i в дополненой строке. Построим строку: d0 звёздочек, палка, d1 звёздочек, палка, ..., палка, d9 звёздочек. В получившейся строке будет ровно n звёздочек и девять палочек.

Например n = 14 и число 12229999. Тогда d0 = 6 – столько нулей нужно чтобы общая длина стала n. d1 = 1, d2 = 3, d9 = 4. Остальные значения - нули. Строка из звёздочек и палочек: ******|*|***|||||||****.

Имеем отображение, которое любое возрастающее число < 10n переводит в строку из n звёздочек и девяти палочек. Разные числа переходят в разные строки.

Обратно, любая строка из n звёздочек и девяти палочек переводится в число. В первой группе звёздочки заменяет на нули, во второй на единицы и так далее. Палочки удаляем, ведущие нули удаляем, получается возрастающее число.

И снова любая строка из n звёздочек и девяти палочек переводится в возрастающее число < 10n. Разные строки переводятся в разные числа.

Тогда количество возрастающих чисел и количество строк из n звёздочек и девяти палочек одинаково. Сосчитаем количество строк. Сколькими способами можно расставить девять палочек на n + 9 мест? В статье Сочетание сказано что это число сочетаний из n + 9 по 9:

(n+99) = Cn+99 = (n + 9)!/(n!·9!)

Убывающие числа ...

... подсчитываются немного сложнее. Снова нужно дополнить число нулями до длины n. На этот раз нужно будет вставить десять палочек, чтобы разделить одиннадцать групп – нули встречаются в начале и в конце числа. Числа отображаются в строки аналогично, но нет однозначного соответствия для нуля. Одному нулю соответствуют n + 1 строка. Это все строки в которых все десять палочек собраны вместе. Количество возрастающих чисел равно Cn+1010 - n.

Код

Функция cnk вычисляет число сочетаний по формулам:

  • Cn0 = 1;
  • Cn+1k+1 = (n + 1)Cnk / (k + 1).
#include <iostream>

unsigned long long cnk(unsigned long long n, unsigned long long k) {
    unsigned long long p = 1;
    for (unsigned long long i = 1; i <= k; ++i) {
        p = p * (n - k + i) / i;
    }
    return p;
}

unsigned long long mono(unsigned n) {
    return cnk(n + 9, 9) + cnk(n + 10, 10) - 10 * n - 1;
}

int main() {
    unsigned n;
    if (std::cin >> n) {
        std::cout << mono(n) << '\n';
    }
}

Этот код решает задачу для n ≤ 298. Дальше возникает переполнение. Если хотите увеличить диапазон, в этом ответе есть cnk, которая решит задачу для n ≤ 375, так же как вариант в главке Динамическое программирование.

→ Ссылка