Spec-Zone.ru › Kotlin 1.9

Kotlin для соревновательного программирования

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

Соревновательное программирование — это интеллектуальный спорт, где участники пишут программы для решения чётко сформулированных алгоритмических задач в строгих ограничениях. Задачи могут варьироваться от простых, которые может решить любой разработчик программного обеспечения и требуют небольшого кода для получения правильного решения, до сложных, которые требуют знания специальных алгоритмов, структур данных и большого опыта. Хотя Kotlin не был специально разработан для соревновательного программирования, он хорошо подходит для этой области, сводя к минимуму стандартный объём служебного кода, который программисту нужно написать и прочитать, практически до уровня, предлагаемого динамически типизированными языками сценариев, при этом сохраняя инструментарий и производительность статически типизированного языка.

См. Начало работы с Kotlin/JVM, чтобы настроить среду разработки для Kotlin. В соревновательном программировании обычно создаётся один проект, а решение каждой задачи записывается в отдельном исходном файле.

Простой пример: задача «Достижимые числа»

Давайте рассмотрим конкретный пример.

Codeforces Round 555 проходил 26 апреля для 3-го дивизиона, что означает, что задачи подходят для попыток любым разработчикам. Вы можете использовать эту ссылку, чтобы ознакомиться с задачами. Самая простая задача в наборе — Задача A: Достижимые числа. Она требует реализации прямолинейного алгоритма, описанного в условии задачи.

Мы начнём решение, создав файл исходного кода Kotlin с произвольным именем. A.kt подойдёт. Сначала вам нужно реализовать функцию, указанную в условии задачи, как:

Обозначим функцию f(x) следующим образом: мы добавляем 1 к x, затем, пока в полученном числе есть хотя бы один конечный ноль, мы удаляем этот ноль.

Kotlin — это прагматичный и непредвзятый язык, поддерживающий как императивный, так и функциональный стили программирования без принуждения разработчика к одному из них. Вы можете реализовать функцию f в функциональном стиле, используя такие функции Kotlin, как рекурсия хвоста:

tailrec fun removeZeroes(x: Int): Int =
    if (x % 10 == 0) removeZeroes(x / 10) else x

fun f(x: Int) = removeZeroes(x + 1)

В качестве альтернативы вы можете написать императивную реализацию функции f с использованием традиционного цикла while и изменяемых переменных, которые обозначаются в Kotlin с помощью var:

fun f(x: Int): Int {
    var cur = x + 1
    while (cur % 10 == 0) cur /= 10
    return cur
}

Типы в Kotlin необязательны во многих местах из-за повсеместного использования вывода типов, но каждое объявление всё же имеет хорошо определённый статический тип, известный на этапе компиляции.

Теперь всё, что осталось, — это написать главную функцию, которая считывает входные данные и реализует остальную часть алгоритма, требуемую условием задачи — вычислить количество различных целых чисел, получаемых при многократном применении функции f к начальному числу n, которое задаётся в стандартном входе.

По умолчанию Kotlin выполняется на JVM и предоставляет прямой доступ к богатой и эффективной библиотеке коллекций с универсальными коллекциями и структурами данных, такими как динамически изменяемые массивы (ArrayList), карты и множества на основе хешей (HashMap/HashSet), упорядоченные карты и множества на основе деревьев (TreeMap/TreeSet). Используя множество хешей целых чисел для отслеживания уже достигнутых значений при применении функции f, прямолинейная императивная версия решения задачи может быть записана следующим образом:

fun main() {
    var n = readln().toInt() // read integer from the input
    val reached = HashSet<Int>() // a mutable hash set 
    while (reached.add(n)) n = f(n) // iterate function f
    println(reached.size) // print answer to the output
}

В соревновательном программировании нет необходимости обрабатывать случай неверно отформатированных входных данных. Формат входных данных всегда чётко определён в условиях задачи, а фактические входные данные не могут отклоняться от спецификации входных данных в условии задачи. Поэтому вы можете использовать функцию Kotlin readln(). Она проверяет, что строка ввода присутствует, и в противном случае генерирует исключение. Аналогично, функция String.toInt() генерирует исключение, если строка ввода не является целым числом.

fun main() {
    var n = readLine()!!.toInt() // read integer from the input
    val reached = HashSet<Int>() // a mutable hash set 
    while (reached.add(n)) n = f(n) // iterate function f
    println(reached.size) // print answer to the output
}

Обратите внимание на использование оператора явного обращения к не-null значению !! после вызова функции readLine(). Функция Kotlin readLine() определена как возвращающая типа с возможным значением null String? и возвращает null в конце ввода, что явно заставляет разработчика обрабатывать случай отсутствия ввода.

В соревнованиях по программированию нет необходимости обрабатывать случай неверно отформатированных входных данных. В соревнованиях по программированию формат входных данных всегда чётко определён, и фактические входные данные не могут отклоняться от спецификации входных данных в условии задачи. Именно это и делает оператор явного обращения к не-null значению !! — он проверяет, что строка ввода присутствует, и в противном случае генерирует исключение. Аналогично, функция String.toInt().

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

private fun readStr() = readln() // string line
private fun readInt() = readStr().toInt() // single int
// similar for other types you'd use in your solutions
private fun readStr() = readLine()!! // string line
private fun readInt() = readStr().toInt() // single int
// similar for other types you'd use in your solutions

Обратите внимание на использование private модификатора видимости здесь. Хотя концепция модификатора видимости совершенно не относится к соревновательному программированию, она позволяет размещать несколько файлов решений на основе одного шаблона без получения ошибки из-за конфликтующих публичных объявлений в одном пакете.

Пример с функциональными операторами: задача «Большое число»

Для более сложных задач обширная библиотека функциональных операций над коллекциями Kotlin полезна для минимизации служебного кода и преобразования кода в линейную, последовательную и удобочитаемую цепочку преобразований данных. Например, задача Задача B: Большое число реализуется простым жадным алгоритмом, и её можно написать в таком стиле без одной изменяемой переменной:

fun main() {
    // read input
    val n = readln().toInt()
    val s = readln()
    val fl = readln().split(" ").map { it.toInt() }
    // define local function f
    fun f(c: Char) = '0' + fl[c - '1']
    // greedily find first and last indices
    val i = s.indexOfFirst { c -> f(c) > c }
        .takeIf { it >= 0 } ?: s.length
    val j = s.withIndex().indexOfFirst { (j, c) -> j > i && f(c) < c }
        .takeIf { it >= 0 } ?: s.length
    // compose and write the answer
    val ans =
        s.substring(0, i) +
        s.substring(i, j).map { c -> f(c) }.joinToString("") +
        s.substring(j)
    println(ans)
}
fun main() {
    // read input
    val n = readLine()!!.toInt()
    val s = readLine()!!
    val fl = readLine()!!.split(" ").map { it.toInt() }
    // define local function f
    fun f(c: Char) = '0' + fl[c - '1']
    // greedily find first and last indices
    val i = s.indexOfFirst { c -> f(c) > c }
        .takeIf { it >= 0 } ?: s.length
    val j = s.withIndex().indexOfFirst { (j, c) -> j > i && f(c) < c }
        .takeIf { it >= 0 } ?: s.length
    // compose and write the answer
    val ans =
        s.substring(0, i) +
        s.substring(i, j).map { c -> f(c) }.joinToString("") + 
        s.substring(j)
    println(ans)
}

В этом плотном коде, помимо преобразований коллекций, вы можете увидеть такие полезные функции Kotlin, как локальные функции и оператор выбора ?:, которые позволяют выразить выражения, такие как «взять значение, если оно положительно, или использовать длину», с компактными и читаемыми выражениями, такими как .takeIf { it >= 0 } ?: s.length, но вполне нормально в Kotlin создавать дополнительные изменяемые переменные и выражать тот же код в императивном стиле.

Чтобы сделать чтение входных данных в задачах соревновательного программирования, таких как эта, более компактным, можно иметь следующий список вспомогательных функций чтения входных данных:

private fun readStr() = readln() // string line
private fun readInt() = readStr().toInt() // single int
private fun readStrings() = readStr().split(" ") // list of strings
private fun readInts() = readStrings().map { it.toInt() } // list of ints
private fun readStr() = readLine()!! // string line
private fun readInt() = readStr().toInt() // single int
private fun readStrings() = readStr().split(" ") // list of strings
private fun readInts() = readStrings().map { it.toInt() } // list of ints

С помощью этих вспомогательных функций часть кода для чтения входных данных становится проще, тесно следуя спецификации входных данных в условии задачи строка за строкой:

// read input
val n = readInt()
val s = readStr()
val fl = readInts()

Обратите внимание, что в соревнованиях по программированию принято давать переменным более короткие имена, чем это типично в промышленном программировании, поскольку код нужно написать только один раз и не поддерживать его впоследствии. Однако эти имена обычно всё ещё являются мнемоническими — a для массивов, i, j, и другие для индексов, r, и c для номеров строк и столбцов в таблицах, x и y для координат и так далее. Легче сохранять те же имена для входных данных, что и в условии задачи. Однако более сложные задачи требуют большего объёма кода, что приводит к использованию более длинных, самодокументирующих имён переменных и функций.

Дополнительные советы и хитрости

Задачи по программированию для соревнований часто имеют такой ввод:

Первая строка ввода содержит два целых числа n и k

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

val (n, k) = readInts()

Возможно, будет соблазн использовать класс JVM java.util.Scanner для разбора менее структурированных форматов ввода. Kotlin разработан для хорошей работы с библиотеками JVM, поэтому их использование в Kotlin кажется вполне естественным. Однако будьте осторожны, что java.util.Scanner чрезвычайно медленный. Настолько медленный, что разбор 105 или более целых чисел с его помощью может не вписаться в типичный лимит времени в 2 секунды, с которым справится простой Kotlin's split(" ").map { it.toInt() }

Вывод в Kotlin обычно прост с вызовами println(...) и использованием шаблонов строк Kotlin. Однако следует проявлять осторожность, когда вывод содержит порядка 105 строк или более. Выдача стольких println вызовов слишком медленная, так как вывод в Kotlin автоматически сбрасывается после каждой строки. Более быстрый способ записи многих строк из массива или списка — использование функции joinToString() с "\n" в качестве разделителя, как это показано ниже:

println(a.joinToString("\n")) // each element of array/list of a separate line

Изучение Kotlin

Kotlin легко изучить, особенно для тех, кто уже знаком с Java. Краткое введение в базовый синтаксис Kotlin для разработчиков программного обеспечения можно найти непосредственно в разделе справки веб-сайта, начиная с основного синтаксиса.

IDEA имеет встроенный конвертер Java в Kotlin. Его могут использовать люди, знакомые с Java, для изучения соответствующих синтаксических конструкций Kotlin, но он не идеален, и все же стоит ознакомиться с Kotlin и изучить приемы программирования Kotlin.

Отличным ресурсом для изучения синтаксиса Kotlin и API стандартной библиотеки Kotlin являются Kotlin Koans.

Последнее изменение: 21 октября 2022 г.
Kotlin для науки о данных Что нового в Kotlin 1.9.0

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/competitive-programming.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API