Блог пользователя dkirienko

Автор dkirienko, 3 месяца назад, По-русски

После регионального этапа всероссийской олимпиады школьников по программированию пришлось услышать много хейта, что задача B-1 про короля — "глина", что написание 400 строк кода заняло три часа и всё равно не принесло 100 баллов и т.д. Поэтому я решил написать решение этой задачи сам и показать его вам. Да-да, я не писал решение этой задачи раньше.

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

  1. Нужно не строить маршрут короля, проходящий через заданный отрезок, а, наоборот, построить разные маршруты короля, содержащие все возможные отрезки (то есть все горизональные и все вертикальные), и выбрать тот маршрут, который содержит заданный отрезок.
  2. Принципиально разных маршрутов всего два — это "змейка" для случая, когда обе стороны чётные, и когда одна сторона — чётная, а другая — нечётная. Все остальные конфигурации получаются отражением этой змейки относительно вертикальной или горизонтальной оси. Вот примеры двух змеек для этих случаев.
  3. Такая змейка содержит почти все горизонтальные отрезки. Останутся только несколько отрезков у краёв доски, Но если эту змейку отразить относительно вертикальной или горизонтальной прямой, то можно будет покрыть все оставшиеся горизонтальные отрезки. А аналогичная змейка, но с длинными вертикальным линиями, также вместе с отражениями, покроет все вертикальные отрезки.
  4. Поэтому существенная функция построения змейки в решении будет одна. Ещё объединим в общий код построение общей части змеек для чётного и нечётного числа строк. Различие двух случает будет только в построении одной последней строки для чётного числа строк и двух последних строк для нечётного числа строк. Все остальные преобразования змеек сделаем функциями преобразования таблицы: вертикальные и горизонтальные отражения, а также функция транспонирования матрицы, при помощи которой из змейки с горизонтальными линиями можно будет получить змейку с вертикальными линиями.

Также мне показалось, что удобней не хранить змейку в виде последовательности клеток, как это требуется при выводе ответа, а заполнить двумерный массив n*m числами, обозначающими номера клеток в порядке их обхода. Так проще строить змейку и выполнять последующие преобразования. А уже при выводе ответа вывести его в виде последовательности клеток.

В результате получился код на 90 строк на питоне, из них непустых строк — 71. Наверняка можно что-то ещё упростить, например, сделать функцию snake построения змейки более элегантной.

import sys

def snake(n, m):
    a = [[-1] * m for i in range(n)]
    x, y = 0, 0
    for val in range(n * m):
        a[x][y] = val
        val += 1
        if x == 0 and y == 0:
            y += 1
        elif y == 0:
            x -=1
        elif n % 2 == 0 and x == n - 1:
            y -= 1
        elif n % 2 == 1 and x == n - 1:
            if y % 2 == 1:
                y -= 1
            else:
                x -= 1
        elif n % 2 == 1 and x == n - 2:
            if y % 2 == 1:
                x += 1
            else:
                y -= 1
        elif y == m - 1 and x % 2 == 0:
            x += 1
        elif y == 1 and x % 2 == 1:
            x += 1
        elif x % 2 == 0:
            y += 1
        elif x % 2 == 1:
            y -= 1
    return a


def rot(a):
    return [[a[j][i] for j in range(len(a))] for i in range(len(a[0]))]


def flip1(a):
    for i in range(len(a) // 2):
        a[i], a[-1 - i] = a[-1 - i], a[i]


def flip2(a):
    for i in range(len(a)):
        a[i] = a[i][::-1]


def print_ans(a):
    ans = [""] * (n * m + 1)
    for x in range(n):
        for y in range(m):
            ans[a[x][y]] = str(x + 1) + " " + str(y + 1)
    ans[-1] = ans[0]
    print("\n".join(ans))
    sys.exit(0)


def check(a):
    v1 = a[x1][y1]
    v2 = a[x2][y2]
    if abs(v1 - v2) == 1 or abs(v1 - v2) == n * m - 1:
        print_ans(a)


n, m = map(int, input().split())
x1, y1, x2, y2 = map(int, input().split())
x1 -= 1
y1 -= 1
x2 -= 1
y2 -= 1

if n * m % 2 == 1:
    print(-1)
    sys.exit(0)

for a in (snake(n, m), rot(snake(m, n))):
    check(a)
    flip1(a)
    check(a)
    flip2(a)
    check(a)
    flip1(a)
    check(a)
print(-1)

Написание этого кода заняло менее часа. Первый сабмит принёс 88 баллов, оказалось, была глупая опечатка при проверке условия n * m — нечётное. После исправления этой ошибки получилось 96 баллов, из-за TL на двух тестах (действительно, в задаче доска содержит до миллиона клеток, а в решении строится до 8 разных маршрутов, что для питона уже многовато). Но пересдача с использованием PyPy принесла 100 баллов. То есть фактически алгоритм был написан сразу без ошибок. Но ещё у меня была одна функция, печатающая всю доску со змейкой для отладки, она не вошла в этот код.

Какое место этой задачи в региональном этапе? В этой задаче есть конструктивная идея, если вы начнёте думать в направлении "а какие бы змейки мне построить, чтобы покрыть все горизонтальные и вертикальные отрезки", несложно к ней прийти. Также есть умение работать с преобразованием плоскости (отражения, повороты). Ну и наконец нужно аккуратно всё написать. Как вы видите, у этой задачи есть весьма короткое решение, которое можно реализовать за приемлемое время (примерно час). А остальное зависит от вас — те, кто умеет писать код хорошо, используют общие идеи, функции для реализации, получают несложное решение. Ну а если у вас код на 400 и больше строк с разбором массы случаев, то и времени это отнимет у вас куда больше, что скажется на итоговом балле.

Олимпиады по программированию — это не только знание каких-то алгоритмов, но и умение писать хороший, простой код. Задачи, которые приносят баллы тем, кто умеет хорошо программировать, но не требуют знания каких-либо алгоритмов, тоже нужны.

  • Проголосовать: нравится
  • +30
  • Проголосовать: не нравится

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится -34 Проголосовать: не нравится

Автокомментарий: текст был обновлен пользователем dkirienko (предыдущая версия, новая версия, сравнить).

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится -37 Проголосовать: не нравится

Автокомментарий: текст был обновлен пользователем dkirienko (предыдущая версия, новая версия, сравнить).

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится +20 Проголосовать: не нравится

Согласен -- задачи, которые приносят баллы тем, кто умеет хорошо программировать, тоже нужны. Поэтому задача B1 не нужна.

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится +27 Проголосовать: не нравится

Можно всего за 30 строчек.

n, m, x1, y1, x2, y2 = [int(i) for i in (input() + ' ' + input()).split()]
swap_nm = abs(x1 - x2) == 1
n, m, x1, y1, x2, y2 = [(n, m, x1, y1, x2, y2), (m, n, y1, x1, y2, x2)][swap_nm]
y1, y2, inv_ver, inv_hor = min(y1, y2), max(y1, y2), min(y1, y2) == 1, x1 <= 2
 
def print_cell(x, y):
    print(*([x, n - x + 1][inv_hor], [y, m - y + 1][inv_ver])[::-1 if swap_nm else 1])
 
if (m == 2 and 1 < x1 < n) or (n == 3 and x1 == 2 and y1 % 2) or (m % 2 and n % 2):
    print(-1)
elif n % 2 == 0:
    for r in range(1, n + 1)[::-1]:
        print_cell(r, 1)
 
    for r in range(1, n + 1):
        for c in range(1 + (r != n), m + 1)[::1 if r % 2 else -1]:
            print_cell(r, c)
else:
    for c in range(1, m + 1):
        for r in range(1, 3)[::-1 if c % 2 else 1]:
            print_cell(r, c)
 
    for r in range(3, n + 1):
        for c in range(2, m + 1)[::-1 if r % 2 else 1]:
            print_cell(r, c)
 
    for r in range(2, n + 1)[::-1]:
        print_cell(r, 1)
  • »
    »
    3 месяца назад, скрыть # ^ |
     
    Проголосовать: нравится -17 Проголосовать: не нравится

    В этом решении не очень понятно, что происходит. Без пояснений по крайней мере.

    • »
      »
      »
      3 месяца назад, скрыть # ^ |
       
      Проголосовать: нравится +8 Проголосовать: не нравится

      Ну, на то это и короткое решение, постарался упихать всё насколько возможно. Суть решения следующая.

      Если (x1, y1) и (x2, y2) лежат на одной вертикали, то давайте повернём картинку на 90 градусов, после чего (x1, y1) и (x2, y2) будут лежать на одной горизонтали — это 2-ая и 3-ая строчки кода. Теперь рассматриваем только случай, где x1 = x2 = x.

      Если (x, y1) находится правее (x, y2), то поменяем эти клетки местами. Если y1 = 1, то отразим картинку по вертикали. Если x <= 2, то отразим картинку по горизонтали. Всё это упихано в 4-ую строку и создана удобная функция print_cell, которая преобразовывает (x, y) обратно в исходные координаты. Зачем все эти преобразования? Объясню чуть позже. Со строкой, где идёт проверка того, что ответ равен -1, думаю, вопросов нет. Если у кого-то есть, то в авторском решении это объясняется.

      Теперь ответ гарантированно существует, осталось его найти. Если n чётно (возможно, m тоже, но здесь это неважно), то почти все горизонтальные отрезки покрываются точно таким же обходом змейкой, как на первой картинке из Вашего поста. Не покрываются только отрезки, которые стыкуются с левой границей. Поэтому если y1 = 1, то мы отражаем картинку по вертикали — за этим и нужна была часть 4-ой строчки. Если n чётно и ответ существует, то все горизонтальные отрезки мы уже покрыли.

      Если m чётно, а n нет, то снова почти все горизонтальные отрезки покрываются уже точно таким же обходом из второй картинки поста, где змейкой из случая для чётного n мы обходим все верхние n - 1 строк, а потом снизу добавляем одну строку и немного преобразовываем две нижние строки. Здесь, во-первых, исключение составляют некоторые отрезки на горизонталях x = 1 и x = 2. Поэтому, мы переворачиваем картинку уже по горизонтали. Получаем следующие случаи: если n > 3, то x > 2, иначе же n = 3 и тогда x >= 2. Если n = 3, x = 2, то на самом деле все отрезки, для которых ответ не равен -1, уже покрыты обходом с картинки. Если x > 2, то этот обход покрывает все такие отрезки за исключением тех, которые стыкуются с левой границей. И вновь мы отражаем картинку по вертикали. Все эти отражения описываются 4 строкой. Остаётся лишь коротко написать сами эти обходы, с чем отлично помогает Python.

    • »
      »
      »
      3 месяца назад, скрыть # ^ |
       
      Проголосовать: нравится 0 Проголосовать: не нравится

      А в вашем решении прямо все понятно без пояснений

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится +11 Проголосовать: не нравится

Данная задача не сильно идёт на B региона. Не по сложности, а по типу

  • »
    »
    3 месяца назад, скрыть # ^ |
     
    Проголосовать: нравится +1 Проголосовать: не нравится

    по вайбам скорее как D или E муника

  • »
    »
    3 месяца назад, скрыть # ^ |
     
    Проголосовать: нравится +7 Проголосовать: не нравится

    Вообще-то олимпиада — это не ЕГЭ, на олимпиаде должны быть нестандартные, неожиданные задачи. Не может быть такого, что вот на позиции B задачи какого-то определённого типа, и мы готовимся к региону решать задачи определённых типов.

    • »
      »
      »
      3 месяца назад, скрыть # ^ |
       
      Проголосовать: нравится 0 Проголосовать: не нравится

      Можно тогда было бы дать задание по астрофизике, которое нужно реализовать программой. (Не делайте так пожалуйста в следующем году)

      • »
        »
        »
        »
        57 минут назад, скрыть # ^ |
         
        Проголосовать: нравится 0 Проголосовать: не нравится

        букквально финал ломоносова этого года

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится +12 Проголосовать: не нравится

Это всё замечательно, проверка на важное умение писать простой код, все дела, но для B региона вышло сложновато и не похоже на типовую Bшку, и поэтому, как мне кажется, для баланса надо было добавить в эту задачу подгруппы, чтобы она чуть попроще засылалась на 100,

...
  • »
    »
    3 месяца назад, скрыть # ^ |
     
    Проголосовать: нравится +10 Проголосовать: не нравится

    Если вы имеете в виду под подгруппой "пройти все тесты, чтобы получить баллы", это было бы совсем убийственно для этой задачи. Один просмотренный случай — и ноль. Если речь идёт о том, чтобы описать какие-то подгруппы — для того, чтобы навести на решение — то это сложно придумать. Кроме какой-то банальности вида "n = 2" или "отрезок находится на границе", но много баллов за такие группы не дашь, так что особого смысла в этом нет.

    • »
      »
      »
      3 месяца назад, скрыть # ^ |
       
      Проголосовать: нравится 0 Проголосовать: не нравится

      Я имею в виду подгруппы + потестовая оценка

      • »
        »
        »
        »
        3 месяца назад, скрыть # ^ |
         
        Проголосовать: нравится 0 Проголосовать: не нравится

        И какие осмысленные подгруппы можно выделить в этой задаче?

        • »
          »
          »
          »
          »
          3 месяца назад, скрыть # ^ |
           
          Проголосовать: нравится 0 Проголосовать: не нравится

          Ну, хотелось бы, чтобы в случае ошибок в коде, можно было примерно понять, где эти ошибки сделаны, поэтому можно дать подгруппы, в которых n и m определённым образом ограничены по чётности, или, как вы упомянули, подгруппы с особыми случаями

          • »
            »
            »
            »
            »
            »
            3 месяца назад, скрыть # ^ |
             
            Проголосовать: нравится 0 Проголосовать: не нравится

            n и m особо поделены по чётности — ну это по сути две группы. Оба чётные, или одно нечётное. Тогда придётся ещё делать группу "оба нечётные", но это уже странно. Так что никакой особой выгоды от этих групп нет.

»
3 месяца назад, скрыть # |
Rev. 2  
Проголосовать: нравится +8 Проголосовать: не нравится

А почему вы опубликовали это через почти месяц после тура?)

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится 0 Проголосовать: не нравится

Задача условно простая, но неприятная. Из за нее не удалось заслать С1 на сотку. В последнее время начали появляться такие «простые» задачи, которые ломают весь тур. Жюри скучают: типа а что мы одно и тоже даем, давайте удивим. Пусть мяч будет квадратным, а не круглым. И бить надо с центра поля. Кто не ударит, тот сам виноват.

  • »
    »
    3 месяца назад, скрыть # ^ |
     
    Проголосовать: нравится +10 Проголосовать: не нравится

    Что значит "неприятная задача"? Неприятным может быть написание решения. Но для этого и сделан пост, чтобы показать, что ничего неприятного в этом решении нет. Самое "неприятное" — это написать нужную змейку, но это меньше 30 строк кода, и особо много времени не занимает. Если вам не хватило времени на задачу С, то, наверное, вы слишком много времени потратили на B, то есть именно не поняли, как нужно писать решение задачи, чтобы это не отняло много времени. Наконец, распределение времени на туре — это часть стратегии. Если у вас на задачу B ушло более двух часов, наверное, стоит остановиться на достигнутом результате (а какой-то результат должен быть достигнут, хотя бы для случая чётных сторон можно было написать обходы) и уделить внимание другим задачам.

    • »
      »
      »
      3 месяца назад, скрыть # ^ |
       
      Проголосовать: нравится +23 Проголосовать: не нравится

      Вы упомянули про "часть стратегии". Точное знание своих баллов за задачу еще до окончания тура — то за что, я уверен, многие любят олимпиадное программирование — важная часть стратегии. Если человек получил 100 баллов за задачу он больше не задумывается о том как он мог эффективнее написать или какие случаи он забыл рассмотреть — все ресурсы идут на еще не сданные задачи. В первый день этого РЭ многих лишили такой возможности, некоторые, как и я, после ретеста и снятия баллов лишились надежд на участие в Заключительном этапе(Сагдуллин Марсель 100-(100/82)-9-9 100-100-68-10). Можно долго рассуждать о том, как было бы честнее поступить в сложившейся ситуации, ответа в общем-то и нет. Моя точка зрения — эта задача вполне подходит и по сложности и по типу в Б региона. Ваш пост выглядит как попытка приглушить хейт и показать адекватность задачи, но был и будет воспринят в штыки всеми, кто был уверен в своем решении но потерял баллы, ведь по сложившемуся в сообществе мнению, именно вы автор задачи. Я лично считаю что пост максимально некрасивый, если вы автор задачи, потому что тогда именно вы несете ответственность за качество подготовки

      TLDR Пост максимально некрасивый по отношению ко всем у кого сняли баллы, я не прошел из-за этого на всерос, но считаю что задача хорошая

      • »
        »
        »
        »
        3 месяца назад, скрыть # ^ |
         
        Проголосовать: нравится 0 Проголосовать: не нравится

        Марсель, подготовкой этой задачи я не занимался.

        Конечно, ничего приятного в перетестировании нет. Но если бы не было перетестирования, было бы много крика, что кто-то сдал лажу, получил незаслуженные баллы баллы. Были участники, которые обнаружили особенность чекера во время тура и сознательно сдали неверное решение, набрав почти 100 баллов (совсем 100 баллов они не смогли набрать, т.к. не умели отпределять все конфигурации, когда решения нет). У любого принятого решения были бы свои недостатки, принять решение, которое устроило бы всех (и соответствовало бы, например, порядку проведения ВсОШ и требованиям к проведению регионального этапа) было нельзя.

        • »
          »
          »
          »
          »
          3 месяца назад, скрыть # ^ |
           
          Проголосовать: нравится +3 Проголосовать: не нравится

          Денис Павлович, спасибо за ответ, вопросов больше нет

        • »
          »
          »
          »
          »
          3 месяца назад, скрыть # ^ |
           
          Проголосовать: нравится +3 Проголосовать: не нравится

          А нет, есть вопрос, кто занимался подготовкой этой задачи?

          • »
            »
            »
            »
            »
            »
            3 месяца назад, скрыть # ^ |
             
            Проголосовать: нравится 0 Проголосовать: не нравится

            Я не буду на него отвечать. Что изменится от ответа на него?(вопрос риторический, на него тоже можно не отвечать)

        • »
          »
          »
          »
          »
          3 месяца назад, скрыть # ^ |
           
          Проголосовать: нравится +9 Проголосовать: не нравится

          Почему тогда нет перетестирования второго дня, где очень много людей сдало задачу С на полный балл, заслав квадрат?

          • »
            »
            »
            »
            »
            »
            3 месяца назад, скрыть # ^ |
             
            Проголосовать: нравится 0 Проголосовать: не нравится

            Потому что слабые тесты условию задачи соответствуют. Неправильный чекер условию задачи не соответствует.

            • »
              »
              »
              »
              »
              »
              »
              3 месяца назад, скрыть # ^ |
               
              Проголосовать: нравится 0 Проголосовать: не нравится

              почему бы тогда не делать всегда слабые тесты? Они условиям-то соответствуют, ибо входные данные ограничиваются только сверху. Или это ломает замысел задачи? И если ломает, то почему бы тогда не делать сильные тесты/ретест?

      • »
        »
        »
        »
        3 месяца назад, скрыть # ^ |
         
        Проголосовать: нравится 0 Проголосовать: не нравится

        почему так уверен в том, что не пройдешь на всерос?

»
3 месяца назад, скрыть # |
 
Проголосовать: нравится +17 Проголосовать: не нравится

Если бы еще чекер так круто работал,было бы вообще класс

»
3 месяца назад, скрыть # |
Rev. 3  
Проголосовать: нравится +6 Проголосовать: не нравится

Решение то понятное, я его на туре придумал, и написал, и получил сто баллов а потом пришёл домой и увидел свои 82, после туров я за 5 минут отдебажил своё решение, меня во всей этой ситуации больше интересует как на туре надо было понять что сто баллов это оказывается не сто баллов и пойти дебажить, каааааааааааааак?