egekit-study 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
egekit.py ADDED
@@ -0,0 +1,1166 @@
1
+ """EGEKit: алгоритмические инструменты для подготовки к ЕГЭ по информатике.
2
+ Все пользовательские функции перечислены в GUIDE.html и GUIDE.md.
3
+ Python 3.10+, только стандартная библиотека. Условия задач задаёт пользователь.
4
+ """
5
+ from __future__ import annotations
6
+ from collections import Counter, defaultdict, deque
7
+ from functools import lru_cache
8
+ from itertools import product, permutations, combinations
9
+ from math import ceil, floor, isqrt, hypot, inf
10
+ from pathlib import Path
11
+ import csv
12
+ import fnmatch
13
+ import heapq
14
+ import ipaddress
15
+ import re
16
+ import sqlite3
17
+ import zipfile
18
+ import xml.etree.ElementTree as ET
19
+
20
+ __version__ = '1.0.0'
21
+ CATALOG = {}
22
+
23
+ def _task(*numbers):
24
+ def register(func):
25
+ CATALOG[func.__name__] = tuple(numbers)
26
+ return func
27
+ return register
28
+
29
+ @_task(3, 9, 17, 22, 24, 26, 27)
30
+ def read_text(path, encoding='utf-8-sig'):
31
+ """Прочитать текст, сохраняя переводы строк. Для Windows-файла задайте encoding='cp1251'. Пример: read_text('24.txt')."""
32
+ return Path(path).read_text(encoding=encoding)
33
+
34
+ @_task(9, 17, 22, 26, 27)
35
+ def read_rows(path, cast=int, skip=0, delimiter=None, encoding='utf-8-sig'):
36
+ """Прочитать строки таблицы в список списков. skip — число строк заголовка; delimiter=None — любые пробелы. Пустые строки пропускаются. Пример: read_rows('26.txt', skip=1)."""
37
+ return [[cast(x) for x in line.split(delimiter)] for line in read_text(path, encoding).splitlines()[skip:] if line.strip()]
38
+
39
+ @_task(17, 26, 27)
40
+ def read_numbers(path, skip=0, encoding='utf-8-sig'):
41
+ """Прочитать все целые числа после skip строк заголовка, независимо от числа столбцов. Пример: read_numbers('17.txt')."""
42
+ return [int(x) for line in read_text(path, encoding).splitlines()[skip:] for x in line.split()]
43
+
44
+ @_task(3, 9)
45
+ def read_csv(path, delimiter=';', encoding='utf-8-sig'):
46
+ """Прочитать CSV как список словарей; первая строка — названия полей. Значения остаются строками. Пример: read_csv('table.csv'). XLSX нужно предварительно экспортировать в CSV."""
47
+ with open(path, newline='', encoding=encoding) as f:
48
+ return list(csv.DictReader(f, delimiter=delimiter))
49
+
50
+ @_task(3)
51
+ def join_tables(left, right, left_key, right_key):
52
+ """Внутреннее соединение списков словарей по ключам; результат — пары (левая_строка, правая_строка). Дубликаты дают все сочетания. Пример: join_tables([{'id':1}], [{'ref':1}], 'id', 'ref')."""
53
+ index = defaultdict(list)
54
+ for row in right:
55
+ index[row[right_key]].append(row)
56
+ return [(a, b) for a in left for b in index[a[left_key]]]
57
+
58
+ @_task(3)
59
+ def group_aggregate(rows, key, value, aggregate=sum):
60
+ """Сгруппировать словари по полю key, применить aggregate к значениям поля value. Строковые числа сначала преобразуйте. Пример: group_aggregate([{'city':'A','n':3}], 'city','n')."""
61
+ groups = defaultdict(list)
62
+ for row in rows:
63
+ groups[row[key]].append(row[value])
64
+ return {k: aggregate(v) for k, v in groups.items()}
65
+
66
+ @_task(3)
67
+ def sqlite_select(path, query, parameters=()):
68
+ """Выполнить запрос к существующей SQLite-базе в режиме только чтения; вернуть кортежи строк. Пример: sqlite_select('data.db', 'SELECT * FROM items WHERE price > ?', (100,)). Не предназначено для XLSX."""
69
+ uri = Path(path).resolve().as_uri() + '?mode=ro'
70
+ with sqlite3.connect(uri, uri=True) as con:
71
+ con.execute('PRAGMA query_only=ON')
72
+ return con.execute(query, parameters).fetchall()
73
+
74
+ @_task(2, 15)
75
+ def implies(a, b):
76
+ """Логическое следование a → b. Пример: implies(True, False) вернёт False. Для эквивалентности используйте a == b."""
77
+ return not a or bool(b)
78
+
79
+ @_task(2)
80
+ def truth_table(variables, predicate):
81
+ """Полная таблица истинности. variables — строка/список имён; predicate получает именованные аргументы. Результат: словари с полем F. Пример: truth_table('xy', lambda x,y: x and not y)."""
82
+ names = list(variables)
83
+ if len(set(names)) != len(names) or 'F' in names:
84
+ raise ValueError('Нужны уникальные имена переменных, отличные от F')
85
+ return [dict(zip(names, bits), F=int(bool(predicate(**dict(zip(names, bits)))))) for bits in product((0, 1), repeat=len(names))]
86
+
87
+ @_task(2)
88
+ def match_truth_columns(variables, rows, predicate):
89
+ """Найти перестановки переменных по неполной таблице. Каждая строка: n столбцов + F; None — неизвестно. Требует разных полных строк, учитывает неоднозначность. Пример: match_truth_columns('xy', [(0,1,0)], lambda x,y:x and y)."""
90
+ n = len(variables)
91
+ if any(len(r) != n + 1 or any(v not in (0,1,None) for v in r) for r in rows):
92
+ raise ValueError('Строка должна содержать n+1 значений 0, 1 или None')
93
+ answers = []
94
+ for names in permutations(variables):
95
+ candidates = []
96
+ for row in rows:
97
+ fits = []
98
+ for bits in product((0,1), repeat=n):
99
+ f = int(bool(predicate(**dict(zip(names,bits)))))
100
+ if all(a is None or a == b for a,b in zip(row, (*bits,f))):
101
+ fits.append(bits)
102
+ candidates.append(fits)
103
+ def assign(i, used):
104
+ if i == len(candidates): return True
105
+ return any(assign(i+1, used | {b}) for b in candidates[i] if b not in used)
106
+ if assign(0, set()): answers.append(names)
107
+ return answers
108
+
109
+ @_task(1)
110
+ def match_graphs(graph, numbered, compare_weights=False):
111
+ """Все соответствия вершин двух графов (списки словарей смежности с весами). Учитывает направление; по умолчанию сопоставляет только наличие рёбер (для №1), compare_weights=True дополнительно требует одинаковых весов. Все вершины, включая изолированные, должны быть ключами обоих словарей. Полный перебор n!, подходит небольшим графам. Пример: match_graphs({'A':{'B':1},'B':{'A':1}}, {1:{2:1},2:{1:1}})."""
112
+ if len(graph) != len(numbered): return []
113
+ vertices = list(graph)
114
+ answers = []
115
+ for order in permutations(numbered):
116
+ mapping = dict(zip(vertices, order))
117
+ if all((graph[a].get(b) == numbered[mapping[a]].get(mapping[b]) if compare_weights else (b in graph[a]) == (mapping[b] in numbered[mapping[a]])) for a in vertices for b in vertices):
118
+ answers.append(mapping)
119
+ return answers
120
+
121
+ @_task(1, 13)
122
+ def shortest_paths(graph, start):
123
+ """Расстояния от start алгоритмом Дейкстры. graph[u] — словарь сосед:неотрицательный_вес; недостижимые вершины имеют inf. Пример: shortest_paths({'A':{'B':3},'B':{}},'A')."""
124
+ nodes = set(graph) | {v for edges in graph.values() for v in edges} | {start}
125
+ if any(w < 0 for edges in graph.values() for w in edges.values()): raise ValueError('Отрицательный вес')
126
+ dist = dict.fromkeys(nodes, inf); dist[start] = 0
127
+ serial = 0; heap = [(0,serial,start)]
128
+ while heap:
129
+ d,_,u = heapq.heappop(heap)
130
+ if d != dist[u]: continue
131
+ for v,w in graph.get(u,{}).items():
132
+ if d+w < dist[v]:
133
+ dist[v] = d+w; serial += 1
134
+ heapq.heappush(heap,(d+w,serial,v))
135
+ return dist
136
+
137
+ @_task(1, 13, 23)
138
+ def count_graph_paths(graph, start, finish, forbidden=()):
139
+ """Число путей в ориентированном ациклическом графе; graph[u] — соседи. forbidden — запрещённые вершины. Достижимый цикл вызывает ValueError. Пример: count_graph_paths({'A':['B','C'],'B':['C']},'A','C')."""
140
+ forbidden = set(forbidden); active = set()
141
+ @lru_cache(None)
142
+ def walk(u):
143
+ if u in forbidden: return 0
144
+ if u == finish: return 1
145
+ if u in active: raise ValueError('Нужен ациклический граф')
146
+ active.add(u)
147
+ result = sum(walk(v) for v in graph.get(u,()))
148
+ active.remove(u)
149
+ return result
150
+ return walk(start)
151
+
152
+ @_task(4, 7, 11)
153
+ def bits_per_symbol(alphabet_size):
154
+ """Минимум бит на символ равномерного кода: ceil(log2(N)), точно для больших целых. N=1 даёт 0. Пример: bits_per_symbol(33) == 6."""
155
+ if alphabet_size < 1: raise ValueError('Алфавит должен быть непустым')
156
+ return (alphabet_size-1).bit_length()
157
+
158
+ @_task(7, 11)
159
+ def storage_bytes(symbols, alphabet_size, records=1, round_each=True):
160
+ """Объём равномерного кодирования в байтах. round_each=True округляет каждый объект отдельно, False — весь поток. Пример: storage_bytes(10,33,100) == 800. Служебные поля прибавляйте отдельно."""
161
+ bits = symbols * bits_per_symbol(alphabet_size)
162
+ return ((bits+7)//8)*records if round_each else (bits*records+7)//8
163
+
164
+ @_task(7)
165
+ def image_bits(width, height, colors):
166
+ """Объём несжатого растра в битах, без заголовка и выравнивания строк. Пример: image_bits(100,100,256) == 80000."""
167
+ return width * height * bits_per_symbol(colors)
168
+
169
+ @_task(7)
170
+ def audio_bits(seconds, sample_rate, depth, channels=1):
171
+ """Объём несжатого звука: секунды × частота Гц × глубина бит × каналы. Возвращает биты. Пример: audio_bits(10,48000,16,2)."""
172
+ return seconds * sample_rate * depth * channels
173
+
174
+ @_task(7)
175
+ def transmission_seconds(bits, rate, compression=1.0, overhead_bits=0):
176
+ """Время передачи при rate бит/с. compression — доля оставшихся данных (0.6 = 60%), служебные биты прибавляются после сжатия. Пример: transmission_seconds(8000,1000) == 8."""
177
+ if rate <= 0 or compression < 0 or overhead_bits < 0: raise ValueError('Некорректные параметры')
178
+ return (bits*compression + overhead_bits)/rate
179
+
180
+ @_task(4)
181
+ def is_prefix_free(codes):
182
+ """Проверить условие Фано для набора непустых двоичных слов. Дубликаты недопустимы. Пример: is_prefix_free(['0','10','11']) == True."""
183
+ codes = sorted(codes)
184
+ return all(c and set(c) <= {'0','1'} for c in codes) and all(not b.startswith(a) for a,b in zip(codes,codes[1:]))
185
+
186
+ @_task(4)
187
+ def available_codewords(codes, length):
188
+ """Все двоичные слова заданной length, совместимые с условием Фано для уже известных codes. Экспоненциальный перебор 2**length. Пример: available_codewords(['0'], 2) == ['10','11']."""
189
+ if length < 1 or not is_prefix_free(codes): raise ValueError('Нужны корректные коды и положительная длина')
190
+ return [''.join(bits) for bits in product('01', repeat=length) if is_prefix_free([*codes,''.join(bits)])]
191
+
192
+ @_task(4)
193
+ def encoded_length(text, codebook):
194
+ """Длина сообщения в битах по словарю символ:код. Пример: encoded_length('ABA', {'A':'0','B':'11'}) == 4."""
195
+ return sum(len(codebook[ch]) for ch in text)
196
+
197
+ @_task(4)
198
+ def huffman_codes(frequencies):
199
+ """Построить оптимальный префиксный код по словарю символ:положительная_частота. Для единственного символа код '0'. Не учитывает дополнительно закреплённые кодовые слова. Пример: huffman_codes({'A':5,'B':2,'C':1})."""
200
+ heap = []; serial = 0
201
+ for symbol,freq in frequencies.items():
202
+ if freq <= 0: raise ValueError('Частоты должны быть положительными')
203
+ heapq.heappush(heap,(freq,serial,{symbol:''})); serial += 1
204
+ if not heap: return {}
205
+ if len(heap) == 1: return {next(iter(heap[0][2])):'0'}
206
+ while len(heap)>1:
207
+ fa,_,a=heapq.heappop(heap); fb,_,b=heapq.heappop(heap)
208
+ merged={**{k:'0'+v for k,v in a.items()}, **{k:'1'+v for k,v in b.items()}}
209
+ heapq.heappush(heap,(fa+fb,serial,merged)); serial+=1
210
+ return heap[0][2]
211
+
212
+ @_task(5, 14)
213
+ def to_base(n, base):
214
+ """Перевести неотрицательное целое в строку основания 2–36. Пример: to_base(31,16) == '1F'."""
215
+ if n < 0 or not 2 <= base <= 36: raise ValueError('Нужны n>=0 и основание 2–36')
216
+ alphabet='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; result=''
217
+ while n:
218
+ n,r=divmod(n,base); result=alphabet[r]+result
219
+ return result or '0'
220
+
221
+ @_task(5, 14)
222
+ def from_base(text, base):
223
+ """Значение записи в основании 2–36; неизвестные цифры сначала замените. Пример: from_base('101',2) == 5."""
224
+ return int(text,base)
225
+
226
+ @_task(14)
227
+ def base_digits(n, base):
228
+ """Цифры неотрицательного числа списком, от старшей к младшей; допускает основания больше 36. Пример: base_digits(10,3) == [1,0,1]."""
229
+ if n < 0 or base < 2: raise ValueError('Нужны n>=0 и основание>=2')
230
+ digits=[]
231
+ while n: n,r=divmod(n,base); digits.append(r)
232
+ return digits[::-1] or [0]
233
+
234
+ @_task(5)
235
+ def binary_transform(n, transform):
236
+ """Передать двоичную запись n без '0b' в transform и прочитать полученную запись как двоичное число. Пример: binary_transform(5, lambda s:s+'0') == 10. Правило дописывания задаётся по условию."""
237
+ return int(transform(to_base(n,2)),2)
238
+
239
+ @_task(5, 14, 15, 25)
240
+ def search_values(values, predicate):
241
+ """Вернуть все элементы конечного values, удовлетворяющие predicate. Пример: search_values(range(100), lambda n:n%7==0). Диапазон нужно обосновать условием."""
242
+ return [v for v in values if predicate(v)]
243
+
244
+ @_task(8)
245
+ def words(alphabet, length):
246
+ """Ленивый перебор слов с повторениями в порядке заданного alphabet. Пример: list(words('АБ',2)) == ['АА','АБ','БА','ББ']."""
247
+ return (''.join(w) for w in product(alphabet, repeat=length))
248
+
249
+ @_task(8)
250
+ def count_words(alphabet, length, predicate=lambda w:True):
251
+ """Количество слов с повторениями, подходящих под predicate. O(|алфавит|**length). Пример: count_words('01',4,lambda w:w.count('1')==2) == 6."""
252
+ return sum(bool(predicate(w)) for w in words(alphabet,length))
253
+
254
+ @_task(8)
255
+ def word_rank(word, alphabet):
256
+ """Номер слова с 1 в полном списке слов той же длины с повторениями. Порядок задаёт alphabet; символы алфавита уникальны. Пример: word_rank('БА','АБ') == 3."""
257
+ if len(set(alphabet))!=len(alphabet) or not alphabet: raise ValueError('Нужен непустой алфавит без повторений')
258
+ rank=0
259
+ for ch in word: rank=rank*len(alphabet)+alphabet.index(ch)
260
+ return rank+1
261
+
262
+ @_task(8)
263
+ def word_at(rank, alphabet, length):
264
+ """Слово по номеру rank с 1; обратная к word_rank. Пример: word_at(3,'АБ',2) == 'БА'."""
265
+ if not alphabet or len(set(alphabet))!=len(alphabet) or length<0 or not 1<=rank<=len(alphabet)**length: raise ValueError('Некорректный номер/алфавит/длина')
266
+ rank-=1; result=[]
267
+ for _ in range(length): rank,r=divmod(rank,len(alphabet)); result.append(alphabet[r])
268
+ return ''.join(result[::-1])
269
+
270
+ @_task(8)
271
+ def unique_permutations(text):
272
+ """Ленивый перебор различных перестановок символов, включая повторяющиеся, без хранения всех перестановок. Пример: list(unique_permutations('ААБ'))."""
273
+ counts=Counter(text); chars=list(counts); prefix=[]
274
+ def generate():
275
+ if len(prefix)==len(text): yield ''.join(prefix); return
276
+ for ch in chars:
277
+ if counts[ch]:
278
+ counts[ch]-=1; prefix.append(ch)
279
+ yield from generate()
280
+ prefix.pop(); counts[ch]+=1
281
+ return generate()
282
+
283
+ @_task(9)
284
+ def row_profile(row):
285
+ """Сводка строки: сумма, min/max (None для пустой), частоты, повторённые и одиночные значения с учётом кратности. Пример: row_profile([1,2,2])['repeated'] == [2,2]."""
286
+ c=Counter(row)
287
+ return dict(total=sum(row), minimum=min(row,default=None), maximum=max(row,default=None), counts=dict(c), repeated=[x for x in row if c[x]>1], unique=[x for x in row if c[x]==1])
288
+
289
+ @_task(9)
290
+ def filter_rows(rows, predicate, start=1):
291
+ """Список (номер_строки, строка) по условию predicate(row). start задаёт первый номер. Пример: filter_rows([[1,2],[3,3]], lambda r:len(set(r))==1)."""
292
+ return [(i,row) for i,row in enumerate(rows,start) if predicate(row)]
293
+
294
+ @_task(10)
295
+ def docx_text(path):
296
+ """Извлечь текст основных абзацев и таблиц DOCX средствами stdlib, без форматирования; колонтитулы/сноски не включены. Пример: docx_text('book.docx'). Для старого DOC сначала сохраните DOCX."""
297
+ ns={'w':'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}
298
+ with zipfile.ZipFile(path) as z: root=ET.fromstring(z.read('word/document.xml'))
299
+ return '\n'.join(''.join(t.text or '' for t in p.findall('.//w:t',ns)) for p in root.findall('.//w:p',ns))
300
+
301
+ @_task(10, 24)
302
+ def count_occurrences(text, pattern, whole_word=False, ignore_case=False, overlap=False):
303
+ """Количество вхождений буквальной строки; whole_word использует границы Unicode-слова, ignore_case игнорирует регистр, overlap разрешает пересечения. Пример: count_occurrences('ААА','АА',overlap=True) == 2. Дефисы и апострофы проверяйте по условию."""
304
+ if not pattern: raise ValueError('Пустой образец')
305
+ p=re.escape(pattern)
306
+ if whole_word: p=r'(?<!\w)'+p+r'(?!\w)'
307
+ if overlap: p='(?='+p+')'
308
+ return sum(1 for _ in re.finditer(p,text,re.I if ignore_case else 0))
309
+
310
+ @_task(12)
311
+ def rewrite(text, rules, condition=None, first_only=True, max_steps=100000):
312
+ """Исполнитель Редактор: rules — упорядоченные пары (что,на_что). first_only=True — за итерацию первое применимое правило (ЕСЛИ/ИНАЧЕ); False — все правила по порядку (несколько ЕСЛИ). Каждая замена меняет первое вхождение. condition(text) проверяет ПОКА. Пример: rewrite('1111',[('11','2')]) == '22'. Лимит вызывает RuntimeError."""
313
+ if any(not old for old,new in rules): raise ValueError('Пустая левая часть правила')
314
+ def running(s): return condition(s) if condition is not None else any(a in s for a,b in rules)
315
+ steps=0
316
+ while running(text):
317
+ if steps>=max_steps: raise RuntimeError('Превышен лимит итераций Редактора')
318
+ changed=False
319
+ for old,new in rules:
320
+ if old in text:
321
+ text=text.replace(old,new,1); changed=True
322
+ if first_only: break
323
+ steps+=1
324
+ if not changed: raise RuntimeError('Условие ПОКА истинно, но ни одно правило не применимо')
325
+ return text
326
+
327
+ @_task(12)
328
+ def turing(tape, transitions, state='q0', head=0, blank='_', halt=('halt',), max_steps=1000000):
329
+ """Симулятор машины Тьюринга. tape — строка или {индекс:символ}; transitions[(состояние,символ)]=(новый_символ,сдвиг,новое_состояние), сдвиг -1/0/1. Возвращает tape/head/state/steps. Пример: turing('1',{('q0','1'):('0',1,'halt')}). Отсутствие перехода — ошибка, не неявная остановка."""
330
+ cells=dict(enumerate(tape)) if isinstance(tape,str) else dict(tape)
331
+ steps=0
332
+ while state not in halt:
333
+ if steps>=max_steps: raise RuntimeError('Превышен лимит шагов МТ')
334
+ key=(state,cells.get(head,blank))
335
+ if key not in transitions: raise ValueError(f'Нет перехода {key!r}')
336
+ symbol,move,next_state=transitions[key]
337
+ if move not in (-1,0,1): raise ValueError('Сдвиг должен быть -1, 0 или 1')
338
+ if symbol==blank: cells.pop(head,None)
339
+ else: cells[head]=symbol
340
+ head+=move; state=next_state; steps+=1
341
+ return dict(tape=cells,head=head,state=state,steps=steps)
342
+
343
+ @_task(13)
344
+ def network_info(address, mask):
345
+ """Параметры IPv4-сети по адресу и маске (число бит или строка). hosts_count — обычные адреса без сети/ broadcast; для /31 и /32 соответствует ipaddress.hosts(). Пример: network_info('192.168.1.12',24)."""
346
+ net=ipaddress.IPv4Network(f'{address}/{mask}',strict=False)
347
+ return dict(network=str(net.network_address), broadcast=str(net.broadcast_address), mask=str(net.netmask), prefix=net.prefixlen, addresses=net.num_addresses, hosts_count=net.num_addresses-2 if net.prefixlen<31 else net.num_addresses)
348
+
349
+ @_task(13)
350
+ def count_ip(address, mask, predicate, include_edges=True, max_addresses=1<<20):
351
+ """Перебрать IPv4-адреса сети и проверить predicate(int_ip). include_edges=True включает сеть и broadcast. Пример: count_ip('192.168.1.0',24,lambda n:n.bit_count()%2==0). Ограничивает размер сети, чтобы случайно не перебирать миллиарды адресов."""
352
+ net=ipaddress.IPv4Network(f'{address}/{mask}',strict=False)
353
+ if net.num_addresses>max_addresses: raise ValueError('Сеть слишком большая для перебора; используйте комбинаторику')
354
+ return sum(bool(predicate(int(ip))) for ip in (net if include_edges else net.hosts()))
355
+
356
+ @_task(13)
357
+ def common_prefix(addresses):
358
+ """Максимальная общая длина префикса IPv4 для непустого списка адресов. Пример: common_prefix(['192.168.1.1','192.168.1.2']) == 30."""
359
+ a=[int(ipaddress.IPv4Address(x)) for x in addresses]
360
+ if not a: raise ValueError('Пустой список')
361
+ return 32-(min(a)^max(a)).bit_length()
362
+
363
+ @_task(13)
364
+ def ip_weight_counts(address, mask):
365
+ """Число всех адресов сети с каждым количеством единичных битов, без перебора. Сеть и broadcast включены. Пример: sum(ip_weight_counts('10.0.0.0',8).values()) == 2**24."""
366
+ from math import comb
367
+ net=ipaddress.IPv4Network(f'{address}/{mask}',strict=False)
368
+ fixed=int(net.network_address).bit_count(); free=32-net.prefixlen
369
+ return {fixed+k:comb(free,k) for k in range(free+1)}
370
+
371
+ @_task(15)
372
+ def valid_parameters(parameters, domain, predicate):
373
+ """Найти A, для которых predicate(A,x) истинно для ВСЕХ x из конечного domain. Для пары переменных domain задайте product(...), а x распакуйте. Пример: valid_parameters(range(10), range(5), lambda a,x:a>=x). Это проверка конечной области, не доказательство для всех целых/вещественных."""
374
+ domain=tuple(domain)
375
+ return [a for a in parameters if all(predicate(a,x) for x in domain)]
376
+
377
+ @_task(15)
378
+ def interval_contains(x, left, right, closed=True):
379
+ """Принадлежность закрытому [left,right] или открытому (left,right) интервалу. Пример: interval_contains(3,1,3) == True."""
380
+ return left<=x<=right if closed else left<x<right
381
+
382
+ @_task(16)
383
+ def recurrence_values(start, stop, initial, rule):
384
+ """Вычислить рекуррентную функцию снизу вверх на [start,stop]. initial — словарь базовых значений; rule(n,values) видит уже вычисленные значения. Пример: recurrence_values(2,5,{0:0,1:1},lambda n,f:f[n-1]+f[n-2])[5] == 5. Для обратных зависимостей измените направление в своём цикле."""
385
+ values=dict(initial)
386
+ for n in range(start,stop+1):
387
+ if n not in values: values[n]=rule(n,values)
388
+ return values
389
+
390
+ @_task(16)
391
+ def recursive_function(rule):
392
+ """Создать мемоизированную функцию f(n); rule(f,n) содержит базу и рекурсию. Пример: f=recursive_function(lambda f,n:1 if n<=1 else n*f(n-1)); f(5)==120. Для большой глубины используйте recurrence_values, лимит рекурсии не меняется."""
393
+ @lru_cache(None)
394
+ def f(n): return rule(f,n)
395
+ return f
396
+
397
+ @_task(17)
398
+ def adjacent_windows(data, width, predicate):
399
+ """Подходящие последовательные окна: список (индекс_с_0, кортеж_значений). Пример: adjacent_windows([1,2,3],2,lambda w:sum(w)>3). В отличие от всех пар берёт только соседние элементы."""
400
+ if width<1: raise ValueError('Нужна положительная ширина')
401
+ return [(i,tuple(data[i:i+width])) for i in range(len(data)-width+1) if predicate(tuple(data[i:i+width]))]
402
+
403
+ @_task(17)
404
+ def adjacent_summary(data, width, predicate, score=sum):
405
+ """Количество подходящих соседних окон и минимум/максимум score(window); None при отсутствии. Пример: adjacent_summary([1,2,3],2,lambda w:True) == {'count':2,'min':3,'max':5}."""
406
+ values=[score(w) for i,w in adjacent_windows(data,width,predicate)]
407
+ return dict(count=len(values),min=min(values,default=None),max=max(values,default=None))
408
+
409
+ @_task(18)
410
+ def grid_path_extremes(grid, moves=((1,0),(0,1)), blocked=None):
411
+ """Минимальная и максимальная сумма пути из верхней левой в нижнюю правую клетку, включая обе. None — стена. moves допускает только сдвиги вниз/вправо без (0,0). Возвращает (None,None), если пути нет. Пример: grid_path_extremes([[1,2],[3,4]]) == (7,8). Стены между клетками моделируйте отдельно графом."""
412
+ if not grid or not grid[0]: raise ValueError('Пустая таблица')
413
+ h=len(grid); w=len(grid[0])
414
+ if any(len(row)!=w for row in grid): raise ValueError('Нужна прямоугольная таблица')
415
+ if any(dr<0 or dc<0 or dr+dc==0 for dr,dc in moves): raise ValueError('Движения должны вести вниз/вправо')
416
+ low=[[inf]*w for _ in range(h)]; high=[[-inf]*w for _ in range(h)]
417
+ for r in range(h):
418
+ for c in range(w):
419
+ if grid[r][c]==blocked: continue
420
+ if r==c==0: low[r][c]=high[r][c]=grid[r][c]; continue
421
+ for dr,dc in moves:
422
+ pr,pc=r-dr,c-dc
423
+ if pr>=0 and pc>=0:
424
+ low[r][c]=min(low[r][c],low[pr][pc]+grid[r][c])
425
+ high[r][c]=max(high[r][c],high[pr][pc]+grid[r][c])
426
+ return (None,None) if low[-1][-1]==inf else (low[-1][-1],high[-1][-1])
427
+
428
+ @_task(19, 20, 21)
429
+ def game_solver(moves, terminal, max_plies):
430
+ """Функция оценки конечной игры с полной информацией, без ничьих. state — хешируемое состояние; moves(state) — ходы; terminal(state) — уже достигнутая цель. Метки W1/W2 — победа за 1/2 своих хода, L1/L2 — неизбежное поражение, '?' — глубины мало. max_plies — число полуходов; terminal получает 'L0'. Пример: g=game_solver(lambda s:(s+1,s*2),lambda s:s>=10,4); g(9)=='W1'. Для задач 19 с НЕУДАЧНЫМ первым ходом отдельно проверьте существование хода в W1."""
431
+ if max_plies<0: raise ValueError('Отрицательная глубина')
432
+ @lru_cache(None)
433
+ def solve(state,depth):
434
+ if terminal(state): return ('L',0)
435
+ if depth==0: return ('?',0)
436
+ children=[solve(s,depth-1) for s in moves(state)]
437
+ if not children: return ('L',0)
438
+ wins=[k+1 for mark,k in children if mark=='L']
439
+ if wins: return ('W',min(wins))
440
+ if all(mark=='W' for mark,k in children): return ('L',max(k for mark,k in children))
441
+ return ('?',0)
442
+ def result(state):
443
+ mark,k=solve(state,max_plies)
444
+ return '?' if mark=='?' else f'{mark}{k}'
445
+ return result
446
+
447
+ @_task(22)
448
+ def process_schedule(durations, dependencies, lag=0):
449
+ """Ранние start/finish процессов при неограниченном числе исполнителей. durations — {ID:длительность}, dependencies — {ID:[предшественники]}; лаг lag добавляется после зависимостей, не к корневым процессам. Возвращает starts, finishes, total. Циклы/неизвестные ID — ошибка. Пример: process_schedule({'A':3,'B':2},{'B':['A']})['total']==5. Для ограниченного числа процессоров нужен отдельный планировщик."""
450
+ if lag<0 or any(d<0 for d in durations.values()): raise ValueError('Отрицательная длительность/лаг')
451
+ active=set(); starts={}; finishes={}
452
+ def finish(u):
453
+ if u in finishes: return finishes[u]
454
+ if u not in durations: raise ValueError(f'Неизвестный процесс {u!r}')
455
+ if u in active: raise ValueError('Цикл зависимостей')
456
+ active.add(u); parents=dependencies.get(u,())
457
+ starts[u]=max((finish(p)+lag for p in parents),default=0)
458
+ finishes[u]=starts[u]+durations[u]; active.remove(u)
459
+ return finishes[u]
460
+ for u in durations: finish(u)
461
+ return dict(starts=starts,finishes=finishes,total=max(finishes.values(),default=0))
462
+
463
+ @_task(22, 26)
464
+ def peak_overlap(intervals):
465
+ """Максимум одновременно активных полуоткрытых интервалов [start,end), например процессов или посетителей. В момент окончания ресурс освобождается. Пример: peak_overlap([(0,3),(2,4),(3,5)]) == 2."""
466
+ events=defaultdict(int)
467
+ for a,b in intervals:
468
+ if b<a: raise ValueError('Конец раньше начала')
469
+ events[a]+=1; events[b]-=1
470
+ cur=best=0
471
+ for t in sorted(events): cur+=events[t]; best=max(best,cur)
472
+ return best
473
+
474
+ @_task(23)
475
+ def count_programs(start, finish, commands, allowed, max_states=100000):
476
+ """Число программ в конечном ациклическом пространстве состояний. commands — функции одного хода; allowed(state) ограничивает область и запреты. Разные команды с одинаковым результатом считаются разными программами. Строит граф и считает без глубокой рекурсии, обнаруживает циклы. Пример: count_programs(1,5,[lambda x:x+1,lambda x:x*2],lambda x:1<=x<=5) == 4."""
477
+ if not allowed(start) or not allowed(finish): return 0
478
+ graph={}; pending=[start]; seen={start}
479
+ while pending:
480
+ u=pending.pop()
481
+ graph[u]=[] if u==finish else [v for cmd in commands if allowed(v:=cmd(u))]
482
+ for v in graph[u]:
483
+ if v not in seen:
484
+ seen.add(v); pending.append(v)
485
+ if len(seen)>max_states: raise ValueError('Превышен лимит состояний')
486
+ indegree=dict.fromkeys(seen,0)
487
+ for edges in graph.values():
488
+ for v in edges: indegree[v]+=1
489
+ q=deque(u for u in seen if indegree[u]==0); count=dict.fromkeys(seen,0); count[start]=1; visited=0
490
+ while q:
491
+ u=q.popleft(); visited+=1
492
+ for v in graph[u]:
493
+ count[v]+=count[u]; indegree[v]-=1
494
+ if indegree[v]==0: q.append(v)
495
+ if visited!=len(seen): raise ValueError('В достижимых состояниях есть цикл')
496
+ return count.get(finish,0)
497
+
498
+ @_task(23)
499
+ def count_programs_via(start, finish, checkpoints, commands, allowed):
500
+ """Число программ с посещением checkpoints по порядку: произведение числа путей между контрольными точками. Применять только при строгой монотонности числовых состояний, чтобы промежуточный сегмент не пересёк будущую точку раньше срока. Пример: count_programs_via(1,5,[3],[lambda x:x+1],lambda x:1<=x<=5)==1."""
501
+ points=[start,*checkpoints,finish]
502
+ if not (all(a<b for a,b in zip(points,points[1:])) or all(a>b for a,b in zip(points,points[1:]))): raise ValueError('Нужны строго упорядоченные точки')
503
+ result=1
504
+ direction=1 if finish>start else -1
505
+ def checked(cmd):
506
+ def apply(x):
507
+ y=cmd(x)
508
+ if (y-x)*direction<=0: raise ValueError('Команды должны быть строго монотонны')
509
+ return y
510
+ return apply
511
+ commands=[checked(c) for c in commands]
512
+ for a,b in zip(points,points[1:]):
513
+ result*=count_programs(a,b,commands,lambda x,a=a,b=b: min(a,b)<=x<=max(a,b) and allowed(x))
514
+ return result
515
+
516
+ @_task(24)
517
+ def longest_run(text, allowed):
518
+ """Самая длинная непрерывная подстрока из символов allowed (множество или строка). Возвращает длину. Пример: longest_run('ABXAAA','AB') == 3."""
519
+ cur=best=0
520
+ for ch in text:
521
+ cur=cur+1 if ch in allowed else 0; best=max(best,cur)
522
+ return best
523
+
524
+ @_task(24)
525
+ def longest_adjacent(text, compatible):
526
+ """Максимальная длина подстроки, где каждая соседняя пара удовлетворяет compatible(a,b). O(n). Пример: longest_adjacent('AABAB',lambda a,b:a!=b) == 4."""
527
+ if not text: return 0
528
+ cur=best=1
529
+ for a,b in zip(text,text[1:]):
530
+ cur=cur+1 if compatible(a,b) else 1; best=max(best,cur)
531
+ return best
532
+
533
+ @_task(24)
534
+ def longest_at_most(text, predicate, k):
535
+ """Максимальная длина подстроки, содержащей не более k символов, удовлетворяющих predicate(ch). O(n). Пример: longest_at_most('ABACA',lambda c:c=='A',1)==3."""
536
+ if k<0: raise ValueError('k>=0')
537
+ left=count=best=0
538
+ for right,ch in enumerate(text):
539
+ count+=bool(predicate(ch))
540
+ while count>k: count-=bool(predicate(text[left])); left+=1
541
+ best=max(best,right-left+1)
542
+ return best
543
+
544
+ @_task(24)
545
+ def shortest_at_least(text, predicate, k):
546
+ """Минимальная длина подстроки с минимум k отмеченными символами, None если нет; k=0 даёт 0. O(n). Пример: shortest_at_least('ABACA',lambda c:c=='A',2)==3."""
547
+ if k<0: raise ValueError('k>=0')
548
+ if k==0: return 0
549
+ left=count=0; best=inf
550
+ for right,ch in enumerate(text):
551
+ count+=bool(predicate(ch))
552
+ while count>=k:
553
+ best=min(best,right-left+1); count-=bool(predicate(text[left])); left+=1
554
+ return None if best==inf else best
555
+
556
+ @_task(24)
557
+ def longest_exactly(text, predicate, k):
558
+ """Максимальная длина непустой подстроки ровно с k отмеченными символами; 0, если подходящей нет. O(n). Пример: longest_exactly('ABACA',lambda c:c=='A',2)==4."""
559
+ if k<0: raise ValueError('k>=0')
560
+ left=count=best=0
561
+ for right,ch in enumerate(text):
562
+ count+=bool(predicate(ch))
563
+ while count>k: count-=bool(predicate(text[left])); left+=1
564
+ if count==k: best=max(best,right-left+1)
565
+ return best
566
+
567
+ @_task(24)
568
+ def longest_distinct(text):
569
+ """Максимальная длина подстроки без повторяющихся символов, O(n). Пример: longest_distinct('ABCADE') == 5."""
570
+ last={}; left=best=0
571
+ for right,ch in enumerate(text):
572
+ left=max(left,last.get(ch,-1)+1); last[ch]=right; best=max(best,right-left+1)
573
+ return best
574
+
575
+ @_task(10, 24)
576
+ def regex_matches(text, pattern, overlap=False):
577
+ """Список (начало, конец_исключая, текст) совпадений регулярного выражения. overlap=True ищет и пересекающиеся совпадения через match в каждой позиции; на длинных строках это может быть O(n²). Пример: regex_matches('12+34 A','[0-9]+(?:[+*][0-9]+)*'). Регулярку и запрет ведущих нулей задавайте по условию."""
578
+ if overlap:
579
+ compiled=re.compile(pattern)
580
+ return [(i,m.end(),m.group()) for i in range(len(text)+1) if (m:=compiled.match(text,i)) is not None]
581
+ return [(m.start(),m.end(),m.group()) for m in re.finditer(pattern,text)]
582
+
583
+ @_task(25)
584
+ def is_prime(n):
585
+ """Проверка простоты целого числа пробными делителями до sqrt(n), O(sqrt(n)); n<2 не простое. Пример: is_prime(97) == True."""
586
+ if n<2: return False
587
+ if n%2==0: return n==2
588
+ return all(n%d for d in range(3,isqrt(n)+1,2))
589
+
590
+ @_task(25)
591
+ def divisors(n, proper=False):
592
+ """Все положительные делители положительного n по возрастанию. proper=True исключает 1 и само n (нетривиальные). Пример: divisors(12,True) == [2,3,4,6]."""
593
+ if n<1: raise ValueError('n должно быть положительным')
594
+ low=[]; high=[]
595
+ for d in range(1,isqrt(n)+1):
596
+ if n%d==0:
597
+ low.append(d)
598
+ if d*d!=n: high.append(n//d)
599
+ result=low+high[::-1]
600
+ return [d for d in result if d not in (1,n)] if proper else result
601
+
602
+ @_task(25)
603
+ def prime_factors(n):
604
+ """Простые множители с кратностями в словаре; n>=1. Пример: prime_factors(72) == {2:3,3:2}."""
605
+ if n<1: raise ValueError('n>=1')
606
+ result={}; d=2
607
+ while d*d<=n:
608
+ while n%d==0: result[d]=result.get(d,0)+1; n//=d
609
+ d=3 if d==2 else d+2
610
+ if n>1: result[n]=result.get(n,0)+1
611
+ return result
612
+
613
+ @_task(25)
614
+ def sieve(limit):
615
+ """Список простых чисел от 2 до limit включительно, решето Эратосфена. Память O(limit). Пример: sieve(10) == [2,3,5,7]."""
616
+ if limit<2: return []
617
+ flags=bytearray(b'\1')*(limit+1); flags[:2]=b'\0\0'
618
+ for d in range(2,isqrt(limit)+1):
619
+ if flags[d]: flags[d*d:limit+1:d]=b'\0'*((limit-d*d)//d+1)
620
+ return [i for i,b in enumerate(flags) if b]
621
+
622
+ @_task(25)
623
+ def mask_matches(n, mask):
624
+ """Соответствие неотрицательного числа десятичной маске: ? — ровно одна цифра, * — любая последовательность, включая пустую. Другие символы запрещены. Пример: mask_matches(12345,'1?3*') == True."""
625
+ if any(ch not in '0123456789?*' for ch in mask): raise ValueError('Допустимы только цифры, ? и *')
626
+ return n>=0 and fnmatch.fnmatchcase(str(n),mask)
627
+
628
+ @_task(25)
629
+ def masked_multiples(mask, divisor, limit, start=1):
630
+ """Пары (число,частное) для кратных положительному divisor в включительном [start,limit], подходящих маске. Перебор O(limit/divisor). Пример: masked_multiples('1*',7,100) == [(14,2)]. При малом делителе и огромной границе нужен другой перебор."""
631
+ if divisor<=0 or start<0: raise ValueError('divisor>0, start>=0')
632
+ first=((start+divisor-1)//divisor)*divisor
633
+ return [(n,n//divisor) for n in range(first,limit+1,divisor) if mask_matches(n,mask)]
634
+
635
+ @_task(6)
636
+ def turtle_vertices(commands, start=(0,0), heading=0):
637
+ """Математическая Черепаха без окна: commands — ('F',длина), ('L',градусы), ('R',градусы); heading=0 направлен вправо, 90 вверх. Возвращает вершины ломаной, включая старт. Пример: turtle_vertices([('F',3),('L',90),('F',4)]). Повторы заранее разверните умножением списка; перо/дуги не моделируются."""
638
+ from math import sin,cos,radians
639
+ x,y=start; points=[(x,y)]
640
+ for cmd,value in commands:
641
+ if cmd=='L': heading+=value
642
+ elif cmd=='R': heading-=value
643
+ elif cmd=='F':
644
+ x+=value*cos(radians(heading)); y+=value*sin(radians(heading)); points.append((x,y))
645
+ else: raise ValueError('Команда должна быть F/L/R')
646
+ return points
647
+
648
+ @_task(6)
649
+ def polygon_area(polygon):
650
+ """Площадь простого многоугольника по формуле Гаусса. Вершины идут по контуру; повтор последней первой необязателен. Для самопересечений вернёт модуль ориентированной площади, а не площадь закраски. Пример: polygon_area([(0,0),(3,0),(3,4)]) == 6."""
651
+ p=list(polygon)
652
+ return abs(sum(a[0]*b[1]-b[0]*a[1] for a,b in zip(p,p[1:]+p[:1])))/2
653
+
654
+ @_task(6)
655
+ def point_in_polygon(point, polygon, boundary=False, eps=1e-9):
656
+ """Проверить точку в простом многоугольнике методом луча; boundary включает контур. eps — допуск для вещественных координат. Пример: point_in_polygon((1,1),[(0,0),(3,0),(3,3),(0,3)]) == True."""
657
+ x,y=point; p=list(polygon); inside=False
658
+ for (ax,ay),(bx,by) in zip(p,p[1:]+p[:1]):
659
+ cross=(x-ax)*(by-ay)-(y-ay)*(bx-ax)
660
+ if abs(cross)<=eps and min(ax,bx)-eps<=x<=max(ax,bx)+eps and min(ay,by)-eps<=y<=max(ay,by)+eps: return boundary
661
+ if (ay>y)!=(by>y) and x<(bx-ax)*(y-ay)/(by-ay)+ax: inside=not inside
662
+ return inside
663
+
664
+ @_task(6)
665
+ def lattice_points(polygons, mode='intersection', boundary=False, max_candidates=2000000):
666
+ """Целочисленные точки внутри пересечения/объединения простых многоугольников, mode='intersection'/'union'. boundary включает границы. Возвращает координаты, не только количество. Перебор ограничен max_candidates. Пример: len(lattice_points([[(0,0),(3,0),(3,3),(0,3)]])) == 4."""
667
+ if mode not in ('intersection','union'): raise ValueError('Неизвестный mode')
668
+ if not polygons or any(len(p)<3 for p in polygons): raise ValueError('Нужны многоугольники')
669
+ vertices=[point for p in polygons for point in p]
670
+ xmin=ceil(min(x for x,y in vertices)-1e-9); xmax=floor(max(x for x,y in vertices)+1e-9)
671
+ ymin=ceil(min(y for x,y in vertices)-1e-9); ymax=floor(max(y for x,y in vertices)+1e-9)
672
+ if (xmax-xmin+1)*(ymax-ymin+1)>max_candidates: raise ValueError('Слишком большой перебор точек')
673
+ combine=all if mode=='intersection' else any
674
+ return [(x,y) for x in range(xmin,xmax+1) for y in range(ymin,ymax+1) if combine(point_in_polygon((x,y),p,boundary) for p in polygons)]
675
+
676
+ @_task(6)
677
+ def pick_interior(polygon):
678
+ """Количество строго внутренних решётчатых точек простого многоугольника с ЦЕЛЫМИ вершинами по теореме Пика. Быстро, без перебора. Самопересечения/дыры не поддерживаются. Пример: pick_interior([(0,0),(3,0),(3,3),(0,3)]) == 4."""
679
+ from math import gcd
680
+ p=list(polygon)
681
+ if len(p)>1 and p[-1]==p[0]: p.pop()
682
+ if len(p)<3 or any(int(x)!=x or int(y)!=y for x,y in p): raise ValueError('Нужны целые вершины')
683
+ edges=list(zip(p,p[1:]+p[:1]))
684
+ twice=abs(sum(int(a[0])*int(b[1])-int(b[0])*int(a[1]) for a,b in edges))
685
+ border=sum(gcd(abs(int(a[0]-b[0])),abs(int(a[1]-b[1]))) for a,b in edges)
686
+ return (twice-border+2)//2
687
+
688
+ @_task(26)
689
+ def archive_files(sizes, capacity):
690
+ """Архивирование: максимум файлов, затем максимальный размер одного из выбранных. Возвращает count, max_file, selected. Каждый размер/ёмкость неотрицательны. Пример: archive_files([2,3,8,9],12)['selected'] == [2,9]."""
691
+ if capacity<0 or any(s<0 for s in sizes): raise ValueError('Отрицательный размер')
692
+ a=sorted(sizes); total=0; k=0
693
+ while k<len(a) and total+a[k]<=capacity: total+=a[k]; k+=1
694
+ if not k: return dict(count=0,max_file=None,selected=[])
695
+ remaining=capacity-sum(a[:k-1]); largest=max(v for v in a[k-1:] if v<=remaining)
696
+ return dict(count=k,max_file=largest,selected=a[:k-1]+[largest])
697
+
698
+ @_task(26)
699
+ def select_intervals(intervals, gap=0):
700
+ """Максимальное количество непересекающихся интервалов одним исполнителем, жадно по окончанию. gap — пауза между работами. Пример: select_intervals([(0,2),(1,4),(2,3)]) == [(0,2),(2,3)]. Для максимальной прибыли используйте weighted_intervals."""
701
+ if gap<0 or any(b<a for a,b in intervals): raise ValueError('Некорректные интервалы/пауза')
702
+ result=[]; end=-inf
703
+ for a,b in sorted(intervals,key=lambda x:(x[1],x[0])):
704
+ if a>=end+gap: result.append((a,b)); end=b
705
+ return result
706
+
707
+ @_task(26, 27)
708
+ def weighted_intervals(jobs):
709
+ """Максимальная суммарная прибыль непересекающихся работ (start,end,profit), границы [start,end). Возвращает прибыль; O(n log n). Пример: weighted_intervals([(0,2,5),(1,3,9),(2,4,5)]) == 10."""
710
+ from bisect import bisect_right
711
+ a=sorted(jobs,key=lambda x:x[1]); ends=[b for s,b,w in a]; dp=[0]
712
+ for i,(s,b,w) in enumerate(a):
713
+ if b<s: raise ValueError('Конец раньше начала')
714
+ j=bisect_right(ends,s,0,i)
715
+ dp.append(max(dp[-1],dp[j]+w))
716
+ return dp[-1]
717
+
718
+ @_task(26)
719
+ def nested_boxes(sizes, gap):
720
+ """Максимальная цепочка одномерных коробок: разница соседних размеров >=gap>0. Возвращает размеры по возрастанию. Пример: nested_boxes([1,2,5,8],3)==[1,5,8]. Для многомерных коробок это правило неприменимо."""
721
+ if gap<=0: raise ValueError('gap>0')
722
+ chosen=[]
723
+ for size in sorted(sizes):
724
+ if not chosen or size-chosen[-1]>=gap: chosen.append(size)
725
+ return chosen
726
+
727
+ @_task(26)
728
+ def missing_seats(occupied, free_count=2):
729
+ """Найти ровно free_count свободных мест между соседними занятыми в одном ряду. occupied — пары (ряд,место); возвращает (ряд,первое_свободное) по убыванию ряда, затем возрастанию места. Пример: missing_seats([(5,2),(5,5)])==[(5,3)]."""
730
+ if free_count<1: raise ValueError('free_count>=1')
731
+ rows=defaultdict(set)
732
+ for row,seat in occupied: rows[row].add(seat)
733
+ result=[]
734
+ for row,seats in rows.items():
735
+ a=sorted(seats)
736
+ result.extend((row,x+1) for x,y in zip(a,a[1:]) if y-x-1==free_count)
737
+ return sorted(result,key=lambda x:(-x[0],x[1]))
738
+
739
+ @_task(26, 27)
740
+ def knapsack(weights, capacity):
741
+ """Максимальная достижимая сумма весов <=capacity, каждый предмет используется один раз. Битовая динамика, память O(capacity) бит. Пример: knapsack([3,5,8],10) == 8. Не возвращает состав набора."""
742
+ if capacity<0 or any(w<0 or int(w)!=w for w in weights): raise ValueError('Нужны неотрицательные целые веса и capacity')
743
+ bits=1; mask=(1<<(capacity+1))-1
744
+ for w in weights:
745
+ if w<=capacity: bits=(bits | bits<<w)&mask
746
+ return bits.bit_length()-1
747
+
748
+ @_task(26)
749
+ def service_queue(arrivals, servers, max_wait=None):
750
+ """FCFS-очередь к одинаковым серверам. arrivals — (приход,длительность); servers>=1. При ожидании >max_wait клиент уходит (None). Результат в исходном порядке: (номер_сервера_с_1,начало,конец) либо None. Пример: service_queue([(0,5),(1,2)],1)==[(1,0,5),(1,5,7)]. Для задач с выбором сервера по номеру/без ожидания правила надо изменить."""
751
+ if servers<1 or (max_wait is not None and max_wait<0): raise ValueError('Некорректные параметры')
752
+ heap=[(-inf,i+1) for i in range(servers)]; heapq.heapify(heap); result=[None]*len(arrivals)
753
+ for index,(arrival,duration) in sorted(enumerate(arrivals),key=lambda item:(item[1][0],item[0])):
754
+ if duration<0: raise ValueError('Отрицательная длительность')
755
+ available,server=heap[0]; start=max(arrival,available)
756
+ if max_wait is not None and start-arrival>max_wait: continue
757
+ heapq.heapreplace(heap,(start+duration,server)); result[index]=(server,start,start+duration)
758
+ return result
759
+
760
+ @_task(27)
761
+ def prefix_sums(data):
762
+ """Префиксные суммы с нулём в начале. Сумма data[l:r] равна p[r]-p[l]. Пример: prefix_sums([2,3,-1]) == [0,2,5,4]."""
763
+ p=[0]
764
+ for x in data: p.append(p[-1]+x)
765
+ return p
766
+
767
+ @_task(27)
768
+ def count_subarrays_mod(data, modulus, remainder=0):
769
+ """Количество непустых непрерывных подмассивов с нужным остатком суммы. O(n) времени, O(min(n,modulus)) памяти; отрицательные числа поддерживаются. Пример: count_subarrays_mod([1,2,3],3)==3."""
770
+ if modulus<1: raise ValueError('modulus>=1')
771
+ freq=Counter({0:1}); prefix=answer=0
772
+ for x in data:
773
+ prefix=(prefix+x)%modulus; answer+=freq[(prefix-remainder)%modulus]; freq[prefix]+=1
774
+ return answer
775
+
776
+ @_task(27)
777
+ def max_subarray_sum_mod(data, modulus, remainder=0):
778
+ """Максимальная сумма непустого подмассива с заданным остатком; None, если нет. O(n), работает с отрицательными. Пример: max_subarray_sum_mod([1,2,3],3)==6."""
779
+ if modulus<1: raise ValueError('modulus>=1')
780
+ minimum={0:0}; prefix=0; best=None
781
+ for x in data:
782
+ prefix+=x; r=prefix%modulus; previous=minimum.get((r-remainder)%modulus)
783
+ if previous is not None:
784
+ value=prefix-previous; best=value if best is None else max(best,value)
785
+ minimum[r]=min(minimum.get(r,inf),prefix)
786
+ return best
787
+
788
+ @_task(27)
789
+ def longest_subarray_sum_mod(data, modulus, remainder=0):
790
+ """Длина самого длинного подмассива с нужным остатком суммы, O(n). Возвращает 0 при отсутствии. Пример: longest_subarray_sum_mod([1,2,3],3)==3."""
791
+ if modulus<1: raise ValueError('modulus>=1')
792
+ first={0:0}; prefix=best=0
793
+ for i,x in enumerate(data,1):
794
+ prefix=(prefix+x)%modulus; r=(prefix-remainder)%modulus
795
+ if r in first: best=max(best,i-first[r])
796
+ first.setdefault(prefix,i)
797
+ return best
798
+
799
+ @_task(17, 27)
800
+ def count_pairs_mod(data, modulus, remainder=0, min_gap=1):
801
+ """Количество пар индексов i<j, j-i>=min_gap с нужным остатком суммы. min_gap=1 — любые разные элементы. O(n), отрицательные числа допустимы. Пример: count_pairs_mod([1,2,3,4],3)==2."""
802
+ if modulus<1 or min_gap<1: raise ValueError('modulus,min_gap>=1')
803
+ freq=Counter(); answer=0
804
+ for j,x in enumerate(data):
805
+ if j>=min_gap: freq[data[j-min_gap]%modulus]+=1
806
+ answer+=freq[(remainder-x)%modulus]
807
+ return answer
808
+
809
+ @_task(27)
810
+ def max_pair_sum_mod(data, modulus, remainder=0, min_gap=1):
811
+ """Максимальная сумма пары с условием на остаток и расстояние индексов; None при отсутствии. O(n), хранит максимумы по остаткам. Пример: max_pair_sum_mod([1,2,3,4],3)==6."""
812
+ if modulus<1 or min_gap<1: raise ValueError('modulus,min_gap>=1')
813
+ best_by={}; answer=None
814
+ for j,x in enumerate(data):
815
+ if j>=min_gap:
816
+ y=data[j-min_gap]; r=y%modulus; best_by[r]=max(best_by.get(r,-inf),y)
817
+ y=best_by.get((remainder-x)%modulus)
818
+ if y is not None: answer=x+y if answer is None else max(answer,x+y)
819
+ return answer
820
+
821
+ @_task(27)
822
+ def count_pairs_product(data, divisor, min_gap=1):
823
+ """Количество пар i<j с j-i>=min_gap и произведением, кратным divisor. Нули/отрицательные поддерживаются. O(n*tau(divisor)), хранит классы gcd. Пример: count_pairs_product([2,3,4],6)==2."""
824
+ from math import gcd
825
+ if divisor<1 or min_gap<1: raise ValueError('divisor,min_gap>=1')
826
+ counts=Counter(); answer=0
827
+ for j,x in enumerate(data):
828
+ if j>=min_gap: counts[gcd(data[j-min_gap],divisor)]+=1
829
+ g=gcd(x,divisor)
830
+ answer+=sum(c for d,c in counts.items() if d*g%divisor==0)
831
+ return answer
832
+
833
+ @_task(27)
834
+ def best_choice_mod(groups, modulus, remainder=0, maximize=True):
835
+ """Выбрать ровно одно число из каждой группы, оптимизируя сумму с нужным остатком. Динамика O(число_групп*modulus*размер_группы). Возвращает сумму или None. Пример: best_choice_mod([[1,2],[3,4]],3)==6. Для выбора двух из трёх передайте группы попарных сумм."""
836
+ if modulus<1: raise ValueError('modulus>=1')
837
+ dp={0:0}; better=max if maximize else min
838
+ for group in groups:
839
+ nxt={}
840
+ for total in dp.values():
841
+ for x in group:
842
+ value=total+x; r=value%modulus
843
+ nxt[r]=value if r not in nxt else better(nxt[r],value)
844
+ dp=nxt
845
+ return dp.get(remainder%modulus)
846
+
847
+ @_task(27)
848
+ def max_subarray(data):
849
+ """Максимальная сумма непустого подмассива (Кадане), O(n), O(1) памяти. None для пустого входа. Пример: max_subarray([-5,-2,-7]) == -2."""
850
+ best=cur=None
851
+ for x in data:
852
+ cur=x if cur is None else max(x,cur+x); best=cur if best is None else max(best,cur)
853
+ return best
854
+
855
+ @_task(27)
856
+ def min_window_sum_at_least(data, target):
857
+ """Минимальная длина непустого подмассива с суммой >=target для НЕОТРИЦАТЕЛЬНЫХ чисел. O(n), None если нет. Пример: min_window_sum_at_least([2,1,4,2],6)==2."""
858
+ if any(x<0 for x in data): raise ValueError('Нужны неотрицательные числа')
859
+ left=total=0; best=inf
860
+ for right,x in enumerate(data):
861
+ total+=x
862
+ while left<=right and total>=target:
863
+ best=min(best,right-left+1); total-=data[left]; left+=1
864
+ return None if best==inf else best
865
+
866
+ @_task(27)
867
+ def weighted_median(points):
868
+ """Взвешенная медиана на прямой для (координата,вес). Минимизирует сумму вес*расстояние. Возвращает (координата,стоимость), веса >=0 с положительной общей суммой. Пример: weighted_median([(0,1),(10,3)]) == (10,10)."""
869
+ a=sorted(points)
870
+ if not a or any(w<0 for x,w in a) or sum(w for x,w in a)<=0: raise ValueError('Нужны неотрицательные веса с положительной суммой')
871
+ total=sum(w for x,w in a); accumulated=0
872
+ for coordinate,weight in a:
873
+ accumulated+=weight
874
+ if accumulated*2>=total:
875
+ return coordinate,sum(w*abs(x-coordinate) for x,w in a)
876
+
877
+ @_task(27)
878
+ def circular_costs(weights):
879
+ """Стоимость пункта в каждой позиции равномерно размеченного кольца: sum(weights[j]*min(|i-j|,n-|i-j|)). O(n) через префиксы, n>=1. Пример: circular_costs([1,0,0,0]) == [0,1,2,1]. Умножьте на физический шаг между позициями."""
880
+ n=len(weights)
881
+ if not n: return []
882
+ a=list(weights)*3; p=prefix_sums(a); q=prefix_sums([i*w for i,w in enumerate(a)])
883
+ answer=[]; half=n//2
884
+ for i in range(n,2*n):
885
+ # Для чётного n противоположную точку относим к правой половине.
886
+ lo=i-(n-1)//2; hi=i+half
887
+ left=i*(p[i]-p[lo])-(q[i]-q[lo])
888
+ right=(q[hi+1]-q[i+1])-i*(p[hi+1]-p[i+1])
889
+ answer.append(left+right)
890
+ return answer
891
+
892
+ @_task(27)
893
+ def distance(a,b):
894
+ """Евклидово расстояние между двумя точками плоскости. Пример: distance((0,0),(3,4)) == 5."""
895
+ return hypot(a[0]-b[0],a[1]-b[1])
896
+
897
+ @_task(27)
898
+ def cluster_by_distance(points, threshold):
899
+ """Компоненты графа близости: точки связаны, если расстояние <=threshold; связь транзитивна. Это не k-means и не полный DBSCAN. Пространственная сетка ускоряет разреженные данные, худший случай O(n²). Возвращает список кластеров (координаты). Пример: cluster_by_distance([(0,0),(1,0),(9,0)],2). Порог выбирайте по условию/визуализации, цепочки могут объединить скопления."""
900
+ if threshold<=0: raise ValueError('threshold>0')
901
+ points=[tuple(p) for p in points]; buckets=defaultdict(list)
902
+ parent=list(range(len(points))); size=[1]*len(points)
903
+ def root(i):
904
+ while parent[i]!=i: parent[i]=parent[parent[i]]; i=parent[i]
905
+ return i
906
+ for i,(x,y) in enumerate(points):
907
+ cx,cy=floor(x/threshold),floor(y/threshold)
908
+ for dx,dy in product((-1,0,1), repeat=2):
909
+ for j in buckets.get((cx+dx,cy+dy),()):
910
+ if distance(points[i],points[j])<=threshold:
911
+ u,v=root(i),root(j)
912
+ if u!=v:
913
+ if size[u]<size[v]: u,v=v,u
914
+ parent[v]=u; size[u]+=size[v]
915
+ buckets[cx,cy].append(i)
916
+ groups=defaultdict(list)
917
+ for i,p in enumerate(points): groups[root(i)].append(p)
918
+ return list(groups.values())
919
+
920
+ @_task(27)
921
+ def kmeans(points, centers, max_steps=100, tolerance=1e-9):
922
+ """Разделение по ближайшему среднему с заданными начальными centers. Возвращает clusters, centers, converged. Может попасть в локальный минимум; не гарантирует кластеры из условия. Пустой кластер сохраняет свой центр. Пример: kmeans([(0,0),(1,0),(9,0)],[(0,0),(9,0)]). Для финального центра-медоида затем вызовите medoid."""
923
+ points=[tuple(p) for p in points]; centers=[tuple(c) for c in centers]
924
+ if not centers or max_steps<1: raise ValueError('Нужны центры и положительный лимит')
925
+ converged=False
926
+ for _ in range(max_steps):
927
+ groups=[[] for c in centers]
928
+ for p in points: groups[min(range(len(centers)),key=lambda i:distance(p,centers[i]))].append(p)
929
+ new=[centroid(g) if g else centers[i] for i,g in enumerate(groups)]
930
+ shift=max(distance(a,b) for a,b in zip(centers,new)); centers=new
931
+ if shift<=tolerance: converged=True; break
932
+ groups=[[] for c in centers]
933
+ for p in points: groups[min(range(len(centers)),key=lambda i:distance(p,centers[i]))].append(p)
934
+ return dict(clusters=groups,centers=centers,converged=converged)
935
+
936
+ @_task(27)
937
+ def centroid(points):
938
+ """Среднее арифметическое координат; обычно не совпадает с точкой исходного набора. Пример: centroid([(0,0),(2,4)]) == (1.0,2.0). Не заменяет медоид."""
939
+ if not points: raise ValueError('Пустой набор')
940
+ return sum(p[0] for p in points)/len(points),sum(p[1] for p in points)/len(points)
941
+
942
+ @_task(27)
943
+ def medoid(points):
944
+ """Точка набора с минимальной суммой евклидовых расстояний до остальных. Точный O(n²), O(n) памяти; при равенстве первая во входе. Пример: medoid([(0,0),(1,0),(10,0)]) == (1,0). На очень больших кластерах может работать долго."""
945
+ if not points: raise ValueError('Пустой набор')
946
+ points=[tuple(p) for p in points]; costs=[0.0]*len(points)
947
+ for i in range(len(points)):
948
+ for j in range(i):
949
+ d=distance(points[i],points[j]); costs[i]+=d; costs[j]+=d
950
+ return points[min(range(len(points)),key=costs.__getitem__)]
951
+
952
+ @_task(27)
953
+ def cluster_centers(clusters, method='medoid'):
954
+ """Центры непустых кластеров: method='medoid' или 'centroid'. Пример: cluster_centers([[(0,0),(1,0),(9,0)]]) == [(1,0)]. Выбирайте определение центра строго по условию."""
955
+ if method not in ('medoid','centroid'): raise ValueError('Неизвестный метод')
956
+ return [(medoid if method=='medoid' else centroid)(c) for c in clusters]
957
+
958
+ @_task(27)
959
+ def scaled_center_mean(centers, scale=10000, rounding='trunc'):
960
+ """Среднее координат центров, умноженное на scale. rounding='trunc' отбрасывает дробную часть к нулю, 'floor' округляет вниз, 'round' использует Python round. Пример: scaled_center_mean([(1,2),(3,4)]) == (20000,30000). Модуль/порядок координат задавайте по условию отдельно."""
961
+ x,y=centroid(centers)
962
+ if rounding not in ('trunc','floor','round'): raise ValueError('Неизвестное округление')
963
+ fn={'trunc':int,'floor':floor,'round':round}[rounding]
964
+ return fn(x*scale),fn(y*scale)
965
+
966
+ @_task(27)
967
+ def read_points(path, skip=1, decimal_comma=True, encoding='utf-8-sig'):
968
+ """Прочитать двумерные координаты из TXT, разделённые пробелами/табуляцией. skip=1 пропускает заголовок; decimal_comma заменяет десятичные запятые. Пример: read_points('27_A.txt'). При файле без заголовка задайте skip=0."""
969
+ text=read_text(path,encoding)
970
+ if decimal_comma: text=text.replace(',','.')
971
+ points=[tuple(map(float,line.split())) for line in text.splitlines()[skip:] if line.strip()]
972
+ if any(len(p)!=2 for p in points): raise ValueError('Нужны ровно две координаты в строке')
973
+ return points
974
+
975
+
976
+ @_task(3, 9, 18, 22)
977
+ def read_xlsx(path, sheet=0):
978
+ """Читать XLSX без внешних зависимостей: выбранный лист по индексу с 0, результат — строки значений. Числа преобразуются в int/float, пропуски в None. Читает сохранённые результаты формул, НЕ пересчитывает их; отсутствующий кеш формулы — ошибка. Пример: read_xlsx('9.xlsx'). Сложные типы/стили/даты не интерпретируются."""
979
+ ns={'m':'http://schemas.openxmlformats.org/spreadsheetml/2006/main'}
980
+ relns='{http://schemas.openxmlformats.org/officeDocument/2006/relationships}'
981
+ with zipfile.ZipFile(path) as z:
982
+ workbook=ET.fromstring(z.read('xl/workbook.xml'))
983
+ sheets=workbook.findall('m:sheets/m:sheet',ns)
984
+ selected=sheets[sheet]; relid=selected.attrib[relns+'id']
985
+ rels=ET.fromstring(z.read('xl/_rels/workbook.xml.rels'))
986
+ target=next(r.attrib['Target'] for r in rels if r.attrib['Id']==relid)
987
+ import posixpath
988
+ target=target.lstrip('/') if target.startswith('/') else posixpath.normpath('xl/'+target)
989
+ shared=[]
990
+ if 'xl/sharedStrings.xml' in z.namelist():
991
+ shared=[''.join(t.text or '' for t in si.findall('.//m:t',ns)) for si in ET.fromstring(z.read('xl/sharedStrings.xml')).findall('m:si',ns)]
992
+ root=ET.fromstring(z.read(target)); rows=[]
993
+ for row in root.findall('m:sheetData/m:row',ns):
994
+ row_index=int(row.attrib['r'])-1
995
+ while len(rows)<=row_index: rows.append([])
996
+ for cell in row.findall('m:c',ns):
997
+ letters=re.match(r'[A-Z]+',cell.attrib['r']).group(); col=0
998
+ for ch in letters: col=col*26+ord(ch)-64
999
+ col-=1
1000
+ while len(rows[row_index])<=col: rows[row_index].append(None)
1001
+ kind=cell.attrib.get('t'); v=cell.find('m:v',ns)
1002
+ if cell.find('m:f',ns) is not None and (v is None or v.text is None): raise ValueError('Формула без сохранённого результата; пересчитайте файл в табличном редакторе')
1003
+ if kind=='inlineStr': value=''.join(t.text or '' for t in cell.findall('.//m:t',ns))
1004
+ elif v is None or v.text is None: value=None
1005
+ elif kind=='s': value=shared[int(v.text)]
1006
+ elif kind=='b': value=bool(int(v.text))
1007
+ elif kind in ('str','e','d'): value=v.text
1008
+ else:
1009
+ value=float(v.text)
1010
+ if value.is_integer(): value=int(value)
1011
+ rows[row_index][col]=value
1012
+ return rows
1013
+
1014
+ @_task(3, 9, 18, 22)
1015
+ def read_ods(path, sheet=0, max_cells=2000000):
1016
+ """Читать непустую область листа ODS в строки значений, включая сохранённые результаты формул. Числа — int/float, пропуски — None. Не пересчитывает формулы; повторения пустых хвостовых строк/ячеек обрезает. Пример: read_ods('demo_22.ods'). Не поддерживает сложное форматирование/объединённые ячейки."""
1017
+ t='{urn:oasis:names:tc:opendocument:xmlns:table:1.0}'
1018
+ o='{urn:oasis:names:tc:opendocument:xmlns:office:1.0}'
1019
+ textns='{urn:oasis:names:tc:opendocument:xmlns:text:1.0}'
1020
+ with zipfile.ZipFile(path) as z: root=ET.fromstring(z.read('content.xml'))
1021
+ tables=root.findall('.//'+o+'spreadsheet/'+t+'table'); table=tables[sheet]
1022
+ result=[]; pending_rows=0; cells_used=0
1023
+ for row in table.iter(t+'table-row'):
1024
+ values=[]; pending_cells=0
1025
+ for cell in row:
1026
+ if cell.tag not in (t+'table-cell',t+'covered-table-cell'): continue
1027
+ repeat=int(cell.get(t+'number-columns-repeated','1')); kind=cell.get(o+'value-type')
1028
+ if kind in ('float','currency','percentage'):
1029
+ value=float(cell.get(o+'value')); value=int(value) if value.is_integer() else value
1030
+ elif kind=='boolean': value=cell.get(o+'boolean-value')=='true'
1031
+ else:
1032
+ value='\n'.join(''.join(p.itertext()) for p in cell.findall(textns+'p')) or None
1033
+ if cell.get(t+'formula') and value is None: raise ValueError('Формула без сохранённого результата')
1034
+ if value is None: pending_cells+=repeat
1035
+ else:
1036
+ if len(values)+pending_cells+repeat>max_cells: raise ValueError('Слишком много ячеек')
1037
+ values.extend([None]*pending_cells); values.extend([value]*repeat); pending_cells=0
1038
+ repeats=int(row.get(t+'number-rows-repeated','1'))
1039
+ if not values: pending_rows+=repeats; continue
1040
+ cells_used+=len(values)*repeats+pending_rows
1041
+ if cells_used>max_cells: raise ValueError('Слишком много ячеек')
1042
+ result.extend([[] for _ in range(pending_rows)]); pending_rows=0
1043
+ result.extend([values.copy() for _ in range(repeats)])
1044
+ return result
1045
+
1046
+ @_task(22)
1047
+ def processes_from_rows(rows, skip=1):
1048
+ """Преобразовать строки [ID,длительность,'предшественник;предшественник'] в durations/dependencies. '0'/0/None означает отсутствие зависимостей. Возвращает два словаря, ID — строки. Пример: processes_from_rows([['1',3,'0'],['2',4,'1']],skip=0)."""
1049
+ durations={}; dependencies={}
1050
+ for row in rows[skip:]:
1051
+ if len(row)<3: continue
1052
+ identifier=str(row[0]).strip()
1053
+ if identifier in durations: raise ValueError('Повтор ID процесса')
1054
+ durations[identifier]=int(row[1]); raw=str(row[2]).strip()
1055
+ dependencies[identifier]=[] if raw in ('0','None','') else [x.strip() for x in raw.split(';')]
1056
+ return durations,dependencies
1057
+
1058
+ @_task(22)
1059
+ def active_processes(schedule, millisecond):
1060
+ """ID процессов, исполняющихся на миллисекунде с номером millisecond, нумерация с 1. schedule — результат process_schedule с нулевым началом времени. Пример: active_processes(process_schedule({'A':3},{}),3)==['A']."""
1061
+ if millisecond<1: raise ValueError('Миллисекунды нумеруются с 1')
1062
+ t=millisecond-1
1063
+ return [u for u in schedule['starts'] if schedule['starts'][u]<=t<schedule['finishes'][u]]
1064
+
1065
+ @_task(23)
1066
+ def read_weighted_graph(path, skip=0, weight=float):
1067
+ """Прочитать ориентированный граф из строк 'u v вес'; ID — int, weight можно заменить на Decimal для точной десятичной арифметики. Результат: {u:{v:вес}}. Пример: read_weighted_graph('23.txt'). Повторное ребро — ошибка."""
1068
+ graph={}
1069
+ for row in read_rows(path,cast=str,skip=skip):
1070
+ if len(row)!=3: raise ValueError('Нужны три поля u v вес')
1071
+ u,v=int(row[0]),int(row[1]); w=weight(row[2].replace(',','.'))
1072
+ if v in graph.setdefault(u,{}): raise ValueError('Повторное ребро')
1073
+ graph[u][v]=w; graph.setdefault(v,{})
1074
+ return graph
1075
+
1076
+ @_task(23)
1077
+ def dag_analysis(graph, start, finish):
1078
+ """Для взвешенного DAG: число путей и min/max их длины; все вершины упорядочивает топологически, циклы — ошибка. O(V+E), без рекурсии; поддерживает отрицательные веса. Пример: dag_analysis({'A':{'B':2},'B':{}},'A','B') == {'count':1,'min':2,'max':2}."""
1079
+ nodes=set(graph) | {v for edges in graph.values() for v in edges} | {start,finish}
1080
+ indegree=dict.fromkeys(nodes,0)
1081
+ for edges in graph.values():
1082
+ for v in edges: indegree[v]+=1
1083
+ q=deque(u for u in nodes if not indegree[u]); count=dict.fromkeys(nodes,0); count[start]=1
1084
+ low=dict.fromkeys(nodes,inf); high=dict.fromkeys(nodes,-inf); low[start]=high[start]=0; visited=0
1085
+ while q:
1086
+ u=q.popleft(); visited+=1
1087
+ for v,w in graph.get(u,{}).items():
1088
+ if count[u]:
1089
+ count[v]+=count[u]; low[v]=min(low[v],low[u]+w); high[v]=max(high[v],high[u]+w)
1090
+ indegree[v]-=1
1091
+ if indegree[v]==0: q.append(v)
1092
+ if visited!=len(nodes): raise ValueError('Граф содержит цикл')
1093
+ return dict(count=count[finish],min=None if low[finish]==inf else low[finish],max=None if high[finish]==-inf else high[finish])
1094
+
1095
+ @_task(26)
1096
+ def backup_log(records, capacity, deadline='11:59:59'):
1097
+ """Модель журнала сервера из демо-2027: records — (ЧЧ:ММ:СС,ID,объём). При переполнении до запроса копируется накопленное; финальный остаток не копируется. Возвращает top_client, backups, top_two_sum. Все передачи учитываются при выборе клиента, при равенстве берётся меньший ID. Копии суммируются только не позднее deadline. Пример: backup_log([('01:00:00',1,8),('02:00:00',2,4)],10)['top_two_sum']==8."""
1098
+ if capacity<=0: raise ValueError('capacity>0')
1099
+ totals=Counter(); backups=[]; used=0
1100
+ for time,client,size in sorted(records,key=lambda r:r[0]):
1101
+ if not 0<=size<=capacity: raise ValueError('Размер запроса вне ёмкости')
1102
+ totals[client]+=size
1103
+ if used+size>capacity:
1104
+ backups.append((time,used)); used=0
1105
+ used+=size
1106
+ top=min(totals,key=lambda c:(-totals[c],c)) if totals else None
1107
+ largest=heapq.nlargest(2,(size for time,size in backups if time<=deadline))
1108
+ return dict(top_client=top,backups=backups,top_two_sum=sum(largest))
1109
+
1110
+ @_task(27)
1111
+ def read_particles(path, skip=0):
1112
+ """Читать частицы из строк x y vx vy mass type, десятичная запятая допустима. Возвращает словари полей, включая energy=mass*(vx²+vy²)/2. Пример: read_particles('27.txt'). Строки заголовка явно пропустите skip."""
1113
+ result=[]
1114
+ for row in read_rows(path,cast=str,skip=skip):
1115
+ if len(row)!=6: raise ValueError('Нужно шесть полей частицы')
1116
+ x,y,vx,vy,mass=(float(s.replace(',','.').replace('–','-').replace('−','-')) for s in row[:5])
1117
+ if mass<0: raise ValueError('Отрицательная масса')
1118
+ result.append(dict(x=x,y=y,vx=vx,vy=vy,mass=mass,type=row[5],energy=mass*(vx*vx+vy*vy)/2))
1119
+ return result
1120
+
1121
+ @_task(27)
1122
+ def cluster_by_span(items, key, max_span):
1123
+ """Разбить по одномерному признаку key на МИНИМАЛЬНОЕ число кластеров с max(key)-min(key)<=max_span. Сортировка O(n log n), greedy от минимального значения. Это ограничение диаметра, не цепочка соседей. Пример: cluster_by_span([0,1,2,3],lambda x:x,2)==[[0,1,2],[3]]. Если требуется заданное K, проверьте число кластеров; произвольное разбиение на K не ищется."""
1124
+ if max_span<0: raise ValueError('max_span>=0')
1125
+ ordered=sorted(items,key=key); clusters=[]
1126
+ for item in ordered:
1127
+ if not clusters or key(item)-key(clusters[-1][0])>max_span: clusters.append([])
1128
+ clusters[-1].append(item)
1129
+ return clusters
1130
+
1131
+ @_task(27)
1132
+ def median_item(items, key):
1133
+ """Элемент, минимизирующий сумму абсолютных разностей одномерного признака; сортировка O(n log n). При нескольких оптимальных берёт нижнюю медиану. Пример: median_item([9,1,2],lambda x:x)==2. Для энергий: key=lambda p:p['energy']."""
1134
+ if not items: raise ValueError('Пустой набор')
1135
+ a=sorted(items,key=key)
1136
+ return a[(len(a)-1)//2]
1137
+
1138
+ @_task(27)
1139
+ def diameter(points):
1140
+ """Наибольшее расстояние между двумя точками; 0 при менее чем двух точках. Точный полный перебор O(n²), подходит отфильтрованным небольшим наборам. Пример: diameter([(0,0),(3,4)])==5."""
1141
+ return max((distance(a,b) for a,b in combinations(points,2)),default=0.0)
1142
+
1143
+ # Соответствие номеров: базовая таблица — 2026; альтернативная — проект 2027.
1144
+ # Общие алгоритмы графов относятся к №1 и новому №23, а не IPv4 №13.
1145
+ CATALOG['shortest_paths']=(1,23)
1146
+ CATALOG['count_graph_paths']=(1,23)
1147
+ CATALOG_2027={name:tuple(tasks) for name,tasks in CATALOG.items()}
1148
+ for _name in ('network_info','count_ip','common_prefix','ip_weight_counts'):
1149
+ CATALOG_2027[_name]=(10,)
1150
+ for _name in ('count_programs','count_programs_via'):
1151
+ CATALOG_2027[_name]=(13,)
1152
+ for _name in ('docx_text',): CATALOG_2027[_name]=()
1153
+ CATALOG_2027['count_occurrences']=(24,)
1154
+ CATALOG_2027['regex_matches']=(24,)
1155
+
1156
+ # Справочник функций по году экзамена.
1157
+ @_task(*range(1,28))
1158
+ def functions_for(task, year=2026):
1159
+ """Имена функций для номера task (1–27) и year=2026 либо 2027 (проект ФИПИ). Пример: functions_for(10,2027) покажет функции для IPv4, functions_for(10,2026) — поиск в тексте. Смежные/исторические алгоритмы включены как дополнительные инструменты."""
1160
+ if year not in (2026,2027) or task not in range(1,28): raise ValueError('Нужны task=1..27 и year=2026/2027')
1161
+ catalog=CATALOG if year==2026 else CATALOG_2027
1162
+ return sorted(name for name,tasks in catalog.items() if task in tasks)
1163
+
1164
+ CATALOG_2027['functions_for']=tuple(range(1,28))
1165
+
1166
+ __all__ = ['CATALOG','CATALOG_2027','__version__',*CATALOG]