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

Advent of Code — это ежегодный адвент-календарь с задачами по программированию, посвященный подготовке эльфов Санты к Рождеству. Причудливая обстановка скрывает тот факт, что многие задачи требуют серьезного алгоритмического решения, особенно ближе к концу календаря. В предыдущей статье мы обсуждали важность алгоритмического мышления для специалистов по обработке данных, даже несмотря на то, что программирование с использованием ИИ становится нормой. Поскольку Advent of Code 2025 завершился в прошлом месяце, в этой статье мы более подробно рассмотрим ряд задач с мероприятия, которые особенно актуальны для специалистов по обработке данных. Мы наметим несколько интересных подходов к решению на Python, выделив алгоритмы и библиотеки, которые можно использовать в широком спектре реальных задач в области обработки данных.
Навигация по тахионным многообразиям с помощью множеств и динамического программирования.
Первая задача, которую мы рассмотрим, — это задание 7-го дня: Лабораторные работы. В файле input_d7.txt нам дано тахионное многообразие, как показано ниже:
…….С……. …………… …….^……. …………… ……^.^…… …………… …..^.^.^….. …………… ….^…..^…. …………… …^.^…^.^… …………… ..^…^…..^.. …………… .^…^.^…..^. ……………
Тахионный луч («|») начинается в верхней части многообразия и движется вниз. Если луч попадает в разделитель («^»), он разделяется на два луча, по одному с каждой стороны от разделителя. Первая часть головоломки требует определить, сколько раз луч разделится при заданных начальных условиях (начальная точка луча и конфигурация многообразия). Обратите внимание, что простое подсчитывание количества разделителей и умножение на два не даст правильного ответа, поскольку перекрывающиеся лучи учитываются только один раз, а некоторые разделители никогда не достигаются ни одним из лучей. Мы можем использовать алгебру множеств для учета этих ограничений, как показано в приведенной ниже реализации:
import functools def find_all_indexes(s, ch): «»»Возвращает набор всех позиций, где символ ch встречается в s.»»» return {i for i, c in enumerate(s) if c == ch} with open(«input_d7.txt») as f: first_row = f.readline() # строка, содержащая начальные лучи ('S') f.readline() # пропустить разделительную линию rows = f.readlines() # оставшиеся строки многообразия beam_ids = find_all_indexes(first_row, «S») # позиции столбцов активных лучей split_counter = 0 # общее количество разветвлений for row_index, line in enumerate(rows): # Только строки с четными индексами содержат разветвители if row_index % 2 != 0: continue # Найти позиции разветвителей в этой строке splitter_ids = find_all_indexes(line, «^») # Лучи, которые попадают в разветвитель (пересечение) hits = beam_ids.intersection(splitter_ids) split_counter += len(hits) # Новые лучи, созданные разветвлениями (левое и правое) if hits: new_beams = functools.reduce(lambda acc, h: acc.union({h — 1, h + 1}), hits, set()) else: new_beams = set() # Обновление активных лучей (добавление новых лучей, удаление лучей, попадающих в разветвители) beam_ids = beam_ids.union(new_beams).difference(splitter_ids) print(split_counter)
Мы используем операцию пересечения для идентификации разделителей, на которые непосредственно попадают активные лучи, идущие сверху. Новые лучи создаются слева и справа от каждого разделителя, на который попадают лучи, но перекрывающиеся лучи учитываются только один раз с помощью оператора объединения. Множество лучей, возникающих от каждого слоя разделителей в тахионовом многообразии, вычисляется с помощью генератора списков, обернутого в функцию редукции — функцию высшего порядка, которая помогает упростить код и обычно используется в функциональном программировании. Оператор разности гарантирует, что исходные лучи, падающие на разделитель, не учитываются в множестве исходящих активных лучей.
В классической системе, если тахионная частица проходит через многообразие и сталкивается с разветвителем, она может продолжить движение только по одному единственному пути влево или вправо от разветвителя. Во второй части головоломки представлена квантовая версия этой ситуации, в которой частица одновременно движется по левому и правому путям, фактически порождая две параллельные временные линии. Наша задача — определить общее количество временных линий, существующих после того, как частица прошла все возможные пути в таком квантовом тахионном многообразии. Эту задачу можно эффективно решить с помощью динамического программирования, как показано ниже:
from functools import lru_cache def count_timelines_with_dfs_and_memo(path): «»»Подсчет различных квантовых временных линий с использованием DFS + мемоизация (нисходящий динамический код)»»» with open(path) as f: lines = [line.rstrip(«n») for line in f if line.strip()] height = len(lines) width = len(lines[0]) # Найти начальный столбец start_col = next(i for i, ch in enumerate(lines[0]) if ch == «S») @lru_cache(maxsize=None) def dfs_with_memo(row, col): «»»Возвращает количество временных линий от (row, col) до низа с использованием DFS + мемоизация»»» # Выход за пределы горизонтали if col < 0 or col >= width: return 0 # За нижней строкой: одна полная временная линия if row == height: return 1 if lines[row][col] == «^»: # Разделить слева и справа return dfs_with_memo(row+1, col-1) + dfs_with_memo(row+1, col+1) else: # Продолжить прямо вниз return dfs_with_memo(row+1, col) return dfs_with_memo(1, start_col) print(count_timelines_with_dfs_and_memo(«input_d7.txt»))
Рекурсивный поиск в глубину с мемоизацией используется для построения нисходящей формы динамического программирования, где каждая подзадача решается один раз и многократно используется повторно. Определены два базовых случая: допустимая временная шкала не создается, если частица выходит за пределы горизонтальной области, и полная временная шкала считается достигнутой, когда частица достигает дна многообразия. Рекурсивный шаг учитывает два случая: всякий раз, когда частица достигает разветвления, она разветвляется на две временные шкалы, в противном случае она продолжает движение прямо вниз по существующей временной шкале. Мемоизация (с использованием декоратора @lru_cache) предотвращает пересчет известных значений, когда несколько путей сходятся в одном и том же месте на многообразии.
На практике специалисты по анализу данных могут использовать описанные выше инструменты и методы в самых разных ситуациях. Концепция разделения лучей в некотором смысле схожа с распространением пакетов данных в сложной коммуникационной сети. Моделирование каскадного процесса чем-то похоже на моделирование сбоев в цепочке поставок, эпидемий и распространения информации. На более абстрактном уровне эту задачу можно представить как задачу обхода графа с ограничениями или задачу подсчета путей. Алгебра множеств и динамическое программирование — это универсальные концепции, которые специалисты по анализу данных могут использовать для решения таких, казалось бы, сложных алгоритмических задач.
Построение схем с использованием поиска ближайшего соседа
Следующая задача, которую мы рассмотрим, — День 8: Детская площадка. В файле input_d8.txt нам предоставлен список троек, представляющих трехмерные координаты расположения электрических распределительных коробок, как показано ниже:
162,817,810 59,618,56 901,360,560 …
В первой части нам предлагается последовательно определить и соединить пары распределительных коробок, наиболее близких друг к другу по прямой (евклидовой) дистанции. Соединенные коробки образуют цепь, по которой может течь электричество. В конечном итоге задача состоит в том, чтобы сообщить результат перемножения размеров трех самых больших цепей после соединения 1000 пар наиболее близких друг к другу распределительных коробок. Одно из удачных решений включает использование минимальной кучи для хранения пар координат распределительных коробок. Ниже приведена реализация, основанная на обучающем видео Джеймса Перальта:
from collections import defaultdict import heapq from math import dist as euclidean_dist # Загрузка точек with open(«input_d8.txt») as f: points = [tuple(map(int, line.split(«,»))) for line in f.read().split()] k = 1000 # Создание минимальной кучи всех попарных расстояний dist_heap = [ (euclidean_dist(points[i], points[j]), points[i], points[j]) for i in range(len(points)) for j in range(i + 1, len(points)) ] heapq.heapify(dist_heap) # Выбираем k кратчайших ребер и создаем список смежности neighbors = defaultdict(list) for _ in range(k): _, a, b = heapq.heappop(dist_heap) neighbors[a].append(b) neighbors[b].append(a) # Использование DFS для вычисления размера компонента def dfs(start, seen): stack = [start] seen.add(start) size = 0 while stack: node = stack.pop() size += 1 for nxt in neighbors[node]: if nxt not in seen: seen.add(nxt) stack.append(nxt) return size # Вычисление размеров всех связанных компонентов seen = set() sizes = [dfs(p, seen) for p in points if p not in seen] # Получение окончательного ответа sizes.sort(reverse=True) a, b, c = sizes[:3] print(«Решение:», a * b * c)
Минимальная куча — это бинарное дерево, в котором значения родительских узлов меньше или равны значениям их дочерних узлов; это гарантирует, что наименьшее значение хранится в вершине дерева и к нему можно эффективно получить доступ. В приведенном выше решении это полезное свойство минимальных куч используется для быстрого определения ближайших соседей среди заданных соединительных узлов. 1000 ближайших пар, таким образом, представляют собой трехмерный граф. Для обхода графа, начиная с заданного соединительного узла, используется поиск в глубину, и подсчитывается количество узлов, которые находятся в одном и том же связном компоненте графа (т.е. цепи).
Во второй части рассматривается проблема нехватки ресурсов (недостаточно удлинительных кабелей). Теперь нам необходимо продолжать соединять ближайшие неподключенные пары распределительных коробок, пока они не станут частью одной большой цепи. Требуемый ответ — результат перемножения x-координат последних двух подключенных распределительных коробок. Для решения этой задачи мы можем использовать структуру данных «объединение-поиск» и алгоритм Крускала для построения минимальных остовных деревьев следующим образом:
import heapq from math import dist as euclidean_dist # Загрузка точек with open(«input_d8.txt») as f: points = [tuple(map(int, line.split(«,»))) for line in f.read().split()] # Построение минимальной кучи всех попарных расстояний dist_heap = [ (euclidean_dist(a, b), a, b) for i, a in enumerate(points) for b in points[i+1:] ] heapq.heapify(dist_heap) # Определение функций для реализации Union-Find parent = {p: p for p in points} def find(x): if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(a, b): ra, rb = find(a), find(b) if ra == rb: return False parent[rb] = ra return True # Использование Алгоритм Крускала для соединения точек до тех пор, пока все они не окажутся в одной компоненте edges_used = 0 last_pair = None while dist_heap: _, a, b = heapq.heappop(dist_heap) if union(a, b): edges_used += 1 last_pair = (a, b) if edges_used == len(points) — 1: break # Получение окончательного ответа x_product = last_pair[0][0] * last_pair[1][0] print(x_product)
Данные о местоположении хранятся в минимальной куче, и строятся связные компоненты графа. Мы многократно выбираем кратчайшее оставшееся ребро между двумя точками и сохраняем это ребро только в том случае, если оно соединяет две ранее не связанные компоненты; это основная идея алгоритма Крускала. Но для эффективного выполнения этой задачи нам нужен способ быстрого определения того, соединены ли две точки уже. Если да, то union(a, b) == False, и мы пропускаем ребро, чтобы избежать создания цикла. В противном случае мы объединяем их компоненты графа. Union-find — это структура данных, которая может выполнить эту проверку почти за постоянное время. Используя корпоративную аналогию, это немного похоже на многократный вопрос «Кто ваш начальник?», пока вы не дойдете до генерального директора, а затем перепишете значение «начальник каждого» на имя генерального директора (т.е. корень). В следующий раз, когда кто-то спросит: «Кто ваш начальник?», вы сможете быстро ответить именем генерального директора. Если корни двух узлов одинаковы, соответствующие компоненты объединяются путем присоединения одного корня к другому.
Задача построения цепей связана с кластеризацией и обнаружением сообществ, что является важными понятиями для реального применения в науке о данных. Например, построение компонентов графа путем определения ближайших соседей может быть частью практического алгоритма для группировки клиентов по сходству предпочтений, обнаружения сообществ в социальных сетях и кластеризации географических местоположений. Алгоритм Крускала можно использовать для проектирования и оптимизации сетей путем минимизации затрат на маршрутизацию. Абстрактные понятия, такие как евклидовы расстояния, минимальные кучи и объединение-поиск, помогают нам измерять, расставлять приоритеты и организовывать данные в больших масштабах.
Конфигурирование заводского оборудования с помощью линейного программирования
Далее мы рассмотрим задачу, поставленную в День 10: Игровая площадка. Нам предоставлено руководство по настройке заводских станков в файле input_d10.txt, как показано ниже:
[.##.] (2) (0,3) (2) (2,3) (0,2) (0,1) {3,5,4,7} [..##.] (0,2,3) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,8,2} [.###.#] (0,1,2,3) (0,3,4) (0,1,2,4,5) (1,2) {10,11,9,5,10,5}
Каждая строка описывает одну машину. Количество символов в квадратных скобках отражает количество индикаторных лампочек и их желаемые состояния («.» означает выключено, а «#» — включено). Все лампочки изначально будут выключены. Схемы подключения кнопок показаны в скобках; например, нажатие кнопки со схемой «(2, 3)» изменит текущее состояние индикаторных лампочек в позициях 2 и 3 с «.» на «#» или наоборот. Цель первой части — определить минимальное количество нажатий кнопок, необходимое для правильной настройки индикаторных лампочек на всех заданных машинах. Элегантное решение с использованием смешанного целочисленного линейного программирования (MILP) показано ниже:
import re import numpy as np from scipy.optimize import milp, LinearConstraint, Bounds # Разбор одной строки описания машины def parse_machine(line: str): # Извлечение светового шаблона match = re.search(r»[([.#]+)]», line) if not match: raise ValueError(f»Недопустимая строка: {line}») pattern = match.group(1) m = len(pattern) # Целевой вектор: '#' -> 1, '.' -> 0 target = np.fromiter((ch == «#» for ch in pattern), dtype=int) # Извлечение проводки кнопок buttons = [ [int(x) for x in grp.split(«,»)] if grp.strip() else [] for grp in re.findall(r»(([^)]*))», line) ] # Создание матрицы переключения A n = len(buttons) A = np.zeros((m, n), dtype=int) for j, btn in enumerate(buttons): for idx in btn: if not (0 <= idx < m): raise ValueError(f"Индекс кнопки {idx} выходит за пределы диапазона для {m} ламп") A[idx, j] = 1 return A, target # Решение всех задач в файле def solve_d10_part1(filename): with open(filename) as f: lines = [line.strip() for line in f if line.strip()] total = 0 for line in lines: A, target = parse_machine(line) m, n = A.shape # Цель: минимизировать сумму(x) c = np.r_[np.ones(n), np.zeros(m)] # Задаем ограничение A_eq = np.hstack([A, -2 * np.eye(m)]) lc = LinearConstraint(A_eq, target, target) # Определяем границы lb = np.zeros(n + m) ub = np.r_[np.ones(n), np.full(m, np.inf)] bounds = Bounds(lb, ub) # Задаем целочисленность integrality = np.r_[np.full(n, 2), np.full(m, 1)] res = milp(c=c, constraints=[lc], integrality=integrality, bounds=bounds) if not res.success: raise RuntimeError(f"No feasible solution for line: {line}") total += round(res.x[:n].sum()) return total print(solve_d10_part1("input_d10.txt"))
Сначала каждая машина кодируется в виде матрицы A, в которой строки представляют собой индикаторы, а столбцы — кнопки. A[i, j] = 1, если кнопка j переключает индикатор i. Для сопоставления с шаблонами входных данных используются регулярные выражения. Далее мы ставим задачу оптимизации с бинарным вектором нажатий кнопок x, целочисленными переменными запаса k и целевым шаблоном индикаторов t. Для каждой машины наша цель — выбрать нажатия кнопок x таким образом, чтобы xj = 1, если нажата j-я кнопка, и 0 в противном случае. Условие «после нажатия кнопок x индикаторы равны целевому шаблону t» отражает конгруэнцию Ax ≡ t (mod 2), но поскольку решатель MILP не может напрямую работать с mod 2, мы выражаем это условие как Ax – 2k = t для некоторого вектора k, состоящего только из неотрицательных целых чисел; эта переформулировка работает, потому что вычитание четного числа не меняет четность. Спецификация целочисленности гласит, что первые n переменных (нажатия кнопок) являются бинарными, а оставшиеся m переменных (свободное значение) — неотрицательными целыми числами. Затем мы запускаем решатель MILP с целью минимизации количества нажатий кнопок, необходимых для достижения целевого состояния. Если решатель успешно справляется с задачей, res.x[:n] содержит оптимальные варианты нажатия кнопок, и код добавляет количество нажатых кнопок к текущей сумме.
Во второй части задача состоит в достижении целевого состояния, описываемого так называемыми требованиями к «энергии толчка», которые показаны в фигурных скобках для каждой машины. Счетчики энергии толчка машины изначально установлены на 0, и кнопки можно нажимать любое количество раз для обновления уровней энергии толчка. Например, первая машина начинает работу со значениями энергии толчка «{0, 0, 0, 0}». Нажатие кнопки «(3)» один раз, «(1, 3)» три раза, «(2,3)» три раза, «(0,2)» один раз и (0,1) два раза приводит к целевому состоянию «{3, 5, 4, 7}». Это также является наименьшим количеством нажатий кнопок, необходимых для достижения целевого состояния. Наша задача состоит в вычислении минимального количества нажатий кнопок, необходимых для достижения целевых состояний энергии толчка для всех машин. Опять же, это можно решить с помощью MILP следующим образом:
import re import numpy as np from scipy.optimize import milp, LinearConstraint, Bounds def parse_machine(line: str): # Извлечение требований к джойстику match = re.search(r»{([^}]*)}», line) if not match: raise ValueError(f»Нет требований к джойстику в line: {line}») target = np.fromiter((int(x) for x in match.group(1).split(«,»)), dtype=int) m = len(target) # Извлечение проводки кнопок buttons = [ [int(x) for x in grp.split(«,»)] if grp.strip() else [] for grp in re.findall(r»(([^)]*))», line) ] # Создание A (m × n) n = len(buttons) A = np.zeros((m, n), dtype=int) for j, btn in enumerate(buttons): for idx in btn: if not (0 <= idx < m): raise ValueError(f"Индекс кнопки {idx} выходит за пределы диапазона для {m} счетчиков") A[idx, j] += 1 return A, target def solve_machine(A, target): m, n = A.shape # Минимизировать сумму(x) c = np.ones(n) # Ограничение: A x = target lc = LinearConstraint(A, target, target) # Границы: x ≥ 0 bounds = Bounds(np.zeros(n), np.full(n, np.inf)) # Все x — целые числа integrality = np.ones(n, dtype=int) res = milp(c=c, constraints=[lc], integrality=integrality, bounds=bounds) if not res.success: raise RuntimeError("Нет допустимого решения" решение") return int(round(res.fun)) def solve_d10_part2(filename): with open(filename) as f: lines = [line.strip() for line in f if line.strip()] return sum(solve_machine(*parse_machine(line)) for line in lines) print(solve_d10_part2("input_d10.txt"))
Если первая часть была задачей на четность, то вторая — задачей на подсчет. Основное ограничение второй части можно выразить линейным уравнением Ax = t, и никаких дополнительных переменных не требуется. В некотором смысле вторая часть напоминает задачу о рюкзаке с целыми числами, где рюкзак должен быть наполнен правильной комбинацией предметов разного веса/размера.
Подобные задачи оптимизации часто встречаются в задачах анализа данных в таких областях, как логистика, управление цепочками поставок и управление финансовым портфелем. Основная цель — минимизировать или максимизировать некоторую целевую функцию с учетом различных ограничений. Специалистам по анализу данных также полезно освоить модульную арифметику; см. эту статью для концептуального обзора модульной арифметики и изучения ее практического применения в анализе данных. Наконец, существует интересная концептуальная связь между MILP и понятием выбора признаков с регуляризацией в машинном обучении. Выбор признаков заключается в выборе наименьшего количества признаков для обучения модели без негативного влияния на точность прогнозирования. Использование MILP похоже на выполнение явного комбинаторного поиска по подмножествам признаков с отсечением и оптимизацией. L1-регуляризация представляет собой непрерывную релаксацию MILP; штраф L1 смещает коэффициенты неважных признаков к нулю. L2-регуляризация еще больше ослабляет ограничения MILP, уменьшая коэффициенты неважных признаков, не устанавливая их точно равными нулю.
Поиск и устранение неисправностей реактора с помощью сетевого анализа.
Последняя задача, которую мы рассмотрим, — это задача 11-го дня: «Реактор». В файле input_d11.txt нам предоставлено словарное представление сети узлов и ребер, как показано ниже:
ты: ххх ккк ххх: ккк ффф iii … iii: наружу
Ключами и значениями являются исходные и целевые узлы (или устройства, в зависимости от сюжета задачи) соответственно. В приведенном выше примере узел «вы» соединен с узлами «hhh» и «ccc». Задача в первой части — подсчитать количество различных путей через сеть, ведущих от узла «вы» к «из». Это можно сделать с помощью поиска в глубину следующим образом:
from collections import defaultdict def parse_input(filename): «»» Преобразует входной файл в ориентированный граф. Каждая строка имеет формат: источник: dest1 dest2 … «»» graph = defaultdict(list) with open(filename) as f: for line in f: line = line.strip() if not line: continue src, dests = line.split(«:») src = src.strip() for d in dests.strip().split(): graph[src].append(d.strip()) return graph def dfs_paths(graph, start, goal): «»» Генерирует все пути от начала до цели, используя DFS. «»» stack = [(start, [start])] while stack: (node, path) = stack.pop() for next_node in graph.get(node, []): if next_node in path: # Избегать циклов continue if next_node == goal: yield path + [next_node] else: stack.append((next_node, path + [next_node])) def solve_d11_part1(filename): graph = parse_input(filename) all_paths = list(dfs_paths(graph, «you», «out»)) print(len(all_paths)) solve_d11_part1(«input_d11.txt»)
Для реализации поиска мы используем явный стек. Каждая запись в стеке содержит информацию о текущем узле и пройденном пути. Для каждого соседа мы пропускаем его, если он уже находится на пути, возвращаем завершенный путь, если сосед является «исходящим» узлом, или помещаем соседа и обновленный путь в стек, чтобы продолжить исследование оставшейся части сети. Таким образом, процесс поиска перечисляет все допустимые пути от «вашего» узла до «исходящего», и окончательный результат работы кода — это количество уникальных допустимых путей.
Во второй части нам нужно подсчитать количество путей, идущих от «svr» к «out» через узлы «dac» и «fft». Ограничение на количество промежуточных узлов фактически ограничивает количество допустимых путей в сети. Ниже приведен пример решения:
from collections import defaultdict from functools import lru_cache def parse_input(filename): graph = defaultdict(list) with open(filename) as f: for line in f: line = line.strip() if not line: continue src, dests = line.split(«:») src = src.strip() dests = [d.strip() for d in dests.strip().split()] graph[src].extend(dests) for d in dests: if d not in graph: graph[d] = [] return graph def count_paths_with_constraints(graph, start, goal, must_visit): must_visit = frozenset(must_visit) @lru_cache(maxsize=None) def dfs(node, seen_required): seen_required = frozenset(seen_required) if node == goal: return 1 if seen_required == must_visit else 0 total = 0 for nxt in graph[node]: # Избегаем циклов, не посещая повторно узлы, уже находящиеся в seen_required+path # Вместо отслеживания полного пути предполагаем DAG или небольшие циклы new_seen = seen_required | (frozenset([nxt]) & must_visit) total += dfs(nxt, new_seen) return total return dfs(start, frozenset([start]) & must_visit) def solve_d11_part2(filename): graph = parse_input(filename) must_visit = {«dac», «fft»} total_valid_paths = count_paths_with_constraints(graph, «svr», «out», must_visit) print(total_valid_paths) solve_d11_part2(«input_d11.txt»)
Код основан на логике первой части, поэтому теперь мы дополнительно отслеживаем посещения промежуточных узлов «dac» и «fft» в рамках алгоритма поиска в глубину. Как и в задаче о квантовом тахионном многообразии, мы используем мемоизацию для предотвращения избыточных вычислений.
Задачи, связанные с сетевым анализом, являются неотъемлемой частью науки о данных. Перечисление путей напрямую связано с задачами, касающимися телекоммуникаций, маршрутизации в интернете и оптимизации энергосетей. Сложные конвейеры ETL часто представляются в виде сетей (например, ориентированных ациклических графов), и алгоритмы подсчета путей могут использоваться для выявления критических зависимостей или узких мест в рабочем процессе. В контексте рекомендательных систем, работающих на основе графов знаний, анализ путей, проходящих через граф, может помочь в интерпретации ответов рекомендателей. Такие рекомендательные системы могут использовать пути между сущностями для обоснования рекомендаций, делая систему прозрачной, показывая, как предлагаемый элемент связан с известными предпочтениями пользователя — в конце концов, мы можем явно проследить логику.
The Pack
В этой статье мы рассмотрели, как игривые сценарии, составляющие основу головоломок Advent of Code, могут выявлять действительно мощные идеи, начиная от поиска и оптимизации графов и заканчивая линейным программированием, комбинаторикой и решением задач с ограничениями. Анализируя эти проблемы и экспериментируя с различными стратегиями решения, специалисты по обработке данных могут отточить свои алгоритмические навыки и создать универсальный инструментарий, который напрямую применим на практике, охватывая проектирование признаков, интерпретируемость моделей, оптимизационные конвейеры и многое другое. По мере развития программирования с использованием ИИ, способность формулировать, решать и критически анализировать такие проблемы, вероятно, останется ключевым отличием для специалистов по обработке данных. Advent of Code предлагает увлекательный и несложный способ поддерживать эти навыки в тонусе – читателям предлагается попробовать решить другие головоломки из издания 2025 года и испытать радость от решения сложных задач с помощью алгоритмического мышления.
Источник: towardsdatascience.com



























