Лекція 09. Класичні лінійні коди із захистом від помилок
Курс “Теорія інформації та кодування”

Тут будуть розглянуті деякі найбільш популярні нині коди із захистом від помилок, які можна вважати класичними. Зокрема ми детально познайомимось із кодом Хемінга, що використовується для усунення помилок в оперативній пам'яті серверів, а також із кодом CRC для виявлення групових помилок при передачі даних. На цих прикладах розглянемо засади математичного апарату, який використовується для побудування та аналізу широкого класу так званих лінійних кодів.

Огляд сучасних кодів із захистом від помилок
Засади математичного аппарату лінійних кодів
Код Хеммінга з виправленням поодиноких помилок
Циклічні коди CRC

9.1 Огляд сучасних кодів із захистом від помилок

Напрями застосування
Ми уже знайомі із принципами кодування повідомлень на рівні каналу передачі, де завданням є захист від помилок внаслідок інформаційного шуму. При цьому різним напрямкам використання відповідають різні вимоги щодо здатності коду виявляти та виправляти помилки, оперативності корекції, а також рівню збитковості. Отже на практиці використовується досить широкий спектр спеціалізованих рішень, з якими ми будемо знайомитись надалі. Огляд відповідних напрямків відображений на рис.9.1.

Рисунок 9.1 Огляд сфер застосування кодів із захистом від помилок

- виділяються сфери використання завадозахисних кодів для передачі повідомлень в часі та в просторі (на рис.9.1 відповідно зліва та справа). Зокрема передачі в часі відповідають збереження інформації в оперативній пам'яті та на зовнішніх носіях. Найбільш важливі для нас сфери передачі в просторі — комп'ютерні мережі та мобільний звязок. В кожній із цих сфер існують власні специфічні вимоги, а отже й використовуються відмінні способи коування;

- принципова відмінність кодів — в здатності до корекції помилок. Для певних використань застосовуються лише коди із виявленням помилок (при цьому їх виправлення виконується за рахунок повторної передачі). Для інших виправлення помилок повинно виконуватись саме кодом (на рис.9.1 відповідно знизу та зверху). Перший підхід застосовується при збереженні даних на зовнішніх носіях та при передачі в мережах, а другий — в оперативній пам'яті (тут неможливо зберігати копії даних для повторної передачі) та в мобільному зв'язку (тут повторна передача викликала б перерви в потоці даних);

- інша важлива відмінність — спосіб введення контролю. Для так званих блокових кодів контрольні розряди відповідають певному блоку інформації (саме такий спосіб ми розглядали для кодів із перевіркою парності). Такий спосіб є більш економічним щодо збитковості, а отже і найбільш поширеним (рис.9.1) однак він може обмежувати оперативність, оскільки для виявлення помилки необхідно дочекатись обробки всього блоку. В потокових кодах контрольні розряди перемежовуються з інформаційними, отже помилки тут можуть виправлятись без затримок, однак збитковість є значно вищою. Цей спосіб зокрема широко використовують в мобільному зв'язку. Також в цій сфері застосовуються гібридні рішення, які зазвичай називають каскадними (рис.9.1). Блокові та потокові коди мають суттєві розбіжності не тільки в сферах використання, а й щодо відповідного математичного апарату, тому надалі ми будемо розглядати їх окремо.

Контрольні питання
1) Використовуючи рис.9.1, поясніть, в яких сферах використовуються коди із корекцією помилок і виклюючно з виявленням помилок. Поясніть причини
2) Використовуючи рис.9.1, поясніть, в яких сферах використовуються блокові та потокові коди. Поясніть причини
3) Спираючись на рис.9.2, охарактеризуйте особливості використання найбільш популярних блокових кодів.


Огляд популярних блокових кодів
В цій лекції ми зосередимось на розгляді саме найбільш застосовуваних блокових кодів і познайомимось із відповідним математичним апаратом (рис.9.2):

Рисунок 9.2 Огляд популярних блокових кодів

- найпростішими тут є коди із перевіркою парності, з якими ми вже знайомились в лекції 5. Нагадаємо, що їх модифікація із перевіркою парності в окремому блоці даних дозволяє виявляти всі помилки непарної кратності (такий спосіб зокрема використовується в широко розповсюджених мікросхемах послідовних com-портів). Також варіант коду із поєднанням множини блоків дозволяє усувати однократні помилки (застосовувався в стрічкових накопичувачах), а його модифікація забезпечує можливість відновлення помилкового блоку (нині використовується при передачі архівних файлів та в дискових RAID-масивах серверів). На практиці код з перевіркою парності нині зазвичай використвується в комбінації із більш складними та надійними кодами;

- в оперативній пам'яті серверів широко застосовується код Хеммінга, який за рахунок паралельного використання кількох перевірок парності дозволяє точно локалізувати, а отже й виправляти одиничні помилки (саме вони найбільш характерні для оперативної пам'яті). Класичний код Хеммінга використовується більше як півсторіччя і повністю відповідає вимогам високої швидкості, які притаманні його сфері застосування;

- мабуть найбільш поширеним є код CRC (Cyclic redundancy check - циклічна надлишкова перевірка), який повсюдно використовується при послідовній передачі даних. Зокрема саме він забезпечує виявлення помилок при передачі між вузлами компютерних мереж, а також при збереженні даних на зовнішніх носіях (таких як диски або флеш-пам'ять). Такий код здатен виявляти досить довгі серії помилок, при цьому їх виправленням опікується механізм повторної передачі.

Надалі в цій лекції ми детально розглянемо код Хеммінга та код CRC. При цьому спочатку нам потрібно буде познайомитись із засадами відповідного математичного апарату, який базується на засобах лінійної алгебри. Саме цей апарат лежить в основі абсолютної більшості сучасних збиткових кодів.

9.2 Засади математичного аппарату лінійних кодів

Принципи побудови лінійних кодів
Для побудови надлишкових завадозахисні кодів широко використовується математичний апарат лінійної алгебри (рис.9.3):

Рисунок 9.3 До уявлення про математичний апарат лінійних кодів

- код як послідовність двійкових розрядів математично може бути представлений в двох різних формах (рис.9.3 A). По-перше, це вектор X, в якому все розряди x1 ... xn незалежні і можуть оброблятись паралельно. По-друге, це многочлен f(x), де всі розряди мають власну вагу, що пов'язана з їх позицією. В такому випадку розряди коду можуть оброблятися тільки у взаємозв'язку, а тому — саме послідовно. Вибір форми для опису конкретних кодів визначається вимогами до їх застосування (зокрема, операції з векторами швидше, а використання поліномів надає коду масштабованість);

- лінійні перетворення включають тільки операції підсумовування і множення на постійні коефіцієнти (рис.9.3 Б). Для двійкових змінних такі коефіцієнти можуть мати значення 0 або 1. Крім того для підсумовування використовується операція додавання по модулю 2. Використання такої операції дозволяє отримувати всі результати в межах одного розряду;

- в подальшому ми будемо розглядати в основному так звані "систематичні" коди (рис.9.3 B), у яких інформаційні та контрольні розряди розділені: в загальному випадку послідовність з n кодових розрядів включає k інформаційних і m контрольних (останні утворюють так званий "хвостовик"). Такий код часто позначають парою параметрів n, k. Наприклад, запис 7,4 означає, що довжина кодових блоків n = 7, при цьому k = 4 розряду в блоці - інформаційні (залишилися m=3 розряду контрольні). В практиці кодування застосовуються також несистематичні коди, для яких інформаційні та контрольні розряди можуть чергуватись. Однак розгляд саме систематичних кодів дозволяє спростити аналіз математичного апарату.

Утворюючі матриці лінійних кодів
Важливою перевагою лінійних кодів є можливість їх компактного завдання за допомогою так званих утворюючих матриць (рис.9.4):

Рисунок 9.4 Утворюючі матриці лінійних кодів

- в загальному вигляді утворює матриця G коду n,k включає k рядків, яким відповідають n-розрядні вектора X1 - Xk. Всі ці вектори довжиною n повинні бути лінійно незалежними (тобто, жоден з них не може бути отримана лінійними перетвореннями з інших рядків). Крім того всередині матриці G має забезпечуватися задане значення dmin;

- важлива властивість утворючої матриці в тому, що всі 2k дозволених значення коду можуть бути отримані лінійними перетвореннями її рядків. Таким чином матриця G компактно задає весь код. У прикладі на рис 9.4 для коду парності 5,4 матриця GA включає 4 рядки X1 - X4, з яких лінійними комбінаціями виходять ще 12 які залишилися. При збільшенні значення k ефективність такого способу зростає. Наприклад, якщо інформаційна частина блоку включає 1 байт (k = 8, то матриця з 8 рядків визначає всі 256 дозволених значень коду;

- вид утворючої матриці задає і додаткові властивості коду. Так, матриці GB і GC подібні за структурою (задають код 7,4) і забезпечують значення dmin = 3. Проте, властивості кодів, які вони визначають, значно різняться. Зокрема код Хеммінга (матриця GB) орієнтований на виправлення помилок в оперативній пам'яті комп'ютера, які в силу її організації з високою імовірністю будуть поодинокими. Циклічний код (матриця GC) націлений на виявлення "пачок" помилок в сусідніх розрядах, які можуть виникати при передачі по лініях зв'язку. В останньому випадку всі рядки матриці можуть бути отримані з першого рядка шляхом її циклічного зсуву. Це означає, що властивості коду визначаються лише одним першим рядком. Його опис у вигляді полінома має вигляд g (x) = x3 + x + 1. Такий многочлен, як і матриця, називається утворюючим.

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

Процедури кодування та декодування
Загальні підходи до процедур кодування та декодування надлишковими завадозахисні кодами відображає рис.9.5:

Рисунок 9.5 До процедур кодування та декодування

- кодування зводиться до визначення контрольних розрядів ("хвостовика"), які б разом з інформаційною частиною забезпечували приналежність всього кодового блоку до дозволених (тобто таких, що узгоджуються із утворюючою матрицею). Наприклад, для коду парності хвостовик включає всього один розряд xk+1 і його значення повинно приводити весь блок до умови, коли в ньому парне число 1 (математично це означає, що значення xk + 1 визначається як сума mod2 для k інформаційних розрядів);

- декодування означає знаходження так званого "синдрому" помилки. У разі, коли необхідно просто виявити помилку, синдром може складатися з одного розряду. Наприклад, для коду парності синдром z визначається як сума по модулю 2 всіх n отриманих розрядів коду yi. Якщо z=1 (умову парності порушено), це вказує на помилку передачі. Якщо потрібно виправлення, синдром повинен вказати на номери помилкових розрядів. Наприклад, для коду Хеммінга 7,4, який виправляє одноразові помилки, синдром включає 3 розряду і може мати 23 = 8 значень. Сім з них відповідають помилкам в кожному з 7 розрядів блоку, а восьме значення вказує на правильну передачу;

- при кодуванні і декодуванні можуть використовуватися різні математичні процедури, вимоги до яких визначаються сферою застосування коду. Так, для виправлення помилок в оперативній пам'яті комп'ютера доцільно використовувати швидкі операції з векторами, а при послідовній передачі коду по лініях зв'язку - більш повільні, але масштабовані операції з многочленами.

Контрольні питання
1) Використовуючи рис.9.3, поясніть, які існують форми математичного опису коду. Які їх переваги та обмеження.
2) Які коди називають лінійними. Поясніть загальний вигляд лінійних перетворень і їх особливості для двійкових змінних.
3) Поясніть загальну структуру блоку систематичного коду. Чому в даному курсі розглядається саме такий клас кодів.
4) Поясніть поняття утворючої матриці лінійного коду. Яка її загальна структура.
5) Спираючись на рис.9.4, поясніть особливості утворюючих матриць наведених тут кодів. Як за допомогою утворючої матриці можна отримати всі дозволені значення коду.
6) Спираючись на рис.9.5, поясніть поняття кодування і декодування для надлишкових систематичних завадозахисні кодів.


9.3 Код Хеммінга з виправленням поодиноких помилок

Загальна характеристика коду Хеммінга
Код Хеммінга, як ми вже знаємо, використовується для виправлення одноразових помилок в оперативній комп'ютерної пам'яті. На практиці це рішення застосовується для серверів, які повинні мати підвищену надійність порівняно із звичайними комп'ютерами. Особливості реалізації такого коду характеризує рис.9.6:

Рисунок 9.6 Загальна характеристика коду Хеммінга

- виходячи з вимоги високої швидкості обробки для коду Хеммінга застосовуються математичний апарат, орієнтований на вектори. Крім того, з урахуванням організації оперативної пам'яті (розряди кодового слова зберігаються тут незалежно) можна виходити з припущення, що помилки в основному поодинокі. Звідси випливає умова sm=1, а значить - dmin= 3. З того ж припущення випливає умова для визначення кількості контрольних розрядів: воно повинно забезпечувати кодування n = k + m можливих позицій помилок, а також тієї ситуації, коли помилки відсутні. Наприклад, якщо k = 32, то буде потрібно m = 6 контрольних розрядів (26 = 64> 32 + 6);

- в основу процедур кодування та декодування покладена умова ортогональності перевірочної матриці коду U. Матриця U будується як ортогональна утворюючій матриці G (умовою ортогональності є рівність нулю скалярних творів всіх рядків цих матриць - см. рис.9.6). Оскільки всі дозволені значення коду Х повинні відповідати утворюючій матриці, вони також будуть ортогональні перевірочній матриці U;

- процедура кодування зводиться до знаходження m контрольних розрядів ("хвостовика") виходячи з m рівнянь ортогональності шуканого вектора X до матриці U - см. рис.9.6. Процедура декодування реалізується через перевірку ортогональности отриманого коду Y до тієї ж перевірочної матриці U. При цьому в розрядах, де ортогональність порушена, замість нульових ми отримали поодинокі значення розрядів zj. У сукупності код Z складе "синдром" помилки, який вказує на позицію спотвореного розряду.

Побудування коду Хеммінга 7,4
Для кращого розуміння розглянемо приклад побудови коду Хеммінга 7,4, включаючи конкретні процедури кодування та декодування (рис.9.7):

Рисунок 9.7 Побудування коду Хеммінга 7,4

- при побудуванні утворюючою матриці G лінійна незалежність рядків забезпечується "діагнальним" розташуванням 1 в лівій квадратної частини матриці. При цьому умова dmin=3 забезпечується вибором значень в правій частині матриці;

- перевірочна матриця U будується з умови ортогональності G при кількості рядків m=3 (для перевірки можна переконатися в ортогональности довільної пари рядків цих матриць). Насправді наведена на рис.9.7 матриця U - лише один з 8 варіантів матриць, які будуть ортогональні G, при цьому будь-який з них міг бути використаний при кодуванні. Однак обраний варіант має деякий додаткову зручність, яке ми оцінимо в подальшому;

- умови ортогональності довільного вектора X до матриці U визначаються, якщо підставити в рівняння ортогональности конкретні значення її елементів. Очевидно, що в рівняння увійдуть тільки ті значення xij, для яких відповідні uij=1 (множення xij на 0 дає нульовий результат);

- рівняння кодування вийдуть, якщо в рівняннях ортогональности невідомі значення контрольних розрядів x5-x7 виразити через відомі значення інформаційних розрядів x1-x4. Це можна зробити, зокрема, підсумовуючи рівняння ортогональности по модулю 2 так, щоб виключати деякі невідомі: наприклад, підсумувавши перше і друге рівняння ортогональности, можна отримати рівняння для розряду x5, першого і третього рівнянь - для розряду x6, а за сумою всіх трьох рівнянь ортогональності висловити значення x7;

- рівняння декодування за структурою відповідають рівнянням ортогональности. Тільки тепер замість вихідних розрядів xij в них братимуть участь отримані в результаті передачі yij. Оскільки останні можуть містити помилки, в правих частинах рівнянь виявляться вже не фіксовані 0, а змінні zj, значення яких утворюють синдром помилки.

Приклад кодування і декодування по Хеммінга
Приклад використання коду Хеммінга 7,4 показаний на рис.9.8.

Рисунок 9.8.Приклад кодування за Хеммінгом

- нехай інформаційні розряди x1 - x4 мають значення 1010;
- використовуючи рівняння кодування, отримаємо значення x5-x7 - 101;
- нехай тепер при передачі виникла помилка в розряді 3. Даний розряд присутній в рівняннях 2 і 3, тому під час декодування буде отримано код z1-z3 = 011;
- отримане значення вектора Z є двійковим кодом номера помилкового розряду 3, що дозволяє виправити помилку в даному розряді.

Відзначимо, що умова "синдром помилки дорівнює бінарного номеру помилкового розряду" в принципі не є обов'язковим. Важливо, що кожне значення синдром однозначно відповідає певному розряду. Однак, збіг з двійковим номером створює додаткову зручність. Його наявність визначається варіантом матриці U (саме тому в даному прикладі використана матриця, показана на рис.9.8.

Звернемо увагу, що значення Z = 011 буде отримано також при одночасному виникненні помилок в розрядах 1 і 2. При цьому код не тільки не виправить їх, але і внесе нову помилку (в розряді 6). Нагадаємо, що в силу організації оперативної пам'яті одночасні помилки в двох різних розрядах кодового слова відбуваються виключно рідко. У зв'язку з цим подібна ситуація при побудові коду не враховується.

Контрольні питання
1) Обгрунтуйте вимоги до коду Хеммінга, виходячи з області його застосування
2) Спираючись на рис.9.7, поясніть що означає термін «ортогональність» і як умова ортогональності використовується при побудові коду Хеммінга.
3) Використовуючи рис.9.8, підтвердіть ортогональность для довільної пари рядків матриць G і U для коду Хеммінга 7,4.
4) Поясніть, яким чином з виду перевірочної матриці U для коду Хеммінга 7,4 виходять рівняння ортогональности.
5) Поясніть як з рівнянь ортогональності виходять рівняння кодування з декодування.
6) Самостійно виконайте приклад кодування і декодування із застосуванням показаних на рис.9.8 рівнянь. В якості інформаційних розрядів використовуйте двійкового коду свого номера в списку підгрупи виконання лабораторних.


9.4 Циклічні коди CRC

Загальна характеристика циклічного коду
Нагадаємо, що назва "циклічних" кодів пов'язана з особливістю побудови їх утворючої матриці: всі її рядки утворюються циклічним зсувом першого рядка. Ці коди також часто позначаються абревіатурою CRC (Cyclic redundancy check - циклічна надлишкова перевірка). Вимоги до CRC-кодів зважаючи на сферу їх застосування ілюструє рис.9.9:

Рисунок 9.9 Вимоги до циклічного коду

- при передачі інформаційних блоків в послідовному режимі (наприклад, по лініях зв'язку) контрольні розряди утворюють "хвостовик", який може формуватися в міру надходження інформації. На практиці це означає, що швидкість кодування не важлива, оскільки темп надходження даних для передачі на порядки нижче, ніж швидкість логічної обробки. Разом з тим, важливою вимогою є масштабованість - інформаційні блоки можуть мати різну довжину, в тому числі і велику. Наприклад, це можуть бути тисячі розрядів коду;

- для передачі по лініях зв'язку характерно виникнення "пачок" помилок в сусідніх розрядах. Наприклад, при спотворенні сигналу, який несе кілька біт інформації. Ключовою вимогою до коду є виявлення таких випадків. При цьому виправлення виявлених помилок виконується методом перезапиту. Виходячи зі згаданих вимог оптимальним математичним інструментом кодування є операції з поліномами;

- ми уже знаємо, що будь-яка кодова послідовність може бути представлена у вигляді поліному f(x). Зокрема, ознакою правильності коду може бути зокрема його подільність без остачі на певний утворюючий поліном g(x) (як серед чисел виділяються ті, що діляться, наприклад, на десяткове 11). Щоб забезпечити таку подільність, при кодуванні до числа (полінома), що передається, слід додати остачу r(x), а при декодувані перевірити збереження подільності;

- структура утворючого поліному коду відображує структуру першого рядка утворюючої матриці, а отже — задає всі властивості коду. Наприклад, десятковому числу 11 відповідає двійкове 1011, а отже й поліном g(x)=x3+x+1 (порівняймо із прикладом утворюючої матриці на рис.9.4). Вочевидь, чим більше ділитель, ти більшою буде здатність коду виявляти помилки. В сучасних рішеннях використовується старша ступінь поліному m від 8 до 32.

Математичний апарат циклічних кодів
Особливості математичного апарату циклічних кодів ілюструє рис.9.10:

Рисунок 9.10 До огляду математичного апарату циклічних кодів

- нагадаємо, що вихідного k-розрядному значенням інформаційного коду відповідає можна поставити у відповідність певний, який позначимо b(x). Щоб перейти до повного n-розрядному формату доповнимо вихідний код m нулями. Це відповідає множенню полінома на xm;

- результуючий n-розрядний поліном f(x) за умовами кодування повинен без остачі ділитися на який утворючий g (x) зі старшим ступенем m. Щоб це забезпечити, поділимо b(x) на xm і залишок r(x), який «заважає» поділу без остачі, додамо до b(x)xm. Результат, який тепер напевно поділиться на g(x), і буде шуканим f(x). Таким чином, кодування виконано;

- декодування зводиться до поділу отриманого многочлена f*(x) на g(x). Якщо помилок не було, то він поділиться без остачі. Ненульовий залишок від ділення говорить про помилки передачі. Важливо, що CRC-код дозволяє виявити будь-які поєднання помилок на відрізку коду довжиною m розрядів. Наприклад, якщо ми використовуємо 9 контрольних розрядів (m = 9, то код зможе виявити будь-яке поєднання помилок в сусідніх 9 розрядах;

- окреме питання - вибір утворючого многочлену коду. Він повинен відповідати ряду математичних умов (рис.9.10). Однак на практиці такі многочлени обирають з готових таблиць. Найбільш зручні варіанти стандартизовані. Крім прикладів, показаних на рис.9.10, існують стандарти для 4-, 12- і 32-розрядних многочленів. У сучасній практиці CRC-коди з утворюють многочленами g16(x) широко використовуються в глобальних комп'ютерних мережах і дискових контролерах, а g32 (x) - в контролерах мережевих карт ПК (для локальних мереж).

Приклад циклічного коду 7,4
Розглянемо приклад кодування і декодування циклічним кодом 7,4. Для даного коду кількість контрольних розрядів становить m = 3. Зазначеним вище вимогам відповідають лише два варіанти утворюючого многочлену: g (x) = x3 + x +1 і g (x) = x3 + x2 + 1. Надалі в прикладі використовується перший з цих двох поліномів (рис.9.11).

Рисунок 9.11 Приклад кодування та декодування для коду 7,4

Нехай k = 4 інформаційних розряду мають значення 1010. На верхній частині рис.9.11 показана послідовність виконання кодування і декодування згідно з описаною вище процедурою:
- інформаційному блоку 1010 відповідає многочлен b(x) = x3 + x. Відповідно b (x)*x3 = x6 + x4;

- контрольні розряди 011 відображають многочлен r(x) = x + 1 - залишок від ділення b (x)*x3 на який утворює многочлен g (x) =x3 + x2 + 1. В результаті кодовий многочлен має вигляд f (x) = x6 + x4 + x + 1;

- при передачі без помилок отриманий кодовий многочлен f*(x) збігається з переданим і тому залишок від його поділу на утворюючий дорівнює 0 (це ознака правильної передачі);

При виникненні помилок залишок від ділення f*(x) на g(x) відмінний від 0, що є ознакою неправильної передачі. У прикладі видно, що таким чином виявляються не тільки одноразова помилка в розряді 3, але і послідовність з трьох помилок в розрядах 1-3.

На нижній частині рис.9.11 відображені детальні процедури кодування та декодування за допомогою ділення поліномів. Нагадаємо, що в якості операції підсумовування використовується сума по модулю 2. При цьому на кожному кроці ділення старша ступінь многочлена-залишку підбирається так, щоб "погасити" старший елемент діленого. Старша ступінь залишку повинна бути менше m (інакше поділ можна продовжувати).

Контрольні запитання
1) Як назва «циклічних кодів» пов'язана із структурою їх утворюючої матриці
2) Чому саме при послідовній передачі даних не важливі вимоги до швидкості і, напроти, важлива вимога масштабованості довжини кодових блоків
3) Наведіть приклади виникнення «пачок» помилок при передачі даних в просторі та часі в послідовному режимі




О дисциплине ТИК
Почему «Теория информации и кодирования» - одна из самых интересных дисциплин, которые изучают будущие системщики и защитники информации?

В ней сочетаются золотая классика и самая актуальная современность computer-science.

продолжение
О сайте
Здесь вы найдете материалы, которые помогут в изучении дисциплины “Теория информации и кодирования” (ТИК) в том виде, как она преподается на кафедре ЭВМ ДИИТа.

На сайте размещены методические материалы:
  • электронный конспект лекций;
  • методическое обеспечение к лабораторным работам;
  • полезные ссылки.

продолжение
© 2008-2021 • Теория информации и кодирования
UP