C/C++

Leetcode. 2221. Треугольная сумма массива. Казалось бы, при чём здесь Паскаль?

В общем-то, задача не особо сложная, в комментариях куча выпускников видеокурсов, закодивших решение "в лоб" на своём любимом Питончике, удивляются, почему у неё уровень Medium, а не Easy. Сложность, как обычно, заключается в том, чтобы сделать качественно: чтобы тесты проходили за единицы миллисекунд, а не за 700 (C++) или 1200 (Питончик).

Есть массив десятичных цифр 'a'. Суммируем попарно все цифры: a[0] + a[1], a[1] + a[2] и т.д., и берём каждую сумму по модулю 10. Получается новый массив десятичных цифр на единицу меньшей размерности. Повторяем процедуру, пока не остаётся одна цифра. Она - и есть ответ.
Здесь - подробности, картинки, граничные условия и тест кейсы: https://leetcode.com/problems/find-triangular-sum-of-an-array/

Кто сказал "рекурсия"? А, это нейросеть. Ладно, что с неё возьмёшь, она же думать не умеет...
В общем-то, если посмотреть, сколько раз каждая исходная цифра входит в итоговую сумму, то можно заметить закономерность:
 длина массива   веса элементов в итоговой сумме
1 1
2 1 1
3 1 2 1
4 1 3 3 1
5 1 4 6 4 1
...
Нам нужна строка № n-1 треугольника Паскаля, где n - длина исходного массива, и тогда ответом будет скалярное произведение по модулю 10 этой строки и массива, рассматриваемых как векторы. Проблемка только одна: n принимает значения до 500, а как известно, точное представление максимального биномиального коэффициента в этом ряду C(500; 250) требует очень много бит. Много бит - много вычислений, и вот мы уже почти в топе, но с другой стороны (где времена максимальные). Вычислять же биномиальные коэффициенты классической формулой
  C(n; k) = C(n; k - 1) * (n - k + 1) / k 
но по модулю 10 мы не можем, т. к. делимое в этой операции требуется полностью. Но зато нам и весь коэффициент не нужен, а хватит его последней циферки.

Естественно, существуют и другие варианты:
  • Тупо просуммировать числа 500*499/2 ~ 125000 раз и столько же раз взять остатки от деления. Это неспортивно, и такими решениями заполнен диапазон от 170 до 700 мс в разделе C++ (в других не смотрел, но говорят, Чарльз Бэббидж на своём арифмометре считает быстрее тех решений).
  • Можно брать остаток не каждый раз, а где-то по достижении суммой значения 2⁶³, сэкономив на тяжёлых делениях. Но плохая асимптотика никуда не денется. Да и ещё неизвестно, что хуже, деление или if для его обхода.
  • Один лихой решатель просуммировал динамическим программированием не сами числа, а треугольник Паскаля по модулю 10, и потом сосчитал скалярное произведение. Так себе затея, как по мне, но 20 мс выжать у него получилось.
В общем, эти способы не подходят, если мы хотим достичь заветных 7 мс (быстрее пока реализовать не смогли даже по Лукасу с табличками заранее посчитанных остатков и малых комбинаций). Нужно нормальное решение.
КБ
Костя Б-С
87 571
Этот код
 class Solution { 
public:
int triangularSum(vector& nums) noexcept {
ios::sync_with_stdio(false);
cin.tie();
constexpr auto ten = 10;
const auto first = &nums[0];
const auto end = &nums[1];
auto last = &nums[nums.size()];
auto count = 0;
while (last != end) {
for (auto prev = first, next = end; next != last; ++prev, ++next) {
*prev += *next;
}
if (!(++count % 27)) {
for (auto begin = first; begin != last; ++begin) {
*begin %= ten;
}
}
--last;
}
return nums[0] % ten;
}
};
у меня показал такой результатВ 4 раза хуже ожидаемого результата, но вполне терпимо.
ВА
Владик Агеев
64 989
Лучший ответ
Костя Б-С "Вполне терпимо" я могу и сам нарисовать. У меня первая попытка выдала 19 ms. Не, тут спортивный интерес - обогнать топовых алгоритмистов с их 7-8 ms. Причём, в этой задаче они считают по чесноку.
Я тоже за предварительные расчеты, в любом виде.
Чисто практически, либо это счастье нужно нам 1 раз, и тогда мы можем подождать 700 мс, или оно нам нужно кучу раз, и тогда нет ничего плохого в том, чтобы посчитать все заранее.
Руслан Аухадеев Насколько я понимаю, "счастье" предварительного расчёта там входит в стоимость (если только не хитрить со статическими инициализаторами, исполняющимися до начала замеров), а количество тестов - на уровне от десятков до тысяч в разных задачах. Так что предварительные расчёты на литкоде - это вопрос баланса, который в каждой задаче приходится решать творчески.
Например, если нам все строки треугольника до 500-й не нужны, а нужно 5% от них, то вычисление всех 500 - лишняя работа, которая испортит статистику. А какие нужны, заранее неизвестно (опять же, если играть по-честному, а то находятся люди, подбирающие решение точно под данный набор тестов, а на других тестах оно не просто медленное - оно вообще не будет работать).
Если вы хотите достичь оптимального времени выполнения, следуя описанным требованиям, то можно использовать следующий алгоритм:

1. Вычислить строку n-1 треугольника Паскаля по модулю 10.
2. Вычислить скалярное произведение полученной строки и исходного массива по модулю 10.

Для оптимизации этого процесса, вы можете использовать следующий метод для вычисления строки треугольника Паскаля по модулю 10:
 def pascal_row_mod10(n): 
row = [1]
for k in range(n):
row.append((row[-1] * (n - k)) // (k + 1) % 10)
return row[:n//2+1] + row[:n//2][::-1]
Это функция вычисляет строку треугольника Паскаля с помощью сочетаний (C(n, k), где n - номер строки, а k - номер элемента в строке) и использует свойство, что строка треугольника Паскаля является симметричной (первая половина строки равна второй половине строки в обратном порядке).

Затем вы можете вычислить скалярное произведение:
 def dot_mod10(a, b): 
return sum(x*y for x, y in zip(a, b)) % 10
Итоговая функция, которую вы можете использовать, будет выглядеть следующим образом:
 def triangular_sum(array): 
n = len(array)
pascal_row = pascal_row_mod10(n)
return dot_mod10(array, pascal_row)
Этот алгоритм выполняет вычисления быстро и эффективно для больших значений n, и он должен быть эффективным для n до 500.
Костя Б-С Я в задании пояснил, почему это - не наш метод. Он медленный. На будущее зафиксируй себе: я бы не стал обращаться с вопросом, ответ на который может наскоро сляпать твоя нейросеть. А пока - честно заслуженный дизлайк.
Так на c++ просто делаем предподсчет шаблонами на этапе компиляции, и всё
Костя Б-С Да, видел сколько-то лет назад такой алгоритм для чисел Фиббоначчи. Неспортивно, но забавно. Там есть деятели, которые вычисления запихивают в статические инициализаторы, и у них получается "быстро". Но это ж - просто перекладывание стоимости из кармана в карман.