Skip to content

aaa2ppp/contestio

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

65 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Contest IO

Go Version License

Contest IO - удобный ввод-вывод, который не тормозит. Специально для контестов на Go.

Статус: Бета-версия. Твои замечания и PR делают библиотеку лучше.


Содержание


Почему Contest IO?

Ты здесь, чтобы побеждать! А не писать ввод/вывод.

fmt? Комфортный седан. Довезет, но с аллокациями.
bufio.Scanner? Шустрее, без аллокаций. Но на смешанных данных - танцы с бубном.
bufio.Reader? Быстро. Но циклы и парсинг - знакомо, да?

Contest IO - это спорткар.

  • Ноль аллокаций - только твои данные
  • Токенизатор обгоняет bufio.Scanner в 1.5 раза
  • Любые целые - на скорости Atoi
  • ScanInt/PrintInt для целых, ScanFloat/PrintFloat для float, ScanWord/PrintWord для слов
  • Ввод/вывод массивов - одной строкой
  • Режим контролируемой паники - альтернатива if err != nil
  • Киллер-фича: contestio-inline встроит код библиотеки в твое решение

Испытай Contest IO в деле!

в начало


Быстрый старт

Установка

go get github.com/aaa2ppp/contestio@latest

Читаем массив, обрабатываем, выводим

package main

import (
    "os"
    . "github.com/aaa2ppp/contestio"  // Точечный импорт делает код кратким
)

func main() {
    br := NewReader(os.Stdin)
    bw := NewWriter(os.Stdout)
    defer bw.Flush()

    var n int
    if _, err := ScanIntLn(br, &n); err != nil {
        panic(err) // В задачах ввод всегда корректен, поэтому можно падать
    }

    a := make([]int, n)
    if _, err := ScanInts(br, a); err != nil {
        panic(err)
    }

    for i := range a {
        a[i] *= a[i]
    }

    PrintIntsLn(bw, a)
}

Важно: Не игнорируй ошибки ввода! Используй контролируемую панику. В большинстве случаев она подскажет, если ты неверно понимаешь условие. Многие "почему не работает?" решаются такой проверкой.

в начало

Надоели if err != nil?

Включи режим Must - Scan будет паниковать на любой ошибке, кроме EOF.

package main

import (
    "os"
    . "github.com/aaa2ppp/contestio"
)

func main() {
    br := NewReader(os.Stdin)
    bw := NewWriter(os.Stdout)
    defer bw.Flush()

    var n int
    ScanIntLn(br, &n)

    a := make([]int, n)
    ScanInts(br, a)

    for i := range a {
        a[i] *= a[i]
    }

    PrintIntsLn(bw, a)
}

в начало

Чтение всех чисел из строки (неизвестная длина)

// Вариант 1: предварительная аллокация
a := make([]int32, 0, 1<<20) 
a, _ = ScanIntsLn(br, a) // функция сделает append

// Вариант 2: аллокация на лету (нужно явно указать тип слайса)
b, _ := ScanIntsLn[[]int32](br, nil)

Чтение строк ScanString / ScanBytes

for {
    s, err := ScanString(br, '\n') // прочтет строку и обрежет финальные пробелы
    if err != nil {
        break
    }
    // ...
}

Нужны байты? Используй ScanBytes. Обе функции читают до разделителя или конца файла, обрезают разделитель и финальные пробелы, возвращают копию данных. Размер строки ограничен только доступной оперативкой.

Ввод/вывод слов (последовательность байт без пробелов)

Аналогично числам.

var word1, word2 string
ScanWordLn(br, &word1, &word2)

words := make([]string, 0, 100)
words, _ = ScanWordsLn(br, words)

PrintWordLn(bw, word1, word2)
PrintWordsLn(bw, words)

Ограничение: при сканировании длина токена не может превышать размер внутреннего буфера (по умолчанию 4КБ) минус один символ. Для чисел это не критично, а при чтении длинных слов может возникнуть ошибка ErrTokenTooLong.

Если максимальная длина слова заранее неизвестна, используй ScanString/ScanBytes в цикле.

в начало

Вывод с разделителями

op := WO{Begin: "[", Sep: ", ", End: "]\n"}
PrintInts(bw, op, []int32{1, 2, 3, 4}) // [1, 2, 3, 4]

Смешанный ввод/вывод (contestio / bufio / fmt)

var age int
var name string

ScanIntLn(br, &age)
name, _ = br.ReadString('\n') // стандартный метод bufio.Reader
name = strings.TrimSpace(name)

// Выводим через fmt
fmt.Fprintf(bw, "name: %q, age: %d\n", name, age)

Тебе доступны все методы bufio.Reader и bufio.Writer.

Чтение/вывод матрицы

var rows, cols int
ScanIntLn(br, &rows, &cols) // можно читать несколько чисел за раз

matrix := make([][]int, rows)
for i := range matrix {
    matrix[i] = make([]int, cols)
    ScanInts(br, matrix[i])
}

for _, row := range matrix {
    PrintIntsLn(bw, row)
}

в начало

Единый интерфейс для разных типов

var (
    a []int8
    b []int32
    c []uint64

    d []float32
    e []float64

    f []string // Слова (последовательности без пробелов)
)

a, _ = ScanIntsLn(br, a)
b, _ = ScanIntsLn(br, b)
c, _ = ScanIntsLn(br, c)

d, _ = ScanFloatsLn(br, d)
e, _ = ScanFloatsLn(br, e)

f, _ = ScanWordsLn(br, f)

PrintIntsLn(bw, a)
PrintIntsLn(bw, b)
PrintIntsLn(bw, c)

PrintFloatsLn(bw, d)
PrintFloatsLn(bw, e)

PrintWordsLn(bw, f)

в начало

Ввод/вывод значений разных типов ScanAny / PrintAny

var product string // слово
var price float64
var amount uint

ScanAny(br, &product, &price, &amount)
PrintAnyLn(bw, "Товар:", &product, "\nЦена:", &price, "\nКол-во:", &amount)

Экспериментальный функционал под тегом -tags=any

Как избежать лишних аллокаций в PrintAny?

  1. Всегда передавай указатели (не значения).
  2. В циклах объявляй переменные вне цикла и переиспользуй их.

Правильно:

var x int
for i := 0; i < N; i++ {
    x = data[i]
    PrintAny(bw, op, &x)   // без аллокаций
}

Неправильно (аллокации на каждой итерации):

for i := 0; i < N; i++ {
    x := data[i]           // новая переменная
    PrintAny(bw, op, &x)   // всё равно аллокация
}

Также неправильно (передача значения):

PrintAny(bw, op, data[i])  // аллокация для каждого значения

в начало

Больше примеров

Больше примеров можешь найти в github.com/aaa2ppp/ya-algo-training9-pub.


Обработка ошибок: тег сборки must

По умолчанию все функции сканирования и печати возвращают ошибки.
В задачах с гарантированно корректным вводом это приводит к шаблонным проверкам if err != nil { panic(err) }.

Чтобы избавиться от них, добавь тег сборки must:

go test -tags=must ./solve
cat input.txt | go run -tags=must ./solve

При активном теге must любая ошибка (кроме io.EOF) вызывает панику.
Это касается всех публичных функций ScanXXX/PrintXXX и их вариантов с Ln.

Важно: Функции библиотеки возвращают io.EOF только, если данных больше нет. Неполный ввод возвращает io.UnexpectedEOF. Функции никогда не возвращают ошибку, если прочли все запрошенные данные.

Пример:

// Без тега must
if _, err := ScanInt(br, &x); err != nil {
    panic(err)
}

// С тегом - тоже поведение, но меньше кода
ScanInt(br, &x)

в начало


Киллер-фича: contestio-inline

Многие проверяющие платформы (Яндекс.Контест, Codeforces) запрещают внешние зависимости.
Ручное копирование парсеров в каждое решение - громоздко и неудобно.

contestio-inline решает эту проблему:

  • Пишешь решение с точечным импортом contestio - код чистый и краткий.
  • Одной командой утилита встраивает в твой файл только те функции библиотеки, которые реально используются.
  • Полученный файл самодостаточен и готов к отправке.
# Установка
go install github.com/aaa2ppp/contestio/cmd/contestio-inline@latest

# Инлайн (требуется точечный импорт!)
contestio-inline main.go

# Откат (восстанавливает исходный код)
contestio-inline -clear main.go

Важно:

  • Утилита работает только с точечным импортом: import . "github.com/aaa2ppp/contestio". Обычный или алиасный импорт не поддерживается.
  • Если используешь теги сборки (например, -tags=must), укажи их: contestio-inline -tags=must main.go.
  • При ошибках (неверный импорт, синтаксическая ошибка) утилита ничего не меняет - твой код в безопасности. На крайний случай есть бэкап main.go~.

в начало


Производительность

Все бенчмарки - для 1M (2^20) чисел (int или float64), буфер 4 КБ.
Система: Intel Core i3-7100 @ 3.90GHz, Windows, Go 1.24.2.

В таблицах жирным выделены core функции contestio.
Время в миллисекундах - чем меньше, тем лучше.

Чтение целых чисел

Метод Время (ms) Память (KB) Аллокации
fmt.Fscan 473 12857 1 005 000
ReadString + Fields 67 43670 3 400
Scanner + ScanWords 106 0 0
scanSlice 62 0 0
scanSliceLn 62 0 0
scanVars (in loop) 65 0 0

Вывод целых чисел

Метод Время (ms) Память (KB) Аллокации
fmt.Fprintf 109 7540 965 000
AppendInt 37 50 2 600
printSlice 39 0 0
printVals (in loop) 52 0 0

в начало

Бенчмарки для чисел с плавающей точкой (нажми, чтобы развернуть)

Чтение чисел с плавающей точкой

Метод Время (ms) Память (KB) Аллокации
fmt.Fscan 759 24216 1 049 000
ReadString + Fields 141 59646 5 400
Scanner + ScanWords 194 0 0
scanSlice 130 0 0
scanSliceLn 131 0 0
scanVars (in loop) 134 0 0

Вывод чисел с плавающей точкой

Метод Время (ms) Память (KB) Аллокации
fmt.Fprintf 201 8193 1 049 000
AppendFloat 109 179 7 900
printSlice 113 0 0
printVals (in loop) 123 0 0

в начало

Что значат цифры?

  • fmt.Fscan - универсален, для каждого значения создает интерфейс. 1 млн чисел - 1 млн аллокаций.
  • ReadString + Fields - сначала должен все прочесть.
  • Scanner + ScanWords - универсальность съедает время.
  • AppendXXX - пишет напрямую в буфер, но иногда места уже нет (но это решаемо).
  • Функции contestio - парсят и пишут числа напрямую в буфер.

Почему Contest IO не быстрее AppendInt?
Потому что под капотом использует его. В бенчмарках printSlice на 2 ms медленнее AppendInt.
Это плата за возможность работать с любыми числовыми слайсами и разделители.

Выводы

  • fmt - когда не торопишься и не жалко ресурсов.
  • Scanner + AppendXXX - хорошая пара, но требует ручного кода.
  • Contest IO - дает скорость и ноль аллокаций с минимумом усилий.

в начало


Расширенное использование: API с интерфейсами (тег sugar)

Если хочешь абстрагироваться от конкретных типов и использовать интерфейсы - включи сахар.
Он активируется тегом сборки -tags=sugar и добавляет обертки над слайсами Ints[T Int] и Floats[T Float], которые реализуют интерфейсы для универсальных функций ScanSlice и PrintSlice.

Пример с «сахаром»:

package main

import (
    "os"
    . "github.com/aaa2ppp/contestio"
)

func main() {
    br := NewReader(os.Stdin)
    bw := NewWriter(os.Stdout)
    defer bw.Flush()

    var n int
    var a Ints[int] // сахарный тип - работает, как обычный слайс

    ScanIntLn(br, &n)

    a = Resize(a, n)
    ScanSlice(br, a)

    a = solve(a)

    PrintSliceLn(bw, a)
}

// Здесь нет ошибки! Да, Ints[int] можно присваивать переменной []int и наоборот
func solve(a []int) []int {
    for i := range a {
        a[i] *= a[i]
    }
    return a
}

Особенности:

  • Позволяет писать более абстрактный код, не привязываясь к конкретным типам срезов, и отказаться от специализированных функций вроде ScanInts, PrintInts.
  • Типы Ints[T], Floats[T] и Words[T] прозрачно работают как обычные слайсы: их можно передавать, изменять, индексировать и итерировать range.
  • Требует указания тега сборки: go build -tags=sugar.
  • Для комфортной работы в VS Code необходимо добавить "go.buildTags": "sugar" в настройки проекта (иначе автодополнение и тесты не увидят код с тегом).

Сахар не обязателен для эффективного использования библиотеки. Основной API (ScanInts, PrintInts и т.п.) справляется со всем, что нужно на контестах.
Сахар - это дополнительный слой для тех, кто хочет поэкспериментировать с интерфейсами или использовать библиотеку вне соревнований.

в начало


Настройка VS Code для работы с тегами сборки

Если ты используешь теги must или sugar, добавь в .vscode/settings.json:

"go.buildTags": "must,sugar"

После этого автодополнение, анализ кода и тесты будут корректно видеть код, защищённый тегами.

в начало


About

Contest IO - удобный ввод-вывод, который не тормозит. Специально для контестов на Go.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors