Генерация кода Хаффмана, который никогда не создает строку «00» при кодировании

Я пытаюсь использовать кодирование Хаффмана для создания оптимального кодирования набора символов. Однако на кодировки накладывается ограничение, так что ни одна кодировка не содержит строку «00». Например, кодировка «A» = «0» и «B» = «10» не будет удовлетворять ограничению, потому что строка «BA» кодируется как «100», которая содержит подстроку «00». Это означает, что кодовые слова также не могут содержать строку «00». Например, кодировка «A» = «1», B = «00» и C = «01» не будет удовлетворять этому ограничению, потому что кодировка «B» всегда приводит к появлению в кодировке «00».

Я попытался изменить алгоритм кодирования Хаффмана, найденный в Википедии:

  1. Создайте конечный узел для каждого символа и добавьте его в очередь приоритетов.
  2. Пока в очереди больше одного узла:
    1. Удалите из очереди два узла с наивысшим приоритетом (самой низкой вероятностью).
      • Если оба узла не являются конечными узлами, выберите узел с наивысшим приоритетом и конечный узел с наивысшим приоритетом. Это гарантирует, что по крайней мере один из выбранных узлов является конечным узлом.
    2. Создайте новый внутренний узел с этими двумя узлами в качестве дочерних и с вероятностью, равной сумме вероятностей двух узлов.
      • Если один узел не является конечным узлом, сделайте этот узел правым дочерним элементом нового внутреннего узла (сделав его «1» при кодировании). Это позволяет избежать создания подстроки «00».
    3. Добавьте новый узел в очередь.
  3. Оставшийся узел является корневым узлом, и дерево завершено.
  4. Добавьте «1» в начало всех кодов, чтобы избежать подстроки «00» при кодировании двух соседних символов.

Также возможен случай, когда в очереди остались только два узла, оба неконечные узлы. Я не знаю, как решить эту проблему. В противном случае я считаю, что это создает код, удовлетворяющий ограничению, но я не уверен, что он оптимален, и я хотел бы быть уверенным, что это так.

Жадный алгоритм, подобный Хаффману, для этой задачи невозможен, потому что не всегда два узла с наименьшей вероятностью в оптимальном дереве на самом деле являются братьями и сестрами. (Я добавил контрпример в комментарий к принятому решению.) Из любопытства, где вы столкнулись с этой проблемой? Мне кажется, это слишком сложно для конкурентного сайта по программированию или курса бакалавриата; люди трудятся над этим уже более шестидесяти лет, и, насколько я могу судить, он до сих пор не очень хорошо понят.

rici 17.02.2023 22:22
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Инструменты для веб-скрапинга с открытым исходным кодом: Python Developer Toolkit
Веб-скрейпинг, как мы все знаем, это дисциплина, которая развивается с течением времени. Появляются все более сложные средства борьбы с ботами, а...
Калькулятор CGPA 12 для семестра
Калькулятор CGPA 12 для семестра
Чтобы запустить этот код и рассчитать CGPA, необходимо сохранить код как HTML-файл, а затем открыть его в веб-браузере. Для этого выполните следующие...
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
ONLBest Online HTML CSS JAVASCRIPT Training In INDIA 2023
О тренинге HTML JavaScript :HTML (язык гипертекстовой разметки) и CSS (каскадные таблицы стилей) - две основные технологии для создания веб-страниц....
Как собрать/развернуть часть вашего приложения Angular
Как собрать/развернуть часть вашего приложения Angular
Вам когда-нибудь требовалось собрать/развернуть только часть вашего приложения Angular или, возможно, скрыть некоторые маршруты в определенных средах?
Запуск PHP на IIS без использования программы установки веб-платформы
Запуск PHP на IIS без использования программы установки веб-платформы
Установщик веб-платформы, предлагаемый компанией Microsoft, перестанет работать 31 декабря 2022 года. Его закрытие привело к тому, что мы не можем...
Оптимизация React Context шаг за шагом в 4 примерах
Оптимизация React Context шаг за шагом в 4 примерах
При использовании компонентов React в сочетании с Context вы можете оптимизировать рендеринг, обернув ваш компонент React в React.memo сразу после...
2
1
117
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Думаю, я бы начал с правила, согласно которому за любым «0» в коде должна следовать «1». Это удовлетворяет ограничению, согласно которому коды не могут содержать «00». Это также позволяет избежать проблемы создания подстроки «00» при кодировании двух соседних символов.

Результирующее дерево кода показано ниже, где

  • узлы в областях, заштрихованных красным, представляют собой коды, содержащие «00».
  • узлы, содержащие красный X, - это коды, оканчивающиеся на «0».
  • зеленые узлы - доступные действительные коды

Обратите внимание, что, поскольку код Хаффмана является кодом без префиксов, выбор одного из допустимых кодов исключает все потомки этого узла. Например, выбор кода «01» исключает все остальные узлы в левой части дерева. Другими словами, выбор «01» делает «01» листом и разрывает два соединения ниже «01».

Также обратите внимание, что левый дочерний узел внутреннего узла будет иметь более длинный код, чем правый дочерний узел, поэтому дочерний элемент с меньшей вероятностью должен быть подключен слева. Это, безусловно, необходимо. Это остается в качестве упражнения, чтобы доказать, что этого достаточно. (Если недостаточно, то упражнение состоит в том, чтобы придумать оптимальный алгоритм назначения.)

Это не оптимально. Контрпример с пятью символами (и частотами): {a:4, b:4, c:4, d:3, e:3}. В оптимальном дереве d и e не братья и сестры; конструкция Хаффмана вынуждает их быть такими, и поэтому один из них оказывается с более коротким кодом, чем один из более высокочастотных символов. (В данном случае она близка к оптимальной, но я уверен, что есть контрпримеры с большим расхождением. Я не тратил много времени на поиски.) Это интересная проблема, и она довольно хорошо изучена. Afaics, оптимальный алгоритм для этого конкретного случая — O(n^2); показатель степени зависит от большей длины кода.

rici 17.02.2023 19:07

@rici Вы делаете вывод об алгоритме присваивания. Я явно не указывал такой алгоритм. Я лишь отметил, что если за каждым 0 следует 1, то требования выполняются, а доступные коды отображаются зеленым цветом. В конечном итоге целевая функция, которую необходимо минимизировать при назначении кода, равна sum(P[i] * L[i]), где P[i] — вероятность символа i, а L[i] — длина кода, присвоенного символу i. Итак, чтобы опровергнуть мой ответ, вам нужно показать, что существует схема кодирования, которая соответствует требованиям и лучше, чем кодирование с 01 и 1 в качестве кодовых единиц.

user3386109 17.02.2023 21:26

@rici С другой стороны, если вы согласны с тем, что использование кодовых единиц 01 и 1 является оптимальным, и вы знаете оптимальный алгоритм назначения, то вам решать, хотите ли вы поделиться этим с миром.

user3386109 17.02.2023 21:28

Я не пытался опровергнуть ваш ответ, просто чтобы предоставить некоторую информацию о достаточности упорядочения вероятностей в одном внутреннем узле. (Ваше «упражнение».) Я не могу сказать, что знаю, как решить задачу о присваивании, но я знаю, что существует решение O(n²) для простейшей задачи о неравных весах с весами 1 и 2 (в основном DP, но с использованием Алгоритм SMAWK для уменьшения пространства поиска). Я еще не закончил читать статьи, и это не простой алгоритм. Я мог бы сделать удар по нему на следующей неделе. Я нашел противоположный пример, о котором упоминал, когда пытался его понять.

rici 17.02.2023 21:49

@rici А, да, понятно. Спасибо за разъяснение. Я взглянул на статью в Википедии для SMAWK. Статья была недостаточно подробной, чтобы я мог адаптироваться к этой проблеме. Поэтому я с нетерпением жду вашего объяснения.

user3386109 17.02.2023 22:13

Самый простой способ — вообще не связываться с кодом Хаффмана. Вместо этого выполните постобработку вывода.

Мы начнем с простой битовой начинки. При кодировании возьмите закодированный битовый поток и всякий раз, когда есть 0, вставьте 1 после него. В конце декодирования всякий раз, когда вы видите 0, удалите следующий бит (который будет вставленным 1). Затем выполните обычное декодирование Хаффмана.

Это не оптимально, но отклонение от оптимальности ограничено. Вы можете уменьшить влияние наполнения битами, поменяв местами ветви в каждом узле по мере необходимости, чтобы поместить более низкие вероятности или веса на 0 сторонах.

Это простое вложение битов увеличивает входные данные в среднем в 1,5 раза.

Итак, насколько близка эта простая битовая начинка к оптимальной? Оказывается, количество возможных n-битных паттернов, в которых не встречаются два 0 бита подряд, равно Fn+2, где Fn — nth число Фибоначчи. С такой последовательностью мы можем закодировать не более log2(Fn+2) бит. Тогда оптимальный коэффициент расширения n бит равен n / log2(Fn+2). В пределе больших n это 1 / log2(𝜑), где 𝜑 — золотое сечение. Этот оптимальный коэффициент расширения равен 1,44042.

Так что 1,5 от простой битовой начинки на самом деле не так уж и плохо. Это в пределах 4% от оптимального.

Но мы можем сделать лучше.

Мы можем использовать последовательность Фибоначчи, которая представляет количество возможных закодированных значений для каждого бита, добавленного к последовательности без повторения 0s, для кодирования входных битов. Мы показываем такой подход ниже, хотя для удобства мы избегаем повторяющихся 1 вместо повторяющихся 0. (Просто инвертируйте вывод, чтобы избежать повторения 0s.) Вот пример кода C, который выполняет кодирование и декодирование для ввода и вывода фиксированного размера:

typedef unsigned __int128 u128_t;

// Encode 87 bits to a 125-bit sequence that does not have two 1 bits in a row
// anywhere. Note that if such sequences are concatenated and the high bit of
// the 125 is a 1, then a 0 bit needs to be appended to make it 126 bits. This
// will occur 38.2% of the time (1 / goldenratio^2). The final expansion ratio
// of this encoding is then 125.382 / 87 = 1.44117. The theoretical optimum
// ratio is 1 / lg(goldenratio) = 1.44042. This encoding gets within 0.05% of
// optimal.
u128_t encode87to125(u128_t n) {
    n &= ((u128_t)1 << 87) - 1;
    u128_t e = 0;

    // Fibonacci numbers 126 and 125. (gcc and clang do not support 128-bit
    // literals, so these are assembled, which will occur at compile time.)
    u128_t fn = ((u128_t)0x4f88f2 << 64) | 0x877183a413128aa8u,
           fnm1 = ((u128_t)0x3127c1 << 64) | 0xed0f4dff88ba1575u;
    for (;;) {
        // Encode one bit.
        e <<= 1;
        if (n >= fn) {
            e |= 1;
            n -= fn;
        }

        if (fn == 1)
            // Done when the start of the Fibonacci sequence (1, 1) reached.
            break;

        // Compute the Fibonacci number that precedes fnm1, and move fn and
        // fnm1 both down one in the sequence.
        u128_t fnm2 = fn - fnm1;
        fn = fnm1;
        fnm1 = fnm2;
    }
    return e;
}

// Decode a 125-bit value encoded by encode87to125() back to the orignal 87-bit
// value.
u128_t decode125to87(u128_t e) {
    // Start at the beginning of the Fibonacci sequence (1, 1).
    u128_t d = 0, fn = 1, fnm1 = 1;
    for (;;) {
        // Decode the low bit of e.
        if (e & 1)
            d += fn;
        e >>= 1;

        if (e == 0)
            // Done when there are no more 1 bits in e, since nothing more will
            // be added to d.
            break;

        // Advance fnm1 and fn up one spot in the Fibonacci sequence.
        u128_t fnp1 = fn + fnm1;
        fnm1 = fn;
        fn = fnp1;
    }
    return d;
}

Затем ввод кодируется по 87 бит за раз, а вывод составляет 125 или 126 бит для каждого входного блока, последнее, если 125 бит имеет бит 1 в верхней позиции, и в этом случае 0 необходимо заполнить.

Значения 87 и 125 были выбраны, поскольку они являются наиболее эффективным набором, позволяющим уместить выходные данные в 128 бит. Это дает коэффициент расширения 1,44117, в пределах 0,05% от оптимального. Возможны многие другие варианты. Выходные данные кодируются побитно и декодируются побитно, поэтому нет необходимости накапливать их в виде целого числа. Тогда мы могли бы перейти к 112 битам, закодированным в 161 бит. Или мы могли бы ограничиться 64-битной арифметикой и преобразовать 62 бита в 89 бит (в пределах 0,09% от оптимального).

В конце битовой последовательности оставшиеся биты могут быть расширены до 87 битов со старшими нулями, и тогда закодированные биты будут иметь старшие нули, которые не нужно отправлять. При декодировании заполните последние биты 125 старшими 0s. Если вы не знаете, сколько бит ожидать при декодировании, добавьте к входным данным один старший бит 1 перед кодированием, за которым следуют старшие биты 0. Затем при декодировании просканируйте с конца через 0 биты, чтобы добраться до первого 1 бита, а затем отбросьте все это.

Этот подход к постобработке для удовлетворения ограничений на битовый поток, возможно, предпочтительнее любой попытки изменить код Хаффмана, чтобы он был оптимальным для букв разного веса. Он не только позволяет использовать быстрые и хорошо протестированные реализации алгоритма Хаффмана как есть, но также будет работать с любой схемой энтропийного кодирования, будь то алгоритм Хаффмана, арифметические или асимметричные системы счисления.

Да, ты прав. Я подсчитал, сколько битовых последовательностей останется при удалении всех тех, где где-либо есть 00. (Оказывается, это число Фибоначчи.) В результате в пределе вам потребуется логарифм по основанию 2 золотого сечения, равный примерно 1,44, умноженный на n бит, чтобы представить информацию в виде неограниченной последовательности из n битов. Расширение, полученное в результате простого заполнения битами, которое я предложил, равно 1,5. Так что битовая начинка на самом деле не оптимальна.

Mark Adler 17.02.2023 01:06

Есть способ оптимально закодировать это, но места для этого комментария слишком мало, чтобы описать его. :-)

Mark Adler 17.02.2023 01:09

Согласитесь, это решение обеспечивает хороший компромисс между простотой и оптимальностью.

user3386109 17.02.2023 01:27

Чем это отличается от использования 01 и 1 в качестве единиц кода со стандартным алгоритмом Хаффмана (который, если я правильно понимаю, предлагает @user3386109)?

rici 17.02.2023 19:10

@rici Я удалил свои комментарии после того, как этот ответ был обновлен. Полагаю, мне следовало оставить контрпример, показывающий, что заполнение единицами неоптимально. Рассмотрим сообщение из 8 уникальных символов (например, abcdefgh), то есть 8 символов с равной вероятностью. Стандартный алгоритм Хаффмана присваивает каждому символу 3-битный код и кодирует сообщение 24-битным кодом. После заполнения единицами размер сообщения составляет 36 бит. Здесь показано лучшее назначение кода. В этом коде пять символов кодируются 4 битами, а три символа кодируются 5 битами. Общий размер сообщения составляет 35 бит.

user3386109 17.02.2023 21:09

@rici Я не думаю, что это предложил пользователь 3386109, но да, это то же самое, что использовать алгоритм Хаффмана и испускать 01 и 1 вместо 0 и 1. Затем алгоритм Хаффмана рассматривает два письма как имеющие одинаковую стоимость, несмотря на то, что одно из них стоит в два раза больше, чем другое.

Mark Adler 18.02.2023 03:38

@user3386109 user3386109 Я добавил почти оптимальный подход к ответу, который, как я сказал, слишком мал, чтобы поместиться в комментарии.

Mark Adler 21.02.2023 04:22

Также дайте знать @rici.

Mark Adler 21.02.2023 04:23

Интересный. Должен признаться, что я не совсем понимаю, как это работает. Я должен буду изучить это немного позже. Дал бы вам еще +1, если бы мог.

user3386109 21.02.2023 17:43

Другие вопросы по теме

Похожие вопросы