Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/18nov25/file_haffman.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from haffman import decode, encode


def bits_to_bytes(bits: str) -> tuple[bytes, int]:
padding = (8 - len(bits) % 8) % 8
# заполняем до нужного количества нулей
bits += "0" * padding
data = bytearray()
for i in range(0, len(bits), 8):
data.append(int(bits[i:i+8], 2))
return bytes(data), padding


def bytes_to_bits(data: bytes, padding: int) -> str:
bits = "".join(f"{b:08b}" for b in data)
return bits[:-padding] if padding else bits


def compress_file(source_path: str, destination_path: str) -> None:
with open(source_path, encoding="utf-8") as f:
text = f.read()
encoded, table = encode(text)
# сохраняем таблицу, зашифрованную в utf8, в заголовок файла
print(table)
header = "\n-".join(f"{k}\t{v}" for k, v in table.items()).encode("utf-8")
data, padding = bits_to_bytes(encoded)
with open(destination_path, "wb") as f:
# сохраняем длину заголовка и сам заголовок
f.write(len(header).to_bytes(4, "big"))
f.write(header)
# сохраняем количество добавленных нулей
f.write(bytes([padding]))
f.write(data)


def decompress_file(source_path: str, destination_path: str) -> None:
with open(source_path, "rb") as f:
header_len = int.from_bytes(f.read(4), "big")
header = f.read(header_len).decode("utf-8")
table = {}
for line in header.split("\n-"):
print(line)
ch, code = line.split("\t")
table[ch] = code
padding = f.read(1)[0]
data = f.read()
bits = bytes_to_bits(data, padding)
text = decode(bits, table)
with open(destination_path, "w", encoding="utf-8") as f:
f.write(text)
56 changes: 56 additions & 0 deletions src/18nov25/haffman.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import heapq
from collections import Counter


class Node:
def __init__(self, freq, char=None, left=None, right=None):
self.freq = freq
self.char = char
self.left = left
self.right = right

def __lt__(self, other):
return self.freq < other.freq


def build_tree(freq: dict[str, int]) -> Node:
heap = [Node(f, c) for c, f in freq.items()]
heapq.heapify(heap)
while len(heap) > 1:
a = heapq.heappop(heap)
b = heapq.heappop(heap)
heapq.heappush(heap, Node(a.freq + b.freq, left=a, right=b))
return heap[0]


def build_table(node: Node, prefix="", table=None) -> dict[str, str]:
if table is None:
table = {}
if node.char is not None:
table[node.char] = prefix or "0"
return table
build_table(node.left, prefix + "0", table)
build_table(node.right, prefix + "1", table)
return table


def encode(msg: str) -> tuple[str, dict[str, str]]:
# считаем частоту появления подстроки
freq = Counter(msg)
tree = build_tree(freq)
table = build_table(tree)
encoded = "".join(table[ch] for ch in msg)
return encoded, table


def decode(encoded: str, table: dict[str, str]) -> str:
reverse = {v: k for k, v in table.items()}
result = []
buf = ""
for bit in encoded:
buf += bit
if buf in reverse:
result.append(reverse[buf])
buf = ""
return "".join(result)

5 changes: 5 additions & 0 deletions src/18nov25/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from file_haffman import compress_file, decompress_file

compress_file("sample_text.txt", "sample.haffmancompressed")
decompress_file("sample.haffmancompressed", "sample_text2.txt")
# 8 мб -> 6 мб
92 changes: 92 additions & 0 deletions src/18nov25/sample_text.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
Есть текст из символов $a_{1}, ..., a_{n}$ с вероятностью появления каждого символа $p_{1}, ..., p_{n}$. Как закодировать их последовательностью из 0 и 1 переменной длины $s_{i}$, чтобы $\sum p_{i} s_{i}$ (математическое ожидание длины текста) была минимальной. Притом, чтобы не нужен был разделитель, должно быть префиксное кодирование (никакой код не является начальной частью другого), то есть $\sum_{i = 1}^{n} \frac{1}{2^{s_{i}}} \le 1$. Задачу решает алгоритм Хаффмена

### Алгоритм Хаффмена

Символы: a - 0.07, b - 0.21, c - 0.17, d - 0.35, f - 0.2
Склеиваем два самых редких a и c: ac - 0.24
Затем с наименее редким b и f: bf - 0.41
acd - 0.59
Далее приписываем 0 acd и 1 bf
Приписываем к b и f 0 и 1: b=10, f=11
ac=00, d=01
a=000, c=001
Итого они шифруются: a - 000, b - 10, c - 001, d - 01, f - 11

*(Л 1)* $p_{1} \ge ... \ge p_{n} \rArr s_{1} \le ... \le s_{n}$ дает $\min p s$

*(Л 2)* $p_{1} \ge ... \ge p_{n}, s_{1} \le ... \le s_{n} \rArr s_{n - 1} = s_{n}$ даст минимальный исход

<aside>

Пусть не так: $s_{n-1} < s_{n}$

Тогда код $n$ можно обрезать до длины кода $n - 1$

Они не будут равны по префиксному кодированию, но длина уменьшится

</aside>

*(Л 3)* Определим задачу $\mathcal{P}_{n}$, где вероятности $p_{1} ... p_{n}$ упорядочены $p_{1} \ge ... \ge p_{n}$. Пусть для $\mathcal{P}_{n}$ найдены длины кодов $s_{1} \le ... \le s_{n}$ и $\sigma(\mathcal{P}_{n}) = \sum_{i = 1}^{n} p_{i} s_{i}$. Сформулируем задачу $\mathcal{P}_{n-1}$, являющееся порождением $\mathcal{P}_{n}$ в алг. Хаффмена $p’ = p_{n-1} + p_{n}$. В $\mathcal{P}_{n-1}$ найдем $s_{1}’ \le s_{2}’ \le ... \le s_{n}’$ и $\sigma(\mathcal{P}_{n-1})$
Если $s_{1}', s_{2}', ..., s_{n}'$ дает оптимальное решение $\mathcal{P}_{n-1}$, то оптимальное решение задачи $\mathcal{P}$ с $p’$ и $s’$ тогда, когда $a_{n-1}$ сопоставлено s’0, $a_{n}$ - s’1

<aside>

$\sigma(\mathcal{P}_{n-1}) = \sum_{i \ne n - 1, \ i \ne n} p_{i} s_{i} + p' s'$ $= \sum_{i \ne n - 1, \ i \ne n} p_{i} s_{i} + p_{n-1} s’ + p_{n} s’$

$\sigma(\mathcal{P}_{n}) = \sum_{i \ne n - 1, \ i \ne n} p_{i} s_{i} + p_{n-1} (s’ + 1) + p_{n} (s’ + 1)$

$\sigma(\mathcal{P}_{n}) - \sigma(\mathcal{P}_{n-1}) = p_{n-1} + p_{n}$

Предположим, что изложенное реш. не оптимально

Тогда $\exists$ оптимальное решение $\mathcal{P}_{n}$ такое, что $\sigma < \sigma(\mathcal{P}_{n}) = \sigma(\mathcal{P}_{n-1}) + p_{n} + p_{n-1}$

Длины $t_{1} \le ... \le t_{n}$. $\sigma = \sum_{i = 1}^{n} p_{i} t_{i}$

$t_{n-1} = t_{n}$ по Л2. Склеим символы в один, а код будет $t_{n}$ без последнего бита.

По префиксности такой существует, и будет решением для $\mathcal{P}_{n-1}$

$\sigma(\mathcal{P}_{n-1}) > \sigma_{n-1}$, противоречие с тем, что решение $\mathcal{P}_{n-1}$ опт.

Эта лемма доказывает алгоритм Хаффмена

</aside>

### Алгоритм Зива-Лемпеля

Исходная строка aabbbababdbadabbaab$
1) Если подстроки еще не было, то записываем 0 и этот символ aabbbababdbadabbaab$ 0a
2) Если подстрока была, то пишем ее номер и следующий символ aabbbababdbadabbaab$ 1b
3) aabbbababdbadabbaab$ 0b
4) aabbbababdbadabbaab$ 3a
5) aabbbababdbadabbaab$ 4b
6) aabbbababdbadabbaab$ 0d
7) aabbbababdbadabbaab$ 4d
8) aabbbababdbadabbaab$ 2b
9) aabbbababdbadabbaab$ 1a
10) aabbbababdbadabbaab$ 3$

Восстановление из 0a1b0b3a4b0d4d2b1a3$. Если 0, то вставляем сам символ, иначе вставляем подстроку, на которую ссылается и его

### Метод Барроуза-Уиллера

Суффиксное дерево для строки `карл_у_клары_украл$`

Запишем символы в алфавитном порядке `$_аклруы`

Представляем в виде дерева все возможные суффиксы в лексикографическом порядке

![](https://i.imgur.com/tmE8WZx.png)

Первое преобразование. Для каждого суффикса выпишем предыдущий символ по порядку суффиксного дерева `лулыркл$_уарккаа__р` . После этого многие символы склеились

Второе преобразование. Записываем s₂ без повторов `луырк$_а`. Способом MTF: в первом столбце таблицы элементы s₂; сверху вниз пишем 0, если символа нет в 3 колонке, и добавляем его в начало строки из 3 колонки, если есть, то пишем в 2 его порядковый номер и переставляем на первое место. Получаем r `0020004007077131514`

| л | у | л | ы | р | к | л | $ | _ | у | а | р | к | к | а | а | _ | _ | р |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 0 | 2 | 0 | 0 | 0 | 4 | 0 | 0 | 7 | 0 | 7 | 7 | 1 | 3 | 1 | 5 | 1 | 4 |
| л | ул | лу | ылу | рылу | крылу | лкрыу | $лкрыу | _$лкрыу | у_$лкры | ау_$лкры | рау_$лкы | крау_$лы | крау_$лы | акру_$лы | акру_$лы | _акру$лы | _акру$лы | р_аку$лы |

Обратный из s₃ и r. Восстанавливаем ту же таблицу Move-To-Front. Если 0, ко вписываем следующий символ s₃, иначе символ по индексу.
Обратный из s₂. Выпишем в алфавитном порядке `$_аклруы`. Массив a будет содержать количество вхождений в s₃ `1, 3, 3, 3, 3, 3, 2, 1`. Строим массив p по принципу p[1] = a[1], p[i] = p[i-1] + a[i-1] `1, 2, 5, 8, 11, 14, 17, 19` . Массив q заполняем соответствующим для символа s₂ число из p, и увеличиваем его на 1 `11, 17, 12, 19, 14, 8, 13, 1, 2, 18, 5, 15, 9, 10, 6, 7, 3, 4, 16`. После от символа конца строки вставляем их в начало финальной строки, соответствующий ему номер из q показывает порядковый номер символа s₂, который надо взять следующим