Problém řetězcového násobení matic (Matrix-chain multiplication) je klasickým úkolem z oblasti algoritmů a analýzy složitosti, který se zaměřuje na hledání optimálního způsobu násobení posloupnosti matic. Vstupem tohoto problému je řetězec matic A1,A2,,AnA_1, A_2, \dots, A_n, kde každá matice AiA_i má rozměry pi1×pip_{i-1} \times p_i. Cílem je zjistit, jakým způsobem zapsat produkt těchto matic, aby se minimalizoval počet skalárních multiplikací.

Řetězcové násobení matic je problém dynamického programování, který spočívá ve vytvoření optimálního pořadí pro násobení matic. V tomto případě se hledá nejlepší způsob, jak rodičovské a podřízené matice seskupit, což znamená, že je třeba najít optimální způsob "zabalování" matic do závorek tak, aby celkový počet skalárních multiplikací byl co nejmenší.

Pro efektivní řešení tohoto problému je potřeba využít dynamické programování. Základní myšlenkou je vypočítat počet skalárních multiplikací pro různé způsoby seskupení matic a postupně vybrat ten, který vede k minimálnímu počtu operací. Algoritmus pro tento úkol obvykle používá dvě tabulky:

  • Tabulka m (m[i,j]) obsahuje minimální počet skalárních multiplikací pro výpočet součinu matic AiA_iAjA_j.

  • Tabulka s (s[i,j]) uchovává indexy, kde se má provést rozdělení matice pro optimální seskupení.

Algoritmus pro řetězcové násobení matic

MATRIX-CHAIN-ORDER (p[], n)

  1. Inicializace:

    • Pro každý ii nastavíme m[i,i]m[i,i] na 0, protože pokud máme jednu matici, žádné násobení není potřeba.

  2. Výpočet pro různé délky podřetězců matic:

    • Pro každou délku podřetězce ll (od 2 do n):

      • Pro každou možnou počáteční pozici ii (od 1 do nl+1n-l+1):

        • Nastavíme j=i+l1j = i + l - 1.

        • Inicializujeme m[i,j]m[i,j] na nekonečno.

        • Poté pro každé možné rozdělení na dvě části kk (od ii do j1j-1):

          • Spočítáme q=m[i,k]+m[k+1,j]+p[i1]×p[k]×p[j]q = m[i,k] + m[k+1,j] + p[i-1] \times p[k] \times p[j], což je celkový počet multiplikací, pokud by rozdělení bylo na pozici kk.

          • Pokud je tento počet menší než aktuální hodnota m[i,j]m[i,j], aktualizujeme m[i,j]m[i,j] a zaznamenáme kk do tabulky s[i,j]s[i,j].

Tento algoritmus běží v čase O(n3)O(n^3), což je efektivní pro většinu praktických aplikací, i když pro velmi velké hodnoty nn mohou být potřeba složitější optimalizace.

Problém prohledávání grafu: Prohledávání do hloubky (DFS)

Dalším příkladem algoritmu je prohledávání grafu do hloubky (DFS), což je technika používaná k průchodu nebo prohledávání grafů. Pomocí DFS můžeme snadno najít topologické uspořádání grafu. V průběhu prohledávání přiřazujeme každému vrcholu dvě časové značky:

  1. d[v] – čas, kdy je vrchol navštíven poprvé.

  2. f[v] – čas, kdy je vrchol považován za "mrtvý", tj. kdy je zcela prozkoumán.

Tato časová razítka umožňují získat topologické seřazení vrcholů. Algoritmus DFS využívá rekurzivní prohledávání všech sousedních vrcholů a jejich označování v pořadí návštěvy.

Kruskalův algoritmus

Kruskalův algoritmus je známý algoritmus pro hledání minimálního kostrového stromu (MST) v grafu. Tento algoritmus pracuje takto:

  1. Seřadí všechny hrany podle jejich váhy.

  2. Postupně přidává hrany do kostrového stromu, pokud tím nevytvoří cyklus. Tento krok se opakuje, dokud strom neobsahuje V1V-1 hran.

Kruskalův algoritmus má časovou složitost O(ElogV)O(E \log V), kde EE je počet hran a VV počet vrcholů v grafu.

Primův algoritmus

Primův algoritmus je alternativní metodou pro hledání minimálního kostrového stromu. Na rozdíl od Kruskalova algoritmu začíná s jedním vrcholem a postupně přidává nejlevnější hrany k již existujícímu stromu.

Co je důležité pochopit?

Při práci s těmito algoritmy je klíčové pochopit, že každé rozhodnutí o pořadí nebo strukturování operací může výrazně ovlivnit výsledek v závislosti na konkrétních parametrech problému. Optimalizace často znamená vyvážení mezi složitostí algoritmu a efektivitou řešení pro daný problém. V mnoha případech je rozhodující nejen teoretická složitost, ale také implementační detaily a specifika konkrétního prostředí nebo aplikace, kde je algoritmus nasazen.

Jak analyzovat složitost rekurzivních algoritmů pomocí rekurzivních rovnic?

V analýze algoritmů se jedním z klíčových nástrojů pro pochopení výkonnosti algoritmů stává metoda zvaná analýza pomocí rekurzivních rovnic. Rekurzivní algoritmy, jako je například problém Hanojské věže nebo výpočet faktoriálu, jsou definovány tak, že se volají rekurzivně na menších podproblémech stejného typu. Tato struktura vede k tomu, že časová složitost algoritmu často závisí na tom, jak efektivně je možné řešit rekurzivní volání.

Základem analýzy složitosti rekurzivních algoritmů je formulace rekurzivní rovnice, která popisuje, jak se výpočet složitosti algoritmu vyvíjí s ohledem na velikost problému. Například u problému Hanojské věže, kde se provádí přesouvání disků mezi kolíky, můžeme definovat počet pohybů disků, M(n), pomocí rekurzivní rovnice.

Rekurzivní rovnice Hanojské věže

Pro problém Hanojské věže, kde je potřeba přesunout n disků z jednoho kolíku na jiný, využíváme následující rekurzivní rovnice:

  1. Pokud máme pouze jeden disk, jednoduše ho přesuneme z kolíku A na kolík C:

    M(1)=1M(1) = 1

  2. Pro více než jeden disk (n > 1) nejprve přesuneme n - 1 disků z kolíku A na kolík B pomocí kolíku C, potom přesuneme největší disk z kolíku A na kolík C, a nakonec přesuneme n - 1 disků z kolíku B na kolík C pomocí kolíku A. Tento postup lze zapsat rekurzivně jako:
    M(n)=2M(n1)+1M(n) = 2M(n - 1) + 1

Řešení rekurzivní rovnice

Pro zjištění složitosti algoritmu musíme tuto rekurzivní rovnici vyřešit. Existují různé metody, jak tuto rovnici rozřešit, přičemž dvě nejběžnější jsou metoda přímé substituce a metoda rekurzivního stromu.

Metoda přímé substituce

Metoda přímé substituce spočívá v tom, že postupně dosazujeme hodnoty do rekurzivní rovnice a hledáme vzorec pro M(n). Například:

  • Pro n=2n = 2:

    M(2)=2M(1)+1=2×1+1=3M(2) = 2M(1) + 1 = 2 \times 1 + 1 = 3

  • Pro n=3n = 3:

    M(3)=2M(2)+1=2×3+1=7M(3) = 2M(2) + 1 = 2 \times 3 + 1 = 7

  • Pro n=4n = 4:

    M(4)=2M(3)+1=2×7+1=15M(4) = 2M(3) + 1 = 2 \times 7 + 1 = 15

Pokud tento postup opakujeme pro stále větší hodnoty n, dojdeme k závěru, že složitost růstu je exponentní a odpovídá hodnotě M(n)=2n1M(n) = 2^n - 1. To znamená, že časová složitost tohoto algoritmu je Θ(2n)\Theta(2^n).

Použití indukce pro důkaz

Pokud chceme dokázat platnost rekurzivní rovnice, použijeme princip matematické indukce. Nejdříve pro n = 1 ověříme základní krok, že M(1)=1M(1) = 1, což je pravda. Poté předpokládáme, že pro n = k platí M(k)=2k1M(k) = 2^k - 1, a ukážeme, že pro n = k + 1 platí stejný vztah. Tento důkaz ukáže, že složitost algoritmu je skutečně Θ(2n)\Theta(2^n).

Význam analýzy pomocí rekurzivních rovnic

Je důležité si uvědomit, že analýza složitosti rekurzivních algoritmů nám umožňuje pochopit, jak rychle nebo pomalu bude algoritmus růst s rostoucí velikostí vstupu. Například v případě Hanojské věže, kde složitost roste exponenciálně, můžeme vidět, že i relativně malý n může vést k velmi velkému počtu pohybů disků, což činí algoritmus neefektivním pro velké hodnoty n.

Další metody analýzy

Kromě metody substituce existuje také metoda rekurzivního stromu, která vizualizuje strukturu rekurzivních volání. Každá úroveň stromu odpovídá jedné iteraci rekurzivního volání, a její velikost roste exponenciálně s n. Tato metoda je užitečná pro složitější případy, kde není možné jednoduše použít substituci.

Také existuje Masterova věta, která poskytuje elegantní způsob, jak analyzovat složitost algoritmů s dělením a ziskem, a která může být aplikována na širší třídu rekurzivních rovnic. Pomocí této věty lze získat konkrétní výsledky pro různé typy rekurzivních funkcí.

Co je potřeba mít na paměti?

Při analýze rekurzivních algoritmů pomocí rekurzivních rovnic je důležité vždy správně identifikovat základní případ a rekurzivní krok. Je také nezbytné si uvědomit, že rekurzivní algoritmy mohou mít velmi odlišné časové složitosti v závislosti na povaze problému. U některých algoritmů může složitost růst lineárně, u jiných může být exponenciální. Proto je analýza časové složitosti klíčová pro určení, zda je algoritmus praktický pro reálné použití na velkých datech.

Jak porovnat kvadratní prohledávání a dvojité hashování

V oblasti návrhu efektivních algoritmů pro zpracování dat v hashovacích tabulkách se často vyskytují dvě klíčové techniky: kvadratní prohledávání a dvojité hashování. Každá z těchto metod má své výhody a nevýhody, které je třeba zvážit při výběru nejvhodnějšího přístupu pro konkrétní aplikace.

Dvojité hashování je založeno na použití dvou hashovacích funkcí. První funkce určuje počáteční místo pro hledání v tabulce, zatímco druhá poskytuje metodu pro postupné hledání v případě kolize. Výhodou dvojitého hashování je, že je schopné efektivněji distribuovat klíče do tabulky, čímž se zvyšuje pravděpodobnost, že kolize nebudou příliš časté. Na druhou stranu, dvojité hashování vyžaduje implementaci dvou hashovacích funkcí, což zvyšuje složitost kódu. Navíc je tato technika složitější pro implementaci ve srovnání s kvadratním prohledáváním, což může být pro některé vývojáře výzvou.

Naopak kvadratní prohledávání je o něco jednodušší a efektivnější v případech, kdy je potřeba vyřešit kolize. Tento přístup používá jediné hashovací funkce a při kolizi prohledává tabulku podle kvadratického vzoru, tedy s nárůstem délky prohledávaných vzdáleností (např. 1, 4, 9, 16 atd.). Díky této jednoduchosti je kvadratní prohledávání rychlejší než dvojité hashování, ale může mít horší výkon při větších tabulkách nebo v případech, kdy dochází k častým kolizím.

Při výběru mezi těmito dvěma metodami závisí volba na specifických požadavcích aplikace. Pokud je důležitá jednoduchost implementace a výkon při nižší frekvenci kolizí, kvadratní prohledávání je ideálním řešením. V případě potřeby robustnosti a lepší distribuce klíčů při častějších kolizích se vyplatí sáhnout po dvojitém hashování.

Hashovací funkce a jejich typy

K dosažení co nejefektivnějšího umístění dat do hashovací tabulky je třeba zvolit správnou hashovací funkci. Existuje několik různých metod pro návrh těchto funkcí:

  1. Metoda dělení: Tato metoda spočívá v dělení klíče a zjišťování zbytku po dělení s velikostí tabulky. Výsledný zbytek určuje místo, kde bude hodnota uložena. Například, pokud máme klíč 1522756 a velikost tabulky je 10000, výsledná hashovací hodnota bude 1522756 % 10000 = 2756.

  2. Metoda středového čtverce: V této metodě se klíč nejprve umocní na druhou, poté se získají střední číslice z výsledku. Například, pokud máme klíč 1212, jeho čtverec bude 1468944 a střední číslice (689) se použijí jako nový hash.

  3. Metoda skládání: Tato metoda spočívá v rozdělení čísla na několik částí a jejich sečítání. Například, pro klíč 1468944 a adresu o délce 3 číslic rozdělíme číslo na části 1, 468 a 944, a jejich součet dává výsledný hash 413.

Tyto různé hashovací funkce jsou základem pro účinné ukládání dat v hashovacích tabulkách a pro minimalizaci rizika kolizí. Výběr správné metody závisí na specifických potřebách aplikace a požadavcích na rychlost a účinnost.

Význam algoritmů a datových struktur v informatice

Algoritmy a datové struktury tvoří páteř jakéhokoli kvalitního programu. Bez správného výběru datových struktur a efektivních algoritmů může být výkon aplikace značně ovlivněn. Významným příkladem moderního využití pokročilých technik, jako je hashování, je služba Google Search, která používá hashing k rychlému vyhledávání a indexování obrovského množství dat.

Efektivní algoritmy nejen že zajišťují rychlé a přesné zpracování dat, ale také šetří prostředky, jako je čas a paměť. Například různé metody řazení a vyhledávání mají specifické výhody a nevýhody v závislosti na typu a velikosti dat. Mezi nejběžnější techniky patří vnitřní řazení pro menší množství dat, zatímco pro velké objemy dat se používají metody jako externí řazení, které efektivně využívají sekundární úložné zařízení.

Důležité je také chápat, že analýza algoritmů se nezaměřuje pouze na časovou složitost, ale i na prostorovou složitost. Výběr vhodné datové struktury a algoritmu může výrazně ovlivnit nejen výkon, ale i spotřebu paměti aplikace.

Další důležité aspekty

Při práci s datovými strukturami a algoritmy je nezbytné mít na paměti několik klíčových aspektů, které mohou ovlivnit výkon celého systému. Mezi nejdůležitější faktory patří:

  • Optimalizace pro konkrétní případy použití: Například, pro aplikace s velkým množstvím čtení a malým množstvím zápisu mohou být vhodné jiné techniky než pro aplikace s častými aktualizacemi.

  • Testování a ladění: I nejlepší algoritmy a datové struktury mohou mít problémy, pokud nejsou správně implementovány nebo testovány. Je důležité věnovat čas ladění kódu, abyste zajistili, že aplikace bude nejen rychlá, ale i stabilní.

  • Závislost na hardware a prostředí: Efektivita algoritmů se může lišit v závislosti na specifikacích hardwaru a operačního systému, na kterém běží. To je důvod, proč je třeba provádět testy a optimalizace i s ohledem na konkrétní hardware, na kterém aplikace poběží.

Jak najít minimální kostní strom v grafu?

Graf může mít mnoho различных остенних деревьев. Каждый из них имеет своё применение, если мы придадим каждому ребру определённый вес. Этот вес ребра представляет собой числовую функцию стоимости, которая указывает, насколько выгодно или невыгодно включать это ребро в остовное дерево. Важно отметить, что минимальное остовное дерево (или минимальное дерево с наименьшей стоимостью) — это остовное дерево, вес которого меньше или равен весам всех других остовных деревьев. В общем случае любой ненаправленный граф имеет минимальное остовное лесное множество, представляющее собой объединение минимальных остовных деревьев для его связных компонент.

Когда мы проходим через связный граф с помощью алгоритмов поиска в глубину (DFS) или поиска в ширину (BFS), начиная с любой вершины, все вершины графа будут посещены. В этом случае ребра графа делятся на два множества: T (для ребер дерева) и B (для обратных ребер), где T — это множество ребер, использованных или пройденных в процессе поиска, а B — множество оставшихся ребер. Ребра из множества T образуют дерево, включающее все вершины графа G. Если в это остовное дерево T добавить любое из ребер из множества B, то возникнет цикл. Дерево — это связный граф без циклов, то есть в дереве не может быть самопетли. С другой стороны, граф может содержать цикл.

Таким образом, дерево T называется остовным деревом связного графа G, если T является подграфом G и содержит все вершины G. Остовное дерево определяется только для связного графа, так как дерево всегда связано. Но если граф разорван, то для него невозможно найти связный подграф с n вершинами. Для заданного графа наша цель — найти остовное дерево. Мы стремимся найти минимальное остовное дерево с наименьшей стоимостью, что подразумевает выбор подмножества ребер с минимальными затратами. Это также соответствует критериям жадного алгоритма, то есть оптимальной подструктуре и жадному выбору. На каждом шаге жадного алгоритма выбирается одно ребро. Поскольку количество ребер конечное, то и количество шагов конечное и равно количеству ребер. Таким образом, вычислительное время этого алгоритма — O(n), где n — это количество ребер в графе.

Процесс нахождения минимального остовного дерева может следовать определённым правилам:

  1. Если граф G не содержит цикла, то он сам является остовным деревом.

  2. Если граф G содержит цикл, то нужно удалить одно из ребер цикла, чтобы оставшийся граф остался связанным, тем самым образуя остовное дерево.

  3. Если циклов несколько, то повторяем действия до тех пор, пока не будет удалено ребро из последнего цикла, оставляя связанный граф без циклов, который включает все вершины G.

  4. Если граф имеет n вершин, то для связного графа обязательно должно быть как минимум n-1 ребер, и такие графы всегда являются деревьями.

  5. Граф может иметь любое количество остовных деревьев.

Кроме того, остовные деревья обладают рядом свойств:

  1. Множество остовных деревьев: может быть несколько минимальных остовных деревьев с одинаковым весом. Если все веса ребер одинаковы, то каждое остовное дерево будет минимальным.

  2. Уникальность: если каждому ребру в графе назначен уникальный вес, то существует только одно минимальное остовное дерево.

  3. Минимальная стоимость подграфа: если ребра графа имеют положительные веса, минимальное остовное дерево — это минимальный подграф стоимости, который соединяет все вершины.

  4. Свойство цикла: если в графе существует цикл, где одно из ребер имеет вес, больше чем другие ребра цикла, то это ребро не может быть частью минимального остовного дерева.

  5. Полезность: минимальные остовные деревья можно вычислить быстро и легко, чтобы получить оптимальные решения. Эти деревья создают разреженный подграф, который многое может рассказать о самом графе.

  6. Простота: минимальное остовное дерево взвешенного графа — это не что иное, как остовное дерево графа, которое включает в себя n-1 ребер с минимальной общей стоимостью. Для графа без весов любое остовное дерево будет минимальным.

  7. Свойство разреза: для любого разреза в графе, если вес ребра E этого разреза меньше, чем вес других ребер, то это ребро будет присутствовать во всех минимальных остовных деревьях графа.

Минимальные остовные деревья имеют различные области применения:

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

  2. Они применяются в проектировании коммуникационных сетей, помогая найти соединение с минимальными затратами.

  3. Минимальные остовные деревья используются для оптимизации авиамаршрутов между городами, где под оптимальностью понимается минимальный путь, наименьшая стоимость или кратчайшее время.

  4. Они находят применение в нахождении и оптимизации независимого набора уравнений цепей для электрической сети. Для начала строится остовное дерево для сети, затем поочередно вводятся ребра из множества B (т.е. не входящие в остовное дерево). Введение каждого ребра создает цикл, который затем используется для составления уравнений с использованием закона Кирхгофа. Эти циклы независимы, так как каждый содержит хотя бы одно ребро из B, которое не соединяется с ребрами других циклов, что делает уравнения независимыми.

  5. Остовное дерево — это минимальный подграф G′ графа G, такой что V(G′) = V(G) и G′ соединён, при этом минимальность подграфа означает, что количество ребер минимально. Любой связный граф с n вершинами должен содержать как минимум (n – 1) ребер, и все связные графы с (n – 1) ребрами являются деревьями. Если узлы G представляют города, а ребра представляют возможные коммуникационные связи между двумя городами, то минимальное количество связей, необходимое для соединения n городов, равно n – 1.

Для нахождения минимального остовного дерева часто используются два жадных алгоритма: алгоритм Краскала и алгоритм Прима. Эти алгоритмы мы рассмотрим в следующих разделах.

Jak optimalizovat násobení matic pomocí dynamického programování?

Násobení řetězce matic je problém, který lze efektivně vyřešit pomocí dynamického programování. Tento přístup je možný, protože tento problém splňuje oba základní požadavky pro použití dynamického programování: optimální podstruktura a překrývající se podproblémy.

Definice problému:

Máme řetězec n matic A1,A2,,AnA_1, A_2, \dots, A_n, které je třeba vynásobit. Abychom odstranili možné nejednoznačnosti, je třeba správně seřadit závorky mezi maticemi, neboť existuje více způsobů, jak tento řetězec zapsat. Různé způsoby závorkování však mohou vést k různým nákladům na výpočet. Cílem je najít ten nejefektivnější způsob závorkování, který minimalizuje počet skalárních násobení.

Například pro řetězec tří matic A1,A2,A3A_1, A_2, A_3 existují dva možné způsoby závorkování:

  1. (A1,(A2A3))(A_1, (A_2 A_3))

  2. ((A1A2),A3)((A_1 A_2), A_3)

Pokud mají matice rozměry 2×3, 3×4 a 4×3, výpočty pro jednotlivé závorkování jsou následující:

  • Příklad 1: Pro (A1(A2A3))(A_1 (A_2 A_3)) jsou počty násobení:

    • Násobení A2×A3A_2 \times A_3: 3×4×3=363 \times 4 \times 3 = 36

    • Násobení A1×BA_1 \times B (kde BB je výsledná matice z předchozího výpočtu): 2×3×3=182 \times 3 \times 3 = 18

    • Celkový počet násobení: 36+18=5436 + 18 = 54

  • Příklad 2: Pro ((A1A2),A3)((A_1 A_2), A_3) jsou počty násobení:

    • Násobení A1×A2A_1 \times A_2: 2×3×4=242 \times 3 \times 4 = 24

    • Násobení A3×BA_3 \times B (kde BB je výsledná matice z předchozího výpočtu): 2×4×3=242 \times 4 \times 3 = 24

    • Celkový počet násobení: 24+24=4824 + 24 = 48

Jak vidíme, i když obě metody vedou k výsledné matici stejného rozměru, náklady na výpočet se liší. Nejnižší počet skalárních násobení představuje optimální řešení.

Algoritmus pro násobení matic

Pro dva matice AA a BB, kde výsledná matice CC je jejich součin, můžeme použít následující algoritmus:

  1. Ověření kompatibility matic:
    Pokud se počet sloupců matice AA nerovná počtu řádků matice BB, pak násobení není možné a algoritmus vrátí chybu.

  2. Násobení matic:

    Pro matice AA a BB, které mají kompatibilní rozměry, provedeme skalární součet:

    C[i,j]=k=1nA[i,k]×B[k,j]C[i,j] = \sum_{k=1}^{n} A[i,k] \times B[k,j]

    Tento proces se opakuje pro všechny prvky matice CC.

Cílem tohoto problému je najít optimální způsob závorkování, který minimalizuje počet skalárních násobení pro daný řetězec matic.

Dynamické programování na maticovém řetězci
Dynamické programování je metodou, kterou lze využít k efektivnímu řešení tohoto problému. Tento přístup spočívá ve strategickém dělení problému na menší podproblémy, jejichž řešení pak skládáme do celkového řešení.

Krok 1: Struktura optimálního závorkování

Pro podřetězec matic Ai,Ai+1,,AjA_i, A_{i+1}, \dots, A_j existuje optimální způsob závorkování, který minimalizuje počet skalárních násobení. Pokud i=ji = j, máme pouze jednu matici a není potřeba závorkování. Pokud i<ji < j, rozdělíme tento řetězec na dvě části, AiAkA_i \dots A_k a Ak+1AjA_{k+1} \dots A_j, a rekurzivně vypočítáme optimální náklady pro obě části.

Celkový náklad na násobení řetězce matic je dán následujícím vzorcem:

Celkovyˊ naˊklad=Naˊklady pro AiAk+Naˊklady pro Ak+1Aj+Naˊklady na naˊsobenıˊ dvou vyˊslednyˊch matic.\text{Celkový náklad} = \text{Náklady pro } A_i \dots A_k + \text{Náklady pro } A_{k+1} \dots A_j + \text{Náklady na násobení dvou výsledných matic}.

Krok 2: Rekurzivní definice optimálního řešení

Pro maticový řetězec AiAjA_i \dots A_j definujeme m[i,j]m[i,j] jako minimální počet skalárních násobení potřebných k výpočtu tohoto podřetězce. Pokud i=ji = j, pak m[i,j]=0m[i,j] = 0. Pokud i<ji < j, použijeme optimální strukturu a vypočteme m[i,j]m[i,j] jako minimum ze všech možných dělení řetězce na AiAkA_i \dots A_k a Ak+1AjA_{k+1} \dots A_j.

Krok 3: Výpočet optimálních nákladů
Pomocí dynamického programování použijeme tabulkový přístup, kde pro každý podřetězec AiAjA_i \dots A_j uchováváme hodnotu m[i,j]m[i,j], která udává minimální počet skalárních násobení. Navíc potřebujeme uchovávat indexy kk, kde se optimálně dělí řetězec, abychom později mohli rekonstruovat optimální závorkování.

Krok 4: Rekonstrukce optimálního závorkování

Po výpočtu tabulek m[i,j]m[i,j] a s[i,j]s[i,j] použijeme rekurzivní algoritmus k rekonstrukci optimálního závorkování. Tento algoritmus, nazvaný PRINT_OPTIMAL_PARENS, pracuje tak, že pro každý podřetězec vypíše matici nebo rekurzivně zavolá funkci pro menší podřetězce.

Tento algoritmus má časovou složitost O(n3)O(n^3) a prostorovou složitost O(n2)O(n^2), kde nn je počet matic v řetězci.

Důležité aspekty pro čtenáře:

Je nezbytné chápat, že samotné řešení problému násobení matic nezahrnuje pouze výpočet jejich součinu, ale i optimální způsob jejich závorkování, který minimalizuje výpočetní náklady. S tímto algoritmem se tedy dostáváme k efektivnímu řešení problému, který by jinak mohl být výpočetně velmi náročný. Zvlášť u dlouhých řetězců matic je optimální přístup nezbytný k dosažení praktického výkonu.