11. Компресиране - Хъфман

** ХХХII Републиканска студентска олимпиада по програмиране
https://www.hackerrank.com/openbcpc

Задача А. МОДУЛНО СОРТИРАНЕ
Задача E. ИЗОБРАЖЕНИЕ (тестове)
Задача F. ФЕРИБОТНИ ЛИНИИ (тестове)

Кодиране - декодиране
Компресиране - декомпресиране

Без загуба на информация
Със загуба на информация (закръгляване)

Кодиране на редици
Статистически (вероятностни) методи
Речникови методи
Вълнови методи
Фрактални методи
Програмиране = ++Алгоритми; (Programming = ++Algorithms;) Адаптивно компресиране

**Премахване на нулите [10.3.1]
Задача: Кодиране на числови редици с много поредни нули.
Алгоритъм: Дългите последователности от нули да се заменят с кодираща двойка, състояща се от специален символ, указващ наличието на компресия, последван от брояч, указващ броя последователни нули. Процесът на декодиране е тривиален и се свежда до просто заместване на кодиращата двойка със съответния брой нули.
Пример:
12 17 86 93 0 0 1 2 0 0 0 0 0 19 20 0 8 3 12 0 0 0 6

След кодиране със специален символ 0 се получава:
12 17 86 93 0 1 1 2 0 4 19 20 0 0 8 3 12 0 2 6
Използва се 1 бийт за кодиране на едно число.

** Кодиране на редици [10.3.9]
Задача: Кодиране на редици от символи (букви) с много поредни еднакви символи.
Алгоритъм: Заменяиме всяка редица от еднакви символи с един-единствен негов екземпляр, предшестван от число, указващо броя на срещанията му.
Пример: BBBBBBBCCCCCAAAAAAAAAAAAACCCABABBCCAAAAAAAAAAAAAAAAAAAA
След кодиране: 7B5C13A3CABABB2C20A

Задача: Кодиране на редици от латински букви с поредни еднакви букви.
Алгоритъм: Избира се произволен символ от азбуката за escape (ESC) символ. Всяка негова поява означава началото на нова "ESC редица" -  следващите два символа се интерпретират като двойка от вида XY (брой_срещания,символ). Броят срещания на Y се дава от поредния номер на X в азбуката. Пример:  Нека Z e ESC символ.
В кода 7B5C13A3CABABB2C20A вместо 7 пишем G, вместо 5 — E, вместо 13 — M,  вместо 20 — T и т.н.
Получаваме: ZGBZECZMACCCABABBCCZTA

Програмиране = ++Алгоритми; (Programming = ++Algorithms;) Задача: Кодиране на редици от  единици и нули с много поредни еднакви символи.
Алгоритъм: Записваме само броя на поредните нули, после броя на поредните единици и т.н.
Пример:
000000001111111000000 -> 8 7 6
000000011111111110000 -> 7 10 4
000001111111111111000 -> 5 13 3
000001111111111111000 -> 5 13 3

** Алгоритъм на Хъфман [10.4.2]  (Huffman coding)
Задача: Код, при който по-често срещаните символи се кодират с по-къси кодове (редици от битовe).
Алгоритъм:
1. Пресмятаме вероятностите за срещане на всеки символ (честота).
2. Построяваме дърво на Хъфман:
   2a. Построяваме гора - от всеки символ тривиално дърво, в корена (единствения връх) на което записваме вероятността на срещане на съответния символ.
   2b. Намираме двата върха с най-малки вероятности и ги обединяваме в ново дърво с корен, съдържащ сумата от вероятностите им.
   2c. Ако има поне две дървета, преход към 2b.
3. Поставяме 0 на ляв клон и 1 на десен клон.
4а. Кодиране: За всяко листо (символ) определяме код от единици и нули, получени оп пътя от корена до това листо.
4b. Декодиране: Тръгваме от корена и вървим по ляво или дясно поддърво, докато стигнем листо - кодирания символ
Пример: afbabcdefacbabcdecde (дължина 20)
Пресмятаме вероятностите:
a - 4 пъти: вероятност 4/20 = 0.2
b - 4 пъти:  вероятност 4/20 = 0.2
c - 4 пъти:  вероятност 4/20 = 0.2
d - 3 пъти: вероятност 3/20 = 0.15
e - 3 пъти: вероятност 3/20 = 0.15
f - 2 пъти - вероятност 2/20 = 0.10

Началнa гора (6 дървета):
 (a: 0,20) 
(b: 0,20) 
(c: 0,20) 
(d: 0,15) 
(e: 0,15) 
(f: 0,10)
Обединяваме e и f (5 дървета) - в [] са наследниците на възела:
(a: 0.20) 
(b: 0.20) 
(c: 0.20) 
(d: 0.15)   
(e,f: 0.25)[(e: 0.15) , (f: 0.10)]
Обединяваме c и d (4 дървета):
(a: 0.20) 
(b: 0.20) 
(cd: 0.35)[(c: 0.20), (d: 0.15)]   
(ef: 0.25)[(e: 0.15) , (f: 0.10)]
Обединяваме a и b (3 дървета):
(ab: 0.40)[(a: 0.20), (b: 0.20)] 
(cd: 0.35)[(c: 0.20), (d: 0.15)]   
(ef: 0.25)[(e: 0.15), (f: 0.10)]
Обединяваме cd и ef (2 дървета):
(ab: 0.40)[(a: 0.20), (b: 0.20)]
(cdef, 0.60)[(cd: 0.35)[(c: 0.20), (d: 0.15)], (ef: 0.25)[(e: 0.15), (f: 0.10)]]
Обединяваме двете дървета:
(abcdef, 1.00)[(ab: 0.40)[(a: 0.20), (b: 0.20)], (cdef, 0.60)[(cd: 0.35)[(c: 0.20), (d: 0.15)], (ef: 0.25)[(e: 0.15), (f: 0.10)]]]
hufman
Кодиране:
Кодовете на символите са:  a = 00, b = 01, c = 100, d = 101, e = 110 и f = 111.
Кодът на съобщението от условието на задачата е:
00 111 01 00 01 100 101 110 111 00 100 01 00 01 100 101 110 100 101 110
или
0011101000110010111011100100010001100101110100101110
Компресия на Хъфман: 52 бита за 20 символа - 52/20 = 2.6 бита на символ срещу 8 бита (min 4 бита) за символ.
Декодиране:
От корена 00 значи ляво-ляво, достигаме до a, отново тръгваме от корена 111, достигаме до f и т.н.
Визуализация
Програмиране = ++Алгоритми; (Programming = ++Algorithms;)
** Код с разделители [10.4.4]
Задача: Кодиране с надежден код - промяна на един бит да довежда до невъзможност за декодиране на най-много една-две букви.
Алгоритъм: Всяка буква се записва в еднакъв брой битове (равномерни кодове).
Пример: Дадено е входно съобщение със следните символи и честоти: (a: 0,4), (b: 0,2), (c: 0,2), (d: 0,15) и (e: 0,05). Ако използваме 3 бита за всяка буква (равномерен код), получаваме цена 3.
При кодиране по Хъфман, получаваме кода: (a = 11), (b = 10), (c = 01), (d = 001), (e = 000) с цена:
L = 2.0,4 + 2.0,2 + 2.0,2 + 3.0,15 + 3.0,05 = 2,2

Не бихме ли могли да получим код, съчетаващ висока надеждност и относително добра ефективност, заемащ междинно положение между равномерните кодове и кодирането по Хъф- ман? Пример за такъв код е така нареченият код с разделители (англ. comma code).
Алгоритъм: Кодът на всеки символ завършва с разделител, указващ края му (код с разделители). Сортираме буквите по вероятност на срещане. На първия символ съпоставяме код 1, на втория - 01, на третия - 001, на четвъртия - 0001 и т.н. Очевидно така конструираният код е префиксен, позволява бързо и еднозначно декодиране и в общия случай е по-ефективен от равномерните кодове. Повреждането на единствен бит води до невъзможност за декодиране на най-много две букви.
Пример: За горния пример получаваме кода: a = 1, b = 01, c = 001, d = 0001, e = 00001 с цена:
L = 1.0,4 + 2.0,2 + 3.0,2 + 4.0,15 + 5.0,05 = 2,25
Получихме сравнително надежден код с ефективност, близка до тази на кода на Хъфман.