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

Автор Nickolas, 10 лет назад, По-русски

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

530A - Квадратное уравнение

Решение квадратного уравнения требовало использования библиотечной функции sqrt и конструкции if-then-else. Кроме того, эта задача должна была продемонстрировать, что в языке есть числа с плавающей точкой :-)

main =>
    A = read_real(), B = read_real(), C = read_real(),
    D = B * B - 4 * A * C,
    X1 = (-B - sqrt(D)) / (2 * A),
    X2 = (-B + sqrt(D)) / (2 * A),
    if D == 0 then
        println(X1)
    else
        printf("%w %w%n", min(X1, X2), max(X1, X2))
    end.

530B - Строка наизнанку

Задача на работу со строками: библиотечная функция slice позволяет получить подстроку заданной строки, reverse разворачивает ее, а ++ — оператор конкатенации. Как вариант, можно было использовать цикл и вывести ответ посимвольно.

main =>
    S = read_line(),
    N = length(S),
    X = reverse(slice(S, 1, N // 2)) ++ reverse(slice(S, 1 + N // 2)),
    println(X).

530C - Диофантово уравнение

Эту задачу можно решить полным перебором (для каждого X проверить, существует ли подходящий Y). Авторское решение использует constraint programming ("программирование в ограничениях"), которое по сути реализует перебор с отсечениями, позволяющее избавиться от явного цикла перебора.

import cp.
main =>
    A = read_int(), B = read_int(), C = read_int(),
    X #> 0, Y #> 0,
    if A * X + B * Y #= C then
        Solutions = solve_all([X, Y])
    else
        Solutions = []
    end,
    println(length(Solutions)),
    foreach ([I, J] in Solutions)
        printf("%d %d%n", I, J)
    end.

Выражение A * X + B * Y #= C задает ограничение на переменные. Уже на этом этапе система может понять, что решений нет, и перейти в ветку else. Иначе функция solve_all вернет список всех решений.

530D - Разность множеств

Эту задачу можно решить множеством способов: от императивных циклов до того же constraint programming. Авторское решение использует библиотечную функцию subtract, которая вычитает из одного отсортированного списка другой. A..B формирует список чисел от A до B. Для вывода используется list comprehension. Эта задача — единственная, в которой авторское решение использует := — чисто императивный оператор деструктивного присваивания.

import ordset.
import util.
main =>
    X = 1..1000,
    N = read_int(),
    foreach (_I in 1..N)
        A = read_int(), B = read_int(),
        X := subtract(X, A..B)
    end,
    println(join([to_string(V) : V in [length(X) | X]])).

530E - Сумма и произведение

Эту задачу можно решать как минимум двумя способами. "Математический" способ предлагает заметить, что решение, состоящее из N-2 единиц, двойки и N+D, всегда подходит под описанные условия. В некоторых случаях существуют другие решения, например, для теста N = 4, D = 1 подходит решение 1, 1, 3, 3.

Авторское решение использует constraint programming, что позволяет не думать, а просто перевести условие задачи на Picat.

import cp.
import util.
main =>
    N = read_int(), D = read_int(),
    Vals = new_list(N),
    Vals :: 1..10000,
    prod(Vals) #= sum(Vals) + D,
    solve(Vals),
    println(join([to_string(V) : V in Vals])).

530F - Прыгающие лягушки

На обычном языке программирования эта задача решалась бы поиском в глубину или в ширину. Такой поиск можно реализовать и на Picat. В Picat, как и в Prolog, поиск в глубину с возвратами встроен в язык. Техника tabling используется для мемоизации и для автоматического поиска максимума: выражение table(+, +, +, max) означает, что первые три параметра предиката — входные, последный — выходной, и его надо максимизировать. ?=> означает недетерминизм: сам по себе вызов jump может возвращать разные значения для одних и тех же входных параметров, table выбирает из них максимальное. best_frog использует ту же самую технику, чтобы избежать явного цикла по лягушкам для поиска лучшей из лягушек.

table(+, +, +, max)
jump(X, Y, K, Dist) ?=>
    (
      NewX = X + K, NewY = Y ;
      NewX = X - K, NewY = Y ;
      NewX = X, NewY = Y + K ;
      NewX = X, NewY = Y - K
    ),
    land(NewX, NewY),
    jump(NewX, NewY, K, Dist).
jump(X, Y, _K, Dist) =>
    Dist = abs(X) + abs(Y).

table(-, max)
best_frog(K, Dist) ?=>
    between(1, 10, K),
    jump(0, 0, K, Dist).

main =>
    N = read_int(),
    Facts = [$land(X, Y) : _I in 1..N, X = read_int(), Y = read_int()],
    cl_facts(Facts),
    best_frog(_K, Dist),
    println(Dist).

530G - Расстояние Левенштейна

Классическая задача на динамическое программирование. Авторское решение использует tabling для реализации рекурсии с мемоизацией, аналогично задаче 530F - Прыгающие лягушки.

table(+, +, min)
edit(I, J, Cost) ?=>
    str(S), goal(G),
    N = length(S), M = length(G),
    (
        I <= N, J <= M,
        edit(I + 1, J + 1, NextCost),
        Cost = abs(ord(S[I]) - ord(G[J])) + NextCost
    ;
        I <= N,
        edit(I + 1, J, NextCost),
        Cost = ord(S[I]) - 0'a + 1 + NextCost
    ;
        J <= M,
        edit(I, J + 1, NextCost),
        Cost = ord(G[J]) - 0'a + 1 + NextCost
    ;
        I > N, J > M,
        Cost = 0
    ).

main =>
    S = read_line(), G = read_line(),
    cl_facts([$str(S), $goal(G)]),
    edit(1, 1, Cost),
    println(Cost).

530H - Точки в треугольнике

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

import cp.
main =>
    N = read_int(),
    A :: 1..200, B :: 1..200,
    foreach (_I in 1..N)
        Xi = read_int(), Yi = read_int(),
        B * Xi #<= A * (B - Yi)
    end,
    solve([$min(A * B)], [A, B]),
    println(A * B / 2).

530I - Разные переменные

Эта задача, по сути, задача раскраски графа. Авторское решение использует constraint programming с ограничениями all_distinct. Для ускорения работы программы можно заметить, что первая переменная всегда принимает значение 1. С этим наблюдением можно использовать ограничение all_different (эти ограничения отличаются внутренней реализацией алгоритмов), без него all_different слишком медленно.

import cp.
import util.
main =>
    N = read_int(), K = read_int(),
    Vars = new_array(N),
    Vars :: 1..N, Vars[1] #= 1,
    foreach(_I in 1..K)
        [_ | Indexes] = [to_integer(A) : A in split(read_line())],
        all_distinct([Vars[Idx] : Idx in Indexes])
    end,
    List = to_list(Vars), MaxC #= max(List),
    solve([$min(MaxC)], Vars),
    println(join([to_string(V) : V in Vars])).
  • Проголосовать: нравится
  • +45
  • Проголосовать: не нравится

»
10 лет назад, # |
Rev. 2   Проголосовать: нравится +7 Проголосовать: не нравится

Задачу I в отличие от остальных нельзя было решать "как угодно", 10 слишком большое ограничение для решения за O((N - 1)N).

UPD Первый раз участвую в Surprise раунде, сейчас читаю другие решения и складывается ощущение что остальные участники за время контеста продвинулись сильно дальше меня — table синтаксис, solve'ы, какой-то жуткий синтаксический сахар.

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

    Видимо это результат функциональной парадигмы языка, многие олимпиадники хоть раз писали на ФП. А те, кто углублено программировал на ФП уже знают, какие полезные штуки надо искать в документации.

»
10 лет назад, # |
  Проголосовать: нравится +1 Проголосовать: не нравится

Как можно оценить асимптотику constraint programming? Почему в задаче Е компилятор "догадывается" до быстрого решения вместо полного перебора? Или он перебирает, но из-за особенностей решения результат находится быстро?

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

    Асимптотика почти всегда — экспонента. Происходит перебор с отсечениями. Понять, сколько действительно будет работать — трудно.

    В задаче E все очень просто: перебираются все значения пременных, начиная с последней. Т.е. изначально все переменные 1, потом последней присваивается 2, 3, и т.д. На каждом этапе проверяется, не равна ли сумма + D произведению. Когда последняя переменная достигнет максимума (10000 в авторском решении), предпоследней переменной присвоится 2, а последней — снова 1. Далее опять перебирается последняя переменная, вплоть до N + D, когда будет найдено решение.

    Т.е. в задаче E в авторском решении будет сделано примерно 10000 + N + D шагов.

»
10 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Гм, а может кто-нибудь пояснить, почему единственное прошедшее решение задачи I (10507969) ставит ограничение множества значений :: 1..10?

  • »
    »
    10 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

    Максимальное количество переменных, согласно условию, N = 10. Граф из N вершин всегда можно раскрасить в N цветов, просто присвоив каждой вершине свой цвет 1..N.

    Кстати, в параллельном раунде "VK Cup 2015 — Уайлд-кард раунд 1 (интернет-трансляция)" прошло еще два решения.

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

      У меня, кстати, без Xs[1] #= 1 не влезало по времени ни с all_different, ни с all_distinct.

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

    Ой, я же мог даже 1..N поставить.

    А вообще — именно то, что сказано выше. N ≤ 10 по условию, а в самом худшем случае, если никакие две переменные не могут быть равны, ответ будет 1 2 3 4 5 … N — а если ограничений меньше, то всегда выгоднее присвоить переменным значения ещё меньше. Значит, правильный ответ никогда не будет содержать ни одно значение, превышающее N.

»
10 лет назад, # |
  Проголосовать: нравится +17 Проголосовать: не нравится

Чёрт, а у меня вообще все решения императивные получились. Ну, с небольшим количеством pattern matching и рекурсии. Но что такое "декларативный" и как это использовать я не догадался посмотреть.

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

    М-да. Я вроде знал, что такое декларативное программирование, и писал раньше на Прологе, в том числе и для себя, но у меня тоже получился в большинстве императивный код. Слишком уж привычно это — проще написать так, чем красиво декларативно, тем более что язык новый и неизвестно, что именно он умеет. На Прологе вот императивно особо не напишешь. А на Пикате можно — вот мы и написали. :)

»
10 лет назад, # |
Rev. 3   Проголосовать: нравится 0 Проголосовать: не нравится

Я не знал и не подозревал про тейблинг (хотя мне его очень хотелось! но я не знал даже, что искать в документации). Это что-то специфичное для Пиката, или на Прологе тоже так можно?

»
10 лет назад, # |
  Проголосовать: нравится +11 Проголосовать: не нравится

Если лень знакомиться со строковыми возможностями языка, то Б можно было решить Вот так :DD

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

    Но почему 14 в конце?

    • »
      »
      »
      10 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится -7 Проголосовать: не нравится

      Ну изначально задумывалось, что длины пойдут по возрастанию, как положено :) и вначале при копировании ревностно за этим следил :) но "терпение и труд — все, я устал" :))

»
9 лет назад, # |
  Проголосовать: нравится +13 Проголосовать: не нравится

The main author of Picat, prof. Neng-Fa Zhou, has just published an article "Learning Programming in Picat by Examples from Google Code Jam" — http://picat-lang.org/gcj/gcj_in_picat.pdf

BTW, lot of things have changed in Picat since "VK Cup 2015 Wild Card Round 1"; current version (released just three days ago) is 1.8 — http://picat-lang.org/download.html