1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

# теория · шаг 3 из 7

BWT: перестановка, которую можно развернуть

Коротко

Преобразование Барроуза-Уилера (BWT) само ничего не сжимает: оно переставляет символы текста. К тексту дописывается символ конца, все его циклические сдвиги сортируются, и из отсортированной таблицы берётся последний столбец. Для banana получается annb$aa. У перестановки два свойства. Она обратима: по одному этому столбцу, без всяких дополнительных данных, текст восстанавливается за линейное время. И она собирает одинаковые символы в серии, потому что ставит рядом символы, после которых в тексте идёт одно и то же. В журнале (826 символов и символ конца) до преобразования 775 серий, после - 85. Строится BWT по суффиксному массиву: bwt[i] = t[sa[i] − 1].

Если ты построишь BWT для abab, восстановишь текст по последнему столбцу и объяснишь, почему после преобразования одинаковые символы стоят рядом, - листай до «Теперь сам».

Как думаешь, можно ли вернуть текст из перестановки

Тебе дали строку annb$aa и сказали: это буквы слова, переставленные по некоторому правилу; знак $ стоял в конце. Больше ничего - ни номеров, ни таблиц.

Перестановка обычно необратима: из букв aaabnn слово не восстановить, вариантов десятки. Прежде чем читать дальше, подумай: что должно быть особенного в ПРАВИЛЕ перестановки, чтобы по результату можно было однозначно вернуть исходный порядок? И зачем вообще переставлять символы, если длина не меняется?

На что это похоже

Представь указатель к тексту, в котором выписано каждое место: текст, начиная с этого места, а на полях слева - буква, стоявшая прямо перед ним. Строки указателя отсортированы по алфавиту.

Теперь посмотри на строки, которые начинаются с he. Они стоят подряд - сортировка собрала их вместе. А на полях почти у всех одна и та же буква: t, потому что перед he в английском тексте обычно стоит t. Если прочитать сверху вниз одни поля, получатся длинные полосы одинаковых букв: буквы сгруппированы не по тому, где они стояли, а по тому, что шло ПОСЛЕ них. BWT - это и есть столбец полей.

Механизм: сдвиги, сортировка и последний столбец

К тексту дописывается символ конца $ - он не встречается в тексте и считается меньше любого символа. Циклический сдвиг - текст, прочитанный с некоторой позиции по кругу: дошли до конца - продолжаем с начала.

текст: banana$

место    сдвиг (отсортированы)     первый символ    последний символ
  0      $banana                         $                 a
  1      a$banan                         a                 n
  2      ana$ban                         a                 n
  3      anana$b                         a                 b
  4      banana$                         b                 $
  5      na$bana                         n                 a
  6      nana$ba                         n                 a

BWT - последний столбец, прочитанный сверху вниз: annb$aa.

В каждой строке последний символ - это тот, что стоит в тексте прямо ПЕРЕД первым: сдвиг читается по кругу. Строки 1, 2, 3 начинаются с a, и перед этими тремя a в тексте стоят n, n, b.

Сдвиги до символа $ - это суффиксы. Символ $ в тексте один, поэтому два сдвига обязательно различаются не позже, чем один из них дойдёт до $, и всё, что стоит после него, в сравнении уже не участвует. Значит, порядок строк таблицы - это суффиксный массив текста banana$: [6, 5, 3, 1, 0, 4, 2]. Таблицу строить не надо: символ BWT на месте i - это символ текста перед суффиксом sa[i].

Теперь обратимость. Первый столбец известен без таблицы: это те же символы, отсортированные, - $aaabnn. И работает одно наблюдение: первая a в последнем столбце и первая a в первом столбце - это одна и та же буква текста; вторая - вторая, третья - третья. Причина: строки, начинающиеся с a, упорядочены по тому, что идёт после этой a. Строки, КОНЧАЮЩИЕСЯ на a, упорядочены по своему началу - а оно и есть то, что идёт после этой a в тексте. Порядок один и тот же.

Правило

BWT текста - это символы, стоящие перед суффиксами в порядке суффиксного массива: bwt[i] = t[sa[i] − 1] (для sa[i] = 0 берётся символ конца). Длина равна длине текста с символом конца. Обращение: k-е по счёту вхождение символа c в последнем столбце соответствует k-му вхождению c в первом столбце; это соответствие строк называют LF-отображением: последний столбец принято обозначать L, первый - F. Начав со строки 0 и переходя по нему, получаем символы текста от последнего к первому. Символ конца обязан быть единственным и наименьшим - иначе восстановление неоднозначно.

Разбор: прямое преобразование и обратное

def bwt(s, konec):
    t = s + konec
    sa = suf_massiv(t)                # из главы про суффиксный массив
    out = []
    for i in sa:
        out.append(t[i - 1])          # при i = 0 это t[-1], то есть символ конца
    return "".join(out)

def obratno_bwt(L):
    n = len(L)
    kol = {}
    for c in L:
        kol[c] = kol.get(c, 0) + 1
    nach = {}                         # с какого места первого столбца идут символы c
    mesto = 0
    for c in sorted(kol.keys()):
        nach[c] = mesto
        mesto = mesto + kol[c]
    lf = [0] * n
    vstrecheno = {}
    i = 0
    while i < n:
        c = L[i]
        k = vstrecheno.get(c, 0)      # какое это по счёту c в последнем столбце
        lf[i] = nach[c] + k           # строка, которая с этого же c начинается
        vstrecheno[c] = k + 1
        i = i + 1
    out = []
    stroka = 0                        # строка 0 начинается с символа конца
    i = 0
    while i < n - 1:
        out.append(L[stroka])         # символ текста перед началом этой строки
        stroka = lf[stroka]
        i = i + 1
    out.reverse()
    return "".join(out)

print(bwt("banana", "$"))             # annb$aa
print(obratno_bwt("annb$aa"))         # banana

Проход для annb$aa. Первый столбец - $aaabnn: символ $ на месте 0, a на местах 1-3, b на месте 4, n на местах 5-6. Таблица lf: [1, 5, 6, 4, 0, 2, 3].

строка    её последний символ    почему следующая строка такая
  0              a               первая a последнего столбца → первая a первого: строка 1
  1              n               первая n → строка 5
  5              a               вторая a → строка 2
  2              n               вторая n → строка 6
  6              a               третья a → строка 3
  3              b               единственная b → строка 4

Выписано a n a n a b - текст от конца к началу. В обратном порядке: banana. Ответ на вопрос из начала урока: перестановка обратима, потому что правило - сортировка - оставляет след. Отсортированный столбец можно получить из результата, а связь между двумя столбцами восстанавливает порядок.

Разбор: что перестановка делает с сериями

Серия - один или несколько одинаковых символов подряд. Считаем серии до и после преобразования.

текст                             символов    серий до    серий после
banana$                               7           7             5
abracadabra$                         12          12             8
"abracadabra" * 10 + "$"            111         102             9
zhurnal + символ конца              827         775            85
tekst(1000, 4, 1) + "$"            1001         757           731

Третья строка - преобразование в чистом виде. BWT этого текста: a, десять r, десять d, девять a, $, десять r, десять c, сорок a, двадцать b. Десять букв r, стоящих перед десятью одинаковыми продолжениями acadabra…, собрались в одну серию, и так с каждым символом. Было 102 серии, стало девять.

Журнал: 775 серий превратились в 85. В преобразованном тексте почти в самом начале стоят сорок нулей подряд (последняя цифра кода 200 перед каждым переводом строки), сразу за ними сорок букв T (перед каждым пробелом после GET). «Меньше» для символов значит «с меньшим кодом»: у пробела код 32, у перевода строки 10, у знака $ - 36. Поэтому для журнала $ не годится, и символом конца там служит символ с кодом 0, в Python - chr(0).

Последняя строка - граница. В случайном тексте символ не зависит от того, что идёт после него, и группировать нечего: 757 серий и 731.

Сам текст при этом не стал короче ни на символ. Преобразование - подготовка: оно превращает закономерность «символ зависит от соседей», которую простые методы не видят, в закономерность «одинаковые символы стоят рядом», которую видят все. Что делать с ней дальше - в следующем уроке.

Что ломается без этого

Первое: символ конца не наименьший или встречается в тексте. Строка, начинающаяся с него, перестаёт быть нулевой, и обход стартует не оттуда. Для текста с пробелами знак $ уже не годится: пробел меньше. Форматы, которым негде взять лишний символ, вместо него хранят номер строки таблицы, в которой стоит исходный текст, - одно число на блок.

Второе: таблица сдвигов целиком. Построить n строк по n символов и отсортировать - квадратичная память: для блока в сто тысяч символов это десять гигабайт. BWT строят через суффиксный массив.

Третье: обращение сортировкой. Наивный способ развернуть BWT - n раз приписать столбец слева к таблице и отсортировать её. Это n сортировок по n строк. LF-отображение делает то же за один проход.

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

Зачем это в работе

Первый сюжет - архиватор bzip2. Он режет файл на блоки до 900 килобайт, применяет к каждому BWT и затем простые методы. На текстах он сжимает плотнее gzip - на уроках этого курса разница почти в полтора раза. Платит временем распаковки: обратное преобразование прыгает по всему блоку, и разворачивается архив в разы медленнее, чем у gzip.

Второй - поиск в сжатом тексте. LF-отображение умеет больше, чем разворачивать текст. Если уметь быстро отвечать на вопрос «сколько символов c в первых i позициях BWT», можно искать образец, проходя его справа налево, за время порядка его длины - не распаковывая текст. Такая структура называется FM-индексом, и это тот самый сжатый индекс, обещанный в главе про суффиксный массив: геном человека с ним занимает несколько гигабайт вместо двенадцати. На нём работают программы, раскладывающие прочитанные фрагменты ДНК по геному.

Третий - общий приём. Обратимое преобразование, которое не сжимает, а делает данные удобными для простого сжатия, встречается постоянно: разности соседних значений в рядах измерений, перестановка байтов чисел по разрядам в таблицах. BWT - самый сильный пример этой идеи для текста.

Теперь сам

Построй BWT для текста abab с символом конца $. Затем восстанови текст по результату.

Ответ. Суффиксы abab$ в порядке сортировки и символ перед каждым:

место    суффикс     начало    символ перед ним
  0      $              4           b
  1      ab$            2           b
  2      abab$          0           $
  3      b$             3           a
  4      bab$           1           a

BWT: bb$aa. Пять символов - столько же, сколько в тексте с символом конца; серий три вместо пяти.

Обратно. Первый столбец - $aabb: $ на месте 0, a на местах 1-2, b на местах 3-4. Таблица lf для bb$aa: первая b → 3, вторая b → 4, $ → 0, первая a → 1, вторая a → 2.

строка 0: последний символ b, это первая b  → строка 3
строка 3: последний символ a, первая a      → строка 1
строка 1: последний символ b, вторая b      → строка 4
строка 4: последний символ a, вторая a      → строка 2

Выписано b a b a, в обратном порядке - abab.

Главное

BWT - обратимая перестановка, которая сортирует символы текста по тому, что идёт после них. Раз после одинаковых контекстов обычно стоят одинаковые символы, в результате они собираются в серии: зависимость символа от соседей превращается в соседство одинаковых символов. Вернуть текст позволяет LF-отображение - k-е вхождение символа в последнем столбце и k-е в первом суть один и тот же символ текста. Само преобразование не сжимает ничего; оно делает текст лёгкой добычей для методов, которые умеют только серии и частоты.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.