search icon search icon ВЕРСИЯ ДЛЯ СЛАБОВИДЯЩИХ

Инженерный тур. 2 этап

Задача 1.1.(100 баллов)
Нормальная регрессия
Темы: линейная регрессия, нормализация

Имя входного файла: стандартный ввод или input.txt.

Имя выходного файла: стандартный вывод или output.txt.

Ограничение по времени выполнения программы: 1 с.

Ограничение по памяти: 256 Мбайт.

Условие

Игорю и Олегу поручили построить линейную регрессию на наборе данных.

Сначала Игорь преобразовал все признаки, включая целевой минимакс нормализацией, а затем Олег построил линейную регрессию на нем. Оказалось, что полученное уравнение прямой не подходит для дальнейшего предсказания, так как оно аппроксимирует нормализованные, а не оригинальные данные. Тогда Олег решил построить уравнение прямой на оригинальном наборе данных, но оказалось, что он потерялся.

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

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

Формат входных данных

Первая строка содержит одно натуральное число \(N\) (\(1 \leqslant N \leqslant 10^5\)) — число признаков в наборе данных без учета целевого признака.

Вторая строка содержит \((N + 1)\) разделенное пробелом целое число \(l_i\) (\(\left| l_i \right| \leqslant 10^9\)) — минимальное значение для каждого признака до нормализации. Первые \(N\) из этих чисел чисел соответствуют обычным признакам, а последнее — целевому.

Третья строка содержит \((N + 1)\) разделенное пробелом целое число \(u_i\) (\(\left| u_i \right| \leqslant 10^9\)) — максимальные значения в аналогичном формате. Гарантируется, что все максимумы строго больше соответствующих минимумов.

Четвертая строка содержит \((N + 1)\) разделенное пробелом целое число \(c_i\) (\(\left| c_i \right| \leqslant 10^9\)) — коэффициенты прямой, полученной после нормализации. Первые \(N\) из этих чисел чисел соответствуют признакам, а последнее — свободный коэффициент.

Пятая строка содержит одно натуральное число \(M\) (\(1 \leqslant M \leqslant 10^5\)) — число запросов Олега.

Далее идут \(M\) строк, каждая содержит описание соответствующего запроса. Каждый запрос начинается с одной маленькой латинской буквы l, u, c или q в зависимости от его вида:

  • запрос вида l \(i\) \(v\) — запрос на изменение минимального значения до нормализации для \(i\)-го признака на \(v\);
  • запрос вида u \(i\) \(v\) — запрос на изменение максимального значения до нормализации для \(i\)-го признака на \(v\);
  • запрос вида c \(i\) \(v\) — запрос на изменение \(i\)-го коэффициента прямой на \(v\);
  • запрос вида q \(i\) — запрос значения \(i\)-го коэффициента оригинальной прямой.

Гарантируется, что \(i\) — целое число и \(1 \leqslant i \leqslant n + 1\). Если \(i\) равняется \((n + 1)\) в запросах l и u, то это значит, что в запросе изменялся минимум или максимум целевого признака, а в запросах c и q имелся в виду свободный коэффициент прямой. Гарантируется, что \(v\) — целое число и \(\left| v \right| \leqslant 10^9\), а после запросов l и u минимальное значение соответствующего признака по-прежнему строго меньше максимального.

Формат выходных данных

Для каждого запроса коэффициента оригинальной прямой выведите его значение — число с плавающей точкой.

Решение будет засчитано, если абсолютная или относительная погрешность не будет превышать \(10^{-9}\).

Тестовые данные

Номер тестаСтандартный вводСтандартный вывод
1
4
1 1 1 1 0
2 2 2 2 1
1 2 3 4 5
5
q 5
l 1 0
u 2 6
c 3 4
q 5
-5
-3.4

Решение

После минимакс-нормализации уравнение для предсказания выглядит как:

\[y(x) = \sum_{i=1}^n c_i x_i + c_{n+1}.\]

Подставим вместо \(x_i\) нормализованную версию \(x_i = \frac{x'_i-l_i}{u_i-l_i}\), где \(x'\) — оригинальные данные до нормализации:

\[y(x) = \sum_{i=1}^n c_i \frac{x'_i-l_i}{u_i-l_i} + c_{n+1}.\]

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

\[y_d(x) =(u_{n+1} - l_{n+1}) \cdot y(x) + l_{n+1} = (u_{n+1} - l_{n+1}) \cdot \left ( \sum_{i=1}^n c_i \frac{x'_i-l_i}{u_i-l_i} + c_{n+1} \right ) + l_{n+1}.\]

Так как нормализация и обратное к ней преобразование являются линейными, их можно скомбинировать в новое линейное уравнение:

\[y_d(x) =\sum_{i=1}^n \frac{ (u_{n+1} - l_{n+1})c_i}{u_i-l_i} x'_i + (u_{n+1} - l_{n+1}) \left(-\sum_{i=1}^n \frac{c_i l_i}{u_i-l_i} +c_{n+1}\right) + l_{n+1}.\]

В этом уравнении коэффициент при \(x'_i\) равен: \(\frac{ (u_{n+1} - l_{n+1})c_i}{u_i-l_i}\), его можно просто вычислить за \(\mathcal O(1)\) при ответе на соответствующий запрос.

В том же уравнении свободный коэффициент равен \[(u_{n+1} - l_{n+1}) \left(c_{n+1} - \sum_{i=1}^n \frac{c_i \cdot l_i}{u_i-l_i} \right)+ l_{n+1}.\] Наивное его вычисление потребует \(\mathcal O(N)\) времени, так как содержит в себе сумму \(\sum_{i=1}^n \frac{c_i \cdot l_i}{u_i-l_i}\). Но эту сумму можно предподсчитать, а затем обновлять соответствующие слагаемое при запросе на изменение. Тогда на запрос значения свободного коэффициента можно отвечать за \(\mathcal O(1)\).

Пример программы-решения

Ниже представлено решение на языке Python.

Python
n = int(input())

l = list(map(int, input().split()))
u = list(map(int, input().split()))
c = list(map(int, input().split()))

total_sum = 0

for i in range(n):
    total_sum += l[i] * c[i] / (u[i] - l[i])

m = int(input())

for _ in range(m):
    row = list(input().split())
    
    
    t = row[0]
    i = int(row[1]) - 1

    if t == 'q':
        if i == n:
            print((c[n] - total_sum) * (u[n] - l[n]) + l[n])
        else:
            print(c[i] * (u[n] - l[n]) / (u[i] - l[i]))
    else:
        v = int(row[2])
        if i != n:
            total_sum -= l[i] * c[i] / (u[i] - l[i])

        if t == 'l':
            l[i] = v
        elif t == 'u':
            u[i] = v
        else:
            c[i] = v

        if i != n:
            total_sum += l[i] * c[i] / (u[i] - l[i])
Задача 1.2.(100 баллов)
Дикий лес
Тема: сортировки

Имя входного файла: стандартный ввод или input.txt.

Имя выходного файла: стандартный вывод или output.txt.

Ограничение по времени выполнения программы: 1 с.

Ограничение по памяти: 256 Мбайт.

Условие

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

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

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

Формат входных данных

Первая строка содержит натуральное число \(N\) (\(4 \leqslant N \leqslant 10^5\)) — число деревьев в ряду.

Вторая строка содержит \(N\) натуральных чисел \(h_i\) (\(1 \leqslant h_i \leqslant 10^9\)) — высоты соответствующих деревьев.

Формат выходных данных

Выведите \((N - 3)\) оценки для всех возможных разбиений. Крайние разбиения не учитываются, чтобы избежать деления на 0.

Каждая оценка должна состоять из трех вещественных чисел с плавающей точкой: значение для первой части разбиения, между частями и для второй части. Ответ будет засчитан, если его абсолютная или относительная погрешность не будет превышать \(10^{-9}\).

Тестовые данные

Номер тестаСтандартный вводСтандартный вывод
1
5
1 3 8 9 12
2.0 7.916228058025278 2.943920288775949
5.0990195135927845 7.291547618075786 3.0

Примечания

В примере для первого оцениваемого разбиения деревья разбиваются на две части: 1 3 и 8 9 12.

Значение оценки для первой части равно \(\sqrt{\frac{(1-3)^2}{1}}\).

Значение оценки между частями равно \(\sqrt{\frac{7^2+8^2+11^2+5^2+6^2+9^2}{6}}\).

Значение оценки для второй части равно \(\sqrt{\frac{1^2+3^2+4^2}{3}}\).

Решение

Среднее квадратическое вычисляется как корень из суммы квадратов, деленной на число слагаемых в этой сумме. Рассмотрим \(p\)-е разбиение. Оно будет состоять из двух частей. В первой части будет \(l\, = \,p\, +\, 1\) элемент, а во второй — \(r = n - l = n - p - 1\). Тогда сумма для оценки среднеквадратической разности для первой части будет содержать \(l(l-1)/2\) слагаемых, для второй будет \(r(r-1)/2\), а между частями будет \(lr\). Потому что ровно столько будет различных пар разностей для соответствующих средних квадратических.

Теперь необходимо научиться быстро вычислять сумму квадратов разностей для различных разбиений и между ними. Для начала решим задачу только для первой части каждого разбиения. Если рассматривать все разбиения в порядке возрастания номера, то каждый раз к первой части будет добавляться один элемент из второй. Получается, что сумма квадратов разностей будет состоять из предыдущей суммы плюс суммы квадратов разности добавляемого элемента \(h_{l}\) с существующими \(\left\{h_i\right\}_{i=1}^{l-1}\):

\[\sum_{i=1}^l\sum_{j=1}^{i-1} (h_i - h_j)^2 = \sum_{i=1}^{l-1}\sum_{j=1}^{i-1} (h_i - h_j)^2 + \sum_{i=1}^{l-1} (h_i - h_l)^2.\]

Добавляемую сумму можно переписать как:

\[\begin{aligned} \sum_{i=1}^{l-1} (h_i - h_l)^2 = \sum_{i=1}^{l-1} (h_i^2 - 2 h_i h_l + h_l^2) =\sum_{i=1}^{l-1} h_i^2 - \sum_{i=1}^{l-1} 2 h_i h_l + \sum_{i=1}^{l-1} h_l^2 =\\ {}=\sum_{i=1}^{l-1} h_i^2 - 2h_l \sum_{i=1}^{l-1} h_i + (l-1) h_l ^ 2. \end{aligned}\]

В свою очередь, суммы \(\sum_{i=1}^{l-1} h_i\) и \(\sum_{i=1}^{l-1} h_i^2\) также можно не вычислять заново, а воспользоваться уже вычисленными суммами \(\sum_{i=1}^{l-2} h_i\) и \(\sum_{i=1}^{l-2} h_i^2\) с предыдущего разбиения: \(\sum_{i=1}^{l-1} h_i = \sum_{i=1}^{l-2} h_i + h_{l-1}\) и \(\sum_{i=1}^{l-1} h_i^2 = \sum_{i=1}^{l-2} h_i^2 + h_{l-1}^2\). Получается, что каждое обновление каждой суммы производится за \(\mathcal O(1)\).

Для вычисления суммы квадратов разностей второй части каждого разбиения можно развернуть массив и применить алгоритм, который использовался для первых частей. Заметим, что в конце этого же алгоритма при добавлении всех элементов в одну часть получится сумма квадратов разностей между всеми элементами \(\sum_{i=1}^{n} \sum_{j=1}^{i-1} (h_i-h_j)^2\), которая также пригодится в дальнейшем.

Для любого разбиения сумму квадратов разностей между частями можно вычислить как разницу суммы квадратов разностей между всеми элементами и между элементами первой и второй частей, так как оба элемента из каждой пары лежат либо в одной части, либо в разных: \[\sum_{i=1}^{n} \sum_{j=1}^{i-1} (h_i-h_j)^2 = \sum_{i=1}^{l} \sum_{j=1}^{i-1} (h_i-h_j)^2 + \sum_{i=1}^{l} \sum_{j=l+1}^{n} (h_i-h_j)^2 + \sum_{i=l+1}^{n} \sum_{j=i+1}^{n} (h_i-h_j)^2 \Rightarrow\] \[\Rightarrow \sum_{i=1}^{l} \sum_{j=l+1}^{n} (h_i-h_j)^2 = \sum_{i=1}^{n} \sum_{j=1}^{i-1} (h_i-h_j)^2 - \sum_{i=1}^{l} \sum_{j=1}^{i-1} (h_i-h_j)^2 - \sum_{i=l+1}^{n} \sum_{j=i+1}^{n} (h_i-h_j)^2.\] Получается, что для каждого разбиения сумму квадратов разностей между частями также можно вычислить за \(\mathcal O(1)\) из уже вычисленных сумм.

Пример программы-решения

Ниже представлено решение на языке Python.

Python
import math

def solve_in_half(n, h, res):
    sum1 = h[0]
    sum2 = h[0] * h[0]
    ans = 0

    for p in range(1, n):
        res[p - 1] = ans
        hh = h[p] * h[p]
        ans += sum2
        ans += hh * p
        ans -= 2 * h[p] * sum1
        sum1 += h[p]
        sum2 += hh
    return ans

def solve(n, h):
    h = h[:]
    l = [0] * (n - 1)
    m = [0] * (n - 1)
    r = [0] * (n - 1)

    solve_in_half(n, h, l)
    reverse(n, h)
    total = solve_in_half(n, h, r)
    reverse(n - 1, r)

    for i in range(n - 1):
        m[i] = total - l[i] - r[i]

    for i in range(1, n - 2):
        cntL = i + 1
        cntR = n - cntL
        cntM = cntL * cntR
        cntL = cntL * (cntL - 1) / 2
        cntR = cntR * (cntR - 1) / 2
        l[i] = math.sqrt(l[i] / cntL)
        m[i] = math.sqrt(m[i] / cntM)
        r[i] = math.sqrt(r[i] / cntR)

    return l, m, r

def reverse(n, a):
    l, r = 0, n - 1
    while l < r:
        a[l], a[r] = a[r], a[l]
        l += 1
        r -= 1


n = int(input())
h = list(map(int, input().split()))

res = solve(n, h)
l, c, r = res

for i in range(1, n - 2):
    print(l[i], c[i], r[i])    
Задача 1.3.(100 баллов)
Выборы
Темы: машинное обучение, классификация, графы

Условие

Скоро в округе Самая Шуточная Агломерация должны пройти выборы нового мэра. За этот пост борются два кандидата: Даниил Вичсенд и Камила Маклис. Для анализа предвыборной кампании был нанят программист Марк Ящеров, который решил выяснить, как голосуют избиратели в зависимости от их предпочтений. Он провел опрос в одной социальной сети и выяснил, как собираются голосовать люди, но не смог узнать никаких характеристик о них. Вместо этого у него есть информация о некотором коэффициенте схожести между пользователями. Но как на основе него можно построить предсказание, Марк не разобрался. Помогите ему с этим.

Набор данных состоит из двух таблиц в csv формате: train.csv и sim.csv.

  • train.csv состоит из столбцов id и class, он содержит информацию о том, за кого избиратели собираются голосовать.
  • sim.csv состоит из столбцов id1, id2 и sim, он содержит информацию о схожести между измерителями с индексами id1 и id2. Схожесть симметрична.

Необходимо загрузить в тестирующую систему csv-файл со столбцами id и class, в котором будут предсказания результата голосования для избирателей с id от 10000 до 14999.

Решение

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

Более продвинутый подход основывается на предварительном извлечении признаков.

Пример программы-решения

Ниже представлено решение на языке Python.

Python
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression

# Загрузка данных
train_data = pd.read_csv("train.csv")
sim_data = pd.read_csv("sim.csv")

#Построение полной матрицы схожести
similar = pd.pivot_table(sim_data, values='sim', index='id1', columns='id2').fillna(0)

X = similar.loc[train_data['id']].fillna(0)
y = train_data['class']
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.05, random_state=42)

# Извлечение признаков
pca = PCA(n_components=95, random_state=2750)
pca.fit_transform(X_train)
X_train = pca.transform(X_train)
X_val = pca.transform(X_val)

# Обучение модели и построение предсказаний
model = LogisticRegression(C=0.844, max_iter=4000)
model.fit(X_train, y_train)

test_id = range(10000, 15000)
test_data = similar.loc[test_id]
predictions = model.predict(pca.transform(test_data.fillna(0)))

ans = pd.DataFrame({'id': test_id, 'class':predictions})
ans.to_csv('predictions.csv', index=False)

Критерии оценивания

Для оценки решения используется F-мера.

Баллы, которые получатся в результате, будут вычислены по формуле: \[max\left(0; 100 \frac{S - B}{M - B}\right),\] где \(S\) — качество решения задачи командой, \(B\) — качество базового решения, а \(M\) — качество наилучшего среди всех команд решения.

Задача 1.4.(100 баллов)
Беспорядочные категории
Темы: машинное обучение, извлечение признаков

Условие

Однажды Роман попал в клуб настольных игр «Куб Ведер», где надумал посетить зал карточных игр. Роман там был в первый раз и решил разобраться в правилах всех игр. Он не хотел выглядеть новичком, поэтому попытался понять правила, просто наблюдая за играми. Это оказалось не так уж и просто. Для каждой игры задействуются несколько различных колод карт. В каждом раскладе используется ровно по одной карте из каждой колоды, которые выбираются в зависимости от других уже выложенных карт, но независимо от предыдущих раскладов. Более того, это специальные фантазийные карты, на которых не указано достоинство, а просто изображено какое-то сказочное существо.

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

Помогите восстановить порядок карт по истории раскладов для каждой игры.

Дано множество csv-файлов. Каждый из них содержит информацию о раскладах для соответствующей игры. Название каждого файла имеет формат gid_n_m.csv, где gid — индекс игры, n — число выписанных раскладов в этой игре, а m — число используемых колод и карт в соответствующем раскладе. Каждый расклад содержит для каждой колоды условный номер карты, которая была выбрана для этого расклада.

В тестирующую систему необходимо загрузить csv-файл со столбцами: game, deck и order, где game — индекс игры, deck — номер колоды, order — предполагаемый порядок карт.

Решение

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

Так как наборы данных не содержали информации о целевой задачи, для обучения эмбендингов необходимо было использовать самообучение. В качестве суррогатной задачи можно было использовать задачу обучения автокодировщика, который по значению эмбендингов должен предсказать их же самих. Архитектура автокодировщика должна была содержать регуляризирующий фактор, например, «бутылочное горлышко», чтобы тождественная функция, которую аппроксимирует автокодировщик, не была тривиальной. Другая важная часть автокодировщика — пакетная нормализация, которая призвана предотвратить вырождение эмбедигов, поскольку тривиальным решением будет заполнение всех эмбендингов нулями

Пример программы-решения

Ниже представлено решение на языке Python.

Python
import os
import numpy as np
import pandas as pd
import torch
import torch.nn as nn

class AutoEncoder(nn.Module):
    def __init__(self, dims):
        super(AutoEncoder,self).__init__()

        self.k = len(dims)
        self.embedings = []
        self.dims = dims

        for dim in dims:
            self.embedings.append(nn.Embedding(dim, 1))
            
        self.embedings = nn.ModuleList(self.embedings)
        
        self.input_layer = nn.Linear(self.k, 5)
        self.relu = nn.ReLU()
        self.output_layer = nn.Linear(5, self.k)
        self.bn = nn.BatchNorm1d(self.k,affine=False)

    def embded(self,x):
        s = torch.zeros((x.size()[0], self.k))

        for i in range(self.k):
            s[:,i] = self.embedings[i](x[:,i]).flatten()
        
        return s
    
    def forward(self,x):
        out = self.embded(x)
        out = self.relu(self.input_layer(out))
        out = self.output_layer(out)
        out = self.bn(out)
        return out

directory = "games"

game_id = []
deck_id = []
order = []

for filename in os.listdir(directory):
    f = os.path.join(directory, filename)
    data = pd.read_csv(f) - 1
    g,n,m = map(int,filename[:-4].split("_"))
    
    t = torch.tensor(data.to_numpy())
    dims = data.max() + 1
    model = AutoEncoder(dims)
    
    learning_rate = 0.1
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(),lr=learning_rate)

    for epoch in range(100):
        optimizer.zero_grad()
        output_train = model(t)
        loss = criterion(output_train, model.embded(t))
        loss.backward()
        optimizer.step()

    for d, embd in enumerate(model.embedings):
        game_id.append(g)
        deck_id.append(d + 1)
      order.append(" ".join(map(str, np.argsort(embd.weight.detach().numpy().flatten()) + 1)))
    
pd.DataFrame({"game" : game_id, "deck" : deck_id, "order" : order}).to_csv("submission.csv", index=False)

Критерии оценивания

Для оценки решения используется усредненный по колодам, а затем — по играм модуль коэффициента корреляции Кенделла.

Баллы, которые получатся в результате, будут вычислены по формуле: \[max\left(0; 100 \frac{S - B}{M - B}\right),\] где \(S\) — качество решения задачи командой, \(B\) — качество базового решения, а \(M\) — качество наилучшего среди всех команд решения.

text slider background image text slider background image
text slider background image text slider background image text slider background image text slider background image