Оптимизируйте с точки зрения алгоритма и реализации следующий Python‑фрагмент для подсчёта уникальных подстрок строки: def uniq_subs(s): res = set() for i in range(len(s)): for j in range(i+1,len(s)+1): res.add(s[i:j]) return res — оцените сложность, предложите более эффективные подходы и обсудите компромиссы памяти/времени

6 Окт в 10:39
11 +1
0
Ответы
1

Код, который вы привели,

def uniq_subssss:
res = set for i in rangelen(s)len(s)len(s):
for j in rangei+1,len(s)+1i+1, len(s)+1i+1,len(s)+1:
res.adds[i:j]s[i:j]s[i:j] return res

корректен, но очень неэффективен. Разберём почему и какие есть более быстрые подходы, с оценкой сложности и примерами реализации на Python.

1) Оценка сложности исходного кода

Количество порождаемых подстрок = nn+1n+1n+1/2 = Θn2n^2n2.Каждое si:ji:ji:j — срез строки, который копирует j−ij-iji символов. Суммарный объём копируемых символов = Θn3n^3n3 точноn(n+1)(n+2)/6точно n(n+1)(n+2)/6точноn(n+1)(n+2)/6.Хеширование/вставка строки в set также занимает Olengthlengthlength для каждой строки.
Итого: время Θn3n^3n3 в худшем случае; память — хранение всех уникальных подстрок занимает Θn2n^2n2 строк и Θn2n^2n2 символов в сумме.

Для даже умеренно больших n напримерn=104например n = 10^4напримерn=104 это невозможно.

2) Цель: посчитать количество различных подстрок илипростополучитьмножествоуникальныхили просто получить множество уникальныхилипростополучитьмножествоуникальных — алгоритмы лучше On2n^2n2 по времени и Onnn по памяти существуют.

Основные эффективные подходы

Суффиксный массив + LCP: построить суффиксный массив SA времяO(nlogn)впростыхреализациях;существуютO(n)алгоритмывремя O(n log n) в простых реализациях; существуют O(n) алгоритмывремяO(nlogn)впростыхреализациях;существуютO(n)алгоритмы, затем LCP КэсайКэсайКэсай за Onnn. Количество уникальных подстрок = n*n+1n+1n+1/2 − sumLCPLCPLCP.

Сложность: Onlognn log nnlogn впростомвариантессортировкойв простом варианте с сортировкойвпростомвариантессортировкой или Onnn. Память: Onnn.Подходит, если нужно только количество, и если реализовать SA аккуратно вPythonSAO(nlogn)обычноприемлемв Python SA O(n log n) обычно приемлемвPythonSAO(nlogn)обычноприемлем.

Суффиксный автомат SuffixAutomaton,SAMSuffix Automaton, SAMSuffixAutomaton,SAM: строится за Onnn и позволяет посчитать число различных подстрок как сумма по вершинам: sumlen[v]−len[link[v]]len[v] − len[link[v]]len[v]len[link[v]].

Сложность: Onnn время, Onnn память числосостояний≤2n−1число состояний ≤ 2n−1числосостояний2n1. В Python реализуется компактно.Очень удобен для подсчёта, поиска k‑й лексикографической подстроки и т. п.

Суффиксное дерево UkkonenUkkonenUkkonen: Onnn время, Onnn память, но сложнее в реализации на Python.

Хеши rollinghashrolling hashrollinghash: можно посчитать хеши всех подстрок фиксированной длины L за Onnn и получить число различных подстрок длины L. Но суммирование по всем L даёт On2n^2n2 время. Можно комбинировать с бинпоиском — но для общего подсчёта уникальных подстрок всё равно менее эффективно, чем SAM/SA.

3) Рекомендации

Если нужно только количество различных подстрок — используйте SAM или SA+LCP.Если нужно сам набор всех уникальных подстрок перечислитьперечислитьперечислить, то размер результата может быть Θn2n^2n2 и это физически дорого; в этом случае подумайте, действительно ли нужно перечисление, или достаточно статистики/ограничения по длине.В Python обычно проще и быстрее писать SAM для задач подсчёта; SA с сортировкой doubling+tuplesortdoubling + tuple sortdoubling+tuplesort тоже часто хорош и понятен.

4) Код: суффиксный автомат быстрыйикомпактныйбыстрый и компактныйбыстрыйикомпактный. Возвращает количество различных подстрок.

def count_distinct_substrings_samsss:

Возвращает количество различных подстрок строки ssa_trans = # список словарей переходов
sa_link = sa_len =
sa_trans.append{} # state 0
sa_link.append−1-11 sa_len.append000 last = 0
for ch in s:
cur = lensatranssa_transsat rans sa_trans.append{} sa_len.appendsalen[last]+1sa_len[last] + 1sal en[last]+1 sa_link.append000
p = last
while p != -1 and ch not in sa_transppp:
sa_transpppchchch = cur
p = sa_linkppp if p == -1:
sa_linkcurcurcur = 0
else:
q = sa_transpppchchch if sa_lenppp + 1 == sa_lenqqq:
sa_linkcurcurcur = q
else:
# clone q
clone = lensatranssa_transsat rans sa_trans.appendsatrans[q].copy()sa_trans[q].copy()sat rans[q].copy() sa_len.appendsalen[p]+1sa_len[p] + 1sal en[p]+1 sa_link.appendsalink[q]sa_link[q]sal ink[q] while p != -1 and sa_transppp.getchchch == q:
sa_transpppchchch = clone
p = sa_linkppp sa_linkqqq = sa_linkcurcurcur = clone
last = cur
# число различных подстрок = сумма len[v]−len[link[v]]len[v] - len[link[v]]len[v]len[link[v]] по всем состояниям v>0
res = 0
for v in range1,len(satrans)1, len(sa_trans)1,len(sat rans):
res += sa_lenvvv - sa_lensalink[v]sa_link[v]sal ink[v] return res

Сложность: Onnn в среднем и худшем для реализаций SAM; память Onnn состояний каждое—dictпереходов,попамятинемного«дороже»чеммассивы,новсёжелинейнокаждое — dict переходов, по памяти немного «дороже» чем массивы, но всё же линейнокаждоеdictпереходов,попамятинемного«дороже»чеммассивы,новсёжелинейно.

5) Код: суффиксный массив + LCP простаяреализацияO(nlogn)из−засортировкипростая реализация O(n log n) из-за сортировкипростаяреализацияO(nlogn)иззасортировки

def count_distinct_substrings_sasss:
n = lensss

постройка SA методом "doubling"sa = listrange(n)range(n)range(n) rank_ = ord(c)forcinsord(c) for c in sord(c)forcins + −1-11 k = 1
while k <= n:
sa.sort(key=lambda i: (rank_[i], rank_[i + k] if i + k < n else -1))
tmp = 000 * n
for i in range1,n1, n1,n:
prev, cur = sai−1i-1i1, saiii tmpcurcurcur = tmpprevprevprev + ( (rank_[prev], rank_[prev+k] if prev+k<n else -1) != (rank_[cur], rank_[cur+k] if cur+k<n else -1) )
rank_:n:n:n = tmp
k <<= 1
# Kasai LCP
rank_sa = 000*n
for i, p in enumeratesasasa:
rank_sappp = i
lcp = 000*n−1n-1n1 h = 0
for i in rangennn:
r = rank_saiii if r == 0:
continue
j = sar−1r-1r1 while i+h < n and j+h < n and si+hi+hi+h == sj+hj+hj+h:
h += 1
lcpr−1r-1r1 = h
if h:
h -= 1
total = n*n+1n+1n+1//2 - sumlcplcplcp return total

Сложность: Onlognn log nnlogn из-за сортировки с ключем; LCP — Onnn. Память Onnn.

6) Компромиссы памяти/времени

Naive срезы+setсрезы + setсрезы+set: очень дорого по времени Θ(n3)Θ(n^3)Θ(n3) и памяти Θ(n2)Θ(n^2)Θ(n2), но очень прост и достаточен для маленьких строк (n < ~500).Rolling hash: можно хранить только хеши целыечислацелые числацелыечисла вместо строк, что экономит память по сравнению со строками, но вычислительно всё ещё Θn2n^2n2 по времени, и нужны меры против коллизий двойнойхешдвойной хешдвойнойхеш.SAM: лучшее сочетание — линейное время и линейная память, но реализация на Python использует словари для переходов — это добавляет константные факторы по памяти/времени.SA+LCP: чуть более простая структура по памяти массивы/спискимассивы/спискимассивы/списки, быстрее в Python для некоторых входов используетсортировкусбыстрымиC‑реализациямииспользует сортировку с быстрыми C‑реализациямииспользуетсортировкусбыстрымиCреализациями, но асимптотика Onlognn log nnlogn в простых реализациях.

7) Практические советы по Python

Если n ≤ ~2000, исходный подход можно использовать нолучшезаменитьстрокинапредставленияввидесрезовпамяти—вPythonэтонереализуемовстроенноно лучше заменить строки на представления в виде срезов памяти — в Python это не реализуемо встроеннонолучшезаменитьстрокинапредставленияввидесрезовпамятивPythonэтонереализуемовстроенно.Для n ~ 10^4–10^6 используйте SAM или хорошо оптимизированный SA иливызывайтеC/C++реализациюили вызывайте C/C++ реализациюиливызывайтеC/C++реализацию.Если готовы потреблять больше памяти, храните в set не строки, а 64‑бит хеши двойноймодульныйхешдвойной модульный хешдвойноймодульныйхеш, но помните о рисках коллизий.

8) Резюме

Исходный алгоритм: Θn3n^3n3 время, Θn2n^2n2 память — неприемлем для больших n.Лучший выбор для подсчёта количества различных подстрок: суффиксный автомат O(n)время/памятьO(n) время/памятьO(n)время/память или суффиксный массив + LCP O(nlogn)простаяреализацияO(n log n) простая реализацияO(nlogn)простаяреализация.Для перечисления всех уникальных подстрок размер вывода сам по себе может быть Θn2n^2n2 — это ограничивает возможную оптимизацию.

Если нужно, могу:

Прислать готовую и оптимизированную реализацию SAM/SA+LCP, тесты и бенчмарки на ваших входных данных.Написать вариант, который возвращает само множество уникальных подстрок спредупредениемобобъёмерезультатас предупредением об объёме результатаспредупредениемобобъёмерезультата или вариант с хранением только хешей.
6 Окт в 11:22
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир