Možnosti kompresie súborov. Keď je potrebná kompresia údajov

GORKOFF 24. februára 2015 o 11:41 hod

Metódy kompresie údajov

  • Algoritmy

S mojím vedeckým poradcom pripravujeme malú monografiu o spracovaní obrazu. Rozhodol som sa predložiť kapitolu o algoritmoch kompresie obrazu na posúdenie HabraSociety. Keďže je ťažké zmestiť celú kapitolu do jedného príspevku, rozhodol som sa to rozdeliť do troch príspevkov:
1. Metódy kompresie dát;
2. Bezstratová kompresia obrázkov;
3. Kompresia obrázkov so stratou.
Nižšie si môžete prečítať prvý príspevok zo série.

V súčasnosti existuje veľké množstvo bezstratových kompresných algoritmov, ktoré možno podmienečne rozdeliť do dvoch veľkých skupín:
1. Streamové a slovníkové algoritmy. Táto skupina zahŕňa algoritmy rodín RLE (run-length encoding), LZ * atď. Vlastnosťou všetkých algoritmov tejto skupiny je, že kódovanie nepoužíva informácie o frekvenciách znakov v správe, ale informácie o sekvenciách ktoré sa vyskytli skôr.
2. Algoritmy pre štatistickú (entropickú) kompresiu. Táto skupina algoritmov komprimuje informácie pomocou nerovnomernosti frekvencií, s ktorými sa v správe vyskytujú rôzne znaky. Algoritmy v tejto skupine zahŕňajú aritmetické a prefixové kódovacie algoritmy (používajúce Shannon-Fanno, Huffman, secantové stromy).
Algoritmy transformácie informácií možno rozdeliť do samostatnej skupiny. Algoritmy tejto skupiny nekomprimujú informácie priamo, ale ich aplikácia značne zjednodušuje ďalšiu kompresiu pomocou prúdových, slovníkových a entropických algoritmov.

Streamovacie a slovníkové algoritmy

Kódovanie dĺžky chodu

Run-Length Encoding (RLE) je jedným z najjednoduchších a najbežnejších algoritmov kompresie údajov. V tomto algoritme je sekvencia opakovaných znakov nahradená znakom a počtom opakovaní.
Napríklad reťazec "AAAAA", ktorý vyžaduje 5 bajtov na uloženie (za predpokladu, že bajt je alokovaný na uloženie jedného znaku), možno nahradiť reťazcom "5A" pozostávajúcim z dvoch bajtov. Je zrejmé, že čím je séria opakovania dlhšia, tým je tento algoritmus efektívnejší.

Hlavnou nevýhodou tohto algoritmu je extrémne nízka účinnosť na sekvencie neopakujúcich sa znakov. Napríklad, ak vezmeme do úvahy sekvenciu „ABABAB“ (6 bajtov), ​​potom sa po použití algoritmu RLE zmení na „1A1B1A1B1A1B“ (12 bajtov). Existujú rôzne metódy, ako sa vysporiadať s problémom neopakujúcich sa znakov.

Najjednoduchšou metódou je nasledujúca úprava: bajt kódujúci počet opakovaní musí uchovávať informácie nielen o počte opakovaní, ale aj o ich prítomnosti. Ak je prvý bit 1, potom ďalších 7 bitov označuje počet opakovaní zodpovedajúceho znaku a ak je prvý bit 0, potom ďalších 7 bitov označuje počet znakov, ktoré je potrebné vziať bez opakovania. Ak zakódujete "ABABAB" pomocou tejto modifikácie, dostaneme "-6ABABAB" (7 bajtov). Je zrejmé, že navrhovaná technika môže výrazne zlepšiť účinnosť algoritmu RLE na neopakujúcich sa sekvenciách znakov. Implementácia navrhovaného prístupu je znázornená v zozname 1:

  1. typu
  2. funkcia RLEEncode (InMsg: ShortString): TRLEEncodedString;
  3. MatchFl: boolean;
  4. MatchCount: shortint;
  5. EncodedString: TRLEEncodedString;
  6. N, i: byte;
  7. začať
  8. N: = 0;
  9. SetLength (EncodedString, 2 * dĺžka (InMsg));
  10. pričom dĺžka (InMsg)> = 1 do
  11. začať
  12. MatchFl: = (dĺžka (InMsg)> 1) a (InMsg [1] = InMsg [2]);
  13. Počet zhôd: = 1;
  14. zatiaľ čo (MatchCount<= 126 ) and (MatchCount < length(InMsg) ) and ((InMsg[ MatchCount] = InMsg[ MatchCount + 1 ] ) = MatchFl) do
  15. Počet zhôd: = Počet zhôd + 1;
  16. ak MatchFl tak
  17. začať
  18. N: = N + 2;
  19. EncodedString [N - 2]: = MatchCount + 128;
  20. EncodedString [N - 1]: = ord (InMsg [1]);
  21. inak
  22. začať
  23. ak MatchCount<>dĺžka (InMsg).
  24. Počet zhôd: = Počet zhôd - 1;
  25. N: = N + 1 + MatchCount;
  26. EncodedString [N - 1 - MatchCount]: = - MatchCount + 128;
  27. for i: = 1 to MatchCount urobiť
  28. EncodedString [N - 1 - MatchCount + i]: = ord (InMsg [i]);
  29. koniec;
  30. vymazať (InMsg, 1, MatchCount);
  31. koniec;
  32. SetLength (EncodedString, N);
  33. RLEEncode: = EncodedString;
  34. koniec;

Dekódovanie komprimovanej správy je veľmi jednoduché a zredukuje sa na jeden prechod cez komprimovanú správu, pozri Výpis 2:
  1. typu
  2. TRLEEncodedString = pole bajtov;
  3. funkcia RLEDecode (InMsg: TRLEEncodedString): ShortString;
  4. RepeatCount: shortint;
  5. i, j: slovo;
  6. OutMsg: ShortString;
  7. začať
  8. OutMsg: = "";
  9. i: = 0;
  10. kým< length(InMsg) do
  11. začať
  12. Počet opakovaní: = InMsg [i] - 128;
  13. i: = i + 1;
  14. ak RepeatCount< 0 then
  15. začať
  16. RepeatCount: = abs (RepeatCount);
  17. pre j: = i až i + RepeatCount - 1 do
  18. OutMsg: = OutMsg + chr (InMsg [j]);
  19. i: = i + Počet opakovaní;
  20. inak
  21. začať
  22. pre j: = 1 do RepeatCount do
  23. OutMsg: = OutMsg + chr (InMsg [i]);
  24. i: = i + 1;
  25. koniec;
  26. koniec;
  27. RLEDecode: = OutMsg;
  28. koniec;

Druhou metódou na zvýšenie efektívnosti algoritmu RLE je použitie algoritmov transformácie informácií, ktoré dáta priamo nekomprimujú, ale privedú ich do vhodnejšej formy na kompresiu. Ako príklad takéhoto algoritmu budeme uvažovať o permutácii BWT pomenovanej po vynálezcoch Burrows-Wheelerovej transformácie. Táto permutácia nemení samotné znaky, ale iba mení ich poradie v reťazci, pričom opakované podreťazce sa po aplikácii permutácie zhromažďujú do hustých skupín, ktoré sú oveľa lepšie komprimované pomocou algoritmu RLE. Priama konverzia BWT je zredukovaná na sekvenciu nasledujúcich krokov:
1. Pridanie špeciálneho znaku konca riadku do pôvodného reťazca, ktorý sa nikde inde nenachádza;
2. Získanie všetkých cyklických permutácií pôvodného reťazca;
3. Triedenie prijatých riadkov v lexikografickom poradí;
4. Vrátenie posledného stĺpca výslednej matice.
Implementácia tohto algoritmu je znázornená vo výpise 3.
  1. konšt
  2. EOMsg = "|" ;
  3. funkcia BWTEncode (InMsg: ShortString): ShortString;
  4. OutMsg: ShortString;
  5. LastChar: ANSIChar;
  6. N, i: slovo;
  7. začať
  8. InMsg: = InMsg + EOMsg;
  9. N: = dĺžka (InMsg);
  10. ShiftTable [1]: = InMsg;
  11. pre i: = 2 až N do
  12. začať
  13. LastChar: = InMsg [N];
  14. InMsg: = LastChar + kópia (InMsg, 1, N - 1);
  15. ShiftTable [i]: = InMsg;
  16. koniec;
  17. Zoradiť (ShiftTable);
  18. OutMsg: = "";
  19. pre i: = 1 až N do
  20. OutMsg: = OutMsg + ShiftTable [i] [N];
  21. BWTEncode: = OutMsg;
  22. koniec;

Najjednoduchší spôsob, ako vysvetliť túto transformáciu, je na konkrétnom príklade. Vezmime si reťazec „ANANAS“ a dohodneme sa, že znak konca riadku bude symbol „|“. Všetky cyklické permutácie tohto radu a výsledok ich lexikografického triedenia sú uvedené v tabuľke. jeden.

Tie. výsledkom priamej konverzie bude reťazec "| ННААС". Je ľahké vidieť, že tento reťazec je oveľa lepší ako pôvodný; je komprimovaný algoritmom RLE, pretože obsahuje dlhé podsekvencie opakujúcich sa písmen.
Podobný efekt je možné dosiahnuť aj pomocou iných transformácií, výhodou BWT transformácie je však to, že je reverzibilná, inverzná transformácia je však náročnejšia ako priama. Ak chcete obnoviť pôvodný reťazec, musíte urobiť nasledovné:
Vytvorte prázdnu maticu n * n, kde n je počet znakov v zakódovanej správe;
Vyplňte prázdny stĺpec úplne vpravo kódovanou správou;
Triediť riadky tabuľky v lexikografickom poradí;
Opakujte kroky 2-3, kým nezostanú prázdne stĺpce;
Vráti riadok, ktorý končí znakom konca riadka.

Na prvý pohľad je spätná transformácia jednoduchá a jedna z možností je zobrazená vo výpise 4.

  1. konšt
  2. EOMsg = "|" ;
  3. funkcia BWTDecode (InMsg: ShortString): ShortString;
  4. OutMsg: ShortString;
  5. ShiftTable: pole ShortString;
  6. N, i, j: slovo;
  7. začať
  8. OutMsg: = "";
  9. N: = dĺžka (InMsg);
  10. SetLength (ShiftTable, N + 1);
  11. pre i: = 0 až N do
  12. ShiftTable [i]: = "";
  13. pre i: = 1 až N do
  14. začať
  15. pre j: = 1 až N do
  16. ShiftTable [j]: = InMsg [j] + ShiftTable [j];
  17. Zoradiť (ShiftTable);
  18. koniec;
  19. pre i: = 1 až N do
  20. ak ShiftTable [i] [N] = EOMsg potom
  21. OutMsg: = ShiftTable [i];
  22. vymazať (OutMsg, N, 1);
  23. BWTDecode: = OutMsg;
  24. koniec;

V praxi však účinnosť závisí od zvoleného triediaceho algoritmu. Triviálne algoritmy s kvadratickou zložitosťou budú mať samozrejme extrémne negatívny vplyv na výkon, preto sa odporúča používať efektívne algoritmy.

Po zoradení tabuľky získanej v siedmom kroku je potrebné vybrať z tabuľky riadok končiaci znakom "|". Je ľahké vidieť, že toto je jediný riadok. To. skúmali sme transformáciu BWT na konkrétnom príklade.

Ak to zhrnieme, môžeme povedať, že hlavnou výhodou skupiny algoritmov RLE je jednoduchosť a rýchlosť prevádzky (vrátane rýchlosti dekódovania) a hlavnou nevýhodou je neefektívnosť na neopakujúcich sa znakových sadách. Použitie špeciálnych permutácií zvyšuje efektivitu algoritmu, ale tiež výrazne zvyšuje čas chodu (najmä dekódovanie).

Slovníková kompresia (algoritmy LZ)

Skupina slovníkových algoritmov, na rozdiel od algoritmov skupiny RLE, nekóduje počet opakovaní znakov, ale skôr zaznamenané sekvencie znakov. Počas prevádzky uvažovaných algoritmov sa dynamicky vytvorí tabuľka so zoznamom predtým zaznamenaných sekvencií a ich zodpovedajúcich kódov. Táto tabuľka sa často nazýva slovník a zodpovedajúca skupina algoritmov sa nazýva slovník.

Najjednoduchšia verzia slovníkového algoritmu je opísaná nižšie:
Inicializujte slovník so všetkými znakmi, ktoré sa vyskytujú vo vstupnom reťazci;
Nájdite v slovníku najdlhšiu sekvenciu (S), ktorá zodpovedá začiatku zakódovanej správy;
Vydajte kód nájdenej sekvencie a odstráňte ho zo začiatku kódovanej správy;
Ak sa nedosiahne koniec správy, prečítajte si ďalší znak a pridajte Sc do slovníka, prejdite na krok 2. V opačnom prípade ukončite.

Napríklad novo inicializovaná slovná zásoba pre frázu „CUCKOOKUCHONKUPILACAPUCHON“ je uvedená v tabuľke. 3:

Počas procesu kompresie bude slovník doplnený o sekvencie nájdené v správe. Proces dopĺňania slovníka je uvedený v tabuľke. 4.

Pri popise algoritmu bol zámerne vynechaný popis situácie, kedy je slovník úplne zaplnený. V závislosti od variantu algoritmu je možné rôzne správanie: úplné alebo čiastočné vyprázdnenie slovníka, zastavenie plnenia slovníka alebo rozšírenie slovníka so zodpovedajúcim zväčšením šírky kódu. Každý z týchto prístupov má určité nevýhody. Napríklad zastavenie dokončovania slovníka môže viesť k situácii, keď slovník ukladá sekvencie, ktoré sa vyskytujú na začiatku komprimovaného reťazca, ale neskôr sa už nevyskytujú. Vyčistenie slovníka zároveň môže odstrániť časté sekvencie. Väčšina používaných implementácií pri vypĺňaní slovníka začne sledovať kompresný pomer a keď klesne pod určitú úroveň, slovník sa prestaví. Ďalej zvážime najjednoduchšiu implementáciu, ktorá zastaví dopĺňanie slovníka, keď je plný.

Najprv si definujme slovník ako záznam, ktorý uchováva nielen nájdené podreťazce, ale aj počet podreťazcov uložených v slovníku:

Predtým nájdené subsekvencie sú uložené v poli Words a ich kódy sú číslami subsekvencií v tomto poli.
Definujeme tiež funkcie pre vyhľadávanie v slovníku a pridávanie do slovníka:

  1. konšt
  2. MAX_DICT_LENGTH = 256;
  3. funkcia FindInDict (D: TDictionary; str: ShortString): celé číslo;
  4. r: celé číslo;
  5. i: celé číslo;
  6. fl: boolean;
  7. začať
  8. r = -1;
  9. ak D. WordCount> 0 potom
  10. začať
  11. i: = D. Počet slov;
  12. fl: = nepravda;
  13. zatiaľ čo (nie fl) a (i> = 0) áno
  14. začať
  15. i: = i - 1;
  16. fl: = D. Slová [i] = str;
  17. koniec;
  18. koniec;
  19. ak fl tak
  20. r: = i;
  21. FindInDict: = r;
  22. koniec;
  23. procedure AddToDict (var D: TDictionary; str: ShortString);
  24. začať
  25. ak D. WordCount< MAX_DICT_LENGTH then
  26. začať
  27. D. Počet slov: = D. Počet slov + 1;
  28. SetLength (D. Slová, D. Počet slov);
  29. D. Slová [D. Počet slov - 1]: = str;
  30. koniec;
  31. koniec;

Pomocou týchto funkcií možno proces kódovania podľa opísaného algoritmu implementovať nasledovne:
  1. funkcia LZWEncode (InMsg: ShortString): TEncodedString;
  2. OutMsg: TEncodedString;
  3. tmpstr: ShortString;
  4. D: TDslovník;
  5. i, N: bajt;
  6. začať
  7. SetLength (OutMsg, dĺžka (InMsg));
  8. N: = 0;
  9. InitDict (D);
  10. pričom dĺžka (InMsg)> 0 do
  11. začať
  12. tmpstr: = InMsg [1];
  13. pričom (FindInDict (D, tmpstr)> = 0) a (dĺžka (InMsg)> dĺžka (tmpstr))
  14. tmpstr: = tmpstr + InMsg [dĺžka (tmpstr) + 1];
  15. if FindInDict (D, tmpstr)< 0 then
  16. delete (tmpstr, dĺžka (tmpstr), 1);
  17. OutMsg [N]: = FindInDict (D, tmpstr);
  18. N: = N + 1;
  19. vymazať (InMsg, 1, dĺžka (tmpstr));
  20. ak dĺžka (InMsg)> 0 potom
  21. AddToDict (D, tmpstr + InMsg [1]);
  22. koniec;
  23. SetLength (OutMsg, N);
  24. LZWEncode: = OutMsg;
  25. koniec;

Výsledkom kódovania budú čísla slov v slovníku.
Proces dekódovania sa redukuje na priame dešifrovanie kódov, pričom nie je potrebné prenášať vytvorený slovník, stačí, že pri dekódovaní sa slovník inicializuje rovnako ako pri kódovaní. Potom sa slovník úplne obnoví priamo počas procesu dekódovania zreťazením predchádzajúcej podsekvencie a aktuálneho symbolu.

Jediný problém je možný v nasledujúcej situácii: keď je potrebné dekódovať podsekvenciu, ktorá ešte nie je v slovníku. Je ľahké vidieť, že je to možné iba vtedy, keď je potrebné extrahovať podreťazec, ktorý sa má pridať v aktuálnom kroku. To znamená, že podreťazec sa zhoduje so vzorom cSc, t.j. začína a končí rovnakým znakom. Okrem toho, cS je podreťazec pridaný v predchádzajúcom kroku. Uvažovaná situácia je jediná, kedy je potrebné dekódovať riadok, ktorý ešte nebol pridaný. Vzhľadom na vyššie uvedené môžeme navrhnúť nasledujúcu možnosť na dekódovanie komprimovaného reťazca:

  1. funkcia LZWDecode (InMsg: TEncodedString): ShortString;
  2. D: TDslovník;
  3. OutMsg, tmpstr: ShortString;
  4. i: byte;
  5. začať
  6. OutMsg: = "";
  7. tmpstr: = "";
  8. InitDict (D);
  9. pre i: = 0 do dĺžky (InMsg) - 1 do
  10. začať
  11. ak InMsg [i]> = D. WordCount potom
  12. tmpstr: = D. Slová [InMsg [i - 1]] + D. Slová [InMsg [i - 1]] [1]
  13. inak
  14. tmpstr: = D. Slová [InMsg [i]];
  15. OutMsg: = OutMsg + tmpstr;
  16. ak i> 0 potom
  17. AddToDict (D, D. Slová [InMsg [i - 1]] + tmpstr [1]);
  18. koniec;
  19. LZWDecode: = OutMsg;
  20. koniec;

Medzi výhody slovníkových algoritmov patrí vyššia účinnosť kompresie v porovnaní s RLE. Malo by sa však chápať, že skutočné použitie týchto algoritmov je spojené s určitými ťažkosťami pri implementácii.

Entropické kódovanie

Kódovanie so stromami Shannon-Fano

Algoritmus Shannon-Fano je jedným z prvých vyvinutých kompresných algoritmov. Algoritmus je založený na myšlienke reprezentovať častejšie znaky pomocou kratších kódov. Okrem toho kódy získané pomocou algoritmu Shannon-Fano majú vlastnosť predpony: t.j. žiadny kód nie je začiatkom žiadneho iného kódu. Vlastnosť prefix zaisťuje, že kódovanie je jedna k jednej. Algoritmus na zostavenie Shannon-Fano kódov je uvedený nižšie:
1. Rozdeľte abecedu na dve časti, pričom celkové pravdepodobnosti symbolov sú čo najbližšie k sebe.
2. Do prefixového kódu prvej časti znakov pridajte 0, do prefixového kódu druhej časti znakov pridajte 1.
3. Pre každú časť (v ktorej sú aspoň dve postavy) rekurzívne vykonajte kroky 1-3.
Napriek svojej porovnateľnej jednoduchosti nie je algoritmus Shannon-Fano bez nedostatkov, z ktorých najvýznamnejšou je neoptimálne kódovanie. Aj keď je rozdelenie v každom kroku optimálne, algoritmus nezaručuje optimálny výsledok ako celok. Zvážte napríklad nasledujúci riadok: "AAAABVGDEZH". Zodpovedajúci Shannon-Fano strom a z neho odvodené kódy sú znázornené na obr. jeden:

Bez kódovania bude správa zaberať 40 bitov (za predpokladu, že každý znak je zakódovaný 4 bitmi) a pomocou algoritmu Shannon-Fano 4 * 2 + 2 + 4 + 4 + 3 + 3 + 3 = 27 bitov. Objem správ sa znížil o 32,5 %, ale nižšie sa ukáže, že tento výsledok možno výrazne zlepšiť.

Kódovanie pomocou Huffman Trees

Huffmanov kódovací algoritmus, vyvinutý niekoľko rokov po algoritme Shannon-Fano, má tiež vlastnosť predpony a navyše preukázanú minimálnu redundanciu, práve preto je extrémne rozšírený. Na získanie Huffmanových kódov sa používa nasledujúci algoritmus:
1. Všetky symboly abecedy sú reprezentované ako voľné uzly, pričom váha uzla je úmerná frekvencii symbolu v správe;
2. Z množiny voľných uzlov sa vyberú dva uzly s minimálnou váhou a vytvorí sa nový (nadradený) uzol s váhou rovnajúcou sa súčtu váh vybraných uzlov;
3. Vybrané uzly sa odstránia z voľného zoznamu a nadradený uzol vytvorený na ich základe sa pridá do tohto zoznamu;
4. Kroky 2-3 sa opakujú, kým sa vo voľnom zozname nenachádza viac ako jeden uzol;
5. Na základe vytvoreného stromu je každému znaku abecedy priradený prefixový kód;
6. Správa je zakódovaná prijatými kódmi.

Zvážte rovnaký príklad ako v prípade algoritmu Shannon-Fano. Huffmanov strom a kódy získané pre správu „AAAAABVGDEZH“ sú znázornené na obr. 2:

Je ľahké vypočítať, že veľkosť zakódovanej správy bude 26 bitov, čo je menej ako v algoritme Shannon-Fano. Samostatne je potrebné poznamenať, že vzhľadom na popularitu Huffmanovho algoritmu v súčasnosti existuje veľa možností pre Huffmanovo kódovanie, vrátane adaptívneho kódovania, ktoré nevyžaduje prenos symbolových frekvencií.
Medzi nevýhody Huffmanovho algoritmu tvoria významnú časť problémy spojené so zložitosťou implementácie. Použitie reálnych premenných na ukladanie frekvencií symbolov je spojené so stratou presnosti, preto sa v praxi často používajú celočíselné premenné, ale od r. hmotnosť rodičovských uzlov neustále rastie, skôr či neskôr dôjde k pretečeniu. Preto, napriek jednoduchosti algoritmu, jeho správna implementácia môže stále spôsobovať určité ťažkosti, najmä pre veľké abecedy.

Kódovanie pomocou sečných funkčných stromov

Kódovanie pomocou sekantových funkcií je algoritmus vyvinutý autormi, ktorý umožňuje získavanie prefixových kódov. Algoritmus je založený na myšlienke vybudovať strom, ktorého každý uzol obsahuje funkciu sekantu. Pre detailnejší popis algoritmu je potrebné uviesť niekoľko definícií.
Slovo je usporiadaná postupnosť m bitov (číslo m sa nazýva dĺžka slova).
Sekantový literál je dvojica typu číslica-hodnota číslice. Napríklad literál (4,1) znamená, že 4 bity slova musia byť 1. Ak je splnená podmienka literálu, potom sa literál považuje za pravdivý, inak je nepravdivý.
K-bitový sekant je množina k literálov. Ak sú všetky literály pravdivé, potom je samotná funkcia sekantu pravdivá, inak je nepravdivá.

Strom je zostavený tak, že každý uzol rozdeľuje abecedu na čo najbližšie časti. Na obr. 3 ukazuje príklad sečného stromu:

Strom sečných funkcií vo všeobecnom prípade nezaručuje optimálne kódovanie, ale poskytuje extrémne vysokú rýchlosť práce vďaka jednoduchosti prevádzky v uzloch.

Aritmetické kódovanie

Aritmetické kódovanie je jedným z najefektívnejších spôsobov kompresie informácií. Na rozdiel od Huffmanovho algoritmu vám aritmetické kódovanie umožňuje kódovať správy s entropiou menšou ako 1 bit na symbol. Pretože väčšina aritmetických kódovacích algoritmov je chránená patentmi, nižšie budú popísané iba základné myšlienky.
Predpokladajme, že v použitej abecede je N symbolov a_1, ..., a_N, s frekvenciami p_1, ..., p_N. Potom bude algoritmus aritmetického kódovania vyzerať takto:
Berte ako pracovný polinterval)
Zdieľajte to