Изменения

Перейти к: навигация, поиск

Оптимальное хранение словаря в алгоритме Хаффмана

13 186 байт добавлено, 16:25, 15 января 2015
Используемая память
При сжатии данных [[Алгоритм Хаффмана|алгоритмом Хаффмана ]] появляется проблема передачи дополнительной информации об оптимальном префиксном коде оптимального кода зашифрованного сообщения, который поможет получателю расшифровать его. Рассмотрим некоторые варианты решения этой задачи. {{Задача|definition =Пусть у нас есть алфавит <tex>\Sigma = \{a_1, a_2, \cdots, a_n\}</tex>, <tex>|\Sigma| = n</tex>, и код <tex>c</tex>, сопоставляющий каждому символу <tex>a_i</tex> его код <tex>c_i</tex>.Нужно придумать эффективное представление кода <tex> c </tex> в памяти.}} == Простое решение == Заметим, что <tex>\forall i: |c_i| \leqslant n</tex>. Дополним все коды до длины <tex> n </tex> нулями с конца и запишем их друг за другом. Также необходимо передать <tex> n </tex>. При условии, что <tex> n </tex> не превышает <tex> 2^{32} - 1 </tex> получаем <tex> 32 + n^2 </tex> дополнительных бит на передачу префиксного кода. Чтобы декодировать полученную информацию о коде, достаточно сначала узнать <tex>n</tex> и записать в префиксное дерево коды всех символов. Построив соответствующее дерево, можно заметить, что некоторые вершины являются единственным ребенком — такие вершины получены в результате дополнения нашего оптимального кода до <tex>n</tex> бит нулями. Все такие вершины можно просто удалить из дерева, и мы получим оптимальный префиксный код, который мы передавали. Докажем, что код который мы получили совпадает с изначальным. В результате дополнения всех кодов до длины <tex>n</tex> мы, очевидно, ничего не потеряли в структуре дерева, ведь дополнение кода нулями фактически равносильно добавлению к дереву пустых (не несущих информации) листьев и вершин. В результате удаления вершин, являющихся единственным ребенком, мы также не удалим лишние: предположим мы удалили вершину, в которой содержался какой-то код, тогда она не могла быть единственным ребенком ввиду самого алгоритма Хаффмана, ведь мы выбираем два узла с наименьшим весом и добавляем их к вновь сформированному узлу.  === Пример === Для примера возьмем слово "миссисипи". Тогда код <tex> c </tex> будет выглядеть следующим образом: {| class="wikitable"! Символ || и || м || п || с|-| Код || 0 || 100 || 101 || 11|} При помощи нашего алгоритма <tex> c </tex> будет закодирован как: <tex>0000\ 1000\ 1010\ 1100</tex> Построим префиксное дерево по полученному коду: [[Файл:Missisipi2.png|600px|Дерево Хаффмана для слова ''"миссисипи"'']] Удалив вершины, являющиеся единственными детьми у других вершин, получим зашифрованное префиксное дерево: [[Файл:Mississippi.png|400px|Дерево Хаффмана для слова ''"миссисипи"'']] == Эффективное решение == Построим префиксное дерево, соответствующее нашему коду <tex>c</tex>. Наша задача состоит из двух подзадач: передача структуры этого дерева и передача информации, по которой впоследствии можно будет понять, где какой лист находится. === Передача структуры дерева === Обойдем дерево, используя [[Обход в глубину, цвета вершин|обход в глубину]], причем будем сначала всегда спускаться в левого ребенка, а только потом — в правого. Каждый раз, проходя по ребру запишем одну из букв <tex> L </tex>, <tex> R </tex> или <tex> U </tex>, в зависимости от того, куда по ребру мы прошли (<tex>L</tex> — в левого ребенка, <tex>R</tex> — в правого ребенка, <tex>U</tex> — в родителя). Эта информация поможет нам восстановить дерево. Построим обход дерева Хаффмана для слова "миссисипи": <tex>LURLLURUURUU</tex> ==== Первая модификация ==== Заметим, что на самом деле мы можем объединить ребра типа <tex>L</tex> и <tex>R</tex> в ребро типа <tex>D</tex>, которое значит, что мы спускаемся вниз, а в которого ребенка — в левого или в правого, можно определить на месте. Модифицируем наш обход: <tex>DUDDDUDUUDUU</tex> ==== Вторая модификация ====Модифицируем значение команды <tex>U</tex>. Пусть теперь символ <tex>U</tex> значит, что мы поднимаемся к предку текущей вершины, пока мы — правый ребенок или пока не достигли корня, и если мы, остановившись, пришли из левого сына вершины, перейдем по ребру в правого ребенка. После такой модификации записывая каждый символ мы ровно по одному ребру проходим в первый раз.Конечный вариант кодировки дерева:<tex>DUDDUUU</tex>Распишем подробнее:{| class=wikitable| <tex>D</tex> || Спускаемся влево|-| <tex>U</tex> || Поднимаемся и спускаемся вправо|-| <tex>D</tex> || Спускаемся влево|-| <tex>D</tex> || Спускаемся влево|-| <tex>U</tex> || Поднимаемся и спускаемся вправо|-| <tex>U</tex> || Поднимаемся, поднимаемся и спускаемся вправо|-| <tex>U</tex> || Поднимаемся до корня|}Оценим используемое количество памяти. Так как в нашем дереве <tex> n </tex> листьев, то в нем <tex> 2 \cdot n - 2 </tex> ребер (это легко показать из алгоритма Хаффмана, в нем <tex> n - 1 </tex> итерация и на каждой в дерево добавляется по два ребра), а символов в нашей записи будет <tex> 2 \cdot n - 1 </tex>, так как на каждое ребро приходится по символу плюс последний, терминальный, <tex> U </tex>.Заметим, что терминальный <tex>U</tex> нам вообще говоря не нужен: мы всегда в конце поднимаемся в корень дерева, а значит, если все символы прочитаны — то обход закончен. Итоговая оценка памяти: <tex> 2 \cdot n - 2 </tex>. === Передача информации для восстановления листьев ===Алфавит нам будет дан изначально, не будет лишь кодов каждого символа, следовательно нам нужно просто указать, какой лист в дереве соответствует каждому символу. Занумеруем подряд все символы алфавита. Сопоставим каждому символу алфавита код фиксированной <tex>c'</tex> длины <tex> \lceil \log _2 \rceil n</tex> — его порядковый номер в алфавите, записанный в двоичной системе счисления. Все коды имеют фиксированную длину, а значит легко понять где заканчивается один, и начинается другой код. Тогда, если выписать подряд коды <tex>c'</tex> для всех символов в том порядке, в котором обход в глубину посещает соответствующие им листы, несложно восстановить, какому символу какой код <tex>c</tex> соответствует. Когда мы, в результате обхода в глубину, пришли в вершину, у которой нет детей, мы возьмем следующий переданный нам номер, посмотрим в нашем алфавите что это за символ, и присвоим текущей вершине этот символ. === Отсутствие некоторых символов в тексте ===Предположим теперь, что не все символы из алфавита были использованы в тексте, тогда возникает вопрос: получив очередной код, как узнать какому символу он принадлежит? Для решения этой проблемы поступим следующим образом: выпишем подряд коды <tex>c'</tex> в том порядке, в котором обход в глубину посещает соответствующие им листы, лишь для тех символов, что были использованы в нашем сообщении. Тогда встретив очередной символ, мы гарантированно будем знать, что он встречался в нашем сообщении. === Используемая память === В этом решении нам также придется передавать размер алфавита (<tex>32</tex> бита). Итого, задача решена с использованием <tex>n \lceil \log_2 \rceil n + 2n - 1 + 32 = n \lceil \log_2 \rceil n + 2n + 31 </tex> бит. Если же были использованы не все символы, то будет использовано <tex>k \lceil \log_2 \rceil n + 2k - 1 + 32 = k \lceil \log_2 \rceil n + 2k + 31 </tex> бит, где <tex>k</tex> — количество использованных символов. === Реализация === <span style="color:Green">// s — наша строка обхода, tree[] — дерево вершин, code — текущий код, alphabet[] — массив кодов символов, </span> <span style="color:Green">// c'[] — номера символов в порядке обхода</span> '''function''' huffman(): <span style="color:Green">//текущая вершина — корень дерева</span> curV = root '''for''' i = 0..n - 2 '''if''' s[i] == 'D' curV = tree[curV].leftChild code += '0' '''if''' curV не имеет детей alphabet[следующий номер c'].push(code) '''else''' '''while''' curV является правым ребенком и curV не корень curV = tree[curV].parent удалить из code последний символ curV = tree[curV].rightChild code += '1' '''if''' curV не имеет детей alphabet[следующий номер c'].push(code) == См. также ==*[[Алгоритм_Хаффмана_за_O(n) | Алгоритм Хаффмана за O(n)]] == Источники информации == *[http://answerstop.org/question/127217/efficient-way-of-storing-huffman-tree Answerstop.org — Efficient way of storing Huffman tree]*[http://hashcode.ru/questions/283569/алгоритм-как-хранить-дерево-хаффмана Hashcode.ru — Как хранить дерево Хаффмана?]  [[Категория: Дискретная математика и алгоритмы]] [[Категория: Алгоритмы сжатия ]]
32
правки

Навигация