воскресенье, 6 декабря 2020 г.

Подготовка

Мишу из школы направляют на региональную олимпиаду по информатике и выдали примеры задач для подготовки. Он их делает на Питоне, а потом мы сложные случаи разбираем. Задачи проверяются на сервере, который работает с 9 утра до 9 вечера. Ограничения для всех задач: по времени - 2 секунды, по памяти - 256 мегабайт. Задачи очень разные! Есть элементарные:
Задано четыре числа. Требуется разбить их на две пары, чтобы сумма произведений в этих парах была максимальной. Например, для чисел 2,3,4,5 надо вывести 26 (2*3+4*5).

А бывают архисложные, требующие подключения серьёзного математического аппарата:
Для целого положительного числа от 1 до 1000 требуется найти число способов представить его в виде суммы нечётных слагаемых. Порядок слагаемых неважен. Например, для числа 6 надо вывести 4. Сами разбиения выводить не надо:
[1+1+1+1+1+1], [3+1+1+1], [3+3], [5+1]

Я сначала решил "в лоб" - прямым перебором, используя Set для фильтрации. Потом ускорил в несколько сотен раз, отказавшись от Set. Это позволило мне обнаружить, что происходит переполнение для больших чисел. Посему мне пришлось заменить long на BigInteger, замедлив программу раза в два.
fun count(array: IntArray, size: Int): BigInteger {
  var count = BigInteger.ONE
  val size2 = size - 2
  if (size > 2 && array[size - 1] == 1 && array[size2] == 1) {
    array[0] += 2
    count = count.plus(count(list, array, size2))
    array[0] -= 2
    var index = 0
    while (++index < size2) {
      if (array[0] == array[index]) continue
      if (array[0] == array[index] + 2) {
        array[index] += 2
        count = count.plus(count(list, array, size2))
        array[index] -= 2
      }
      break
    }
  }
  return count
}
Тут стало очевидно, что после 170 мы перестаём укладываться в две секунды и подсчёт ответа для 400 занял более двух дней:
in: 100; out: 444793; time: 21 ms
in: 200; out: 487067746; time: 7958 ms
in: 300; out: 114872472064; time: 1873429 ms
in: 400; out: 11962163400706; time: 196301147 ms

Написал код для вывода комбинаций и пытаемся анализировать, чтобы для получившейся выше рекурсивной функции вывести формулу, ибо полный перебор - это уже перебор. 8)

34 комментария:

  1. Я не силён в спортивном программировании, но судя по числам (1000) - похоже, стоит воспользоваться динамических программированием со сложностью n*n

    ОтветитьУдалить
    Ответы
    1. import java.math.BigInteger

      fun main() {
      val n = readLine()!!.toInt()
      val dp = Array(n + 1) { Array(n + 1) { BigInteger.ZERO } }
      dp[0][1] = BigInteger.ONE
      for (i in 1..n) {
      for (max in 1..i step 2) {
      for (rem in 1..max step 2) {
      dp[i][max] += dp[i - max][rem]
      }
      }
      }
      val ans = dp[n].reduce { a, b -> a + b }
      println(ans)
      }

      Удалить
    2. Идея такая:
      dp - массив динамического программиования
      первый индекс - набираемое число
      второй индекс - максимальное используемое число в наборе

      Рекурсия: проходим по всем наборам
      и в каждом наборе максимальное число будет от 1 до размера набора

      rem - остаток (remainder), если уберём максимальное число, то получим ответ для набора, меньшего на это число

      Удалить
    3. Спасибо! Код конечно же работает, но я пока не понимаю почему.

      Удалить
    4. Создавайте митинг, я подключусь сразу же.

      Удалить
    5. Ну, раз не выходит, попробую словами.
      Надо перебрать все варианты, какими можно представить n нечётными числами.

      Попробуем представить это в отсортированном по возрастанию порядке.

      Вот этот код, слегка раскрывает идею

      fun main() {
      val n = readLine()!!.toInt()
      val dp = Array(n+1) {Array(n+1) { emptyList>()} }
      dp[0][1] = listOf(emptyList())
      for (i in 1..n) {
      for (max in 1..i step 2) {
      for (rem in 1..max step 2) {
      dp[i][max] += dp[i - max][rem].map { it + max }
      }
      }
      }
      val ans = dp[n].reduce { a, b -> a + b }
      println(ans)
      }

      Удалить
    6. Введите на входе 6 и получите отсортированный набор вариантов:

      [[1, 1, 1, 1, 1, 1], [1, 1, 1, 3], [3, 3], [1, 5]]

      Удалить
    7. Отсортированный по последнему элементу списка.

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

      Удалить
    9. Это идея динамического программирования (или индукция). Когда мы для решения для n находим решения для значения меньше n.

      Одна из популярных задач на дп - это рюкзак.
      https://neerc.ifmo.ru/wiki/index.php?title=%D0%97%D0%B0%D0%B4%D0%B0%D1%87%D0%B0_%D0%BE_%D1%80%D1%8E%D0%BA%D0%B7%D0%B0%D0%BA%D0%B5

      Если будет желание поговорить голосом - я согласен.

      Удалить
    10. Поговорить голосом - это завсегда. Можно завтра по meet, например.

      Удалить
    11. По meet - это как? Что надо установить? Он умеет шарить экран?

      Удалить
    12. У тебя Chrome стоит? Аккаунт, вроде, есть.
      https://meet.google.com/zch-osaz-uot

      Удалить
    13. Кул, похоже, это работает в браузере (хром), и ставить ничего не надо (хром стоит уже).

      Правда я ни к кому не подключился, так что полной проверки не получилось.
      Там только видео связь или можно экран шарить?

      Удалить
    14. Я просто не успел Admit нажать. Ты как-то быстро ушёл

      Удалить
    15. ОК, наверное, это работает после соедиения.
      Когда встречаемся в meet?

      Удалить
    16. Если можешь, то сейчас, а то после 14:00 у меня сегодня митинги

      Удалить


    17. Вот статья про динамическое программирование
      https://neerc.ifmo.ru/wiki/index.php?title=%D0%94%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5

      Удалить
    18. А вот и Открытая олимпиада по программированию для 3-7 классов
      http://prog.matolimp-spb.org/2021/#dec

      Удалить
    19. 3-7 класс? Мишка уже перерос, а Лёшка пока особо не интересуется.

      Удалить
    20. Понятно.
      Если Мишке интересно спортивное программирование, то есть раунды и посерьёзнее. Сегодня в 18:05, например, будет двухчасовой раунд. Думаю, в первый раз достаточно будет решить задачку А и для задачек B и C просто почитать условие особо не напрягаясь, если решить не получится. Если будет желание, потом можно будет почитать разбор решений этих задач.
      https://codeforces.com/contests/1465

      Удалить
    21. Забыл написать, если будет желание участвовать, то надо зарегистрироваться на раунд заранее. Регистрация даёт право на участие. Но если вы не будете участвовать, то никаких штрафов нет.
      Например, рейтинг тоже не будет считаться.

      Удалить
    22. Спасибо, мы зарегистрировались

      Удалить
    23. Отлично!
      Наверное, Мише стоило знать, что в большинстве случаев PyPy будет быстрее, чем Python. Так что если Миша пока хочет остаться на питоне, рекомендую использовать PyPy 3.

      Удалить
  2. Папа, я эту идею не понял.
    Объясни пожалуйста.

    ОтветитьУдалить
  3. Какая клёвая задачка, прямо учебный материал!
    Я даже потратил на неё сколько-то времени.
    Смотри, решение на питоне в лоб не вывозит, хотя пишется очень просто (со списками).
    Можно научить запускать профайлер и показать, что реально тормозит. Если заменить списки на просто суммирование, но с рекурсией, то тоже в профайлере видно, что беда.
    Если убрать рекурсию, как в примере выше, то питон тоже не вывозит, я проверил, на моём ноуте решение в 2 секунды не укладывается.
    Могу ещё проверить с numba, но позже.

    ОтветитьУдалить
  4. Хотя нет, проверил с numba (оказалась под рукой, установлена).
    Считает за 75ms, не оптимизировал, матрица хранится флотами.

    ОтветитьУдалить
    Ответы
    1. Какие вы все тут умные! Ставите меня в неловкое положение. Ничего... Ничего... Сейчас прокачаемся 8)

      Удалить
    2. Флотами может не зайти.
      Хорошо бы сравнить результат для разных n: 500: 732986521245024
      600: 30703043607660730
      700: 962056220379782044
      800: 23926108358272941056
      900: 492244122149966911170
      1000: 8635565795744155161506

      Удалить
    3. И, как обычно говорят составители задач на спортивное программирование, среди языков, на которых решение существует, часто есть только C++ и Java
      https://docs.google.com/document/d/e/2PACX-1vRhazTXxSdj7JEIC7dp-nOWcUFiY8bXi9lLju-k6vVMKf4IiBmweJoOAMI-ZEZxatXF08I9wMOQpMqC/pub

      If there is a possibility of Java solutions working slow (including, but not limited to C++ solutions working more than a ¼ of TL or C++ solutions using more than 50 MB), a correct java solution is needed.

      Удалить
    4. Решение флотами 100% процентов не зайдёт, там автоматическая валидация, всякие 1e20 точно не устроят :)
      Просто я хотел проверить, сможет ли питон пробить две секунды лимита на моём ноуте с тройным циклом... Может :)

      Удалить