From 30186948071bdeb6b91e4059d45837c66888c935 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 4 Jun 2019 21:35:25 +0000 Subject: [PATCH 001/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- .travis.yml | 10 + README.md | 3 + build.sh | 3 + en/index.md | 1 + img/.editorial.html.swp | Bin 0 -> 12288 bytes .../aho-corasick-checkpoint.ipynb | 146 +++ .../bayans-checkpoint.ipynb | 533 ++++++++++ .../bitset-checkpoint.ipynb | 241 +++++ .../centroid-checkpoint.ipynb | 249 +++++ img/.ipynb_checkpoints/cuda-checkpoint.ipynb | 475 +++++++++ .../dp-intro-checkpoint.ipynb | 109 ++ .../dp-optimizations-checkpoint.ipynb | 288 ++++++ .../fenwick-checkpoint.ipynb | 285 ++++++ img/.ipynb_checkpoints/games-checkpoint.ipynb | 331 ++++++ .../geometry-checkpoint.ipynb | 421 ++++++++ .../hashing-checkpoint.ipynb | 426 ++++++++ img/.ipynb_checkpoints/hld-checkpoint.ipynb | 186 ++++ img/.ipynb_checkpoints/index-checkpoint.ipynb | 81 ++ .../karatsuba-checkpoint.ipynb | 122 +++ img/.ipynb_checkpoints/lca-checkpoint.ipynb | 339 +++++++ .../linalg-checkpoint.ipynb | 453 +++++++++ .../matching-checkpoint.ipynb | 234 +++++ .../matroid-checkpoint.ipynb | 178 ++++ .../mincost-maxflow-checkpoint.ipynb | 239 +++++ .../segtree-checkpoint.ipynb | 680 +++++++++++++ .../sparse-table-checkpoint.ipynb | 119 +++ img/.ipynb_checkpoints/sse-checkpoint.ipynb | 404 ++++++++ .../strings-checkpoint.ipynb | 448 ++++++++ .../suffix-array-checkpoint.ipynb | 158 +++ img/.ipynb_checkpoints/treap-checkpoint.ipynb | 956 ++++++++++++++++++ img/.ipynb_checkpoints/trie-checkpoint.ipynb | 129 +++ ru/aho-corasick.md | 91 ++ ru/annealing.md | 138 +++ ru/bayans.md | 273 +++++ ru/bitset.md | 130 +++ ru/centroid.md | 167 +++ ru/convex-hulls.md | 201 ++++ ru/cross-entropy.md | 22 + ru/cuda.md | 492 +++++++++ ru/dp-intro.md | 40 + ru/dp-optimizations.md | 167 +++ ru/dp_intro.md | 42 + ru/dp_optimizations.md | 167 +++ ru/fenwick.md | 162 +++ ru/games.md | 197 ++++ ru/geometry.md | 237 +++++ ru/hashing.md | 224 ++++ ru/hld.md | 120 +++ ru/index.md | 51 + ru/ioi.md | 70 ++ ru/karatsuba.md | 69 ++ ru/lca.md | 193 ++++ ru/linalg.md | 315 ++++++ ru/matching.md | 139 +++ ru/matroid.md | 102 ++ ru/mincost-maxflow.md | 148 +++ ru/mst.md | 209 ++++ ru/pollard.md | 142 +++ ru/probability.md | 86 ++ ru/reciprocal.md | 128 +++ ru/segtree.md | 369 +++++++ ru/sparse-table.md | 53 + ru/sse.md | 364 +++++++ ru/stress-test.md | 51 + ru/strings.md | 206 ++++ ru/suffix-array.md | 109 ++ ru/treap.md | 460 +++++++++ ru/trie.md | 71 ++ web/en/index.html | 14 + web/img/.editorial.html.swp | Bin 0 -> 12288 bytes .../aho-corasick-checkpoint.ipynb | 146 +++ .../bayans-checkpoint.ipynb | 533 ++++++++++ .../bitset-checkpoint.ipynb | 241 +++++ .../centroid-checkpoint.ipynb | 249 +++++ .../.ipynb_checkpoints/cuda-checkpoint.ipynb | 475 +++++++++ .../dp-intro-checkpoint.ipynb | 109 ++ .../dp-optimizations-checkpoint.ipynb | 288 ++++++ .../fenwick-checkpoint.ipynb | 285 ++++++ .../.ipynb_checkpoints/games-checkpoint.ipynb | 331 ++++++ .../geometry-checkpoint.ipynb | 421 ++++++++ .../hashing-checkpoint.ipynb | 426 ++++++++ .../.ipynb_checkpoints/hld-checkpoint.ipynb | 186 ++++ .../.ipynb_checkpoints/index-checkpoint.ipynb | 81 ++ .../karatsuba-checkpoint.ipynb | 122 +++ .../.ipynb_checkpoints/lca-checkpoint.ipynb | 339 +++++++ .../linalg-checkpoint.ipynb | 453 +++++++++ .../matching-checkpoint.ipynb | 234 +++++ .../matroid-checkpoint.ipynb | 178 ++++ .../mincost-maxflow-checkpoint.ipynb | 239 +++++ .../segtree-checkpoint.ipynb | 680 +++++++++++++ .../sparse-table-checkpoint.ipynb | 119 +++ .../.ipynb_checkpoints/sse-checkpoint.ipynb | 404 ++++++++ .../strings-checkpoint.ipynb | 448 ++++++++ .../suffix-array-checkpoint.ipynb | 158 +++ .../.ipynb_checkpoints/treap-checkpoint.ipynb | 956 ++++++++++++++++++ .../.ipynb_checkpoints/trie-checkpoint.ipynb | 129 +++ web/index.html | 1 + web/pandoc.css | 375 +++++++ web/ru/aho-corasick.html | 45 + web/ru/annealing.html | 141 +++ web/ru/bayans.html | 191 ++++ web/ru/bitset.html | 55 + web/ru/centroid.html | 80 ++ web/ru/convex-hulls.html | 175 ++++ web/ru/cross-entropy.html | 23 + web/ru/cuda.html | 316 ++++++ web/ru/dp-intro.html | 45 + web/ru/dp-optimizations.html | 87 ++ web/ru/dp_intro.html | 46 + web/ru/dp_optimizations.html | 87 ++ web/ru/fenwick.html | 83 ++ web/ru/games.html | 102 ++ web/ru/geometry.html | 138 +++ web/ru/hashing.html | 120 +++ web/ru/hld.html | 59 ++ web/ru/index.html | 51 + web/ru/ioi.html | 46 + web/ru/karatsuba.html | 53 + web/ru/lca.html | 88 ++ web/ru/linalg.html | 180 ++++ web/ru/matching.html | 74 ++ web/ru/matroid.html | 101 ++ web/ru/mincost-maxflow.html | 102 ++ web/ru/mst.html | 182 ++++ web/ru/pollard.html | 112 ++ web/ru/probability.html | 81 ++ web/ru/reciprocal.html | 68 ++ web/ru/segtree.html | 131 +++ web/ru/sparse-table.html | 44 + web/ru/sse.html | 365 +++++++ web/ru/stress-test.html | 66 ++ web/ru/strings.html | 147 +++ web/ru/suffix-array.html | 91 ++ web/ru/treap.html | 162 +++ web/ru/trie.html | 44 + 135 files changed, 27053 insertions(+) create mode 100644 .travis.yml create mode 100644 README.md create mode 100755 build.sh create mode 100644 en/index.md create mode 100644 img/.editorial.html.swp create mode 100644 img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/bayans-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/bitset-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/centroid-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/cuda-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/fenwick-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/games-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/geometry-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/hashing-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/hld-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/index-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/lca-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/linalg-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/matching-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/matroid-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/segtree-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/sse-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/strings-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/treap-checkpoint.ipynb create mode 100644 img/.ipynb_checkpoints/trie-checkpoint.ipynb create mode 100644 ru/aho-corasick.md create mode 100644 ru/annealing.md create mode 100644 ru/bayans.md create mode 100644 ru/bitset.md create mode 100644 ru/centroid.md create mode 100644 ru/convex-hulls.md create mode 100644 ru/cross-entropy.md create mode 100644 ru/cuda.md create mode 100644 ru/dp-intro.md create mode 100644 ru/dp-optimizations.md create mode 100644 ru/dp_intro.md create mode 100644 ru/dp_optimizations.md create mode 100644 ru/fenwick.md create mode 100644 ru/games.md create mode 100644 ru/geometry.md create mode 100644 ru/hashing.md create mode 100644 ru/hld.md create mode 100644 ru/index.md create mode 100644 ru/ioi.md create mode 100644 ru/karatsuba.md create mode 100644 ru/lca.md create mode 100644 ru/linalg.md create mode 100644 ru/matching.md create mode 100644 ru/matroid.md create mode 100644 ru/mincost-maxflow.md create mode 100644 ru/mst.md create mode 100644 ru/pollard.md create mode 100644 ru/probability.md create mode 100644 ru/reciprocal.md create mode 100644 ru/segtree.md create mode 100644 ru/sparse-table.md create mode 100644 ru/sse.md create mode 100644 ru/stress-test.md create mode 100644 ru/strings.md create mode 100644 ru/suffix-array.md create mode 100644 ru/treap.md create mode 100644 ru/trie.md create mode 100644 web/en/index.html create mode 100644 web/img/.editorial.html.swp create mode 100644 web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/games-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/hld-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/index-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/lca-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/matching-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/sse-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/strings-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/treap-checkpoint.ipynb create mode 100644 web/img/.ipynb_checkpoints/trie-checkpoint.ipynb create mode 100644 web/index.html create mode 100644 web/pandoc.css create mode 100644 web/ru/aho-corasick.html create mode 100644 web/ru/annealing.html create mode 100644 web/ru/bayans.html create mode 100644 web/ru/bitset.html create mode 100644 web/ru/centroid.html create mode 100644 web/ru/convex-hulls.html create mode 100644 web/ru/cross-entropy.html create mode 100644 web/ru/cuda.html create mode 100644 web/ru/dp-intro.html create mode 100644 web/ru/dp-optimizations.html create mode 100644 web/ru/dp_intro.html create mode 100644 web/ru/dp_optimizations.html create mode 100644 web/ru/fenwick.html create mode 100644 web/ru/games.html create mode 100644 web/ru/geometry.html create mode 100644 web/ru/hashing.html create mode 100644 web/ru/hld.html create mode 100644 web/ru/index.html create mode 100644 web/ru/ioi.html create mode 100644 web/ru/karatsuba.html create mode 100644 web/ru/lca.html create mode 100644 web/ru/linalg.html create mode 100644 web/ru/matching.html create mode 100644 web/ru/matroid.html create mode 100644 web/ru/mincost-maxflow.html create mode 100644 web/ru/mst.html create mode 100644 web/ru/pollard.html create mode 100644 web/ru/probability.html create mode 100644 web/ru/reciprocal.html create mode 100644 web/ru/segtree.html create mode 100644 web/ru/sparse-table.html create mode 100644 web/ru/sse.html create mode 100644 web/ru/stress-test.html create mode 100644 web/ru/strings.html create mode 100644 web/ru/suffix-array.html create mode 100644 web/ru/treap.html create mode 100644 web/ru/trie.html diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..57bf0a9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +before_script: sudo apt-get install pandoc +script: ./build.sh + +deploy: + provider: pages + skip_cleanup: true + github_token: $GITHUB_TOKEN + keep_history: true + on: + branch: master diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed8aea0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Algorithmica.org + +test diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..829ed55 --- /dev/null +++ b/build.sh @@ -0,0 +1,3 @@ +cp -r img web/img +mkdir web/{ru,en} +find {ru,en} -iname "*.md" -type f -exec sh -c 'pandoc "${0}" -o "web/${0%.md}.html" --css /pandoc.css --standalone --mathjax' {} \; diff --git a/en/index.md b/en/index.md new file mode 100644 index 0000000..e1ebd34 --- /dev/null +++ b/en/index.md @@ -0,0 +1 @@ +some english index diff --git a/img/.editorial.html.swp b/img/.editorial.html.swp new file mode 100644 index 0000000000000000000000000000000000000000..eed31bfbef5ecb81e88ef7890edd52f724b5de2a GIT binary patch literal 12288 zcmeI%ze@u#6bJBE2RBi4a1!Dvbn4~&F^GCjT?7RY>g1B9&tA&?sJW*F2ldbJPwAiM zUG;_zb}0yjZyDC|ZDxC?rLI45~fB*y_009U< z00Izz00bZ~FJJ~lUz&EO42z z7?p^OJN*3OUgN;?ljkxClr3`MT;d?QP`&$`xLwaW*E;7(XTtr`35T}G+JVwn^bO^g BJ0$=B literal 0 HcmV?d00001 diff --git a/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb b/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb new file mode 100644 index 0000000..3679909 --- /dev/null +++ b/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Алгоритм Ахо-Корасик\n", + "\n", + "Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время.\n", + "\n", + "Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик.\n", + "\n", + "\n", + "\n", + "Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы.\n", + "\n", + "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", + "\n", + "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", + "\n", + "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", + "\n", + "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Зачем это нужно\n", + "\n", + "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", + "\n", + "Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор.\n", + "\n", + "Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Алгоритм Ахо-Корасик*\n", + "\n", + "Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой.\n", + "\n", + "**Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход.\n", + "\n", + "**Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда.\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "const int k = 26;\n", + "\n", + "struct Vertex {\n", + " Vertex *to[k] = {0}, *go[k] = {0};\n", + " Vertex *link = 0, *p;\n", + " int pch;\n", + " Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; }\n", + "};\n", + "\n", + "Vertex *root = new Vertex(-1, 0);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "void add_string (string s) {\n", + " Vertex *v = root;\n", + " for (char _c : s) {\n", + " c -= 'a';\n", + " if (!v->to[c])\n", + " v->to[c] = new Vertex(c, v);\n", + " v = v->to[c];\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Vertex* go (Vertex *v, int c);\n", + "\n", + "Vertex* link (Vertex *v) {\n", + " if (!v->link) {\n", + " if (v == root || v->p == root) v->link = root;\n", + " else v->link = go(link(v->p), v->pch);\n", + " }\n", + " return v->link;\n", + "}\n", + "\n", + "Vertex* go (Vertex *v, int c) {\n", + " if (!v->go[c]) {\n", + " if (v->to[c]) v->go[c] = v->to[c];\n", + " else if (v == root) v->go[c] = root;\n", + " else v->go[c] = go(link(v), c);\n", + " }\n", + " return v->go[c];\n", + "}" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/bayans-checkpoint.ipynb b/img/.ipynb_checkpoints/bayans-checkpoint.ipynb new file mode 100644 index 0000000..87cb744 --- /dev/null +++ b/img/.ipynb_checkpoints/bayans-checkpoint.ipynb @@ -0,0 +1,533 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Красивые идейные задачи\n", + "\n", + "Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда.\n", + "\n", + "Задачи идут в порядке вспоминания, то есть в весьма рандомном." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Попугаи\n", + "\n", + "Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Минимум и максимум\n", + "\n", + "Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Перестановка\n", + "\n", + "Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Выпуклая оболочка\n", + "Требуется отвечать на 2 типа запросов:\n", + "\n", + "1. Добавить точку в выпуклую оболочку.\n", + "2. Проверить, лежит ли точка внутри выпуклой оболочки.\n", + "\n", + "Обе операции онлайн за $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Геометрическая прогрессия\n", + "\n", + "Найдите способ посчитать $\\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Покемоны\n", + "\n", + "В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \\times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \\to b$ и $b \\to c$ не следует, что $a \\to c$.\n", + "\n", + "У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Сортировка\n", + "Можно ли отсортировать\n", + "* 5 камней за 8 взвешиваний?\n", + "* 5 камней за 7 взвешиваний?\n", + "* 20 камней за 60 взвешиваний?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Точки в круге\n", + "\n", + "Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Замкнутые ломаные\n", + "\n", + "Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Неубывающий массив\n", + "\n", + "Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Чётный цикл\n", + "\n", + "Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## $k$-ая порядковая статистика\n", + "\n", + "Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Доминирующий элемент\n", + "\n", + "Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз. Время работы $O((n+m) \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Разрушение дерева\n", + "\n", + "Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## $k$-ый элемент на отрезке\n", + "\n", + "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Различные числа на отрезке\n", + "\n", + "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\\sqrt{n})$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Физкультура\n", + "\n", + "Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов:\n", + "\n", + "1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя)\n", + "2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно)\n", + "\n", + "При этом приказ может частично отменить действие предыдущих приказов.\n", + "\n", + "После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Нулевая сумма\n", + "\n", + "Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мета-задача\n", + "\n", + "В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ниточка\n", + "\n", + "В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Пельмени\n", + "\n", + "Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## От нуля до единицы\n", + "\n", + "Дан следующий код:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "x = 0\n", + "while x < 1:\n", + " x += random()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Требуется посчитать матожидание `x`.\n", + "\n", + "(`random` в питоне возвращает случайное действительное число от 0 до 1.)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Площадь\n", + "\n", + "Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Окружности\n", + "\n", + "Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна\n", + "\n", + "* соприкасаться с *внешней*,\n", + "* соприкасаться с предыдущей (($k-1$)-ой),\n", + "* иметь при этом максимальный радиус.\n", + "\n", + "Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Блеф\n", + "\n", + "Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может:\n", + "\n", + "* Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается.\n", + "* Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом.\n", + "\n", + "С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Достижимость\n", + "Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Нумизмат\n", + "\n", + "Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой.\n", + "\n", + "Придумайте любой полиномиальный алгоритм." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Принцесса\n", + "\n", + "В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве.\n", + "\n", + "Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством.\n", + "\n", + "У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми.\n", + "\n", + "Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу.\n", + "\n", + "Асимптотика $O(n^2)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Спираль\n", + "\n", + "Определим спираль $(2n+1) \\times (2n+1)$ как матрицу следующего вида:\n", + "\n", + "$$\n", + "\\begin{matrix}\n", + "21 & 22 & 23 & 24 & 25 \\\\\n", + "20 & 7 & 8 & 9 & 10 \\\\\n", + "19 & 6 & 1 & 2 & 11 \\\\\n", + "18 & 5 & 4 & 3 & 12 \\\\\n", + "17 & 16 & 15 & 14 & 13 \\\\\n", + "\\end{matrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$).\n", + "\n", + "$q \\leq 100$, $n \\leq 10^9$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Польский лабиринт\n", + "\n", + "Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`.\n", + "\n", + "![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png)\n", + "\n", + "Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам.\n", + "\n", + "$n \\leq 10^5$, изначальные координаты туристов до $10^9$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Нимные подмножества\n", + "\n", + "Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \\subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \\subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Баланс степеней\n", + "\n", + "Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Два пути\n", + "\n", + "Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Пьяница\n", + "\n", + "Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ксоровый рюкзак\n", + "\n", + "Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Иван Сусанин\n", + "\n", + "Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш.\n", + "\n", + "- Камиль руководит армией днём и водит армию по *дорогам*.\n", + "- Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. \n", + "\n", + "Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск.\n", + "\n", + "Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу.\n", + "Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось.\n", + "\n", + "Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Варенье\n", + "\n", + "В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм.\n", + "\n", + "Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья.\n", + "\n", + "Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной.\n", + "\n", + "$n, m \\leq 10^5$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Лабиринт\n", + "\n", + "Серёжа потерялся в лабиринте $n \\times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт.\n", + "\n", + "Придумайте любой полиномиальный алгоритм." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Обезьяна\n", + "\n", + "Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ожидание минимума\n", + "\n", + "Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин.\n", + "\n", + "Придумайте любой точный полиномиальный алгоритм." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Шумный ксор\n", + "\n", + "Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \\frac15$ быть единичным, то есть каждый бит $x \\oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = # ...\n", + "\n", + "def mask(p=0.2):\n", + " r = 0\n", + " for i in range(32):\n", + " if random.random() < p:\n", + " r += 2**i\n", + " return r\n", + "\n", + "def query(y):\n", + " return bin(x ^ y ^ mask()).count('1')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ваша задача — отгадать число, используя не более 10000 попыток." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/bitset-checkpoint.ipynb b/img/.ipynb_checkpoints/bitset-checkpoint.ipynb new file mode 100644 index 0000000..30b7828 --- /dev/null +++ b/img/.ipynb_checkpoints/bitset-checkpoint.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Битовое сжатие\n", + "\n", + "* Из-за него в «асимптотиках» появляется `/64`\n", + "* На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов\n", + "* `bitset` есть в stl; говорят, самописный быстрее" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости.\n", + "\n", + "Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. \n", + "\n", + "Работать с ним нужно вот так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "const int lim = 1000;\n", + "bitset b; // создать битсет размера lim (должно быть константой)\n", + "b.set(); // заполнить единицами\n", + "b.reset(); // заполнить нулями\n", + "b.flip(); // заменить единички на нули и наоборот\n", + "b.count(); // посчитать число единичек\n", + "cout << b; // вывести битовую строку" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Рюкзак\n", + "\n", + "Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака.\n", + "\n", + "Обычно его решают так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "bool dp[lim] = {}; // так можно его заполнить нулями\n", + "dp[0] = 1;\n", + "for (int i = 0; i < n; i++)\n", + " for (int x = lim - a[i]; x >= 0; x--)\n", + " dp[x + a[i]] |= dp[x];" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "…а с битсетом оно разгоняется так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "bitset b;\n", + "b[0] = 1;\n", + "for (int i = 0; i < n; i++)\n", + " b |= b << a[i];" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Цикл длины 3\n", + "\n", + "Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\\frac{n^3}{64})$ следующим образом:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "bitset g[maxn]; // матрица смежности\n", + "for (int a = 0; a < n; a++) {\n", + " for (int b = 0; b < n; b++) {\n", + " if (g[a][b] && (~g[a] & g[b]).any()) {\n", + " // цикл найден\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Перемножение матриц\n", + "\n", + "Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "typedef bitset t;\n", + "typedef array matrix;\n", + "\n", + "matrix operator* (matrix a, matrix b) {\n", + " matrix c;\n", + " for(int i = 0; i < n; i++)\n", + " for(int j = 0; j < n; j++)\n", + " if(a[i][j])\n", + " c[i] |= b[j];\n", + " return c;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Гаусс\n", + "\n", + "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", + "\n", + "Нас по сути просят решить следующую систему:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", + "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", + "\\ldots \\\\\n", + "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", + "\n", + "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "t gauss (matrix a) {\n", + " for (int i = 0; i < n; i++) {\n", + " int nonzero = i;\n", + " for (int j = i+1; j < n; j++)\n", + " if (a[j][i])\n", + " nonzero = j;\n", + " swap(a[nonzero], a[i]);\n", + " for (int j = 0; j < n; j++)\n", + " if (j != i && a[j][i])\n", + " a[j] ^= a[i];\n", + " }\n", + " t x;\n", + " for (int i = 0; i < n; i++)\n", + " x[i] = a[i][n] ^ a[i][i];\n", + " return x;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/centroid-checkpoint.ipynb b/img/.ipynb_checkpoints/centroid-checkpoint.ipynb new file mode 100644 index 0000000..37746bb --- /dev/null +++ b/img/.ipynb_checkpoints/centroid-checkpoint.ipynb @@ -0,0 +1,249 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Центроидная декомпозиция\n", + "\n", + "Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».\n", + "\n", + "Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще).\n", + "\n", + "![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\\frac{n}{2}$.\n", + "\n", + "Выясняется, что центр всегда существует — это следует из алгоритма его поиска:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int s[maxn];\n", + "\n", + "int sizes (int v) {\n", + " s[v] = 1;\n", + " for (int u : g[v])\n", + " // для простоты считаем, что дерево корневое\n", + " s[v] += sizes(u);\n", + " return s[v];\n", + "}\n", + "\n", + "// второй параметр -- размер дерева\n", + "int centroid (int v, int n) {\n", + " for (int u : g[v])\n", + " if (s[u] > n/2)\n", + " return centroid(u, n);\n", + " return v;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение.** `centroid` действительно находит цетроид.\n", + "\n", + "**Доказательство:**\n", + "\n", + "* `centroid` вернет вершину, у которой размеры всех детей не больше $\\frac{n}{2}$ (это явно проверяется в `if`-е).\n", + "* Мы пришли в эту вершину, когда её размер был больше $\\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\\frac{n}{2}+1) = \\frac{n}{2}-1$ вершин.\n", + "* Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен.\n", + "\n", + "Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».\n", + "\n", + "**Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.\n", + "\n", + "Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Теперь поймём, зачем мы всё это делали.\n", + "\n", + "**Утверждение.** Каждая вершина входит в $O(\\log n)$ компонент.\n", + "\n", + "**Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\\lceil \\log_2 n \\rceil$ разделений.\n", + "\n", + "**Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \\log n)$.\n", + "\n", + "**Утверждение.** Для любого пути $a \\leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$.\n", + "\n", + "**Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$.\n", + "\n", + "Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Подсчет путей с заданным свойством\n", + "\n", + "Рассмотрим конкретный пример: подсчёт путей заданной длины.\n", + "\n", + "Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид.\n", + "\n", + "Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int l = 179; // нужная нам длина\n", + "int ans = 0;\n", + "\n", + "// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена\n", + "bool used[maxn];\n", + "int s[maxn]; // размеры поддеревьев\n", + "\n", + "void sizes (int v, int p) {\n", + " s[v] = 1;\n", + " for (int u : g[v])\n", + " if (u != p && !used[u])\n", + " sizes(u, v), s[v] += s[u];\n", + "}\n", + "\n", + "int centroid (int v, int p, int n) {\n", + " for (int u : g[v])\n", + " if (u != p && !used[u] && s[u] > n/2)\n", + " return centroid(u, v, n);\n", + " return v;\n", + "}\n", + "\n", + "// записывает в t[] глубины вершин\n", + "void dfs (int v, int p, int d, vector &t) {\n", + " t.push_back(d);\n", + " for (int u : g[v])\n", + " if (u != p && !used[u])\n", + " dfs(u, v, d + 1, t);\n", + "} \n", + "\n", + "void solve (int v) {\n", + " /* <единственный зависящий от конкретной задачи код> */\n", + " size(v);\n", + " vector d(s[v], 0);\n", + " d[0] = 1;\n", + " for (int u : g[v]) {\n", + " if (!used[u]) {\n", + " vector t;\n", + " dfs(u, v, 1, t);\n", + " for (int x : t)\n", + " if (x <= l)\n", + " ans += d[l-x];\n", + " for (int x : t)\n", + " d[x]++;\n", + " }\n", + " }\n", + " /* */\n", + "\n", + " used[v] = 1;\n", + " for (int u : g[v])\n", + " if (!used[u])\n", + " solve(centroid(u, v, s[u]/2));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Асимптотика** $O(n \\log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\\log n)$ «уровней» центроидной декомпозиции." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на путях — offline\n", + "\n", + "Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.\n", + "\n", + "А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем.\n", + "\n", + "Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. \n", + "\n", + "Таким образом, каждый запрос будет просмотрен $O(\\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \\log n + n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на путях — online\n", + "\n", + "Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.\n", + "\n", + "Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \\times \\log n$, в котором для каждой вершины будем хранить $O(\\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.\n", + "\n", + "Тогда, при ответе на запрос, мы за $O(\\log n)$ или даже $O(\\log \\log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Асимптотика при более долгих пересчётах\n", + "\n", + "TODO: сослаться на мастер-теорему\n", + "\n", + "Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \\log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени?\n", + "\n", + "Возьмем худший случай — когда каждый раз компонента разбивается на две равные части.\n", + "\n", + "\n", + "\n", + "Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:\n", + "\n", + "$$ \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log \\frac{n}{2^k}\n", + "\\leq \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log n\n", + "= n \\log n \\sum_{k=0}^{\\log n} \\frac{1}{2^k}\n", + "< 2 n \\log n\n", + "= O(n \\log n) $$\n", + "\n", + "Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \\log n)$ — как, впрочем, и во всех остальных «разделяйках»." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/cuda-checkpoint.ipynb b/img/.ipynb_checkpoints/cuda-checkpoint.ipynb new file mode 100644 index 0000000..a1f3db1 --- /dev/null +++ b/img/.ipynb_checkpoints/cuda-checkpoint.ipynb @@ -0,0 +1,475 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Supercomputing for the Masses\n", + "\n", + "This is htmt-rendered notebook is supposed to be opened with Colab or a debian-based linux machine with a CUDA-capable GPU.\n", + "\n", + "You need to complete a quest and install CUDA.\n", + "\n", + "We do not expect anything from reader except some knowledge of C, Python basic algorithms and generally how computers work." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Subtlties of the Moore's law\n", + "\n", + "There are physical limitations to the speed of processors.\n", + "\n", + "One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some of them have" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Why multiprocessing?\n", + "\n", + "Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two types of " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## General-purpose GPU\n", + "\n", + "There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills.\n", + "\n", + "There are several.\n", + "\n", + "This is like with Windows and Linux.\n", + "\n", + "We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can easily dump 98% of performance of you think this way." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Everything should work if you're on Colab. Otherwise, you might need to complete a [little quest](https://wiki.tiker.net/PyCuda/Installation/Linux/Ubuntu). It will probably be enough to do `sudo apt-get install nvidia-cuda-dev` (install CUDA itself) and then return to `pip install pycuda`.\n", + "\n", + "You can get it by running `lspci | grep NVIDIA`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install pycuda" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy\n", + "\n", + "from pycuda.compiler import SourceModule\n", + "import pycuda.driver as drv\n", + "import pycuda.autoinit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time.\n", + "\n", + "You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Why CUDA\n", + "\n", + "Most of it still applicable.\n", + "\n", + "Again, GPU programming is very specific.\n", + "\n", + "SSE and tensor cores." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Kernels\n", + "\n", + "Just like C or C++, except that you use some custom built-in functions and specifiers.\n", + "\n", + "CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:\n", + "\n", + "You need to think of your computer as a heterogenious machine: there is host data and device data.\n", + "\n", + "* You move input data to device memory.\n", + "* You run some computation on device.\n", + "* You retrieve back this data.\n", + "\n", + "In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## NumPy\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example: $a + b$\n", + "\n", + "For coordination and testing, we will use `numpy` package. If you don't have it, install it: `pip install numpy`. This is also very optimized, so we will benchmark against it." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "ename": "LaunchError", + "evalue": "cuMemAlloc failed: unspecified launch failure", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0ma\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mastype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumpy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfloat32\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0ma_gpu\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmem_alloc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdtype\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitemsize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmemcpy_htod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma_gpu\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mLaunchError\u001b[0m: cuMemAlloc failed: unspecified launch failure" + ] + } + ], + "source": [ + "a = numpy.random.randn(4,4)\n", + "\n", + "a = a.astype(numpy.float32)\n", + "\n", + "a_gpu = cuda.mem_alloc(a.size * a.dtype.itemsize)\n", + "\n", + "cuda.memcpy_htod(a_gpu, a)\n", + "\n", + "mod = SourceModule(\"\"\"\n", + " __global__ void doublify(float *a)\n", + " {\n", + " int idx = threadIdx.x + threadIdx.y*4;\n", + " a[idx] *= 2;\n", + " }\n", + "\"\"\")\n", + "\n", + "func = mod.get_function(\"doublify\")\n", + "func(a_gpu, block=(4,4,1))\n", + "\n", + "a_doubled = numpy.empty_like(a)\n", + "cuda.memcpy_dtoh(a_doubled, a_gpu)\n", + "print(a)\n", + "print(a_doubled)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "ename": "LaunchError", + "evalue": "cuModuleLoadDataEx failed: unspecified launch failure - ", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mdest\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m }\n\u001b[0;32m----> 6\u001b[0;31m \"\"\")\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0madd_kernel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmod\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"add\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/anaconda3/lib/python3.7/site-packages/pycuda/compiler.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, source, nvcc, options, keep, no_extern_c, arch, code, cache_dir, include_dirs)\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 293\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mpycuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdriver\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 294\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodule\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcubin\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 295\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 296\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_bind_module\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mLaunchError\u001b[0m: cuModuleLoadDataEx failed: unspecified launch failure - " + ] + } + ], + "source": [ + "mod = SourceModule(\"\"\"\n", + " __global__ void add(float *dest, float *a, float *b) {\n", + " const int i = threadIdx.x;\n", + " dest[i] = a[i] + b[i];\n", + " }\n", + "\"\"\")\n", + "\n", + "add_kernel = mod.get_function(\"add\")\n", + "\n", + "a = numpy.random.randn(400).astype(numpy.float32)\n", + "b = numpy.random.randn(400).astype(numpy.float32)\n", + "\n", + "dest = numpy.zeros_like(a)\n", + "\n", + "add_kernel(drv.Out(dest), drv.In(a), drv.In(b), block=(400,1,1))\n", + "\n", + "print(dest-a*b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mod = SourceModule(\"\"\"\n", + " __global__ void check_prime(int *a, int *b, int *c) {\n", + " int n = blockIdx.x*blockDim.x + threadIdx.x;\n", + " for (int i = 2; i < 10000; i++)\n", + " a[i] |= (n % i == 0);\n", + " }\n", + "\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = numpy.zeros(10**8, dtype=numpy.int)\n", + "\n", + "func = mod.get_function(\"check_prime\")\n", + "func(cuda.InOut(a), block=(len(a)//32,32, 1))\n", + "\n", + "print(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What you need to understand about GPUs is that they are extremely specialised for their applications.\n", + "\n", + "Intrinsics for that.\n", + "\n", + "Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision.\n", + "\n", + "First, they introduced \"multiply-accumulate\" operation (e. g. `x += y * z`) per 1 GPU clock cycle.\n", + "\n", + "Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell).\n", + "\n", + "Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix).\n", + "\n", + "This is a lot of work per \n", + "\n", + "Well, you don't really need anything more precise than that for deep learning anyway.\n", + "\n", + "\n", + "It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices.\n", + "\n", + "Probably, the proper name would be \"4x4 matrix cores\", however NVIDIA marketing team decided to use \"tensor cores\".\n", + "\n", + "So, see, this is not exactly fair comparison.\n", + "\n", + "\n", + "\n", + "*
You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull
*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "down to int4 (16-valued, you heard correct)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Warps and thread blocks\n", + "\n", + "Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties.\n", + "\n", + "\n", + "\n", + "You can actually do the same stuff with 2d and 3d indexing — weird, right?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reducing an array\n", + "\n", + "It seems to be simple: you just need to .\n", + "\n", + "What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen:\n", + "\n", + "1. Read $x$ into register\n", + "2. Read $s$ into register\n", + "3. Calculate $s + x$\n", + "4. Write it back to wherever $s$ was initially\n", + "\n", + "Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value.\n", + "\n", + "\n", + "\n", + "Note: Atomics to do that\n", + "\n", + "for small data types they are implemented on the hardware level and much more faster than that.\n", + "\n", + "std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Memory types\n", + "\n", + "If all the various types of device memory were to race, here’s how the race would turn out:\n", + "\n", + "Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible).\n", + "\n", + "* 1st place: **Register memory**\n", + "
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread.\n", + "* 2nd place: **Shared Memory**\n", + "
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should\n", + "* 3rd place: **Constant Memory**\n", + "
\n", + "* 4th: Texture Memory\n", + "* Tie for last place: Local Memory and Global Memory\n", + "\n", + "What you need to care for now is register \n", + "\n", + "For now, you need to care about differe\n", + "\n", + "Accessing global memory takes hundreds." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Heterogineous computing\n", + "\n", + "CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more CUDA-enabled NVIDIA GPU devices.\n", + "\n", + "Threading resources\n", + "Execution pipelines on host systems can support a limited number of concurrent threads. Servers that have four hex-core processors today can run only 24 threads concurrently (or 48 if the CPUs support Hyper-Threading.) By comparison, the smallest executable unit of parallelism on a CUDA device comprises 32 threads (termed a warp of threads). Modern NVIDIA GPUs can support up to 1536 active threads concurrently per multiprocessor (see Features and Specifications of the CUDA C Programming Guide) On GPUs with 16 multiprocessors, this leads to more than 24,000 concurrently active threads.\n", + "Threads\n", + "Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches (when two threads are swapped) are therefore slow and expensive. By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work (in warps of 32 threads each). If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput.\n", + "RAM\n", + "The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: dense matrix multiplication\n", + "\n", + "A lot of these are actually sparse. You can do stuff with social network graphs or web graphs.\n", + "\n", + "Cool. But let's disapploint us for a bit:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: dynamic programming" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Some primitives\n", + "\n", + "Before we go deeper into more complex algorithms, we need to master some " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: sorting" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb b/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb new file mode 100644 index 0000000..3506ced --- /dev/null +++ b/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb @@ -0,0 +1,109 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Динамическое программирование" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Разберем некоторые классические идеи и задачи в динамическом программировании." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Рюкзак\n", + "\n", + "> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наибольшая общая подпоследовательность\n", + "\n", + "> Есть две строки. Найти длину их наибольшей общей подпоследовательности.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Расстановка скобок\n", + "\n", + "> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.\n", + "\n", + "Можно завести динамику «можно ли »." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наибольшая возрастающая подпоследовательность\n", + "\n", + "Есть также алгоритм за $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мемоизация\n", + "\n", + "Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.\n", + "\n", + "Первый предполагает" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Число разбиений\n", + "\n", + ">" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Замена параметра на ответ\n", + "\n", + ">" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb b/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb new file mode 100644 index 0000000..30cde99 --- /dev/null +++ b/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Оптимизации динамики\n", + "\n", + "* Разделяй и властвуй\n", + "* Оптимизация Кнута\n", + "* Convex Hull Trick\n", + "* Лямбда-оптимизация" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Задача\n", + "\n", + "> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.\n", + "\n", + "Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.).\n", + "\n", + "Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// x[] — отсортированный массив координат точек, нумерация с нуля\n", + "\n", + "// квадрат длины отрезка от i-той до j-той точки\n", + "int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }\n", + "\n", + "// TODO: предподсчитать cost\n", + "\n", + "for (int i = 0; i <= m; i++)\n", + " f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо\n", + "// все остальные f предполагаем равными бесконечности\n", + "\n", + "for (int i = 1; i <= n; i++)\n", + " for (int j = 1; j <= m; j++)\n", + " for (int k = 0; k < i; k++)\n", + " f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1));" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Заметим, что циклы по `i` и `j` можно поменять местами.\n", + "\n", + "Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Разделяй-и-властвуй\n", + "\n", + "Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.\n", + "\n", + "*Утверждение.* $opt[i, j] \\leq opt[i, j+1]$.\n", + "\n", + "Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.\n", + "\n", + "Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$.\n", + "\n", + "Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "void solve (int l, int r, int _l, int _r, int k) {\n", + " if (l > r) return; // отрезок пустой — выходим\n", + " int t = (l + r) / 2;\n", + " int opt = _l;\n", + " for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы \n", + " int val = f[i+1][k-1] + cost(i, j);\n", + " if (val < f[t][k])\n", + " f[t][k] = val, opt = i;\n", + " }\n", + " solve(l, t-1, _l, opt, k);\n", + " solve(t+1, r, opt, _r, k);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Вызываться она будет просто последовательно для каждого слоя:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "for (int k = 1; k <= m; k++)\n", + " solve(0, n-1, 0, n-1, k);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Теперь пересчет одного «слоя» динамики занимает $O(n \\log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\\log n)$, а значит и каждый элемент будет просмотрен не более $O(\\log n)$ раз.\n", + "\n", + "Получается, что асимптотика улучшилась до $O(n m \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Оптимизация Кнута\n", + "\n", + "Предыдущий метод основывался на том факте, что $opt[i, j] \\leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру?\n", + "\n", + "$$ opt[i-1, j] \\leq opt[i, j] \\leq opt[i, j+1] $$\n", + "\n", + "В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок.\n", + "\n", + "Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:\n", + "\n", + "$$ \\sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \\sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$\n", + "\n", + "Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for (int i = 1; i <= n; i++) {\n", + " for (int j = m; j >= 1; j--) {\n", + " for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) {\n", + " int val = f[i+1][k-1] + cost(i, j);\n", + " if (val < f[t][k])\n", + " f[t][k] = val, opt[i][j] = i;\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Сравните с базовым решением — всего 3 новых строчки." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Convex Hull Trick\n", + "\n", + "Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение.\n", + "\n", + "$$ f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\} = \\min_{k < i} \\{\n", + "f[k, j-1] + x_{i-1}^2\n", + "- 2x_{i-1} x_k\n", + "+ x_k^2\n", + "\\}$$\n", + "\n", + "Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется\n", + "$\n", + "\\underbrace{f[k, j-1] + x_k^2}_{a_k}\n", + "\\underbrace{-2x_k}_{b_k} x_{i-1}\n", + "$.\n", + "\n", + "Это теперь можно переписать как $\n", + "\\min_k\n", + "(a_k, b_k)\n", + "\\cdot\n", + "(1, X_{i-1})\n", + "$ (имеется в виду скалярное произведение)\n", + "\n", + "Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: иллюстрация." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: мем про Скуби-Ду." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ли Шао" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Лямбда-оптимизация\n", + "\n", + "Не «фольклорное» название — дискретный метод множителей Лагранжа.\n", + "\n", + "Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .\n", + "\n", + "Заметим следующее: \n", + "\n", + "Давайте заменим $f[i, j]$ на $g_\\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\\lambda$, за использование отрезка.\n", + "\n", + "Давайте сделаем бинпоиск по $\\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \\lambda k$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Суммируем\n", + "\n", + "TODO: сделать табличку\n", + "\n", + "* Разделяйка: $O(nm \\log n)$, если `cost` такой, что `opt` монотонна по одному аргументу.\n", + "* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам.\n", + "* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение.\n", + "* Лагранж: $O(n \\log n)$. Функция должна быть выпуклой." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Другие задачи\n", + "\n", + "Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.\n", + "\n", + "> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "c++", + "file_extension": ".c++", + "mimetype": "text/x-c++src", + "name": "c++" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb b/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb new file mode 100644 index 0000000..3c3d9f3 --- /dev/null +++ b/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb @@ -0,0 +1,285 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "НЯ!\n", + "
Эта статья полна любви и обожания.\n", + "
Возможно, стоит добавить ещё больше?\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Дерево Фенвика\n", + "\n", + "Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Определение\n", + "\n", + "Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: \n", + " \n", + "$$ t_i = \\sum_{k=F(i)}^i a_k $$\n", + " \n", + "где $F$ это какая-то функцию, для которой выполнено $F(i) \\leq i$. Конкретно её определим потом.\n", + " \n", + "Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле:\n", + " \n", + "$$ sum(k) = t_k + sum(F(k)-1) $$\n", + " \n", + "Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\\log n)$. Популярны две функции:\n", + "\n", + "* $F_1(x) =$ `x & (x + 1)`\n", + "* $F_2(x) =$ `x - (x & -x) + 1`\n", + "\n", + "Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.\n", + "\n", + "**Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\\log n)$ таких отрезков." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Реализация\n", + "\n", + "Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int t[maxn];\n", + "\n", + "// возвращает сумму на префиксе\n", + "int sum (int r) {\n", + " int res = 0;\n", + " for (; r > 0; r -= r & -r)\n", + " res += t[r];\n", + " return res;\n", + "}\n", + "\n", + "int sum (int l, int r) {\n", + " return sum(r) - sum(l-1);\n", + "}\n", + "\n", + "// обновляет нужные t\n", + "void add (int k, int x) {\n", + " for (; k <= n; k += k & -k)\n", + " t[k] += x;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Многомерный случай\n", + "\n", + "> $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку\n", + "\n", + "Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре.\n", + "\n", + "`sum` перепишется следующим образом:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int sum (int r1, int r2) {\n", + " int res = 0;\n", + " for (int i = r1; i > 0; i -= i & -i)\n", + " for (int j = r2; j > 0; j -= j & -j)\n", + " ans += t[i][j];\n", + " return res;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах.\n", + "\n", + "Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \\log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Бинпоиск\n", + "\n", + "Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\\log n)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// возвращает индекс, на котором сумма уже больше\n", + "int lower_bound (int s) {\n", + " int k = 0;\n", + " for (int l = logn; l >= 0; l--) {\n", + " if (k + (1<= k > i - (i & -i)`. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Будем перебирать префиксы TODO\n", + "\n", + "Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя.\n", + "\n", + "Можно представить дерево так: ячейка 2^k содержит все\n", + "\n", + "TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Название\n", + "\n", + "Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree».\n", + "\n", + "Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.\n", + "\n", + "Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).\n", + "\n", + "В частном случае, когда длина массива равна $2^k$, то дерево будет только одно." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/games-checkpoint.ipynb b/img/.ipynb_checkpoints/games-checkpoint.ipynb new file mode 100644 index 0000000..f183b10 --- /dev/null +++ b/img/.ipynb_checkpoints/games-checkpoint.ipynb @@ -0,0 +1,331 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "c0JYtYks_TYp" + }, + "source": [ + "# Теория игр\n", + "\n", + "Начнём с самого баянного примера математической игры, который можно вспомнить:\n", + "\n", + "> На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков.\n", + "\n", + "Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так:\n", + "\n", + "$$ f_k = \\lnot f_{k-1} \\lor \\lnot f_{k-2} \\lor \\lnot f_{k-3} = \\lnot (f_{k-1} \\land f_{k-2} \\land f_{k-3}) $$\n", + "\n", + "то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния).\n", + "\n", + "**Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Игры на графах\n", + "\n", + "В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек».\n", + "\n", + "Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные*\n", + "\n", + "Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*.\n", + "\n", + "Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ретроанализ\n", + "\n", + "Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности:\n", + "\n", + "* Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины.\n", + "* Вершина *выигрышная* — если из неё есть переход в проигрышную вершину.\n", + "* Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные).\n", + "\n", + "Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся.\n", + "\n", + "**Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин.\n", + "\n", + "Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние.\n", + "\n", + "Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось.\n", + "\n", + "Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные.\n", + "\n", + "**Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали.\n", + "\n", + "* При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). \n", + "* При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше.\n", + "\n", + "Асимптотика составит $O(n + m)$, как и у любого bfs-а." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа\n", + "int cnt[maxn]; // счётчик исходящих рёбер\n", + "\n", + "enum StatusType { win, loss, unknown };\n", + "StatusType status[maxn]; // выгрышность вершины;\n", + "// по умолчанию все кроме терминальных считаются unknown\n", + "// те, кто в итоге остаются unknown -- ничейные\n", + "\n", + "queue q = {/* нужно заранее добавить сюда все терминальные вершины*/};\n", + "\n", + "while (!q.empty()) {\n", + " int v = q.front();\n", + " q.pop();\n", + " for (int u : t[v]) {\n", + " cnt[u]--; // удаляем это ребро\n", + " if (status[v] == unknown) {\n", + " // из u есть ребро в проигрышную -- значит она выигрышная\n", + " if (status[v] == loss)\n", + " status[u] = win;\n", + " // все ребра u ведут в выигрышные вершины -- значит она проигрышная\n", + " if (status[v] == win && cnt[u] == 0) {\n", + " status[u] = loss;\n", + " // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь\n", + " if (status[v] != unknown)\n", + " q.push(u);\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "StatusType dfs(int v) {\n", + " if (status[v] != unknown)\n", + " return status[v];\n", + " status[v] = loss;\n", + " // изменим статус, когда найдём переход в проигрышную вершину\n", + " for (int u : g[v])\n", + " if (dfs(u) == loss)\n", + " status[v] = win;\n", + " return status[v];\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: можно ли здесь циклы учесть?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Минимаксные игры\n", + "\n", + "Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно.\n", + "\n", + "Типичный граф минимаксной игры выглядит следующим образом:\n", + "\n", + "\n", + "\n", + "В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников.\n", + "\n", + "Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ретроанализ для больших графов\n", + "\n", + "Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго.\n", + "\n", + "Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей).\n", + "\n", + "Ретроанализ нужно оптимизировать." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ограничение перебора\n", + "\n", + "Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. \n", + "\n", + "Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит.\n", + "\n", + "Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Мемоизация позиций\n", + "\n", + "В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает .\n", + "\n", + "Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Альфа-бета отсечение\n", + "\n", + "\n", + "\n", + "Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым.\n", + "\n", + "На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым.\n", + "\n", + "Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6.\n", + "\n", + "Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем.\n", + "\n", + "Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monte-Carlo Tree Search\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Игры с ненулевой суммой\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Теория Шпрага-Гранди\n", + "\n", + "Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые.\n", + "\n", + "Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями.\n", + "\n", + "**Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \\oplus a_2 \\oplus \\ldots \\oplus a_n $ размеров кучек отлична от нуля.\n", + "\n", + "**Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы:\n", + "\n", + "* Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \\oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия.\n", + "* Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \\oplus a_i \\oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \\oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$.\n", + "\n", + "Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \\oplus a_i$.\n", + "\n", + "Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ним с увеличениями\n", + "\n", + "Пусть у нас" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Эквивалентность игр ниму" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Игры с неполной информацией\n", + "\n", + "Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков.\n", + "\n", + "Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет.\n", + "\n", + "Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Дилемма заключенного" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Камень-ножницы-бумага" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Покер\n", + "\n", + "Рассмотрим такую у" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "29_game-theory.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/img/.ipynb_checkpoints/geometry-checkpoint.ipynb b/img/.ipynb_checkpoints/geometry-checkpoint.ipynb new file mode 100644 index 0000000..555b3b9 --- /dev/null +++ b/img/.ipynb_checkpoints/geometry-checkpoint.ipynb @@ -0,0 +1,421 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Геометрия и ООП" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали.\n", + "\n", + "![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg)\n", + "\n", + "Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:\n", + "\n", + "$$ a \\cdot b = x_a x_b + y_a y_b $$\n", + "\n", + "![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg)\n", + "\n", + "У него есть полезные свойства:\n", + "\n", + "* Скалярное произведение симметрично ($a \\cdot b = b \\cdot a$).\n", + "* Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.\n", + "* Перпендикулярные вектора должны иметь нулевое скалярное произведение.\n", + "* Если угол острый, то оно положительное\n", + "* Если угол тупой, то отрицательное\n", + "\n", + "**Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:\n", + "\n", + "$$ a \\times b = x_a y_b - y_a x_b $$\n", + "\n", + "![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg)\n", + "\n", + "Его свойства:\n", + "\n", + "* Скалярное произведение *анти*симметрично ($a \\times b = - (b \\times a)$).\n", + "* Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.\n", + "* Коллинеарные вектора должны иметь нулевое векторное произведение.\n", + "* Если b «слева» от a, то оно положительное.\n", + "* Если b «справа» — то отрицательное." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Всякие проверки\n", + "\n", + "Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.\n", + "\n", + "**Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.\n", + "\n", + "$$\n", + "\\text{P лежит внутри ABC} \\iff \\begin{cases}\n", + "(B-A) \\times (P-A) \\geq 0 \\\\\n", + "(C-B) \\times (P-B) \\geq 0 \\\\\n", + "(A-C) \\times (P-C) \\geq 0 \\\\\n", + "\\end{cases}\n", + "$$\n", + "\n", + "**Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.\n", + "\n", + "$$ V = \\frac{1}{2} (B-A) \\times (C-A) $$\n", + "\n", + "**Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).\n", + "\n", + "![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg)\n", + "\n", + "Забудьте о формуле Герона и всегда считайте площади через векторное произведение.\n", + "\n", + "Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.\n", + "\n", + "**Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\\times(c-a) > 0$).\n", + "\n", + "**Пересекаются ли отрезки**.\n", + "\n", + "![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Уравнение прямой\n", + "\n", + "Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством.\n", + "\n", + "У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \\geq 0$ будет указывать в сторону самой полуплоскости.\n", + "\n", + "Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой:\n", + "\n", + "$$ d = \\frac{|Ax_0+By_0+C|}{\\sqrt{A^2+B^2}} $$\n", + "\n", + "**Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "A_1 x + B_1 y + C_1 = 0 \\\\\n", + "A_2 x + B_2 y + C_2 = 0\n", + "\\end{cases}\n", + "\\implies \n", + "\\begin{cases}\n", + "-x = \\frac{B_1 y + C_1}{A_1} \\\\\n", + "-x = \\frac{B_2 y + C_2}{A_2}\n", + "\\end{cases}\n", + "\\implies \n", + "\\frac{B_1 y + C_1}{A_1} = \\frac{B_2 y + C_2}{A_2}\n", + "\\implies \n", + "y = - \\frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1}\n", + "$$\n", + "\n", + "Аналогично, $x = \\frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки).\n", + "\n", + "Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Как это кодить в C++" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Точка $\\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "struct r {\n", + " double x, y;\n", + " r () {}\n", + " r (int _x, int _y) { x = _x, y = _y; }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Операции над векторами\n", + "\n", + "Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "double len (r a) { return sqrt(a.x*a.x + a.y*a.y); }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Операторы\n", + "\n", + "В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д.\n", + "\n", + "Давайте для будущих нужд определим `+` и `-`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); }\n", + "r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Скалярное произведение" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int operator*(r a, r b){ return a.x*b.x + a.y*b.y; }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Векторное произведение\n", + "\n", + "Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int operator^(r a, r b){ return a.x*b.y - b.x*a.y; }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ввод-вывод\n", + "\n", + "Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "istream& operator>>(istream &in, r &p){ \n", + " in >> p.x >> p.y;\n", + " return in;\n", + "}\n", + "\n", + "ostream& operator<<(ostream &out, r &p){ \n", + " out << p.x << \" \" << p.y << endl;\n", + " return out; \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Алгебра VS Алгоритмы или зачем мы всё это делали\n", + "\n", + "Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Векторное представление прямой ($Ax + By + C = 0 \\rightarrow r = at + b$)\n", + "\n", + "Тут нужно просто выбрать две любые точки на прямой." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// даны A, B, C (A^2 + B^2 != 0)\n", + "r a, b;\n", + "if (eq(A, 0)) // значит, это горизонтальная прямая\n", + " a = r(0, -C/B), b = r(1, -C/B);\n", + "else\n", + " a = r(-C/A, 0), b = (1, -(C+B)/A, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Пример: отражение от прямой\n", + "\n", + "Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.\n", + "\n", + "$\\Pr_a b = \\frac{a \\cdot b}{|a|} \\frac{a}{|a|} = \\frac{|a| |b| \\cos \\alpha}{|a|} \\frac{a}{|a|} = |b| \\cos \\alpha \\frac{a}{|a|} $\n", + "\n", + "Формула имеет смысл: длина на единичный вектор направления.\n", + "\n", + "Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// прямая r = at + b, точка c\n", + "r pr (r a, r b, r c) {\n", + " c -= b; // пусть c и a выходят из одной точки\n", + " return b + (a*b / len(a) / len(a)) * a;\n", + "}\n", + "\n", + "r reflect (r a, r b, r c) {\n", + " return c + 2*(pr(a, b, c)-c);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Типичные баги" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Точность\n", + "\n", + "> Первое правило действительных чисел — не использовать действительные числа\n", + "\n", + "Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "const double eps = 1e-8;\n", + "\n", + "bool eq (double a, double b) { return abs(a-b) < eps }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### $0 \\neq -0$\n", + "\n", + "Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Область определения обратных функций\n", + "\n", + "`acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/hashing-checkpoint.ipynb b/img/.ipynb_checkpoints/hashing-checkpoint.ipynb new file mode 100644 index 0000000..f1a6144 --- /dev/null +++ b/img/.ipynb_checkpoints/hashing-checkpoint.ipynb @@ -0,0 +1,426 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "tqKO26jI7Eg2" + }, + "source": [ + "# Хэширование в строковых задачах\n", + "\n", + "Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.\n", + "\n", + "«Хорошая» хэш-функция:\n", + "* Быстро считается — за линейное от размера объекта время;\n", + "* Имеет не очень большие значения — влезающие в 64 бита;\n", + "* «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\\frac{1}{n}$.\n", + "\n", + "Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*.\n", + "\n", + "Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \\cdot n \\cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.\n", + "\n", + " ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "mihqVF9EHNW3" + }, + "source": [ + "## Применения в реальной жизни\n", + "\n", + "* **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.\n", + "* **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\\frac{k}{n}$ = $O(1)$ при правильном выборе $n$.\n", + "* **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.\n", + "* **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.\n", + "* **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.\n", + "* **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "sb5PdKP87Eg3" + }, + "source": [ + "Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы.\n", + "\n", + "Сегодня же мы остановимся на строках." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "AwuPsbSa7Eg3" + }, + "source": [ + "# Полиномиальное хэширование\n", + "\n", + "**Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами.\n", + "\n", + "Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`.\n", + "\n", + "Определим *прямой полиномиальный хэш* строки как значение следующего многочлена:\n", + "\n", + "$$ h_f = (s_0 + s_1 k + s_2 k^2 + \\ldots + s_n k^n) \\mod p $$\n", + "\n", + "Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой.\n", + "\n", + "Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "h3UUC_Vl80Is" + }, + "outputs": [], + "source": [ + "const int k = 31, mod = 1e9+7;\n", + "\n", + "string s = \"abacabadaba\";\n", + "long long h = 0, m = 1;\n", + "for (char c : s) {\n", + " int x = (int) (c - 'a' + 1);\n", + " h = (h + m * x) % mod;\n", + " m = (m * k) % mod;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "7_1zG4nb80I7" + }, + "source": [ + "Можем ещё определить *обратный полиномиальный хэш*:\n", + "\n", + "$$ h_b = (s_0 k^n + s_1 k^{n-1} + \\ldots + s_n) \\mod p $$\n", + "\n", + "Его преимущество в том, что можно написать на одну строчку кода меньше:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "kShe7vvz80JC" + }, + "outputs": [], + "source": [ + "long long h = 0;\n", + "for (char c : s) {\n", + " int x = (int) (c - 'a' + 1);\n", + " h = (h * k + x) % mod;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "VWd2N6Ic80JL" + }, + "source": [ + "Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hKCe8eBr80JP" + }, + "source": [ + "## Зачем он нужен?\n", + "\n", + "Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций.\n", + "\n", + "Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$:\n", + "\n", + "$$ h(ab) = h(a) + k^{|a|} \\cdot h(b) $$\n", + "\n", + "Удалить префикс строки можно так:\n", + "\n", + "$$ h(b) = \\frac{h(ab) - h(a)}{k^{|a|}} $$\n", + "\n", + "А суффикс — ещё проще:\n", + "\n", + "$$ h(a) = h(ab) - k^{|a|} \\cdot h(b) $$\n", + "\n", + "В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "0IBufJNe_jVG" + }, + "outputs": [], + "source": [ + "const int maxn = 1e5+5;\n", + "\n", + "int p[maxn];\n", + "p[0] = 1;\n", + "\n", + "for (int i = 1; i < maxn; i++)\n", + " p[i] = (p[i-1] * k) % mod;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_PKLEmZU80Jb" + }, + "source": [ + "Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "qyJGTChd80Jd" + }, + "outputs": [], + "source": [ + "int h[maxn];\n", + "h[0] = 0; // h[k] -- хэш префикса длины k\n", + "\n", + "// будем считать, что s это уже последовательность int-ов\n", + "\n", + "for (int i = 0; i < n; i++) \n", + " h[i+1] = (h[i] + p[i] * s[i]) % mod;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "L7n-KRvs80Jr" + }, + "source": [ + "Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке:\n", + "\n", + "$$ h(s[l:r]) = \\frac{h_r-h_l}{k^l} $$\n", + "\n", + "Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.\n", + "\n", + "Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить.\n", + "\n", + "$$ \\hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "FMjf8Rej80Jv" + }, + "outputs": [], + "source": [ + "int hash_substring (int l, int r) {\n", + " return (h[r+1] - h[l]) * p[n-l] % mod;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "l5XFHeaJ80J5" + }, + "source": [ + "Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "VuqWJtAvCCaQ" + }, + "source": [ + "### Упражнение\n", + "\n", + "Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "moSrfcxI7EhB" + }, + "source": [ + "## Примеры задач\n", + "\n", + "**Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`.\n", + "\n", + "**Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.\n", + "\n", + "**Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.\n", + "\n", + "**Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором.\n", + "\n", + "**Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "krDNbmroGTTs" + }, + "source": [ + "### Изменения строки*\n", + "\n", + "Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.\n", + "\n", + "Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.\n", + "\n", + "Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "8SFqBzPV7EhD" + }, + "source": [ + "# Вероятность ошибки и почему это всё вообще работает\n", + "\n", + "У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.\n", + "\n", + "Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "PE38pSDXQxcH" + }, + "source": [ + "## Выбор констант\n", + "\n", + "Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \\cdot n^2$. Обоснование — см. парадокс дней рождений.\n", + "\n", + "Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно.\n", + "\n", + "Можно также брать модуль $2^{64}$. У него есть несколько преимуществ:\n", + "* Он большой — второй модуль точно не понадобится.\n", + "* С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении.\n", + "* С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`.\n", + "\n", + "Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду.\n", + "\n", + "В выборе же $k$ ограничения не такие серьезные:\n", + "* Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.\n", + "* Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.\n", + "\n", + "Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "DSfMhJRhRaZe" + }, + "source": [ + "## Парадокс дней рождений\n", + "\n", + "> В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%.\n", + "\n", + "Более общее утверждение: в мультимножество нужно добавить $\\Theta(\\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали.\n", + "\n", + "**Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения.\n", + "Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$.\n", + "\n", + "$$f(n, d) = (1-\\frac{1}{d}) \\times (1-\\frac{2}{d}) \\times ... \\times (1-\\frac{n-1}{d})$$\n", + "\n", + "Попытаемся оценить $f$:\n", + "\n", + "$$\n", + "\\begin{align}\n", + " \\begin{aligned}\n", + " e^x & = 1 + x + \\frac{x^2}{2!} + \\ldots & \\text{(ряд Тейлора для экспоненты)} \\\\\n", + " & \\simeq 1 + x & \\text{(аппроксимация для $|x| \\ll 1$)} \\\\\n", + " e^{-\\frac{n}{d}} & \\simeq 1 - \\frac{n}{d} & \\text{(подставим $\\frac{n}{d} \\ll 1$)} \\\\\n", + " f(n, d) & \\simeq e^{-\\frac{1}{d}} \\times e^{-\\frac{2}{d}} \\times \\ldots \\times e^{-\\frac{n-1}{d}} & \\\\\n", + " & = e^{-\\frac{n(n-1)}{2d}} & \\\\\n", + " & \\simeq e^{-\\frac{n^2}{2d}} & \\\\\n", + " \\end{aligned}\n", + "\\end{align}\n", + "$$\n", + "\n", + "Из последнего выражения более-менее понятно, что вероятность $\\frac{1}{2}$ достигается при $n \\approx \\sqrt{d}$ и в этой точке изменяется очень быстро.\n", + "\n", + "**Второе доказательство** (для любителей теорвера). Введем $\\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\\frac{1}{d}$.\n", + "\n", + "Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\\frac{n (n-1)}{2} \\cdot \\frac{1}{d}$.\n", + "\n", + "Отсюда понятно, что если $d = \\Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно.\n", + "\n", + "*Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "2WAkqxpvRfj0" + }, + "source": [ + "### Бонус: «мета-задача»\n", + "\n", + "Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.\n", + "\n", + "«Решите» задачу." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "7_string-hashing.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/img/.ipynb_checkpoints/hld-checkpoint.ipynb b/img/.ipynb_checkpoints/hld-checkpoint.ipynb new file mode 100644 index 0000000..5faf5cd --- /dev/null +++ b/img/.ipynb_checkpoints/hld-checkpoint.ipynb @@ -0,0 +1,186 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Heavy-light декомпозиция\n", + "\n", + "HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid).\n", + "\n", + "\n", + "\n", + "*
TODO: найти менее уродливую иллюстрацию
*\n", + "\n", + "Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.\n", + "\n", + "Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.\n", + "\n", + "**Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер.\n", + "\n", + "**Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.\n", + "\n", + "Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.\n", + "\n", + "**Утверждение.** На любом вертикальном пути будет не более $O(\\log n)$ блоков.\n", + "\n", + "**Доказательство** разбивается на две части:\n", + "\n", + "* Лёгких ребер на вертикальном пути будет не более $O(\\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего.\n", + "* Непрерывных путей из тяжелых рёбер будет не более $O(\\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\\log n)$.\n", + "\n", + "**Следствие.** На любом пути будет не более $O(\\log n)$ блоков.\n", + "\n", + "Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Реализация\n", + "\n", + "Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.\n", + "\n", + "А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.\n", + "\n", + "Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vector g[maxn];\n", + "int s[maxn], p[maxn], tin[maxn], tout[maxn];\n", + "int head[maxn]; // «голова» тяжелого пути, которому принадлежит v\n", + "int t = 0;\n", + "\n", + "void sizes (int v = 0) {\n", + " s[v] = 1;\n", + " for (int &u : g[v]) {\n", + " sizes(u);\n", + " s[v] += s[u];\n", + " if (s[u] > s[g[v][0]])\n", + " // &u -- это ссылка, так что её легально использовать при swap-е\n", + " swap(u, g[v][0]);\n", + " }\n", + "}\n", + "\n", + "void hld (int v = 0) {\n", + " rin[t] = v;\n", + " tin[v] = t++;\n", + " for (int u : g[v]) {\n", + " // если это тяжелый ребенок -- его next нужно передать\n", + " // в противном случае он сам является головой нового пути\n", + " head[u] = (u == g[v][0] ? head[v] : u);\n", + " hld(u);\n", + " }\n", + " tout[v] = t;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Как им решать задачи\n", + "\n", + "Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:\n", + "\n", + "1. Узнать минимальное число на пути между $v_i$ и $u_i$.\n", + "2. Изменить число у $v_i$-той вершины на $x_i$.\n", + "\n", + "Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int val[maxn];\n", + "segtree st(0, n);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "void upd (int v, int x) {\n", + " st.upd(tin[v], x);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int ancestor (int a, int b) {\n", + " return tin[a] <= tin[b] && tin[b] <= tout[a];\n", + "}\n", + "\n", + "void up (int &a, int &b, int &ans) {\n", + " while (!ancestor(head[a], b)) {\n", + " ans = min(ans, st.get_min(tin[head[a]], tin[a]));\n", + " a = p[head[a]];\n", + " }\n", + "}\n", + "\n", + "int get_min (int a, int b) {\n", + " int ans = inf;\n", + " up(a, b, ans);\n", + " up(b, a, ans);\n", + " if (!ancestor(a, b))\n", + " swap(a, b);\n", + " ans = min(ans, st.get_min(tin[a], tin[b]));\n", + " return ans;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/index-checkpoint.ipynb b/img/.ipynb_checkpoints/index-checkpoint.ipynb new file mode 100644 index 0000000..b2dd755 --- /dev/null +++ b/img/.ipynb_checkpoints/index-checkpoint.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Алгоритмика\n", + "\n", + "Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/).\n", + "\n", + "Более-менее готово:\n", + "\n", + "* [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное\n", + "* [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое\n", + "* [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки\n", + "* [Центроидная декомпозиция](http://sereja.me/a/centroid)\n", + "* [Heavy-light декомпозиция](http://sereja.me/a/hld)\n", + "* [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла\n", + "* [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву\n", + "* [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость\n", + "* [Ро-алгоритм Полларда](http://sereja.me/a/pollard)\n", + "* [Sparse table](http://sereja.me/a/sparse-table)\n", + "* [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона\n", + "* [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса\n", + "* [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера\n", + "* [Классные задачки](http://sereja.me/a/bayans): пока что без решений\n", + "\n", + "Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально.\n", + "\n", + "Не очень готово:\n", + "\n", + "* [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция\n", + "* [Суффиксный массив](http://sereja.me/a/suffix-array)\n", + "* [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++\n", + "* [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick\n", + "* [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала\n", + "* [Обратное по модулю](http://sereja.me/a/reciprocal)\n", + "* [Метод отжига](http://sereja.me/a/annealing)\n", + "* [Стресс-тестирование](http://sereja.me/a/stress-test)\n", + "* [Бор](http://sereja.me/a/trie)\n", + "* [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько\n", + "* Выпуклые оболочки\n", + "* [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса\n", + "* CUDA: программирование на GPU (на английском)\n", + "\n", + "Вообще не готово:\n", + "* [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема\n", + "* [Ахо-Корасик](http://sereja.me/a/aho-corasick)\n", + "* [Теорвер](http://sereja.me/a/probability)\n", + "* Корневая декомпозиция\n", + "* Теория информации\n", + "* Функции потерь в машинном обучении\n", + "* Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском)\n", + "* Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском)\n", + "\n", + "Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb b/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb new file mode 100644 index 0000000..7200ab9 --- /dev/null +++ b/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Быстрое умножение\n", + "\n", + "В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).\n", + "\n", + "[Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.\n", + "\n", + "Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.\n", + "\n", + "Алгоритм Карацубы имеет довольно значимое место в истории науки.\n", + "\n", + "{\\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}).\n", + "У Колмогорова была гипотеза, что нижняя оценка для {\\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности\n", + "{\\displaystyle M(n)=O(n^{\\log _{2}3})} M(n)=O(n^{{\\log _{2}3}})\n", + "и тем самым опроверг «гипотезу {\\displaystyle n^{2}} n^{2}».\n", + "\n", + "(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)\n", + "\n", + "Можно представить себе какого-то грозного японца." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мастер-теорема\n", + "\n", + "Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек»." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Мастер-теорема.** Пусть имеется рекуррента:\n", + "\n", + "$$ T(n) = \\begin{cases}\n", + "a T(\\frac{n}{b}) + \\Theta(n^c), & n > n_0\n", + "\\\\ \\Theta(1), & n \\leq n_0\n", + "\\end{cases} $$\n", + "\n", + "Тогда:\n", + "\n", + "* **A.** Если $c > \\log_b a$, то $T(n) = \\Theta(n^c)$.\n", + "* **B.** Если $c = \\log_b a$, то $T(n) = \\Theta(n^c \\log n)$.\n", + "* **C.** Если $c < \\log_b a$, то $T(n) = \\Theta(n^{\\log_b a})$.\n", + "\n", + "---\n", + "\n", + "\n", + "\n", + "---\n", + "\n", + "**Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням:\n", + "\n", + "$$ T(n) = \\sum_{k=0}^{\\log_b n} a^k (\\frac{n}{b^k})^c = n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k $$\n", + "\n", + "* **A.** Если $c > \\log_b a$, то $\\sum (\\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \\Theta(n^c)$.\n", + "* **B.** Если $c = \\log_b a$, то\n", + "$$\\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\sum_{k=0}^{\\log_b n} 1^k = \\Theta(n^c \\log_b n)$$\n", + "* **C.** Если $c < \\log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,\n", + "\n", + "$$ n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\Theta(n^c (\\frac{a}{b^c})^{\\log_b n}) = \\Theta(n^c \\cdot \\frac{a^{\\log_b n}}{n^c}) = \\Theta(a^{\\log_b n}) = \\Theta(n^{\\log_b a}) $$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Алгоритм Карацубы\n", + "\n", + "Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат.\n", + "\n", + "Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Развитие идеи\n", + "\n", + "То же самое можно применить матрицам.\n", + "\n", + "Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\\epsilon})$ для произвольного $\\epsilon$ ещё никто не доказал." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/lca-checkpoint.ipynb b/img/.ipynb_checkpoints/lca-checkpoint.ipynb new file mode 100644 index 0000000..f4e7190 --- /dev/null +++ b/img/.ipynb_checkpoints/lca-checkpoint.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "1MwyLmBaV1hj" + }, + "source": [ + "# Корневые деревья" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hMjCrvgNV1hk" + }, + "source": [ + "Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*.\n", + "\n", + "Примеры корневых деревьев:\n", + "* наследование классов в языках программирования (если множественное наследование запрещено),\n", + "* дерево факторизации числа на простые (в общем случае не уникальное),\n", + "* иерархия в какой-нибудь компании,\n", + "* просто множество как-то вложенных объектов (ссылка на крысу-роботягу).\n", + "\n", + "Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Напоминание: DFS\n", + "\n", + "Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vector g[maxn];\n", + "int p[maxn], tin[maxn], tout[maxn];\n", + "int t = 0;\n", + "\n", + "void dfs (int v) {\n", + " tin[v] = t++;\n", + " for (int u : g[v])\n", + " dfs(u);\n", + " tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "У этих массивов много полезных свойств:\n", + "* Вершина $u$ является предком $v$ $\\iff tin_v \\in [tin_u, tout_u) $. Эту проверку можно делать за константу.\n", + "* Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой.\n", + "* В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер.\n", + "* Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$.\n", + "* Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на поддеревьях\n", + "\n", + "Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.\n", + "\n", + "> Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$.\n", + "\n", + "Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на уровнях\n", + "\n", + "> Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$).\n", + "\n", + "Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам.\n", + "\n", + "Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наименьший общий предок\n", + "\n", + "Очень много задач нам поможет решить следующая вспомогательная задача.\n", + "\n", + "> Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких.\n", + "\n", + "По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные.\n", + "\n", + "![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png)\n", + "\n", + "Для лучшего понимания: медленно (за линейное время) это можно делать так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bool a (int u, int v) {\n", + " return tin[u] <= tin[v] && tin[v] <= tout[u];\n", + "}\n", + "\n", + "int lca (int u, int v) {\n", + " while (!ancestor(u, v))\n", + " u = p[u];\n", + " return u;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## LCA: двоичные подъемы\n", + "\n", + "Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \\times \\lceil \\log n \\rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень.\n", + "\n", + "Такой препроцессинг можно выполнить за $O(n \\log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int up[maxn][logn];\n", + "\n", + "void dfs (int v) {\n", + " for (int l = 1; l < logn; l++)\n", + " up[v][l] = up[up[v][l-1]][l-1];\n", + " tin[v] = t++;\n", + " for (int u : g[v]) {\n", + " up[u][0] = v;\n", + " dfs(u);\n", + " }\n", + " tout[v] = t++;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Пусть поступил очередной запрос — пара вершин $(u, v)$:\n", + "* Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.\n", + "* Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.\n", + "\n", + "Подробнее про второй пункт. Пусть $L = \\lceil \\log n \\rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "cFYU1Y5qV1hs" + }, + "outputs": [], + "source": [ + "int lca (int v, int u) {\n", + " if (a(v, u)) return v;\n", + " if (a(u, v)) return u;\n", + " for (int l = logn-1; l >= 0; l--)\n", + " if (!ancestor(up[v][l], u))\n", + " v = up[v][l];\n", + " return up[v][0];\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Асимптотика\n", + "\n", + "Препроцессинг — $O(n \\log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу.\n", + "\n", + "Ответ на запрос — $O(\\log n)$, потому что по сути мы делаем один бинпоиск." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на путях\n", + "\n", + "Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).\n", + "\n", + "Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути.\n", + "\n", + "Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int get_min (int v, int u) {\n", + " int ans = inf;\n", + " for (int l = logn-1; l >= 0; l--)\n", + " if (!ancestor(up[v][l], u))\n", + " v = up[v][l], ans = min(ans, mn[v][l]);\n", + " for (int l = logn-1; l >= 0; l--)\n", + " if (!ancestor(up[u][l], v))\n", + " u = up[u][l], ans = min(ans, mn[u][l]);\n", + " return min({ans, mn[v][0], mn[u][0]})\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "PXpE5vHfV1hu" + }, + "source": [ + "## Сведение к RMQ\n", + "\n", + "Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Во втором массиве мы по сути выписали наш проход dfs-а.\n", + "\n", + "Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.\n", + "\n", + "Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "b7R7XMv4V1hy" + }, + "source": [ + "### Разреженная таблица\n", + "\n", + "На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \\log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.\n", + "\n", + "Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \\log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## А наоборот можно?*\n", + "\n", + "*Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.\n", + "\n", + "Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот.\n", + "Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.\n", + "\n", + "Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль.\n", + "\n", + "Возьмем константу $k = \\lfloor \\frac{\\log n}{2} \\rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\\frac{2 n}{\\log n})$, и построение будет работать за линейное время:\n", + "\n", + "$$O(\\frac{2 n}{\\log n} \\log \\frac{2 n}{\\log n}) = O(\\frac{2 n}{\\log n} (\\log 2n - \\log \\log n)) = O(n)$$\n", + "\n", + "Также посчитаем для каждой возможной маски размера $\\frac{\\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\\sqrt n$ (ради этого мы и делили логарифм на два).\n", + "\n", + "ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.\n", + "\n", + "Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.\n", + "\n", + "Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear).\n", + "\n", + "Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.\n", + "\n", + "Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.**" + ] + } + ], + "metadata": { + "colab": { + "name": "10_lca.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/img/.ipynb_checkpoints/linalg-checkpoint.ipynb b/img/.ipynb_checkpoints/linalg-checkpoint.ipynb new file mode 100644 index 0000000..f5b8c0b --- /dev/null +++ b/img/.ipynb_checkpoints/linalg-checkpoint.ipynb @@ -0,0 +1,453 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ликбез по линейной алгебре\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Определение**. Функция $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ называется *линейной*, если для неё выполнено\n", + "\n", + "1. $ f(x+y) = f(x) + f(y) $\n", + "2. $ f(ax) = a f(x), \\; a \\in R $\n", + "\n", + "Примеры:\n", + "* $ f(x) = 0 $\n", + "* $ f(x) = x $\n", + "* $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\\mathbb{R}^3$ в $\\mathbb{R}$)\n", + "* $ f(x) = (x, -x, 0) $ (из $\\mathbb{R}$ в $\\mathbb{R}^3$)\n", + "\n", + "Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства:\n", + "* Сумма линейных функций — линейная функция.\n", + "* Сумма коммутативна: $f+g = g+f$).\n", + "* Сумма ассоциативна: $(f+g)+h = f+(g+h)$.\n", + "* Композиция $f(g(x)) = (f \\circ g)(x)$ линейных функций — линейная функция.\n", + "* Композция ассоциативна: $(f \\circ g) \\circ h = f \\circ (g \\circ h) = f \\circ g \\circ h$.\n", + "* Композиция в общем случае не коммутативна.
\n", + "Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен.\n", + "\n", + "Все свойства можно вывести лишь из этих двух пунктов в определении." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Что такое матрица?\n", + "\n", + "Можно показать, что любую линейную функцию $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ можно представить в таком виде:\n", + "\n", + "$$\n", + "f(x) =\n", + "\\begin{pmatrix}\n", + "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\\\\n", + "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\\\\n", + "\\ldots \\\\\n", + "a_{m1} x_1 + a_{m2} x_2 + \\ldots + a_{mn} x_n \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "*Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$.\n", + "\n", + "$$\n", + "A =\n", + "\\begin{pmatrix}\n", + "a_{11} & a_{12} & \\ldots & a_{1n} \\\\\n", + "a_{21} & a_{22} & \\ldots & a_{2n} \\\\\n", + "\\vdots & \\vdots & \\ddots & \\vdots \\\\\n", + "a_{m1} & a_{m2} & \\ldots & a_{mn} \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "Каждой линейной функции из $\\mathbb{R}^n$ в $\\mathbb{R}^m$ соответствует какая-то матрица размера $n \\times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте.\n", + "\n", + "Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \\circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так:\n", + "\n", + "$$ C = AB: C_{ij} = \\sum_{i=1}^{k} A_{ik} B_{kj} $$\n", + "\n", + "Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$.\n", + "\n", + "Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \\times k$, то вторая должна иметь размер $k \\times m$, то есть «средние» размерности обязательно должны совпадать.\n", + "\n", + "\n", + "\n", + "Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.\n", + "\n", + "Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:\n", + "\n", + "* Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$.\n", + "* Сумма коммутативна: $A+B = B+A$)\n", + "* Сумма ассоциативна: $(A+B)+C = A+(B+C)$\n", + "* Умножение ассоциативно: $(AB)C = A(BC) = ABC$.\n", + "* Умножение в общем случае не коммутативно." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Пример**: матрица поворота в 2d.\n", + "$$\n", + "\\begin{pmatrix}\n", + "\\cos \\alpha & -\\sin \\alpha \\\\\n", + "\\sin \\alpha & \\cos \\alpha \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "**Пример**: матрица проецирования на $Ox$ в 3d.\n", + "$$\n", + "\\begin{pmatrix}\n", + "1 & 0 & 0 \\\\\n", + "0 & 0 & 0 \\\\\n", + "0 & 0 & 0 \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "**Пример**: матрица «свапни $x$ и $y$».\n", + "$$\n", + "\\begin{pmatrix}\n", + "0 & 1 \\\\\n", + "1 & 0 \\\\\n", + "\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Напишем класс, который реализует матричное умножение." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "struct matrix {\n", + " int n, m;\n", + " int t[];\n", + " matrix (int _n, int _m) {\n", + " n = _n, m = _m;\n", + " t = new int(n*m);\n", + " memset(t, 0, sizeof t);\n", + " }\n", + " int[] operator[] (int k) {\n", + " return t[k*m];\n", + " }\n", + "}\n", + "\n", + "matrix operator* (matrix a, matrix b) {\n", + " matrix c(a.n, b.m);\n", + " for (int i = 0; i < a.n; i++)\n", + " for (int j = 0; j < b.m; j++)\n", + " for (int k = 0; k < a.m; k++)\n", + " c[i][j] += a[i][k] * b[i][k];\n", + " return c;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Динамика\n", + "\n", + "Некоторые динамики можно выразить в терминах матричного умножения.\n", + "\n", + "Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{pmatrix}\n", + "f_{n+1} \\\\\n", + "f_{n+2} \\\\\n", + "\\end{pmatrix}\n", + "=\n", + "\\begin{pmatrix}\n", + "0+f_{n+1} \\\\\n", + "f_{n}+f_{n+1} \\\\\n", + "\\end{pmatrix}\n", + "=\n", + "\\begin{pmatrix}\n", + "0 & 1 \\\\\n", + "1 & 1 \\\\\n", + "\\end{pmatrix}\n", + "\\begin{pmatrix}\n", + "f_{n} \\\\\n", + "f_{n+1} \\\\\n", + "\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$.\n", + "\n", + "Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:\n", + "\n", + "$$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$\n", + "\n", + "Это будет работать за $O(n^3 \\log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "matrix binpow (matrix a, int p) {\n", + " matrix b(n, n);\n", + " for (int i = 0; i < n; i++)\n", + " b[i][i] = 1;\n", + " while (p) {\n", + " if (p&1) b = b*a;\n", + " a = a*a;\n", + " p >>= 1;\n", + " }\n", + " return b;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$.\n", + "\n", + "$$\n", + "\\begin{vmatrix}\n", + "1 & 0 & 0 \\\\\n", + "0 & 1 & 0 \\\\\n", + "0 & 0 & 1 \\\\\n", + "\\end{vmatrix}\n", + "$$\n", + "\n", + "В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \\ldots + a_k f_{n-k}$ имеет такую матрицу перехода:\n", + "\n", + "\\begin{pmatrix}\n", + "0 & 1 & 0 & \\ldots & 0 \\\\\n", + "0 & 0 & 1 & \\ldots & 0 \\\\\n", + "\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\\n", + "0 & 0 & 0 & \\ldots & 1 \\\\\n", + "a_k & a_{k-1} & a_{k-2} & \\ldots & a_1 \\\\\n", + "\\end{pmatrix}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Матрица смежности\n", + "\n", + "У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов.\n", + "\n", + "Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86))." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Обобщения\n", + "\n", + "Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел.\n", + "\n", + "Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Определитель\n", + "\n", + "*Определителем* кадратной матрицы $A$ называется такое выражение:\n", + "\n", + "$$ \\det A = |A| = \\ldots $$ \n", + "\n", + "Где $\\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:\n", + "\n", + "$$\n", + "\\begin{vmatrix}\n", + "a & b \\\\\n", + "c & d \\\\\n", + "\\end{vmatrix}\n", + "=\n", + "ad - bc\n", + "$$\n", + "\n", + "У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.\n", + "\n", + "В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Базис\n", + "\n", + "Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. \n", + "\n", + "Базисы есть не только в линейной алгебре. Например, $\\{1, x, x^2\\}$ является базисом всех квадратных трёхчленов. Или $\\{\\neg, \\land, \\lor\\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Метод Крамера и easy пересечение прямых\n", + "\n", + "Пусть нам надо пересечь две прямые.\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "a_1 x + b_1 y + c_1 = 0\n", + "a_2 x + b_2 y + c_2 = 0\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Это то же самое, что найти такие коэффициенты $x$ и $y$, что\n", + "\n", + "$$ x \\vec{a} + y \\vec{b} = -\\vec{c} $$\n", + "\n", + "Площадь параллелограмма, натянутого на $\\vec{a}$ и $\\vec{b}$, равна векторному произведению, или детерминанту.\n", + "\n", + "По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$.\n", + "\n", + "Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## *Собственные векторы\n", + "\n", + "Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление.\n", + "\n", + "$ Av = k v $, где $k \\neq 0$.\n", + "\n", + "$ Av - kv = (A-kI)v = 0 $. Это означает\n", + "\n", + "Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Системы уравнений и метод Гаусса\n", + "\n", + "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", + "\n", + "Нас по сути просят решить следующую систему:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", + "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", + "\\ldots \\\\\n", + "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", + "\n", + "Метод Крамера неоптимален — там $O(n^4)$ операций.\n", + "\n", + "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "t gauss (matrix a) {\n", + " for (int i = 0; i < n; i++) {\n", + " int nonzero = i;\n", + " for (int j = i+1; j < n; j++)\n", + " if (a[j][i])\n", + " nonzero = j;\n", + " swap(a[nonzero], a[i]);\n", + " for (int j = 0; j < n; j++)\n", + " if (j != i && a[j][i])\n", + " a[j] ^= a[i];\n", + " }\n", + " t x;\n", + " for (int i = 0; i < n; i++)\n", + " x[i] = a[i][n] ^ a[i][i];\n", + " return x;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81))." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/matching-checkpoint.ipynb b/img/.ipynb_checkpoints/matching-checkpoint.ipynb new file mode 100644 index 0000000..e91f1a4 --- /dev/null +++ b/img/.ipynb_checkpoints/matching-checkpoint.ipynb @@ -0,0 +1,234 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ipi0UHuYJPXr" + }, + "source": [ + "# Паросочетания\n", + "\n", + "Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "YEm8GM0TKncl" + }, + "source": [ + "**Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$).\n", + "\n", + "Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием.\n", + "\n", + "*Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны.\n", + "\n", + "Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ThCPzQPwU5y0" + }, + "source": [ + "**Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер.\n", + "\n", + "**Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.\n", + "\n", + "**Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию.\n", + "\n", + "![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg)\n", + "\n", + "*Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \\to 8 \\to 4 \\to 6 \\to 3 \\to 7$.*\n", + "\n", + "Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.\n", + "\n", + "В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.\n", + "\n", + "**Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "wrrJplYwbCB8" + }, + "outputs": [], + "source": [ + "const int maxn;\n", + "\n", + "vector g[maxn]; // будем хранить только рёбра из левой доли в правую\n", + "int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой)\n", + "bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом\n", + "\n", + "// dfs возвращает, можно ли найти путь из вершины v\n", + "// в какую-нибудь вершину правой доли\n", + "// если можно, то ещё и проводит чередование\n", + "bool dfs (int v) {\n", + " if (used[v])\n", + " return false;\n", + " used[v] = true;\n", + " for (int u : g[v]) {\n", + " // если вершина свободна, то можно сразу с ней соединиться\n", + " // если она занята, то с нейможно соединиться только тогда,\n", + " // когда из её текущей пары можно найти какую-нибудь другую вершину\n", + " if (mt[u] == -1 || dfs(mt[u])) {\n", + " mt[u] = v;\n", + " return true;\n", + " }\n", + " }\n", + " return false;\n", + "}\n", + "\n", + "\n", + "// где-то в main:\n", + "\n", + "memset(mt, -1, sizeof(mt));\n", + "for (int i = 0; i < n; i++) {\n", + " memset(used, 0, sizeof(mt));\n", + " if (dfs(i))\n", + " cnt++;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fm3P_M7fKdRQ" + }, + "source": [ + "## Корректность\n", + "\n", + "Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.\n", + "\n", + "**Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным.\n", + "\n", + "**Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \\leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу.\n", + "\n", + "Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hVHj5qr1czGd" + }, + "source": [ + "## Скорость работы\n", + "\n", + "Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$.\n", + " \n", + "Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \\approx 10^4$), потому что для него есть мощные неасимптотические оптимизации:\n", + "\n", + "* Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).\n", + "\n", + "* Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу.\n", + "\n", + "* Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \\times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.\n", + " \n", + "Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \\sqrt m)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Покрытие путями DAG-а\n", + "\n", + "Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.\n", + "\n", + "Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\\overline{H}$ следующим образом:\n", + "* В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно.\n", + "* Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$.\n", + "* Теперь из графа $H$ сделаем граф $\\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$.\n", + "\n", + "Если мы рассмотрим любой путь $v_1, v_2, \\ldots, v_k$ в исходном графе $G$, то в графе $\\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \\ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$.\n", + "\n", + "Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\\overline{H}$ — это паросочетание в $H$ (напомним, это $\\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$.\n", + "\n", + "Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них.\n", + "\n", + "Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "B_YEyFpNUS36" + }, + "source": [ + "## Лемма Холла\n", + "\n", + "*Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.\n", + "\n", + "![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif)\n", + "\n", + "**Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.\n", + "\n", + "**Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием.\n", + "\n", + "В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.\n", + "\n", + "**База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. \n", + "\n", + "**Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием.\n", + "\n", + "Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)\n", + "\n", + "Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \\leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$).\n", + "\n", + "Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yq3YRWxFJWaY" + }, + "source": [ + "## Для ноулайферов: матроиды\n", + "\n", + "С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid).\n", + "\n", + "Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*.\n", + "\n", + "**Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "21_matching.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/img/.ipynb_checkpoints/matroid-checkpoint.ipynb b/img/.ipynb_checkpoints/matroid-checkpoint.ipynb new file mode 100644 index 0000000..43ba14e --- /dev/null +++ b/img/.ipynb_checkpoints/matroid-checkpoint.ipynb @@ -0,0 +1,178 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Матроиды" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства:\n", + "\n", + "* Пустое множество является независимым: $\\varnothing \\in I$\n", + "\n", + "* Любое подмножество независимого множества тоже независимо: \n", + "$$A \\subset B, B \\in I \\implies A \\in I$$\n", + "\n", + "* Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$:\n", + "$$A, B \\in I, |A| < |B| \\implies \\exists x \\in B \\setminus A: A \\cup \\{x\\} \\in I$$\n", + "\n", + "Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \\sum w(a_i)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "X.sort()\n", + "s = []\n", + "for x in X:\n", + " if good(s + [x]):\n", + " s += [x]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Здесь под `good` имеется в виду $s \\cup x \\in I$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Корректность этого алгоритма для любого матроида доказывает следующая теорема:\n", + "\n", + "## Теорема Радо-Эдмондса\n", + "\n", + "Пусть $A \\in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \\cup x \\in I,\\;x \\notin A,\\;w(x)$ — минимальна. Тогда $A \\cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*Доказательство: **\n", + "\n", + "Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$.\n", + "\n", + "Из свойств матроида: $\\exists y \\in B \\setminus A : A \\cup y \\in I$.\n", + "\n", + "Тогда верны два неравенства:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "w(A \\cup y) = w(A) + w(y) \\geq w(B) \\implies w(A) \\geq w(B) - w(y) \\\\\n", + "w(B \\setminus y) = w(B) - w(y) \\geq w(A) \\implies w(A) \\leq w(B) - w(y)\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно,\n", + "$w(A \\cup y) = w(A) + w(y) = w(B)$.\n", + "\n", + "Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \\cup x \\in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Примеры\n", + "\n", + "Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.\n", + "\n", + "### Минимальный остов\n", + "Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом:\n", + "\n", + "* Граф без ребер является лесом.\n", + "* Если удалить из леса ребра, он останется лесом.\n", + "* Пусть есть два леса $|A| \\leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \\cup \\{x\\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности.\n", + "\n", + "Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.\n", + "\n", + "### Расписания\n", + "Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.\n", + "\n", + "Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \\geq i$ для всех $i$.\n", + "\n", + "Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: \n", + "\n", + "* Пустой набор заданий всегда можно сделать.\n", + "* Если у нас стало меньше заданий, то их сделать мы тоже успеем.\n", + "* Пусть есть два правильных набора $|A| \\leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \\cup x$ — тоже правильный набор.\n", + "\n", + "Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных.\n", + "\n", + "### Паросочетания\n", + "Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом:\n", + "\n", + "* Любое паросочетание покрывает пустое множество вершин.\n", + "* Исходное паросочетание покрывает также и любое подмножество исходных вершин.\n", + "* Пусть есть два множества вершин $|A| \\leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$.\n", + "\n", + "Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна.\n", + "\n", + "### Линейно независимые вектора\n", + "\n", + "(TODO) (Школьники не обязаны знать линал.)\n", + "\n", + "Такие штуки будем называть базисами.\n", + "\n", + "* Ноль есть в любом базисе.\n", + "* Подмножество базиса — базис.\n", + "* ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb b/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb new file mode 100644 index 0000000..b8487b7 --- /dev/null +++ b/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Поток минимальной стоимости\n", + "\n", + "Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна:\n", + "\n", + "$$ \\sum_{(u, v) \\in E} f_{uv} \\to \\max $$\n", + "$$ \\sum_{(u, v) \\in E} f_{uv} w_{uv} \\to \\min $$\n", + "\n", + "Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Критерий оптимальности\n", + "\n", + "Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот).\n", + "\n", + "**Доказательство:** \n", + "\n", + "$\\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению.\n", + "\n", + "$\\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Отмена циклов\n", + "\n", + "Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу.\n", + "\n", + "Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Дополняющие пути\n", + "\n", + "Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.\n", + "\n", + "**Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса.\n", + "\n", + "Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз.\n", + "\n", + "Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Потенциалы Джонсона\n", + "\n", + "Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:\n", + "\n", + "$$ w_{uv}' = w_{uv} + d_u - d_v $$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение 1**. Веса всех рёбер графа неотрицательные.\n", + "\n", + "**Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$?\n", + "\n", + "Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой:\n", + "$$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими.\n", + "\n", + "**Доказательство**. Распишем новую стоимость пути из $a$ в $z$.\n", + "\n", + "$$\n", + "\\begin{align}\n", + "w_{ab}' + \\ldots + w_{yz}'\n", + "&= (w_{ab} + \\ldots + w_{yz}) + (d_a + \\ldots + d_y) - (d_b + \\ldots + d_z)\n", + "\\\\&= (w_{ab} + \\ldots + w_{yz}) + d_a - d_z\n", + "\\end{align}\n", + "$$\n", + "\n", + "Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу.\n", + "\n", + "Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.\n", + "\n", + "Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими).\n", + "\n", + "**Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Итоговый алгоритм\n", + "\n", + "* Модифицируем сеть, добавивив обратные рёбра.\n", + "* Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.\n", + "* Пока максимальный поток не найден:\n", + "* * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$.\n", + "* * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \\leadsto t$, обновить остаточную сеть." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Асимптотика\n", + "\n", + "Алгоритм работает за $O(U m \\log n)$ или $O(U n^2)$ в случае плотных графов.\n", + "\n", + "В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \\leq n$ или $U \\leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Реализация\n", + "\n", + "Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).\n", + "\n", + "* `cost`, `cap` — параметры сети\n", + "* `pot` — потенциалы\n", + "* `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)\n", + "* `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "const int maxn = 305, inf = 1e9;\n", + "\n", + "int n;\n", + "int cost[maxn][maxn], cap[maxn][maxn];\n", + "int d[maxn], pot[maxn], par[maxn];\n", + "\n", + "bool dijkstra (int s, int t) {\n", + " used[maxn] = {0};\n", + "\n", + " fill(d, d+n, inf);\n", + " d[s] = 0;\n", + "\n", + " while (1) {\n", + " int v = -1;\n", + " for (int u = 0; u < n; u++)\n", + " if (!used[u] && (v == -1 && d[u] < d[v]))\n", + " v = u;\n", + " if (v == -1 || d[v] == inf)\n", + " break;\n", + " used[v] = 1;\n", + " for (int u = 0; u < n; u++) {\n", + " int w = cost[v][u] + pot[v] - pot[u];\n", + " if (cap[v][u] && d[u] > d[v] + w) {\n", + " d[u] = d[v] + w;\n", + " par[u] = v;\n", + " }\n", + " }\n", + " }\n", + "\n", + " return d[t] < inf;\n", + "}\n", + "\n", + "int mincost_maxflow (int s, int t) {\n", + " int ans = 0;\n", + " while (dijkstra(s, t)) {\n", + " memcpy(pot, d, sizeof(d));\n", + " int delta = inf;\n", + " for (int v = t; v != s; v = par[v])\n", + " delta = min(delta, cap[par[v]][v]);\n", + " for (int v = t; v != s; v = par[v]) {\n", + " cap[par[v]][v] -= delta;\n", + " cap[v][par[v]] += delta;\n", + " ans += cost[par[v]][v]*delta;\n", + " }\n", + " }\n", + " return ans;\n", + "}" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/segtree-checkpoint.ipynb b/img/.ipynb_checkpoints/segtree-checkpoint.ipynb new file mode 100644 index 0000000..a1dfb3b --- /dev/null +++ b/img/.ipynb_checkpoints/segtree-checkpoint.ipynb @@ -0,0 +1,680 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Nyg7f6SD69cK" + }, + "source": [ + "# Дерево отрезков" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "bYsinHWyrf6J" + }, + "source": [ + "**Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "LQzmaXb_G-r4" + }, + "source": [ + "**Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "UmP1mb6M69cM" + }, + "source": [ + "Рассмотрим конкретную задачу:\n", + "\n", + ">Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов:\n", + "\n", + ">1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`).\n", + ">2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$.\n", + "\n", + ">Оба запроса нужно обрабатывать за время $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "CG0jO6Nt69cN" + }, + "source": [ + "Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:\n", + "\n", + "Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.\n", + "\n", + "Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков»." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "f8HIyZkR69cO" + }, + "source": [ + "![alt text](http://i.imgur.com/GGBmcEP.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "084RAPPptpaJ" + }, + "source": [ + "Строить его можно рекурсивной функцией:\n", + "* Если вершина является листом, взять в качестве суммы значение соответствующей ячейки.\n", + "* Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "NNMuiaKW69cQ" + }, + "source": [ + "## Разные свойства\n", + "\n", + "Высота такого дерева есть величина $\\Theta(\\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики.\n", + "\n", + "Более того, любой полуинтервал разбивается на $O(\\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков.\n", + "\n", + "Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \\frac{n}{2} + \\frac{n}{4} + \\frac{n}{8} + \\ldots + 1 < 2n$. Значит, оно линейное по памяти.\n", + "\n", + "При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "NQDy4Wn_69cT" + }, + "source": [ + "## Ок, как это нам поможет?\n", + "\n", + "Опишем теперь, как с помощью такой структуры решить задачу.\n", + "\n", + "**Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$.\n", + "\n", + "Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\\Theta(\\log n)$ — по одной с каждого уровня.\n", + "\n", + "Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков.\n", + "\n", + "**Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения.\n", + "\n", + "Сделаем тоже рекурсивную функцию, рассмотрев три случая:\n", + "\n", + "* Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму.\n", + "* Если отрезки вершины и запроса не пересекаются, то вернуть 0.\n", + "* Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей.\n", + "\n", + "Чтобы разобраться, почему это работает за $O(\\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\\log n)$ отрезках, а значит и итоговая асимптотика будет такая же." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fJ3qz5gzQYHO" + }, + "source": [ + "## Ликбез по C++\n", + "\n", + "Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества.\n", + "\n", + "Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HPeG6w4DRHG4" + }, + "source": [ + "Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\\log n)$.\n", + "\n", + "В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hWKKu4mgQ4kA" + }, + "source": [ + "У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CodmY2a4QmjN" + }, + "outputs": [], + "source": [ + "struct A {\n", + " int param1, param2; // тут можно что-то хранить\n", + " char param3 = 'k';\n", + " A (int var) {\n", + " // эта часть называется конструктором\n", + " // ...\n", + " }\n", + " void do_something () {\n", + " // это какой-то другой метод\n", + " // ...\n", + " }\n", + "}; // <- не забудьте точку с запятой" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6xCBNd6TR1vD" + }, + "source": [ + "Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве.\n", + "\n", + "Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "dHxKkzeLsaLW" + }, + "outputs": [], + "source": [ + "A x(179);\n", + "x.do_something();\n", + "x.param1 = 57;\n", + "\n", + "A *y = new A(42); // new возвращает адрес, по которому можно найти объект\n", + "y->do_something();\n", + "y.param3 = '!';" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6xy-KwmI2DiJ" + }, + "source": [ + "Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Z3VyDnQ769cU" + }, + "source": [ + "## Реализация" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "EKO6Hwdl69cV" + }, + "source": [ + "Общий план реализации любых структур данных:\n", + "1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc.\n", + "2. Формально описать, что должны делать методы и за какую асимптотику.\n", + "3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "x_z_73w-69cW" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb; // левые и правые границы отрезков\n", + " int sum = 0; // сумма на текущем отрезке\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " if (lb + 1 < rb) {\n", + " // если не лист, создаем детей\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void add (int k, int x) {\n", + " sum += x;\n", + " if (l) {\n", + " if (k < l->rb)\n", + " l->add(k, x);\n", + " else\n", + " r->add(k, x);\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " if (lb >= lq && rb <= rq)\n", + " // если мы лежим полностью в отрезке запроса, вывести сумму\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " // если мы не пересекаемся с отрезком запроса, вывести ноль\n", + " return 0;\n", + " // иначе всё сложно -- запускаемся от детей и пусть они там сами решают\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "30NQsY2jytu2" + }, + "source": [ + "> Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$).\n", + "\n", + "Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи:\n", + "* Запросим сумму от $k$ до $n$ в ДО.\n", + "* Добавим единичку в $k$-тую позицию в ДО.\n", + "\n", + "Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \\log n)$ запросов." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "S8DH5Zxg3UqU" + }, + "source": [ + "> Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике.\n", + "\n", + "Ответим на все запросы в оффлайн, используя метод сканирующей прямой:\n", + "\n", + "* Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \\times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \\times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \\times [y_1, y_2]$.\n", + "* Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше.\n", + "* Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$».\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "47Fb26oz69ca" + }, + "source": [ + "## Отложенные операции" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "tANMKBYT5CVZ" + }, + "source": [ + "Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному.\n", + "\n", + "Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем.\n", + "\n", + "Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено.\n", + "\n", + "Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.\n", + "\n", + "Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "eHe1O2d769cc" + }, + "source": [ + "При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "rJo8yvgHUPHL" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb;\n", + " int sum = 0, assign = -1;\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " if (lb + 1 < rb) {\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void push () {\n", + " if (assign != -1) {\n", + " sum = (rb-lb) * assign;\n", + " if (l) { // если дети есть\n", + " l->assign = assign;\n", + " r->assign = assign;\n", + " }\n", + " }\n", + " assign = -1;\n", + " }\n", + " void upd (int lq, int rq, int x) {\n", + " push();\n", + " if (lq <= lb && rb <= rq)\n", + " assign = x;\n", + " else if (l && max(lb, lq) < min(rb, rq)) {\n", + " // если есть дети и отрезок запроса хоть как-то пересекается с нашим\n", + " l->upd(lq, rq, x);\n", + " r->upd(lq, rq, x);\n", + " // ...дальше они сами разберутся\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " push();\n", + " if (lb >= lq && rb <= rq)\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " return 0;\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_DmNvAVzMzq4" + }, + "source": [ + "По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах.\n", + "\n", + "Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "oGLSfyMG69cd" + }, + "source": [ + "## Динамическое построение" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fZM4fTzu69cd" + }, + "source": [ + "А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\\log_2 10^6 \\approx 20$, $\\log_2 10^9 \\approx 30$), кроме этапа построения.\n", + "\n", + "Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.\n", + "\n", + "Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "z8PYwWA9niAY" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb;\n", + " int sum = 0;\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " // а тут ничего нет\n", + " }\n", + " void extend () {\n", + " if (!l && lb + 1 < rb) {\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void add (int k, int x) {\n", + " extend();\n", + " sum += x;\n", + " if (l) {\n", + " if (k < l->rb)\n", + " l->add(k, x);\n", + " else\n", + " r->add(k, x);\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " if (lb >= lq && rb <= rq)\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " return 0;\n", + " extend();\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SSlRrVNXxREm" + }, + "source": [ + "Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "lGfAy5Af1S5J" + }, + "outputs": [], + "source": [ + "vector compress (vector a) {\n", + " vector b = a;\n", + " sort(b.begin(), b.end());\n", + " b.erase(unique(b.begin(), b.end()), b.end());\n", + " for (int &x : a) \n", + " x = int(lower_bound(b.begin(), b.end(), x) - b.begin());\n", + " return a;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "eRM_sfhirR_W" + }, + "source": [ + "## Персистентность\n", + "\n", + "Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния.\n", + "\n", + "Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.\n", + "\n", + "У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \\log n)$." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "OA93mqmdsO46" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb;\n", + " int sum = 0;\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " if (lb != rb) {\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void copy () {\n", + " if (l) {\n", + " l = new segtree(l);\n", + " r = new segtree(r);\n", + " }\n", + " }\n", + " void add (int k, int x) {\n", + " copy();\n", + " sum += x;\n", + " if (l) {\n", + " if (k < l->rb) l->add(k, x);\n", + " else r->add(k, x);\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " // этот метод ничего не меняет -- он и так хороший\n", + " if (lq <= lb && rb <= rq)\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " return 0;\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "kMg_NB4AsRMa" + }, + "source": [ + "> Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике.\n", + "\n", + "Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \\log n)$ памяти.\n", + "\n", + "> Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке.\n", + "\n", + "Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу.\n", + "\n", + "Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`.\n", + "\n", + "Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом.\n", + "\n", + "> Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз.\n", + "\n", + "У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\\frac{1}{2^{100}}$, и ей на практике можно пренебречь.\n", + "\n", + "Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "sUXS9Q1zQO1P" + }, + "source": [ + "## Другие реализации\n", + "\n", + "Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:\n", + "\n", + "**На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree).\n", + "\n", + "**«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yCjbp1XhwsU2" + }, + "source": [ + "## Задачи\n", + "\n", + "* [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции.\n", + "* Второй контест — на отложенные операции.\n", + "* [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "1-2_segtree.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb b/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb new file mode 100644 index 0000000..d7e57b7 --- /dev/null +++ b/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb @@ -0,0 +1,119 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Разреженная таблица\n", + "\n", + "- Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \\log n)$ с малой константой.\n", + "- Обновления не поддерживает (static RMQ).\n", + "- Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.\n", + "- Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).\n", + "- Требует $O(n \\log n)$ памяти.\n", + "- Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Определим разреженную таблицу как двумерный массив размера $n \\times\\log n$:\n", + "\n", + "$$\n", + "t[i][k] = \\min \\{ a_i, a_{i+1}, \\ldots, a_{i+2^k-1} \\}\n", + "$$\n", + "\n", + "Идея такая: считаем минимум на каждом отрезке длины $2^k$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Такой массив можно посчитать за его размер: $t[i][k] = \\min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int a[maxn], lg[maxn], mx[maxn][logn];\n", + "\n", + "int rmq (int l, int r) {\n", + " int t = lg[r-l+1];\n", + " return min(mx[l][t], mx[r-(1<= 0; i--) {\n", + " mx[i][0] = a[i];\n", + " for (int l = 0; l < logn-1; l++)\n", + " mx[i][l+1] = max(mx[i][l], mx[i+(1< Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него.\n", + "\n", + "Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \\cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго.\n", + "\n", + "Для решения этой задачи за линейное время придумали **префикс-функцию**.\n", + "\n", + "**Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \\ldots s_i$, который также является и суффиксом этой строки (не считая всю строку).\n", + "\n", + "Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 638, + "status": "ok", + "timestamp": 1541170751859, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "RUT-T92559lq", + "outputId": "b2a1115c-11ec-46c8-bc95-a62aa3458597" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 0, 1, 0, 1, 2, 3, 4]" + ] + }, + "execution_count": 7, + "metadata": { + "tags": [] + }, + "output_type": "execute_result" + } + ], + "source": [ + "def slow_prefix_function(s):\n", + " n = len(s)\n", + " p = [0]*n\n", + " for i in range(n):\n", + " prefix = s[:i]\n", + " for l in range(1, i):\n", + " if prefix[:l] == prefix[-l:]:\n", + " p[i] = l\n", + " return p\n", + "\n", + "slow_prefix_function('aataataa')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "rm6kd4D86GG3" + }, + "source": [ + "(Этот алгоритм работает за $O(n^3)$, но это только пока.)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "1-8TSS2X3KV_" + }, + "source": [ + "## Как это поможет решить исходную задачу?\n", + "\n", + "Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.\n", + "\n", + "Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\\#t$." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 74 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 615, + "status": "ok", + "timestamp": 1541171240549, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "ppqZBPNJ43v3", + "outputId": "c23a3d9f-79aa-4f91-aff1-c7b574018535" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!\n", + "00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000\n" + ] + } + ], + "source": [ + "s = \"let it go\"\n", + "t = \"\"\"let it go, let it go\n", + "can't hold it back anymore\n", + "let it go, let it go\n", + "turn away and slam the door!\"\"\"\n", + "\n", + "print((s + '#' + t).replace('\\n', ' '))\n", + "print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "42poAv3b44FM" + }, + "source": [ + "Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$.\n", + "\n", + "Такой алгоритм (посчитать префикс-функцию от $s\\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "mTIyGVuD3F8W" + }, + "source": [ + "## Как её быстро считать\n", + "\n", + "Рассмотрим ещё несколько примеров префикс-функций:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 90 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 610, + "status": "ok", + "timestamp": 1541170871458, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "GqsPPTjl75Wp", + "outputId": "2b05fe9f-5eab-44bb-e651-c207c0723ae4" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0, 0, 1, 2, 3]\n", + "[0, 0, 0, 0, 0, 0]\n", + "[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]\n", + "[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n" + ] + } + ], + "source": [ + "for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:\n", + " print(slow_prefix_function(s))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HO_PjgzR7514" + }, + "source": [ + "Можно заметить несколько особенностей:\n", + "* $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс;\n", + "* $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \\ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \\ldots s_i$, длина которого будет ровно на единицу меньше.\n", + "\n", + "Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения.\n", + "\n", + "Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\\underbrace{aabaa}t\\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$.\n", + "\n", + "Но что происходит, когда $s_{p_i}\\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$.\n", + "* $\\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5.\n", + "* $\\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa».\n", + "* $\\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$.\n", + "* $\\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$.\n", + "\n", + "В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \\neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 844, + "status": "ok", + "timestamp": 1541171880825, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "n22PhVaYUlp4", + "outputId": "830132ae-5568-4c3d-ae32-3ecaf51e7734" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]" + ] + }, + "execution_count": 20, + "metadata": { + "tags": [] + }, + "output_type": "execute_result" + } + ], + "source": [ + "def fast_prefix_function(s):\n", + " n = len(s)\n", + " p = [0]*n\n", + " for i in range(1, n):\n", + " cur = p[i - 1]\n", + " # перебираем префикс-функцию, пока не найдем равный символ\n", + " while s[i] != s[cur] and cur > 0:\n", + " cur = p[cur - 1]\n", + " # если нашли, то значение на единицу больше\n", + " if s[i] == s[cur]:\n", + " p[i] = cur + 1\n", + " return p\n", + "\n", + "fast_prefix_function('abacabadabacabax')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "skwRhzxQUlp9" + }, + "source": [ + "### Асимптотика\n", + "\n", + "Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой.\n", + "\n", + "**Доказательство**:\n", + "* Как мы ранее заметили, префикс функция возрастает максимум на единицу.\n", + "* $\\implies$ Вырасти она может максимум $n-1$ раз.\n", + "* Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один.\n", + "* $\\implies$ Количество понижений не больше количества повышений, то есть $O(n)$.\n", + "* $\\implies$ Суммарно итераций цикла `while` — $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "cDkYM1JMUlqC" + }, + "source": [ + "## Z-функция\n", + "\n", + "Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания.\n", + "\n", + "Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$.\n", + "\n", + "$$\\underbrace{aba}c\\overbrace{aba}daba \\hspace{1em} (z_4 = 3)$$" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]\n", + "[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]\n", + "[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]\n" + ] + } + ], + "source": [ + "def slow_z_function(s):\n", + " n = len(s)\n", + " z = [0]*n\n", + " for i in range(1, n):\n", + " suffix = s[i:]\n", + " for l in range(1, i):\n", + " if s[:l] == suffix[:l]:\n", + " z[i] = l\n", + " return z\n", + "\n", + "for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:\n", + " print(slow_z_function(s))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Как её быстро считать\n", + "\n", + "Заметим, что:\n", + "* $z_0 = 0$ — из-за договоренности (потому что информации не несет);\n", + "* $z_i = 0 \\iff s_i \\neq s_0$;\n", + "* $z_i > 0 \\iff s_i = s_0$.\n", + "\n", + "Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$.\n", + "\n", + "Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее.\n", + "* Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком.\n", + "* Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def fast_z_function(s):\n", + " n = len(s)\n", + " z = [0]*n\n", + " l = 0\n", + " r = 0\n", + " for i in range(1, n):\n", + " if i <= r:\n", + " z[i] = min(r - i + 1, z[i-l])\n", + " while i + z[i] < n and s[z[i]] == s[i+z[i]]:\n", + " z[i] += 1\n", + " if i + z[i] - 1 > r:\n", + " l = i\n", + " r = i + z[i] - 1\n", + " return z" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Зачем тогда люди используют префикс-функцию\n", + "\n", + "![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg)\n", + "\n", + "В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.\n", + "\n", + "Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "8_kmp-trie.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb b/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb new file mode 100644 index 0000000..16bf12b --- /dev/null +++ b/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Суффиксный массив\n", + "\n", + "Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.\n", + "\n", + "Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.\n", + "\n", + "\n", + "\n", + "*
«Паблик с тупыми шутками про проганье»
*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мотивация\n", + "\n", + "**Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки.\n", + "\n", + "\n", + "\n", + "**Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно.\n", + "\n", + "В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс.\n", + "\n", + "Работать такой алгоритм будет за $O(|t| \\log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \\log |s|)$, что является одним из самых оптимальных алгоритмов поиска.\n", + "\n", + "Теперь научимся его строить." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Построение за $O(n \\log n)$\n", + "\n", + "Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара.\n", + "\n", + "Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \\log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \\log^2 n)$. Но это не самый быстрый и удобный алгоритм.\n", + "\n", + "Наш алгоритм будет состоять из $\\lceil \\log n \\rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив.\n", + "\n", + "Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов).\n", + "\n", + "Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4.\n", + "\n", + "$$\n", + " p_0 = (0, 1, 3, 2) \\;\\;\\; c_0 = (0, 0, 1, 0)\n", + "\\\\ p_1 = (0, 3, 1, 2) \\;\\;\\; c_1 = (0, 1, 2, 0)\n", + "\\\\ p_2 = (3, 0, 1, 2) \\;\\;\\; c_2 = (1, 2, 3, 0)\n", + "$$\n", + "\n", + "Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом.\n", + "\n", + "Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором.\n", + "\n", + "Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \\log^2 n)$, потому что каждый этап будет работать за $O(n \\log n$).\n", + "\n", + "**Оптимизация до $O(n \\log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "// строка -- это последовательность чисел от 1 до размера алфавита\n", + "vector suffix_array (vector &s) {\n", + " s.push_back(0); // добавляем нулевой символ в конец строки\n", + " int n = (int) s.size(),\n", + " cnt = 0, // вспомогательная переменная: счётчик для сортировки \n", + " cls = 0; // количество классов эквивалентности\n", + " vector c(n), p(n);\n", + " \n", + " map< int, vector > t;\n", + " for (int i = 0; i < n; i++)\n", + " t[s[i]].push_back(i);\n", + " \n", + " // «нулевой» этап\n", + " for (auto &x : t) {\n", + " for (int u : x.second)\n", + " c[u] = cls, p[cnt++] = u;\n", + " cls++;\n", + " }\n", + " \n", + " // пока все суффиксы не стали уникальными\n", + " for (int l = 1; cls < n; l++) {\n", + " vector< vector > a(cls); // массив для сортировки подсчётом\n", + " vector _c(n); // новые классы эквивалентности\n", + " int d = (1<(p.begin()+1, p.end());\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: переписать это" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наибольшие общие префиксы\n", + "\n", + "Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами.\n", + "\n", + "TODO" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/img/.ipynb_checkpoints/treap-checkpoint.ipynb b/img/.ipynb_checkpoints/treap-checkpoint.ipynb new file mode 100644 index 0000000..abe71e6 --- /dev/null +++ b/img/.ipynb_checkpoints/treap-checkpoint.ipynb @@ -0,0 +1,956 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "dMVa59dX96P7" + }, + "source": [ + "# Декартово дерево" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ya9SKeu196QG" + }, + "source": [ + "Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века.\n", + "\n", + "Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.\n", + "\n", + "Декартово дерево же определяется и строится так:\n", + "\n", + "* Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*.\n", + "* Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*.\n", + "* От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.\n", + "* Аналогично, запустимся от правой части и добавим корню правого сына.\n", + "\n", + "Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно.\n", + "\n", + "Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "eRd8qABz96QP" + }, + "source": [ + "![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "DLS2nKEPK1lC" + }, + "source": [ + "Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий:\n", + "\n", + "* Дерамида (дерево + пирамида)\n", + "* ПиВо (пирамида + дерево)\n", + "* КуРево (куча + дерево)\n", + "* Treap (tree + heap)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "wSD-3iTFK7s8" + }, + "source": [ + " ## Бинарные деревья\n", + " \n", + " С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например:\n", + "\n", + "* добавить число $x$ в множество\n", + "* определить, есть ли в множестве число $x$\n", + "* найти первое число, не меньшее $x$ (`lower_bound`)\n", + "* найти количество чисел в промежутке $[l, r]$\n", + "\n", + "При этом все операции — за $O(\\log n)$.\n", + "\n", + "На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "2zy0SLY296QW" + }, + "source": [ + "## Приоритеты и асимптотика\n", + "\n", + "В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Ox2hsVjW96Qg" + }, + "source": [ + "**Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \\log n)$.\n", + "\n", + "Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.\n", + "\n", + "**Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*.\n", + "\n", + "Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна\n", + "\n", + "$$d_i = \\sum_{j=1}^n a(j, i)$$\n", + "\n", + "Её матожидание равно\n", + "\n", + "$$E[d_i] = E[\\sum_{j \\neq i} a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} p(j, i)$$\n", + "\n", + "где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.\n", + "\n", + "Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.\n", + "\n", + "**Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$).\n", + "\n", + "**Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. \n", + "\n", + "**Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет.\n", + "\n", + "У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей:\n", + "\n", + "$$p(x, y) = \\frac{1}{y-x+1}$$\n", + "\n", + "Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:\n", + "\n", + "$$E[d_i] = \\sum_{j \\neq i} p(j, i) = \\sum_{j \\neq i} \\frac{1}{|i-j|+1} \\leq \\sum_{i=1}^n \\frac{1}{n} = O(\\log n)$$\n", + "\n", + "Перед последним переходом мы получили сумму гармонического ряда.\n", + "\n", + "Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя.\n", + "\n", + "**Упражнение***. Выведите из этого доказательства асимптотику `quicksort`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "qmboul4Bfkpc" + }, + "source": [ + "## Реализация" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "G0lF7L2E96Qj" + }, + "source": [ + "Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "alEQhUGK46qI" + }, + "source": [ + "Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "11VDG7yx96Qm" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " int key, prior;\n", + " Node *l = 0, *r = 0;\n", + " Node (int _key) { key = _key, prior = rand(); }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "kTPpsriu96Qx" + }, + "source": [ + "Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Vnu5kpTE96Q0" + }, + "source": [ + "### Merge\n", + "\n", + "Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.\n", + "\n", + "Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше.\n", + "\n", + "Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "1pdbdJ_W96Q2" + }, + "outputs": [], + "source": [ + "Node* merge (Node *l, Node *r) {\n", + " if (!l) return r;\n", + " if (!r) return l;\n", + " if (l->prior > r->prior) {\n", + " l->r = merge(l->r, r);\n", + " return l;\n", + " }\n", + " else {\n", + " r->l = merge(l, r->l);\n", + " return r;\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "byktXFHw96RA" + }, + "source": [ + "### Split\n", + "\n", + "Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$.\n", + "\n", + "В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "jyUv7F-o96RD" + }, + "outputs": [], + "source": [ + "typedef pair Pair;\n", + "\n", + "Pair split (Node *p, int x) {\n", + " if (!p) return {0, 0};\n", + " if (p->key <= x) {\n", + " Pair q = split(p->r, x);\n", + " p->r = q.first;\n", + " return {p, q.second};\n", + " }\n", + " else {\n", + " Pair q = split(p->l, x);\n", + " p->l = q.second;\n", + " return {q.first, p};\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "pOVCO70W96RN" + }, + "source": [ + "### Пример: вставка\n", + "\n", + "`merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. \n", + "\n", + "Вот так, например, будет выглядеть код, добавляющий $x$ в сет." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "wpMr1dwA96RQ" + }, + "outputs": [], + "source": [ + "Node *root = 0;\n", + "\n", + "void insert (int x) {\n", + " Pair q = split(root, x);\n", + " Node *t = new Node(x);\n", + " root = merge(q.first, merge(t, q.second));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "VhOc9WdYjOC0" + }, + "source": [ + "### Пример: модификация для суммы на отрезке\n", + "\n", + "Иногда нам нужно написать какие-то модификации для более продвинутых операций.\n", + "\n", + "Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке»." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "j04PUXvmun3y" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " int val, sum;\n", + " // ...\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "nIYZ3qnxvKiV" + }, + "source": [ + "При `merge` и `split` надо будет поддерживать эту сумму актуальной.\n", + "\n", + "Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "BH_-Bjc4utNt" + }, + "outputs": [], + "source": [ + "void sum (Node* v) { return v ? v->sum : 0; }\n", + "// обращаться по пустому указателю нельзя -- выдаст ошибку\n", + "\n", + "void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; }" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "iduA9cMYjyLI" + }, + "source": [ + "В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "SZod8er7wPVL" + }, + "outputs": [], + "source": [ + "Node* merge (Node *l, Node *r) {\n", + " // ...\n", + " if (...) {\n", + " l->r = merge(l->r, r);\n", + " upd(l);\n", + " return l;\n", + " }\n", + " else {\n", + " // ...\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "5RKCFfiWwZZc" + }, + "outputs": [], + "source": [ + "typedef pair Pair;\n", + "\n", + "Pair split (Node *p, int x) {\n", + " // ...\n", + " if (...) {\n", + " // ...\n", + " upd(p);\n", + " return {p, q.second};\n", + " }\n", + " else {\n", + " // ...\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gRsq8Pqbvsbb" + }, + "source": [ + " Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Gox_llXkdJct" + }, + "outputs": [], + "source": [ + "int sum (int l, int r) {\n", + " Pair rq = split(root, r);\n", + " Pair lq = split(rq.first, l);\n", + " int res = sum(lr.second);\n", + " root = merge(lq.first, merge(lq.second, rq.second));\n", + " return res;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "CSPnJjIe96Rc" + }, + "source": [ + "# Неявный ключ" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "MwlB7zcTOqr-" + }, + "source": [ + "Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.\n", + "\n", + "Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.\n", + "\n", + "Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "uL8WtJztiOHy" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " int key, prior, size = 1;\n", + " // ^ размер поддерева\n", + " Node *l = 0, *r = 0;\n", + " Node (int _key) { key = _key, prior = rand(); }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "sl4BYIseiRoq" + }, + "source": [ + "Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "c9oX5sP_kHhx" + }, + "outputs": [], + "source": [ + "int size (Node *v) { return v ? v->size : 0; }\n", + "\n", + "void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); }" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "n8r-PdHdkIRm" + }, + "source": [ + "`merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа.\n", + "\n", + "Про `split` теперь удобнее думать как \"вырежи первые `k` элементов\". " + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "G2ck_VLK96Rk" + }, + "outputs": [], + "source": [ + "typedef pair Pair;\n", + "\n", + "Pair split (Node *p, int k) {\n", + " if (!p) return {0, 0};\n", + " if (size(p->l) + 1 <= k) {\n", + " Pair q = split(p->r, k - size(p->l) - 1);\n", + " // ^ правый сын не знает количество вершин слева от него\n", + " p->r = q.first;\n", + " upd(p);\n", + " return {p, q.second};\n", + " }\n", + " else {\n", + " Pair q = split(p->l, k);\n", + " p->l = q.second;\n", + " upd(p);\n", + " return {q.first, p};\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "na66j6wGkeW1" + }, + "source": [ + "Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "PqDn4on8gx4u" + }, + "source": [ + "### Пример: ctrl+x, ctrl+v" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "tSiJl_dEjE-1" + }, + "outputs": [], + "source": [ + "Node* ctrlx (int l, int r) {\n", + " Pair q1 = split(root, r);\n", + " Pair q2 = split(q1.first, l);\n", + " root = merge(q2.first, q1.second);\n", + " return q2.second;\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "7VUUuQhpD3r0" + }, + "outputs": [], + "source": [ + "void ctrlv (Node *v, int k) {\n", + " Pair q = split(root, k);\n", + " root = merge(q.first, merge(v, q.second));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hTmR8xpkgp3a" + }, + "source": [ + "### Пример: переворот\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "d-lA26yQZVUZ" + }, + "source": [ + "Нужно за $O(\\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д.\n", + "\n", + "Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "q7ic3tQ4jWco" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " bool rev;\n", + " // ...\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Bg-tJvl-jfql" + }, + "source": [ + "Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Q7nC0YI0jeTi" + }, + "outputs": [], + "source": [ + "void push (node *v) {\n", + " if (v->rev) {\n", + " swap(v->l, v->r);\n", + " if (v->l)\n", + " v->rev ^= 1;\n", + " if (v->r)\n", + " v->rev ^= 1;\n", + " }\n", + " v->rev = 0;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "40Ir36SwZ5GN" + }, + "source": [ + "Аналогично, эту функцию будем вызывать в начале `merge` и `split`.\n", + "\n", + "Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "8Ev2xhTSjRgI" + }, + "outputs": [], + "source": [ + "void reverse (int l, int r) {\n", + " Pair q1 = split(root, r);\n", + " Pair q2 = split(q1.first, l)\n", + " q2.second->rev ^= 1;\n", + " root = merge(q2.first, merge(q2.second, q1.second));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9BI3binGkCmg" + }, + "source": [ + "# Функциональное программирование*\n", + "\n", + "Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно.\n", + "\n", + "Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "dIXpRozXkFVh" + }, + "outputs": [], + "source": [ + "auto apply (int l, int r, auto f) {\n", + " Pair q1 = split(root, r);\n", + " Pair q2 = split(q1.first, l)\n", + " q2.second = f(q2.second);\n", + " root = merge(q2.first, merge(q2.second, q1.second));\n", + "}\n", + "\n", + "void reverse (Node *v) {\n", + " if (v)\n", + " v->rev ^= 1;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "C8eKhyK5fnDK" + }, + "source": [ + "Применять её нужно так:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "GbQ-OEOWEQxM" + }, + "outputs": [], + "source": [ + "apply(l, r, reverse);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "P8_dHs3BHBKo" + }, + "source": [ + "Это работает в плюсах, начиная с `g++14`.\n", + "\n", + "Для простых операций можно даже написать лямбду:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "nPmyzNjKHpNc" + }, + "outputs": [], + "source": [ + "apply(l, r, [](Node *v){\n", + " if (v)\n", + " v->rev ^= 1;\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ZV7EMe68kpvi" + }, + "source": [ + "# Персистентность*\n", + "\n", + "Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.\n", + "\n", + "> Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.\n", + "\n", + "Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим.\n", + "\n", + "> Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**.\n", + "\n", + "Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами.\n", + "\n", + "Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "1sqJhTKQnkWY" + }, + "source": [ + "Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "vrd0UjRu1qDv" + }, + "outputs": [], + "source": [ + "Node* copy (Node *v) { return new Node(*v); }" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "L6METC4WKGbA" + }, + "source": [ + "Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "LuFo9MChKF7b" + }, + "outputs": [], + "source": [ + "Pair split (Node *p, int x) {\n", + " p = copy(p);\n", + " // ...\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "NnMzBYiY1zD7" + }, + "source": [ + "В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.\n", + "\n", + "У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\\frac{L}{L+R}$, иначе за правое.\n", + "\n", + "**Теорема**. Такое переподвешивание эквивалентно приоритетам.\n", + "\n", + "**Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции:\n", + "\n", + "* Лист имеет вероятность 1 быть корнем себя (база индукции)\n", + "* Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\\frac{1}{L} \\cdot \\frac{L}{L+R} = \\frac{1}{L+R}$. С вершинами правого дерева аналогично.\n", + "\n", + "Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "EamOdCWWKM_y" + }, + "outputs": [], + "source": [ + "Node* merge (Node *l, Node *r) {\n", + " if (!l) return r;\n", + " if (!r) return l;\n", + " l = copy(l), r = copy(r);\n", + " if (rand() % (size(l) + size(r)) < size(l)) {\n", + " // ...\n", + " }\n", + " else {\n", + " // ...\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "YcdzH43FyPDl" + }, + "source": [ + "Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$?" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "3-4_treap.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/img/.ipynb_checkpoints/trie-checkpoint.ipynb b/img/.ipynb_checkpoints/trie-checkpoint.ipynb new file mode 100644 index 0000000..4162c8e --- /dev/null +++ b/img/.ipynb_checkpoints/trie-checkpoint.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Бор\n", + "\n", + "Бор — это структура данных для компактного хранения строк.\n", + "\n", + "Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. \n", + "\n", + "![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png)\n", + "\n", + "Бор можно удобно использовать для разных задач:\n", + "* Хранение строк — занимает гораздо меньше места, чем массив или сет строк.\n", + "* Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.\n", + "\n", + "* Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Реализация\n", + "\n", + "Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация:\n", + "* терминальная ли вершина,\n", + "* ссылки на детей,\n", + "* возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "const int k = 26;\n", + "\n", + "struct Vertex {\n", + " Vertex* to[k] = {0};\n", + " bool terminal = 0;\n", + "};\n", + "\n", + "Vertex *root = new Vertex();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "void add_string (string &s) {\n", + " v = root;\n", + " for (char c : s) {\n", + " c -= 'a';\n", + " if (!v->to[c]) \n", + " v->to[c] = new Vertex();\n", + " v = v->to[c];\n", + " }\n", + " v->terminal = true;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.\n", + "\n", + "Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Как хранить ссылки\n", + "\n", + "Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми.\n", + "\n", + "В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например.\n", + "\n", + "Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Суффиксные ссылки\n", + "\n", + "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", + "\n", + "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", + "\n", + "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", + "\n", + "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ru/aho-corasick.md b/ru/aho-corasick.md new file mode 100644 index 0000000..06035f9 --- /dev/null +++ b/ru/aho-corasick.md @@ -0,0 +1,91 @@ + +# Алгоритм Ахо-Корасик + +Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время. + +Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик. + + + +Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы. + + + +> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. + +Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. + +Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. + +Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). + +## Зачем это нужно + +> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. + +Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. + +Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. + +Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс. + +## Алгоритм Ахо-Корасик* + +Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой. + +**Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход. + +**Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда. + + + + +```c++ +const int k = 26; + +struct Vertex { + Vertex *to[k] = {0}, *go[k] = {0}; + Vertex *link = 0, *p; + int pch; + Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; } +}; + +Vertex *root = new Vertex(-1, 0); +``` + + +```c++ +void add_string (string s) { + Vertex *v = root; + for (char _c : s) { + c -= 'a'; + if (!v->to[c]) + v->to[c] = new Vertex(c, v); + v = v->to[c]; + } +} +``` + +Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться. + + +```c++ +Vertex* go (Vertex *v, int c); + +Vertex* link (Vertex *v) { + if (!v->link) { + if (v == root || v->p == root) v->link = root; + else v->link = go(link(v->p), v->pch); + } + return v->link; +} + +Vertex* go (Vertex *v, int c) { + if (!v->go[c]) { + if (v->to[c]) v->go[c] = v->to[c]; + else if (v == root) v->go[c] = root; + else v->go[c] = go(link(v), c); + } + return v->go[c]; +} +``` diff --git a/ru/annealing.md b/ru/annealing.md new file mode 100644 index 0000000..8d5bb22 --- /dev/null +++ b/ru/annealing.md @@ -0,0 +1,138 @@ + +# Метод отжига + +* Эвристический метод глобальной оптимизации +* Работает с дискретными функциями в предположении, что у близких «точек» близкое значение, и есть что-то типа градиента +* Очень простой в написании +* Никаких гарантий нет, но хорошо работает на практике +* Метод настолько мощный, что иногда им можно сдать задачи, у которых автором предполагалось адекватное решение (пример: [Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам](http://codeforces.com/contest/745/submission/23067030)) + +![](https://camo.githubusercontent.com/574bf26bf301827efaa18748440fa77fab8c5a4f/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f642f64352f48696c6c5f436c696d62696e675f776974685f53696d756c617465645f416e6e65616c696e672e676966) + +## Постановка задачи + +Имеется некоторая функция $f(x)$, которую мы хотим минимизировать. + +Наука решать эту задачу в общем случае не умеет, и вряд ли когда-либо научится. + +TODO: описание алгоритма, физическая аналогия, конструирование гладких функций + +При решении конкретной задачи нужно менять только функцию `fitness` и, возможно, параметры алгоритма. + +## Ферзи + +Рассмотрим такую задачу: дана шахматная доска $n \times n$ и $n$ ферзей. Нужно расставить их так, чтобы они не били друг друга. + +Будем кодировать состояние перестановкой чисел от $1$ до $n$. Такое представление кодирует не все состояния, но оно точно не учитывает те, где ферзи бьют друг друга по вертикали или горизонтали. + +Выберем такую функцию: fitness(p) = число пар ферзей, которые бьют друг друга. + +Важная деталь: скорость одной итерации прямо пропорционально скорости работы `fitness`, поэтому очень важно реализовать её эффективно. Конкретно здесь её можно реализовать за $O(n^2)$ (используется в коде для наглядности), $O(n)$ и даже $O(1)$. + +Мы будем использовать `python`, потому что в нём удобнее строить графики. Эквивалентный код на C++ приведена сразу после. + + +```python +import random + +# Эти библиотеки будут нужны, чтобы рисовать графики: +import matplotlib.pyplot as plt +%matplotlib inline + +import seaborn as sns +sns.set() +``` + + +```python +def fitness(p): + s = 0 + for i in range(len(p)): + d = 1 + for j in range(i): + if abs(i-j) == abs(p[i]-p[j]): # ферзи стоят на одной диагонали + d = 0 + s += d + return s +``` + + +```python +n = 100 # кол-во ферзей +k = 1000 # кол-во итераций +p = list(range(n)) # исходная перестановка + +scores = [] + +t = 1 +for i in range(k): + # уменьшаем температуру + t *= 0.99 # этот параметр лучше перебрать руками + + # копируем перестановку и делаем небольшое изменение + q = p[:] + a, b = random.sample(range(n), 2) + q[a], q[b] = q[b], q[a] + + # считаем разницу fitness и решаем, нужно ли переходить в новое состояние + d = fitness(q) - fitness(p) + if d > 0 or random.random() < math.exp(d/t): + p = q[:] + scores += [fitness(q)] + +print('Best score:', max(scores)) +plt.plot(scores) +plt.show() +``` + + Best score: 95 + + + +![png](annealing_files/annealing_11_1.png) + + +Примерно эквивалентный код на C++: + + +```python +const int n = 100, k = 1000; + +int f(vector p) { + int s = 0; + for (int i = 0; i < n; i++) { + int d = 1; + for (int j = 0; j < i; j++) + if abs(i-j) == abs(p[i]-p[j]): + d = 0; + s += d; + } + return s; +} + +double rnd() { return double(rand()) / RAND_MAX; } + +int main() { + + for (int i = 0; i < n; i++) + v[i] = i; + + int ans = 1; + + double t = 1; + for (int i = 0; i < k; i++) { + t *= 0.99; + vector u = v; + swap(u[rand()%n], u[rand()%n]); + int val = f(u); + if (val > ans || rnd() < exp((val-ans)/t)) + v = u, ans = val; + if (ans == n) break; + } + + for (int x : v) + cout << x+1 << " "; + + return 0; +} +``` diff --git a/ru/bayans.md b/ru/bayans.md new file mode 100644 index 0000000..063656e --- /dev/null +++ b/ru/bayans.md @@ -0,0 +1,273 @@ + +# Красивые идейные задачи + +Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда. + +Задачи идут в порядке вспоминания, то есть в весьма рандомном. + +## Попугаи + +Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы. + +## Минимум и максимум + +Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый. + +## Перестановка + +Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов. + +## Выпуклая оболочка +Требуется отвечать на 2 типа запросов: + +1. Добавить точку в выпуклую оболочку. +2. Проверить, лежит ли точка внутри выпуклой оболочки. + +Обе операции онлайн за $O(\log n)$. + +## Геометрическая прогрессия + +Найдите способ посчитать $\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\log n)$. + +## Покемоны + +В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \to b$ и $b \to c$ не следует, что $a \to c$. + +У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш. + +## Сортировка +Можно ли отсортировать +* 5 камней за 8 взвешиваний? +* 5 камней за 7 взвешиваний? +* 20 камней за 60 взвешиваний? + +## Точки в круге + +Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат. + +## Замкнутые ломаные + +Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии? + +## Неубывающий массив + +Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим. + +## Чётный цикл + +Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины. + +## $k$-ая порядковая статистика + +Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$. + +## Доминирующий элемент + +Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. Время работы $O((n+m) \log n)$. + +## Разрушение дерева + +Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева. + +## $k$-ый элемент на отрезке + +Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \log n)$. + +## Различные числа на отрезке + +Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\sqrt{n})$. + +## Физкультура + +Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов: + +1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя) +2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно) + +При этом приказ может частично отменить действие предыдущих приказов. + +После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \log n)$. + +## Нулевая сумма + +Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$. + +## Мета-задача + +В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу. + +## Ниточка + +В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления. + +## Пельмени + +Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \log n)$. + +## От нуля до единицы + +Дан следующий код: + + +```python +x = 0 +while x < 1: + x += random() +``` + +Требуется посчитать матожидание `x`. + +(`random` в питоне возвращает случайное действительное число от 0 до 1.) + +## Площадь + +Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%. + +## Окружности + +Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна + +* соприкасаться с *внешней*, +* соприкасаться с предыдущей (($k-1$)-ой), +* иметь при этом максимальный радиус. + +Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. + +## Блеф + +Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может: + +* Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается. +* Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом. + +С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$. + +## Достижимость +Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000. + +## Нумизмат + +Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой. + +Придумайте любой полиномиальный алгоритм. + +## Принцесса + +В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве. + +Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством. + +У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми. + +Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу. + +Асимптотика $O(n^2)$. + +## Спираль + +Определим спираль $(2n+1) \times (2n+1)$ как матрицу следующего вида: + +$$ +\begin{matrix} +21 & 22 & 23 & 24 & 25 \\ +20 & 7 & 8 & 9 & 10 \\ +19 & 6 & 1 & 2 & 11 \\ +18 & 5 & 4 & 3 & 12 \\ +17 & 16 & 15 & 14 & 13 \\ +\end{matrix} +$$ + +Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$). + +$q \leq 100$, $n \leq 10^9$. + +## Польский лабиринт + +Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`. + +![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png) + +Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам. + +$n \leq 10^5$, изначальные координаты туристов до $10^9$. + +## Нимные подмножества + +Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \log n)$. + +## Баланс степеней + +Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода. + +## Два пути + +Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$. + +## Пьяница + +Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой? + +## Ксоровый рюкзак + +Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$. + +## Иван Сусанин + +Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш. + +- Камиль руководит армией днём и водит армию по *дорогам*. +- Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. + +Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск. + +Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу. +Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось. + +Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$. + +## Варенье + +В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм. + +Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья. + +Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной. + +$n, m \leq 10^5$ + +## Лабиринт + +Серёжа потерялся в лабиринте $n \times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт. + +Придумайте любой полиномиальный алгоритм. + +## Обезьяна + +Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт? + +## Ожидание минимума + +Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин. + +Придумайте любой точный полиномиальный алгоритм. + +## Шумный ксор + +Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \frac15$ быть единичным, то есть каждый бит $x \oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности: + + +```python +x = # ... + +def mask(p=0.2): + r = 0 + for i in range(32): + if random.random() < p: + r += 2**i + return r + +def query(y): + return bin(x ^ y ^ mask()).count('1') +``` + +Ваша задача — отгадать число, используя не более 10000 попыток. diff --git a/ru/bitset.md b/ru/bitset.md new file mode 100644 index 0000000..cb5dcd7 --- /dev/null +++ b/ru/bitset.md @@ -0,0 +1,130 @@ + +# Битовое сжатие + +* Из-за него в «асимптотиках» появляется `/64` +* На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов +* `bitset` есть в stl; говорят, самописный быстрее + +Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости. + +Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию. + +Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. + +Работать с ним нужно вот так: + + +```c++ +const int lim = 1000; +bitset b; // создать битсет размера lim (должно быть константой) +b.set(); // заполнить единицами +b.reset(); // заполнить нулями +b.flip(); // заменить единички на нули и наоборот +b.count(); // посчитать число единичек +cout << b; // вывести битовую строку +``` + +Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`. + +## Рюкзак + +Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака. + +Обычно его решают так: + + +```c++ +bool dp[lim] = {}; // так можно его заполнить нулями +dp[0] = 1; +for (int i = 0; i < n; i++) + for (int x = lim - a[i]; x >= 0; x--) + dp[x + a[i]] |= dp[x]; +``` + +…а с битсетом оно разгоняется так: + + +```c++ +bitset b; +b[0] = 1; +for (int i = 0; i < n; i++) + b |= b << a[i]; +``` + +## Цикл длины 3 + +Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\frac{n^3}{64})$ следующим образом: + + +```c++ +bitset g[maxn]; // матрица смежности +for (int a = 0; a < n; a++) { + for (int b = 0; b < n; b++) { + if (g[a][b] && (~g[a] & g[b]).any()) { + // цикл найден + } + } +} +``` + +Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд. + +## Перемножение матриц + +Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения: + + +```c++ +typedef bitset t; +typedef array matrix; + +matrix operator* (matrix a, matrix b) { + matrix c; + for(int i = 0; i < n; i++) + for(int j = 0; j < n; j++) + if(a[i][j]) + c[i] |= b[j]; + return c; +} +``` + +## Гаусс + +Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. + +Нас по сути просят решить следующую систему: + +$$ +\begin{cases} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ +\ldots \\ +a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 +\end{cases} +$$ + +Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. + +В таком случае можно значительно ускорить и упростить обычный метод Гаусса: + + +```c++ +t gauss (matrix a) { + for (int i = 0; i < n; i++) { + int nonzero = i; + for (int j = i+1; j < n; j++) + if (a[j][i]) + nonzero = j; + swap(a[nonzero], a[i]); + for (int j = 0; j < n; j++) + if (j != i && a[j][i]) + a[j] ^= a[i]; + } + t x; + for (int i = 0; i < n; i++) + x[i] = a[i][n] ^ a[i][i]; + return x; +} +``` + +Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. diff --git a/ru/centroid.md b/ru/centroid.md new file mode 100644 index 0000000..b439d33 --- /dev/null +++ b/ru/centroid.md @@ -0,0 +1,167 @@ + +# Центроидная декомпозиция + +Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути». + +Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще). + +![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700) + +**Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\frac{n}{2}$. + +Выясняется, что центр всегда существует — это следует из алгоритма его поиска: + + +```c++ +int s[maxn]; + +int sizes (int v) { + s[v] = 1; + for (int u : g[v]) + // для простоты считаем, что дерево корневое + s[v] += sizes(u); + return s[v]; +} + +// второй параметр -- размер дерева +int centroid (int v, int n) { + for (int u : g[v]) + if (s[u] > n/2) + return centroid(u, n); + return v; +} +``` + +**Утверждение.** `centroid` действительно находит цетроид. + +**Доказательство:** + +* `centroid` вернет вершину, у которой размеры всех детей не больше $\frac{n}{2}$ (это явно проверяется в `if`-е). +* Мы пришли в эту вершину, когда её размер был больше $\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\frac{n}{2}+1) = \frac{n}{2}-1$ вершин. +* Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен. + +Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид. + +**Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент». + +**Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением. + +Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты. + +Теперь поймём, зачем мы всё это делали. + +**Утверждение.** Каждая вершина входит в $O(\log n)$ компонент. + +**Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\lceil \log_2 n \rceil$ разделений. + +**Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \log n)$. + +**Утверждение.** Для любого пути $a \leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$. + +**Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$. + +Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний. + +## Подсчет путей с заданным свойством + +Рассмотрим конкретный пример: подсчёт путей заданной длины. + +Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид. + +Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз. + + +```c++ +int l = 179; // нужная нам длина +int ans = 0; + +// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена +bool used[maxn]; +int s[maxn]; // размеры поддеревьев + +void sizes (int v, int p) { + s[v] = 1; + for (int u : g[v]) + if (u != p && !used[u]) + sizes(u, v), s[v] += s[u]; +} + +int centroid (int v, int p, int n) { + for (int u : g[v]) + if (u != p && !used[u] && s[u] > n/2) + return centroid(u, v, n); + return v; +} + +// записывает в t[] глубины вершин +void dfs (int v, int p, int d, vector &t) { + t.push_back(d); + for (int u : g[v]) + if (u != p && !used[u]) + dfs(u, v, d + 1, t); +} + +void solve (int v) { + /* <единственный зависящий от конкретной задачи код> */ + size(v); + vector d(s[v], 0); + d[0] = 1; + for (int u : g[v]) { + if (!used[u]) { + vector t; + dfs(u, v, 1, t); + for (int x : t) + if (x <= l) + ans += d[l-x]; + for (int x : t) + d[x]++; + } + } + /* */ + + used[v] = 1; + for (int u : g[v]) + if (!used[u]) + solve(centroid(u, v, s[u]/2)); +} +``` + +**Асимптотика** $O(n \log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\log n)$ «уровней» центроидной декомпозиции. + +## Запросы на путях — offline + +Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline. + +А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем. + +Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. + +Таким образом, каждый запрос будет просмотрен $O(\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \log n + n \log n)$. + +## Запросы на путях — online + +Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы. + +Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \times \log n$, в котором для каждой вершины будем хранить $O(\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути. + +Тогда, при ответе на запрос, мы за $O(\log n)$ или даже $O(\log \log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида. + +## Асимптотика при более долгих пересчётах + +TODO: сослаться на мастер-теорему + +Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени? + +Возьмем худший случай — когда каждый раз компонента разбивается на две равные части. + + + +Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень: + +$$ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} +\leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n += n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} +< 2 n \log n += O(n \log n) $$ + +Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \log n)$ — как, впрочем, и во всех остальных «разделяйках». diff --git a/ru/convex-hulls.md b/ru/convex-hulls.md new file mode 100644 index 0000000..c754863 --- /dev/null +++ b/ru/convex-hulls.md @@ -0,0 +1,201 @@ + +# Задачи на многоугольники + +Очень часто геометрические задачи на контестах - это задачи на многоугольники. Некоторые из таких задач мы уже обсудили на предыдущих занятиях. Так мы уже обсудили задачу проверки многоугольника на выпуклость и научились находить площадь многоугольника. Напомним, что такое полярный угол. + +![alt text](https://upload.wikimedia.org/wikipedia/commons/1/13/Polar_graph_paper.svg) + + + +# Выпуклые оболочки + +**Выпуклое множество** - такое множество точек, что все точки отрезка, образуемого любыми двумя точками данного множества, также принадлежат данному множеству + + +**Выпуклая оболочка фигуры** - такое выпуклое множество точек, что все точки фигуры также лежат в нем. + + +**Минимальная выпуклая оболочка фигуры** - это минимальная по площади выпуклая оболочка. + +![alt text](http://informatics.letovo.ru/geom/Volgin_Asatrian/images/vup.png) + +![alt text](http://grafika.me/files/les_screens/convex_1.png) + +![alt text](https://2.bp.blogspot.com/-JngZWPM7wig/WlRz58cIMHI/AAAAAAAADGA/Pyhfngs_GKQMkb9B2_kqZJRQ5ZLEzrVMQCK4BGAYYCw/s1600/convexhullex.jpg) + +Дано множество точек, требуется построить его минимальную выпуклую оболочку : + +## Построение за $O(nh)$ + +### Алгоритм Джарвиса(метод заворачивания подарка) + +Давайте выберем какую-то точку, которая гарантированно попадет в минимальную выпуклую оболочкуо, например обычно берут нижнюю и если таких несколько, то самую левую из них. Теперь давайте по одной набирать точки, как бы заворачивая нашу выпуклую оболочку(отсюда и название). Как же нам найти следующую точку в выпуклую оболочку, давайте пройдемся по точкам, которые мы еще не взяли в МВО и среди них выберем с минимальным полярным углом. + +![alt text](http://neerc.ifmo.ru/wiki/images/b/b3/Graham1.png) + +Корректность алгоритма легко доказывается по индукции, так как на первом шагу мы выбрали точку, точно лежащую в МВО, а на i, взяли такую точку, что все остальные лежат в нужной нам стороне. + +Асимптотика : для каждой точки выпуклой оболочки мы из всех оставшихся точек будем искать оптимальную - что будет работать за h(размер выпуклой оболочки) * n + +### Важно помнить, что именно $O(hn)$, а не $O(n^2)$, так как существуют задачи на это + + + +``` +int base = 0; +for (int i = 1; i < n; i++) { + if (mas[i].y < mas[base].y) { + base = i; + } + else if (mas[i].y == mas[base].y && mas[i].x < mas[base].x) { + base = i; + } +} +convex_hull.push_back(base); +point first = mas[base]; +point cur = first; +point prev = point(first.x - 1, first.y); +do { + double minCosAngle = 1e9; // чем больше угол, тем меньше его косинус + double maxLen = 1e9; + int next = -1; + for (int i = 0; i < n; i++) { + double curCosAngle = CosAngle(prev, cur, mas[i]); + if (Less(curCosAngle,minCosAngle)) {//если меньше сразу меняем + next = i; + minCosAngle = curCosAngle; + maxLen = dist(cur, mas[i]); + } + else if (Equal(curCosAngle, minCosAngle)) {// смотрим по длине + double curLen = dist(cur,mas[i]); + if (More(curLen,maxLen)) { + next = i; + maxLen = curLen; + } + } + } + prev = cur; + cur = mas[next]; + convex_hull.push_back(next); +} +while (cur != first); +``` + +## Построение за $O(n \log n)$ + +### Алгоритм Грэхема + +Алгоритм Грэхема базируется на следующей идее: Давайте не искать следующую точку каждый раз, а сделаем так, чтобы у нас всегда была оптимальная точка и мы могли бы просто ее достать и проверить. + +В прошлом алгоритме мы искали точку, оптимальную по полярному углу, тогда давайте сейчас сразу отсортируем точки по полярному углу и сразу возьмем две первые точки в МВО. + +Теперь будем делать следующий алгоритм, пока все точки не будут просмотрены : + +1) Возьмем первую из отсортированных точек. + +2) Проверем последние три точки из взятых, если они образуют правый поворот, то удалим предпоследнюю точку + +Сделать это можно, например, стеком. Код есть ниже. + +![alt text](https://cdn.turkaramamotoru.com/ru/algoritm-grehema-4131.jpg) + +Асимптотика : Мы просмотрим одну точку и либо удалим ее, либо оставим, то есть сам поиск МВО работает за линейное время, но мы еще делаем сортировку, а $\rightarrow$ алгоритм работает за $O(n\log(n))$, при этом его корректность вытекает из предыдущего алгоритма. + +Красивая визуализация - https://visualgo.net/en/convexhull + +красивое видео - https://www.youtube.com/watch?v=BTgjXwhoMuI +. + + +``` +struct Point { + int x, y; +}; + +Point operator -(Point a, Point b) +{ + return {a.x - b.x, a.y - b.y}; +} + +int operator * (Point a, Point b) +{ + return a.x * b.x + b.y * a.y; +} + +int operator ^(Point a, Point b) +{ + return a.x * b.y - b.x * a.y; +} + +bool cw(Point a, Point b, Point c) +{ + return (a - b) * (c - b) > 0; +} + +bool ccw(Point a, Point b, Point c) +{ + return (a - b) * (c - b) < 0; +} + +int main() +{ + sort(all(p2), comp); + vector s; + s.push_back(p[min_ind]); + for (int i = 0; i < n - 1; i++) { + if (p2[i].x == s[s.size() - 1].x && p2[i].y == s[s.size() - 1].y) + continue; + while (s.size() > 1 && (vect(s[s.size() - 1], s[s.size() - 2]) ^ + vect(s[s.size() - 1], p2[i])) > 0) + s.pop_back(); + s.push_back(p2[i]); + } +} +``` + +### Алгоритм Эндрю + +Алгоритм Эндрю опирается на то, что вещественные числа не точны и предлагает поменять компаратор и строить не одну выпуклую оболочку, а две : + +Давайте выберем самую нижнюю и самую правую точку, затем отсортируем точки по самому простому из возможных компараторов, теперь будем строить две оболочки от самой правой точки и самой левой, в итоге мы получим верхнюю и нижнюю части выпуклой оболочки + + + +``` +bool comp(Point a, Point b) { + if(a.x == b.x) { + return a.y < b.y; + } + return a.x < b.x; +} + +int main() { + sort(all(p), comp); + vector up, down; + up.pb(p[0]); + down.pb(p[0]); + Point p1 = p[0], p2 = p.back(); + for(int i = 1; i < n; i++) { + if (i == n - 1 || cw(p1, p[i], p2)) { + while (up.size() >= 2 && !cw(up[up.size() - 2], up[up.size() - 1], p[i])) { + up.pop_back(); + } + up.pb(p[i]); + } + if (i == n - 1 || ccw(p1, p[i], p2)) { + while (down.size() >= 2 && !ccw(down[down.size() - 2], down[down.size() - 1], p[i])) { + down.pop_back(); + } + down.pb(p[i]); + } + } +} +``` + +### Алгоритм Чена + +Также существует алгоритм, объединяющий Джарвиса и Грэхема(Эндрю) и работающий за $O(n\log(h))$, но он разбираться не будет + +# Задачи + +1) Базовые задачи - достаточно простые, например найти длину забора, чтобы ограничить многоугольник и подобные, но есть достаточно интересные задачи, в которых выпуклая оболочка неочевидна, например следующая : Даны $n$ пунктов в городе и $n$ почтальонов, для каждого пункта известно расстояние от почты $c_{i}$. Требуется каждому пункту доставить почту, $i$-ый почтальон просит $a_{i}$ монет, чтобы проснуться и $b_{i}$, чтобы проехать один километр, требуется для каждого пункта сказать, кто доставит почту наиболее выгодно. (Подсказка : $a_{i} + b_{i} * c_{j}$ - это прямая и стоимость доставки от $i$ почтальона к $j$ пункту). diff --git a/ru/cross-entropy.md b/ru/cross-entropy.md new file mode 100644 index 0000000..5b144f7 --- /dev/null +++ b/ru/cross-entropy.md @@ -0,0 +1,22 @@ + +# Почему машинное обучение работает + +b + +Принцип максимального правдоподобия — + +## Функция правдоподобия + + + +$$ \hat{\theta} = \argmax_\theta P(X | \theta) $$ + +## Принцип максимального правдоподобия + + + +## Логистическая регрессия + +## Нормальное распределение и MSE + +## Почему сигмоида diff --git a/ru/cuda.md b/ru/cuda.md new file mode 100644 index 0000000..4b1ff20 --- /dev/null +++ b/ru/cuda.md @@ -0,0 +1,492 @@ + +# Supercomputing for the Masses + +This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in [Colab]() or [download]() and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: +* `apt-get install nvidia-cuda-dev nvidia-cuda-toolkit` +* `pip install pycuda` + +Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work. + +## Subtlties of the Moore's law + +Here is a graph that roughly represents what is happening in the CPU world: + + + +**Moore's law** is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too. + +You can see that around 2005 there became a shift in design . + +The cores are more or less independent. + +Modern GPUs appeared in early 2000s. They exploit the specific area they operate. + +There are physical limitations to the speed of a core. + +One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another. + +Some of them have + +The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome. + +## Why multiprocessing? + +Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound + +There are two types of + +## General-purpose GPU + +There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills. + +There are several. + +This is like with Windows and Linux. + +We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning. + +## Heterogineous computing + +CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs. + +## Differences from CPUs + +### Threads + +Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive. + +By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. + +In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput. + +### Memory + +The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device? + +You can easily dump 98% of performance of you think this way. + +## Installing PyCUDA + +CUDA is available for many languages. + +Nice documentation can be found here: https://documen.tician.de/pycuda/index.html + +If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU". + + +```python +# you may want to clear the output of this cell after installation +from IPython.display import clear_output + +# this might take a while +!pip install pycuda + +clear_output() +``` + + +```python +import numpy as np + +from pycuda.compiler import SourceModule +import pycuda.driver as drv +import pycuda.autoinit +``` + +## The basics + +Let's start with a simple example and then dive deeper. + +## Kernels + +Just like C or C++, except that you use some custom built-in functions and specifiers. + +CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: + +You need to think of your computer as a heterogenious machine: there is host data and device data. + +* You move input data to device memory. +* You run some computation on device. +* You retrieve back this data. + +In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. + +## The famous $A + B$ problem + +For testing and coordination with host, we will use **NumPy** package. If you don't have it, install it: `pip install numpy`. + +NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it. + + +```python +# lets generate our test data: two float arrays filled with something random +a = numpy.random.randn(100).astype('float32') +b = numpy.random.randn(100).astype('float32') +# the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it + +# we need to create space where kernel should write its answers to +dest = numpy.zeros_like(a) + +# this is the kernel itself +mod = SourceModule(""" + __global__ void add(float *dest, float *a, float *b) { + const int i = threadIdx.x; + dest[i] = a[i] + b[i]; + } +""") + +# you need to specify the source code, and PyCUDA will compile it +add_kernel = mod.get_function("add") + +add_kernel( + drv.Out(dest), # specifies that this memory should be accessible for writing + drv.In(a), # specifies this should be accessible for reading + drv.In(b), + block=(100,1,1) # we'll talk about it in a minute +) + +assert np.allclose(dest, a + b), 'WA' # checks that these are equal +print('OK') +``` + + + File "", line 19 + %%time + ^ + SyntaxError: invalid syntax + + + +### Memory management + +In CUDA C API, you need to allocate memory explicitly. So this is actually really nice. + +There is also `drv.InOut` function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too. + +Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough. + +GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time. + +You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications + +## Synchronization + +**Reduction** is any array-wise operation. + +Assume the following problem: + + +## Dynamic programming + +Consider the following recurrence: + + +```python +## Problem: dynamic programming +``` + +## Work vs. Latency + +We actually think about both work and step complexity now. + +Some tasks, especially in cryptography, cannot be parallelized. But some can. + +## Summing arrays in $O(\log n)$ time + +Assume we want to perform some associative (i. e. $A*(B*C) = (A*B)*C$) operation on an array of $n$ elements. Say, sum it up. + +Normally, we would do that with a simple loop: + +```c++ +float s = 0; +for (int i = 0; i < n; i++) { + s += a[i]; +} +``` + +Its computation graphs looks like this: + + + +This is optimal in terms of work complexity, but not in terms of step complexity: it's $O(n)$. We may want something that is a bit worse in terms of work complexity, but can be parallelized. + +Let's try this divide-and-conquer approach: + + + +Now it's still $O(n)$ work complexity (you actually need exactly the same number of additions), but this is $O(\log n)$ step complexity. + +When you unroll the recursion from top to bottom, you will see that to get each required value, + + + +## Reducing small arrays + + +```python +a = numpy.random.randn(2048).astype('float32') + +mod = SourceModule(""" + __global__ void sum(float *dest, float *a, float *b) { + const int i = threadIdx.x; + // for l from 0 to logn: + // __sync_threads() + // if the thread is active + // sum two elements into where they belong + // a[0] should containt the needed sum + } +""") + +sum_kernel = mod.get_function("sum") + +add_kernel( + drv.InOut(a), + block=(1024,1,1) +) + +assert np.allclose(dest, a + b), 'WA' # checks that these are equal +print('OK') +``` + +## Warps and thread blocks + +Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties. + + + +You can actually do the same stuff with 2d and 3d indexing — weird, right? + +## Atomics + +## Reducing big arrays + +## Reducing very big arrays + +Now, things get harder. It's time to tell how exactly GPU parallelism works. + + + + +```python + +``` + +## Dense Matrix multiplication + +Let's get to our first example where using GPUs actually makes sense: matrix multiplication. + +## Sorting + +Our last (and hardest task) is to implement sorting. + +You might notice that we advocated divide-and-conquer approaches most of the time. + +It's true. They work. But we can't get an algorithm that works already. + + +```python +# we'll use a deep learning library for benchmarking because I'm not familiar with anything else +import torch + +a = torch.randn(10**8) +b = a.cuda() +``` + + +```python +# this should run for ~15 secs +%time c = torch.sort(a) +%time c = torch.sort(b) +``` + + CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s + Wall time: 15.2 s + CPU times: user 274 ms, sys: 237 ms, total: 511 ms + Wall time: 511 ms + + +So, 30 times speedup. So, we now what we need to compete against. + + +```python +b.sort() +``` + + + + + (tensor([-5.4567, -5.3551, -5.3288, ..., 5.3529, 5.4484, 5.4486], + device='cuda:0'), + tensor([55083205, 8383169, 73705953, ..., 79814161, 50474932, 27805828], + device='cuda:0')) + + + +There are two types of sorting algorithms: data-driven. + +The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort. + + + +It has $O(\log n)$ stages, in total they have $1 + 2 + 3 + \ldots + \log n = O(\log^2 n$ blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has $O(n \log^ n)$ work complexity, but $O(\log^2 n)$ step complexity, which is pretty sweet. + +It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation: + + +```python +def bitonic_sort(a, up=False): + if len(a) <= 1: + return a + else: + l = bitonic_sort(x[:len(a) // 2], True) + r = bitonic_sort(x[len(a) // 2:], False) + return bitonic_merge(first + second, up) + +def bitonic_merge(a, up): + # assume input a is bitonic, and sorted list is returned + if len(a) == 1: + return a + else: + bitonic_compare(a, up) + l = bitonic_merge(a[:len(a) // 2], up) + r = bitonic_merge(a[len(a) // 2:], up) + return l + r + +def bitonic_compare(a, up): + dist = len(a) // 2 + for i in range(dist): + if (a[i] > a[i + dist]) == up: + a[i], a[i + dist] = a[i + dist], x[i] # this is how swap is done in Python +``` + + +```python +bitonic_sort([57, 179, 42, 17, 300, 111]) +``` + + + + + [300, 179, 111, 57, 42, 17] + + + + +```python +a = np.random.randn(10**8).astype('float32') +``` + + + --------------------------------------------------------------------------- + + NameError Traceback (most recent call last) + + in () + ----> 1 a = np.random.randn(10**8).astype('float32') + + + NameError: name 'np' is not defined + + +## Why CUDA + +Most of it still applicable. + +Again, GPU programming is very specific. + +SSE and tensor cores. + +## Kernels + +Just like C or C++, except that you use some custom built-in functions and specifiers. + +CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: + +You need to think of your computer as a heterogenious machine: there is host data and device data. + +* You move input data to device memory. +* You run some computation on device. +* You retrieve back this data. + +In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. + +What you need to understand about GPUs is that they are extremely specialised for their applications. + +Intrinsics for that. + +Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision. + +First, they introduced "multiply-accumulate" operation (e. g. `x += y * z`) per 1 GPU clock cycle. + +Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell). + +Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix). + +This is a lot of work per + +Well, you don't really need anything more precise than that for deep learning anyway. + + +It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices. + +Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores". + +So, see, this is not exactly fair comparison. + + + +*
You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull
* + +down to int4 (16-valued, you heard correct) + +You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch. + +Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply. + +## Reducing an array + +It seems to be simple: you just need to . + +What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen: + +1. Read $x$ into register +2. Read $s$ into register +3. Calculate $s + x$ +4. Write it back to wherever $s$ was initially + +Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value. + + + +Note: Atomics to do that + +for small data types they are implemented on the hardware level and much more faster than that. + +std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. + +## Memory types + +If all the various types of device memory were to race, here’s how the race would turn out: + +Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible). + +* 1st place: **Register memory** +
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread. +* 2nd place: **Shared Memory** +
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should +* 3rd place: **Constant Memory** +
+* 4th: Texture Memory +* Tie for last place: Local Memory and Global Memory + +What you need to care for now is register + +For now, you need to care about differe + +Accessing global memory takes hundreds. + +## Problem: dense matrix multiplication + +A lot of these are actually sparse. You can do stuff with social network graphs or web graphs. + +Cool. But let's disapploint us for a bit: diff --git a/ru/dp-intro.md b/ru/dp-intro.md new file mode 100644 index 0000000..6a848db --- /dev/null +++ b/ru/dp-intro.md @@ -0,0 +1,40 @@ + +# Динамическое программирование + +> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. + +Разберем некоторые классические идеи и задачи в динамическом программировании. + +## Рюкзак + +> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. + +## Наибольшая общая подпоследовательность + +> Есть две строки. Найти длину их наибольшей общей подпоследовательности. + + + +## Расстановка скобок + +> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. + +Можно завести динамику «можно ли ». + +## Наибольшая возрастающая подпоследовательность + +Есть также алгоритм за $O(n \log n)$. + +## Мемоизация + +Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. + +Первый предполагает + +## Число разбиений + +> + +## Замена параметра на ответ + +> diff --git a/ru/dp-optimizations.md b/ru/dp-optimizations.md new file mode 100644 index 0000000..f48dd53 --- /dev/null +++ b/ru/dp-optimizations.md @@ -0,0 +1,167 @@ + +## Оптимизации динамики + +* Разделяй и властвуй +* Оптимизация Кнута +* Convex Hull Trick +* Лямбда-оптимизация + +## Задача + +> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. + +Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). + +Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. + + +```c++ +// x[] — отсортированный массив координат точек, нумерация с нуля + +// квадрат длины отрезка от i-той до j-той точки +int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } + +// TODO: предподсчитать cost + +for (int i = 0; i <= m; i++) + f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо +// все остальные f предполагаем равными бесконечности + +for (int i = 1; i <= n; i++) + for (int j = 1; j <= m; j++) + for (int k = 0; k < i; k++) + f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); +``` + +Заметим, что циклы по `i` и `j` можно поменять местами. + +Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. + +## Разделяй-и-властвуй + +Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. + +*Утверждение.* $opt[i, j] \leq opt[i, j+1]$. + +Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. + +Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. + +Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. + + +```c++ +void solve (int l, int r, int _l, int _r, int k) { + if (l > r) return; // отрезок пустой — выходим + int t = (l + r) / 2; + int opt = _l; + for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы + int val = f[i+1][k-1] + cost(i, j); + if (val < f[t][k]) + f[t][k] = val, opt = i; + } + solve(l, t-1, _l, opt, k); + solve(t+1, r, opt, _r, k); +} +``` + +Вызываться она будет просто последовательно для каждого слоя: + + +```c++ +for (int k = 1; k <= m; k++) + solve(0, n-1, 0, n-1, k); +``` + +Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. + +Получается, что асимптотика улучшилась до $O(n m \log n)$. + +## Оптимизация Кнута + +Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? + +$$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ + +В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. + +Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: + +$$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ + +Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. + + +```c++ +for (int i = 1; i <= n; i++) { + for (int j = m; j >= 1; j--) { + for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { + int val = f[i+1][k-1] + cost(i, j); + if (val < f[t][k]) + f[t][k] = val, opt[i][j] = i; + } + } +} +``` + +Сравните с базовым решением — всего 3 новых строчки. + +## Convex Hull Trick + +Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. + +$$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ +f[k, j-1] + x_{i-1}^2 +- 2x_{i-1} x_k ++ x_k^2 +\}$$ + +Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется +$ +\underbrace{f[k, j-1] + x_k^2}_{a_k} +\underbrace{-2x_k}_{b_k} x_{i-1} +$. + +Это теперь можно переписать как $ +\min_k +(a_k, b_k) +\cdot +(1, X_{i-1}) +$ (имеется в виду скалярное произведение) + +Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. + +TODO: иллюстрация. + +TODO: мем про Скуби-Ду. + +### Ли Шао + +Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. + +## Лямбда-оптимизация + +Не «фольклорное» название — дискретный метод множителей Лагранжа. + +Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . + +Заметим следующее: + +Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. + +Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. + +## Суммируем + +TODO: сделать табличку + +* Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. +* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. +* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. +* Лагранж: $O(n \log n)$. Функция должна быть выпуклой. + +## Другие задачи + +Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. + +> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. diff --git a/ru/dp_intro.md b/ru/dp_intro.md new file mode 100644 index 0000000..8badbb7 --- /dev/null +++ b/ru/dp_intro.md @@ -0,0 +1,42 @@ + +# Динамическое программирование + +test123 + +> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. + +Разберем некоторые классические идеи и задачи в динамическом программировании. + +## Рюкзак + +> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. + +## Наибольшая общая подпоследовательность + +> Есть две строки. Найти длину их наибольшей общей подпоследовательности. + + + +## Расстановка скобок + +> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. + +Можно завести динамику «можно ли ». + +## Наибольшая возрастающая подпоследовательность + +Есть также алгоритм за $O(n \log n)$. + +## Мемоизация + +Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. + +Первый предполагает + +## Число разбиений + +> + +## Замена параметра на ответ + +> diff --git a/ru/dp_optimizations.md b/ru/dp_optimizations.md new file mode 100644 index 0000000..f48dd53 --- /dev/null +++ b/ru/dp_optimizations.md @@ -0,0 +1,167 @@ + +## Оптимизации динамики + +* Разделяй и властвуй +* Оптимизация Кнута +* Convex Hull Trick +* Лямбда-оптимизация + +## Задача + +> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. + +Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). + +Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. + + +```c++ +// x[] — отсортированный массив координат точек, нумерация с нуля + +// квадрат длины отрезка от i-той до j-той точки +int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } + +// TODO: предподсчитать cost + +for (int i = 0; i <= m; i++) + f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо +// все остальные f предполагаем равными бесконечности + +for (int i = 1; i <= n; i++) + for (int j = 1; j <= m; j++) + for (int k = 0; k < i; k++) + f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); +``` + +Заметим, что циклы по `i` и `j` можно поменять местами. + +Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. + +## Разделяй-и-властвуй + +Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. + +*Утверждение.* $opt[i, j] \leq opt[i, j+1]$. + +Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. + +Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. + +Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. + + +```c++ +void solve (int l, int r, int _l, int _r, int k) { + if (l > r) return; // отрезок пустой — выходим + int t = (l + r) / 2; + int opt = _l; + for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы + int val = f[i+1][k-1] + cost(i, j); + if (val < f[t][k]) + f[t][k] = val, opt = i; + } + solve(l, t-1, _l, opt, k); + solve(t+1, r, opt, _r, k); +} +``` + +Вызываться она будет просто последовательно для каждого слоя: + + +```c++ +for (int k = 1; k <= m; k++) + solve(0, n-1, 0, n-1, k); +``` + +Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. + +Получается, что асимптотика улучшилась до $O(n m \log n)$. + +## Оптимизация Кнута + +Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? + +$$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ + +В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. + +Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: + +$$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ + +Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. + + +```c++ +for (int i = 1; i <= n; i++) { + for (int j = m; j >= 1; j--) { + for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { + int val = f[i+1][k-1] + cost(i, j); + if (val < f[t][k]) + f[t][k] = val, opt[i][j] = i; + } + } +} +``` + +Сравните с базовым решением — всего 3 новых строчки. + +## Convex Hull Trick + +Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. + +$$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ +f[k, j-1] + x_{i-1}^2 +- 2x_{i-1} x_k ++ x_k^2 +\}$$ + +Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется +$ +\underbrace{f[k, j-1] + x_k^2}_{a_k} +\underbrace{-2x_k}_{b_k} x_{i-1} +$. + +Это теперь можно переписать как $ +\min_k +(a_k, b_k) +\cdot +(1, X_{i-1}) +$ (имеется в виду скалярное произведение) + +Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. + +TODO: иллюстрация. + +TODO: мем про Скуби-Ду. + +### Ли Шао + +Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. + +## Лямбда-оптимизация + +Не «фольклорное» название — дискретный метод множителей Лагранжа. + +Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . + +Заметим следующее: + +Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. + +Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. + +## Суммируем + +TODO: сделать табличку + +* Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. +* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. +* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. +* Лагранж: $O(n \log n)$. Функция должна быть выпуклой. + +## Другие задачи + +Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. + +> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. diff --git a/ru/fenwick.md b/ru/fenwick.md new file mode 100644 index 0000000..780f868 --- /dev/null +++ b/ru/fenwick.md @@ -0,0 +1,162 @@ + +
+ +НЯ! +
Эта статья полна любви и обожания. +
Возможно, стоит добавить ещё больше? +
+ +# Дерево Фенвика + +Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности. + +## Определение + +Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: + +$$ t_i = \sum_{k=F(i)}^i a_k $$ + +где $F$ это какая-то функцию, для которой выполнено $F(i) \leq i$. Конкретно её определим потом. + +Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле: + +$$ sum(k) = t_k + sum(F(k)-1) $$ + +Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка. + +$F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\log n)$. Популярны две функции: + +* $F_1(x) =$ `x & (x + 1)` +* $F_2(x) =$ `x - (x & -x) + 1` + +Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать. + +**Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\log n)$ таких отрезков. + +## Реализация + +Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$. + + +```c++ +int t[maxn]; + +// возвращает сумму на префиксе +int sum (int r) { + int res = 0; + for (; r > 0; r -= r & -r) + res += t[r]; + return res; +} + +int sum (int l, int r) { + return sum(r) - sum(l-1); +} + +// обновляет нужные t +void add (int k, int x) { + for (; k <= n; k += k & -k) + t[k] += x; +} +``` + +Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии. + +## Многомерный случай + +> $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку + +Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре. + +`sum` перепишется следующим образом: + + +```c++ +int sum (int r1, int r2) { + int res = 0; + for (int i = r1; i > 0; i -= i & -i) + for (int j = r2; j > 0; j -= j & -j) + ans += t[i][j]; + return res; +} +``` + +В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах. + +Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017. + +## Бинпоиск + +Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\log n)$. + + +```c++ +// возвращает индекс, на котором сумма уже больше +int lower_bound (int s) { + int k = 0; + for (int l = logn; l >= 0; l--) { + if (k + (1<= k > i - (i & -i)`. + +Будем перебирать префиксы TODO + +Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя. + +Можно представить дерево так: ячейка 2^k содержит все + +TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию. + +## Название + +Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree». + +Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев. + +Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных). + +В частном случае, когда длина массива равна $2^k$, то дерево будет только одно. diff --git a/ru/games.md b/ru/games.md new file mode 100644 index 0000000..b5f33db --- /dev/null +++ b/ru/games.md @@ -0,0 +1,197 @@ + +# Теория игр + +Начнём с самого баянного примера математической игры, который можно вспомнить: + +> На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков. + +Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так: + +$$ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) $$ + +то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния). + +**Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях. + +## Игры на графах + +В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек». + +Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные* + +Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*. + +Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*. + +## Ретроанализ + +Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности: + +* Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины. +* Вершина *выигрышная* — если из неё есть переход в проигрышную вершину. +* Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные). + +Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся. + +**Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин. + +Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние. + +Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось. + +Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные. + +**Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали. + +* При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). +* При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше. + +Асимптотика составит $O(n + m)$, как и у любого bfs-а. + + +```c++ +vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа +int cnt[maxn]; // счётчик исходящих рёбер + +enum StatusType { win, loss, unknown }; +StatusType status[maxn]; // выгрышность вершины; +// по умолчанию все кроме терминальных считаются unknown +// те, кто в итоге остаются unknown -- ничейные + +queue q = {/* нужно заранее добавить сюда все терминальные вершины*/}; + +while (!q.empty()) { + int v = q.front(); + q.pop(); + for (int u : t[v]) { + cnt[u]--; // удаляем это ребро + if (status[v] == unknown) { + // из u есть ребро в проигрышную -- значит она выигрышная + if (status[v] == loss) + status[u] = win; + // все ребра u ведут в выигрышные вершины -- значит она проигрышная + if (status[v] == win && cnt[u] == 0) { + status[u] = loss; + // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь + if (status[v] != unknown) + q.push(u); + } + } +} +``` + +Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации: + + +```c++ +StatusType dfs(int v) { + if (status[v] != unknown) + return status[v]; + status[v] = loss; + // изменим статус, когда найдём переход в проигрышную вершину + for (int u : g[v]) + if (dfs(u) == loss) + status[v] = win; + return status[v]; +} +``` + +TODO: можно ли здесь циклы учесть? + +## Минимаксные игры + +Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно. + +Типичный граф минимаксной игры выглядит следующим образом: + + + +В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников. + +Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей. + +## Ретроанализ для больших графов + +Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго. + +Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей). + +Ретроанализ нужно оптимизировать. + +### Ограничение перебора + +Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. + +Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит. + +Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли). + + +### Мемоизация позиций + +В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает . + +Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары + +### Альфа-бета отсечение + + + +Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым. + +На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым. + +Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6. + +Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем. + +Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации. + +### Monte-Carlo Tree Search + + + +## Игры с ненулевой суммой + + + +## Теория Шпрага-Гранди + +Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые. + +Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями. + +**Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \oplus a_2 \oplus \ldots \oplus a_n $ размеров кучек отлична от нуля. + +**Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы: + +* Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия. +* Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \oplus a_i \oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$. + +Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \oplus a_i$. + +Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории. + +**Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен. + +### Ним с увеличениями + +Пусть у нас + +### Эквивалентность игр ниму + +## Игры с неполной информацией + +Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков. + +Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет. + +Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло. + +### Дилемма заключенного + +### Камень-ножницы-бумага + +### Покер + +Рассмотрим такую у diff --git a/ru/geometry.md b/ru/geometry.md new file mode 100644 index 0000000..7cff658 --- /dev/null +++ b/ru/geometry.md @@ -0,0 +1,237 @@ + +# Геометрия и ООП + +Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали. + +![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg) + +Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь. + +**Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается: + +$$ a \cdot b = x_a x_b + y_a y_b $$ + +![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg) + +У него есть полезные свойства: + +* Скалярное произведение симметрично ($a \cdot b = b \cdot a$). +* Геометрически, это проекция вектора b на вектор a, помноженынй на длину а. +* Перпендикулярные вектора должны иметь нулевое скалярное произведение. +* Если угол острый, то оно положительное +* Если угол тупой, то отрицательное + +**Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах: + +$$ a \times b = x_a y_b - y_a x_b $$ + +![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg) + +Его свойства: + +* Скалярное произведение *анти*симметрично ($a \times b = - (b \times a)$). +* Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора. +* Коллинеарные вектора должны иметь нулевое векторное произведение. +* Если b «слева» от a, то оно положительное. +* Если b «справа» — то отрицательное. + +## Всякие проверки + +Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями. + +**Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником. + +$$ +\text{P лежит внутри ABC} \iff \begin{cases} +(B-A) \times (P-A) \geq 0 \\ +(C-B) \times (P-B) \geq 0 \\ +(A-C) \times (P-C) \geq 0 \\ +\end{cases} +$$ + +**Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения. + +$$ V = \frac{1}{2} (B-A) \times (C-A) $$ + +**Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными). + +![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg) + +Забудьте о формуле Герона и всегда считайте площади через векторное произведение. + +Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2. + +**Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\times(c-a) > 0$). + +**Пересекаются ли отрезки**. + +![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png) + +## Уравнение прямой + +Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством. + +У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \geq 0$ будет указывать в сторону самой полуплоскости. + +Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой: + +$$ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} $$ + +**Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений: + +$$ +\begin{cases} +A_1 x + B_1 y + C_1 = 0 \\ +A_2 x + B_2 y + C_2 = 0 +\end{cases} +\implies +\begin{cases} +-x = \frac{B_1 y + C_1}{A_1} \\ +-x = \frac{B_2 y + C_2}{A_2} +\end{cases} +\implies +\frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} +\implies +y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} +$$ + +Аналогично, $x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки). + +Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать. + +# Как это кодить в C++ + +Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. + +**Точка $\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно. + + +```c++ +struct r { + double x, y; + r () {} + r (int _x, int _y) { x = _x, y = _y; } +}; +``` + +Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$. + +## Операции над векторами + +Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину: + + +```c++ +double len (r a) { return sqrt(a.x*a.x + a.y*a.y); } +``` + +## Операторы + +В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д. + +Давайте для будущих нужд определим `+` и `-`: + + +```c++ +r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); } +r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); } +``` + +## Скалярное произведение + + +```c++ +int operator*(r a, r b){ return a.x*b.x + a.y*b.y; } +``` + +## Векторное произведение + +Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике. + + +```c++ +int operator^(r a, r b){ return a.x*b.y - b.x*a.y; } +``` + +### Ввод-вывод + +Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так: + + +```c++ +istream& operator>>(istream &in, r &p){ + in >> p.x >> p.y; + return in; +} + +ostream& operator<<(ostream &out, r &p){ + out << p.x << " " << p.y << endl; + return out; +} +``` + +## Алгебра VS Алгоритмы или зачем мы всё это делали + +Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен. + +### Векторное представление прямой ($Ax + By + C = 0 \rightarrow r = at + b$) + +Тут нужно просто выбрать две любые точки на прямой. + + +```c++ +// даны A, B, C (A^2 + B^2 != 0) +r a, b; +if (eq(A, 0)) // значит, это горизонтальная прямая + a = r(0, -C/B), b = r(1, -C/B); +else + a = r(-C/A, 0), b = (1, -(C+B)/A, 1) +``` + +### Пример: отражение от прямой + +Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать. + +$\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} $ + +Формула имеет смысл: длина на единичный вектор направления. + +Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить. + + +```c++ +// прямая r = at + b, точка c +r pr (r a, r b, r c) { + c -= b; // пусть c и a выходят из одной точки + return b + (a*b / len(a) / len(a)) * a; +} + +r reflect (r a, r b, r c) { + return c + 2*(pr(a, b, c)-c); +} +``` + +## Типичные баги + +### Точность + +> Первое правило действительных чисел — не использовать действительные числа + +Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`: + + +```c++ +const double eps = 1e-8; + +bool eq (double a, double b) { return abs(a-b) < eps } +``` + +Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре. + +### $0 \neq -0$ + +Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду. + +### Область определения обратных функций + +`acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции. diff --git a/ru/hashing.md b/ru/hashing.md new file mode 100644 index 0000000..0838d72 --- /dev/null +++ b/ru/hashing.md @@ -0,0 +1,224 @@ + +# Хэширование в строковых задачах + +Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка. + +«Хорошая» хэш-функция: +* Быстро считается — за линейное от размера объекта время; +* Имеет не очень большие значения — влезающие в 64 бита; +* «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\frac{1}{n}$. + +Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*. + +Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \cdot n \cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки. + + ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png) + +## Применения в реальной жизни + +* **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить. +* **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\frac{k}{n}$ = $O(1)$ при правильном выборе $n$. +* **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш. +* **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками. +* **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить. +* **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса. + +Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы. + +Сегодня же мы остановимся на строках. + +# Полиномиальное хэширование + +**Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами. + +Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`. + +Определим *прямой полиномиальный хэш* строки как значение следующего многочлена: + +$$ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p $$ + +Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой. + +Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$: + + +```c++ +const int k = 31, mod = 1e9+7; + +string s = "abacabadaba"; +long long h = 0, m = 1; +for (char c : s) { + int x = (int) (c - 'a' + 1); + h = (h + m * x) % mod; + m = (m * k) % mod; +} +``` + +Можем ещё определить *обратный полиномиальный хэш*: + +$$ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p $$ + +Его преимущество в том, что можно написать на одну строчку кода меньше: + + +```c++ +long long h = 0; +for (char c : s) { + int x = (int) (c - 'a' + 1); + h = (h * k + x) % mod; +} +``` + +Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$. + +## Зачем он нужен? + +Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций. + +Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$: + +$$ h(ab) = h(a) + k^{|a|} \cdot h(b) $$ + +Удалить префикс строки можно так: + +$$ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} $$ + +А суффикс — ещё проще: + +$$ h(a) = h(ab) - k^{|a|} \cdot h(b) $$ + +В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве: + + +```c++ +const int maxn = 1e5+5; + +int p[maxn]; +p[0] = 1; + +for (int i = 1; i < maxn; i++) + p[i] = (p[i-1] * k) % mod; +``` + +Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса: + + +```c++ +int h[maxn]; +h[0] = 0; // h[k] -- хэш префикса длины k + +// будем считать, что s это уже последовательность int-ов + +for (int i = 0; i < n; i++) + h[i+1] = (h[i] + p[i] * s[i]) % mod; +``` + +Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке: + +$$ h(s[l:r]) = \frac{h_r-h_l}{k^l} $$ + +Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим. + +Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить. + +$$ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$ + + +```c++ +int hash_substring (int l, int r) { + return (h[r+1] - h[l]) * p[n-l] % mod; +} +``` + +Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$. + +### Упражнение + +Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. + +## Примеры задач + +**Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`. + +**Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа. + +**Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним. + +**Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором. + +**Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно. + +### Изменения строки* + +Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына. + +Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне. + +Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова. + +# Вероятность ошибки и почему это всё вообще работает + +У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения. + +Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого? + +## Выбор констант + +Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \cdot n^2$. Обоснование — см. парадокс дней рождений. + +Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно. + +Можно также брать модуль $2^{64}$. У него есть несколько преимуществ: +* Он большой — второй модуль точно не понадобится. +* С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении. +* С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`. + +Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду. + +В выборе же $k$ ограничения не такие серьезные: +* Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. +* Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться. + +Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты. + +## Парадокс дней рождений + +> В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%. + +Более общее утверждение: в мультимножество нужно добавить $\Theta(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. + +**Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. +Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. + +$$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ + +Попытаемся оценить $f$: + +$$ +\begin{align} + \begin{aligned} + e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ + & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ + e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ + f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ + & = e^{-\frac{n(n-1)}{2d}} & \\ + & \simeq e^{-\frac{n^2}{2d}} & \\ + \end{aligned} +\end{align} +$$ + +Из последнего выражения более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется очень быстро. + +**Второе доказательство** (для любителей теорвера). Введем $\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\frac{1}{d}$. + +Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\frac{n (n-1)}{2} \cdot \frac{1}{d}$. + +Отсюда понятно, что если $d = \Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно. + +*Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1. + +### Бонус: «мета-задача» + +Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. + +«Решите» задачу. diff --git a/ru/hld.md b/ru/hld.md new file mode 100644 index 0000000..8354d38 --- /dev/null +++ b/ru/hld.md @@ -0,0 +1,120 @@ + +# Heavy-light декомпозиция + +HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid). + + + +*
TODO: найти менее уродливую иллюстрацию
* + +Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого. + +Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения. + +**Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер. + +**Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра. + +Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер. + +**Утверждение.** На любом вертикальном пути будет не более $O(\log n)$ блоков. + +**Доказательство** разбивается на две части: + +* Лёгких ребер на вертикальном пути будет не более $O(\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего. +* Непрерывных путей из тяжелых рёбер будет не более $O(\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\log n)$. + +**Следствие.** На любом пути будет не более $O(\log n)$ блоков. + +Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам. + +## Реализация + +Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера. + +А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь. + +Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре. + + +```c++ +vector g[maxn]; +int s[maxn], p[maxn], tin[maxn], tout[maxn]; +int head[maxn]; // «голова» тяжелого пути, которому принадлежит v +int t = 0; + +void sizes (int v = 0) { + s[v] = 1; + for (int &u : g[v]) { + sizes(u); + s[v] += s[u]; + if (s[u] > s[g[v][0]]) + // &u -- это ссылка, так что её легально использовать при swap-е + swap(u, g[v][0]); + } +} + +void hld (int v = 0) { + rin[t] = v; + tin[v] = t++; + for (int u : g[v]) { + // если это тяжелый ребенок -- его next нужно передать + // в противном случае он сам является головой нового пути + head[u] = (u == g[v][0] ? head[v] : u); + hld(u); + } + tout[v] = t; +} +``` + +## Как им решать задачи + +Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов: + +1. Узнать минимальное число на пути между $v_i$ и $u_i$. +2. Изменить число у $v_i$-той вершины на $x_i$. + +Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`. + + +```c++ +int val[maxn]; +segtree st(0, n); +``` + +При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков: + + +```c++ +void upd (int v, int x) { + st.upd(tin[v], x); +} +``` + +Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям. + + +```c++ +int ancestor (int a, int b) { + return tin[a] <= tin[b] && tin[b] <= tout[a]; +} + +void up (int &a, int &b, int &ans) { + while (!ancestor(head[a], b)) { + ans = min(ans, st.get_min(tin[head[a]], tin[a])); + a = p[head[a]]; + } +} + +int get_min (int a, int b) { + int ans = inf; + up(a, b, ans); + up(b, a, ans); + if (!ancestor(a, b)) + swap(a, b); + ans = min(ans, st.get_min(tin[a], tin[b])); + return ans; +} +``` + + diff --git a/ru/index.md b/ru/index.md new file mode 100644 index 0000000..e213e68 --- /dev/null +++ b/ru/index.md @@ -0,0 +1,51 @@ + +# Алгоритмика + +Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/). + +Более-менее готово: + +* [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное +* [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое +* [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки +* [Центроидная декомпозиция](http://sereja.me/a/centroid) +* [Heavy-light декомпозиция](http://sereja.me/a/hld) +* [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла +* [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву +* [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость +* [Ро-алгоритм Полларда](http://sereja.me/a/pollard) +* [Sparse table](http://sereja.me/a/sparse-table) +* [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона +* [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса +* [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера +* [Классные задачки](http://sereja.me/a/bayans): пока что без решений + +Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально. + +Не очень готово: + +* [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция +* [Суффиксный массив](http://sereja.me/a/suffix-array) +* [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++ +* [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick +* [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала +* [Обратное по модулю](http://sereja.me/a/reciprocal) +* [Метод отжига](http://sereja.me/a/annealing) +* [Стресс-тестирование](http://sereja.me/a/stress-test) +* [Бор](http://sereja.me/a/trie) +* [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько +* Выпуклые оболочки +* [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса +* CUDA: программирование на GPU (на английском) + +Вообще не готово: +* [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема +* [Ахо-Корасик](http://sereja.me/a/aho-corasick) +* [Теорвер](http://sereja.me/a/probability) +* Корневая декомпозиция +* Теория информации +* Функции потерь в машинном обучении +* Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском) +* Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском) + +Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности. diff --git a/ru/ioi.md b/ru/ioi.md new file mode 100644 index 0000000..91928d3 --- /dev/null +++ b/ru/ioi.md @@ -0,0 +1,70 @@ + +# IOI Upsolving Challenge + +https://contest.yandex.com/ioi/total + +Я решал их почти 2 года назад и уже не очень хорошо всё помню. + +# 09-2 + +12 мая + +### Медведь Миша + +Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за $O(n^2 \log n)$. + +### Регионы + +В ограничениях нам спалили важную идею: задачу можно просто решить в случае, если все регионы небольшие, а также в случае, если регионов немного. Обычно это означает, что решения можно объединить с помощью корневой эвристики. + +Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так: + +* Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк +* Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос + +Такое должно работать за $O(n \sqrt{n \log n})$, и TL такое позволяет. + +### Коммивояжёр + +Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать. + +Подумайте, какой вид будет иметь эта функция, если зафиксировать время. + +TODO + +# 05-1 + +31 мая + +### Сад + +Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно. + +### Средняя последовательность + +Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы: + +```c++ +int a = -inf, b = inf, _d = 0, d = 0, t; +for(int i = 0; i < n; i++){ + cin >> t; + _d = d; + d = 2*t - d; + if (i % 2 == 0) + b = min(b, (d-_d)/2); + else + a = max(a, (_d-d)/2); +} + +cout << max(b - a + 1, 0ll); +``` + +### Польские горки + +По сути от нас просят обрабатывать запросы «присвоить всем элементам с $l$ по $r$ значение $x$» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше. + +# 05-2 + +### Прямоугольник + +Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста. diff --git a/ru/karatsuba.md b/ru/karatsuba.md new file mode 100644 index 0000000..27ad057 --- /dev/null +++ b/ru/karatsuba.md @@ -0,0 +1,69 @@ + +# Быстрое умножение + +В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел). + +[Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал. + +Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба. + +Алгоритм Карацубы имеет довольно значимое место в истории науки. + +{\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}). +У Колмогорова была гипотеза, что нижняя оценка для {\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности +{\displaystyle M(n)=O(n^{\log _{2}3})} M(n)=O(n^{{\log _{2}3}}) +и тем самым опроверг «гипотезу {\displaystyle n^{2}} n^{2}». + +(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.) + +Можно представить себе какого-то грозного японца. + + + +## Мастер-теорема + +Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек». + +**Мастер-теорема.** Пусть имеется рекуррента: + +$$ T(n) = \begin{cases} +a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 +\\ \Theta(1), & n \leq n_0 +\end{cases} $$ + +Тогда: + +* **A.** Если $c > \log_b a$, то $T(n) = \Theta(n^c)$. +* **B.** Если $c = \log_b a$, то $T(n) = \Theta(n^c \log n)$. +* **C.** Если $c < \log_b a$, то $T(n) = \Theta(n^{\log_b a})$. + +--- + + + +--- + +**Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням: + +$$ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k $$ + +* **A.** Если $c > \log_b a$, то $\sum (\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \Theta(n^c)$. +* **B.** Если $c = \log_b a$, то +$$\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)$$ +* **C.** Если $c < \log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу, + +$$ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) $$ + +Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$ + +## Алгоритм Карацубы + +Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат. + +Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. + +## Развитие идеи + +То же самое можно применить матрицам. + +Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\epsilon})$ для произвольного $\epsilon$ ещё никто не доказал. diff --git a/ru/lca.md b/ru/lca.md new file mode 100644 index 0000000..9665c9b --- /dev/null +++ b/ru/lca.md @@ -0,0 +1,193 @@ + +# Корневые деревья + +Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*. + +Примеры корневых деревьев: +* наследование классов в языках программирования (если множественное наследование запрещено), +* дерево факторизации числа на простые (в общем случае не уникальное), +* иерархия в какой-нибудь компании, +* просто множество как-то вложенных объектов (ссылка на крысу-роботягу). + +Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах. + +![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png) + +## Напоминание: DFS + +Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. + + +```c++ +vector g[maxn]; +int p[maxn], tin[maxn], tout[maxn]; +int t = 0; + +void dfs (int v) { + tin[v] = t++; + for (int u : g[v]) + dfs(u); + tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет +} +``` + +У этих массивов много полезных свойств: +* Вершина $u$ является предком $v$ $\iff tin_v \in [tin_u, tout_u) $. Эту проверку можно делать за константу. +* Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой. +* В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер. +* Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$. +* Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации. + +## Запросы на поддеревьях + +Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО. + +> Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$. + +Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree). + +## Запросы на уровнях + +> Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$). + +Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам. + +Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку. + +## Наименьший общий предок + +Очень много задач нам поможет решить следующая вспомогательная задача. + +> Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких. + +По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные. + +![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png) + +Для лучшего понимания: медленно (за линейное время) это можно делать так: + + +```c++ +bool a (int u, int v) { + return tin[u] <= tin[v] && tin[v] <= tout[u]; +} + +int lca (int u, int v) { + while (!ancestor(u, v)) + u = p[u]; + return u; +} +``` + +## LCA: двоичные подъемы + +Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \times \lceil \log n \rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень. + +Такой препроцессинг можно выполнить за $O(n \log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$: + + +```c++ +int up[maxn][logn]; + +void dfs (int v) { + for (int l = 1; l < logn; l++) + up[v][l] = up[up[v][l-1]][l-1]; + tin[v] = t++; + for (int u : g[v]) { + up[u][0] = v; + dfs(u); + } + tout[v] = t++; +} +``` + +Пусть поступил очередной запрос — пара вершин $(u, v)$: +* Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. +* Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA. + +Подробнее про второй пункт. Пусть $L = \lceil \log n \rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$. + + +```c++ +int lca (int v, int u) { + if (a(v, u)) return v; + if (a(u, v)) return u; + for (int l = logn-1; l >= 0; l--) + if (!ancestor(up[v][l], u)) + v = up[v][l]; + return up[v][0]; +} +``` + +### Асимптотика + +Препроцессинг — $O(n \log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу. + +Ответ на запрос — $O(\log n)$, потому что по сути мы делаем один бинпоиск. + +## Запросы на путях + +Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа). + +Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути. + +Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA. + + +```c++ +int get_min (int v, int u) { + int ans = inf; + for (int l = logn-1; l >= 0; l--) + if (!ancestor(up[v][l], u)) + v = up[v][l], ans = min(ans, mn[v][l]); + for (int l = logn-1; l >= 0; l--) + if (!ancestor(up[u][l], v)) + u = up[u][l], ans = min(ans, mn[u][l]); + return min({ans, mn[v][0], mn[u][0]}) +} +``` + +Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная. + +## Сведение к RMQ + +Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить. + + + +Во втором массиве мы по сути выписали наш проход dfs-а. + +Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути. + +Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree). + +### Разреженная таблица + +На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива. + +Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table). + +## А наоборот можно?* + +*Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения. + +Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. +Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя. + +Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль. + +Возьмем константу $k = \lfloor \frac{\log n}{2} \rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\frac{2 n}{\log n})$, и построение будет работать за линейное время: + +$$O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)$$ + +Также посчитаем для каждой возможной маски размера $\frac{\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\sqrt n$ (ради этого мы и делили логарифм на два). + +ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу. + +Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали. + +Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear). + +Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики. + +Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.** diff --git a/ru/linalg.md b/ru/linalg.md new file mode 100644 index 0000000..5efa4ea --- /dev/null +++ b/ru/linalg.md @@ -0,0 +1,315 @@ + +# Ликбез по линейной алгебре + + + +**Определение**. Функция $f: \mathbb{R}^n \to \mathbb{R}^m$ называется *линейной*, если для неё выполнено + +1. $ f(x+y) = f(x) + f(y) $ +2. $ f(ax) = a f(x), \; a \in R $ + +Примеры: +* $ f(x) = 0 $ +* $ f(x) = x $ +* $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\mathbb{R}^3$ в $\mathbb{R}$) +* $ f(x) = (x, -x, 0) $ (из $\mathbb{R}$ в $\mathbb{R}^3$) + +Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: +* Сумма линейных функций — линейная функция. +* Сумма коммутативна: $f+g = g+f$). +* Сумма ассоциативна: $(f+g)+h = f+(g+h)$. +* Композиция $f(g(x)) = (f \circ g)(x)$ линейных функций — линейная функция. +* Композция ассоциативна: $(f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h$. +* Композиция в общем случае не коммутативна.
+Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен. + +Все свойства можно вывести лишь из этих двух пунктов в определении. + +## Что такое матрица? + +Можно показать, что любую линейную функцию $f: \mathbb{R}^n \to \mathbb{R}^m$ можно представить в таком виде: + +$$ +f(x) = +\begin{pmatrix} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ +\ldots \\ +a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ +\end{pmatrix} +$$ + +*Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$. + +$$ +A = +\begin{pmatrix} +a_{11} & a_{12} & \ldots & a_{1n} \\ +a_{21} & a_{22} & \ldots & a_{2n} \\ +\vdots & \vdots & \ddots & \vdots \\ +a_{m1} & a_{m2} & \ldots & a_{mn} \\ +\end{pmatrix} +$$ + +Каждой линейной функции из $\mathbb{R}^n$ в $\mathbb{R}^m$ соответствует какая-то матрица размера $n \times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте. + +Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так: + +$$ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} $$ + +Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$. + +Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \times k$, то вторая должна иметь размер $k \times m$, то есть «средние» размерности обязательно должны совпадать. + + + +Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом. + +К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы. + +Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы: + +* Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$. +* Сумма коммутативна: $A+B = B+A$) +* Сумма ассоциативна: $(A+B)+C = A+(B+C)$ +* Умножение ассоциативно: $(AB)C = A(BC) = ABC$. +* Умножение в общем случае не коммутативно. + +**Пример**: матрица поворота в 2d. +$$ +\begin{pmatrix} +\cos \alpha & -\sin \alpha \\ +\sin \alpha & \cos \alpha \\ +\end{pmatrix} +$$ + +**Пример**: матрица проецирования на $Ox$ в 3d. +$$ +\begin{pmatrix} +1 & 0 & 0 \\ +0 & 0 & 0 \\ +0 & 0 & 0 \\ +\end{pmatrix} +$$ + +**Пример**: матрица «свапни $x$ и $y$». +$$ +\begin{pmatrix} +0 & 1 \\ +1 & 0 \\ +\end{pmatrix} +$$ + +Напишем класс, который реализует матричное умножение. + + +```c++ +struct matrix { + int n, m; + int t[]; + matrix (int _n, int _m) { + n = _n, m = _m; + t = new int(n*m); + memset(t, 0, sizeof t); + } + int[] operator[] (int k) { + return t[k*m]; + } +} + +matrix operator* (matrix a, matrix b) { + matrix c(a.n, b.m); + for (int i = 0; i < a.n; i++) + for (int j = 0; j < b.m; j++) + for (int k = 0; k < a.m; k++) + c[i][j] += a[i][k] * b[i][k]; + return c; +} +``` + +## Динамика + +Некоторые динамики можно выразить в терминах матричного умножения. + +Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика? + +$$ +\begin{pmatrix} +f_{n+1} \\ +f_{n+2} \\ +\end{pmatrix} += +\begin{pmatrix} +0+f_{n+1} \\ +f_{n}+f_{n+1} \\ +\end{pmatrix} += +\begin{pmatrix} +0 & 1 \\ +1 & 1 \\ +\end{pmatrix} +\begin{pmatrix} +f_{n} \\ +f_{n+1} \\ +\end{pmatrix} +$$ + +Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$. + +Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам: + +$$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$ + +Это будет работать за $O(n^3 \log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны. + + +```c++ +matrix binpow (matrix a, int p) { + matrix b(n, n); + for (int i = 0; i < n; i++) + b[i][i] = 1; + while (p) { + if (p&1) b = b*a; + a = a*a; + p >>= 1; + } + return b; +} +``` + +Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$. + +$$ +\begin{vmatrix} +1 & 0 & 0 \\ +0 & 1 & 0 \\ +0 & 0 & 1 \\ +\end{vmatrix} +$$ + +В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы. + +В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}$ имеет такую матрицу перехода: + +\begin{pmatrix} +0 & 1 & 0 & \ldots & 0 \\ +0 & 0 & 1 & \ldots & 0 \\ +\vdots & \vdots & \vdots & \ddots & \vdots \\ +0 & 0 & 0 & \ldots & 1 \\ +a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ +\end{pmatrix} + +## Матрица смежности + +У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов. + +Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86)). + +## Обобщения + +Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел. + +Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два. + +## Определитель + +*Определителем* кадратной матрицы $A$ называется такое выражение: + +$$ \det A = |A| = \ldots $$ + +Где $\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например: + +$$ +\begin{vmatrix} +a & b \\ +c & d \\ +\end{vmatrix} += +ad - bc +$$ + +У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться. + +В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем. + +## Базис + +Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. + +Базисы есть не только в линейной алгебре. Например, $\{1, x, x^2\}$ является базисом всех квадратных трёхчленов. Или $\{\neg, \land, \lor\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом. + +## Метод Крамера и easy пересечение прямых + +Пусть нам надо пересечь две прямые. + +$$ +\begin{cases} +a_1 x + b_1 y + c_1 = 0 +a_2 x + b_2 y + c_2 = 0 +\end{cases} +$$ + +Это то же самое, что найти такие коэффициенты $x$ и $y$, что + +$$ x \vec{a} + y \vec{b} = -\vec{c} $$ + +Площадь параллелограмма, натянутого на $\vec{a}$ и $\vec{b}$, равна векторному произведению, или детерминанту. + +По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$. + +Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да? + +## *Собственные векторы + +Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление. + +$ Av = k v $, где $k \neq 0$. + +$ Av - kv = (A-kI)v = 0 $. Это означает + +Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике. + +## Системы уравнений и метод Гаусса + +Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. + +Нас по сути просят решить следующую систему: + +$$ +\begin{cases} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ +\ldots \\ +a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 +\end{cases} +$$ + +Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. + +Метод Крамера неоптимален — там $O(n^4)$ операций. + +В таком случае можно значительно ускорить и упростить обычный метод Гаусса: + + +```c++ +t gauss (matrix a) { + for (int i = 0; i < n; i++) { + int nonzero = i; + for (int j = i+1; j < n; j++) + if (a[j][i]) + nonzero = j; + swap(a[nonzero], a[i]); + for (int j = 0; j < n; j++) + if (j != i && a[j][i]) + a[j] ^= a[i]; + } + t x; + for (int i = 0; i < n; i++) + x[i] = a[i][n] ^ a[i][i]; + return x; +} +``` + +Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. + +Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81)). diff --git a/ru/matching.md b/ru/matching.md new file mode 100644 index 0000000..46027c8 --- /dev/null +++ b/ru/matching.md @@ -0,0 +1,139 @@ + +# Паросочетания + +Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом. + + + +**Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$). + +Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием. + +*Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны. + +Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$. + +**Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер. + +**Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию. + +**Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию. + +![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg) + +*Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \to 8 \to 4 \to 6 \to 3 \to 7$.* + +Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу. + +В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу. + +**Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже. + + +```c++ +const int maxn; + +vector g[maxn]; // будем хранить только рёбра из левой доли в правую +int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой) +bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом + +// dfs возвращает, можно ли найти путь из вершины v +// в какую-нибудь вершину правой доли +// если можно, то ещё и проводит чередование +bool dfs (int v) { + if (used[v]) + return false; + used[v] = true; + for (int u : g[v]) { + // если вершина свободна, то можно сразу с ней соединиться + // если она занята, то с нейможно соединиться только тогда, + // когда из её текущей пары можно найти какую-нибудь другую вершину + if (mt[u] == -1 || dfs(mt[u])) { + mt[u] = v; + return true; + } + } + return false; +} + + +// где-то в main: + +memset(mt, -1, sizeof(mt)); +for (int i = 0; i < n; i++) { + memset(used, 0, sizeof(mt)); + if (dfs(i)) + cnt++; +} +``` + +## Корректность + +Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить. + +**Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным. + +**Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу. + +Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие. + +## Скорость работы + +Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$. + +Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \approx 10^4$), потому что для него есть мощные неасимптотические оптимизации: + +* Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть). + +* Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу. + +* Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются. + +Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \sqrt m)$. + +## Покрытие путями DAG-а + +Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути. + +Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\overline{H}$ следующим образом: +* В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно. +* Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$. +* Теперь из графа $H$ сделаем граф $\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$. + +Если мы рассмотрим любой путь $v_1, v_2, \ldots, v_k$ в исходном графе $G$, то в графе $\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$. + +Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\overline{H}$ — это паросочетание в $H$ (напомним, это $\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$. + +Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них. + +Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин. + +## Лемма Холла + +*Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно. + +![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif) + +**Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли. + +**Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием. + +В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу. + +**База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. + +**Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием. + +Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна) + +Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$). + +Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно. + +## Для ноулайферов: матроиды + +С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid). + +Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*. + +**Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна. diff --git a/ru/matroid.md b/ru/matroid.md new file mode 100644 index 0000000..c3479af --- /dev/null +++ b/ru/matroid.md @@ -0,0 +1,102 @@ + +# Матроиды + +Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры). + +**Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства: + +* Пустое множество является независимым: $\varnothing \in I$ + +* Любое подмножество независимого множества тоже независимо: +$$A \subset B, B \in I \implies A \in I$$ + +* Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$: +$$A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I$$ + +Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \sum w(a_i)$. + +Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ: + + +```python +X.sort() +s = [] +for x in X: + if good(s + [x]): + s += [x] +``` + +Здесь под `good` имеется в виду $s \cup x \in I$. + +Корректность этого алгоритма для любого матроида доказывает следующая теорема: + +## Теорема Радо-Эдмондса + +Пусть $A \in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \cup x \in I,\;x \notin A,\;w(x)$ — минимальна. Тогда $A \cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. + +*Доказательство: ** + +Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. + +Из свойств матроида: $\exists y \in B \setminus A : A \cup y \in I$. + +Тогда верны два неравенства: + +$$ +\begin{cases} +w(A \cup y) = w(A) + w(y) \geq w(B) \implies w(A) \geq w(B) - w(y) \\ +w(B \setminus y) = w(B) - w(y) \geq w(A) \implies w(A) \leq w(B) - w(y) +\end{cases} +$$ + +Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно, +$w(A \cup y) = w(A) + w(y) = w(B)$. + +Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \cup x \in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. + +Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым. + +## Примеры + +Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее. + +### Минимальный остов +Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом: + +* Граф без ребер является лесом. +* Если удалить из леса ребра, он останется лесом. +* Пусть есть два леса $|A| \leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \cup \{x\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности. + +Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова. + +### Расписания +Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград. + +Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \geq i$ для всех $i$. + +Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: + +* Пустой набор заданий всегда можно сделать. +* Если у нас стало меньше заданий, то их сделать мы тоже успеем. +* Пусть есть два правильных набора $|A| \leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \cup x$ — тоже правильный набор. + +Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных. + +### Паросочетания +Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом: + +* Любое паросочетание покрывает пустое множество вершин. +* Исходное паросочетание покрывает также и любое подмножество исходных вершин. +* Пусть есть два множества вершин $|A| \leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$. + +Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна. + +### Линейно независимые вектора + +(TODO) (Школьники не обязаны знать линал.) + +Такие штуки будем называть базисами. + +* Ноль есть в любом базисе. +* Подмножество базиса — базис. +* ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше. diff --git a/ru/mincost-maxflow.md b/ru/mincost-maxflow.md new file mode 100644 index 0000000..61c2a55 --- /dev/null +++ b/ru/mincost-maxflow.md @@ -0,0 +1,148 @@ + +# Поток минимальной стоимости + +Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна: + +$$ \sum_{(u, v) \in E} f_{uv} \to \max $$ +$$ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min $$ + +Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет. + +Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$). + +## Критерий оптимальности + +Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот). + +**Доказательство:** + +$\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению. + +$\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости. + +### Отмена циклов + +Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу. + +Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли). + +## Дополняющие пути + +Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости. + +**Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса. + +Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$. + +Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз. + +Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер. + +## Потенциалы Джонсона + +Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами: + +$$ w_{uv}' = w_{uv} + d_u - d_v $$ + +**Утверждение 1**. Веса всех рёбер графа неотрицательные. + +**Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$? + +Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой: +$$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$ + +**Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими. + +**Доказательство**. Распишем новую стоимость пути из $a$ в $z$. + +$$ +\begin{align} +w_{ab}' + \ldots + w_{yz}' +&= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) +\\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z +\end{align} +$$ + +Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу. + +Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути. + +Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа. + +**Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими). + +**Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2). + +## Итоговый алгоритм + +* Модифицируем сеть, добавивив обратные рёбра. +* Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю. +* Пока максимальный поток не найден: +* * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$. +* * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \leadsto t$, обновить остаточную сеть. + +### Асимптотика + +Алгоритм работает за $O(U m \log n)$ или $O(U n^2)$ в случае плотных графов. + +В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \leq n$ или $U \leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма. + +### Реализация + +Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию). + +* `cost`, `cap` — параметры сети +* `pot` — потенциалы +* `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока) +* `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния + + +```c++ +const int maxn = 305, inf = 1e9; + +int n; +int cost[maxn][maxn], cap[maxn][maxn]; +int d[maxn], pot[maxn], par[maxn]; + +bool dijkstra (int s, int t) { + used[maxn] = {0}; + + fill(d, d+n, inf); + d[s] = 0; + + while (1) { + int v = -1; + for (int u = 0; u < n; u++) + if (!used[u] && (v == -1 && d[u] < d[v])) + v = u; + if (v == -1 || d[v] == inf) + break; + used[v] = 1; + for (int u = 0; u < n; u++) { + int w = cost[v][u] + pot[v] - pot[u]; + if (cap[v][u] && d[u] > d[v] + w) { + d[u] = d[v] + w; + par[u] = v; + } + } + } + + return d[t] < inf; +} + +int mincost_maxflow (int s, int t) { + int ans = 0; + while (dijkstra(s, t)) { + memcpy(pot, d, sizeof(d)); + int delta = inf; + for (int v = t; v != s; v = par[v]) + delta = min(delta, cap[par[v]][v]); + for (int v = t; v != s; v = par[v]) { + cap[par[v]][v] -= delta; + cap[v][par[v]] += delta; + ans += cost[par[v]][v]*delta; + } + } + return ans; +} +``` diff --git a/ru/mst.md b/ru/mst.md new file mode 100644 index 0000000..da0c712 --- /dev/null +++ b/ru/mst.md @@ -0,0 +1,209 @@ + +# Минимальные остовы + +> Авиакомпания содержит $m$ рейсов между $n$ городами, $i$-ый из них обходится в $w_i$ рублей, причём из любого города можно добраться до любого другого. В стране наступил кризис, и нужно отказаться от как можно большего числа из них таким образом, что содержание оставшиъся рейсов будет наиболее дешевым. + +Иными словами, нужно найти дерево минимального веса, которое является подграфом данного неориентированного графа. Такие деревья называют *остовами* (*каркас*, *скелет*; ударение на первый слог, но так мало кто произносит). По-английски — *minimum spanning tree* (дословно, минимальное покрывающее дерево). + +Почему дерево? Потому в противном случае там был бы цикл из которго можно удалить какое-то ребро и получить ответ пооптимальней. А если это больше, чем одно дерево, то какие-то две вершины остаются несвязаны. + +Вообще, следующие утверждения про деревья являются эквивалентными: +* Граф — дерево. +* В графе из $n$ вершин $n-1$ рёбер и нет циклов. +* Из любой вершины можно дойти в любоую другую единственным образом. + +## Лемма о безопасном ребре + +Назовем подграф $T$ графа $G$ безопасным, если они является подграфом какого-то минимального остова. + +Назовем ребро безопасным, если при добавлении его в подграф $T$ получившийся граф $T'$ тоже является безопасным, то есть подграфом какого-то минимального остова. + +Все алгоритмы для поиска минимального остова опираются на следующее утверждение: + +> Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным. + +**Доказательство**: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже. + +![safe](https://neerc.ifmo.ru/wiki/images/thumb/8/81/%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png/300px-%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png) + +Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов. + +# Алгоритм Прима + +Минимальный остов строится постепенно, добавлением в него рёбер по одному. + +* Изначально остов — одна произвольная вершина. +* Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден. + +Очень похоже на алгоритм Дейкстры, только мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё. + +Совсем наивная реализация за $O(nm)$ — каждый раз перебираем все рёбра: + + +```python +const int maxn = 1e5, inf = 1e9; +vector from, to, weight; +bool used[maxn] + +// считать все рёбра в массивы + +used[0] = 1; +for (int i = 0; i < n-1; i++) { + int opt_w = inf, opt_from, opt_to; + for (int j = 0; j < m; j++) + if (opt_w > weight[j] && used[from[j]] && !used[to[j]]) + opt_w = weight[j], opt_from = from[j], opt_to = to[j] + used[opt_to] = 1; + cout << opt_from << " " << opt_to << endl; +} +``` + +Реализация за $O(n^2)$: + + +```python +const int maxn = 1e5, inf = 1e9; +bool used[maxn]; +vector< pair > g[maxn]; +int min_edge[maxn] = {inf}, best_edge[maxn]; +min_edge[0] = 0; + +// ... + +for (int i = 0; i < n; i++) { + int v = -1; + for (int u = 0; u < n; j++) + if (!used[u] && (v == -1 || min_edge[u] < min_edge[v])) + v = u; + + used[v] = 1; + if (v != 0) + cout << v << " " << best_edge[v] << endl; + + for (auto e : g[v]) { + int u = e.first, w = e.second; + if (w < min_edge[u]) { + min_edge[u] = w; + best_edge[u] = v; + } + } +} +``` + +Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за $O(m \log n)$: + + +```python +set< pair > q; +int d[maxn]; + +while (q.size()) { + v = q.begin()->second; + q.erase(q.begin()); + + for (auto e : g[v]) { + int u = e.first, w = e.second; + if (w < d[u]) { + q.erase({d[u], u}); + d[u] = w; + q.insert({d[u], u}); + } + } +} +``` + +Про алгоритм за $O(n^2)$ забывать не стоит — он работает лучше в случае плотных графов. + +# Алгоритм Крускала + +Будем добавлять рёбра в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить. + +Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Однако для этой проверки нам нужна будет целая отдельная структура. + +# Система непересекающихся множеств + +Эта структура данных предоставляет следующие возможности. Изначально имеется несколько элементов, каждый из которых находится в отдельном (своём собственном) множестве. Структура поддерживает две операции: +* объединить два каких-либо множества +* запросить, в каком множестве сейчас находится указанный элемент + +Обе операции будут выполняться в среднем почти за $O(1)$ (но не совсем — этот сложный вопрос будет разъяснен позже). + +Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив `_p`, в котором для каждого элемента мы храним номер его предка в дерева. Для корней деревьев будем считать, что их предок — они сами. + +Наивная реализация, которую мы потом ускорим: + + +```python +int _p[maxn]; + +int p (int v) { + if (_p[v] == v) + return v; + else + return p(_p[v]); +} + +void unite (int a, int b) { + a = p(a), b = p(b); + _p[a] = b; +} + +for (int i = 0; i < n; i++) + _p[i] = i; +``` + +**Эвристика сжатия пути**. Оптимизируем работу функции `p`. Давайте перед тем, как вернуть ответ, запишем его в `_p` от текущей вершины, то есть переподвесим его за самую высокую. + +Насколько лучше это сделает асимптотику? Выясняется, что $O(n \log n)$. + +Тут должен быть мем из опросов. + +**Ранговая эвристика**. Эта штука напрямую пытается минимизировать высоту дерева. Давайте делать переподвешивание за то, которое менее глубоко. Ну понятно, что тогда любое дерево будет не более логарифма. + +**Весовая эвристика**. Давайте каждый раз подвешивать за более крупное. Работать будет быстро, так как . + +Автор предпочитает именно её, потому что часто эти размеры компонент требуются сами по себе. + +Оказывается, что сжатия + ранговая или сжатия + весовая работает быстро. + +Асимптотика объединения обеих эвристик (сжатия путей и одной из ранговых) — O(a(n)), где a(n) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4). Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует. + + +```python +int _p[maxn], s[maxn]; + +int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); } + +void unite (int a, int b) { + a = p(a), b = p(b); + if (s[a] > s[b]) swap(a, b); + s[b] += s[a]; + _p[a] = b; +} + +for (int i = 0; i < n; i++) + _p[i] = i; +``` + +## Полезные свойства и классические задачи + +* Если веса всех рёбер различны, то остов будет уникален. +* Минимальный остов является также и остовом с минимальным произведением весов рёбер (замените веса всех рёбер на их логарифмы) +* Минимальный остов является также и остовом с минимальным весом самого тяжелого ребра. +* Если вы решаете задачу, где ребра не добавляются, а удаляются, то можно попробовать решать задачу «с конца» и применить алгоритм Крускала. + +## Персистентная СНМ* + +Как вы уже заметили, автор очень любит персистентные структуры данных. + +СНМ — тоже структура, и её тоже можно сделать персистентной. В СНМ мы изменяем массивы, а массивы можно сделать персистентными через персистентное ДО (только так, проще не получается — многие пытались). + +Здесь есть нюанс — амортизированные структуры не очень хорошо дружат с персистентностью. Поэтому нам придется отказаться от эвристики сжатия путей, и поэтому асимптотика составит $O(n \log^2 n)$ времени и памяти — один логарифм от СНМа, другой от персистентного ДО. + +## Динамическая связность* + +*Dynamic Connectivity Problem:* + +> Даны $n$ запросов добавления ребра (`+`), удаления ребра (`-` и какого-то запроса про граф (`?`), например, о связности двух вершин. + +О решении этой задачи в online и в offline можете почитать в [этом посте](https://codeforces.com/blog/entry/15296). diff --git a/ru/pollard.md b/ru/pollard.md new file mode 100644 index 0000000..dd2c5a5 --- /dev/null +++ b/ru/pollard.md @@ -0,0 +1,142 @@ + +# Факторизация за $O(\sqrt[4]{n})$ + +Наука умеет раскладывать целые числа на множители за $O(n^\frac{1}{4})$. Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера. + +## Парадокс дней рождений + +Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. +Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. + +$$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ + +Попытаемся оценить $f$: + +$$ +\begin{align} + \begin{aligned} + e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ + & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ + e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ + f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ + & = e^{-\frac{n(n-1)}{2d}} & \\ + & \simeq e^{-\frac{n^2}{2d}} & \\ + \end{aligned} +\end{align} +$$ + +Из формулы более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее: + +**Утверждение**. В мультимножество нужно добавить $O(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. + +Если вы не доверяете математике, посмотрите на графики: + + +```python +import matplotlib.pyplot as plt +%matplotlib inline + +import seaborn as sns +sns.set() +``` + + +```python +def f(n, d): + p = 1 + for x in range(1, n): + p *= 1-x/d + return p + +def g(n, d): + return 2.71**(-0.5 * n**2 / d) +``` + + +```python +exact = [f(n, 365) for n in range(40)] +approx = [g(n, 365) for n in range(40)] +plt.plot(exact) +plt.plot(approx) +plt.show() +``` + + +![png](pollard_files/pollard_7_0.png) + + +На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать $n \approx \sqrt{d}$, построим такую же для больших чисел и перейдём в логарифмический масштаб. + + +```python +approx = [g(n, 10**10) for n in range(10**6)] +plt.xscale('log') +plt.plot(approx) +plt.show() +``` + + +![png](pollard_files/pollard_9_0.png) + + +## $\rho$-алгоритм Полларда + +Итак, мы хотим факторизовать число $n$. Предположим, что $n = p q$ и $p \approx q$. Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей. + +Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например $f(x) = (x+1)^2 \mod n$. + +Как выглядит граф, в котором из вершины есть единственное ребро $x \to f(x)$? Такой граф называется *функциональным*. Если в нём нарисовать «траекторию» произвольного элемента, — какой-то путь, превращающийся в цикл, — то получится что-то похожее на букву $\rho$ (ро). Алгоритм из-за этого так и назван. + +Рассмотрим траекторию какого-нибудь элемента $x_0$: {$x_0$, $f(x_0)$, $f(f(x_0))$, $\ldots$}. Теперь сделаем из неё новую последовательность, взяв каждый элемент по модулю $p$ — наименьшего из простых делителей $n$. + +**Утверждение**. Ожидаемая длина цикла в этой последовательности $O(\sqrt[4]{n})$. + +*Доказательство:* так как $p$ — меньший делитель, то $p \leq \sqrt{n}$. Теперь просто подставим в предыдущее утверждение: в множество нужно добавить $O(\sqrt{p}) = O(\sqrt[4]{n})$ элементов, чтобы какие-то два совпали, а значит последовательность зациклилась. + +Если мы найдём цикл в такой последовательности (то есть такие $i$ и $j$, что $f^i(x_0) \equiv f^j(x_0)$), то мы можем восстановить какой-то делитель $n$, а именно $\gcd(|f^i(x_0) - f^j(x_0)|, n)$ — это число меньше $n$ и делится на $p$. + +Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя $i$ и $j$ ($i = 2j$) и проверять, что $f^i(x_0) \equiv f^j(x_0) \pmod p$, что эквивалентно проверке $\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1$. + + +```python +from math import gcd +from random import randint +``` + + +```python +def find_divisor(n, seed = 1, f = lambda x: (x+1)**2): + x = y = seed + d = 1 + while d == 1: + x = f(f(x)) % n + y = f(y) % n + d = gcd(abs(x-y), n) + return d +``` + + +```python +n = 3*5*7 # = 105 +print([find_divisor(n, seed=i) for i in range(n)]) +``` + + [3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3] + + + +```python +n = (10**9+7)*(10**9+9) +print([find_divisor(n, seed=randint(1, n)) for _ in range(20)]) +``` + + [1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009] + + +Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за $O(\sqrt{n})$ (нужно добавить отсечение по времени). + +## Разное + +Формально, мы показали, что алгоритм работает за $O(\sqrt[4]{n} \log n)$ за счёт поиска $\gcd$, но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет. + +Пародокс дней рождения также можно использовать при подсчёте вероятности коллизии при хэшировании. Хороший модуль должен быть не менее квадрата количества хэшей, а чего-то вроде $5 n^2$ уже почти всегда достаточно. diff --git a/ru/probability.md b/ru/probability.md new file mode 100644 index 0000000..dc5f126 --- /dev/null +++ b/ru/probability.md @@ -0,0 +1,86 @@ + +# Ликбез по теорверу +* Вероятностные распределения +* Матожидание и дисперсия +* Парадокс дней рождения +* Нормальное распределение +* Правдоподобие и оценка качества предсказания +* Энтропия и оптимальное кодирование + +Эта статья представляет собой выжимку самых интересных фактов и «больших идей» теорвера, которые обычно рассказывают на курсах статистики, машинного обучения и теории информации. + +(Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.) + + +```python +import numpy as np + +import matplotlib as plt +%matplotlib inline + +import seaborn as sns +sns.set() +``` + +## Матожидание + + + +## Дисперсия + +Какие два числа лучше всего описывают распределение? + +## Нормальное распределение + +Центральная предельная теорема названа так пафосно вполне обоснованно. + +Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию. + +$$ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} $$ + +Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса. + +Трудно даже доказать, что это распределение, т. е. что $\int_{-\inf}^\inf f(x) dx = 1$. + +## Применения + +Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный. + +Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как + +## Линейные рекурренты + +Чтобы решать следующие задачи, нам нужно будет использовать следующий факт: + +... + +Доказательство мы не приведем. + +В частности, таким образом получается формула для чисел Фибоначчи. + +$$ f_n = \ldots $$ + +Кто бы мог подумать, что все эти иррациональности и степени сократятся и вообще дадут целое число?.. + +## Классика + +Парадокс дней рождения. + +Это на самом деле очень часто используемый результат. Так можно считать вероятность коллизии хэшей, а также он используется во многих теоретико-числовых алгоритмах, в которых используется предположения (весьма справедливые) о распределении простых чисел. + +Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт? + +TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. +Казино. Мы приходим в казино с 1000\$ и следующим образом проводим там время: ставим по 1\$, пока не обанкротимся или не выиграем 1100\$. Какая вероятность того, что мы уйдём с деньгами? + +## Принцип максимального правдоподобия + +## Энтропия + +Энтропией называется минимальное число бит, которым теоретически возможно сжать сообщение. Эта величина важна, потому что на практике если её можно посчитать, то сжатие с соответствующей кратностью реально достижимо. + +Шумный канал. + +Пусть у вас есть 1тб данных и два китайских терабайтника, на каждый из которых можно записать столько данных, но каждый бит имеет вероятность 10% записаться на противоположный. Требуется сохранить данные с первого раза без потерь. Совсем без потерь. + +Причём это делается почти впритык. diff --git a/ru/reciprocal.md b/ru/reciprocal.md new file mode 100644 index 0000000..235d841 --- /dev/null +++ b/ru/reciprocal.md @@ -0,0 +1,128 @@ + +# Обратный элемент по модулю + +Часто в задачах требуется посчитать что-то по простому модулю (чаще всего $10^9 + 7$). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче. + +Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например: + + +```c++ +c = (a + b) % mod; +c = (mod + a - b) % mod; +c = a * b % mod; +``` + +Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: $\frac{8}{2} = 4$, но $\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4$. + +Нужно найти некоторый элемент, который будет себя вести как $\frac{1}{a} = a^{-1}$, и вместо «деления» домножать на него. Назовем такой элемент обратным. + +## Способ 1: бинарное возведение в степень + +Если модуль $p$ простой, то решением будет $a^{-1} \equiv a^{p-2}$. Это следует из малой теоремы Ферма: + +**Теорема.** $a^p \equiv a \pmod p$ для всех $a$, не делящихся на $p$. + +**Доказательство**. (для понимания несущественно, можно пропустить) + +$$ +\begin{align} +a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p +\\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} +\\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} +\\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} +\\ &= a +\end{align} +$$ + +Здесь $P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}$ это мультиномиальный коеффициент — количество раз, которое элемент $a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}$ появится при раскрытии скобки $(a_1 + a_2 + \ldots + a_n)^k$. + +Теперь два раза «поделим» наш результат на $a$. + +$$ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} $$ + +Получается, что $a^{p-2}$ ведет себя как $a^{-1}$, что нам по сути и нужно. +Посчитать $a^{p-2}$ можно за $O(\log p)$ бинарным возведением в степень. + +Приведем код, который позволяет считает $C_n^k$. + + +```c++ +int t[maxn]; // факториалы, можно предподситать простым циклом + +// бинарное возведение в степень +int bp (int a, int n) { + int res = 1; + while (n) { + if (n & 1) res = res * a % mod; + a = a * a % mod; + n >>= 1; + } + return res; +} + +// находит обратный элемент как a^(p-2) +int inv (int x) { + return bp(x, mod-2); +} + +int c (int n, int k) { + return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod; +} +``` + +## Способ 2: диофантово уравнение + +Диофантовыми уравнениями называют такие штуки: + +$$ ax + by = 1 $$ + +Требуется решить их в целых числах, то есть $a$ и $b$ известны, и нужно найти такие целые (возможно, отрицательные) $x$ и $y$, чтобы равенство выполнялось. Решают такие вещи расширенным алгоритмом Евклида. TODO: описать, как он работает. + +Подставим в качестве $a$ и $b$ соответственно $a$ и $m$ + +$$ ax + my = 1 $$ + +Одним из решений уравнения и будет $a^{-1}$, потому что если взять уравнение по модулю $m$, то получим + +$$ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m $$ + + + +Преимущества этого метода над возведением в степень: +* Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. +* Алгоритм проще выполнять руками. + +Сам автор почти всегда использует возведение в степень. + +## Почему $10^9+7$? + +1. Это выражение довольно легко вбивать (`1e9+7`). +2. Простое число. +3. Достаточно большое. +4. `int` не переполняется при сложении. +5. `long long` не переполняется при умножении. + +Кстати, $10^9 + 9$ обладает теми же свойствами. Иногда используют и его. + +## Предподсчёт обратных факториалов за линейное время + +Пусть нам нужно зачем-то посчитать все те же $C_n^k$, но для больших $n$ и $k$, поэтому асимптотика $O(n \log m)$ нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам. + +Если у нас уже написан `inv`, то нам не жалко потратить $O(\log m)$ операций, посчитав $m!^{-1}$. + +После этого мы будем считать $(m-1)!^{-1}$ как $m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}$. + + +```c++ +int f[maxn]; +f[0] = 1; +for (int i = 1; i < maxn; i++) + f[i] = i*f[i-1] % mod; + +int r[maxn]; +r[maxn-1] = inv(f[maxn-1]) +for (int i = maxn-1; i >= 1; i--) + r[i-1] = r[i]*i % mod; +``` + +TODO: техника с сайта емакса. diff --git a/ru/segtree.md b/ru/segtree.md new file mode 100644 index 0000000..f2e3f67 --- /dev/null +++ b/ru/segtree.md @@ -0,0 +1,369 @@ + +# Дерево отрезков + +**Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля. + +**Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках. + +Рассмотрим конкретную задачу: + +>Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов: + +>1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`). +>2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$. + +>Оба запроса нужно обрабатывать за время $O(\log n)$. + +Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции: + +Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1. + +Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков». + +![alt text](http://i.imgur.com/GGBmcEP.png) + +Строить его можно рекурсивной функцией: +* Если вершина является листом, взять в качестве суммы значение соответствующей ячейки. +* Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей. + +## Разные свойства + +Высота такого дерева есть величина $\Theta(\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики. + +Более того, любой полуинтервал разбивается на $O(\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков. + +Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} + \ldots + 1 < 2n$. Значит, оно линейное по памяти. + +При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом. + +## Ок, как это нам поможет? + +Опишем теперь, как с помощью такой структуры решить задачу. + +**Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$. + +Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\Theta(\log n)$ — по одной с каждого уровня. + +Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков. + +**Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения. + +Сделаем тоже рекурсивную функцию, рассмотрев три случая: + +* Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму. +* Если отрезки вершины и запроса не пересекаются, то вернуть 0. +* Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей. + +Чтобы разобраться, почему это работает за $O(\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\log n)$ отрезках, а значит и итоговая асимптотика будет такая же. + +## Ликбез по C++ + +Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества. + +Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел. + +Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\log n)$. + +В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. + + + +У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса. + + +```c++ +struct A { + int param1, param2; // тут можно что-то хранить + char param3 = 'k'; + A (int var) { + // эта часть называется конструктором + // ... + } + void do_something () { + // это какой-то другой метод + // ... + } +}; // <- не забудьте точку с запятой +``` + +Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве. + +Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется: + + +```c++ +A x(179); +x.do_something(); +x.param1 = 57; + +A *y = new A(42); // new возвращает адрес, по которому можно найти объект +y->do_something(); +y.param3 = '!'; +``` + +Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались + +## Реализация + +Общий план реализации любых структур данных: +1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc. +2. Формально описать, что должны делать методы и за какую асимптотику. +3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты. + + +```c++ +struct segtree { + int lb, rb; // левые и правые границы отрезков + int sum = 0; // сумма на текущем отрезке + segtree *l = 0, *r = 0; + segtree (int _lb, int _rb) { + lb = _lb, rb = _rb; + if (lb + 1 < rb) { + // если не лист, создаем детей + int t = (lb + rb) / 2; + l = new segtree(lb, t); + r = new segtree(t, rb); + } + } + void add (int k, int x) { + sum += x; + if (l) { + if (k < l->rb) + l->add(k, x); + else + r->add(k, x); + } + } + int get_sum (int lq, int rq) { + if (lb >= lq && rb <= rq) + // если мы лежим полностью в отрезке запроса, вывести сумму + return sum; + if (max(lb, lq) >= min(rb, rq)) + // если мы не пересекаемся с отрезком запроса, вывести ноль + return 0; + // иначе всё сложно -- запускаемся от детей и пусть они там сами решают + return l->get_sum(lq, rq) + r->get_sum(lq, rq); + } +}; +``` + +> Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$). + +Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи: +* Запросим сумму от $k$ до $n$ в ДО. +* Добавим единичку в $k$-тую позицию в ДО. + +Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \log n)$ запросов. + +> Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике. + +Ответим на все запросы в оффлайн, используя метод сканирующей прямой: + +* Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \times [y_1, y_2]$. +* Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше. +* Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$». + + +## Отложенные операции + +Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному. + +Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем. + +Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено. + +Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся. + +Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \log n)$. + +При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны. + + + +```c++ +struct segtree { + int lb, rb; + int sum = 0, assign = -1; + segtree *l = 0, *r = 0; + segtree (int _lb, int _rb) { + lb = _lb, rb = _rb; + if (lb + 1 < rb) { + int t = (lb + rb) / 2; + l = new segtree(lb, t); + r = new segtree(t, rb); + } + } + void push () { + if (assign != -1) { + sum = (rb-lb) * assign; + if (l) { // если дети есть + l->assign = assign; + r->assign = assign; + } + } + assign = -1; + } + void upd (int lq, int rq, int x) { + push(); + if (lq <= lb && rb <= rq) + assign = x; + else if (l && max(lb, lq) < min(rb, rq)) { + // если есть дети и отрезок запроса хоть как-то пересекается с нашим + l->upd(lq, rq, x); + r->upd(lq, rq, x); + // ...дальше они сами разберутся + } + } + int get_sum (int lq, int rq) { + push(); + if (lb >= lq && rb <= rq) + return sum; + if (max(lb, lq) >= min(rb, rq)) + return 0; + return l->get_sum(lq, rq) + r->get_sum(lq, rq); + } +}; +``` + +По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах. + +Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни. + +## Динамическое построение + +А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\log_2 10^6 \approx 20$, $\log_2 10^9 \approx 30$), кроме этапа построения. + +Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation. + +Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так. + + +```c++ +struct segtree { + int lb, rb; + int sum = 0; + segtree *l = 0, *r = 0; + segtree (int _lb, int _rb) { + lb = _lb, rb = _rb; + // а тут ничего нет + } + void extend () { + if (!l && lb + 1 < rb) { + int t = (lb + rb) / 2; + l = new segtree(lb, t); + r = new segtree(t, rb); + } + } + void add (int k, int x) { + extend(); + sum += x; + if (l) { + if (k < l->rb) + l->add(k, x); + else + r->add(k, x); + } + } + int get_sum (int lq, int rq) { + if (lb >= lq && rb <= rq) + return sum; + if (max(lb, lq) >= min(rb, rq)) + return 0; + extend(); + return l->get_sum(lq, rq) + r->get_sum(lq, rq); + } +}; +``` + +Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так: + + +```c++ +vector compress (vector a) { + vector b = a; + sort(b.begin(), b.end()); + b.erase(unique(b.begin(), b.end()), b.end()); + for (int &x : a) + x = int(lower_bound(b.begin(), b.end(), x) - b.begin()); + return a; +} +``` + +## Персистентность + +Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния. + +Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается. + +У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \log n)$. + + +```c++ +struct segtree { + int lb, rb; + int sum = 0; + segtree *l = 0, *r = 0; + segtree (int _lb, int _rb) { + lb = _lb, rb = _rb; + if (lb != rb) { + int t = (lb + rb) / 2; + l = new segtree(lb, t); + r = new segtree(t, rb); + } + } + void copy () { + if (l) { + l = new segtree(l); + r = new segtree(r); + } + } + void add (int k, int x) { + copy(); + sum += x; + if (l) { + if (k < l->rb) l->add(k, x); + else r->add(k, x); + } + } + int get_sum (int lq, int rq) { + // этот метод ничего не меняет -- он и так хороший + if (lq <= lb && rb <= rq) + return sum; + if (max(lb, lq) >= min(rb, rq)) + return 0; + return l->get_sum(lq, rq) + r->get_sum(lq, rq); + } +}; +``` + +> Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике. + +Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \log n)$ памяти. + +> Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке. + +Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу. + +Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`. + +Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом. + +> Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. + +У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\frac{1}{2^{100}}$, и ей на практике можно пренебречь. + +Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`. + +## Другие реализации + +Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы: + +**На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree). + +**«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051). + +## Задачи + +* [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции. +* Второй контест — на отложенные операции. +* [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность. diff --git a/ru/sparse-table.md b/ru/sparse-table.md new file mode 100644 index 0000000..49a6b71 --- /dev/null +++ b/ru/sparse-table.md @@ -0,0 +1,53 @@ + +# Разреженная таблица + +- Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \log n)$ с малой константой. +- Обновления не поддерживает (static RMQ). +- Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья. +- Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос). +- Требует $O(n \log n)$ памяти. +- Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ. + +Определим разреженную таблицу как двумерный массив размера $n \times\log n$: + +$$ +t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} +$$ + +Идея такая: считаем минимум на каждом отрезке длины $2^k$. + +Такой массив можно посчитать за его размер: $t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования. + +Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам. + +![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png) + +Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов. + + +```c++ +int a[maxn], lg[maxn], mx[maxn][logn]; + +int rmq (int l, int r) { + int t = lg[r-l+1]; + return min(mx[l][t], mx[r-(1<= 0; i--) { + mx[i][0] = a[i]; + for (int l = 0; l < logn-1; l++) + mx[i][l+1] = max(mx[i][l], mx[i+(1< test.txt' % gen) + v1 = os.popen('./%s < test.txt' % f1).read() + v2 = os.popen('./%s < test.txt' % f2).read() + if v1 != v2: + print test + print("Correct:") + print v1 + print("Wrong:") + print v2 + break +``` + +Автор обычно запускает его командой `python3 checker.py stupid smart gen.py 100`, предварительно скомпилировав `stupid` и `smart` в ту же директорию, что и сам `checker.py`. + +Скрипт написан под Linux. Для Windows нужно убрать «`./`» во всех системных вызовах. + +`gen.py` автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через `./gen`. Пример `gen`-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10: + + +```python +from random import randint, choice + +n = randint(1, 10) + +print(n) + +for _ in range(n): + print(choice('abc'), end='') +``` diff --git a/ru/strings.md b/ru/strings.md new file mode 100644 index 0000000..e04c28b --- /dev/null +++ b/ru/strings.md @@ -0,0 +1,206 @@ + +# Базовые строковые алгоритмы + +## Префикс-функция + +Рассмотрим задачу, которая возникает каждый раз, когда вы делаете `ctrl+f`: + +> Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него. + +Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго. + +Для решения этой задачи за линейное время придумали **префикс-функцию**. + +**Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \ldots s_i$, который также является и суффиксом этой строки (не считая всю строку). + +Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$. + + +```python +def slow_prefix_function(s): + n = len(s) + p = [0]*n + for i in range(n): + prefix = s[:i] + for l in range(1, i): + if prefix[:l] == prefix[-l:]: + p[i] = l + return p + +slow_prefix_function('aataataa') +``` + + + + + [0, 0, 1, 0, 1, 2, 3, 4] + + + +(Этот алгоритм работает за $O(n^3)$, но это только пока.) + +## Как это поможет решить исходную задачу? + +Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке. + +Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\#t$. + + +```python +s = "let it go" +t = """let it go, let it go +can't hold it back anymore +let it go, let it go +turn away and slam the door!""" + +print((s + '#' + t).replace('\n', ' ')) +print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)])) +``` + + let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door! + 00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000 + + +Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$. + +Такой алгоритм (посчитать префикс-функцию от $s\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**. + +## Как её быстро считать + +Рассмотрим ещё несколько примеров префикс-функций: + + +```python +for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']: + print(slow_prefix_function(s)) +``` + + [0, 0, 1, 2, 3] + [0, 0, 0, 0, 0, 0] + [0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0] + [0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + + +Можно заметить несколько особенностей: +* $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; +* $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \ldots s_i$, длина которого будет ровно на единицу меньше. + +Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения. + +Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\underbrace{aabaa}t\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$. + +Но что происходит, когда $s_{p_i}\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$. +* $\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. +* $\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa». +* $\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$. +* $\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$. + +В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым. + + +```python +def fast_prefix_function(s): + n = len(s) + p = [0]*n + for i in range(1, n): + cur = p[i - 1] + # перебираем префикс-функцию, пока не найдем равный символ + while s[i] != s[cur] and cur > 0: + cur = p[cur - 1] + # если нашли, то значение на единицу больше + if s[i] == s[cur]: + p[i] = cur + 1 + return p + +fast_prefix_function('abacabadabacabax') +``` + + + + + [0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0] + + + +### Асимптотика + +Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой. + +**Доказательство**: +* Как мы ранее заметили, префикс функция возрастает максимум на единицу. +* $\implies$ Вырасти она может максимум $n-1$ раз. +* Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один. +* $\implies$ Количество понижений не больше количества повышений, то есть $O(n)$. +* $\implies$ Суммарно итераций цикла `while` — $O(n)$. + +## Z-функция + +Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания. + +Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$. + +$$\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)$$ + + +```python +def slow_z_function(s): + n = len(s) + z = [0]*n + for i in range(1, n): + suffix = s[i:] + for l in range(1, i): + if s[:l] == suffix[:l]: + z[i] = l + return z + +for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']: + print(slow_z_function(s)) +``` + + [0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1] + [0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1] + [0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0] + + +Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$. + +## Как её быстро считать + +Заметим, что: +* $z_0 = 0$ — из-за договоренности (потому что информации не несет); +* $z_i = 0 \iff s_i \neq s_0$; +* $z_i > 0 \iff s_i = s_0$. + +Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$. + +Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. +* Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком. +* Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку. + + +```python +def fast_z_function(s): + n = len(s) + z = [0]*n + l = 0 + r = 0 + for i in range(1, n): + if i <= r: + z[i] = min(r - i + 1, z[i-l]) + while i + z[i] < n and s[z[i]] == s[i+z[i]]: + z[i] += 1 + if i + z[i] - 1 > r: + l = i + r = i + z[i] - 1 + return z +``` + +**Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$. + +## Зачем тогда люди используют префикс-функцию + +![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg) + +В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов. + +Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение. diff --git a/ru/suffix-array.md b/ru/suffix-array.md new file mode 100644 index 0000000..7442186 --- /dev/null +++ b/ru/suffix-array.md @@ -0,0 +1,109 @@ + +# Суффиксный массив + +Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике. + +Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём. + + + +*
«Паблик с тупыми шутками про проганье»
* + +## Мотивация + +**Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки. + + + +**Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно. + +В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс. + +Работать такой алгоритм будет за $O(|t| \log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \log |s|)$, что является одним из самых оптимальных алгоритмов поиска. + +Теперь научимся его строить. + +## Построение за $O(n \log n)$ + +Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара. + +Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \log^2 n)$. Но это не самый быстрый и удобный алгоритм. + +Наш алгоритм будет состоять из $\lceil \log n \rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив. + +Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов). + +Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4. + +$$ + p_0 = (0, 1, 3, 2) \;\;\; c_0 = (0, 0, 1, 0) +\\ p_1 = (0, 3, 1, 2) \;\;\; c_1 = (0, 1, 2, 0) +\\ p_2 = (3, 0, 1, 2) \;\;\; c_2 = (1, 2, 3, 0) +$$ + +Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом. + +Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором. + +Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \log^2 n)$, потому что каждый этап будет работать за $O(n \log n$). + +**Оптимизация до $O(n \log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$. + + +```c++ +// строка -- это последовательность чисел от 1 до размера алфавита +vector suffix_array (vector &s) { + s.push_back(0); // добавляем нулевой символ в конец строки + int n = (int) s.size(), + cnt = 0, // вспомогательная переменная: счётчик для сортировки + cls = 0; // количество классов эквивалентности + vector c(n), p(n); + + map< int, vector > t; + for (int i = 0; i < n; i++) + t[s[i]].push_back(i); + + // «нулевой» этап + for (auto &x : t) { + for (int u : x.second) + c[u] = cls, p[cnt++] = u; + cls++; + } + + // пока все суффиксы не стали уникальными + for (int l = 1; cls < n; l++) { + vector< vector > a(cls); // массив для сортировки подсчётом + vector _c(n); // новые классы эквивалентности + int d = (1<(p.begin()+1, p.end()); +} +``` + +TODO: переписать это + +## Наибольшие общие префиксы + +Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами. + +TODO diff --git a/ru/treap.md b/ru/treap.md new file mode 100644 index 0000000..78e8aac --- /dev/null +++ b/ru/treap.md @@ -0,0 +1,460 @@ + +# Декартово дерево + +Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века. + +Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим. + +Декартово дерево же определяется и строится так: + +* Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*. +* Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*. +* От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня. +* Аналогично, запустимся от правой части и добавим корню правого сына. + +Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно. + +Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх: + +![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png) + +Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий: + +* Дерамида (дерево + пирамида) +* ПиВо (пирамида + дерево) +* КуРево (куча + дерево) +* Treap (tree + heap) + + ## Бинарные деревья + + С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например: + +* добавить число $x$ в множество +* определить, есть ли в множестве число $x$ +* найти первое число, не меньшее $x$ (`lower_bound`) +* найти количество чисел в промежутке $[l, r]$ + +При этом все операции — за $O(\log n)$. + +На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\log n)$. + +## Приоритеты и асимптотика + +В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска. + +**Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \log n)$. + +Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить. + +**Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*. + +Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна + +$$d_i = \sum_{j=1}^n a(j, i)$$ + +Её матожидание равно + +$$E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)$$ + +где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно. + +Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение. + +**Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$). + +**Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. + +**Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет. + +У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей: + +$$p(x, y) = \frac{1}{y-x+1}$$ + +Теперь, чтобы найти матожидание, эти вероятности надо просуммировать: + +$$E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)$$ + +Перед последним переходом мы получили сумму гармонического ряда. + +Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя. + +**Упражнение***. Выведите из этого доказательства асимптотику `quicksort`. + +## Реализация + +Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте. + +Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву. + + +```c++ +struct Node { + int key, prior; + Node *l = 0, *r = 0; + Node (int _key) { key = _key, prior = rand(); } +}; +``` + +Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся. + +### Merge + +Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча. + +Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше. + +Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына. + + +```c++ +Node* merge (Node *l, Node *r) { + if (!l) return r; + if (!r) return l; + if (l->prior > r->prior) { + l->r = merge(l->r, r); + return l; + } + else { + r->l = merge(l, r->l); + return r; + } +} +``` + +### Split + +Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$. + +В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо: + + +```c++ +typedef pair Pair; + +Pair split (Node *p, int x) { + if (!p) return {0, 0}; + if (p->key <= x) { + Pair q = split(p->r, x); + p->r = q.first; + return {p, q.second}; + } + else { + Pair q = split(p->l, x); + p->l = q.second; + return {q.first, p}; + } +} +``` + +### Пример: вставка + +`merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. + +Вот так, например, будет выглядеть код, добавляющий $x$ в сет. + + +```c++ +Node *root = 0; + +void insert (int x) { + Pair q = split(root, x); + Node *t = new Node(x); + root = merge(q.first, merge(t, q.second)); +} +``` + +### Пример: модификация для суммы на отрезке + +Иногда нам нужно написать какие-то модификации для более продвинутых операций. + +Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке». + + +```c++ +struct Node { + int val, sum; + // ... +}; +``` + +При `merge` и `split` надо будет поддерживать эту сумму актуальной. + +Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины. + + +```c++ +void sum (Node* v) { return v ? v->sum : 0; } +// обращаться по пустому указателю нельзя -- выдаст ошибку + +void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; } +``` + +В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается: + + +```c++ +Node* merge (Node *l, Node *r) { + // ... + if (...) { + l->r = merge(l->r, r); + upd(l); + return l; + } + else { + // ... + } +} +``` + + +```c++ +typedef pair Pair; + +Pair split (Node *p, int x) { + // ... + if (...) { + // ... + upd(p); + return {p, q.second}; + } + else { + // ... + } +} +``` + + Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму: + + +```c++ +int sum (int l, int r) { + Pair rq = split(root, r); + Pair lq = split(rq.first, l); + int res = sum(lr.second); + root = merge(lq.first, merge(lq.second, rq.second)); + return res; +} +``` + +# Неявный ключ + +Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку. + +Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными. + +Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева: + + + +```c++ +struct Node { + int key, prior, size = 1; + // ^ размер поддерева + Node *l = 0, *r = 0; + Node (int _key) { key = _key, prior = rand(); } +}; +``` + +Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины. + + +```c++ +int size (Node *v) { return v ? v->size : 0; } + +void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); } +``` + +`merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа. + +Про `split` теперь удобнее думать как "вырежи первые `k` элементов". + + +```c++ +typedef pair Pair; + +Pair split (Node *p, int k) { + if (!p) return {0, 0}; + if (size(p->l) + 1 <= k) { + Pair q = split(p->r, k - size(p->l) - 1); + // ^ правый сын не знает количество вершин слева от него + p->r = q.first; + upd(p); + return {p, q.second}; + } + else { + Pair q = split(p->l, k); + p->l = q.second; + upd(p); + return {q.first, p}; + } +} +``` + +Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно. + +### Пример: ctrl+x, ctrl+v + + +```c++ +Node* ctrlx (int l, int r) { + Pair q1 = split(root, r); + Pair q2 = split(q1.first, l); + root = merge(q2.first, q1.second); + return q2.second; +} +``` + + +```c++ +void ctrlv (Node *v, int k) { + Pair q = split(root, k); + root = merge(q.first, merge(v, q.second)); +} +``` + +### Пример: переворот + + + +Нужно за $O(\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д. + +Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут: + + +```c++ +struct Node { + bool rev; + // ... +}; +``` + +Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку: + + +```c++ +void push (node *v) { + if (v->rev) { + swap(v->l, v->r); + if (v->l) + v->rev ^= 1; + if (v->r) + v->rev ^= 1; + } + v->rev = 0; +} +``` + +Аналогично, эту функцию будем вызывать в начале `merge` и `split`. + +Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг. + + +```c++ +void reverse (int l, int r) { + Pair q1 = split(root, r); + Pair q2 = split(q1.first, l) + q2.second->rev ^= 1; + root = merge(q2.first, merge(q2.second, q1.second)); +} +``` + +# Функциональное программирование* + +Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно. + +Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке. + + +```c++ +auto apply (int l, int r, auto f) { + Pair q1 = split(root, r); + Pair q2 = split(q1.first, l) + q2.second = f(q2.second); + root = merge(q2.first, merge(q2.second, q1.second)); +} + +void reverse (Node *v) { + if (v) + v->rev ^= 1; +} +``` + +Применять её нужно так: + + +```c++ +apply(l, r, reverse); +``` + +Это работает в плюсах, начиная с `g++14`. + +Для простых операций можно даже написать лямбду: + + +```c++ +apply(l, r, [](Node *v){ + if (v) + v->rev ^= 1; +}); +``` + +# Персистентность* + +Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи. + +> Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции. + +Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим. + +> Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**. + +Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами. + +Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ. + +Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`: + + +```c++ +Node* copy (Node *v) { return new Node(*v); } +``` + +Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так: + + +```c++ +Pair split (Node *p, int x) { + p = copy(p); + // ... +} +``` + +В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию. + +У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\frac{L}{L+R}$, иначе за правое. + +**Теорема**. Такое переподвешивание эквивалентно приоритетам. + +**Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции: + +* Лист имеет вероятность 1 быть корнем себя (база индукции) +* Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}$. С вершинами правого дерева аналогично. + +Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД. + + +```c++ +Node* merge (Node *l, Node *r) { + if (!l) return r; + if (!r) return l; + l = copy(l), r = copy(r); + if (rand() % (size(l) + size(r)) < size(l)) { + // ... + } + else { + // ... + } +} +``` + +Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$? diff --git a/ru/trie.md b/ru/trie.md new file mode 100644 index 0000000..314c5a1 --- /dev/null +++ b/ru/trie.md @@ -0,0 +1,71 @@ + +# Бор + +Бор — это структура данных для компактного хранения строк. + +Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. + +![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png) + +Бор можно удобно использовать для разных задач: +* Хранение строк — занимает гораздо меньше места, чем массив или сет строк. +* Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке. + +* Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи. + +## Реализация + +Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: +* терминальная ли вершина, +* ссылки на детей, +* возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет. + + +```c++ +const int k = 26; + +struct Vertex { + Vertex* to[k] = {0}; + bool terminal = 0; +}; + +Vertex *root = new Vertex(); +``` + +Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной. + + +```c++ +void add_string (string &s) { + v = root; + for (char c : s) { + c -= 'a'; + if (!v->to[c]) + v->to[c] = new Vertex(); + v = v->to[c]; + } + v->terminal = true; +} +``` + +Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть. + +Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности. + +### Как хранить ссылки + +Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми. + +В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например. + +Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями. + +## Суффиксные ссылки + +> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. + +Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. + +Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. + +Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). diff --git a/web/en/index.html b/web/en/index.html new file mode 100644 index 0000000..3285e2a --- /dev/null +++ b/web/en/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + + +

some english index

+ + diff --git a/web/img/.editorial.html.swp b/web/img/.editorial.html.swp new file mode 100644 index 0000000000000000000000000000000000000000..eed31bfbef5ecb81e88ef7890edd52f724b5de2a GIT binary patch literal 12288 zcmeI%ze@u#6bJBE2RBi4a1!Dvbn4~&F^GCjT?7RY>g1B9&tA&?sJW*F2ldbJPwAiM zUG;_zb}0yjZyDC|ZDxC?rLI45~fB*y_009U< z00Izz00bZ~FJJ~lUz&EO42z z7?p^OJN*3OUgN;?ljkxClr3`MT;d?QP`&$`xLwaW*E;7(XTtr`35T}G+JVwn^bO^g BJ0$=B literal 0 HcmV?d00001 diff --git a/web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb b/web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb new file mode 100644 index 0000000..3679909 --- /dev/null +++ b/web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Алгоритм Ахо-Корасик\n", + "\n", + "Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время.\n", + "\n", + "Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик.\n", + "\n", + "\n", + "\n", + "Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы.\n", + "\n", + "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", + "\n", + "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", + "\n", + "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", + "\n", + "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Зачем это нужно\n", + "\n", + "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", + "\n", + "Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор.\n", + "\n", + "Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Алгоритм Ахо-Корасик*\n", + "\n", + "Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой.\n", + "\n", + "**Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход.\n", + "\n", + "**Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда.\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "const int k = 26;\n", + "\n", + "struct Vertex {\n", + " Vertex *to[k] = {0}, *go[k] = {0};\n", + " Vertex *link = 0, *p;\n", + " int pch;\n", + " Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; }\n", + "};\n", + "\n", + "Vertex *root = new Vertex(-1, 0);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "void add_string (string s) {\n", + " Vertex *v = root;\n", + " for (char _c : s) {\n", + " c -= 'a';\n", + " if (!v->to[c])\n", + " v->to[c] = new Vertex(c, v);\n", + " v = v->to[c];\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Vertex* go (Vertex *v, int c);\n", + "\n", + "Vertex* link (Vertex *v) {\n", + " if (!v->link) {\n", + " if (v == root || v->p == root) v->link = root;\n", + " else v->link = go(link(v->p), v->pch);\n", + " }\n", + " return v->link;\n", + "}\n", + "\n", + "Vertex* go (Vertex *v, int c) {\n", + " if (!v->go[c]) {\n", + " if (v->to[c]) v->go[c] = v->to[c];\n", + " else if (v == root) v->go[c] = root;\n", + " else v->go[c] = go(link(v), c);\n", + " }\n", + " return v->go[c];\n", + "}" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb b/web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb new file mode 100644 index 0000000..87cb744 --- /dev/null +++ b/web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb @@ -0,0 +1,533 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Красивые идейные задачи\n", + "\n", + "Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда.\n", + "\n", + "Задачи идут в порядке вспоминания, то есть в весьма рандомном." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Попугаи\n", + "\n", + "Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Минимум и максимум\n", + "\n", + "Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Перестановка\n", + "\n", + "Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Выпуклая оболочка\n", + "Требуется отвечать на 2 типа запросов:\n", + "\n", + "1. Добавить точку в выпуклую оболочку.\n", + "2. Проверить, лежит ли точка внутри выпуклой оболочки.\n", + "\n", + "Обе операции онлайн за $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Геометрическая прогрессия\n", + "\n", + "Найдите способ посчитать $\\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Покемоны\n", + "\n", + "В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \\times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \\to b$ и $b \\to c$ не следует, что $a \\to c$.\n", + "\n", + "У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Сортировка\n", + "Можно ли отсортировать\n", + "* 5 камней за 8 взвешиваний?\n", + "* 5 камней за 7 взвешиваний?\n", + "* 20 камней за 60 взвешиваний?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Точки в круге\n", + "\n", + "Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Замкнутые ломаные\n", + "\n", + "Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Неубывающий массив\n", + "\n", + "Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Чётный цикл\n", + "\n", + "Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## $k$-ая порядковая статистика\n", + "\n", + "Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Доминирующий элемент\n", + "\n", + "Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз. Время работы $O((n+m) \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Разрушение дерева\n", + "\n", + "Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## $k$-ый элемент на отрезке\n", + "\n", + "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Различные числа на отрезке\n", + "\n", + "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\\sqrt{n})$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Физкультура\n", + "\n", + "Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов:\n", + "\n", + "1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя)\n", + "2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно)\n", + "\n", + "При этом приказ может частично отменить действие предыдущих приказов.\n", + "\n", + "После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Нулевая сумма\n", + "\n", + "Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мета-задача\n", + "\n", + "В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ниточка\n", + "\n", + "В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Пельмени\n", + "\n", + "Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## От нуля до единицы\n", + "\n", + "Дан следующий код:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "x = 0\n", + "while x < 1:\n", + " x += random()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Требуется посчитать матожидание `x`.\n", + "\n", + "(`random` в питоне возвращает случайное действительное число от 0 до 1.)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Площадь\n", + "\n", + "Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Окружности\n", + "\n", + "Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна\n", + "\n", + "* соприкасаться с *внешней*,\n", + "* соприкасаться с предыдущей (($k-1$)-ой),\n", + "* иметь при этом максимальный радиус.\n", + "\n", + "Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Блеф\n", + "\n", + "Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может:\n", + "\n", + "* Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается.\n", + "* Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом.\n", + "\n", + "С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Достижимость\n", + "Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Нумизмат\n", + "\n", + "Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой.\n", + "\n", + "Придумайте любой полиномиальный алгоритм." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Принцесса\n", + "\n", + "В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве.\n", + "\n", + "Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством.\n", + "\n", + "У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми.\n", + "\n", + "Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу.\n", + "\n", + "Асимптотика $O(n^2)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Спираль\n", + "\n", + "Определим спираль $(2n+1) \\times (2n+1)$ как матрицу следующего вида:\n", + "\n", + "$$\n", + "\\begin{matrix}\n", + "21 & 22 & 23 & 24 & 25 \\\\\n", + "20 & 7 & 8 & 9 & 10 \\\\\n", + "19 & 6 & 1 & 2 & 11 \\\\\n", + "18 & 5 & 4 & 3 & 12 \\\\\n", + "17 & 16 & 15 & 14 & 13 \\\\\n", + "\\end{matrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$).\n", + "\n", + "$q \\leq 100$, $n \\leq 10^9$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Польский лабиринт\n", + "\n", + "Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`.\n", + "\n", + "![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png)\n", + "\n", + "Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам.\n", + "\n", + "$n \\leq 10^5$, изначальные координаты туристов до $10^9$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Нимные подмножества\n", + "\n", + "Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \\subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \\subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Баланс степеней\n", + "\n", + "Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Два пути\n", + "\n", + "Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Пьяница\n", + "\n", + "Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ксоровый рюкзак\n", + "\n", + "Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Иван Сусанин\n", + "\n", + "Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш.\n", + "\n", + "- Камиль руководит армией днём и водит армию по *дорогам*.\n", + "- Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. \n", + "\n", + "Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск.\n", + "\n", + "Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу.\n", + "Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось.\n", + "\n", + "Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Варенье\n", + "\n", + "В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм.\n", + "\n", + "Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья.\n", + "\n", + "Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной.\n", + "\n", + "$n, m \\leq 10^5$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Лабиринт\n", + "\n", + "Серёжа потерялся в лабиринте $n \\times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт.\n", + "\n", + "Придумайте любой полиномиальный алгоритм." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Обезьяна\n", + "\n", + "Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ожидание минимума\n", + "\n", + "Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин.\n", + "\n", + "Придумайте любой точный полиномиальный алгоритм." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Шумный ксор\n", + "\n", + "Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \\frac15$ быть единичным, то есть каждый бит $x \\oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x = # ...\n", + "\n", + "def mask(p=0.2):\n", + " r = 0\n", + " for i in range(32):\n", + " if random.random() < p:\n", + " r += 2**i\n", + " return r\n", + "\n", + "def query(y):\n", + " return bin(x ^ y ^ mask()).count('1')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ваша задача — отгадать число, используя не более 10000 попыток." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb b/web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb new file mode 100644 index 0000000..30b7828 --- /dev/null +++ b/web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Битовое сжатие\n", + "\n", + "* Из-за него в «асимптотиках» появляется `/64`\n", + "* На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов\n", + "* `bitset` есть в stl; говорят, самописный быстрее" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости.\n", + "\n", + "Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. \n", + "\n", + "Работать с ним нужно вот так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "const int lim = 1000;\n", + "bitset b; // создать битсет размера lim (должно быть константой)\n", + "b.set(); // заполнить единицами\n", + "b.reset(); // заполнить нулями\n", + "b.flip(); // заменить единички на нули и наоборот\n", + "b.count(); // посчитать число единичек\n", + "cout << b; // вывести битовую строку" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Рюкзак\n", + "\n", + "Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака.\n", + "\n", + "Обычно его решают так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "bool dp[lim] = {}; // так можно его заполнить нулями\n", + "dp[0] = 1;\n", + "for (int i = 0; i < n; i++)\n", + " for (int x = lim - a[i]; x >= 0; x--)\n", + " dp[x + a[i]] |= dp[x];" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "…а с битсетом оно разгоняется так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "bitset b;\n", + "b[0] = 1;\n", + "for (int i = 0; i < n; i++)\n", + " b |= b << a[i];" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Цикл длины 3\n", + "\n", + "Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\\frac{n^3}{64})$ следующим образом:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "bitset g[maxn]; // матрица смежности\n", + "for (int a = 0; a < n; a++) {\n", + " for (int b = 0; b < n; b++) {\n", + " if (g[a][b] && (~g[a] & g[b]).any()) {\n", + " // цикл найден\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Перемножение матриц\n", + "\n", + "Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "typedef bitset t;\n", + "typedef array matrix;\n", + "\n", + "matrix operator* (matrix a, matrix b) {\n", + " matrix c;\n", + " for(int i = 0; i < n; i++)\n", + " for(int j = 0; j < n; j++)\n", + " if(a[i][j])\n", + " c[i] |= b[j];\n", + " return c;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Гаусс\n", + "\n", + "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", + "\n", + "Нас по сути просят решить следующую систему:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", + "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", + "\\ldots \\\\\n", + "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", + "\n", + "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "t gauss (matrix a) {\n", + " for (int i = 0; i < n; i++) {\n", + " int nonzero = i;\n", + " for (int j = i+1; j < n; j++)\n", + " if (a[j][i])\n", + " nonzero = j;\n", + " swap(a[nonzero], a[i]);\n", + " for (int j = 0; j < n; j++)\n", + " if (j != i && a[j][i])\n", + " a[j] ^= a[i];\n", + " }\n", + " t x;\n", + " for (int i = 0; i < n; i++)\n", + " x[i] = a[i][n] ^ a[i][i];\n", + " return x;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb b/web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb new file mode 100644 index 0000000..37746bb --- /dev/null +++ b/web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb @@ -0,0 +1,249 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Центроидная декомпозиция\n", + "\n", + "Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».\n", + "\n", + "Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще).\n", + "\n", + "![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\\frac{n}{2}$.\n", + "\n", + "Выясняется, что центр всегда существует — это следует из алгоритма его поиска:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int s[maxn];\n", + "\n", + "int sizes (int v) {\n", + " s[v] = 1;\n", + " for (int u : g[v])\n", + " // для простоты считаем, что дерево корневое\n", + " s[v] += sizes(u);\n", + " return s[v];\n", + "}\n", + "\n", + "// второй параметр -- размер дерева\n", + "int centroid (int v, int n) {\n", + " for (int u : g[v])\n", + " if (s[u] > n/2)\n", + " return centroid(u, n);\n", + " return v;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение.** `centroid` действительно находит цетроид.\n", + "\n", + "**Доказательство:**\n", + "\n", + "* `centroid` вернет вершину, у которой размеры всех детей не больше $\\frac{n}{2}$ (это явно проверяется в `if`-е).\n", + "* Мы пришли в эту вершину, когда её размер был больше $\\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\\frac{n}{2}+1) = \\frac{n}{2}-1$ вершин.\n", + "* Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен.\n", + "\n", + "Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».\n", + "\n", + "**Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.\n", + "\n", + "Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Теперь поймём, зачем мы всё это делали.\n", + "\n", + "**Утверждение.** Каждая вершина входит в $O(\\log n)$ компонент.\n", + "\n", + "**Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\\lceil \\log_2 n \\rceil$ разделений.\n", + "\n", + "**Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \\log n)$.\n", + "\n", + "**Утверждение.** Для любого пути $a \\leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$.\n", + "\n", + "**Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$.\n", + "\n", + "Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Подсчет путей с заданным свойством\n", + "\n", + "Рассмотрим конкретный пример: подсчёт путей заданной длины.\n", + "\n", + "Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид.\n", + "\n", + "Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int l = 179; // нужная нам длина\n", + "int ans = 0;\n", + "\n", + "// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена\n", + "bool used[maxn];\n", + "int s[maxn]; // размеры поддеревьев\n", + "\n", + "void sizes (int v, int p) {\n", + " s[v] = 1;\n", + " for (int u : g[v])\n", + " if (u != p && !used[u])\n", + " sizes(u, v), s[v] += s[u];\n", + "}\n", + "\n", + "int centroid (int v, int p, int n) {\n", + " for (int u : g[v])\n", + " if (u != p && !used[u] && s[u] > n/2)\n", + " return centroid(u, v, n);\n", + " return v;\n", + "}\n", + "\n", + "// записывает в t[] глубины вершин\n", + "void dfs (int v, int p, int d, vector &t) {\n", + " t.push_back(d);\n", + " for (int u : g[v])\n", + " if (u != p && !used[u])\n", + " dfs(u, v, d + 1, t);\n", + "} \n", + "\n", + "void solve (int v) {\n", + " /* <единственный зависящий от конкретной задачи код> */\n", + " size(v);\n", + " vector d(s[v], 0);\n", + " d[0] = 1;\n", + " for (int u : g[v]) {\n", + " if (!used[u]) {\n", + " vector t;\n", + " dfs(u, v, 1, t);\n", + " for (int x : t)\n", + " if (x <= l)\n", + " ans += d[l-x];\n", + " for (int x : t)\n", + " d[x]++;\n", + " }\n", + " }\n", + " /* */\n", + "\n", + " used[v] = 1;\n", + " for (int u : g[v])\n", + " if (!used[u])\n", + " solve(centroid(u, v, s[u]/2));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Асимптотика** $O(n \\log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\\log n)$ «уровней» центроидной декомпозиции." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на путях — offline\n", + "\n", + "Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.\n", + "\n", + "А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем.\n", + "\n", + "Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. \n", + "\n", + "Таким образом, каждый запрос будет просмотрен $O(\\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \\log n + n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на путях — online\n", + "\n", + "Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.\n", + "\n", + "Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \\times \\log n$, в котором для каждой вершины будем хранить $O(\\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.\n", + "\n", + "Тогда, при ответе на запрос, мы за $O(\\log n)$ или даже $O(\\log \\log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Асимптотика при более долгих пересчётах\n", + "\n", + "TODO: сослаться на мастер-теорему\n", + "\n", + "Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \\log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени?\n", + "\n", + "Возьмем худший случай — когда каждый раз компонента разбивается на две равные части.\n", + "\n", + "\n", + "\n", + "Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:\n", + "\n", + "$$ \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log \\frac{n}{2^k}\n", + "\\leq \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log n\n", + "= n \\log n \\sum_{k=0}^{\\log n} \\frac{1}{2^k}\n", + "< 2 n \\log n\n", + "= O(n \\log n) $$\n", + "\n", + "Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \\log n)$ — как, впрочем, и во всех остальных «разделяйках»." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb b/web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb new file mode 100644 index 0000000..a1f3db1 --- /dev/null +++ b/web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb @@ -0,0 +1,475 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Supercomputing for the Masses\n", + "\n", + "This is htmt-rendered notebook is supposed to be opened with Colab or a debian-based linux machine with a CUDA-capable GPU.\n", + "\n", + "You need to complete a quest and install CUDA.\n", + "\n", + "We do not expect anything from reader except some knowledge of C, Python basic algorithms and generally how computers work." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Subtlties of the Moore's law\n", + "\n", + "There are physical limitations to the speed of processors.\n", + "\n", + "One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some of them have" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Why multiprocessing?\n", + "\n", + "Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two types of " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## General-purpose GPU\n", + "\n", + "There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills.\n", + "\n", + "There are several.\n", + "\n", + "This is like with Windows and Linux.\n", + "\n", + "We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can easily dump 98% of performance of you think this way." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Everything should work if you're on Colab. Otherwise, you might need to complete a [little quest](https://wiki.tiker.net/PyCuda/Installation/Linux/Ubuntu). It will probably be enough to do `sudo apt-get install nvidia-cuda-dev` (install CUDA itself) and then return to `pip install pycuda`.\n", + "\n", + "You can get it by running `lspci | grep NVIDIA`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install pycuda" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy\n", + "\n", + "from pycuda.compiler import SourceModule\n", + "import pycuda.driver as drv\n", + "import pycuda.autoinit" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time.\n", + "\n", + "You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Why CUDA\n", + "\n", + "Most of it still applicable.\n", + "\n", + "Again, GPU programming is very specific.\n", + "\n", + "SSE and tensor cores." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Kernels\n", + "\n", + "Just like C or C++, except that you use some custom built-in functions and specifiers.\n", + "\n", + "CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:\n", + "\n", + "You need to think of your computer as a heterogenious machine: there is host data and device data.\n", + "\n", + "* You move input data to device memory.\n", + "* You run some computation on device.\n", + "* You retrieve back this data.\n", + "\n", + "In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## NumPy\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example: $a + b$\n", + "\n", + "For coordination and testing, we will use `numpy` package. If you don't have it, install it: `pip install numpy`. This is also very optimized, so we will benchmark against it." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "ename": "LaunchError", + "evalue": "cuMemAlloc failed: unspecified launch failure", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0ma\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mastype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumpy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfloat32\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0ma_gpu\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmem_alloc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdtype\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitemsize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmemcpy_htod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma_gpu\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mLaunchError\u001b[0m: cuMemAlloc failed: unspecified launch failure" + ] + } + ], + "source": [ + "a = numpy.random.randn(4,4)\n", + "\n", + "a = a.astype(numpy.float32)\n", + "\n", + "a_gpu = cuda.mem_alloc(a.size * a.dtype.itemsize)\n", + "\n", + "cuda.memcpy_htod(a_gpu, a)\n", + "\n", + "mod = SourceModule(\"\"\"\n", + " __global__ void doublify(float *a)\n", + " {\n", + " int idx = threadIdx.x + threadIdx.y*4;\n", + " a[idx] *= 2;\n", + " }\n", + "\"\"\")\n", + "\n", + "func = mod.get_function(\"doublify\")\n", + "func(a_gpu, block=(4,4,1))\n", + "\n", + "a_doubled = numpy.empty_like(a)\n", + "cuda.memcpy_dtoh(a_doubled, a_gpu)\n", + "print(a)\n", + "print(a_doubled)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "ename": "LaunchError", + "evalue": "cuModuleLoadDataEx failed: unspecified launch failure - ", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mdest\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m }\n\u001b[0;32m----> 6\u001b[0;31m \"\"\")\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0madd_kernel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmod\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"add\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/anaconda3/lib/python3.7/site-packages/pycuda/compiler.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, source, nvcc, options, keep, no_extern_c, arch, code, cache_dir, include_dirs)\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 293\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mpycuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdriver\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 294\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodule\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcubin\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 295\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 296\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_bind_module\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mLaunchError\u001b[0m: cuModuleLoadDataEx failed: unspecified launch failure - " + ] + } + ], + "source": [ + "mod = SourceModule(\"\"\"\n", + " __global__ void add(float *dest, float *a, float *b) {\n", + " const int i = threadIdx.x;\n", + " dest[i] = a[i] + b[i];\n", + " }\n", + "\"\"\")\n", + "\n", + "add_kernel = mod.get_function(\"add\")\n", + "\n", + "a = numpy.random.randn(400).astype(numpy.float32)\n", + "b = numpy.random.randn(400).astype(numpy.float32)\n", + "\n", + "dest = numpy.zeros_like(a)\n", + "\n", + "add_kernel(drv.Out(dest), drv.In(a), drv.In(b), block=(400,1,1))\n", + "\n", + "print(dest-a*b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "mod = SourceModule(\"\"\"\n", + " __global__ void check_prime(int *a, int *b, int *c) {\n", + " int n = blockIdx.x*blockDim.x + threadIdx.x;\n", + " for (int i = 2; i < 10000; i++)\n", + " a[i] |= (n % i == 0);\n", + " }\n", + "\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = numpy.zeros(10**8, dtype=numpy.int)\n", + "\n", + "func = mod.get_function(\"check_prime\")\n", + "func(cuda.InOut(a), block=(len(a)//32,32, 1))\n", + "\n", + "print(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What you need to understand about GPUs is that they are extremely specialised for their applications.\n", + "\n", + "Intrinsics for that.\n", + "\n", + "Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision.\n", + "\n", + "First, they introduced \"multiply-accumulate\" operation (e. g. `x += y * z`) per 1 GPU clock cycle.\n", + "\n", + "Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell).\n", + "\n", + "Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix).\n", + "\n", + "This is a lot of work per \n", + "\n", + "Well, you don't really need anything more precise than that for deep learning anyway.\n", + "\n", + "\n", + "It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices.\n", + "\n", + "Probably, the proper name would be \"4x4 matrix cores\", however NVIDIA marketing team decided to use \"tensor cores\".\n", + "\n", + "So, see, this is not exactly fair comparison.\n", + "\n", + "\n", + "\n", + "*
You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull
*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "down to int4 (16-valued, you heard correct)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Warps and thread blocks\n", + "\n", + "Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties.\n", + "\n", + "\n", + "\n", + "You can actually do the same stuff with 2d and 3d indexing — weird, right?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reducing an array\n", + "\n", + "It seems to be simple: you just need to .\n", + "\n", + "What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen:\n", + "\n", + "1. Read $x$ into register\n", + "2. Read $s$ into register\n", + "3. Calculate $s + x$\n", + "4. Write it back to wherever $s$ was initially\n", + "\n", + "Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value.\n", + "\n", + "\n", + "\n", + "Note: Atomics to do that\n", + "\n", + "for small data types they are implemented on the hardware level and much more faster than that.\n", + "\n", + "std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Memory types\n", + "\n", + "If all the various types of device memory were to race, here’s how the race would turn out:\n", + "\n", + "Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible).\n", + "\n", + "* 1st place: **Register memory**\n", + "
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread.\n", + "* 2nd place: **Shared Memory**\n", + "
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should\n", + "* 3rd place: **Constant Memory**\n", + "
\n", + "* 4th: Texture Memory\n", + "* Tie for last place: Local Memory and Global Memory\n", + "\n", + "What you need to care for now is register \n", + "\n", + "For now, you need to care about differe\n", + "\n", + "Accessing global memory takes hundreds." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Heterogineous computing\n", + "\n", + "CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more CUDA-enabled NVIDIA GPU devices.\n", + "\n", + "Threading resources\n", + "Execution pipelines on host systems can support a limited number of concurrent threads. Servers that have four hex-core processors today can run only 24 threads concurrently (or 48 if the CPUs support Hyper-Threading.) By comparison, the smallest executable unit of parallelism on a CUDA device comprises 32 threads (termed a warp of threads). Modern NVIDIA GPUs can support up to 1536 active threads concurrently per multiprocessor (see Features and Specifications of the CUDA C Programming Guide) On GPUs with 16 multiprocessors, this leads to more than 24,000 concurrently active threads.\n", + "Threads\n", + "Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches (when two threads are swapped) are therefore slow and expensive. By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work (in warps of 32 threads each). If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput.\n", + "RAM\n", + "The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: dense matrix multiplication\n", + "\n", + "A lot of these are actually sparse. You can do stuff with social network graphs or web graphs.\n", + "\n", + "Cool. But let's disapploint us for a bit:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: dynamic programming" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Some primitives\n", + "\n", + "Before we go deeper into more complex algorithms, we need to master some " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Problem: sorting" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb b/web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb new file mode 100644 index 0000000..3506ced --- /dev/null +++ b/web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb @@ -0,0 +1,109 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Динамическое программирование" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Разберем некоторые классические идеи и задачи в динамическом программировании." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Рюкзак\n", + "\n", + "> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наибольшая общая подпоследовательность\n", + "\n", + "> Есть две строки. Найти длину их наибольшей общей подпоследовательности.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Расстановка скобок\n", + "\n", + "> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.\n", + "\n", + "Можно завести динамику «можно ли »." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наибольшая возрастающая подпоследовательность\n", + "\n", + "Есть также алгоритм за $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мемоизация\n", + "\n", + "Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.\n", + "\n", + "Первый предполагает" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Число разбиений\n", + "\n", + ">" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Замена параметра на ответ\n", + "\n", + ">" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb b/web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb new file mode 100644 index 0000000..30cde99 --- /dev/null +++ b/web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Оптимизации динамики\n", + "\n", + "* Разделяй и властвуй\n", + "* Оптимизация Кнута\n", + "* Convex Hull Trick\n", + "* Лямбда-оптимизация" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Задача\n", + "\n", + "> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.\n", + "\n", + "Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.).\n", + "\n", + "Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// x[] — отсортированный массив координат точек, нумерация с нуля\n", + "\n", + "// квадрат длины отрезка от i-той до j-той точки\n", + "int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }\n", + "\n", + "// TODO: предподсчитать cost\n", + "\n", + "for (int i = 0; i <= m; i++)\n", + " f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо\n", + "// все остальные f предполагаем равными бесконечности\n", + "\n", + "for (int i = 1; i <= n; i++)\n", + " for (int j = 1; j <= m; j++)\n", + " for (int k = 0; k < i; k++)\n", + " f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1));" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Заметим, что циклы по `i` и `j` можно поменять местами.\n", + "\n", + "Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Разделяй-и-властвуй\n", + "\n", + "Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.\n", + "\n", + "*Утверждение.* $opt[i, j] \\leq opt[i, j+1]$.\n", + "\n", + "Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.\n", + "\n", + "Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$.\n", + "\n", + "Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "void solve (int l, int r, int _l, int _r, int k) {\n", + " if (l > r) return; // отрезок пустой — выходим\n", + " int t = (l + r) / 2;\n", + " int opt = _l;\n", + " for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы \n", + " int val = f[i+1][k-1] + cost(i, j);\n", + " if (val < f[t][k])\n", + " f[t][k] = val, opt = i;\n", + " }\n", + " solve(l, t-1, _l, opt, k);\n", + " solve(t+1, r, opt, _r, k);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Вызываться она будет просто последовательно для каждого слоя:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "for (int k = 1; k <= m; k++)\n", + " solve(0, n-1, 0, n-1, k);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Теперь пересчет одного «слоя» динамики занимает $O(n \\log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\\log n)$, а значит и каждый элемент будет просмотрен не более $O(\\log n)$ раз.\n", + "\n", + "Получается, что асимптотика улучшилась до $O(n m \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Оптимизация Кнута\n", + "\n", + "Предыдущий метод основывался на том факте, что $opt[i, j] \\leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру?\n", + "\n", + "$$ opt[i-1, j] \\leq opt[i, j] \\leq opt[i, j+1] $$\n", + "\n", + "В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок.\n", + "\n", + "Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:\n", + "\n", + "$$ \\sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \\sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$\n", + "\n", + "Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for (int i = 1; i <= n; i++) {\n", + " for (int j = m; j >= 1; j--) {\n", + " for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) {\n", + " int val = f[i+1][k-1] + cost(i, j);\n", + " if (val < f[t][k])\n", + " f[t][k] = val, opt[i][j] = i;\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Сравните с базовым решением — всего 3 новых строчки." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Convex Hull Trick\n", + "\n", + "Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение.\n", + "\n", + "$$ f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\} = \\min_{k < i} \\{\n", + "f[k, j-1] + x_{i-1}^2\n", + "- 2x_{i-1} x_k\n", + "+ x_k^2\n", + "\\}$$\n", + "\n", + "Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется\n", + "$\n", + "\\underbrace{f[k, j-1] + x_k^2}_{a_k}\n", + "\\underbrace{-2x_k}_{b_k} x_{i-1}\n", + "$.\n", + "\n", + "Это теперь можно переписать как $\n", + "\\min_k\n", + "(a_k, b_k)\n", + "\\cdot\n", + "(1, X_{i-1})\n", + "$ (имеется в виду скалярное произведение)\n", + "\n", + "Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: иллюстрация." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: мем про Скуби-Ду." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ли Шао" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Лямбда-оптимизация\n", + "\n", + "Не «фольклорное» название — дискретный метод множителей Лагранжа.\n", + "\n", + "Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .\n", + "\n", + "Заметим следующее: \n", + "\n", + "Давайте заменим $f[i, j]$ на $g_\\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\\lambda$, за использование отрезка.\n", + "\n", + "Давайте сделаем бинпоиск по $\\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \\lambda k$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Суммируем\n", + "\n", + "TODO: сделать табличку\n", + "\n", + "* Разделяйка: $O(nm \\log n)$, если `cost` такой, что `opt` монотонна по одному аргументу.\n", + "* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам.\n", + "* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение.\n", + "* Лагранж: $O(n \\log n)$. Функция должна быть выпуклой." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Другие задачи\n", + "\n", + "Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.\n", + "\n", + "> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "c++", + "file_extension": ".c++", + "mimetype": "text/x-c++src", + "name": "c++" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb b/web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb new file mode 100644 index 0000000..3c3d9f3 --- /dev/null +++ b/web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb @@ -0,0 +1,285 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
\n", + "\n", + "НЯ!\n", + "
Эта статья полна любви и обожания.\n", + "
Возможно, стоит добавить ещё больше?\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Дерево Фенвика\n", + "\n", + "Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Определение\n", + "\n", + "Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: \n", + " \n", + "$$ t_i = \\sum_{k=F(i)}^i a_k $$\n", + " \n", + "где $F$ это какая-то функцию, для которой выполнено $F(i) \\leq i$. Конкретно её определим потом.\n", + " \n", + "Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле:\n", + " \n", + "$$ sum(k) = t_k + sum(F(k)-1) $$\n", + " \n", + "Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\\log n)$. Популярны две функции:\n", + "\n", + "* $F_1(x) =$ `x & (x + 1)`\n", + "* $F_2(x) =$ `x - (x & -x) + 1`\n", + "\n", + "Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.\n", + "\n", + "**Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\\log n)$ таких отрезков." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Реализация\n", + "\n", + "Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int t[maxn];\n", + "\n", + "// возвращает сумму на префиксе\n", + "int sum (int r) {\n", + " int res = 0;\n", + " for (; r > 0; r -= r & -r)\n", + " res += t[r];\n", + " return res;\n", + "}\n", + "\n", + "int sum (int l, int r) {\n", + " return sum(r) - sum(l-1);\n", + "}\n", + "\n", + "// обновляет нужные t\n", + "void add (int k, int x) {\n", + " for (; k <= n; k += k & -k)\n", + " t[k] += x;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Многомерный случай\n", + "\n", + "> $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку\n", + "\n", + "Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре.\n", + "\n", + "`sum` перепишется следующим образом:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int sum (int r1, int r2) {\n", + " int res = 0;\n", + " for (int i = r1; i > 0; i -= i & -i)\n", + " for (int j = r2; j > 0; j -= j & -j)\n", + " ans += t[i][j];\n", + " return res;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах.\n", + "\n", + "Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \\log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Бинпоиск\n", + "\n", + "Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\\log n)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// возвращает индекс, на котором сумма уже больше\n", + "int lower_bound (int s) {\n", + " int k = 0;\n", + " for (int l = logn; l >= 0; l--) {\n", + " if (k + (1<= k > i - (i & -i)`. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Будем перебирать префиксы TODO\n", + "\n", + "Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя.\n", + "\n", + "Можно представить дерево так: ячейка 2^k содержит все\n", + "\n", + "TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Название\n", + "\n", + "Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree».\n", + "\n", + "Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.\n", + "\n", + "Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).\n", + "\n", + "В частном случае, когда длина массива равна $2^k$, то дерево будет только одно." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/games-checkpoint.ipynb b/web/img/.ipynb_checkpoints/games-checkpoint.ipynb new file mode 100644 index 0000000..f183b10 --- /dev/null +++ b/web/img/.ipynb_checkpoints/games-checkpoint.ipynb @@ -0,0 +1,331 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "c0JYtYks_TYp" + }, + "source": [ + "# Теория игр\n", + "\n", + "Начнём с самого баянного примера математической игры, который можно вспомнить:\n", + "\n", + "> На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков.\n", + "\n", + "Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так:\n", + "\n", + "$$ f_k = \\lnot f_{k-1} \\lor \\lnot f_{k-2} \\lor \\lnot f_{k-3} = \\lnot (f_{k-1} \\land f_{k-2} \\land f_{k-3}) $$\n", + "\n", + "то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния).\n", + "\n", + "**Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Игры на графах\n", + "\n", + "В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек».\n", + "\n", + "Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные*\n", + "\n", + "Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*.\n", + "\n", + "Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ретроанализ\n", + "\n", + "Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности:\n", + "\n", + "* Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины.\n", + "* Вершина *выигрышная* — если из неё есть переход в проигрышную вершину.\n", + "* Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные).\n", + "\n", + "Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся.\n", + "\n", + "**Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин.\n", + "\n", + "Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние.\n", + "\n", + "Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось.\n", + "\n", + "Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные.\n", + "\n", + "**Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали.\n", + "\n", + "* При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). \n", + "* При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше.\n", + "\n", + "Асимптотика составит $O(n + m)$, как и у любого bfs-а." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа\n", + "int cnt[maxn]; // счётчик исходящих рёбер\n", + "\n", + "enum StatusType { win, loss, unknown };\n", + "StatusType status[maxn]; // выгрышность вершины;\n", + "// по умолчанию все кроме терминальных считаются unknown\n", + "// те, кто в итоге остаются unknown -- ничейные\n", + "\n", + "queue q = {/* нужно заранее добавить сюда все терминальные вершины*/};\n", + "\n", + "while (!q.empty()) {\n", + " int v = q.front();\n", + " q.pop();\n", + " for (int u : t[v]) {\n", + " cnt[u]--; // удаляем это ребро\n", + " if (status[v] == unknown) {\n", + " // из u есть ребро в проигрышную -- значит она выигрышная\n", + " if (status[v] == loss)\n", + " status[u] = win;\n", + " // все ребра u ведут в выигрышные вершины -- значит она проигрышная\n", + " if (status[v] == win && cnt[u] == 0) {\n", + " status[u] = loss;\n", + " // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь\n", + " if (status[v] != unknown)\n", + " q.push(u);\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "StatusType dfs(int v) {\n", + " if (status[v] != unknown)\n", + " return status[v];\n", + " status[v] = loss;\n", + " // изменим статус, когда найдём переход в проигрышную вершину\n", + " for (int u : g[v])\n", + " if (dfs(u) == loss)\n", + " status[v] = win;\n", + " return status[v];\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: можно ли здесь циклы учесть?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Минимаксные игры\n", + "\n", + "Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно.\n", + "\n", + "Типичный граф минимаксной игры выглядит следующим образом:\n", + "\n", + "\n", + "\n", + "В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников.\n", + "\n", + "Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ретроанализ для больших графов\n", + "\n", + "Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго.\n", + "\n", + "Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей).\n", + "\n", + "Ретроанализ нужно оптимизировать." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ограничение перебора\n", + "\n", + "Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. \n", + "\n", + "Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит.\n", + "\n", + "Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Мемоизация позиций\n", + "\n", + "В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает .\n", + "\n", + "Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Альфа-бета отсечение\n", + "\n", + "\n", + "\n", + "Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым.\n", + "\n", + "На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым.\n", + "\n", + "Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6.\n", + "\n", + "Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем.\n", + "\n", + "Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Monte-Carlo Tree Search\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Игры с ненулевой суммой\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Теория Шпрага-Гранди\n", + "\n", + "Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые.\n", + "\n", + "Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями.\n", + "\n", + "**Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \\oplus a_2 \\oplus \\ldots \\oplus a_n $ размеров кучек отлична от нуля.\n", + "\n", + "**Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы:\n", + "\n", + "* Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \\oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия.\n", + "* Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \\oplus a_i \\oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \\oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$.\n", + "\n", + "Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \\oplus a_i$.\n", + "\n", + "Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ним с увеличениями\n", + "\n", + "Пусть у нас" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Эквивалентность игр ниму" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Игры с неполной информацией\n", + "\n", + "Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков.\n", + "\n", + "Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет.\n", + "\n", + "Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Дилемма заключенного" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Камень-ножницы-бумага" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Покер\n", + "\n", + "Рассмотрим такую у" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "29_game-theory.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb b/web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb new file mode 100644 index 0000000..555b3b9 --- /dev/null +++ b/web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb @@ -0,0 +1,421 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Геометрия и ООП" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали.\n", + "\n", + "![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg)\n", + "\n", + "Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:\n", + "\n", + "$$ a \\cdot b = x_a x_b + y_a y_b $$\n", + "\n", + "![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg)\n", + "\n", + "У него есть полезные свойства:\n", + "\n", + "* Скалярное произведение симметрично ($a \\cdot b = b \\cdot a$).\n", + "* Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.\n", + "* Перпендикулярные вектора должны иметь нулевое скалярное произведение.\n", + "* Если угол острый, то оно положительное\n", + "* Если угол тупой, то отрицательное\n", + "\n", + "**Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:\n", + "\n", + "$$ a \\times b = x_a y_b - y_a x_b $$\n", + "\n", + "![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg)\n", + "\n", + "Его свойства:\n", + "\n", + "* Скалярное произведение *анти*симметрично ($a \\times b = - (b \\times a)$).\n", + "* Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.\n", + "* Коллинеарные вектора должны иметь нулевое векторное произведение.\n", + "* Если b «слева» от a, то оно положительное.\n", + "* Если b «справа» — то отрицательное." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Всякие проверки\n", + "\n", + "Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.\n", + "\n", + "**Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.\n", + "\n", + "$$\n", + "\\text{P лежит внутри ABC} \\iff \\begin{cases}\n", + "(B-A) \\times (P-A) \\geq 0 \\\\\n", + "(C-B) \\times (P-B) \\geq 0 \\\\\n", + "(A-C) \\times (P-C) \\geq 0 \\\\\n", + "\\end{cases}\n", + "$$\n", + "\n", + "**Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.\n", + "\n", + "$$ V = \\frac{1}{2} (B-A) \\times (C-A) $$\n", + "\n", + "**Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).\n", + "\n", + "![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg)\n", + "\n", + "Забудьте о формуле Герона и всегда считайте площади через векторное произведение.\n", + "\n", + "Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.\n", + "\n", + "**Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\\times(c-a) > 0$).\n", + "\n", + "**Пересекаются ли отрезки**.\n", + "\n", + "![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Уравнение прямой\n", + "\n", + "Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством.\n", + "\n", + "У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \\geq 0$ будет указывать в сторону самой полуплоскости.\n", + "\n", + "Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой:\n", + "\n", + "$$ d = \\frac{|Ax_0+By_0+C|}{\\sqrt{A^2+B^2}} $$\n", + "\n", + "**Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "A_1 x + B_1 y + C_1 = 0 \\\\\n", + "A_2 x + B_2 y + C_2 = 0\n", + "\\end{cases}\n", + "\\implies \n", + "\\begin{cases}\n", + "-x = \\frac{B_1 y + C_1}{A_1} \\\\\n", + "-x = \\frac{B_2 y + C_2}{A_2}\n", + "\\end{cases}\n", + "\\implies \n", + "\\frac{B_1 y + C_1}{A_1} = \\frac{B_2 y + C_2}{A_2}\n", + "\\implies \n", + "y = - \\frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1}\n", + "$$\n", + "\n", + "Аналогично, $x = \\frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки).\n", + "\n", + "Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Как это кодить в C++" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Точка $\\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "struct r {\n", + " double x, y;\n", + " r () {}\n", + " r (int _x, int _y) { x = _x, y = _y; }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Операции над векторами\n", + "\n", + "Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "double len (r a) { return sqrt(a.x*a.x + a.y*a.y); }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Операторы\n", + "\n", + "В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д.\n", + "\n", + "Давайте для будущих нужд определим `+` и `-`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); }\n", + "r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Скалярное произведение" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int operator*(r a, r b){ return a.x*b.x + a.y*b.y; }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Векторное произведение\n", + "\n", + "Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int operator^(r a, r b){ return a.x*b.y - b.x*a.y; }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Ввод-вывод\n", + "\n", + "Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "istream& operator>>(istream &in, r &p){ \n", + " in >> p.x >> p.y;\n", + " return in;\n", + "}\n", + "\n", + "ostream& operator<<(ostream &out, r &p){ \n", + " out << p.x << \" \" << p.y << endl;\n", + " return out; \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Алгебра VS Алгоритмы или зачем мы всё это делали\n", + "\n", + "Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Векторное представление прямой ($Ax + By + C = 0 \\rightarrow r = at + b$)\n", + "\n", + "Тут нужно просто выбрать две любые точки на прямой." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// даны A, B, C (A^2 + B^2 != 0)\n", + "r a, b;\n", + "if (eq(A, 0)) // значит, это горизонтальная прямая\n", + " a = r(0, -C/B), b = r(1, -C/B);\n", + "else\n", + " a = r(-C/A, 0), b = (1, -(C+B)/A, 1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Пример: отражение от прямой\n", + "\n", + "Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.\n", + "\n", + "$\\Pr_a b = \\frac{a \\cdot b}{|a|} \\frac{a}{|a|} = \\frac{|a| |b| \\cos \\alpha}{|a|} \\frac{a}{|a|} = |b| \\cos \\alpha \\frac{a}{|a|} $\n", + "\n", + "Формула имеет смысл: длина на единичный вектор направления.\n", + "\n", + "Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "// прямая r = at + b, точка c\n", + "r pr (r a, r b, r c) {\n", + " c -= b; // пусть c и a выходят из одной точки\n", + " return b + (a*b / len(a) / len(a)) * a;\n", + "}\n", + "\n", + "r reflect (r a, r b, r c) {\n", + " return c + 2*(pr(a, b, c)-c);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Типичные баги" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Точность\n", + "\n", + "> Первое правило действительных чисел — не использовать действительные числа\n", + "\n", + "Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "const double eps = 1e-8;\n", + "\n", + "bool eq (double a, double b) { return abs(a-b) < eps }" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### $0 \\neq -0$\n", + "\n", + "Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Область определения обратных функций\n", + "\n", + "`acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb b/web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb new file mode 100644 index 0000000..f1a6144 --- /dev/null +++ b/web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb @@ -0,0 +1,426 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "tqKO26jI7Eg2" + }, + "source": [ + "# Хэширование в строковых задачах\n", + "\n", + "Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.\n", + "\n", + "«Хорошая» хэш-функция:\n", + "* Быстро считается — за линейное от размера объекта время;\n", + "* Имеет не очень большие значения — влезающие в 64 бита;\n", + "* «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\\frac{1}{n}$.\n", + "\n", + "Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*.\n", + "\n", + "Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \\cdot n \\cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.\n", + "\n", + " ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "mihqVF9EHNW3" + }, + "source": [ + "## Применения в реальной жизни\n", + "\n", + "* **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.\n", + "* **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\\frac{k}{n}$ = $O(1)$ при правильном выборе $n$.\n", + "* **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.\n", + "* **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.\n", + "* **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.\n", + "* **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "sb5PdKP87Eg3" + }, + "source": [ + "Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы.\n", + "\n", + "Сегодня же мы остановимся на строках." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "AwuPsbSa7Eg3" + }, + "source": [ + "# Полиномиальное хэширование\n", + "\n", + "**Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами.\n", + "\n", + "Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`.\n", + "\n", + "Определим *прямой полиномиальный хэш* строки как значение следующего многочлена:\n", + "\n", + "$$ h_f = (s_0 + s_1 k + s_2 k^2 + \\ldots + s_n k^n) \\mod p $$\n", + "\n", + "Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой.\n", + "\n", + "Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "h3UUC_Vl80Is" + }, + "outputs": [], + "source": [ + "const int k = 31, mod = 1e9+7;\n", + "\n", + "string s = \"abacabadaba\";\n", + "long long h = 0, m = 1;\n", + "for (char c : s) {\n", + " int x = (int) (c - 'a' + 1);\n", + " h = (h + m * x) % mod;\n", + " m = (m * k) % mod;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "7_1zG4nb80I7" + }, + "source": [ + "Можем ещё определить *обратный полиномиальный хэш*:\n", + "\n", + "$$ h_b = (s_0 k^n + s_1 k^{n-1} + \\ldots + s_n) \\mod p $$\n", + "\n", + "Его преимущество в том, что можно написать на одну строчку кода меньше:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "kShe7vvz80JC" + }, + "outputs": [], + "source": [ + "long long h = 0;\n", + "for (char c : s) {\n", + " int x = (int) (c - 'a' + 1);\n", + " h = (h * k + x) % mod;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "VWd2N6Ic80JL" + }, + "source": [ + "Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hKCe8eBr80JP" + }, + "source": [ + "## Зачем он нужен?\n", + "\n", + "Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций.\n", + "\n", + "Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$:\n", + "\n", + "$$ h(ab) = h(a) + k^{|a|} \\cdot h(b) $$\n", + "\n", + "Удалить префикс строки можно так:\n", + "\n", + "$$ h(b) = \\frac{h(ab) - h(a)}{k^{|a|}} $$\n", + "\n", + "А суффикс — ещё проще:\n", + "\n", + "$$ h(a) = h(ab) - k^{|a|} \\cdot h(b) $$\n", + "\n", + "В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "0IBufJNe_jVG" + }, + "outputs": [], + "source": [ + "const int maxn = 1e5+5;\n", + "\n", + "int p[maxn];\n", + "p[0] = 1;\n", + "\n", + "for (int i = 1; i < maxn; i++)\n", + " p[i] = (p[i-1] * k) % mod;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_PKLEmZU80Jb" + }, + "source": [ + "Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "qyJGTChd80Jd" + }, + "outputs": [], + "source": [ + "int h[maxn];\n", + "h[0] = 0; // h[k] -- хэш префикса длины k\n", + "\n", + "// будем считать, что s это уже последовательность int-ов\n", + "\n", + "for (int i = 0; i < n; i++) \n", + " h[i+1] = (h[i] + p[i] * s[i]) % mod;" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "L7n-KRvs80Jr" + }, + "source": [ + "Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке:\n", + "\n", + "$$ h(s[l:r]) = \\frac{h_r-h_l}{k^l} $$\n", + "\n", + "Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.\n", + "\n", + "Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить.\n", + "\n", + "$$ \\hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "FMjf8Rej80Jv" + }, + "outputs": [], + "source": [ + "int hash_substring (int l, int r) {\n", + " return (h[r+1] - h[l]) * p[n-l] % mod;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "l5XFHeaJ80J5" + }, + "source": [ + "Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "VuqWJtAvCCaQ" + }, + "source": [ + "### Упражнение\n", + "\n", + "Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "moSrfcxI7EhB" + }, + "source": [ + "## Примеры задач\n", + "\n", + "**Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`.\n", + "\n", + "**Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.\n", + "\n", + "**Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.\n", + "\n", + "**Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором.\n", + "\n", + "**Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "krDNbmroGTTs" + }, + "source": [ + "### Изменения строки*\n", + "\n", + "Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.\n", + "\n", + "Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.\n", + "\n", + "Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "8SFqBzPV7EhD" + }, + "source": [ + "# Вероятность ошибки и почему это всё вообще работает\n", + "\n", + "У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.\n", + "\n", + "Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "PE38pSDXQxcH" + }, + "source": [ + "## Выбор констант\n", + "\n", + "Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \\cdot n^2$. Обоснование — см. парадокс дней рождений.\n", + "\n", + "Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно.\n", + "\n", + "Можно также брать модуль $2^{64}$. У него есть несколько преимуществ:\n", + "* Он большой — второй модуль точно не понадобится.\n", + "* С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении.\n", + "* С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`.\n", + "\n", + "Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду.\n", + "\n", + "В выборе же $k$ ограничения не такие серьезные:\n", + "* Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.\n", + "* Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.\n", + "\n", + "Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "DSfMhJRhRaZe" + }, + "source": [ + "## Парадокс дней рождений\n", + "\n", + "> В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%.\n", + "\n", + "Более общее утверждение: в мультимножество нужно добавить $\\Theta(\\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали.\n", + "\n", + "**Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения.\n", + "Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$.\n", + "\n", + "$$f(n, d) = (1-\\frac{1}{d}) \\times (1-\\frac{2}{d}) \\times ... \\times (1-\\frac{n-1}{d})$$\n", + "\n", + "Попытаемся оценить $f$:\n", + "\n", + "$$\n", + "\\begin{align}\n", + " \\begin{aligned}\n", + " e^x & = 1 + x + \\frac{x^2}{2!} + \\ldots & \\text{(ряд Тейлора для экспоненты)} \\\\\n", + " & \\simeq 1 + x & \\text{(аппроксимация для $|x| \\ll 1$)} \\\\\n", + " e^{-\\frac{n}{d}} & \\simeq 1 - \\frac{n}{d} & \\text{(подставим $\\frac{n}{d} \\ll 1$)} \\\\\n", + " f(n, d) & \\simeq e^{-\\frac{1}{d}} \\times e^{-\\frac{2}{d}} \\times \\ldots \\times e^{-\\frac{n-1}{d}} & \\\\\n", + " & = e^{-\\frac{n(n-1)}{2d}} & \\\\\n", + " & \\simeq e^{-\\frac{n^2}{2d}} & \\\\\n", + " \\end{aligned}\n", + "\\end{align}\n", + "$$\n", + "\n", + "Из последнего выражения более-менее понятно, что вероятность $\\frac{1}{2}$ достигается при $n \\approx \\sqrt{d}$ и в этой точке изменяется очень быстро.\n", + "\n", + "**Второе доказательство** (для любителей теорвера). Введем $\\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\\frac{1}{d}$.\n", + "\n", + "Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\\frac{n (n-1)}{2} \\cdot \\frac{1}{d}$.\n", + "\n", + "Отсюда понятно, что если $d = \\Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно.\n", + "\n", + "*Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "2WAkqxpvRfj0" + }, + "source": [ + "### Бонус: «мета-задача»\n", + "\n", + "Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.\n", + "\n", + "«Решите» задачу." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "7_string-hashing.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/web/img/.ipynb_checkpoints/hld-checkpoint.ipynb b/web/img/.ipynb_checkpoints/hld-checkpoint.ipynb new file mode 100644 index 0000000..5faf5cd --- /dev/null +++ b/web/img/.ipynb_checkpoints/hld-checkpoint.ipynb @@ -0,0 +1,186 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Heavy-light декомпозиция\n", + "\n", + "HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid).\n", + "\n", + "\n", + "\n", + "*
TODO: найти менее уродливую иллюстрацию
*\n", + "\n", + "Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.\n", + "\n", + "Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.\n", + "\n", + "**Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер.\n", + "\n", + "**Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.\n", + "\n", + "Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.\n", + "\n", + "**Утверждение.** На любом вертикальном пути будет не более $O(\\log n)$ блоков.\n", + "\n", + "**Доказательство** разбивается на две части:\n", + "\n", + "* Лёгких ребер на вертикальном пути будет не более $O(\\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего.\n", + "* Непрерывных путей из тяжелых рёбер будет не более $O(\\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\\log n)$.\n", + "\n", + "**Следствие.** На любом пути будет не более $O(\\log n)$ блоков.\n", + "\n", + "Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Реализация\n", + "\n", + "Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.\n", + "\n", + "А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.\n", + "\n", + "Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vector g[maxn];\n", + "int s[maxn], p[maxn], tin[maxn], tout[maxn];\n", + "int head[maxn]; // «голова» тяжелого пути, которому принадлежит v\n", + "int t = 0;\n", + "\n", + "void sizes (int v = 0) {\n", + " s[v] = 1;\n", + " for (int &u : g[v]) {\n", + " sizes(u);\n", + " s[v] += s[u];\n", + " if (s[u] > s[g[v][0]])\n", + " // &u -- это ссылка, так что её легально использовать при swap-е\n", + " swap(u, g[v][0]);\n", + " }\n", + "}\n", + "\n", + "void hld (int v = 0) {\n", + " rin[t] = v;\n", + " tin[v] = t++;\n", + " for (int u : g[v]) {\n", + " // если это тяжелый ребенок -- его next нужно передать\n", + " // в противном случае он сам является головой нового пути\n", + " head[u] = (u == g[v][0] ? head[v] : u);\n", + " hld(u);\n", + " }\n", + " tout[v] = t;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Как им решать задачи\n", + "\n", + "Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:\n", + "\n", + "1. Узнать минимальное число на пути между $v_i$ и $u_i$.\n", + "2. Изменить число у $v_i$-той вершины на $x_i$.\n", + "\n", + "Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int val[maxn];\n", + "segtree st(0, n);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "void upd (int v, int x) {\n", + " st.upd(tin[v], x);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int ancestor (int a, int b) {\n", + " return tin[a] <= tin[b] && tin[b] <= tout[a];\n", + "}\n", + "\n", + "void up (int &a, int &b, int &ans) {\n", + " while (!ancestor(head[a], b)) {\n", + " ans = min(ans, st.get_min(tin[head[a]], tin[a]));\n", + " a = p[head[a]];\n", + " }\n", + "}\n", + "\n", + "int get_min (int a, int b) {\n", + " int ans = inf;\n", + " up(a, b, ans);\n", + " up(b, a, ans);\n", + " if (!ancestor(a, b))\n", + " swap(a, b);\n", + " ans = min(ans, st.get_min(tin[a], tin[b]));\n", + " return ans;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/index-checkpoint.ipynb b/web/img/.ipynb_checkpoints/index-checkpoint.ipynb new file mode 100644 index 0000000..b2dd755 --- /dev/null +++ b/web/img/.ipynb_checkpoints/index-checkpoint.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Алгоритмика\n", + "\n", + "Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/).\n", + "\n", + "Более-менее готово:\n", + "\n", + "* [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное\n", + "* [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое\n", + "* [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки\n", + "* [Центроидная декомпозиция](http://sereja.me/a/centroid)\n", + "* [Heavy-light декомпозиция](http://sereja.me/a/hld)\n", + "* [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла\n", + "* [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву\n", + "* [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость\n", + "* [Ро-алгоритм Полларда](http://sereja.me/a/pollard)\n", + "* [Sparse table](http://sereja.me/a/sparse-table)\n", + "* [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона\n", + "* [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса\n", + "* [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера\n", + "* [Классные задачки](http://sereja.me/a/bayans): пока что без решений\n", + "\n", + "Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально.\n", + "\n", + "Не очень готово:\n", + "\n", + "* [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция\n", + "* [Суффиксный массив](http://sereja.me/a/suffix-array)\n", + "* [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++\n", + "* [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick\n", + "* [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала\n", + "* [Обратное по модулю](http://sereja.me/a/reciprocal)\n", + "* [Метод отжига](http://sereja.me/a/annealing)\n", + "* [Стресс-тестирование](http://sereja.me/a/stress-test)\n", + "* [Бор](http://sereja.me/a/trie)\n", + "* [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько\n", + "* Выпуклые оболочки\n", + "* [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса\n", + "* CUDA: программирование на GPU (на английском)\n", + "\n", + "Вообще не готово:\n", + "* [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема\n", + "* [Ахо-Корасик](http://sereja.me/a/aho-corasick)\n", + "* [Теорвер](http://sereja.me/a/probability)\n", + "* Корневая декомпозиция\n", + "* Теория информации\n", + "* Функции потерь в машинном обучении\n", + "* Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском)\n", + "* Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском)\n", + "\n", + "Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb b/web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb new file mode 100644 index 0000000..7200ab9 --- /dev/null +++ b/web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Быстрое умножение\n", + "\n", + "В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).\n", + "\n", + "[Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.\n", + "\n", + "Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.\n", + "\n", + "Алгоритм Карацубы имеет довольно значимое место в истории науки.\n", + "\n", + "{\\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}).\n", + "У Колмогорова была гипотеза, что нижняя оценка для {\\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности\n", + "{\\displaystyle M(n)=O(n^{\\log _{2}3})} M(n)=O(n^{{\\log _{2}3}})\n", + "и тем самым опроверг «гипотезу {\\displaystyle n^{2}} n^{2}».\n", + "\n", + "(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)\n", + "\n", + "Можно представить себе какого-то грозного японца." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мастер-теорема\n", + "\n", + "Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек»." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Мастер-теорема.** Пусть имеется рекуррента:\n", + "\n", + "$$ T(n) = \\begin{cases}\n", + "a T(\\frac{n}{b}) + \\Theta(n^c), & n > n_0\n", + "\\\\ \\Theta(1), & n \\leq n_0\n", + "\\end{cases} $$\n", + "\n", + "Тогда:\n", + "\n", + "* **A.** Если $c > \\log_b a$, то $T(n) = \\Theta(n^c)$.\n", + "* **B.** Если $c = \\log_b a$, то $T(n) = \\Theta(n^c \\log n)$.\n", + "* **C.** Если $c < \\log_b a$, то $T(n) = \\Theta(n^{\\log_b a})$.\n", + "\n", + "---\n", + "\n", + "\n", + "\n", + "---\n", + "\n", + "**Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням:\n", + "\n", + "$$ T(n) = \\sum_{k=0}^{\\log_b n} a^k (\\frac{n}{b^k})^c = n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k $$\n", + "\n", + "* **A.** Если $c > \\log_b a$, то $\\sum (\\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \\Theta(n^c)$.\n", + "* **B.** Если $c = \\log_b a$, то\n", + "$$\\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\sum_{k=0}^{\\log_b n} 1^k = \\Theta(n^c \\log_b n)$$\n", + "* **C.** Если $c < \\log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,\n", + "\n", + "$$ n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\Theta(n^c (\\frac{a}{b^c})^{\\log_b n}) = \\Theta(n^c \\cdot \\frac{a^{\\log_b n}}{n^c}) = \\Theta(a^{\\log_b n}) = \\Theta(n^{\\log_b a}) $$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Алгоритм Карацубы\n", + "\n", + "Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат.\n", + "\n", + "Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Развитие идеи\n", + "\n", + "То же самое можно применить матрицам.\n", + "\n", + "Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\\epsilon})$ для произвольного $\\epsilon$ ещё никто не доказал." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/lca-checkpoint.ipynb b/web/img/.ipynb_checkpoints/lca-checkpoint.ipynb new file mode 100644 index 0000000..f4e7190 --- /dev/null +++ b/web/img/.ipynb_checkpoints/lca-checkpoint.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "1MwyLmBaV1hj" + }, + "source": [ + "# Корневые деревья" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hMjCrvgNV1hk" + }, + "source": [ + "Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*.\n", + "\n", + "Примеры корневых деревьев:\n", + "* наследование классов в языках программирования (если множественное наследование запрещено),\n", + "* дерево факторизации числа на простые (в общем случае не уникальное),\n", + "* иерархия в какой-нибудь компании,\n", + "* просто множество как-то вложенных объектов (ссылка на крысу-роботягу).\n", + "\n", + "Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Напоминание: DFS\n", + "\n", + "Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "vector g[maxn];\n", + "int p[maxn], tin[maxn], tout[maxn];\n", + "int t = 0;\n", + "\n", + "void dfs (int v) {\n", + " tin[v] = t++;\n", + " for (int u : g[v])\n", + " dfs(u);\n", + " tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "У этих массивов много полезных свойств:\n", + "* Вершина $u$ является предком $v$ $\\iff tin_v \\in [tin_u, tout_u) $. Эту проверку можно делать за константу.\n", + "* Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой.\n", + "* В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер.\n", + "* Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$.\n", + "* Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на поддеревьях\n", + "\n", + "Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.\n", + "\n", + "> Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$.\n", + "\n", + "Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на уровнях\n", + "\n", + "> Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$).\n", + "\n", + "Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам.\n", + "\n", + "Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наименьший общий предок\n", + "\n", + "Очень много задач нам поможет решить следующая вспомогательная задача.\n", + "\n", + "> Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких.\n", + "\n", + "По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные.\n", + "\n", + "![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png)\n", + "\n", + "Для лучшего понимания: медленно (за линейное время) это можно делать так:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bool a (int u, int v) {\n", + " return tin[u] <= tin[v] && tin[v] <= tout[u];\n", + "}\n", + "\n", + "int lca (int u, int v) {\n", + " while (!ancestor(u, v))\n", + " u = p[u];\n", + " return u;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## LCA: двоичные подъемы\n", + "\n", + "Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \\times \\lceil \\log n \\rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень.\n", + "\n", + "Такой препроцессинг можно выполнить за $O(n \\log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int up[maxn][logn];\n", + "\n", + "void dfs (int v) {\n", + " for (int l = 1; l < logn; l++)\n", + " up[v][l] = up[up[v][l-1]][l-1];\n", + " tin[v] = t++;\n", + " for (int u : g[v]) {\n", + " up[u][0] = v;\n", + " dfs(u);\n", + " }\n", + " tout[v] = t++;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Пусть поступил очередной запрос — пара вершин $(u, v)$:\n", + "* Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.\n", + "* Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.\n", + "\n", + "Подробнее про второй пункт. Пусть $L = \\lceil \\log n \\rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "cFYU1Y5qV1hs" + }, + "outputs": [], + "source": [ + "int lca (int v, int u) {\n", + " if (a(v, u)) return v;\n", + " if (a(u, v)) return u;\n", + " for (int l = logn-1; l >= 0; l--)\n", + " if (!ancestor(up[v][l], u))\n", + " v = up[v][l];\n", + " return up[v][0];\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Асимптотика\n", + "\n", + "Препроцессинг — $O(n \\log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу.\n", + "\n", + "Ответ на запрос — $O(\\log n)$, потому что по сути мы делаем один бинпоиск." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Запросы на путях\n", + "\n", + "Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).\n", + "\n", + "Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути.\n", + "\n", + "Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "int get_min (int v, int u) {\n", + " int ans = inf;\n", + " for (int l = logn-1; l >= 0; l--)\n", + " if (!ancestor(up[v][l], u))\n", + " v = up[v][l], ans = min(ans, mn[v][l]);\n", + " for (int l = logn-1; l >= 0; l--)\n", + " if (!ancestor(up[u][l], v))\n", + " u = up[u][l], ans = min(ans, mn[u][l]);\n", + " return min({ans, mn[v][0], mn[u][0]})\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "PXpE5vHfV1hu" + }, + "source": [ + "## Сведение к RMQ\n", + "\n", + "Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Во втором массиве мы по сути выписали наш проход dfs-а.\n", + "\n", + "Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.\n", + "\n", + "Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "b7R7XMv4V1hy" + }, + "source": [ + "### Разреженная таблица\n", + "\n", + "На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \\log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.\n", + "\n", + "Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \\log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## А наоборот можно?*\n", + "\n", + "*Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.\n", + "\n", + "Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот.\n", + "Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.\n", + "\n", + "Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль.\n", + "\n", + "Возьмем константу $k = \\lfloor \\frac{\\log n}{2} \\rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\\frac{2 n}{\\log n})$, и построение будет работать за линейное время:\n", + "\n", + "$$O(\\frac{2 n}{\\log n} \\log \\frac{2 n}{\\log n}) = O(\\frac{2 n}{\\log n} (\\log 2n - \\log \\log n)) = O(n)$$\n", + "\n", + "Также посчитаем для каждой возможной маски размера $\\frac{\\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\\sqrt n$ (ради этого мы и делили логарифм на два).\n", + "\n", + "ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.\n", + "\n", + "Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.\n", + "\n", + "Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear).\n", + "\n", + "Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.\n", + "\n", + "Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.**" + ] + } + ], + "metadata": { + "colab": { + "name": "10_lca.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb b/web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb new file mode 100644 index 0000000..f5b8c0b --- /dev/null +++ b/web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb @@ -0,0 +1,453 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Ликбез по линейной алгебре\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Определение**. Функция $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ называется *линейной*, если для неё выполнено\n", + "\n", + "1. $ f(x+y) = f(x) + f(y) $\n", + "2. $ f(ax) = a f(x), \\; a \\in R $\n", + "\n", + "Примеры:\n", + "* $ f(x) = 0 $\n", + "* $ f(x) = x $\n", + "* $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\\mathbb{R}^3$ в $\\mathbb{R}$)\n", + "* $ f(x) = (x, -x, 0) $ (из $\\mathbb{R}$ в $\\mathbb{R}^3$)\n", + "\n", + "Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства:\n", + "* Сумма линейных функций — линейная функция.\n", + "* Сумма коммутативна: $f+g = g+f$).\n", + "* Сумма ассоциативна: $(f+g)+h = f+(g+h)$.\n", + "* Композиция $f(g(x)) = (f \\circ g)(x)$ линейных функций — линейная функция.\n", + "* Композция ассоциативна: $(f \\circ g) \\circ h = f \\circ (g \\circ h) = f \\circ g \\circ h$.\n", + "* Композиция в общем случае не коммутативна.
\n", + "Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен.\n", + "\n", + "Все свойства можно вывести лишь из этих двух пунктов в определении." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Что такое матрица?\n", + "\n", + "Можно показать, что любую линейную функцию $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ можно представить в таком виде:\n", + "\n", + "$$\n", + "f(x) =\n", + "\\begin{pmatrix}\n", + "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\\\\n", + "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\\\\n", + "\\ldots \\\\\n", + "a_{m1} x_1 + a_{m2} x_2 + \\ldots + a_{mn} x_n \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "*Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$.\n", + "\n", + "$$\n", + "A =\n", + "\\begin{pmatrix}\n", + "a_{11} & a_{12} & \\ldots & a_{1n} \\\\\n", + "a_{21} & a_{22} & \\ldots & a_{2n} \\\\\n", + "\\vdots & \\vdots & \\ddots & \\vdots \\\\\n", + "a_{m1} & a_{m2} & \\ldots & a_{mn} \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "Каждой линейной функции из $\\mathbb{R}^n$ в $\\mathbb{R}^m$ соответствует какая-то матрица размера $n \\times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте.\n", + "\n", + "Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \\circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так:\n", + "\n", + "$$ C = AB: C_{ij} = \\sum_{i=1}^{k} A_{ik} B_{kj} $$\n", + "\n", + "Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$.\n", + "\n", + "Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \\times k$, то вторая должна иметь размер $k \\times m$, то есть «средние» размерности обязательно должны совпадать.\n", + "\n", + "\n", + "\n", + "Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.\n", + "\n", + "Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:\n", + "\n", + "* Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$.\n", + "* Сумма коммутативна: $A+B = B+A$)\n", + "* Сумма ассоциативна: $(A+B)+C = A+(B+C)$\n", + "* Умножение ассоциативно: $(AB)C = A(BC) = ABC$.\n", + "* Умножение в общем случае не коммутативно." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Пример**: матрица поворота в 2d.\n", + "$$\n", + "\\begin{pmatrix}\n", + "\\cos \\alpha & -\\sin \\alpha \\\\\n", + "\\sin \\alpha & \\cos \\alpha \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "**Пример**: матрица проецирования на $Ox$ в 3d.\n", + "$$\n", + "\\begin{pmatrix}\n", + "1 & 0 & 0 \\\\\n", + "0 & 0 & 0 \\\\\n", + "0 & 0 & 0 \\\\\n", + "\\end{pmatrix}\n", + "$$\n", + "\n", + "**Пример**: матрица «свапни $x$ и $y$».\n", + "$$\n", + "\\begin{pmatrix}\n", + "0 & 1 \\\\\n", + "1 & 0 \\\\\n", + "\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Напишем класс, который реализует матричное умножение." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "struct matrix {\n", + " int n, m;\n", + " int t[];\n", + " matrix (int _n, int _m) {\n", + " n = _n, m = _m;\n", + " t = new int(n*m);\n", + " memset(t, 0, sizeof t);\n", + " }\n", + " int[] operator[] (int k) {\n", + " return t[k*m];\n", + " }\n", + "}\n", + "\n", + "matrix operator* (matrix a, matrix b) {\n", + " matrix c(a.n, b.m);\n", + " for (int i = 0; i < a.n; i++)\n", + " for (int j = 0; j < b.m; j++)\n", + " for (int k = 0; k < a.m; k++)\n", + " c[i][j] += a[i][k] * b[i][k];\n", + " return c;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Динамика\n", + "\n", + "Некоторые динамики можно выразить в терминах матричного умножения.\n", + "\n", + "Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$$\n", + "\\begin{pmatrix}\n", + "f_{n+1} \\\\\n", + "f_{n+2} \\\\\n", + "\\end{pmatrix}\n", + "=\n", + "\\begin{pmatrix}\n", + "0+f_{n+1} \\\\\n", + "f_{n}+f_{n+1} \\\\\n", + "\\end{pmatrix}\n", + "=\n", + "\\begin{pmatrix}\n", + "0 & 1 \\\\\n", + "1 & 1 \\\\\n", + "\\end{pmatrix}\n", + "\\begin{pmatrix}\n", + "f_{n} \\\\\n", + "f_{n+1} \\\\\n", + "\\end{pmatrix}\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$.\n", + "\n", + "Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:\n", + "\n", + "$$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$\n", + "\n", + "Это будет работать за $O(n^3 \\log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "matrix binpow (matrix a, int p) {\n", + " matrix b(n, n);\n", + " for (int i = 0; i < n; i++)\n", + " b[i][i] = 1;\n", + " while (p) {\n", + " if (p&1) b = b*a;\n", + " a = a*a;\n", + " p >>= 1;\n", + " }\n", + " return b;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$.\n", + "\n", + "$$\n", + "\\begin{vmatrix}\n", + "1 & 0 & 0 \\\\\n", + "0 & 1 & 0 \\\\\n", + "0 & 0 & 1 \\\\\n", + "\\end{vmatrix}\n", + "$$\n", + "\n", + "В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \\ldots + a_k f_{n-k}$ имеет такую матрицу перехода:\n", + "\n", + "\\begin{pmatrix}\n", + "0 & 1 & 0 & \\ldots & 0 \\\\\n", + "0 & 0 & 1 & \\ldots & 0 \\\\\n", + "\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\\n", + "0 & 0 & 0 & \\ldots & 1 \\\\\n", + "a_k & a_{k-1} & a_{k-2} & \\ldots & a_1 \\\\\n", + "\\end{pmatrix}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Матрица смежности\n", + "\n", + "У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов.\n", + "\n", + "Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86))." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Обобщения\n", + "\n", + "Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел.\n", + "\n", + "Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Определитель\n", + "\n", + "*Определителем* кадратной матрицы $A$ называется такое выражение:\n", + "\n", + "$$ \\det A = |A| = \\ldots $$ \n", + "\n", + "Где $\\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:\n", + "\n", + "$$\n", + "\\begin{vmatrix}\n", + "a & b \\\\\n", + "c & d \\\\\n", + "\\end{vmatrix}\n", + "=\n", + "ad - bc\n", + "$$\n", + "\n", + "У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.\n", + "\n", + "В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Базис\n", + "\n", + "Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. \n", + "\n", + "Базисы есть не только в линейной алгебре. Например, $\\{1, x, x^2\\}$ является базисом всех квадратных трёхчленов. Или $\\{\\neg, \\land, \\lor\\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Метод Крамера и easy пересечение прямых\n", + "\n", + "Пусть нам надо пересечь две прямые.\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "a_1 x + b_1 y + c_1 = 0\n", + "a_2 x + b_2 y + c_2 = 0\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Это то же самое, что найти такие коэффициенты $x$ и $y$, что\n", + "\n", + "$$ x \\vec{a} + y \\vec{b} = -\\vec{c} $$\n", + "\n", + "Площадь параллелограмма, натянутого на $\\vec{a}$ и $\\vec{b}$, равна векторному произведению, или детерминанту.\n", + "\n", + "По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$.\n", + "\n", + "Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## *Собственные векторы\n", + "\n", + "Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление.\n", + "\n", + "$ Av = k v $, где $k \\neq 0$.\n", + "\n", + "$ Av - kv = (A-kI)v = 0 $. Это означает\n", + "\n", + "Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Системы уравнений и метод Гаусса\n", + "\n", + "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", + "\n", + "Нас по сути просят решить следующую систему:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", + "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", + "\\ldots \\\\\n", + "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", + "\n", + "Метод Крамера неоптимален — там $O(n^4)$ операций.\n", + "\n", + "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "t gauss (matrix a) {\n", + " for (int i = 0; i < n; i++) {\n", + " int nonzero = i;\n", + " for (int j = i+1; j < n; j++)\n", + " if (a[j][i])\n", + " nonzero = j;\n", + " swap(a[nonzero], a[i]);\n", + " for (int j = 0; j < n; j++)\n", + " if (j != i && a[j][i])\n", + " a[j] ^= a[i];\n", + " }\n", + " t x;\n", + " for (int i = 0; i < n; i++)\n", + " x[i] = a[i][n] ^ a[i][i];\n", + " return x;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81))." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/matching-checkpoint.ipynb b/web/img/.ipynb_checkpoints/matching-checkpoint.ipynb new file mode 100644 index 0000000..e91f1a4 --- /dev/null +++ b/web/img/.ipynb_checkpoints/matching-checkpoint.ipynb @@ -0,0 +1,234 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ipi0UHuYJPXr" + }, + "source": [ + "# Паросочетания\n", + "\n", + "Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "YEm8GM0TKncl" + }, + "source": [ + "**Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$).\n", + "\n", + "Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием.\n", + "\n", + "*Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны.\n", + "\n", + "Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ThCPzQPwU5y0" + }, + "source": [ + "**Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер.\n", + "\n", + "**Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.\n", + "\n", + "**Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию.\n", + "\n", + "![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg)\n", + "\n", + "*Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \\to 8 \\to 4 \\to 6 \\to 3 \\to 7$.*\n", + "\n", + "Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.\n", + "\n", + "В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.\n", + "\n", + "**Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "wrrJplYwbCB8" + }, + "outputs": [], + "source": [ + "const int maxn;\n", + "\n", + "vector g[maxn]; // будем хранить только рёбра из левой доли в правую\n", + "int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой)\n", + "bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом\n", + "\n", + "// dfs возвращает, можно ли найти путь из вершины v\n", + "// в какую-нибудь вершину правой доли\n", + "// если можно, то ещё и проводит чередование\n", + "bool dfs (int v) {\n", + " if (used[v])\n", + " return false;\n", + " used[v] = true;\n", + " for (int u : g[v]) {\n", + " // если вершина свободна, то можно сразу с ней соединиться\n", + " // если она занята, то с нейможно соединиться только тогда,\n", + " // когда из её текущей пары можно найти какую-нибудь другую вершину\n", + " if (mt[u] == -1 || dfs(mt[u])) {\n", + " mt[u] = v;\n", + " return true;\n", + " }\n", + " }\n", + " return false;\n", + "}\n", + "\n", + "\n", + "// где-то в main:\n", + "\n", + "memset(mt, -1, sizeof(mt));\n", + "for (int i = 0; i < n; i++) {\n", + " memset(used, 0, sizeof(mt));\n", + " if (dfs(i))\n", + " cnt++;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fm3P_M7fKdRQ" + }, + "source": [ + "## Корректность\n", + "\n", + "Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.\n", + "\n", + "**Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным.\n", + "\n", + "**Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \\leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу.\n", + "\n", + "Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hVHj5qr1czGd" + }, + "source": [ + "## Скорость работы\n", + "\n", + "Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$.\n", + " \n", + "Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \\approx 10^4$), потому что для него есть мощные неасимптотические оптимизации:\n", + "\n", + "* Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).\n", + "\n", + "* Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу.\n", + "\n", + "* Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \\times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.\n", + " \n", + "Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \\sqrt m)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Покрытие путями DAG-а\n", + "\n", + "Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.\n", + "\n", + "Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\\overline{H}$ следующим образом:\n", + "* В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно.\n", + "* Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$.\n", + "* Теперь из графа $H$ сделаем граф $\\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$.\n", + "\n", + "Если мы рассмотрим любой путь $v_1, v_2, \\ldots, v_k$ в исходном графе $G$, то в графе $\\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \\ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$.\n", + "\n", + "Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\\overline{H}$ — это паросочетание в $H$ (напомним, это $\\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$.\n", + "\n", + "Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них.\n", + "\n", + "Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "B_YEyFpNUS36" + }, + "source": [ + "## Лемма Холла\n", + "\n", + "*Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.\n", + "\n", + "![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif)\n", + "\n", + "**Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.\n", + "\n", + "**Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием.\n", + "\n", + "В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.\n", + "\n", + "**База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. \n", + "\n", + "**Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием.\n", + "\n", + "Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)\n", + "\n", + "Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \\leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$).\n", + "\n", + "Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yq3YRWxFJWaY" + }, + "source": [ + "## Для ноулайферов: матроиды\n", + "\n", + "С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid).\n", + "\n", + "Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*.\n", + "\n", + "**Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "21_matching.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb b/web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb new file mode 100644 index 0000000..43ba14e --- /dev/null +++ b/web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb @@ -0,0 +1,178 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Матроиды" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства:\n", + "\n", + "* Пустое множество является независимым: $\\varnothing \\in I$\n", + "\n", + "* Любое подмножество независимого множества тоже независимо: \n", + "$$A \\subset B, B \\in I \\implies A \\in I$$\n", + "\n", + "* Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$:\n", + "$$A, B \\in I, |A| < |B| \\implies \\exists x \\in B \\setminus A: A \\cup \\{x\\} \\in I$$\n", + "\n", + "Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \\sum w(a_i)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "X.sort()\n", + "s = []\n", + "for x in X:\n", + " if good(s + [x]):\n", + " s += [x]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Здесь под `good` имеется в виду $s \\cup x \\in I$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Корректность этого алгоритма для любого матроида доказывает следующая теорема:\n", + "\n", + "## Теорема Радо-Эдмондса\n", + "\n", + "Пусть $A \\in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \\cup x \\in I,\\;x \\notin A,\\;w(x)$ — минимальна. Тогда $A \\cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "*Доказательство: **\n", + "\n", + "Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$.\n", + "\n", + "Из свойств матроида: $\\exists y \\in B \\setminus A : A \\cup y \\in I$.\n", + "\n", + "Тогда верны два неравенства:\n", + "\n", + "$$\n", + "\\begin{cases}\n", + "w(A \\cup y) = w(A) + w(y) \\geq w(B) \\implies w(A) \\geq w(B) - w(y) \\\\\n", + "w(B \\setminus y) = w(B) - w(y) \\geq w(A) \\implies w(A) \\leq w(B) - w(y)\n", + "\\end{cases}\n", + "$$\n", + "\n", + "Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно,\n", + "$w(A \\cup y) = w(A) + w(y) = w(B)$.\n", + "\n", + "Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \\cup x \\in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Примеры\n", + "\n", + "Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.\n", + "\n", + "### Минимальный остов\n", + "Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом:\n", + "\n", + "* Граф без ребер является лесом.\n", + "* Если удалить из леса ребра, он останется лесом.\n", + "* Пусть есть два леса $|A| \\leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \\cup \\{x\\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности.\n", + "\n", + "Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.\n", + "\n", + "### Расписания\n", + "Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.\n", + "\n", + "Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \\geq i$ для всех $i$.\n", + "\n", + "Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: \n", + "\n", + "* Пустой набор заданий всегда можно сделать.\n", + "* Если у нас стало меньше заданий, то их сделать мы тоже успеем.\n", + "* Пусть есть два правильных набора $|A| \\leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \\cup x$ — тоже правильный набор.\n", + "\n", + "Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных.\n", + "\n", + "### Паросочетания\n", + "Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом:\n", + "\n", + "* Любое паросочетание покрывает пустое множество вершин.\n", + "* Исходное паросочетание покрывает также и любое подмножество исходных вершин.\n", + "* Пусть есть два множества вершин $|A| \\leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$.\n", + "\n", + "Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна.\n", + "\n", + "### Линейно независимые вектора\n", + "\n", + "(TODO) (Школьники не обязаны знать линал.)\n", + "\n", + "Такие штуки будем называть базисами.\n", + "\n", + "* Ноль есть в любом базисе.\n", + "* Подмножество базиса — базис.\n", + "* ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb b/web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb new file mode 100644 index 0000000..b8487b7 --- /dev/null +++ b/web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb @@ -0,0 +1,239 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Поток минимальной стоимости\n", + "\n", + "Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна:\n", + "\n", + "$$ \\sum_{(u, v) \\in E} f_{uv} \\to \\max $$\n", + "$$ \\sum_{(u, v) \\in E} f_{uv} w_{uv} \\to \\min $$\n", + "\n", + "Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Критерий оптимальности\n", + "\n", + "Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот).\n", + "\n", + "**Доказательство:** \n", + "\n", + "$\\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению.\n", + "\n", + "$\\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Отмена циклов\n", + "\n", + "Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу.\n", + "\n", + "Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Дополняющие пути\n", + "\n", + "Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.\n", + "\n", + "**Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса.\n", + "\n", + "Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз.\n", + "\n", + "Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Потенциалы Джонсона\n", + "\n", + "Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:\n", + "\n", + "$$ w_{uv}' = w_{uv} + d_u - d_v $$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение 1**. Веса всех рёбер графа неотрицательные.\n", + "\n", + "**Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$?\n", + "\n", + "Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой:\n", + "$$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими.\n", + "\n", + "**Доказательство**. Распишем новую стоимость пути из $a$ в $z$.\n", + "\n", + "$$\n", + "\\begin{align}\n", + "w_{ab}' + \\ldots + w_{yz}'\n", + "&= (w_{ab} + \\ldots + w_{yz}) + (d_a + \\ldots + d_y) - (d_b + \\ldots + d_z)\n", + "\\\\&= (w_{ab} + \\ldots + w_{yz}) + d_a - d_z\n", + "\\end{align}\n", + "$$\n", + "\n", + "Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу.\n", + "\n", + "Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.\n", + "\n", + "Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими).\n", + "\n", + "**Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Итоговый алгоритм\n", + "\n", + "* Модифицируем сеть, добавивив обратные рёбра.\n", + "* Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.\n", + "* Пока максимальный поток не найден:\n", + "* * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$.\n", + "* * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \\leadsto t$, обновить остаточную сеть." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Асимптотика\n", + "\n", + "Алгоритм работает за $O(U m \\log n)$ или $O(U n^2)$ в случае плотных графов.\n", + "\n", + "В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \\leq n$ или $U \\leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Реализация\n", + "\n", + "Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).\n", + "\n", + "* `cost`, `cap` — параметры сети\n", + "* `pot` — потенциалы\n", + "* `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)\n", + "* `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "const int maxn = 305, inf = 1e9;\n", + "\n", + "int n;\n", + "int cost[maxn][maxn], cap[maxn][maxn];\n", + "int d[maxn], pot[maxn], par[maxn];\n", + "\n", + "bool dijkstra (int s, int t) {\n", + " used[maxn] = {0};\n", + "\n", + " fill(d, d+n, inf);\n", + " d[s] = 0;\n", + "\n", + " while (1) {\n", + " int v = -1;\n", + " for (int u = 0; u < n; u++)\n", + " if (!used[u] && (v == -1 && d[u] < d[v]))\n", + " v = u;\n", + " if (v == -1 || d[v] == inf)\n", + " break;\n", + " used[v] = 1;\n", + " for (int u = 0; u < n; u++) {\n", + " int w = cost[v][u] + pot[v] - pot[u];\n", + " if (cap[v][u] && d[u] > d[v] + w) {\n", + " d[u] = d[v] + w;\n", + " par[u] = v;\n", + " }\n", + " }\n", + " }\n", + "\n", + " return d[t] < inf;\n", + "}\n", + "\n", + "int mincost_maxflow (int s, int t) {\n", + " int ans = 0;\n", + " while (dijkstra(s, t)) {\n", + " memcpy(pot, d, sizeof(d));\n", + " int delta = inf;\n", + " for (int v = t; v != s; v = par[v])\n", + " delta = min(delta, cap[par[v]][v]);\n", + " for (int v = t; v != s; v = par[v]) {\n", + " cap[par[v]][v] -= delta;\n", + " cap[v][par[v]] += delta;\n", + " ans += cost[par[v]][v]*delta;\n", + " }\n", + " }\n", + " return ans;\n", + "}" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb b/web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb new file mode 100644 index 0000000..a1dfb3b --- /dev/null +++ b/web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb @@ -0,0 +1,680 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Nyg7f6SD69cK" + }, + "source": [ + "# Дерево отрезков" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "bYsinHWyrf6J" + }, + "source": [ + "**Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "LQzmaXb_G-r4" + }, + "source": [ + "**Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "UmP1mb6M69cM" + }, + "source": [ + "Рассмотрим конкретную задачу:\n", + "\n", + ">Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов:\n", + "\n", + ">1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`).\n", + ">2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$.\n", + "\n", + ">Оба запроса нужно обрабатывать за время $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "CG0jO6Nt69cN" + }, + "source": [ + "Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:\n", + "\n", + "Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.\n", + "\n", + "Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков»." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "f8HIyZkR69cO" + }, + "source": [ + "![alt text](http://i.imgur.com/GGBmcEP.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "084RAPPptpaJ" + }, + "source": [ + "Строить его можно рекурсивной функцией:\n", + "* Если вершина является листом, взять в качестве суммы значение соответствующей ячейки.\n", + "* Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "NNMuiaKW69cQ" + }, + "source": [ + "## Разные свойства\n", + "\n", + "Высота такого дерева есть величина $\\Theta(\\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики.\n", + "\n", + "Более того, любой полуинтервал разбивается на $O(\\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков.\n", + "\n", + "Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \\frac{n}{2} + \\frac{n}{4} + \\frac{n}{8} + \\ldots + 1 < 2n$. Значит, оно линейное по памяти.\n", + "\n", + "При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "NQDy4Wn_69cT" + }, + "source": [ + "## Ок, как это нам поможет?\n", + "\n", + "Опишем теперь, как с помощью такой структуры решить задачу.\n", + "\n", + "**Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$.\n", + "\n", + "Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\\Theta(\\log n)$ — по одной с каждого уровня.\n", + "\n", + "Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков.\n", + "\n", + "**Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения.\n", + "\n", + "Сделаем тоже рекурсивную функцию, рассмотрев три случая:\n", + "\n", + "* Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму.\n", + "* Если отрезки вершины и запроса не пересекаются, то вернуть 0.\n", + "* Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей.\n", + "\n", + "Чтобы разобраться, почему это работает за $O(\\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\\log n)$ отрезках, а значит и итоговая асимптотика будет такая же." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fJ3qz5gzQYHO" + }, + "source": [ + "## Ликбез по C++\n", + "\n", + "Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества.\n", + "\n", + "Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HPeG6w4DRHG4" + }, + "source": [ + "Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\\log n)$.\n", + "\n", + "В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hWKKu4mgQ4kA" + }, + "source": [ + "У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "CodmY2a4QmjN" + }, + "outputs": [], + "source": [ + "struct A {\n", + " int param1, param2; // тут можно что-то хранить\n", + " char param3 = 'k';\n", + " A (int var) {\n", + " // эта часть называется конструктором\n", + " // ...\n", + " }\n", + " void do_something () {\n", + " // это какой-то другой метод\n", + " // ...\n", + " }\n", + "}; // <- не забудьте точку с запятой" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6xCBNd6TR1vD" + }, + "source": [ + "Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве.\n", + "\n", + "Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "dHxKkzeLsaLW" + }, + "outputs": [], + "source": [ + "A x(179);\n", + "x.do_something();\n", + "x.param1 = 57;\n", + "\n", + "A *y = new A(42); // new возвращает адрес, по которому можно найти объект\n", + "y->do_something();\n", + "y.param3 = '!';" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "6xy-KwmI2DiJ" + }, + "source": [ + "Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Z3VyDnQ769cU" + }, + "source": [ + "## Реализация" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "EKO6Hwdl69cV" + }, + "source": [ + "Общий план реализации любых структур данных:\n", + "1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc.\n", + "2. Формально описать, что должны делать методы и за какую асимптотику.\n", + "3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "x_z_73w-69cW" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb; // левые и правые границы отрезков\n", + " int sum = 0; // сумма на текущем отрезке\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " if (lb + 1 < rb) {\n", + " // если не лист, создаем детей\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void add (int k, int x) {\n", + " sum += x;\n", + " if (l) {\n", + " if (k < l->rb)\n", + " l->add(k, x);\n", + " else\n", + " r->add(k, x);\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " if (lb >= lq && rb <= rq)\n", + " // если мы лежим полностью в отрезке запроса, вывести сумму\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " // если мы не пересекаемся с отрезком запроса, вывести ноль\n", + " return 0;\n", + " // иначе всё сложно -- запускаемся от детей и пусть они там сами решают\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "30NQsY2jytu2" + }, + "source": [ + "> Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$).\n", + "\n", + "Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи:\n", + "* Запросим сумму от $k$ до $n$ в ДО.\n", + "* Добавим единичку в $k$-тую позицию в ДО.\n", + "\n", + "Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \\log n)$ запросов." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "S8DH5Zxg3UqU" + }, + "source": [ + "> Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике.\n", + "\n", + "Ответим на все запросы в оффлайн, используя метод сканирующей прямой:\n", + "\n", + "* Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \\times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \\times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \\times [y_1, y_2]$.\n", + "* Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше.\n", + "* Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$».\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "47Fb26oz69ca" + }, + "source": [ + "## Отложенные операции" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "tANMKBYT5CVZ" + }, + "source": [ + "Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному.\n", + "\n", + "Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем.\n", + "\n", + "Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено.\n", + "\n", + "Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.\n", + "\n", + "Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "eHe1O2d769cc" + }, + "source": [ + "При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "rJo8yvgHUPHL" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb;\n", + " int sum = 0, assign = -1;\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " if (lb + 1 < rb) {\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void push () {\n", + " if (assign != -1) {\n", + " sum = (rb-lb) * assign;\n", + " if (l) { // если дети есть\n", + " l->assign = assign;\n", + " r->assign = assign;\n", + " }\n", + " }\n", + " assign = -1;\n", + " }\n", + " void upd (int lq, int rq, int x) {\n", + " push();\n", + " if (lq <= lb && rb <= rq)\n", + " assign = x;\n", + " else if (l && max(lb, lq) < min(rb, rq)) {\n", + " // если есть дети и отрезок запроса хоть как-то пересекается с нашим\n", + " l->upd(lq, rq, x);\n", + " r->upd(lq, rq, x);\n", + " // ...дальше они сами разберутся\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " push();\n", + " if (lb >= lq && rb <= rq)\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " return 0;\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "_DmNvAVzMzq4" + }, + "source": [ + "По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах.\n", + "\n", + "Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "oGLSfyMG69cd" + }, + "source": [ + "## Динамическое построение" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "fZM4fTzu69cd" + }, + "source": [ + "А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\\log_2 10^6 \\approx 20$, $\\log_2 10^9 \\approx 30$), кроме этапа построения.\n", + "\n", + "Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.\n", + "\n", + "Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "z8PYwWA9niAY" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb;\n", + " int sum = 0;\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " // а тут ничего нет\n", + " }\n", + " void extend () {\n", + " if (!l && lb + 1 < rb) {\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void add (int k, int x) {\n", + " extend();\n", + " sum += x;\n", + " if (l) {\n", + " if (k < l->rb)\n", + " l->add(k, x);\n", + " else\n", + " r->add(k, x);\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " if (lb >= lq && rb <= rq)\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " return 0;\n", + " extend();\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "SSlRrVNXxREm" + }, + "source": [ + "Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "lGfAy5Af1S5J" + }, + "outputs": [], + "source": [ + "vector compress (vector a) {\n", + " vector b = a;\n", + " sort(b.begin(), b.end());\n", + " b.erase(unique(b.begin(), b.end()), b.end());\n", + " for (int &x : a) \n", + " x = int(lower_bound(b.begin(), b.end(), x) - b.begin());\n", + " return a;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "eRM_sfhirR_W" + }, + "source": [ + "## Персистентность\n", + "\n", + "Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния.\n", + "\n", + "Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.\n", + "\n", + "У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \\log n)$." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "OA93mqmdsO46" + }, + "outputs": [], + "source": [ + "struct segtree {\n", + " int lb, rb;\n", + " int sum = 0;\n", + " segtree *l = 0, *r = 0;\n", + " segtree (int _lb, int _rb) {\n", + " lb = _lb, rb = _rb;\n", + " if (lb != rb) {\n", + " int t = (lb + rb) / 2;\n", + " l = new segtree(lb, t);\n", + " r = new segtree(t, rb);\n", + " }\n", + " }\n", + " void copy () {\n", + " if (l) {\n", + " l = new segtree(l);\n", + " r = new segtree(r);\n", + " }\n", + " }\n", + " void add (int k, int x) {\n", + " copy();\n", + " sum += x;\n", + " if (l) {\n", + " if (k < l->rb) l->add(k, x);\n", + " else r->add(k, x);\n", + " }\n", + " }\n", + " int get_sum (int lq, int rq) {\n", + " // этот метод ничего не меняет -- он и так хороший\n", + " if (lq <= lb && rb <= rq)\n", + " return sum;\n", + " if (max(lb, lq) >= min(rb, rq))\n", + " return 0;\n", + " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", + " }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "kMg_NB4AsRMa" + }, + "source": [ + "> Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике.\n", + "\n", + "Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \\log n)$ памяти.\n", + "\n", + "> Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке.\n", + "\n", + "Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу.\n", + "\n", + "Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`.\n", + "\n", + "Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом.\n", + "\n", + "> Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз.\n", + "\n", + "У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\\frac{1}{2^{100}}$, и ей на практике можно пренебречь.\n", + "\n", + "Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "sUXS9Q1zQO1P" + }, + "source": [ + "## Другие реализации\n", + "\n", + "Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:\n", + "\n", + "**На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree).\n", + "\n", + "**«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051)." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "yCjbp1XhwsU2" + }, + "source": [ + "## Задачи\n", + "\n", + "* [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции.\n", + "* Второй контест — на отложенные операции.\n", + "* [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "1-2_segtree.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb b/web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb new file mode 100644 index 0000000..d7e57b7 --- /dev/null +++ b/web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb @@ -0,0 +1,119 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Разреженная таблица\n", + "\n", + "- Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \\log n)$ с малой константой.\n", + "- Обновления не поддерживает (static RMQ).\n", + "- Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.\n", + "- Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).\n", + "- Требует $O(n \\log n)$ памяти.\n", + "- Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Определим разреженную таблицу как двумерный массив размера $n \\times\\log n$:\n", + "\n", + "$$\n", + "t[i][k] = \\min \\{ a_i, a_{i+1}, \\ldots, a_{i+2^k-1} \\}\n", + "$$\n", + "\n", + "Идея такая: считаем минимум на каждом отрезке длины $2^k$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Такой массив можно посчитать за его размер: $t[i][k] = \\min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "int a[maxn], lg[maxn], mx[maxn][logn];\n", + "\n", + "int rmq (int l, int r) {\n", + " int t = lg[r-l+1];\n", + " return min(mx[l][t], mx[r-(1<= 0; i--) {\n", + " mx[i][0] = a[i];\n", + " for (int l = 0; l < logn-1; l++)\n", + " mx[i][l+1] = max(mx[i][l], mx[i+(1< Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него.\n", + "\n", + "Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \\cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго.\n", + "\n", + "Для решения этой задачи за линейное время придумали **префикс-функцию**.\n", + "\n", + "**Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \\ldots s_i$, который также является и суффиксом этой строки (не считая всю строку).\n", + "\n", + "Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 638, + "status": "ok", + "timestamp": 1541170751859, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "RUT-T92559lq", + "outputId": "b2a1115c-11ec-46c8-bc95-a62aa3458597" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 0, 1, 0, 1, 2, 3, 4]" + ] + }, + "execution_count": 7, + "metadata": { + "tags": [] + }, + "output_type": "execute_result" + } + ], + "source": [ + "def slow_prefix_function(s):\n", + " n = len(s)\n", + " p = [0]*n\n", + " for i in range(n):\n", + " prefix = s[:i]\n", + " for l in range(1, i):\n", + " if prefix[:l] == prefix[-l:]:\n", + " p[i] = l\n", + " return p\n", + "\n", + "slow_prefix_function('aataataa')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "rm6kd4D86GG3" + }, + "source": [ + "(Этот алгоритм работает за $O(n^3)$, но это только пока.)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "1-8TSS2X3KV_" + }, + "source": [ + "## Как это поможет решить исходную задачу?\n", + "\n", + "Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.\n", + "\n", + "Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\\#t$." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 74 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 615, + "status": "ok", + "timestamp": 1541171240549, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "ppqZBPNJ43v3", + "outputId": "c23a3d9f-79aa-4f91-aff1-c7b574018535" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!\n", + "00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000\n" + ] + } + ], + "source": [ + "s = \"let it go\"\n", + "t = \"\"\"let it go, let it go\n", + "can't hold it back anymore\n", + "let it go, let it go\n", + "turn away and slam the door!\"\"\"\n", + "\n", + "print((s + '#' + t).replace('\\n', ' '))\n", + "print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "42poAv3b44FM" + }, + "source": [ + "Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$.\n", + "\n", + "Такой алгоритм (посчитать префикс-функцию от $s\\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "mTIyGVuD3F8W" + }, + "source": [ + "## Как её быстро считать\n", + "\n", + "Рассмотрим ещё несколько примеров префикс-функций:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 90 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 610, + "status": "ok", + "timestamp": 1541170871458, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "GqsPPTjl75Wp", + "outputId": "2b05fe9f-5eab-44bb-e651-c207c0723ae4" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0, 0, 1, 2, 3]\n", + "[0, 0, 0, 0, 0, 0]\n", + "[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]\n", + "[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n" + ] + } + ], + "source": [ + "for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:\n", + " print(slow_prefix_function(s))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "HO_PjgzR7514" + }, + "source": [ + "Можно заметить несколько особенностей:\n", + "* $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс;\n", + "* $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \\ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \\ldots s_i$, длина которого будет ровно на единицу меньше.\n", + "\n", + "Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения.\n", + "\n", + "Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\\underbrace{aabaa}t\\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$.\n", + "\n", + "Но что происходит, когда $s_{p_i}\\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$.\n", + "* $\\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5.\n", + "* $\\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa».\n", + "* $\\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$.\n", + "* $\\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$.\n", + "\n", + "В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \\neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 + }, + "colab_type": "code", + "executionInfo": { + "elapsed": 844, + "status": "ok", + "timestamp": 1541171880825, + "user": { + "displayName": "Сергей Слотин", + "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", + "userId": "13456807618518804390" + }, + "user_tz": -180 + }, + "id": "n22PhVaYUlp4", + "outputId": "830132ae-5568-4c3d-ae32-3ecaf51e7734" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]" + ] + }, + "execution_count": 20, + "metadata": { + "tags": [] + }, + "output_type": "execute_result" + } + ], + "source": [ + "def fast_prefix_function(s):\n", + " n = len(s)\n", + " p = [0]*n\n", + " for i in range(1, n):\n", + " cur = p[i - 1]\n", + " # перебираем префикс-функцию, пока не найдем равный символ\n", + " while s[i] != s[cur] and cur > 0:\n", + " cur = p[cur - 1]\n", + " # если нашли, то значение на единицу больше\n", + " if s[i] == s[cur]:\n", + " p[i] = cur + 1\n", + " return p\n", + "\n", + "fast_prefix_function('abacabadabacabax')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "skwRhzxQUlp9" + }, + "source": [ + "### Асимптотика\n", + "\n", + "Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой.\n", + "\n", + "**Доказательство**:\n", + "* Как мы ранее заметили, префикс функция возрастает максимум на единицу.\n", + "* $\\implies$ Вырасти она может максимум $n-1$ раз.\n", + "* Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один.\n", + "* $\\implies$ Количество понижений не больше количества повышений, то есть $O(n)$.\n", + "* $\\implies$ Суммарно итераций цикла `while` — $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "cDkYM1JMUlqC" + }, + "source": [ + "## Z-функция\n", + "\n", + "Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания.\n", + "\n", + "Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$.\n", + "\n", + "$$\\underbrace{aba}c\\overbrace{aba}daba \\hspace{1em} (z_4 = 3)$$" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]\n", + "[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]\n", + "[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]\n" + ] + } + ], + "source": [ + "def slow_z_function(s):\n", + " n = len(s)\n", + " z = [0]*n\n", + " for i in range(1, n):\n", + " suffix = s[i:]\n", + " for l in range(1, i):\n", + " if s[:l] == suffix[:l]:\n", + " z[i] = l\n", + " return z\n", + "\n", + "for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:\n", + " print(slow_z_function(s))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Как её быстро считать\n", + "\n", + "Заметим, что:\n", + "* $z_0 = 0$ — из-за договоренности (потому что информации не несет);\n", + "* $z_i = 0 \\iff s_i \\neq s_0$;\n", + "* $z_i > 0 \\iff s_i = s_0$.\n", + "\n", + "Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$.\n", + "\n", + "Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее.\n", + "* Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком.\n", + "* Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "def fast_z_function(s):\n", + " n = len(s)\n", + " z = [0]*n\n", + " l = 0\n", + " r = 0\n", + " for i in range(1, n):\n", + " if i <= r:\n", + " z[i] = min(r - i + 1, z[i-l])\n", + " while i + z[i] < n and s[z[i]] == s[i+z[i]]:\n", + " z[i] += 1\n", + " if i + z[i] - 1 > r:\n", + " l = i\n", + " r = i + z[i] - 1\n", + " return z" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Зачем тогда люди используют префикс-функцию\n", + "\n", + "![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg)\n", + "\n", + "В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.\n", + "\n", + "Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение." + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "8_kmp-trie.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.1" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb b/web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb new file mode 100644 index 0000000..16bf12b --- /dev/null +++ b/web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb @@ -0,0 +1,158 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Суффиксный массив\n", + "\n", + "Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.\n", + "\n", + "Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.\n", + "\n", + "\n", + "\n", + "*
«Паблик с тупыми шутками про проганье»
*" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Мотивация\n", + "\n", + "**Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки.\n", + "\n", + "\n", + "\n", + "**Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно.\n", + "\n", + "В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс.\n", + "\n", + "Работать такой алгоритм будет за $O(|t| \\log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \\log |s|)$, что является одним из самых оптимальных алгоритмов поиска.\n", + "\n", + "Теперь научимся его строить." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Построение за $O(n \\log n)$\n", + "\n", + "Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара.\n", + "\n", + "Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \\log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \\log^2 n)$. Но это не самый быстрый и удобный алгоритм.\n", + "\n", + "Наш алгоритм будет состоять из $\\lceil \\log n \\rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив.\n", + "\n", + "Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов).\n", + "\n", + "Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4.\n", + "\n", + "$$\n", + " p_0 = (0, 1, 3, 2) \\;\\;\\; c_0 = (0, 0, 1, 0)\n", + "\\\\ p_1 = (0, 3, 1, 2) \\;\\;\\; c_1 = (0, 1, 2, 0)\n", + "\\\\ p_2 = (3, 0, 1, 2) \\;\\;\\; c_2 = (1, 2, 3, 0)\n", + "$$\n", + "\n", + "Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом.\n", + "\n", + "Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором.\n", + "\n", + "Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \\log^2 n)$, потому что каждый этап будет работать за $O(n \\log n$).\n", + "\n", + "**Оптимизация до $O(n \\log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "// строка -- это последовательность чисел от 1 до размера алфавита\n", + "vector suffix_array (vector &s) {\n", + " s.push_back(0); // добавляем нулевой символ в конец строки\n", + " int n = (int) s.size(),\n", + " cnt = 0, // вспомогательная переменная: счётчик для сортировки \n", + " cls = 0; // количество классов эквивалентности\n", + " vector c(n), p(n);\n", + " \n", + " map< int, vector > t;\n", + " for (int i = 0; i < n; i++)\n", + " t[s[i]].push_back(i);\n", + " \n", + " // «нулевой» этап\n", + " for (auto &x : t) {\n", + " for (int u : x.second)\n", + " c[u] = cls, p[cnt++] = u;\n", + " cls++;\n", + " }\n", + " \n", + " // пока все суффиксы не стали уникальными\n", + " for (int l = 1; cls < n; l++) {\n", + " vector< vector > a(cls); // массив для сортировки подсчётом\n", + " vector _c(n); // новые классы эквивалентности\n", + " int d = (1<(p.begin()+1, p.end());\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: переписать это" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Наибольшие общие префиксы\n", + "\n", + "Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами.\n", + "\n", + "TODO" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/img/.ipynb_checkpoints/treap-checkpoint.ipynb b/web/img/.ipynb_checkpoints/treap-checkpoint.ipynb new file mode 100644 index 0000000..abe71e6 --- /dev/null +++ b/web/img/.ipynb_checkpoints/treap-checkpoint.ipynb @@ -0,0 +1,956 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "dMVa59dX96P7" + }, + "source": [ + "# Декартово дерево" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ya9SKeu196QG" + }, + "source": [ + "Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века.\n", + "\n", + "Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.\n", + "\n", + "Декартово дерево же определяется и строится так:\n", + "\n", + "* Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*.\n", + "* Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*.\n", + "* От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.\n", + "* Аналогично, запустимся от правой части и добавим корню правого сына.\n", + "\n", + "Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно.\n", + "\n", + "Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "eRd8qABz96QP" + }, + "source": [ + "![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "DLS2nKEPK1lC" + }, + "source": [ + "Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий:\n", + "\n", + "* Дерамида (дерево + пирамида)\n", + "* ПиВо (пирамида + дерево)\n", + "* КуРево (куча + дерево)\n", + "* Treap (tree + heap)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "wSD-3iTFK7s8" + }, + "source": [ + " ## Бинарные деревья\n", + " \n", + " С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например:\n", + "\n", + "* добавить число $x$ в множество\n", + "* определить, есть ли в множестве число $x$\n", + "* найти первое число, не меньшее $x$ (`lower_bound`)\n", + "* найти количество чисел в промежутке $[l, r]$\n", + "\n", + "При этом все операции — за $O(\\log n)$.\n", + "\n", + "На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\\log n)$." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "2zy0SLY296QW" + }, + "source": [ + "## Приоритеты и асимптотика\n", + "\n", + "В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Ox2hsVjW96Qg" + }, + "source": [ + "**Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \\log n)$.\n", + "\n", + "Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.\n", + "\n", + "**Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*.\n", + "\n", + "Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна\n", + "\n", + "$$d_i = \\sum_{j=1}^n a(j, i)$$\n", + "\n", + "Её матожидание равно\n", + "\n", + "$$E[d_i] = E[\\sum_{j \\neq i} a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} p(j, i)$$\n", + "\n", + "где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.\n", + "\n", + "Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.\n", + "\n", + "**Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$).\n", + "\n", + "**Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. \n", + "\n", + "**Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет.\n", + "\n", + "У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей:\n", + "\n", + "$$p(x, y) = \\frac{1}{y-x+1}$$\n", + "\n", + "Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:\n", + "\n", + "$$E[d_i] = \\sum_{j \\neq i} p(j, i) = \\sum_{j \\neq i} \\frac{1}{|i-j|+1} \\leq \\sum_{i=1}^n \\frac{1}{n} = O(\\log n)$$\n", + "\n", + "Перед последним переходом мы получили сумму гармонического ряда.\n", + "\n", + "Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя.\n", + "\n", + "**Упражнение***. Выведите из этого доказательства асимптотику `quicksort`." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "qmboul4Bfkpc" + }, + "source": [ + "## Реализация" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "G0lF7L2E96Qj" + }, + "source": [ + "Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "alEQhUGK46qI" + }, + "source": [ + "Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "11VDG7yx96Qm" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " int key, prior;\n", + " Node *l = 0, *r = 0;\n", + " Node (int _key) { key = _key, prior = rand(); }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "kTPpsriu96Qx" + }, + "source": [ + "Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Vnu5kpTE96Q0" + }, + "source": [ + "### Merge\n", + "\n", + "Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.\n", + "\n", + "Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше.\n", + "\n", + "Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "1pdbdJ_W96Q2" + }, + "outputs": [], + "source": [ + "Node* merge (Node *l, Node *r) {\n", + " if (!l) return r;\n", + " if (!r) return l;\n", + " if (l->prior > r->prior) {\n", + " l->r = merge(l->r, r);\n", + " return l;\n", + " }\n", + " else {\n", + " r->l = merge(l, r->l);\n", + " return r;\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "byktXFHw96RA" + }, + "source": [ + "### Split\n", + "\n", + "Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$.\n", + "\n", + "В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "jyUv7F-o96RD" + }, + "outputs": [], + "source": [ + "typedef pair Pair;\n", + "\n", + "Pair split (Node *p, int x) {\n", + " if (!p) return {0, 0};\n", + " if (p->key <= x) {\n", + " Pair q = split(p->r, x);\n", + " p->r = q.first;\n", + " return {p, q.second};\n", + " }\n", + " else {\n", + " Pair q = split(p->l, x);\n", + " p->l = q.second;\n", + " return {q.first, p};\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "pOVCO70W96RN" + }, + "source": [ + "### Пример: вставка\n", + "\n", + "`merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. \n", + "\n", + "Вот так, например, будет выглядеть код, добавляющий $x$ в сет." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "wpMr1dwA96RQ" + }, + "outputs": [], + "source": [ + "Node *root = 0;\n", + "\n", + "void insert (int x) {\n", + " Pair q = split(root, x);\n", + " Node *t = new Node(x);\n", + " root = merge(q.first, merge(t, q.second));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "VhOc9WdYjOC0" + }, + "source": [ + "### Пример: модификация для суммы на отрезке\n", + "\n", + "Иногда нам нужно написать какие-то модификации для более продвинутых операций.\n", + "\n", + "Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке»." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "j04PUXvmun3y" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " int val, sum;\n", + " // ...\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "nIYZ3qnxvKiV" + }, + "source": [ + "При `merge` и `split` надо будет поддерживать эту сумму актуальной.\n", + "\n", + "Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "BH_-Bjc4utNt" + }, + "outputs": [], + "source": [ + "void sum (Node* v) { return v ? v->sum : 0; }\n", + "// обращаться по пустому указателю нельзя -- выдаст ошибку\n", + "\n", + "void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; }" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "iduA9cMYjyLI" + }, + "source": [ + "В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "SZod8er7wPVL" + }, + "outputs": [], + "source": [ + "Node* merge (Node *l, Node *r) {\n", + " // ...\n", + " if (...) {\n", + " l->r = merge(l->r, r);\n", + " upd(l);\n", + " return l;\n", + " }\n", + " else {\n", + " // ...\n", + " }\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "5RKCFfiWwZZc" + }, + "outputs": [], + "source": [ + "typedef pair Pair;\n", + "\n", + "Pair split (Node *p, int x) {\n", + " // ...\n", + " if (...) {\n", + " // ...\n", + " upd(p);\n", + " return {p, q.second};\n", + " }\n", + " else {\n", + " // ...\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "gRsq8Pqbvsbb" + }, + "source": [ + " Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Gox_llXkdJct" + }, + "outputs": [], + "source": [ + "int sum (int l, int r) {\n", + " Pair rq = split(root, r);\n", + " Pair lq = split(rq.first, l);\n", + " int res = sum(lr.second);\n", + " root = merge(lq.first, merge(lq.second, rq.second));\n", + " return res;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "CSPnJjIe96Rc" + }, + "source": [ + "# Неявный ключ" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "MwlB7zcTOqr-" + }, + "source": [ + "Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.\n", + "\n", + "Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.\n", + "\n", + "Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "uL8WtJztiOHy" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " int key, prior, size = 1;\n", + " // ^ размер поддерева\n", + " Node *l = 0, *r = 0;\n", + " Node (int _key) { key = _key, prior = rand(); }\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "sl4BYIseiRoq" + }, + "source": [ + "Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "c9oX5sP_kHhx" + }, + "outputs": [], + "source": [ + "int size (Node *v) { return v ? v->size : 0; }\n", + "\n", + "void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); }" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "n8r-PdHdkIRm" + }, + "source": [ + "`merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа.\n", + "\n", + "Про `split` теперь удобнее думать как \"вырежи первые `k` элементов\". " + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "G2ck_VLK96Rk" + }, + "outputs": [], + "source": [ + "typedef pair Pair;\n", + "\n", + "Pair split (Node *p, int k) {\n", + " if (!p) return {0, 0};\n", + " if (size(p->l) + 1 <= k) {\n", + " Pair q = split(p->r, k - size(p->l) - 1);\n", + " // ^ правый сын не знает количество вершин слева от него\n", + " p->r = q.first;\n", + " upd(p);\n", + " return {p, q.second};\n", + " }\n", + " else {\n", + " Pair q = split(p->l, k);\n", + " p->l = q.second;\n", + " upd(p);\n", + " return {q.first, p};\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "na66j6wGkeW1" + }, + "source": [ + "Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "PqDn4on8gx4u" + }, + "source": [ + "### Пример: ctrl+x, ctrl+v" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "tSiJl_dEjE-1" + }, + "outputs": [], + "source": [ + "Node* ctrlx (int l, int r) {\n", + " Pair q1 = split(root, r);\n", + " Pair q2 = split(q1.first, l);\n", + " root = merge(q2.first, q1.second);\n", + " return q2.second;\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "7VUUuQhpD3r0" + }, + "outputs": [], + "source": [ + "void ctrlv (Node *v, int k) {\n", + " Pair q = split(root, k);\n", + " root = merge(q.first, merge(v, q.second));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "hTmR8xpkgp3a" + }, + "source": [ + "### Пример: переворот\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "d-lA26yQZVUZ" + }, + "source": [ + "Нужно за $O(\\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д.\n", + "\n", + "Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "q7ic3tQ4jWco" + }, + "outputs": [], + "source": [ + "struct Node {\n", + " bool rev;\n", + " // ...\n", + "};" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "Bg-tJvl-jfql" + }, + "source": [ + "Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "Q7nC0YI0jeTi" + }, + "outputs": [], + "source": [ + "void push (node *v) {\n", + " if (v->rev) {\n", + " swap(v->l, v->r);\n", + " if (v->l)\n", + " v->rev ^= 1;\n", + " if (v->r)\n", + " v->rev ^= 1;\n", + " }\n", + " v->rev = 0;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "40Ir36SwZ5GN" + }, + "source": [ + "Аналогично, эту функцию будем вызывать в начале `merge` и `split`.\n", + "\n", + "Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "8Ev2xhTSjRgI" + }, + "outputs": [], + "source": [ + "void reverse (int l, int r) {\n", + " Pair q1 = split(root, r);\n", + " Pair q2 = split(q1.first, l)\n", + " q2.second->rev ^= 1;\n", + " root = merge(q2.first, merge(q2.second, q1.second));\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "9BI3binGkCmg" + }, + "source": [ + "# Функциональное программирование*\n", + "\n", + "Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно.\n", + "\n", + "Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "dIXpRozXkFVh" + }, + "outputs": [], + "source": [ + "auto apply (int l, int r, auto f) {\n", + " Pair q1 = split(root, r);\n", + " Pair q2 = split(q1.first, l)\n", + " q2.second = f(q2.second);\n", + " root = merge(q2.first, merge(q2.second, q1.second));\n", + "}\n", + "\n", + "void reverse (Node *v) {\n", + " if (v)\n", + " v->rev ^= 1;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "C8eKhyK5fnDK" + }, + "source": [ + "Применять её нужно так:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "GbQ-OEOWEQxM" + }, + "outputs": [], + "source": [ + "apply(l, r, reverse);" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "P8_dHs3BHBKo" + }, + "source": [ + "Это работает в плюсах, начиная с `g++14`.\n", + "\n", + "Для простых операций можно даже написать лямбду:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "nPmyzNjKHpNc" + }, + "outputs": [], + "source": [ + "apply(l, r, [](Node *v){\n", + " if (v)\n", + " v->rev ^= 1;\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "ZV7EMe68kpvi" + }, + "source": [ + "# Персистентность*\n", + "\n", + "Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.\n", + "\n", + "> Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.\n", + "\n", + "Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим.\n", + "\n", + "> Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**.\n", + "\n", + "Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами.\n", + "\n", + "Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "1sqJhTKQnkWY" + }, + "source": [ + "Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "vrd0UjRu1qDv" + }, + "outputs": [], + "source": [ + "Node* copy (Node *v) { return new Node(*v); }" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "L6METC4WKGbA" + }, + "source": [ + "Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так:" + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "LuFo9MChKF7b" + }, + "outputs": [], + "source": [ + "Pair split (Node *p, int x) {\n", + " p = copy(p);\n", + " // ...\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "NnMzBYiY1zD7" + }, + "source": [ + "В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.\n", + "\n", + "У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\\frac{L}{L+R}$, иначе за правое.\n", + "\n", + "**Теорема**. Такое переподвешивание эквивалентно приоритетам.\n", + "\n", + "**Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции:\n", + "\n", + "* Лист имеет вероятность 1 быть корнем себя (база индукции)\n", + "* Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\\frac{1}{L} \\cdot \\frac{L}{L+R} = \\frac{1}{L+R}$. С вершинами правого дерева аналогично.\n", + "\n", + "Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД." + ] + }, + { + "cell_type": "code", + "execution_count": 0, + "metadata": { + "colab": {}, + "colab_type": "code", + "id": "EamOdCWWKM_y" + }, + "outputs": [], + "source": [ + "Node* merge (Node *l, Node *r) {\n", + " if (!l) return r;\n", + " if (!r) return l;\n", + " l = copy(l), r = copy(r);\n", + " if (rand() % (size(l) + size(r)) < size(l)) {\n", + " // ...\n", + " }\n", + " else {\n", + " // ...\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "YcdzH43FyPDl" + }, + "source": [ + "Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$?" + ] + } + ], + "metadata": { + "colab": { + "collapsed_sections": [], + "name": "3-4_treap.ipynb", + "provenance": [], + "version": "0.3.2" + }, + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xcpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/web/img/.ipynb_checkpoints/trie-checkpoint.ipynb b/web/img/.ipynb_checkpoints/trie-checkpoint.ipynb new file mode 100644 index 0000000..4162c8e --- /dev/null +++ b/web/img/.ipynb_checkpoints/trie-checkpoint.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Бор\n", + "\n", + "Бор — это структура данных для компактного хранения строк.\n", + "\n", + "Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. \n", + "\n", + "![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png)\n", + "\n", + "Бор можно удобно использовать для разных задач:\n", + "* Хранение строк — занимает гораздо меньше места, чем массив или сет строк.\n", + "* Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.\n", + "\n", + "* Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Реализация\n", + "\n", + "Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация:\n", + "* терминальная ли вершина,\n", + "* ссылки на детей,\n", + "* возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "const int k = 26;\n", + "\n", + "struct Vertex {\n", + " Vertex* to[k] = {0};\n", + " bool terminal = 0;\n", + "};\n", + "\n", + "Vertex *root = new Vertex();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "void add_string (string &s) {\n", + " v = root;\n", + " for (char c : s) {\n", + " c -= 'a';\n", + " if (!v->to[c]) \n", + " v->to[c] = new Vertex();\n", + " v = v->to[c];\n", + " }\n", + " v->terminal = true;\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.\n", + "\n", + "Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Как хранить ссылки\n", + "\n", + "Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми.\n", + "\n", + "В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например.\n", + "\n", + "Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Суффиксные ссылки\n", + "\n", + "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", + "\n", + "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", + "\n", + "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", + "\n", + "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++17", + "language": "C++17", + "name": "xeus-cling-cpp17" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "-std=c++17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..4474553 --- /dev/null +++ b/web/index.html @@ -0,0 +1 @@ +some custom index file diff --git a/web/pandoc.css b/web/pandoc.css new file mode 100644 index 0000000..873f2c3 --- /dev/null +++ b/web/pandoc.css @@ -0,0 +1,375 @@ +/* + * Forked from https://gist.github.com/killercup/5917178 + */ + +@font-face { + font-family: 'Inconsolata'; + src: url(http://sereja.me/fonts/inconsolata.ttf); +} + +html { + font-size: 100%; + overflow-y: scroll; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + /* color: #444; */ + font-family: 'Times New Roman', Palatino, 'Palatino Linotype', Times, serif; + font-size: 12px; + line-height: 1.7; + padding: 1em; + margin: auto; + max-width: 48em; + background: #fefefe; +} + +a { + color: #0645ad; + text-decoration: none; +} + +a:visited { + color: #0b0080; +} + +a:hover { + color: #06e; +} + +a:active { + color: #faa700; +} + +a:focus { + outline: thin dotted; +} + +p { + margin: 0.8em 0; +} + +img { + max-width: 100%; +} + +h1, h2, h3, h4, h5, h6 { + color: #111; + line-height: 125%; + margin-top: 1em; + margin-bottom: 0em; + font-weight: normal; +} + +h4, h5, h6 { + font-weight: bold; +} + +h1 { + font-size: 2.5em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.5em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1em; +} + +h6 { + font-size: 0.9em; +} + +blockquote { + color: #666666; + margin: 0; + padding-left: 3em; + border-left: 0.5em #EEE solid; +} + +hr { + display: block; + height: 2px; + border: 0; + border-top: 1px solid #aaa; + border-bottom: 1px solid #eee; + margin: 1em 0; + padding: 0; +} + +pre, code, kbd, samp { + color: #000; + font-family: 'Inconsolata', monospace; +} + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +b, strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +ins { + background: #ff9; + color: #000; + text-decoration: none; +} + +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold; +} + +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +ul, ol { + margin: 1em 0; + padding: 0 0 0 2em; +} + +li p:last-child { + margin-bottom: 0; +} + +ul ul, ol ol { + margin: .3em 0; +} + +dl { + margin-bottom: 1em; +} + +dt { + font-weight: bold; + margin-bottom: .8em; +} + +dd { + margin: 0 0 .8em 2em; +} + +dd:last-child { + margin-bottom: 0; +} + +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; +} + +figure { + display: block; + text-align: center; + margin: 1em 0; +} + +figure img { + border: none; + margin: 0 auto; +} + +figcaption { + font-size: 0.8em; + font-style: italic; + margin: 0 0 .8em; +} + +table { + margin-bottom: 2em; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + border-spacing: 0; + border-collapse: collapse; +} + +table th { + padding: .2em 1em; + background-color: #eee; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} + +table td { + padding: .2em 1em; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; + vertical-align: top; +} + +.author { + display: block; + width: 100%; + text-align: right; + margin-top: -30px; + margin-bottom: 40px; + padding-right: 20px; + /* position: absolute; */ + color: #444; + font-size: 0.8em; +} + +code:not([class]) { + border: 1px solid #ddd !important; + background-color: #f8f8f8 !important; + border-radius: 3px !important; + padding-left: 2px !important; + padding-right: 2px !important; + font-size: 0.85em; +} + +pre { + border: 1px solid #ddd !important; + background-color: #f8f8f8 !important; + border-radius: 3px !important; + padding: 2px !important; + padding-left: 8px !important; + font-size: 0.9em; +} + +@media only screen and (min-width: 480px) { + body { + font-size: 14px; + } +} +@media only screen and (min-width: 768px) { + body { + font-size: 16px; + } +} +@media print { + * { + background: transparent !important; + color: black !important; + filter: none !important; + -ms-filter: none !important; + } + + body { + font-size: 12pt; + max-width: 100%; + } + + a, a:visited { + text-decoration: underline; + } + + hr { + height: 1px; + border: 0; + border-bottom: 1px solid black; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { + content: ""; + } + + pre, blockquote { + border: 1px solid #999; + padding-right: 1em; + page-break-inside: avoid; + } + + tr, img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page :left { + margin: 15mm 20mm 15mm 10mm; +} + + @page :right { + margin: 15mm 10mm 15mm 20mm; +} + + p, h2, h3 { + orphans: 3; + widows: 3; + } + + h2, h3 { + page-break-after: avoid; + } +} + +a[href^='http://sereja.me']:before { + content: url('http://sereja.me/favicon.ico'); + margin-right: 4px; +} + +a[href^='http://e-maxx.ru']:before { + content: ""; + display: inline-block; + position: relative; + top: 4px; + width: 20px; + height: 20px; + background: url('http://e-maxx.ru/favicon.ico') no-repeat; + background-size: 18px; +} + +/* https://habrahabr.ru/favicon.ico */ + +a[href^='https://neerc.ifmo.ru']:before { + content: url('https://neerc.ifmo.ru/favicon.ico'); + margin-right: 4px; +} + +a[href^='https://www.youtube.com']:before { + content: url('https://www.youtube.com/favicon.ico'); + margin-right: 4px; +} + +a[href^='https://codeforces.com']:before { + content: url('http://codeforces.com/favicon.ico'); + margin-right: 4px; +} + +a[href^='https://www.topcoder.com/']:before { + content: url('https://s3.amazonaws.com/app.topcoder.com/favicon.ico'); + margin-right: 4px; +} diff --git a/web/ru/aho-corasick.html b/web/ru/aho-corasick.html new file mode 100644 index 0000000..6458ec5 --- /dev/null +++ b/web/ru/aho-corasick.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + +

Алгоритм Ахо-Корасик

+

Пусть дан набор строк в алфавите размера \(k\) суммарной длины \(n\). Алгоритм Ахо-Корасик за \(O(nk)\) времени и памяти строит бор для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время.

+

Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик.

+

+

Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы.

+
+

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

+
+

Эту и много других задач помогают решать суффиксные ссылки. Суффиксная ссылка для вершины \(v\) — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине \(v\), и присутствующий в боре. Будем считать, что мы их умеем быстро находить.

+

Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

+

Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за \(O(nk)\), где \(n\) и \(k\) это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в отдельную статью.

+

Зачем это нужно

+
+

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

+
+

Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор.

+

Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

+

Помимо суффиксных ссылок, нужно найти ещё переходы, чтобы поддерживать самый длинный суффикс.

+

Алгоритм Ахо-Корасик*

+

Заметим, что всего суффиксных ссылок нужно найти \(O(n)\), а переходов — \(O(nk)\). Суффиксные ссылки и переходы можно быстро найти динамикой.

+

Ссылки. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход.

+

Переходы. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на одну суффиксную ссылку и взять уже посчитанный переход оттуда.

+

+

```c++ const int k = 26;

+

struct Vertex { Vertex to[k] = {0}, go[k] = {0}; Vertex link = 0, p; int pch; Vertex (int _pch, Vertex *p) { pch = pch, p = _p; } };

+

Vertex *root = new Vertex(-1, 0); ```

+

c++ void add_string (string s) { Vertex *v = root; for (char _c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(c, v); v = v->to[c]; } }

+

Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться.

+

```c++ Vertex* go (Vertex *v, int c);

+

Vertex* link (Vertex *v) { if (!v->link) { if (v == root || v->p == root) v->link = root; else v->link = go(link(v->p), v->pch); } return v->link; }

+

Vertex* go (Vertex *v, int c) { if (!v->go[c]) { if (v->to[c]) v->go[c] = v->to[c]; else if (v == root) v->go[c] = root; else v->go[c] = go(link(v), c); } return v->go[c]; } ```

+ + diff --git a/web/ru/annealing.html b/web/ru/annealing.html new file mode 100644 index 0000000..23f544d --- /dev/null +++ b/web/ru/annealing.html @@ -0,0 +1,141 @@ + + + + + + + + + + + + + +

Метод отжига

+
    +
  • Эвристический метод глобальной оптимизации
  • +
  • Работает с дискретными функциями в предположении, что у близких «точек» близкое значение, и есть что-то типа градиента
  • +
  • Очень простой в написании
  • +
  • Никаких гарантий нет, но хорошо работает на практике
  • +
  • Метод настолько мощный, что иногда им можно сдать задачи, у которых автором предполагалось адекватное решение (пример: Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам)
  • +
+
+ +
+

Постановка задачи

+

Имеется некоторая функция \(f(x)\), которую мы хотим минимизировать.

+

Наука решать эту задачу в общем случае не умеет, и вряд ли когда-либо научится.

+

TODO: описание алгоритма, физическая аналогия, конструирование гладких функций

+

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

+

Ферзи

+

Рассмотрим такую задачу: дана шахматная доска \(n \times n\) и \(n\) ферзей. Нужно расставить их так, чтобы они не били друг друга.

+

Будем кодировать состояние перестановкой чисел от \(1\) до \(n\). Такое представление кодирует не все состояния, но оно точно не учитывает те, где ферзи бьют друг друга по вертикали или горизонтали.

+

Выберем такую функцию: fitness(p) = число пар ферзей, которые бьют друг друга.

+

Важная деталь: скорость одной итерации прямо пропорционально скорости работы fitness, поэтому очень важно реализовать её эффективно. Конкретно здесь её можно реализовать за \(O(n^2)\) (используется в коде для наглядности), \(O(n)\) и даже \(O(1)\).

+

Мы будем использовать python, потому что в нём удобнее строить графики. Эквивалентный код на C++ приведена сразу после.

+
import random
+
+# Эти библиотеки будут нужны, чтобы рисовать графики:
+import matplotlib.pyplot as plt
+%matplotlib inline
+
+import seaborn as sns
+sns.set()
+
def fitness(p):
+    s = 0
+    for i in range(len(p)):
+        d = 1
+        for j in range(i):
+            if abs(i-j) == abs(p[i]-p[j]): # ферзи стоят на одной диагонали
+                d = 0
+        s += d
+    return s
+
n = 100 # кол-во ферзей
+k = 1000 # кол-во итераций
+p = list(range(n)) # исходная перестановка
+
+scores = []
+
+t = 1
+for i in range(k):
+    # уменьшаем температуру
+    t *= 0.99 # этот параметр лучше перебрать руками
+    
+    # копируем перестановку и делаем небольшое изменение
+    q = p[:]
+    a, b = random.sample(range(n), 2)
+    q[a], q[b] = q[b], q[a]
+    
+    # считаем разницу fitness и решаем, нужно ли переходить в новое состояние
+    d = fitness(q) - fitness(p)
+    if d > 0 or random.random() < math.exp(d/t):
+        p = q[:]
+    scores += [fitness(q)]
+
+print('Best score:', max(scores))
+plt.plot(scores)
+plt.show()
+
Best score: 95
+
+png

png

+
+

Примерно эквивалентный код на C++:

+
const int n = 100, k = 1000;
+
+int f(vector<int> p) {
+    int s = 0;
+    for (int i = 0; i < n; i++) {
+        int d = 1;
+        for (int j = 0; j < i; j++)
+            if abs(i-j) == abs(p[i]-p[j]):
+                d = 0;
+        s += d;
+    }
+    return s;
+}
+
+double rnd() { return double(rand()) / RAND_MAX; }
+
+int main() {
+
+    for (int i = 0; i < n; i++)
+        v[i] = i;
+
+    int ans = 1;
+
+    double t = 1;
+    for (int i = 0; i < k; i++) {
+        t *= 0.99;
+        vector<int> u = v;
+        swap(u[rand()%n], u[rand()%n]);
+        int val = f(u);
+        if (val > ans || rnd() < exp((val-ans)/t))
+            v = u, ans = val;
+        if (ans == n) break;
+    }
+
+    for (int x : v)
+        cout << x+1 << " ";
+
+    return 0;
+}
+ + diff --git a/web/ru/bayans.html b/web/ru/bayans.html new file mode 100644 index 0000000..0364d76 --- /dev/null +++ b/web/ru/bayans.html @@ -0,0 +1,191 @@ + + + + + + + + + + + + + +

Красивые идейные задачи

+

Везде, где не указано — время работы \(O(n)\), а если есть конкретные числа, то TL 1 секунда.

+

Задачи идут в порядке вспоминания, то есть в весьма рандомном.

+

Попугаи

+

Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы.

+

Минимум и максимум

+

Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый.

+

Перестановка

+

Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов.

+

Выпуклая оболочка

+

Требуется отвечать на 2 типа запросов:

+
    +
  1. Добавить точку в выпуклую оболочку.
  2. +
  3. Проверить, лежит ли точка внутри выпуклой оболочки.
  4. +
+

Обе операции онлайн за \(O(\log n)\).

+

Геометрическая прогрессия

+

Найдите способ посчитать \(\frac{1-a^n}{1-a}\) по произвольному модулю за \(O(\log n)\).

+

Покемоны

+

В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-турнир из 1024 вершин и \(1023 \times 1022 : 2\) рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из \(a \to b\) и \(b \to c\) не следует, что \(a \to c\).

+

У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш.

+

Сортировка

+

Можно ли отсортировать * 5 камней за 8 взвешиваний? * 5 камней за 7 взвешиваний? * 20 камней за 60 взвешиваний?

+

Точки в круге

+

Даны \(n\) точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за \(O(n)\) в среднем сортирует их по удаленности от начала координат.

+

Замкнутые ломаные

+

Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии?

+

Неубывающий массив

+

Дан массив из \(n\) целых чисел. Требуется за \(2n\) операций «прибавить к одному элементу любой другой» сделать его неубывающим.

+

Чётный цикл

+

Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины.

+

\(k\)-ая порядковая статистика

+

Дан массив из \(n\) целых чисел. Найдите его \(k\)-й наименьший элемент за \(O(n)\).

+

Доминирующий элемент

+

Дан массив из \(n\) элементов. Требуется ответить на \(m\) запросов, есть ли на отрезке \([l, r]\) доминирующий элемент — тот, который встречается на нём хотя бы \(\frac{r-l}{2}\) раз. Время работы \(O((n+m) \log n)\).

+

Разрушение дерева

+

Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева.

+

\(k\)-ый элемент на отрезке

+

Дан массив из \(n\) целых чисел. Требуется ответить на \(m\) запросов \(k\)-ой порядковой статистики на произвольном отрезке. Время работы \(O((n+m) \log n)\).

+

Различные числа на отрезке

+

Дан массив из \(n\) целых чисел. Требуется ответить на \(m\) запросов количества различных элементов на произвольном отрезке. Время работы \(O(m\sqrt{n})\).

+

Физкультура

+

Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится \(m\) дней. Деканат последовательно издает \(n\) приказов двух типов:

+
    +
  1. Объявить все дни с \(l\) по \(r\) выходными (физру закрывать нельзя)
  2. +
  3. Объявить все дни с \(l\) по \(r\) учебными (физру закрывать можно)
  4. +
+

При этом приказ может частично отменить действие предыдущих приказов.

+

После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика \(O(n \log n)\).

+

Нулевая сумма

+

Дано мультимножество из \(n\) целых чисел. Найдите любое его подмножество, сумма чисел которого делится на \(n\).

+

Мета-задача

+

В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу.

+

Ниточка

+

В плоскую доску вбили \(n\) гвоздей радиуса \(r\), причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления.

+

Пельмени

+

Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено \(n\) возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика \(O(n^2 \log n)\).

+

От нуля до единицы

+

Дан следующий код:

+
x = 0
+while x < 1:
+    x += random()
+

Требуется посчитать матожидание x.

+

(random в питоне возвращает случайное действительное число от 0 до 1.)

+

Площадь

+

Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%.

+

Окружности

+

Имеется окружность радиуса \(R\), назовём её внешней. Внутри неё лежит окружность радиуса \(r < R\) и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: \(k\)-я окружность должна

+
    +
  • соприкасаться с внешней,
  • +
  • соприкасаться с предыдущей ((\(k-1\))-ой),
  • +
  • иметь при этом максимальный радиус.
  • +
+

Найдите (выведите формулу за \(O(1)\)) радиус \(k\)-й такой окружности.

+

Блеф

+

Катя и Серёжа играют в игру. У Кати есть \(n\) карт, у Серёжи — \(m\). Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её особой. Цель игроков — её отгадать. Все \(n+m+1\) карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может:

+
    +
  • Попытаться угадать особую карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается.
  • +
  • Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом.
  • +
+

С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика \(O(nm)\).

+

Достижимость

+

Дан ориентированный граф без кратных рёбер. Для всех пар вершин \(u\) и \(v\) определите, можно ли дойти из \(u\) в \(v\). Вершин меньше 2000.

+

Нумизмат

+

Есть \(n\) жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть \(k\) типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой.

+

Придумайте любой полиномиальный алгоритм.

+

Принцесса

+

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

+

Она составила список из \(n\) самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством.

+

У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми.

+

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

+

Асимптотика \(O(n^2)\).

+

Спираль

+

Определим спираль \((2n+1) \times (2n+1)\) как матрицу следующего вида:

+

\[ +\begin{matrix} +21 & 22 & 23 & 24 & 25 \\ +20 & 7 & 8 & 9 & 10 \\ +19 & 6 & 1 & 2 & 11 \\ +18 & 5 & 4 & 3 & 12 \\ +17 & 16 & 15 & 14 & 13 \\ +\end{matrix} +\]

+

Ваша задача — рассчитать ответы на \(q\) запросов суммы чисел в произвольной прямоугольной области (по модулю \(10^9+7\)).

+

\(q \leq 100\), \(n \leq 10^9\).

+

Польский лабиринт

+

Группа из \(n\) туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка \((x, y)\) свободна, только если x & y == 0.

+
+ +
+

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

+

\(n \leq 10^5\), изначальные координаты туристов до \(10^9\).

+

Нимные подмножества

+

Есть множество \(A\), состоящее из \(n\) чисел от 0 до \(2^{32}-1\). Требуется выбрать его подмножество \(B \subseteq A\) максимальной суммы такое, что нельзя выбрать его подмножество \(C \subseteq B\) такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика \(O(n \log n)\).

+

Баланс степеней

+

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

+

Два пути

+

Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из \(s\) в \(t\).

+

Пьяница

+

Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью \(p\) и влево с вероятностью \(1-p\). С какой вероятностью он когда-либо окажется в точке с отрицательной координатой?

+

Ксоровый рюкзак

+

Дан массив из \(10^5\) целых чисел от \(0\) до \((2^{30}-1)\). Найти количество различных подпоследовательностей этого массива, xor-сумма которых равна заданному числу \(x\).

+

Иван Сусанин

+

Польская армия хочет добраться из поселения \(s\) в поселение \(t\). Ей руководят два гетмана — Камиль и Матеуш.

+
    +
  • Камиль руководит армией днём и водит армию по дорогам.
  • +
  • Матеуш руководит армией ночью и совершает маневры по секретным тропам.
  • +
+

Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск.

+

Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу. Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением \(t\) и войском по соответствующей карте строго уменьшилось.

+

Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из \(s\) в \(t\).

+

Варенье

+

В ряд стоят \(n\) пустых банок из-под варенья. Вместительность \(i\)-й банки равна \(v_i\) грамм.

+

Карлсон наполняет эти банки вареньем в \(m\) этапов. На каждом этапе он выбирает числа \(l\), \(r\), \(x\) и \(y\), а затем пролетает над банками с \(l\) по \(r\), выполняя следующие операции: в банку номер \(l\) он добавляет \(x\) грамм варенья, в банку номер \((l + 1)\)\((x + y)\) грамм варенья, в банку номер \((l + 2)\)\((x + 2y)\), и так далее до \(r\)-той банки, в которую он положит \(x + y(r - l)\) грамм варенья.

+

Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной.

+

\(n, m \leq 10^5\)

+

Лабиринт

+

Серёжа потерялся в лабиринте \(n \times m\). Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт.

+

Придумайте любой полиномиальный алгоритм.

+

Обезьяна

+

Дана строка из \(10^5\) символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт?

+

Ожидание минимума

+

Даны \(n\) случайных величин, равномерно распределенных на отрезках \([l_i, r_i]\) — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин.

+

Придумайте любой точный полиномиальный алгоритм.

+

Шумный ксор

+

Загадано некое число \(x\). Вы можете делать запросы следующего типа: назвать число \(y\) и получить в ответ число единичных битов в ксор-сумме \(x\), \(y\) и \(m\), где \(m\) это случайно сгенерированная маска, в которой каждый бит имеет вероятность \(p = \frac15\) быть единичным, то есть каждый бит \(x \oplus y\) заменяется на противоположный с вероятностью \(y\), и вам возвращается количество единичных битов. Для ясности:

+
x = # ...
+
+def mask(p=0.2):
+    r = 0
+    for i in range(32):
+        if random.random() < p:
+            r += 2**i
+    return r
+
+def query(y):
+    return bin(x ^ y ^ mask()).count('1')
+

Ваша задача — отгадать число, используя не более 10000 попыток.

+ + diff --git a/web/ru/bitset.html b/web/ru/bitset.html new file mode 100644 index 0000000..570ceb1 --- /dev/null +++ b/web/ru/bitset.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + +

Битовое сжатие

+
    +
  • Из-за него в «асимптотиках» появляется /64
  • +
  • На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов
  • +
  • bitset есть в stl; говорят, самописный быстрее
  • +
+

Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать & двух bool-ом и двух long-ов примерно одинаково по скорости.

+

Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию.

+

Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — bitset.

+

Работать с ним нужно вот так:

+

c++ const int lim = 1000; bitset<lim> b; // создать битсет размера lim (должно быть константой) b.set(); // заполнить единицами b.reset(); // заполнить нулями b.flip(); // заменить единички на нули и наоборот b.count(); // посчитать число единичек cout << b; // вывести битовую строку

+

Также для битсетов работает вся битовая арифметика — &, |, ^, ~, <<, >> и их варианты с [operator]=.

+

Рюкзак

+

Задача: даны \(n\) предметов с положительными целыми весами \(a_i\) и рюкзак размера \(lim\), выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака.

+

Обычно его решают так:

+

c++ bool dp[lim] = {}; // так можно его заполнить нулями dp[0] = 1; for (int i = 0; i < n; i++) for (int x = lim - a[i]; x >= 0; x--) dp[x + a[i]] |= dp[x];

+

…а с битсетом оно разгоняется так:

+

c++ bitset<lim> b; b[0] = 1; for (int i = 0; i < n; i++) b |= b << a[i];

+

Цикл длины 3

+

Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из \(n\) вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за \(O(\frac{n^3}{64})\) следующим образом:

+

c++ bitset<maxn> g[maxn]; // матрица смежности for (int a = 0; a < n; a++) { for (int b = 0; b < n; b++) { if (g[a][b] && (~g[a] & g[b]).any()) { // цикл найден } } }

+

Benchmark: на серверах CodeForces этот код при \(n = 5000\) работает за 7 секунд.

+

Перемножение матриц

+

Матрица смежности графа, возведенная в степень \(n\), имеет комбинаторный смысл: количество способов дойти из \(a\) в \(b\), используя ровно \(n\) переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через \(n\) ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения:

+

```c++ typedef bitset t; typedef array matrix;

+

matrix operator* (matrix a, matrix b) { matrix c; for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) if(a[i][j]) c[i] |= b[j]; return c; } ```

+

Гаусс

+

Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

+

Нас по сути просят решить следующую систему:

+

\[ +\begin{cases} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ +\ldots \\ +a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 +\end{cases} +\]

+

Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

+

В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

+

c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; }

+

Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

+ + diff --git a/web/ru/centroid.html b/web/ru/centroid.html new file mode 100644 index 0000000..247b2e8 --- /dev/null +++ b/web/ru/centroid.html @@ -0,0 +1,80 @@ + + + + + + + + + + + + +

Центроидная декомпозиция

+

Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».

+

Иногда вместо неё можно написать Heavy-light декомпозицию (чуть сложнее) или метод переливаний (чуть проще).

+
+ +
+

Определение. Центром или цетроидом (англ. centroid) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более \(\frac{n}{2}\).

+

Выясняется, что центр всегда существует — это следует из алгоритма его поиска:

+

```c++ int s[maxn];

+

int sizes (int v) { s[v] = 1; for (int u : g[v]) // для простоты считаем, что дерево корневое s[v] += sizes(u); return s[v]; }

+

// второй параметр -- размер дерева int centroid (int v, int n) { for (int u : g[v]) if (s[u] > n/2) return centroid(u, n); return v; } ```

+

Утверждение. centroid действительно находит цетроид.

+

Доказательство:

+
    +
  • centroid вернет вершину, у которой размеры всех детей не больше \(\frac{n}{2}\) (это явно проверяется в if-е).
  • +
  • Мы пришли в эту вершину, когда её размер был больше \(\frac{n}{2}\), а это значит, что в «обратном» направлении есть не более \(n - (\frac{n}{2}+1) = \frac{n}{2}-1\) вершин.
  • +
  • Значит, размеры всех соседей не больше половины \(n\), и алгоритм корректен.
  • +
+

Иногда центров два (пример: 1-2-3-4), тогда алгоритм вернёт «нижний» центроид.

+

Определение. Центроидной декомпозицией будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».

+

Определение. Компонентой центроида будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.

+

Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты.

+

Теперь поймём, зачем мы всё это делали.

+

Утверждение. Каждая вершина входит в \(O(\log n)\) компонент.

+

Доказательство. Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более \(\lceil \log_2 n \rceil\) разделений.

+

Следствие. Центроидная декомпозиция (см. определение выше) работает за \(O(n \log n)\).

+

Утверждение. Для любого пути \(a \leadsto b\) есть единственный центроид \(c\), в чьей компоненте были и \(a\), и \(b\).

+

Доказательство. Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила \(a\) и \(b\).

+

Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний.

+

Подсчет путей с заданным свойством

+

Рассмотрим конкретный пример: подсчёт путей заданной длины.

+

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

+

Количество таких путей можно посчитать за размер текущей компоненты: заведём массив d, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей dfs, который будет возвращать временный массив t — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению x в нём и добавить к ответу d[l-x], а затем добавить все значения из t в d. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз.

+

```c++ int l = 179; // нужная нам длина int ans = 0;

+

// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена bool used[maxn]; int s[maxn]; // размеры поддеревьев

+

void sizes (int v, int p) { s[v] = 1; for (int u : g[v]) if (u != p && !used[u]) sizes(u, v), s[v] += s[u]; }

+

int centroid (int v, int p, int n) { for (int u : g[v]) if (u != p && !used[u] && s[u] > n/2) return centroid(u, v, n); return v; }

+

// записывает в t[] глубины вершин void dfs (int v, int p, int d, vector &t) { t.push_back(d); for (int u : g[v]) if (u != p && !used[u]) dfs(u, v, d + 1, t); }

+

void solve (int v) { /* <единственный зависящий от конкретной задачи код> / size(v); vector d(s[v], 0); d[0] = 1; for (int u : g[v]) { if (!used[u]) { vector t; dfs(u, v, 1, t); for (int x : t) if (x <= l) ans += d[l-x]; for (int x : t) d[x]++; } } / */

+
used[v] = 1;
+for (int u : g[v])
+    if (!used[u])
+        solve(centroid(u, v, s[u]/2));
+

} ```

+

Асимптотика \(O(n \log n)\), потому что на каждую из \(O(n)\) верщин мы потратим \(O(1)\) операций на каждом из \(O(\log n)\) «уровней» центроидной декомпозиции.

+

Запросы на путях — offline

+

Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.

+

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

+

Например, при запросах суммы на пути, мы можем насчитать во внутреннем dfs для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины.

+

Таким образом, каждый запрос будет просмотрен \(O(\log n)\) раз, пока не будет удален, и асимптотика составит \(O(q \log n + n \log n)\).

+

Запросы на путях — online

+

Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.

+

Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив centroid[][] размера \(n \times \log n\), в котором для каждой вершины будем хранить \(O(\log n)\) центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.

+

Тогда, при ответе на запрос, мы за \(O(\log n)\) или даже \(O(\log \log n)\) операций находим центроид на нужном нам пути (первые сколько-то значений centroid[v] и centroid[u] будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида.

+

Асимптотика при более долгих пересчётах

+

TODO: сослаться на мастер-теорему

+

Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за \(O(n \log n)\) (например, где-то использовав set). Сильно ли это хуже по времени?

+

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

+

+

Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:

+

\[ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} +\leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n += n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} +< 2 n \log n += O(n \log n) \]

+

Получается, что мы можем «мердж» в центроидной декомпозиции писать и за \(O(n \log n)\) — как, впрочем, и во всех остальных «разделяйках».

+ + diff --git a/web/ru/convex-hulls.html b/web/ru/convex-hulls.html new file mode 100644 index 0000000..3040053 --- /dev/null +++ b/web/ru/convex-hulls.html @@ -0,0 +1,175 @@ + + + + + + + + + + + + +

Задачи на многоугольники

+

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

+
+alt text

alt text

+
+

Выпуклые оболочки

+

Выпуклое множество - такое множество точек, что все точки отрезка, образуемого любыми двумя точками данного множества, также принадлежат данному множеству

+

Выпуклая оболочка фигуры - такое выпуклое множество точек, что все точки фигуры также лежат в нем.

+

Минимальная выпуклая оболочка фигуры - это минимальная по площади выпуклая оболочка.

+
+alt text

alt text

+
+
+alt text

alt text

+
+
+alt text

alt text

+
+

Дано множество точек, требуется построить его минимальную выпуклую оболочку :

+

Построение за \(O(nh)\)

+

Алгоритм Джарвиса(метод заворачивания подарка)

+

Давайте выберем какую-то точку, которая гарантированно попадет в минимальную выпуклую оболочкуо, например обычно берут нижнюю и если таких несколько, то самую левую из них. Теперь давайте по одной набирать точки, как бы заворачивая нашу выпуклую оболочку(отсюда и название). Как же нам найти следующую точку в выпуклую оболочку, давайте пройдемся по точкам, которые мы еще не взяли в МВО и среди них выберем с минимальным полярным углом.

+
+alt text

alt text

+
+

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

+

Асимптотика : для каждой точки выпуклой оболочки мы из всех оставшихся точек будем искать оптимальную - что будет работать за h(размер выпуклой оболочки) * n

+

Важно помнить, что именно \(O(hn)\), а не \(O(n^2)\), так как существуют задачи на это

+
int base = 0;
+for (int i = 1; i < n; i++) {
+    if (mas[i].y < mas[base].y) {
+        base = i;
+    }
+    else if (mas[i].y == mas[base].y && mas[i].x < mas[base].x) {
+        base = i;
+    }
+}
+convex_hull.push_back(base);
+point first = mas[base];
+point cur = first;
+point prev = point(first.x - 1, first.y);
+do {
+    double minCosAngle = 1e9; // чем больше угол, тем меньше его косинус
+    double maxLen = 1e9;
+    int next = -1;
+    for (int i = 0; i < n; i++) {
+        double curCosAngle = CosAngle(prev, cur, mas[i]);
+        if (Less(curCosAngle,minCosAngle)) {//если меньше сразу меняем
+            next = i;
+            minCosAngle = curCosAngle;
+            maxLen = dist(cur, mas[i]);
+        }
+        else if (Equal(curCosAngle, minCosAngle)) {// смотрим по длине
+            double curLen = dist(cur,mas[i]);
+            if (More(curLen,maxLen)) {
+                next = i;
+                maxLen = curLen;
+            }
+        }
+    }
+    prev = cur;
+    cur = mas[next];
+    convex_hull.push_back(next);
+}
+while (cur != first);
+

Построение за \(O(n \log n)\)

+

Алгоритм Грэхема

+

Алгоритм Грэхема базируется на следующей идее: Давайте не искать следующую точку каждый раз, а сделаем так, чтобы у нас всегда была оптимальная точка и мы могли бы просто ее достать и проверить.

+

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

+

Теперь будем делать следующий алгоритм, пока все точки не будут просмотрены :

+
    +
  1. Возьмем первую из отсортированных точек.

  2. +
  3. Проверем последние три точки из взятых, если они образуют правый поворот, то удалим предпоследнюю точку

  4. +
+

Сделать это можно, например, стеком. Код есть ниже.

+
+alt text

alt text

+
+

Асимптотика : Мы просмотрим одну точку и либо удалим ее, либо оставим, то есть сам поиск МВО работает за линейное время, но мы еще делаем сортировку, а \(\rightarrow\) алгоритм работает за \(O(n\log(n))\), при этом его корректность вытекает из предыдущего алгоритма.

+

Красивая визуализация - https://visualgo.net/en/convexhull

+

красивое видео - https://www.youtube.com/watch?v=BTgjXwhoMuI .

+
struct Point {
+    int x, y;
+};
+
+Point operator -(Point a, Point b)
+{
+    return {a.x - b.x, a.y - b.y};
+}
+
+int operator * (Point a, Point b)
+{
+    return a.x * b.x + b.y * a.y;
+}
+
+int operator ^(Point a, Point b)
+{
+    return a.x * b.y - b.x * a.y;
+}
+
+bool cw(Point a, Point b, Point c)
+{
+    return (a - b) * (c - b) > 0;
+}
+
+bool ccw(Point a, Point b, Point c)
+{
+    return (a - b) * (c - b) < 0;
+}
+
+int main()
+{
+    sort(all(p2), comp);
+    vector<Point> s;
+    s.push_back(p[min_ind]);
+    for (int i = 0; i < n - 1; i++) {
+        if (p2[i].x == s[s.size() - 1].x && p2[i].y == s[s.size() - 1].y)
+            continue;
+        while (s.size() > 1 && (vect(s[s.size() - 1], s[s.size() - 2]) ^
+               vect(s[s.size() - 1], p2[i])) > 0)
+            s.pop_back();
+        s.push_back(p2[i]);
+    }
+}
+

Алгоритм Эндрю

+

Алгоритм Эндрю опирается на то, что вещественные числа не точны и предлагает поменять компаратор и строить не одну выпуклую оболочку, а две :

+

Давайте выберем самую нижнюю и самую правую точку, затем отсортируем точки по самому простому из возможных компараторов, теперь будем строить две оболочки от самой правой точки и самой левой, в итоге мы получим верхнюю и нижнюю части выпуклой оболочки

+
bool comp(Point a, Point b) {
+    if(a.x == b.x) {
+        return a.y < b.y;
+    }
+    return a.x < b.x;
+}
+
+int main() {
+    sort(all(p), comp);
+    vector<Point> up, down;
+    up.pb(p[0]);
+    down.pb(p[0]);
+    Point p1 = p[0], p2 = p.back();
+    for(int i = 1; i < n; i++) {
+        if (i == n - 1 || cw(p1, p[i], p2)) {
+            while (up.size() >= 2 && !cw(up[up.size() - 2], up[up.size() - 1], p[i])) {
+                up.pop_back();
+            }
+            up.pb(p[i]);
+        }
+        if (i == n - 1 || ccw(p1, p[i], p2)) {
+            while (down.size() >= 2 && !ccw(down[down.size() - 2], down[down.size() - 1], p[i])) {
+                down.pop_back();
+            }
+            down.pb(p[i]);
+        }
+    }
+}
+

Алгоритм Чена

+

Также существует алгоритм, объединяющий Джарвиса и Грэхема(Эндрю) и работающий за \(O(n\log(h))\), но он разбираться не будет

+

Задачи

+
    +
  1. Базовые задачи - достаточно простые, например найти длину забора, чтобы ограничить многоугольник и подобные, но есть достаточно интересные задачи, в которых выпуклая оболочка неочевидна, например следующая : Даны \(n\) пунктов в городе и \(n\) почтальонов, для каждого пункта известно расстояние от почты \(c_{i}\). Требуется каждому пункту доставить почту, \(i\)-ый почтальон просит \(a_{i}\) монет, чтобы проснуться и \(b_{i}\), чтобы проехать один километр, требуется для каждого пункта сказать, кто доставит почту наиболее выгодно. (Подсказка : \(a_{i} + b_{i} * c_{j}\) - это прямая и стоимость доставки от \(i\) почтальона к \(j\) пункту).
  2. +
+ + diff --git a/web/ru/cross-entropy.html b/web/ru/cross-entropy.html new file mode 100644 index 0000000..2d66255 --- /dev/null +++ b/web/ru/cross-entropy.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + +

Почему машинное обучение работает

+

b

+

Принцип максимального правдоподобия —

+

Функция правдоподобия

+

\[ \hat{\theta} = \argmax_\theta P(X | \theta) \]

+

Принцип максимального правдоподобия

+

Логистическая регрессия

+

Нормальное распределение и MSE

+

Почему сигмоида

+ + diff --git a/web/ru/cuda.html b/web/ru/cuda.html new file mode 100644 index 0000000..3b90d98 --- /dev/null +++ b/web/ru/cuda.html @@ -0,0 +1,316 @@ + + + + + + + + + + + + + +

Supercomputing for the Masses

+

This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in Colab or download and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: * apt-get install nvidia-cuda-dev nvidia-cuda-toolkit * pip install pycuda

+

Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work.

+

Subtlties of the Moore's law

+

Here is a graph that roughly represents what is happening in the CPU world:

+

+

Moore's law is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too.

+

You can see that around 2005 there became a shift in design .

+

The cores are more or less independent.

+

Modern GPUs appeared in early 2000s. They exploit the specific area they operate.

+

There are physical limitations to the speed of a core.

+

One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another.

+

Some of them have

+

The default free GPUs available on Google Colab are rather powerfull. Author has no idea why Google does this, but this is awesome.

+

Why multiprocessing?

+

Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound

+

There are two types of

+

General-purpose GPU

+

There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills.

+

There are several.

+

This is like with Windows and Linux.

+

We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning.

+

Heterogineous computing

+

CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs.

+

Differences from CPUs

+

Threads

+

Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive.

+

By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution.

+

In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput.

+

Memory

+

The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device?

+

You can easily dump 98% of performance of you think this way.

+

Installing PyCUDA

+

CUDA is available for many languages.

+

Nice documentation can be found here: https://documen.tician.de/pycuda/index.html

+

If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU".

+
# you may want to clear the output of this cell after installation
+from IPython.display import clear_output
+ 
+# this might take a while
+!pip install pycuda
+
+clear_output()
+
import numpy as np
+
+from pycuda.compiler import SourceModule
+import pycuda.driver as drv
+import pycuda.autoinit
+

The basics

+

Let's start with a simple example and then dive deeper.

+

Kernels

+

Just like C or C++, except that you use some custom built-in functions and specifiers.

+

CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:

+

You need to think of your computer as a heterogenious machine: there is host data and device data.

+
    +
  • You move input data to device memory.
  • +
  • You run some computation on device.
  • +
  • You retrieve back this data.
  • +
+

In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results.

+

The famous \(A + B\) problem

+

For testing and coordination with host, we will use NumPy package. If you don't have it, install it: pip install numpy.

+

NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it.

+
# lets generate our test data: two float arrays filled with something random
+a = numpy.random.randn(100).astype('float32')
+b = numpy.random.randn(100).astype('float32')
+# the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it
+
+# we need to create space where kernel should write its answers to
+dest = numpy.zeros_like(a)
+
+# this is the kernel itself
+mod = SourceModule("""
+    __global__ void add(float *dest, float *a, float *b) {
+        const int i = threadIdx.x;
+        dest[i] = a[i] + b[i];
+    }
+""")
+
+# you need to specify the source code, and PyCUDA will compile it
+add_kernel = mod.get_function("add")
+
+add_kernel(
+    drv.Out(dest),  # specifies that this memory should be accessible for writing
+    drv.In(a),  # specifies this should be accessible for reading
+    drv.In(b),
+    block=(100,1,1)  # we'll talk about it in a minute
+)
+
+assert np.allclose(dest, a + b), 'WA'  # checks that these are equal
+print('OK')
+
  File "<ipython-input-27-afc857479fe4>", line 19
+    %%time
+    ^
+SyntaxError: invalid syntax
+

Memory management

+

In CUDA C API, you need to allocate memory explicitly. So this is actually really nice.

+

There is also drv.InOut function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too.

+

Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough.

+

GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have compute capabilities (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability \(x\) is also available at later versions. These can be checked at run-time or compile-time.

+

You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications

+

Synchronization

+

Reduction is any array-wise operation.

+

Assume the following problem:

+

Dynamic programming

+

Consider the following recurrence:

+
## Problem: dynamic programming
+

Work vs. Latency

+

We actually think about both work and step complexity now.

+

Some tasks, especially in cryptography, cannot be parallelized. But some can.

+

Summing arrays in \(O(\log n)\) time

+

Assume we want to perform some associative (i. e. \(A*(B*C) = (A*B)*C\)) operation on an array of \(n\) elements. Say, sum it up.

+

Normally, we would do that with a simple loop:

+

c++ float s = 0; for (int i = 0; i < n; i++) { s += a[i]; }

+

Its computation graphs looks like this:

+

+

This is optimal in terms of work complexity, but not in terms of step complexity: it's \(O(n)\). We may want something that is a bit worse in terms of work complexity, but can be parallelized.

+

Let's try this divide-and-conquer approach:

+

+

Now it's still \(O(n)\) work complexity (you actually need exactly the same number of additions), but this is \(O(\log n)\) step complexity.

+

When you unroll the recursion from top to bottom, you will see that to get each required value,

+

+

Reducing small arrays

+
a = numpy.random.randn(2048).astype('float32')
+
+mod = SourceModule("""
+    __global__ void sum(float *dest, float *a, float *b) {
+        const int i = threadIdx.x;
+        // for l from 0 to logn:
+        //   __sync_threads()
+        //   if the thread is active
+        //     sum two elements into where they belong
+        // a[0] should containt the needed sum
+    }
+""")
+
+sum_kernel = mod.get_function("sum")
+
+add_kernel(
+    drv.InOut(a),
+    block=(1024,1,1)
+)
+
+assert np.allclose(dest, a + b), 'WA'  # checks that these are equal
+print('OK')
+

Warps and thread blocks

+

Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties.

+

+

You can actually do the same stuff with 2d and 3d indexing — weird, right?

+

Atomics

+

Reducing big arrays

+

Reducing very big arrays

+

Now, things get harder. It's time to tell how exactly GPU parallelism works.

+
+

Dense Matrix multiplication

+

Let's get to our first example where using GPUs actually makes sense: matrix multiplication.

+

Sorting

+

Our last (and hardest task) is to implement sorting.

+

You might notice that we advocated divide-and-conquer approaches most of the time.

+

It's true. They work. But we can't get an algorithm that works already.

+
# we'll use a deep learning library for benchmarking because I'm not familiar with anything else 
+import torch
+
+a = torch.randn(10**8)
+b = a.cuda()
+
# this should run for ~15 secs
+%time c = torch.sort(a)
+%time c = torch.sort(b)
+
CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s
+Wall time: 15.2 s
+CPU times: user 274 ms, sys: 237 ms, total: 511 ms
+Wall time: 511 ms
+

So, 30 times speedup. So, we now what we need to compete against.

+
b.sort()
+
(tensor([-5.4567, -5.3551, -5.3288,  ...,  5.3529,  5.4484,  5.4486],
+        device='cuda:0'),
+ tensor([55083205,  8383169, 73705953,  ..., 79814161, 50474932, 27805828],
+        device='cuda:0'))
+

There are two types of sorting algorithms: data-driven.

+

The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort.

+

+

It has \(O(\log n)\) stages, in total they have \(1 + 2 + 3 + \ldots + \log n = O(\log^2 n\) blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has \(O(n \log^ n)\) work complexity, but \(O(\log^2 n)\) step complexity, which is pretty sweet.

+

It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation:

+
def bitonic_sort(a, up=False):
+    if len(a) <= 1:
+        return a
+    else: 
+        l = bitonic_sort(x[:len(a) // 2], True)
+        r = bitonic_sort(x[len(a) // 2:], False)
+        return bitonic_merge(first + second, up)
+
+def bitonic_merge(a, up): 
+    # assume input a is bitonic, and sorted list is returned 
+    if len(a) == 1:
+        return a
+    else:
+        bitonic_compare(a, up)
+        l = bitonic_merge(a[:len(a) // 2], up)
+        r = bitonic_merge(a[len(a) // 2:], up)
+        return l + r
+
+def bitonic_compare(a, up):
+    dist = len(a) // 2
+    for i in range(dist):  
+        if (a[i] > a[i + dist]) == up:
+            a[i], a[i + dist] = a[i + dist], x[i]  # this is how swap is done in Python
+
bitonic_sort([57, 179, 42, 17, 300, 111])
+
[300, 179, 111, 57, 42, 17]
+
a = np.random.randn(10**8).astype('float32')
+
---------------------------------------------------------------------------
+
+NameError                                 Traceback (most recent call last)
+
+<ipython-input-27-58a927c14aae> in <module>()
+----> 1 a = np.random.randn(10**8).astype('float32')
+
+
+NameError: name 'np' is not defined
+

Why CUDA

+

Most of it still applicable.

+

Again, GPU programming is very specific.

+

SSE and tensor cores.

+

Kernels

+

Just like C or C++, except that you use some custom built-in functions and specifiers.

+

CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:

+

You need to think of your computer as a heterogenious machine: there is host data and device data.

+
    +
  • You move input data to device memory.
  • +
  • You run some computation on device.
  • +
  • You retrieve back this data.
  • +
+

In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results.

+

What you need to understand about GPUs is that they are extremely specialised for their applications.

+

Intrinsics for that.

+

Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision.

+

First, they introduced "multiply-accumulate" operation (e. g. x += y * z) per 1 GPU clock cycle.

+

Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell).

+

Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix).

+

This is a lot of work per

+

Well, you don't really need anything more precise than that for deep learning anyway.

+

It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices.

+

Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores".

+

So, see, this is not exactly fair comparison.

+

+* +
+You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull +
+
    +
  • +
+

down to int4 (16-valued, you heard correct)

+

You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch.

+

Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply.

+

Reducing an array

+

It seems to be simple: you just need to .

+

What actually happens when you do s += x? This is not a single operation. Actually, four things happen:

+
    +
  1. Read \(x\) into register
  2. +
  3. Read \(s\) into register
  4. +
  5. Calculate \(s + x\)
  6. +
  7. Write it back to wherever \(s\) was initially
  8. +
+

Two threads may execute it in an interleaved fashion. Say thread A could get \(s\), but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value.

+

Note: Atomics to do that

+

for small data types they are implemented on the hardware level and much more faster than that.

+

std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions.

+

Memory types

+

If all the various types of device memory were to race, here’s how the race would turn out:

+

Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible).

+
    +
  • 1st place: Register memory
    This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread.
  • +
  • 2nd place: Shared Memory
    Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should
  • +
  • 3rd place: Constant Memory
  • +
  • 4th: Texture Memory
  • +
  • Tie for last place: Local Memory and Global Memory
  • +
+

What you need to care for now is register

+

For now, you need to care about differe

+

Accessing global memory takes hundreds.

+

Problem: dense matrix multiplication

+

A lot of these are actually sparse. You can do stuff with social network graphs or web graphs.

+

Cool. But let's disapploint us for a bit:

+ + diff --git a/web/ru/dp-intro.html b/web/ru/dp-intro.html new file mode 100644 index 0000000..f879257 --- /dev/null +++ b/web/ru/dp-intro.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + +

Динамическое программирование

+
+

Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

+
+

Разберем некоторые классические идеи и задачи в динамическом программировании.

+

Рюкзак

+
+

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

+
+

Наибольшая общая подпоследовательность

+
+

Есть две строки. Найти длину их наибольшей общей подпоследовательности.

+
+

Расстановка скобок

+
+

Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

+
+

Можно завести динамику «можно ли ».

+

Наибольшая возрастающая подпоследовательность

+

Есть также алгоритм за \(O(n \log n)\).

+

Мемоизация

+

Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

+

Первый предполагает

+

Число разбиений

+
+ +
+

Замена параметра на ответ

+
+ +
+ + diff --git a/web/ru/dp-optimizations.html b/web/ru/dp-optimizations.html new file mode 100644 index 0000000..6f58d86 --- /dev/null +++ b/web/ru/dp-optimizations.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + +

Оптимизации динамики

+
    +
  • Разделяй и властвуй
  • +
  • Оптимизация Кнута
  • +
  • Convex Hull Trick
  • +
  • Лямбда-оптимизация
  • +
+

Задача

+
+

Даны \(n\) точек на прямой. Нужно найти \(m\) отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.

+
+

Базовое решение — следующая динамика: \(f[i, j]\) — минимальная стоимость покрытия \(i\) первых (самых левых) точек, используя не более \(j\) отрезков (итоговый ответ будет записан в \(f[n, m]\) — прим. К. О.).

+

Переход — перебор всех возможных последних отрезков, то есть \(f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}\).

+

```c++ // x[] — отсортированный массив координат точек, нумерация с нуля

+

// квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }

+

// TODO: предподсчитать cost

+

for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности

+

for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ```

+

Заметим, что циклы по i и j можно поменять местами.

+

Такое решение пока работает за \(O(n^2 m)\) — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить.

+

Разделяй-и-властвуй

+

Обозначим за \(opt[i, j]\) оптимальный \(k\), на котором \(f[i, j]\) минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.

+

Утверждение. \(opt[i, j] \leq opt[i, j+1]\).

+

Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.

+

Что это нам даёт? Если мы уже знаем \(opt[i, l]\) и \(opt[i, r]\) и хотим посчитать \(opt[i, j]\) для какого-то \(j\) между \(l\) и \(r\), то мы можем сузить отрезок поиска оптимального индекса с \([0, i-1]\) до \([opt[i, l], opt[i][r]]\).

+

Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка \([l, r]\), зная, что их \(opt\)-ы лежат между \(l'\) и \(r'\). Она берет середину отрезка \([l, r]\) и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно.

+

c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); }

+

Вызываться она будет просто последовательно для каждого слоя:

+

c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k);

+

Теперь пересчет одного «слоя» динамики занимает \(O(n \log n)\) вместо \(O(n^2)\). Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет \(O(\log n)\), а значит и каждый элемент будет просмотрен не более \(O(\log n)\) раз.

+

Получается, что асимптотика улучшилась до \(O(n m \log n)\).

+

Оптимизация Кнута

+

Предыдущий метод основывался на том факте, что \(opt[i, j] \leq opt[i, j+1]\). А что, если \(opt\) монотонен ещё и по первому параметру?

+

\[ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] \]

+

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

+

Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:

+

\[ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) \]

+

Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из \(opt[i, j]\) не более \(O(n)\).

+

c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } }

+

Сравните с базовым решением — всего 3 новых строчки.

+

Convex Hull Trick

+

Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на cost и увидев там скалярное произведение.

+

\[ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ +f[k, j-1] + x_{i-1}^2 +- 2x_{i-1} x_k ++ x_k^2 +\}\]

+

Посмотрим внимательнее на минимизируемое выражение. \(x_{i-1}^2\) не зависит от \(k\), значит его можно вынести. Под минимумом останется $ {ak} {bk} x_{i-1} $.

+

Это теперь можно переписать как $ k (ak, b_k) (1, X_{i-1}) $ (имеется в виду скалярное произведение)

+

Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

+

TODO: иллюстрация.

+

TODO: мем про Скуби-Ду.

+

Ли Шао

+

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

+

Лямбда-оптимизация

+

Не «фольклорное» название — дискретный метод множителей Лагранжа.

+

Пусть у нас есть функция \(f(x)\), которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .

+

Заметим следующее:

+

Давайте заменим \(f[i, j]\) на \(g_\lambda[i]\) — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный \(\lambda\), за использование отрезка.

+

Давайте сделаем бинпоиск по \(\lambda\). Понятно, что для каких-то лямбд мы будем использовать ровно \(j\) отрезков. Тогда ответ на динамику \(f[i, j] = g[i] - \lambda k\).

+

Суммируем

+

TODO: сделать табличку

+
    +
  • Разделяйка: \(O(nm \log n)\), если cost такой, что opt монотонна по одному аргументу.
  • +
  • Кнут: \(O(nm)\), если cost такой, что opt монотонна по обоим аргументам.
  • +
  • CHT: \(O(nm)\). В оптимизируемой функции нужно увидеть скалярное произведение.
  • +
  • Лагранж: \(O(n \log n)\). Функция должна быть выпуклой.
  • +
+

Другие задачи

+

Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.

+
+

Есть \(n\) ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить \(m\) амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар.

+
+ + diff --git a/web/ru/dp_intro.html b/web/ru/dp_intro.html new file mode 100644 index 0000000..4cbcdf8 --- /dev/null +++ b/web/ru/dp_intro.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + +

Динамическое программирование

+

test123

+
+

Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

+
+

Разберем некоторые классические идеи и задачи в динамическом программировании.

+

Рюкзак

+
+

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

+
+

Наибольшая общая подпоследовательность

+
+

Есть две строки. Найти длину их наибольшей общей подпоследовательности.

+
+

Расстановка скобок

+
+

Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

+
+

Можно завести динамику «можно ли ».

+

Наибольшая возрастающая подпоследовательность

+

Есть также алгоритм за \(O(n \log n)\).

+

Мемоизация

+

Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

+

Первый предполагает

+

Число разбиений

+
+ +
+

Замена параметра на ответ

+
+ +
+ + diff --git a/web/ru/dp_optimizations.html b/web/ru/dp_optimizations.html new file mode 100644 index 0000000..6f58d86 --- /dev/null +++ b/web/ru/dp_optimizations.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + +

Оптимизации динамики

+
    +
  • Разделяй и властвуй
  • +
  • Оптимизация Кнута
  • +
  • Convex Hull Trick
  • +
  • Лямбда-оптимизация
  • +
+

Задача

+
+

Даны \(n\) точек на прямой. Нужно найти \(m\) отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.

+
+

Базовое решение — следующая динамика: \(f[i, j]\) — минимальная стоимость покрытия \(i\) первых (самых левых) точек, используя не более \(j\) отрезков (итоговый ответ будет записан в \(f[n, m]\) — прим. К. О.).

+

Переход — перебор всех возможных последних отрезков, то есть \(f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}\).

+

```c++ // x[] — отсортированный массив координат точек, нумерация с нуля

+

// квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }

+

// TODO: предподсчитать cost

+

for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности

+

for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ```

+

Заметим, что циклы по i и j можно поменять местами.

+

Такое решение пока работает за \(O(n^2 m)\) — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить.

+

Разделяй-и-властвуй

+

Обозначим за \(opt[i, j]\) оптимальный \(k\), на котором \(f[i, j]\) минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.

+

Утверждение. \(opt[i, j] \leq opt[i, j+1]\).

+

Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.

+

Что это нам даёт? Если мы уже знаем \(opt[i, l]\) и \(opt[i, r]\) и хотим посчитать \(opt[i, j]\) для какого-то \(j\) между \(l\) и \(r\), то мы можем сузить отрезок поиска оптимального индекса с \([0, i-1]\) до \([opt[i, l], opt[i][r]]\).

+

Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка \([l, r]\), зная, что их \(opt\)-ы лежат между \(l'\) и \(r'\). Она берет середину отрезка \([l, r]\) и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно.

+

c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); }

+

Вызываться она будет просто последовательно для каждого слоя:

+

c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k);

+

Теперь пересчет одного «слоя» динамики занимает \(O(n \log n)\) вместо \(O(n^2)\). Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет \(O(\log n)\), а значит и каждый элемент будет просмотрен не более \(O(\log n)\) раз.

+

Получается, что асимптотика улучшилась до \(O(n m \log n)\).

+

Оптимизация Кнута

+

Предыдущий метод основывался на том факте, что \(opt[i, j] \leq opt[i, j+1]\). А что, если \(opt\) монотонен ещё и по первому параметру?

+

\[ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] \]

+

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

+

Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:

+

\[ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) \]

+

Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из \(opt[i, j]\) не более \(O(n)\).

+

c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } }

+

Сравните с базовым решением — всего 3 новых строчки.

+

Convex Hull Trick

+

Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на cost и увидев там скалярное произведение.

+

\[ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ +f[k, j-1] + x_{i-1}^2 +- 2x_{i-1} x_k ++ x_k^2 +\}\]

+

Посмотрим внимательнее на минимизируемое выражение. \(x_{i-1}^2\) не зависит от \(k\), значит его можно вынести. Под минимумом останется $ {ak} {bk} x_{i-1} $.

+

Это теперь можно переписать как $ k (ak, b_k) (1, X_{i-1}) $ (имеется в виду скалярное произведение)

+

Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

+

TODO: иллюстрация.

+

TODO: мем про Скуби-Ду.

+

Ли Шао

+

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

+

Лямбда-оптимизация

+

Не «фольклорное» название — дискретный метод множителей Лагранжа.

+

Пусть у нас есть функция \(f(x)\), которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .

+

Заметим следующее:

+

Давайте заменим \(f[i, j]\) на \(g_\lambda[i]\) — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный \(\lambda\), за использование отрезка.

+

Давайте сделаем бинпоиск по \(\lambda\). Понятно, что для каких-то лямбд мы будем использовать ровно \(j\) отрезков. Тогда ответ на динамику \(f[i, j] = g[i] - \lambda k\).

+

Суммируем

+

TODO: сделать табличку

+
    +
  • Разделяйка: \(O(nm \log n)\), если cost такой, что opt монотонна по одному аргументу.
  • +
  • Кнут: \(O(nm)\), если cost такой, что opt монотонна по обоим аргументам.
  • +
  • CHT: \(O(nm)\). В оптимизируемой функции нужно увидеть скалярное произведение.
  • +
  • Лагранж: \(O(n \log n)\). Функция должна быть выпуклой.
  • +
+

Другие задачи

+

Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.

+
+

Есть \(n\) ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить \(m\) амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар.

+
+ + diff --git a/web/ru/fenwick.html b/web/ru/fenwick.html new file mode 100644 index 0000000..6ffeaf5 --- /dev/null +++ b/web/ru/fenwick.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + +
+ НЯ!
Эта статья полна любви и обожания.
Возможно, стоит добавить ещё больше? +
+

Дерево Фенвика

+

Дерево Фенвика или бинарно индеквированное дерево (англ. binary indexed tree) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности.

+

Определение

+

Пусть дан массив \(a\) длины \(n\). Деревом Фенвика будем называть массив \(t\) той же длины, который объявим так:

+

\[ t_i = \sum_{k=F(i)}^i a_k \]

+

где \(F\) это какая-то функцию, для которой выполнено \(F(i) \leq i\). Конкретно её определим потом.

+

Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе (\(sum(l, r) = sum(r) - sum(l-1)\)), каждый из которых будем считать по этой формуле:

+

\[ sum(k) = t_k + sum(F(k)-1) \]

+

Когда мы изменяем \(k\)-ю ячейку исходного массива, мы обновляем все \(t_i\), в которых учтена эта ячейка.

+

\(F\) можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам \(t_i\) при обновлении будет будет \(O(\log n)\). Популярны две функции:

+
    +
  • \(F_1(x) =\) x & (x + 1)
  • +
  • \(F_2(x) =\) x - (x & -x) + 1
  • +
+

Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.

+

Disclaimer: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на \(O(\log n)\) отрезков вида \([F(i), i]\), и любой элемент входит в не более \(O(\log n)\) таких отрезков.

+

Реализация

+

Из-за того, что \(F(0) = 1 > 0\) и поэтому \([0, F(0)]\) не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать \(t_0\).

+

```c++ int t[maxn];

+

// возвращает сумму на префиксе int sum (int r) { int res = 0; for (; r > 0; r -= r & -r) res += t[r]; return res; }

+

int sum (int l, int r) { return sum(r) - sum(l-1); }

+

// обновляет нужные t void add (int k, int x) { for (; k <= n; k += k & -k) t[k] += x; } ```

+

Автор отмечает красивую симметрию в формулах r += r & -r и k -= k & -k, которой нет в «традиционной» версии.

+

Многомерный случай

+
+

\(k\)-мерное дерево Фенвика пишется в \((k+1)\) строчку

+
+

Нужно добавить всего одну такую же строчку в sum, add, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре.

+

sum перепишется следующим образом:

+

c++ int sum (int r1, int r2) { int res = 0; for (int i = r1; i > 0; i -= i & -i) for (int j = r2; j > 0; j -= j & -j) ans += t[i][j]; return res; }

+

В \(k\)-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно \(2^k\) запросов суммы на префиксах.

+

Если размерности больше, чем позволяет память, то можно вместо массива t использовать хэш-таблицу — так потенциально потребуется \(O(q \log^2 A)\) памяти (\(A\) — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом решил какую-то задачу на 2d-сумму с USACO 2017.

+

Бинпоиск

+

Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за \(O(\log n)\).

+

c++ // возвращает индекс, на котором сумма уже больше int lower_bound (int s) { int k = 0; for (int l = logn; l >= 0; l--) { if (k + (1<<l) <= n && t[k + (1<<l)] < s) { k += (1<<l); s -= t[k]; } } return k; }

+

Если знать, что \(F(x)\) удаляет последний бит \(x\), то принцип понятен: просто делаем спуск по бинарному дереву, как в ДО. Чем-то похоже на генерацию \(k\)-го лексикографического комбинаторного объекта: пытаемся увеличить следующий символ всегда, когда это возможно.

+

Отметим, что в «традиционной» индексации такое делать нельзя.

+

Ограничения

+

Дерево Фенвика можно использовать, когда наша операция обратима, а также когда трюк с префиксными суммами работает. Это обычно простые операции типа суммы, xor, умножения по модулю (если гарантируется, что на этот модуль ничего не делится). Минимум и gcd, отложенные операции и персистентность прикрутить в общем случае уже не получится — тогда уже нужно писать ДО.

+

Почему работает

+

Итак, мы выбрали вариант с \(F(x)\) = x - (x & -x) + 1. Поймем, что означает x & -x.

+

Лемма. x & -x возвращает последний единичный бит в двоичной записи x.

+

Доказательство потребует знания, как в компьютерах хранятся целые числа. Чтобы процессор не сжигал лишние такты, проверяя знак числа при арифметических операциях, их хранят как бы по модулю \(2^k\), а первый бит отвечает за знак (0 для положительных и 1 для отрицательных). Поэтому когда мы хотим узнать, как выглядит отрицательное число, нужно его вычесть из нуля: \(-x = 0-x = 2^k-x\).

+

Например,

+

\[ +\begin{align} + \begin{aligned} + +90 = 2+8+16+64 & = 0 \, 10110_2 \\ + -90 = 00000_2 - 10110_2 & = 1 \, 01010_2 \\ + \implies (+90) \text{ & } (-90) & = 0 \, 00010_2 \\ + \end{aligned} +\end{align} +\]

+

Вернёмся к доказательству леммы. Когда мы вычитаем, мы идем справа налево. В ответе можно мысленно разделить на три блока: * Первые сколько-то (возможно, нисколько) нулей с конца ими же и останутся. * Потом, ровно на самом младшем единичном бите, мы «займём» много единиц, так что весь префикс станет единицами. В ответе на этом месте точно будет единица. Потом отменятся ровно те из них, которые были единицами в исходном числе.

+

Делаем &: в каком-то префиксе все биты будут противоположными, младший единичный бит останется, а на суффиксе все как было нулями, так и осталось. Выживет только этот самый младший единичный бит.

+

Теперь сразу понятно, почему sum будет работать за логарифм — каждый раз мы делаем x -= x & -x, то есть удаляем один бит.

+

Теперь сложная (для понимания) часть — как делать add. Какие \(t_i\) содержат \(k\)-й элемент? Для них должно выполняться i >= k > i - (i & -i).

+

Будем перебирать префиксы TODO

+

Мы знаем, что \(t_i\) вложены друг в друга. Минимальный подходящий \(i\) равен \(k\). Какой следующий? Нам нужно для каждого \(i\) уметь находить его непосредственного родителя.

+

Можно представить дерево так: ячейка 2^k содержит все

+

TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию.

+

Название

+

Потому что \(F\) использует битовые операции, по-английски структура называется «Binary Indexed Tree».

+

Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.

+

Можно показать, что множества элементов, учтенных в \(t_i\) и \(t_j\), либо не пересекаются, либо одно является подмножеством другого. Значит, между \(t_i\) можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).

+

В частном случае, когда длина массива равна \(2^k\), то дерево будет только одно.

+ + diff --git a/web/ru/games.html b/web/ru/games.html new file mode 100644 index 0000000..5a983a6 --- /dev/null +++ b/web/ru/games.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + +

Теория игр

+

Начнём с самого баянного примера математической игры, который можно вспомнить:

+
+

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

+
+

Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет \(f_k\) — выиграет ли первый игрок, если спичек сейчас \(n\), и ход принадлежит ему. Пересчитывать её нужно так:

+

\[ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) \]

+

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

+

Примечание. Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли \(n\) на 4, но мы всё равно рассмотрим этот пример в педагогических целях.

+

Игры на графах

+

В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «\(n\) спичек».

+

Любую игру можно описать в виде графа (возможно, бесконечного) состояний игры, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как терминальные

+

Состояние называется выигрышным, если игрок, начинающий в нём, побеждает, и проигрышным в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём ничейными.

+

Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют играми с нулевой суммой (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется ретроанализом.

+

Ретроанализ

+

Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности:

+
    +
  • Вершина проигрышная — если все её переходы ведут в выигрышные вершины.
  • +
  • Вершина выигрышная — если из неё есть переход в проигрышную вершину.
  • +
  • Вершина ничейная — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные).
  • +
+

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

+

Корректность. Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин.

+

Рассмотрим граф неизвестных вершин \(U\). Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина \(v\), у которой нет исходящих рёбер в \(U\) — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины \(v\) определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние.

+

Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось.

+

Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф \(U\). В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в \(U\) — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные.

+

Асимптотика. Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали.

+
    +
  • При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф).
  • +
  • При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше.
  • +
+

Асимптотика составит \(O(n + m)\), как и у любого bfs-а.

+

```c++ vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа int cnt[maxn]; // счётчик исходящих рёбер

+

enum StatusType { win, loss, unknown }; StatusType status[maxn]; // выгрышность вершины; // по умолчанию все кроме терминальных считаются unknown // те, кто в итоге остаются unknown -- ничейные

+

queue q = {/* нужно заранее добавить сюда все терминальные вершины*/};

+

while (!q.empty()) { int v = q.front(); q.pop(); for (int u : t[v]) { cnt[u]--; // удаляем это ребро if (status[v] == unknown) { // из u есть ребро в проигрышную -- значит она выигрышная if (status[v] == loss) status[u] = win; // все ребра u ведут в выигрышные вершины -- значит она проигрышная if (status[v] == win && cnt[u] == 0) { status[u] = loss; // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь if (status[v] != unknown) q.push(u); } } } ```

+

Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации:

+

c++ StatusType dfs(int v) { if (status[v] != unknown) return status[v]; status[v] = loss; // изменим статус, когда найдём переход в проигрышную вершину for (int u : g[v]) if (dfs(u) == loss) status[v] = win; return status[v]; }

+

TODO: можно ли здесь циклы учесть?

+

Минимаксные игры

+

Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются минимаксными — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно.

+

Типичный граф минимаксной игры выглядит следующим образом:

+

+

В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников.

+

Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей.

+

Ретроанализ для больших графов

+

Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго.

+

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

+

Ретроанализ нужно оптимизировать.

+

Ограничение перебора

+

Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено \(K\) ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через \(K\) ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая численно оценивает, насколько первый игрок выигрывает.

+

Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за \(K\) ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит.

+

Ясно, что это дает существенное ускорение по времени: можно подобрать \(K\) так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли).

+

Мемоизация позиций

+

В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает .

+

Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары

+

Альфа-бета отсечение

+

+

Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым.

+

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

+

Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния \(X\) (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6.

+

Давайте зайдем в третьего сына, назовем его \(Y\), вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына \(Y\), обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына \(Y\) можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня \(X\) в вершину \(Y\), то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына \(X\) уже дает 6, что больше, и рассматривать вершину \(Y\) далее бессмысленно, мы в нее уже точно не пойдем.

+

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

+ +

Игры с ненулевой суммой

+

Теория Шпрага-Гранди

+

Рассмотрим игру «ним»: даны \(n\) кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые.

+

Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в \(i\)-й как \(a_i\). Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями.

+

Теорема. Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 a_2 a_n $ размеров кучек отлична от нуля.

+

Доказательство проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы:

+
    +
  • Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на \(a_i \oplus b_i \), где \(b_i < a_i\) — это число камней в \(i\)-й кучке после нашего действия.
  • +
  • Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой \(b_i < a_i\), что xor-сумма станет нулевой, то есть \(S \oplus a_i \oplus b_i = 0\). Для этого посмотрим на старший взведенный бит \(S\) и возьмем любой \(a_i\), у которого этот бит тоже взведен. Такой \(a_i\) найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый \(b_i\) равен \(S \oplus a_i\), и выясняется, что это корректный новый размер кучки, то есть \(b_i < a_i\). Почему так? Потому что все старшие биты в выражении остались нетронутыми, \(k\)-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем \(2^k\).
  • +
+

Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех \(a_i\), найти такой \(a_i\), у которого старший бит взведен, и заменить его на \(S \oplus a_i\).

+

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

+

Зачем это надо? Есть много игр, в которых присутствует какой-то подобный цугцванг (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен.

+

Ним с увеличениями

+

Пусть у нас

+

Эквивалентность игр ниму

+

Игры с неполной информацией

+

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

+

Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет.

+

Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло.

+

Дилемма заключенного

+

Камень-ножницы-бумага

+

Покер

+

Рассмотрим такую у

+ + diff --git a/web/ru/geometry.html b/web/ru/geometry.html new file mode 100644 index 0000000..9272cbc --- /dev/null +++ b/web/ru/geometry.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + +

Геометрия и ООП

+

Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется вектором. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали.

+
+vector

vector

+
+

Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь.

+

Скалярное произведение (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:

+

\[ a \cdot b = x_a x_b + y_a y_b \]

+
+dot

dot

+
+

У него есть полезные свойства:

+
    +
  • Скалярное произведение симметрично (\(a \cdot b = b \cdot a\)).
  • +
  • Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.
  • +
  • Перпендикулярные вектора должны иметь нулевое скалярное произведение.
  • +
  • Если угол острый, то оно положительное
  • +
  • Если угол тупой, то отрицательное
  • +
+

Векторное произведение (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:

+

\[ a \times b = x_a y_b - y_a x_b \]

+
+cross

cross

+
+

Его свойства:

+
    +
  • Скалярное произведение антисимметрично (\(a \times b = - (b \times a)\)).
  • +
  • Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.
  • +
  • Коллинеарные вектора должны иметь нулевое векторное произведение.
  • +
  • Если b «слева» от a, то оно положительное.
  • +
  • Если b «справа» — то отрицательное.
  • +
+

Всякие проверки

+

Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.

+

Принадлежность точки треугольнику. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.

+

\[ +\text{P лежит внутри ABC} \iff \begin{cases} +(B-A) \times (P-A) \geq 0 \\ +(C-B) \times (P-B) \geq 0 \\ +(A-C) \times (P-C) \geq 0 \\ +\end{cases} +\]

+

Площадь треугольника. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.

+

\[ V = \frac{1}{2} (B-A) \times (C-A) \]

+

Площадь произвольного многоугольника. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).

+
+any

any

+
+

Забудьте о формуле Герона и всегда считайте площади через векторное произведение.

+

Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.

+

Проверка на выпуклость. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то \((b-a)\times(c-a) > 0\)).

+

Пересекаются ли отрезки.

+
+segments

segments

+
+

Уравнение прямой

+

Прямая задается уравнением вида \(Ax + By + C = 0\). Полуплоскость можно задать таким же неравенством.

+

У прямой есть вектор нормали с координатами \((A, B)\). Он перпендиуклярен прямой, а в случае с полуплоскостью \(Ax + By + C \geq 0\) будет указывать в сторону самой полуплоскости.

+

Чтобы найти расстояние от точки \((x_0, y_0)\) до прямой \(Ax + By + C = 0\), можно воспользоваться следующей формулой:

+

\[ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} \]

+

Точка пересечения. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:

+

\[ +\begin{cases} +A_1 x + B_1 y + C_1 = 0 \\ +A_2 x + B_2 y + C_2 = 0 +\end{cases} +\implies +\begin{cases} +-x = \frac{B_1 y + C_1}{A_1} \\ +-x = \frac{B_2 y + C_2}{A_2} +\end{cases} +\implies +\frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} +\implies +y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} +\]

+

Аналогично, \(x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}\) (обратите внимание на знаки).

+

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

+

Как это кодить в C++

+

Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

+

Точка \(\simeq\) вектор. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её радиус-вектор — вектор из начала координат, ведущий в эту точку. По принятой в математике и физике нотации, будем обозночать вектора как r. Вы можете обозвать их как point, pt, vec — как угодно.

+

c++ struct r { double x, y; r () {} r (int _x, int _y) { x = _x, y = _y; } };

+

Функция r внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, r() вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами \((x, y)\).

+

Операции над векторами

+

Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:

+

c++ double len (r a) { return sqrt(a.x*a.x + a.y*a.y); }

+

Операторы

+

В C++ можно перегружать почти все стандартные операторы, например, +, -, << и т. д.

+

Давайте для будущих нужд определим + и -:

+

c++ r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); } r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); }

+

Скалярное произведение

+

c++ int operator*(r a, r b){ return a.x*b.x + a.y*b.y; }

+

Векторное произведение

+

Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике.

+

c++ int operator^(r a, r b){ return a.x*b.y - b.x*a.y; }

+

Ввод-вывод

+

Как вы думаете, как на самом деле работает cin >> x;? Это тоже перегрузка оператора >>. Делать это нужно так:

+

```c++ istream& operator>>(istream &in, r &p){ in >> p.x >> p.y; return in; }

+

ostream& operator<<(ostream &out, r &p){ out << p.x << " " << p.y << endl; return out;
} ```

+

Алгебра VS Алгоритмы или зачем мы всё это делали

+

Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать intersect(a, b), в корректности которого он точно уверен.

+

Векторное представление прямой (\(Ax + By + C = 0 \rightarrow r = at + b\))

+

Тут нужно просто выбрать две любые точки на прямой.

+

c++ // даны A, B, C (A^2 + B^2 != 0) r a, b; if (eq(A, 0)) // значит, это горизонтальная прямая a = r(0, -C/B), b = r(1, -C/B); else a = r(-C/A, 0), b = (1, -(C+B)/A, 1)

+

Пример: отражение от прямой

+

Пусть нам надо отразить точку \((x_0, y_0)\) симметрично относительно заданной прямой \(ax+by+c=0\). Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.

+

\(\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} \)

+

Формула имеет смысл: длина на единичный вектор направления.

+

Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить.

+

```c++ // прямая r = at + b, точка c r pr (r a, r b, r c) { c -= b; // пусть c и a выходят из одной точки return b + (ab / len(a) / len(a)) a; }

+

r reflect (r a, r b, r c) { return c + 2*(pr(a, b, c)-c); } ```

+

Типичные баги

+

Точность

+
+

Первое правило действительных чисел — не использовать действительные числа

+
+

Все переменные типа double хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно всегда учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число eps:

+

```c++ const double eps = 1e-8;

+

bool eq (double a, double b) { return abs(a-b) < eps } ```

+

Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре.

+

\(0 \neq -0\)

+

Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду.

+

Область определения обратных функций

+

acos, asin и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции.

+ + diff --git a/web/ru/hashing.html b/web/ru/hashing.html new file mode 100644 index 0000000..c003d4c --- /dev/null +++ b/web/ru/hashing.html @@ -0,0 +1,120 @@ + + + + + + + + + + + + +

Хэширование в строковых задачах

+

Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.

+

«Хорошая» хэш-функция: * Быстро считается — за линейное от размера объекта время; * Имеет не очень большие значения — влезающие в 64 бита; * «Детерминированно-случайная» — если хэш может принимать \(n\) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно \(\frac{1}{n}\).

+

Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюрьективными.

+

Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\), и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

+
+hash

hash

+
+

Применения в реальной жизни

+
    +
  • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
  • +
  • Хэш-таблица. Класс unordered_set из STL можно реализовать так: заведём \(n\) изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию \(f\) с областью значений \([0, n)\). При обработке .insert(x) мы будем добавлять элемент \(x\) в \(f(x)\)-тый список. При ответе на .find(x) мы будем проверять, лежит ли \(x\)-тый элемент в \(f(x)\)-том списке. Благодаря «равномерности» хэш-функции, после \(k\) добавлений ожидаемое количество сравнений будет равно \(\frac{k}{n}\) = \(O(1)\) при правильном выборе \(n\).
  • +
  • Мемоизация. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.
  • +
  • Проверка на изоморфизм. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.
  • +
  • Криптография. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.
  • +
  • Поиск в многомерных пространствах. Детерминированный поиск ближайшей точки среди \(m\) точек в \(n\)-мерном пространстве быстро не решается. Однако можно придумать хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши, и делать поиск только среди элементов с тем же хэшом, что у запроса.
  • +
+

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

+

Сегодня же мы остановимся на строках.

+

Полиномиальное хэширование

+

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

+

Будем считать, что строка — это последовательность чисел от \(1\) до \(m\) (размер алфавита). В C++ char это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: int x = (int) (c - 'a' + 1).

+

Определим прямой полиномиальный хэш строки как значение следующего многочлена:

+

\[ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p \]

+

Здесь \(k\) — произвольное число больше размера алфавита, а \(p\) — достаточно большой модуль, вообще говоря, не обязательно простой.

+

Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени \(k\):

+

```c++ const int k = 31, mod = 1e9+7;

+

string s = "abacabadaba"; long long h = 0, m = 1; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h + m * x) % mod; m = (m * k) % mod; } ```

+

Можем ещё определить обратный полиномиальный хэш:

+

\[ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p \]

+

Его преимущество в том, что можно написать на одну строчку кода меньше:

+

c++ long long h = 0; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h * k + x) % mod; }

+

Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой \(h\).

+

Зачем он нужен?

+

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

+

Например, если нужно посчитать хэш от конкатенации строк \(a\) и \(b\) (т. е. \(b\) приписали в конец строки \(a\)), то можно просто хэш \(b\) домножить на \(k^{|a|}\) и сложить с хэшом \(a\):

+

\[ h(ab) = h(a) + k^{|a|} \cdot h(b) \]

+

Удалить префикс строки можно так:

+

\[ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} \]

+

А суффикс — ещё проще:

+

\[ h(a) = h(ab) - k^{|a|} \cdot h(b) \]

+

В задачах нам часто понадобится домножать \(k\) в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:

+

```c++ const int maxn = 1e5+5;

+

int p[maxn]; p[0] = 1;

+

for (int i = 1; i < maxn; i++) p[i] = (p[i-1] * k) % mod; ```

+

Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:

+

```c++ int h[maxn]; h[0] = 0; // h[k] -- хэш префикса длины k

+

// будем считать, что s это уже последовательность int-ов

+

for (int i = 0; i < n; i++) h[i+1] = (h[i] + p[i] * s[i]) % mod; ```

+

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

+

\[ h(s[l:r]) = \frac{h_r-h_l}{k^l} \]

+

Деление по модулю воможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

+

Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к \(n\)-ной. Так проще — нужно будет домножать, а не делить.

+

\[ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) \]

+

c++ int hash_substring (int l, int r) { return (h[r+1] - h[l]) * p[n-l] % mod; }

+

Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за \(O(1)\).

+

Упражнение

+

Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще.

+

Примеры задач

+

Количество разных подстрок. Посчитаем хэши от всех подстрок за \(O(n^2)\) и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

+

Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

+

Сравнение строк (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.

+

Палиндромность подстроки. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений hash_substring() на первом массиве и на втором.

+

Количество палиндромов. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно.

+

Изменения строки*

+

Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в декартово дерево. В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в upd() пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.

+

Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.

+

Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова.

+

Вероятность ошибки и почему это всё вообще работает

+

У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.

+

Событие, когда два хэша совпали, а не должны, называется коллизией. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в set \(O(n^2)\) различных случайных значений в промежутке \([0, m)\). Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать \(m\), чтобы не бояться такого?

+

Выбор констант

+

Практическое правило: если вам нужно хранить \(n\) различных хэшей, то безопасный модуль — это число порядка \(10 \cdot n^2\). Обоснование — см. парадокс дней рождений.

+

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

+

Можно также брать модуль \(2^{64}\). У него есть несколько преимуществ: * Он большой — второй модуль точно не понадобится. * С ним ни о каких переполнениях заботиться не нужно — если все хранить в unsigned long long, процессор сам автоматически сделает эти взятия остатков при переполнении. * С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию %.

+

Всё с этим модулем было прекрасно, пока не придумали тест против него. Однако, его добавляют далеко не на все контесты — имейте это в виду.

+

В выборе же \(k\) ограничения не такие серьезные: * Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. * Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.

+

Главное — чтобы значения \(k\) и модуля не знал человек, который генерирует тесты.

+

Парадокс дней рождений

+
+

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

+
+

Более общее утверждение: в мультимножество нужно добавить \(\Theta(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

+

Первое доказательство (для любителей матана). Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

+

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

+

Попытаемся оценить \(f\):

+

\[ +\begin{align} + \begin{aligned} + e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ + & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ + e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ + f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ + & = e^{-\frac{n(n-1)}{2d}} & \\ + & \simeq e^{-\frac{n^2}{2d}} & \\ + \end{aligned} +\end{align} +\]

+

Из последнего выражения более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется очень быстро.

+

Второе доказательство (для любителей теорвера). Введем \(\frac{n(n-1)}{2}\) индикаторов — по одному для каждой пары людей \((i, j)\) — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна \(\frac{1}{d}\).

+

Обозначим за \(X\) число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть \(\frac{n (n-1)}{2} \cdot \frac{1}{d}\).

+

Отсюда понятно, что если \(d = \Theta(n^2)\), то ожидание равно константе, а если \(d\) асимптотически больше или меньше, то \(X\) стремится нулю или бесконечности соответственно.

+

Примечание: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1.

+

Бонус: «мета-задача»

+

Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.

+

«Решите» задачу.

+ + diff --git a/web/ru/hld.html b/web/ru/hld.html new file mode 100644 index 0000000..1153bb4 --- /dev/null +++ b/web/ru/hld.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + +

Heavy-light декомпозиция

+

HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать что-нибудь попроще.

+

+* +
+TODO: найти менее уродливую иллюстрацию +
+
    +
  • +
+

Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины \(v\) посмотрим на всех её непосредственных детей \(u\), выберем среди них ребёнка \(u_{max}\) (с самым большим размером поддерева) и назовём ребро \((v, u)\) тяжелым (heavy), а все остальные рёбра — лёгкими (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.

+

Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.

+

Утверждение. Дерево разбивается на непересекающиеся пути из тяжелых рёбер.

+

Доказательство. В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.

+

Назовём блоком либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.

+

Утверждение. На любом вертикальном пути будет не более \(O(\log n)\) блоков.

+

Доказательство разбивается на две части:

+
    +
  • Лёгких ребер на вертикальном пути будет не более \(O(\log n)\): рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего.
  • +
  • Непрерывных путей из тяжелых рёбер будет не более \(O(\log n\): если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего \(O(\log n)\).
  • +
+

Следствие. На любом пути будет не более \(O(\log n)\) блоков.

+

Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на \(O(\log n)\) запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам.

+

Реализация

+

Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.

+

А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то tin-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.

+

Теперь мы можем построить какую-нибудь структуру поверх массива размера \(n\) (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре.

+

```c++ vector g[maxn]; int s[maxn], p[maxn], tin[maxn], tout[maxn]; int head[maxn]; // «голова» тяжелого пути, которому принадлежит v int t = 0;

+

void sizes (int v = 0) { s[v] = 1; for (int &u : g[v]) { sizes(u); s[v] += s[u]; if (s[u] > s[g[v][0]]) // &u -- это ссылка, так что её легально использовать при swap-е swap(u, g[v][0]); } }

+

void hld (int v = 0) { rin[t] = v; tin[v] = t++; for (int u : g[v]) { // если это тяжелый ребенок -- его next нужно передать // в противном случае он сам является головой нового пути head[u] = (u == g[v][0] ? head[v] : u); hld(u); } tout[v] = t; } ```

+

Как им решать задачи

+

Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:

+
    +
  1. Узнать минимальное число на пути между \(v_i\) и \(u_i\).
  2. +
  3. Изменить число у \(v_i\)-той вершины на \(x_i\).
  4. +
+

Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в соответствующей статье и имеет методы upd(k, x) и get_min(l, r).

+

c++ int val[maxn]; segtree st(0, n);

+

При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:

+

c++ void upd (int v, int x) { st.upd(tin[v], x); }

+

Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям.

+

```c++ int ancestor (int a, int b) { return tin[a] <= tin[b] && tin[b] <= tout[a]; }

+

void up (int &a, int &b, int &ans) { while (!ancestor(head[a], b)) { ans = min(ans, st.get_min(tin[head[a]], tin[a])); a = p[head[a]]; } }

+

int get_min (int a, int b) { int ans = inf; up(a, b, ans); up(b, a, ans); if (!ancestor(a, b)) swap(a, b); ans = min(ans, st.get_min(tin[a], tin[b])); return ans; } ```

+

+ + diff --git a/web/ru/index.html b/web/ru/index.html new file mode 100644 index 0000000..774a290 --- /dev/null +++ b/web/ru/index.html @@ -0,0 +1,51 @@ + + + + + + + + + + + +

Алгоритмика

+

Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса Tinkoff Generation.

+

Более-менее готово:

+ +

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

+

Не очень готово:

+ +

Вообще не готово: * Алгоритм Карацубы: введение в разделяй-и-властвуй, мастер-теорема * Ахо-Корасик * Теорвер * Корневая декомпозиция * Теория информации * Функции потерь в машинном обучении * Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском) * Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском)

+

Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности.

+ + diff --git a/web/ru/ioi.html b/web/ru/ioi.html new file mode 100644 index 0000000..b6476d3 --- /dev/null +++ b/web/ru/ioi.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + +

IOI Upsolving Challenge

+

https://contest.yandex.com/ioi/total

+

Я решал их почти 2 года назад и уже не очень хорошо всё помню.

+

09-2

+

12 мая

+

Медведь Миша

+

Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за \(O(n^2 \log n)\).

+

Регионы

+

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

+

Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так:

+
    +
  • Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк
  • +
  • Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос
  • +
+

Такое должно работать за \(O(n \sqrt{n \log n})\), и TL такое позволяет.

+

Коммивояжёр

+

Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать.

+

Подумайте, какой вид будет иметь эта функция, если зафиксировать время.

+

TODO

+

05-1

+

31 мая

+

Сад

+

Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно.

+

Средняя последовательность

+

Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы:

+

```c++ int a = -inf, b = inf, d = 0, d = 0, t; for(int i = 0; i < n; i++){ cin >> t; d = d; d = 2*t - d; if (i % 2 == 0) b = min(b, (d-d)/2); else a = max(a, (d-d)/2); }

+

cout << max(b - a + 1, 0ll); ```

+

Польские горки

+

По сути от нас просят обрабатывать запросы «присвоить всем элементам с \(l\) по \(r\) значение \(x\)» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше.

+

05-2

+

Прямоугольник

+

Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста.

+ + diff --git a/web/ru/karatsuba.html b/web/ru/karatsuba.html new file mode 100644 index 0000000..3ac6f14 --- /dev/null +++ b/web/ru/karatsuba.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + +

Быстрое умножение

+

В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).

+

Андрей Колмогоров и несколько других пионеров компьютер саенса выдвинули «гипотезу \(n^2\)» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

+

Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.

+

Алгоритм Карацубы имеет довольно значимое место в истории науки.

+

{M(n)=O(n^{2}).} M(n)=O(n^{2}). У Колмогорова была гипотеза, что нижняя оценка для {M(n)} M(n) при любом методе умножения есть также величина порядка {n^{2}} n^{2}. На правдоподобность «гипотезы {n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности {M(n)=O(n^{{2}3})} M(n)=O(n^{{{2}3}}) и тем самым опроверг «гипотезу {n^{2}} n^{2}».

+

(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)

+

Можно представить себе какого-то грозного японца.

+

Мастер-теорема

+

Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек».

+

Мастер-теорема. Пусть имеется рекуррента:

+

\[ T(n) = \begin{cases} +a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 +\\ \Theta(1), & n \leq n_0 +\end{cases} \]

+

Тогда:

+
    +
  • A. Если \(c > \log_b a\), то \(T(n) = \Theta(n^c)\).
  • +
  • B. Если \(c = \log_b a\), то \(T(n) = \Theta(n^c \log n)\).
  • +
  • C. Если \(c < \log_b a\), то \(T(n) = \Theta(n^{\log_b a})\).
  • +
+
+

+
+

Доказательство. Рассмотрим «дерево рекурсии» этого соотношения. В нём будет \(log_b n\) уровней. На \(k\)-том уровне будет \(a^k\) вершин, каждая из которых будет стоить \((\frac{n}{b^k})^c\) операций. Просуммируем значения во всех вершинах по всем уровням:

+

\[ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k \]

+
    +
  • A. Если \(c > \log_b a\), то \(\sum (\frac{a}{b^с})^k\) это сумма убывающей геометрической прогрессии, которая не зависит от \(n\) и просто равна какой-то константе. Значит, \(T(n) = \Theta(n^c)\).
  • +
  • B. Если \(c = \log_b a\), то \[\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)\]
  • +
  • C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,
  • +
+

\[ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) \]

+

Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает \(O(n)\)

+

Алгоритм Карацубы

+

Алгоритм Карацубы сводит задачу умножения двух чисел длины \(n\) к возведению \(n\)-значного числа в квадрат.

+

Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи.

+

Развитие идеи

+

То же самое можно применить матрицам.

+

Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за \(O(n^{1+\epsilon})\) для произвольного \(\epsilon\) ещё никто не доказал.

+ + diff --git a/web/ru/lca.html b/web/ru/lca.html new file mode 100644 index 0000000..3527172 --- /dev/null +++ b/web/ru/lca.html @@ -0,0 +1,88 @@ + + + + + + + + + + + + +

Корневые деревья

+

Дерево называется корневым, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется корнем.

+

Примеры корневых деревьев: * наследование классов в языках программирования (если множественное наследование запрещено), * дерево факторизации числа на простые (в общем случае не уникальное), * иерархия в какой-нибудь компании, * просто множество как-то вложенных объектов (ссылка на крысу-роботягу).

+

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

+
+dfs

dfs

+
+

Напоминание: DFS

+

Посчитаем для каждой вершины времена входа (\(tin\)) и выхода (\(tout\)) из неё во время эйлерова прохода.

+

```c++ vector g[maxn]; int p[maxn], tin[maxn], tout[maxn]; int t = 0;

+

void dfs (int v) { tin[v] = t++; for (int u : g[v]) dfs(u); tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет } ```

+

У этих массивов много полезных свойств: * Вершина \(u\) является предком \(v\) \(\iff tin_v \in [tin_u, tout_u) \). Эту проверку можно делать за константу. * Два полуинтервала — \([tin_v, tout_v)\) и \([tin_u, tout_u)\) — либо не пересекаются, либо вложены один в другой. * В \(tin\) есть все числа из промежутка от 0 до \(n-1\). У каждой вершины — свой номер. * Размер поддерева вершины \(v\) (включая саму вершину) равен \(tout_v - tin_v\). * Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации.

+

Запросы на поддеревьях

+

Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.

+
+

Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число \(x_i\) и найти значение числа у вершины \(v_i\).

+
+

Давайте запишем все числа у вершин в позиции, соответствующие \(tin\)-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать какой-нибудь достаточно продвинутой структурой.

+

Запросы на уровнях

+
+

Дано корневое дерево. Требуется отвечать на запросы нахождения \(d_i\)-того предка вершины \(v_i\) (т. е. вершины-предка, находящейся на расстоянии \(d_i\)).

+
+

Создадим \(h\) векторов, где \(h\) — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их \(tin\)-ам.

+

Теперь заметим, что отрезки их поддеревьев — \([tin_v, tout_v)\) — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять \(tin\) вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку.

+

Наименьший общий предок

+

Очень много задач нам поможет решить следующая вспомогательная задача.

+
+

Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин \(u_i\) и \(v_i\), то есть вершины \(w\), которая лежит на пути от корня до \(u_i\), на пути от корня до \(v_i\), и при этом самую глубокую (нижнюю) из всех таких.

+
+

По-английский эта задача называется Least Common Ancestor. Есть много разных способов её решать, и мы рассмотрим основные.

+
+lca

lca

+
+

Для лучшего понимания: медленно (за линейное время) это можно делать так:

+

```c++ bool a (int u, int v) { return tin[u] <= tin[v] && tin[v] <= tout[u]; }

+

int lca (int u, int v) { while (!ancestor(u, v)) u = p[u]; return u; } ```

+

LCA: двоичные подъемы

+

Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве up размера \(n \times \lceil \log n \rceil\) — столько точно хватит. В up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует — то корень.

+

Такой препроцессинг можно выполнить за \(O(n \log n)\), используя тот факт, что предок на расстоянии \(2^{d+1}\) — это предок на расстоянии \(2^d\) предка на расстоянии \(2^d\):

+

```c++ int up[maxn][logn];

+

void dfs (int v) { for (int l = 1; l < logn; l++) up[v][l] = up[up[v][l-1]][l-1]; tin[v] = t++; for (int u : g[v]) { up[u][0] = v; dfs(u); } tout[v] = t++; } ```

+

Пусть поступил очередной запрос — пара вершин \((u, v)\): * Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. * Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.

+

Подробнее про второй пункт. Пусть \(L = \lceil \log n \rceil\). Присвоим \(i = L\). Будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком \(u\) (указатель up[v][i] изначально будет корнем, а затем каждую итерацию спускаться на \(2^i\)). Когда это произойдёт, подвинем указатель на \(2^i\)-го предка \(v\), и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на \(2^{i+1}\).

+

c++ int lca (int v, int u) { if (a(v, u)) return v; if (a(u, v)) return u; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l]; return up[v][0]; }

+

Асимптотика

+

Препроцессинг — \(O(n \log n)\). Размер массива up ровно такой, и каждый его элемент вычисляется за константу.

+

Ответ на запрос — \(O(\log n)\), потому что по сути мы делаем один бинпоиск.

+

Запросы на путях

+

Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).

+

Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером \(2^d\)-го предка минимум на соответствующем пути.

+

Мы знаем, что минимум на пути от \(u\) до \(v\) — это минимум от минимума на пути от \(u\) до \(lca(u, v)\) и от минимума на пути от \(v\) до \(lca(u, v)\). А каждый минимум — это минимум на всех двоичных подъемах до LCA.

+

c++ int get_min (int v, int u) { int ans = inf; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l], ans = min(ans, mn[v][l]); for (int l = logn-1; l >= 0; l--) if (!ancestor(up[u][l], v)) u = up[u][l], ans = min(ans, mn[u][l]); return min({ans, mn[v][0], mn[u][0]}) }

+

Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная.

+

Сведение к RMQ

+

Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.

+

+

Во втором массиве мы по сути выписали наш проход dfs-а.

+

Пусть у нас есть запрос: найти LCA вершин \(v\) и \(u\). Для определенности положим, что \(tin_v < tin_u\). Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из \(v\) и в первй раз вошли в \(u\). Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.

+

Получается, что можно найти LCA, просто найдя позицию минимума на отрезке \([tout_v, tin_u]\) в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы уже умеем.

+

Разреженная таблица

+

На практике асимптотику мы особо не улучшили — пока что всё равно требуется \(O(n \log n)\) времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.

+

Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за \(O(1)\), но использующая \(O(n \log n)\) препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в отдельной статье.

+

А наоборот можно?*

+

Примечание: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

+

Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.

+

Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера \(n-1\): единица стоит, если следующее значение больше, единица в противном случае ноль.

+

Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

+

\[O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)\]

+

Также посчитаем для каждой возможной маски размера \(\frac{\log n}{2}\) минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего \(\sqrt n\) (ради этого мы и делили логарифм на два).

+

ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.

+

Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.

+

Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать у Емакса.

+

Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.

+

Важный вывод такой: RMQ более общая задача, чем LCA. UPD: это неправда, я глупый.

+ + diff --git a/web/ru/linalg.html b/web/ru/linalg.html new file mode 100644 index 0000000..0b311e2 --- /dev/null +++ b/web/ru/linalg.html @@ -0,0 +1,180 @@ + + + + + + + + + + + + +

Ликбез по линейной алгебре

+

+

Определение. Функция \(f: \mathbb{R}^n \to \mathbb{R}^m\) называется линейной, если для неё выполнено

+
    +
  1. $ f(x+y) = f(x) + f(y) $
  2. +
  3. $ f(ax) = a f(x), ; a R $
  4. +
+

Примеры: * $ f(x) = 0 $ * $ f(x) = x $ * $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из \(\mathbb{R}^3\) в \(\mathbb{R}\)) * $ f(x) = (x, -x, 0) $ (из \(\mathbb{R}\) в \(\mathbb{R}^3\))

+

Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: * Сумма линейных функций — линейная функция. * Сумма коммутативна: \(f+g = g+f\)). * Сумма ассоциативна: \((f+g)+h = f+(g+h)\). * Композиция \(f(g(x)) = (f \circ g)(x)\) линейных функций — линейная функция. * Композция ассоциативна: \((f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h\). * Композиция в общем случае не коммутативна.
Пример: \(f = (-x_2, x_1)\) — поворот точки на плоскости на прямой угол, \(g = (x_1, 0)\) — проекция на \(Ox\). Почти для всех точек порядок этих операций важен.

+

Все свойства можно вывести лишь из этих двух пунктов в определении.

+

Что такое матрица?

+

Можно показать, что любую линейную функцию \(f: \mathbb{R}^n \to \mathbb{R}^m\) можно представить в таком виде:

+

\[ +f(x) = +\begin{pmatrix} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ +\ldots \\ +a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ +\end{pmatrix} +\]

+

Матрицы ввели просто как очень компактную запись этих коэффициентов \(a_{ij}\).

+

\[ +A = +\begin{pmatrix} +a_{11} & a_{12} & \ldots & a_{1n} \\ +a_{21} & a_{22} & \ldots & a_{2n} \\ +\vdots & \vdots & \ddots & \vdots \\ +a_{m1} & a_{m2} & \ldots & a_{mn} \\ +\end{pmatrix} +\]

+

Каждой линейной функции из \(\mathbb{R}^n\) в \(\mathbb{R}^m\) соответствует какая-то матрица размера \(n \times m\) (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении \(i\)-го строки и \(j\)-го столбца будем обозначать \(A_{ij}\). Не перепутайте.

+

Пусть линейной функции \(f\) соответствует матрица \(A\), а функции \(g\)\(B\). Тогда композиции этих функций \(h = f \circ g\) будет соответствовать произведение \(C\) матриц \(A\) и \(B\), определяемое так:

+

\[ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} \]

+

Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из \(g\) подставить в \(f\).

+

Когда перемножаете руками, удобно думать так: элемент на пересечении \(i\)-го столбца и \(j\)-той строки — это скалярное произведение \(i\)-той строки \(A\) и \(j\)-того столбца \(B\). Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер \(n \times k\), то вторая должна иметь размер \(k \times m\), то есть «средние» размерности обязательно должны совпадать.

+

+

Исходное выражение для \(f(x)\) теперь можно компактно записать как \(f(x) = Ax\) вместо \(m\) уравнений с \(n\) слагаемыми в каждом.

+

К матрицам не нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.

+

Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:

+
    +
  • Сумма матриц \(A\) и \(B\) — матрица \(C = A+B: C_{ij} = A_{ij} + B_{ij}\).
  • +
  • Сумма коммутативна: \(A+B = B+A\))
  • +
  • Сумма ассоциативна: \((A+B)+C = A+(B+C)\)
  • +
  • Умножение ассоциативно: \((AB)C = A(BC) = ABC\).
  • +
  • Умножение в общем случае не коммутативно.
  • +
+

Пример: матрица поворота в 2d. \[ +\begin{pmatrix} +\cos \alpha & -\sin \alpha \\ +\sin \alpha & \cos \alpha \\ +\end{pmatrix} +\]

+

Пример: матрица проецирования на \(Ox\) в 3d. \[ +\begin{pmatrix} +1 & 0 & 0 \\ +0 & 0 & 0 \\ +0 & 0 & 0 \\ +\end{pmatrix} +\]

+

Пример: матрица «свапни \(x\) и \(y\)». \[ +\begin{pmatrix} +0 & 1 \\ +1 & 0 \\ +\end{pmatrix} +\]

+

Напишем класс, который реализует матричное умножение.

+

```c++ struct matrix { int n, m; int t[]; matrix (int n, int m) { n = n, m = m; t = new int(n*m); memset(t, 0, sizeof t); } int[] operator[] (int k) { return t[k*m]; } }

+

matrix operator* (matrix a, matrix b) { matrix c(a.n, b.m); for (int i = 0; i < a.n; i++) for (int j = 0; j < b.m; j++) for (int k = 0; k < a.m; k++) c[i][j] += a[i][k] * b[i][k]; return c; } ```

+

Динамика

+

Некоторые динамики можно выразить в терминах матричного умножения.

+

Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?

+

\[ +\begin{pmatrix} +f_{n+1} \\ +f_{n+2} \\ +\end{pmatrix} += +\begin{pmatrix} +0+f_{n+1} \\ +f_{n}+f_{n+1} \\ +\end{pmatrix} += +\begin{pmatrix} +0 & 1 \\ +1 & 1 \\ +\end{pmatrix} +\begin{pmatrix} +f_{n} \\ +f_{n+1} \\ +\end{pmatrix} +\]

+

Обозначим за \(A\) эту матрицу перехода. Чтобы посчитать \(n\)-е число Фибоначчи, нужно применить \(n\) раз эту матрицу к вектору \((f_0, f_1) = (0, 1)\).

+

Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:

+

\[ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) \]

+

Это будет работать за \(O(n^3 \log n)\). Мы делаем \(O(n^3)\) операций для одного умножения, а всего их нужно сделать \(O(\log n)\). Кстати, наука знает и более быстрые способы перемножить матрицы, но на контестах они не нужны.

+

c++ matrix binpow (matrix a, int p) { matrix b(n, n); for (int i = 0; i < n; i++) b[i][i] = 1; while (p) { if (p&1) b = b*a; a = a*a; p >>= 1; } return b; }

+

Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: \(I\).

+

\[ +\begin{vmatrix} +1 & 0 & 0 \\ +0 & 1 & 0 \\ +0 & 0 & 1 \\ +\end{vmatrix} +\]

+

В плане умножения она действительно ведет себя как единица: \(AI = A = IA\). В коде она используется вместо единицы.

+

В общем случае, линейная рекуррента \(f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}\) имеет такую матрицу перехода:

+ +

Матрица смежности

+

У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины \(a\) в вершину \(b\) за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из \(a\) в \(b\), используя ровно \(n\) переходов.

+

Когда нам нужна только информация, можно ли дойти из \(a\) в \(b\) (количество способов не важно), то решение можно ускорить (см. Битсет).

+

Обобщения

+

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

+

Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два.

+

Определитель

+

Определителем кадратной матрицы \(A\) называется такое выражение:

+

\[ \det A = |A| = \ldots \]

+

Где \(\epsilon\) — чётность числа инверсий в перестановке (\(-1\) или \(+1\)). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:

+

\[ +\begin{vmatrix} +a & b \\ +c & d \\ +\end{vmatrix} += +ad - bc +\]

+

У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.

+

В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем.

+

Базис

+

Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их.

+

Базисы есть не только в линейной алгебре. Например, \(\{1, x, x^2\}\) является базисом всех квадратных трёхчленов. Или \(\{\neg, \land, \lor\}\) является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом.

+

Метод Крамера и easy пересечение прямых

+

Пусть нам надо пересечь две прямые.

+

\[ +\begin{cases} +a_1 x + b_1 y + c_1 = 0 +a_2 x + b_2 y + c_2 = 0 +\end{cases} +\]

+

Это то же самое, что найти такие коэффициенты \(x\) и \(y\), что

+

\[ x \vec{a} + y \vec{b} = -\vec{c} \]

+

Площадь параллелограмма, натянутого на \(\vec{a}\) и \(\vec{b}\), равна векторному произведению, или детерминанту.

+

По сути, нам нужно выразить \(c\) в другом базисе. Давайте спроецируем её на \(a\).

+

Аналогично, напрягите воображение и спроецируйте эту точку в \(n\)-мерном пространстве. Это уже сложно, да?

+

*Собственные векторы

+

Очень часто у матриц есть собственные вектора -- те, которые не меняют направление.

+

$ Av = k v \(, где \)k 0$.

+

$ Av - kv = (A-kI)v = 0 $. Это означает

+

Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике.

+

Системы уравнений и метод Гаусса

+

Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

+

Нас по сути просят решить следующую систему:

+

\[ +\begin{cases} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ +\ldots \\ +a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 +\end{cases} +\]

+

Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

+

Метод Крамера неоптимален — там \(O(n^4)\) операций.

+

В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

+

c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; }

+

Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

+

Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. Битсет).

+ + diff --git a/web/ru/matching.html b/web/ru/matching.html new file mode 100644 index 0000000..7db5cde --- /dev/null +++ b/web/ru/matching.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + +

Паросочетания

+

Пусть есть \(n\) мальчиков и \(m\) девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.

+

+

Паросочетанием \(M\) называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из \(M\)).

+

Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном \(M\)), назовём насыщенными этим паросочетанием.

+

Мощностью паросочетания назовём количество рёбер в нём. Наибольшим (максимальным) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а совершенным — где все вершины левой доли им насыщенны.

+

Паросочетания можно искать в любых графах, однако этот алгоритм неприятно кодить, и он работает за \(O(n^3)\), так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как \(L\), а правую долю как \(R\).

+

Цепью длины \(k\) назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно \(k\) рёбер.

+

Чередующейся цепью относительно некоторого паросочетания назовём простой путь длины \(k\) в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.

+

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

+
+ +
+

Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: \(1 \to 8 \to 4 \to 6 \to 3 \to 7\).

+

Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести чередование — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.

+

В примере добавятся синие рёбра \((1, 8)\), \((3, 7)\) и \((4, 6)\), а удалятся красные \((3, 6)\) и \((4, 8)\). С ребром \((2, 5)\) ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.

+

Алгоритм Куна в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже.

+

```c++ const int maxn;

+

vector g[maxn]; // будем хранить только рёбра из левой доли в правую int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой) bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом

+

// dfs возвращает, можно ли найти путь из вершины v // в какую-нибудь вершину правой доли // если можно, то ещё и проводит чередование bool dfs (int v) { if (used[v]) return false; used[v] = true; for (int u : g[v]) { // если вершина свободна, то можно сразу с ней соединиться // если она занята, то с нейможно соединиться только тогда, // когда из её текущей пары можно найти какую-нибудь другую вершину if (mt[u] == -1 || dfs(mt[u])) { mt[u] = v; return true; } } return false; }

+

// где-то в main:

+

memset(mt, -1, sizeof(mt)); for (int i = 0; i < n; i++) { memset(used, 0, sizeof(mt)); if (dfs(i)) cnt++; } ```

+

Корректность

+

Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.

+

Теорема (Бержа). Паросочетание без увеличивающих цепей является максимальным.

+

Доказательство проведём от противного: пусть есть два паросочетания вершин \(|A| \leq |B|\), и для \(A\) нет увеличивающих путей, и покажем, как найти этот путь и увеличить \(A\) на единицу.

+

Раскрасим ребра из паросочетания, соответствующего \(A\) в красный цвет, \(B\) — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для \(A\), а значит \(A\) не оптимальное, и мы получили противоречие.

+

Скорость работы

+

Такой алгоритм ровно \(n\) раз ищет увеличивающий путь, каждый раз просматривая не более \(m\) рёбер, а значит работает за \(O(nm)\).

+

Что примечательно, его можно не бояться запускать на ограничениях и побольше (\(n, m \approx 10^4\)), потому что для него есть мощные неасимптотические оптимизации:

+
    +
  • Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).

  • +
  • Можно не заполнять нулями на каждой итерации массив used, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало true. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как false. В каком-то смысле это позволяет эмулировать очищение массива за константу.

  • +
  • Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы \(n \times m\). Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.

  • +
+

Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить алгоритм Диница к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за \(O(n \sqrt m)\).

+

Покрытие путями DAG-а

+

Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф \(G\) (англ. directed acyclic graph). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.

+

Построим соответствующие изначальному графу \(G\) два двудольных графа \(H\) и \(\overline{H}\) следующим образом: * В каждой доле графа \(H\) будет по \(n\) вершин. Обозначим их через \(a_i\) и \(b_i\) соответственно. * Для каждого ребра \((i, j)\) исходного графа \(G\) проведём соответствующее ребро \((a_i, b_j)\) в графе \(H\). * Теперь из графа \(H\) сделаем граф \(\overline{H}\), добавив обратное ребро \((b_i, a_i)\) для каждого \(i\).

+

Если мы рассмотрим любой путь \(v_1, v_2, \ldots, v_k\) в исходном графе \(G\), то в графе \(\overline{H}\) ему будет соответствовать путь \(a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}\). Обратное тоже верно: любой путь, начинающийся в левой доле \(\overline{H}\) и заканчивающийся в правой будет соответствовать какому-то пути в \(G\).

+

Итак, есть взаимно однозначное соответствие между путями в \(G\) и путями \(\overline{H}\), идущими из левой доли в правую. Заметим, что любой такой путь в \(\overline{H}\) — это паросочетание в \(H\) (напомним, это \(\overline{H}\) без обратных рёбер). Получается, любому пути из \(G\) можно поставить в соответствие паросочетание в \(H\), и наоборот. Более того, непересекающимся путям в \(G\) соответствуют непересекающиеся паросочетания в \(H\).

+

Заметим, что если есть \(p\) непересекающихся путей, покрывающих все \(n\) вершин графа, то они вместе содержат \(r = n - p\) рёбер. Отсюда получаем, что чтобы минимизировать число путей \(p\), мы должны максимизировать число рёбер \(r\) в них.

+

Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе \(H\). После нахождения этого паросочетания мы должны преобразовать его в набор путей в \(G\). Это делается тривиальным алгоритмом: возьмем \(a_1\), посмотрим, с какой \(b_k\) она соединена, посмотрим на \(a_k\) и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин.

+

Лемма Холла

+

Лемма Холла (или: теорема о свадьбах) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.

+
+ +
+

Лемма Холла. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.

+

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

+

В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.

+

База индукции: одна вершина из \(L\), которая по условию соединена с хотя бы одной вершиной из \(R\).

+

Индукционный переход: пусть после \(k < n\) шагов построено паросочетание \(M\). Докажем, что в \(M\) можно добавить вершину \(v\) из \(L\), не насыщенную паросочетанием.

+

Рассмотрим множество вершин \(H\) — все вершины, достижимые из \(x\), если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)

+

Тогда в \(H\) найдется вершина \(y\) из \(R\), не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины \(H_L\) (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что \(|H_L| \leq |N(H_L)|\) (здесь \(N(X)\) — множество вершин, соединенным паросочетанием с \(X\)).

+

Тогда должен существовать путь из \(x\) в \(y\), и он будет увеличивающим для паросочетания \(M\), потому что из \(R\) в \(L\) мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно.

+

Для ноулайферов: матроиды

+

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

+

Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию хороших подмножеств (паросочетаний) обобщает понятие матроида. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше хорошее множество, если оно после добавления остается хорошим.

+

Применимо к паросочетаниям: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна.

+ + diff --git a/web/ru/matroid.html b/web/ru/matroid.html new file mode 100644 index 0000000..406c106 --- /dev/null +++ b/web/ru/matroid.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + + +

Матроиды

+

Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры).

+

Матроидом называется пара \((X, I)\), где \(X\) — множество элементов, называемое носителем матроида, а \(I\) — некоторое множество подмножеств \(X\), называемое семейством независимых множеств. В матроиде должны выполняться следующие свойства:

+
    +
  • Пустое множество является независимым: \(\varnothing \in I\)

  • +
  • Любое подмножество независимого множества тоже независимо: \[A \subset B, B \in I \implies A \in I\]

  • +
  • Если в независимом множестве \(A\) меньше элементов, чем в независимом множестве \(B\), то будет существовать элемент из \(B\), дополняющий \(A\) до независимого множества размера \(|A|+1\): \[A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I\]

  • +
+

Матроид называется взвешенным, если на нем существует аддитивная весовая функция: \(w(A) = \sum w(a_i)\).

+

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

+
X.sort()
+s = []
+for x in X:
+    if good(s + [x]):
+        s += [x]
+

Здесь под good имеется в виду \(s \cup x \in I\).

+

Корректность этого алгоритма для любого матроида доказывает следующая теорема:

+

Теорема Радо-Эдмондса

+

Пусть \(A \in I\) — множество минимального веса среди всех независимых подмножеств \(X\) мощности \(k\). Возьмем \(x: A \cup x \in I,\;x \notin A,\;w(x)\) — минимальна. Тогда \(A \cup x\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

+

*Доказательство: **

+

Рассмотрим \(B\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

+

Из свойств матроида: \(\exists y \in B \setminus A : A \cup y \in I\).

+

Тогда верны два неравенства:

+

\[ +\begin{cases} +w(A \cup y) = w(A) + w(y) \geq w(B) \implies w(A) \geq w(B) - w(y) \\ +w(B \setminus y) = w(B) - w(y) \geq w(A) \implies w(A) \leq w(B) - w(y) +\end{cases} +\]

+

Величина \(w(A)\) с двух сторон ограничивает величину \(w(B) - w(y)\). Значит, они равны. Cледовательно, \(w(A \cup y) = w(A) + w(y) = w(B)\).

+

Получаем, что если объединить множество \(A\) с \(x\) — минимальным из таких, что \(A \cup x \in I\), — то получим множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

+

Иными словами, если у нас есть оптимальное \(k\)-элементарное независимое множество, то мы можем индуктивно построить оптимальное \((k+1)\)-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым.

+

Примеры

+

Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.

+

Минимальный остов

+

Рассмотрим неориентированный граф \(G = (V, E)\). Пусть \(I\) — множество лесов графа (ациклических подмножеств \(E\)). Тогда \(M = (E, I)\) является матроидом:

+
    +
  • Граф без ребер является лесом.
  • +
  • Если удалить из леса ребра, он останется лесом.
  • +
  • Пусть есть два леса \(|A| \leq |B|\). В \(A\) будет \(|V| - |A|\) компонент связности, в \(B\) будет \(|V|-|B|\) компонент связности. Так как в \(B\) компонент связности меньше, то будет существовать какое-то ребро \(x\), связывающее две компоненты связности из \(A\). Его и возьмем: \(A \cup \{x\}\) тоже будет лесом, так как \(x\) только соединило две разные компоненты связности.
  • +
+

Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.

+

Расписания

+

Пусть у нас есть \(n\) заданий, на выполнение каждого требуется \(1\) час. Награда за выполнение \(i\)-го задания не позже \(d_i\)-того часа равна \(w_i\). В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.

+

Назовём правильными те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам (\(d_i\)) и проверить, что \(d_i \geq i\) для всех \(i\).

+

Тогда \(M = \) (множество всех заданий, множество правильных наборов заданий) является матроидом:

+
    +
  • Пустой набор заданий всегда можно сделать.
  • +
  • Если у нас стало меньше заданий, то их сделать мы тоже успеем.
  • +
  • Пусть есть два правильных набора \(|A| \leq |B|\). Тогда в \(B\) будет существовать задание \(x\) с дедлайном позже \(|A|\). Все задания \(A\) можно сделать не позже \(|A|\)-го часа, а в \((|A|+1)\)-й час будем делать \(x\). Значит, \(A \cup x\) — тоже правильный набор.
  • +
+

Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных.

+

Паросочетания

+

Рассмотрим двудольный граф \(G = (L, R, E)\). Пусть \(I\) — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда \(M = (L, I)\) является матроидом:

+
    +
  • Любое паросочетание покрывает пустое множество вершин.
  • +
  • Исходное паросочетание покрывает также и любое подмножество исходных вершин.
  • +
  • Пусть есть два множества вершин \(|A| \leq |B|\). Раскрасим ребра из паросочетания, соответствующего \(A\) в красный цвет, \(B\) — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству \(A\) добавилась какая-то вершина из левой доли, принадлежавшая ранее \(B\).
  • +
+

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

+

Линейно независимые вектора

+

(TODO) (Школьники не обязаны знать линал.)

+

Такие штуки будем называть базисами.

+
    +
  • Ноль есть в любом базисе.
  • +
  • Подмножество базиса — базис.
  • +
  • ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора \(B\) лежат в \(A\). Значит, размерность \(B\) уж точно не больше.
  • +
+ + diff --git a/web/ru/mincost-maxflow.html b/web/ru/mincost-maxflow.html new file mode 100644 index 0000000..53d74b0 --- /dev/null +++ b/web/ru/mincost-maxflow.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + +

Поток минимальной стоимости

+

Рассмотрим ориентированный граф \(G = (V, E)\) с истоком \(s\) и стоком \(t\), в котором у каждого ребра \((u, v)\) задана целая стоимость \(w_{uv}\) и целая положительная пропускная способность \(c_{uv}\). Требуется найти максимальный поток, стоимость которого минимальна:

+

\[ \sum_{(u, v) \in E} f_{uv} \to \max \] \[ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min \]

+

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

+

Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра \((u, v)\) добавим \((v, u)\), для которого \(c_{vu} = 0\) и \(w_{vu} = -w_{uv}\). Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая (\(c_{uv}-f_{uv} > 0\)).

+

Критерий оптимальности

+

Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот).

+

Доказательство:

+

\(\rightarrow\) Рассмотрим произвольный неоптимальный поток \(f\) и оптимальный поток \(f^*\). Рассмотрим разность \(f^*-f\). Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость \(f^*\) меньше стоимости \(f\), что противоречит предположению.

+

\(\leftarrow\) Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости.

+

Отмена циклов

+

Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более \(mUC\) раз где \(U\) — величина потока, \(C\) — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу.

+

Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит \(O(m^2nUC)\) (предполагая, что какой-нибудь максимальный поток мы уже нашли).

+

Дополняющие пути

+

Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.

+

Утверждеие. Алгоритм не создает в остаточной сети циклов отрицательного веса.

+

Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из \(s\) в \(t\) и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро \((v, u)\) находится в цикле отрицательного веса. Тогда есть путь Из \(u\) в \(v\) стоимости меньше, чем \(w_{uv}\). Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра \((u, v)\).

+

Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит \(O(nmU)\) — искать каждый дополняющий путь мы будем не более \(U\) раз.

+

Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер.

+

Потенциалы Джонсона

+

Потенциалом вершины \(v\) будем называть расстояние \(d_v\) от вершины \(s\). Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:

+

\[ w_{uv}' = w_{uv} + d_u - d_v \]

+

Утверждение 1. Веса всех рёбер графа неотрицательные.

+

Доказательство. Пусть вес какого-то ребра \((u, v)\) отрицателен, то есть \(w_{uv}' = w_{uv} + d_u - d_v < 0\). Тогда \(d_u + w_{uv} < d_v\), и нарушилось неравенство треугольника: почему мы тогда не использовали ребро \((u, v)\), когда искали кратчайший путь до \(v\)?

+

Аналогично можно показать, что рёбра на кратчайших путях из \(s\) имеют нулевую стоимость. Заметим, что стоимость обратных рёбер на кратчайших путях тоже будет нулевой: \[ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 \]

+

Утверждение 2. Кратчайшие пути между любыми вершинами остались кратчайшими.

+

Доказательство. Распишем новую стоимость пути из \(a\) в \(z\).

+

\[ +\begin{align} +w_{ab}' + \ldots + w_{yz}' +&= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) +\\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z +\end{align} +\]

+

Получаем, что стоимость всех путей из \(a\) в \(z\) лишь изменилась на константу.

+

Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.

+

Заметьте, что в доказательстве мы не использовали то, что \(d_v\) — кратчайшие расстояния. Это вообще могут быть произвольные числа.

+

Утверждение 3. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими).

+

Доказательство. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2).

+

Итоговый алгоритм

+
    +
  • Модифицируем сеть, добавивив обратные рёбра.
  • +
  • Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.
  • +
  • Пока максимальный поток не найден:
  • +
    • +
    • Посчитать алгоритмом Дейкстры кратчайшие расстояния от \(s\), используя для веса формулу с потенциалами, записать их в \(d\).
    • +
  • +
    • +
    • Протолкнуть максимально возможный поток вдоль кратчайшего пути \(s \leadsto t\), обновить остаточную сеть.
    • +
  • +
+

Асимптотика

+

Алгоритм работает за \(O(U m \log n)\) или \(O(U n^2)\) в случае плотных графов.

+

В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и \(U \leq n\) или \(U \leq m\). Например, в задаче о назначениях (паросочетание минимального веса) \(U = n\) и алгоритм работает за \(O(n^3)\), что совпадает с асимптикой венгерского алгоритма.

+

Реализация

+

Решение задачи о назначениях за \(O(n^3)\). Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).

+
    +
  • cost, cap — параметры сети
  • +
  • pot — потенциалы
  • +
  • par — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)
  • +
  • d — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния
  • +
+

```c++ const int maxn = 305, inf = 1e9;

+

int n; int cost[maxn][maxn], cap[maxn][maxn]; int d[maxn], pot[maxn], par[maxn];

+

bool dijkstra (int s, int t) { used[maxn] = {0};

+
fill(d, d+n, inf);
+d[s] = 0;
+
+while (1) {
+    int v = -1;
+    for (int u = 0; u < n; u++)
+        if (!used[u] && (v == -1 && d[u] < d[v]))
+            v = u;
+    if (v == -1 || d[v] == inf)
+        break;
+    used[v] = 1;
+    for (int u = 0; u < n; u++) {
+        int w = cost[v][u] + pot[v] - pot[u];
+        if (cap[v][u] && d[u] > d[v] + w) {
+            d[u] = d[v] + w;
+            par[u] = v;
+        }
+    }
+}
+
+return d[t] < inf;
+

}

+

int mincost_maxflow (int s, int t) { int ans = 0; while (dijkstra(s, t)) { memcpy(pot, d, sizeof(d)); int delta = inf; for (int v = t; v != s; v = par[v]) delta = min(delta, cap[par[v]][v]); for (int v = t; v != s; v = par[v]) { cap[par[v]][v] -= delta; cap[v][par[v]] += delta; ans += cost[par[v]][v]*delta; } } return ans; } ```

+ + diff --git a/web/ru/mst.html b/web/ru/mst.html new file mode 100644 index 0000000..d68529f --- /dev/null +++ b/web/ru/mst.html @@ -0,0 +1,182 @@ + + + + + + + + + + + + + +

Минимальные остовы

+
+

Авиакомпания содержит \(m\) рейсов между \(n\) городами, \(i\)-ый из них обходится в \(w_i\) рублей, причём из любого города можно добраться до любого другого. В стране наступил кризис, и нужно отказаться от как можно большего числа из них таким образом, что содержание оставшиъся рейсов будет наиболее дешевым.

+
+

Иными словами, нужно найти дерево минимального веса, которое является подграфом данного неориентированного графа. Такие деревья называют остовами (каркас, скелет; ударение на первый слог, но так мало кто произносит). По-английски — minimum spanning tree (дословно, минимальное покрывающее дерево).

+

Почему дерево? Потому в противном случае там был бы цикл из которго можно удалить какое-то ребро и получить ответ пооптимальней. А если это больше, чем одно дерево, то какие-то две вершины остаются несвязаны.

+

Вообще, следующие утверждения про деревья являются эквивалентными: * Граф — дерево. * В графе из \(n\) вершин \(n-1\) рёбер и нет циклов. * Из любой вершины можно дойти в любоую другую единственным образом.

+

Лемма о безопасном ребре

+

Назовем подграф \(T\) графа \(G\) безопасным, если они является подграфом какого-то минимального остова.

+

Назовем ребро безопасным, если при добавлении его в подграф \(T\) получившийся граф \(T'\) тоже является безопасным, то есть подграфом какого-то минимального остова.

+

Все алгоритмы для поиска минимального остова опираются на следующее утверждение:

+
+

Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным.

+
+

Доказательство: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже.

+
+safe

safe

+
+

Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов.

+

Алгоритм Прима

+

Минимальный остов строится постепенно, добавлением в него рёбер по одному.

+
    +
  • Изначально остов — одна произвольная вершина.
  • +
  • Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден.
  • +
+

Очень похоже на алгоритм Дейкстры, только мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё.

+

Совсем наивная реализация за \(O(nm)\) — каждый раз перебираем все рёбра:

+
const int maxn = 1e5, inf = 1e9;
+vector<int> from, to, weight;
+bool used[maxn]
+
+// считать все рёбра в массивы
+
+used[0] = 1;
+for (int i = 0; i < n-1; i++) {
+    int opt_w = inf, opt_from, opt_to;
+    for (int j = 0; j < m; j++)
+        if (opt_w > weight[j] && used[from[j]] && !used[to[j]])
+            opt_w = weight[j], opt_from = from[j], opt_to = to[j]
+    used[opt_to] = 1;
+    cout << opt_from << " " << opt_to << endl;
+}
+

Реализация за \(O(n^2)\):

+
const int maxn = 1e5, inf = 1e9;
+bool used[maxn];
+vector< pair<int, int> > g[maxn];
+int min_edge[maxn] = {inf}, best_edge[maxn];
+min_edge[0] = 0;
+
+// ...
+
+for (int i = 0; i < n; i++) {
+    int v = -1;
+    for (int u = 0; u < n; j++)
+        if (!used[u] && (v == -1 || min_edge[u] < min_edge[v]))
+            v = u;
+ 
+    used[v] = 1;
+    if (v != 0)
+        cout << v << " " << best_edge[v] << endl;
+ 
+    for (auto e : g[v]) {
+        int u = e.first, w = e.second;
+        if (w < min_edge[u]) {
+            min_edge[u] = w;
+            best_edge[u] = v;
+        }
+    }
+}
+

Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за \(O(m \log n)\):

+
set< pair<int, int> > q;
+int d[maxn];
+
+while (q.size()) {
+    v = q.begin()->second;
+    q.erase(q.begin());
+ 
+    for (auto e : g[v]) {
+        int u = e.first, w = e.second;
+        if (w < d[u]) {
+            q.erase({d[u], u});
+            d[u] = w;
+            q.insert({d[u], u});
+        }
+    }
+}
+

Про алгоритм за \(O(n^2)\) забывать не стоит — он работает лучше в случае плотных графов.

+

Алгоритм Крускала

+

Будем добавлять рёбра в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить.

+

Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Однако для этой проверки нам нужна будет целая отдельная структура.

+

Система непересекающихся множеств

+

Эта структура данных предоставляет следующие возможности. Изначально имеется несколько элементов, каждый из которых находится в отдельном (своём собственном) множестве. Структура поддерживает две операции: * объединить два каких-либо множества * запросить, в каком множестве сейчас находится указанный элемент

+

Обе операции будут выполняться в среднем почти за \(O(1)\) (но не совсем — этот сложный вопрос будет разъяснен позже).

+

Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив _p, в котором для каждого элемента мы храним номер его предка в дерева. Для корней деревьев будем считать, что их предок — они сами.

+

Наивная реализация, которую мы потом ускорим:

+
int _p[maxn];
+
+int p (int v) {
+    if (_p[v] == v)
+        return v;
+    else
+        return p(_p[v]);
+}
+
+void unite (int a, int b) {
+    a = p(a), b = p(b);
+    _p[a] = b;
+}
+
+for (int i = 0; i < n; i++)
+    _p[i] = i;
+

Эвристика сжатия пути. Оптимизируем работу функции p. Давайте перед тем, как вернуть ответ, запишем его в _p от текущей вершины, то есть переподвесим его за самую высокую.

+

Насколько лучше это сделает асимптотику? Выясняется, что \(O(n \log n)\).

+

Тут должен быть мем из опросов.

+

Ранговая эвристика. Эта штука напрямую пытается минимизировать высоту дерева. Давайте делать переподвешивание за то, которое менее глубоко. Ну понятно, что тогда любое дерево будет не более логарифма.

+

Весовая эвристика. Давайте каждый раз подвешивать за более крупное. Работать будет быстро, так как .

+

Автор предпочитает именно её, потому что часто эти размеры компонент требуются сами по себе.

+

Оказывается, что сжатия + ранговая или сжатия + весовая работает быстро.

+

Асимптотика объединения обеих эвристик (сжатия путей и одной из ранговых) — O(a(n)), где a(n) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4). Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует.

+
int _p[maxn], s[maxn];
+
+int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); }
+
+void unite (int a, int b) {
+    a = p(a), b = p(b);
+    if (s[a] > s[b]) swap(a, b);
+    s[b] += s[a];
+    _p[a] = b;
+}
+
+for (int i = 0; i < n; i++)
+    _p[i] = i;
+

Полезные свойства и классические задачи

+
    +
  • Если веса всех рёбер различны, то остов будет уникален.
  • +
  • Минимальный остов является также и остовом с минимальным произведением весов рёбер (замените веса всех рёбер на их логарифмы)
  • +
  • Минимальный остов является также и остовом с минимальным весом самого тяжелого ребра.
  • +
  • Если вы решаете задачу, где ребра не добавляются, а удаляются, то можно попробовать решать задачу «с конца» и применить алгоритм Крускала.
  • +
+

Персистентная СНМ*

+

Как вы уже заметили, автор очень любит персистентные структуры данных.

+

СНМ — тоже структура, и её тоже можно сделать персистентной. В СНМ мы изменяем массивы, а массивы можно сделать персистентными через персистентное ДО (только так, проще не получается — многие пытались).

+

Здесь есть нюанс — амортизированные структуры не очень хорошо дружат с персистентностью. Поэтому нам придется отказаться от эвристики сжатия путей, и поэтому асимптотика составит \(O(n \log^2 n)\) времени и памяти — один логарифм от СНМа, другой от персистентного ДО.

+

Динамическая связность*

+

Dynamic Connectivity Problem:

+
+

Даны \(n\) запросов добавления ребра (+), удаления ребра (- и какого-то запроса про граф (?), например, о связности двух вершин.

+
+

О решении этой задачи в online и в offline можете почитать в этом посте.

+ + diff --git a/web/ru/pollard.html b/web/ru/pollard.html new file mode 100644 index 0000000..f6fe01a --- /dev/null +++ b/web/ru/pollard.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + +

Факторизация за \(O(\sqrt[4]{n})\)

+

Наука умеет раскладывать целые числа на множители за \(O(n^\frac{1}{4})\). Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера.

+

Парадокс дней рождений

+

Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

+

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

+

Попытаемся оценить \(f\):

+

\[ +\begin{align} + \begin{aligned} + e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ + & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ + e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ + f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ + & = e^{-\frac{n(n-1)}{2d}} & \\ + & \simeq e^{-\frac{n^2}{2d}} & \\ + \end{aligned} +\end{align} +\]

+

Из формулы более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее:

+

Утверждение. В мультимножество нужно добавить \(O(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

+

Если вы не доверяете математике, посмотрите на графики:

+
import matplotlib.pyplot as plt
+%matplotlib inline
+
+import seaborn as sns
+sns.set()
+
def f(n, d):
+    p = 1
+    for x in range(1, n):
+        p *= 1-x/d
+    return p
+
+def g(n, d):
+    return 2.71**(-0.5 * n**2 / d)
+
exact = [f(n, 365) for n in range(40)]
+approx = [g(n, 365) for n in range(40)]
+plt.plot(exact)
+plt.plot(approx)
+plt.show()
+
+png

png

+
+

На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать \(n \approx \sqrt{d}\), построим такую же для больших чисел и перейдём в логарифмический масштаб.

+
approx = [g(n, 10**10) for n in range(10**6)]
+plt.xscale('log')
+plt.plot(approx)
+plt.show()
+
+png

png

+
+

\(\rho\)-алгоритм Полларда

+

Итак, мы хотим факторизовать число \(n\). Предположим, что \(n = p q\) и \(p \approx q\). Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей.

+

Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например \(f(x) = (x+1)^2 \mod n\).

+

Как выглядит граф, в котором из вершины есть единственное ребро \(x \to f(x)\)? Такой граф называется функциональным. Если в нём нарисовать «траекторию» произвольного элемента, — какой-то путь, превращающийся в цикл, — то получится что-то похожее на букву \(\rho\) (ро). Алгоритм из-за этого так и назван.

+

Рассмотрим траекторию какого-нибудь элемента \(x_0\): {\(x_0\), \(f(x_0)\), \(f(f(x_0))\), \(\ldots\)}. Теперь сделаем из неё новую последовательность, взяв каждый элемент по модулю \(p\) — наименьшего из простых делителей \(n\).

+

Утверждение. Ожидаемая длина цикла в этой последовательности \(O(\sqrt[4]{n})\).

+

Доказательство: так как \(p\) — меньший делитель, то \(p \leq \sqrt{n}\). Теперь просто подставим в предыдущее утверждение: в множество нужно добавить \(O(\sqrt{p}) = O(\sqrt[4]{n})\) элементов, чтобы какие-то два совпали, а значит последовательность зациклилась.

+

Если мы найдём цикл в такой последовательности (то есть такие \(i\) и \(j\), что \(f^i(x_0) \equiv f^j(x_0)\)), то мы можем восстановить какой-то делитель \(n\), а именно \(\gcd(|f^i(x_0) - f^j(x_0)|, n)\) — это число меньше \(n\) и делится на \(p\).

+

Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя \(i\) и \(j\) (\(i = 2j\)) и проверять, что \(f^i(x_0) \equiv f^j(x_0) \pmod p\), что эквивалентно проверке \(\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1\).

+
from math import gcd
+from random import randint
+
def find_divisor(n, seed = 1, f = lambda x: (x+1)**2):
+    x = y = seed
+    d = 1
+    while d == 1:
+        x = f(f(x)) % n
+        y = f(y) % n
+        d = gcd(abs(x-y), n)
+    return d
+
n = 3*5*7 # = 105
+print([find_divisor(n, seed=i) for i in range(n)])
+
[3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3]
+
n = (10**9+7)*(10**9+9)
+print([find_divisor(n, seed=randint(1, n)) for _ in range(20)])
+
[1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009]
+

Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за \(O(\sqrt{n})\) (нужно добавить отсечение по времени).

+

Разное

+

Формально, мы показали, что алгоритм работает за \(O(\sqrt[4]{n} \log n)\) за счёт поиска \(\gcd\), но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет.

+

Пародокс дней рождения также можно использовать при подсчёте вероятности коллизии при хэшировании. Хороший модуль должен быть не менее квадрата количества хэшей, а чего-то вроде \(5 n^2\) уже почти всегда достаточно.

+ + diff --git a/web/ru/probability.html b/web/ru/probability.html new file mode 100644 index 0000000..edf0290 --- /dev/null +++ b/web/ru/probability.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + +

Ликбез по теорверу

+
    +
  • Вероятностные распределения
  • +
  • Матожидание и дисперсия
  • +
  • Парадокс дней рождения
  • +
  • Нормальное распределение
  • +
  • Правдоподобие и оценка качества предсказания
  • +
  • Энтропия и оптимальное кодирование
  • +
+

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

+

(Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.)

+
import numpy as np
+
+import matplotlib as plt
+%matplotlib inline
+
+import seaborn as sns
+sns.set()
+

Матожидание

+

Дисперсия

+

Какие два числа лучше всего описывают распределение?

+

Нормальное распределение

+

Центральная предельная теорема названа так пафосно вполне обоснованно.

+

Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию.

+

\[ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} \]

+

Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса.

+

Трудно даже доказать, что это распределение, т. е. что \(\int_{-\inf}^\inf f(x) dx = 1\).

+

Применения

+

Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный.

+

Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как

+

Линейные рекурренты

+

Чтобы решать следующие задачи, нам нужно будет использовать следующий факт:

+

...

+

Доказательство мы не приведем.

+

В частности, таким образом получается формула для чисел Фибоначчи.

+

\[ f_n = \ldots \]

+

Кто бы мог подумать, что все эти иррациональности и степени сократятся и вообще дадут целое число?..

+

Классика

+

Парадокс дней рождения.

+

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

+

Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт?

+

TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. Казино. Мы приходим в казино с 1000$ и следующим образом проводим там время: ставим по 1$, пока не обанкротимся или не выиграем 1100$. Какая вероятность того, что мы уйдём с деньгами?

+

Принцип максимального правдоподобия

+

Энтропия

+

Энтропией называется минимальное число бит, которым теоретически возможно сжать сообщение. Эта величина важна, потому что на практике если её можно посчитать, то сжатие с соответствующей кратностью реально достижимо.

+

Шумный канал.

+

Пусть у вас есть 1тб данных и два китайских терабайтника, на каждый из которых можно записать столько данных, но каждый бит имеет вероятность 10% записаться на противоположный. Требуется сохранить данные с первого раза без потерь. Совсем без потерь.

+

Причём это делается почти впритык.

+ + diff --git a/web/ru/reciprocal.html b/web/ru/reciprocal.html new file mode 100644 index 0000000..5a0b542 --- /dev/null +++ b/web/ru/reciprocal.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + +

Обратный элемент по модулю

+

Часто в задачах требуется посчитать что-то по простому модулю (чаще всего \(10^9 + 7\)). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче.

+

Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например:

+

c++ c = (a + b) % mod; c = (mod + a - b) % mod; c = a * b % mod;

+

Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: \(\frac{8}{2} = 4\), но \(\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4\).

+

Нужно найти некоторый элемент, который будет себя вести как \(\frac{1}{a} = a^{-1}\), и вместо «деления» домножать на него. Назовем такой элемент обратным.

+

Способ 1: бинарное возведение в степень

+

Если модуль \(p\) простой, то решением будет \(a^{-1} \equiv a^{p-2}\). Это следует из малой теоремы Ферма:

+

Теорема. \(a^p \equiv a \pmod p\) для всех \(a\), не делящихся на \(p\).

+

Доказательство. (для понимания несущественно, можно пропустить)

+

\[ +\begin{align} +a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p +\\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} +\\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} +\\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} +\\ &= a +\end{align} +\]

+

Здесь \(P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}\) это мультиномиальный коеффициент — количество раз, которое элемент \(a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}\) появится при раскрытии скобки \((a_1 + a_2 + \ldots + a_n)^k\).

+

Теперь два раза «поделим» наш результат на \(a\).

+

\[ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} \]

+

Получается, что \(a^{p-2}\) ведет себя как \(a^{-1}\), что нам по сути и нужно. Посчитать \(a^{p-2}\) можно за \(O(\log p)\) бинарным возведением в степень.

+

Приведем код, который позволяет считает \(C_n^k\).

+

```c++ int t[maxn]; // факториалы, можно предподситать простым циклом

+

// бинарное возведение в степень int bp (int a, int n) { int res = 1; while (n) { if (n & 1) res = res * a % mod; a = a * a % mod; n >>= 1; } return res; }

+

// находит обратный элемент как a^(p-2) int inv (int x) { return bp(x, mod-2); }

+

int c (int n, int k) { return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod; } ```

+

Способ 2: диофантово уравнение

+

Диофантовыми уравнениями называют такие штуки:

+

\[ ax + by = 1 \]

+

Требуется решить их в целых числах, то есть \(a\) и \(b\) известны, и нужно найти такие целые (возможно, отрицательные) \(x\) и \(y\), чтобы равенство выполнялось. Решают такие вещи расширенным алгоритмом Евклида. TODO: описать, как он работает.

+

Подставим в качестве \(a\) и \(b\) соответственно \(a\) и \(m\)

+

\[ ax + my = 1 \]

+

Одним из решений уравнения и будет \(a^{-1}\), потому что если взять уравнение по модулю \(m\), то получим

+

\[ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m \]

+

Преимущества этого метода над возведением в степень: * Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. * Алгоритм проще выполнять руками.

+

Сам автор почти всегда использует возведение в степень.

+

Почему \(10^9+7\)?

+
    +
  1. Это выражение довольно легко вбивать (1e9+7).
  2. +
  3. Простое число.
  4. +
  5. Достаточно большое.
  6. +
  7. int не переполняется при сложении.
  8. +
  9. long long не переполняется при умножении.
  10. +
+

Кстати, \(10^9 + 9\) обладает теми же свойствами. Иногда используют и его.

+

Предподсчёт обратных факториалов за линейное время

+

Пусть нам нужно зачем-то посчитать все те же \(C_n^k\), но для больших \(n\) и \(k\), поэтому асимптотика \(O(n \log m)\) нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам.

+

Если у нас уже написан inv, то нам не жалко потратить \(O(\log m)\) операций, посчитав \(m!^{-1}\).

+

После этого мы будем считать \((m-1)!^{-1}\) как \(m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}\).

+

```c++ int f[maxn]; f[0] = 1; for (int i = 1; i < maxn; i++) f[i] = i*f[i-1] % mod;

+

int r[maxn]; r[maxn-1] = inv(f[maxn-1]) for (int i = maxn-1; i >= 1; i--) r[i-1] = r[i]*i % mod; ```

+

TODO: техника с сайта емакса.

+ + diff --git a/web/ru/segtree.html b/web/ru/segtree.html new file mode 100644 index 0000000..3bcae1f --- /dev/null +++ b/web/ru/segtree.html @@ -0,0 +1,131 @@ + + + + + + + + + + + + +

Дерево отрезков

+

Замечание. Почти везде мы будем использовать полуинтервалы — обозначаемые как \([l, r)\) — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля.

+

Дерево отрезков — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках.

+

Рассмотрим конкретную задачу:

+
+

Дан массив \(a\) из \(n\) целых чисел, нужно уметь отвечать на запросы двух типов:

+
+
+
    +
  1. Изменить значение в ячейке (т. е. отреагировать на присвоение a[k] = x).
  2. +
  3. Вывести сумму элементов \(a_i\) на отрезке с \(l\) по \(r\).
  4. +
+
+
+

Оба запроса нужно обрабатывать за время \(O(\log n)\).

+
+

Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:

+

Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.

+

Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку \([0, n)\), а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков».

+
+alt text

alt text

+
+

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

+

Разные свойства

+

Высота такого дерева есть величина \(\Theta(\log n)\): на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики.

+

Более того, любой полуинтервал разбивается на \(O(\log n)\) неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков.

+

Дерево также содержит менее \(2n\) вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет \(n\). Таким образом, число вершин в худшем случае оценивается суммой \(n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} + \ldots + 1 < 2n\). Значит, оно линейное по памяти.

+

При \(n\), отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при \(n=3\) левый сын корня есть отрезок \([0, 2)\), имеющий двух потомков, в то время как правый сын корня — отрезок \([2, 3)\), являющийся листом.

+

Ок, как это нам поможет?

+

Опишем теперь, как с помощью такой структуры решить задачу.

+

Запрос обновления. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению \(a[k] = x\).

+

Изменим все вершины, в суммах которых участвует \(k\)-тый элемент. Их будет \(\Theta(\log n)\) — по одной с каждого уровня.

+

Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит \(k\)-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков.

+

Запрос суммы. Мы знаем, что во всех вершинах лежат корректные значения.

+

Сделаем тоже рекурсивную функцию, рассмотрев три случая:

+
    +
  • Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму.
  • +
  • Если отрезки вершины и запроса не пересекаются, то вернуть 0.
  • +
  • Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей.
  • +
+

Чтобы разобраться, почему это работает за \(O(\log n)\), нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в \(O(\log n)\) отрезках, а значит и итоговая асимптотика будет такая же.

+

Ликбез по C++

+

Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества.

+

Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел.

+

Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за \(O(\log n)\).

+

В C++ есть два способа объявлять классы (объект — это экземпляр класса): через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

+

У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например конструктор — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса.

+

c++ struct A { int param1, param2; // тут можно что-то хранить char param3 = 'k'; A (int var) { // эта часть называется конструктором // ... } void do_something () { // это какой-то другой метод // ... } }; // <- не забудьте точку с запятой

+

Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (аллокатор) выделяет место в массиве (оперативной памяти) под этот объект и возвращает позицию (указатель) на место в этом массиве.

+

Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:

+

```c++ A x(179); x.do_something(); x.param1 = 57;

+

A *y = new A(42); // new возвращает адрес, по которому можно найти объект y->do_something(); y.param3 = '!'; ```

+

Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем \(2^{32}\) байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с long long быстрее считались

+

Реализация

+

Общий план реализации любых структур данных: 1. Полностью понять все инварианты — как должна выглядеть структура, какие значения должны принимать поля, etc. 2. Формально описать, что должны делать методы и за какую асимптотику. 3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты.

+

c++ struct segtree { int lb, rb; // левые и правые границы отрезков int sum = 0; // сумма на текущем отрезке segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { // если не лист, создаем детей int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) // если мы лежим полностью в отрезке запроса, вывести сумму return sum; if (max(lb, lq) >= min(rb, rq)) // если мы не пересекаемся с отрезком запроса, вывести ноль return 0; // иначе всё сложно -- запускаемся от детей и пусть они там сами решают return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

+
+

Посчитать число беспорядков в перестановке из \(n\) элементов (беспорядок или инверсия — это пара чисел \(i < j\), для которых \(p_i > p_j\)).

+
+

Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на \(n\) элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число \(x\), будем делать две вещи: * Запросим сумму от \(k\) до \(n\) в ДО. * Добавим единичку в \(k\)-тую позицию в ДО.

+

Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за \(O(n \log n)\) запросов.

+
+

Даны \(n\) точек на плоскости с целыми координатами от 1до \(n\). Требуется ответить на \(m\) запросов количества точек на прямоугольнике.

+
+

Ответим на все запросы в оффлайн, используя метод сканирующей прямой:

+
    +
  • Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике \([x_1, x_2] \times [y_1, y_2]\) равна сумме на прямоугольнике \([0, x_2] \times [y_1, y_2]\) минус сумма на прямоугольнике \([0, x_1] \times [y_1, y_2]\).
  • +
  • Отсортируем теперь все точки и префиксные запросы по их \(x\). При этом, если у точки и запроса одинаковый \(x\), то точка должна идти раньше.
  • +
  • Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в \(y_i\)» и «вывести сумму с \(y_1\) по \(y_2\)».
  • +
+

Отложенные операции

+

Пусть теперь наш запрос обновления — это присвоение значения \(x\) всем элементам некоторого отрезка \([l, r)\), а не только одному.

+

Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем.

+

Например, если пришел запрос «присвой число \(x\) на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено.

+

Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.

+

Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за \(O(n \log n)\).

+

При реализации создадим вспомогательную функцию push, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.

+

c++ struct segtree { int lb, rb; int sum = 0, assign = -1; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void push () { if (assign != -1) { sum = (rb-lb) * assign; if (l) { // если дети есть l->assign = assign; r->assign = assign; } } assign = -1; } void upd (int lq, int rq, int x) { push(); if (lq <= lb && rb <= rq) assign = x; else if (l && max(lb, lq) < min(rb, rq)) { // если есть дети и отрезок запроса хоть как-то пересекается с нашим l->upd(lq, rq, x); r->upd(lq, rq, x); // ...дальше они сами разберутся } } int get_sum (int lq, int rq) { push(); if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

+

По-английски эта техника называется lazy propagation. Очень важно научиться её писать — она часто встречается на олимпиадах.

+

Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни.

+

Динамическое построение

+

А что, если у нас все индексы лежать не от в пределах \(10^5\), а, например, \(10^9\). Все асимптотики нас по прежнему устраивают (\(\log_2 10^6 \approx 20\), \(\log_2 10^9 \approx 30\)), кроме этапа построения.

+

Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.

+

Реализовать это можно так же, как и с push-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так.

+

c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; // а тут ничего нет } void extend () { if (!l && lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { extend(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; extend(); return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

+

Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:

+

c++ vector<int> compress (vector<int> a) { vector<int> b = a; sort(b.begin(), b.end()); b.erase(unique(b.begin(), b.end()), b.end()); for (int &x : a) x = int(lower_bound(b.begin(), b.end(), x) - b.begin()); return a; }

+

Персистентность

+

Структуры данных называют персистентными, если их можно быстро «откатить» до произвольного предыдущего состояния.

+

Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент \(t\) никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.

+

У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать \(O(\log n)\) новых вершин на запрос, что означает общее потребление памяти \(O(m \log n)\).

+

c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb != rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void copy () { if (l) { l = new segtree(l); r = new segtree(r); } } void add (int k, int x) { copy(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { // этот метод ничего не меняет -- он и так хороший if (lq <= lb && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

+
+

Даны \(n\) точек на плоскости. Нужно в онлайн ответить на \(q\) запросов суммы на прямоугольнике.

+
+

Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения \(x_i\) и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с \(O(n \log n)\) памяти.

+
+

Дан отрезок из \(n\) чисел от 1 до \(n\). Требуется ответить на \(q\) запросов \(k\)-той порядковой статистики на подотрезке.

+
+

Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент \(k\), добавим единицу к \(k\)-ому элементу.

+

Дальше определим разность деревьев как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо sum использовать везде sum_r - sum_l.

+

Что будет находиться в разности \(r\)-го и \(l\)-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает \(k\) — она и будет ответом.

+
+

Дан массив из \(n\) элементов. Требуется ответить на \(m\) запросов, есть ли на отрезке \([l, r]\) доминирующий элемент — тот, который встречается на нём хотя бы \(\frac{r-l}{2}\) раз.

+
+

У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за \(O(\log n)\), посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна \(\frac{1}{2^{100}}\), и ей на практике можно пренебречь.

+

Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее \(\frac{n}{2}\) в массиве на \(n\) элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем true, иначе false.

+

Другие реализации

+

Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:

+

На массивах. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у Емакса.

+

«ДО снизу». Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с CodeForces.

+

Задачи

+ + + diff --git a/web/ru/sparse-table.html b/web/ru/sparse-table.html new file mode 100644 index 0000000..bd8d5f7 --- /dev/null +++ b/web/ru/sparse-table.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + +

Разреженная таблица

+
    +
  • Нужна для нахождения минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) с малой константой.
  • +
  • Обновления не поддерживает (static RMQ).
  • +
  • Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.
  • +
  • Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).
  • +
  • Требует \(O(n \log n)\) памяти.
  • +
  • Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ.
  • +
+

Определим разреженную таблицу как двумерный массив размера \(n \times\log n\):

+

\[ +t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} +\]

+

Идея такая: считаем минимум на каждом отрезке длины \(2^k\).

+

Такой массив можно посчитать за его размер: \(t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])\). Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования.

+

Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.

+
+ +
+

Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

+

```c++ int a[maxn], lg[maxn], mx[maxn][logn];

+

int rmq (int l, int r) { int t = lg[r-l+1]; return min(mx[l][t], mx[r-(1<<t)+1][t]); }

+

// Это считается уже где-то в первых строчках main:

+

for (int l = 1; l < logn; l++) for (int i = (1<= 0; i--) { mx[i][0] = a[i]; for (int l = 0; l < logn-1; l++) mx[i][l+1] = max(mx[i][l], mx[i+(1<<l)][l]); } ```

+

2d Static RMQ

+

Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

+

В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

+ + diff --git a/web/ru/sse.html b/web/ru/sse.html new file mode 100644 index 0000000..57523b3 --- /dev/null +++ b/web/ru/sse.html @@ -0,0 +1,365 @@ + + + + + + + + + + + +

Streaming SIMD Extensions

+

This is

+
processor   : 0
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 945.615
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 0
+cpu cores   : 6
+apicid      : 0
+initial apicid  : 0
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+

c++ !cat /proc/cpuinfo | less -20

+
processor   : 0
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3422.395
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 0
+cpu cores   : 6
+apicid      : 0
+initial apicid  : 0
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 1
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3702.863
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 1
+cpu cores   : 6
+apicid      : 2
+initial apicid  : 2
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 2
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3556.265
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 2
+cpu cores   : 6
+apicid      : 4
+initial apicid  : 4
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 3
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3512.268
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 3
+cpu cores   : 6
+apicid      : 6
+initial apicid  : 6
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 4
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3642.787
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 4
+cpu cores   : 6
+apicid      : 8
+initial apicid  : 8
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 5
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3136.167
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 5
+cpu cores   : 6
+apicid      : 10
+initial apicid  : 10
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 6
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3535.684
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 0
+cpu cores   : 6
+apicid      : 1
+initial apicid  : 1
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 7
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 2251.174
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 1
+cpu cores   : 6
+apicid      : 3
+initial apicid  : 3
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 8
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 2202.563
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 2
+cpu cores   : 6
+apicid      : 5
+initial apicid  : 5
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 9
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 2413.537
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 3
+cpu cores   : 6
+apicid      : 7
+initial apicid  : 7
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 10
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 1974.963
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 4
+cpu cores   : 6
+apicid      : 9
+initial apicid  : 9
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 11
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3336.891
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 5
+cpu cores   : 6
+apicid      : 11
+initial apicid  : 11
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+ + diff --git a/web/ru/stress-test.html b/web/ru/stress-test.html new file mode 100644 index 0000000..b5f725e --- /dev/null +++ b/web/ru/stress-test.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + +

Стресс-тестирование

+
    +
  • Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений
  • +
  • Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написанно решение на маленькие подгруппы
  • +
+

Суть такая: * Есть решение smart — быстрое, но в котором есть баг, который хотим найти * Пишем решение stupid — медленное, но точно корректное * Пишем генератор gen — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются

+

Примерный код скрипта:

+
import os, sys
+
+f1, f2, gen, iters = sys.argv
+
+for i in range(int(iters)):
+    print('Test', i+1)
+    os.popen('python3 %s > test.txt' % gen)
+    v1 = os.popen('./%s < test.txt' % f1).read()
+    v2 = os.popen('./%s < test.txt' % f2).read()
+    if v1 != v2:
+        print test
+        print("Correct:")
+        print v1
+        print("Wrong:")
+        print v2
+        break
+

Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py.

+

Скрипт написан под Linux. Для Windows нужно убрать «./» во всех системных вызовах.

+

gen.py автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через ./gen. Пример gen-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10:

+
from random import randint, choice
+
+n = randint(1, 10)
+
+print(n)
+
+for _ in range(n):
+    print(choice('abc'), end='')
+ + diff --git a/web/ru/strings.html b/web/ru/strings.html new file mode 100644 index 0000000..3ecce74 --- /dev/null +++ b/web/ru/strings.html @@ -0,0 +1,147 @@ + + + + + + + + + + + + + +

Базовые строковые алгоритмы

+

Префикс-функция

+

Рассмотрим задачу, которая возникает каждый раз, когда вы делаете ctrl+f:

+
+

Есть большой текст \(t\). Нужно найти все вхождения строки \(s\) в него.

+
+

Наивное решение со сравнением всех подстрок \(t\) длины \(|s|\) со строкой \(s\) работает за \(O(|t| \cdot |s|)\). Если текст большой, то длинные слова в нем искать становится очень долго.

+

Для решения этой задачи за линейное время придумали префикс-функцию.

+

Определение. Префикс-функцией от строки \(s\) называется массив \(p\), где \(p_i\) равно длине самого большого префикса строки \(s_0 s_1 s_2 \ldots s_i\), который также является и суффиксом этой строки (не считая всю строку).

+

Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна \([0, 1, 0, 1, 2, 3, 4, 5]\).

+
def slow_prefix_function(s):
+    n = len(s)
+    p = [0]*n
+    for i in range(n):
+        prefix = s[:i]
+        for l in range(1, i):
+            if prefix[:l] == prefix[-l:]:
+                p[i] = l
+    return p
+
+slow_prefix_function('aataataa')
+
[0, 0, 1, 0, 1, 2, 3, 4]
+

(Этот алгоритм работает за \(O(n^3)\), но это только пока.)

+

Как это поможет решить исходную задачу?

+

Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.

+

Соединим подстроки \(s\) и \(t\) каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки \(s\#t\).

+
s = "let it go"
+t = """let it go, let it go
+can't hold it back anymore
+let it go, let it go
+turn away and slam the door!"""
+
+print((s + '#' + t).replace('\n', ' '))
+print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))
+
let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!
+00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000
+

Видно, что все места, где значения равны 9 (длине S) — это концы вхождений \(s\) в текст \(t\).

+

Такой алгоритм (посчитать префикс-функцию от \(s\#t\) и посмотреть, в каких позициях она равна \(|s|\)) называется алгоритмом Кнута-Морриса-Пратта.

+

Как её быстро считать

+

Рассмотрим ещё несколько примеров префикс-функций:

+
for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:
+    print(slow_prefix_function(s))
+
[0, 0, 1, 2, 3]
+[0, 0, 0, 0, 0, 0]
+[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]
+[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+

Можно заметить несколько особенностей: * \(p_0 = 0\) для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; * \(p_{i+1}\) максимум на единицу превосходит \(p_i\): если есть префикс, равный суффиксу строки \(s_0 s_1 \ldots s_{i+1}\) длины \(p_{i+1}\), то, отбросив последний символ, можно получить и правильный суффикс для строки \(s_0 s_1 \ldots s_i\), длина которого будет ровно на единицу меньше.

+

Хочется придумать алгоритм, как считать префикс-функцию за \(O(N)\). Логично это делать с помощью динамики: найти формулу для \(p_i\) через предыдущие значения.

+

Заметим, что \(p_{i+1} = p_i + 1\) в том и только том случае, когда \(s_{p_i} =s_{i+1}\). Например, в строке \(\underbrace{aabaa}t\overbrace{aabaa}\) выделен максимальный префикс, равный суффиксу: \(p_{10} = 5\). Если следующий символ равен будет равен \(t\), то \(p_{11} = p_{10} + 1 = 6\).

+

Но что происходит, когда \(s_{p_i}\neq s_{i+1}\)? Пусть следующий символ в этой же строке равен \(b\). * \(\implies\) Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. * \(\implies\) Значит, помимо того, что он является суффиксом «aabaab», префикс является префиксом и подстроки «aabaa». * \(\implies\) Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть \(p_4 = 2\). * \(\implies\) Если \(s_2 = s_{11}\) (т. е. новый символ совпадает с идущим после префикса-кандидата), то \(p_{11} = p_2 + 1 = 2 + 1 = 3\).

+

В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, \(p_{i+1} \neq p_{p_i+1} \)? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — \(p_{p_{p_i}}\). Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым.

+
def fast_prefix_function(s):
+    n = len(s)
+    p = [0]*n
+    for i in range(1, n):
+        cur = p[i - 1]
+        # перебираем префикс-функцию, пока не найдем равный символ
+        while s[i] != s[cur] and cur > 0:
+            cur = p[cur - 1]
+         # если нашли, то значение на единицу больше
+        if s[i] == s[cur]:
+            p[i] = cur + 1
+    return p
+
+fast_prefix_function('abacabadabacabax')
+
[0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]
+

Асимптотика

+

Почему это работает за \(O(N)\)? В худшем случае этот while может работать \(O(N)\) раз за одну итерацию. Но оказывается, что в среднем каждый while работает за \(O(1)\) — это называется амортизированной асимптотикой.

+

Доказательство: * Как мы ранее заметили, префикс функция возрастает максимум на единицу. * \(\implies\) Вырасти она может максимум \(n-1\) раз. * Если мы зашли в while, то каждая его итерация понижает значение префикс-функции хотя бы на один. * \(\implies\) Количество понижений не больше количества повышений, то есть \(O(n)\). * \(\implies\) Суммарно итераций цикла while\(O(n)\).

+

Z-функция

+

Альтернатива префикс-функции — z-функция (примечание: не «зи», а «зет»). Она немного проще для понимания.

+

Z-функция от строки \(s\) — это такой массив \(z\), что \(z_i\) равно длине максимальной подстроки, начинающейся с \(i\)-й позиции, которая равна префиксу \(s\).

+

\[\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)\]

+
def slow_z_function(s):
+    n = len(s)
+    z = [0]*n
+    for i in range(1, n):
+        suffix = s[i:]
+        for l in range(1, i):
+            if s[:l] == suffix[:l]:
+                z[i] = l
+    return z
+
+for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:
+    print(slow_z_function(s))
+
[0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]
+[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]
+[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]
+

Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c \(s\), а не заканчиваться. Осталось научиться её искать за \(O(n)\).

+

Как её быстро считать

+

Заметим, что: * \(z_0 = 0\) — из-за договоренности (потому что информации не несет); * \(z_i = 0 \iff s_i \neq s_0\); * \(z_i > 0 \iff s_i = s_0\).

+

Будем идти слева направо и хранить z-блок — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как \(l\) и \(r\).

+

Пусть мы сейчас хотим найти \(z_i\), а все предыдущие уже нашли. Если новый, \(i\)-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. * Если правее, то мы просто наивно перебором найдем \(z_i\) (максимальный отрезок, начинающийся с \(s_i\) и равный префиксу), и объявим его новым z-блоком. * Если \(i\)-й элемент лежит внутри z-блока, то мы можем посмотреть на значение \(z_{i-l}\) и использовать его, чтобы инициализировать \(z_i\) чем-то, возможно, отличным от нуля. Если \(z_{i-l}\) «не хватает» до границы \(z\)-блока, то \(z_i = z_{i-l}\). Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку.

+
def fast_z_function(s):
+    n = len(s)
+    z = [0]*n
+    l = 0
+    r = 0
+    for i in range(1, n):
+        if i <= r:
+            z[i] = min(r - i + 1, z[i-l])
+        while i + z[i] < n and s[z[i]] == s[i+z[i]]:
+            z[i] += 1
+        if i + z[i] - 1 > r:
+            l = i
+            r = i +  z[i] - 1
+    return z
+

Асимптотика. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это \(O(n)\).

+

Зачем тогда люди используют префикс-функцию

+
+hz

hz

+
+

В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.

+

Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение.

+ + diff --git a/web/ru/suffix-array.html b/web/ru/suffix-array.html new file mode 100644 index 0000000..35f4feb --- /dev/null +++ b/web/ru/suffix-array.html @@ -0,0 +1,91 @@ + + + + + + + + + + + + +

Суффиксный массив

+

Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.

+

Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.

+

+* +
Паблик с тупыми шутками про проганье» +
+
    +
  • +
+

Мотивация

+

Суффиксным массивом строки \(s\) называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки.

+

+

Как это использовать. Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно.

+

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

+

Работать такой алгоритм будет за \(O(|t| \log |s|)\), и позже это можно будет оптимизировать до \(O(|t| + \log |s|)\), что является одним из самых оптимальных алгоритмов поиска.

+

Теперь научимся его строить.

+

Построение за \(O(n \log n)\)

+

Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара.

+

Мы могли бы просто взять перестановку от \(0\) до \(n\), написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в std::sort, что будет работать за \(O(n^2 \log n)\), потому что внутреннее сравнение работает за \(O(n)\). Однако, если сравнивать суффиксы хэшами, то уже тут можно получить \(O(n \log^2 n)\). Но это не самый быстрый и удобный алгоритм.

+

Наш алгоритм будет состоять из \(\lceil \log n \rceil\) этапов. На \(k\)-том этапе мы будем рассматривать циклические подстроки длины \(2^k\). На последнем этапе мы отсортируем строки длины \(\geq n\) (это легально — они ведь циклические), и мы получим нужный суффиксный массив.

+

Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки \(p\) индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции \(i\) с длиной 2^k, номер \(c_i\) класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие \(c_i\)). Количество классов эквивалентности будем хранить в переменной cls (изначально она равна количеству различных символов).

+

Пример: \(s = aaba\). Этапов будет 3: для подстрок длины 1, 2 и 4.

+

\[ + p_0 = (0, 1, 3, 2) \;\;\; c_0 = (0, 0, 1, 0) +\\ p_1 = (0, 3, 1, 2) \;\;\; c_1 = (0, 1, 2, 0) +\\ p_2 = (3, 0, 1, 2) \;\;\; c_2 = (1, 2, 3, 0) +\]

+

Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины \(2^0 = 1\), то есть просто символы. Это мы сделаем сортировкой подсчётом.

+

Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней std::sort со своим компаратором.

+

Как быстро сравнить две подстроки? Мы можем использовать \(c_i\) — каждой строке длины \(2^k\) сопоставить биграмму (строку из двух символов), а именно строка \(s[i..i+2^k-1]\) с точки зрения сортировки будет эквивалентна паре \((c_i, c_{i+2^{k-1}})\). Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за \(O(1)\). Однако, это всё ещё будет работать за \(O(n \log^2 n)\), потому что каждый этап будет работать за \(O(n \log n\)).

+

Оптимизация до \(O(n \log n)\). Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся цифровой сортировкой — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив \(p\) с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в \(p\) отнять \(2^{k-1}\). Таким образом, можно проводить этап за \(O(n)\).

+

```c++ // строка -- это последовательность чисел от 1 до размера алфавита vector suffix_array (vector &s) { s.push_back(0); // добавляем нулевой символ в конец строки int n = (int) s.size(), cnt = 0, // вспомогательная переменная: счётчик для сортировки cls = 0; // количество классов эквивалентности vector c(n), p(n);

+
map< int, vector<int> > t;
+for (int i = 0; i < n; i++)
+    t[s[i]].push_back(i);
+
+// «нулевой» этап
+for (auto &x : t) {
+    for (int u : x.second)
+        c[u] = cls, p[cnt++] = u;
+    cls++;
+}
+
+// пока все суффиксы не стали уникальными
+for (int l = 1; cls < n; l++) {
+    vector< vector<int> > a(cls);  // массив для сортировки подсчётом
+    vector<int> _c(n);  // новые классы эквивалентности
+    int d = (1<<l)/2;
+    int _cls = cnt = 0;  // новое количество классов
+    
+    for (int i = 0; i < n; i++) {
+        int k = (p[i]-d+n)%n;
+        a[c[k]].push_back(k);
+    }
+    
+    for (int i = 0; i < cls; i++) {
+        for (size_t j = 0; j < a[i].size(); j++) {
+            // если суффикс начинает новый класс эквивалентности
+            if (j == 0 || c[(a[i][j]+d)%n] != c[(a[i][j-1]+d)%n])
+                _cls++;
+            _c[a[i][j]] = _cls-1;
+            p[cnt++] = a[i][j];
+        }
+    }
+    
+    c = _c;
+    cls = _cls;
+}
+
+return vector<int>(p.begin()+1, p.end());
+

} ```

+

TODO: переписать это

+

Наибольшие общие префиксы

+

Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы делали с хэшами.

+

TODO

+ + diff --git a/web/ru/treap.html b/web/ru/treap.html new file mode 100644 index 0000000..a56b3c7 --- /dev/null +++ b/web/ru/treap.html @@ -0,0 +1,162 @@ + + + + + + + + + + + + +

Декартово дерево

+

Рене Декарт (фр. René Descartes) — великий французский математик и философ XVII века.

+

Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.

+

Декартово дерево же определяется и строится так:

+
    +
  • Нанесём на плоскость набор из \(n\) точек. Их \(x\) зачем-то назовем ключем, а \(y\) приоритетом.
  • +
  • Выберем самую верхнюю точку (с наибольшим \(y\), а если таких несколько — любую) и назовём её корнем.
  • +
  • От всех вершин, лежащих слева (с меньшим \(x\)) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.
  • +
  • Аналогично, запустимся от правой части и добавим корню правого сына.
  • +
+

Заметим, что если все \(y\) и \(x\) различны, то дерево строится однозначно.

+

Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:

+
+treap

treap

+
+

Таким образом, декартово дерево — это одновременно бинарное дерево по \(x\) и куча по \(y\). Поэтому ему придумали много альтернативных названий:

+
    +
  • Дерамида (дерево + пирамида)
  • +
  • ПиВо (пирамида + дерево)
  • +
  • КуРево (куча + дерево)
  • +
  • Treap (tree + heap)
  • +
+

## Бинарные деревья

+

С небольшими модификациями, декартово дерево умеет всё то же, что и любое бинарное дерево поиска, например:

+
    +
  • добавить число \(x\) в множество
  • +
  • определить, есть ли в множестве число \(x\)
  • +
  • найти первое число, не меньшее \(x\) (lower_bound)
  • +
  • найти количество чисел в промежутке \([l, r]\)
  • +
+

При этом все операции — за \(O(\log n)\).

+

На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до \(O(\log n)\).

+

Приоритеты и асимптотика

+

В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты (\(y\)) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска.

+

Теорема. Ожидание глубины вершины в декартовом дереве равно \(O(n \log n)\).

+

Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.

+

Доказательство. Введем функцию \(a(x, y)\) равную единице, если \(x\) является предком \(y\), и нулем в противном случае. Такие функции называются индикаторами*.

+

Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна

+

\[d_i = \sum_{j=1}^n a(j, i)\]

+

Её матожидание равно

+

\[E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)\]

+

где \(p(x, y)\) это веряотность, что \(a(x, y) = 1\). Здесь мы воспользовались важным свойством линейности: матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.

+

Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.

+

Лемма. Вершина \(x\) является предком \(y\), если у неё приоритет больше, чем у всех вершин из отрезка \((x, y]\) (без ограничения общности, будем считать, что \(x < y\)).

+

Необходимость. Если это не так, то где-то между \(x\) и \(y\) есть вершина с большим приоритетом, чем \(x\). Она не может быть потомком \(x\), а значит \(x\) и \(y\) будут разделены.

+

Достаточность. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком \(x\). Таким образом, всё, что справа от \(y\), ни на что влиять не будет.

+

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

+

\[p(x, y) = \frac{1}{y-x+1}\]

+

Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:

+

\[E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)\]

+

Перед последним переходом мы получили сумму гармонического ряда.

+

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

+

Упражнение*. Выведите из этого доказательства асимптотику quicksort.

+

Реализация

+

Декартово дерево удобно писать на указателях и структурах. Поэтому мы рассказали дерево отрезков на указателях, а не стандартную рекурсию на 5 параметрах. Не знаете, что это такое — посмотрите в предыдущем конспекте.

+

Создадим структуру Node, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву.

+

c++ struct Node { int key, prior; Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } };

+

Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся.

+

Merge

+

Принимает два дерева (два корня, \(L\) и \(R\)), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.

+

Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень \(L\) или правый \(R\) — просто возьмем тот, у кого приоритет больше.

+

Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном \(L\). С правым сыном сложнее: возможно, его нужно смерджить с \(R\). Поэтому рекурсивно сделаем merge(l->r, r) и запишем результат в качестве правого сына.

+

c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; if (l->prior > r->prior) { l->r = merge(l->r, r); return l; } else { r->l = merge(l, r->l); return r; } }

+

Split

+

Принимает дерево и ключ \(x\), по которому его нужно разделить на два: \(L\) должно иметь все ключи не больше \(x\), а \(R\) должно иметь все ключи больше \(x\).

+

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

+

```c++ typedef pair Pair;

+

Pair split (Node *p, int x) { if (!p) return {0, 0}; if (p->key <= x) { Pair q = split(p->r, x); p->r = q.first; return {p, q.second}; } else { Pair q = split(p->l, x); p->l = q.second; return {q.first, p}; } } ```

+

Пример: вставка

+

merge и split сами по себе не очень полезные, но помогут написать все остальное.

+

Вот так, например, будет выглядеть код, добавляющий \(x\) в сет.

+

```c++ Node *root = 0;

+

void insert (int x) { Pair q = split(root, x); Node *t = new Node(x); root = merge(q.first, merge(t, q.second)); } ```

+

Пример: модификация для суммы на отрезке

+

Иногда нам нужно написать какие-то модификации для более продвинутых операций.

+

Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке».

+

c++ struct Node { int val, sum; // ... };

+

При merge и split надо будет поддерживать эту сумму актуальной.

+

Вместо того, чтобы модифицировать и merge, и split под наши хотелки, напишем вспомогательные функцию upd, которую будем вызывать при обновлении детей вершины.

+

```c++ void sum (Node* v) { return v ? v->sum : 0; } // обращаться по пустому указателю нельзя -- выдаст ошибку

+

void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; } ```

+

В merge и split теперь можно просто вызывать upd перед тем, как вернуть вершину, и тогда ничего не сломается:

+

c++ Node* merge (Node *l, Node *r) { // ... if (...) { l->r = merge(l->r, r); upd(l); return l; } else { // ... } }

+

```c++ typedef pair Pair;

+

Pair split (Node *p, int x) { // ... if (...) { // ... upd(p); return {p, q.second}; } else { // ... } } ```

+

Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:

+

c++ int sum (int l, int r) { Pair rq = split(root, r); Pair lq = split(rq.first, l); int res = sum(lr.second); root = merge(lq.first, merge(lq.second, rq.second)); return res; }

+

Неявный ключ

+

Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.

+

Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.

+

Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:

+

c++ struct Node { int key, prior, size = 1; // ^ размер поддерева Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } };

+

Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины.

+

```c++ int size (Node *v) { return v ? v->size : 0; }

+

void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); } ```

+

merge не меняется, а вот в split нужно использовать позицию корня вместо его ключа.

+

Про split теперь удобнее думать как "вырежи первые k элементов".

+

```c++ typedef pair Pair;

+

Pair split (Node *p, int k) { if (!p) return {0, 0}; if (size(p->l) + 1 <= k) { Pair q = split(p->r, k - size(p->l) - 1); // ^ правый сын не знает количество вершин слева от него p->r = q.first; upd(p); return {p, q.second}; } else { Pair q = split(p->l, k); p->l = q.second; upd(p); return {q.first, p}; } } ```

+

Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно.

+

Пример: ctrl+x, ctrl+v

+

c++ Node* ctrlx (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l); root = merge(q2.first, q1.second); return q2.second; }

+

c++ void ctrlv (Node *v, int k) { Pair q = split(root, k); root = merge(q.first, merge(v, q.second)); }

+

Пример: переворот

+

Нужно за \(O(\log n)\) обрабатывать запросы переворота произвольных подстрок: значение \(a_l\) поменять с \(a_r\), \(a_{l+1}\) поменять с \(a_{r-1}\) и т. д.

+

Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:

+

c++ struct Node { bool rev; // ... };

+

Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:

+

c++ void push (node *v) { if (v->rev) { swap(v->l, v->r); if (v->l) v->rev ^= 1; if (v->r) v->rev ^= 1; } v->rev = 0; }

+

Аналогично, эту функцию будем вызывать в начале merge и split.

+

Саму функцию reverse реализуем так: вырезать нужный отрезок, поменять флаг.

+

c++ void reverse (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second->rev ^= 1; root = merge(q2.first, merge(q2.second, q1.second)); }

+

Функциональное программирование*

+

Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с \(l\) по \(r\), что-то с ним делаем и склеиваем обратно.

+

Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке.

+

```c++ auto apply (int l, int r, auto f) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second = f(q2.second); root = merge(q2.first, merge(q2.second, q1.second)); }

+

void reverse (Node *v) { if (v) v->rev ^= 1; } ```

+

Применять её нужно так:

+

c++ apply(l, r, reverse);

+

Это работает в плюсах, начиная с g++14.

+

Для простых операций можно даже написать лямбду:

+

c++ apply(l, r, [](Node *v){ if (v) v->rev ^= 1; });

+

Персистентность*

+

Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.

+
+

Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.

+
+

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

+
+

Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции и сравнение произвольных подстрок.

+
+

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

+

Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ.

+

Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию copy:

+

c++ Node* copy (Node *v) { return new Node(*v); }

+

Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный split начнётся так:

+

c++ Pair split (Node *p, int x) { p = copy(p); // ... }

+

В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.

+

У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен \(L\), а размер правого \(R\), то будем подвешивать за левое с вероятностью \(\frac{L}{L+R}\), иначе за правое.

+

Теорема. Такое переподвешивание эквивалентно приоритетам.

+

Доказательство. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции:

+
    +
  • Лист имеет вероятность 1 быть корнем себя (база индукции)
  • +
  • Переход индукции — операция merge. Любая вершина левого дерева была корнем с вероятностью \(\frac{1}{L}\) (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью \(\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}\). С вершинами правого дерева аналогично.
  • +
+

Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД.

+

c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; l = copy(l), r = copy(r); if (rand() % (size(l) + size(r)) < size(l)) { // ... } else { // ... } }

+

Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и \(x\), и \(y\)?

+ + diff --git a/web/ru/trie.html b/web/ru/trie.html new file mode 100644 index 0000000..6f151d5 --- /dev/null +++ b/web/ru/trie.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + +

Бор

+

Бор — это структура данных для компактного хранения строк.

+

Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин.

+
+trie

trie

+
+

Бор можно удобно использовать для разных задач: * Хранение строк — занимает гораздо меньше места, чем массив или сет строк. * Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.

+
    +
  • Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи.
  • +
+

Реализация

+

Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: * терминальная ли вершина, * ссылки на детей, * возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет.

+

```c++ const int k = 26;

+

struct Vertex { Vertex* to[k] = {0}; bool terminal = 0; };

+

Vertex *root = new Vertex(); ```

+

Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной.

+

c++ void add_string (string &s) { v = root; for (char c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(); v = v->to[c]; } v->terminal = true; }

+

Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.

+

Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности.

+

Как хранить ссылки

+

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

+

В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (map) или хэш-таблице (unordered_map). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У map-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например.

+

Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями.

+

Суффиксные ссылки

+
+

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

+
+

Эту и много других задач помогают решать суффиксные ссылки. Суффиксная ссылка для вершины \(v\) — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине \(v\), и присутствующий в боре. Будем считать, что мы их умеем быстро находить.

+

Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

+

Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за \(O(nk)\), где \(n\) и \(k\) это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в отдельную статью.

+ + From ee529851b4cdda8e169ad38fa47e9bfa3db9a9f8 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 4 Jun 2019 21:35:38 +0000 Subject: [PATCH 002/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- publish.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 publish.sh diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..d8e957e --- /dev/null +++ b/publish.sh @@ -0,0 +1,6 @@ +#!/bin/bash +jupyter-nbconvert articles/*.ipynb +rsync -r articles/*.{html,pdf,css} root@sereja.me:/var/www/html/a/ +git add * +git commit --allow-empty-message -m '' +git push From 8e97799d042854725b4742c853845ace1bc3a6ae Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 4 Jun 2019 22:09:59 +0000 Subject: [PATCH 003/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- build.sh | 2 +- publish.sh | 6 ------ web/en/index.html | 2 +- web/ru/aho-corasick.html | 2 +- web/ru/annealing.html | 2 +- web/ru/bayans.html | 2 +- web/ru/bitset.html | 2 +- web/ru/centroid.html | 2 +- web/ru/convex-hulls.html | 2 +- web/ru/cross-entropy.html | 2 +- web/ru/cuda.html | 2 +- web/ru/dp-intro.html | 2 +- web/ru/dp-optimizations.html | 2 +- web/ru/dp_intro.html | 2 +- web/ru/dp_optimizations.html | 2 +- web/ru/fenwick.html | 2 +- web/ru/games.html | 2 +- web/ru/geometry.html | 2 +- web/ru/hashing.html | 2 +- web/ru/hld.html | 2 +- web/ru/index.html | 2 +- web/ru/ioi.html | 2 +- web/ru/karatsuba.html | 2 +- web/ru/lca.html | 2 +- web/ru/linalg.html | 2 +- web/ru/matching.html | 2 +- web/ru/matroid.html | 2 +- web/ru/mincost-maxflow.html | 2 +- web/ru/mst.html | 2 +- web/ru/pollard.html | 2 +- web/ru/probability.html | 2 +- web/ru/reciprocal.html | 2 +- web/ru/segtree.html | 2 +- web/ru/sparse-table.html | 2 +- web/ru/sse.html | 2 +- web/ru/stress-test.html | 2 +- web/ru/strings.html | 2 +- web/ru/suffix-array.html | 2 +- web/ru/treap.html | 2 +- web/ru/trie.html | 2 +- 40 files changed, 39 insertions(+), 45 deletions(-) delete mode 100755 publish.sh diff --git a/build.sh b/build.sh index 829ed55..8ff88d7 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,3 @@ cp -r img web/img mkdir web/{ru,en} -find {ru,en} -iname "*.md" -type f -exec sh -c 'pandoc "${0}" -o "web/${0%.md}.html" --css /pandoc.css --standalone --mathjax' {} \; +find {ru,en} -iname "*.md" -type f -exec sh -c 'pandoc "${0}" -o "web/${0%.md}.html" --css ../pandoc.css --standalone --mathjax' {} \; diff --git a/publish.sh b/publish.sh deleted file mode 100755 index d8e957e..0000000 --- a/publish.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -jupyter-nbconvert articles/*.ipynb -rsync -r articles/*.{html,pdf,css} root@sereja.me:/var/www/html/a/ -git add * -git commit --allow-empty-message -m '' -git push diff --git a/web/en/index.html b/web/en/index.html index 3285e2a..aa69e89 100644 --- a/web/en/index.html +++ b/web/en/index.html @@ -6,7 +6,7 @@ - +

some english index

diff --git a/web/ru/aho-corasick.html b/web/ru/aho-corasick.html index 6458ec5..9509ecc 100644 --- a/web/ru/aho-corasick.html +++ b/web/ru/aho-corasick.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/annealing.html b/web/ru/annealing.html index 23f544d..1e08a54 100644 --- a/web/ru/annealing.html +++ b/web/ru/annealing.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/bayans.html b/web/ru/bayans.html index 0364d76..889aa32 100644 --- a/web/ru/bayans.html +++ b/web/ru/bayans.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/bitset.html b/web/ru/bitset.html index 570ceb1..a1dc9a8 100644 --- a/web/ru/bitset.html +++ b/web/ru/bitset.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/centroid.html b/web/ru/centroid.html index 247b2e8..aaae7ee 100644 --- a/web/ru/centroid.html +++ b/web/ru/centroid.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/convex-hulls.html b/web/ru/convex-hulls.html index 3040053..94f11cf 100644 --- a/web/ru/convex-hulls.html +++ b/web/ru/convex-hulls.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/cross-entropy.html b/web/ru/cross-entropy.html index 2d66255..9ad3dc1 100644 --- a/web/ru/cross-entropy.html +++ b/web/ru/cross-entropy.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/cuda.html b/web/ru/cuda.html index 3b90d98..6e19f33 100644 --- a/web/ru/cuda.html +++ b/web/ru/cuda.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/dp-intro.html b/web/ru/dp-intro.html index f879257..341ca37 100644 --- a/web/ru/dp-intro.html +++ b/web/ru/dp-intro.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/dp-optimizations.html b/web/ru/dp-optimizations.html index 6f58d86..ac2305b 100644 --- a/web/ru/dp-optimizations.html +++ b/web/ru/dp-optimizations.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/dp_intro.html b/web/ru/dp_intro.html index 4cbcdf8..94e888a 100644 --- a/web/ru/dp_intro.html +++ b/web/ru/dp_intro.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/dp_optimizations.html b/web/ru/dp_optimizations.html index 6f58d86..ac2305b 100644 --- a/web/ru/dp_optimizations.html +++ b/web/ru/dp_optimizations.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/fenwick.html b/web/ru/fenwick.html index 6ffeaf5..8ba5386 100644 --- a/web/ru/fenwick.html +++ b/web/ru/fenwick.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/games.html b/web/ru/games.html index 5a983a6..36b31b5 100644 --- a/web/ru/games.html +++ b/web/ru/games.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/geometry.html b/web/ru/geometry.html index 9272cbc..70ed75d 100644 --- a/web/ru/geometry.html +++ b/web/ru/geometry.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/hashing.html b/web/ru/hashing.html index c003d4c..ae9b984 100644 --- a/web/ru/hashing.html +++ b/web/ru/hashing.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/hld.html b/web/ru/hld.html index 1153bb4..9c5552b 100644 --- a/web/ru/hld.html +++ b/web/ru/hld.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/index.html b/web/ru/index.html index 774a290..f6871b1 100644 --- a/web/ru/index.html +++ b/web/ru/index.html @@ -6,7 +6,7 @@ - +

Алгоритмика

diff --git a/web/ru/ioi.html b/web/ru/ioi.html index b6476d3..803575c 100644 --- a/web/ru/ioi.html +++ b/web/ru/ioi.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/karatsuba.html b/web/ru/karatsuba.html index 3ac6f14..0f347e9 100644 --- a/web/ru/karatsuba.html +++ b/web/ru/karatsuba.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/lca.html b/web/ru/lca.html index 3527172..29f5315 100644 --- a/web/ru/lca.html +++ b/web/ru/lca.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/linalg.html b/web/ru/linalg.html index 0b311e2..f6aa629 100644 --- a/web/ru/linalg.html +++ b/web/ru/linalg.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/matching.html b/web/ru/matching.html index 7db5cde..08c34af 100644 --- a/web/ru/matching.html +++ b/web/ru/matching.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/matroid.html b/web/ru/matroid.html index 406c106..ac8027c 100644 --- a/web/ru/matroid.html +++ b/web/ru/matroid.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/mincost-maxflow.html b/web/ru/mincost-maxflow.html index 53d74b0..4480d16 100644 --- a/web/ru/mincost-maxflow.html +++ b/web/ru/mincost-maxflow.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/mst.html b/web/ru/mst.html index d68529f..acb8a6f 100644 --- a/web/ru/mst.html +++ b/web/ru/mst.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/pollard.html b/web/ru/pollard.html index f6fe01a..09efcb3 100644 --- a/web/ru/pollard.html +++ b/web/ru/pollard.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/probability.html b/web/ru/probability.html index edf0290..bd2e97e 100644 --- a/web/ru/probability.html +++ b/web/ru/probability.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/reciprocal.html b/web/ru/reciprocal.html index 5a0b542..357bc00 100644 --- a/web/ru/reciprocal.html +++ b/web/ru/reciprocal.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/segtree.html b/web/ru/segtree.html index 3bcae1f..5a8690e 100644 --- a/web/ru/segtree.html +++ b/web/ru/segtree.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/sparse-table.html b/web/ru/sparse-table.html index bd8d5f7..4846b0a 100644 --- a/web/ru/sparse-table.html +++ b/web/ru/sparse-table.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/sse.html b/web/ru/sse.html index 57523b3..02f0f97 100644 --- a/web/ru/sse.html +++ b/web/ru/sse.html @@ -6,7 +6,7 @@ - +

Streaming SIMD Extensions

diff --git a/web/ru/stress-test.html b/web/ru/stress-test.html index b5f725e..6066e07 100644 --- a/web/ru/stress-test.html +++ b/web/ru/stress-test.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - +

Стресс-тестирование

diff --git a/web/ru/strings.html b/web/ru/strings.html index 3ecce74..3c21c6e 100644 --- a/web/ru/strings.html +++ b/web/ru/strings.html @@ -25,7 +25,7 @@ code > span.fu { color: #06287e; } code > span.er { color: #ff0000; font-weight: bold; } - + diff --git a/web/ru/suffix-array.html b/web/ru/suffix-array.html index 35f4feb..c4f2f77 100644 --- a/web/ru/suffix-array.html +++ b/web/ru/suffix-array.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/treap.html b/web/ru/treap.html index a56b3c7..2abea3c 100644 --- a/web/ru/treap.html +++ b/web/ru/treap.html @@ -6,7 +6,7 @@ - + diff --git a/web/ru/trie.html b/web/ru/trie.html index 6f151d5..ce2e0c4 100644 --- a/web/ru/trie.html +++ b/web/ru/trie.html @@ -6,7 +6,7 @@ - + From adb51c5757ff0c96e12875825102f9b8cc9715dd Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 4 Jun 2019 22:12:48 +0000 Subject: [PATCH 004/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- .travis.yml | 10 - README.md | 3 - build.sh | 3 - {web/en => en}/index.html | 0 en/index.md | 1 - web/index.html => index.html | 0 web/pandoc.css => pandoc.css | 0 {web/ru => ru}/aho-corasick.html | 0 ru/aho-corasick.md | 91 -- {web/ru => ru}/annealing.html | 0 ru/annealing.md | 138 --- {web/ru => ru}/bayans.html | 0 ru/bayans.md | 273 ----- {web/ru => ru}/bitset.html | 0 ru/bitset.md | 130 --- {web/ru => ru}/centroid.html | 0 ru/centroid.md | 167 --- {web/ru => ru}/convex-hulls.html | 0 ru/convex-hulls.md | 201 ---- {web/ru => ru}/cross-entropy.html | 0 ru/cross-entropy.md | 22 - {web/ru => ru}/cuda.html | 0 ru/cuda.md | 492 --------- {web/ru => ru}/dp-intro.html | 0 ru/dp-intro.md | 40 - {web/ru => ru}/dp-optimizations.html | 0 ru/dp-optimizations.md | 167 --- {web/ru => ru}/dp_intro.html | 0 ru/dp_intro.md | 42 - {web/ru => ru}/dp_optimizations.html | 0 ru/dp_optimizations.md | 167 --- {web/ru => ru}/fenwick.html | 0 ru/fenwick.md | 162 --- {web/ru => ru}/games.html | 0 ru/games.md | 197 ---- {web/ru => ru}/geometry.html | 0 ru/geometry.md | 237 ----- {web/ru => ru}/hashing.html | 0 ru/hashing.md | 224 ---- {web/ru => ru}/hld.html | 0 ru/hld.md | 120 --- {web/ru => ru}/index.html | 0 ru/index.md | 51 - {web/ru => ru}/ioi.html | 0 ru/ioi.md | 70 -- {web/ru => ru}/karatsuba.html | 0 ru/karatsuba.md | 69 -- {web/ru => ru}/lca.html | 0 ru/lca.md | 193 ---- {web/ru => ru}/linalg.html | 0 ru/linalg.md | 315 ------ {web/ru => ru}/matching.html | 0 ru/matching.md | 139 --- {web/ru => ru}/matroid.html | 0 ru/matroid.md | 102 -- {web/ru => ru}/mincost-maxflow.html | 0 ru/mincost-maxflow.md | 148 --- {web/ru => ru}/mst.html | 0 ru/mst.md | 209 ---- {web/ru => ru}/pollard.html | 0 ru/pollard.md | 142 --- {web/ru => ru}/probability.html | 0 ru/probability.md | 86 -- {web/ru => ru}/reciprocal.html | 0 ru/reciprocal.md | 128 --- {web/ru => ru}/segtree.html | 0 ru/segtree.md | 369 ------- {web/ru => ru}/sparse-table.html | 0 ru/sparse-table.md | 53 - {web/ru => ru}/sse.html | 0 ru/sse.md | 364 ------- {web/ru => ru}/stress-test.html | 0 ru/stress-test.md | 51 - {web/ru => ru}/strings.html | 0 ru/strings.md | 206 ---- {web/ru => ru}/suffix-array.html | 0 ru/suffix-array.md | 109 -- {web/ru => ru}/treap.html | 0 ru/treap.md | 460 --------- {web/ru => ru}/trie.html | 0 ru/trie.md | 71 -- web/img/.editorial.html.swp | Bin 12288 -> 0 bytes .../aho-corasick-checkpoint.ipynb | 146 --- .../bayans-checkpoint.ipynb | 533 ---------- .../bitset-checkpoint.ipynb | 241 ----- .../centroid-checkpoint.ipynb | 249 ----- .../.ipynb_checkpoints/cuda-checkpoint.ipynb | 475 --------- .../dp-intro-checkpoint.ipynb | 109 -- .../dp-optimizations-checkpoint.ipynb | 288 ------ .../fenwick-checkpoint.ipynb | 285 ------ .../.ipynb_checkpoints/games-checkpoint.ipynb | 331 ------ .../geometry-checkpoint.ipynb | 421 -------- .../hashing-checkpoint.ipynb | 426 -------- .../.ipynb_checkpoints/hld-checkpoint.ipynb | 186 ---- .../.ipynb_checkpoints/index-checkpoint.ipynb | 81 -- .../karatsuba-checkpoint.ipynb | 122 --- .../.ipynb_checkpoints/lca-checkpoint.ipynb | 339 ------- .../linalg-checkpoint.ipynb | 453 --------- .../matching-checkpoint.ipynb | 234 ----- .../matroid-checkpoint.ipynb | 178 ---- .../mincost-maxflow-checkpoint.ipynb | 239 ----- .../segtree-checkpoint.ipynb | 680 ------------- .../sparse-table-checkpoint.ipynb | 119 --- .../.ipynb_checkpoints/sse-checkpoint.ipynb | 404 -------- .../strings-checkpoint.ipynb | 448 -------- .../suffix-array-checkpoint.ipynb | 158 --- .../.ipynb_checkpoints/treap-checkpoint.ipynb | 956 ------------------ .../.ipynb_checkpoints/trie-checkpoint.ipynb | 129 --- 108 files changed, 14452 deletions(-) delete mode 100644 .travis.yml delete mode 100644 README.md delete mode 100755 build.sh rename {web/en => en}/index.html (100%) delete mode 100644 en/index.md rename web/index.html => index.html (100%) rename web/pandoc.css => pandoc.css (100%) rename {web/ru => ru}/aho-corasick.html (100%) delete mode 100644 ru/aho-corasick.md rename {web/ru => ru}/annealing.html (100%) delete mode 100644 ru/annealing.md rename {web/ru => ru}/bayans.html (100%) delete mode 100644 ru/bayans.md rename {web/ru => ru}/bitset.html (100%) delete mode 100644 ru/bitset.md rename {web/ru => ru}/centroid.html (100%) delete mode 100644 ru/centroid.md rename {web/ru => ru}/convex-hulls.html (100%) delete mode 100644 ru/convex-hulls.md rename {web/ru => ru}/cross-entropy.html (100%) delete mode 100644 ru/cross-entropy.md rename {web/ru => ru}/cuda.html (100%) delete mode 100644 ru/cuda.md rename {web/ru => ru}/dp-intro.html (100%) delete mode 100644 ru/dp-intro.md rename {web/ru => ru}/dp-optimizations.html (100%) delete mode 100644 ru/dp-optimizations.md rename {web/ru => ru}/dp_intro.html (100%) delete mode 100644 ru/dp_intro.md rename {web/ru => ru}/dp_optimizations.html (100%) delete mode 100644 ru/dp_optimizations.md rename {web/ru => ru}/fenwick.html (100%) delete mode 100644 ru/fenwick.md rename {web/ru => ru}/games.html (100%) delete mode 100644 ru/games.md rename {web/ru => ru}/geometry.html (100%) delete mode 100644 ru/geometry.md rename {web/ru => ru}/hashing.html (100%) delete mode 100644 ru/hashing.md rename {web/ru => ru}/hld.html (100%) delete mode 100644 ru/hld.md rename {web/ru => ru}/index.html (100%) delete mode 100644 ru/index.md rename {web/ru => ru}/ioi.html (100%) delete mode 100644 ru/ioi.md rename {web/ru => ru}/karatsuba.html (100%) delete mode 100644 ru/karatsuba.md rename {web/ru => ru}/lca.html (100%) delete mode 100644 ru/lca.md rename {web/ru => ru}/linalg.html (100%) delete mode 100644 ru/linalg.md rename {web/ru => ru}/matching.html (100%) delete mode 100644 ru/matching.md rename {web/ru => ru}/matroid.html (100%) delete mode 100644 ru/matroid.md rename {web/ru => ru}/mincost-maxflow.html (100%) delete mode 100644 ru/mincost-maxflow.md rename {web/ru => ru}/mst.html (100%) delete mode 100644 ru/mst.md rename {web/ru => ru}/pollard.html (100%) delete mode 100644 ru/pollard.md rename {web/ru => ru}/probability.html (100%) delete mode 100644 ru/probability.md rename {web/ru => ru}/reciprocal.html (100%) delete mode 100644 ru/reciprocal.md rename {web/ru => ru}/segtree.html (100%) delete mode 100644 ru/segtree.md rename {web/ru => ru}/sparse-table.html (100%) delete mode 100644 ru/sparse-table.md rename {web/ru => ru}/sse.html (100%) delete mode 100644 ru/sse.md rename {web/ru => ru}/stress-test.html (100%) delete mode 100644 ru/stress-test.md rename {web/ru => ru}/strings.html (100%) delete mode 100644 ru/strings.md rename {web/ru => ru}/suffix-array.html (100%) delete mode 100644 ru/suffix-array.md rename {web/ru => ru}/treap.html (100%) delete mode 100644 ru/treap.md rename {web/ru => ru}/trie.html (100%) delete mode 100644 ru/trie.md delete mode 100644 web/img/.editorial.html.swp delete mode 100644 web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/games-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/hld-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/index-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/lca-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/matching-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/sse-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/strings-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/treap-checkpoint.ipynb delete mode 100644 web/img/.ipynb_checkpoints/trie-checkpoint.ipynb diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 57bf0a9..0000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -before_script: sudo apt-get install pandoc -script: ./build.sh - -deploy: - provider: pages - skip_cleanup: true - github_token: $GITHUB_TOKEN - keep_history: true - on: - branch: master diff --git a/README.md b/README.md deleted file mode 100644 index ed8aea0..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Algorithmica.org - -test diff --git a/build.sh b/build.sh deleted file mode 100755 index 8ff88d7..0000000 --- a/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -cp -r img web/img -mkdir web/{ru,en} -find {ru,en} -iname "*.md" -type f -exec sh -c 'pandoc "${0}" -o "web/${0%.md}.html" --css ../pandoc.css --standalone --mathjax' {} \; diff --git a/web/en/index.html b/en/index.html similarity index 100% rename from web/en/index.html rename to en/index.html diff --git a/en/index.md b/en/index.md deleted file mode 100644 index e1ebd34..0000000 --- a/en/index.md +++ /dev/null @@ -1 +0,0 @@ -some english index diff --git a/web/index.html b/index.html similarity index 100% rename from web/index.html rename to index.html diff --git a/web/pandoc.css b/pandoc.css similarity index 100% rename from web/pandoc.css rename to pandoc.css diff --git a/web/ru/aho-corasick.html b/ru/aho-corasick.html similarity index 100% rename from web/ru/aho-corasick.html rename to ru/aho-corasick.html diff --git a/ru/aho-corasick.md b/ru/aho-corasick.md deleted file mode 100644 index 06035f9..0000000 --- a/ru/aho-corasick.md +++ /dev/null @@ -1,91 +0,0 @@ - -# Алгоритм Ахо-Корасик - -Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время. - -Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик. - - - -Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы. - - - -> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. - -Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. - -Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. - -Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). - -## Зачем это нужно - -> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. - -Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. - -Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. - -Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс. - -## Алгоритм Ахо-Корасик* - -Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой. - -**Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход. - -**Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда. - - - - -```c++ -const int k = 26; - -struct Vertex { - Vertex *to[k] = {0}, *go[k] = {0}; - Vertex *link = 0, *p; - int pch; - Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; } -}; - -Vertex *root = new Vertex(-1, 0); -``` - - -```c++ -void add_string (string s) { - Vertex *v = root; - for (char _c : s) { - c -= 'a'; - if (!v->to[c]) - v->to[c] = new Vertex(c, v); - v = v->to[c]; - } -} -``` - -Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться. - - -```c++ -Vertex* go (Vertex *v, int c); - -Vertex* link (Vertex *v) { - if (!v->link) { - if (v == root || v->p == root) v->link = root; - else v->link = go(link(v->p), v->pch); - } - return v->link; -} - -Vertex* go (Vertex *v, int c) { - if (!v->go[c]) { - if (v->to[c]) v->go[c] = v->to[c]; - else if (v == root) v->go[c] = root; - else v->go[c] = go(link(v), c); - } - return v->go[c]; -} -``` diff --git a/web/ru/annealing.html b/ru/annealing.html similarity index 100% rename from web/ru/annealing.html rename to ru/annealing.html diff --git a/ru/annealing.md b/ru/annealing.md deleted file mode 100644 index 8d5bb22..0000000 --- a/ru/annealing.md +++ /dev/null @@ -1,138 +0,0 @@ - -# Метод отжига - -* Эвристический метод глобальной оптимизации -* Работает с дискретными функциями в предположении, что у близких «точек» близкое значение, и есть что-то типа градиента -* Очень простой в написании -* Никаких гарантий нет, но хорошо работает на практике -* Метод настолько мощный, что иногда им можно сдать задачи, у которых автором предполагалось адекватное решение (пример: [Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам](http://codeforces.com/contest/745/submission/23067030)) - -![](https://camo.githubusercontent.com/574bf26bf301827efaa18748440fa77fab8c5a4f/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f642f64352f48696c6c5f436c696d62696e675f776974685f53696d756c617465645f416e6e65616c696e672e676966) - -## Постановка задачи - -Имеется некоторая функция $f(x)$, которую мы хотим минимизировать. - -Наука решать эту задачу в общем случае не умеет, и вряд ли когда-либо научится. - -TODO: описание алгоритма, физическая аналогия, конструирование гладких функций - -При решении конкретной задачи нужно менять только функцию `fitness` и, возможно, параметры алгоритма. - -## Ферзи - -Рассмотрим такую задачу: дана шахматная доска $n \times n$ и $n$ ферзей. Нужно расставить их так, чтобы они не били друг друга. - -Будем кодировать состояние перестановкой чисел от $1$ до $n$. Такое представление кодирует не все состояния, но оно точно не учитывает те, где ферзи бьют друг друга по вертикали или горизонтали. - -Выберем такую функцию: fitness(p) = число пар ферзей, которые бьют друг друга. - -Важная деталь: скорость одной итерации прямо пропорционально скорости работы `fitness`, поэтому очень важно реализовать её эффективно. Конкретно здесь её можно реализовать за $O(n^2)$ (используется в коде для наглядности), $O(n)$ и даже $O(1)$. - -Мы будем использовать `python`, потому что в нём удобнее строить графики. Эквивалентный код на C++ приведена сразу после. - - -```python -import random - -# Эти библиотеки будут нужны, чтобы рисовать графики: -import matplotlib.pyplot as plt -%matplotlib inline - -import seaborn as sns -sns.set() -``` - - -```python -def fitness(p): - s = 0 - for i in range(len(p)): - d = 1 - for j in range(i): - if abs(i-j) == abs(p[i]-p[j]): # ферзи стоят на одной диагонали - d = 0 - s += d - return s -``` - - -```python -n = 100 # кол-во ферзей -k = 1000 # кол-во итераций -p = list(range(n)) # исходная перестановка - -scores = [] - -t = 1 -for i in range(k): - # уменьшаем температуру - t *= 0.99 # этот параметр лучше перебрать руками - - # копируем перестановку и делаем небольшое изменение - q = p[:] - a, b = random.sample(range(n), 2) - q[a], q[b] = q[b], q[a] - - # считаем разницу fitness и решаем, нужно ли переходить в новое состояние - d = fitness(q) - fitness(p) - if d > 0 or random.random() < math.exp(d/t): - p = q[:] - scores += [fitness(q)] - -print('Best score:', max(scores)) -plt.plot(scores) -plt.show() -``` - - Best score: 95 - - - -![png](annealing_files/annealing_11_1.png) - - -Примерно эквивалентный код на C++: - - -```python -const int n = 100, k = 1000; - -int f(vector p) { - int s = 0; - for (int i = 0; i < n; i++) { - int d = 1; - for (int j = 0; j < i; j++) - if abs(i-j) == abs(p[i]-p[j]): - d = 0; - s += d; - } - return s; -} - -double rnd() { return double(rand()) / RAND_MAX; } - -int main() { - - for (int i = 0; i < n; i++) - v[i] = i; - - int ans = 1; - - double t = 1; - for (int i = 0; i < k; i++) { - t *= 0.99; - vector u = v; - swap(u[rand()%n], u[rand()%n]); - int val = f(u); - if (val > ans || rnd() < exp((val-ans)/t)) - v = u, ans = val; - if (ans == n) break; - } - - for (int x : v) - cout << x+1 << " "; - - return 0; -} -``` diff --git a/web/ru/bayans.html b/ru/bayans.html similarity index 100% rename from web/ru/bayans.html rename to ru/bayans.html diff --git a/ru/bayans.md b/ru/bayans.md deleted file mode 100644 index 063656e..0000000 --- a/ru/bayans.md +++ /dev/null @@ -1,273 +0,0 @@ - -# Красивые идейные задачи - -Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда. - -Задачи идут в порядке вспоминания, то есть в весьма рандомном. - -## Попугаи - -Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы. - -## Минимум и максимум - -Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый. - -## Перестановка - -Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов. - -## Выпуклая оболочка -Требуется отвечать на 2 типа запросов: - -1. Добавить точку в выпуклую оболочку. -2. Проверить, лежит ли точка внутри выпуклой оболочки. - -Обе операции онлайн за $O(\log n)$. - -## Геометрическая прогрессия - -Найдите способ посчитать $\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\log n)$. - -## Покемоны - -В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \to b$ и $b \to c$ не следует, что $a \to c$. - -У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш. - -## Сортировка -Можно ли отсортировать -* 5 камней за 8 взвешиваний? -* 5 камней за 7 взвешиваний? -* 20 камней за 60 взвешиваний? - -## Точки в круге - -Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат. - -## Замкнутые ломаные - -Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии? - -## Неубывающий массив - -Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим. - -## Чётный цикл - -Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины. - -## $k$-ая порядковая статистика - -Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$. - -## Доминирующий элемент - -Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. Время работы $O((n+m) \log n)$. - -## Разрушение дерева - -Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева. - -## $k$-ый элемент на отрезке - -Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \log n)$. - -## Различные числа на отрезке - -Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\sqrt{n})$. - -## Физкультура - -Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов: - -1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя) -2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно) - -При этом приказ может частично отменить действие предыдущих приказов. - -После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \log n)$. - -## Нулевая сумма - -Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$. - -## Мета-задача - -В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу. - -## Ниточка - -В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления. - -## Пельмени - -Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \log n)$. - -## От нуля до единицы - -Дан следующий код: - - -```python -x = 0 -while x < 1: - x += random() -``` - -Требуется посчитать матожидание `x`. - -(`random` в питоне возвращает случайное действительное число от 0 до 1.) - -## Площадь - -Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%. - -## Окружности - -Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна - -* соприкасаться с *внешней*, -* соприкасаться с предыдущей (($k-1$)-ой), -* иметь при этом максимальный радиус. - -Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. - -## Блеф - -Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может: - -* Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается. -* Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом. - -С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$. - -## Достижимость -Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000. - -## Нумизмат - -Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой. - -Придумайте любой полиномиальный алгоритм. - -## Принцесса - -В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве. - -Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством. - -У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми. - -Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу. - -Асимптотика $O(n^2)$. - -## Спираль - -Определим спираль $(2n+1) \times (2n+1)$ как матрицу следующего вида: - -$$ -\begin{matrix} -21 & 22 & 23 & 24 & 25 \\ -20 & 7 & 8 & 9 & 10 \\ -19 & 6 & 1 & 2 & 11 \\ -18 & 5 & 4 & 3 & 12 \\ -17 & 16 & 15 & 14 & 13 \\ -\end{matrix} -$$ - -Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$). - -$q \leq 100$, $n \leq 10^9$. - -## Польский лабиринт - -Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`. - -![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png) - -Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам. - -$n \leq 10^5$, изначальные координаты туристов до $10^9$. - -## Нимные подмножества - -Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \log n)$. - -## Баланс степеней - -Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода. - -## Два пути - -Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$. - -## Пьяница - -Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой? - -## Ксоровый рюкзак - -Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$. - -## Иван Сусанин - -Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш. - -- Камиль руководит армией днём и водит армию по *дорогам*. -- Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. - -Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск. - -Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу. -Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось. - -Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$. - -## Варенье - -В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм. - -Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья. - -Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной. - -$n, m \leq 10^5$ - -## Лабиринт - -Серёжа потерялся в лабиринте $n \times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт. - -Придумайте любой полиномиальный алгоритм. - -## Обезьяна - -Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт? - -## Ожидание минимума - -Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин. - -Придумайте любой точный полиномиальный алгоритм. - -## Шумный ксор - -Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \frac15$ быть единичным, то есть каждый бит $x \oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности: - - -```python -x = # ... - -def mask(p=0.2): - r = 0 - for i in range(32): - if random.random() < p: - r += 2**i - return r - -def query(y): - return bin(x ^ y ^ mask()).count('1') -``` - -Ваша задача — отгадать число, используя не более 10000 попыток. diff --git a/web/ru/bitset.html b/ru/bitset.html similarity index 100% rename from web/ru/bitset.html rename to ru/bitset.html diff --git a/ru/bitset.md b/ru/bitset.md deleted file mode 100644 index cb5dcd7..0000000 --- a/ru/bitset.md +++ /dev/null @@ -1,130 +0,0 @@ - -# Битовое сжатие - -* Из-за него в «асимптотиках» появляется `/64` -* На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов -* `bitset` есть в stl; говорят, самописный быстрее - -Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости. - -Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию. - -Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. - -Работать с ним нужно вот так: - - -```c++ -const int lim = 1000; -bitset b; // создать битсет размера lim (должно быть константой) -b.set(); // заполнить единицами -b.reset(); // заполнить нулями -b.flip(); // заменить единички на нули и наоборот -b.count(); // посчитать число единичек -cout << b; // вывести битовую строку -``` - -Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`. - -## Рюкзак - -Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака. - -Обычно его решают так: - - -```c++ -bool dp[lim] = {}; // так можно его заполнить нулями -dp[0] = 1; -for (int i = 0; i < n; i++) - for (int x = lim - a[i]; x >= 0; x--) - dp[x + a[i]] |= dp[x]; -``` - -…а с битсетом оно разгоняется так: - - -```c++ -bitset b; -b[0] = 1; -for (int i = 0; i < n; i++) - b |= b << a[i]; -``` - -## Цикл длины 3 - -Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\frac{n^3}{64})$ следующим образом: - - -```c++ -bitset g[maxn]; // матрица смежности -for (int a = 0; a < n; a++) { - for (int b = 0; b < n; b++) { - if (g[a][b] && (~g[a] & g[b]).any()) { - // цикл найден - } - } -} -``` - -Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд. - -## Перемножение матриц - -Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения: - - -```c++ -typedef bitset t; -typedef array matrix; - -matrix operator* (matrix a, matrix b) { - matrix c; - for(int i = 0; i < n; i++) - for(int j = 0; j < n; j++) - if(a[i][j]) - c[i] |= b[j]; - return c; -} -``` - -## Гаусс - -Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. - -Нас по сути просят решить следующую систему: - -$$ -\begin{cases} -a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ -a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ -\ldots \\ -a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 -\end{cases} -$$ - -Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. - -В таком случае можно значительно ускорить и упростить обычный метод Гаусса: - - -```c++ -t gauss (matrix a) { - for (int i = 0; i < n; i++) { - int nonzero = i; - for (int j = i+1; j < n; j++) - if (a[j][i]) - nonzero = j; - swap(a[nonzero], a[i]); - for (int j = 0; j < n; j++) - if (j != i && a[j][i]) - a[j] ^= a[i]; - } - t x; - for (int i = 0; i < n; i++) - x[i] = a[i][n] ^ a[i][i]; - return x; -} -``` - -Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. diff --git a/web/ru/centroid.html b/ru/centroid.html similarity index 100% rename from web/ru/centroid.html rename to ru/centroid.html diff --git a/ru/centroid.md b/ru/centroid.md deleted file mode 100644 index b439d33..0000000 --- a/ru/centroid.md +++ /dev/null @@ -1,167 +0,0 @@ - -# Центроидная декомпозиция - -Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути». - -Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще). - -![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700) - -**Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\frac{n}{2}$. - -Выясняется, что центр всегда существует — это следует из алгоритма его поиска: - - -```c++ -int s[maxn]; - -int sizes (int v) { - s[v] = 1; - for (int u : g[v]) - // для простоты считаем, что дерево корневое - s[v] += sizes(u); - return s[v]; -} - -// второй параметр -- размер дерева -int centroid (int v, int n) { - for (int u : g[v]) - if (s[u] > n/2) - return centroid(u, n); - return v; -} -``` - -**Утверждение.** `centroid` действительно находит цетроид. - -**Доказательство:** - -* `centroid` вернет вершину, у которой размеры всех детей не больше $\frac{n}{2}$ (это явно проверяется в `if`-е). -* Мы пришли в эту вершину, когда её размер был больше $\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\frac{n}{2}+1) = \frac{n}{2}-1$ вершин. -* Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен. - -Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид. - -**Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент». - -**Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением. - -Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты. - -Теперь поймём, зачем мы всё это делали. - -**Утверждение.** Каждая вершина входит в $O(\log n)$ компонент. - -**Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\lceil \log_2 n \rceil$ разделений. - -**Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \log n)$. - -**Утверждение.** Для любого пути $a \leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$. - -**Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$. - -Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний. - -## Подсчет путей с заданным свойством - -Рассмотрим конкретный пример: подсчёт путей заданной длины. - -Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид. - -Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз. - - -```c++ -int l = 179; // нужная нам длина -int ans = 0; - -// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена -bool used[maxn]; -int s[maxn]; // размеры поддеревьев - -void sizes (int v, int p) { - s[v] = 1; - for (int u : g[v]) - if (u != p && !used[u]) - sizes(u, v), s[v] += s[u]; -} - -int centroid (int v, int p, int n) { - for (int u : g[v]) - if (u != p && !used[u] && s[u] > n/2) - return centroid(u, v, n); - return v; -} - -// записывает в t[] глубины вершин -void dfs (int v, int p, int d, vector &t) { - t.push_back(d); - for (int u : g[v]) - if (u != p && !used[u]) - dfs(u, v, d + 1, t); -} - -void solve (int v) { - /* <единственный зависящий от конкретной задачи код> */ - size(v); - vector d(s[v], 0); - d[0] = 1; - for (int u : g[v]) { - if (!used[u]) { - vector t; - dfs(u, v, 1, t); - for (int x : t) - if (x <= l) - ans += d[l-x]; - for (int x : t) - d[x]++; - } - } - /* */ - - used[v] = 1; - for (int u : g[v]) - if (!used[u]) - solve(centroid(u, v, s[u]/2)); -} -``` - -**Асимптотика** $O(n \log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\log n)$ «уровней» центроидной декомпозиции. - -## Запросы на путях — offline - -Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline. - -А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем. - -Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. - -Таким образом, каждый запрос будет просмотрен $O(\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \log n + n \log n)$. - -## Запросы на путях — online - -Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы. - -Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \times \log n$, в котором для каждой вершины будем хранить $O(\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути. - -Тогда, при ответе на запрос, мы за $O(\log n)$ или даже $O(\log \log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида. - -## Асимптотика при более долгих пересчётах - -TODO: сослаться на мастер-теорему - -Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени? - -Возьмем худший случай — когда каждый раз компонента разбивается на две равные части. - - - -Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень: - -$$ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} -\leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n -= n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} -< 2 n \log n -= O(n \log n) $$ - -Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \log n)$ — как, впрочем, и во всех остальных «разделяйках». diff --git a/web/ru/convex-hulls.html b/ru/convex-hulls.html similarity index 100% rename from web/ru/convex-hulls.html rename to ru/convex-hulls.html diff --git a/ru/convex-hulls.md b/ru/convex-hulls.md deleted file mode 100644 index c754863..0000000 --- a/ru/convex-hulls.md +++ /dev/null @@ -1,201 +0,0 @@ - -# Задачи на многоугольники - -Очень часто геометрические задачи на контестах - это задачи на многоугольники. Некоторые из таких задач мы уже обсудили на предыдущих занятиях. Так мы уже обсудили задачу проверки многоугольника на выпуклость и научились находить площадь многоугольника. Напомним, что такое полярный угол. - -![alt text](https://upload.wikimedia.org/wikipedia/commons/1/13/Polar_graph_paper.svg) - - - -# Выпуклые оболочки - -**Выпуклое множество** - такое множество точек, что все точки отрезка, образуемого любыми двумя точками данного множества, также принадлежат данному множеству - - -**Выпуклая оболочка фигуры** - такое выпуклое множество точек, что все точки фигуры также лежат в нем. - - -**Минимальная выпуклая оболочка фигуры** - это минимальная по площади выпуклая оболочка. - -![alt text](http://informatics.letovo.ru/geom/Volgin_Asatrian/images/vup.png) - -![alt text](http://grafika.me/files/les_screens/convex_1.png) - -![alt text](https://2.bp.blogspot.com/-JngZWPM7wig/WlRz58cIMHI/AAAAAAAADGA/Pyhfngs_GKQMkb9B2_kqZJRQ5ZLEzrVMQCK4BGAYYCw/s1600/convexhullex.jpg) - -Дано множество точек, требуется построить его минимальную выпуклую оболочку : - -## Построение за $O(nh)$ - -### Алгоритм Джарвиса(метод заворачивания подарка) - -Давайте выберем какую-то точку, которая гарантированно попадет в минимальную выпуклую оболочкуо, например обычно берут нижнюю и если таких несколько, то самую левую из них. Теперь давайте по одной набирать точки, как бы заворачивая нашу выпуклую оболочку(отсюда и название). Как же нам найти следующую точку в выпуклую оболочку, давайте пройдемся по точкам, которые мы еще не взяли в МВО и среди них выберем с минимальным полярным углом. - -![alt text](http://neerc.ifmo.ru/wiki/images/b/b3/Graham1.png) - -Корректность алгоритма легко доказывается по индукции, так как на первом шагу мы выбрали точку, точно лежащую в МВО, а на i, взяли такую точку, что все остальные лежат в нужной нам стороне. - -Асимптотика : для каждой точки выпуклой оболочки мы из всех оставшихся точек будем искать оптимальную - что будет работать за h(размер выпуклой оболочки) * n - -### Важно помнить, что именно $O(hn)$, а не $O(n^2)$, так как существуют задачи на это - - - -``` -int base = 0; -for (int i = 1; i < n; i++) { - if (mas[i].y < mas[base].y) { - base = i; - } - else if (mas[i].y == mas[base].y && mas[i].x < mas[base].x) { - base = i; - } -} -convex_hull.push_back(base); -point first = mas[base]; -point cur = first; -point prev = point(first.x - 1, first.y); -do { - double minCosAngle = 1e9; // чем больше угол, тем меньше его косинус - double maxLen = 1e9; - int next = -1; - for (int i = 0; i < n; i++) { - double curCosAngle = CosAngle(prev, cur, mas[i]); - if (Less(curCosAngle,minCosAngle)) {//если меньше сразу меняем - next = i; - minCosAngle = curCosAngle; - maxLen = dist(cur, mas[i]); - } - else if (Equal(curCosAngle, minCosAngle)) {// смотрим по длине - double curLen = dist(cur,mas[i]); - if (More(curLen,maxLen)) { - next = i; - maxLen = curLen; - } - } - } - prev = cur; - cur = mas[next]; - convex_hull.push_back(next); -} -while (cur != first); -``` - -## Построение за $O(n \log n)$ - -### Алгоритм Грэхема - -Алгоритм Грэхема базируется на следующей идее: Давайте не искать следующую точку каждый раз, а сделаем так, чтобы у нас всегда была оптимальная точка и мы могли бы просто ее достать и проверить. - -В прошлом алгоритме мы искали точку, оптимальную по полярному углу, тогда давайте сейчас сразу отсортируем точки по полярному углу и сразу возьмем две первые точки в МВО. - -Теперь будем делать следующий алгоритм, пока все точки не будут просмотрены : - -1) Возьмем первую из отсортированных точек. - -2) Проверем последние три точки из взятых, если они образуют правый поворот, то удалим предпоследнюю точку - -Сделать это можно, например, стеком. Код есть ниже. - -![alt text](https://cdn.turkaramamotoru.com/ru/algoritm-grehema-4131.jpg) - -Асимптотика : Мы просмотрим одну точку и либо удалим ее, либо оставим, то есть сам поиск МВО работает за линейное время, но мы еще делаем сортировку, а $\rightarrow$ алгоритм работает за $O(n\log(n))$, при этом его корректность вытекает из предыдущего алгоритма. - -Красивая визуализация - https://visualgo.net/en/convexhull - -красивое видео - https://www.youtube.com/watch?v=BTgjXwhoMuI -. - - -``` -struct Point { - int x, y; -}; - -Point operator -(Point a, Point b) -{ - return {a.x - b.x, a.y - b.y}; -} - -int operator * (Point a, Point b) -{ - return a.x * b.x + b.y * a.y; -} - -int operator ^(Point a, Point b) -{ - return a.x * b.y - b.x * a.y; -} - -bool cw(Point a, Point b, Point c) -{ - return (a - b) * (c - b) > 0; -} - -bool ccw(Point a, Point b, Point c) -{ - return (a - b) * (c - b) < 0; -} - -int main() -{ - sort(all(p2), comp); - vector s; - s.push_back(p[min_ind]); - for (int i = 0; i < n - 1; i++) { - if (p2[i].x == s[s.size() - 1].x && p2[i].y == s[s.size() - 1].y) - continue; - while (s.size() > 1 && (vect(s[s.size() - 1], s[s.size() - 2]) ^ - vect(s[s.size() - 1], p2[i])) > 0) - s.pop_back(); - s.push_back(p2[i]); - } -} -``` - -### Алгоритм Эндрю - -Алгоритм Эндрю опирается на то, что вещественные числа не точны и предлагает поменять компаратор и строить не одну выпуклую оболочку, а две : - -Давайте выберем самую нижнюю и самую правую точку, затем отсортируем точки по самому простому из возможных компараторов, теперь будем строить две оболочки от самой правой точки и самой левой, в итоге мы получим верхнюю и нижнюю части выпуклой оболочки - - - -``` -bool comp(Point a, Point b) { - if(a.x == b.x) { - return a.y < b.y; - } - return a.x < b.x; -} - -int main() { - sort(all(p), comp); - vector up, down; - up.pb(p[0]); - down.pb(p[0]); - Point p1 = p[0], p2 = p.back(); - for(int i = 1; i < n; i++) { - if (i == n - 1 || cw(p1, p[i], p2)) { - while (up.size() >= 2 && !cw(up[up.size() - 2], up[up.size() - 1], p[i])) { - up.pop_back(); - } - up.pb(p[i]); - } - if (i == n - 1 || ccw(p1, p[i], p2)) { - while (down.size() >= 2 && !ccw(down[down.size() - 2], down[down.size() - 1], p[i])) { - down.pop_back(); - } - down.pb(p[i]); - } - } -} -``` - -### Алгоритм Чена - -Также существует алгоритм, объединяющий Джарвиса и Грэхема(Эндрю) и работающий за $O(n\log(h))$, но он разбираться не будет - -# Задачи - -1) Базовые задачи - достаточно простые, например найти длину забора, чтобы ограничить многоугольник и подобные, но есть достаточно интересные задачи, в которых выпуклая оболочка неочевидна, например следующая : Даны $n$ пунктов в городе и $n$ почтальонов, для каждого пункта известно расстояние от почты $c_{i}$. Требуется каждому пункту доставить почту, $i$-ый почтальон просит $a_{i}$ монет, чтобы проснуться и $b_{i}$, чтобы проехать один километр, требуется для каждого пункта сказать, кто доставит почту наиболее выгодно. (Подсказка : $a_{i} + b_{i} * c_{j}$ - это прямая и стоимость доставки от $i$ почтальона к $j$ пункту). diff --git a/web/ru/cross-entropy.html b/ru/cross-entropy.html similarity index 100% rename from web/ru/cross-entropy.html rename to ru/cross-entropy.html diff --git a/ru/cross-entropy.md b/ru/cross-entropy.md deleted file mode 100644 index 5b144f7..0000000 --- a/ru/cross-entropy.md +++ /dev/null @@ -1,22 +0,0 @@ - -# Почему машинное обучение работает - -b - -Принцип максимального правдоподобия — - -## Функция правдоподобия - - - -$$ \hat{\theta} = \argmax_\theta P(X | \theta) $$ - -## Принцип максимального правдоподобия - - - -## Логистическая регрессия - -## Нормальное распределение и MSE - -## Почему сигмоида diff --git a/web/ru/cuda.html b/ru/cuda.html similarity index 100% rename from web/ru/cuda.html rename to ru/cuda.html diff --git a/ru/cuda.md b/ru/cuda.md deleted file mode 100644 index 4b1ff20..0000000 --- a/ru/cuda.md +++ /dev/null @@ -1,492 +0,0 @@ - -# Supercomputing for the Masses - -This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in [Colab]() or [download]() and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: -* `apt-get install nvidia-cuda-dev nvidia-cuda-toolkit` -* `pip install pycuda` - -Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work. - -## Subtlties of the Moore's law - -Here is a graph that roughly represents what is happening in the CPU world: - - - -**Moore's law** is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too. - -You can see that around 2005 there became a shift in design . - -The cores are more or less independent. - -Modern GPUs appeared in early 2000s. They exploit the specific area they operate. - -There are physical limitations to the speed of a core. - -One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another. - -Some of them have - -The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome. - -## Why multiprocessing? - -Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound - -There are two types of - -## General-purpose GPU - -There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills. - -There are several. - -This is like with Windows and Linux. - -We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning. - -## Heterogineous computing - -CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs. - -## Differences from CPUs - -### Threads - -Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive. - -By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. - -In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput. - -### Memory - -The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device? - -You can easily dump 98% of performance of you think this way. - -## Installing PyCUDA - -CUDA is available for many languages. - -Nice documentation can be found here: https://documen.tician.de/pycuda/index.html - -If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU". - - -```python -# you may want to clear the output of this cell after installation -from IPython.display import clear_output - -# this might take a while -!pip install pycuda - -clear_output() -``` - - -```python -import numpy as np - -from pycuda.compiler import SourceModule -import pycuda.driver as drv -import pycuda.autoinit -``` - -## The basics - -Let's start with a simple example and then dive deeper. - -## Kernels - -Just like C or C++, except that you use some custom built-in functions and specifiers. - -CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: - -You need to think of your computer as a heterogenious machine: there is host data and device data. - -* You move input data to device memory. -* You run some computation on device. -* You retrieve back this data. - -In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. - -## The famous $A + B$ problem - -For testing and coordination with host, we will use **NumPy** package. If you don't have it, install it: `pip install numpy`. - -NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it. - - -```python -# lets generate our test data: two float arrays filled with something random -a = numpy.random.randn(100).astype('float32') -b = numpy.random.randn(100).astype('float32') -# the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it - -# we need to create space where kernel should write its answers to -dest = numpy.zeros_like(a) - -# this is the kernel itself -mod = SourceModule(""" - __global__ void add(float *dest, float *a, float *b) { - const int i = threadIdx.x; - dest[i] = a[i] + b[i]; - } -""") - -# you need to specify the source code, and PyCUDA will compile it -add_kernel = mod.get_function("add") - -add_kernel( - drv.Out(dest), # specifies that this memory should be accessible for writing - drv.In(a), # specifies this should be accessible for reading - drv.In(b), - block=(100,1,1) # we'll talk about it in a minute -) - -assert np.allclose(dest, a + b), 'WA' # checks that these are equal -print('OK') -``` - - - File "", line 19 - %%time - ^ - SyntaxError: invalid syntax - - - -### Memory management - -In CUDA C API, you need to allocate memory explicitly. So this is actually really nice. - -There is also `drv.InOut` function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too. - -Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough. - -GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time. - -You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications - -## Synchronization - -**Reduction** is any array-wise operation. - -Assume the following problem: - - -## Dynamic programming - -Consider the following recurrence: - - -```python -## Problem: dynamic programming -``` - -## Work vs. Latency - -We actually think about both work and step complexity now. - -Some tasks, especially in cryptography, cannot be parallelized. But some can. - -## Summing arrays in $O(\log n)$ time - -Assume we want to perform some associative (i. e. $A*(B*C) = (A*B)*C$) operation on an array of $n$ elements. Say, sum it up. - -Normally, we would do that with a simple loop: - -```c++ -float s = 0; -for (int i = 0; i < n; i++) { - s += a[i]; -} -``` - -Its computation graphs looks like this: - - - -This is optimal in terms of work complexity, but not in terms of step complexity: it's $O(n)$. We may want something that is a bit worse in terms of work complexity, but can be parallelized. - -Let's try this divide-and-conquer approach: - - - -Now it's still $O(n)$ work complexity (you actually need exactly the same number of additions), but this is $O(\log n)$ step complexity. - -When you unroll the recursion from top to bottom, you will see that to get each required value, - - - -## Reducing small arrays - - -```python -a = numpy.random.randn(2048).astype('float32') - -mod = SourceModule(""" - __global__ void sum(float *dest, float *a, float *b) { - const int i = threadIdx.x; - // for l from 0 to logn: - // __sync_threads() - // if the thread is active - // sum two elements into where they belong - // a[0] should containt the needed sum - } -""") - -sum_kernel = mod.get_function("sum") - -add_kernel( - drv.InOut(a), - block=(1024,1,1) -) - -assert np.allclose(dest, a + b), 'WA' # checks that these are equal -print('OK') -``` - -## Warps and thread blocks - -Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties. - - - -You can actually do the same stuff with 2d and 3d indexing — weird, right? - -## Atomics - -## Reducing big arrays - -## Reducing very big arrays - -Now, things get harder. It's time to tell how exactly GPU parallelism works. - - - - -```python - -``` - -## Dense Matrix multiplication - -Let's get to our first example where using GPUs actually makes sense: matrix multiplication. - -## Sorting - -Our last (and hardest task) is to implement sorting. - -You might notice that we advocated divide-and-conquer approaches most of the time. - -It's true. They work. But we can't get an algorithm that works already. - - -```python -# we'll use a deep learning library for benchmarking because I'm not familiar with anything else -import torch - -a = torch.randn(10**8) -b = a.cuda() -``` - - -```python -# this should run for ~15 secs -%time c = torch.sort(a) -%time c = torch.sort(b) -``` - - CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s - Wall time: 15.2 s - CPU times: user 274 ms, sys: 237 ms, total: 511 ms - Wall time: 511 ms - - -So, 30 times speedup. So, we now what we need to compete against. - - -```python -b.sort() -``` - - - - - (tensor([-5.4567, -5.3551, -5.3288, ..., 5.3529, 5.4484, 5.4486], - device='cuda:0'), - tensor([55083205, 8383169, 73705953, ..., 79814161, 50474932, 27805828], - device='cuda:0')) - - - -There are two types of sorting algorithms: data-driven. - -The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort. - - - -It has $O(\log n)$ stages, in total they have $1 + 2 + 3 + \ldots + \log n = O(\log^2 n$ blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has $O(n \log^ n)$ work complexity, but $O(\log^2 n)$ step complexity, which is pretty sweet. - -It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation: - - -```python -def bitonic_sort(a, up=False): - if len(a) <= 1: - return a - else: - l = bitonic_sort(x[:len(a) // 2], True) - r = bitonic_sort(x[len(a) // 2:], False) - return bitonic_merge(first + second, up) - -def bitonic_merge(a, up): - # assume input a is bitonic, and sorted list is returned - if len(a) == 1: - return a - else: - bitonic_compare(a, up) - l = bitonic_merge(a[:len(a) // 2], up) - r = bitonic_merge(a[len(a) // 2:], up) - return l + r - -def bitonic_compare(a, up): - dist = len(a) // 2 - for i in range(dist): - if (a[i] > a[i + dist]) == up: - a[i], a[i + dist] = a[i + dist], x[i] # this is how swap is done in Python -``` - - -```python -bitonic_sort([57, 179, 42, 17, 300, 111]) -``` - - - - - [300, 179, 111, 57, 42, 17] - - - - -```python -a = np.random.randn(10**8).astype('float32') -``` - - - --------------------------------------------------------------------------- - - NameError Traceback (most recent call last) - - in () - ----> 1 a = np.random.randn(10**8).astype('float32') - - - NameError: name 'np' is not defined - - -## Why CUDA - -Most of it still applicable. - -Again, GPU programming is very specific. - -SSE and tensor cores. - -## Kernels - -Just like C or C++, except that you use some custom built-in functions and specifiers. - -CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: - -You need to think of your computer as a heterogenious machine: there is host data and device data. - -* You move input data to device memory. -* You run some computation on device. -* You retrieve back this data. - -In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. - -What you need to understand about GPUs is that they are extremely specialised for their applications. - -Intrinsics for that. - -Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision. - -First, they introduced "multiply-accumulate" operation (e. g. `x += y * z`) per 1 GPU clock cycle. - -Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell). - -Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix). - -This is a lot of work per - -Well, you don't really need anything more precise than that for deep learning anyway. - - -It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices. - -Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores". - -So, see, this is not exactly fair comparison. - - - -*
You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull
* - -down to int4 (16-valued, you heard correct) - -You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch. - -Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply. - -## Reducing an array - -It seems to be simple: you just need to . - -What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen: - -1. Read $x$ into register -2. Read $s$ into register -3. Calculate $s + x$ -4. Write it back to wherever $s$ was initially - -Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value. - - - -Note: Atomics to do that - -for small data types they are implemented on the hardware level and much more faster than that. - -std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. - -## Memory types - -If all the various types of device memory were to race, here’s how the race would turn out: - -Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible). - -* 1st place: **Register memory** -
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread. -* 2nd place: **Shared Memory** -
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should -* 3rd place: **Constant Memory** -
-* 4th: Texture Memory -* Tie for last place: Local Memory and Global Memory - -What you need to care for now is register - -For now, you need to care about differe - -Accessing global memory takes hundreds. - -## Problem: dense matrix multiplication - -A lot of these are actually sparse. You can do stuff with social network graphs or web graphs. - -Cool. But let's disapploint us for a bit: diff --git a/web/ru/dp-intro.html b/ru/dp-intro.html similarity index 100% rename from web/ru/dp-intro.html rename to ru/dp-intro.html diff --git a/ru/dp-intro.md b/ru/dp-intro.md deleted file mode 100644 index 6a848db..0000000 --- a/ru/dp-intro.md +++ /dev/null @@ -1,40 +0,0 @@ - -# Динамическое программирование - -> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. - -Разберем некоторые классические идеи и задачи в динамическом программировании. - -## Рюкзак - -> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. - -## Наибольшая общая подпоследовательность - -> Есть две строки. Найти длину их наибольшей общей подпоследовательности. - - - -## Расстановка скобок - -> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. - -Можно завести динамику «можно ли ». - -## Наибольшая возрастающая подпоследовательность - -Есть также алгоритм за $O(n \log n)$. - -## Мемоизация - -Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. - -Первый предполагает - -## Число разбиений - -> - -## Замена параметра на ответ - -> diff --git a/web/ru/dp-optimizations.html b/ru/dp-optimizations.html similarity index 100% rename from web/ru/dp-optimizations.html rename to ru/dp-optimizations.html diff --git a/ru/dp-optimizations.md b/ru/dp-optimizations.md deleted file mode 100644 index f48dd53..0000000 --- a/ru/dp-optimizations.md +++ /dev/null @@ -1,167 +0,0 @@ - -## Оптимизации динамики - -* Разделяй и властвуй -* Оптимизация Кнута -* Convex Hull Trick -* Лямбда-оптимизация - -## Задача - -> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. - -Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). - -Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. - - -```c++ -// x[] — отсортированный массив координат точек, нумерация с нуля - -// квадрат длины отрезка от i-той до j-той точки -int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } - -// TODO: предподсчитать cost - -for (int i = 0; i <= m; i++) - f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо -// все остальные f предполагаем равными бесконечности - -for (int i = 1; i <= n; i++) - for (int j = 1; j <= m; j++) - for (int k = 0; k < i; k++) - f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); -``` - -Заметим, что циклы по `i` и `j` можно поменять местами. - -Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. - -## Разделяй-и-властвуй - -Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. - -*Утверждение.* $opt[i, j] \leq opt[i, j+1]$. - -Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. - -Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. - -Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. - - -```c++ -void solve (int l, int r, int _l, int _r, int k) { - if (l > r) return; // отрезок пустой — выходим - int t = (l + r) / 2; - int opt = _l; - for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы - int val = f[i+1][k-1] + cost(i, j); - if (val < f[t][k]) - f[t][k] = val, opt = i; - } - solve(l, t-1, _l, opt, k); - solve(t+1, r, opt, _r, k); -} -``` - -Вызываться она будет просто последовательно для каждого слоя: - - -```c++ -for (int k = 1; k <= m; k++) - solve(0, n-1, 0, n-1, k); -``` - -Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. - -Получается, что асимптотика улучшилась до $O(n m \log n)$. - -## Оптимизация Кнута - -Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? - -$$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ - -В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. - -Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: - -$$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ - -Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. - - -```c++ -for (int i = 1; i <= n; i++) { - for (int j = m; j >= 1; j--) { - for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { - int val = f[i+1][k-1] + cost(i, j); - if (val < f[t][k]) - f[t][k] = val, opt[i][j] = i; - } - } -} -``` - -Сравните с базовым решением — всего 3 новых строчки. - -## Convex Hull Trick - -Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. - -$$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ -f[k, j-1] + x_{i-1}^2 -- 2x_{i-1} x_k -+ x_k^2 -\}$$ - -Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется -$ -\underbrace{f[k, j-1] + x_k^2}_{a_k} -\underbrace{-2x_k}_{b_k} x_{i-1} -$. - -Это теперь можно переписать как $ -\min_k -(a_k, b_k) -\cdot -(1, X_{i-1}) -$ (имеется в виду скалярное произведение) - -Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. - -TODO: иллюстрация. - -TODO: мем про Скуби-Ду. - -### Ли Шао - -Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. - -## Лямбда-оптимизация - -Не «фольклорное» название — дискретный метод множителей Лагранжа. - -Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . - -Заметим следующее: - -Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. - -Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. - -## Суммируем - -TODO: сделать табличку - -* Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. -* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. -* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. -* Лагранж: $O(n \log n)$. Функция должна быть выпуклой. - -## Другие задачи - -Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. - -> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. diff --git a/web/ru/dp_intro.html b/ru/dp_intro.html similarity index 100% rename from web/ru/dp_intro.html rename to ru/dp_intro.html diff --git a/ru/dp_intro.md b/ru/dp_intro.md deleted file mode 100644 index 8badbb7..0000000 --- a/ru/dp_intro.md +++ /dev/null @@ -1,42 +0,0 @@ - -# Динамическое программирование - -test123 - -> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. - -Разберем некоторые классические идеи и задачи в динамическом программировании. - -## Рюкзак - -> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. - -## Наибольшая общая подпоследовательность - -> Есть две строки. Найти длину их наибольшей общей подпоследовательности. - - - -## Расстановка скобок - -> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. - -Можно завести динамику «можно ли ». - -## Наибольшая возрастающая подпоследовательность - -Есть также алгоритм за $O(n \log n)$. - -## Мемоизация - -Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. - -Первый предполагает - -## Число разбиений - -> - -## Замена параметра на ответ - -> diff --git a/web/ru/dp_optimizations.html b/ru/dp_optimizations.html similarity index 100% rename from web/ru/dp_optimizations.html rename to ru/dp_optimizations.html diff --git a/ru/dp_optimizations.md b/ru/dp_optimizations.md deleted file mode 100644 index f48dd53..0000000 --- a/ru/dp_optimizations.md +++ /dev/null @@ -1,167 +0,0 @@ - -## Оптимизации динамики - -* Разделяй и властвуй -* Оптимизация Кнута -* Convex Hull Trick -* Лямбда-оптимизация - -## Задача - -> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. - -Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). - -Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. - - -```c++ -// x[] — отсортированный массив координат точек, нумерация с нуля - -// квадрат длины отрезка от i-той до j-той точки -int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } - -// TODO: предподсчитать cost - -for (int i = 0; i <= m; i++) - f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо -// все остальные f предполагаем равными бесконечности - -for (int i = 1; i <= n; i++) - for (int j = 1; j <= m; j++) - for (int k = 0; k < i; k++) - f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); -``` - -Заметим, что циклы по `i` и `j` можно поменять местами. - -Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. - -## Разделяй-и-властвуй - -Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. - -*Утверждение.* $opt[i, j] \leq opt[i, j+1]$. - -Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. - -Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. - -Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. - - -```c++ -void solve (int l, int r, int _l, int _r, int k) { - if (l > r) return; // отрезок пустой — выходим - int t = (l + r) / 2; - int opt = _l; - for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы - int val = f[i+1][k-1] + cost(i, j); - if (val < f[t][k]) - f[t][k] = val, opt = i; - } - solve(l, t-1, _l, opt, k); - solve(t+1, r, opt, _r, k); -} -``` - -Вызываться она будет просто последовательно для каждого слоя: - - -```c++ -for (int k = 1; k <= m; k++) - solve(0, n-1, 0, n-1, k); -``` - -Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. - -Получается, что асимптотика улучшилась до $O(n m \log n)$. - -## Оптимизация Кнута - -Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? - -$$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ - -В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. - -Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: - -$$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ - -Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. - - -```c++ -for (int i = 1; i <= n; i++) { - for (int j = m; j >= 1; j--) { - for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { - int val = f[i+1][k-1] + cost(i, j); - if (val < f[t][k]) - f[t][k] = val, opt[i][j] = i; - } - } -} -``` - -Сравните с базовым решением — всего 3 новых строчки. - -## Convex Hull Trick - -Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. - -$$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ -f[k, j-1] + x_{i-1}^2 -- 2x_{i-1} x_k -+ x_k^2 -\}$$ - -Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется -$ -\underbrace{f[k, j-1] + x_k^2}_{a_k} -\underbrace{-2x_k}_{b_k} x_{i-1} -$. - -Это теперь можно переписать как $ -\min_k -(a_k, b_k) -\cdot -(1, X_{i-1}) -$ (имеется в виду скалярное произведение) - -Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. - -TODO: иллюстрация. - -TODO: мем про Скуби-Ду. - -### Ли Шао - -Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. - -## Лямбда-оптимизация - -Не «фольклорное» название — дискретный метод множителей Лагранжа. - -Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . - -Заметим следующее: - -Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. - -Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. - -## Суммируем - -TODO: сделать табличку - -* Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. -* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. -* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. -* Лагранж: $O(n \log n)$. Функция должна быть выпуклой. - -## Другие задачи - -Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. - -> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. diff --git a/web/ru/fenwick.html b/ru/fenwick.html similarity index 100% rename from web/ru/fenwick.html rename to ru/fenwick.html diff --git a/ru/fenwick.md b/ru/fenwick.md deleted file mode 100644 index 780f868..0000000 --- a/ru/fenwick.md +++ /dev/null @@ -1,162 +0,0 @@ - -
- -НЯ! -
Эта статья полна любви и обожания. -
Возможно, стоит добавить ещё больше? -
- -# Дерево Фенвика - -Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности. - -## Определение - -Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: - -$$ t_i = \sum_{k=F(i)}^i a_k $$ - -где $F$ это какая-то функцию, для которой выполнено $F(i) \leq i$. Конкретно её определим потом. - -Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле: - -$$ sum(k) = t_k + sum(F(k)-1) $$ - -Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка. - -$F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\log n)$. Популярны две функции: - -* $F_1(x) =$ `x & (x + 1)` -* $F_2(x) =$ `x - (x & -x) + 1` - -Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать. - -**Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\log n)$ таких отрезков. - -## Реализация - -Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$. - - -```c++ -int t[maxn]; - -// возвращает сумму на префиксе -int sum (int r) { - int res = 0; - for (; r > 0; r -= r & -r) - res += t[r]; - return res; -} - -int sum (int l, int r) { - return sum(r) - sum(l-1); -} - -// обновляет нужные t -void add (int k, int x) { - for (; k <= n; k += k & -k) - t[k] += x; -} -``` - -Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии. - -## Многомерный случай - -> $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку - -Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре. - -`sum` перепишется следующим образом: - - -```c++ -int sum (int r1, int r2) { - int res = 0; - for (int i = r1; i > 0; i -= i & -i) - for (int j = r2; j > 0; j -= j & -j) - ans += t[i][j]; - return res; -} -``` - -В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах. - -Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017. - -## Бинпоиск - -Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\log n)$. - - -```c++ -// возвращает индекс, на котором сумма уже больше -int lower_bound (int s) { - int k = 0; - for (int l = logn; l >= 0; l--) { - if (k + (1<= k > i - (i & -i)`. - -Будем перебирать префиксы TODO - -Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя. - -Можно представить дерево так: ячейка 2^k содержит все - -TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию. - -## Название - -Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree». - -Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев. - -Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных). - -В частном случае, когда длина массива равна $2^k$, то дерево будет только одно. diff --git a/web/ru/games.html b/ru/games.html similarity index 100% rename from web/ru/games.html rename to ru/games.html diff --git a/ru/games.md b/ru/games.md deleted file mode 100644 index b5f33db..0000000 --- a/ru/games.md +++ /dev/null @@ -1,197 +0,0 @@ - -# Теория игр - -Начнём с самого баянного примера математической игры, который можно вспомнить: - -> На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков. - -Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так: - -$$ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) $$ - -то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния). - -**Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях. - -## Игры на графах - -В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек». - -Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные* - -Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*. - -Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*. - -## Ретроанализ - -Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности: - -* Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины. -* Вершина *выигрышная* — если из неё есть переход в проигрышную вершину. -* Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные). - -Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся. - -**Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин. - -Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние. - -Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось. - -Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные. - -**Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали. - -* При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). -* При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше. - -Асимптотика составит $O(n + m)$, как и у любого bfs-а. - - -```c++ -vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа -int cnt[maxn]; // счётчик исходящих рёбер - -enum StatusType { win, loss, unknown }; -StatusType status[maxn]; // выгрышность вершины; -// по умолчанию все кроме терминальных считаются unknown -// те, кто в итоге остаются unknown -- ничейные - -queue q = {/* нужно заранее добавить сюда все терминальные вершины*/}; - -while (!q.empty()) { - int v = q.front(); - q.pop(); - for (int u : t[v]) { - cnt[u]--; // удаляем это ребро - if (status[v] == unknown) { - // из u есть ребро в проигрышную -- значит она выигрышная - if (status[v] == loss) - status[u] = win; - // все ребра u ведут в выигрышные вершины -- значит она проигрышная - if (status[v] == win && cnt[u] == 0) { - status[u] = loss; - // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь - if (status[v] != unknown) - q.push(u); - } - } -} -``` - -Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации: - - -```c++ -StatusType dfs(int v) { - if (status[v] != unknown) - return status[v]; - status[v] = loss; - // изменим статус, когда найдём переход в проигрышную вершину - for (int u : g[v]) - if (dfs(u) == loss) - status[v] = win; - return status[v]; -} -``` - -TODO: можно ли здесь циклы учесть? - -## Минимаксные игры - -Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно. - -Типичный граф минимаксной игры выглядит следующим образом: - - - -В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников. - -Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей. - -## Ретроанализ для больших графов - -Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго. - -Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей). - -Ретроанализ нужно оптимизировать. - -### Ограничение перебора - -Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. - -Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит. - -Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли). - - -### Мемоизация позиций - -В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает . - -Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары - -### Альфа-бета отсечение - - - -Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым. - -На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым. - -Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6. - -Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем. - -Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации. - -### Monte-Carlo Tree Search - - - -## Игры с ненулевой суммой - - - -## Теория Шпрага-Гранди - -Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые. - -Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями. - -**Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \oplus a_2 \oplus \ldots \oplus a_n $ размеров кучек отлична от нуля. - -**Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы: - -* Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия. -* Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \oplus a_i \oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$. - -Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \oplus a_i$. - -Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории. - -**Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен. - -### Ним с увеличениями - -Пусть у нас - -### Эквивалентность игр ниму - -## Игры с неполной информацией - -Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков. - -Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет. - -Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло. - -### Дилемма заключенного - -### Камень-ножницы-бумага - -### Покер - -Рассмотрим такую у diff --git a/web/ru/geometry.html b/ru/geometry.html similarity index 100% rename from web/ru/geometry.html rename to ru/geometry.html diff --git a/ru/geometry.md b/ru/geometry.md deleted file mode 100644 index 7cff658..0000000 --- a/ru/geometry.md +++ /dev/null @@ -1,237 +0,0 @@ - -# Геометрия и ООП - -Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали. - -![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg) - -Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь. - -**Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается: - -$$ a \cdot b = x_a x_b + y_a y_b $$ - -![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg) - -У него есть полезные свойства: - -* Скалярное произведение симметрично ($a \cdot b = b \cdot a$). -* Геометрически, это проекция вектора b на вектор a, помноженынй на длину а. -* Перпендикулярные вектора должны иметь нулевое скалярное произведение. -* Если угол острый, то оно положительное -* Если угол тупой, то отрицательное - -**Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах: - -$$ a \times b = x_a y_b - y_a x_b $$ - -![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg) - -Его свойства: - -* Скалярное произведение *анти*симметрично ($a \times b = - (b \times a)$). -* Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора. -* Коллинеарные вектора должны иметь нулевое векторное произведение. -* Если b «слева» от a, то оно положительное. -* Если b «справа» — то отрицательное. - -## Всякие проверки - -Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями. - -**Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником. - -$$ -\text{P лежит внутри ABC} \iff \begin{cases} -(B-A) \times (P-A) \geq 0 \\ -(C-B) \times (P-B) \geq 0 \\ -(A-C) \times (P-C) \geq 0 \\ -\end{cases} -$$ - -**Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения. - -$$ V = \frac{1}{2} (B-A) \times (C-A) $$ - -**Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными). - -![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg) - -Забудьте о формуле Герона и всегда считайте площади через векторное произведение. - -Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2. - -**Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\times(c-a) > 0$). - -**Пересекаются ли отрезки**. - -![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png) - -## Уравнение прямой - -Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством. - -У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \geq 0$ будет указывать в сторону самой полуплоскости. - -Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой: - -$$ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} $$ - -**Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений: - -$$ -\begin{cases} -A_1 x + B_1 y + C_1 = 0 \\ -A_2 x + B_2 y + C_2 = 0 -\end{cases} -\implies -\begin{cases} --x = \frac{B_1 y + C_1}{A_1} \\ --x = \frac{B_2 y + C_2}{A_2} -\end{cases} -\implies -\frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} -\implies -y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} -$$ - -Аналогично, $x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки). - -Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать. - -# Как это кодить в C++ - -Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. - -**Точка $\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно. - - -```c++ -struct r { - double x, y; - r () {} - r (int _x, int _y) { x = _x, y = _y; } -}; -``` - -Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$. - -## Операции над векторами - -Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину: - - -```c++ -double len (r a) { return sqrt(a.x*a.x + a.y*a.y); } -``` - -## Операторы - -В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д. - -Давайте для будущих нужд определим `+` и `-`: - - -```c++ -r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); } -r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); } -``` - -## Скалярное произведение - - -```c++ -int operator*(r a, r b){ return a.x*b.x + a.y*b.y; } -``` - -## Векторное произведение - -Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике. - - -```c++ -int operator^(r a, r b){ return a.x*b.y - b.x*a.y; } -``` - -### Ввод-вывод - -Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так: - - -```c++ -istream& operator>>(istream &in, r &p){ - in >> p.x >> p.y; - return in; -} - -ostream& operator<<(ostream &out, r &p){ - out << p.x << " " << p.y << endl; - return out; -} -``` - -## Алгебра VS Алгоритмы или зачем мы всё это делали - -Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен. - -### Векторное представление прямой ($Ax + By + C = 0 \rightarrow r = at + b$) - -Тут нужно просто выбрать две любые точки на прямой. - - -```c++ -// даны A, B, C (A^2 + B^2 != 0) -r a, b; -if (eq(A, 0)) // значит, это горизонтальная прямая - a = r(0, -C/B), b = r(1, -C/B); -else - a = r(-C/A, 0), b = (1, -(C+B)/A, 1) -``` - -### Пример: отражение от прямой - -Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать. - -$\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} $ - -Формула имеет смысл: длина на единичный вектор направления. - -Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить. - - -```c++ -// прямая r = at + b, точка c -r pr (r a, r b, r c) { - c -= b; // пусть c и a выходят из одной точки - return b + (a*b / len(a) / len(a)) * a; -} - -r reflect (r a, r b, r c) { - return c + 2*(pr(a, b, c)-c); -} -``` - -## Типичные баги - -### Точность - -> Первое правило действительных чисел — не использовать действительные числа - -Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`: - - -```c++ -const double eps = 1e-8; - -bool eq (double a, double b) { return abs(a-b) < eps } -``` - -Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре. - -### $0 \neq -0$ - -Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду. - -### Область определения обратных функций - -`acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции. diff --git a/web/ru/hashing.html b/ru/hashing.html similarity index 100% rename from web/ru/hashing.html rename to ru/hashing.html diff --git a/ru/hashing.md b/ru/hashing.md deleted file mode 100644 index 0838d72..0000000 --- a/ru/hashing.md +++ /dev/null @@ -1,224 +0,0 @@ - -# Хэширование в строковых задачах - -Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка. - -«Хорошая» хэш-функция: -* Быстро считается — за линейное от размера объекта время; -* Имеет не очень большие значения — влезающие в 64 бита; -* «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\frac{1}{n}$. - -Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*. - -Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \cdot n \cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки. - - ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png) - -## Применения в реальной жизни - -* **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить. -* **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\frac{k}{n}$ = $O(1)$ при правильном выборе $n$. -* **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш. -* **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками. -* **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить. -* **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса. - -Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы. - -Сегодня же мы остановимся на строках. - -# Полиномиальное хэширование - -**Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами. - -Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`. - -Определим *прямой полиномиальный хэш* строки как значение следующего многочлена: - -$$ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p $$ - -Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой. - -Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$: - - -```c++ -const int k = 31, mod = 1e9+7; - -string s = "abacabadaba"; -long long h = 0, m = 1; -for (char c : s) { - int x = (int) (c - 'a' + 1); - h = (h + m * x) % mod; - m = (m * k) % mod; -} -``` - -Можем ещё определить *обратный полиномиальный хэш*: - -$$ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p $$ - -Его преимущество в том, что можно написать на одну строчку кода меньше: - - -```c++ -long long h = 0; -for (char c : s) { - int x = (int) (c - 'a' + 1); - h = (h * k + x) % mod; -} -``` - -Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$. - -## Зачем он нужен? - -Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций. - -Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$: - -$$ h(ab) = h(a) + k^{|a|} \cdot h(b) $$ - -Удалить префикс строки можно так: - -$$ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} $$ - -А суффикс — ещё проще: - -$$ h(a) = h(ab) - k^{|a|} \cdot h(b) $$ - -В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве: - - -```c++ -const int maxn = 1e5+5; - -int p[maxn]; -p[0] = 1; - -for (int i = 1; i < maxn; i++) - p[i] = (p[i-1] * k) % mod; -``` - -Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса: - - -```c++ -int h[maxn]; -h[0] = 0; // h[k] -- хэш префикса длины k - -// будем считать, что s это уже последовательность int-ов - -for (int i = 0; i < n; i++) - h[i+1] = (h[i] + p[i] * s[i]) % mod; -``` - -Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке: - -$$ h(s[l:r]) = \frac{h_r-h_l}{k^l} $$ - -Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим. - -Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить. - -$$ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$ - - -```c++ -int hash_substring (int l, int r) { - return (h[r+1] - h[l]) * p[n-l] % mod; -} -``` - -Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$. - -### Упражнение - -Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. - -## Примеры задач - -**Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`. - -**Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа. - -**Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним. - -**Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором. - -**Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно. - -### Изменения строки* - -Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына. - -Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне. - -Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова. - -# Вероятность ошибки и почему это всё вообще работает - -У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения. - -Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого? - -## Выбор констант - -Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \cdot n^2$. Обоснование — см. парадокс дней рождений. - -Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно. - -Можно также брать модуль $2^{64}$. У него есть несколько преимуществ: -* Он большой — второй модуль точно не понадобится. -* С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении. -* С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`. - -Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду. - -В выборе же $k$ ограничения не такие серьезные: -* Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. -* Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться. - -Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты. - -## Парадокс дней рождений - -> В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%. - -Более общее утверждение: в мультимножество нужно добавить $\Theta(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. - -**Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. -Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. - -$$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ - -Попытаемся оценить $f$: - -$$ -\begin{align} - \begin{aligned} - e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ - & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ - e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ - f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ - & = e^{-\frac{n(n-1)}{2d}} & \\ - & \simeq e^{-\frac{n^2}{2d}} & \\ - \end{aligned} -\end{align} -$$ - -Из последнего выражения более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется очень быстро. - -**Второе доказательство** (для любителей теорвера). Введем $\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\frac{1}{d}$. - -Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\frac{n (n-1)}{2} \cdot \frac{1}{d}$. - -Отсюда понятно, что если $d = \Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно. - -*Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1. - -### Бонус: «мета-задача» - -Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. - -«Решите» задачу. diff --git a/web/ru/hld.html b/ru/hld.html similarity index 100% rename from web/ru/hld.html rename to ru/hld.html diff --git a/ru/hld.md b/ru/hld.md deleted file mode 100644 index 8354d38..0000000 --- a/ru/hld.md +++ /dev/null @@ -1,120 +0,0 @@ - -# Heavy-light декомпозиция - -HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid). - - - -*
TODO: найти менее уродливую иллюстрацию
* - -Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого. - -Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения. - -**Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер. - -**Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра. - -Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер. - -**Утверждение.** На любом вертикальном пути будет не более $O(\log n)$ блоков. - -**Доказательство** разбивается на две части: - -* Лёгких ребер на вертикальном пути будет не более $O(\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего. -* Непрерывных путей из тяжелых рёбер будет не более $O(\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\log n)$. - -**Следствие.** На любом пути будет не более $O(\log n)$ блоков. - -Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам. - -## Реализация - -Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера. - -А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь. - -Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре. - - -```c++ -vector g[maxn]; -int s[maxn], p[maxn], tin[maxn], tout[maxn]; -int head[maxn]; // «голова» тяжелого пути, которому принадлежит v -int t = 0; - -void sizes (int v = 0) { - s[v] = 1; - for (int &u : g[v]) { - sizes(u); - s[v] += s[u]; - if (s[u] > s[g[v][0]]) - // &u -- это ссылка, так что её легально использовать при swap-е - swap(u, g[v][0]); - } -} - -void hld (int v = 0) { - rin[t] = v; - tin[v] = t++; - for (int u : g[v]) { - // если это тяжелый ребенок -- его next нужно передать - // в противном случае он сам является головой нового пути - head[u] = (u == g[v][0] ? head[v] : u); - hld(u); - } - tout[v] = t; -} -``` - -## Как им решать задачи - -Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов: - -1. Узнать минимальное число на пути между $v_i$ и $u_i$. -2. Изменить число у $v_i$-той вершины на $x_i$. - -Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`. - - -```c++ -int val[maxn]; -segtree st(0, n); -``` - -При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков: - - -```c++ -void upd (int v, int x) { - st.upd(tin[v], x); -} -``` - -Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям. - - -```c++ -int ancestor (int a, int b) { - return tin[a] <= tin[b] && tin[b] <= tout[a]; -} - -void up (int &a, int &b, int &ans) { - while (!ancestor(head[a], b)) { - ans = min(ans, st.get_min(tin[head[a]], tin[a])); - a = p[head[a]]; - } -} - -int get_min (int a, int b) { - int ans = inf; - up(a, b, ans); - up(b, a, ans); - if (!ancestor(a, b)) - swap(a, b); - ans = min(ans, st.get_min(tin[a], tin[b])); - return ans; -} -``` - - diff --git a/web/ru/index.html b/ru/index.html similarity index 100% rename from web/ru/index.html rename to ru/index.html diff --git a/ru/index.md b/ru/index.md deleted file mode 100644 index e213e68..0000000 --- a/ru/index.md +++ /dev/null @@ -1,51 +0,0 @@ - -# Алгоритмика - -Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/). - -Более-менее готово: - -* [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное -* [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое -* [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки -* [Центроидная декомпозиция](http://sereja.me/a/centroid) -* [Heavy-light декомпозиция](http://sereja.me/a/hld) -* [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла -* [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву -* [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость -* [Ро-алгоритм Полларда](http://sereja.me/a/pollard) -* [Sparse table](http://sereja.me/a/sparse-table) -* [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона -* [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса -* [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера -* [Классные задачки](http://sereja.me/a/bayans): пока что без решений - -Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально. - -Не очень готово: - -* [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция -* [Суффиксный массив](http://sereja.me/a/suffix-array) -* [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++ -* [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick -* [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала -* [Обратное по модулю](http://sereja.me/a/reciprocal) -* [Метод отжига](http://sereja.me/a/annealing) -* [Стресс-тестирование](http://sereja.me/a/stress-test) -* [Бор](http://sereja.me/a/trie) -* [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько -* Выпуклые оболочки -* [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса -* CUDA: программирование на GPU (на английском) - -Вообще не готово: -* [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема -* [Ахо-Корасик](http://sereja.me/a/aho-corasick) -* [Теорвер](http://sereja.me/a/probability) -* Корневая декомпозиция -* Теория информации -* Функции потерь в машинном обучении -* Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском) -* Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском) - -Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности. diff --git a/web/ru/ioi.html b/ru/ioi.html similarity index 100% rename from web/ru/ioi.html rename to ru/ioi.html diff --git a/ru/ioi.md b/ru/ioi.md deleted file mode 100644 index 91928d3..0000000 --- a/ru/ioi.md +++ /dev/null @@ -1,70 +0,0 @@ - -# IOI Upsolving Challenge - -https://contest.yandex.com/ioi/total - -Я решал их почти 2 года назад и уже не очень хорошо всё помню. - -# 09-2 - -12 мая - -### Медведь Миша - -Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за $O(n^2 \log n)$. - -### Регионы - -В ограничениях нам спалили важную идею: задачу можно просто решить в случае, если все регионы небольшие, а также в случае, если регионов немного. Обычно это означает, что решения можно объединить с помощью корневой эвристики. - -Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так: - -* Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк -* Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос - -Такое должно работать за $O(n \sqrt{n \log n})$, и TL такое позволяет. - -### Коммивояжёр - -Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать. - -Подумайте, какой вид будет иметь эта функция, если зафиксировать время. - -TODO - -# 05-1 - -31 мая - -### Сад - -Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно. - -### Средняя последовательность - -Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы: - -```c++ -int a = -inf, b = inf, _d = 0, d = 0, t; -for(int i = 0; i < n; i++){ - cin >> t; - _d = d; - d = 2*t - d; - if (i % 2 == 0) - b = min(b, (d-_d)/2); - else - a = max(a, (_d-d)/2); -} - -cout << max(b - a + 1, 0ll); -``` - -### Польские горки - -По сути от нас просят обрабатывать запросы «присвоить всем элементам с $l$ по $r$ значение $x$» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше. - -# 05-2 - -### Прямоугольник - -Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста. diff --git a/web/ru/karatsuba.html b/ru/karatsuba.html similarity index 100% rename from web/ru/karatsuba.html rename to ru/karatsuba.html diff --git a/ru/karatsuba.md b/ru/karatsuba.md deleted file mode 100644 index 27ad057..0000000 --- a/ru/karatsuba.md +++ /dev/null @@ -1,69 +0,0 @@ - -# Быстрое умножение - -В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел). - -[Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал. - -Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба. - -Алгоритм Карацубы имеет довольно значимое место в истории науки. - -{\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}). -У Колмогорова была гипотеза, что нижняя оценка для {\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности -{\displaystyle M(n)=O(n^{\log _{2}3})} M(n)=O(n^{{\log _{2}3}}) -и тем самым опроверг «гипотезу {\displaystyle n^{2}} n^{2}». - -(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.) - -Можно представить себе какого-то грозного японца. - - - -## Мастер-теорема - -Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек». - -**Мастер-теорема.** Пусть имеется рекуррента: - -$$ T(n) = \begin{cases} -a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 -\\ \Theta(1), & n \leq n_0 -\end{cases} $$ - -Тогда: - -* **A.** Если $c > \log_b a$, то $T(n) = \Theta(n^c)$. -* **B.** Если $c = \log_b a$, то $T(n) = \Theta(n^c \log n)$. -* **C.** Если $c < \log_b a$, то $T(n) = \Theta(n^{\log_b a})$. - ---- - - - ---- - -**Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням: - -$$ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k $$ - -* **A.** Если $c > \log_b a$, то $\sum (\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \Theta(n^c)$. -* **B.** Если $c = \log_b a$, то -$$\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)$$ -* **C.** Если $c < \log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу, - -$$ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) $$ - -Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$ - -## Алгоритм Карацубы - -Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат. - -Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. - -## Развитие идеи - -То же самое можно применить матрицам. - -Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\epsilon})$ для произвольного $\epsilon$ ещё никто не доказал. diff --git a/web/ru/lca.html b/ru/lca.html similarity index 100% rename from web/ru/lca.html rename to ru/lca.html diff --git a/ru/lca.md b/ru/lca.md deleted file mode 100644 index 9665c9b..0000000 --- a/ru/lca.md +++ /dev/null @@ -1,193 +0,0 @@ - -# Корневые деревья - -Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*. - -Примеры корневых деревьев: -* наследование классов в языках программирования (если множественное наследование запрещено), -* дерево факторизации числа на простые (в общем случае не уникальное), -* иерархия в какой-нибудь компании, -* просто множество как-то вложенных объектов (ссылка на крысу-роботягу). - -Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах. - -![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png) - -## Напоминание: DFS - -Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. - - -```c++ -vector g[maxn]; -int p[maxn], tin[maxn], tout[maxn]; -int t = 0; - -void dfs (int v) { - tin[v] = t++; - for (int u : g[v]) - dfs(u); - tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет -} -``` - -У этих массивов много полезных свойств: -* Вершина $u$ является предком $v$ $\iff tin_v \in [tin_u, tout_u) $. Эту проверку можно делать за константу. -* Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой. -* В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер. -* Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$. -* Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации. - -## Запросы на поддеревьях - -Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО. - -> Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$. - -Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree). - -## Запросы на уровнях - -> Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$). - -Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам. - -Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку. - -## Наименьший общий предок - -Очень много задач нам поможет решить следующая вспомогательная задача. - -> Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких. - -По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные. - -![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png) - -Для лучшего понимания: медленно (за линейное время) это можно делать так: - - -```c++ -bool a (int u, int v) { - return tin[u] <= tin[v] && tin[v] <= tout[u]; -} - -int lca (int u, int v) { - while (!ancestor(u, v)) - u = p[u]; - return u; -} -``` - -## LCA: двоичные подъемы - -Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \times \lceil \log n \rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень. - -Такой препроцессинг можно выполнить за $O(n \log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$: - - -```c++ -int up[maxn][logn]; - -void dfs (int v) { - for (int l = 1; l < logn; l++) - up[v][l] = up[up[v][l-1]][l-1]; - tin[v] = t++; - for (int u : g[v]) { - up[u][0] = v; - dfs(u); - } - tout[v] = t++; -} -``` - -Пусть поступил очередной запрос — пара вершин $(u, v)$: -* Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. -* Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA. - -Подробнее про второй пункт. Пусть $L = \lceil \log n \rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$. - - -```c++ -int lca (int v, int u) { - if (a(v, u)) return v; - if (a(u, v)) return u; - for (int l = logn-1; l >= 0; l--) - if (!ancestor(up[v][l], u)) - v = up[v][l]; - return up[v][0]; -} -``` - -### Асимптотика - -Препроцессинг — $O(n \log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу. - -Ответ на запрос — $O(\log n)$, потому что по сути мы делаем один бинпоиск. - -## Запросы на путях - -Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа). - -Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути. - -Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA. - - -```c++ -int get_min (int v, int u) { - int ans = inf; - for (int l = logn-1; l >= 0; l--) - if (!ancestor(up[v][l], u)) - v = up[v][l], ans = min(ans, mn[v][l]); - for (int l = logn-1; l >= 0; l--) - if (!ancestor(up[u][l], v)) - u = up[u][l], ans = min(ans, mn[u][l]); - return min({ans, mn[v][0], mn[u][0]}) -} -``` - -Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная. - -## Сведение к RMQ - -Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить. - - - -Во втором массиве мы по сути выписали наш проход dfs-а. - -Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути. - -Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree). - -### Разреженная таблица - -На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива. - -Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table). - -## А наоборот можно?* - -*Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения. - -Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. -Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя. - -Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль. - -Возьмем константу $k = \lfloor \frac{\log n}{2} \rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\frac{2 n}{\log n})$, и построение будет работать за линейное время: - -$$O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)$$ - -Также посчитаем для каждой возможной маски размера $\frac{\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\sqrt n$ (ради этого мы и делили логарифм на два). - -ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу. - -Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали. - -Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear). - -Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики. - -Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.** diff --git a/web/ru/linalg.html b/ru/linalg.html similarity index 100% rename from web/ru/linalg.html rename to ru/linalg.html diff --git a/ru/linalg.md b/ru/linalg.md deleted file mode 100644 index 5efa4ea..0000000 --- a/ru/linalg.md +++ /dev/null @@ -1,315 +0,0 @@ - -# Ликбез по линейной алгебре - - - -**Определение**. Функция $f: \mathbb{R}^n \to \mathbb{R}^m$ называется *линейной*, если для неё выполнено - -1. $ f(x+y) = f(x) + f(y) $ -2. $ f(ax) = a f(x), \; a \in R $ - -Примеры: -* $ f(x) = 0 $ -* $ f(x) = x $ -* $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\mathbb{R}^3$ в $\mathbb{R}$) -* $ f(x) = (x, -x, 0) $ (из $\mathbb{R}$ в $\mathbb{R}^3$) - -Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: -* Сумма линейных функций — линейная функция. -* Сумма коммутативна: $f+g = g+f$). -* Сумма ассоциативна: $(f+g)+h = f+(g+h)$. -* Композиция $f(g(x)) = (f \circ g)(x)$ линейных функций — линейная функция. -* Композция ассоциативна: $(f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h$. -* Композиция в общем случае не коммутативна.
-Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен. - -Все свойства можно вывести лишь из этих двух пунктов в определении. - -## Что такое матрица? - -Можно показать, что любую линейную функцию $f: \mathbb{R}^n \to \mathbb{R}^m$ можно представить в таком виде: - -$$ -f(x) = -\begin{pmatrix} -a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ -a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ -\ldots \\ -a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ -\end{pmatrix} -$$ - -*Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$. - -$$ -A = -\begin{pmatrix} -a_{11} & a_{12} & \ldots & a_{1n} \\ -a_{21} & a_{22} & \ldots & a_{2n} \\ -\vdots & \vdots & \ddots & \vdots \\ -a_{m1} & a_{m2} & \ldots & a_{mn} \\ -\end{pmatrix} -$$ - -Каждой линейной функции из $\mathbb{R}^n$ в $\mathbb{R}^m$ соответствует какая-то матрица размера $n \times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте. - -Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так: - -$$ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} $$ - -Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$. - -Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \times k$, то вторая должна иметь размер $k \times m$, то есть «средние» размерности обязательно должны совпадать. - - - -Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом. - -К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы. - -Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы: - -* Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$. -* Сумма коммутативна: $A+B = B+A$) -* Сумма ассоциативна: $(A+B)+C = A+(B+C)$ -* Умножение ассоциативно: $(AB)C = A(BC) = ABC$. -* Умножение в общем случае не коммутативно. - -**Пример**: матрица поворота в 2d. -$$ -\begin{pmatrix} -\cos \alpha & -\sin \alpha \\ -\sin \alpha & \cos \alpha \\ -\end{pmatrix} -$$ - -**Пример**: матрица проецирования на $Ox$ в 3d. -$$ -\begin{pmatrix} -1 & 0 & 0 \\ -0 & 0 & 0 \\ -0 & 0 & 0 \\ -\end{pmatrix} -$$ - -**Пример**: матрица «свапни $x$ и $y$». -$$ -\begin{pmatrix} -0 & 1 \\ -1 & 0 \\ -\end{pmatrix} -$$ - -Напишем класс, который реализует матричное умножение. - - -```c++ -struct matrix { - int n, m; - int t[]; - matrix (int _n, int _m) { - n = _n, m = _m; - t = new int(n*m); - memset(t, 0, sizeof t); - } - int[] operator[] (int k) { - return t[k*m]; - } -} - -matrix operator* (matrix a, matrix b) { - matrix c(a.n, b.m); - for (int i = 0; i < a.n; i++) - for (int j = 0; j < b.m; j++) - for (int k = 0; k < a.m; k++) - c[i][j] += a[i][k] * b[i][k]; - return c; -} -``` - -## Динамика - -Некоторые динамики можно выразить в терминах матричного умножения. - -Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика? - -$$ -\begin{pmatrix} -f_{n+1} \\ -f_{n+2} \\ -\end{pmatrix} -= -\begin{pmatrix} -0+f_{n+1} \\ -f_{n}+f_{n+1} \\ -\end{pmatrix} -= -\begin{pmatrix} -0 & 1 \\ -1 & 1 \\ -\end{pmatrix} -\begin{pmatrix} -f_{n} \\ -f_{n+1} \\ -\end{pmatrix} -$$ - -Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$. - -Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам: - -$$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$ - -Это будет работать за $O(n^3 \log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны. - - -```c++ -matrix binpow (matrix a, int p) { - matrix b(n, n); - for (int i = 0; i < n; i++) - b[i][i] = 1; - while (p) { - if (p&1) b = b*a; - a = a*a; - p >>= 1; - } - return b; -} -``` - -Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$. - -$$ -\begin{vmatrix} -1 & 0 & 0 \\ -0 & 1 & 0 \\ -0 & 0 & 1 \\ -\end{vmatrix} -$$ - -В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы. - -В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}$ имеет такую матрицу перехода: - -\begin{pmatrix} -0 & 1 & 0 & \ldots & 0 \\ -0 & 0 & 1 & \ldots & 0 \\ -\vdots & \vdots & \vdots & \ddots & \vdots \\ -0 & 0 & 0 & \ldots & 1 \\ -a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ -\end{pmatrix} - -## Матрица смежности - -У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов. - -Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86)). - -## Обобщения - -Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел. - -Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два. - -## Определитель - -*Определителем* кадратной матрицы $A$ называется такое выражение: - -$$ \det A = |A| = \ldots $$ - -Где $\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например: - -$$ -\begin{vmatrix} -a & b \\ -c & d \\ -\end{vmatrix} -= -ad - bc -$$ - -У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться. - -В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем. - -## Базис - -Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. - -Базисы есть не только в линейной алгебре. Например, $\{1, x, x^2\}$ является базисом всех квадратных трёхчленов. Или $\{\neg, \land, \lor\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом. - -## Метод Крамера и easy пересечение прямых - -Пусть нам надо пересечь две прямые. - -$$ -\begin{cases} -a_1 x + b_1 y + c_1 = 0 -a_2 x + b_2 y + c_2 = 0 -\end{cases} -$$ - -Это то же самое, что найти такие коэффициенты $x$ и $y$, что - -$$ x \vec{a} + y \vec{b} = -\vec{c} $$ - -Площадь параллелограмма, натянутого на $\vec{a}$ и $\vec{b}$, равна векторному произведению, или детерминанту. - -По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$. - -Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да? - -## *Собственные векторы - -Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление. - -$ Av = k v $, где $k \neq 0$. - -$ Av - kv = (A-kI)v = 0 $. Это означает - -Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике. - -## Системы уравнений и метод Гаусса - -Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. - -Нас по сути просят решить следующую систему: - -$$ -\begin{cases} -a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ -a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ -\ldots \\ -a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 -\end{cases} -$$ - -Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. - -Метод Крамера неоптимален — там $O(n^4)$ операций. - -В таком случае можно значительно ускорить и упростить обычный метод Гаусса: - - -```c++ -t gauss (matrix a) { - for (int i = 0; i < n; i++) { - int nonzero = i; - for (int j = i+1; j < n; j++) - if (a[j][i]) - nonzero = j; - swap(a[nonzero], a[i]); - for (int j = 0; j < n; j++) - if (j != i && a[j][i]) - a[j] ^= a[i]; - } - t x; - for (int i = 0; i < n; i++) - x[i] = a[i][n] ^ a[i][i]; - return x; -} -``` - -Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. - -Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81)). diff --git a/web/ru/matching.html b/ru/matching.html similarity index 100% rename from web/ru/matching.html rename to ru/matching.html diff --git a/ru/matching.md b/ru/matching.md deleted file mode 100644 index 46027c8..0000000 --- a/ru/matching.md +++ /dev/null @@ -1,139 +0,0 @@ - -# Паросочетания - -Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом. - - - -**Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$). - -Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием. - -*Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны. - -Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$. - -**Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер. - -**Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию. - -**Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию. - -![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg) - -*Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \to 8 \to 4 \to 6 \to 3 \to 7$.* - -Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу. - -В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу. - -**Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже. - - -```c++ -const int maxn; - -vector g[maxn]; // будем хранить только рёбра из левой доли в правую -int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой) -bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом - -// dfs возвращает, можно ли найти путь из вершины v -// в какую-нибудь вершину правой доли -// если можно, то ещё и проводит чередование -bool dfs (int v) { - if (used[v]) - return false; - used[v] = true; - for (int u : g[v]) { - // если вершина свободна, то можно сразу с ней соединиться - // если она занята, то с нейможно соединиться только тогда, - // когда из её текущей пары можно найти какую-нибудь другую вершину - if (mt[u] == -1 || dfs(mt[u])) { - mt[u] = v; - return true; - } - } - return false; -} - - -// где-то в main: - -memset(mt, -1, sizeof(mt)); -for (int i = 0; i < n; i++) { - memset(used, 0, sizeof(mt)); - if (dfs(i)) - cnt++; -} -``` - -## Корректность - -Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить. - -**Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным. - -**Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу. - -Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие. - -## Скорость работы - -Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$. - -Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \approx 10^4$), потому что для него есть мощные неасимптотические оптимизации: - -* Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть). - -* Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу. - -* Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются. - -Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \sqrt m)$. - -## Покрытие путями DAG-а - -Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути. - -Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\overline{H}$ следующим образом: -* В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно. -* Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$. -* Теперь из графа $H$ сделаем граф $\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$. - -Если мы рассмотрим любой путь $v_1, v_2, \ldots, v_k$ в исходном графе $G$, то в графе $\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$. - -Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\overline{H}$ — это паросочетание в $H$ (напомним, это $\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$. - -Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них. - -Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин. - -## Лемма Холла - -*Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно. - -![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif) - -**Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли. - -**Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием. - -В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу. - -**База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. - -**Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием. - -Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна) - -Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$). - -Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно. - -## Для ноулайферов: матроиды - -С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid). - -Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*. - -**Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна. diff --git a/web/ru/matroid.html b/ru/matroid.html similarity index 100% rename from web/ru/matroid.html rename to ru/matroid.html diff --git a/ru/matroid.md b/ru/matroid.md deleted file mode 100644 index c3479af..0000000 --- a/ru/matroid.md +++ /dev/null @@ -1,102 +0,0 @@ - -# Матроиды - -Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры). - -**Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства: - -* Пустое множество является независимым: $\varnothing \in I$ - -* Любое подмножество независимого множества тоже независимо: -$$A \subset B, B \in I \implies A \in I$$ - -* Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$: -$$A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I$$ - -Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \sum w(a_i)$. - -Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ: - - -```python -X.sort() -s = [] -for x in X: - if good(s + [x]): - s += [x] -``` - -Здесь под `good` имеется в виду $s \cup x \in I$. - -Корректность этого алгоритма для любого матроида доказывает следующая теорема: - -## Теорема Радо-Эдмондса - -Пусть $A \in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \cup x \in I,\;x \notin A,\;w(x)$ — минимальна. Тогда $A \cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. - -*Доказательство: ** - -Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. - -Из свойств матроида: $\exists y \in B \setminus A : A \cup y \in I$. - -Тогда верны два неравенства: - -$$ -\begin{cases} -w(A \cup y) = w(A) + w(y) \geq w(B) \implies w(A) \geq w(B) - w(y) \\ -w(B \setminus y) = w(B) - w(y) \geq w(A) \implies w(A) \leq w(B) - w(y) -\end{cases} -$$ - -Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно, -$w(A \cup y) = w(A) + w(y) = w(B)$. - -Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \cup x \in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. - -Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым. - -## Примеры - -Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее. - -### Минимальный остов -Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом: - -* Граф без ребер является лесом. -* Если удалить из леса ребра, он останется лесом. -* Пусть есть два леса $|A| \leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \cup \{x\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности. - -Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова. - -### Расписания -Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград. - -Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \geq i$ для всех $i$. - -Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: - -* Пустой набор заданий всегда можно сделать. -* Если у нас стало меньше заданий, то их сделать мы тоже успеем. -* Пусть есть два правильных набора $|A| \leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \cup x$ — тоже правильный набор. - -Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных. - -### Паросочетания -Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом: - -* Любое паросочетание покрывает пустое множество вершин. -* Исходное паросочетание покрывает также и любое подмножество исходных вершин. -* Пусть есть два множества вершин $|A| \leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$. - -Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна. - -### Линейно независимые вектора - -(TODO) (Школьники не обязаны знать линал.) - -Такие штуки будем называть базисами. - -* Ноль есть в любом базисе. -* Подмножество базиса — базис. -* ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше. diff --git a/web/ru/mincost-maxflow.html b/ru/mincost-maxflow.html similarity index 100% rename from web/ru/mincost-maxflow.html rename to ru/mincost-maxflow.html diff --git a/ru/mincost-maxflow.md b/ru/mincost-maxflow.md deleted file mode 100644 index 61c2a55..0000000 --- a/ru/mincost-maxflow.md +++ /dev/null @@ -1,148 +0,0 @@ - -# Поток минимальной стоимости - -Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна: - -$$ \sum_{(u, v) \in E} f_{uv} \to \max $$ -$$ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min $$ - -Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет. - -Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$). - -## Критерий оптимальности - -Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот). - -**Доказательство:** - -$\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению. - -$\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости. - -### Отмена циклов - -Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу. - -Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли). - -## Дополняющие пути - -Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости. - -**Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса. - -Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$. - -Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз. - -Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер. - -## Потенциалы Джонсона - -Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами: - -$$ w_{uv}' = w_{uv} + d_u - d_v $$ - -**Утверждение 1**. Веса всех рёбер графа неотрицательные. - -**Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$? - -Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой: -$$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$ - -**Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими. - -**Доказательство**. Распишем новую стоимость пути из $a$ в $z$. - -$$ -\begin{align} -w_{ab}' + \ldots + w_{yz}' -&= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) -\\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z -\end{align} -$$ - -Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу. - -Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути. - -Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа. - -**Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими). - -**Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2). - -## Итоговый алгоритм - -* Модифицируем сеть, добавивив обратные рёбра. -* Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю. -* Пока максимальный поток не найден: -* * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$. -* * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \leadsto t$, обновить остаточную сеть. - -### Асимптотика - -Алгоритм работает за $O(U m \log n)$ или $O(U n^2)$ в случае плотных графов. - -В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \leq n$ или $U \leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма. - -### Реализация - -Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию). - -* `cost`, `cap` — параметры сети -* `pot` — потенциалы -* `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока) -* `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния - - -```c++ -const int maxn = 305, inf = 1e9; - -int n; -int cost[maxn][maxn], cap[maxn][maxn]; -int d[maxn], pot[maxn], par[maxn]; - -bool dijkstra (int s, int t) { - used[maxn] = {0}; - - fill(d, d+n, inf); - d[s] = 0; - - while (1) { - int v = -1; - for (int u = 0; u < n; u++) - if (!used[u] && (v == -1 && d[u] < d[v])) - v = u; - if (v == -1 || d[v] == inf) - break; - used[v] = 1; - for (int u = 0; u < n; u++) { - int w = cost[v][u] + pot[v] - pot[u]; - if (cap[v][u] && d[u] > d[v] + w) { - d[u] = d[v] + w; - par[u] = v; - } - } - } - - return d[t] < inf; -} - -int mincost_maxflow (int s, int t) { - int ans = 0; - while (dijkstra(s, t)) { - memcpy(pot, d, sizeof(d)); - int delta = inf; - for (int v = t; v != s; v = par[v]) - delta = min(delta, cap[par[v]][v]); - for (int v = t; v != s; v = par[v]) { - cap[par[v]][v] -= delta; - cap[v][par[v]] += delta; - ans += cost[par[v]][v]*delta; - } - } - return ans; -} -``` diff --git a/web/ru/mst.html b/ru/mst.html similarity index 100% rename from web/ru/mst.html rename to ru/mst.html diff --git a/ru/mst.md b/ru/mst.md deleted file mode 100644 index da0c712..0000000 --- a/ru/mst.md +++ /dev/null @@ -1,209 +0,0 @@ - -# Минимальные остовы - -> Авиакомпания содержит $m$ рейсов между $n$ городами, $i$-ый из них обходится в $w_i$ рублей, причём из любого города можно добраться до любого другого. В стране наступил кризис, и нужно отказаться от как можно большего числа из них таким образом, что содержание оставшиъся рейсов будет наиболее дешевым. - -Иными словами, нужно найти дерево минимального веса, которое является подграфом данного неориентированного графа. Такие деревья называют *остовами* (*каркас*, *скелет*; ударение на первый слог, но так мало кто произносит). По-английски — *minimum spanning tree* (дословно, минимальное покрывающее дерево). - -Почему дерево? Потому в противном случае там был бы цикл из которго можно удалить какое-то ребро и получить ответ пооптимальней. А если это больше, чем одно дерево, то какие-то две вершины остаются несвязаны. - -Вообще, следующие утверждения про деревья являются эквивалентными: -* Граф — дерево. -* В графе из $n$ вершин $n-1$ рёбер и нет циклов. -* Из любой вершины можно дойти в любоую другую единственным образом. - -## Лемма о безопасном ребре - -Назовем подграф $T$ графа $G$ безопасным, если они является подграфом какого-то минимального остова. - -Назовем ребро безопасным, если при добавлении его в подграф $T$ получившийся граф $T'$ тоже является безопасным, то есть подграфом какого-то минимального остова. - -Все алгоритмы для поиска минимального остова опираются на следующее утверждение: - -> Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным. - -**Доказательство**: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже. - -![safe](https://neerc.ifmo.ru/wiki/images/thumb/8/81/%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png/300px-%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png) - -Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов. - -# Алгоритм Прима - -Минимальный остов строится постепенно, добавлением в него рёбер по одному. - -* Изначально остов — одна произвольная вершина. -* Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден. - -Очень похоже на алгоритм Дейкстры, только мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё. - -Совсем наивная реализация за $O(nm)$ — каждый раз перебираем все рёбра: - - -```python -const int maxn = 1e5, inf = 1e9; -vector from, to, weight; -bool used[maxn] - -// считать все рёбра в массивы - -used[0] = 1; -for (int i = 0; i < n-1; i++) { - int opt_w = inf, opt_from, opt_to; - for (int j = 0; j < m; j++) - if (opt_w > weight[j] && used[from[j]] && !used[to[j]]) - opt_w = weight[j], opt_from = from[j], opt_to = to[j] - used[opt_to] = 1; - cout << opt_from << " " << opt_to << endl; -} -``` - -Реализация за $O(n^2)$: - - -```python -const int maxn = 1e5, inf = 1e9; -bool used[maxn]; -vector< pair > g[maxn]; -int min_edge[maxn] = {inf}, best_edge[maxn]; -min_edge[0] = 0; - -// ... - -for (int i = 0; i < n; i++) { - int v = -1; - for (int u = 0; u < n; j++) - if (!used[u] && (v == -1 || min_edge[u] < min_edge[v])) - v = u; - - used[v] = 1; - if (v != 0) - cout << v << " " << best_edge[v] << endl; - - for (auto e : g[v]) { - int u = e.first, w = e.second; - if (w < min_edge[u]) { - min_edge[u] = w; - best_edge[u] = v; - } - } -} -``` - -Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за $O(m \log n)$: - - -```python -set< pair > q; -int d[maxn]; - -while (q.size()) { - v = q.begin()->second; - q.erase(q.begin()); - - for (auto e : g[v]) { - int u = e.first, w = e.second; - if (w < d[u]) { - q.erase({d[u], u}); - d[u] = w; - q.insert({d[u], u}); - } - } -} -``` - -Про алгоритм за $O(n^2)$ забывать не стоит — он работает лучше в случае плотных графов. - -# Алгоритм Крускала - -Будем добавлять рёбра в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить. - -Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Однако для этой проверки нам нужна будет целая отдельная структура. - -# Система непересекающихся множеств - -Эта структура данных предоставляет следующие возможности. Изначально имеется несколько элементов, каждый из которых находится в отдельном (своём собственном) множестве. Структура поддерживает две операции: -* объединить два каких-либо множества -* запросить, в каком множестве сейчас находится указанный элемент - -Обе операции будут выполняться в среднем почти за $O(1)$ (но не совсем — этот сложный вопрос будет разъяснен позже). - -Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив `_p`, в котором для каждого элемента мы храним номер его предка в дерева. Для корней деревьев будем считать, что их предок — они сами. - -Наивная реализация, которую мы потом ускорим: - - -```python -int _p[maxn]; - -int p (int v) { - if (_p[v] == v) - return v; - else - return p(_p[v]); -} - -void unite (int a, int b) { - a = p(a), b = p(b); - _p[a] = b; -} - -for (int i = 0; i < n; i++) - _p[i] = i; -``` - -**Эвристика сжатия пути**. Оптимизируем работу функции `p`. Давайте перед тем, как вернуть ответ, запишем его в `_p` от текущей вершины, то есть переподвесим его за самую высокую. - -Насколько лучше это сделает асимптотику? Выясняется, что $O(n \log n)$. - -Тут должен быть мем из опросов. - -**Ранговая эвристика**. Эта штука напрямую пытается минимизировать высоту дерева. Давайте делать переподвешивание за то, которое менее глубоко. Ну понятно, что тогда любое дерево будет не более логарифма. - -**Весовая эвристика**. Давайте каждый раз подвешивать за более крупное. Работать будет быстро, так как . - -Автор предпочитает именно её, потому что часто эти размеры компонент требуются сами по себе. - -Оказывается, что сжатия + ранговая или сжатия + весовая работает быстро. - -Асимптотика объединения обеих эвристик (сжатия путей и одной из ранговых) — O(a(n)), где a(n) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4). Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует. - - -```python -int _p[maxn], s[maxn]; - -int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); } - -void unite (int a, int b) { - a = p(a), b = p(b); - if (s[a] > s[b]) swap(a, b); - s[b] += s[a]; - _p[a] = b; -} - -for (int i = 0; i < n; i++) - _p[i] = i; -``` - -## Полезные свойства и классические задачи - -* Если веса всех рёбер различны, то остов будет уникален. -* Минимальный остов является также и остовом с минимальным произведением весов рёбер (замените веса всех рёбер на их логарифмы) -* Минимальный остов является также и остовом с минимальным весом самого тяжелого ребра. -* Если вы решаете задачу, где ребра не добавляются, а удаляются, то можно попробовать решать задачу «с конца» и применить алгоритм Крускала. - -## Персистентная СНМ* - -Как вы уже заметили, автор очень любит персистентные структуры данных. - -СНМ — тоже структура, и её тоже можно сделать персистентной. В СНМ мы изменяем массивы, а массивы можно сделать персистентными через персистентное ДО (только так, проще не получается — многие пытались). - -Здесь есть нюанс — амортизированные структуры не очень хорошо дружат с персистентностью. Поэтому нам придется отказаться от эвристики сжатия путей, и поэтому асимптотика составит $O(n \log^2 n)$ времени и памяти — один логарифм от СНМа, другой от персистентного ДО. - -## Динамическая связность* - -*Dynamic Connectivity Problem:* - -> Даны $n$ запросов добавления ребра (`+`), удаления ребра (`-` и какого-то запроса про граф (`?`), например, о связности двух вершин. - -О решении этой задачи в online и в offline можете почитать в [этом посте](https://codeforces.com/blog/entry/15296). diff --git a/web/ru/pollard.html b/ru/pollard.html similarity index 100% rename from web/ru/pollard.html rename to ru/pollard.html diff --git a/ru/pollard.md b/ru/pollard.md deleted file mode 100644 index dd2c5a5..0000000 --- a/ru/pollard.md +++ /dev/null @@ -1,142 +0,0 @@ - -# Факторизация за $O(\sqrt[4]{n})$ - -Наука умеет раскладывать целые числа на множители за $O(n^\frac{1}{4})$. Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера. - -## Парадокс дней рождений - -Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. -Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. - -$$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ - -Попытаемся оценить $f$: - -$$ -\begin{align} - \begin{aligned} - e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ - & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ - e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ - f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ - & = e^{-\frac{n(n-1)}{2d}} & \\ - & \simeq e^{-\frac{n^2}{2d}} & \\ - \end{aligned} -\end{align} -$$ - -Из формулы более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее: - -**Утверждение**. В мультимножество нужно добавить $O(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. - -Если вы не доверяете математике, посмотрите на графики: - - -```python -import matplotlib.pyplot as plt -%matplotlib inline - -import seaborn as sns -sns.set() -``` - - -```python -def f(n, d): - p = 1 - for x in range(1, n): - p *= 1-x/d - return p - -def g(n, d): - return 2.71**(-0.5 * n**2 / d) -``` - - -```python -exact = [f(n, 365) for n in range(40)] -approx = [g(n, 365) for n in range(40)] -plt.plot(exact) -plt.plot(approx) -plt.show() -``` - - -![png](pollard_files/pollard_7_0.png) - - -На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать $n \approx \sqrt{d}$, построим такую же для больших чисел и перейдём в логарифмический масштаб. - - -```python -approx = [g(n, 10**10) for n in range(10**6)] -plt.xscale('log') -plt.plot(approx) -plt.show() -``` - - -![png](pollard_files/pollard_9_0.png) - - -## $\rho$-алгоритм Полларда - -Итак, мы хотим факторизовать число $n$. Предположим, что $n = p q$ и $p \approx q$. Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей. - -Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например $f(x) = (x+1)^2 \mod n$. - -Как выглядит граф, в котором из вершины есть единственное ребро $x \to f(x)$? Такой граф называется *функциональным*. Если в нём нарисовать «траекторию» произвольного элемента, — какой-то путь, превращающийся в цикл, — то получится что-то похожее на букву $\rho$ (ро). Алгоритм из-за этого так и назван. - -Рассмотрим траекторию какого-нибудь элемента $x_0$: {$x_0$, $f(x_0)$, $f(f(x_0))$, $\ldots$}. Теперь сделаем из неё новую последовательность, взяв каждый элемент по модулю $p$ — наименьшего из простых делителей $n$. - -**Утверждение**. Ожидаемая длина цикла в этой последовательности $O(\sqrt[4]{n})$. - -*Доказательство:* так как $p$ — меньший делитель, то $p \leq \sqrt{n}$. Теперь просто подставим в предыдущее утверждение: в множество нужно добавить $O(\sqrt{p}) = O(\sqrt[4]{n})$ элементов, чтобы какие-то два совпали, а значит последовательность зациклилась. - -Если мы найдём цикл в такой последовательности (то есть такие $i$ и $j$, что $f^i(x_0) \equiv f^j(x_0)$), то мы можем восстановить какой-то делитель $n$, а именно $\gcd(|f^i(x_0) - f^j(x_0)|, n)$ — это число меньше $n$ и делится на $p$. - -Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя $i$ и $j$ ($i = 2j$) и проверять, что $f^i(x_0) \equiv f^j(x_0) \pmod p$, что эквивалентно проверке $\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1$. - - -```python -from math import gcd -from random import randint -``` - - -```python -def find_divisor(n, seed = 1, f = lambda x: (x+1)**2): - x = y = seed - d = 1 - while d == 1: - x = f(f(x)) % n - y = f(y) % n - d = gcd(abs(x-y), n) - return d -``` - - -```python -n = 3*5*7 # = 105 -print([find_divisor(n, seed=i) for i in range(n)]) -``` - - [3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3] - - - -```python -n = (10**9+7)*(10**9+9) -print([find_divisor(n, seed=randint(1, n)) for _ in range(20)]) -``` - - [1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009] - - -Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за $O(\sqrt{n})$ (нужно добавить отсечение по времени). - -## Разное - -Формально, мы показали, что алгоритм работает за $O(\sqrt[4]{n} \log n)$ за счёт поиска $\gcd$, но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет. - -Пародокс дней рождения также можно использовать при подсчёте вероятности коллизии при хэшировании. Хороший модуль должен быть не менее квадрата количества хэшей, а чего-то вроде $5 n^2$ уже почти всегда достаточно. diff --git a/web/ru/probability.html b/ru/probability.html similarity index 100% rename from web/ru/probability.html rename to ru/probability.html diff --git a/ru/probability.md b/ru/probability.md deleted file mode 100644 index dc5f126..0000000 --- a/ru/probability.md +++ /dev/null @@ -1,86 +0,0 @@ - -# Ликбез по теорверу -* Вероятностные распределения -* Матожидание и дисперсия -* Парадокс дней рождения -* Нормальное распределение -* Правдоподобие и оценка качества предсказания -* Энтропия и оптимальное кодирование - -Эта статья представляет собой выжимку самых интересных фактов и «больших идей» теорвера, которые обычно рассказывают на курсах статистики, машинного обучения и теории информации. - -(Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.) - - -```python -import numpy as np - -import matplotlib as plt -%matplotlib inline - -import seaborn as sns -sns.set() -``` - -## Матожидание - - - -## Дисперсия - -Какие два числа лучше всего описывают распределение? - -## Нормальное распределение - -Центральная предельная теорема названа так пафосно вполне обоснованно. - -Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию. - -$$ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} $$ - -Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса. - -Трудно даже доказать, что это распределение, т. е. что $\int_{-\inf}^\inf f(x) dx = 1$. - -## Применения - -Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный. - -Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как - -## Линейные рекурренты - -Чтобы решать следующие задачи, нам нужно будет использовать следующий факт: - -... - -Доказательство мы не приведем. - -В частности, таким образом получается формула для чисел Фибоначчи. - -$$ f_n = \ldots $$ - -Кто бы мог подумать, что все эти иррациональности и степени сократятся и вообще дадут целое число?.. - -## Классика - -Парадокс дней рождения. - -Это на самом деле очень часто используемый результат. Так можно считать вероятность коллизии хэшей, а также он используется во многих теоретико-числовых алгоритмах, в которых используется предположения (весьма справедливые) о распределении простых чисел. - -Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт? - -TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. -Казино. Мы приходим в казино с 1000\$ и следующим образом проводим там время: ставим по 1\$, пока не обанкротимся или не выиграем 1100\$. Какая вероятность того, что мы уйдём с деньгами? - -## Принцип максимального правдоподобия - -## Энтропия - -Энтропией называется минимальное число бит, которым теоретически возможно сжать сообщение. Эта величина важна, потому что на практике если её можно посчитать, то сжатие с соответствующей кратностью реально достижимо. - -Шумный канал. - -Пусть у вас есть 1тб данных и два китайских терабайтника, на каждый из которых можно записать столько данных, но каждый бит имеет вероятность 10% записаться на противоположный. Требуется сохранить данные с первого раза без потерь. Совсем без потерь. - -Причём это делается почти впритык. diff --git a/web/ru/reciprocal.html b/ru/reciprocal.html similarity index 100% rename from web/ru/reciprocal.html rename to ru/reciprocal.html diff --git a/ru/reciprocal.md b/ru/reciprocal.md deleted file mode 100644 index 235d841..0000000 --- a/ru/reciprocal.md +++ /dev/null @@ -1,128 +0,0 @@ - -# Обратный элемент по модулю - -Часто в задачах требуется посчитать что-то по простому модулю (чаще всего $10^9 + 7$). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче. - -Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например: - - -```c++ -c = (a + b) % mod; -c = (mod + a - b) % mod; -c = a * b % mod; -``` - -Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: $\frac{8}{2} = 4$, но $\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4$. - -Нужно найти некоторый элемент, который будет себя вести как $\frac{1}{a} = a^{-1}$, и вместо «деления» домножать на него. Назовем такой элемент обратным. - -## Способ 1: бинарное возведение в степень - -Если модуль $p$ простой, то решением будет $a^{-1} \equiv a^{p-2}$. Это следует из малой теоремы Ферма: - -**Теорема.** $a^p \equiv a \pmod p$ для всех $a$, не делящихся на $p$. - -**Доказательство**. (для понимания несущественно, можно пропустить) - -$$ -\begin{align} -a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p -\\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} -\\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} -\\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} -\\ &= a -\end{align} -$$ - -Здесь $P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}$ это мультиномиальный коеффициент — количество раз, которое элемент $a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}$ появится при раскрытии скобки $(a_1 + a_2 + \ldots + a_n)^k$. - -Теперь два раза «поделим» наш результат на $a$. - -$$ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} $$ - -Получается, что $a^{p-2}$ ведет себя как $a^{-1}$, что нам по сути и нужно. -Посчитать $a^{p-2}$ можно за $O(\log p)$ бинарным возведением в степень. - -Приведем код, который позволяет считает $C_n^k$. - - -```c++ -int t[maxn]; // факториалы, можно предподситать простым циклом - -// бинарное возведение в степень -int bp (int a, int n) { - int res = 1; - while (n) { - if (n & 1) res = res * a % mod; - a = a * a % mod; - n >>= 1; - } - return res; -} - -// находит обратный элемент как a^(p-2) -int inv (int x) { - return bp(x, mod-2); -} - -int c (int n, int k) { - return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod; -} -``` - -## Способ 2: диофантово уравнение - -Диофантовыми уравнениями называют такие штуки: - -$$ ax + by = 1 $$ - -Требуется решить их в целых числах, то есть $a$ и $b$ известны, и нужно найти такие целые (возможно, отрицательные) $x$ и $y$, чтобы равенство выполнялось. Решают такие вещи расширенным алгоритмом Евклида. TODO: описать, как он работает. - -Подставим в качестве $a$ и $b$ соответственно $a$ и $m$ - -$$ ax + my = 1 $$ - -Одним из решений уравнения и будет $a^{-1}$, потому что если взять уравнение по модулю $m$, то получим - -$$ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m $$ - - - -Преимущества этого метода над возведением в степень: -* Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. -* Алгоритм проще выполнять руками. - -Сам автор почти всегда использует возведение в степень. - -## Почему $10^9+7$? - -1. Это выражение довольно легко вбивать (`1e9+7`). -2. Простое число. -3. Достаточно большое. -4. `int` не переполняется при сложении. -5. `long long` не переполняется при умножении. - -Кстати, $10^9 + 9$ обладает теми же свойствами. Иногда используют и его. - -## Предподсчёт обратных факториалов за линейное время - -Пусть нам нужно зачем-то посчитать все те же $C_n^k$, но для больших $n$ и $k$, поэтому асимптотика $O(n \log m)$ нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам. - -Если у нас уже написан `inv`, то нам не жалко потратить $O(\log m)$ операций, посчитав $m!^{-1}$. - -После этого мы будем считать $(m-1)!^{-1}$ как $m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}$. - - -```c++ -int f[maxn]; -f[0] = 1; -for (int i = 1; i < maxn; i++) - f[i] = i*f[i-1] % mod; - -int r[maxn]; -r[maxn-1] = inv(f[maxn-1]) -for (int i = maxn-1; i >= 1; i--) - r[i-1] = r[i]*i % mod; -``` - -TODO: техника с сайта емакса. diff --git a/web/ru/segtree.html b/ru/segtree.html similarity index 100% rename from web/ru/segtree.html rename to ru/segtree.html diff --git a/ru/segtree.md b/ru/segtree.md deleted file mode 100644 index f2e3f67..0000000 --- a/ru/segtree.md +++ /dev/null @@ -1,369 +0,0 @@ - -# Дерево отрезков - -**Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля. - -**Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках. - -Рассмотрим конкретную задачу: - ->Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов: - ->1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`). ->2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$. - ->Оба запроса нужно обрабатывать за время $O(\log n)$. - -Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции: - -Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1. - -Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков». - -![alt text](http://i.imgur.com/GGBmcEP.png) - -Строить его можно рекурсивной функцией: -* Если вершина является листом, взять в качестве суммы значение соответствующей ячейки. -* Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей. - -## Разные свойства - -Высота такого дерева есть величина $\Theta(\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики. - -Более того, любой полуинтервал разбивается на $O(\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков. - -Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} + \ldots + 1 < 2n$. Значит, оно линейное по памяти. - -При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом. - -## Ок, как это нам поможет? - -Опишем теперь, как с помощью такой структуры решить задачу. - -**Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$. - -Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\Theta(\log n)$ — по одной с каждого уровня. - -Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков. - -**Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения. - -Сделаем тоже рекурсивную функцию, рассмотрев три случая: - -* Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму. -* Если отрезки вершины и запроса не пересекаются, то вернуть 0. -* Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей. - -Чтобы разобраться, почему это работает за $O(\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\log n)$ отрезках, а значит и итоговая асимптотика будет такая же. - -## Ликбез по C++ - -Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества. - -Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел. - -Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\log n)$. - -В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. - - - -У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса. - - -```c++ -struct A { - int param1, param2; // тут можно что-то хранить - char param3 = 'k'; - A (int var) { - // эта часть называется конструктором - // ... - } - void do_something () { - // это какой-то другой метод - // ... - } -}; // <- не забудьте точку с запятой -``` - -Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве. - -Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется: - - -```c++ -A x(179); -x.do_something(); -x.param1 = 57; - -A *y = new A(42); // new возвращает адрес, по которому можно найти объект -y->do_something(); -y.param3 = '!'; -``` - -Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались - -## Реализация - -Общий план реализации любых структур данных: -1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc. -2. Формально описать, что должны делать методы и за какую асимптотику. -3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты. - - -```c++ -struct segtree { - int lb, rb; // левые и правые границы отрезков - int sum = 0; // сумма на текущем отрезке - segtree *l = 0, *r = 0; - segtree (int _lb, int _rb) { - lb = _lb, rb = _rb; - if (lb + 1 < rb) { - // если не лист, создаем детей - int t = (lb + rb) / 2; - l = new segtree(lb, t); - r = new segtree(t, rb); - } - } - void add (int k, int x) { - sum += x; - if (l) { - if (k < l->rb) - l->add(k, x); - else - r->add(k, x); - } - } - int get_sum (int lq, int rq) { - if (lb >= lq && rb <= rq) - // если мы лежим полностью в отрезке запроса, вывести сумму - return sum; - if (max(lb, lq) >= min(rb, rq)) - // если мы не пересекаемся с отрезком запроса, вывести ноль - return 0; - // иначе всё сложно -- запускаемся от детей и пусть они там сами решают - return l->get_sum(lq, rq) + r->get_sum(lq, rq); - } -}; -``` - -> Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$). - -Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи: -* Запросим сумму от $k$ до $n$ в ДО. -* Добавим единичку в $k$-тую позицию в ДО. - -Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \log n)$ запросов. - -> Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике. - -Ответим на все запросы в оффлайн, используя метод сканирующей прямой: - -* Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \times [y_1, y_2]$. -* Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше. -* Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$». - - -## Отложенные операции - -Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному. - -Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем. - -Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено. - -Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся. - -Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \log n)$. - -При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны. - - - -```c++ -struct segtree { - int lb, rb; - int sum = 0, assign = -1; - segtree *l = 0, *r = 0; - segtree (int _lb, int _rb) { - lb = _lb, rb = _rb; - if (lb + 1 < rb) { - int t = (lb + rb) / 2; - l = new segtree(lb, t); - r = new segtree(t, rb); - } - } - void push () { - if (assign != -1) { - sum = (rb-lb) * assign; - if (l) { // если дети есть - l->assign = assign; - r->assign = assign; - } - } - assign = -1; - } - void upd (int lq, int rq, int x) { - push(); - if (lq <= lb && rb <= rq) - assign = x; - else if (l && max(lb, lq) < min(rb, rq)) { - // если есть дети и отрезок запроса хоть как-то пересекается с нашим - l->upd(lq, rq, x); - r->upd(lq, rq, x); - // ...дальше они сами разберутся - } - } - int get_sum (int lq, int rq) { - push(); - if (lb >= lq && rb <= rq) - return sum; - if (max(lb, lq) >= min(rb, rq)) - return 0; - return l->get_sum(lq, rq) + r->get_sum(lq, rq); - } -}; -``` - -По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах. - -Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни. - -## Динамическое построение - -А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\log_2 10^6 \approx 20$, $\log_2 10^9 \approx 30$), кроме этапа построения. - -Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation. - -Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так. - - -```c++ -struct segtree { - int lb, rb; - int sum = 0; - segtree *l = 0, *r = 0; - segtree (int _lb, int _rb) { - lb = _lb, rb = _rb; - // а тут ничего нет - } - void extend () { - if (!l && lb + 1 < rb) { - int t = (lb + rb) / 2; - l = new segtree(lb, t); - r = new segtree(t, rb); - } - } - void add (int k, int x) { - extend(); - sum += x; - if (l) { - if (k < l->rb) - l->add(k, x); - else - r->add(k, x); - } - } - int get_sum (int lq, int rq) { - if (lb >= lq && rb <= rq) - return sum; - if (max(lb, lq) >= min(rb, rq)) - return 0; - extend(); - return l->get_sum(lq, rq) + r->get_sum(lq, rq); - } -}; -``` - -Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так: - - -```c++ -vector compress (vector a) { - vector b = a; - sort(b.begin(), b.end()); - b.erase(unique(b.begin(), b.end()), b.end()); - for (int &x : a) - x = int(lower_bound(b.begin(), b.end(), x) - b.begin()); - return a; -} -``` - -## Персистентность - -Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния. - -Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается. - -У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \log n)$. - - -```c++ -struct segtree { - int lb, rb; - int sum = 0; - segtree *l = 0, *r = 0; - segtree (int _lb, int _rb) { - lb = _lb, rb = _rb; - if (lb != rb) { - int t = (lb + rb) / 2; - l = new segtree(lb, t); - r = new segtree(t, rb); - } - } - void copy () { - if (l) { - l = new segtree(l); - r = new segtree(r); - } - } - void add (int k, int x) { - copy(); - sum += x; - if (l) { - if (k < l->rb) l->add(k, x); - else r->add(k, x); - } - } - int get_sum (int lq, int rq) { - // этот метод ничего не меняет -- он и так хороший - if (lq <= lb && rb <= rq) - return sum; - if (max(lb, lq) >= min(rb, rq)) - return 0; - return l->get_sum(lq, rq) + r->get_sum(lq, rq); - } -}; -``` - -> Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике. - -Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \log n)$ памяти. - -> Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке. - -Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу. - -Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`. - -Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом. - -> Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. - -У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\frac{1}{2^{100}}$, и ей на практике можно пренебречь. - -Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`. - -## Другие реализации - -Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы: - -**На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree). - -**«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051). - -## Задачи - -* [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции. -* Второй контест — на отложенные операции. -* [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность. diff --git a/web/ru/sparse-table.html b/ru/sparse-table.html similarity index 100% rename from web/ru/sparse-table.html rename to ru/sparse-table.html diff --git a/ru/sparse-table.md b/ru/sparse-table.md deleted file mode 100644 index 49a6b71..0000000 --- a/ru/sparse-table.md +++ /dev/null @@ -1,53 +0,0 @@ - -# Разреженная таблица - -- Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \log n)$ с малой константой. -- Обновления не поддерживает (static RMQ). -- Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья. -- Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос). -- Требует $O(n \log n)$ памяти. -- Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ. - -Определим разреженную таблицу как двумерный массив размера $n \times\log n$: - -$$ -t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} -$$ - -Идея такая: считаем минимум на каждом отрезке длины $2^k$. - -Такой массив можно посчитать за его размер: $t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования. - -Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам. - -![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png) - -Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов. - - -```c++ -int a[maxn], lg[maxn], mx[maxn][logn]; - -int rmq (int l, int r) { - int t = lg[r-l+1]; - return min(mx[l][t], mx[r-(1<= 0; i--) { - mx[i][0] = a[i]; - for (int l = 0; l < logn-1; l++) - mx[i][l+1] = max(mx[i][l], mx[i+(1< test.txt' % gen) - v1 = os.popen('./%s < test.txt' % f1).read() - v2 = os.popen('./%s < test.txt' % f2).read() - if v1 != v2: - print test - print("Correct:") - print v1 - print("Wrong:") - print v2 - break -``` - -Автор обычно запускает его командой `python3 checker.py stupid smart gen.py 100`, предварительно скомпилировав `stupid` и `smart` в ту же директорию, что и сам `checker.py`. - -Скрипт написан под Linux. Для Windows нужно убрать «`./`» во всех системных вызовах. - -`gen.py` автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через `./gen`. Пример `gen`-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10: - - -```python -from random import randint, choice - -n = randint(1, 10) - -print(n) - -for _ in range(n): - print(choice('abc'), end='') -``` diff --git a/web/ru/strings.html b/ru/strings.html similarity index 100% rename from web/ru/strings.html rename to ru/strings.html diff --git a/ru/strings.md b/ru/strings.md deleted file mode 100644 index e04c28b..0000000 --- a/ru/strings.md +++ /dev/null @@ -1,206 +0,0 @@ - -# Базовые строковые алгоритмы - -## Префикс-функция - -Рассмотрим задачу, которая возникает каждый раз, когда вы делаете `ctrl+f`: - -> Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него. - -Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго. - -Для решения этой задачи за линейное время придумали **префикс-функцию**. - -**Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \ldots s_i$, который также является и суффиксом этой строки (не считая всю строку). - -Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$. - - -```python -def slow_prefix_function(s): - n = len(s) - p = [0]*n - for i in range(n): - prefix = s[:i] - for l in range(1, i): - if prefix[:l] == prefix[-l:]: - p[i] = l - return p - -slow_prefix_function('aataataa') -``` - - - - - [0, 0, 1, 0, 1, 2, 3, 4] - - - -(Этот алгоритм работает за $O(n^3)$, но это только пока.) - -## Как это поможет решить исходную задачу? - -Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке. - -Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\#t$. - - -```python -s = "let it go" -t = """let it go, let it go -can't hold it back anymore -let it go, let it go -turn away and slam the door!""" - -print((s + '#' + t).replace('\n', ' ')) -print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)])) -``` - - let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door! - 00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000 - - -Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$. - -Такой алгоритм (посчитать префикс-функцию от $s\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**. - -## Как её быстро считать - -Рассмотрим ещё несколько примеров префикс-функций: - - -```python -for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']: - print(slow_prefix_function(s)) -``` - - [0, 0, 1, 2, 3] - [0, 0, 0, 0, 0, 0] - [0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0] - [0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - - -Можно заметить несколько особенностей: -* $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; -* $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \ldots s_i$, длина которого будет ровно на единицу меньше. - -Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения. - -Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\underbrace{aabaa}t\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$. - -Но что происходит, когда $s_{p_i}\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$. -* $\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. -* $\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa». -* $\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$. -* $\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$. - -В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым. - - -```python -def fast_prefix_function(s): - n = len(s) - p = [0]*n - for i in range(1, n): - cur = p[i - 1] - # перебираем префикс-функцию, пока не найдем равный символ - while s[i] != s[cur] and cur > 0: - cur = p[cur - 1] - # если нашли, то значение на единицу больше - if s[i] == s[cur]: - p[i] = cur + 1 - return p - -fast_prefix_function('abacabadabacabax') -``` - - - - - [0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0] - - - -### Асимптотика - -Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой. - -**Доказательство**: -* Как мы ранее заметили, префикс функция возрастает максимум на единицу. -* $\implies$ Вырасти она может максимум $n-1$ раз. -* Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один. -* $\implies$ Количество понижений не больше количества повышений, то есть $O(n)$. -* $\implies$ Суммарно итераций цикла `while` — $O(n)$. - -## Z-функция - -Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания. - -Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$. - -$$\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)$$ - - -```python -def slow_z_function(s): - n = len(s) - z = [0]*n - for i in range(1, n): - suffix = s[i:] - for l in range(1, i): - if s[:l] == suffix[:l]: - z[i] = l - return z - -for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']: - print(slow_z_function(s)) -``` - - [0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1] - [0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1] - [0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0] - - -Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$. - -## Как её быстро считать - -Заметим, что: -* $z_0 = 0$ — из-за договоренности (потому что информации не несет); -* $z_i = 0 \iff s_i \neq s_0$; -* $z_i > 0 \iff s_i = s_0$. - -Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$. - -Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. -* Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком. -* Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку. - - -```python -def fast_z_function(s): - n = len(s) - z = [0]*n - l = 0 - r = 0 - for i in range(1, n): - if i <= r: - z[i] = min(r - i + 1, z[i-l]) - while i + z[i] < n and s[z[i]] == s[i+z[i]]: - z[i] += 1 - if i + z[i] - 1 > r: - l = i - r = i + z[i] - 1 - return z -``` - -**Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$. - -## Зачем тогда люди используют префикс-функцию - -![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg) - -В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов. - -Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение. diff --git a/web/ru/suffix-array.html b/ru/suffix-array.html similarity index 100% rename from web/ru/suffix-array.html rename to ru/suffix-array.html diff --git a/ru/suffix-array.md b/ru/suffix-array.md deleted file mode 100644 index 7442186..0000000 --- a/ru/suffix-array.md +++ /dev/null @@ -1,109 +0,0 @@ - -# Суффиксный массив - -Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике. - -Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём. - - - -*
«Паблик с тупыми шутками про проганье»
* - -## Мотивация - -**Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки. - - - -**Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно. - -В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс. - -Работать такой алгоритм будет за $O(|t| \log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \log |s|)$, что является одним из самых оптимальных алгоритмов поиска. - -Теперь научимся его строить. - -## Построение за $O(n \log n)$ - -Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара. - -Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \log^2 n)$. Но это не самый быстрый и удобный алгоритм. - -Наш алгоритм будет состоять из $\lceil \log n \rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив. - -Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов). - -Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4. - -$$ - p_0 = (0, 1, 3, 2) \;\;\; c_0 = (0, 0, 1, 0) -\\ p_1 = (0, 3, 1, 2) \;\;\; c_1 = (0, 1, 2, 0) -\\ p_2 = (3, 0, 1, 2) \;\;\; c_2 = (1, 2, 3, 0) -$$ - -Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом. - -Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором. - -Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \log^2 n)$, потому что каждый этап будет работать за $O(n \log n$). - -**Оптимизация до $O(n \log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$. - - -```c++ -// строка -- это последовательность чисел от 1 до размера алфавита -vector suffix_array (vector &s) { - s.push_back(0); // добавляем нулевой символ в конец строки - int n = (int) s.size(), - cnt = 0, // вспомогательная переменная: счётчик для сортировки - cls = 0; // количество классов эквивалентности - vector c(n), p(n); - - map< int, vector > t; - for (int i = 0; i < n; i++) - t[s[i]].push_back(i); - - // «нулевой» этап - for (auto &x : t) { - for (int u : x.second) - c[u] = cls, p[cnt++] = u; - cls++; - } - - // пока все суффиксы не стали уникальными - for (int l = 1; cls < n; l++) { - vector< vector > a(cls); // массив для сортировки подсчётом - vector _c(n); // новые классы эквивалентности - int d = (1<(p.begin()+1, p.end()); -} -``` - -TODO: переписать это - -## Наибольшие общие префиксы - -Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами. - -TODO diff --git a/web/ru/treap.html b/ru/treap.html similarity index 100% rename from web/ru/treap.html rename to ru/treap.html diff --git a/ru/treap.md b/ru/treap.md deleted file mode 100644 index 78e8aac..0000000 --- a/ru/treap.md +++ /dev/null @@ -1,460 +0,0 @@ - -# Декартово дерево - -Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века. - -Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим. - -Декартово дерево же определяется и строится так: - -* Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*. -* Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*. -* От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня. -* Аналогично, запустимся от правой части и добавим корню правого сына. - -Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно. - -Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх: - -![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png) - -Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий: - -* Дерамида (дерево + пирамида) -* ПиВо (пирамида + дерево) -* КуРево (куча + дерево) -* Treap (tree + heap) - - ## Бинарные деревья - - С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например: - -* добавить число $x$ в множество -* определить, есть ли в множестве число $x$ -* найти первое число, не меньшее $x$ (`lower_bound`) -* найти количество чисел в промежутке $[l, r]$ - -При этом все операции — за $O(\log n)$. - -На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\log n)$. - -## Приоритеты и асимптотика - -В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска. - -**Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \log n)$. - -Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить. - -**Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*. - -Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна - -$$d_i = \sum_{j=1}^n a(j, i)$$ - -Её матожидание равно - -$$E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)$$ - -где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно. - -Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение. - -**Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$). - -**Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. - -**Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет. - -У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей: - -$$p(x, y) = \frac{1}{y-x+1}$$ - -Теперь, чтобы найти матожидание, эти вероятности надо просуммировать: - -$$E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)$$ - -Перед последним переходом мы получили сумму гармонического ряда. - -Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя. - -**Упражнение***. Выведите из этого доказательства асимптотику `quicksort`. - -## Реализация - -Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте. - -Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву. - - -```c++ -struct Node { - int key, prior; - Node *l = 0, *r = 0; - Node (int _key) { key = _key, prior = rand(); } -}; -``` - -Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся. - -### Merge - -Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча. - -Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше. - -Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына. - - -```c++ -Node* merge (Node *l, Node *r) { - if (!l) return r; - if (!r) return l; - if (l->prior > r->prior) { - l->r = merge(l->r, r); - return l; - } - else { - r->l = merge(l, r->l); - return r; - } -} -``` - -### Split - -Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$. - -В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо: - - -```c++ -typedef pair Pair; - -Pair split (Node *p, int x) { - if (!p) return {0, 0}; - if (p->key <= x) { - Pair q = split(p->r, x); - p->r = q.first; - return {p, q.second}; - } - else { - Pair q = split(p->l, x); - p->l = q.second; - return {q.first, p}; - } -} -``` - -### Пример: вставка - -`merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. - -Вот так, например, будет выглядеть код, добавляющий $x$ в сет. - - -```c++ -Node *root = 0; - -void insert (int x) { - Pair q = split(root, x); - Node *t = new Node(x); - root = merge(q.first, merge(t, q.second)); -} -``` - -### Пример: модификация для суммы на отрезке - -Иногда нам нужно написать какие-то модификации для более продвинутых операций. - -Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке». - - -```c++ -struct Node { - int val, sum; - // ... -}; -``` - -При `merge` и `split` надо будет поддерживать эту сумму актуальной. - -Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины. - - -```c++ -void sum (Node* v) { return v ? v->sum : 0; } -// обращаться по пустому указателю нельзя -- выдаст ошибку - -void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; } -``` - -В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается: - - -```c++ -Node* merge (Node *l, Node *r) { - // ... - if (...) { - l->r = merge(l->r, r); - upd(l); - return l; - } - else { - // ... - } -} -``` - - -```c++ -typedef pair Pair; - -Pair split (Node *p, int x) { - // ... - if (...) { - // ... - upd(p); - return {p, q.second}; - } - else { - // ... - } -} -``` - - Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму: - - -```c++ -int sum (int l, int r) { - Pair rq = split(root, r); - Pair lq = split(rq.first, l); - int res = sum(lr.second); - root = merge(lq.first, merge(lq.second, rq.second)); - return res; -} -``` - -# Неявный ключ - -Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку. - -Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными. - -Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева: - - - -```c++ -struct Node { - int key, prior, size = 1; - // ^ размер поддерева - Node *l = 0, *r = 0; - Node (int _key) { key = _key, prior = rand(); } -}; -``` - -Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины. - - -```c++ -int size (Node *v) { return v ? v->size : 0; } - -void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); } -``` - -`merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа. - -Про `split` теперь удобнее думать как "вырежи первые `k` элементов". - - -```c++ -typedef pair Pair; - -Pair split (Node *p, int k) { - if (!p) return {0, 0}; - if (size(p->l) + 1 <= k) { - Pair q = split(p->r, k - size(p->l) - 1); - // ^ правый сын не знает количество вершин слева от него - p->r = q.first; - upd(p); - return {p, q.second}; - } - else { - Pair q = split(p->l, k); - p->l = q.second; - upd(p); - return {q.first, p}; - } -} -``` - -Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно. - -### Пример: ctrl+x, ctrl+v - - -```c++ -Node* ctrlx (int l, int r) { - Pair q1 = split(root, r); - Pair q2 = split(q1.first, l); - root = merge(q2.first, q1.second); - return q2.second; -} -``` - - -```c++ -void ctrlv (Node *v, int k) { - Pair q = split(root, k); - root = merge(q.first, merge(v, q.second)); -} -``` - -### Пример: переворот - - - -Нужно за $O(\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д. - -Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут: - - -```c++ -struct Node { - bool rev; - // ... -}; -``` - -Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку: - - -```c++ -void push (node *v) { - if (v->rev) { - swap(v->l, v->r); - if (v->l) - v->rev ^= 1; - if (v->r) - v->rev ^= 1; - } - v->rev = 0; -} -``` - -Аналогично, эту функцию будем вызывать в начале `merge` и `split`. - -Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг. - - -```c++ -void reverse (int l, int r) { - Pair q1 = split(root, r); - Pair q2 = split(q1.first, l) - q2.second->rev ^= 1; - root = merge(q2.first, merge(q2.second, q1.second)); -} -``` - -# Функциональное программирование* - -Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно. - -Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке. - - -```c++ -auto apply (int l, int r, auto f) { - Pair q1 = split(root, r); - Pair q2 = split(q1.first, l) - q2.second = f(q2.second); - root = merge(q2.first, merge(q2.second, q1.second)); -} - -void reverse (Node *v) { - if (v) - v->rev ^= 1; -} -``` - -Применять её нужно так: - - -```c++ -apply(l, r, reverse); -``` - -Это работает в плюсах, начиная с `g++14`. - -Для простых операций можно даже написать лямбду: - - -```c++ -apply(l, r, [](Node *v){ - if (v) - v->rev ^= 1; -}); -``` - -# Персистентность* - -Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи. - -> Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции. - -Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим. - -> Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**. - -Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами. - -Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ. - -Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`: - - -```c++ -Node* copy (Node *v) { return new Node(*v); } -``` - -Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так: - - -```c++ -Pair split (Node *p, int x) { - p = copy(p); - // ... -} -``` - -В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию. - -У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\frac{L}{L+R}$, иначе за правое. - -**Теорема**. Такое переподвешивание эквивалентно приоритетам. - -**Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции: - -* Лист имеет вероятность 1 быть корнем себя (база индукции) -* Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}$. С вершинами правого дерева аналогично. - -Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД. - - -```c++ -Node* merge (Node *l, Node *r) { - if (!l) return r; - if (!r) return l; - l = copy(l), r = copy(r); - if (rand() % (size(l) + size(r)) < size(l)) { - // ... - } - else { - // ... - } -} -``` - -Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$? diff --git a/web/ru/trie.html b/ru/trie.html similarity index 100% rename from web/ru/trie.html rename to ru/trie.html diff --git a/ru/trie.md b/ru/trie.md deleted file mode 100644 index 314c5a1..0000000 --- a/ru/trie.md +++ /dev/null @@ -1,71 +0,0 @@ - -# Бор - -Бор — это структура данных для компактного хранения строк. - -Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. - -![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png) - -Бор можно удобно использовать для разных задач: -* Хранение строк — занимает гораздо меньше места, чем массив или сет строк. -* Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке. - -* Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи. - -## Реализация - -Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: -* терминальная ли вершина, -* ссылки на детей, -* возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет. - - -```c++ -const int k = 26; - -struct Vertex { - Vertex* to[k] = {0}; - bool terminal = 0; -}; - -Vertex *root = new Vertex(); -``` - -Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной. - - -```c++ -void add_string (string &s) { - v = root; - for (char c : s) { - c -= 'a'; - if (!v->to[c]) - v->to[c] = new Vertex(); - v = v->to[c]; - } - v->terminal = true; -} -``` - -Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть. - -Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности. - -### Как хранить ссылки - -Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми. - -В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например. - -Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями. - -## Суффиксные ссылки - -> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. - -Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. - -Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. - -Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). diff --git a/web/img/.editorial.html.swp b/web/img/.editorial.html.swp deleted file mode 100644 index eed31bfbef5ecb81e88ef7890edd52f724b5de2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI%ze@u#6bJBE2RBi4a1!Dvbn4~&F^GCjT?7RY>g1B9&tA&?sJW*F2ldbJPwAiM zUG;_zb}0yjZyDC|ZDxC?rLI45~fB*y_009U< z00Izz00bZ~FJJ~lUz&EO42z z7?p^OJN*3OUgN;?ljkxClr3`MT;d?QP`&$`xLwaW*E;7(XTtr`35T}G+JVwn^bO^g BJ0$=B diff --git a/web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb b/web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb deleted file mode 100644 index 3679909..0000000 --- a/web/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb +++ /dev/null @@ -1,146 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Алгоритм Ахо-Корасик\n", - "\n", - "Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время.\n", - "\n", - "Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик.\n", - "\n", - "\n", - "\n", - "Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы.\n", - "\n", - "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", - "\n", - "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", - "\n", - "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", - "\n", - "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Зачем это нужно\n", - "\n", - "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", - "\n", - "Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор.\n", - "\n", - "Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Алгоритм Ахо-Корасик*\n", - "\n", - "Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой.\n", - "\n", - "**Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход.\n", - "\n", - "**Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда.\n", - "\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "const int k = 26;\n", - "\n", - "struct Vertex {\n", - " Vertex *to[k] = {0}, *go[k] = {0};\n", - " Vertex *link = 0, *p;\n", - " int pch;\n", - " Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; }\n", - "};\n", - "\n", - "Vertex *root = new Vertex(-1, 0);" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "void add_string (string s) {\n", - " Vertex *v = root;\n", - " for (char _c : s) {\n", - " c -= 'a';\n", - " if (!v->to[c])\n", - " v->to[c] = new Vertex(c, v);\n", - " v = v->to[c];\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Vertex* go (Vertex *v, int c);\n", - "\n", - "Vertex* link (Vertex *v) {\n", - " if (!v->link) {\n", - " if (v == root || v->p == root) v->link = root;\n", - " else v->link = go(link(v->p), v->pch);\n", - " }\n", - " return v->link;\n", - "}\n", - "\n", - "Vertex* go (Vertex *v, int c) {\n", - " if (!v->go[c]) {\n", - " if (v->to[c]) v->go[c] = v->to[c];\n", - " else if (v == root) v->go[c] = root;\n", - " else v->go[c] = go(link(v), c);\n", - " }\n", - " return v->go[c];\n", - "}" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb b/web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb deleted file mode 100644 index 87cb744..0000000 --- a/web/img/.ipynb_checkpoints/bayans-checkpoint.ipynb +++ /dev/null @@ -1,533 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Красивые идейные задачи\n", - "\n", - "Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда.\n", - "\n", - "Задачи идут в порядке вспоминания, то есть в весьма рандомном." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Попугаи\n", - "\n", - "Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Минимум и максимум\n", - "\n", - "Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Перестановка\n", - "\n", - "Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Выпуклая оболочка\n", - "Требуется отвечать на 2 типа запросов:\n", - "\n", - "1. Добавить точку в выпуклую оболочку.\n", - "2. Проверить, лежит ли точка внутри выпуклой оболочки.\n", - "\n", - "Обе операции онлайн за $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Геометрическая прогрессия\n", - "\n", - "Найдите способ посчитать $\\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Покемоны\n", - "\n", - "В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \\times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \\to b$ и $b \\to c$ не следует, что $a \\to c$.\n", - "\n", - "У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Сортировка\n", - "Можно ли отсортировать\n", - "* 5 камней за 8 взвешиваний?\n", - "* 5 камней за 7 взвешиваний?\n", - "* 20 камней за 60 взвешиваний?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Точки в круге\n", - "\n", - "Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Замкнутые ломаные\n", - "\n", - "Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Неубывающий массив\n", - "\n", - "Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Чётный цикл\n", - "\n", - "Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## $k$-ая порядковая статистика\n", - "\n", - "Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Доминирующий элемент\n", - "\n", - "Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз. Время работы $O((n+m) \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Разрушение дерева\n", - "\n", - "Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## $k$-ый элемент на отрезке\n", - "\n", - "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Различные числа на отрезке\n", - "\n", - "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\\sqrt{n})$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Физкультура\n", - "\n", - "Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов:\n", - "\n", - "1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя)\n", - "2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно)\n", - "\n", - "При этом приказ может частично отменить действие предыдущих приказов.\n", - "\n", - "После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Нулевая сумма\n", - "\n", - "Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мета-задача\n", - "\n", - "В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ниточка\n", - "\n", - "В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Пельмени\n", - "\n", - "Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## От нуля до единицы\n", - "\n", - "Дан следующий код:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "x = 0\n", - "while x < 1:\n", - " x += random()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Требуется посчитать матожидание `x`.\n", - "\n", - "(`random` в питоне возвращает случайное действительное число от 0 до 1.)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Площадь\n", - "\n", - "Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Окружности\n", - "\n", - "Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна\n", - "\n", - "* соприкасаться с *внешней*,\n", - "* соприкасаться с предыдущей (($k-1$)-ой),\n", - "* иметь при этом максимальный радиус.\n", - "\n", - "Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Блеф\n", - "\n", - "Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может:\n", - "\n", - "* Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается.\n", - "* Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом.\n", - "\n", - "С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Достижимость\n", - "Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Нумизмат\n", - "\n", - "Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой.\n", - "\n", - "Придумайте любой полиномиальный алгоритм." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Принцесса\n", - "\n", - "В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве.\n", - "\n", - "Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством.\n", - "\n", - "У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми.\n", - "\n", - "Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу.\n", - "\n", - "Асимптотика $O(n^2)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Спираль\n", - "\n", - "Определим спираль $(2n+1) \\times (2n+1)$ как матрицу следующего вида:\n", - "\n", - "$$\n", - "\\begin{matrix}\n", - "21 & 22 & 23 & 24 & 25 \\\\\n", - "20 & 7 & 8 & 9 & 10 \\\\\n", - "19 & 6 & 1 & 2 & 11 \\\\\n", - "18 & 5 & 4 & 3 & 12 \\\\\n", - "17 & 16 & 15 & 14 & 13 \\\\\n", - "\\end{matrix}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$).\n", - "\n", - "$q \\leq 100$, $n \\leq 10^9$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Польский лабиринт\n", - "\n", - "Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`.\n", - "\n", - "![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png)\n", - "\n", - "Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам.\n", - "\n", - "$n \\leq 10^5$, изначальные координаты туристов до $10^9$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Нимные подмножества\n", - "\n", - "Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \\subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \\subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Баланс степеней\n", - "\n", - "Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Два пути\n", - "\n", - "Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Пьяница\n", - "\n", - "Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ксоровый рюкзак\n", - "\n", - "Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Иван Сусанин\n", - "\n", - "Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш.\n", - "\n", - "- Камиль руководит армией днём и водит армию по *дорогам*.\n", - "- Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. \n", - "\n", - "Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск.\n", - "\n", - "Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу.\n", - "Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось.\n", - "\n", - "Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Варенье\n", - "\n", - "В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм.\n", - "\n", - "Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья.\n", - "\n", - "Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной.\n", - "\n", - "$n, m \\leq 10^5$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Лабиринт\n", - "\n", - "Серёжа потерялся в лабиринте $n \\times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт.\n", - "\n", - "Придумайте любой полиномиальный алгоритм." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Обезьяна\n", - "\n", - "Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ожидание минимума\n", - "\n", - "Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин.\n", - "\n", - "Придумайте любой точный полиномиальный алгоритм." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Шумный ксор\n", - "\n", - "Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \\frac15$ быть единичным, то есть каждый бит $x \\oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x = # ...\n", - "\n", - "def mask(p=0.2):\n", - " r = 0\n", - " for i in range(32):\n", - " if random.random() < p:\n", - " r += 2**i\n", - " return r\n", - "\n", - "def query(y):\n", - " return bin(x ^ y ^ mask()).count('1')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ваша задача — отгадать число, используя не более 10000 попыток." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb b/web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb deleted file mode 100644 index 30b7828..0000000 --- a/web/img/.ipynb_checkpoints/bitset-checkpoint.ipynb +++ /dev/null @@ -1,241 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Битовое сжатие\n", - "\n", - "* Из-за него в «асимптотиках» появляется `/64`\n", - "* На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов\n", - "* `bitset` есть в stl; говорят, самописный быстрее" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости.\n", - "\n", - "Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. \n", - "\n", - "Работать с ним нужно вот так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "const int lim = 1000;\n", - "bitset b; // создать битсет размера lim (должно быть константой)\n", - "b.set(); // заполнить единицами\n", - "b.reset(); // заполнить нулями\n", - "b.flip(); // заменить единички на нули и наоборот\n", - "b.count(); // посчитать число единичек\n", - "cout << b; // вывести битовую строку" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Рюкзак\n", - "\n", - "Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака.\n", - "\n", - "Обычно его решают так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "bool dp[lim] = {}; // так можно его заполнить нулями\n", - "dp[0] = 1;\n", - "for (int i = 0; i < n; i++)\n", - " for (int x = lim - a[i]; x >= 0; x--)\n", - " dp[x + a[i]] |= dp[x];" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "…а с битсетом оно разгоняется так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "bitset b;\n", - "b[0] = 1;\n", - "for (int i = 0; i < n; i++)\n", - " b |= b << a[i];" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Цикл длины 3\n", - "\n", - "Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\\frac{n^3}{64})$ следующим образом:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "bitset g[maxn]; // матрица смежности\n", - "for (int a = 0; a < n; a++) {\n", - " for (int b = 0; b < n; b++) {\n", - " if (g[a][b] && (~g[a] & g[b]).any()) {\n", - " // цикл найден\n", - " }\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Перемножение матриц\n", - "\n", - "Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "typedef bitset t;\n", - "typedef array matrix;\n", - "\n", - "matrix operator* (matrix a, matrix b) {\n", - " matrix c;\n", - " for(int i = 0; i < n; i++)\n", - " for(int j = 0; j < n; j++)\n", - " if(a[i][j])\n", - " c[i] |= b[j];\n", - " return c;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Гаусс\n", - "\n", - "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", - "\n", - "Нас по сути просят решить следующую систему:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", - "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", - "\\ldots \\\\\n", - "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", - "\n", - "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "t gauss (matrix a) {\n", - " for (int i = 0; i < n; i++) {\n", - " int nonzero = i;\n", - " for (int j = i+1; j < n; j++)\n", - " if (a[j][i])\n", - " nonzero = j;\n", - " swap(a[nonzero], a[i]);\n", - " for (int j = 0; j < n; j++)\n", - " if (j != i && a[j][i])\n", - " a[j] ^= a[i];\n", - " }\n", - " t x;\n", - " for (int i = 0; i < n; i++)\n", - " x[i] = a[i][n] ^ a[i][i];\n", - " return x;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb b/web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb deleted file mode 100644 index 37746bb..0000000 --- a/web/img/.ipynb_checkpoints/centroid-checkpoint.ipynb +++ /dev/null @@ -1,249 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Центроидная декомпозиция\n", - "\n", - "Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».\n", - "\n", - "Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще).\n", - "\n", - "![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\\frac{n}{2}$.\n", - "\n", - "Выясняется, что центр всегда существует — это следует из алгоритма его поиска:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int s[maxn];\n", - "\n", - "int sizes (int v) {\n", - " s[v] = 1;\n", - " for (int u : g[v])\n", - " // для простоты считаем, что дерево корневое\n", - " s[v] += sizes(u);\n", - " return s[v];\n", - "}\n", - "\n", - "// второй параметр -- размер дерева\n", - "int centroid (int v, int n) {\n", - " for (int u : g[v])\n", - " if (s[u] > n/2)\n", - " return centroid(u, n);\n", - " return v;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение.** `centroid` действительно находит цетроид.\n", - "\n", - "**Доказательство:**\n", - "\n", - "* `centroid` вернет вершину, у которой размеры всех детей не больше $\\frac{n}{2}$ (это явно проверяется в `if`-е).\n", - "* Мы пришли в эту вершину, когда её размер был больше $\\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\\frac{n}{2}+1) = \\frac{n}{2}-1$ вершин.\n", - "* Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен.\n", - "\n", - "Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».\n", - "\n", - "**Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.\n", - "\n", - "Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Теперь поймём, зачем мы всё это делали.\n", - "\n", - "**Утверждение.** Каждая вершина входит в $O(\\log n)$ компонент.\n", - "\n", - "**Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\\lceil \\log_2 n \\rceil$ разделений.\n", - "\n", - "**Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \\log n)$.\n", - "\n", - "**Утверждение.** Для любого пути $a \\leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$.\n", - "\n", - "**Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$.\n", - "\n", - "Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Подсчет путей с заданным свойством\n", - "\n", - "Рассмотрим конкретный пример: подсчёт путей заданной длины.\n", - "\n", - "Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид.\n", - "\n", - "Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int l = 179; // нужная нам длина\n", - "int ans = 0;\n", - "\n", - "// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена\n", - "bool used[maxn];\n", - "int s[maxn]; // размеры поддеревьев\n", - "\n", - "void sizes (int v, int p) {\n", - " s[v] = 1;\n", - " for (int u : g[v])\n", - " if (u != p && !used[u])\n", - " sizes(u, v), s[v] += s[u];\n", - "}\n", - "\n", - "int centroid (int v, int p, int n) {\n", - " for (int u : g[v])\n", - " if (u != p && !used[u] && s[u] > n/2)\n", - " return centroid(u, v, n);\n", - " return v;\n", - "}\n", - "\n", - "// записывает в t[] глубины вершин\n", - "void dfs (int v, int p, int d, vector &t) {\n", - " t.push_back(d);\n", - " for (int u : g[v])\n", - " if (u != p && !used[u])\n", - " dfs(u, v, d + 1, t);\n", - "} \n", - "\n", - "void solve (int v) {\n", - " /* <единственный зависящий от конкретной задачи код> */\n", - " size(v);\n", - " vector d(s[v], 0);\n", - " d[0] = 1;\n", - " for (int u : g[v]) {\n", - " if (!used[u]) {\n", - " vector t;\n", - " dfs(u, v, 1, t);\n", - " for (int x : t)\n", - " if (x <= l)\n", - " ans += d[l-x];\n", - " for (int x : t)\n", - " d[x]++;\n", - " }\n", - " }\n", - " /* */\n", - "\n", - " used[v] = 1;\n", - " for (int u : g[v])\n", - " if (!used[u])\n", - " solve(centroid(u, v, s[u]/2));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Асимптотика** $O(n \\log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\\log n)$ «уровней» центроидной декомпозиции." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на путях — offline\n", - "\n", - "Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.\n", - "\n", - "А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем.\n", - "\n", - "Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. \n", - "\n", - "Таким образом, каждый запрос будет просмотрен $O(\\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \\log n + n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на путях — online\n", - "\n", - "Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.\n", - "\n", - "Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \\times \\log n$, в котором для каждой вершины будем хранить $O(\\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.\n", - "\n", - "Тогда, при ответе на запрос, мы за $O(\\log n)$ или даже $O(\\log \\log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Асимптотика при более долгих пересчётах\n", - "\n", - "TODO: сослаться на мастер-теорему\n", - "\n", - "Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \\log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени?\n", - "\n", - "Возьмем худший случай — когда каждый раз компонента разбивается на две равные части.\n", - "\n", - "\n", - "\n", - "Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:\n", - "\n", - "$$ \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log \\frac{n}{2^k}\n", - "\\leq \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log n\n", - "= n \\log n \\sum_{k=0}^{\\log n} \\frac{1}{2^k}\n", - "< 2 n \\log n\n", - "= O(n \\log n) $$\n", - "\n", - "Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \\log n)$ — как, впрочем, и во всех остальных «разделяйках»." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb b/web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb deleted file mode 100644 index a1f3db1..0000000 --- a/web/img/.ipynb_checkpoints/cuda-checkpoint.ipynb +++ /dev/null @@ -1,475 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Supercomputing for the Masses\n", - "\n", - "This is htmt-rendered notebook is supposed to be opened with Colab or a debian-based linux machine with a CUDA-capable GPU.\n", - "\n", - "You need to complete a quest and install CUDA.\n", - "\n", - "We do not expect anything from reader except some knowledge of C, Python basic algorithms and generally how computers work." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Subtlties of the Moore's law\n", - "\n", - "There are physical limitations to the speed of processors.\n", - "\n", - "One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Some of them have" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Why multiprocessing?\n", - "\n", - "Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are two types of " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## General-purpose GPU\n", - "\n", - "There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills.\n", - "\n", - "There are several.\n", - "\n", - "This is like with Windows and Linux.\n", - "\n", - "We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting up" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can easily dump 98% of performance of you think this way." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Everything should work if you're on Colab. Otherwise, you might need to complete a [little quest](https://wiki.tiker.net/PyCuda/Installation/Linux/Ubuntu). It will probably be enough to do `sudo apt-get install nvidia-cuda-dev` (install CUDA itself) and then return to `pip install pycuda`.\n", - "\n", - "You can get it by running `lspci | grep NVIDIA`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install pycuda" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy\n", - "\n", - "from pycuda.compiler import SourceModule\n", - "import pycuda.driver as drv\n", - "import pycuda.autoinit" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time.\n", - "\n", - "You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Why CUDA\n", - "\n", - "Most of it still applicable.\n", - "\n", - "Again, GPU programming is very specific.\n", - "\n", - "SSE and tensor cores." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Kernels\n", - "\n", - "Just like C or C++, except that you use some custom built-in functions and specifiers.\n", - "\n", - "CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:\n", - "\n", - "You need to think of your computer as a heterogenious machine: there is host data and device data.\n", - "\n", - "* You move input data to device memory.\n", - "* You run some computation on device.\n", - "* You retrieve back this data.\n", - "\n", - "In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## NumPy\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example: $a + b$\n", - "\n", - "For coordination and testing, we will use `numpy` package. If you don't have it, install it: `pip install numpy`. This is also very optimized, so we will benchmark against it." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "ename": "LaunchError", - "evalue": "cuMemAlloc failed: unspecified launch failure", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0ma\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mastype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumpy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfloat32\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0ma_gpu\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmem_alloc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdtype\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitemsize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmemcpy_htod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma_gpu\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mLaunchError\u001b[0m: cuMemAlloc failed: unspecified launch failure" - ] - } - ], - "source": [ - "a = numpy.random.randn(4,4)\n", - "\n", - "a = a.astype(numpy.float32)\n", - "\n", - "a_gpu = cuda.mem_alloc(a.size * a.dtype.itemsize)\n", - "\n", - "cuda.memcpy_htod(a_gpu, a)\n", - "\n", - "mod = SourceModule(\"\"\"\n", - " __global__ void doublify(float *a)\n", - " {\n", - " int idx = threadIdx.x + threadIdx.y*4;\n", - " a[idx] *= 2;\n", - " }\n", - "\"\"\")\n", - "\n", - "func = mod.get_function(\"doublify\")\n", - "func(a_gpu, block=(4,4,1))\n", - "\n", - "a_doubled = numpy.empty_like(a)\n", - "cuda.memcpy_dtoh(a_doubled, a_gpu)\n", - "print(a)\n", - "print(a_doubled)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "ename": "LaunchError", - "evalue": "cuModuleLoadDataEx failed: unspecified launch failure - ", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mdest\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m }\n\u001b[0;32m----> 6\u001b[0;31m \"\"\")\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0madd_kernel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmod\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"add\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/anaconda3/lib/python3.7/site-packages/pycuda/compiler.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, source, nvcc, options, keep, no_extern_c, arch, code, cache_dir, include_dirs)\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 293\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mpycuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdriver\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 294\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodule\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcubin\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 295\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 296\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_bind_module\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mLaunchError\u001b[0m: cuModuleLoadDataEx failed: unspecified launch failure - " - ] - } - ], - "source": [ - "mod = SourceModule(\"\"\"\n", - " __global__ void add(float *dest, float *a, float *b) {\n", - " const int i = threadIdx.x;\n", - " dest[i] = a[i] + b[i];\n", - " }\n", - "\"\"\")\n", - "\n", - "add_kernel = mod.get_function(\"add\")\n", - "\n", - "a = numpy.random.randn(400).astype(numpy.float32)\n", - "b = numpy.random.randn(400).astype(numpy.float32)\n", - "\n", - "dest = numpy.zeros_like(a)\n", - "\n", - "add_kernel(drv.Out(dest), drv.In(a), drv.In(b), block=(400,1,1))\n", - "\n", - "print(dest-a*b)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mod = SourceModule(\"\"\"\n", - " __global__ void check_prime(int *a, int *b, int *c) {\n", - " int n = blockIdx.x*blockDim.x + threadIdx.x;\n", - " for (int i = 2; i < 10000; i++)\n", - " a[i] |= (n % i == 0);\n", - " }\n", - "\"\"\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "a = numpy.zeros(10**8, dtype=numpy.int)\n", - "\n", - "func = mod.get_function(\"check_prime\")\n", - "func(cuda.InOut(a), block=(len(a)//32,32, 1))\n", - "\n", - "print(a)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "What you need to understand about GPUs is that they are extremely specialised for their applications.\n", - "\n", - "Intrinsics for that.\n", - "\n", - "Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision.\n", - "\n", - "First, they introduced \"multiply-accumulate\" operation (e. g. `x += y * z`) per 1 GPU clock cycle.\n", - "\n", - "Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell).\n", - "\n", - "Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix).\n", - "\n", - "This is a lot of work per \n", - "\n", - "Well, you don't really need anything more precise than that for deep learning anyway.\n", - "\n", - "\n", - "It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices.\n", - "\n", - "Probably, the proper name would be \"4x4 matrix cores\", however NVIDIA marketing team decided to use \"tensor cores\".\n", - "\n", - "So, see, this is not exactly fair comparison.\n", - "\n", - "\n", - "\n", - "*
You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull
*" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "down to int4 (16-valued, you heard correct)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Warps and thread blocks\n", - "\n", - "Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties.\n", - "\n", - "\n", - "\n", - "You can actually do the same stuff with 2d and 3d indexing — weird, right?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Reducing an array\n", - "\n", - "It seems to be simple: you just need to .\n", - "\n", - "What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen:\n", - "\n", - "1. Read $x$ into register\n", - "2. Read $s$ into register\n", - "3. Calculate $s + x$\n", - "4. Write it back to wherever $s$ was initially\n", - "\n", - "Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value.\n", - "\n", - "\n", - "\n", - "Note: Atomics to do that\n", - "\n", - "for small data types they are implemented on the hardware level and much more faster than that.\n", - "\n", - "std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Memory types\n", - "\n", - "If all the various types of device memory were to race, here’s how the race would turn out:\n", - "\n", - "Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible).\n", - "\n", - "* 1st place: **Register memory**\n", - "
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread.\n", - "* 2nd place: **Shared Memory**\n", - "
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should\n", - "* 3rd place: **Constant Memory**\n", - "
\n", - "* 4th: Texture Memory\n", - "* Tie for last place: Local Memory and Global Memory\n", - "\n", - "What you need to care for now is register \n", - "\n", - "For now, you need to care about differe\n", - "\n", - "Accessing global memory takes hundreds." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Heterogineous computing\n", - "\n", - "CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more CUDA-enabled NVIDIA GPU devices.\n", - "\n", - "Threading resources\n", - "Execution pipelines on host systems can support a limited number of concurrent threads. Servers that have four hex-core processors today can run only 24 threads concurrently (or 48 if the CPUs support Hyper-Threading.) By comparison, the smallest executable unit of parallelism on a CUDA device comprises 32 threads (termed a warp of threads). Modern NVIDIA GPUs can support up to 1536 active threads concurrently per multiprocessor (see Features and Specifications of the CUDA C Programming Guide) On GPUs with 16 multiprocessors, this leads to more than 24,000 concurrently active threads.\n", - "Threads\n", - "Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches (when two threads are swapped) are therefore slow and expensive. By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work (in warps of 32 threads each). If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput.\n", - "RAM\n", - "The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem: dense matrix multiplication\n", - "\n", - "A lot of these are actually sparse. You can do stuff with social network graphs or web graphs.\n", - "\n", - "Cool. But let's disapploint us for a bit:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem: dynamic programming" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Some primitives\n", - "\n", - "Before we go deeper into more complex algorithms, we need to master some " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem: sorting" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb b/web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb deleted file mode 100644 index 3506ced..0000000 --- a/web/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb +++ /dev/null @@ -1,109 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Динамическое программирование" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Разберем некоторые классические идеи и задачи в динамическом программировании." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Рюкзак\n", - "\n", - "> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наибольшая общая подпоследовательность\n", - "\n", - "> Есть две строки. Найти длину их наибольшей общей подпоследовательности.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Расстановка скобок\n", - "\n", - "> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.\n", - "\n", - "Можно завести динамику «можно ли »." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наибольшая возрастающая подпоследовательность\n", - "\n", - "Есть также алгоритм за $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мемоизация\n", - "\n", - "Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.\n", - "\n", - "Первый предполагает" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Число разбиений\n", - "\n", - ">" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Замена параметра на ответ\n", - "\n", - ">" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb b/web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb deleted file mode 100644 index 30cde99..0000000 --- a/web/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb +++ /dev/null @@ -1,288 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Оптимизации динамики\n", - "\n", - "* Разделяй и властвуй\n", - "* Оптимизация Кнута\n", - "* Convex Hull Trick\n", - "* Лямбда-оптимизация" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Задача\n", - "\n", - "> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.\n", - "\n", - "Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.).\n", - "\n", - "Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\}$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// x[] — отсортированный массив координат точек, нумерация с нуля\n", - "\n", - "// квадрат длины отрезка от i-той до j-той точки\n", - "int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }\n", - "\n", - "// TODO: предподсчитать cost\n", - "\n", - "for (int i = 0; i <= m; i++)\n", - " f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо\n", - "// все остальные f предполагаем равными бесконечности\n", - "\n", - "for (int i = 1; i <= n; i++)\n", - " for (int j = 1; j <= m; j++)\n", - " for (int k = 0; k < i; k++)\n", - " f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Заметим, что циклы по `i` и `j` можно поменять местами.\n", - "\n", - "Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Разделяй-и-властвуй\n", - "\n", - "Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.\n", - "\n", - "*Утверждение.* $opt[i, j] \\leq opt[i, j+1]$.\n", - "\n", - "Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.\n", - "\n", - "Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$.\n", - "\n", - "Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "void solve (int l, int r, int _l, int _r, int k) {\n", - " if (l > r) return; // отрезок пустой — выходим\n", - " int t = (l + r) / 2;\n", - " int opt = _l;\n", - " for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы \n", - " int val = f[i+1][k-1] + cost(i, j);\n", - " if (val < f[t][k])\n", - " f[t][k] = val, opt = i;\n", - " }\n", - " solve(l, t-1, _l, opt, k);\n", - " solve(t+1, r, opt, _r, k);\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Вызываться она будет просто последовательно для каждого слоя:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "for (int k = 1; k <= m; k++)\n", - " solve(0, n-1, 0, n-1, k);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Теперь пересчет одного «слоя» динамики занимает $O(n \\log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\\log n)$, а значит и каждый элемент будет просмотрен не более $O(\\log n)$ раз.\n", - "\n", - "Получается, что асимптотика улучшилась до $O(n m \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Оптимизация Кнута\n", - "\n", - "Предыдущий метод основывался на том факте, что $opt[i, j] \\leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру?\n", - "\n", - "$$ opt[i-1, j] \\leq opt[i, j] \\leq opt[i, j+1] $$\n", - "\n", - "В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок.\n", - "\n", - "Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:\n", - "\n", - "$$ \\sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \\sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$\n", - "\n", - "Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for (int i = 1; i <= n; i++) {\n", - " for (int j = m; j >= 1; j--) {\n", - " for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) {\n", - " int val = f[i+1][k-1] + cost(i, j);\n", - " if (val < f[t][k])\n", - " f[t][k] = val, opt[i][j] = i;\n", - " }\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Сравните с базовым решением — всего 3 новых строчки." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Convex Hull Trick\n", - "\n", - "Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение.\n", - "\n", - "$$ f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\} = \\min_{k < i} \\{\n", - "f[k, j-1] + x_{i-1}^2\n", - "- 2x_{i-1} x_k\n", - "+ x_k^2\n", - "\\}$$\n", - "\n", - "Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется\n", - "$\n", - "\\underbrace{f[k, j-1] + x_k^2}_{a_k}\n", - "\\underbrace{-2x_k}_{b_k} x_{i-1}\n", - "$.\n", - "\n", - "Это теперь можно переписать как $\n", - "\\min_k\n", - "(a_k, b_k)\n", - "\\cdot\n", - "(1, X_{i-1})\n", - "$ (имеется в виду скалярное произведение)\n", - "\n", - "Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: иллюстрация." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: мем про Скуби-Ду." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ли Шао" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Лямбда-оптимизация\n", - "\n", - "Не «фольклорное» название — дискретный метод множителей Лагранжа.\n", - "\n", - "Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .\n", - "\n", - "Заметим следующее: \n", - "\n", - "Давайте заменим $f[i, j]$ на $g_\\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\\lambda$, за использование отрезка.\n", - "\n", - "Давайте сделаем бинпоиск по $\\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \\lambda k$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Суммируем\n", - "\n", - "TODO: сделать табличку\n", - "\n", - "* Разделяйка: $O(nm \\log n)$, если `cost` такой, что `opt` монотонна по одному аргументу.\n", - "* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам.\n", - "* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение.\n", - "* Лагранж: $O(n \\log n)$. Функция должна быть выпуклой." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Другие задачи\n", - "\n", - "Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.\n", - "\n", - "> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "c++", - "file_extension": ".c++", - "mimetype": "text/x-c++src", - "name": "c++" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb b/web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb deleted file mode 100644 index 3c3d9f3..0000000 --- a/web/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb +++ /dev/null @@ -1,285 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "\n", - "НЯ!\n", - "
Эта статья полна любви и обожания.\n", - "
Возможно, стоит добавить ещё больше?\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Дерево Фенвика\n", - "\n", - "Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Определение\n", - "\n", - "Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: \n", - " \n", - "$$ t_i = \\sum_{k=F(i)}^i a_k $$\n", - " \n", - "где $F$ это какая-то функцию, для которой выполнено $F(i) \\leq i$. Конкретно её определим потом.\n", - " \n", - "Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле:\n", - " \n", - "$$ sum(k) = t_k + sum(F(k)-1) $$\n", - " \n", - "Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\\log n)$. Популярны две функции:\n", - "\n", - "* $F_1(x) =$ `x & (x + 1)`\n", - "* $F_2(x) =$ `x - (x & -x) + 1`\n", - "\n", - "Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.\n", - "\n", - "**Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\\log n)$ таких отрезков." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Реализация\n", - "\n", - "Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int t[maxn];\n", - "\n", - "// возвращает сумму на префиксе\n", - "int sum (int r) {\n", - " int res = 0;\n", - " for (; r > 0; r -= r & -r)\n", - " res += t[r];\n", - " return res;\n", - "}\n", - "\n", - "int sum (int l, int r) {\n", - " return sum(r) - sum(l-1);\n", - "}\n", - "\n", - "// обновляет нужные t\n", - "void add (int k, int x) {\n", - " for (; k <= n; k += k & -k)\n", - " t[k] += x;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Многомерный случай\n", - "\n", - "> $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку\n", - "\n", - "Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре.\n", - "\n", - "`sum` перепишется следующим образом:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int sum (int r1, int r2) {\n", - " int res = 0;\n", - " for (int i = r1; i > 0; i -= i & -i)\n", - " for (int j = r2; j > 0; j -= j & -j)\n", - " ans += t[i][j];\n", - " return res;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах.\n", - "\n", - "Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \\log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Бинпоиск\n", - "\n", - "Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\\log n)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// возвращает индекс, на котором сумма уже больше\n", - "int lower_bound (int s) {\n", - " int k = 0;\n", - " for (int l = logn; l >= 0; l--) {\n", - " if (k + (1<= k > i - (i & -i)`. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Будем перебирать префиксы TODO\n", - "\n", - "Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя.\n", - "\n", - "Можно представить дерево так: ячейка 2^k содержит все\n", - "\n", - "TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Название\n", - "\n", - "Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree».\n", - "\n", - "Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.\n", - "\n", - "Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).\n", - "\n", - "В частном случае, когда длина массива равна $2^k$, то дерево будет только одно." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/games-checkpoint.ipynb b/web/img/.ipynb_checkpoints/games-checkpoint.ipynb deleted file mode 100644 index f183b10..0000000 --- a/web/img/.ipynb_checkpoints/games-checkpoint.ipynb +++ /dev/null @@ -1,331 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "c0JYtYks_TYp" - }, - "source": [ - "# Теория игр\n", - "\n", - "Начнём с самого баянного примера математической игры, который можно вспомнить:\n", - "\n", - "> На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков.\n", - "\n", - "Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так:\n", - "\n", - "$$ f_k = \\lnot f_{k-1} \\lor \\lnot f_{k-2} \\lor \\lnot f_{k-3} = \\lnot (f_{k-1} \\land f_{k-2} \\land f_{k-3}) $$\n", - "\n", - "то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния).\n", - "\n", - "**Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Игры на графах\n", - "\n", - "В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек».\n", - "\n", - "Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные*\n", - "\n", - "Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*.\n", - "\n", - "Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ретроанализ\n", - "\n", - "Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности:\n", - "\n", - "* Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины.\n", - "* Вершина *выигрышная* — если из неё есть переход в проигрышную вершину.\n", - "* Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные).\n", - "\n", - "Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся.\n", - "\n", - "**Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин.\n", - "\n", - "Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние.\n", - "\n", - "Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось.\n", - "\n", - "Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные.\n", - "\n", - "**Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали.\n", - "\n", - "* При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). \n", - "* При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше.\n", - "\n", - "Асимптотика составит $O(n + m)$, как и у любого bfs-а." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа\n", - "int cnt[maxn]; // счётчик исходящих рёбер\n", - "\n", - "enum StatusType { win, loss, unknown };\n", - "StatusType status[maxn]; // выгрышность вершины;\n", - "// по умолчанию все кроме терминальных считаются unknown\n", - "// те, кто в итоге остаются unknown -- ничейные\n", - "\n", - "queue q = {/* нужно заранее добавить сюда все терминальные вершины*/};\n", - "\n", - "while (!q.empty()) {\n", - " int v = q.front();\n", - " q.pop();\n", - " for (int u : t[v]) {\n", - " cnt[u]--; // удаляем это ребро\n", - " if (status[v] == unknown) {\n", - " // из u есть ребро в проигрышную -- значит она выигрышная\n", - " if (status[v] == loss)\n", - " status[u] = win;\n", - " // все ребра u ведут в выигрышные вершины -- значит она проигрышная\n", - " if (status[v] == win && cnt[u] == 0) {\n", - " status[u] = loss;\n", - " // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь\n", - " if (status[v] != unknown)\n", - " q.push(u);\n", - " }\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "StatusType dfs(int v) {\n", - " if (status[v] != unknown)\n", - " return status[v];\n", - " status[v] = loss;\n", - " // изменим статус, когда найдём переход в проигрышную вершину\n", - " for (int u : g[v])\n", - " if (dfs(u) == loss)\n", - " status[v] = win;\n", - " return status[v];\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: можно ли здесь циклы учесть?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Минимаксные игры\n", - "\n", - "Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно.\n", - "\n", - "Типичный граф минимаксной игры выглядит следующим образом:\n", - "\n", - "\n", - "\n", - "В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников.\n", - "\n", - "Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ретроанализ для больших графов\n", - "\n", - "Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго.\n", - "\n", - "Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей).\n", - "\n", - "Ретроанализ нужно оптимизировать." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ограничение перебора\n", - "\n", - "Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. \n", - "\n", - "Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит.\n", - "\n", - "Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли).\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Мемоизация позиций\n", - "\n", - "В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает .\n", - "\n", - "Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Альфа-бета отсечение\n", - "\n", - "\n", - "\n", - "Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым.\n", - "\n", - "На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым.\n", - "\n", - "Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6.\n", - "\n", - "Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем.\n", - "\n", - "Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Monte-Carlo Tree Search\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Игры с ненулевой суммой\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Теория Шпрага-Гранди\n", - "\n", - "Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые.\n", - "\n", - "Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями.\n", - "\n", - "**Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \\oplus a_2 \\oplus \\ldots \\oplus a_n $ размеров кучек отлична от нуля.\n", - "\n", - "**Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы:\n", - "\n", - "* Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \\oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия.\n", - "* Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \\oplus a_i \\oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \\oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$.\n", - "\n", - "Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \\oplus a_i$.\n", - "\n", - "Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ним с увеличениями\n", - "\n", - "Пусть у нас" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Эквивалентность игр ниму" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Игры с неполной информацией\n", - "\n", - "Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков.\n", - "\n", - "Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет.\n", - "\n", - "Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Дилемма заключенного" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Камень-ножницы-бумага" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Покер\n", - "\n", - "Рассмотрим такую у" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "29_game-theory.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb b/web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb deleted file mode 100644 index 555b3b9..0000000 --- a/web/img/.ipynb_checkpoints/geometry-checkpoint.ipynb +++ /dev/null @@ -1,421 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Геометрия и ООП" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали.\n", - "\n", - "![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg)\n", - "\n", - "Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:\n", - "\n", - "$$ a \\cdot b = x_a x_b + y_a y_b $$\n", - "\n", - "![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg)\n", - "\n", - "У него есть полезные свойства:\n", - "\n", - "* Скалярное произведение симметрично ($a \\cdot b = b \\cdot a$).\n", - "* Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.\n", - "* Перпендикулярные вектора должны иметь нулевое скалярное произведение.\n", - "* Если угол острый, то оно положительное\n", - "* Если угол тупой, то отрицательное\n", - "\n", - "**Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:\n", - "\n", - "$$ a \\times b = x_a y_b - y_a x_b $$\n", - "\n", - "![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg)\n", - "\n", - "Его свойства:\n", - "\n", - "* Скалярное произведение *анти*симметрично ($a \\times b = - (b \\times a)$).\n", - "* Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.\n", - "* Коллинеарные вектора должны иметь нулевое векторное произведение.\n", - "* Если b «слева» от a, то оно положительное.\n", - "* Если b «справа» — то отрицательное." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Всякие проверки\n", - "\n", - "Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.\n", - "\n", - "**Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.\n", - "\n", - "$$\n", - "\\text{P лежит внутри ABC} \\iff \\begin{cases}\n", - "(B-A) \\times (P-A) \\geq 0 \\\\\n", - "(C-B) \\times (P-B) \\geq 0 \\\\\n", - "(A-C) \\times (P-C) \\geq 0 \\\\\n", - "\\end{cases}\n", - "$$\n", - "\n", - "**Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.\n", - "\n", - "$$ V = \\frac{1}{2} (B-A) \\times (C-A) $$\n", - "\n", - "**Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).\n", - "\n", - "![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg)\n", - "\n", - "Забудьте о формуле Герона и всегда считайте площади через векторное произведение.\n", - "\n", - "Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.\n", - "\n", - "**Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\\times(c-a) > 0$).\n", - "\n", - "**Пересекаются ли отрезки**.\n", - "\n", - "![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Уравнение прямой\n", - "\n", - "Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством.\n", - "\n", - "У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \\geq 0$ будет указывать в сторону самой полуплоскости.\n", - "\n", - "Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой:\n", - "\n", - "$$ d = \\frac{|Ax_0+By_0+C|}{\\sqrt{A^2+B^2}} $$\n", - "\n", - "**Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "A_1 x + B_1 y + C_1 = 0 \\\\\n", - "A_2 x + B_2 y + C_2 = 0\n", - "\\end{cases}\n", - "\\implies \n", - "\\begin{cases}\n", - "-x = \\frac{B_1 y + C_1}{A_1} \\\\\n", - "-x = \\frac{B_2 y + C_2}{A_2}\n", - "\\end{cases}\n", - "\\implies \n", - "\\frac{B_1 y + C_1}{A_1} = \\frac{B_2 y + C_2}{A_2}\n", - "\\implies \n", - "y = - \\frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1}\n", - "$$\n", - "\n", - "Аналогично, $x = \\frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки).\n", - "\n", - "Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Как это кодить в C++" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Точка $\\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "struct r {\n", - " double x, y;\n", - " r () {}\n", - " r (int _x, int _y) { x = _x, y = _y; }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Операции над векторами\n", - "\n", - "Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "double len (r a) { return sqrt(a.x*a.x + a.y*a.y); }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Операторы\n", - "\n", - "В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д.\n", - "\n", - "Давайте для будущих нужд определим `+` и `-`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); }\n", - "r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Скалярное произведение" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int operator*(r a, r b){ return a.x*b.x + a.y*b.y; }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Векторное произведение\n", - "\n", - "Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int operator^(r a, r b){ return a.x*b.y - b.x*a.y; }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ввод-вывод\n", - "\n", - "Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "istream& operator>>(istream &in, r &p){ \n", - " in >> p.x >> p.y;\n", - " return in;\n", - "}\n", - "\n", - "ostream& operator<<(ostream &out, r &p){ \n", - " out << p.x << \" \" << p.y << endl;\n", - " return out; \n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Алгебра VS Алгоритмы или зачем мы всё это делали\n", - "\n", - "Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Векторное представление прямой ($Ax + By + C = 0 \\rightarrow r = at + b$)\n", - "\n", - "Тут нужно просто выбрать две любые точки на прямой." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// даны A, B, C (A^2 + B^2 != 0)\n", - "r a, b;\n", - "if (eq(A, 0)) // значит, это горизонтальная прямая\n", - " a = r(0, -C/B), b = r(1, -C/B);\n", - "else\n", - " a = r(-C/A, 0), b = (1, -(C+B)/A, 1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Пример: отражение от прямой\n", - "\n", - "Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.\n", - "\n", - "$\\Pr_a b = \\frac{a \\cdot b}{|a|} \\frac{a}{|a|} = \\frac{|a| |b| \\cos \\alpha}{|a|} \\frac{a}{|a|} = |b| \\cos \\alpha \\frac{a}{|a|} $\n", - "\n", - "Формула имеет смысл: длина на единичный вектор направления.\n", - "\n", - "Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// прямая r = at + b, точка c\n", - "r pr (r a, r b, r c) {\n", - " c -= b; // пусть c и a выходят из одной точки\n", - " return b + (a*b / len(a) / len(a)) * a;\n", - "}\n", - "\n", - "r reflect (r a, r b, r c) {\n", - " return c + 2*(pr(a, b, c)-c);\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Типичные баги" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Точность\n", - "\n", - "> Первое правило действительных чисел — не использовать действительные числа\n", - "\n", - "Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "const double eps = 1e-8;\n", - "\n", - "bool eq (double a, double b) { return abs(a-b) < eps }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### $0 \\neq -0$\n", - "\n", - "Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Область определения обратных функций\n", - "\n", - "`acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb b/web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb deleted file mode 100644 index f1a6144..0000000 --- a/web/img/.ipynb_checkpoints/hashing-checkpoint.ipynb +++ /dev/null @@ -1,426 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "tqKO26jI7Eg2" - }, - "source": [ - "# Хэширование в строковых задачах\n", - "\n", - "Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.\n", - "\n", - "«Хорошая» хэш-функция:\n", - "* Быстро считается — за линейное от размера объекта время;\n", - "* Имеет не очень большие значения — влезающие в 64 бита;\n", - "* «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\\frac{1}{n}$.\n", - "\n", - "Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*.\n", - "\n", - "Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \\cdot n \\cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.\n", - "\n", - " ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "mihqVF9EHNW3" - }, - "source": [ - "## Применения в реальной жизни\n", - "\n", - "* **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.\n", - "* **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\\frac{k}{n}$ = $O(1)$ при правильном выборе $n$.\n", - "* **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.\n", - "* **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.\n", - "* **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.\n", - "* **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "sb5PdKP87Eg3" - }, - "source": [ - "Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы.\n", - "\n", - "Сегодня же мы остановимся на строках." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "AwuPsbSa7Eg3" - }, - "source": [ - "# Полиномиальное хэширование\n", - "\n", - "**Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами.\n", - "\n", - "Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`.\n", - "\n", - "Определим *прямой полиномиальный хэш* строки как значение следующего многочлена:\n", - "\n", - "$$ h_f = (s_0 + s_1 k + s_2 k^2 + \\ldots + s_n k^n) \\mod p $$\n", - "\n", - "Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой.\n", - "\n", - "Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "h3UUC_Vl80Is" - }, - "outputs": [], - "source": [ - "const int k = 31, mod = 1e9+7;\n", - "\n", - "string s = \"abacabadaba\";\n", - "long long h = 0, m = 1;\n", - "for (char c : s) {\n", - " int x = (int) (c - 'a' + 1);\n", - " h = (h + m * x) % mod;\n", - " m = (m * k) % mod;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "7_1zG4nb80I7" - }, - "source": [ - "Можем ещё определить *обратный полиномиальный хэш*:\n", - "\n", - "$$ h_b = (s_0 k^n + s_1 k^{n-1} + \\ldots + s_n) \\mod p $$\n", - "\n", - "Его преимущество в том, что можно написать на одну строчку кода меньше:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "kShe7vvz80JC" - }, - "outputs": [], - "source": [ - "long long h = 0;\n", - "for (char c : s) {\n", - " int x = (int) (c - 'a' + 1);\n", - " h = (h * k + x) % mod;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "VWd2N6Ic80JL" - }, - "source": [ - "Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hKCe8eBr80JP" - }, - "source": [ - "## Зачем он нужен?\n", - "\n", - "Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций.\n", - "\n", - "Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$:\n", - "\n", - "$$ h(ab) = h(a) + k^{|a|} \\cdot h(b) $$\n", - "\n", - "Удалить префикс строки можно так:\n", - "\n", - "$$ h(b) = \\frac{h(ab) - h(a)}{k^{|a|}} $$\n", - "\n", - "А суффикс — ещё проще:\n", - "\n", - "$$ h(a) = h(ab) - k^{|a|} \\cdot h(b) $$\n", - "\n", - "В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "0IBufJNe_jVG" - }, - "outputs": [], - "source": [ - "const int maxn = 1e5+5;\n", - "\n", - "int p[maxn];\n", - "p[0] = 1;\n", - "\n", - "for (int i = 1; i < maxn; i++)\n", - " p[i] = (p[i-1] * k) % mod;" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "_PKLEmZU80Jb" - }, - "source": [ - "Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "qyJGTChd80Jd" - }, - "outputs": [], - "source": [ - "int h[maxn];\n", - "h[0] = 0; // h[k] -- хэш префикса длины k\n", - "\n", - "// будем считать, что s это уже последовательность int-ов\n", - "\n", - "for (int i = 0; i < n; i++) \n", - " h[i+1] = (h[i] + p[i] * s[i]) % mod;" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "L7n-KRvs80Jr" - }, - "source": [ - "Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке:\n", - "\n", - "$$ h(s[l:r]) = \\frac{h_r-h_l}{k^l} $$\n", - "\n", - "Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.\n", - "\n", - "Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить.\n", - "\n", - "$$ \\hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "FMjf8Rej80Jv" - }, - "outputs": [], - "source": [ - "int hash_substring (int l, int r) {\n", - " return (h[r+1] - h[l]) * p[n-l] % mod;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "l5XFHeaJ80J5" - }, - "source": [ - "Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "VuqWJtAvCCaQ" - }, - "source": [ - "### Упражнение\n", - "\n", - "Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "moSrfcxI7EhB" - }, - "source": [ - "## Примеры задач\n", - "\n", - "**Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`.\n", - "\n", - "**Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.\n", - "\n", - "**Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.\n", - "\n", - "**Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором.\n", - "\n", - "**Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "krDNbmroGTTs" - }, - "source": [ - "### Изменения строки*\n", - "\n", - "Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.\n", - "\n", - "Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.\n", - "\n", - "Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "8SFqBzPV7EhD" - }, - "source": [ - "# Вероятность ошибки и почему это всё вообще работает\n", - "\n", - "У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.\n", - "\n", - "Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого?" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "PE38pSDXQxcH" - }, - "source": [ - "## Выбор констант\n", - "\n", - "Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \\cdot n^2$. Обоснование — см. парадокс дней рождений.\n", - "\n", - "Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно.\n", - "\n", - "Можно также брать модуль $2^{64}$. У него есть несколько преимуществ:\n", - "* Он большой — второй модуль точно не понадобится.\n", - "* С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении.\n", - "* С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`.\n", - "\n", - "Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду.\n", - "\n", - "В выборе же $k$ ограничения не такие серьезные:\n", - "* Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.\n", - "* Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.\n", - "\n", - "Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "DSfMhJRhRaZe" - }, - "source": [ - "## Парадокс дней рождений\n", - "\n", - "> В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%.\n", - "\n", - "Более общее утверждение: в мультимножество нужно добавить $\\Theta(\\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали.\n", - "\n", - "**Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения.\n", - "Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$.\n", - "\n", - "$$f(n, d) = (1-\\frac{1}{d}) \\times (1-\\frac{2}{d}) \\times ... \\times (1-\\frac{n-1}{d})$$\n", - "\n", - "Попытаемся оценить $f$:\n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\begin{aligned}\n", - " e^x & = 1 + x + \\frac{x^2}{2!} + \\ldots & \\text{(ряд Тейлора для экспоненты)} \\\\\n", - " & \\simeq 1 + x & \\text{(аппроксимация для $|x| \\ll 1$)} \\\\\n", - " e^{-\\frac{n}{d}} & \\simeq 1 - \\frac{n}{d} & \\text{(подставим $\\frac{n}{d} \\ll 1$)} \\\\\n", - " f(n, d) & \\simeq e^{-\\frac{1}{d}} \\times e^{-\\frac{2}{d}} \\times \\ldots \\times e^{-\\frac{n-1}{d}} & \\\\\n", - " & = e^{-\\frac{n(n-1)}{2d}} & \\\\\n", - " & \\simeq e^{-\\frac{n^2}{2d}} & \\\\\n", - " \\end{aligned}\n", - "\\end{align}\n", - "$$\n", - "\n", - "Из последнего выражения более-менее понятно, что вероятность $\\frac{1}{2}$ достигается при $n \\approx \\sqrt{d}$ и в этой точке изменяется очень быстро.\n", - "\n", - "**Второе доказательство** (для любителей теорвера). Введем $\\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\\frac{1}{d}$.\n", - "\n", - "Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\\frac{n (n-1)}{2} \\cdot \\frac{1}{d}$.\n", - "\n", - "Отсюда понятно, что если $d = \\Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно.\n", - "\n", - "*Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "2WAkqxpvRfj0" - }, - "source": [ - "### Бонус: «мета-задача»\n", - "\n", - "Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.\n", - "\n", - "«Решите» задачу." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "7_string-hashing.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/web/img/.ipynb_checkpoints/hld-checkpoint.ipynb b/web/img/.ipynb_checkpoints/hld-checkpoint.ipynb deleted file mode 100644 index 5faf5cd..0000000 --- a/web/img/.ipynb_checkpoints/hld-checkpoint.ipynb +++ /dev/null @@ -1,186 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Heavy-light декомпозиция\n", - "\n", - "HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid).\n", - "\n", - "\n", - "\n", - "*
TODO: найти менее уродливую иллюстрацию
*\n", - "\n", - "Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.\n", - "\n", - "Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.\n", - "\n", - "**Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер.\n", - "\n", - "**Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.\n", - "\n", - "Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.\n", - "\n", - "**Утверждение.** На любом вертикальном пути будет не более $O(\\log n)$ блоков.\n", - "\n", - "**Доказательство** разбивается на две части:\n", - "\n", - "* Лёгких ребер на вертикальном пути будет не более $O(\\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего.\n", - "* Непрерывных путей из тяжелых рёбер будет не более $O(\\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\\log n)$.\n", - "\n", - "**Следствие.** На любом пути будет не более $O(\\log n)$ блоков.\n", - "\n", - "Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Реализация\n", - "\n", - "Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.\n", - "\n", - "А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.\n", - "\n", - "Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vector g[maxn];\n", - "int s[maxn], p[maxn], tin[maxn], tout[maxn];\n", - "int head[maxn]; // «голова» тяжелого пути, которому принадлежит v\n", - "int t = 0;\n", - "\n", - "void sizes (int v = 0) {\n", - " s[v] = 1;\n", - " for (int &u : g[v]) {\n", - " sizes(u);\n", - " s[v] += s[u];\n", - " if (s[u] > s[g[v][0]])\n", - " // &u -- это ссылка, так что её легально использовать при swap-е\n", - " swap(u, g[v][0]);\n", - " }\n", - "}\n", - "\n", - "void hld (int v = 0) {\n", - " rin[t] = v;\n", - " tin[v] = t++;\n", - " for (int u : g[v]) {\n", - " // если это тяжелый ребенок -- его next нужно передать\n", - " // в противном случае он сам является головой нового пути\n", - " head[u] = (u == g[v][0] ? head[v] : u);\n", - " hld(u);\n", - " }\n", - " tout[v] = t;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Как им решать задачи\n", - "\n", - "Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:\n", - "\n", - "1. Узнать минимальное число на пути между $v_i$ и $u_i$.\n", - "2. Изменить число у $v_i$-той вершины на $x_i$.\n", - "\n", - "Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int val[maxn];\n", - "segtree st(0, n);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "void upd (int v, int x) {\n", - " st.upd(tin[v], x);\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int ancestor (int a, int b) {\n", - " return tin[a] <= tin[b] && tin[b] <= tout[a];\n", - "}\n", - "\n", - "void up (int &a, int &b, int &ans) {\n", - " while (!ancestor(head[a], b)) {\n", - " ans = min(ans, st.get_min(tin[head[a]], tin[a]));\n", - " a = p[head[a]];\n", - " }\n", - "}\n", - "\n", - "int get_min (int a, int b) {\n", - " int ans = inf;\n", - " up(a, b, ans);\n", - " up(b, a, ans);\n", - " if (!ancestor(a, b))\n", - " swap(a, b);\n", - " ans = min(ans, st.get_min(tin[a], tin[b]));\n", - " return ans;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/index-checkpoint.ipynb b/web/img/.ipynb_checkpoints/index-checkpoint.ipynb deleted file mode 100644 index b2dd755..0000000 --- a/web/img/.ipynb_checkpoints/index-checkpoint.ipynb +++ /dev/null @@ -1,81 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Алгоритмика\n", - "\n", - "Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/).\n", - "\n", - "Более-менее готово:\n", - "\n", - "* [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное\n", - "* [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое\n", - "* [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки\n", - "* [Центроидная декомпозиция](http://sereja.me/a/centroid)\n", - "* [Heavy-light декомпозиция](http://sereja.me/a/hld)\n", - "* [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла\n", - "* [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву\n", - "* [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость\n", - "* [Ро-алгоритм Полларда](http://sereja.me/a/pollard)\n", - "* [Sparse table](http://sereja.me/a/sparse-table)\n", - "* [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона\n", - "* [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса\n", - "* [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера\n", - "* [Классные задачки](http://sereja.me/a/bayans): пока что без решений\n", - "\n", - "Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально.\n", - "\n", - "Не очень готово:\n", - "\n", - "* [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция\n", - "* [Суффиксный массив](http://sereja.me/a/suffix-array)\n", - "* [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++\n", - "* [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick\n", - "* [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала\n", - "* [Обратное по модулю](http://sereja.me/a/reciprocal)\n", - "* [Метод отжига](http://sereja.me/a/annealing)\n", - "* [Стресс-тестирование](http://sereja.me/a/stress-test)\n", - "* [Бор](http://sereja.me/a/trie)\n", - "* [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько\n", - "* Выпуклые оболочки\n", - "* [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса\n", - "* CUDA: программирование на GPU (на английском)\n", - "\n", - "Вообще не готово:\n", - "* [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема\n", - "* [Ахо-Корасик](http://sereja.me/a/aho-corasick)\n", - "* [Теорвер](http://sereja.me/a/probability)\n", - "* Корневая декомпозиция\n", - "* Теория информации\n", - "* Функции потерь в машинном обучении\n", - "* Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском)\n", - "* Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском)\n", - "\n", - "Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb b/web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb deleted file mode 100644 index 7200ab9..0000000 --- a/web/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb +++ /dev/null @@ -1,122 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Быстрое умножение\n", - "\n", - "В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).\n", - "\n", - "[Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.\n", - "\n", - "Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.\n", - "\n", - "Алгоритм Карацубы имеет довольно значимое место в истории науки.\n", - "\n", - "{\\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}).\n", - "У Колмогорова была гипотеза, что нижняя оценка для {\\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности\n", - "{\\displaystyle M(n)=O(n^{\\log _{2}3})} M(n)=O(n^{{\\log _{2}3}})\n", - "и тем самым опроверг «гипотезу {\\displaystyle n^{2}} n^{2}».\n", - "\n", - "(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)\n", - "\n", - "Можно представить себе какого-то грозного японца." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мастер-теорема\n", - "\n", - "Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек»." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Мастер-теорема.** Пусть имеется рекуррента:\n", - "\n", - "$$ T(n) = \\begin{cases}\n", - "a T(\\frac{n}{b}) + \\Theta(n^c), & n > n_0\n", - "\\\\ \\Theta(1), & n \\leq n_0\n", - "\\end{cases} $$\n", - "\n", - "Тогда:\n", - "\n", - "* **A.** Если $c > \\log_b a$, то $T(n) = \\Theta(n^c)$.\n", - "* **B.** Если $c = \\log_b a$, то $T(n) = \\Theta(n^c \\log n)$.\n", - "* **C.** Если $c < \\log_b a$, то $T(n) = \\Theta(n^{\\log_b a})$.\n", - "\n", - "---\n", - "\n", - "\n", - "\n", - "---\n", - "\n", - "**Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням:\n", - "\n", - "$$ T(n) = \\sum_{k=0}^{\\log_b n} a^k (\\frac{n}{b^k})^c = n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k $$\n", - "\n", - "* **A.** Если $c > \\log_b a$, то $\\sum (\\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \\Theta(n^c)$.\n", - "* **B.** Если $c = \\log_b a$, то\n", - "$$\\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\sum_{k=0}^{\\log_b n} 1^k = \\Theta(n^c \\log_b n)$$\n", - "* **C.** Если $c < \\log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,\n", - "\n", - "$$ n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\Theta(n^c (\\frac{a}{b^c})^{\\log_b n}) = \\Theta(n^c \\cdot \\frac{a^{\\log_b n}}{n^c}) = \\Theta(a^{\\log_b n}) = \\Theta(n^{\\log_b a}) $$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Алгоритм Карацубы\n", - "\n", - "Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат.\n", - "\n", - "Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Развитие идеи\n", - "\n", - "То же самое можно применить матрицам.\n", - "\n", - "Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\\epsilon})$ для произвольного $\\epsilon$ ещё никто не доказал." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/lca-checkpoint.ipynb b/web/img/.ipynb_checkpoints/lca-checkpoint.ipynb deleted file mode 100644 index f4e7190..0000000 --- a/web/img/.ipynb_checkpoints/lca-checkpoint.ipynb +++ /dev/null @@ -1,339 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "1MwyLmBaV1hj" - }, - "source": [ - "# Корневые деревья" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hMjCrvgNV1hk" - }, - "source": [ - "Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*.\n", - "\n", - "Примеры корневых деревьев:\n", - "* наследование классов в языках программирования (если множественное наследование запрещено),\n", - "* дерево факторизации числа на простые (в общем случае не уникальное),\n", - "* иерархия в какой-нибудь компании,\n", - "* просто множество как-то вложенных объектов (ссылка на крысу-роботягу).\n", - "\n", - "Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Напоминание: DFS\n", - "\n", - "Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vector g[maxn];\n", - "int p[maxn], tin[maxn], tout[maxn];\n", - "int t = 0;\n", - "\n", - "void dfs (int v) {\n", - " tin[v] = t++;\n", - " for (int u : g[v])\n", - " dfs(u);\n", - " tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет \n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "У этих массивов много полезных свойств:\n", - "* Вершина $u$ является предком $v$ $\\iff tin_v \\in [tin_u, tout_u) $. Эту проверку можно делать за константу.\n", - "* Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой.\n", - "* В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер.\n", - "* Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$.\n", - "* Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на поддеревьях\n", - "\n", - "Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.\n", - "\n", - "> Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$.\n", - "\n", - "Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на уровнях\n", - "\n", - "> Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$).\n", - "\n", - "Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам.\n", - "\n", - "Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наименьший общий предок\n", - "\n", - "Очень много задач нам поможет решить следующая вспомогательная задача.\n", - "\n", - "> Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких.\n", - "\n", - "По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные.\n", - "\n", - "![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png)\n", - "\n", - "Для лучшего понимания: медленно (за линейное время) это можно делать так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bool a (int u, int v) {\n", - " return tin[u] <= tin[v] && tin[v] <= tout[u];\n", - "}\n", - "\n", - "int lca (int u, int v) {\n", - " while (!ancestor(u, v))\n", - " u = p[u];\n", - " return u;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## LCA: двоичные подъемы\n", - "\n", - "Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \\times \\lceil \\log n \\rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень.\n", - "\n", - "Такой препроцессинг можно выполнить за $O(n \\log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int up[maxn][logn];\n", - "\n", - "void dfs (int v) {\n", - " for (int l = 1; l < logn; l++)\n", - " up[v][l] = up[up[v][l-1]][l-1];\n", - " tin[v] = t++;\n", - " for (int u : g[v]) {\n", - " up[u][0] = v;\n", - " dfs(u);\n", - " }\n", - " tout[v] = t++;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Пусть поступил очередной запрос — пара вершин $(u, v)$:\n", - "* Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.\n", - "* Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.\n", - "\n", - "Подробнее про второй пункт. Пусть $L = \\lceil \\log n \\rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "cFYU1Y5qV1hs" - }, - "outputs": [], - "source": [ - "int lca (int v, int u) {\n", - " if (a(v, u)) return v;\n", - " if (a(u, v)) return u;\n", - " for (int l = logn-1; l >= 0; l--)\n", - " if (!ancestor(up[v][l], u))\n", - " v = up[v][l];\n", - " return up[v][0];\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Асимптотика\n", - "\n", - "Препроцессинг — $O(n \\log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу.\n", - "\n", - "Ответ на запрос — $O(\\log n)$, потому что по сути мы делаем один бинпоиск." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на путях\n", - "\n", - "Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).\n", - "\n", - "Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути.\n", - "\n", - "Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int get_min (int v, int u) {\n", - " int ans = inf;\n", - " for (int l = logn-1; l >= 0; l--)\n", - " if (!ancestor(up[v][l], u))\n", - " v = up[v][l], ans = min(ans, mn[v][l]);\n", - " for (int l = logn-1; l >= 0; l--)\n", - " if (!ancestor(up[u][l], v))\n", - " u = up[u][l], ans = min(ans, mn[u][l]);\n", - " return min({ans, mn[v][0], mn[u][0]})\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "PXpE5vHfV1hu" - }, - "source": [ - "## Сведение к RMQ\n", - "\n", - "Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Во втором массиве мы по сути выписали наш проход dfs-а.\n", - "\n", - "Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.\n", - "\n", - "Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "b7R7XMv4V1hy" - }, - "source": [ - "### Разреженная таблица\n", - "\n", - "На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \\log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.\n", - "\n", - "Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \\log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## А наоборот можно?*\n", - "\n", - "*Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.\n", - "\n", - "Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот.\n", - "Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.\n", - "\n", - "Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль.\n", - "\n", - "Возьмем константу $k = \\lfloor \\frac{\\log n}{2} \\rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\\frac{2 n}{\\log n})$, и построение будет работать за линейное время:\n", - "\n", - "$$O(\\frac{2 n}{\\log n} \\log \\frac{2 n}{\\log n}) = O(\\frac{2 n}{\\log n} (\\log 2n - \\log \\log n)) = O(n)$$\n", - "\n", - "Также посчитаем для каждой возможной маски размера $\\frac{\\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\\sqrt n$ (ради этого мы и делили логарифм на два).\n", - "\n", - "ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.\n", - "\n", - "Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.\n", - "\n", - "Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear).\n", - "\n", - "Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.\n", - "\n", - "Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.**" - ] - } - ], - "metadata": { - "colab": { - "name": "10_lca.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb b/web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb deleted file mode 100644 index f5b8c0b..0000000 --- a/web/img/.ipynb_checkpoints/linalg-checkpoint.ipynb +++ /dev/null @@ -1,453 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Ликбез по линейной алгебре\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Определение**. Функция $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ называется *линейной*, если для неё выполнено\n", - "\n", - "1. $ f(x+y) = f(x) + f(y) $\n", - "2. $ f(ax) = a f(x), \\; a \\in R $\n", - "\n", - "Примеры:\n", - "* $ f(x) = 0 $\n", - "* $ f(x) = x $\n", - "* $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\\mathbb{R}^3$ в $\\mathbb{R}$)\n", - "* $ f(x) = (x, -x, 0) $ (из $\\mathbb{R}$ в $\\mathbb{R}^3$)\n", - "\n", - "Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства:\n", - "* Сумма линейных функций — линейная функция.\n", - "* Сумма коммутативна: $f+g = g+f$).\n", - "* Сумма ассоциативна: $(f+g)+h = f+(g+h)$.\n", - "* Композиция $f(g(x)) = (f \\circ g)(x)$ линейных функций — линейная функция.\n", - "* Композция ассоциативна: $(f \\circ g) \\circ h = f \\circ (g \\circ h) = f \\circ g \\circ h$.\n", - "* Композиция в общем случае не коммутативна.
\n", - "Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен.\n", - "\n", - "Все свойства можно вывести лишь из этих двух пунктов в определении." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Что такое матрица?\n", - "\n", - "Можно показать, что любую линейную функцию $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ можно представить в таком виде:\n", - "\n", - "$$\n", - "f(x) =\n", - "\\begin{pmatrix}\n", - "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\\\\n", - "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\\\\n", - "\\ldots \\\\\n", - "a_{m1} x_1 + a_{m2} x_2 + \\ldots + a_{mn} x_n \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "*Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$.\n", - "\n", - "$$\n", - "A =\n", - "\\begin{pmatrix}\n", - "a_{11} & a_{12} & \\ldots & a_{1n} \\\\\n", - "a_{21} & a_{22} & \\ldots & a_{2n} \\\\\n", - "\\vdots & \\vdots & \\ddots & \\vdots \\\\\n", - "a_{m1} & a_{m2} & \\ldots & a_{mn} \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "Каждой линейной функции из $\\mathbb{R}^n$ в $\\mathbb{R}^m$ соответствует какая-то матрица размера $n \\times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте.\n", - "\n", - "Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \\circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так:\n", - "\n", - "$$ C = AB: C_{ij} = \\sum_{i=1}^{k} A_{ik} B_{kj} $$\n", - "\n", - "Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$.\n", - "\n", - "Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \\times k$, то вторая должна иметь размер $k \\times m$, то есть «средние» размерности обязательно должны совпадать.\n", - "\n", - "\n", - "\n", - "Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.\n", - "\n", - "Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:\n", - "\n", - "* Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$.\n", - "* Сумма коммутативна: $A+B = B+A$)\n", - "* Сумма ассоциативна: $(A+B)+C = A+(B+C)$\n", - "* Умножение ассоциативно: $(AB)C = A(BC) = ABC$.\n", - "* Умножение в общем случае не коммутативно." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Пример**: матрица поворота в 2d.\n", - "$$\n", - "\\begin{pmatrix}\n", - "\\cos \\alpha & -\\sin \\alpha \\\\\n", - "\\sin \\alpha & \\cos \\alpha \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "**Пример**: матрица проецирования на $Ox$ в 3d.\n", - "$$\n", - "\\begin{pmatrix}\n", - "1 & 0 & 0 \\\\\n", - "0 & 0 & 0 \\\\\n", - "0 & 0 & 0 \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "**Пример**: матрица «свапни $x$ и $y$».\n", - "$$\n", - "\\begin{pmatrix}\n", - "0 & 1 \\\\\n", - "1 & 0 \\\\\n", - "\\end{pmatrix}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Напишем класс, который реализует матричное умножение." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "struct matrix {\n", - " int n, m;\n", - " int t[];\n", - " matrix (int _n, int _m) {\n", - " n = _n, m = _m;\n", - " t = new int(n*m);\n", - " memset(t, 0, sizeof t);\n", - " }\n", - " int[] operator[] (int k) {\n", - " return t[k*m];\n", - " }\n", - "}\n", - "\n", - "matrix operator* (matrix a, matrix b) {\n", - " matrix c(a.n, b.m);\n", - " for (int i = 0; i < a.n; i++)\n", - " for (int j = 0; j < b.m; j++)\n", - " for (int k = 0; k < a.m; k++)\n", - " c[i][j] += a[i][k] * b[i][k];\n", - " return c;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Динамика\n", - "\n", - "Некоторые динамики можно выразить в терминах матричного умножения.\n", - "\n", - "Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\begin{pmatrix}\n", - "f_{n+1} \\\\\n", - "f_{n+2} \\\\\n", - "\\end{pmatrix}\n", - "=\n", - "\\begin{pmatrix}\n", - "0+f_{n+1} \\\\\n", - "f_{n}+f_{n+1} \\\\\n", - "\\end{pmatrix}\n", - "=\n", - "\\begin{pmatrix}\n", - "0 & 1 \\\\\n", - "1 & 1 \\\\\n", - "\\end{pmatrix}\n", - "\\begin{pmatrix}\n", - "f_{n} \\\\\n", - "f_{n+1} \\\\\n", - "\\end{pmatrix}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$.\n", - "\n", - "Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:\n", - "\n", - "$$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$\n", - "\n", - "Это будет работать за $O(n^3 \\log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "matrix binpow (matrix a, int p) {\n", - " matrix b(n, n);\n", - " for (int i = 0; i < n; i++)\n", - " b[i][i] = 1;\n", - " while (p) {\n", - " if (p&1) b = b*a;\n", - " a = a*a;\n", - " p >>= 1;\n", - " }\n", - " return b;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$.\n", - "\n", - "$$\n", - "\\begin{vmatrix}\n", - "1 & 0 & 0 \\\\\n", - "0 & 1 & 0 \\\\\n", - "0 & 0 & 1 \\\\\n", - "\\end{vmatrix}\n", - "$$\n", - "\n", - "В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \\ldots + a_k f_{n-k}$ имеет такую матрицу перехода:\n", - "\n", - "\\begin{pmatrix}\n", - "0 & 1 & 0 & \\ldots & 0 \\\\\n", - "0 & 0 & 1 & \\ldots & 0 \\\\\n", - "\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\\n", - "0 & 0 & 0 & \\ldots & 1 \\\\\n", - "a_k & a_{k-1} & a_{k-2} & \\ldots & a_1 \\\\\n", - "\\end{pmatrix}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Матрица смежности\n", - "\n", - "У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов.\n", - "\n", - "Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86))." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Обобщения\n", - "\n", - "Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел.\n", - "\n", - "Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Определитель\n", - "\n", - "*Определителем* кадратной матрицы $A$ называется такое выражение:\n", - "\n", - "$$ \\det A = |A| = \\ldots $$ \n", - "\n", - "Где $\\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:\n", - "\n", - "$$\n", - "\\begin{vmatrix}\n", - "a & b \\\\\n", - "c & d \\\\\n", - "\\end{vmatrix}\n", - "=\n", - "ad - bc\n", - "$$\n", - "\n", - "У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.\n", - "\n", - "В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Базис\n", - "\n", - "Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. \n", - "\n", - "Базисы есть не только в линейной алгебре. Например, $\\{1, x, x^2\\}$ является базисом всех квадратных трёхчленов. Или $\\{\\neg, \\land, \\lor\\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Метод Крамера и easy пересечение прямых\n", - "\n", - "Пусть нам надо пересечь две прямые.\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "a_1 x + b_1 y + c_1 = 0\n", - "a_2 x + b_2 y + c_2 = 0\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Это то же самое, что найти такие коэффициенты $x$ и $y$, что\n", - "\n", - "$$ x \\vec{a} + y \\vec{b} = -\\vec{c} $$\n", - "\n", - "Площадь параллелограмма, натянутого на $\\vec{a}$ и $\\vec{b}$, равна векторному произведению, или детерминанту.\n", - "\n", - "По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$.\n", - "\n", - "Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## *Собственные векторы\n", - "\n", - "Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление.\n", - "\n", - "$ Av = k v $, где $k \\neq 0$.\n", - "\n", - "$ Av - kv = (A-kI)v = 0 $. Это означает\n", - "\n", - "Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Системы уравнений и метод Гаусса\n", - "\n", - "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", - "\n", - "Нас по сути просят решить следующую систему:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", - "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", - "\\ldots \\\\\n", - "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", - "\n", - "Метод Крамера неоптимален — там $O(n^4)$ операций.\n", - "\n", - "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "t gauss (matrix a) {\n", - " for (int i = 0; i < n; i++) {\n", - " int nonzero = i;\n", - " for (int j = i+1; j < n; j++)\n", - " if (a[j][i])\n", - " nonzero = j;\n", - " swap(a[nonzero], a[i]);\n", - " for (int j = 0; j < n; j++)\n", - " if (j != i && a[j][i])\n", - " a[j] ^= a[i];\n", - " }\n", - " t x;\n", - " for (int i = 0; i < n; i++)\n", - " x[i] = a[i][n] ^ a[i][i];\n", - " return x;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81))." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/matching-checkpoint.ipynb b/web/img/.ipynb_checkpoints/matching-checkpoint.ipynb deleted file mode 100644 index e91f1a4..0000000 --- a/web/img/.ipynb_checkpoints/matching-checkpoint.ipynb +++ /dev/null @@ -1,234 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ipi0UHuYJPXr" - }, - "source": [ - "# Паросочетания\n", - "\n", - "Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "YEm8GM0TKncl" - }, - "source": [ - "**Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$).\n", - "\n", - "Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием.\n", - "\n", - "*Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны.\n", - "\n", - "Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ThCPzQPwU5y0" - }, - "source": [ - "**Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер.\n", - "\n", - "**Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.\n", - "\n", - "**Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию.\n", - "\n", - "![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg)\n", - "\n", - "*Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \\to 8 \\to 4 \\to 6 \\to 3 \\to 7$.*\n", - "\n", - "Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.\n", - "\n", - "В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.\n", - "\n", - "**Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "wrrJplYwbCB8" - }, - "outputs": [], - "source": [ - "const int maxn;\n", - "\n", - "vector g[maxn]; // будем хранить только рёбра из левой доли в правую\n", - "int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой)\n", - "bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом\n", - "\n", - "// dfs возвращает, можно ли найти путь из вершины v\n", - "// в какую-нибудь вершину правой доли\n", - "// если можно, то ещё и проводит чередование\n", - "bool dfs (int v) {\n", - " if (used[v])\n", - " return false;\n", - " used[v] = true;\n", - " for (int u : g[v]) {\n", - " // если вершина свободна, то можно сразу с ней соединиться\n", - " // если она занята, то с нейможно соединиться только тогда,\n", - " // когда из её текущей пары можно найти какую-нибудь другую вершину\n", - " if (mt[u] == -1 || dfs(mt[u])) {\n", - " mt[u] = v;\n", - " return true;\n", - " }\n", - " }\n", - " return false;\n", - "}\n", - "\n", - "\n", - "// где-то в main:\n", - "\n", - "memset(mt, -1, sizeof(mt));\n", - "for (int i = 0; i < n; i++) {\n", - " memset(used, 0, sizeof(mt));\n", - " if (dfs(i))\n", - " cnt++;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "fm3P_M7fKdRQ" - }, - "source": [ - "## Корректность\n", - "\n", - "Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.\n", - "\n", - "**Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным.\n", - "\n", - "**Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \\leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу.\n", - "\n", - "Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hVHj5qr1czGd" - }, - "source": [ - "## Скорость работы\n", - "\n", - "Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$.\n", - " \n", - "Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \\approx 10^4$), потому что для него есть мощные неасимптотические оптимизации:\n", - "\n", - "* Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).\n", - "\n", - "* Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу.\n", - "\n", - "* Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \\times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.\n", - " \n", - "Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \\sqrt m)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Покрытие путями DAG-а\n", - "\n", - "Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.\n", - "\n", - "Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\\overline{H}$ следующим образом:\n", - "* В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно.\n", - "* Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$.\n", - "* Теперь из графа $H$ сделаем граф $\\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$.\n", - "\n", - "Если мы рассмотрим любой путь $v_1, v_2, \\ldots, v_k$ в исходном графе $G$, то в графе $\\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \\ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$.\n", - "\n", - "Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\\overline{H}$ — это паросочетание в $H$ (напомним, это $\\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$.\n", - "\n", - "Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них.\n", - "\n", - "Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "B_YEyFpNUS36" - }, - "source": [ - "## Лемма Холла\n", - "\n", - "*Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.\n", - "\n", - "![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif)\n", - "\n", - "**Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.\n", - "\n", - "**Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием.\n", - "\n", - "В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.\n", - "\n", - "**База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. \n", - "\n", - "**Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием.\n", - "\n", - "Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)\n", - "\n", - "Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \\leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$).\n", - "\n", - "Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "yq3YRWxFJWaY" - }, - "source": [ - "## Для ноулайферов: матроиды\n", - "\n", - "С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid).\n", - "\n", - "Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*.\n", - "\n", - "**Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "21_matching.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb b/web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb deleted file mode 100644 index 43ba14e..0000000 --- a/web/img/.ipynb_checkpoints/matroid-checkpoint.ipynb +++ /dev/null @@ -1,178 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Матроиды" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства:\n", - "\n", - "* Пустое множество является независимым: $\\varnothing \\in I$\n", - "\n", - "* Любое подмножество независимого множества тоже независимо: \n", - "$$A \\subset B, B \\in I \\implies A \\in I$$\n", - "\n", - "* Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$:\n", - "$$A, B \\in I, |A| < |B| \\implies \\exists x \\in B \\setminus A: A \\cup \\{x\\} \\in I$$\n", - "\n", - "Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \\sum w(a_i)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "X.sort()\n", - "s = []\n", - "for x in X:\n", - " if good(s + [x]):\n", - " s += [x]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Здесь под `good` имеется в виду $s \\cup x \\in I$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Корректность этого алгоритма для любого матроида доказывает следующая теорема:\n", - "\n", - "## Теорема Радо-Эдмондса\n", - "\n", - "Пусть $A \\in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \\cup x \\in I,\\;x \\notin A,\\;w(x)$ — минимальна. Тогда $A \\cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "*Доказательство: **\n", - "\n", - "Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$.\n", - "\n", - "Из свойств матроида: $\\exists y \\in B \\setminus A : A \\cup y \\in I$.\n", - "\n", - "Тогда верны два неравенства:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "w(A \\cup y) = w(A) + w(y) \\geq w(B) \\implies w(A) \\geq w(B) - w(y) \\\\\n", - "w(B \\setminus y) = w(B) - w(y) \\geq w(A) \\implies w(A) \\leq w(B) - w(y)\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно,\n", - "$w(A \\cup y) = w(A) + w(y) = w(B)$.\n", - "\n", - "Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \\cup x \\in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Примеры\n", - "\n", - "Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.\n", - "\n", - "### Минимальный остов\n", - "Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом:\n", - "\n", - "* Граф без ребер является лесом.\n", - "* Если удалить из леса ребра, он останется лесом.\n", - "* Пусть есть два леса $|A| \\leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \\cup \\{x\\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности.\n", - "\n", - "Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.\n", - "\n", - "### Расписания\n", - "Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.\n", - "\n", - "Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \\geq i$ для всех $i$.\n", - "\n", - "Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: \n", - "\n", - "* Пустой набор заданий всегда можно сделать.\n", - "* Если у нас стало меньше заданий, то их сделать мы тоже успеем.\n", - "* Пусть есть два правильных набора $|A| \\leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \\cup x$ — тоже правильный набор.\n", - "\n", - "Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных.\n", - "\n", - "### Паросочетания\n", - "Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом:\n", - "\n", - "* Любое паросочетание покрывает пустое множество вершин.\n", - "* Исходное паросочетание покрывает также и любое подмножество исходных вершин.\n", - "* Пусть есть два множества вершин $|A| \\leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$.\n", - "\n", - "Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна.\n", - "\n", - "### Линейно независимые вектора\n", - "\n", - "(TODO) (Школьники не обязаны знать линал.)\n", - "\n", - "Такие штуки будем называть базисами.\n", - "\n", - "* Ноль есть в любом базисе.\n", - "* Подмножество базиса — базис.\n", - "* ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb b/web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb deleted file mode 100644 index b8487b7..0000000 --- a/web/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb +++ /dev/null @@ -1,239 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Поток минимальной стоимости\n", - "\n", - "Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна:\n", - "\n", - "$$ \\sum_{(u, v) \\in E} f_{uv} \\to \\max $$\n", - "$$ \\sum_{(u, v) \\in E} f_{uv} w_{uv} \\to \\min $$\n", - "\n", - "Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Критерий оптимальности\n", - "\n", - "Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот).\n", - "\n", - "**Доказательство:** \n", - "\n", - "$\\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению.\n", - "\n", - "$\\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Отмена циклов\n", - "\n", - "Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу.\n", - "\n", - "Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Дополняющие пути\n", - "\n", - "Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.\n", - "\n", - "**Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса.\n", - "\n", - "Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз.\n", - "\n", - "Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Потенциалы Джонсона\n", - "\n", - "Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:\n", - "\n", - "$$ w_{uv}' = w_{uv} + d_u - d_v $$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение 1**. Веса всех рёбер графа неотрицательные.\n", - "\n", - "**Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$?\n", - "\n", - "Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой:\n", - "$$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими.\n", - "\n", - "**Доказательство**. Распишем новую стоимость пути из $a$ в $z$.\n", - "\n", - "$$\n", - "\\begin{align}\n", - "w_{ab}' + \\ldots + w_{yz}'\n", - "&= (w_{ab} + \\ldots + w_{yz}) + (d_a + \\ldots + d_y) - (d_b + \\ldots + d_z)\n", - "\\\\&= (w_{ab} + \\ldots + w_{yz}) + d_a - d_z\n", - "\\end{align}\n", - "$$\n", - "\n", - "Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу.\n", - "\n", - "Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.\n", - "\n", - "Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими).\n", - "\n", - "**Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Итоговый алгоритм\n", - "\n", - "* Модифицируем сеть, добавивив обратные рёбра.\n", - "* Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.\n", - "* Пока максимальный поток не найден:\n", - "* * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$.\n", - "* * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \\leadsto t$, обновить остаточную сеть." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Асимптотика\n", - "\n", - "Алгоритм работает за $O(U m \\log n)$ или $O(U n^2)$ в случае плотных графов.\n", - "\n", - "В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \\leq n$ или $U \\leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Реализация\n", - "\n", - "Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).\n", - "\n", - "* `cost`, `cap` — параметры сети\n", - "* `pot` — потенциалы\n", - "* `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)\n", - "* `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "const int maxn = 305, inf = 1e9;\n", - "\n", - "int n;\n", - "int cost[maxn][maxn], cap[maxn][maxn];\n", - "int d[maxn], pot[maxn], par[maxn];\n", - "\n", - "bool dijkstra (int s, int t) {\n", - " used[maxn] = {0};\n", - "\n", - " fill(d, d+n, inf);\n", - " d[s] = 0;\n", - "\n", - " while (1) {\n", - " int v = -1;\n", - " for (int u = 0; u < n; u++)\n", - " if (!used[u] && (v == -1 && d[u] < d[v]))\n", - " v = u;\n", - " if (v == -1 || d[v] == inf)\n", - " break;\n", - " used[v] = 1;\n", - " for (int u = 0; u < n; u++) {\n", - " int w = cost[v][u] + pot[v] - pot[u];\n", - " if (cap[v][u] && d[u] > d[v] + w) {\n", - " d[u] = d[v] + w;\n", - " par[u] = v;\n", - " }\n", - " }\n", - " }\n", - "\n", - " return d[t] < inf;\n", - "}\n", - "\n", - "int mincost_maxflow (int s, int t) {\n", - " int ans = 0;\n", - " while (dijkstra(s, t)) {\n", - " memcpy(pot, d, sizeof(d));\n", - " int delta = inf;\n", - " for (int v = t; v != s; v = par[v])\n", - " delta = min(delta, cap[par[v]][v]);\n", - " for (int v = t; v != s; v = par[v]) {\n", - " cap[par[v]][v] -= delta;\n", - " cap[v][par[v]] += delta;\n", - " ans += cost[par[v]][v]*delta;\n", - " }\n", - " }\n", - " return ans;\n", - "}" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb b/web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb deleted file mode 100644 index a1dfb3b..0000000 --- a/web/img/.ipynb_checkpoints/segtree-checkpoint.ipynb +++ /dev/null @@ -1,680 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Nyg7f6SD69cK" - }, - "source": [ - "# Дерево отрезков" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "bYsinHWyrf6J" - }, - "source": [ - "**Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "LQzmaXb_G-r4" - }, - "source": [ - "**Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "UmP1mb6M69cM" - }, - "source": [ - "Рассмотрим конкретную задачу:\n", - "\n", - ">Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов:\n", - "\n", - ">1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`).\n", - ">2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$.\n", - "\n", - ">Оба запроса нужно обрабатывать за время $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "CG0jO6Nt69cN" - }, - "source": [ - "Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:\n", - "\n", - "Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.\n", - "\n", - "Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков»." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "f8HIyZkR69cO" - }, - "source": [ - "![alt text](http://i.imgur.com/GGBmcEP.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "084RAPPptpaJ" - }, - "source": [ - "Строить его можно рекурсивной функцией:\n", - "* Если вершина является листом, взять в качестве суммы значение соответствующей ячейки.\n", - "* Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "NNMuiaKW69cQ" - }, - "source": [ - "## Разные свойства\n", - "\n", - "Высота такого дерева есть величина $\\Theta(\\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики.\n", - "\n", - "Более того, любой полуинтервал разбивается на $O(\\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков.\n", - "\n", - "Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \\frac{n}{2} + \\frac{n}{4} + \\frac{n}{8} + \\ldots + 1 < 2n$. Значит, оно линейное по памяти.\n", - "\n", - "При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "NQDy4Wn_69cT" - }, - "source": [ - "## Ок, как это нам поможет?\n", - "\n", - "Опишем теперь, как с помощью такой структуры решить задачу.\n", - "\n", - "**Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$.\n", - "\n", - "Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\\Theta(\\log n)$ — по одной с каждого уровня.\n", - "\n", - "Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков.\n", - "\n", - "**Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения.\n", - "\n", - "Сделаем тоже рекурсивную функцию, рассмотрев три случая:\n", - "\n", - "* Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму.\n", - "* Если отрезки вершины и запроса не пересекаются, то вернуть 0.\n", - "* Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей.\n", - "\n", - "Чтобы разобраться, почему это работает за $O(\\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\\log n)$ отрезках, а значит и итоговая асимптотика будет такая же." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "fJ3qz5gzQYHO" - }, - "source": [ - "## Ликбез по C++\n", - "\n", - "Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества.\n", - "\n", - "Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "HPeG6w4DRHG4" - }, - "source": [ - "Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\\log n)$.\n", - "\n", - "В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hWKKu4mgQ4kA" - }, - "source": [ - "У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "CodmY2a4QmjN" - }, - "outputs": [], - "source": [ - "struct A {\n", - " int param1, param2; // тут можно что-то хранить\n", - " char param3 = 'k';\n", - " A (int var) {\n", - " // эта часть называется конструктором\n", - " // ...\n", - " }\n", - " void do_something () {\n", - " // это какой-то другой метод\n", - " // ...\n", - " }\n", - "}; // <- не забудьте точку с запятой" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "6xCBNd6TR1vD" - }, - "source": [ - "Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве.\n", - "\n", - "Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "dHxKkzeLsaLW" - }, - "outputs": [], - "source": [ - "A x(179);\n", - "x.do_something();\n", - "x.param1 = 57;\n", - "\n", - "A *y = new A(42); // new возвращает адрес, по которому можно найти объект\n", - "y->do_something();\n", - "y.param3 = '!';" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "6xy-KwmI2DiJ" - }, - "source": [ - "Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Z3VyDnQ769cU" - }, - "source": [ - "## Реализация" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "EKO6Hwdl69cV" - }, - "source": [ - "Общий план реализации любых структур данных:\n", - "1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc.\n", - "2. Формально описать, что должны делать методы и за какую асимптотику.\n", - "3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "x_z_73w-69cW" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb; // левые и правые границы отрезков\n", - " int sum = 0; // сумма на текущем отрезке\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " if (lb + 1 < rb) {\n", - " // если не лист, создаем детей\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void add (int k, int x) {\n", - " sum += x;\n", - " if (l) {\n", - " if (k < l->rb)\n", - " l->add(k, x);\n", - " else\n", - " r->add(k, x);\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " if (lb >= lq && rb <= rq)\n", - " // если мы лежим полностью в отрезке запроса, вывести сумму\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " // если мы не пересекаемся с отрезком запроса, вывести ноль\n", - " return 0;\n", - " // иначе всё сложно -- запускаемся от детей и пусть они там сами решают\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "30NQsY2jytu2" - }, - "source": [ - "> Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$).\n", - "\n", - "Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи:\n", - "* Запросим сумму от $k$ до $n$ в ДО.\n", - "* Добавим единичку в $k$-тую позицию в ДО.\n", - "\n", - "Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \\log n)$ запросов." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "S8DH5Zxg3UqU" - }, - "source": [ - "> Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике.\n", - "\n", - "Ответим на все запросы в оффлайн, используя метод сканирующей прямой:\n", - "\n", - "* Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \\times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \\times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \\times [y_1, y_2]$.\n", - "* Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше.\n", - "* Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$».\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "47Fb26oz69ca" - }, - "source": [ - "## Отложенные операции" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "tANMKBYT5CVZ" - }, - "source": [ - "Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному.\n", - "\n", - "Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем.\n", - "\n", - "Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено.\n", - "\n", - "Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.\n", - "\n", - "Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "eHe1O2d769cc" - }, - "source": [ - "При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "rJo8yvgHUPHL" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb;\n", - " int sum = 0, assign = -1;\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " if (lb + 1 < rb) {\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void push () {\n", - " if (assign != -1) {\n", - " sum = (rb-lb) * assign;\n", - " if (l) { // если дети есть\n", - " l->assign = assign;\n", - " r->assign = assign;\n", - " }\n", - " }\n", - " assign = -1;\n", - " }\n", - " void upd (int lq, int rq, int x) {\n", - " push();\n", - " if (lq <= lb && rb <= rq)\n", - " assign = x;\n", - " else if (l && max(lb, lq) < min(rb, rq)) {\n", - " // если есть дети и отрезок запроса хоть как-то пересекается с нашим\n", - " l->upd(lq, rq, x);\n", - " r->upd(lq, rq, x);\n", - " // ...дальше они сами разберутся\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " push();\n", - " if (lb >= lq && rb <= rq)\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " return 0;\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "_DmNvAVzMzq4" - }, - "source": [ - "По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах.\n", - "\n", - "Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "oGLSfyMG69cd" - }, - "source": [ - "## Динамическое построение" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "fZM4fTzu69cd" - }, - "source": [ - "А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\\log_2 10^6 \\approx 20$, $\\log_2 10^9 \\approx 30$), кроме этапа построения.\n", - "\n", - "Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.\n", - "\n", - "Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "z8PYwWA9niAY" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb;\n", - " int sum = 0;\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " // а тут ничего нет\n", - " }\n", - " void extend () {\n", - " if (!l && lb + 1 < rb) {\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void add (int k, int x) {\n", - " extend();\n", - " sum += x;\n", - " if (l) {\n", - " if (k < l->rb)\n", - " l->add(k, x);\n", - " else\n", - " r->add(k, x);\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " if (lb >= lq && rb <= rq)\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " return 0;\n", - " extend();\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "SSlRrVNXxREm" - }, - "source": [ - "Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "lGfAy5Af1S5J" - }, - "outputs": [], - "source": [ - "vector compress (vector a) {\n", - " vector b = a;\n", - " sort(b.begin(), b.end());\n", - " b.erase(unique(b.begin(), b.end()), b.end());\n", - " for (int &x : a) \n", - " x = int(lower_bound(b.begin(), b.end(), x) - b.begin());\n", - " return a;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "eRM_sfhirR_W" - }, - "source": [ - "## Персистентность\n", - "\n", - "Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния.\n", - "\n", - "Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.\n", - "\n", - "У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \\log n)$." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "OA93mqmdsO46" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb;\n", - " int sum = 0;\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " if (lb != rb) {\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void copy () {\n", - " if (l) {\n", - " l = new segtree(l);\n", - " r = new segtree(r);\n", - " }\n", - " }\n", - " void add (int k, int x) {\n", - " copy();\n", - " sum += x;\n", - " if (l) {\n", - " if (k < l->rb) l->add(k, x);\n", - " else r->add(k, x);\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " // этот метод ничего не меняет -- он и так хороший\n", - " if (lq <= lb && rb <= rq)\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " return 0;\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "kMg_NB4AsRMa" - }, - "source": [ - "> Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике.\n", - "\n", - "Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \\log n)$ памяти.\n", - "\n", - "> Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке.\n", - "\n", - "Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу.\n", - "\n", - "Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`.\n", - "\n", - "Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом.\n", - "\n", - "> Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз.\n", - "\n", - "У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\\frac{1}{2^{100}}$, и ей на практике можно пренебречь.\n", - "\n", - "Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "sUXS9Q1zQO1P" - }, - "source": [ - "## Другие реализации\n", - "\n", - "Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:\n", - "\n", - "**На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree).\n", - "\n", - "**«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "yCjbp1XhwsU2" - }, - "source": [ - "## Задачи\n", - "\n", - "* [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции.\n", - "* Второй контест — на отложенные операции.\n", - "* [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "1-2_segtree.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb b/web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb deleted file mode 100644 index d7e57b7..0000000 --- a/web/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb +++ /dev/null @@ -1,119 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Разреженная таблица\n", - "\n", - "- Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \\log n)$ с малой константой.\n", - "- Обновления не поддерживает (static RMQ).\n", - "- Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.\n", - "- Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).\n", - "- Требует $O(n \\log n)$ памяти.\n", - "- Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Определим разреженную таблицу как двумерный массив размера $n \\times\\log n$:\n", - "\n", - "$$\n", - "t[i][k] = \\min \\{ a_i, a_{i+1}, \\ldots, a_{i+2^k-1} \\}\n", - "$$\n", - "\n", - "Идея такая: считаем минимум на каждом отрезке длины $2^k$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Такой массив можно посчитать за его размер: $t[i][k] = \\min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int a[maxn], lg[maxn], mx[maxn][logn];\n", - "\n", - "int rmq (int l, int r) {\n", - " int t = lg[r-l+1];\n", - " return min(mx[l][t], mx[r-(1<= 0; i--) {\n", - " mx[i][0] = a[i];\n", - " for (int l = 0; l < logn-1; l++)\n", - " mx[i][l+1] = max(mx[i][l], mx[i+(1< Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него.\n", - "\n", - "Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \\cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго.\n", - "\n", - "Для решения этой задачи за линейное время придумали **префикс-функцию**.\n", - "\n", - "**Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \\ldots s_i$, который также является и суффиксом этой строки (не считая всю строку).\n", - "\n", - "Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 638, - "status": "ok", - "timestamp": 1541170751859, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "RUT-T92559lq", - "outputId": "b2a1115c-11ec-46c8-bc95-a62aa3458597" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[0, 0, 1, 0, 1, 2, 3, 4]" - ] - }, - "execution_count": 7, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - } - ], - "source": [ - "def slow_prefix_function(s):\n", - " n = len(s)\n", - " p = [0]*n\n", - " for i in range(n):\n", - " prefix = s[:i]\n", - " for l in range(1, i):\n", - " if prefix[:l] == prefix[-l:]:\n", - " p[i] = l\n", - " return p\n", - "\n", - "slow_prefix_function('aataataa')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "rm6kd4D86GG3" - }, - "source": [ - "(Этот алгоритм работает за $O(n^3)$, но это только пока.)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "1-8TSS2X3KV_" - }, - "source": [ - "## Как это поможет решить исходную задачу?\n", - "\n", - "Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.\n", - "\n", - "Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\\#t$." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 74 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 615, - "status": "ok", - "timestamp": 1541171240549, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "ppqZBPNJ43v3", - "outputId": "c23a3d9f-79aa-4f91-aff1-c7b574018535" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!\n", - "00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000\n" - ] - } - ], - "source": [ - "s = \"let it go\"\n", - "t = \"\"\"let it go, let it go\n", - "can't hold it back anymore\n", - "let it go, let it go\n", - "turn away and slam the door!\"\"\"\n", - "\n", - "print((s + '#' + t).replace('\\n', ' '))\n", - "print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "42poAv3b44FM" - }, - "source": [ - "Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$.\n", - "\n", - "Такой алгоритм (посчитать префикс-функцию от $s\\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "mTIyGVuD3F8W" - }, - "source": [ - "## Как её быстро считать\n", - "\n", - "Рассмотрим ещё несколько примеров префикс-функций:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 90 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 610, - "status": "ok", - "timestamp": 1541170871458, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "GqsPPTjl75Wp", - "outputId": "2b05fe9f-5eab-44bb-e651-c207c0723ae4" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0, 0, 1, 2, 3]\n", - "[0, 0, 0, 0, 0, 0]\n", - "[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]\n", - "[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n" - ] - } - ], - "source": [ - "for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:\n", - " print(slow_prefix_function(s))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "HO_PjgzR7514" - }, - "source": [ - "Можно заметить несколько особенностей:\n", - "* $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс;\n", - "* $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \\ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \\ldots s_i$, длина которого будет ровно на единицу меньше.\n", - "\n", - "Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения.\n", - "\n", - "Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\\underbrace{aabaa}t\\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$.\n", - "\n", - "Но что происходит, когда $s_{p_i}\\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$.\n", - "* $\\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5.\n", - "* $\\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa».\n", - "* $\\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$.\n", - "* $\\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$.\n", - "\n", - "В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \\neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 844, - "status": "ok", - "timestamp": 1541171880825, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "n22PhVaYUlp4", - "outputId": "830132ae-5568-4c3d-ae32-3ecaf51e7734" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]" - ] - }, - "execution_count": 20, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - } - ], - "source": [ - "def fast_prefix_function(s):\n", - " n = len(s)\n", - " p = [0]*n\n", - " for i in range(1, n):\n", - " cur = p[i - 1]\n", - " # перебираем префикс-функцию, пока не найдем равный символ\n", - " while s[i] != s[cur] and cur > 0:\n", - " cur = p[cur - 1]\n", - " # если нашли, то значение на единицу больше\n", - " if s[i] == s[cur]:\n", - " p[i] = cur + 1\n", - " return p\n", - "\n", - "fast_prefix_function('abacabadabacabax')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "skwRhzxQUlp9" - }, - "source": [ - "### Асимптотика\n", - "\n", - "Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой.\n", - "\n", - "**Доказательство**:\n", - "* Как мы ранее заметили, префикс функция возрастает максимум на единицу.\n", - "* $\\implies$ Вырасти она может максимум $n-1$ раз.\n", - "* Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один.\n", - "* $\\implies$ Количество понижений не больше количества повышений, то есть $O(n)$.\n", - "* $\\implies$ Суммарно итераций цикла `while` — $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "cDkYM1JMUlqC" - }, - "source": [ - "## Z-функция\n", - "\n", - "Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания.\n", - "\n", - "Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$.\n", - "\n", - "$$\\underbrace{aba}c\\overbrace{aba}daba \\hspace{1em} (z_4 = 3)$$" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]\n", - "[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]\n", - "[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]\n" - ] - } - ], - "source": [ - "def slow_z_function(s):\n", - " n = len(s)\n", - " z = [0]*n\n", - " for i in range(1, n):\n", - " suffix = s[i:]\n", - " for l in range(1, i):\n", - " if s[:l] == suffix[:l]:\n", - " z[i] = l\n", - " return z\n", - "\n", - "for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:\n", - " print(slow_z_function(s))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Как её быстро считать\n", - "\n", - "Заметим, что:\n", - "* $z_0 = 0$ — из-за договоренности (потому что информации не несет);\n", - "* $z_i = 0 \\iff s_i \\neq s_0$;\n", - "* $z_i > 0 \\iff s_i = s_0$.\n", - "\n", - "Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$.\n", - "\n", - "Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее.\n", - "* Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком.\n", - "* Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "def fast_z_function(s):\n", - " n = len(s)\n", - " z = [0]*n\n", - " l = 0\n", - " r = 0\n", - " for i in range(1, n):\n", - " if i <= r:\n", - " z[i] = min(r - i + 1, z[i-l])\n", - " while i + z[i] < n and s[z[i]] == s[i+z[i]]:\n", - " z[i] += 1\n", - " if i + z[i] - 1 > r:\n", - " l = i\n", - " r = i + z[i] - 1\n", - " return z" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Зачем тогда люди используют префикс-функцию\n", - "\n", - "![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg)\n", - "\n", - "В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.\n", - "\n", - "Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "8_kmp-trie.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.1" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb b/web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb deleted file mode 100644 index 16bf12b..0000000 --- a/web/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb +++ /dev/null @@ -1,158 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Суффиксный массив\n", - "\n", - "Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.\n", - "\n", - "Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.\n", - "\n", - "\n", - "\n", - "*
«Паблик с тупыми шутками про проганье»
*" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мотивация\n", - "\n", - "**Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки.\n", - "\n", - "\n", - "\n", - "**Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно.\n", - "\n", - "В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс.\n", - "\n", - "Работать такой алгоритм будет за $O(|t| \\log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \\log |s|)$, что является одним из самых оптимальных алгоритмов поиска.\n", - "\n", - "Теперь научимся его строить." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Построение за $O(n \\log n)$\n", - "\n", - "Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара.\n", - "\n", - "Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \\log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \\log^2 n)$. Но это не самый быстрый и удобный алгоритм.\n", - "\n", - "Наш алгоритм будет состоять из $\\lceil \\log n \\rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив.\n", - "\n", - "Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов).\n", - "\n", - "Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4.\n", - "\n", - "$$\n", - " p_0 = (0, 1, 3, 2) \\;\\;\\; c_0 = (0, 0, 1, 0)\n", - "\\\\ p_1 = (0, 3, 1, 2) \\;\\;\\; c_1 = (0, 1, 2, 0)\n", - "\\\\ p_2 = (3, 0, 1, 2) \\;\\;\\; c_2 = (1, 2, 3, 0)\n", - "$$\n", - "\n", - "Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом.\n", - "\n", - "Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором.\n", - "\n", - "Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \\log^2 n)$, потому что каждый этап будет работать за $O(n \\log n$).\n", - "\n", - "**Оптимизация до $O(n \\log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "// строка -- это последовательность чисел от 1 до размера алфавита\n", - "vector suffix_array (vector &s) {\n", - " s.push_back(0); // добавляем нулевой символ в конец строки\n", - " int n = (int) s.size(),\n", - " cnt = 0, // вспомогательная переменная: счётчик для сортировки \n", - " cls = 0; // количество классов эквивалентности\n", - " vector c(n), p(n);\n", - " \n", - " map< int, vector > t;\n", - " for (int i = 0; i < n; i++)\n", - " t[s[i]].push_back(i);\n", - " \n", - " // «нулевой» этап\n", - " for (auto &x : t) {\n", - " for (int u : x.second)\n", - " c[u] = cls, p[cnt++] = u;\n", - " cls++;\n", - " }\n", - " \n", - " // пока все суффиксы не стали уникальными\n", - " for (int l = 1; cls < n; l++) {\n", - " vector< vector > a(cls); // массив для сортировки подсчётом\n", - " vector _c(n); // новые классы эквивалентности\n", - " int d = (1<(p.begin()+1, p.end());\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: переписать это" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наибольшие общие префиксы\n", - "\n", - "Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами.\n", - "\n", - "TODO" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/web/img/.ipynb_checkpoints/treap-checkpoint.ipynb b/web/img/.ipynb_checkpoints/treap-checkpoint.ipynb deleted file mode 100644 index abe71e6..0000000 --- a/web/img/.ipynb_checkpoints/treap-checkpoint.ipynb +++ /dev/null @@ -1,956 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "dMVa59dX96P7" - }, - "source": [ - "# Декартово дерево" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ya9SKeu196QG" - }, - "source": [ - "Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века.\n", - "\n", - "Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.\n", - "\n", - "Декартово дерево же определяется и строится так:\n", - "\n", - "* Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*.\n", - "* Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*.\n", - "* От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.\n", - "* Аналогично, запустимся от правой части и добавим корню правого сына.\n", - "\n", - "Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно.\n", - "\n", - "Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "eRd8qABz96QP" - }, - "source": [ - "![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "DLS2nKEPK1lC" - }, - "source": [ - "Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий:\n", - "\n", - "* Дерамида (дерево + пирамида)\n", - "* ПиВо (пирамида + дерево)\n", - "* КуРево (куча + дерево)\n", - "* Treap (tree + heap)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "wSD-3iTFK7s8" - }, - "source": [ - " ## Бинарные деревья\n", - " \n", - " С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например:\n", - "\n", - "* добавить число $x$ в множество\n", - "* определить, есть ли в множестве число $x$\n", - "* найти первое число, не меньшее $x$ (`lower_bound`)\n", - "* найти количество чисел в промежутке $[l, r]$\n", - "\n", - "При этом все операции — за $O(\\log n)$.\n", - "\n", - "На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "2zy0SLY296QW" - }, - "source": [ - "## Приоритеты и асимптотика\n", - "\n", - "В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Ox2hsVjW96Qg" - }, - "source": [ - "**Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \\log n)$.\n", - "\n", - "Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.\n", - "\n", - "**Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*.\n", - "\n", - "Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна\n", - "\n", - "$$d_i = \\sum_{j=1}^n a(j, i)$$\n", - "\n", - "Её матожидание равно\n", - "\n", - "$$E[d_i] = E[\\sum_{j \\neq i} a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} p(j, i)$$\n", - "\n", - "где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.\n", - "\n", - "Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.\n", - "\n", - "**Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$).\n", - "\n", - "**Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. \n", - "\n", - "**Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет.\n", - "\n", - "У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей:\n", - "\n", - "$$p(x, y) = \\frac{1}{y-x+1}$$\n", - "\n", - "Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:\n", - "\n", - "$$E[d_i] = \\sum_{j \\neq i} p(j, i) = \\sum_{j \\neq i} \\frac{1}{|i-j|+1} \\leq \\sum_{i=1}^n \\frac{1}{n} = O(\\log n)$$\n", - "\n", - "Перед последним переходом мы получили сумму гармонического ряда.\n", - "\n", - "Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя.\n", - "\n", - "**Упражнение***. Выведите из этого доказательства асимптотику `quicksort`." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "qmboul4Bfkpc" - }, - "source": [ - "## Реализация" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "G0lF7L2E96Qj" - }, - "source": [ - "Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "alEQhUGK46qI" - }, - "source": [ - "Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "11VDG7yx96Qm" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " int key, prior;\n", - " Node *l = 0, *r = 0;\n", - " Node (int _key) { key = _key, prior = rand(); }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "kTPpsriu96Qx" - }, - "source": [ - "Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Vnu5kpTE96Q0" - }, - "source": [ - "### Merge\n", - "\n", - "Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.\n", - "\n", - "Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше.\n", - "\n", - "Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "1pdbdJ_W96Q2" - }, - "outputs": [], - "source": [ - "Node* merge (Node *l, Node *r) {\n", - " if (!l) return r;\n", - " if (!r) return l;\n", - " if (l->prior > r->prior) {\n", - " l->r = merge(l->r, r);\n", - " return l;\n", - " }\n", - " else {\n", - " r->l = merge(l, r->l);\n", - " return r;\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "byktXFHw96RA" - }, - "source": [ - "### Split\n", - "\n", - "Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$.\n", - "\n", - "В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "jyUv7F-o96RD" - }, - "outputs": [], - "source": [ - "typedef pair Pair;\n", - "\n", - "Pair split (Node *p, int x) {\n", - " if (!p) return {0, 0};\n", - " if (p->key <= x) {\n", - " Pair q = split(p->r, x);\n", - " p->r = q.first;\n", - " return {p, q.second};\n", - " }\n", - " else {\n", - " Pair q = split(p->l, x);\n", - " p->l = q.second;\n", - " return {q.first, p};\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "pOVCO70W96RN" - }, - "source": [ - "### Пример: вставка\n", - "\n", - "`merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. \n", - "\n", - "Вот так, например, будет выглядеть код, добавляющий $x$ в сет." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "wpMr1dwA96RQ" - }, - "outputs": [], - "source": [ - "Node *root = 0;\n", - "\n", - "void insert (int x) {\n", - " Pair q = split(root, x);\n", - " Node *t = new Node(x);\n", - " root = merge(q.first, merge(t, q.second));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "VhOc9WdYjOC0" - }, - "source": [ - "### Пример: модификация для суммы на отрезке\n", - "\n", - "Иногда нам нужно написать какие-то модификации для более продвинутых операций.\n", - "\n", - "Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке»." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "j04PUXvmun3y" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " int val, sum;\n", - " // ...\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "nIYZ3qnxvKiV" - }, - "source": [ - "При `merge` и `split` надо будет поддерживать эту сумму актуальной.\n", - "\n", - "Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "BH_-Bjc4utNt" - }, - "outputs": [], - "source": [ - "void sum (Node* v) { return v ? v->sum : 0; }\n", - "// обращаться по пустому указателю нельзя -- выдаст ошибку\n", - "\n", - "void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; }" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "iduA9cMYjyLI" - }, - "source": [ - "В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "SZod8er7wPVL" - }, - "outputs": [], - "source": [ - "Node* merge (Node *l, Node *r) {\n", - " // ...\n", - " if (...) {\n", - " l->r = merge(l->r, r);\n", - " upd(l);\n", - " return l;\n", - " }\n", - " else {\n", - " // ...\n", - " }\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "5RKCFfiWwZZc" - }, - "outputs": [], - "source": [ - "typedef pair Pair;\n", - "\n", - "Pair split (Node *p, int x) {\n", - " // ...\n", - " if (...) {\n", - " // ...\n", - " upd(p);\n", - " return {p, q.second};\n", - " }\n", - " else {\n", - " // ...\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "gRsq8Pqbvsbb" - }, - "source": [ - " Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Gox_llXkdJct" - }, - "outputs": [], - "source": [ - "int sum (int l, int r) {\n", - " Pair rq = split(root, r);\n", - " Pair lq = split(rq.first, l);\n", - " int res = sum(lr.second);\n", - " root = merge(lq.first, merge(lq.second, rq.second));\n", - " return res;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "CSPnJjIe96Rc" - }, - "source": [ - "# Неявный ключ" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "MwlB7zcTOqr-" - }, - "source": [ - "Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.\n", - "\n", - "Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.\n", - "\n", - "Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "uL8WtJztiOHy" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " int key, prior, size = 1;\n", - " // ^ размер поддерева\n", - " Node *l = 0, *r = 0;\n", - " Node (int _key) { key = _key, prior = rand(); }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "sl4BYIseiRoq" - }, - "source": [ - "Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "c9oX5sP_kHhx" - }, - "outputs": [], - "source": [ - "int size (Node *v) { return v ? v->size : 0; }\n", - "\n", - "void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); }" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "n8r-PdHdkIRm" - }, - "source": [ - "`merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа.\n", - "\n", - "Про `split` теперь удобнее думать как \"вырежи первые `k` элементов\". " - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "G2ck_VLK96Rk" - }, - "outputs": [], - "source": [ - "typedef pair Pair;\n", - "\n", - "Pair split (Node *p, int k) {\n", - " if (!p) return {0, 0};\n", - " if (size(p->l) + 1 <= k) {\n", - " Pair q = split(p->r, k - size(p->l) - 1);\n", - " // ^ правый сын не знает количество вершин слева от него\n", - " p->r = q.first;\n", - " upd(p);\n", - " return {p, q.second};\n", - " }\n", - " else {\n", - " Pair q = split(p->l, k);\n", - " p->l = q.second;\n", - " upd(p);\n", - " return {q.first, p};\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "na66j6wGkeW1" - }, - "source": [ - "Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "PqDn4on8gx4u" - }, - "source": [ - "### Пример: ctrl+x, ctrl+v" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "tSiJl_dEjE-1" - }, - "outputs": [], - "source": [ - "Node* ctrlx (int l, int r) {\n", - " Pair q1 = split(root, r);\n", - " Pair q2 = split(q1.first, l);\n", - " root = merge(q2.first, q1.second);\n", - " return q2.second;\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "7VUUuQhpD3r0" - }, - "outputs": [], - "source": [ - "void ctrlv (Node *v, int k) {\n", - " Pair q = split(root, k);\n", - " root = merge(q.first, merge(v, q.second));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hTmR8xpkgp3a" - }, - "source": [ - "### Пример: переворот\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "d-lA26yQZVUZ" - }, - "source": [ - "Нужно за $O(\\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д.\n", - "\n", - "Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "q7ic3tQ4jWco" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " bool rev;\n", - " // ...\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Bg-tJvl-jfql" - }, - "source": [ - "Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Q7nC0YI0jeTi" - }, - "outputs": [], - "source": [ - "void push (node *v) {\n", - " if (v->rev) {\n", - " swap(v->l, v->r);\n", - " if (v->l)\n", - " v->rev ^= 1;\n", - " if (v->r)\n", - " v->rev ^= 1;\n", - " }\n", - " v->rev = 0;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "40Ir36SwZ5GN" - }, - "source": [ - "Аналогично, эту функцию будем вызывать в начале `merge` и `split`.\n", - "\n", - "Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "8Ev2xhTSjRgI" - }, - "outputs": [], - "source": [ - "void reverse (int l, int r) {\n", - " Pair q1 = split(root, r);\n", - " Pair q2 = split(q1.first, l)\n", - " q2.second->rev ^= 1;\n", - " root = merge(q2.first, merge(q2.second, q1.second));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "9BI3binGkCmg" - }, - "source": [ - "# Функциональное программирование*\n", - "\n", - "Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно.\n", - "\n", - "Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "dIXpRozXkFVh" - }, - "outputs": [], - "source": [ - "auto apply (int l, int r, auto f) {\n", - " Pair q1 = split(root, r);\n", - " Pair q2 = split(q1.first, l)\n", - " q2.second = f(q2.second);\n", - " root = merge(q2.first, merge(q2.second, q1.second));\n", - "}\n", - "\n", - "void reverse (Node *v) {\n", - " if (v)\n", - " v->rev ^= 1;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "C8eKhyK5fnDK" - }, - "source": [ - "Применять её нужно так:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "GbQ-OEOWEQxM" - }, - "outputs": [], - "source": [ - "apply(l, r, reverse);" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "P8_dHs3BHBKo" - }, - "source": [ - "Это работает в плюсах, начиная с `g++14`.\n", - "\n", - "Для простых операций можно даже написать лямбду:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "nPmyzNjKHpNc" - }, - "outputs": [], - "source": [ - "apply(l, r, [](Node *v){\n", - " if (v)\n", - " v->rev ^= 1;\n", - "});" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ZV7EMe68kpvi" - }, - "source": [ - "# Персистентность*\n", - "\n", - "Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.\n", - "\n", - "> Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.\n", - "\n", - "Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим.\n", - "\n", - "> Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**.\n", - "\n", - "Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами.\n", - "\n", - "Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "1sqJhTKQnkWY" - }, - "source": [ - "Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "vrd0UjRu1qDv" - }, - "outputs": [], - "source": [ - "Node* copy (Node *v) { return new Node(*v); }" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "L6METC4WKGbA" - }, - "source": [ - "Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "LuFo9MChKF7b" - }, - "outputs": [], - "source": [ - "Pair split (Node *p, int x) {\n", - " p = copy(p);\n", - " // ...\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "NnMzBYiY1zD7" - }, - "source": [ - "В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.\n", - "\n", - "У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\\frac{L}{L+R}$, иначе за правое.\n", - "\n", - "**Теорема**. Такое переподвешивание эквивалентно приоритетам.\n", - "\n", - "**Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции:\n", - "\n", - "* Лист имеет вероятность 1 быть корнем себя (база индукции)\n", - "* Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\\frac{1}{L} \\cdot \\frac{L}{L+R} = \\frac{1}{L+R}$. С вершинами правого дерева аналогично.\n", - "\n", - "Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "EamOdCWWKM_y" - }, - "outputs": [], - "source": [ - "Node* merge (Node *l, Node *r) {\n", - " if (!l) return r;\n", - " if (!r) return l;\n", - " l = copy(l), r = copy(r);\n", - " if (rand() % (size(l) + size(r)) < size(l)) {\n", - " // ...\n", - " }\n", - " else {\n", - " // ...\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "YcdzH43FyPDl" - }, - "source": [ - "Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$?" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "3-4_treap.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/web/img/.ipynb_checkpoints/trie-checkpoint.ipynb b/web/img/.ipynb_checkpoints/trie-checkpoint.ipynb deleted file mode 100644 index 4162c8e..0000000 --- a/web/img/.ipynb_checkpoints/trie-checkpoint.ipynb +++ /dev/null @@ -1,129 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Бор\n", - "\n", - "Бор — это структура данных для компактного хранения строк.\n", - "\n", - "Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. \n", - "\n", - "![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png)\n", - "\n", - "Бор можно удобно использовать для разных задач:\n", - "* Хранение строк — занимает гораздо меньше места, чем массив или сет строк.\n", - "* Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.\n", - "\n", - "* Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Реализация\n", - "\n", - "Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация:\n", - "* терминальная ли вершина,\n", - "* ссылки на детей,\n", - "* возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "const int k = 26;\n", - "\n", - "struct Vertex {\n", - " Vertex* to[k] = {0};\n", - " bool terminal = 0;\n", - "};\n", - "\n", - "Vertex *root = new Vertex();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "void add_string (string &s) {\n", - " v = root;\n", - " for (char c : s) {\n", - " c -= 'a';\n", - " if (!v->to[c]) \n", - " v->to[c] = new Vertex();\n", - " v = v->to[c];\n", - " }\n", - " v->terminal = true;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.\n", - "\n", - "Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Как хранить ссылки\n", - "\n", - "Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми.\n", - "\n", - "В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например.\n", - "\n", - "Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Суффиксные ссылки\n", - "\n", - "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", - "\n", - "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", - "\n", - "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", - "\n", - "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 0db25103a3fa61ee316c5d8c5af691a4def36e01 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 5 Jun 2019 10:45:08 +0000 Subject: [PATCH 005/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- img/.editorial.html.swp | Bin 12288 -> 0 bytes .../aho-corasick-checkpoint.ipynb | 146 --- .../bayans-checkpoint.ipynb | 533 ---------- .../bitset-checkpoint.ipynb | 241 ----- .../centroid-checkpoint.ipynb | 249 ----- img/.ipynb_checkpoints/cuda-checkpoint.ipynb | 475 --------- .../dp-intro-checkpoint.ipynb | 109 -- .../dp-optimizations-checkpoint.ipynb | 288 ------ .../fenwick-checkpoint.ipynb | 285 ------ img/.ipynb_checkpoints/games-checkpoint.ipynb | 331 ------ .../geometry-checkpoint.ipynb | 421 -------- .../hashing-checkpoint.ipynb | 426 -------- img/.ipynb_checkpoints/hld-checkpoint.ipynb | 186 ---- img/.ipynb_checkpoints/index-checkpoint.ipynb | 81 -- .../karatsuba-checkpoint.ipynb | 122 --- img/.ipynb_checkpoints/lca-checkpoint.ipynb | 339 ------- .../linalg-checkpoint.ipynb | 453 --------- .../matching-checkpoint.ipynb | 234 ----- .../matroid-checkpoint.ipynb | 178 ---- .../mincost-maxflow-checkpoint.ipynb | 239 ----- .../segtree-checkpoint.ipynb | 680 ------------- .../sparse-table-checkpoint.ipynb | 119 --- img/.ipynb_checkpoints/sse-checkpoint.ipynb | 404 -------- .../strings-checkpoint.ipynb | 448 -------- .../suffix-array-checkpoint.ipynb | 158 --- img/.ipynb_checkpoints/treap-checkpoint.ipynb | 956 ------------------ img/.ipynb_checkpoints/trie-checkpoint.ipynb | 129 --- 27 files changed, 8230 deletions(-) delete mode 100644 img/.editorial.html.swp delete mode 100644 img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/bayans-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/bitset-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/centroid-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/cuda-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/fenwick-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/games-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/geometry-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/hashing-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/hld-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/index-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/lca-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/linalg-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/matching-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/matroid-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/segtree-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/sse-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/strings-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/treap-checkpoint.ipynb delete mode 100644 img/.ipynb_checkpoints/trie-checkpoint.ipynb diff --git a/img/.editorial.html.swp b/img/.editorial.html.swp deleted file mode 100644 index eed31bfbef5ecb81e88ef7890edd52f724b5de2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI%ze@u#6bJBE2RBi4a1!Dvbn4~&F^GCjT?7RY>g1B9&tA&?sJW*F2ldbJPwAiM zUG;_zb}0yjZyDC|ZDxC?rLI45~fB*y_009U< z00Izz00bZ~FJJ~lUz&EO42z z7?p^OJN*3OUgN;?ljkxClr3`MT;d?QP`&$`xLwaW*E;7(XTtr`35T}G+JVwn^bO^g BJ0$=B diff --git a/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb b/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb deleted file mode 100644 index 3679909..0000000 --- a/img/.ipynb_checkpoints/aho-corasick-checkpoint.ipynb +++ /dev/null @@ -1,146 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Алгоритм Ахо-Корасик\n", - "\n", - "Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время.\n", - "\n", - "Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик.\n", - "\n", - "\n", - "\n", - "Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы.\n", - "\n", - "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", - "\n", - "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", - "\n", - "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", - "\n", - "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Зачем это нужно\n", - "\n", - "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", - "\n", - "Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор.\n", - "\n", - "Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Алгоритм Ахо-Корасик*\n", - "\n", - "Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой.\n", - "\n", - "**Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход.\n", - "\n", - "**Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда.\n", - "\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "const int k = 26;\n", - "\n", - "struct Vertex {\n", - " Vertex *to[k] = {0}, *go[k] = {0};\n", - " Vertex *link = 0, *p;\n", - " int pch;\n", - " Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; }\n", - "};\n", - "\n", - "Vertex *root = new Vertex(-1, 0);" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "void add_string (string s) {\n", - " Vertex *v = root;\n", - " for (char _c : s) {\n", - " c -= 'a';\n", - " if (!v->to[c])\n", - " v->to[c] = new Vertex(c, v);\n", - " v = v->to[c];\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "Vertex* go (Vertex *v, int c);\n", - "\n", - "Vertex* link (Vertex *v) {\n", - " if (!v->link) {\n", - " if (v == root || v->p == root) v->link = root;\n", - " else v->link = go(link(v->p), v->pch);\n", - " }\n", - " return v->link;\n", - "}\n", - "\n", - "Vertex* go (Vertex *v, int c) {\n", - " if (!v->go[c]) {\n", - " if (v->to[c]) v->go[c] = v->to[c];\n", - " else if (v == root) v->go[c] = root;\n", - " else v->go[c] = go(link(v), c);\n", - " }\n", - " return v->go[c];\n", - "}" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/bayans-checkpoint.ipynb b/img/.ipynb_checkpoints/bayans-checkpoint.ipynb deleted file mode 100644 index 87cb744..0000000 --- a/img/.ipynb_checkpoints/bayans-checkpoint.ipynb +++ /dev/null @@ -1,533 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Красивые идейные задачи\n", - "\n", - "Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда.\n", - "\n", - "Задачи идут в порядке вспоминания, то есть в весьма рандомном." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Попугаи\n", - "\n", - "Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Минимум и максимум\n", - "\n", - "Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Перестановка\n", - "\n", - "Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Выпуклая оболочка\n", - "Требуется отвечать на 2 типа запросов:\n", - "\n", - "1. Добавить точку в выпуклую оболочку.\n", - "2. Проверить, лежит ли точка внутри выпуклой оболочки.\n", - "\n", - "Обе операции онлайн за $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Геометрическая прогрессия\n", - "\n", - "Найдите способ посчитать $\\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Покемоны\n", - "\n", - "В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \\times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \\to b$ и $b \\to c$ не следует, что $a \\to c$.\n", - "\n", - "У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Сортировка\n", - "Можно ли отсортировать\n", - "* 5 камней за 8 взвешиваний?\n", - "* 5 камней за 7 взвешиваний?\n", - "* 20 камней за 60 взвешиваний?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Точки в круге\n", - "\n", - "Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Замкнутые ломаные\n", - "\n", - "Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Неубывающий массив\n", - "\n", - "Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Чётный цикл\n", - "\n", - "Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## $k$-ая порядковая статистика\n", - "\n", - "Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Доминирующий элемент\n", - "\n", - "Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз. Время работы $O((n+m) \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Разрушение дерева\n", - "\n", - "Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## $k$-ый элемент на отрезке\n", - "\n", - "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Различные числа на отрезке\n", - "\n", - "Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\\sqrt{n})$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Физкультура\n", - "\n", - "Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов:\n", - "\n", - "1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя)\n", - "2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно)\n", - "\n", - "При этом приказ может частично отменить действие предыдущих приказов.\n", - "\n", - "После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Нулевая сумма\n", - "\n", - "Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мета-задача\n", - "\n", - "В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ниточка\n", - "\n", - "В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Пельмени\n", - "\n", - "Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## От нуля до единицы\n", - "\n", - "Дан следующий код:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "x = 0\n", - "while x < 1:\n", - " x += random()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Требуется посчитать матожидание `x`.\n", - "\n", - "(`random` в питоне возвращает случайное действительное число от 0 до 1.)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Площадь\n", - "\n", - "Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Окружности\n", - "\n", - "Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна\n", - "\n", - "* соприкасаться с *внешней*,\n", - "* соприкасаться с предыдущей (($k-1$)-ой),\n", - "* иметь при этом максимальный радиус.\n", - "\n", - "Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Блеф\n", - "\n", - "Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может:\n", - "\n", - "* Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается.\n", - "* Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом.\n", - "\n", - "С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Достижимость\n", - "Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Нумизмат\n", - "\n", - "Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой.\n", - "\n", - "Придумайте любой полиномиальный алгоритм." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Принцесса\n", - "\n", - "В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве.\n", - "\n", - "Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством.\n", - "\n", - "У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми.\n", - "\n", - "Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу.\n", - "\n", - "Асимптотика $O(n^2)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Спираль\n", - "\n", - "Определим спираль $(2n+1) \\times (2n+1)$ как матрицу следующего вида:\n", - "\n", - "$$\n", - "\\begin{matrix}\n", - "21 & 22 & 23 & 24 & 25 \\\\\n", - "20 & 7 & 8 & 9 & 10 \\\\\n", - "19 & 6 & 1 & 2 & 11 \\\\\n", - "18 & 5 & 4 & 3 & 12 \\\\\n", - "17 & 16 & 15 & 14 & 13 \\\\\n", - "\\end{matrix}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$).\n", - "\n", - "$q \\leq 100$, $n \\leq 10^9$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Польский лабиринт\n", - "\n", - "Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`.\n", - "\n", - "![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png)\n", - "\n", - "Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам.\n", - "\n", - "$n \\leq 10^5$, изначальные координаты туристов до $10^9$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Нимные подмножества\n", - "\n", - "Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \\subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \\subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Баланс степеней\n", - "\n", - "Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Два пути\n", - "\n", - "Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Пьяница\n", - "\n", - "Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ксоровый рюкзак\n", - "\n", - "Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Иван Сусанин\n", - "\n", - "Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш.\n", - "\n", - "- Камиль руководит армией днём и водит армию по *дорогам*.\n", - "- Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. \n", - "\n", - "Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск.\n", - "\n", - "Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу.\n", - "Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось.\n", - "\n", - "Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Варенье\n", - "\n", - "В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм.\n", - "\n", - "Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья.\n", - "\n", - "Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной.\n", - "\n", - "$n, m \\leq 10^5$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Лабиринт\n", - "\n", - "Серёжа потерялся в лабиринте $n \\times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт.\n", - "\n", - "Придумайте любой полиномиальный алгоритм." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Обезьяна\n", - "\n", - "Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ожидание минимума\n", - "\n", - "Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин.\n", - "\n", - "Придумайте любой точный полиномиальный алгоритм." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Шумный ксор\n", - "\n", - "Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \\frac15$ быть единичным, то есть каждый бит $x \\oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x = # ...\n", - "\n", - "def mask(p=0.2):\n", - " r = 0\n", - " for i in range(32):\n", - " if random.random() < p:\n", - " r += 2**i\n", - " return r\n", - "\n", - "def query(y):\n", - " return bin(x ^ y ^ mask()).count('1')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ваша задача — отгадать число, используя не более 10000 попыток." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/bitset-checkpoint.ipynb b/img/.ipynb_checkpoints/bitset-checkpoint.ipynb deleted file mode 100644 index 30b7828..0000000 --- a/img/.ipynb_checkpoints/bitset-checkpoint.ipynb +++ /dev/null @@ -1,241 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Битовое сжатие\n", - "\n", - "* Из-за него в «асимптотиках» появляется `/64`\n", - "* На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов\n", - "* `bitset` есть в stl; говорят, самописный быстрее" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости.\n", - "\n", - "Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. \n", - "\n", - "Работать с ним нужно вот так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "const int lim = 1000;\n", - "bitset b; // создать битсет размера lim (должно быть константой)\n", - "b.set(); // заполнить единицами\n", - "b.reset(); // заполнить нулями\n", - "b.flip(); // заменить единички на нули и наоборот\n", - "b.count(); // посчитать число единичек\n", - "cout << b; // вывести битовую строку" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Рюкзак\n", - "\n", - "Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака.\n", - "\n", - "Обычно его решают так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "bool dp[lim] = {}; // так можно его заполнить нулями\n", - "dp[0] = 1;\n", - "for (int i = 0; i < n; i++)\n", - " for (int x = lim - a[i]; x >= 0; x--)\n", - " dp[x + a[i]] |= dp[x];" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "…а с битсетом оно разгоняется так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "bitset b;\n", - "b[0] = 1;\n", - "for (int i = 0; i < n; i++)\n", - " b |= b << a[i];" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Цикл длины 3\n", - "\n", - "Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\\frac{n^3}{64})$ следующим образом:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "bitset g[maxn]; // матрица смежности\n", - "for (int a = 0; a < n; a++) {\n", - " for (int b = 0; b < n; b++) {\n", - " if (g[a][b] && (~g[a] & g[b]).any()) {\n", - " // цикл найден\n", - " }\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Перемножение матриц\n", - "\n", - "Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "typedef bitset t;\n", - "typedef array matrix;\n", - "\n", - "matrix operator* (matrix a, matrix b) {\n", - " matrix c;\n", - " for(int i = 0; i < n; i++)\n", - " for(int j = 0; j < n; j++)\n", - " if(a[i][j])\n", - " c[i] |= b[j];\n", - " return c;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Гаусс\n", - "\n", - "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", - "\n", - "Нас по сути просят решить следующую систему:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", - "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", - "\\ldots \\\\\n", - "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", - "\n", - "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "t gauss (matrix a) {\n", - " for (int i = 0; i < n; i++) {\n", - " int nonzero = i;\n", - " for (int j = i+1; j < n; j++)\n", - " if (a[j][i])\n", - " nonzero = j;\n", - " swap(a[nonzero], a[i]);\n", - " for (int j = 0; j < n; j++)\n", - " if (j != i && a[j][i])\n", - " a[j] ^= a[i];\n", - " }\n", - " t x;\n", - " for (int i = 0; i < n; i++)\n", - " x[i] = a[i][n] ^ a[i][i];\n", - " return x;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/centroid-checkpoint.ipynb b/img/.ipynb_checkpoints/centroid-checkpoint.ipynb deleted file mode 100644 index 37746bb..0000000 --- a/img/.ipynb_checkpoints/centroid-checkpoint.ipynb +++ /dev/null @@ -1,249 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Центроидная декомпозиция\n", - "\n", - "Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».\n", - "\n", - "Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще).\n", - "\n", - "![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\\frac{n}{2}$.\n", - "\n", - "Выясняется, что центр всегда существует — это следует из алгоритма его поиска:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int s[maxn];\n", - "\n", - "int sizes (int v) {\n", - " s[v] = 1;\n", - " for (int u : g[v])\n", - " // для простоты считаем, что дерево корневое\n", - " s[v] += sizes(u);\n", - " return s[v];\n", - "}\n", - "\n", - "// второй параметр -- размер дерева\n", - "int centroid (int v, int n) {\n", - " for (int u : g[v])\n", - " if (s[u] > n/2)\n", - " return centroid(u, n);\n", - " return v;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение.** `centroid` действительно находит цетроид.\n", - "\n", - "**Доказательство:**\n", - "\n", - "* `centroid` вернет вершину, у которой размеры всех детей не больше $\\frac{n}{2}$ (это явно проверяется в `if`-е).\n", - "* Мы пришли в эту вершину, когда её размер был больше $\\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\\frac{n}{2}+1) = \\frac{n}{2}-1$ вершин.\n", - "* Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен.\n", - "\n", - "Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».\n", - "\n", - "**Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.\n", - "\n", - "Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Теперь поймём, зачем мы всё это делали.\n", - "\n", - "**Утверждение.** Каждая вершина входит в $O(\\log n)$ компонент.\n", - "\n", - "**Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\\lceil \\log_2 n \\rceil$ разделений.\n", - "\n", - "**Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \\log n)$.\n", - "\n", - "**Утверждение.** Для любого пути $a \\leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$.\n", - "\n", - "**Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$.\n", - "\n", - "Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Подсчет путей с заданным свойством\n", - "\n", - "Рассмотрим конкретный пример: подсчёт путей заданной длины.\n", - "\n", - "Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид.\n", - "\n", - "Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int l = 179; // нужная нам длина\n", - "int ans = 0;\n", - "\n", - "// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена\n", - "bool used[maxn];\n", - "int s[maxn]; // размеры поддеревьев\n", - "\n", - "void sizes (int v, int p) {\n", - " s[v] = 1;\n", - " for (int u : g[v])\n", - " if (u != p && !used[u])\n", - " sizes(u, v), s[v] += s[u];\n", - "}\n", - "\n", - "int centroid (int v, int p, int n) {\n", - " for (int u : g[v])\n", - " if (u != p && !used[u] && s[u] > n/2)\n", - " return centroid(u, v, n);\n", - " return v;\n", - "}\n", - "\n", - "// записывает в t[] глубины вершин\n", - "void dfs (int v, int p, int d, vector &t) {\n", - " t.push_back(d);\n", - " for (int u : g[v])\n", - " if (u != p && !used[u])\n", - " dfs(u, v, d + 1, t);\n", - "} \n", - "\n", - "void solve (int v) {\n", - " /* <единственный зависящий от конкретной задачи код> */\n", - " size(v);\n", - " vector d(s[v], 0);\n", - " d[0] = 1;\n", - " for (int u : g[v]) {\n", - " if (!used[u]) {\n", - " vector t;\n", - " dfs(u, v, 1, t);\n", - " for (int x : t)\n", - " if (x <= l)\n", - " ans += d[l-x];\n", - " for (int x : t)\n", - " d[x]++;\n", - " }\n", - " }\n", - " /* */\n", - "\n", - " used[v] = 1;\n", - " for (int u : g[v])\n", - " if (!used[u])\n", - " solve(centroid(u, v, s[u]/2));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Асимптотика** $O(n \\log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\\log n)$ «уровней» центроидной декомпозиции." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на путях — offline\n", - "\n", - "Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.\n", - "\n", - "А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем.\n", - "\n", - "Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. \n", - "\n", - "Таким образом, каждый запрос будет просмотрен $O(\\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \\log n + n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на путях — online\n", - "\n", - "Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.\n", - "\n", - "Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \\times \\log n$, в котором для каждой вершины будем хранить $O(\\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.\n", - "\n", - "Тогда, при ответе на запрос, мы за $O(\\log n)$ или даже $O(\\log \\log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Асимптотика при более долгих пересчётах\n", - "\n", - "TODO: сослаться на мастер-теорему\n", - "\n", - "Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \\log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени?\n", - "\n", - "Возьмем худший случай — когда каждый раз компонента разбивается на две равные части.\n", - "\n", - "\n", - "\n", - "Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:\n", - "\n", - "$$ \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log \\frac{n}{2^k}\n", - "\\leq \\sum_{k=0}^{\\log n} \\frac{n}{2^k} \\log n\n", - "= n \\log n \\sum_{k=0}^{\\log n} \\frac{1}{2^k}\n", - "< 2 n \\log n\n", - "= O(n \\log n) $$\n", - "\n", - "Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \\log n)$ — как, впрочем, и во всех остальных «разделяйках»." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/cuda-checkpoint.ipynb b/img/.ipynb_checkpoints/cuda-checkpoint.ipynb deleted file mode 100644 index a1f3db1..0000000 --- a/img/.ipynb_checkpoints/cuda-checkpoint.ipynb +++ /dev/null @@ -1,475 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Supercomputing for the Masses\n", - "\n", - "This is htmt-rendered notebook is supposed to be opened with Colab or a debian-based linux machine with a CUDA-capable GPU.\n", - "\n", - "You need to complete a quest and install CUDA.\n", - "\n", - "We do not expect anything from reader except some knowledge of C, Python basic algorithms and generally how computers work." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Subtlties of the Moore's law\n", - "\n", - "There are physical limitations to the speed of processors.\n", - "\n", - "One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Some of them have" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Why multiprocessing?\n", - "\n", - "Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are two types of " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## General-purpose GPU\n", - "\n", - "There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills.\n", - "\n", - "There are several.\n", - "\n", - "This is like with Windows and Linux.\n", - "\n", - "We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting up" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can easily dump 98% of performance of you think this way." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Everything should work if you're on Colab. Otherwise, you might need to complete a [little quest](https://wiki.tiker.net/PyCuda/Installation/Linux/Ubuntu). It will probably be enough to do `sudo apt-get install nvidia-cuda-dev` (install CUDA itself) and then return to `pip install pycuda`.\n", - "\n", - "You can get it by running `lspci | grep NVIDIA`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install pycuda" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy\n", - "\n", - "from pycuda.compiler import SourceModule\n", - "import pycuda.driver as drv\n", - "import pycuda.autoinit" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time.\n", - "\n", - "You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Why CUDA\n", - "\n", - "Most of it still applicable.\n", - "\n", - "Again, GPU programming is very specific.\n", - "\n", - "SSE and tensor cores." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Kernels\n", - "\n", - "Just like C or C++, except that you use some custom built-in functions and specifiers.\n", - "\n", - "CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:\n", - "\n", - "You need to think of your computer as a heterogenious machine: there is host data and device data.\n", - "\n", - "* You move input data to device memory.\n", - "* You run some computation on device.\n", - "* You retrieve back this data.\n", - "\n", - "In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## NumPy\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example: $a + b$\n", - "\n", - "For coordination and testing, we will use `numpy` package. If you don't have it, install it: `pip install numpy`. This is also very optimized, so we will benchmark against it." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "ename": "LaunchError", - "evalue": "cuMemAlloc failed: unspecified launch failure", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0ma\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mastype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumpy\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfloat32\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0ma_gpu\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmem_alloc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdtype\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitemsize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mcuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmemcpy_htod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma_gpu\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mLaunchError\u001b[0m: cuMemAlloc failed: unspecified launch failure" - ] - } - ], - "source": [ - "a = numpy.random.randn(4,4)\n", - "\n", - "a = a.astype(numpy.float32)\n", - "\n", - "a_gpu = cuda.mem_alloc(a.size * a.dtype.itemsize)\n", - "\n", - "cuda.memcpy_htod(a_gpu, a)\n", - "\n", - "mod = SourceModule(\"\"\"\n", - " __global__ void doublify(float *a)\n", - " {\n", - " int idx = threadIdx.x + threadIdx.y*4;\n", - " a[idx] *= 2;\n", - " }\n", - "\"\"\")\n", - "\n", - "func = mod.get_function(\"doublify\")\n", - "func(a_gpu, block=(4,4,1))\n", - "\n", - "a_doubled = numpy.empty_like(a)\n", - "cuda.memcpy_dtoh(a_doubled, a_gpu)\n", - "print(a)\n", - "print(a_doubled)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "ename": "LaunchError", - "evalue": "cuModuleLoadDataEx failed: unspecified launch failure - ", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mLaunchError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mdest\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mb\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m }\n\u001b[0;32m----> 6\u001b[0;31m \"\"\")\n\u001b[0m\u001b[1;32m 7\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0madd_kernel\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmod\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"add\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/anaconda3/lib/python3.7/site-packages/pycuda/compiler.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, source, nvcc, options, keep, no_extern_c, arch, code, cache_dir, include_dirs)\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 293\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mpycuda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdriver\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 294\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodule\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodule_from_buffer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcubin\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 295\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 296\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_bind_module\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mLaunchError\u001b[0m: cuModuleLoadDataEx failed: unspecified launch failure - " - ] - } - ], - "source": [ - "mod = SourceModule(\"\"\"\n", - " __global__ void add(float *dest, float *a, float *b) {\n", - " const int i = threadIdx.x;\n", - " dest[i] = a[i] + b[i];\n", - " }\n", - "\"\"\")\n", - "\n", - "add_kernel = mod.get_function(\"add\")\n", - "\n", - "a = numpy.random.randn(400).astype(numpy.float32)\n", - "b = numpy.random.randn(400).astype(numpy.float32)\n", - "\n", - "dest = numpy.zeros_like(a)\n", - "\n", - "add_kernel(drv.Out(dest), drv.In(a), drv.In(b), block=(400,1,1))\n", - "\n", - "print(dest-a*b)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mod = SourceModule(\"\"\"\n", - " __global__ void check_prime(int *a, int *b, int *c) {\n", - " int n = blockIdx.x*blockDim.x + threadIdx.x;\n", - " for (int i = 2; i < 10000; i++)\n", - " a[i] |= (n % i == 0);\n", - " }\n", - "\"\"\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "a = numpy.zeros(10**8, dtype=numpy.int)\n", - "\n", - "func = mod.get_function(\"check_prime\")\n", - "func(cuda.InOut(a), block=(len(a)//32,32, 1))\n", - "\n", - "print(a)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "What you need to understand about GPUs is that they are extremely specialised for their applications.\n", - "\n", - "Intrinsics for that.\n", - "\n", - "Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision.\n", - "\n", - "First, they introduced \"multiply-accumulate\" operation (e. g. `x += y * z`) per 1 GPU clock cycle.\n", - "\n", - "Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell).\n", - "\n", - "Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix).\n", - "\n", - "This is a lot of work per \n", - "\n", - "Well, you don't really need anything more precise than that for deep learning anyway.\n", - "\n", - "\n", - "It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices.\n", - "\n", - "Probably, the proper name would be \"4x4 matrix cores\", however NVIDIA marketing team decided to use \"tensor cores\".\n", - "\n", - "So, see, this is not exactly fair comparison.\n", - "\n", - "\n", - "\n", - "*
You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull
*" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "down to int4 (16-valued, you heard correct)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Warps and thread blocks\n", - "\n", - "Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties.\n", - "\n", - "\n", - "\n", - "You can actually do the same stuff with 2d and 3d indexing — weird, right?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Reducing an array\n", - "\n", - "It seems to be simple: you just need to .\n", - "\n", - "What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen:\n", - "\n", - "1. Read $x$ into register\n", - "2. Read $s$ into register\n", - "3. Calculate $s + x$\n", - "4. Write it back to wherever $s$ was initially\n", - "\n", - "Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value.\n", - "\n", - "\n", - "\n", - "Note: Atomics to do that\n", - "\n", - "for small data types they are implemented on the hardware level and much more faster than that.\n", - "\n", - "std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Memory types\n", - "\n", - "If all the various types of device memory were to race, here’s how the race would turn out:\n", - "\n", - "Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible).\n", - "\n", - "* 1st place: **Register memory**\n", - "
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread.\n", - "* 2nd place: **Shared Memory**\n", - "
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should\n", - "* 3rd place: **Constant Memory**\n", - "
\n", - "* 4th: Texture Memory\n", - "* Tie for last place: Local Memory and Global Memory\n", - "\n", - "What you need to care for now is register \n", - "\n", - "For now, you need to care about differe\n", - "\n", - "Accessing global memory takes hundreds." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Heterogineous computing\n", - "\n", - "CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more CUDA-enabled NVIDIA GPU devices.\n", - "\n", - "Threading resources\n", - "Execution pipelines on host systems can support a limited number of concurrent threads. Servers that have four hex-core processors today can run only 24 threads concurrently (or 48 if the CPUs support Hyper-Threading.) By comparison, the smallest executable unit of parallelism on a CUDA device comprises 32 threads (termed a warp of threads). Modern NVIDIA GPUs can support up to 1536 active threads concurrently per multiprocessor (see Features and Specifications of the CUDA C Programming Guide) On GPUs with 16 multiprocessors, this leads to more than 24,000 concurrently active threads.\n", - "Threads\n", - "Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches (when two threads are swapped) are therefore slow and expensive. By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work (in warps of 32 threads each). If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput.\n", - "RAM\n", - "The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem: dense matrix multiplication\n", - "\n", - "A lot of these are actually sparse. You can do stuff with social network graphs or web graphs.\n", - "\n", - "Cool. But let's disapploint us for a bit:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem: dynamic programming" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Some primitives\n", - "\n", - "Before we go deeper into more complex algorithms, we need to master some " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Problem: sorting" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb b/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb deleted file mode 100644 index 3506ced..0000000 --- a/img/.ipynb_checkpoints/dp-intro-checkpoint.ipynb +++ /dev/null @@ -1,109 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Динамическое программирование" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "> Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Разберем некоторые классические идеи и задачи в динамическом программировании." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Рюкзак\n", - "\n", - "> Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наибольшая общая подпоследовательность\n", - "\n", - "> Есть две строки. Найти длину их наибольшей общей подпоследовательности.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Расстановка скобок\n", - "\n", - "> Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.\n", - "\n", - "Можно завести динамику «можно ли »." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наибольшая возрастающая подпоследовательность\n", - "\n", - "Есть также алгоритм за $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мемоизация\n", - "\n", - "Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.\n", - "\n", - "Первый предполагает" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Число разбиений\n", - "\n", - ">" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Замена параметра на ответ\n", - "\n", - ">" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb b/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb deleted file mode 100644 index 30cde99..0000000 --- a/img/.ipynb_checkpoints/dp-optimizations-checkpoint.ipynb +++ /dev/null @@ -1,288 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Оптимизации динамики\n", - "\n", - "* Разделяй и властвуй\n", - "* Оптимизация Кнута\n", - "* Convex Hull Trick\n", - "* Лямбда-оптимизация" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Задача\n", - "\n", - "> Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.\n", - "\n", - "Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.).\n", - "\n", - "Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\}$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// x[] — отсортированный массив координат точек, нумерация с нуля\n", - "\n", - "// квадрат длины отрезка от i-той до j-той точки\n", - "int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }\n", - "\n", - "// TODO: предподсчитать cost\n", - "\n", - "for (int i = 0; i <= m; i++)\n", - " f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо\n", - "// все остальные f предполагаем равными бесконечности\n", - "\n", - "for (int i = 1; i <= n; i++)\n", - " for (int j = 1; j <= m; j++)\n", - " for (int k = 0; k < i; k++)\n", - " f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1));" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Заметим, что циклы по `i` и `j` можно поменять местами.\n", - "\n", - "Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Разделяй-и-властвуй\n", - "\n", - "Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.\n", - "\n", - "*Утверждение.* $opt[i, j] \\leq opt[i, j+1]$.\n", - "\n", - "Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.\n", - "\n", - "Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$.\n", - "\n", - "Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "void solve (int l, int r, int _l, int _r, int k) {\n", - " if (l > r) return; // отрезок пустой — выходим\n", - " int t = (l + r) / 2;\n", - " int opt = _l;\n", - " for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы \n", - " int val = f[i+1][k-1] + cost(i, j);\n", - " if (val < f[t][k])\n", - " f[t][k] = val, opt = i;\n", - " }\n", - " solve(l, t-1, _l, opt, k);\n", - " solve(t+1, r, opt, _r, k);\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Вызываться она будет просто последовательно для каждого слоя:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "for (int k = 1; k <= m; k++)\n", - " solve(0, n-1, 0, n-1, k);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Теперь пересчет одного «слоя» динамики занимает $O(n \\log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\\log n)$, а значит и каждый элемент будет просмотрен не более $O(\\log n)$ раз.\n", - "\n", - "Получается, что асимптотика улучшилась до $O(n m \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Оптимизация Кнута\n", - "\n", - "Предыдущий метод основывался на том факте, что $opt[i, j] \\leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру?\n", - "\n", - "$$ opt[i-1, j] \\leq opt[i, j] \\leq opt[i, j+1] $$\n", - "\n", - "В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок.\n", - "\n", - "Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:\n", - "\n", - "$$ \\sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \\sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$\n", - "\n", - "Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for (int i = 1; i <= n; i++) {\n", - " for (int j = m; j >= 1; j--) {\n", - " for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) {\n", - " int val = f[i+1][k-1] + cost(i, j);\n", - " if (val < f[t][k])\n", - " f[t][k] = val, opt[i][j] = i;\n", - " }\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Сравните с базовым решением — всего 3 новых строчки." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Convex Hull Trick\n", - "\n", - "Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение.\n", - "\n", - "$$ f[i, j] = \\min_{k < i} \\{f[k, j-1] + (x_{i-1}-x_k)^2 \\} = \\min_{k < i} \\{\n", - "f[k, j-1] + x_{i-1}^2\n", - "- 2x_{i-1} x_k\n", - "+ x_k^2\n", - "\\}$$\n", - "\n", - "Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется\n", - "$\n", - "\\underbrace{f[k, j-1] + x_k^2}_{a_k}\n", - "\\underbrace{-2x_k}_{b_k} x_{i-1}\n", - "$.\n", - "\n", - "Это теперь можно переписать как $\n", - "\\min_k\n", - "(a_k, b_k)\n", - "\\cdot\n", - "(1, X_{i-1})\n", - "$ (имеется в виду скалярное произведение)\n", - "\n", - "Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: иллюстрация." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: мем про Скуби-Ду." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ли Шао" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Лямбда-оптимизация\n", - "\n", - "Не «фольклорное» название — дискретный метод множителей Лагранжа.\n", - "\n", - "Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .\n", - "\n", - "Заметим следующее: \n", - "\n", - "Давайте заменим $f[i, j]$ на $g_\\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\\lambda$, за использование отрезка.\n", - "\n", - "Давайте сделаем бинпоиск по $\\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \\lambda k$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Суммируем\n", - "\n", - "TODO: сделать табличку\n", - "\n", - "* Разделяйка: $O(nm \\log n)$, если `cost` такой, что `opt` монотонна по одному аргументу.\n", - "* Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам.\n", - "* CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение.\n", - "* Лагранж: $O(n \\log n)$. Функция должна быть выпуклой." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Другие задачи\n", - "\n", - "Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.\n", - "\n", - "> Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "c++", - "file_extension": ".c++", - "mimetype": "text/x-c++src", - "name": "c++" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb b/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb deleted file mode 100644 index 3c3d9f3..0000000 --- a/img/.ipynb_checkpoints/fenwick-checkpoint.ipynb +++ /dev/null @@ -1,285 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "\n", - "НЯ!\n", - "
Эта статья полна любви и обожания.\n", - "
Возможно, стоит добавить ещё больше?\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Дерево Фенвика\n", - "\n", - "Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Определение\n", - "\n", - "Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: \n", - " \n", - "$$ t_i = \\sum_{k=F(i)}^i a_k $$\n", - " \n", - "где $F$ это какая-то функцию, для которой выполнено $F(i) \\leq i$. Конкретно её определим потом.\n", - " \n", - "Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле:\n", - " \n", - "$$ sum(k) = t_k + sum(F(k)-1) $$\n", - " \n", - "Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\\log n)$. Популярны две функции:\n", - "\n", - "* $F_1(x) =$ `x & (x + 1)`\n", - "* $F_2(x) =$ `x - (x & -x) + 1`\n", - "\n", - "Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.\n", - "\n", - "**Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\\log n)$ таких отрезков." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Реализация\n", - "\n", - "Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int t[maxn];\n", - "\n", - "// возвращает сумму на префиксе\n", - "int sum (int r) {\n", - " int res = 0;\n", - " for (; r > 0; r -= r & -r)\n", - " res += t[r];\n", - " return res;\n", - "}\n", - "\n", - "int sum (int l, int r) {\n", - " return sum(r) - sum(l-1);\n", - "}\n", - "\n", - "// обновляет нужные t\n", - "void add (int k, int x) {\n", - " for (; k <= n; k += k & -k)\n", - " t[k] += x;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Многомерный случай\n", - "\n", - "> $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку\n", - "\n", - "Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре.\n", - "\n", - "`sum` перепишется следующим образом:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int sum (int r1, int r2) {\n", - " int res = 0;\n", - " for (int i = r1; i > 0; i -= i & -i)\n", - " for (int j = r2; j > 0; j -= j & -j)\n", - " ans += t[i][j];\n", - " return res;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах.\n", - "\n", - "Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \\log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Бинпоиск\n", - "\n", - "Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\\log n)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// возвращает индекс, на котором сумма уже больше\n", - "int lower_bound (int s) {\n", - " int k = 0;\n", - " for (int l = logn; l >= 0; l--) {\n", - " if (k + (1<= k > i - (i & -i)`. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Будем перебирать префиксы TODO\n", - "\n", - "Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя.\n", - "\n", - "Можно представить дерево так: ячейка 2^k содержит все\n", - "\n", - "TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Название\n", - "\n", - "Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree».\n", - "\n", - "Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.\n", - "\n", - "Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).\n", - "\n", - "В частном случае, когда длина массива равна $2^k$, то дерево будет только одно." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/games-checkpoint.ipynb b/img/.ipynb_checkpoints/games-checkpoint.ipynb deleted file mode 100644 index f183b10..0000000 --- a/img/.ipynb_checkpoints/games-checkpoint.ipynb +++ /dev/null @@ -1,331 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "c0JYtYks_TYp" - }, - "source": [ - "# Теория игр\n", - "\n", - "Начнём с самого баянного примера математической игры, который можно вспомнить:\n", - "\n", - "> На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков.\n", - "\n", - "Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так:\n", - "\n", - "$$ f_k = \\lnot f_{k-1} \\lor \\lnot f_{k-2} \\lor \\lnot f_{k-3} = \\lnot (f_{k-1} \\land f_{k-2} \\land f_{k-3}) $$\n", - "\n", - "то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния).\n", - "\n", - "**Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Игры на графах\n", - "\n", - "В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек».\n", - "\n", - "Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные*\n", - "\n", - "Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*.\n", - "\n", - "Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ретроанализ\n", - "\n", - "Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности:\n", - "\n", - "* Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины.\n", - "* Вершина *выигрышная* — если из неё есть переход в проигрышную вершину.\n", - "* Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные).\n", - "\n", - "Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся.\n", - "\n", - "**Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин.\n", - "\n", - "Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние.\n", - "\n", - "Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось.\n", - "\n", - "Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные.\n", - "\n", - "**Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали.\n", - "\n", - "* При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). \n", - "* При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше.\n", - "\n", - "Асимптотика составит $O(n + m)$, как и у любого bfs-а." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа\n", - "int cnt[maxn]; // счётчик исходящих рёбер\n", - "\n", - "enum StatusType { win, loss, unknown };\n", - "StatusType status[maxn]; // выгрышность вершины;\n", - "// по умолчанию все кроме терминальных считаются unknown\n", - "// те, кто в итоге остаются unknown -- ничейные\n", - "\n", - "queue q = {/* нужно заранее добавить сюда все терминальные вершины*/};\n", - "\n", - "while (!q.empty()) {\n", - " int v = q.front();\n", - " q.pop();\n", - " for (int u : t[v]) {\n", - " cnt[u]--; // удаляем это ребро\n", - " if (status[v] == unknown) {\n", - " // из u есть ребро в проигрышную -- значит она выигрышная\n", - " if (status[v] == loss)\n", - " status[u] = win;\n", - " // все ребра u ведут в выигрышные вершины -- значит она проигрышная\n", - " if (status[v] == win && cnt[u] == 0) {\n", - " status[u] = loss;\n", - " // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь\n", - " if (status[v] != unknown)\n", - " q.push(u);\n", - " }\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "StatusType dfs(int v) {\n", - " if (status[v] != unknown)\n", - " return status[v];\n", - " status[v] = loss;\n", - " // изменим статус, когда найдём переход в проигрышную вершину\n", - " for (int u : g[v])\n", - " if (dfs(u) == loss)\n", - " status[v] = win;\n", - " return status[v];\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: можно ли здесь циклы учесть?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Минимаксные игры\n", - "\n", - "Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно.\n", - "\n", - "Типичный граф минимаксной игры выглядит следующим образом:\n", - "\n", - "\n", - "\n", - "В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников.\n", - "\n", - "Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Ретроанализ для больших графов\n", - "\n", - "Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго.\n", - "\n", - "Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей).\n", - "\n", - "Ретроанализ нужно оптимизировать." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ограничение перебора\n", - "\n", - "Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. \n", - "\n", - "Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит.\n", - "\n", - "Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли).\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Мемоизация позиций\n", - "\n", - "В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает .\n", - "\n", - "Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Альфа-бета отсечение\n", - "\n", - "\n", - "\n", - "Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым.\n", - "\n", - "На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым.\n", - "\n", - "Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6.\n", - "\n", - "Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем.\n", - "\n", - "Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Monte-Carlo Tree Search\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Игры с ненулевой суммой\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Теория Шпрага-Гранди\n", - "\n", - "Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые.\n", - "\n", - "Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями.\n", - "\n", - "**Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \\oplus a_2 \\oplus \\ldots \\oplus a_n $ размеров кучек отлична от нуля.\n", - "\n", - "**Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы:\n", - "\n", - "* Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \\oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия.\n", - "* Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \\oplus a_i \\oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \\oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$.\n", - "\n", - "Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \\oplus a_i$.\n", - "\n", - "Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ним с увеличениями\n", - "\n", - "Пусть у нас" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Эквивалентность игр ниму" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Игры с неполной информацией\n", - "\n", - "Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков.\n", - "\n", - "Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет.\n", - "\n", - "Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Дилемма заключенного" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Камень-ножницы-бумага" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Покер\n", - "\n", - "Рассмотрим такую у" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "29_game-theory.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/img/.ipynb_checkpoints/geometry-checkpoint.ipynb b/img/.ipynb_checkpoints/geometry-checkpoint.ipynb deleted file mode 100644 index 555b3b9..0000000 --- a/img/.ipynb_checkpoints/geometry-checkpoint.ipynb +++ /dev/null @@ -1,421 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Геометрия и ООП" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали.\n", - "\n", - "![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg)\n", - "\n", - "Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:\n", - "\n", - "$$ a \\cdot b = x_a x_b + y_a y_b $$\n", - "\n", - "![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg)\n", - "\n", - "У него есть полезные свойства:\n", - "\n", - "* Скалярное произведение симметрично ($a \\cdot b = b \\cdot a$).\n", - "* Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.\n", - "* Перпендикулярные вектора должны иметь нулевое скалярное произведение.\n", - "* Если угол острый, то оно положительное\n", - "* Если угол тупой, то отрицательное\n", - "\n", - "**Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:\n", - "\n", - "$$ a \\times b = x_a y_b - y_a x_b $$\n", - "\n", - "![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg)\n", - "\n", - "Его свойства:\n", - "\n", - "* Скалярное произведение *анти*симметрично ($a \\times b = - (b \\times a)$).\n", - "* Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.\n", - "* Коллинеарные вектора должны иметь нулевое векторное произведение.\n", - "* Если b «слева» от a, то оно положительное.\n", - "* Если b «справа» — то отрицательное." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Всякие проверки\n", - "\n", - "Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.\n", - "\n", - "**Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.\n", - "\n", - "$$\n", - "\\text{P лежит внутри ABC} \\iff \\begin{cases}\n", - "(B-A) \\times (P-A) \\geq 0 \\\\\n", - "(C-B) \\times (P-B) \\geq 0 \\\\\n", - "(A-C) \\times (P-C) \\geq 0 \\\\\n", - "\\end{cases}\n", - "$$\n", - "\n", - "**Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.\n", - "\n", - "$$ V = \\frac{1}{2} (B-A) \\times (C-A) $$\n", - "\n", - "**Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).\n", - "\n", - "![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg)\n", - "\n", - "Забудьте о формуле Герона и всегда считайте площади через векторное произведение.\n", - "\n", - "Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.\n", - "\n", - "**Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\\times(c-a) > 0$).\n", - "\n", - "**Пересекаются ли отрезки**.\n", - "\n", - "![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Уравнение прямой\n", - "\n", - "Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством.\n", - "\n", - "У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \\geq 0$ будет указывать в сторону самой полуплоскости.\n", - "\n", - "Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой:\n", - "\n", - "$$ d = \\frac{|Ax_0+By_0+C|}{\\sqrt{A^2+B^2}} $$\n", - "\n", - "**Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "A_1 x + B_1 y + C_1 = 0 \\\\\n", - "A_2 x + B_2 y + C_2 = 0\n", - "\\end{cases}\n", - "\\implies \n", - "\\begin{cases}\n", - "-x = \\frac{B_1 y + C_1}{A_1} \\\\\n", - "-x = \\frac{B_2 y + C_2}{A_2}\n", - "\\end{cases}\n", - "\\implies \n", - "\\frac{B_1 y + C_1}{A_1} = \\frac{B_2 y + C_2}{A_2}\n", - "\\implies \n", - "y = - \\frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1}\n", - "$$\n", - "\n", - "Аналогично, $x = \\frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки).\n", - "\n", - "Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Как это кодить в C++" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Точка $\\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "struct r {\n", - " double x, y;\n", - " r () {}\n", - " r (int _x, int _y) { x = _x, y = _y; }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Операции над векторами\n", - "\n", - "Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "double len (r a) { return sqrt(a.x*a.x + a.y*a.y); }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Операторы\n", - "\n", - "В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д.\n", - "\n", - "Давайте для будущих нужд определим `+` и `-`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); }\n", - "r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Скалярное произведение" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int operator*(r a, r b){ return a.x*b.x + a.y*b.y; }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Векторное произведение\n", - "\n", - "Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int operator^(r a, r b){ return a.x*b.y - b.x*a.y; }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ввод-вывод\n", - "\n", - "Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "istream& operator>>(istream &in, r &p){ \n", - " in >> p.x >> p.y;\n", - " return in;\n", - "}\n", - "\n", - "ostream& operator<<(ostream &out, r &p){ \n", - " out << p.x << \" \" << p.y << endl;\n", - " return out; \n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Алгебра VS Алгоритмы или зачем мы всё это делали\n", - "\n", - "Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Векторное представление прямой ($Ax + By + C = 0 \\rightarrow r = at + b$)\n", - "\n", - "Тут нужно просто выбрать две любые точки на прямой." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// даны A, B, C (A^2 + B^2 != 0)\n", - "r a, b;\n", - "if (eq(A, 0)) // значит, это горизонтальная прямая\n", - " a = r(0, -C/B), b = r(1, -C/B);\n", - "else\n", - " a = r(-C/A, 0), b = (1, -(C+B)/A, 1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Пример: отражение от прямой\n", - "\n", - "Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.\n", - "\n", - "$\\Pr_a b = \\frac{a \\cdot b}{|a|} \\frac{a}{|a|} = \\frac{|a| |b| \\cos \\alpha}{|a|} \\frac{a}{|a|} = |b| \\cos \\alpha \\frac{a}{|a|} $\n", - "\n", - "Формула имеет смысл: длина на единичный вектор направления.\n", - "\n", - "Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "// прямая r = at + b, точка c\n", - "r pr (r a, r b, r c) {\n", - " c -= b; // пусть c и a выходят из одной точки\n", - " return b + (a*b / len(a) / len(a)) * a;\n", - "}\n", - "\n", - "r reflect (r a, r b, r c) {\n", - " return c + 2*(pr(a, b, c)-c);\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Типичные баги" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Точность\n", - "\n", - "> Первое правило действительных чисел — не использовать действительные числа\n", - "\n", - "Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "const double eps = 1e-8;\n", - "\n", - "bool eq (double a, double b) { return abs(a-b) < eps }" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### $0 \\neq -0$\n", - "\n", - "Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Область определения обратных функций\n", - "\n", - "`acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/hashing-checkpoint.ipynb b/img/.ipynb_checkpoints/hashing-checkpoint.ipynb deleted file mode 100644 index f1a6144..0000000 --- a/img/.ipynb_checkpoints/hashing-checkpoint.ipynb +++ /dev/null @@ -1,426 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "tqKO26jI7Eg2" - }, - "source": [ - "# Хэширование в строковых задачах\n", - "\n", - "Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.\n", - "\n", - "«Хорошая» хэш-функция:\n", - "* Быстро считается — за линейное от размера объекта время;\n", - "* Имеет не очень большие значения — влезающие в 64 бита;\n", - "* «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\\frac{1}{n}$.\n", - "\n", - "Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*.\n", - "\n", - "Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \\cdot n \\cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.\n", - "\n", - " ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "mihqVF9EHNW3" - }, - "source": [ - "## Применения в реальной жизни\n", - "\n", - "* **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.\n", - "* **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\\frac{k}{n}$ = $O(1)$ при правильном выборе $n$.\n", - "* **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.\n", - "* **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.\n", - "* **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.\n", - "* **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "sb5PdKP87Eg3" - }, - "source": [ - "Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы.\n", - "\n", - "Сегодня же мы остановимся на строках." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "AwuPsbSa7Eg3" - }, - "source": [ - "# Полиномиальное хэширование\n", - "\n", - "**Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами.\n", - "\n", - "Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`.\n", - "\n", - "Определим *прямой полиномиальный хэш* строки как значение следующего многочлена:\n", - "\n", - "$$ h_f = (s_0 + s_1 k + s_2 k^2 + \\ldots + s_n k^n) \\mod p $$\n", - "\n", - "Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой.\n", - "\n", - "Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "h3UUC_Vl80Is" - }, - "outputs": [], - "source": [ - "const int k = 31, mod = 1e9+7;\n", - "\n", - "string s = \"abacabadaba\";\n", - "long long h = 0, m = 1;\n", - "for (char c : s) {\n", - " int x = (int) (c - 'a' + 1);\n", - " h = (h + m * x) % mod;\n", - " m = (m * k) % mod;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "7_1zG4nb80I7" - }, - "source": [ - "Можем ещё определить *обратный полиномиальный хэш*:\n", - "\n", - "$$ h_b = (s_0 k^n + s_1 k^{n-1} + \\ldots + s_n) \\mod p $$\n", - "\n", - "Его преимущество в том, что можно написать на одну строчку кода меньше:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "kShe7vvz80JC" - }, - "outputs": [], - "source": [ - "long long h = 0;\n", - "for (char c : s) {\n", - " int x = (int) (c - 'a' + 1);\n", - " h = (h * k + x) % mod;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "VWd2N6Ic80JL" - }, - "source": [ - "Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hKCe8eBr80JP" - }, - "source": [ - "## Зачем он нужен?\n", - "\n", - "Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций.\n", - "\n", - "Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$:\n", - "\n", - "$$ h(ab) = h(a) + k^{|a|} \\cdot h(b) $$\n", - "\n", - "Удалить префикс строки можно так:\n", - "\n", - "$$ h(b) = \\frac{h(ab) - h(a)}{k^{|a|}} $$\n", - "\n", - "А суффикс — ещё проще:\n", - "\n", - "$$ h(a) = h(ab) - k^{|a|} \\cdot h(b) $$\n", - "\n", - "В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "0IBufJNe_jVG" - }, - "outputs": [], - "source": [ - "const int maxn = 1e5+5;\n", - "\n", - "int p[maxn];\n", - "p[0] = 1;\n", - "\n", - "for (int i = 1; i < maxn; i++)\n", - " p[i] = (p[i-1] * k) % mod;" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "_PKLEmZU80Jb" - }, - "source": [ - "Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "qyJGTChd80Jd" - }, - "outputs": [], - "source": [ - "int h[maxn];\n", - "h[0] = 0; // h[k] -- хэш префикса длины k\n", - "\n", - "// будем считать, что s это уже последовательность int-ов\n", - "\n", - "for (int i = 0; i < n; i++) \n", - " h[i+1] = (h[i] + p[i] * s[i]) % mod;" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "L7n-KRvs80Jr" - }, - "source": [ - "Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке:\n", - "\n", - "$$ h(s[l:r]) = \\frac{h_r-h_l}{k^l} $$\n", - "\n", - "Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.\n", - "\n", - "Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить.\n", - "\n", - "$$ \\hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "FMjf8Rej80Jv" - }, - "outputs": [], - "source": [ - "int hash_substring (int l, int r) {\n", - " return (h[r+1] - h[l]) * p[n-l] % mod;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "l5XFHeaJ80J5" - }, - "source": [ - "Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "VuqWJtAvCCaQ" - }, - "source": [ - "### Упражнение\n", - "\n", - "Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "moSrfcxI7EhB" - }, - "source": [ - "## Примеры задач\n", - "\n", - "**Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`.\n", - "\n", - "**Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.\n", - "\n", - "**Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.\n", - "\n", - "**Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором.\n", - "\n", - "**Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "krDNbmroGTTs" - }, - "source": [ - "### Изменения строки*\n", - "\n", - "Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.\n", - "\n", - "Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.\n", - "\n", - "Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "8SFqBzPV7EhD" - }, - "source": [ - "# Вероятность ошибки и почему это всё вообще работает\n", - "\n", - "У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.\n", - "\n", - "Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого?" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "PE38pSDXQxcH" - }, - "source": [ - "## Выбор констант\n", - "\n", - "Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \\cdot n^2$. Обоснование — см. парадокс дней рождений.\n", - "\n", - "Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно.\n", - "\n", - "Можно также брать модуль $2^{64}$. У него есть несколько преимуществ:\n", - "* Он большой — второй модуль точно не понадобится.\n", - "* С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении.\n", - "* С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`.\n", - "\n", - "Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду.\n", - "\n", - "В выборе же $k$ ограничения не такие серьезные:\n", - "* Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.\n", - "* Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.\n", - "\n", - "Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "DSfMhJRhRaZe" - }, - "source": [ - "## Парадокс дней рождений\n", - "\n", - "> В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%.\n", - "\n", - "Более общее утверждение: в мультимножество нужно добавить $\\Theta(\\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали.\n", - "\n", - "**Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения.\n", - "Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$.\n", - "\n", - "$$f(n, d) = (1-\\frac{1}{d}) \\times (1-\\frac{2}{d}) \\times ... \\times (1-\\frac{n-1}{d})$$\n", - "\n", - "Попытаемся оценить $f$:\n", - "\n", - "$$\n", - "\\begin{align}\n", - " \\begin{aligned}\n", - " e^x & = 1 + x + \\frac{x^2}{2!} + \\ldots & \\text{(ряд Тейлора для экспоненты)} \\\\\n", - " & \\simeq 1 + x & \\text{(аппроксимация для $|x| \\ll 1$)} \\\\\n", - " e^{-\\frac{n}{d}} & \\simeq 1 - \\frac{n}{d} & \\text{(подставим $\\frac{n}{d} \\ll 1$)} \\\\\n", - " f(n, d) & \\simeq e^{-\\frac{1}{d}} \\times e^{-\\frac{2}{d}} \\times \\ldots \\times e^{-\\frac{n-1}{d}} & \\\\\n", - " & = e^{-\\frac{n(n-1)}{2d}} & \\\\\n", - " & \\simeq e^{-\\frac{n^2}{2d}} & \\\\\n", - " \\end{aligned}\n", - "\\end{align}\n", - "$$\n", - "\n", - "Из последнего выражения более-менее понятно, что вероятность $\\frac{1}{2}$ достигается при $n \\approx \\sqrt{d}$ и в этой точке изменяется очень быстро.\n", - "\n", - "**Второе доказательство** (для любителей теорвера). Введем $\\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\\frac{1}{d}$.\n", - "\n", - "Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\\frac{n (n-1)}{2} \\cdot \\frac{1}{d}$.\n", - "\n", - "Отсюда понятно, что если $d = \\Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно.\n", - "\n", - "*Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "2WAkqxpvRfj0" - }, - "source": [ - "### Бонус: «мета-задача»\n", - "\n", - "Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.\n", - "\n", - "«Решите» задачу." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "7_string-hashing.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/img/.ipynb_checkpoints/hld-checkpoint.ipynb b/img/.ipynb_checkpoints/hld-checkpoint.ipynb deleted file mode 100644 index 5faf5cd..0000000 --- a/img/.ipynb_checkpoints/hld-checkpoint.ipynb +++ /dev/null @@ -1,186 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Heavy-light декомпозиция\n", - "\n", - "HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid).\n", - "\n", - "\n", - "\n", - "*
TODO: найти менее уродливую иллюстрацию
*\n", - "\n", - "Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.\n", - "\n", - "Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.\n", - "\n", - "**Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер.\n", - "\n", - "**Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.\n", - "\n", - "Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.\n", - "\n", - "**Утверждение.** На любом вертикальном пути будет не более $O(\\log n)$ блоков.\n", - "\n", - "**Доказательство** разбивается на две части:\n", - "\n", - "* Лёгких ребер на вертикальном пути будет не более $O(\\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего.\n", - "* Непрерывных путей из тяжелых рёбер будет не более $O(\\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\\log n)$.\n", - "\n", - "**Следствие.** На любом пути будет не более $O(\\log n)$ блоков.\n", - "\n", - "Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Реализация\n", - "\n", - "Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.\n", - "\n", - "А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.\n", - "\n", - "Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vector g[maxn];\n", - "int s[maxn], p[maxn], tin[maxn], tout[maxn];\n", - "int head[maxn]; // «голова» тяжелого пути, которому принадлежит v\n", - "int t = 0;\n", - "\n", - "void sizes (int v = 0) {\n", - " s[v] = 1;\n", - " for (int &u : g[v]) {\n", - " sizes(u);\n", - " s[v] += s[u];\n", - " if (s[u] > s[g[v][0]])\n", - " // &u -- это ссылка, так что её легально использовать при swap-е\n", - " swap(u, g[v][0]);\n", - " }\n", - "}\n", - "\n", - "void hld (int v = 0) {\n", - " rin[t] = v;\n", - " tin[v] = t++;\n", - " for (int u : g[v]) {\n", - " // если это тяжелый ребенок -- его next нужно передать\n", - " // в противном случае он сам является головой нового пути\n", - " head[u] = (u == g[v][0] ? head[v] : u);\n", - " hld(u);\n", - " }\n", - " tout[v] = t;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Как им решать задачи\n", - "\n", - "Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:\n", - "\n", - "1. Узнать минимальное число на пути между $v_i$ и $u_i$.\n", - "2. Изменить число у $v_i$-той вершины на $x_i$.\n", - "\n", - "Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int val[maxn];\n", - "segtree st(0, n);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "void upd (int v, int x) {\n", - " st.upd(tin[v], x);\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int ancestor (int a, int b) {\n", - " return tin[a] <= tin[b] && tin[b] <= tout[a];\n", - "}\n", - "\n", - "void up (int &a, int &b, int &ans) {\n", - " while (!ancestor(head[a], b)) {\n", - " ans = min(ans, st.get_min(tin[head[a]], tin[a]));\n", - " a = p[head[a]];\n", - " }\n", - "}\n", - "\n", - "int get_min (int a, int b) {\n", - " int ans = inf;\n", - " up(a, b, ans);\n", - " up(b, a, ans);\n", - " if (!ancestor(a, b))\n", - " swap(a, b);\n", - " ans = min(ans, st.get_min(tin[a], tin[b]));\n", - " return ans;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/index-checkpoint.ipynb b/img/.ipynb_checkpoints/index-checkpoint.ipynb deleted file mode 100644 index b2dd755..0000000 --- a/img/.ipynb_checkpoints/index-checkpoint.ipynb +++ /dev/null @@ -1,81 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Алгоритмика\n", - "\n", - "Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/).\n", - "\n", - "Более-менее готово:\n", - "\n", - "* [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное\n", - "* [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое\n", - "* [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки\n", - "* [Центроидная декомпозиция](http://sereja.me/a/centroid)\n", - "* [Heavy-light декомпозиция](http://sereja.me/a/hld)\n", - "* [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла\n", - "* [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву\n", - "* [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость\n", - "* [Ро-алгоритм Полларда](http://sereja.me/a/pollard)\n", - "* [Sparse table](http://sereja.me/a/sparse-table)\n", - "* [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона\n", - "* [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса\n", - "* [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера\n", - "* [Классные задачки](http://sereja.me/a/bayans): пока что без решений\n", - "\n", - "Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально.\n", - "\n", - "Не очень готово:\n", - "\n", - "* [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция\n", - "* [Суффиксный массив](http://sereja.me/a/suffix-array)\n", - "* [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++\n", - "* [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick\n", - "* [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала\n", - "* [Обратное по модулю](http://sereja.me/a/reciprocal)\n", - "* [Метод отжига](http://sereja.me/a/annealing)\n", - "* [Стресс-тестирование](http://sereja.me/a/stress-test)\n", - "* [Бор](http://sereja.me/a/trie)\n", - "* [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько\n", - "* Выпуклые оболочки\n", - "* [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса\n", - "* CUDA: программирование на GPU (на английском)\n", - "\n", - "Вообще не готово:\n", - "* [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема\n", - "* [Ахо-Корасик](http://sereja.me/a/aho-corasick)\n", - "* [Теорвер](http://sereja.me/a/probability)\n", - "* Корневая декомпозиция\n", - "* Теория информации\n", - "* Функции потерь в машинном обучении\n", - "* Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском)\n", - "* Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском)\n", - "\n", - "Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb b/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb deleted file mode 100644 index 7200ab9..0000000 --- a/img/.ipynb_checkpoints/karatsuba-checkpoint.ipynb +++ /dev/null @@ -1,122 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Быстрое умножение\n", - "\n", - "В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).\n", - "\n", - "[Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.\n", - "\n", - "Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.\n", - "\n", - "Алгоритм Карацубы имеет довольно значимое место в истории науки.\n", - "\n", - "{\\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}).\n", - "У Колмогорова была гипотеза, что нижняя оценка для {\\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности\n", - "{\\displaystyle M(n)=O(n^{\\log _{2}3})} M(n)=O(n^{{\\log _{2}3}})\n", - "и тем самым опроверг «гипотезу {\\displaystyle n^{2}} n^{2}».\n", - "\n", - "(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)\n", - "\n", - "Можно представить себе какого-то грозного японца." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мастер-теорема\n", - "\n", - "Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек»." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Мастер-теорема.** Пусть имеется рекуррента:\n", - "\n", - "$$ T(n) = \\begin{cases}\n", - "a T(\\frac{n}{b}) + \\Theta(n^c), & n > n_0\n", - "\\\\ \\Theta(1), & n \\leq n_0\n", - "\\end{cases} $$\n", - "\n", - "Тогда:\n", - "\n", - "* **A.** Если $c > \\log_b a$, то $T(n) = \\Theta(n^c)$.\n", - "* **B.** Если $c = \\log_b a$, то $T(n) = \\Theta(n^c \\log n)$.\n", - "* **C.** Если $c < \\log_b a$, то $T(n) = \\Theta(n^{\\log_b a})$.\n", - "\n", - "---\n", - "\n", - "\n", - "\n", - "---\n", - "\n", - "**Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням:\n", - "\n", - "$$ T(n) = \\sum_{k=0}^{\\log_b n} a^k (\\frac{n}{b^k})^c = n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k $$\n", - "\n", - "* **A.** Если $c > \\log_b a$, то $\\sum (\\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \\Theta(n^c)$.\n", - "* **B.** Если $c = \\log_b a$, то\n", - "$$\\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\sum_{k=0}^{\\log_b n} 1^k = \\Theta(n^c \\log_b n)$$\n", - "* **C.** Если $c < \\log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,\n", - "\n", - "$$ n^c \\sum_{k=0}^{\\log_b n} (\\frac{a}{b^c})^k = \\Theta(n^c (\\frac{a}{b^c})^{\\log_b n}) = \\Theta(n^c \\cdot \\frac{a^{\\log_b n}}{n^c}) = \\Theta(a^{\\log_b n}) = \\Theta(n^{\\log_b a}) $$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Алгоритм Карацубы\n", - "\n", - "Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат.\n", - "\n", - "Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Развитие идеи\n", - "\n", - "То же самое можно применить матрицам.\n", - "\n", - "Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\\epsilon})$ для произвольного $\\epsilon$ ещё никто не доказал." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/lca-checkpoint.ipynb b/img/.ipynb_checkpoints/lca-checkpoint.ipynb deleted file mode 100644 index f4e7190..0000000 --- a/img/.ipynb_checkpoints/lca-checkpoint.ipynb +++ /dev/null @@ -1,339 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "1MwyLmBaV1hj" - }, - "source": [ - "# Корневые деревья" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hMjCrvgNV1hk" - }, - "source": [ - "Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*.\n", - "\n", - "Примеры корневых деревьев:\n", - "* наследование классов в языках программирования (если множественное наследование запрещено),\n", - "* дерево факторизации числа на простые (в общем случае не уникальное),\n", - "* иерархия в какой-нибудь компании,\n", - "* просто множество как-то вложенных объектов (ссылка на крысу-роботягу).\n", - "\n", - "Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Напоминание: DFS\n", - "\n", - "Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "vector g[maxn];\n", - "int p[maxn], tin[maxn], tout[maxn];\n", - "int t = 0;\n", - "\n", - "void dfs (int v) {\n", - " tin[v] = t++;\n", - " for (int u : g[v])\n", - " dfs(u);\n", - " tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет \n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "У этих массивов много полезных свойств:\n", - "* Вершина $u$ является предком $v$ $\\iff tin_v \\in [tin_u, tout_u) $. Эту проверку можно делать за константу.\n", - "* Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой.\n", - "* В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер.\n", - "* Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$.\n", - "* Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на поддеревьях\n", - "\n", - "Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.\n", - "\n", - "> Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$.\n", - "\n", - "Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на уровнях\n", - "\n", - "> Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$).\n", - "\n", - "Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам.\n", - "\n", - "Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наименьший общий предок\n", - "\n", - "Очень много задач нам поможет решить следующая вспомогательная задача.\n", - "\n", - "> Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких.\n", - "\n", - "По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные.\n", - "\n", - "![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png)\n", - "\n", - "Для лучшего понимания: медленно (за линейное время) это можно делать так:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "bool a (int u, int v) {\n", - " return tin[u] <= tin[v] && tin[v] <= tout[u];\n", - "}\n", - "\n", - "int lca (int u, int v) {\n", - " while (!ancestor(u, v))\n", - " u = p[u];\n", - " return u;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## LCA: двоичные подъемы\n", - "\n", - "Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \\times \\lceil \\log n \\rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень.\n", - "\n", - "Такой препроцессинг можно выполнить за $O(n \\log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int up[maxn][logn];\n", - "\n", - "void dfs (int v) {\n", - " for (int l = 1; l < logn; l++)\n", - " up[v][l] = up[up[v][l-1]][l-1];\n", - " tin[v] = t++;\n", - " for (int u : g[v]) {\n", - " up[u][0] = v;\n", - " dfs(u);\n", - " }\n", - " tout[v] = t++;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Пусть поступил очередной запрос — пара вершин $(u, v)$:\n", - "* Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.\n", - "* Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.\n", - "\n", - "Подробнее про второй пункт. Пусть $L = \\lceil \\log n \\rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "cFYU1Y5qV1hs" - }, - "outputs": [], - "source": [ - "int lca (int v, int u) {\n", - " if (a(v, u)) return v;\n", - " if (a(u, v)) return u;\n", - " for (int l = logn-1; l >= 0; l--)\n", - " if (!ancestor(up[v][l], u))\n", - " v = up[v][l];\n", - " return up[v][0];\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Асимптотика\n", - "\n", - "Препроцессинг — $O(n \\log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу.\n", - "\n", - "Ответ на запрос — $O(\\log n)$, потому что по сути мы делаем один бинпоиск." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Запросы на путях\n", - "\n", - "Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).\n", - "\n", - "Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути.\n", - "\n", - "Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "int get_min (int v, int u) {\n", - " int ans = inf;\n", - " for (int l = logn-1; l >= 0; l--)\n", - " if (!ancestor(up[v][l], u))\n", - " v = up[v][l], ans = min(ans, mn[v][l]);\n", - " for (int l = logn-1; l >= 0; l--)\n", - " if (!ancestor(up[u][l], v))\n", - " u = up[u][l], ans = min(ans, mn[u][l]);\n", - " return min({ans, mn[v][0], mn[u][0]})\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "PXpE5vHfV1hu" - }, - "source": [ - "## Сведение к RMQ\n", - "\n", - "Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Во втором массиве мы по сути выписали наш проход dfs-а.\n", - "\n", - "Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.\n", - "\n", - "Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "b7R7XMv4V1hy" - }, - "source": [ - "### Разреженная таблица\n", - "\n", - "На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \\log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.\n", - "\n", - "Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \\log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## А наоборот можно?*\n", - "\n", - "*Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.\n", - "\n", - "Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот.\n", - "Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.\n", - "\n", - "Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль.\n", - "\n", - "Возьмем константу $k = \\lfloor \\frac{\\log n}{2} \\rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\\frac{2 n}{\\log n})$, и построение будет работать за линейное время:\n", - "\n", - "$$O(\\frac{2 n}{\\log n} \\log \\frac{2 n}{\\log n}) = O(\\frac{2 n}{\\log n} (\\log 2n - \\log \\log n)) = O(n)$$\n", - "\n", - "Также посчитаем для каждой возможной маски размера $\\frac{\\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\\sqrt n$ (ради этого мы и делили логарифм на два).\n", - "\n", - "ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.\n", - "\n", - "Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.\n", - "\n", - "Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear).\n", - "\n", - "Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.\n", - "\n", - "Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.**" - ] - } - ], - "metadata": { - "colab": { - "name": "10_lca.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/img/.ipynb_checkpoints/linalg-checkpoint.ipynb b/img/.ipynb_checkpoints/linalg-checkpoint.ipynb deleted file mode 100644 index f5b8c0b..0000000 --- a/img/.ipynb_checkpoints/linalg-checkpoint.ipynb +++ /dev/null @@ -1,453 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Ликбез по линейной алгебре\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Определение**. Функция $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ называется *линейной*, если для неё выполнено\n", - "\n", - "1. $ f(x+y) = f(x) + f(y) $\n", - "2. $ f(ax) = a f(x), \\; a \\in R $\n", - "\n", - "Примеры:\n", - "* $ f(x) = 0 $\n", - "* $ f(x) = x $\n", - "* $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\\mathbb{R}^3$ в $\\mathbb{R}$)\n", - "* $ f(x) = (x, -x, 0) $ (из $\\mathbb{R}$ в $\\mathbb{R}^3$)\n", - "\n", - "Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства:\n", - "* Сумма линейных функций — линейная функция.\n", - "* Сумма коммутативна: $f+g = g+f$).\n", - "* Сумма ассоциативна: $(f+g)+h = f+(g+h)$.\n", - "* Композиция $f(g(x)) = (f \\circ g)(x)$ линейных функций — линейная функция.\n", - "* Композция ассоциативна: $(f \\circ g) \\circ h = f \\circ (g \\circ h) = f \\circ g \\circ h$.\n", - "* Композиция в общем случае не коммутативна.
\n", - "Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен.\n", - "\n", - "Все свойства можно вывести лишь из этих двух пунктов в определении." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Что такое матрица?\n", - "\n", - "Можно показать, что любую линейную функцию $f: \\mathbb{R}^n \\to \\mathbb{R}^m$ можно представить в таком виде:\n", - "\n", - "$$\n", - "f(x) =\n", - "\\begin{pmatrix}\n", - "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\\\\n", - "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\\\\n", - "\\ldots \\\\\n", - "a_{m1} x_1 + a_{m2} x_2 + \\ldots + a_{mn} x_n \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "*Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$.\n", - "\n", - "$$\n", - "A =\n", - "\\begin{pmatrix}\n", - "a_{11} & a_{12} & \\ldots & a_{1n} \\\\\n", - "a_{21} & a_{22} & \\ldots & a_{2n} \\\\\n", - "\\vdots & \\vdots & \\ddots & \\vdots \\\\\n", - "a_{m1} & a_{m2} & \\ldots & a_{mn} \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "Каждой линейной функции из $\\mathbb{R}^n$ в $\\mathbb{R}^m$ соответствует какая-то матрица размера $n \\times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте.\n", - "\n", - "Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \\circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так:\n", - "\n", - "$$ C = AB: C_{ij} = \\sum_{i=1}^{k} A_{ik} B_{kj} $$\n", - "\n", - "Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$.\n", - "\n", - "Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \\times k$, то вторая должна иметь размер $k \\times m$, то есть «средние» размерности обязательно должны совпадать.\n", - "\n", - "\n", - "\n", - "Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.\n", - "\n", - "Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:\n", - "\n", - "* Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$.\n", - "* Сумма коммутативна: $A+B = B+A$)\n", - "* Сумма ассоциативна: $(A+B)+C = A+(B+C)$\n", - "* Умножение ассоциативно: $(AB)C = A(BC) = ABC$.\n", - "* Умножение в общем случае не коммутативно." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Пример**: матрица поворота в 2d.\n", - "$$\n", - "\\begin{pmatrix}\n", - "\\cos \\alpha & -\\sin \\alpha \\\\\n", - "\\sin \\alpha & \\cos \\alpha \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "**Пример**: матрица проецирования на $Ox$ в 3d.\n", - "$$\n", - "\\begin{pmatrix}\n", - "1 & 0 & 0 \\\\\n", - "0 & 0 & 0 \\\\\n", - "0 & 0 & 0 \\\\\n", - "\\end{pmatrix}\n", - "$$\n", - "\n", - "**Пример**: матрица «свапни $x$ и $y$».\n", - "$$\n", - "\\begin{pmatrix}\n", - "0 & 1 \\\\\n", - "1 & 0 \\\\\n", - "\\end{pmatrix}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Напишем класс, который реализует матричное умножение." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "struct matrix {\n", - " int n, m;\n", - " int t[];\n", - " matrix (int _n, int _m) {\n", - " n = _n, m = _m;\n", - " t = new int(n*m);\n", - " memset(t, 0, sizeof t);\n", - " }\n", - " int[] operator[] (int k) {\n", - " return t[k*m];\n", - " }\n", - "}\n", - "\n", - "matrix operator* (matrix a, matrix b) {\n", - " matrix c(a.n, b.m);\n", - " for (int i = 0; i < a.n; i++)\n", - " for (int j = 0; j < b.m; j++)\n", - " for (int k = 0; k < a.m; k++)\n", - " c[i][j] += a[i][k] * b[i][k];\n", - " return c;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Динамика\n", - "\n", - "Некоторые динамики можно выразить в терминах матричного умножения.\n", - "\n", - "Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "$$\n", - "\\begin{pmatrix}\n", - "f_{n+1} \\\\\n", - "f_{n+2} \\\\\n", - "\\end{pmatrix}\n", - "=\n", - "\\begin{pmatrix}\n", - "0+f_{n+1} \\\\\n", - "f_{n}+f_{n+1} \\\\\n", - "\\end{pmatrix}\n", - "=\n", - "\\begin{pmatrix}\n", - "0 & 1 \\\\\n", - "1 & 1 \\\\\n", - "\\end{pmatrix}\n", - "\\begin{pmatrix}\n", - "f_{n} \\\\\n", - "f_{n+1} \\\\\n", - "\\end{pmatrix}\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$.\n", - "\n", - "Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:\n", - "\n", - "$$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$\n", - "\n", - "Это будет работать за $O(n^3 \\log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "matrix binpow (matrix a, int p) {\n", - " matrix b(n, n);\n", - " for (int i = 0; i < n; i++)\n", - " b[i][i] = 1;\n", - " while (p) {\n", - " if (p&1) b = b*a;\n", - " a = a*a;\n", - " p >>= 1;\n", - " }\n", - " return b;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$.\n", - "\n", - "$$\n", - "\\begin{vmatrix}\n", - "1 & 0 & 0 \\\\\n", - "0 & 1 & 0 \\\\\n", - "0 & 0 & 1 \\\\\n", - "\\end{vmatrix}\n", - "$$\n", - "\n", - "В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \\ldots + a_k f_{n-k}$ имеет такую матрицу перехода:\n", - "\n", - "\\begin{pmatrix}\n", - "0 & 1 & 0 & \\ldots & 0 \\\\\n", - "0 & 0 & 1 & \\ldots & 0 \\\\\n", - "\\vdots & \\vdots & \\vdots & \\ddots & \\vdots \\\\\n", - "0 & 0 & 0 & \\ldots & 1 \\\\\n", - "a_k & a_{k-1} & a_{k-2} & \\ldots & a_1 \\\\\n", - "\\end{pmatrix}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Матрица смежности\n", - "\n", - "У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов.\n", - "\n", - "Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86))." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Обобщения\n", - "\n", - "Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел.\n", - "\n", - "Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Определитель\n", - "\n", - "*Определителем* кадратной матрицы $A$ называется такое выражение:\n", - "\n", - "$$ \\det A = |A| = \\ldots $$ \n", - "\n", - "Где $\\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:\n", - "\n", - "$$\n", - "\\begin{vmatrix}\n", - "a & b \\\\\n", - "c & d \\\\\n", - "\\end{vmatrix}\n", - "=\n", - "ad - bc\n", - "$$\n", - "\n", - "У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.\n", - "\n", - "В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Базис\n", - "\n", - "Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. \n", - "\n", - "Базисы есть не только в линейной алгебре. Например, $\\{1, x, x^2\\}$ является базисом всех квадратных трёхчленов. Или $\\{\\neg, \\land, \\lor\\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Метод Крамера и easy пересечение прямых\n", - "\n", - "Пусть нам надо пересечь две прямые.\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "a_1 x + b_1 y + c_1 = 0\n", - "a_2 x + b_2 y + c_2 = 0\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Это то же самое, что найти такие коэффициенты $x$ и $y$, что\n", - "\n", - "$$ x \\vec{a} + y \\vec{b} = -\\vec{c} $$\n", - "\n", - "Площадь параллелограмма, натянутого на $\\vec{a}$ и $\\vec{b}$, равна векторному произведению, или детерминанту.\n", - "\n", - "По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$.\n", - "\n", - "Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## *Собственные векторы\n", - "\n", - "Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление.\n", - "\n", - "$ Av = k v $, где $k \\neq 0$.\n", - "\n", - "$ Av - kv = (A-kI)v = 0 $. Это означает\n", - "\n", - "Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Системы уравнений и метод Гаусса\n", - "\n", - "Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.\n", - "\n", - "Нас по сути просят решить следующую систему:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "a_{11} x_1 + a_{12} x_2 + \\ldots + a_{1n} x_n \\equiv b_1 \\pmod 2\\\\\n", - "a_{21} x_1 + a_{22} x_2 + \\ldots + a_{2n} x_n \\equiv b_2 \\pmod 2\\\\\n", - "\\ldots \\\\\n", - "a_{n1} x_1 + a_{n2} x_2 + \\ldots + a_{nn} x_n \\equiv b_n \\pmod 2\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку.\n", - "\n", - "Метод Крамера неоптимален — там $O(n^4)$ операций.\n", - "\n", - "В таком случае можно значительно ускорить и упростить обычный метод Гаусса:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "t gauss (matrix a) {\n", - " for (int i = 0; i < n; i++) {\n", - " int nonzero = i;\n", - " for (int j = i+1; j < n; j++)\n", - " if (a[j][i])\n", - " nonzero = j;\n", - " swap(a[nonzero], a[i]);\n", - " for (int j = 0; j < n; j++)\n", - " if (j != i && a[j][i])\n", - " a[j] ^= a[i];\n", - " }\n", - " t x;\n", - " for (int i = 0; i < n; i++)\n", - " x[i] = a[i][n] ^ a[i][i];\n", - " return x;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81))." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/matching-checkpoint.ipynb b/img/.ipynb_checkpoints/matching-checkpoint.ipynb deleted file mode 100644 index e91f1a4..0000000 --- a/img/.ipynb_checkpoints/matching-checkpoint.ipynb +++ /dev/null @@ -1,234 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ipi0UHuYJPXr" - }, - "source": [ - "# Паросочетания\n", - "\n", - "Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "YEm8GM0TKncl" - }, - "source": [ - "**Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$).\n", - "\n", - "Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием.\n", - "\n", - "*Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны.\n", - "\n", - "Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ThCPzQPwU5y0" - }, - "source": [ - "**Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер.\n", - "\n", - "**Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.\n", - "\n", - "**Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию.\n", - "\n", - "![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg)\n", - "\n", - "*Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \\to 8 \\to 4 \\to 6 \\to 3 \\to 7$.*\n", - "\n", - "Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.\n", - "\n", - "В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.\n", - "\n", - "**Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "wrrJplYwbCB8" - }, - "outputs": [], - "source": [ - "const int maxn;\n", - "\n", - "vector g[maxn]; // будем хранить только рёбра из левой доли в правую\n", - "int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой)\n", - "bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом\n", - "\n", - "// dfs возвращает, можно ли найти путь из вершины v\n", - "// в какую-нибудь вершину правой доли\n", - "// если можно, то ещё и проводит чередование\n", - "bool dfs (int v) {\n", - " if (used[v])\n", - " return false;\n", - " used[v] = true;\n", - " for (int u : g[v]) {\n", - " // если вершина свободна, то можно сразу с ней соединиться\n", - " // если она занята, то с нейможно соединиться только тогда,\n", - " // когда из её текущей пары можно найти какую-нибудь другую вершину\n", - " if (mt[u] == -1 || dfs(mt[u])) {\n", - " mt[u] = v;\n", - " return true;\n", - " }\n", - " }\n", - " return false;\n", - "}\n", - "\n", - "\n", - "// где-то в main:\n", - "\n", - "memset(mt, -1, sizeof(mt));\n", - "for (int i = 0; i < n; i++) {\n", - " memset(used, 0, sizeof(mt));\n", - " if (dfs(i))\n", - " cnt++;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "fm3P_M7fKdRQ" - }, - "source": [ - "## Корректность\n", - "\n", - "Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.\n", - "\n", - "**Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным.\n", - "\n", - "**Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \\leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу.\n", - "\n", - "Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hVHj5qr1czGd" - }, - "source": [ - "## Скорость работы\n", - "\n", - "Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$.\n", - " \n", - "Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \\approx 10^4$), потому что для него есть мощные неасимптотические оптимизации:\n", - "\n", - "* Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).\n", - "\n", - "* Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу.\n", - "\n", - "* Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \\times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.\n", - " \n", - "Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \\sqrt m)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Покрытие путями DAG-а\n", - "\n", - "Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.\n", - "\n", - "Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\\overline{H}$ следующим образом:\n", - "* В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно.\n", - "* Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$.\n", - "* Теперь из графа $H$ сделаем граф $\\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$.\n", - "\n", - "Если мы рассмотрим любой путь $v_1, v_2, \\ldots, v_k$ в исходном графе $G$, то в графе $\\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \\ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$.\n", - "\n", - "Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\\overline{H}$ — это паросочетание в $H$ (напомним, это $\\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$.\n", - "\n", - "Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них.\n", - "\n", - "Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "B_YEyFpNUS36" - }, - "source": [ - "## Лемма Холла\n", - "\n", - "*Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.\n", - "\n", - "![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif)\n", - "\n", - "**Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.\n", - "\n", - "**Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием.\n", - "\n", - "В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.\n", - "\n", - "**База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. \n", - "\n", - "**Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием.\n", - "\n", - "Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)\n", - "\n", - "Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \\leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$).\n", - "\n", - "Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "yq3YRWxFJWaY" - }, - "source": [ - "## Для ноулайферов: матроиды\n", - "\n", - "С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid).\n", - "\n", - "Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*.\n", - "\n", - "**Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "21_matching.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/img/.ipynb_checkpoints/matroid-checkpoint.ipynb b/img/.ipynb_checkpoints/matroid-checkpoint.ipynb deleted file mode 100644 index 43ba14e..0000000 --- a/img/.ipynb_checkpoints/matroid-checkpoint.ipynb +++ /dev/null @@ -1,178 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Матроиды" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства:\n", - "\n", - "* Пустое множество является независимым: $\\varnothing \\in I$\n", - "\n", - "* Любое подмножество независимого множества тоже независимо: \n", - "$$A \\subset B, B \\in I \\implies A \\in I$$\n", - "\n", - "* Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$:\n", - "$$A, B \\in I, |A| < |B| \\implies \\exists x \\in B \\setminus A: A \\cup \\{x\\} \\in I$$\n", - "\n", - "Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \\sum w(a_i)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "X.sort()\n", - "s = []\n", - "for x in X:\n", - " if good(s + [x]):\n", - " s += [x]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Здесь под `good` имеется в виду $s \\cup x \\in I$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Корректность этого алгоритма для любого матроида доказывает следующая теорема:\n", - "\n", - "## Теорема Радо-Эдмондса\n", - "\n", - "Пусть $A \\in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \\cup x \\in I,\\;x \\notin A,\\;w(x)$ — минимальна. Тогда $A \\cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "*Доказательство: **\n", - "\n", - "Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$.\n", - "\n", - "Из свойств матроида: $\\exists y \\in B \\setminus A : A \\cup y \\in I$.\n", - "\n", - "Тогда верны два неравенства:\n", - "\n", - "$$\n", - "\\begin{cases}\n", - "w(A \\cup y) = w(A) + w(y) \\geq w(B) \\implies w(A) \\geq w(B) - w(y) \\\\\n", - "w(B \\setminus y) = w(B) - w(y) \\geq w(A) \\implies w(A) \\leq w(B) - w(y)\n", - "\\end{cases}\n", - "$$\n", - "\n", - "Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно,\n", - "$w(A \\cup y) = w(A) + w(y) = w(B)$.\n", - "\n", - "Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \\cup x \\in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Примеры\n", - "\n", - "Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.\n", - "\n", - "### Минимальный остов\n", - "Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом:\n", - "\n", - "* Граф без ребер является лесом.\n", - "* Если удалить из леса ребра, он останется лесом.\n", - "* Пусть есть два леса $|A| \\leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \\cup \\{x\\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности.\n", - "\n", - "Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.\n", - "\n", - "### Расписания\n", - "Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.\n", - "\n", - "Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \\geq i$ для всех $i$.\n", - "\n", - "Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: \n", - "\n", - "* Пустой набор заданий всегда можно сделать.\n", - "* Если у нас стало меньше заданий, то их сделать мы тоже успеем.\n", - "* Пусть есть два правильных набора $|A| \\leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \\cup x$ — тоже правильный набор.\n", - "\n", - "Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных.\n", - "\n", - "### Паросочетания\n", - "Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом:\n", - "\n", - "* Любое паросочетание покрывает пустое множество вершин.\n", - "* Исходное паросочетание покрывает также и любое подмножество исходных вершин.\n", - "* Пусть есть два множества вершин $|A| \\leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$.\n", - "\n", - "Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна.\n", - "\n", - "### Линейно независимые вектора\n", - "\n", - "(TODO) (Школьники не обязаны знать линал.)\n", - "\n", - "Такие штуки будем называть базисами.\n", - "\n", - "* Ноль есть в любом базисе.\n", - "* Подмножество базиса — базис.\n", - "* ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.6" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb b/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb deleted file mode 100644 index b8487b7..0000000 --- a/img/.ipynb_checkpoints/mincost-maxflow-checkpoint.ipynb +++ /dev/null @@ -1,239 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Поток минимальной стоимости\n", - "\n", - "Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна:\n", - "\n", - "$$ \\sum_{(u, v) \\in E} f_{uv} \\to \\max $$\n", - "$$ \\sum_{(u, v) \\in E} f_{uv} w_{uv} \\to \\min $$\n", - "\n", - "Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Критерий оптимальности\n", - "\n", - "Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот).\n", - "\n", - "**Доказательство:** \n", - "\n", - "$\\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению.\n", - "\n", - "$\\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Отмена циклов\n", - "\n", - "Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу.\n", - "\n", - "Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Дополняющие пути\n", - "\n", - "Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.\n", - "\n", - "**Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса.\n", - "\n", - "Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз.\n", - "\n", - "Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Потенциалы Джонсона\n", - "\n", - "Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:\n", - "\n", - "$$ w_{uv}' = w_{uv} + d_u - d_v $$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение 1**. Веса всех рёбер графа неотрицательные.\n", - "\n", - "**Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$?\n", - "\n", - "Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой:\n", - "$$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими.\n", - "\n", - "**Доказательство**. Распишем новую стоимость пути из $a$ в $z$.\n", - "\n", - "$$\n", - "\\begin{align}\n", - "w_{ab}' + \\ldots + w_{yz}'\n", - "&= (w_{ab} + \\ldots + w_{yz}) + (d_a + \\ldots + d_y) - (d_b + \\ldots + d_z)\n", - "\\\\&= (w_{ab} + \\ldots + w_{yz}) + d_a - d_z\n", - "\\end{align}\n", - "$$\n", - "\n", - "Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу.\n", - "\n", - "Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.\n", - "\n", - "Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими).\n", - "\n", - "**Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Итоговый алгоритм\n", - "\n", - "* Модифицируем сеть, добавивив обратные рёбра.\n", - "* Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.\n", - "* Пока максимальный поток не найден:\n", - "* * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$.\n", - "* * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \\leadsto t$, обновить остаточную сеть." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Асимптотика\n", - "\n", - "Алгоритм работает за $O(U m \\log n)$ или $O(U n^2)$ в случае плотных графов.\n", - "\n", - "В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \\leq n$ или $U \\leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Реализация\n", - "\n", - "Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).\n", - "\n", - "* `cost`, `cap` — параметры сети\n", - "* `pot` — потенциалы\n", - "* `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)\n", - "* `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "const int maxn = 305, inf = 1e9;\n", - "\n", - "int n;\n", - "int cost[maxn][maxn], cap[maxn][maxn];\n", - "int d[maxn], pot[maxn], par[maxn];\n", - "\n", - "bool dijkstra (int s, int t) {\n", - " used[maxn] = {0};\n", - "\n", - " fill(d, d+n, inf);\n", - " d[s] = 0;\n", - "\n", - " while (1) {\n", - " int v = -1;\n", - " for (int u = 0; u < n; u++)\n", - " if (!used[u] && (v == -1 && d[u] < d[v]))\n", - " v = u;\n", - " if (v == -1 || d[v] == inf)\n", - " break;\n", - " used[v] = 1;\n", - " for (int u = 0; u < n; u++) {\n", - " int w = cost[v][u] + pot[v] - pot[u];\n", - " if (cap[v][u] && d[u] > d[v] + w) {\n", - " d[u] = d[v] + w;\n", - " par[u] = v;\n", - " }\n", - " }\n", - " }\n", - "\n", - " return d[t] < inf;\n", - "}\n", - "\n", - "int mincost_maxflow (int s, int t) {\n", - " int ans = 0;\n", - " while (dijkstra(s, t)) {\n", - " memcpy(pot, d, sizeof(d));\n", - " int delta = inf;\n", - " for (int v = t; v != s; v = par[v])\n", - " delta = min(delta, cap[par[v]][v]);\n", - " for (int v = t; v != s; v = par[v]) {\n", - " cap[par[v]][v] -= delta;\n", - " cap[v][par[v]] += delta;\n", - " ans += cost[par[v]][v]*delta;\n", - " }\n", - " }\n", - " return ans;\n", - "}" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/segtree-checkpoint.ipynb b/img/.ipynb_checkpoints/segtree-checkpoint.ipynb deleted file mode 100644 index a1dfb3b..0000000 --- a/img/.ipynb_checkpoints/segtree-checkpoint.ipynb +++ /dev/null @@ -1,680 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Nyg7f6SD69cK" - }, - "source": [ - "# Дерево отрезков" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "bYsinHWyrf6J" - }, - "source": [ - "**Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "LQzmaXb_G-r4" - }, - "source": [ - "**Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "UmP1mb6M69cM" - }, - "source": [ - "Рассмотрим конкретную задачу:\n", - "\n", - ">Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов:\n", - "\n", - ">1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`).\n", - ">2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$.\n", - "\n", - ">Оба запроса нужно обрабатывать за время $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "CG0jO6Nt69cN" - }, - "source": [ - "Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:\n", - "\n", - "Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.\n", - "\n", - "Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков»." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "f8HIyZkR69cO" - }, - "source": [ - "![alt text](http://i.imgur.com/GGBmcEP.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "084RAPPptpaJ" - }, - "source": [ - "Строить его можно рекурсивной функцией:\n", - "* Если вершина является листом, взять в качестве суммы значение соответствующей ячейки.\n", - "* Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "NNMuiaKW69cQ" - }, - "source": [ - "## Разные свойства\n", - "\n", - "Высота такого дерева есть величина $\\Theta(\\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики.\n", - "\n", - "Более того, любой полуинтервал разбивается на $O(\\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков.\n", - "\n", - "Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \\frac{n}{2} + \\frac{n}{4} + \\frac{n}{8} + \\ldots + 1 < 2n$. Значит, оно линейное по памяти.\n", - "\n", - "При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "NQDy4Wn_69cT" - }, - "source": [ - "## Ок, как это нам поможет?\n", - "\n", - "Опишем теперь, как с помощью такой структуры решить задачу.\n", - "\n", - "**Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$.\n", - "\n", - "Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\\Theta(\\log n)$ — по одной с каждого уровня.\n", - "\n", - "Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков.\n", - "\n", - "**Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения.\n", - "\n", - "Сделаем тоже рекурсивную функцию, рассмотрев три случая:\n", - "\n", - "* Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму.\n", - "* Если отрезки вершины и запроса не пересекаются, то вернуть 0.\n", - "* Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей.\n", - "\n", - "Чтобы разобраться, почему это работает за $O(\\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\\log n)$ отрезках, а значит и итоговая асимптотика будет такая же." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "fJ3qz5gzQYHO" - }, - "source": [ - "## Ликбез по C++\n", - "\n", - "Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества.\n", - "\n", - "Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "HPeG6w4DRHG4" - }, - "source": [ - "Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\\log n)$.\n", - "\n", - "В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hWKKu4mgQ4kA" - }, - "source": [ - "У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "CodmY2a4QmjN" - }, - "outputs": [], - "source": [ - "struct A {\n", - " int param1, param2; // тут можно что-то хранить\n", - " char param3 = 'k';\n", - " A (int var) {\n", - " // эта часть называется конструктором\n", - " // ...\n", - " }\n", - " void do_something () {\n", - " // это какой-то другой метод\n", - " // ...\n", - " }\n", - "}; // <- не забудьте точку с запятой" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "6xCBNd6TR1vD" - }, - "source": [ - "Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве.\n", - "\n", - "Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "dHxKkzeLsaLW" - }, - "outputs": [], - "source": [ - "A x(179);\n", - "x.do_something();\n", - "x.param1 = 57;\n", - "\n", - "A *y = new A(42); // new возвращает адрес, по которому можно найти объект\n", - "y->do_something();\n", - "y.param3 = '!';" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "6xy-KwmI2DiJ" - }, - "source": [ - "Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Z3VyDnQ769cU" - }, - "source": [ - "## Реализация" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "EKO6Hwdl69cV" - }, - "source": [ - "Общий план реализации любых структур данных:\n", - "1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc.\n", - "2. Формально описать, что должны делать методы и за какую асимптотику.\n", - "3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "x_z_73w-69cW" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb; // левые и правые границы отрезков\n", - " int sum = 0; // сумма на текущем отрезке\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " if (lb + 1 < rb) {\n", - " // если не лист, создаем детей\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void add (int k, int x) {\n", - " sum += x;\n", - " if (l) {\n", - " if (k < l->rb)\n", - " l->add(k, x);\n", - " else\n", - " r->add(k, x);\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " if (lb >= lq && rb <= rq)\n", - " // если мы лежим полностью в отрезке запроса, вывести сумму\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " // если мы не пересекаемся с отрезком запроса, вывести ноль\n", - " return 0;\n", - " // иначе всё сложно -- запускаемся от детей и пусть они там сами решают\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "30NQsY2jytu2" - }, - "source": [ - "> Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$).\n", - "\n", - "Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи:\n", - "* Запросим сумму от $k$ до $n$ в ДО.\n", - "* Добавим единичку в $k$-тую позицию в ДО.\n", - "\n", - "Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \\log n)$ запросов." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "S8DH5Zxg3UqU" - }, - "source": [ - "> Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике.\n", - "\n", - "Ответим на все запросы в оффлайн, используя метод сканирующей прямой:\n", - "\n", - "* Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \\times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \\times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \\times [y_1, y_2]$.\n", - "* Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше.\n", - "* Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$».\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "47Fb26oz69ca" - }, - "source": [ - "## Отложенные операции" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "tANMKBYT5CVZ" - }, - "source": [ - "Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному.\n", - "\n", - "Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем.\n", - "\n", - "Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено.\n", - "\n", - "Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.\n", - "\n", - "Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "eHe1O2d769cc" - }, - "source": [ - "При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "rJo8yvgHUPHL" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb;\n", - " int sum = 0, assign = -1;\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " if (lb + 1 < rb) {\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void push () {\n", - " if (assign != -1) {\n", - " sum = (rb-lb) * assign;\n", - " if (l) { // если дети есть\n", - " l->assign = assign;\n", - " r->assign = assign;\n", - " }\n", - " }\n", - " assign = -1;\n", - " }\n", - " void upd (int lq, int rq, int x) {\n", - " push();\n", - " if (lq <= lb && rb <= rq)\n", - " assign = x;\n", - " else if (l && max(lb, lq) < min(rb, rq)) {\n", - " // если есть дети и отрезок запроса хоть как-то пересекается с нашим\n", - " l->upd(lq, rq, x);\n", - " r->upd(lq, rq, x);\n", - " // ...дальше они сами разберутся\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " push();\n", - " if (lb >= lq && rb <= rq)\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " return 0;\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "_DmNvAVzMzq4" - }, - "source": [ - "По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах.\n", - "\n", - "Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "oGLSfyMG69cd" - }, - "source": [ - "## Динамическое построение" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "fZM4fTzu69cd" - }, - "source": [ - "А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\\log_2 10^6 \\approx 20$, $\\log_2 10^9 \\approx 30$), кроме этапа построения.\n", - "\n", - "Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.\n", - "\n", - "Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "z8PYwWA9niAY" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb;\n", - " int sum = 0;\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " // а тут ничего нет\n", - " }\n", - " void extend () {\n", - " if (!l && lb + 1 < rb) {\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void add (int k, int x) {\n", - " extend();\n", - " sum += x;\n", - " if (l) {\n", - " if (k < l->rb)\n", - " l->add(k, x);\n", - " else\n", - " r->add(k, x);\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " if (lb >= lq && rb <= rq)\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " return 0;\n", - " extend();\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "SSlRrVNXxREm" - }, - "source": [ - "Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "lGfAy5Af1S5J" - }, - "outputs": [], - "source": [ - "vector compress (vector a) {\n", - " vector b = a;\n", - " sort(b.begin(), b.end());\n", - " b.erase(unique(b.begin(), b.end()), b.end());\n", - " for (int &x : a) \n", - " x = int(lower_bound(b.begin(), b.end(), x) - b.begin());\n", - " return a;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "eRM_sfhirR_W" - }, - "source": [ - "## Персистентность\n", - "\n", - "Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния.\n", - "\n", - "Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.\n", - "\n", - "У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \\log n)$." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "OA93mqmdsO46" - }, - "outputs": [], - "source": [ - "struct segtree {\n", - " int lb, rb;\n", - " int sum = 0;\n", - " segtree *l = 0, *r = 0;\n", - " segtree (int _lb, int _rb) {\n", - " lb = _lb, rb = _rb;\n", - " if (lb != rb) {\n", - " int t = (lb + rb) / 2;\n", - " l = new segtree(lb, t);\n", - " r = new segtree(t, rb);\n", - " }\n", - " }\n", - " void copy () {\n", - " if (l) {\n", - " l = new segtree(l);\n", - " r = new segtree(r);\n", - " }\n", - " }\n", - " void add (int k, int x) {\n", - " copy();\n", - " sum += x;\n", - " if (l) {\n", - " if (k < l->rb) l->add(k, x);\n", - " else r->add(k, x);\n", - " }\n", - " }\n", - " int get_sum (int lq, int rq) {\n", - " // этот метод ничего не меняет -- он и так хороший\n", - " if (lq <= lb && rb <= rq)\n", - " return sum;\n", - " if (max(lb, lq) >= min(rb, rq))\n", - " return 0;\n", - " return l->get_sum(lq, rq) + r->get_sum(lq, rq);\n", - " }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "kMg_NB4AsRMa" - }, - "source": [ - "> Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике.\n", - "\n", - "Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \\log n)$ памяти.\n", - "\n", - "> Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке.\n", - "\n", - "Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу.\n", - "\n", - "Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`.\n", - "\n", - "Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом.\n", - "\n", - "> Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\\frac{r-l}{2}$ раз.\n", - "\n", - "У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\\frac{1}{2^{100}}$, и ей на практике можно пренебречь.\n", - "\n", - "Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "sUXS9Q1zQO1P" - }, - "source": [ - "## Другие реализации\n", - "\n", - "Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:\n", - "\n", - "**На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree).\n", - "\n", - "**«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "yCjbp1XhwsU2" - }, - "source": [ - "## Задачи\n", - "\n", - "* [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции.\n", - "* Второй контест — на отложенные операции.\n", - "* [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "1-2_segtree.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb b/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb deleted file mode 100644 index d7e57b7..0000000 --- a/img/.ipynb_checkpoints/sparse-table-checkpoint.ipynb +++ /dev/null @@ -1,119 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Разреженная таблица\n", - "\n", - "- Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \\log n)$ с малой константой.\n", - "- Обновления не поддерживает (static RMQ).\n", - "- Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.\n", - "- Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).\n", - "- Требует $O(n \\log n)$ памяти.\n", - "- Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Определим разреженную таблицу как двумерный массив размера $n \\times\\log n$:\n", - "\n", - "$$\n", - "t[i][k] = \\min \\{ a_i, a_{i+1}, \\ldots, a_{i+2^k-1} \\}\n", - "$$\n", - "\n", - "Идея такая: считаем минимум на каждом отрезке длины $2^k$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Такой массив можно посчитать за его размер: $t[i][k] = \\min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "int a[maxn], lg[maxn], mx[maxn][logn];\n", - "\n", - "int rmq (int l, int r) {\n", - " int t = lg[r-l+1];\n", - " return min(mx[l][t], mx[r-(1<= 0; i--) {\n", - " mx[i][0] = a[i];\n", - " for (int l = 0; l < logn-1; l++)\n", - " mx[i][l+1] = max(mx[i][l], mx[i+(1< Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него.\n", - "\n", - "Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \\cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго.\n", - "\n", - "Для решения этой задачи за линейное время придумали **префикс-функцию**.\n", - "\n", - "**Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \\ldots s_i$, который также является и суффиксом этой строки (не считая всю строку).\n", - "\n", - "Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 638, - "status": "ok", - "timestamp": 1541170751859, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "RUT-T92559lq", - "outputId": "b2a1115c-11ec-46c8-bc95-a62aa3458597" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[0, 0, 1, 0, 1, 2, 3, 4]" - ] - }, - "execution_count": 7, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - } - ], - "source": [ - "def slow_prefix_function(s):\n", - " n = len(s)\n", - " p = [0]*n\n", - " for i in range(n):\n", - " prefix = s[:i]\n", - " for l in range(1, i):\n", - " if prefix[:l] == prefix[-l:]:\n", - " p[i] = l\n", - " return p\n", - "\n", - "slow_prefix_function('aataataa')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "rm6kd4D86GG3" - }, - "source": [ - "(Этот алгоритм работает за $O(n^3)$, но это только пока.)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "1-8TSS2X3KV_" - }, - "source": [ - "## Как это поможет решить исходную задачу?\n", - "\n", - "Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.\n", - "\n", - "Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\\#t$." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 74 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 615, - "status": "ok", - "timestamp": 1541171240549, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "ppqZBPNJ43v3", - "outputId": "c23a3d9f-79aa-4f91-aff1-c7b574018535" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!\n", - "00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000\n" - ] - } - ], - "source": [ - "s = \"let it go\"\n", - "t = \"\"\"let it go, let it go\n", - "can't hold it back anymore\n", - "let it go, let it go\n", - "turn away and slam the door!\"\"\"\n", - "\n", - "print((s + '#' + t).replace('\\n', ' '))\n", - "print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "42poAv3b44FM" - }, - "source": [ - "Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$.\n", - "\n", - "Такой алгоритм (посчитать префикс-функцию от $s\\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "mTIyGVuD3F8W" - }, - "source": [ - "## Как её быстро считать\n", - "\n", - "Рассмотрим ещё несколько примеров префикс-функций:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 90 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 610, - "status": "ok", - "timestamp": 1541170871458, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "GqsPPTjl75Wp", - "outputId": "2b05fe9f-5eab-44bb-e651-c207c0723ae4" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0, 0, 1, 2, 3]\n", - "[0, 0, 0, 0, 0, 0]\n", - "[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]\n", - "[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n" - ] - } - ], - "source": [ - "for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:\n", - " print(slow_prefix_function(s))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "HO_PjgzR7514" - }, - "source": [ - "Можно заметить несколько особенностей:\n", - "* $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс;\n", - "* $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \\ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \\ldots s_i$, длина которого будет ровно на единицу меньше.\n", - "\n", - "Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения.\n", - "\n", - "Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\\underbrace{aabaa}t\\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$.\n", - "\n", - "Но что происходит, когда $s_{p_i}\\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$.\n", - "* $\\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5.\n", - "* $\\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa».\n", - "* $\\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$.\n", - "* $\\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$.\n", - "\n", - "В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \\neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "colab_type": "code", - "executionInfo": { - "elapsed": 844, - "status": "ok", - "timestamp": 1541171880825, - "user": { - "displayName": "Сергей Слотин", - "photoUrl": "https://lh3.googleusercontent.com/-_FG0PkHG5nQ/AAAAAAAAAAI/AAAAAAAAAFY/Sckq9Gkm-mc/s64/photo.jpg", - "userId": "13456807618518804390" - }, - "user_tz": -180 - }, - "id": "n22PhVaYUlp4", - "outputId": "830132ae-5568-4c3d-ae32-3ecaf51e7734" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]" - ] - }, - "execution_count": 20, - "metadata": { - "tags": [] - }, - "output_type": "execute_result" - } - ], - "source": [ - "def fast_prefix_function(s):\n", - " n = len(s)\n", - " p = [0]*n\n", - " for i in range(1, n):\n", - " cur = p[i - 1]\n", - " # перебираем префикс-функцию, пока не найдем равный символ\n", - " while s[i] != s[cur] and cur > 0:\n", - " cur = p[cur - 1]\n", - " # если нашли, то значение на единицу больше\n", - " if s[i] == s[cur]:\n", - " p[i] = cur + 1\n", - " return p\n", - "\n", - "fast_prefix_function('abacabadabacabax')" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "skwRhzxQUlp9" - }, - "source": [ - "### Асимптотика\n", - "\n", - "Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой.\n", - "\n", - "**Доказательство**:\n", - "* Как мы ранее заметили, префикс функция возрастает максимум на единицу.\n", - "* $\\implies$ Вырасти она может максимум $n-1$ раз.\n", - "* Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один.\n", - "* $\\implies$ Количество понижений не больше количества повышений, то есть $O(n)$.\n", - "* $\\implies$ Суммарно итераций цикла `while` — $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "cDkYM1JMUlqC" - }, - "source": [ - "## Z-функция\n", - "\n", - "Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания.\n", - "\n", - "Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$.\n", - "\n", - "$$\\underbrace{aba}c\\overbrace{aba}daba \\hspace{1em} (z_4 = 3)$$" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]\n", - "[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]\n", - "[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]\n" - ] - } - ], - "source": [ - "def slow_z_function(s):\n", - " n = len(s)\n", - " z = [0]*n\n", - " for i in range(1, n):\n", - " suffix = s[i:]\n", - " for l in range(1, i):\n", - " if s[:l] == suffix[:l]:\n", - " z[i] = l\n", - " return z\n", - "\n", - "for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:\n", - " print(slow_z_function(s))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Как её быстро считать\n", - "\n", - "Заметим, что:\n", - "* $z_0 = 0$ — из-за договоренности (потому что информации не несет);\n", - "* $z_i = 0 \\iff s_i \\neq s_0$;\n", - "* $z_i > 0 \\iff s_i = s_0$.\n", - "\n", - "Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$.\n", - "\n", - "Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее.\n", - "* Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком.\n", - "* Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "def fast_z_function(s):\n", - " n = len(s)\n", - " z = [0]*n\n", - " l = 0\n", - " r = 0\n", - " for i in range(1, n):\n", - " if i <= r:\n", - " z[i] = min(r - i + 1, z[i-l])\n", - " while i + z[i] < n and s[z[i]] == s[i+z[i]]:\n", - " z[i] += 1\n", - " if i + z[i] - 1 > r:\n", - " l = i\n", - " r = i + z[i] - 1\n", - " return z" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Зачем тогда люди используют префикс-функцию\n", - "\n", - "![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg)\n", - "\n", - "В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.\n", - "\n", - "Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение." - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "8_kmp-trie.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.1" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb b/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb deleted file mode 100644 index 16bf12b..0000000 --- a/img/.ipynb_checkpoints/suffix-array-checkpoint.ipynb +++ /dev/null @@ -1,158 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Суффиксный массив\n", - "\n", - "Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.\n", - "\n", - "Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.\n", - "\n", - "\n", - "\n", - "*
«Паблик с тупыми шутками про проганье»
*" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Мотивация\n", - "\n", - "**Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки.\n", - "\n", - "\n", - "\n", - "**Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно.\n", - "\n", - "В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс.\n", - "\n", - "Работать такой алгоритм будет за $O(|t| \\log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \\log |s|)$, что является одним из самых оптимальных алгоритмов поиска.\n", - "\n", - "Теперь научимся его строить." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Построение за $O(n \\log n)$\n", - "\n", - "Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара.\n", - "\n", - "Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \\log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \\log^2 n)$. Но это не самый быстрый и удобный алгоритм.\n", - "\n", - "Наш алгоритм будет состоять из $\\lceil \\log n \\rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив.\n", - "\n", - "Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов).\n", - "\n", - "Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4.\n", - "\n", - "$$\n", - " p_0 = (0, 1, 3, 2) \\;\\;\\; c_0 = (0, 0, 1, 0)\n", - "\\\\ p_1 = (0, 3, 1, 2) \\;\\;\\; c_1 = (0, 1, 2, 0)\n", - "\\\\ p_2 = (3, 0, 1, 2) \\;\\;\\; c_2 = (1, 2, 3, 0)\n", - "$$\n", - "\n", - "Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом.\n", - "\n", - "Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором.\n", - "\n", - "Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \\log^2 n)$, потому что каждый этап будет работать за $O(n \\log n$).\n", - "\n", - "**Оптимизация до $O(n \\log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "// строка -- это последовательность чисел от 1 до размера алфавита\n", - "vector suffix_array (vector &s) {\n", - " s.push_back(0); // добавляем нулевой символ в конец строки\n", - " int n = (int) s.size(),\n", - " cnt = 0, // вспомогательная переменная: счётчик для сортировки \n", - " cls = 0; // количество классов эквивалентности\n", - " vector c(n), p(n);\n", - " \n", - " map< int, vector > t;\n", - " for (int i = 0; i < n; i++)\n", - " t[s[i]].push_back(i);\n", - " \n", - " // «нулевой» этап\n", - " for (auto &x : t) {\n", - " for (int u : x.second)\n", - " c[u] = cls, p[cnt++] = u;\n", - " cls++;\n", - " }\n", - " \n", - " // пока все суффиксы не стали уникальными\n", - " for (int l = 1; cls < n; l++) {\n", - " vector< vector > a(cls); // массив для сортировки подсчётом\n", - " vector _c(n); // новые классы эквивалентности\n", - " int d = (1<(p.begin()+1, p.end());\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "TODO: переписать это" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Наибольшие общие префиксы\n", - "\n", - "Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами.\n", - "\n", - "TODO" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/img/.ipynb_checkpoints/treap-checkpoint.ipynb b/img/.ipynb_checkpoints/treap-checkpoint.ipynb deleted file mode 100644 index abe71e6..0000000 --- a/img/.ipynb_checkpoints/treap-checkpoint.ipynb +++ /dev/null @@ -1,956 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "dMVa59dX96P7" - }, - "source": [ - "# Декартово дерево" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ya9SKeu196QG" - }, - "source": [ - "Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века.\n", - "\n", - "Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.\n", - "\n", - "Декартово дерево же определяется и строится так:\n", - "\n", - "* Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*.\n", - "* Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*.\n", - "* От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.\n", - "* Аналогично, запустимся от правой части и добавим корню правого сына.\n", - "\n", - "Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно.\n", - "\n", - "Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "eRd8qABz96QP" - }, - "source": [ - "![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "DLS2nKEPK1lC" - }, - "source": [ - "Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий:\n", - "\n", - "* Дерамида (дерево + пирамида)\n", - "* ПиВо (пирамида + дерево)\n", - "* КуРево (куча + дерево)\n", - "* Treap (tree + heap)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "wSD-3iTFK7s8" - }, - "source": [ - " ## Бинарные деревья\n", - " \n", - " С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например:\n", - "\n", - "* добавить число $x$ в множество\n", - "* определить, есть ли в множестве число $x$\n", - "* найти первое число, не меньшее $x$ (`lower_bound`)\n", - "* найти количество чисел в промежутке $[l, r]$\n", - "\n", - "При этом все операции — за $O(\\log n)$.\n", - "\n", - "На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\\log n)$." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "2zy0SLY296QW" - }, - "source": [ - "## Приоритеты и асимптотика\n", - "\n", - "В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Ox2hsVjW96Qg" - }, - "source": [ - "**Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \\log n)$.\n", - "\n", - "Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.\n", - "\n", - "**Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*.\n", - "\n", - "Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна\n", - "\n", - "$$d_i = \\sum_{j=1}^n a(j, i)$$\n", - "\n", - "Её матожидание равно\n", - "\n", - "$$E[d_i] = E[\\sum_{j \\neq i} a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} E[a(j, i)] = \\sum_{j \\neq i} p(j, i)$$\n", - "\n", - "где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.\n", - "\n", - "Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.\n", - "\n", - "**Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$).\n", - "\n", - "**Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. \n", - "\n", - "**Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет.\n", - "\n", - "У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей:\n", - "\n", - "$$p(x, y) = \\frac{1}{y-x+1}$$\n", - "\n", - "Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:\n", - "\n", - "$$E[d_i] = \\sum_{j \\neq i} p(j, i) = \\sum_{j \\neq i} \\frac{1}{|i-j|+1} \\leq \\sum_{i=1}^n \\frac{1}{n} = O(\\log n)$$\n", - "\n", - "Перед последним переходом мы получили сумму гармонического ряда.\n", - "\n", - "Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя.\n", - "\n", - "**Упражнение***. Выведите из этого доказательства асимптотику `quicksort`." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "qmboul4Bfkpc" - }, - "source": [ - "## Реализация" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "G0lF7L2E96Qj" - }, - "source": [ - "Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "alEQhUGK46qI" - }, - "source": [ - "Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "11VDG7yx96Qm" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " int key, prior;\n", - " Node *l = 0, *r = 0;\n", - " Node (int _key) { key = _key, prior = rand(); }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "kTPpsriu96Qx" - }, - "source": [ - "Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Vnu5kpTE96Q0" - }, - "source": [ - "### Merge\n", - "\n", - "Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.\n", - "\n", - "Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше.\n", - "\n", - "Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "1pdbdJ_W96Q2" - }, - "outputs": [], - "source": [ - "Node* merge (Node *l, Node *r) {\n", - " if (!l) return r;\n", - " if (!r) return l;\n", - " if (l->prior > r->prior) {\n", - " l->r = merge(l->r, r);\n", - " return l;\n", - " }\n", - " else {\n", - " r->l = merge(l, r->l);\n", - " return r;\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "byktXFHw96RA" - }, - "source": [ - "### Split\n", - "\n", - "Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$.\n", - "\n", - "В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "jyUv7F-o96RD" - }, - "outputs": [], - "source": [ - "typedef pair Pair;\n", - "\n", - "Pair split (Node *p, int x) {\n", - " if (!p) return {0, 0};\n", - " if (p->key <= x) {\n", - " Pair q = split(p->r, x);\n", - " p->r = q.first;\n", - " return {p, q.second};\n", - " }\n", - " else {\n", - " Pair q = split(p->l, x);\n", - " p->l = q.second;\n", - " return {q.first, p};\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "pOVCO70W96RN" - }, - "source": [ - "### Пример: вставка\n", - "\n", - "`merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. \n", - "\n", - "Вот так, например, будет выглядеть код, добавляющий $x$ в сет." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "wpMr1dwA96RQ" - }, - "outputs": [], - "source": [ - "Node *root = 0;\n", - "\n", - "void insert (int x) {\n", - " Pair q = split(root, x);\n", - " Node *t = new Node(x);\n", - " root = merge(q.first, merge(t, q.second));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "VhOc9WdYjOC0" - }, - "source": [ - "### Пример: модификация для суммы на отрезке\n", - "\n", - "Иногда нам нужно написать какие-то модификации для более продвинутых операций.\n", - "\n", - "Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке»." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "j04PUXvmun3y" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " int val, sum;\n", - " // ...\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "nIYZ3qnxvKiV" - }, - "source": [ - "При `merge` и `split` надо будет поддерживать эту сумму актуальной.\n", - "\n", - "Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "BH_-Bjc4utNt" - }, - "outputs": [], - "source": [ - "void sum (Node* v) { return v ? v->sum : 0; }\n", - "// обращаться по пустому указателю нельзя -- выдаст ошибку\n", - "\n", - "void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; }" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "iduA9cMYjyLI" - }, - "source": [ - "В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "SZod8er7wPVL" - }, - "outputs": [], - "source": [ - "Node* merge (Node *l, Node *r) {\n", - " // ...\n", - " if (...) {\n", - " l->r = merge(l->r, r);\n", - " upd(l);\n", - " return l;\n", - " }\n", - " else {\n", - " // ...\n", - " }\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "5RKCFfiWwZZc" - }, - "outputs": [], - "source": [ - "typedef pair Pair;\n", - "\n", - "Pair split (Node *p, int x) {\n", - " // ...\n", - " if (...) {\n", - " // ...\n", - " upd(p);\n", - " return {p, q.second};\n", - " }\n", - " else {\n", - " // ...\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "gRsq8Pqbvsbb" - }, - "source": [ - " Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Gox_llXkdJct" - }, - "outputs": [], - "source": [ - "int sum (int l, int r) {\n", - " Pair rq = split(root, r);\n", - " Pair lq = split(rq.first, l);\n", - " int res = sum(lr.second);\n", - " root = merge(lq.first, merge(lq.second, rq.second));\n", - " return res;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "CSPnJjIe96Rc" - }, - "source": [ - "# Неявный ключ" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "MwlB7zcTOqr-" - }, - "source": [ - "Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.\n", - "\n", - "Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.\n", - "\n", - "Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:\n" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "uL8WtJztiOHy" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " int key, prior, size = 1;\n", - " // ^ размер поддерева\n", - " Node *l = 0, *r = 0;\n", - " Node (int _key) { key = _key, prior = rand(); }\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "sl4BYIseiRoq" - }, - "source": [ - "Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "c9oX5sP_kHhx" - }, - "outputs": [], - "source": [ - "int size (Node *v) { return v ? v->size : 0; }\n", - "\n", - "void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); }" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "n8r-PdHdkIRm" - }, - "source": [ - "`merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа.\n", - "\n", - "Про `split` теперь удобнее думать как \"вырежи первые `k` элементов\". " - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "G2ck_VLK96Rk" - }, - "outputs": [], - "source": [ - "typedef pair Pair;\n", - "\n", - "Pair split (Node *p, int k) {\n", - " if (!p) return {0, 0};\n", - " if (size(p->l) + 1 <= k) {\n", - " Pair q = split(p->r, k - size(p->l) - 1);\n", - " // ^ правый сын не знает количество вершин слева от него\n", - " p->r = q.first;\n", - " upd(p);\n", - " return {p, q.second};\n", - " }\n", - " else {\n", - " Pair q = split(p->l, k);\n", - " p->l = q.second;\n", - " upd(p);\n", - " return {q.first, p};\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "na66j6wGkeW1" - }, - "source": [ - "Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "PqDn4on8gx4u" - }, - "source": [ - "### Пример: ctrl+x, ctrl+v" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "tSiJl_dEjE-1" - }, - "outputs": [], - "source": [ - "Node* ctrlx (int l, int r) {\n", - " Pair q1 = split(root, r);\n", - " Pair q2 = split(q1.first, l);\n", - " root = merge(q2.first, q1.second);\n", - " return q2.second;\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "7VUUuQhpD3r0" - }, - "outputs": [], - "source": [ - "void ctrlv (Node *v, int k) {\n", - " Pair q = split(root, k);\n", - " root = merge(q.first, merge(v, q.second));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "hTmR8xpkgp3a" - }, - "source": [ - "### Пример: переворот\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "d-lA26yQZVUZ" - }, - "source": [ - "Нужно за $O(\\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д.\n", - "\n", - "Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "q7ic3tQ4jWco" - }, - "outputs": [], - "source": [ - "struct Node {\n", - " bool rev;\n", - " // ...\n", - "};" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "Bg-tJvl-jfql" - }, - "source": [ - "Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "Q7nC0YI0jeTi" - }, - "outputs": [], - "source": [ - "void push (node *v) {\n", - " if (v->rev) {\n", - " swap(v->l, v->r);\n", - " if (v->l)\n", - " v->rev ^= 1;\n", - " if (v->r)\n", - " v->rev ^= 1;\n", - " }\n", - " v->rev = 0;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "40Ir36SwZ5GN" - }, - "source": [ - "Аналогично, эту функцию будем вызывать в начале `merge` и `split`.\n", - "\n", - "Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "8Ev2xhTSjRgI" - }, - "outputs": [], - "source": [ - "void reverse (int l, int r) {\n", - " Pair q1 = split(root, r);\n", - " Pair q2 = split(q1.first, l)\n", - " q2.second->rev ^= 1;\n", - " root = merge(q2.first, merge(q2.second, q1.second));\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "9BI3binGkCmg" - }, - "source": [ - "# Функциональное программирование*\n", - "\n", - "Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно.\n", - "\n", - "Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "dIXpRozXkFVh" - }, - "outputs": [], - "source": [ - "auto apply (int l, int r, auto f) {\n", - " Pair q1 = split(root, r);\n", - " Pair q2 = split(q1.first, l)\n", - " q2.second = f(q2.second);\n", - " root = merge(q2.first, merge(q2.second, q1.second));\n", - "}\n", - "\n", - "void reverse (Node *v) {\n", - " if (v)\n", - " v->rev ^= 1;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "C8eKhyK5fnDK" - }, - "source": [ - "Применять её нужно так:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "GbQ-OEOWEQxM" - }, - "outputs": [], - "source": [ - "apply(l, r, reverse);" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "P8_dHs3BHBKo" - }, - "source": [ - "Это работает в плюсах, начиная с `g++14`.\n", - "\n", - "Для простых операций можно даже написать лямбду:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "nPmyzNjKHpNc" - }, - "outputs": [], - "source": [ - "apply(l, r, [](Node *v){\n", - " if (v)\n", - " v->rev ^= 1;\n", - "});" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "ZV7EMe68kpvi" - }, - "source": [ - "# Персистентность*\n", - "\n", - "Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.\n", - "\n", - "> Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.\n", - "\n", - "Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим.\n", - "\n", - "> Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**.\n", - "\n", - "Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами.\n", - "\n", - "Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "1sqJhTKQnkWY" - }, - "source": [ - "Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "vrd0UjRu1qDv" - }, - "outputs": [], - "source": [ - "Node* copy (Node *v) { return new Node(*v); }" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "L6METC4WKGbA" - }, - "source": [ - "Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так:" - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "LuFo9MChKF7b" - }, - "outputs": [], - "source": [ - "Pair split (Node *p, int x) {\n", - " p = copy(p);\n", - " // ...\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "NnMzBYiY1zD7" - }, - "source": [ - "В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.\n", - "\n", - "У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\\frac{L}{L+R}$, иначе за правое.\n", - "\n", - "**Теорема**. Такое переподвешивание эквивалентно приоритетам.\n", - "\n", - "**Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции:\n", - "\n", - "* Лист имеет вероятность 1 быть корнем себя (база индукции)\n", - "* Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\\frac{1}{L} \\cdot \\frac{L}{L+R} = \\frac{1}{L+R}$. С вершинами правого дерева аналогично.\n", - "\n", - "Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД." - ] - }, - { - "cell_type": "code", - "execution_count": 0, - "metadata": { - "colab": {}, - "colab_type": "code", - "id": "EamOdCWWKM_y" - }, - "outputs": [], - "source": [ - "Node* merge (Node *l, Node *r) {\n", - " if (!l) return r;\n", - " if (!r) return l;\n", - " l = copy(l), r = copy(r);\n", - " if (rand() % (size(l) + size(r)) < size(l)) {\n", - " // ...\n", - " }\n", - " else {\n", - " // ...\n", - " }\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "YcdzH43FyPDl" - }, - "source": [ - "Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$?" - ] - } - ], - "metadata": { - "colab": { - "collapsed_sections": [], - "name": "3-4_treap.ipynb", - "provenance": [], - "version": "0.3.2" - }, - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xcpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/img/.ipynb_checkpoints/trie-checkpoint.ipynb b/img/.ipynb_checkpoints/trie-checkpoint.ipynb deleted file mode 100644 index 4162c8e..0000000 --- a/img/.ipynb_checkpoints/trie-checkpoint.ipynb +++ /dev/null @@ -1,129 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Бор\n", - "\n", - "Бор — это структура данных для компактного хранения строк.\n", - "\n", - "Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. \n", - "\n", - "![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png)\n", - "\n", - "Бор можно удобно использовать для разных задач:\n", - "* Хранение строк — занимает гораздо меньше места, чем массив или сет строк.\n", - "* Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.\n", - "\n", - "* Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Реализация\n", - "\n", - "Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация:\n", - "* терминальная ли вершина,\n", - "* ссылки на детей,\n", - "* возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "const int k = 26;\n", - "\n", - "struct Vertex {\n", - " Vertex* to[k] = {0};\n", - " bool terminal = 0;\n", - "};\n", - "\n", - "Vertex *root = new Vertex();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "void add_string (string &s) {\n", - " v = root;\n", - " for (char c : s) {\n", - " c -= 'a';\n", - " if (!v->to[c]) \n", - " v->to[c] = new Vertex();\n", - " v = v->to[c];\n", - " }\n", - " v->terminal = true;\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.\n", - "\n", - "Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Как хранить ссылки\n", - "\n", - "Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми.\n", - "\n", - "В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например.\n", - "\n", - "Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Суффиксные ссылки\n", - "\n", - "> Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст.\n", - "\n", - "Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить.\n", - "\n", - "Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.\n", - "\n", - "Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick)." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "C++17", - "language": "C++17", - "name": "xeus-cling-cpp17" - }, - "language_info": { - "codemirror_mode": "text/x-c++src", - "file_extension": ".cpp", - "mimetype": "text/x-c++src", - "name": "c++", - "version": "-std=c++17" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From d74fc1332aa4866b276a05c37eacf8b970c01f66 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 5 Jun 2019 10:57:01 +0000 Subject: [PATCH 006/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- index.html | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 4474553..628e1b3 100644 --- a/index.html +++ b/index.html @@ -1 +1,14 @@ -some custom index file + From 13bc7efdc80187c8236a3d807bc7443a9ec64600 Mon Sep 17 00:00:00 2001 From: Sergey Slotin Date: Wed, 5 Jun 2019 15:33:38 +0300 Subject: [PATCH 007/230] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..0838f43 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +algorithmica.org \ No newline at end of file From 7a6e5818e16c12d680eb8f68ee568bd3f9c64eb4 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 5 Jun 2019 13:44:10 +0000 Subject: [PATCH 008/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- CNAME | 1 - 1 file changed, 1 deletion(-) delete mode 100644 CNAME diff --git a/CNAME b/CNAME deleted file mode 100644 index 0838f43..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -algorithmica.org \ No newline at end of file From 1bf20c5d796da49696f962f4927e05dd896d34ee Mon Sep 17 00:00:00 2001 From: Sergey Slotin Date: Thu, 6 Jun 2019 00:09:24 +0300 Subject: [PATCH 009/230] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..0838f43 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +algorithmica.org \ No newline at end of file From bb26ee2bbe77351ef9deeeb373824888f8b5ca90 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 6 Jun 2019 17:57:29 +0000 Subject: [PATCH 010/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- CNAME | 1 - en/index.html | 14 -- metrica.html | 15 ++ ru/aho-corasick.html | 45 ----- ru/annealing.html | 141 --------------- ru/bayans.html | 191 -------------------- ru/bitset.html | 55 ------ ru/centroid.html | 80 --------- ru/convex-hulls.html | 175 ------------------- ru/cross-entropy.html | 23 --- ru/cuda.html | 316 --------------------------------- ru/dp-intro.html | 45 ----- ru/dp-optimizations.html | 87 ---------- ru/dp_intro.html | 46 ----- ru/dp_optimizations.html | 87 ---------- ru/fenwick.html | 83 --------- ru/games.html | 102 ----------- ru/geometry.html | 138 --------------- ru/hashing.html | 120 ------------- ru/hld.html | 59 ------- ru/index.html | 51 ------ ru/ioi.html | 46 ----- ru/karatsuba.html | 53 ------ ru/lca.html | 88 ---------- ru/linalg.html | 180 ------------------- ru/matching.html | 74 -------- ru/matroid.html | 101 ----------- ru/mincost-maxflow.html | 102 ----------- ru/mst.html | 182 ------------------- ru/pollard.html | 112 ------------ ru/probability.html | 81 --------- ru/reciprocal.html | 68 -------- ru/segtree.html | 131 -------------- ru/sparse-table.html | 44 ----- ru/sse.html | 365 --------------------------------------- ru/stress-test.html | 66 ------- ru/strings.html | 147 ---------------- ru/suffix-array.html | 91 ---------- ru/treap.html | 162 ----------------- ru/trie.html | 44 ----- 40 files changed, 15 insertions(+), 3996 deletions(-) delete mode 100644 CNAME delete mode 100644 en/index.html create mode 100644 metrica.html delete mode 100644 ru/aho-corasick.html delete mode 100644 ru/annealing.html delete mode 100644 ru/bayans.html delete mode 100644 ru/bitset.html delete mode 100644 ru/centroid.html delete mode 100644 ru/convex-hulls.html delete mode 100644 ru/cross-entropy.html delete mode 100644 ru/cuda.html delete mode 100644 ru/dp-intro.html delete mode 100644 ru/dp-optimizations.html delete mode 100644 ru/dp_intro.html delete mode 100644 ru/dp_optimizations.html delete mode 100644 ru/fenwick.html delete mode 100644 ru/games.html delete mode 100644 ru/geometry.html delete mode 100644 ru/hashing.html delete mode 100644 ru/hld.html delete mode 100644 ru/index.html delete mode 100644 ru/ioi.html delete mode 100644 ru/karatsuba.html delete mode 100644 ru/lca.html delete mode 100644 ru/linalg.html delete mode 100644 ru/matching.html delete mode 100644 ru/matroid.html delete mode 100644 ru/mincost-maxflow.html delete mode 100644 ru/mst.html delete mode 100644 ru/pollard.html delete mode 100644 ru/probability.html delete mode 100644 ru/reciprocal.html delete mode 100644 ru/segtree.html delete mode 100644 ru/sparse-table.html delete mode 100644 ru/sse.html delete mode 100644 ru/stress-test.html delete mode 100644 ru/strings.html delete mode 100644 ru/suffix-array.html delete mode 100644 ru/treap.html delete mode 100644 ru/trie.html diff --git a/CNAME b/CNAME deleted file mode 100644 index 0838f43..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -algorithmica.org \ No newline at end of file diff --git a/en/index.html b/en/index.html deleted file mode 100644 index aa69e89..0000000 --- a/en/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - -

some english index

- - diff --git a/metrica.html b/metrica.html new file mode 100644 index 0000000..5dbaa4f --- /dev/null +++ b/metrica.html @@ -0,0 +1,15 @@ + + + + diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html deleted file mode 100644 index 9509ecc..0000000 --- a/ru/aho-corasick.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - -

Алгоритм Ахо-Корасик

-

Пусть дан набор строк в алфавите размера \(k\) суммарной длины \(n\). Алгоритм Ахо-Корасик за \(O(nk)\) времени и памяти строит бор для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время.

-

Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик.

-

-

Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы.

-
-

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

-
-

Эту и много других задач помогают решать суффиксные ссылки. Суффиксная ссылка для вершины \(v\) — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине \(v\), и присутствующий в боре. Будем считать, что мы их умеем быстро находить.

-

Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

-

Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за \(O(nk)\), где \(n\) и \(k\) это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в отдельную статью.

-

Зачем это нужно

-
-

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

-
-

Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор.

-

Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

-

Помимо суффиксных ссылок, нужно найти ещё переходы, чтобы поддерживать самый длинный суффикс.

-

Алгоритм Ахо-Корасик*

-

Заметим, что всего суффиксных ссылок нужно найти \(O(n)\), а переходов — \(O(nk)\). Суффиксные ссылки и переходы можно быстро найти динамикой.

-

Ссылки. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход.

-

Переходы. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на одну суффиксную ссылку и взять уже посчитанный переход оттуда.

-

-

```c++ const int k = 26;

-

struct Vertex { Vertex to[k] = {0}, go[k] = {0}; Vertex link = 0, p; int pch; Vertex (int _pch, Vertex *p) { pch = pch, p = _p; } };

-

Vertex *root = new Vertex(-1, 0); ```

-

c++ void add_string (string s) { Vertex *v = root; for (char _c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(c, v); v = v->to[c]; } }

-

Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться.

-

```c++ Vertex* go (Vertex *v, int c);

-

Vertex* link (Vertex *v) { if (!v->link) { if (v == root || v->p == root) v->link = root; else v->link = go(link(v->p), v->pch); } return v->link; }

-

Vertex* go (Vertex *v, int c) { if (!v->go[c]) { if (v->to[c]) v->go[c] = v->to[c]; else if (v == root) v->go[c] = root; else v->go[c] = go(link(v), c); } return v->go[c]; } ```

- - diff --git a/ru/annealing.html b/ru/annealing.html deleted file mode 100644 index 1e08a54..0000000 --- a/ru/annealing.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - -

Метод отжига

-
    -
  • Эвристический метод глобальной оптимизации
  • -
  • Работает с дискретными функциями в предположении, что у близких «точек» близкое значение, и есть что-то типа градиента
  • -
  • Очень простой в написании
  • -
  • Никаких гарантий нет, но хорошо работает на практике
  • -
  • Метод настолько мощный, что иногда им можно сдать задачи, у которых автором предполагалось адекватное решение (пример: Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам)
  • -
-
- -
-

Постановка задачи

-

Имеется некоторая функция \(f(x)\), которую мы хотим минимизировать.

-

Наука решать эту задачу в общем случае не умеет, и вряд ли когда-либо научится.

-

TODO: описание алгоритма, физическая аналогия, конструирование гладких функций

-

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

-

Ферзи

-

Рассмотрим такую задачу: дана шахматная доска \(n \times n\) и \(n\) ферзей. Нужно расставить их так, чтобы они не били друг друга.

-

Будем кодировать состояние перестановкой чисел от \(1\) до \(n\). Такое представление кодирует не все состояния, но оно точно не учитывает те, где ферзи бьют друг друга по вертикали или горизонтали.

-

Выберем такую функцию: fitness(p) = число пар ферзей, которые бьют друг друга.

-

Важная деталь: скорость одной итерации прямо пропорционально скорости работы fitness, поэтому очень важно реализовать её эффективно. Конкретно здесь её можно реализовать за \(O(n^2)\) (используется в коде для наглядности), \(O(n)\) и даже \(O(1)\).

-

Мы будем использовать python, потому что в нём удобнее строить графики. Эквивалентный код на C++ приведена сразу после.

-
import random
-
-# Эти библиотеки будут нужны, чтобы рисовать графики:
-import matplotlib.pyplot as plt
-%matplotlib inline
-
-import seaborn as sns
-sns.set()
-
def fitness(p):
-    s = 0
-    for i in range(len(p)):
-        d = 1
-        for j in range(i):
-            if abs(i-j) == abs(p[i]-p[j]): # ферзи стоят на одной диагонали
-                d = 0
-        s += d
-    return s
-
n = 100 # кол-во ферзей
-k = 1000 # кол-во итераций
-p = list(range(n)) # исходная перестановка
-
-scores = []
-
-t = 1
-for i in range(k):
-    # уменьшаем температуру
-    t *= 0.99 # этот параметр лучше перебрать руками
-    
-    # копируем перестановку и делаем небольшое изменение
-    q = p[:]
-    a, b = random.sample(range(n), 2)
-    q[a], q[b] = q[b], q[a]
-    
-    # считаем разницу fitness и решаем, нужно ли переходить в новое состояние
-    d = fitness(q) - fitness(p)
-    if d > 0 or random.random() < math.exp(d/t):
-        p = q[:]
-    scores += [fitness(q)]
-
-print('Best score:', max(scores))
-plt.plot(scores)
-plt.show()
-
Best score: 95
-
-png

png

-
-

Примерно эквивалентный код на C++:

-
const int n = 100, k = 1000;
-
-int f(vector<int> p) {
-    int s = 0;
-    for (int i = 0; i < n; i++) {
-        int d = 1;
-        for (int j = 0; j < i; j++)
-            if abs(i-j) == abs(p[i]-p[j]):
-                d = 0;
-        s += d;
-    }
-    return s;
-}
-
-double rnd() { return double(rand()) / RAND_MAX; }
-
-int main() {
-
-    for (int i = 0; i < n; i++)
-        v[i] = i;
-
-    int ans = 1;
-
-    double t = 1;
-    for (int i = 0; i < k; i++) {
-        t *= 0.99;
-        vector<int> u = v;
-        swap(u[rand()%n], u[rand()%n]);
-        int val = f(u);
-        if (val > ans || rnd() < exp((val-ans)/t))
-            v = u, ans = val;
-        if (ans == n) break;
-    }
-
-    for (int x : v)
-        cout << x+1 << " ";
-
-    return 0;
-}
- - diff --git a/ru/bayans.html b/ru/bayans.html deleted file mode 100644 index 889aa32..0000000 --- a/ru/bayans.html +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - - - - - - - -

Красивые идейные задачи

-

Везде, где не указано — время работы \(O(n)\), а если есть конкретные числа, то TL 1 секунда.

-

Задачи идут в порядке вспоминания, то есть в весьма рандомном.

-

Попугаи

-

Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы.

-

Минимум и максимум

-

Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый.

-

Перестановка

-

Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов.

-

Выпуклая оболочка

-

Требуется отвечать на 2 типа запросов:

-
    -
  1. Добавить точку в выпуклую оболочку.
  2. -
  3. Проверить, лежит ли точка внутри выпуклой оболочки.
  4. -
-

Обе операции онлайн за \(O(\log n)\).

-

Геометрическая прогрессия

-

Найдите способ посчитать \(\frac{1-a^n}{1-a}\) по произвольному модулю за \(O(\log n)\).

-

Покемоны

-

В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-турнир из 1024 вершин и \(1023 \times 1022 : 2\) рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из \(a \to b\) и \(b \to c\) не следует, что \(a \to c\).

-

У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш.

-

Сортировка

-

Можно ли отсортировать * 5 камней за 8 взвешиваний? * 5 камней за 7 взвешиваний? * 20 камней за 60 взвешиваний?

-

Точки в круге

-

Даны \(n\) точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за \(O(n)\) в среднем сортирует их по удаленности от начала координат.

-

Замкнутые ломаные

-

Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии?

-

Неубывающий массив

-

Дан массив из \(n\) целых чисел. Требуется за \(2n\) операций «прибавить к одному элементу любой другой» сделать его неубывающим.

-

Чётный цикл

-

Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины.

-

\(k\)-ая порядковая статистика

-

Дан массив из \(n\) целых чисел. Найдите его \(k\)-й наименьший элемент за \(O(n)\).

-

Доминирующий элемент

-

Дан массив из \(n\) элементов. Требуется ответить на \(m\) запросов, есть ли на отрезке \([l, r]\) доминирующий элемент — тот, который встречается на нём хотя бы \(\frac{r-l}{2}\) раз. Время работы \(O((n+m) \log n)\).

-

Разрушение дерева

-

Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева.

-

\(k\)-ый элемент на отрезке

-

Дан массив из \(n\) целых чисел. Требуется ответить на \(m\) запросов \(k\)-ой порядковой статистики на произвольном отрезке. Время работы \(O((n+m) \log n)\).

-

Различные числа на отрезке

-

Дан массив из \(n\) целых чисел. Требуется ответить на \(m\) запросов количества различных элементов на произвольном отрезке. Время работы \(O(m\sqrt{n})\).

-

Физкультура

-

Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится \(m\) дней. Деканат последовательно издает \(n\) приказов двух типов:

-
    -
  1. Объявить все дни с \(l\) по \(r\) выходными (физру закрывать нельзя)
  2. -
  3. Объявить все дни с \(l\) по \(r\) учебными (физру закрывать можно)
  4. -
-

При этом приказ может частично отменить действие предыдущих приказов.

-

После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика \(O(n \log n)\).

-

Нулевая сумма

-

Дано мультимножество из \(n\) целых чисел. Найдите любое его подмножество, сумма чисел которого делится на \(n\).

-

Мета-задача

-

В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу.

-

Ниточка

-

В плоскую доску вбили \(n\) гвоздей радиуса \(r\), причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления.

-

Пельмени

-

Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено \(n\) возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика \(O(n^2 \log n)\).

-

От нуля до единицы

-

Дан следующий код:

-
x = 0
-while x < 1:
-    x += random()
-

Требуется посчитать матожидание x.

-

(random в питоне возвращает случайное действительное число от 0 до 1.)

-

Площадь

-

Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%.

-

Окружности

-

Имеется окружность радиуса \(R\), назовём её внешней. Внутри неё лежит окружность радиуса \(r < R\) и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: \(k\)-я окружность должна

-
    -
  • соприкасаться с внешней,
  • -
  • соприкасаться с предыдущей ((\(k-1\))-ой),
  • -
  • иметь при этом максимальный радиус.
  • -
-

Найдите (выведите формулу за \(O(1)\)) радиус \(k\)-й такой окружности.

-

Блеф

-

Катя и Серёжа играют в игру. У Кати есть \(n\) карт, у Серёжи — \(m\). Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её особой. Цель игроков — её отгадать. Все \(n+m+1\) карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может:

-
    -
  • Попытаться угадать особую карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается.
  • -
  • Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом.
  • -
-

С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика \(O(nm)\).

-

Достижимость

-

Дан ориентированный граф без кратных рёбер. Для всех пар вершин \(u\) и \(v\) определите, можно ли дойти из \(u\) в \(v\). Вершин меньше 2000.

-

Нумизмат

-

Есть \(n\) жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть \(k\) типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой.

-

Придумайте любой полиномиальный алгоритм.

-

Принцесса

-

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

-

Она составила список из \(n\) самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством.

-

У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми.

-

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

-

Асимптотика \(O(n^2)\).

-

Спираль

-

Определим спираль \((2n+1) \times (2n+1)\) как матрицу следующего вида:

-

\[ -\begin{matrix} -21 & 22 & 23 & 24 & 25 \\ -20 & 7 & 8 & 9 & 10 \\ -19 & 6 & 1 & 2 & 11 \\ -18 & 5 & 4 & 3 & 12 \\ -17 & 16 & 15 & 14 & 13 \\ -\end{matrix} -\]

-

Ваша задача — рассчитать ответы на \(q\) запросов суммы чисел в произвольной прямоугольной области (по модулю \(10^9+7\)).

-

\(q \leq 100\), \(n \leq 10^9\).

-

Польский лабиринт

-

Группа из \(n\) туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка \((x, y)\) свободна, только если x & y == 0.

-
- -
-

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

-

\(n \leq 10^5\), изначальные координаты туристов до \(10^9\).

-

Нимные подмножества

-

Есть множество \(A\), состоящее из \(n\) чисел от 0 до \(2^{32}-1\). Требуется выбрать его подмножество \(B \subseteq A\) максимальной суммы такое, что нельзя выбрать его подмножество \(C \subseteq B\) такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика \(O(n \log n)\).

-

Баланс степеней

-

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

-

Два пути

-

Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из \(s\) в \(t\).

-

Пьяница

-

Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью \(p\) и влево с вероятностью \(1-p\). С какой вероятностью он когда-либо окажется в точке с отрицательной координатой?

-

Ксоровый рюкзак

-

Дан массив из \(10^5\) целых чисел от \(0\) до \((2^{30}-1)\). Найти количество различных подпоследовательностей этого массива, xor-сумма которых равна заданному числу \(x\).

-

Иван Сусанин

-

Польская армия хочет добраться из поселения \(s\) в поселение \(t\). Ей руководят два гетмана — Камиль и Матеуш.

-
    -
  • Камиль руководит армией днём и водит армию по дорогам.
  • -
  • Матеуш руководит армией ночью и совершает маневры по секретным тропам.
  • -
-

Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск.

-

Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу. Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением \(t\) и войском по соответствующей карте строго уменьшилось.

-

Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из \(s\) в \(t\).

-

Варенье

-

В ряд стоят \(n\) пустых банок из-под варенья. Вместительность \(i\)-й банки равна \(v_i\) грамм.

-

Карлсон наполняет эти банки вареньем в \(m\) этапов. На каждом этапе он выбирает числа \(l\), \(r\), \(x\) и \(y\), а затем пролетает над банками с \(l\) по \(r\), выполняя следующие операции: в банку номер \(l\) он добавляет \(x\) грамм варенья, в банку номер \((l + 1)\)\((x + y)\) грамм варенья, в банку номер \((l + 2)\)\((x + 2y)\), и так далее до \(r\)-той банки, в которую он положит \(x + y(r - l)\) грамм варенья.

-

Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной.

-

\(n, m \leq 10^5\)

-

Лабиринт

-

Серёжа потерялся в лабиринте \(n \times m\). Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт.

-

Придумайте любой полиномиальный алгоритм.

-

Обезьяна

-

Дана строка из \(10^5\) символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт?

-

Ожидание минимума

-

Даны \(n\) случайных величин, равномерно распределенных на отрезках \([l_i, r_i]\) — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин.

-

Придумайте любой точный полиномиальный алгоритм.

-

Шумный ксор

-

Загадано некое число \(x\). Вы можете делать запросы следующего типа: назвать число \(y\) и получить в ответ число единичных битов в ксор-сумме \(x\), \(y\) и \(m\), где \(m\) это случайно сгенерированная маска, в которой каждый бит имеет вероятность \(p = \frac15\) быть единичным, то есть каждый бит \(x \oplus y\) заменяется на противоположный с вероятностью \(y\), и вам возвращается количество единичных битов. Для ясности:

-
x = # ...
-
-def mask(p=0.2):
-    r = 0
-    for i in range(32):
-        if random.random() < p:
-            r += 2**i
-    return r
-
-def query(y):
-    return bin(x ^ y ^ mask()).count('1')
-

Ваша задача — отгадать число, используя не более 10000 попыток.

- - diff --git a/ru/bitset.html b/ru/bitset.html deleted file mode 100644 index a1dc9a8..0000000 --- a/ru/bitset.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - -

Битовое сжатие

-
    -
  • Из-за него в «асимптотиках» появляется /64
  • -
  • На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов
  • -
  • bitset есть в stl; говорят, самописный быстрее
  • -
-

Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать & двух bool-ом и двух long-ов примерно одинаково по скорости.

-

Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию.

-

Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — bitset.

-

Работать с ним нужно вот так:

-

c++ const int lim = 1000; bitset<lim> b; // создать битсет размера lim (должно быть константой) b.set(); // заполнить единицами b.reset(); // заполнить нулями b.flip(); // заменить единички на нули и наоборот b.count(); // посчитать число единичек cout << b; // вывести битовую строку

-

Также для битсетов работает вся битовая арифметика — &, |, ^, ~, <<, >> и их варианты с [operator]=.

-

Рюкзак

-

Задача: даны \(n\) предметов с положительными целыми весами \(a_i\) и рюкзак размера \(lim\), выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака.

-

Обычно его решают так:

-

c++ bool dp[lim] = {}; // так можно его заполнить нулями dp[0] = 1; for (int i = 0; i < n; i++) for (int x = lim - a[i]; x >= 0; x--) dp[x + a[i]] |= dp[x];

-

…а с битсетом оно разгоняется так:

-

c++ bitset<lim> b; b[0] = 1; for (int i = 0; i < n; i++) b |= b << a[i];

-

Цикл длины 3

-

Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из \(n\) вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за \(O(\frac{n^3}{64})\) следующим образом:

-

c++ bitset<maxn> g[maxn]; // матрица смежности for (int a = 0; a < n; a++) { for (int b = 0; b < n; b++) { if (g[a][b] && (~g[a] & g[b]).any()) { // цикл найден } } }

-

Benchmark: на серверах CodeForces этот код при \(n = 5000\) работает за 7 секунд.

-

Перемножение матриц

-

Матрица смежности графа, возведенная в степень \(n\), имеет комбинаторный смысл: количество способов дойти из \(a\) в \(b\), используя ровно \(n\) переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через \(n\) ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения:

-

```c++ typedef bitset t; typedef array matrix;

-

matrix operator* (matrix a, matrix b) { matrix c; for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) if(a[i][j]) c[i] |= b[j]; return c; } ```

-

Гаусс

-

Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

-

Нас по сути просят решить следующую систему:

-

\[ -\begin{cases} -a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ -a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ -\ldots \\ -a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 -\end{cases} -\]

-

Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

-

В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

-

c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; }

-

Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

- - diff --git a/ru/centroid.html b/ru/centroid.html deleted file mode 100644 index aaae7ee..0000000 --- a/ru/centroid.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - -

Центроидная декомпозиция

-

Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».

-

Иногда вместо неё можно написать Heavy-light декомпозицию (чуть сложнее) или метод переливаний (чуть проще).

-
- -
-

Определение. Центром или цетроидом (англ. centroid) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более \(\frac{n}{2}\).

-

Выясняется, что центр всегда существует — это следует из алгоритма его поиска:

-

```c++ int s[maxn];

-

int sizes (int v) { s[v] = 1; for (int u : g[v]) // для простоты считаем, что дерево корневое s[v] += sizes(u); return s[v]; }

-

// второй параметр -- размер дерева int centroid (int v, int n) { for (int u : g[v]) if (s[u] > n/2) return centroid(u, n); return v; } ```

-

Утверждение. centroid действительно находит цетроид.

-

Доказательство:

-
    -
  • centroid вернет вершину, у которой размеры всех детей не больше \(\frac{n}{2}\) (это явно проверяется в if-е).
  • -
  • Мы пришли в эту вершину, когда её размер был больше \(\frac{n}{2}\), а это значит, что в «обратном» направлении есть не более \(n - (\frac{n}{2}+1) = \frac{n}{2}-1\) вершин.
  • -
  • Значит, размеры всех соседей не больше половины \(n\), и алгоритм корректен.
  • -
-

Иногда центров два (пример: 1-2-3-4), тогда алгоритм вернёт «нижний» центроид.

-

Определение. Центроидной декомпозицией будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».

-

Определение. Компонентой центроида будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.

-

Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты.

-

Теперь поймём, зачем мы всё это делали.

-

Утверждение. Каждая вершина входит в \(O(\log n)\) компонент.

-

Доказательство. Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более \(\lceil \log_2 n \rceil\) разделений.

-

Следствие. Центроидная декомпозиция (см. определение выше) работает за \(O(n \log n)\).

-

Утверждение. Для любого пути \(a \leadsto b\) есть единственный центроид \(c\), в чьей компоненте были и \(a\), и \(b\).

-

Доказательство. Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила \(a\) и \(b\).

-

Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний.

-

Подсчет путей с заданным свойством

-

Рассмотрим конкретный пример: подсчёт путей заданной длины.

-

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

-

Количество таких путей можно посчитать за размер текущей компоненты: заведём массив d, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей dfs, который будет возвращать временный массив t — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению x в нём и добавить к ответу d[l-x], а затем добавить все значения из t в d. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз.

-

```c++ int l = 179; // нужная нам длина int ans = 0;

-

// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена bool used[maxn]; int s[maxn]; // размеры поддеревьев

-

void sizes (int v, int p) { s[v] = 1; for (int u : g[v]) if (u != p && !used[u]) sizes(u, v), s[v] += s[u]; }

-

int centroid (int v, int p, int n) { for (int u : g[v]) if (u != p && !used[u] && s[u] > n/2) return centroid(u, v, n); return v; }

-

// записывает в t[] глубины вершин void dfs (int v, int p, int d, vector &t) { t.push_back(d); for (int u : g[v]) if (u != p && !used[u]) dfs(u, v, d + 1, t); }

-

void solve (int v) { /* <единственный зависящий от конкретной задачи код> / size(v); vector d(s[v], 0); d[0] = 1; for (int u : g[v]) { if (!used[u]) { vector t; dfs(u, v, 1, t); for (int x : t) if (x <= l) ans += d[l-x]; for (int x : t) d[x]++; } } / */

-
used[v] = 1;
-for (int u : g[v])
-    if (!used[u])
-        solve(centroid(u, v, s[u]/2));
-

} ```

-

Асимптотика \(O(n \log n)\), потому что на каждую из \(O(n)\) верщин мы потратим \(O(1)\) операций на каждом из \(O(\log n)\) «уровней» центроидной декомпозиции.

-

Запросы на путях — offline

-

Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.

-

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

-

Например, при запросах суммы на пути, мы можем насчитать во внутреннем dfs для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины.

-

Таким образом, каждый запрос будет просмотрен \(O(\log n)\) раз, пока не будет удален, и асимптотика составит \(O(q \log n + n \log n)\).

-

Запросы на путях — online

-

Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.

-

Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив centroid[][] размера \(n \times \log n\), в котором для каждой вершины будем хранить \(O(\log n)\) центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.

-

Тогда, при ответе на запрос, мы за \(O(\log n)\) или даже \(O(\log \log n)\) операций находим центроид на нужном нам пути (первые сколько-то значений centroid[v] и centroid[u] будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида.

-

Асимптотика при более долгих пересчётах

-

TODO: сослаться на мастер-теорему

-

Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за \(O(n \log n)\) (например, где-то использовав set). Сильно ли это хуже по времени?

-

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

-

-

Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:

-

\[ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} -\leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n -= n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} -< 2 n \log n -= O(n \log n) \]

-

Получается, что мы можем «мердж» в центроидной декомпозиции писать и за \(O(n \log n)\) — как, впрочем, и во всех остальных «разделяйках».

- - diff --git a/ru/convex-hulls.html b/ru/convex-hulls.html deleted file mode 100644 index 94f11cf..0000000 --- a/ru/convex-hulls.html +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - - - - - - - -

Задачи на многоугольники

-

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

-
-alt text

alt text

-
-

Выпуклые оболочки

-

Выпуклое множество - такое множество точек, что все точки отрезка, образуемого любыми двумя точками данного множества, также принадлежат данному множеству

-

Выпуклая оболочка фигуры - такое выпуклое множество точек, что все точки фигуры также лежат в нем.

-

Минимальная выпуклая оболочка фигуры - это минимальная по площади выпуклая оболочка.

-
-alt text

alt text

-
-
-alt text

alt text

-
-
-alt text

alt text

-
-

Дано множество точек, требуется построить его минимальную выпуклую оболочку :

-

Построение за \(O(nh)\)

-

Алгоритм Джарвиса(метод заворачивания подарка)

-

Давайте выберем какую-то точку, которая гарантированно попадет в минимальную выпуклую оболочкуо, например обычно берут нижнюю и если таких несколько, то самую левую из них. Теперь давайте по одной набирать точки, как бы заворачивая нашу выпуклую оболочку(отсюда и название). Как же нам найти следующую точку в выпуклую оболочку, давайте пройдемся по точкам, которые мы еще не взяли в МВО и среди них выберем с минимальным полярным углом.

-
-alt text

alt text

-
-

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

-

Асимптотика : для каждой точки выпуклой оболочки мы из всех оставшихся точек будем искать оптимальную - что будет работать за h(размер выпуклой оболочки) * n

-

Важно помнить, что именно \(O(hn)\), а не \(O(n^2)\), так как существуют задачи на это

-
int base = 0;
-for (int i = 1; i < n; i++) {
-    if (mas[i].y < mas[base].y) {
-        base = i;
-    }
-    else if (mas[i].y == mas[base].y && mas[i].x < mas[base].x) {
-        base = i;
-    }
-}
-convex_hull.push_back(base);
-point first = mas[base];
-point cur = first;
-point prev = point(first.x - 1, first.y);
-do {
-    double minCosAngle = 1e9; // чем больше угол, тем меньше его косинус
-    double maxLen = 1e9;
-    int next = -1;
-    for (int i = 0; i < n; i++) {
-        double curCosAngle = CosAngle(prev, cur, mas[i]);
-        if (Less(curCosAngle,minCosAngle)) {//если меньше сразу меняем
-            next = i;
-            minCosAngle = curCosAngle;
-            maxLen = dist(cur, mas[i]);
-        }
-        else if (Equal(curCosAngle, minCosAngle)) {// смотрим по длине
-            double curLen = dist(cur,mas[i]);
-            if (More(curLen,maxLen)) {
-                next = i;
-                maxLen = curLen;
-            }
-        }
-    }
-    prev = cur;
-    cur = mas[next];
-    convex_hull.push_back(next);
-}
-while (cur != first);
-

Построение за \(O(n \log n)\)

-

Алгоритм Грэхема

-

Алгоритм Грэхема базируется на следующей идее: Давайте не искать следующую точку каждый раз, а сделаем так, чтобы у нас всегда была оптимальная точка и мы могли бы просто ее достать и проверить.

-

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

-

Теперь будем делать следующий алгоритм, пока все точки не будут просмотрены :

-
    -
  1. Возьмем первую из отсортированных точек.

  2. -
  3. Проверем последние три точки из взятых, если они образуют правый поворот, то удалим предпоследнюю точку

  4. -
-

Сделать это можно, например, стеком. Код есть ниже.

-
-alt text

alt text

-
-

Асимптотика : Мы просмотрим одну точку и либо удалим ее, либо оставим, то есть сам поиск МВО работает за линейное время, но мы еще делаем сортировку, а \(\rightarrow\) алгоритм работает за \(O(n\log(n))\), при этом его корректность вытекает из предыдущего алгоритма.

-

Красивая визуализация - https://visualgo.net/en/convexhull

-

красивое видео - https://www.youtube.com/watch?v=BTgjXwhoMuI .

-
struct Point {
-    int x, y;
-};
-
-Point operator -(Point a, Point b)
-{
-    return {a.x - b.x, a.y - b.y};
-}
-
-int operator * (Point a, Point b)
-{
-    return a.x * b.x + b.y * a.y;
-}
-
-int operator ^(Point a, Point b)
-{
-    return a.x * b.y - b.x * a.y;
-}
-
-bool cw(Point a, Point b, Point c)
-{
-    return (a - b) * (c - b) > 0;
-}
-
-bool ccw(Point a, Point b, Point c)
-{
-    return (a - b) * (c - b) < 0;
-}
-
-int main()
-{
-    sort(all(p2), comp);
-    vector<Point> s;
-    s.push_back(p[min_ind]);
-    for (int i = 0; i < n - 1; i++) {
-        if (p2[i].x == s[s.size() - 1].x && p2[i].y == s[s.size() - 1].y)
-            continue;
-        while (s.size() > 1 && (vect(s[s.size() - 1], s[s.size() - 2]) ^
-               vect(s[s.size() - 1], p2[i])) > 0)
-            s.pop_back();
-        s.push_back(p2[i]);
-    }
-}
-

Алгоритм Эндрю

-

Алгоритм Эндрю опирается на то, что вещественные числа не точны и предлагает поменять компаратор и строить не одну выпуклую оболочку, а две :

-

Давайте выберем самую нижнюю и самую правую точку, затем отсортируем точки по самому простому из возможных компараторов, теперь будем строить две оболочки от самой правой точки и самой левой, в итоге мы получим верхнюю и нижнюю части выпуклой оболочки

-
bool comp(Point a, Point b) {
-    if(a.x == b.x) {
-        return a.y < b.y;
-    }
-    return a.x < b.x;
-}
-
-int main() {
-    sort(all(p), comp);
-    vector<Point> up, down;
-    up.pb(p[0]);
-    down.pb(p[0]);
-    Point p1 = p[0], p2 = p.back();
-    for(int i = 1; i < n; i++) {
-        if (i == n - 1 || cw(p1, p[i], p2)) {
-            while (up.size() >= 2 && !cw(up[up.size() - 2], up[up.size() - 1], p[i])) {
-                up.pop_back();
-            }
-            up.pb(p[i]);
-        }
-        if (i == n - 1 || ccw(p1, p[i], p2)) {
-            while (down.size() >= 2 && !ccw(down[down.size() - 2], down[down.size() - 1], p[i])) {
-                down.pop_back();
-            }
-            down.pb(p[i]);
-        }
-    }
-}
-

Алгоритм Чена

-

Также существует алгоритм, объединяющий Джарвиса и Грэхема(Эндрю) и работающий за \(O(n\log(h))\), но он разбираться не будет

-

Задачи

-
    -
  1. Базовые задачи - достаточно простые, например найти длину забора, чтобы ограничить многоугольник и подобные, но есть достаточно интересные задачи, в которых выпуклая оболочка неочевидна, например следующая : Даны \(n\) пунктов в городе и \(n\) почтальонов, для каждого пункта известно расстояние от почты \(c_{i}\). Требуется каждому пункту доставить почту, \(i\)-ый почтальон просит \(a_{i}\) монет, чтобы проснуться и \(b_{i}\), чтобы проехать один километр, требуется для каждого пункта сказать, кто доставит почту наиболее выгодно. (Подсказка : \(a_{i} + b_{i} * c_{j}\) - это прямая и стоимость доставки от \(i\) почтальона к \(j\) пункту).
  2. -
- - diff --git a/ru/cross-entropy.html b/ru/cross-entropy.html deleted file mode 100644 index 9ad3dc1..0000000 --- a/ru/cross-entropy.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - -

Почему машинное обучение работает

-

b

-

Принцип максимального правдоподобия —

-

Функция правдоподобия

-

\[ \hat{\theta} = \argmax_\theta P(X | \theta) \]

-

Принцип максимального правдоподобия

-

Логистическая регрессия

-

Нормальное распределение и MSE

-

Почему сигмоида

- - diff --git a/ru/cuda.html b/ru/cuda.html deleted file mode 100644 index 6e19f33..0000000 --- a/ru/cuda.html +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - - - - - - - -

Supercomputing for the Masses

-

This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in Colab or download and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: * apt-get install nvidia-cuda-dev nvidia-cuda-toolkit * pip install pycuda

-

Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work.

-

Subtlties of the Moore's law

-

Here is a graph that roughly represents what is happening in the CPU world:

-

-

Moore's law is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too.

-

You can see that around 2005 there became a shift in design .

-

The cores are more or less independent.

-

Modern GPUs appeared in early 2000s. They exploit the specific area they operate.

-

There are physical limitations to the speed of a core.

-

One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another.

-

Some of them have

-

The default free GPUs available on Google Colab are rather powerfull. Author has no idea why Google does this, but this is awesome.

-

Why multiprocessing?

-

Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound

-

There are two types of

-

General-purpose GPU

-

There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills.

-

There are several.

-

This is like with Windows and Linux.

-

We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning.

-

Heterogineous computing

-

CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs.

-

Differences from CPUs

-

Threads

-

Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive.

-

By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution.

-

In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput.

-

Memory

-

The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device?

-

You can easily dump 98% of performance of you think this way.

-

Installing PyCUDA

-

CUDA is available for many languages.

-

Nice documentation can be found here: https://documen.tician.de/pycuda/index.html

-

If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU".

-
# you may want to clear the output of this cell after installation
-from IPython.display import clear_output
- 
-# this might take a while
-!pip install pycuda
-
-clear_output()
-
import numpy as np
-
-from pycuda.compiler import SourceModule
-import pycuda.driver as drv
-import pycuda.autoinit
-

The basics

-

Let's start with a simple example and then dive deeper.

-

Kernels

-

Just like C or C++, except that you use some custom built-in functions and specifiers.

-

CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:

-

You need to think of your computer as a heterogenious machine: there is host data and device data.

-
    -
  • You move input data to device memory.
  • -
  • You run some computation on device.
  • -
  • You retrieve back this data.
  • -
-

In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results.

-

The famous \(A + B\) problem

-

For testing and coordination with host, we will use NumPy package. If you don't have it, install it: pip install numpy.

-

NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it.

-
# lets generate our test data: two float arrays filled with something random
-a = numpy.random.randn(100).astype('float32')
-b = numpy.random.randn(100).astype('float32')
-# the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it
-
-# we need to create space where kernel should write its answers to
-dest = numpy.zeros_like(a)
-
-# this is the kernel itself
-mod = SourceModule("""
-    __global__ void add(float *dest, float *a, float *b) {
-        const int i = threadIdx.x;
-        dest[i] = a[i] + b[i];
-    }
-""")
-
-# you need to specify the source code, and PyCUDA will compile it
-add_kernel = mod.get_function("add")
-
-add_kernel(
-    drv.Out(dest),  # specifies that this memory should be accessible for writing
-    drv.In(a),  # specifies this should be accessible for reading
-    drv.In(b),
-    block=(100,1,1)  # we'll talk about it in a minute
-)
-
-assert np.allclose(dest, a + b), 'WA'  # checks that these are equal
-print('OK')
-
  File "<ipython-input-27-afc857479fe4>", line 19
-    %%time
-    ^
-SyntaxError: invalid syntax
-

Memory management

-

In CUDA C API, you need to allocate memory explicitly. So this is actually really nice.

-

There is also drv.InOut function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too.

-

Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough.

-

GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have compute capabilities (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability \(x\) is also available at later versions. These can be checked at run-time or compile-time.

-

You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications

-

Synchronization

-

Reduction is any array-wise operation.

-

Assume the following problem:

-

Dynamic programming

-

Consider the following recurrence:

-
## Problem: dynamic programming
-

Work vs. Latency

-

We actually think about both work and step complexity now.

-

Some tasks, especially in cryptography, cannot be parallelized. But some can.

-

Summing arrays in \(O(\log n)\) time

-

Assume we want to perform some associative (i. e. \(A*(B*C) = (A*B)*C\)) operation on an array of \(n\) elements. Say, sum it up.

-

Normally, we would do that with a simple loop:

-

c++ float s = 0; for (int i = 0; i < n; i++) { s += a[i]; }

-

Its computation graphs looks like this:

-

-

This is optimal in terms of work complexity, but not in terms of step complexity: it's \(O(n)\). We may want something that is a bit worse in terms of work complexity, but can be parallelized.

-

Let's try this divide-and-conquer approach:

-

-

Now it's still \(O(n)\) work complexity (you actually need exactly the same number of additions), but this is \(O(\log n)\) step complexity.

-

When you unroll the recursion from top to bottom, you will see that to get each required value,

-

-

Reducing small arrays

-
a = numpy.random.randn(2048).astype('float32')
-
-mod = SourceModule("""
-    __global__ void sum(float *dest, float *a, float *b) {
-        const int i = threadIdx.x;
-        // for l from 0 to logn:
-        //   __sync_threads()
-        //   if the thread is active
-        //     sum two elements into where they belong
-        // a[0] should containt the needed sum
-    }
-""")
-
-sum_kernel = mod.get_function("sum")
-
-add_kernel(
-    drv.InOut(a),
-    block=(1024,1,1)
-)
-
-assert np.allclose(dest, a + b), 'WA'  # checks that these are equal
-print('OK')
-

Warps and thread blocks

-

Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties.

-

-

You can actually do the same stuff with 2d and 3d indexing — weird, right?

-

Atomics

-

Reducing big arrays

-

Reducing very big arrays

-

Now, things get harder. It's time to tell how exactly GPU parallelism works.

-
-

Dense Matrix multiplication

-

Let's get to our first example where using GPUs actually makes sense: matrix multiplication.

-

Sorting

-

Our last (and hardest task) is to implement sorting.

-

You might notice that we advocated divide-and-conquer approaches most of the time.

-

It's true. They work. But we can't get an algorithm that works already.

-
# we'll use a deep learning library for benchmarking because I'm not familiar with anything else 
-import torch
-
-a = torch.randn(10**8)
-b = a.cuda()
-
# this should run for ~15 secs
-%time c = torch.sort(a)
-%time c = torch.sort(b)
-
CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s
-Wall time: 15.2 s
-CPU times: user 274 ms, sys: 237 ms, total: 511 ms
-Wall time: 511 ms
-

So, 30 times speedup. So, we now what we need to compete against.

-
b.sort()
-
(tensor([-5.4567, -5.3551, -5.3288,  ...,  5.3529,  5.4484,  5.4486],
-        device='cuda:0'),
- tensor([55083205,  8383169, 73705953,  ..., 79814161, 50474932, 27805828],
-        device='cuda:0'))
-

There are two types of sorting algorithms: data-driven.

-

The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort.

-

-

It has \(O(\log n)\) stages, in total they have \(1 + 2 + 3 + \ldots + \log n = O(\log^2 n\) blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has \(O(n \log^ n)\) work complexity, but \(O(\log^2 n)\) step complexity, which is pretty sweet.

-

It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation:

-
def bitonic_sort(a, up=False):
-    if len(a) <= 1:
-        return a
-    else: 
-        l = bitonic_sort(x[:len(a) // 2], True)
-        r = bitonic_sort(x[len(a) // 2:], False)
-        return bitonic_merge(first + second, up)
-
-def bitonic_merge(a, up): 
-    # assume input a is bitonic, and sorted list is returned 
-    if len(a) == 1:
-        return a
-    else:
-        bitonic_compare(a, up)
-        l = bitonic_merge(a[:len(a) // 2], up)
-        r = bitonic_merge(a[len(a) // 2:], up)
-        return l + r
-
-def bitonic_compare(a, up):
-    dist = len(a) // 2
-    for i in range(dist):  
-        if (a[i] > a[i + dist]) == up:
-            a[i], a[i + dist] = a[i + dist], x[i]  # this is how swap is done in Python
-
bitonic_sort([57, 179, 42, 17, 300, 111])
-
[300, 179, 111, 57, 42, 17]
-
a = np.random.randn(10**8).astype('float32')
-
---------------------------------------------------------------------------
-
-NameError                                 Traceback (most recent call last)
-
-<ipython-input-27-58a927c14aae> in <module>()
-----> 1 a = np.random.randn(10**8).astype('float32')
-
-
-NameError: name 'np' is not defined
-

Why CUDA

-

Most of it still applicable.

-

Again, GPU programming is very specific.

-

SSE and tensor cores.

-

Kernels

-

Just like C or C++, except that you use some custom built-in functions and specifiers.

-

CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:

-

You need to think of your computer as a heterogenious machine: there is host data and device data.

-
    -
  • You move input data to device memory.
  • -
  • You run some computation on device.
  • -
  • You retrieve back this data.
  • -
-

In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results.

-

What you need to understand about GPUs is that they are extremely specialised for their applications.

-

Intrinsics for that.

-

Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision.

-

First, they introduced "multiply-accumulate" operation (e. g. x += y * z) per 1 GPU clock cycle.

-

Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell).

-

Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix).

-

This is a lot of work per

-

Well, you don't really need anything more precise than that for deep learning anyway.

-

It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices.

-

Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores".

-

So, see, this is not exactly fair comparison.

-

-* -
-You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull -
-
    -
  • -
-

down to int4 (16-valued, you heard correct)

-

You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch.

-

Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply.

-

Reducing an array

-

It seems to be simple: you just need to .

-

What actually happens when you do s += x? This is not a single operation. Actually, four things happen:

-
    -
  1. Read \(x\) into register
  2. -
  3. Read \(s\) into register
  4. -
  5. Calculate \(s + x\)
  6. -
  7. Write it back to wherever \(s\) was initially
  8. -
-

Two threads may execute it in an interleaved fashion. Say thread A could get \(s\), but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value.

-

Note: Atomics to do that

-

for small data types they are implemented on the hardware level and much more faster than that.

-

std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions.

-

Memory types

-

If all the various types of device memory were to race, here’s how the race would turn out:

-

Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible).

-
    -
  • 1st place: Register memory
    This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread.
  • -
  • 2nd place: Shared Memory
    Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should
  • -
  • 3rd place: Constant Memory
  • -
  • 4th: Texture Memory
  • -
  • Tie for last place: Local Memory and Global Memory
  • -
-

What you need to care for now is register

-

For now, you need to care about differe

-

Accessing global memory takes hundreds.

-

Problem: dense matrix multiplication

-

A lot of these are actually sparse. You can do stuff with social network graphs or web graphs.

-

Cool. But let's disapploint us for a bit:

- - diff --git a/ru/dp-intro.html b/ru/dp-intro.html deleted file mode 100644 index 341ca37..0000000 --- a/ru/dp-intro.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - -

Динамическое программирование

-
-

Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

-
-

Разберем некоторые классические идеи и задачи в динамическом программировании.

-

Рюкзак

-
-

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

-
-

Наибольшая общая подпоследовательность

-
-

Есть две строки. Найти длину их наибольшей общей подпоследовательности.

-
-

Расстановка скобок

-
-

Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

-
-

Можно завести динамику «можно ли ».

-

Наибольшая возрастающая подпоследовательность

-

Есть также алгоритм за \(O(n \log n)\).

-

Мемоизация

-

Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

-

Первый предполагает

-

Число разбиений

-
- -
-

Замена параметра на ответ

-
- -
- - diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html deleted file mode 100644 index ac2305b..0000000 --- a/ru/dp-optimizations.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - -

Оптимизации динамики

-
    -
  • Разделяй и властвуй
  • -
  • Оптимизация Кнута
  • -
  • Convex Hull Trick
  • -
  • Лямбда-оптимизация
  • -
-

Задача

-
-

Даны \(n\) точек на прямой. Нужно найти \(m\) отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.

-
-

Базовое решение — следующая динамика: \(f[i, j]\) — минимальная стоимость покрытия \(i\) первых (самых левых) точек, используя не более \(j\) отрезков (итоговый ответ будет записан в \(f[n, m]\) — прим. К. О.).

-

Переход — перебор всех возможных последних отрезков, то есть \(f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}\).

-

```c++ // x[] — отсортированный массив координат точек, нумерация с нуля

-

// квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }

-

// TODO: предподсчитать cost

-

for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности

-

for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ```

-

Заметим, что циклы по i и j можно поменять местами.

-

Такое решение пока работает за \(O(n^2 m)\) — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить.

-

Разделяй-и-властвуй

-

Обозначим за \(opt[i, j]\) оптимальный \(k\), на котором \(f[i, j]\) минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.

-

Утверждение. \(opt[i, j] \leq opt[i, j+1]\).

-

Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.

-

Что это нам даёт? Если мы уже знаем \(opt[i, l]\) и \(opt[i, r]\) и хотим посчитать \(opt[i, j]\) для какого-то \(j\) между \(l\) и \(r\), то мы можем сузить отрезок поиска оптимального индекса с \([0, i-1]\) до \([opt[i, l], opt[i][r]]\).

-

Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка \([l, r]\), зная, что их \(opt\)-ы лежат между \(l'\) и \(r'\). Она берет середину отрезка \([l, r]\) и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно.

-

c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); }

-

Вызываться она будет просто последовательно для каждого слоя:

-

c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k);

-

Теперь пересчет одного «слоя» динамики занимает \(O(n \log n)\) вместо \(O(n^2)\). Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет \(O(\log n)\), а значит и каждый элемент будет просмотрен не более \(O(\log n)\) раз.

-

Получается, что асимптотика улучшилась до \(O(n m \log n)\).

-

Оптимизация Кнута

-

Предыдущий метод основывался на том факте, что \(opt[i, j] \leq opt[i, j+1]\). А что, если \(opt\) монотонен ещё и по первому параметру?

-

\[ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] \]

-

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

-

Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:

-

\[ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) \]

-

Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из \(opt[i, j]\) не более \(O(n)\).

-

c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } }

-

Сравните с базовым решением — всего 3 новых строчки.

-

Convex Hull Trick

-

Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на cost и увидев там скалярное произведение.

-

\[ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ -f[k, j-1] + x_{i-1}^2 -- 2x_{i-1} x_k -+ x_k^2 -\}\]

-

Посмотрим внимательнее на минимизируемое выражение. \(x_{i-1}^2\) не зависит от \(k\), значит его можно вынести. Под минимумом останется $ {ak} {bk} x_{i-1} $.

-

Это теперь можно переписать как $ k (ak, b_k) (1, X_{i-1}) $ (имеется в виду скалярное произведение)

-

Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

-

TODO: иллюстрация.

-

TODO: мем про Скуби-Ду.

-

Ли Шао

-

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

-

Лямбда-оптимизация

-

Не «фольклорное» название — дискретный метод множителей Лагранжа.

-

Пусть у нас есть функция \(f(x)\), которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .

-

Заметим следующее:

-

Давайте заменим \(f[i, j]\) на \(g_\lambda[i]\) — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный \(\lambda\), за использование отрезка.

-

Давайте сделаем бинпоиск по \(\lambda\). Понятно, что для каких-то лямбд мы будем использовать ровно \(j\) отрезков. Тогда ответ на динамику \(f[i, j] = g[i] - \lambda k\).

-

Суммируем

-

TODO: сделать табличку

-
    -
  • Разделяйка: \(O(nm \log n)\), если cost такой, что opt монотонна по одному аргументу.
  • -
  • Кнут: \(O(nm)\), если cost такой, что opt монотонна по обоим аргументам.
  • -
  • CHT: \(O(nm)\). В оптимизируемой функции нужно увидеть скалярное произведение.
  • -
  • Лагранж: \(O(n \log n)\). Функция должна быть выпуклой.
  • -
-

Другие задачи

-

Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.

-
-

Есть \(n\) ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить \(m\) амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар.

-
- - diff --git a/ru/dp_intro.html b/ru/dp_intro.html deleted file mode 100644 index 94e888a..0000000 --- a/ru/dp_intro.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - -

Динамическое программирование

-

test123

-
-

Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

-
-

Разберем некоторые классические идеи и задачи в динамическом программировании.

-

Рюкзак

-
-

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

-
-

Наибольшая общая подпоследовательность

-
-

Есть две строки. Найти длину их наибольшей общей подпоследовательности.

-
-

Расстановка скобок

-
-

Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

-
-

Можно завести динамику «можно ли ».

-

Наибольшая возрастающая подпоследовательность

-

Есть также алгоритм за \(O(n \log n)\).

-

Мемоизация

-

Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

-

Первый предполагает

-

Число разбиений

-
- -
-

Замена параметра на ответ

-
- -
- - diff --git a/ru/dp_optimizations.html b/ru/dp_optimizations.html deleted file mode 100644 index ac2305b..0000000 --- a/ru/dp_optimizations.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - -

Оптимизации динамики

-
    -
  • Разделяй и властвуй
  • -
  • Оптимизация Кнута
  • -
  • Convex Hull Trick
  • -
  • Лямбда-оптимизация
  • -
-

Задача

-
-

Даны \(n\) точек на прямой. Нужно найти \(m\) отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.

-
-

Базовое решение — следующая динамика: \(f[i, j]\) — минимальная стоимость покрытия \(i\) первых (самых левых) точек, используя не более \(j\) отрезков (итоговый ответ будет записан в \(f[n, m]\) — прим. К. О.).

-

Переход — перебор всех возможных последних отрезков, то есть \(f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}\).

-

```c++ // x[] — отсортированный массив координат точек, нумерация с нуля

-

// квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }

-

// TODO: предподсчитать cost

-

for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности

-

for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ```

-

Заметим, что циклы по i и j можно поменять местами.

-

Такое решение пока работает за \(O(n^2 m)\) — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить.

-

Разделяй-и-властвуй

-

Обозначим за \(opt[i, j]\) оптимальный \(k\), на котором \(f[i, j]\) минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.

-

Утверждение. \(opt[i, j] \leq opt[i, j+1]\).

-

Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.

-

Что это нам даёт? Если мы уже знаем \(opt[i, l]\) и \(opt[i, r]\) и хотим посчитать \(opt[i, j]\) для какого-то \(j\) между \(l\) и \(r\), то мы можем сузить отрезок поиска оптимального индекса с \([0, i-1]\) до \([opt[i, l], opt[i][r]]\).

-

Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка \([l, r]\), зная, что их \(opt\)-ы лежат между \(l'\) и \(r'\). Она берет середину отрезка \([l, r]\) и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно.

-

c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); }

-

Вызываться она будет просто последовательно для каждого слоя:

-

c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k);

-

Теперь пересчет одного «слоя» динамики занимает \(O(n \log n)\) вместо \(O(n^2)\). Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет \(O(\log n)\), а значит и каждый элемент будет просмотрен не более \(O(\log n)\) раз.

-

Получается, что асимптотика улучшилась до \(O(n m \log n)\).

-

Оптимизация Кнута

-

Предыдущий метод основывался на том факте, что \(opt[i, j] \leq opt[i, j+1]\). А что, если \(opt\) монотонен ещё и по первому параметру?

-

\[ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] \]

-

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

-

Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:

-

\[ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) \]

-

Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из \(opt[i, j]\) не более \(O(n)\).

-

c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } }

-

Сравните с базовым решением — всего 3 новых строчки.

-

Convex Hull Trick

-

Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на cost и увидев там скалярное произведение.

-

\[ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ -f[k, j-1] + x_{i-1}^2 -- 2x_{i-1} x_k -+ x_k^2 -\}\]

-

Посмотрим внимательнее на минимизируемое выражение. \(x_{i-1}^2\) не зависит от \(k\), значит его можно вынести. Под минимумом останется $ {ak} {bk} x_{i-1} $.

-

Это теперь можно переписать как $ k (ak, b_k) (1, X_{i-1}) $ (имеется в виду скалярное произведение)

-

Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

-

TODO: иллюстрация.

-

TODO: мем про Скуби-Ду.

-

Ли Шао

-

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

-

Лямбда-оптимизация

-

Не «фольклорное» название — дискретный метод множителей Лагранжа.

-

Пусть у нас есть функция \(f(x)\), которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .

-

Заметим следующее:

-

Давайте заменим \(f[i, j]\) на \(g_\lambda[i]\) — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный \(\lambda\), за использование отрезка.

-

Давайте сделаем бинпоиск по \(\lambda\). Понятно, что для каких-то лямбд мы будем использовать ровно \(j\) отрезков. Тогда ответ на динамику \(f[i, j] = g[i] - \lambda k\).

-

Суммируем

-

TODO: сделать табличку

-
    -
  • Разделяйка: \(O(nm \log n)\), если cost такой, что opt монотонна по одному аргументу.
  • -
  • Кнут: \(O(nm)\), если cost такой, что opt монотонна по обоим аргументам.
  • -
  • CHT: \(O(nm)\). В оптимизируемой функции нужно увидеть скалярное произведение.
  • -
  • Лагранж: \(O(n \log n)\). Функция должна быть выпуклой.
  • -
-

Другие задачи

-

Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.

-
-

Есть \(n\) ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить \(m\) амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар.

-
- - diff --git a/ru/fenwick.html b/ru/fenwick.html deleted file mode 100644 index 8ba5386..0000000 --- a/ru/fenwick.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - -
- НЯ!
Эта статья полна любви и обожания.
Возможно, стоит добавить ещё больше? -
-

Дерево Фенвика

-

Дерево Фенвика или бинарно индеквированное дерево (англ. binary indexed tree) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности.

-

Определение

-

Пусть дан массив \(a\) длины \(n\). Деревом Фенвика будем называть массив \(t\) той же длины, который объявим так:

-

\[ t_i = \sum_{k=F(i)}^i a_k \]

-

где \(F\) это какая-то функцию, для которой выполнено \(F(i) \leq i\). Конкретно её определим потом.

-

Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе (\(sum(l, r) = sum(r) - sum(l-1)\)), каждый из которых будем считать по этой формуле:

-

\[ sum(k) = t_k + sum(F(k)-1) \]

-

Когда мы изменяем \(k\)-ю ячейку исходного массива, мы обновляем все \(t_i\), в которых учтена эта ячейка.

-

\(F\) можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам \(t_i\) при обновлении будет будет \(O(\log n)\). Популярны две функции:

-
    -
  • \(F_1(x) =\) x & (x + 1)
  • -
  • \(F_2(x) =\) x - (x & -x) + 1
  • -
-

Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.

-

Disclaimer: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на \(O(\log n)\) отрезков вида \([F(i), i]\), и любой элемент входит в не более \(O(\log n)\) таких отрезков.

-

Реализация

-

Из-за того, что \(F(0) = 1 > 0\) и поэтому \([0, F(0)]\) не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать \(t_0\).

-

```c++ int t[maxn];

-

// возвращает сумму на префиксе int sum (int r) { int res = 0; for (; r > 0; r -= r & -r) res += t[r]; return res; }

-

int sum (int l, int r) { return sum(r) - sum(l-1); }

-

// обновляет нужные t void add (int k, int x) { for (; k <= n; k += k & -k) t[k] += x; } ```

-

Автор отмечает красивую симметрию в формулах r += r & -r и k -= k & -k, которой нет в «традиционной» версии.

-

Многомерный случай

-
-

\(k\)-мерное дерево Фенвика пишется в \((k+1)\) строчку

-
-

Нужно добавить всего одну такую же строчку в sum, add, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре.

-

sum перепишется следующим образом:

-

c++ int sum (int r1, int r2) { int res = 0; for (int i = r1; i > 0; i -= i & -i) for (int j = r2; j > 0; j -= j & -j) ans += t[i][j]; return res; }

-

В \(k\)-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно \(2^k\) запросов суммы на префиксах.

-

Если размерности больше, чем позволяет память, то можно вместо массива t использовать хэш-таблицу — так потенциально потребуется \(O(q \log^2 A)\) памяти (\(A\) — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом решил какую-то задачу на 2d-сумму с USACO 2017.

-

Бинпоиск

-

Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за \(O(\log n)\).

-

c++ // возвращает индекс, на котором сумма уже больше int lower_bound (int s) { int k = 0; for (int l = logn; l >= 0; l--) { if (k + (1<<l) <= n && t[k + (1<<l)] < s) { k += (1<<l); s -= t[k]; } } return k; }

-

Если знать, что \(F(x)\) удаляет последний бит \(x\), то принцип понятен: просто делаем спуск по бинарному дереву, как в ДО. Чем-то похоже на генерацию \(k\)-го лексикографического комбинаторного объекта: пытаемся увеличить следующий символ всегда, когда это возможно.

-

Отметим, что в «традиционной» индексации такое делать нельзя.

-

Ограничения

-

Дерево Фенвика можно использовать, когда наша операция обратима, а также когда трюк с префиксными суммами работает. Это обычно простые операции типа суммы, xor, умножения по модулю (если гарантируется, что на этот модуль ничего не делится). Минимум и gcd, отложенные операции и персистентность прикрутить в общем случае уже не получится — тогда уже нужно писать ДО.

-

Почему работает

-

Итак, мы выбрали вариант с \(F(x)\) = x - (x & -x) + 1. Поймем, что означает x & -x.

-

Лемма. x & -x возвращает последний единичный бит в двоичной записи x.

-

Доказательство потребует знания, как в компьютерах хранятся целые числа. Чтобы процессор не сжигал лишние такты, проверяя знак числа при арифметических операциях, их хранят как бы по модулю \(2^k\), а первый бит отвечает за знак (0 для положительных и 1 для отрицательных). Поэтому когда мы хотим узнать, как выглядит отрицательное число, нужно его вычесть из нуля: \(-x = 0-x = 2^k-x\).

-

Например,

-

\[ -\begin{align} - \begin{aligned} - +90 = 2+8+16+64 & = 0 \, 10110_2 \\ - -90 = 00000_2 - 10110_2 & = 1 \, 01010_2 \\ - \implies (+90) \text{ & } (-90) & = 0 \, 00010_2 \\ - \end{aligned} -\end{align} -\]

-

Вернёмся к доказательству леммы. Когда мы вычитаем, мы идем справа налево. В ответе можно мысленно разделить на три блока: * Первые сколько-то (возможно, нисколько) нулей с конца ими же и останутся. * Потом, ровно на самом младшем единичном бите, мы «займём» много единиц, так что весь префикс станет единицами. В ответе на этом месте точно будет единица. Потом отменятся ровно те из них, которые были единицами в исходном числе.

-

Делаем &: в каком-то префиксе все биты будут противоположными, младший единичный бит останется, а на суффиксе все как было нулями, так и осталось. Выживет только этот самый младший единичный бит.

-

Теперь сразу понятно, почему sum будет работать за логарифм — каждый раз мы делаем x -= x & -x, то есть удаляем один бит.

-

Теперь сложная (для понимания) часть — как делать add. Какие \(t_i\) содержат \(k\)-й элемент? Для них должно выполняться i >= k > i - (i & -i).

-

Будем перебирать префиксы TODO

-

Мы знаем, что \(t_i\) вложены друг в друга. Минимальный подходящий \(i\) равен \(k\). Какой следующий? Нам нужно для каждого \(i\) уметь находить его непосредственного родителя.

-

Можно представить дерево так: ячейка 2^k содержит все

-

TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию.

-

Название

-

Потому что \(F\) использует битовые операции, по-английски структура называется «Binary Indexed Tree».

-

Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.

-

Можно показать, что множества элементов, учтенных в \(t_i\) и \(t_j\), либо не пересекаются, либо одно является подмножеством другого. Значит, между \(t_i\) можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).

-

В частном случае, когда длина массива равна \(2^k\), то дерево будет только одно.

- - diff --git a/ru/games.html b/ru/games.html deleted file mode 100644 index 36b31b5..0000000 --- a/ru/games.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - -

Теория игр

-

Начнём с самого баянного примера математической игры, который можно вспомнить:

-
-

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

-
-

Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет \(f_k\) — выиграет ли первый игрок, если спичек сейчас \(n\), и ход принадлежит ему. Пересчитывать её нужно так:

-

\[ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) \]

-

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

-

Примечание. Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли \(n\) на 4, но мы всё равно рассмотрим этот пример в педагогических целях.

-

Игры на графах

-

В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «\(n\) спичек».

-

Любую игру можно описать в виде графа (возможно, бесконечного) состояний игры, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как терминальные

-

Состояние называется выигрышным, если игрок, начинающий в нём, побеждает, и проигрышным в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём ничейными.

-

Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют играми с нулевой суммой (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется ретроанализом.

-

Ретроанализ

-

Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности:

-
    -
  • Вершина проигрышная — если все её переходы ведут в выигрышные вершины.
  • -
  • Вершина выигрышная — если из неё есть переход в проигрышную вершину.
  • -
  • Вершина ничейная — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные).
  • -
-

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

-

Корректность. Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин.

-

Рассмотрим граф неизвестных вершин \(U\). Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина \(v\), у которой нет исходящих рёбер в \(U\) — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины \(v\) определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние.

-

Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось.

-

Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф \(U\). В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в \(U\) — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные.

-

Асимптотика. Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали.

-
    -
  • При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф).
  • -
  • При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше.
  • -
-

Асимптотика составит \(O(n + m)\), как и у любого bfs-а.

-

```c++ vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа int cnt[maxn]; // счётчик исходящих рёбер

-

enum StatusType { win, loss, unknown }; StatusType status[maxn]; // выгрышность вершины; // по умолчанию все кроме терминальных считаются unknown // те, кто в итоге остаются unknown -- ничейные

-

queue q = {/* нужно заранее добавить сюда все терминальные вершины*/};

-

while (!q.empty()) { int v = q.front(); q.pop(); for (int u : t[v]) { cnt[u]--; // удаляем это ребро if (status[v] == unknown) { // из u есть ребро в проигрышную -- значит она выигрышная if (status[v] == loss) status[u] = win; // все ребра u ведут в выигрышные вершины -- значит она проигрышная if (status[v] == win && cnt[u] == 0) { status[u] = loss; // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь if (status[v] != unknown) q.push(u); } } } ```

-

Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации:

-

c++ StatusType dfs(int v) { if (status[v] != unknown) return status[v]; status[v] = loss; // изменим статус, когда найдём переход в проигрышную вершину for (int u : g[v]) if (dfs(u) == loss) status[v] = win; return status[v]; }

-

TODO: можно ли здесь циклы учесть?

-

Минимаксные игры

-

Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются минимаксными — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно.

-

Типичный граф минимаксной игры выглядит следующим образом:

-

-

В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников.

-

Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей.

-

Ретроанализ для больших графов

-

Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго.

-

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

-

Ретроанализ нужно оптимизировать.

-

Ограничение перебора

-

Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено \(K\) ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через \(K\) ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая численно оценивает, насколько первый игрок выигрывает.

-

Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за \(K\) ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит.

-

Ясно, что это дает существенное ускорение по времени: можно подобрать \(K\) так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли).

-

Мемоизация позиций

-

В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает .

-

Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары

-

Альфа-бета отсечение

-

-

Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым.

-

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

-

Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния \(X\) (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6.

-

Давайте зайдем в третьего сына, назовем его \(Y\), вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына \(Y\), обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына \(Y\) можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня \(X\) в вершину \(Y\), то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына \(X\) уже дает 6, что больше, и рассматривать вершину \(Y\) далее бессмысленно, мы в нее уже точно не пойдем.

-

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

- -

Игры с ненулевой суммой

-

Теория Шпрага-Гранди

-

Рассмотрим игру «ним»: даны \(n\) кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые.

-

Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в \(i\)-й как \(a_i\). Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями.

-

Теорема. Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 a_2 a_n $ размеров кучек отлична от нуля.

-

Доказательство проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы:

-
    -
  • Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на \(a_i \oplus b_i \), где \(b_i < a_i\) — это число камней в \(i\)-й кучке после нашего действия.
  • -
  • Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой \(b_i < a_i\), что xor-сумма станет нулевой, то есть \(S \oplus a_i \oplus b_i = 0\). Для этого посмотрим на старший взведенный бит \(S\) и возьмем любой \(a_i\), у которого этот бит тоже взведен. Такой \(a_i\) найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый \(b_i\) равен \(S \oplus a_i\), и выясняется, что это корректный новый размер кучки, то есть \(b_i < a_i\). Почему так? Потому что все старшие биты в выражении остались нетронутыми, \(k\)-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем \(2^k\).
  • -
-

Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех \(a_i\), найти такой \(a_i\), у которого старший бит взведен, и заменить его на \(S \oplus a_i\).

-

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

-

Зачем это надо? Есть много игр, в которых присутствует какой-то подобный цугцванг (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен.

-

Ним с увеличениями

-

Пусть у нас

-

Эквивалентность игр ниму

-

Игры с неполной информацией

-

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

-

Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет.

-

Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло.

-

Дилемма заключенного

-

Камень-ножницы-бумага

-

Покер

-

Рассмотрим такую у

- - diff --git a/ru/geometry.html b/ru/geometry.html deleted file mode 100644 index 70ed75d..0000000 --- a/ru/geometry.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - -

Геометрия и ООП

-

Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется вектором. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали.

-
-vector

vector

-
-

Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь.

-

Скалярное произведение (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:

-

\[ a \cdot b = x_a x_b + y_a y_b \]

-
-dot

dot

-
-

У него есть полезные свойства:

-
    -
  • Скалярное произведение симметрично (\(a \cdot b = b \cdot a\)).
  • -
  • Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.
  • -
  • Перпендикулярные вектора должны иметь нулевое скалярное произведение.
  • -
  • Если угол острый, то оно положительное
  • -
  • Если угол тупой, то отрицательное
  • -
-

Векторное произведение (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:

-

\[ a \times b = x_a y_b - y_a x_b \]

-
-cross

cross

-
-

Его свойства:

-
    -
  • Скалярное произведение антисимметрично (\(a \times b = - (b \times a)\)).
  • -
  • Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.
  • -
  • Коллинеарные вектора должны иметь нулевое векторное произведение.
  • -
  • Если b «слева» от a, то оно положительное.
  • -
  • Если b «справа» — то отрицательное.
  • -
-

Всякие проверки

-

Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.

-

Принадлежность точки треугольнику. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.

-

\[ -\text{P лежит внутри ABC} \iff \begin{cases} -(B-A) \times (P-A) \geq 0 \\ -(C-B) \times (P-B) \geq 0 \\ -(A-C) \times (P-C) \geq 0 \\ -\end{cases} -\]

-

Площадь треугольника. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.

-

\[ V = \frac{1}{2} (B-A) \times (C-A) \]

-

Площадь произвольного многоугольника. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).

-
-any

any

-
-

Забудьте о формуле Герона и всегда считайте площади через векторное произведение.

-

Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.

-

Проверка на выпуклость. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то \((b-a)\times(c-a) > 0\)).

-

Пересекаются ли отрезки.

-
-segments

segments

-
-

Уравнение прямой

-

Прямая задается уравнением вида \(Ax + By + C = 0\). Полуплоскость можно задать таким же неравенством.

-

У прямой есть вектор нормали с координатами \((A, B)\). Он перпендиуклярен прямой, а в случае с полуплоскостью \(Ax + By + C \geq 0\) будет указывать в сторону самой полуплоскости.

-

Чтобы найти расстояние от точки \((x_0, y_0)\) до прямой \(Ax + By + C = 0\), можно воспользоваться следующей формулой:

-

\[ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} \]

-

Точка пересечения. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:

-

\[ -\begin{cases} -A_1 x + B_1 y + C_1 = 0 \\ -A_2 x + B_2 y + C_2 = 0 -\end{cases} -\implies -\begin{cases} --x = \frac{B_1 y + C_1}{A_1} \\ --x = \frac{B_2 y + C_2}{A_2} -\end{cases} -\implies -\frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} -\implies -y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} -\]

-

Аналогично, \(x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}\) (обратите внимание на знаки).

-

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

-

Как это кодить в C++

-

Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

-

Точка \(\simeq\) вектор. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её радиус-вектор — вектор из начала координат, ведущий в эту точку. По принятой в математике и физике нотации, будем обозночать вектора как r. Вы можете обозвать их как point, pt, vec — как угодно.

-

c++ struct r { double x, y; r () {} r (int _x, int _y) { x = _x, y = _y; } };

-

Функция r внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, r() вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами \((x, y)\).

-

Операции над векторами

-

Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:

-

c++ double len (r a) { return sqrt(a.x*a.x + a.y*a.y); }

-

Операторы

-

В C++ можно перегружать почти все стандартные операторы, например, +, -, << и т. д.

-

Давайте для будущих нужд определим + и -:

-

c++ r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); } r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); }

-

Скалярное произведение

-

c++ int operator*(r a, r b){ return a.x*b.x + a.y*b.y; }

-

Векторное произведение

-

Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике.

-

c++ int operator^(r a, r b){ return a.x*b.y - b.x*a.y; }

-

Ввод-вывод

-

Как вы думаете, как на самом деле работает cin >> x;? Это тоже перегрузка оператора >>. Делать это нужно так:

-

```c++ istream& operator>>(istream &in, r &p){ in >> p.x >> p.y; return in; }

-

ostream& operator<<(ostream &out, r &p){ out << p.x << " " << p.y << endl; return out;
} ```

-

Алгебра VS Алгоритмы или зачем мы всё это делали

-

Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать intersect(a, b), в корректности которого он точно уверен.

-

Векторное представление прямой (\(Ax + By + C = 0 \rightarrow r = at + b\))

-

Тут нужно просто выбрать две любые точки на прямой.

-

c++ // даны A, B, C (A^2 + B^2 != 0) r a, b; if (eq(A, 0)) // значит, это горизонтальная прямая a = r(0, -C/B), b = r(1, -C/B); else a = r(-C/A, 0), b = (1, -(C+B)/A, 1)

-

Пример: отражение от прямой

-

Пусть нам надо отразить точку \((x_0, y_0)\) симметрично относительно заданной прямой \(ax+by+c=0\). Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.

-

\(\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} \)

-

Формула имеет смысл: длина на единичный вектор направления.

-

Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить.

-

```c++ // прямая r = at + b, точка c r pr (r a, r b, r c) { c -= b; // пусть c и a выходят из одной точки return b + (ab / len(a) / len(a)) a; }

-

r reflect (r a, r b, r c) { return c + 2*(pr(a, b, c)-c); } ```

-

Типичные баги

-

Точность

-
-

Первое правило действительных чисел — не использовать действительные числа

-
-

Все переменные типа double хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно всегда учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число eps:

-

```c++ const double eps = 1e-8;

-

bool eq (double a, double b) { return abs(a-b) < eps } ```

-

Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре.

-

\(0 \neq -0\)

-

Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду.

-

Область определения обратных функций

-

acos, asin и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции.

- - diff --git a/ru/hashing.html b/ru/hashing.html deleted file mode 100644 index ae9b984..0000000 --- a/ru/hashing.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - -

Хэширование в строковых задачах

-

Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.

-

«Хорошая» хэш-функция: * Быстро считается — за линейное от размера объекта время; * Имеет не очень большие значения — влезающие в 64 бита; * «Детерминированно-случайная» — если хэш может принимать \(n\) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно \(\frac{1}{n}\).

-

Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюрьективными.

-

Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\), и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

-
-hash

hash

-
-

Применения в реальной жизни

-
    -
  • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
  • -
  • Хэш-таблица. Класс unordered_set из STL можно реализовать так: заведём \(n\) изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию \(f\) с областью значений \([0, n)\). При обработке .insert(x) мы будем добавлять элемент \(x\) в \(f(x)\)-тый список. При ответе на .find(x) мы будем проверять, лежит ли \(x\)-тый элемент в \(f(x)\)-том списке. Благодаря «равномерности» хэш-функции, после \(k\) добавлений ожидаемое количество сравнений будет равно \(\frac{k}{n}\) = \(O(1)\) при правильном выборе \(n\).
  • -
  • Мемоизация. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.
  • -
  • Проверка на изоморфизм. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.
  • -
  • Криптография. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.
  • -
  • Поиск в многомерных пространствах. Детерминированный поиск ближайшей точки среди \(m\) точек в \(n\)-мерном пространстве быстро не решается. Однако можно придумать хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши, и делать поиск только среди элементов с тем же хэшом, что у запроса.
  • -
-

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

-

Сегодня же мы остановимся на строках.

-

Полиномиальное хэширование

-

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

-

Будем считать, что строка — это последовательность чисел от \(1\) до \(m\) (размер алфавита). В C++ char это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: int x = (int) (c - 'a' + 1).

-

Определим прямой полиномиальный хэш строки как значение следующего многочлена:

-

\[ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p \]

-

Здесь \(k\) — произвольное число больше размера алфавита, а \(p\) — достаточно большой модуль, вообще говоря, не обязательно простой.

-

Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени \(k\):

-

```c++ const int k = 31, mod = 1e9+7;

-

string s = "abacabadaba"; long long h = 0, m = 1; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h + m * x) % mod; m = (m * k) % mod; } ```

-

Можем ещё определить обратный полиномиальный хэш:

-

\[ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p \]

-

Его преимущество в том, что можно написать на одну строчку кода меньше:

-

c++ long long h = 0; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h * k + x) % mod; }

-

Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой \(h\).

-

Зачем он нужен?

-

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

-

Например, если нужно посчитать хэш от конкатенации строк \(a\) и \(b\) (т. е. \(b\) приписали в конец строки \(a\)), то можно просто хэш \(b\) домножить на \(k^{|a|}\) и сложить с хэшом \(a\):

-

\[ h(ab) = h(a) + k^{|a|} \cdot h(b) \]

-

Удалить префикс строки можно так:

-

\[ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} \]

-

А суффикс — ещё проще:

-

\[ h(a) = h(ab) - k^{|a|} \cdot h(b) \]

-

В задачах нам часто понадобится домножать \(k\) в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:

-

```c++ const int maxn = 1e5+5;

-

int p[maxn]; p[0] = 1;

-

for (int i = 1; i < maxn; i++) p[i] = (p[i-1] * k) % mod; ```

-

Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:

-

```c++ int h[maxn]; h[0] = 0; // h[k] -- хэш префикса длины k

-

// будем считать, что s это уже последовательность int-ов

-

for (int i = 0; i < n; i++) h[i+1] = (h[i] + p[i] * s[i]) % mod; ```

-

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

-

\[ h(s[l:r]) = \frac{h_r-h_l}{k^l} \]

-

Деление по модулю воможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

-

Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к \(n\)-ной. Так проще — нужно будет домножать, а не делить.

-

\[ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) \]

-

c++ int hash_substring (int l, int r) { return (h[r+1] - h[l]) * p[n-l] % mod; }

-

Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за \(O(1)\).

-

Упражнение

-

Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще.

-

Примеры задач

-

Количество разных подстрок. Посчитаем хэши от всех подстрок за \(O(n^2)\) и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

-

Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

-

Сравнение строк (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.

-

Палиндромность подстроки. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений hash_substring() на первом массиве и на втором.

-

Количество палиндромов. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно.

-

Изменения строки*

-

Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в декартово дерево. В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в upd() пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.

-

Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.

-

Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова.

-

Вероятность ошибки и почему это всё вообще работает

-

У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.

-

Событие, когда два хэша совпали, а не должны, называется коллизией. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в set \(O(n^2)\) различных случайных значений в промежутке \([0, m)\). Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать \(m\), чтобы не бояться такого?

-

Выбор констант

-

Практическое правило: если вам нужно хранить \(n\) различных хэшей, то безопасный модуль — это число порядка \(10 \cdot n^2\). Обоснование — см. парадокс дней рождений.

-

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

-

Можно также брать модуль \(2^{64}\). У него есть несколько преимуществ: * Он большой — второй модуль точно не понадобится. * С ним ни о каких переполнениях заботиться не нужно — если все хранить в unsigned long long, процессор сам автоматически сделает эти взятия остатков при переполнении. * С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию %.

-

Всё с этим модулем было прекрасно, пока не придумали тест против него. Однако, его добавляют далеко не на все контесты — имейте это в виду.

-

В выборе же \(k\) ограничения не такие серьезные: * Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. * Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.

-

Главное — чтобы значения \(k\) и модуля не знал человек, который генерирует тесты.

-

Парадокс дней рождений

-
-

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

-
-

Более общее утверждение: в мультимножество нужно добавить \(\Theta(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

-

Первое доказательство (для любителей матана). Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

-

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

-

Попытаемся оценить \(f\):

-

\[ -\begin{align} - \begin{aligned} - e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ - & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ - e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ - f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ - & = e^{-\frac{n(n-1)}{2d}} & \\ - & \simeq e^{-\frac{n^2}{2d}} & \\ - \end{aligned} -\end{align} -\]

-

Из последнего выражения более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется очень быстро.

-

Второе доказательство (для любителей теорвера). Введем \(\frac{n(n-1)}{2}\) индикаторов — по одному для каждой пары людей \((i, j)\) — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна \(\frac{1}{d}\).

-

Обозначим за \(X\) число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть \(\frac{n (n-1)}{2} \cdot \frac{1}{d}\).

-

Отсюда понятно, что если \(d = \Theta(n^2)\), то ожидание равно константе, а если \(d\) асимптотически больше или меньше, то \(X\) стремится нулю или бесконечности соответственно.

-

Примечание: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1.

-

Бонус: «мета-задача»

-

Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.

-

«Решите» задачу.

- - diff --git a/ru/hld.html b/ru/hld.html deleted file mode 100644 index 9c5552b..0000000 --- a/ru/hld.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - -

Heavy-light декомпозиция

-

HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать что-нибудь попроще.

-

-* -
-TODO: найти менее уродливую иллюстрацию -
-
    -
  • -
-

Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины \(v\) посмотрим на всех её непосредственных детей \(u\), выберем среди них ребёнка \(u_{max}\) (с самым большим размером поддерева) и назовём ребро \((v, u)\) тяжелым (heavy), а все остальные рёбра — лёгкими (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.

-

Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.

-

Утверждение. Дерево разбивается на непересекающиеся пути из тяжелых рёбер.

-

Доказательство. В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.

-

Назовём блоком либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.

-

Утверждение. На любом вертикальном пути будет не более \(O(\log n)\) блоков.

-

Доказательство разбивается на две части:

-
    -
  • Лёгких ребер на вертикальном пути будет не более \(O(\log n)\): рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего.
  • -
  • Непрерывных путей из тяжелых рёбер будет не более \(O(\log n\): если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего \(O(\log n)\).
  • -
-

Следствие. На любом пути будет не более \(O(\log n)\) блоков.

-

Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на \(O(\log n)\) запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам.

-

Реализация

-

Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.

-

А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то tin-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.

-

Теперь мы можем построить какую-нибудь структуру поверх массива размера \(n\) (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре.

-

```c++ vector g[maxn]; int s[maxn], p[maxn], tin[maxn], tout[maxn]; int head[maxn]; // «голова» тяжелого пути, которому принадлежит v int t = 0;

-

void sizes (int v = 0) { s[v] = 1; for (int &u : g[v]) { sizes(u); s[v] += s[u]; if (s[u] > s[g[v][0]]) // &u -- это ссылка, так что её легально использовать при swap-е swap(u, g[v][0]); } }

-

void hld (int v = 0) { rin[t] = v; tin[v] = t++; for (int u : g[v]) { // если это тяжелый ребенок -- его next нужно передать // в противном случае он сам является головой нового пути head[u] = (u == g[v][0] ? head[v] : u); hld(u); } tout[v] = t; } ```

-

Как им решать задачи

-

Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:

-
    -
  1. Узнать минимальное число на пути между \(v_i\) и \(u_i\).
  2. -
  3. Изменить число у \(v_i\)-той вершины на \(x_i\).
  4. -
-

Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в соответствующей статье и имеет методы upd(k, x) и get_min(l, r).

-

c++ int val[maxn]; segtree st(0, n);

-

При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:

-

c++ void upd (int v, int x) { st.upd(tin[v], x); }

-

Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям.

-

```c++ int ancestor (int a, int b) { return tin[a] <= tin[b] && tin[b] <= tout[a]; }

-

void up (int &a, int &b, int &ans) { while (!ancestor(head[a], b)) { ans = min(ans, st.get_min(tin[head[a]], tin[a])); a = p[head[a]]; } }

-

int get_min (int a, int b) { int ans = inf; up(a, b, ans); up(b, a, ans); if (!ancestor(a, b)) swap(a, b); ans = min(ans, st.get_min(tin[a], tin[b])); return ans; } ```

-

- - diff --git a/ru/index.html b/ru/index.html deleted file mode 100644 index f6871b1..0000000 --- a/ru/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - -

Алгоритмика

-

Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса Tinkoff Generation.

-

Более-менее готово:

- -

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

-

Не очень готово:

- -

Вообще не готово: * Алгоритм Карацубы: введение в разделяй-и-властвуй, мастер-теорема * Ахо-Корасик * Теорвер * Корневая декомпозиция * Теория информации * Функции потерь в машинном обучении * Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском) * Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском)

-

Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности.

- - diff --git a/ru/ioi.html b/ru/ioi.html deleted file mode 100644 index 803575c..0000000 --- a/ru/ioi.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - -

IOI Upsolving Challenge

-

https://contest.yandex.com/ioi/total

-

Я решал их почти 2 года назад и уже не очень хорошо всё помню.

-

09-2

-

12 мая

-

Медведь Миша

-

Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за \(O(n^2 \log n)\).

-

Регионы

-

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

-

Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так:

-
    -
  • Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк
  • -
  • Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос
  • -
-

Такое должно работать за \(O(n \sqrt{n \log n})\), и TL такое позволяет.

-

Коммивояжёр

-

Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать.

-

Подумайте, какой вид будет иметь эта функция, если зафиксировать время.

-

TODO

-

05-1

-

31 мая

-

Сад

-

Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно.

-

Средняя последовательность

-

Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы:

-

```c++ int a = -inf, b = inf, d = 0, d = 0, t; for(int i = 0; i < n; i++){ cin >> t; d = d; d = 2*t - d; if (i % 2 == 0) b = min(b, (d-d)/2); else a = max(a, (d-d)/2); }

-

cout << max(b - a + 1, 0ll); ```

-

Польские горки

-

По сути от нас просят обрабатывать запросы «присвоить всем элементам с \(l\) по \(r\) значение \(x\)» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше.

-

05-2

-

Прямоугольник

-

Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста.

- - diff --git a/ru/karatsuba.html b/ru/karatsuba.html deleted file mode 100644 index 0f347e9..0000000 --- a/ru/karatsuba.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - -

Быстрое умножение

-

В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).

-

Андрей Колмогоров и несколько других пионеров компьютер саенса выдвинули «гипотезу \(n^2\)» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

-

Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.

-

Алгоритм Карацубы имеет довольно значимое место в истории науки.

-

{M(n)=O(n^{2}).} M(n)=O(n^{2}). У Колмогорова была гипотеза, что нижняя оценка для {M(n)} M(n) при любом методе умножения есть также величина порядка {n^{2}} n^{2}. На правдоподобность «гипотезы {n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности {M(n)=O(n^{{2}3})} M(n)=O(n^{{{2}3}}) и тем самым опроверг «гипотезу {n^{2}} n^{2}».

-

(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)

-

Можно представить себе какого-то грозного японца.

-

Мастер-теорема

-

Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек».

-

Мастер-теорема. Пусть имеется рекуррента:

-

\[ T(n) = \begin{cases} -a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 -\\ \Theta(1), & n \leq n_0 -\end{cases} \]

-

Тогда:

-
    -
  • A. Если \(c > \log_b a\), то \(T(n) = \Theta(n^c)\).
  • -
  • B. Если \(c = \log_b a\), то \(T(n) = \Theta(n^c \log n)\).
  • -
  • C. Если \(c < \log_b a\), то \(T(n) = \Theta(n^{\log_b a})\).
  • -
-
-

-
-

Доказательство. Рассмотрим «дерево рекурсии» этого соотношения. В нём будет \(log_b n\) уровней. На \(k\)-том уровне будет \(a^k\) вершин, каждая из которых будет стоить \((\frac{n}{b^k})^c\) операций. Просуммируем значения во всех вершинах по всем уровням:

-

\[ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k \]

-
    -
  • A. Если \(c > \log_b a\), то \(\sum (\frac{a}{b^с})^k\) это сумма убывающей геометрической прогрессии, которая не зависит от \(n\) и просто равна какой-то константе. Значит, \(T(n) = \Theta(n^c)\).
  • -
  • B. Если \(c = \log_b a\), то \[\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)\]
  • -
  • C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,
  • -
-

\[ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) \]

-

Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает \(O(n)\)

-

Алгоритм Карацубы

-

Алгоритм Карацубы сводит задачу умножения двух чисел длины \(n\) к возведению \(n\)-значного числа в квадрат.

-

Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи.

-

Развитие идеи

-

То же самое можно применить матрицам.

-

Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за \(O(n^{1+\epsilon})\) для произвольного \(\epsilon\) ещё никто не доказал.

- - diff --git a/ru/lca.html b/ru/lca.html deleted file mode 100644 index 29f5315..0000000 --- a/ru/lca.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - -

Корневые деревья

-

Дерево называется корневым, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется корнем.

-

Примеры корневых деревьев: * наследование классов в языках программирования (если множественное наследование запрещено), * дерево факторизации числа на простые (в общем случае не уникальное), * иерархия в какой-нибудь компании, * просто множество как-то вложенных объектов (ссылка на крысу-роботягу).

-

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

-
-dfs

dfs

-
-

Напоминание: DFS

-

Посчитаем для каждой вершины времена входа (\(tin\)) и выхода (\(tout\)) из неё во время эйлерова прохода.

-

```c++ vector g[maxn]; int p[maxn], tin[maxn], tout[maxn]; int t = 0;

-

void dfs (int v) { tin[v] = t++; for (int u : g[v]) dfs(u); tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет } ```

-

У этих массивов много полезных свойств: * Вершина \(u\) является предком \(v\) \(\iff tin_v \in [tin_u, tout_u) \). Эту проверку можно делать за константу. * Два полуинтервала — \([tin_v, tout_v)\) и \([tin_u, tout_u)\) — либо не пересекаются, либо вложены один в другой. * В \(tin\) есть все числа из промежутка от 0 до \(n-1\). У каждой вершины — свой номер. * Размер поддерева вершины \(v\) (включая саму вершину) равен \(tout_v - tin_v\). * Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации.

-

Запросы на поддеревьях

-

Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.

-
-

Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число \(x_i\) и найти значение числа у вершины \(v_i\).

-
-

Давайте запишем все числа у вершин в позиции, соответствующие \(tin\)-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать какой-нибудь достаточно продвинутой структурой.

-

Запросы на уровнях

-
-

Дано корневое дерево. Требуется отвечать на запросы нахождения \(d_i\)-того предка вершины \(v_i\) (т. е. вершины-предка, находящейся на расстоянии \(d_i\)).

-
-

Создадим \(h\) векторов, где \(h\) — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их \(tin\)-ам.

-

Теперь заметим, что отрезки их поддеревьев — \([tin_v, tout_v)\) — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять \(tin\) вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку.

-

Наименьший общий предок

-

Очень много задач нам поможет решить следующая вспомогательная задача.

-
-

Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин \(u_i\) и \(v_i\), то есть вершины \(w\), которая лежит на пути от корня до \(u_i\), на пути от корня до \(v_i\), и при этом самую глубокую (нижнюю) из всех таких.

-
-

По-английский эта задача называется Least Common Ancestor. Есть много разных способов её решать, и мы рассмотрим основные.

-
-lca

lca

-
-

Для лучшего понимания: медленно (за линейное время) это можно делать так:

-

```c++ bool a (int u, int v) { return tin[u] <= tin[v] && tin[v] <= tout[u]; }

-

int lca (int u, int v) { while (!ancestor(u, v)) u = p[u]; return u; } ```

-

LCA: двоичные подъемы

-

Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве up размера \(n \times \lceil \log n \rceil\) — столько точно хватит. В up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует — то корень.

-

Такой препроцессинг можно выполнить за \(O(n \log n)\), используя тот факт, что предок на расстоянии \(2^{d+1}\) — это предок на расстоянии \(2^d\) предка на расстоянии \(2^d\):

-

```c++ int up[maxn][logn];

-

void dfs (int v) { for (int l = 1; l < logn; l++) up[v][l] = up[up[v][l-1]][l-1]; tin[v] = t++; for (int u : g[v]) { up[u][0] = v; dfs(u); } tout[v] = t++; } ```

-

Пусть поступил очередной запрос — пара вершин \((u, v)\): * Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. * Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.

-

Подробнее про второй пункт. Пусть \(L = \lceil \log n \rceil\). Присвоим \(i = L\). Будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком \(u\) (указатель up[v][i] изначально будет корнем, а затем каждую итерацию спускаться на \(2^i\)). Когда это произойдёт, подвинем указатель на \(2^i\)-го предка \(v\), и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на \(2^{i+1}\).

-

c++ int lca (int v, int u) { if (a(v, u)) return v; if (a(u, v)) return u; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l]; return up[v][0]; }

-

Асимптотика

-

Препроцессинг — \(O(n \log n)\). Размер массива up ровно такой, и каждый его элемент вычисляется за константу.

-

Ответ на запрос — \(O(\log n)\), потому что по сути мы делаем один бинпоиск.

-

Запросы на путях

-

Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).

-

Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером \(2^d\)-го предка минимум на соответствующем пути.

-

Мы знаем, что минимум на пути от \(u\) до \(v\) — это минимум от минимума на пути от \(u\) до \(lca(u, v)\) и от минимума на пути от \(v\) до \(lca(u, v)\). А каждый минимум — это минимум на всех двоичных подъемах до LCA.

-

c++ int get_min (int v, int u) { int ans = inf; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l], ans = min(ans, mn[v][l]); for (int l = logn-1; l >= 0; l--) if (!ancestor(up[u][l], v)) u = up[u][l], ans = min(ans, mn[u][l]); return min({ans, mn[v][0], mn[u][0]}) }

-

Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная.

-

Сведение к RMQ

-

Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.

-

-

Во втором массиве мы по сути выписали наш проход dfs-а.

-

Пусть у нас есть запрос: найти LCA вершин \(v\) и \(u\). Для определенности положим, что \(tin_v < tin_u\). Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из \(v\) и в первй раз вошли в \(u\). Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.

-

Получается, что можно найти LCA, просто найдя позицию минимума на отрезке \([tout_v, tin_u]\) в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы уже умеем.

-

Разреженная таблица

-

На практике асимптотику мы особо не улучшили — пока что всё равно требуется \(O(n \log n)\) времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.

-

Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за \(O(1)\), но использующая \(O(n \log n)\) препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в отдельной статье.

-

А наоборот можно?*

-

Примечание: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

-

Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.

-

Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера \(n-1\): единица стоит, если следующее значение больше, единица в противном случае ноль.

-

Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

-

\[O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)\]

-

Также посчитаем для каждой возможной маски размера \(\frac{\log n}{2}\) минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего \(\sqrt n\) (ради этого мы и делили логарифм на два).

-

ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.

-

Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.

-

Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать у Емакса.

-

Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.

-

Важный вывод такой: RMQ более общая задача, чем LCA. UPD: это неправда, я глупый.

- - diff --git a/ru/linalg.html b/ru/linalg.html deleted file mode 100644 index f6aa629..0000000 --- a/ru/linalg.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - - - - - -

Ликбез по линейной алгебре

-

-

Определение. Функция \(f: \mathbb{R}^n \to \mathbb{R}^m\) называется линейной, если для неё выполнено

-
    -
  1. $ f(x+y) = f(x) + f(y) $
  2. -
  3. $ f(ax) = a f(x), ; a R $
  4. -
-

Примеры: * $ f(x) = 0 $ * $ f(x) = x $ * $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из \(\mathbb{R}^3\) в \(\mathbb{R}\)) * $ f(x) = (x, -x, 0) $ (из \(\mathbb{R}\) в \(\mathbb{R}^3\))

-

Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: * Сумма линейных функций — линейная функция. * Сумма коммутативна: \(f+g = g+f\)). * Сумма ассоциативна: \((f+g)+h = f+(g+h)\). * Композиция \(f(g(x)) = (f \circ g)(x)\) линейных функций — линейная функция. * Композция ассоциативна: \((f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h\). * Композиция в общем случае не коммутативна.
Пример: \(f = (-x_2, x_1)\) — поворот точки на плоскости на прямой угол, \(g = (x_1, 0)\) — проекция на \(Ox\). Почти для всех точек порядок этих операций важен.

-

Все свойства можно вывести лишь из этих двух пунктов в определении.

-

Что такое матрица?

-

Можно показать, что любую линейную функцию \(f: \mathbb{R}^n \to \mathbb{R}^m\) можно представить в таком виде:

-

\[ -f(x) = -\begin{pmatrix} -a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ -a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ -\ldots \\ -a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ -\end{pmatrix} -\]

-

Матрицы ввели просто как очень компактную запись этих коэффициентов \(a_{ij}\).

-

\[ -A = -\begin{pmatrix} -a_{11} & a_{12} & \ldots & a_{1n} \\ -a_{21} & a_{22} & \ldots & a_{2n} \\ -\vdots & \vdots & \ddots & \vdots \\ -a_{m1} & a_{m2} & \ldots & a_{mn} \\ -\end{pmatrix} -\]

-

Каждой линейной функции из \(\mathbb{R}^n\) в \(\mathbb{R}^m\) соответствует какая-то матрица размера \(n \times m\) (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении \(i\)-го строки и \(j\)-го столбца будем обозначать \(A_{ij}\). Не перепутайте.

-

Пусть линейной функции \(f\) соответствует матрица \(A\), а функции \(g\)\(B\). Тогда композиции этих функций \(h = f \circ g\) будет соответствовать произведение \(C\) матриц \(A\) и \(B\), определяемое так:

-

\[ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} \]

-

Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из \(g\) подставить в \(f\).

-

Когда перемножаете руками, удобно думать так: элемент на пересечении \(i\)-го столбца и \(j\)-той строки — это скалярное произведение \(i\)-той строки \(A\) и \(j\)-того столбца \(B\). Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер \(n \times k\), то вторая должна иметь размер \(k \times m\), то есть «средние» размерности обязательно должны совпадать.

-

-

Исходное выражение для \(f(x)\) теперь можно компактно записать как \(f(x) = Ax\) вместо \(m\) уравнений с \(n\) слагаемыми в каждом.

-

К матрицам не нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.

-

Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:

-
    -
  • Сумма матриц \(A\) и \(B\) — матрица \(C = A+B: C_{ij} = A_{ij} + B_{ij}\).
  • -
  • Сумма коммутативна: \(A+B = B+A\))
  • -
  • Сумма ассоциативна: \((A+B)+C = A+(B+C)\)
  • -
  • Умножение ассоциативно: \((AB)C = A(BC) = ABC\).
  • -
  • Умножение в общем случае не коммутативно.
  • -
-

Пример: матрица поворота в 2d. \[ -\begin{pmatrix} -\cos \alpha & -\sin \alpha \\ -\sin \alpha & \cos \alpha \\ -\end{pmatrix} -\]

-

Пример: матрица проецирования на \(Ox\) в 3d. \[ -\begin{pmatrix} -1 & 0 & 0 \\ -0 & 0 & 0 \\ -0 & 0 & 0 \\ -\end{pmatrix} -\]

-

Пример: матрица «свапни \(x\) и \(y\)». \[ -\begin{pmatrix} -0 & 1 \\ -1 & 0 \\ -\end{pmatrix} -\]

-

Напишем класс, который реализует матричное умножение.

-

```c++ struct matrix { int n, m; int t[]; matrix (int n, int m) { n = n, m = m; t = new int(n*m); memset(t, 0, sizeof t); } int[] operator[] (int k) { return t[k*m]; } }

-

matrix operator* (matrix a, matrix b) { matrix c(a.n, b.m); for (int i = 0; i < a.n; i++) for (int j = 0; j < b.m; j++) for (int k = 0; k < a.m; k++) c[i][j] += a[i][k] * b[i][k]; return c; } ```

-

Динамика

-

Некоторые динамики можно выразить в терминах матричного умножения.

-

Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?

-

\[ -\begin{pmatrix} -f_{n+1} \\ -f_{n+2} \\ -\end{pmatrix} -= -\begin{pmatrix} -0+f_{n+1} \\ -f_{n}+f_{n+1} \\ -\end{pmatrix} -= -\begin{pmatrix} -0 & 1 \\ -1 & 1 \\ -\end{pmatrix} -\begin{pmatrix} -f_{n} \\ -f_{n+1} \\ -\end{pmatrix} -\]

-

Обозначим за \(A\) эту матрицу перехода. Чтобы посчитать \(n\)-е число Фибоначчи, нужно применить \(n\) раз эту матрицу к вектору \((f_0, f_1) = (0, 1)\).

-

Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:

-

\[ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) \]

-

Это будет работать за \(O(n^3 \log n)\). Мы делаем \(O(n^3)\) операций для одного умножения, а всего их нужно сделать \(O(\log n)\). Кстати, наука знает и более быстрые способы перемножить матрицы, но на контестах они не нужны.

-

c++ matrix binpow (matrix a, int p) { matrix b(n, n); for (int i = 0; i < n; i++) b[i][i] = 1; while (p) { if (p&1) b = b*a; a = a*a; p >>= 1; } return b; }

-

Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: \(I\).

-

\[ -\begin{vmatrix} -1 & 0 & 0 \\ -0 & 1 & 0 \\ -0 & 0 & 1 \\ -\end{vmatrix} -\]

-

В плане умножения она действительно ведет себя как единица: \(AI = A = IA\). В коде она используется вместо единицы.

-

В общем случае, линейная рекуррента \(f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}\) имеет такую матрицу перехода:

- -

Матрица смежности

-

У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины \(a\) в вершину \(b\) за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из \(a\) в \(b\), используя ровно \(n\) переходов.

-

Когда нам нужна только информация, можно ли дойти из \(a\) в \(b\) (количество способов не важно), то решение можно ускорить (см. Битсет).

-

Обобщения

-

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

-

Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два.

-

Определитель

-

Определителем кадратной матрицы \(A\) называется такое выражение:

-

\[ \det A = |A| = \ldots \]

-

Где \(\epsilon\) — чётность числа инверсий в перестановке (\(-1\) или \(+1\)). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:

-

\[ -\begin{vmatrix} -a & b \\ -c & d \\ -\end{vmatrix} -= -ad - bc -\]

-

У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.

-

В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем.

-

Базис

-

Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их.

-

Базисы есть не только в линейной алгебре. Например, \(\{1, x, x^2\}\) является базисом всех квадратных трёхчленов. Или \(\{\neg, \land, \lor\}\) является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом.

-

Метод Крамера и easy пересечение прямых

-

Пусть нам надо пересечь две прямые.

-

\[ -\begin{cases} -a_1 x + b_1 y + c_1 = 0 -a_2 x + b_2 y + c_2 = 0 -\end{cases} -\]

-

Это то же самое, что найти такие коэффициенты \(x\) и \(y\), что

-

\[ x \vec{a} + y \vec{b} = -\vec{c} \]

-

Площадь параллелограмма, натянутого на \(\vec{a}\) и \(\vec{b}\), равна векторному произведению, или детерминанту.

-

По сути, нам нужно выразить \(c\) в другом базисе. Давайте спроецируем её на \(a\).

-

Аналогично, напрягите воображение и спроецируйте эту точку в \(n\)-мерном пространстве. Это уже сложно, да?

-

*Собственные векторы

-

Очень часто у матриц есть собственные вектора -- те, которые не меняют направление.

-

$ Av = k v \(, где \)k 0$.

-

$ Av - kv = (A-kI)v = 0 $. Это означает

-

Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике.

-

Системы уравнений и метод Гаусса

-

Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

-

Нас по сути просят решить следующую систему:

-

\[ -\begin{cases} -a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ -a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ -\ldots \\ -a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 -\end{cases} -\]

-

Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

-

Метод Крамера неоптимален — там \(O(n^4)\) операций.

-

В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

-

c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; }

-

Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

-

Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. Битсет).

- - diff --git a/ru/matching.html b/ru/matching.html deleted file mode 100644 index 08c34af..0000000 --- a/ru/matching.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - -

Паросочетания

-

Пусть есть \(n\) мальчиков и \(m\) девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.

-

-

Паросочетанием \(M\) называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из \(M\)).

-

Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном \(M\)), назовём насыщенными этим паросочетанием.

-

Мощностью паросочетания назовём количество рёбер в нём. Наибольшим (максимальным) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а совершенным — где все вершины левой доли им насыщенны.

-

Паросочетания можно искать в любых графах, однако этот алгоритм неприятно кодить, и он работает за \(O(n^3)\), так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как \(L\), а правую долю как \(R\).

-

Цепью длины \(k\) назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно \(k\) рёбер.

-

Чередующейся цепью относительно некоторого паросочетания назовём простой путь длины \(k\) в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.

-

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

-
- -
-

Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: \(1 \to 8 \to 4 \to 6 \to 3 \to 7\).

-

Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести чередование — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.

-

В примере добавятся синие рёбра \((1, 8)\), \((3, 7)\) и \((4, 6)\), а удалятся красные \((3, 6)\) и \((4, 8)\). С ребром \((2, 5)\) ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.

-

Алгоритм Куна в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже.

-

```c++ const int maxn;

-

vector g[maxn]; // будем хранить только рёбра из левой доли в правую int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой) bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом

-

// dfs возвращает, можно ли найти путь из вершины v // в какую-нибудь вершину правой доли // если можно, то ещё и проводит чередование bool dfs (int v) { if (used[v]) return false; used[v] = true; for (int u : g[v]) { // если вершина свободна, то можно сразу с ней соединиться // если она занята, то с нейможно соединиться только тогда, // когда из её текущей пары можно найти какую-нибудь другую вершину if (mt[u] == -1 || dfs(mt[u])) { mt[u] = v; return true; } } return false; }

-

// где-то в main:

-

memset(mt, -1, sizeof(mt)); for (int i = 0; i < n; i++) { memset(used, 0, sizeof(mt)); if (dfs(i)) cnt++; } ```

-

Корректность

-

Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.

-

Теорема (Бержа). Паросочетание без увеличивающих цепей является максимальным.

-

Доказательство проведём от противного: пусть есть два паросочетания вершин \(|A| \leq |B|\), и для \(A\) нет увеличивающих путей, и покажем, как найти этот путь и увеличить \(A\) на единицу.

-

Раскрасим ребра из паросочетания, соответствующего \(A\) в красный цвет, \(B\) — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для \(A\), а значит \(A\) не оптимальное, и мы получили противоречие.

-

Скорость работы

-

Такой алгоритм ровно \(n\) раз ищет увеличивающий путь, каждый раз просматривая не более \(m\) рёбер, а значит работает за \(O(nm)\).

-

Что примечательно, его можно не бояться запускать на ограничениях и побольше (\(n, m \approx 10^4\)), потому что для него есть мощные неасимптотические оптимизации:

-
    -
  • Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).

  • -
  • Можно не заполнять нулями на каждой итерации массив used, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало true. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как false. В каком-то смысле это позволяет эмулировать очищение массива за константу.

  • -
  • Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы \(n \times m\). Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.

  • -
-

Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить алгоритм Диница к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за \(O(n \sqrt m)\).

-

Покрытие путями DAG-а

-

Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф \(G\) (англ. directed acyclic graph). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.

-

Построим соответствующие изначальному графу \(G\) два двудольных графа \(H\) и \(\overline{H}\) следующим образом: * В каждой доле графа \(H\) будет по \(n\) вершин. Обозначим их через \(a_i\) и \(b_i\) соответственно. * Для каждого ребра \((i, j)\) исходного графа \(G\) проведём соответствующее ребро \((a_i, b_j)\) в графе \(H\). * Теперь из графа \(H\) сделаем граф \(\overline{H}\), добавив обратное ребро \((b_i, a_i)\) для каждого \(i\).

-

Если мы рассмотрим любой путь \(v_1, v_2, \ldots, v_k\) в исходном графе \(G\), то в графе \(\overline{H}\) ему будет соответствовать путь \(a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}\). Обратное тоже верно: любой путь, начинающийся в левой доле \(\overline{H}\) и заканчивающийся в правой будет соответствовать какому-то пути в \(G\).

-

Итак, есть взаимно однозначное соответствие между путями в \(G\) и путями \(\overline{H}\), идущими из левой доли в правую. Заметим, что любой такой путь в \(\overline{H}\) — это паросочетание в \(H\) (напомним, это \(\overline{H}\) без обратных рёбер). Получается, любому пути из \(G\) можно поставить в соответствие паросочетание в \(H\), и наоборот. Более того, непересекающимся путям в \(G\) соответствуют непересекающиеся паросочетания в \(H\).

-

Заметим, что если есть \(p\) непересекающихся путей, покрывающих все \(n\) вершин графа, то они вместе содержат \(r = n - p\) рёбер. Отсюда получаем, что чтобы минимизировать число путей \(p\), мы должны максимизировать число рёбер \(r\) в них.

-

Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе \(H\). После нахождения этого паросочетания мы должны преобразовать его в набор путей в \(G\). Это делается тривиальным алгоритмом: возьмем \(a_1\), посмотрим, с какой \(b_k\) она соединена, посмотрим на \(a_k\) и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин.

-

Лемма Холла

-

Лемма Холла (или: теорема о свадьбах) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.

-
- -
-

Лемма Холла. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.

-

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

-

В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.

-

База индукции: одна вершина из \(L\), которая по условию соединена с хотя бы одной вершиной из \(R\).

-

Индукционный переход: пусть после \(k < n\) шагов построено паросочетание \(M\). Докажем, что в \(M\) можно добавить вершину \(v\) из \(L\), не насыщенную паросочетанием.

-

Рассмотрим множество вершин \(H\) — все вершины, достижимые из \(x\), если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)

-

Тогда в \(H\) найдется вершина \(y\) из \(R\), не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины \(H_L\) (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что \(|H_L| \leq |N(H_L)|\) (здесь \(N(X)\) — множество вершин, соединенным паросочетанием с \(X\)).

-

Тогда должен существовать путь из \(x\) в \(y\), и он будет увеличивающим для паросочетания \(M\), потому что из \(R\) в \(L\) мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно.

-

Для ноулайферов: матроиды

-

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

-

Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию хороших подмножеств (паросочетаний) обобщает понятие матроида. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше хорошее множество, если оно после добавления остается хорошим.

-

Применимо к паросочетаниям: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна.

- - diff --git a/ru/matroid.html b/ru/matroid.html deleted file mode 100644 index ac8027c..0000000 --- a/ru/matroid.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - -

Матроиды

-

Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры).

-

Матроидом называется пара \((X, I)\), где \(X\) — множество элементов, называемое носителем матроида, а \(I\) — некоторое множество подмножеств \(X\), называемое семейством независимых множеств. В матроиде должны выполняться следующие свойства:

-
    -
  • Пустое множество является независимым: \(\varnothing \in I\)

  • -
  • Любое подмножество независимого множества тоже независимо: \[A \subset B, B \in I \implies A \in I\]

  • -
  • Если в независимом множестве \(A\) меньше элементов, чем в независимом множестве \(B\), то будет существовать элемент из \(B\), дополняющий \(A\) до независимого множества размера \(|A|+1\): \[A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I\]

  • -
-

Матроид называется взвешенным, если на нем существует аддитивная весовая функция: \(w(A) = \sum w(a_i)\).

-

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

-
X.sort()
-s = []
-for x in X:
-    if good(s + [x]):
-        s += [x]
-

Здесь под good имеется в виду \(s \cup x \in I\).

-

Корректность этого алгоритма для любого матроида доказывает следующая теорема:

-

Теорема Радо-Эдмондса

-

Пусть \(A \in I\) — множество минимального веса среди всех независимых подмножеств \(X\) мощности \(k\). Возьмем \(x: A \cup x \in I,\;x \notin A,\;w(x)\) — минимальна. Тогда \(A \cup x\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

-

*Доказательство: **

-

Рассмотрим \(B\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

-

Из свойств матроида: \(\exists y \in B \setminus A : A \cup y \in I\).

-

Тогда верны два неравенства:

-

\[ -\begin{cases} -w(A \cup y) = w(A) + w(y) \geq w(B) \implies w(A) \geq w(B) - w(y) \\ -w(B \setminus y) = w(B) - w(y) \geq w(A) \implies w(A) \leq w(B) - w(y) -\end{cases} -\]

-

Величина \(w(A)\) с двух сторон ограничивает величину \(w(B) - w(y)\). Значит, они равны. Cледовательно, \(w(A \cup y) = w(A) + w(y) = w(B)\).

-

Получаем, что если объединить множество \(A\) с \(x\) — минимальным из таких, что \(A \cup x \in I\), — то получим множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

-

Иными словами, если у нас есть оптимальное \(k\)-элементарное независимое множество, то мы можем индуктивно построить оптимальное \((k+1)\)-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым.

-

Примеры

-

Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.

-

Минимальный остов

-

Рассмотрим неориентированный граф \(G = (V, E)\). Пусть \(I\) — множество лесов графа (ациклических подмножеств \(E\)). Тогда \(M = (E, I)\) является матроидом:

-
    -
  • Граф без ребер является лесом.
  • -
  • Если удалить из леса ребра, он останется лесом.
  • -
  • Пусть есть два леса \(|A| \leq |B|\). В \(A\) будет \(|V| - |A|\) компонент связности, в \(B\) будет \(|V|-|B|\) компонент связности. Так как в \(B\) компонент связности меньше, то будет существовать какое-то ребро \(x\), связывающее две компоненты связности из \(A\). Его и возьмем: \(A \cup \{x\}\) тоже будет лесом, так как \(x\) только соединило две разные компоненты связности.
  • -
-

Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.

-

Расписания

-

Пусть у нас есть \(n\) заданий, на выполнение каждого требуется \(1\) час. Награда за выполнение \(i\)-го задания не позже \(d_i\)-того часа равна \(w_i\). В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.

-

Назовём правильными те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам (\(d_i\)) и проверить, что \(d_i \geq i\) для всех \(i\).

-

Тогда \(M = \) (множество всех заданий, множество правильных наборов заданий) является матроидом:

-
    -
  • Пустой набор заданий всегда можно сделать.
  • -
  • Если у нас стало меньше заданий, то их сделать мы тоже успеем.
  • -
  • Пусть есть два правильных набора \(|A| \leq |B|\). Тогда в \(B\) будет существовать задание \(x\) с дедлайном позже \(|A|\). Все задания \(A\) можно сделать не позже \(|A|\)-го часа, а в \((|A|+1)\)-й час будем делать \(x\). Значит, \(A \cup x\) — тоже правильный набор.
  • -
-

Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных.

-

Паросочетания

-

Рассмотрим двудольный граф \(G = (L, R, E)\). Пусть \(I\) — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда \(M = (L, I)\) является матроидом:

-
    -
  • Любое паросочетание покрывает пустое множество вершин.
  • -
  • Исходное паросочетание покрывает также и любое подмножество исходных вершин.
  • -
  • Пусть есть два множества вершин \(|A| \leq |B|\). Раскрасим ребра из паросочетания, соответствующего \(A\) в красный цвет, \(B\) — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству \(A\) добавилась какая-то вершина из левой доли, принадлежавшая ранее \(B\).
  • -
-

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

-

Линейно независимые вектора

-

(TODO) (Школьники не обязаны знать линал.)

-

Такие штуки будем называть базисами.

-
    -
  • Ноль есть в любом базисе.
  • -
  • Подмножество базиса — базис.
  • -
  • ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора \(B\) лежат в \(A\). Значит, размерность \(B\) уж точно не больше.
  • -
- - diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html deleted file mode 100644 index 4480d16..0000000 --- a/ru/mincost-maxflow.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - -

Поток минимальной стоимости

-

Рассмотрим ориентированный граф \(G = (V, E)\) с истоком \(s\) и стоком \(t\), в котором у каждого ребра \((u, v)\) задана целая стоимость \(w_{uv}\) и целая положительная пропускная способность \(c_{uv}\). Требуется найти максимальный поток, стоимость которого минимальна:

-

\[ \sum_{(u, v) \in E} f_{uv} \to \max \] \[ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min \]

-

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

-

Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра \((u, v)\) добавим \((v, u)\), для которого \(c_{vu} = 0\) и \(w_{vu} = -w_{uv}\). Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая (\(c_{uv}-f_{uv} > 0\)).

-

Критерий оптимальности

-

Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот).

-

Доказательство:

-

\(\rightarrow\) Рассмотрим произвольный неоптимальный поток \(f\) и оптимальный поток \(f^*\). Рассмотрим разность \(f^*-f\). Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость \(f^*\) меньше стоимости \(f\), что противоречит предположению.

-

\(\leftarrow\) Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости.

-

Отмена циклов

-

Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более \(mUC\) раз где \(U\) — величина потока, \(C\) — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу.

-

Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит \(O(m^2nUC)\) (предполагая, что какой-нибудь максимальный поток мы уже нашли).

-

Дополняющие пути

-

Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.

-

Утверждеие. Алгоритм не создает в остаточной сети циклов отрицательного веса.

-

Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из \(s\) в \(t\) и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро \((v, u)\) находится в цикле отрицательного веса. Тогда есть путь Из \(u\) в \(v\) стоимости меньше, чем \(w_{uv}\). Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра \((u, v)\).

-

Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит \(O(nmU)\) — искать каждый дополняющий путь мы будем не более \(U\) раз.

-

Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер.

-

Потенциалы Джонсона

-

Потенциалом вершины \(v\) будем называть расстояние \(d_v\) от вершины \(s\). Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:

-

\[ w_{uv}' = w_{uv} + d_u - d_v \]

-

Утверждение 1. Веса всех рёбер графа неотрицательные.

-

Доказательство. Пусть вес какого-то ребра \((u, v)\) отрицателен, то есть \(w_{uv}' = w_{uv} + d_u - d_v < 0\). Тогда \(d_u + w_{uv} < d_v\), и нарушилось неравенство треугольника: почему мы тогда не использовали ребро \((u, v)\), когда искали кратчайший путь до \(v\)?

-

Аналогично можно показать, что рёбра на кратчайших путях из \(s\) имеют нулевую стоимость. Заметим, что стоимость обратных рёбер на кратчайших путях тоже будет нулевой: \[ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 \]

-

Утверждение 2. Кратчайшие пути между любыми вершинами остались кратчайшими.

-

Доказательство. Распишем новую стоимость пути из \(a\) в \(z\).

-

\[ -\begin{align} -w_{ab}' + \ldots + w_{yz}' -&= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) -\\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z -\end{align} -\]

-

Получаем, что стоимость всех путей из \(a\) в \(z\) лишь изменилась на константу.

-

Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.

-

Заметьте, что в доказательстве мы не использовали то, что \(d_v\) — кратчайшие расстояния. Это вообще могут быть произвольные числа.

-

Утверждение 3. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими).

-

Доказательство. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2).

-

Итоговый алгоритм

-
    -
  • Модифицируем сеть, добавивив обратные рёбра.
  • -
  • Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.
  • -
  • Пока максимальный поток не найден:
  • -
    • -
    • Посчитать алгоритмом Дейкстры кратчайшие расстояния от \(s\), используя для веса формулу с потенциалами, записать их в \(d\).
    • -
  • -
    • -
    • Протолкнуть максимально возможный поток вдоль кратчайшего пути \(s \leadsto t\), обновить остаточную сеть.
    • -
  • -
-

Асимптотика

-

Алгоритм работает за \(O(U m \log n)\) или \(O(U n^2)\) в случае плотных графов.

-

В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и \(U \leq n\) или \(U \leq m\). Например, в задаче о назначениях (паросочетание минимального веса) \(U = n\) и алгоритм работает за \(O(n^3)\), что совпадает с асимптикой венгерского алгоритма.

-

Реализация

-

Решение задачи о назначениях за \(O(n^3)\). Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).

-
    -
  • cost, cap — параметры сети
  • -
  • pot — потенциалы
  • -
  • par — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)
  • -
  • d — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния
  • -
-

```c++ const int maxn = 305, inf = 1e9;

-

int n; int cost[maxn][maxn], cap[maxn][maxn]; int d[maxn], pot[maxn], par[maxn];

-

bool dijkstra (int s, int t) { used[maxn] = {0};

-
fill(d, d+n, inf);
-d[s] = 0;
-
-while (1) {
-    int v = -1;
-    for (int u = 0; u < n; u++)
-        if (!used[u] && (v == -1 && d[u] < d[v]))
-            v = u;
-    if (v == -1 || d[v] == inf)
-        break;
-    used[v] = 1;
-    for (int u = 0; u < n; u++) {
-        int w = cost[v][u] + pot[v] - pot[u];
-        if (cap[v][u] && d[u] > d[v] + w) {
-            d[u] = d[v] + w;
-            par[u] = v;
-        }
-    }
-}
-
-return d[t] < inf;
-

}

-

int mincost_maxflow (int s, int t) { int ans = 0; while (dijkstra(s, t)) { memcpy(pot, d, sizeof(d)); int delta = inf; for (int v = t; v != s; v = par[v]) delta = min(delta, cap[par[v]][v]); for (int v = t; v != s; v = par[v]) { cap[par[v]][v] -= delta; cap[v][par[v]] += delta; ans += cost[par[v]][v]*delta; } } return ans; } ```

- - diff --git a/ru/mst.html b/ru/mst.html deleted file mode 100644 index acb8a6f..0000000 --- a/ru/mst.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - -

Минимальные остовы

-
-

Авиакомпания содержит \(m\) рейсов между \(n\) городами, \(i\)-ый из них обходится в \(w_i\) рублей, причём из любого города можно добраться до любого другого. В стране наступил кризис, и нужно отказаться от как можно большего числа из них таким образом, что содержание оставшиъся рейсов будет наиболее дешевым.

-
-

Иными словами, нужно найти дерево минимального веса, которое является подграфом данного неориентированного графа. Такие деревья называют остовами (каркас, скелет; ударение на первый слог, но так мало кто произносит). По-английски — minimum spanning tree (дословно, минимальное покрывающее дерево).

-

Почему дерево? Потому в противном случае там был бы цикл из которго можно удалить какое-то ребро и получить ответ пооптимальней. А если это больше, чем одно дерево, то какие-то две вершины остаются несвязаны.

-

Вообще, следующие утверждения про деревья являются эквивалентными: * Граф — дерево. * В графе из \(n\) вершин \(n-1\) рёбер и нет циклов. * Из любой вершины можно дойти в любоую другую единственным образом.

-

Лемма о безопасном ребре

-

Назовем подграф \(T\) графа \(G\) безопасным, если они является подграфом какого-то минимального остова.

-

Назовем ребро безопасным, если при добавлении его в подграф \(T\) получившийся граф \(T'\) тоже является безопасным, то есть подграфом какого-то минимального остова.

-

Все алгоритмы для поиска минимального остова опираются на следующее утверждение:

-
-

Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным.

-
-

Доказательство: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже.

-
-safe

safe

-
-

Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов.

-

Алгоритм Прима

-

Минимальный остов строится постепенно, добавлением в него рёбер по одному.

-
    -
  • Изначально остов — одна произвольная вершина.
  • -
  • Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден.
  • -
-

Очень похоже на алгоритм Дейкстры, только мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё.

-

Совсем наивная реализация за \(O(nm)\) — каждый раз перебираем все рёбра:

-
const int maxn = 1e5, inf = 1e9;
-vector<int> from, to, weight;
-bool used[maxn]
-
-// считать все рёбра в массивы
-
-used[0] = 1;
-for (int i = 0; i < n-1; i++) {
-    int opt_w = inf, opt_from, opt_to;
-    for (int j = 0; j < m; j++)
-        if (opt_w > weight[j] && used[from[j]] && !used[to[j]])
-            opt_w = weight[j], opt_from = from[j], opt_to = to[j]
-    used[opt_to] = 1;
-    cout << opt_from << " " << opt_to << endl;
-}
-

Реализация за \(O(n^2)\):

-
const int maxn = 1e5, inf = 1e9;
-bool used[maxn];
-vector< pair<int, int> > g[maxn];
-int min_edge[maxn] = {inf}, best_edge[maxn];
-min_edge[0] = 0;
-
-// ...
-
-for (int i = 0; i < n; i++) {
-    int v = -1;
-    for (int u = 0; u < n; j++)
-        if (!used[u] && (v == -1 || min_edge[u] < min_edge[v]))
-            v = u;
- 
-    used[v] = 1;
-    if (v != 0)
-        cout << v << " " << best_edge[v] << endl;
- 
-    for (auto e : g[v]) {
-        int u = e.first, w = e.second;
-        if (w < min_edge[u]) {
-            min_edge[u] = w;
-            best_edge[u] = v;
-        }
-    }
-}
-

Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за \(O(m \log n)\):

-
set< pair<int, int> > q;
-int d[maxn];
-
-while (q.size()) {
-    v = q.begin()->second;
-    q.erase(q.begin());
- 
-    for (auto e : g[v]) {
-        int u = e.first, w = e.second;
-        if (w < d[u]) {
-            q.erase({d[u], u});
-            d[u] = w;
-            q.insert({d[u], u});
-        }
-    }
-}
-

Про алгоритм за \(O(n^2)\) забывать не стоит — он работает лучше в случае плотных графов.

-

Алгоритм Крускала

-

Будем добавлять рёбра в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить.

-

Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Однако для этой проверки нам нужна будет целая отдельная структура.

-

Система непересекающихся множеств

-

Эта структура данных предоставляет следующие возможности. Изначально имеется несколько элементов, каждый из которых находится в отдельном (своём собственном) множестве. Структура поддерживает две операции: * объединить два каких-либо множества * запросить, в каком множестве сейчас находится указанный элемент

-

Обе операции будут выполняться в среднем почти за \(O(1)\) (но не совсем — этот сложный вопрос будет разъяснен позже).

-

Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив _p, в котором для каждого элемента мы храним номер его предка в дерева. Для корней деревьев будем считать, что их предок — они сами.

-

Наивная реализация, которую мы потом ускорим:

-
int _p[maxn];
-
-int p (int v) {
-    if (_p[v] == v)
-        return v;
-    else
-        return p(_p[v]);
-}
-
-void unite (int a, int b) {
-    a = p(a), b = p(b);
-    _p[a] = b;
-}
-
-for (int i = 0; i < n; i++)
-    _p[i] = i;
-

Эвристика сжатия пути. Оптимизируем работу функции p. Давайте перед тем, как вернуть ответ, запишем его в _p от текущей вершины, то есть переподвесим его за самую высокую.

-

Насколько лучше это сделает асимптотику? Выясняется, что \(O(n \log n)\).

-

Тут должен быть мем из опросов.

-

Ранговая эвристика. Эта штука напрямую пытается минимизировать высоту дерева. Давайте делать переподвешивание за то, которое менее глубоко. Ну понятно, что тогда любое дерево будет не более логарифма.

-

Весовая эвристика. Давайте каждый раз подвешивать за более крупное. Работать будет быстро, так как .

-

Автор предпочитает именно её, потому что часто эти размеры компонент требуются сами по себе.

-

Оказывается, что сжатия + ранговая или сжатия + весовая работает быстро.

-

Асимптотика объединения обеих эвристик (сжатия путей и одной из ранговых) — O(a(n)), где a(n) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4). Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует.

-
int _p[maxn], s[maxn];
-
-int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); }
-
-void unite (int a, int b) {
-    a = p(a), b = p(b);
-    if (s[a] > s[b]) swap(a, b);
-    s[b] += s[a];
-    _p[a] = b;
-}
-
-for (int i = 0; i < n; i++)
-    _p[i] = i;
-

Полезные свойства и классические задачи

-
    -
  • Если веса всех рёбер различны, то остов будет уникален.
  • -
  • Минимальный остов является также и остовом с минимальным произведением весов рёбер (замените веса всех рёбер на их логарифмы)
  • -
  • Минимальный остов является также и остовом с минимальным весом самого тяжелого ребра.
  • -
  • Если вы решаете задачу, где ребра не добавляются, а удаляются, то можно попробовать решать задачу «с конца» и применить алгоритм Крускала.
  • -
-

Персистентная СНМ*

-

Как вы уже заметили, автор очень любит персистентные структуры данных.

-

СНМ — тоже структура, и её тоже можно сделать персистентной. В СНМ мы изменяем массивы, а массивы можно сделать персистентными через персистентное ДО (только так, проще не получается — многие пытались).

-

Здесь есть нюанс — амортизированные структуры не очень хорошо дружат с персистентностью. Поэтому нам придется отказаться от эвристики сжатия путей, и поэтому асимптотика составит \(O(n \log^2 n)\) времени и памяти — один логарифм от СНМа, другой от персистентного ДО.

-

Динамическая связность*

-

Dynamic Connectivity Problem:

-
-

Даны \(n\) запросов добавления ребра (+), удаления ребра (- и какого-то запроса про граф (?), например, о связности двух вершин.

-
-

О решении этой задачи в online и в offline можете почитать в этом посте.

- - diff --git a/ru/pollard.html b/ru/pollard.html deleted file mode 100644 index 09efcb3..0000000 --- a/ru/pollard.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - -

Факторизация за \(O(\sqrt[4]{n})\)

-

Наука умеет раскладывать целые числа на множители за \(O(n^\frac{1}{4})\). Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера.

-

Парадокс дней рождений

-

Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

-

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

-

Попытаемся оценить \(f\):

-

\[ -\begin{align} - \begin{aligned} - e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ - & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ - e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ - f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ - & = e^{-\frac{n(n-1)}{2d}} & \\ - & \simeq e^{-\frac{n^2}{2d}} & \\ - \end{aligned} -\end{align} -\]

-

Из формулы более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее:

-

Утверждение. В мультимножество нужно добавить \(O(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

-

Если вы не доверяете математике, посмотрите на графики:

-
import matplotlib.pyplot as plt
-%matplotlib inline
-
-import seaborn as sns
-sns.set()
-
def f(n, d):
-    p = 1
-    for x in range(1, n):
-        p *= 1-x/d
-    return p
-
-def g(n, d):
-    return 2.71**(-0.5 * n**2 / d)
-
exact = [f(n, 365) for n in range(40)]
-approx = [g(n, 365) for n in range(40)]
-plt.plot(exact)
-plt.plot(approx)
-plt.show()
-
-png

png

-
-

На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать \(n \approx \sqrt{d}\), построим такую же для больших чисел и перейдём в логарифмический масштаб.

-
approx = [g(n, 10**10) for n in range(10**6)]
-plt.xscale('log')
-plt.plot(approx)
-plt.show()
-
-png

png

-
-

\(\rho\)-алгоритм Полларда

-

Итак, мы хотим факторизовать число \(n\). Предположим, что \(n = p q\) и \(p \approx q\). Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей.

-

Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например \(f(x) = (x+1)^2 \mod n\).

-

Как выглядит граф, в котором из вершины есть единственное ребро \(x \to f(x)\)? Такой граф называется функциональным. Если в нём нарисовать «траекторию» произвольного элемента, — какой-то путь, превращающийся в цикл, — то получится что-то похожее на букву \(\rho\) (ро). Алгоритм из-за этого так и назван.

-

Рассмотрим траекторию какого-нибудь элемента \(x_0\): {\(x_0\), \(f(x_0)\), \(f(f(x_0))\), \(\ldots\)}. Теперь сделаем из неё новую последовательность, взяв каждый элемент по модулю \(p\) — наименьшего из простых делителей \(n\).

-

Утверждение. Ожидаемая длина цикла в этой последовательности \(O(\sqrt[4]{n})\).

-

Доказательство: так как \(p\) — меньший делитель, то \(p \leq \sqrt{n}\). Теперь просто подставим в предыдущее утверждение: в множество нужно добавить \(O(\sqrt{p}) = O(\sqrt[4]{n})\) элементов, чтобы какие-то два совпали, а значит последовательность зациклилась.

-

Если мы найдём цикл в такой последовательности (то есть такие \(i\) и \(j\), что \(f^i(x_0) \equiv f^j(x_0)\)), то мы можем восстановить какой-то делитель \(n\), а именно \(\gcd(|f^i(x_0) - f^j(x_0)|, n)\) — это число меньше \(n\) и делится на \(p\).

-

Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя \(i\) и \(j\) (\(i = 2j\)) и проверять, что \(f^i(x_0) \equiv f^j(x_0) \pmod p\), что эквивалентно проверке \(\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1\).

-
from math import gcd
-from random import randint
-
def find_divisor(n, seed = 1, f = lambda x: (x+1)**2):
-    x = y = seed
-    d = 1
-    while d == 1:
-        x = f(f(x)) % n
-        y = f(y) % n
-        d = gcd(abs(x-y), n)
-    return d
-
n = 3*5*7 # = 105
-print([find_divisor(n, seed=i) for i in range(n)])
-
[3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3]
-
n = (10**9+7)*(10**9+9)
-print([find_divisor(n, seed=randint(1, n)) for _ in range(20)])
-
[1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009]
-

Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за \(O(\sqrt{n})\) (нужно добавить отсечение по времени).

-

Разное

-

Формально, мы показали, что алгоритм работает за \(O(\sqrt[4]{n} \log n)\) за счёт поиска \(\gcd\), но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет.

-

Пародокс дней рождения также можно использовать при подсчёте вероятности коллизии при хэшировании. Хороший модуль должен быть не менее квадрата количества хэшей, а чего-то вроде \(5 n^2\) уже почти всегда достаточно.

- - diff --git a/ru/probability.html b/ru/probability.html deleted file mode 100644 index bd2e97e..0000000 --- a/ru/probability.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - -

Ликбез по теорверу

-
    -
  • Вероятностные распределения
  • -
  • Матожидание и дисперсия
  • -
  • Парадокс дней рождения
  • -
  • Нормальное распределение
  • -
  • Правдоподобие и оценка качества предсказания
  • -
  • Энтропия и оптимальное кодирование
  • -
-

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

-

(Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.)

-
import numpy as np
-
-import matplotlib as plt
-%matplotlib inline
-
-import seaborn as sns
-sns.set()
-

Матожидание

-

Дисперсия

-

Какие два числа лучше всего описывают распределение?

-

Нормальное распределение

-

Центральная предельная теорема названа так пафосно вполне обоснованно.

-

Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию.

-

\[ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} \]

-

Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса.

-

Трудно даже доказать, что это распределение, т. е. что \(\int_{-\inf}^\inf f(x) dx = 1\).

-

Применения

-

Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный.

-

Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как

-

Линейные рекурренты

-

Чтобы решать следующие задачи, нам нужно будет использовать следующий факт:

-

...

-

Доказательство мы не приведем.

-

В частности, таким образом получается формула для чисел Фибоначчи.

-

\[ f_n = \ldots \]

-

Кто бы мог подумать, что все эти иррациональности и степени сократятся и вообще дадут целое число?..

-

Классика

-

Парадокс дней рождения.

-

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

-

Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт?

-

TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. Казино. Мы приходим в казино с 1000$ и следующим образом проводим там время: ставим по 1$, пока не обанкротимся или не выиграем 1100$. Какая вероятность того, что мы уйдём с деньгами?

-

Принцип максимального правдоподобия

-

Энтропия

-

Энтропией называется минимальное число бит, которым теоретически возможно сжать сообщение. Эта величина важна, потому что на практике если её можно посчитать, то сжатие с соответствующей кратностью реально достижимо.

-

Шумный канал.

-

Пусть у вас есть 1тб данных и два китайских терабайтника, на каждый из которых можно записать столько данных, но каждый бит имеет вероятность 10% записаться на противоположный. Требуется сохранить данные с первого раза без потерь. Совсем без потерь.

-

Причём это делается почти впритык.

- - diff --git a/ru/reciprocal.html b/ru/reciprocal.html deleted file mode 100644 index 357bc00..0000000 --- a/ru/reciprocal.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - -

Обратный элемент по модулю

-

Часто в задачах требуется посчитать что-то по простому модулю (чаще всего \(10^9 + 7\)). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче.

-

Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например:

-

c++ c = (a + b) % mod; c = (mod + a - b) % mod; c = a * b % mod;

-

Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: \(\frac{8}{2} = 4\), но \(\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4\).

-

Нужно найти некоторый элемент, который будет себя вести как \(\frac{1}{a} = a^{-1}\), и вместо «деления» домножать на него. Назовем такой элемент обратным.

-

Способ 1: бинарное возведение в степень

-

Если модуль \(p\) простой, то решением будет \(a^{-1} \equiv a^{p-2}\). Это следует из малой теоремы Ферма:

-

Теорема. \(a^p \equiv a \pmod p\) для всех \(a\), не делящихся на \(p\).

-

Доказательство. (для понимания несущественно, можно пропустить)

-

\[ -\begin{align} -a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p -\\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} -\\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} -\\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} -\\ &= a -\end{align} -\]

-

Здесь \(P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}\) это мультиномиальный коеффициент — количество раз, которое элемент \(a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}\) появится при раскрытии скобки \((a_1 + a_2 + \ldots + a_n)^k\).

-

Теперь два раза «поделим» наш результат на \(a\).

-

\[ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} \]

-

Получается, что \(a^{p-2}\) ведет себя как \(a^{-1}\), что нам по сути и нужно. Посчитать \(a^{p-2}\) можно за \(O(\log p)\) бинарным возведением в степень.

-

Приведем код, который позволяет считает \(C_n^k\).

-

```c++ int t[maxn]; // факториалы, можно предподситать простым циклом

-

// бинарное возведение в степень int bp (int a, int n) { int res = 1; while (n) { if (n & 1) res = res * a % mod; a = a * a % mod; n >>= 1; } return res; }

-

// находит обратный элемент как a^(p-2) int inv (int x) { return bp(x, mod-2); }

-

int c (int n, int k) { return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod; } ```

-

Способ 2: диофантово уравнение

-

Диофантовыми уравнениями называют такие штуки:

-

\[ ax + by = 1 \]

-

Требуется решить их в целых числах, то есть \(a\) и \(b\) известны, и нужно найти такие целые (возможно, отрицательные) \(x\) и \(y\), чтобы равенство выполнялось. Решают такие вещи расширенным алгоритмом Евклида. TODO: описать, как он работает.

-

Подставим в качестве \(a\) и \(b\) соответственно \(a\) и \(m\)

-

\[ ax + my = 1 \]

-

Одним из решений уравнения и будет \(a^{-1}\), потому что если взять уравнение по модулю \(m\), то получим

-

\[ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m \]

-

Преимущества этого метода над возведением в степень: * Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. * Алгоритм проще выполнять руками.

-

Сам автор почти всегда использует возведение в степень.

-

Почему \(10^9+7\)?

-
    -
  1. Это выражение довольно легко вбивать (1e9+7).
  2. -
  3. Простое число.
  4. -
  5. Достаточно большое.
  6. -
  7. int не переполняется при сложении.
  8. -
  9. long long не переполняется при умножении.
  10. -
-

Кстати, \(10^9 + 9\) обладает теми же свойствами. Иногда используют и его.

-

Предподсчёт обратных факториалов за линейное время

-

Пусть нам нужно зачем-то посчитать все те же \(C_n^k\), но для больших \(n\) и \(k\), поэтому асимптотика \(O(n \log m)\) нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам.

-

Если у нас уже написан inv, то нам не жалко потратить \(O(\log m)\) операций, посчитав \(m!^{-1}\).

-

После этого мы будем считать \((m-1)!^{-1}\) как \(m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}\).

-

```c++ int f[maxn]; f[0] = 1; for (int i = 1; i < maxn; i++) f[i] = i*f[i-1] % mod;

-

int r[maxn]; r[maxn-1] = inv(f[maxn-1]) for (int i = maxn-1; i >= 1; i--) r[i-1] = r[i]*i % mod; ```

-

TODO: техника с сайта емакса.

- - diff --git a/ru/segtree.html b/ru/segtree.html deleted file mode 100644 index 5a8690e..0000000 --- a/ru/segtree.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - -

Дерево отрезков

-

Замечание. Почти везде мы будем использовать полуинтервалы — обозначаемые как \([l, r)\) — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля.

-

Дерево отрезков — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках.

-

Рассмотрим конкретную задачу:

-
-

Дан массив \(a\) из \(n\) целых чисел, нужно уметь отвечать на запросы двух типов:

-
-
-
    -
  1. Изменить значение в ячейке (т. е. отреагировать на присвоение a[k] = x).
  2. -
  3. Вывести сумму элементов \(a_i\) на отрезке с \(l\) по \(r\).
  4. -
-
-
-

Оба запроса нужно обрабатывать за время \(O(\log n)\).

-
-

Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:

-

Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.

-

Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку \([0, n)\), а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков».

-
-alt text

alt text

-
-

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

-

Разные свойства

-

Высота такого дерева есть величина \(\Theta(\log n)\): на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики.

-

Более того, любой полуинтервал разбивается на \(O(\log n)\) неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков.

-

Дерево также содержит менее \(2n\) вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет \(n\). Таким образом, число вершин в худшем случае оценивается суммой \(n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} + \ldots + 1 < 2n\). Значит, оно линейное по памяти.

-

При \(n\), отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при \(n=3\) левый сын корня есть отрезок \([0, 2)\), имеющий двух потомков, в то время как правый сын корня — отрезок \([2, 3)\), являющийся листом.

-

Ок, как это нам поможет?

-

Опишем теперь, как с помощью такой структуры решить задачу.

-

Запрос обновления. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению \(a[k] = x\).

-

Изменим все вершины, в суммах которых участвует \(k\)-тый элемент. Их будет \(\Theta(\log n)\) — по одной с каждого уровня.

-

Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит \(k\)-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков.

-

Запрос суммы. Мы знаем, что во всех вершинах лежат корректные значения.

-

Сделаем тоже рекурсивную функцию, рассмотрев три случая:

-
    -
  • Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму.
  • -
  • Если отрезки вершины и запроса не пересекаются, то вернуть 0.
  • -
  • Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей.
  • -
-

Чтобы разобраться, почему это работает за \(O(\log n)\), нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в \(O(\log n)\) отрезках, а значит и итоговая асимптотика будет такая же.

-

Ликбез по C++

-

Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества.

-

Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел.

-

Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за \(O(\log n)\).

-

В C++ есть два способа объявлять классы (объект — это экземпляр класса): через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

-

У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например конструктор — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса.

-

c++ struct A { int param1, param2; // тут можно что-то хранить char param3 = 'k'; A (int var) { // эта часть называется конструктором // ... } void do_something () { // это какой-то другой метод // ... } }; // <- не забудьте точку с запятой

-

Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (аллокатор) выделяет место в массиве (оперативной памяти) под этот объект и возвращает позицию (указатель) на место в этом массиве.

-

Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:

-

```c++ A x(179); x.do_something(); x.param1 = 57;

-

A *y = new A(42); // new возвращает адрес, по которому можно найти объект y->do_something(); y.param3 = '!'; ```

-

Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем \(2^{32}\) байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с long long быстрее считались

-

Реализация

-

Общий план реализации любых структур данных: 1. Полностью понять все инварианты — как должна выглядеть структура, какие значения должны принимать поля, etc. 2. Формально описать, что должны делать методы и за какую асимптотику. 3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты.

-

c++ struct segtree { int lb, rb; // левые и правые границы отрезков int sum = 0; // сумма на текущем отрезке segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { // если не лист, создаем детей int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) // если мы лежим полностью в отрезке запроса, вывести сумму return sum; if (max(lb, lq) >= min(rb, rq)) // если мы не пересекаемся с отрезком запроса, вывести ноль return 0; // иначе всё сложно -- запускаемся от детей и пусть они там сами решают return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

-
-

Посчитать число беспорядков в перестановке из \(n\) элементов (беспорядок или инверсия — это пара чисел \(i < j\), для которых \(p_i > p_j\)).

-
-

Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на \(n\) элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число \(x\), будем делать две вещи: * Запросим сумму от \(k\) до \(n\) в ДО. * Добавим единичку в \(k\)-тую позицию в ДО.

-

Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за \(O(n \log n)\) запросов.

-
-

Даны \(n\) точек на плоскости с целыми координатами от 1до \(n\). Требуется ответить на \(m\) запросов количества точек на прямоугольнике.

-
-

Ответим на все запросы в оффлайн, используя метод сканирующей прямой:

-
    -
  • Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике \([x_1, x_2] \times [y_1, y_2]\) равна сумме на прямоугольнике \([0, x_2] \times [y_1, y_2]\) минус сумма на прямоугольнике \([0, x_1] \times [y_1, y_2]\).
  • -
  • Отсортируем теперь все точки и префиксные запросы по их \(x\). При этом, если у точки и запроса одинаковый \(x\), то точка должна идти раньше.
  • -
  • Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в \(y_i\)» и «вывести сумму с \(y_1\) по \(y_2\)».
  • -
-

Отложенные операции

-

Пусть теперь наш запрос обновления — это присвоение значения \(x\) всем элементам некоторого отрезка \([l, r)\), а не только одному.

-

Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем.

-

Например, если пришел запрос «присвой число \(x\) на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено.

-

Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.

-

Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за \(O(n \log n)\).

-

При реализации создадим вспомогательную функцию push, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.

-

c++ struct segtree { int lb, rb; int sum = 0, assign = -1; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void push () { if (assign != -1) { sum = (rb-lb) * assign; if (l) { // если дети есть l->assign = assign; r->assign = assign; } } assign = -1; } void upd (int lq, int rq, int x) { push(); if (lq <= lb && rb <= rq) assign = x; else if (l && max(lb, lq) < min(rb, rq)) { // если есть дети и отрезок запроса хоть как-то пересекается с нашим l->upd(lq, rq, x); r->upd(lq, rq, x); // ...дальше они сами разберутся } } int get_sum (int lq, int rq) { push(); if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

-

По-английски эта техника называется lazy propagation. Очень важно научиться её писать — она часто встречается на олимпиадах.

-

Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни.

-

Динамическое построение

-

А что, если у нас все индексы лежать не от в пределах \(10^5\), а, например, \(10^9\). Все асимптотики нас по прежнему устраивают (\(\log_2 10^6 \approx 20\), \(\log_2 10^9 \approx 30\)), кроме этапа построения.

-

Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.

-

Реализовать это можно так же, как и с push-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так.

-

c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; // а тут ничего нет } void extend () { if (!l && lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { extend(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; extend(); return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

-

Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:

-

c++ vector<int> compress (vector<int> a) { vector<int> b = a; sort(b.begin(), b.end()); b.erase(unique(b.begin(), b.end()), b.end()); for (int &x : a) x = int(lower_bound(b.begin(), b.end(), x) - b.begin()); return a; }

-

Персистентность

-

Структуры данных называют персистентными, если их можно быстро «откатить» до произвольного предыдущего состояния.

-

Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент \(t\) никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.

-

У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать \(O(\log n)\) новых вершин на запрос, что означает общее потребление памяти \(O(m \log n)\).

-

c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb != rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void copy () { if (l) { l = new segtree(l); r = new segtree(r); } } void add (int k, int x) { copy(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { // этот метод ничего не меняет -- он и так хороший if (lq <= lb && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } };

-
-

Даны \(n\) точек на плоскости. Нужно в онлайн ответить на \(q\) запросов суммы на прямоугольнике.

-
-

Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения \(x_i\) и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с \(O(n \log n)\) памяти.

-
-

Дан отрезок из \(n\) чисел от 1 до \(n\). Требуется ответить на \(q\) запросов \(k\)-той порядковой статистики на подотрезке.

-
-

Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент \(k\), добавим единицу к \(k\)-ому элементу.

-

Дальше определим разность деревьев как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо sum использовать везде sum_r - sum_l.

-

Что будет находиться в разности \(r\)-го и \(l\)-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает \(k\) — она и будет ответом.

-
-

Дан массив из \(n\) элементов. Требуется ответить на \(m\) запросов, есть ли на отрезке \([l, r]\) доминирующий элемент — тот, который встречается на нём хотя бы \(\frac{r-l}{2}\) раз.

-
-

У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за \(O(\log n)\), посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна \(\frac{1}{2^{100}}\), и ей на практике можно пренебречь.

-

Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее \(\frac{n}{2}\) в массиве на \(n\) элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем true, иначе false.

-

Другие реализации

-

Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:

-

На массивах. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у Емакса.

-

«ДО снизу». Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с CodeForces.

-

Задачи

- - - diff --git a/ru/sparse-table.html b/ru/sparse-table.html deleted file mode 100644 index 4846b0a..0000000 --- a/ru/sparse-table.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - -

Разреженная таблица

-
    -
  • Нужна для нахождения минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) с малой константой.
  • -
  • Обновления не поддерживает (static RMQ).
  • -
  • Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.
  • -
  • Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).
  • -
  • Требует \(O(n \log n)\) памяти.
  • -
  • Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ.
  • -
-

Определим разреженную таблицу как двумерный массив размера \(n \times\log n\):

-

\[ -t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} -\]

-

Идея такая: считаем минимум на каждом отрезке длины \(2^k\).

-

Такой массив можно посчитать за его размер: \(t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])\). Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования.

-

Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.

-
- -
-

Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

-

```c++ int a[maxn], lg[maxn], mx[maxn][logn];

-

int rmq (int l, int r) { int t = lg[r-l+1]; return min(mx[l][t], mx[r-(1<<t)+1][t]); }

-

// Это считается уже где-то в первых строчках main:

-

for (int l = 1; l < logn; l++) for (int i = (1<= 0; i--) { mx[i][0] = a[i]; for (int l = 0; l < logn-1; l++) mx[i][l+1] = max(mx[i][l], mx[i+(1<<l)][l]); } ```

-

2d Static RMQ

-

Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

-

В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

- - diff --git a/ru/sse.html b/ru/sse.html deleted file mode 100644 index 02f0f97..0000000 --- a/ru/sse.html +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - - - - - - -

Streaming SIMD Extensions

-

This is

-
processor   : 0
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 945.615
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 0
-cpu cores   : 6
-apicid      : 0
-initial apicid  : 0
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-

c++ !cat /proc/cpuinfo | less -20

-
processor   : 0
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3422.395
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 0
-cpu cores   : 6
-apicid      : 0
-initial apicid  : 0
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 1
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3702.863
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 1
-cpu cores   : 6
-apicid      : 2
-initial apicid  : 2
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 2
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3556.265
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 2
-cpu cores   : 6
-apicid      : 4
-initial apicid  : 4
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 3
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3512.268
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 3
-cpu cores   : 6
-apicid      : 6
-initial apicid  : 6
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 4
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3642.787
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 4
-cpu cores   : 6
-apicid      : 8
-initial apicid  : 8
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 5
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3136.167
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 5
-cpu cores   : 6
-apicid      : 10
-initial apicid  : 10
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 6
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3535.684
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 0
-cpu cores   : 6
-apicid      : 1
-initial apicid  : 1
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 7
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 2251.174
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 1
-cpu cores   : 6
-apicid      : 3
-initial apicid  : 3
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 8
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 2202.563
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 2
-cpu cores   : 6
-apicid      : 5
-initial apicid  : 5
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 9
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 2413.537
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 3
-cpu cores   : 6
-apicid      : 7
-initial apicid  : 7
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 10
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 1974.963
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 4
-cpu cores   : 6
-apicid      : 9
-initial apicid  : 9
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 11
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3336.891
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 5
-cpu cores   : 6
-apicid      : 11
-initial apicid  : 11
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
- - diff --git a/ru/stress-test.html b/ru/stress-test.html deleted file mode 100644 index 6066e07..0000000 --- a/ru/stress-test.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - -

Стресс-тестирование

-
    -
  • Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений
  • -
  • Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написанно решение на маленькие подгруппы
  • -
-

Суть такая: * Есть решение smart — быстрое, но в котором есть баг, который хотим найти * Пишем решение stupid — медленное, но точно корректное * Пишем генератор gen — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются

-

Примерный код скрипта:

-
import os, sys
-
-f1, f2, gen, iters = sys.argv
-
-for i in range(int(iters)):
-    print('Test', i+1)
-    os.popen('python3 %s > test.txt' % gen)
-    v1 = os.popen('./%s < test.txt' % f1).read()
-    v2 = os.popen('./%s < test.txt' % f2).read()
-    if v1 != v2:
-        print test
-        print("Correct:")
-        print v1
-        print("Wrong:")
-        print v2
-        break
-

Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py.

-

Скрипт написан под Linux. Для Windows нужно убрать «./» во всех системных вызовах.

-

gen.py автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через ./gen. Пример gen-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10:

-
from random import randint, choice
-
-n = randint(1, 10)
-
-print(n)
-
-for _ in range(n):
-    print(choice('abc'), end='')
- - diff --git a/ru/strings.html b/ru/strings.html deleted file mode 100644 index 3c21c6e..0000000 --- a/ru/strings.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - - -

Базовые строковые алгоритмы

-

Префикс-функция

-

Рассмотрим задачу, которая возникает каждый раз, когда вы делаете ctrl+f:

-
-

Есть большой текст \(t\). Нужно найти все вхождения строки \(s\) в него.

-
-

Наивное решение со сравнением всех подстрок \(t\) длины \(|s|\) со строкой \(s\) работает за \(O(|t| \cdot |s|)\). Если текст большой, то длинные слова в нем искать становится очень долго.

-

Для решения этой задачи за линейное время придумали префикс-функцию.

-

Определение. Префикс-функцией от строки \(s\) называется массив \(p\), где \(p_i\) равно длине самого большого префикса строки \(s_0 s_1 s_2 \ldots s_i\), который также является и суффиксом этой строки (не считая всю строку).

-

Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна \([0, 1, 0, 1, 2, 3, 4, 5]\).

-
def slow_prefix_function(s):
-    n = len(s)
-    p = [0]*n
-    for i in range(n):
-        prefix = s[:i]
-        for l in range(1, i):
-            if prefix[:l] == prefix[-l:]:
-                p[i] = l
-    return p
-
-slow_prefix_function('aataataa')
-
[0, 0, 1, 0, 1, 2, 3, 4]
-

(Этот алгоритм работает за \(O(n^3)\), но это только пока.)

-

Как это поможет решить исходную задачу?

-

Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.

-

Соединим подстроки \(s\) и \(t\) каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки \(s\#t\).

-
s = "let it go"
-t = """let it go, let it go
-can't hold it back anymore
-let it go, let it go
-turn away and slam the door!"""
-
-print((s + '#' + t).replace('\n', ' '))
-print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))
-
let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!
-00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000
-

Видно, что все места, где значения равны 9 (длине S) — это концы вхождений \(s\) в текст \(t\).

-

Такой алгоритм (посчитать префикс-функцию от \(s\#t\) и посмотреть, в каких позициях она равна \(|s|\)) называется алгоритмом Кнута-Морриса-Пратта.

-

Как её быстро считать

-

Рассмотрим ещё несколько примеров префикс-функций:

-
for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:
-    print(slow_prefix_function(s))
-
[0, 0, 1, 2, 3]
-[0, 0, 0, 0, 0, 0]
-[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]
-[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
-

Можно заметить несколько особенностей: * \(p_0 = 0\) для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; * \(p_{i+1}\) максимум на единицу превосходит \(p_i\): если есть префикс, равный суффиксу строки \(s_0 s_1 \ldots s_{i+1}\) длины \(p_{i+1}\), то, отбросив последний символ, можно получить и правильный суффикс для строки \(s_0 s_1 \ldots s_i\), длина которого будет ровно на единицу меньше.

-

Хочется придумать алгоритм, как считать префикс-функцию за \(O(N)\). Логично это делать с помощью динамики: найти формулу для \(p_i\) через предыдущие значения.

-

Заметим, что \(p_{i+1} = p_i + 1\) в том и только том случае, когда \(s_{p_i} =s_{i+1}\). Например, в строке \(\underbrace{aabaa}t\overbrace{aabaa}\) выделен максимальный префикс, равный суффиксу: \(p_{10} = 5\). Если следующий символ равен будет равен \(t\), то \(p_{11} = p_{10} + 1 = 6\).

-

Но что происходит, когда \(s_{p_i}\neq s_{i+1}\)? Пусть следующий символ в этой же строке равен \(b\). * \(\implies\) Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. * \(\implies\) Значит, помимо того, что он является суффиксом «aabaab», префикс является префиксом и подстроки «aabaa». * \(\implies\) Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть \(p_4 = 2\). * \(\implies\) Если \(s_2 = s_{11}\) (т. е. новый символ совпадает с идущим после префикса-кандидата), то \(p_{11} = p_2 + 1 = 2 + 1 = 3\).

-

В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, \(p_{i+1} \neq p_{p_i+1} \)? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — \(p_{p_{p_i}}\). Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым.

-
def fast_prefix_function(s):
-    n = len(s)
-    p = [0]*n
-    for i in range(1, n):
-        cur = p[i - 1]
-        # перебираем префикс-функцию, пока не найдем равный символ
-        while s[i] != s[cur] and cur > 0:
-            cur = p[cur - 1]
-         # если нашли, то значение на единицу больше
-        if s[i] == s[cur]:
-            p[i] = cur + 1
-    return p
-
-fast_prefix_function('abacabadabacabax')
-
[0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]
-

Асимптотика

-

Почему это работает за \(O(N)\)? В худшем случае этот while может работать \(O(N)\) раз за одну итерацию. Но оказывается, что в среднем каждый while работает за \(O(1)\) — это называется амортизированной асимптотикой.

-

Доказательство: * Как мы ранее заметили, префикс функция возрастает максимум на единицу. * \(\implies\) Вырасти она может максимум \(n-1\) раз. * Если мы зашли в while, то каждая его итерация понижает значение префикс-функции хотя бы на один. * \(\implies\) Количество понижений не больше количества повышений, то есть \(O(n)\). * \(\implies\) Суммарно итераций цикла while\(O(n)\).

-

Z-функция

-

Альтернатива префикс-функции — z-функция (примечание: не «зи», а «зет»). Она немного проще для понимания.

-

Z-функция от строки \(s\) — это такой массив \(z\), что \(z_i\) равно длине максимальной подстроки, начинающейся с \(i\)-й позиции, которая равна префиксу \(s\).

-

\[\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)\]

-
def slow_z_function(s):
-    n = len(s)
-    z = [0]*n
-    for i in range(1, n):
-        suffix = s[i:]
-        for l in range(1, i):
-            if s[:l] == suffix[:l]:
-                z[i] = l
-    return z
-
-for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:
-    print(slow_z_function(s))
-
[0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]
-[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]
-[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]
-

Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c \(s\), а не заканчиваться. Осталось научиться её искать за \(O(n)\).

-

Как её быстро считать

-

Заметим, что: * \(z_0 = 0\) — из-за договоренности (потому что информации не несет); * \(z_i = 0 \iff s_i \neq s_0\); * \(z_i > 0 \iff s_i = s_0\).

-

Будем идти слева направо и хранить z-блок — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как \(l\) и \(r\).

-

Пусть мы сейчас хотим найти \(z_i\), а все предыдущие уже нашли. Если новый, \(i\)-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. * Если правее, то мы просто наивно перебором найдем \(z_i\) (максимальный отрезок, начинающийся с \(s_i\) и равный префиксу), и объявим его новым z-блоком. * Если \(i\)-й элемент лежит внутри z-блока, то мы можем посмотреть на значение \(z_{i-l}\) и использовать его, чтобы инициализировать \(z_i\) чем-то, возможно, отличным от нуля. Если \(z_{i-l}\) «не хватает» до границы \(z\)-блока, то \(z_i = z_{i-l}\). Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку.

-
def fast_z_function(s):
-    n = len(s)
-    z = [0]*n
-    l = 0
-    r = 0
-    for i in range(1, n):
-        if i <= r:
-            z[i] = min(r - i + 1, z[i-l])
-        while i + z[i] < n and s[z[i]] == s[i+z[i]]:
-            z[i] += 1
-        if i + z[i] - 1 > r:
-            l = i
-            r = i +  z[i] - 1
-    return z
-

Асимптотика. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это \(O(n)\).

-

Зачем тогда люди используют префикс-функцию

-
-hz

hz

-
-

В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.

-

Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение.

- - diff --git a/ru/suffix-array.html b/ru/suffix-array.html deleted file mode 100644 index c4f2f77..0000000 --- a/ru/suffix-array.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - -

Суффиксный массив

-

Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.

-

Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.

-

-* -
Паблик с тупыми шутками про проганье» -
-
    -
  • -
-

Мотивация

-

Суффиксным массивом строки \(s\) называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки.

-

-

Как это использовать. Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно.

-

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

-

Работать такой алгоритм будет за \(O(|t| \log |s|)\), и позже это можно будет оптимизировать до \(O(|t| + \log |s|)\), что является одним из самых оптимальных алгоритмов поиска.

-

Теперь научимся его строить.

-

Построение за \(O(n \log n)\)

-

Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара.

-

Мы могли бы просто взять перестановку от \(0\) до \(n\), написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в std::sort, что будет работать за \(O(n^2 \log n)\), потому что внутреннее сравнение работает за \(O(n)\). Однако, если сравнивать суффиксы хэшами, то уже тут можно получить \(O(n \log^2 n)\). Но это не самый быстрый и удобный алгоритм.

-

Наш алгоритм будет состоять из \(\lceil \log n \rceil\) этапов. На \(k\)-том этапе мы будем рассматривать циклические подстроки длины \(2^k\). На последнем этапе мы отсортируем строки длины \(\geq n\) (это легально — они ведь циклические), и мы получим нужный суффиксный массив.

-

Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки \(p\) индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции \(i\) с длиной 2^k, номер \(c_i\) класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие \(c_i\)). Количество классов эквивалентности будем хранить в переменной cls (изначально она равна количеству различных символов).

-

Пример: \(s = aaba\). Этапов будет 3: для подстрок длины 1, 2 и 4.

-

\[ - p_0 = (0, 1, 3, 2) \;\;\; c_0 = (0, 0, 1, 0) -\\ p_1 = (0, 3, 1, 2) \;\;\; c_1 = (0, 1, 2, 0) -\\ p_2 = (3, 0, 1, 2) \;\;\; c_2 = (1, 2, 3, 0) -\]

-

Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины \(2^0 = 1\), то есть просто символы. Это мы сделаем сортировкой подсчётом.

-

Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней std::sort со своим компаратором.

-

Как быстро сравнить две подстроки? Мы можем использовать \(c_i\) — каждой строке длины \(2^k\) сопоставить биграмму (строку из двух символов), а именно строка \(s[i..i+2^k-1]\) с точки зрения сортировки будет эквивалентна паре \((c_i, c_{i+2^{k-1}})\). Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за \(O(1)\). Однако, это всё ещё будет работать за \(O(n \log^2 n)\), потому что каждый этап будет работать за \(O(n \log n\)).

-

Оптимизация до \(O(n \log n)\). Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся цифровой сортировкой — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив \(p\) с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в \(p\) отнять \(2^{k-1}\). Таким образом, можно проводить этап за \(O(n)\).

-

```c++ // строка -- это последовательность чисел от 1 до размера алфавита vector suffix_array (vector &s) { s.push_back(0); // добавляем нулевой символ в конец строки int n = (int) s.size(), cnt = 0, // вспомогательная переменная: счётчик для сортировки cls = 0; // количество классов эквивалентности vector c(n), p(n);

-
map< int, vector<int> > t;
-for (int i = 0; i < n; i++)
-    t[s[i]].push_back(i);
-
-// «нулевой» этап
-for (auto &x : t) {
-    for (int u : x.second)
-        c[u] = cls, p[cnt++] = u;
-    cls++;
-}
-
-// пока все суффиксы не стали уникальными
-for (int l = 1; cls < n; l++) {
-    vector< vector<int> > a(cls);  // массив для сортировки подсчётом
-    vector<int> _c(n);  // новые классы эквивалентности
-    int d = (1<<l)/2;
-    int _cls = cnt = 0;  // новое количество классов
-    
-    for (int i = 0; i < n; i++) {
-        int k = (p[i]-d+n)%n;
-        a[c[k]].push_back(k);
-    }
-    
-    for (int i = 0; i < cls; i++) {
-        for (size_t j = 0; j < a[i].size(); j++) {
-            // если суффикс начинает новый класс эквивалентности
-            if (j == 0 || c[(a[i][j]+d)%n] != c[(a[i][j-1]+d)%n])
-                _cls++;
-            _c[a[i][j]] = _cls-1;
-            p[cnt++] = a[i][j];
-        }
-    }
-    
-    c = _c;
-    cls = _cls;
-}
-
-return vector<int>(p.begin()+1, p.end());
-

} ```

-

TODO: переписать это

-

Наибольшие общие префиксы

-

Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы делали с хэшами.

-

TODO

- - diff --git a/ru/treap.html b/ru/treap.html deleted file mode 100644 index 2abea3c..0000000 --- a/ru/treap.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - - - - - -

Декартово дерево

-

Рене Декарт (фр. René Descartes) — великий французский математик и философ XVII века.

-

Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.

-

Декартово дерево же определяется и строится так:

-
    -
  • Нанесём на плоскость набор из \(n\) точек. Их \(x\) зачем-то назовем ключем, а \(y\) приоритетом.
  • -
  • Выберем самую верхнюю точку (с наибольшим \(y\), а если таких несколько — любую) и назовём её корнем.
  • -
  • От всех вершин, лежащих слева (с меньшим \(x\)) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.
  • -
  • Аналогично, запустимся от правой части и добавим корню правого сына.
  • -
-

Заметим, что если все \(y\) и \(x\) различны, то дерево строится однозначно.

-

Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:

-
-treap

treap

-
-

Таким образом, декартово дерево — это одновременно бинарное дерево по \(x\) и куча по \(y\). Поэтому ему придумали много альтернативных названий:

-
    -
  • Дерамида (дерево + пирамида)
  • -
  • ПиВо (пирамида + дерево)
  • -
  • КуРево (куча + дерево)
  • -
  • Treap (tree + heap)
  • -
-

## Бинарные деревья

-

С небольшими модификациями, декартово дерево умеет всё то же, что и любое бинарное дерево поиска, например:

-
    -
  • добавить число \(x\) в множество
  • -
  • определить, есть ли в множестве число \(x\)
  • -
  • найти первое число, не меньшее \(x\) (lower_bound)
  • -
  • найти количество чисел в промежутке \([l, r]\)
  • -
-

При этом все операции — за \(O(\log n)\).

-

На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до \(O(\log n)\).

-

Приоритеты и асимптотика

-

В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты (\(y\)) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска.

-

Теорема. Ожидание глубины вершины в декартовом дереве равно \(O(n \log n)\).

-

Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.

-

Доказательство. Введем функцию \(a(x, y)\) равную единице, если \(x\) является предком \(y\), и нулем в противном случае. Такие функции называются индикаторами*.

-

Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна

-

\[d_i = \sum_{j=1}^n a(j, i)\]

-

Её матожидание равно

-

\[E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)\]

-

где \(p(x, y)\) это веряотность, что \(a(x, y) = 1\). Здесь мы воспользовались важным свойством линейности: матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.

-

Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.

-

Лемма. Вершина \(x\) является предком \(y\), если у неё приоритет больше, чем у всех вершин из отрезка \((x, y]\) (без ограничения общности, будем считать, что \(x < y\)).

-

Необходимость. Если это не так, то где-то между \(x\) и \(y\) есть вершина с большим приоритетом, чем \(x\). Она не может быть потомком \(x\), а значит \(x\) и \(y\) будут разделены.

-

Достаточность. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком \(x\). Таким образом, всё, что справа от \(y\), ни на что влиять не будет.

-

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

-

\[p(x, y) = \frac{1}{y-x+1}\]

-

Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:

-

\[E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)\]

-

Перед последним переходом мы получили сумму гармонического ряда.

-

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

-

Упражнение*. Выведите из этого доказательства асимптотику quicksort.

-

Реализация

-

Декартово дерево удобно писать на указателях и структурах. Поэтому мы рассказали дерево отрезков на указателях, а не стандартную рекурсию на 5 параметрах. Не знаете, что это такое — посмотрите в предыдущем конспекте.

-

Создадим структуру Node, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву.

-

c++ struct Node { int key, prior; Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } };

-

Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся.

-

Merge

-

Принимает два дерева (два корня, \(L\) и \(R\)), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.

-

Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень \(L\) или правый \(R\) — просто возьмем тот, у кого приоритет больше.

-

Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном \(L\). С правым сыном сложнее: возможно, его нужно смерджить с \(R\). Поэтому рекурсивно сделаем merge(l->r, r) и запишем результат в качестве правого сына.

-

c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; if (l->prior > r->prior) { l->r = merge(l->r, r); return l; } else { r->l = merge(l, r->l); return r; } }

-

Split

-

Принимает дерево и ключ \(x\), по которому его нужно разделить на два: \(L\) должно иметь все ключи не больше \(x\), а \(R\) должно иметь все ключи больше \(x\).

-

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

-

```c++ typedef pair Pair;

-

Pair split (Node *p, int x) { if (!p) return {0, 0}; if (p->key <= x) { Pair q = split(p->r, x); p->r = q.first; return {p, q.second}; } else { Pair q = split(p->l, x); p->l = q.second; return {q.first, p}; } } ```

-

Пример: вставка

-

merge и split сами по себе не очень полезные, но помогут написать все остальное.

-

Вот так, например, будет выглядеть код, добавляющий \(x\) в сет.

-

```c++ Node *root = 0;

-

void insert (int x) { Pair q = split(root, x); Node *t = new Node(x); root = merge(q.first, merge(t, q.second)); } ```

-

Пример: модификация для суммы на отрезке

-

Иногда нам нужно написать какие-то модификации для более продвинутых операций.

-

Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке».

-

c++ struct Node { int val, sum; // ... };

-

При merge и split надо будет поддерживать эту сумму актуальной.

-

Вместо того, чтобы модифицировать и merge, и split под наши хотелки, напишем вспомогательные функцию upd, которую будем вызывать при обновлении детей вершины.

-

```c++ void sum (Node* v) { return v ? v->sum : 0; } // обращаться по пустому указателю нельзя -- выдаст ошибку

-

void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; } ```

-

В merge и split теперь можно просто вызывать upd перед тем, как вернуть вершину, и тогда ничего не сломается:

-

c++ Node* merge (Node *l, Node *r) { // ... if (...) { l->r = merge(l->r, r); upd(l); return l; } else { // ... } }

-

```c++ typedef pair Pair;

-

Pair split (Node *p, int x) { // ... if (...) { // ... upd(p); return {p, q.second}; } else { // ... } } ```

-

Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:

-

c++ int sum (int l, int r) { Pair rq = split(root, r); Pair lq = split(rq.first, l); int res = sum(lr.second); root = merge(lq.first, merge(lq.second, rq.second)); return res; }

-

Неявный ключ

-

Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.

-

Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.

-

Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:

-

c++ struct Node { int key, prior, size = 1; // ^ размер поддерева Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } };

-

Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины.

-

```c++ int size (Node *v) { return v ? v->size : 0; }

-

void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); } ```

-

merge не меняется, а вот в split нужно использовать позицию корня вместо его ключа.

-

Про split теперь удобнее думать как "вырежи первые k элементов".

-

```c++ typedef pair Pair;

-

Pair split (Node *p, int k) { if (!p) return {0, 0}; if (size(p->l) + 1 <= k) { Pair q = split(p->r, k - size(p->l) - 1); // ^ правый сын не знает количество вершин слева от него p->r = q.first; upd(p); return {p, q.second}; } else { Pair q = split(p->l, k); p->l = q.second; upd(p); return {q.first, p}; } } ```

-

Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно.

-

Пример: ctrl+x, ctrl+v

-

c++ Node* ctrlx (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l); root = merge(q2.first, q1.second); return q2.second; }

-

c++ void ctrlv (Node *v, int k) { Pair q = split(root, k); root = merge(q.first, merge(v, q.second)); }

-

Пример: переворот

-

Нужно за \(O(\log n)\) обрабатывать запросы переворота произвольных подстрок: значение \(a_l\) поменять с \(a_r\), \(a_{l+1}\) поменять с \(a_{r-1}\) и т. д.

-

Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:

-

c++ struct Node { bool rev; // ... };

-

Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:

-

c++ void push (node *v) { if (v->rev) { swap(v->l, v->r); if (v->l) v->rev ^= 1; if (v->r) v->rev ^= 1; } v->rev = 0; }

-

Аналогично, эту функцию будем вызывать в начале merge и split.

-

Саму функцию reverse реализуем так: вырезать нужный отрезок, поменять флаг.

-

c++ void reverse (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second->rev ^= 1; root = merge(q2.first, merge(q2.second, q1.second)); }

-

Функциональное программирование*

-

Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с \(l\) по \(r\), что-то с ним делаем и склеиваем обратно.

-

Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке.

-

```c++ auto apply (int l, int r, auto f) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second = f(q2.second); root = merge(q2.first, merge(q2.second, q1.second)); }

-

void reverse (Node *v) { if (v) v->rev ^= 1; } ```

-

Применять её нужно так:

-

c++ apply(l, r, reverse);

-

Это работает в плюсах, начиная с g++14.

-

Для простых операций можно даже написать лямбду:

-

c++ apply(l, r, [](Node *v){ if (v) v->rev ^= 1; });

-

Персистентность*

-

Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.

-
-

Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.

-
-

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

-
-

Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции и сравнение произвольных подстрок.

-
-

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

-

Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ.

-

Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию copy:

-

c++ Node* copy (Node *v) { return new Node(*v); }

-

Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный split начнётся так:

-

c++ Pair split (Node *p, int x) { p = copy(p); // ... }

-

В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.

-

У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен \(L\), а размер правого \(R\), то будем подвешивать за левое с вероятностью \(\frac{L}{L+R}\), иначе за правое.

-

Теорема. Такое переподвешивание эквивалентно приоритетам.

-

Доказательство. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции:

-
    -
  • Лист имеет вероятность 1 быть корнем себя (база индукции)
  • -
  • Переход индукции — операция merge. Любая вершина левого дерева была корнем с вероятностью \(\frac{1}{L}\) (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью \(\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}\). С вершинами правого дерева аналогично.
  • -
-

Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД.

-

c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; l = copy(l), r = copy(r); if (rand() % (size(l) + size(r)) < size(l)) { // ... } else { // ... } }

-

Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и \(x\), и \(y\)?

- - diff --git a/ru/trie.html b/ru/trie.html deleted file mode 100644 index ce2e0c4..0000000 --- a/ru/trie.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - -

Бор

-

Бор — это структура данных для компактного хранения строк.

-

Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин.

-
-trie

trie

-
-

Бор можно удобно использовать для разных задач: * Хранение строк — занимает гораздо меньше места, чем массив или сет строк. * Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.

-
    -
  • Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи.
  • -
-

Реализация

-

Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: * терминальная ли вершина, * ссылки на детей, * возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет.

-

```c++ const int k = 26;

-

struct Vertex { Vertex* to[k] = {0}; bool terminal = 0; };

-

Vertex *root = new Vertex(); ```

-

Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной.

-

c++ void add_string (string &s) { v = root; for (char c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(); v = v->to[c]; } v->terminal = true; }

-

Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.

-

Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности.

-

Как хранить ссылки

-

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

-

В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (map) или хэш-таблице (unordered_map). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У map-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например.

-

Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями.

-

Суффиксные ссылки

-
-

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

-
-

Эту и много других задач помогают решать суффиксные ссылки. Суффиксная ссылка для вершины \(v\) — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине \(v\), и присутствующий в боре. Будем считать, что мы их умеем быстро находить.

-

Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

-

Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за \(O(nk)\), где \(n\) и \(k\) это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в отдельную статью.

- - From c75678e7601a3abbc338e23a83c9887e8e8330ac Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 6 Jun 2019 18:01:41 +0000 Subject: [PATCH 011/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- en/index.html | 15 +++++++++++++++ ru/aho-corasick.html | 15 +++++++++++++++ ru/annealing.html | 15 +++++++++++++++ ru/bayans.html | 15 +++++++++++++++ ru/bitset.html | 15 +++++++++++++++ ru/centroid.html | 15 +++++++++++++++ ru/convex-hulls.html | 15 +++++++++++++++ ru/cross-entropy.html | 15 +++++++++++++++ ru/cuda.html | 17 +++++++++++++++++ ru/dp-intro.html | 15 +++++++++++++++ ru/dp-optimizations.html | 15 +++++++++++++++ ru/dp_intro.html | 15 +++++++++++++++ ru/dp_optimizations.html | 15 +++++++++++++++ ru/fenwick.html | 16 ++++++++++++++++ ru/games.html | 15 +++++++++++++++ ru/geometry.html | 15 +++++++++++++++ ru/hashing.html | 15 +++++++++++++++ ru/hld.html | 17 +++++++++++++++++ ru/index.html | 15 +++++++++++++++ ru/ioi.html | 15 +++++++++++++++ ru/karatsuba.html | 15 +++++++++++++++ ru/lca.html | 15 +++++++++++++++ ru/linalg.html | 15 +++++++++++++++ ru/matching.html | 15 +++++++++++++++ ru/matroid.html | 15 +++++++++++++++ ru/mincost-maxflow.html | 15 +++++++++++++++ ru/mst.html | 15 +++++++++++++++ ru/pollard.html | 15 +++++++++++++++ ru/probability.html | 15 +++++++++++++++ ru/reciprocal.html | 15 +++++++++++++++ ru/segtree.html | 15 +++++++++++++++ ru/sparse-table.html | 15 +++++++++++++++ ru/sse.html | 15 +++++++++++++++ ru/stress-test.html | 15 +++++++++++++++ ru/strings.html | 15 +++++++++++++++ ru/suffix-array.html | 17 +++++++++++++++++ ru/treap.html | 15 +++++++++++++++ ru/trie.html | 15 +++++++++++++++ 38 files changed, 577 insertions(+) create mode 100644 en/index.html create mode 100644 ru/aho-corasick.html create mode 100644 ru/annealing.html create mode 100644 ru/bayans.html create mode 100644 ru/bitset.html create mode 100644 ru/centroid.html create mode 100644 ru/convex-hulls.html create mode 100644 ru/cross-entropy.html create mode 100644 ru/cuda.html create mode 100644 ru/dp-intro.html create mode 100644 ru/dp-optimizations.html create mode 100644 ru/dp_intro.html create mode 100644 ru/dp_optimizations.html create mode 100644 ru/fenwick.html create mode 100644 ru/games.html create mode 100644 ru/geometry.html create mode 100644 ru/hashing.html create mode 100644 ru/hld.html create mode 100644 ru/index.html create mode 100644 ru/ioi.html create mode 100644 ru/karatsuba.html create mode 100644 ru/lca.html create mode 100644 ru/linalg.html create mode 100644 ru/matching.html create mode 100644 ru/matroid.html create mode 100644 ru/mincost-maxflow.html create mode 100644 ru/mst.html create mode 100644 ru/pollard.html create mode 100644 ru/probability.html create mode 100644 ru/reciprocal.html create mode 100644 ru/segtree.html create mode 100644 ru/sparse-table.html create mode 100644 ru/sse.html create mode 100644 ru/stress-test.html create mode 100644 ru/strings.html create mode 100644 ru/suffix-array.html create mode 100644 ru/treap.html create mode 100644 ru/trie.html diff --git a/en/index.html b/en/index.html new file mode 100644 index 0000000..ee802f9 --- /dev/null +++ b/en/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +some english index + + + diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html new file mode 100644 index 0000000..5fb77e1 --- /dev/null +++ b/ru/aho-corasick.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Алгоритм Ахо-Корасик Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время. Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик. Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы. > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). ## Зачем это нужно > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс. ## Алгоритм Ахо-Корасик* Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой. **Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход. **Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда. ```c++ const int k = 26; struct Vertex { Vertex *to[k] = {0}, *go[k] = {0}; Vertex *link = 0, *p; int pch; Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; } }; Vertex *root = new Vertex(-1, 0); ``` ```c++ void add_string (string s) { Vertex *v = root; for (char _c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(c, v); v = v->to[c]; } } ``` Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться. ```c++ Vertex* go (Vertex *v, int c); Vertex* link (Vertex *v) { if (!v->link) { if (v == root || v->p == root) v->link = root; else v->link = go(link(v->p), v->pch); } return v->link; } Vertex* go (Vertex *v, int c) { if (!v->go[c]) { if (v->to[c]) v->go[c] = v->to[c]; else if (v == root) v->go[c] = root; else v->go[c] = go(link(v), c); } return v->go[c]; } ``` + + + diff --git a/ru/annealing.html b/ru/annealing.html new file mode 100644 index 0000000..33b3b16 --- /dev/null +++ b/ru/annealing.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Метод отжига * Эвристический метод глобальной оптимизации * Работает с дискретными функциями в предположении, что у близких «точек» близкое значение, и есть что-то типа градиента * Очень простой в написании * Никаких гарантий нет, но хорошо работает на практике * Метод настолько мощный, что иногда им можно сдать задачи, у которых автором предполагалось адекватное решение (пример: [Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам](http://codeforces.com/contest/745/submission/23067030)) ![](https://camo.githubusercontent.com/574bf26bf301827efaa18748440fa77fab8c5a4f/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f642f64352f48696c6c5f436c696d62696e675f776974685f53696d756c617465645f416e6e65616c696e672e676966) ## Постановка задачи Имеется некоторая функция $f(x)$, которую мы хотим минимизировать. Наука решать эту задачу в общем случае не умеет, и вряд ли когда-либо научится. TODO: описание алгоритма, физическая аналогия, конструирование гладких функций При решении конкретной задачи нужно менять только функцию `fitness` и, возможно, параметры алгоритма. ## Ферзи Рассмотрим такую задачу: дана шахматная доска $n \times n$ и $n$ ферзей. Нужно расставить их так, чтобы они не били друг друга. Будем кодировать состояние перестановкой чисел от $1$ до $n$. Такое представление кодирует не все состояния, но оно точно не учитывает те, где ферзи бьют друг друга по вертикали или горизонтали. Выберем такую функцию: fitness(p) = число пар ферзей, которые бьют друг друга. Важная деталь: скорость одной итерации прямо пропорционально скорости работы `fitness`, поэтому очень важно реализовать её эффективно. Конкретно здесь её можно реализовать за $O(n^2)$ (используется в коде для наглядности), $O(n)$ и даже $O(1)$. Мы будем использовать `python`, потому что в нём удобнее строить графики. Эквивалентный код на C++ приведена сразу после. ```python import random # Эти библиотеки будут нужны, чтобы рисовать графики: import matplotlib.pyplot as plt %matplotlib inline import seaborn as sns sns.set() ``` ```python def fitness(p): s = 0 for i in range(len(p)): d = 1 for j in range(i): if abs(i-j) == abs(p[i]-p[j]): # ферзи стоят на одной диагонали d = 0 s += d return s ``` ```python n = 100 # кол-во ферзей k = 1000 # кол-во итераций p = list(range(n)) # исходная перестановка scores = [] t = 1 for i in range(k): # уменьшаем температуру t *= 0.99 # этот параметр лучше перебрать руками # копируем перестановку и делаем небольшое изменение q = p[:] a, b = random.sample(range(n), 2) q[a], q[b] = q[b], q[a] # считаем разницу fitness и решаем, нужно ли переходить в новое состояние d = fitness(q) - fitness(p) if d > 0 or random.random() < math.exp(d/t): p = q[:] scores += [fitness(q)] print('Best score:', max(scores)) plt.plot(scores) plt.show() ``` Best score: 95 ![png](annealing_files/annealing_11_1.png) Примерно эквивалентный код на C++: ```python const int n = 100, k = 1000; int f(vector p) { int s = 0; for (int i = 0; i < n; i++) { int d = 1; for (int j = 0; j < i; j++) if abs(i-j) == abs(p[i]-p[j]): d = 0; s += d; } return s; } double rnd() { return double(rand()) / RAND_MAX; } int main() { for (int i = 0; i < n; i++) v[i] = i; int ans = 1; double t = 1; for (int i = 0; i < k; i++) { t *= 0.99; vector u = v; swap(u[rand()%n], u[rand()%n]); int val = f(u); if (val > ans || rnd() < exp((val-ans)/t)) v = u, ans = val; if (ans == n) break; } for (int x : v) cout << x+1 << " "; return 0; } ``` + + + diff --git a/ru/bayans.html b/ru/bayans.html new file mode 100644 index 0000000..bad857f --- /dev/null +++ b/ru/bayans.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Красивые идейные задачи Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда. Задачи идут в порядке вспоминания, то есть в весьма рандомном. ## Попугаи Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы. ## Минимум и максимум Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый. ## Перестановка Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов. ## Выпуклая оболочка Требуется отвечать на 2 типа запросов: 1. Добавить точку в выпуклую оболочку. 2. Проверить, лежит ли точка внутри выпуклой оболочки. Обе операции онлайн за $O(\log n)$. ## Геометрическая прогрессия Найдите способ посчитать $\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\log n)$. ## Покемоны В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \to b$ и $b \to c$ не следует, что $a \to c$. У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш. ## Сортировка Можно ли отсортировать * 5 камней за 8 взвешиваний? * 5 камней за 7 взвешиваний? * 20 камней за 60 взвешиваний? ## Точки в круге Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат. ## Замкнутые ломаные Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии? ## Неубывающий массив Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим. ## Чётный цикл Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины. ## $k$-ая порядковая статистика Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$. ## Доминирующий элемент Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. Время работы $O((n+m) \log n)$. ## Разрушение дерева Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева. ## $k$-ый элемент на отрезке Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \log n)$. ## Различные числа на отрезке Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\sqrt{n})$. ## Физкультура Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов: 1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя) 2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно) При этом приказ может частично отменить действие предыдущих приказов. После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \log n)$. ## Нулевая сумма Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$. ## Мета-задача В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу. ## Ниточка В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления. ## Пельмени Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \log n)$. ## От нуля до единицы Дан следующий код: ```python x = 0 while x < 1: x += random() ``` Требуется посчитать матожидание `x`. (`random` в питоне возвращает случайное действительное число от 0 до 1.) ## Площадь Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%. ## Окружности Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна * соприкасаться с *внешней*, * соприкасаться с предыдущей (($k-1$)-ой), * иметь при этом максимальный радиус. Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. ## Блеф Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может: * Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается. * Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом. С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$. ## Достижимость Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000. ## Нумизмат Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой. Придумайте любой полиномиальный алгоритм. ## Принцесса В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве. Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством. У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми. Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу. Асимптотика $O(n^2)$. ## Спираль Определим спираль $(2n+1) \times (2n+1)$ как матрицу следующего вида: $$ \begin{matrix} 21 & 22 & 23 & 24 & 25 \\ 20 & 7 & 8 & 9 & 10 \\ 19 & 6 & 1 & 2 & 11 \\ 18 & 5 & 4 & 3 & 12 \\ 17 & 16 & 15 & 14 & 13 \\ \end{matrix} $$ Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$). $q \leq 100$, $n \leq 10^9$. ## Польский лабиринт Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`. ![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png) Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам. $n \leq 10^5$, изначальные координаты туристов до $10^9$. ## Нимные подмножества Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \log n)$. ## Баланс степеней Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода. ## Два пути Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$. ## Пьяница Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой? ## Ксоровый рюкзак Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$. ## Иван Сусанин Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш. - Камиль руководит армией днём и водит армию по *дорогам*. - Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск. Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу. Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось. Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$. ## Варенье В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм. Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья. Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной. $n, m \leq 10^5$ ## Лабиринт Серёжа потерялся в лабиринте $n \times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт. Придумайте любой полиномиальный алгоритм. ## Обезьяна Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт? ## Ожидание минимума Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин. Придумайте любой точный полиномиальный алгоритм. ## Шумный ксор Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \frac15$ быть единичным, то есть каждый бит $x \oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности: ```python x = # ... def mask(p=0.2): r = 0 for i in range(32): if random.random() < p: r += 2**i return r def query(y): return bin(x ^ y ^ mask()).count('1') ``` Ваша задача — отгадать число, используя не более 10000 попыток. + + + diff --git a/ru/bitset.html b/ru/bitset.html new file mode 100644 index 0000000..5d52aff --- /dev/null +++ b/ru/bitset.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Битовое сжатие * Из-за него в «асимптотиках» появляется `/64` * На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов * `bitset` есть в stl; говорят, самописный быстрее Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости. Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию. Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. Работать с ним нужно вот так: ```c++ const int lim = 1000; bitset b; // создать битсет размера lim (должно быть константой) b.set(); // заполнить единицами b.reset(); // заполнить нулями b.flip(); // заменить единички на нули и наоборот b.count(); // посчитать число единичек cout << b; // вывести битовую строку ``` Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`. ## Рюкзак Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака. Обычно его решают так: ```c++ bool dp[lim] = {}; // так можно его заполнить нулями dp[0] = 1; for (int i = 0; i < n; i++) for (int x = lim - a[i]; x >= 0; x--) dp[x + a[i]] |= dp[x]; ``` …а с битсетом оно разгоняется так: ```c++ bitset b; b[0] = 1; for (int i = 0; i < n; i++) b |= b << a[i]; ``` ## Цикл длины 3 Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\frac{n^3}{64})$ следующим образом: ```c++ bitset g[maxn]; // матрица смежности for (int a = 0; a < n; a++) { for (int b = 0; b < n; b++) { if (g[a][b] && (~g[a] & g[b]).any()) { // цикл найден } } } ``` Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд. ## Перемножение матриц Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения: ```c++ typedef bitset t; typedef array matrix; matrix operator* (matrix a, matrix b) { matrix c; for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) if(a[i][j]) c[i] |= b[j]; return c; } ``` ## Гаусс Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. Нас по сути просят решить следующую систему: $$ \begin{cases} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ \ldots \\ a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 \end{cases} $$ Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. В таком случае можно значительно ускорить и упростить обычный метод Гаусса: ```c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; } ``` Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. + + + diff --git a/ru/centroid.html b/ru/centroid.html new file mode 100644 index 0000000..5fc150a --- /dev/null +++ b/ru/centroid.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Центроидная декомпозиция Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути». Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще). ![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700) **Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\frac{n}{2}$. Выясняется, что центр всегда существует — это следует из алгоритма его поиска: ```c++ int s[maxn]; int sizes (int v) { s[v] = 1; for (int u : g[v]) // для простоты считаем, что дерево корневое s[v] += sizes(u); return s[v]; } // второй параметр -- размер дерева int centroid (int v, int n) { for (int u : g[v]) if (s[u] > n/2) return centroid(u, n); return v; } ``` **Утверждение.** `centroid` действительно находит цетроид. **Доказательство:** * `centroid` вернет вершину, у которой размеры всех детей не больше $\frac{n}{2}$ (это явно проверяется в `if`-е). * Мы пришли в эту вершину, когда её размер был больше $\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\frac{n}{2}+1) = \frac{n}{2}-1$ вершин. * Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен. Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид. **Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент». **Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением. Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты. Теперь поймём, зачем мы всё это делали. **Утверждение.** Каждая вершина входит в $O(\log n)$ компонент. **Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\lceil \log_2 n \rceil$ разделений. **Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \log n)$. **Утверждение.** Для любого пути $a \leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$. **Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$. Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний. ## Подсчет путей с заданным свойством Рассмотрим конкретный пример: подсчёт путей заданной длины. Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид. Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз. ```c++ int l = 179; // нужная нам длина int ans = 0; // нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена bool used[maxn]; int s[maxn]; // размеры поддеревьев void sizes (int v, int p) { s[v] = 1; for (int u : g[v]) if (u != p && !used[u]) sizes(u, v), s[v] += s[u]; } int centroid (int v, int p, int n) { for (int u : g[v]) if (u != p && !used[u] && s[u] > n/2) return centroid(u, v, n); return v; } // записывает в t[] глубины вершин void dfs (int v, int p, int d, vector &t) { t.push_back(d); for (int u : g[v]) if (u != p && !used[u]) dfs(u, v, d + 1, t); } void solve (int v) { /* */ size(v); vector d(s[v], 0); d[0] = 1; for (int u : g[v]) { if (!used[u]) { vector t; dfs(u, v, 1, t); for (int x : t) if (x <= l) ans += d[l-x]; for (int x : t) d[x]++; } } /* */ used[v] = 1; for (int u : g[v]) if (!used[u]) solve(centroid(u, v, s[u]/2)); } ``` **Асимптотика** $O(n \log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\log n)$ «уровней» центроидной декомпозиции. ## Запросы на путях — offline Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline. А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем. Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. Таким образом, каждый запрос будет просмотрен $O(\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \log n + n \log n)$. ## Запросы на путях — online Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы. Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \times \log n$, в котором для каждой вершины будем хранить $O(\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути. Тогда, при ответе на запрос, мы за $O(\log n)$ или даже $O(\log \log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида. ## Асимптотика при более долгих пересчётах TODO: сослаться на мастер-теорему Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени? Возьмем худший случай — когда каждый раз компонента разбивается на две равные части. Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень: $$ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} \leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n = n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} < 2 n \log n = O(n \log n) $$ Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \log n)$ — как, впрочем, и во всех остальных «разделяйках». + + + diff --git a/ru/convex-hulls.html b/ru/convex-hulls.html new file mode 100644 index 0000000..7029d81 --- /dev/null +++ b/ru/convex-hulls.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Задачи на многоугольники Очень часто геометрические задачи на контестах - это задачи на многоугольники. Некоторые из таких задач мы уже обсудили на предыдущих занятиях. Так мы уже обсудили задачу проверки многоугольника на выпуклость и научились находить площадь многоугольника. Напомним, что такое полярный угол. ![alt text](https://upload.wikimedia.org/wikipedia/commons/1/13/Polar_graph_paper.svg) # Выпуклые оболочки **Выпуклое множество** - такое множество точек, что все точки отрезка, образуемого любыми двумя точками данного множества, также принадлежат данному множеству **Выпуклая оболочка фигуры** - такое выпуклое множество точек, что все точки фигуры также лежат в нем. **Минимальная выпуклая оболочка фигуры** - это минимальная по площади выпуклая оболочка. ![alt text](http://informatics.letovo.ru/geom/Volgin_Asatrian/images/vup.png) ![alt text](http://grafika.me/files/les_screens/convex_1.png) ![alt text](https://2.bp.blogspot.com/-JngZWPM7wig/WlRz58cIMHI/AAAAAAAADGA/Pyhfngs_GKQMkb9B2_kqZJRQ5ZLEzrVMQCK4BGAYYCw/s1600/convexhullex.jpg) Дано множество точек, требуется построить его минимальную выпуклую оболочку : ## Построение за $O(nh)$ ### Алгоритм Джарвиса(метод заворачивания подарка) Давайте выберем какую-то точку, которая гарантированно попадет в минимальную выпуклую оболочкуо, например обычно берут нижнюю и если таких несколько, то самую левую из них. Теперь давайте по одной набирать точки, как бы заворачивая нашу выпуклую оболочку(отсюда и название). Как же нам найти следующую точку в выпуклую оболочку, давайте пройдемся по точкам, которые мы еще не взяли в МВО и среди них выберем с минимальным полярным углом. ![alt text](http://neerc.ifmo.ru/wiki/images/b/b3/Graham1.png) Корректность алгоритма легко доказывается по индукции, так как на первом шагу мы выбрали точку, точно лежащую в МВО, а на i, взяли такую точку, что все остальные лежат в нужной нам стороне. Асимптотика : для каждой точки выпуклой оболочки мы из всех оставшихся точек будем искать оптимальную - что будет работать за h(размер выпуклой оболочки) * n ### Важно помнить, что именно $O(hn)$, а не $O(n^2)$, так как существуют задачи на это ``` int base = 0; for (int i = 1; i < n; i++) { if (mas[i].y < mas[base].y) { base = i; } else if (mas[i].y == mas[base].y && mas[i].x < mas[base].x) { base = i; } } convex_hull.push_back(base); point first = mas[base]; point cur = first; point prev = point(first.x - 1, first.y); do { double minCosAngle = 1e9; // чем больше угол, тем меньше его косинус double maxLen = 1e9; int next = -1; for (int i = 0; i < n; i++) { double curCosAngle = CosAngle(prev, cur, mas[i]); if (Less(curCosAngle,minCosAngle)) {//если меньше сразу меняем next = i; minCosAngle = curCosAngle; maxLen = dist(cur, mas[i]); } else if (Equal(curCosAngle, minCosAngle)) {// смотрим по длине double curLen = dist(cur,mas[i]); if (More(curLen,maxLen)) { next = i; maxLen = curLen; } } } prev = cur; cur = mas[next]; convex_hull.push_back(next); } while (cur != first); ``` ## Построение за $O(n \log n)$ ### Алгоритм Грэхема Алгоритм Грэхема базируется на следующей идее: Давайте не искать следующую точку каждый раз, а сделаем так, чтобы у нас всегда была оптимальная точка и мы могли бы просто ее достать и проверить. В прошлом алгоритме мы искали точку, оптимальную по полярному углу, тогда давайте сейчас сразу отсортируем точки по полярному углу и сразу возьмем две первые точки в МВО. Теперь будем делать следующий алгоритм, пока все точки не будут просмотрены : 1) Возьмем первую из отсортированных точек. 2) Проверем последние три точки из взятых, если они образуют правый поворот, то удалим предпоследнюю точку Сделать это можно, например, стеком. Код есть ниже. ![alt text](https://cdn.turkaramamotoru.com/ru/algoritm-grehema-4131.jpg) Асимптотика : Мы просмотрим одну точку и либо удалим ее, либо оставим, то есть сам поиск МВО работает за линейное время, но мы еще делаем сортировку, а $\rightarrow$ алгоритм работает за $O(n\log(n))$, при этом его корректность вытекает из предыдущего алгоритма. Красивая визуализация - https://visualgo.net/en/convexhull красивое видео - https://www.youtube.com/watch?v=BTgjXwhoMuI . ``` struct Point { int x, y; }; Point operator -(Point a, Point b) { return {a.x - b.x, a.y - b.y}; } int operator * (Point a, Point b) { return a.x * b.x + b.y * a.y; } int operator ^(Point a, Point b) { return a.x * b.y - b.x * a.y; } bool cw(Point a, Point b, Point c) { return (a - b) * (c - b) > 0; } bool ccw(Point a, Point b, Point c) { return (a - b) * (c - b) < 0; } int main() { sort(all(p2), comp); vector s; s.push_back(p[min_ind]); for (int i = 0; i < n - 1; i++) { if (p2[i].x == s[s.size() - 1].x && p2[i].y == s[s.size() - 1].y) continue; while (s.size() > 1 && (vect(s[s.size() - 1], s[s.size() - 2]) ^ vect(s[s.size() - 1], p2[i])) > 0) s.pop_back(); s.push_back(p2[i]); } } ``` ### Алгоритм Эндрю Алгоритм Эндрю опирается на то, что вещественные числа не точны и предлагает поменять компаратор и строить не одну выпуклую оболочку, а две : Давайте выберем самую нижнюю и самую правую точку, затем отсортируем точки по самому простому из возможных компараторов, теперь будем строить две оболочки от самой правой точки и самой левой, в итоге мы получим верхнюю и нижнюю части выпуклой оболочки ``` bool comp(Point a, Point b) { if(a.x == b.x) { return a.y < b.y; } return a.x < b.x; } int main() { sort(all(p), comp); vector up, down; up.pb(p[0]); down.pb(p[0]); Point p1 = p[0], p2 = p.back(); for(int i = 1; i < n; i++) { if (i == n - 1 || cw(p1, p[i], p2)) { while (up.size() >= 2 && !cw(up[up.size() - 2], up[up.size() - 1], p[i])) { up.pop_back(); } up.pb(p[i]); } if (i == n - 1 || ccw(p1, p[i], p2)) { while (down.size() >= 2 && !ccw(down[down.size() - 2], down[down.size() - 1], p[i])) { down.pop_back(); } down.pb(p[i]); } } } ``` ### Алгоритм Чена Также существует алгоритм, объединяющий Джарвиса и Грэхема(Эндрю) и работающий за $O(n\log(h))$, но он разбираться не будет # Задачи 1) Базовые задачи - достаточно простые, например найти длину забора, чтобы ограничить многоугольник и подобные, но есть достаточно интересные задачи, в которых выпуклая оболочка неочевидна, например следующая : Даны $n$ пунктов в городе и $n$ почтальонов, для каждого пункта известно расстояние от почты $c_{i}$. Требуется каждому пункту доставить почту, $i$-ый почтальон просит $a_{i}$ монет, чтобы проснуться и $b_{i}$, чтобы проехать один километр, требуется для каждого пункта сказать, кто доставит почту наиболее выгодно. (Подсказка : $a_{i} + b_{i} * c_{j}$ - это прямая и стоимость доставки от $i$ почтальона к $j$ пункту). + + + diff --git a/ru/cross-entropy.html b/ru/cross-entropy.html new file mode 100644 index 0000000..873d4a7 --- /dev/null +++ b/ru/cross-entropy.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Почему машинное обучение работает b Принцип максимального правдоподобия — ## Функция правдоподобия $$ \hat{\theta} = \argmax_\theta P(X | \theta) $$ ## Принцип максимального правдоподобия ## Логистическая регрессия ## Нормальное распределение и MSE ## Почему сигмоида + + + diff --git a/ru/cuda.html b/ru/cuda.html new file mode 100644 index 0000000..2d9f174 --- /dev/null +++ b/ru/cuda.html @@ -0,0 +1,17 @@ + + + + + + + + + + + +# Supercomputing for the Masses This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in [Colab]() or [download]() and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: * `apt-get install nvidia-cuda-dev nvidia-cuda-toolkit` * `pip install pycuda` Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work. ## Subtlties of the Moore's law Here is a graph that roughly represents what is happening in the CPU world: **Moore's law** is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too. You can see that around 2005 there became a shift in design . The cores are more or less independent. Modern GPUs appeared in early 2000s. They exploit the specific area they operate. There are physical limitations to the speed of a core. One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another. Some of them have The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome. ## Why multiprocessing? Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound There are two types of ## General-purpose GPU There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills. There are several. This is like with Windows and Linux. We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning. ## Heterogineous computing CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs. ## Differences from CPUs ### Threads Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive. By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput. ### Memory The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device? You can easily dump 98% of performance of you think this way. ## Installing PyCUDA CUDA is available for many languages. Nice documentation can be found here: https://documen.tician.de/pycuda/index.html If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU". ```python # you may want to clear the output of this cell after installation from IPython.display import clear_output # this might take a while !pip install pycuda clear_output() ``` ```python import numpy as np from pycuda.compiler import SourceModule import pycuda.driver as drv import pycuda.autoinit ``` ## The basics Let's start with a simple example and then dive deeper. ## Kernels Just like C or C++, except that you use some custom built-in functions and specifiers. CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: You need to think of your computer as a heterogenious machine: there is host data and device data. * You move input data to device memory. * You run some computation on device. * You retrieve back this data. In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. ## The famous $A + B$ problem For testing and coordination with host, we will use **NumPy** package. If you don't have it, install it: `pip install numpy`. NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it. ```python # lets generate our test data: two float arrays filled with something random a = numpy.random.randn(100).astype('float32') b = numpy.random.randn(100).astype('float32') # the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it # we need to create space where kernel should write its answers to dest = numpy.zeros_like(a) # this is the kernel itself mod = SourceModule(""" __global__ void add(float *dest, float *a, float *b) { const int i = threadIdx.x; dest[i] = a[i] + b[i]; } """) # you need to specify the source code, and PyCUDA will compile it add_kernel = mod.get_function("add") add_kernel( drv.Out(dest), # specifies that this memory should be accessible for writing drv.In(a), # specifies this should be accessible for reading drv.In(b), block=(100,1,1) # we'll talk about it in a minute ) assert np.allclose(dest, a + b), 'WA' # checks that these are equal print('OK') ``` File "", line 19 %%time ^ SyntaxError: invalid syntax ### Memory management In CUDA C API, you need to allocate memory explicitly. So this is actually really nice. There is also `drv.InOut` function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too. Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough. GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time. You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications ## Synchronization **Reduction** is any array-wise operation. Assume the following problem: ## Dynamic programming Consider the following recurrence: ```python ## Problem: dynamic programming ``` ## Work vs. Latency We actually think about both work and step complexity now. Some tasks, especially in cryptography, cannot be parallelized. But some can. ## Summing arrays in $O(\log n)$ time Assume we want to perform some associative (i. e. $A*(B*C) = (A*B)*C$) operation on an array of $n$ elements. Say, sum it up. Normally, we would do that with a simple loop: ```c++ float s = 0; for (int i = 0; i < n; i++) { s += a[i]; } ``` Its computation graphs looks like this: This is optimal in terms of work complexity, but not in terms of step complexity: it's $O(n)$. We may want something that is a bit worse in terms of work complexity, but can be parallelized. Let's try this divide-and-conquer approach: Now it's still $O(n)$ work complexity (you actually need exactly the same number of additions), but this is $O(\log n)$ step complexity. When you unroll the recursion from top to bottom, you will see that to get each required value, ## Reducing small arrays ```python a = numpy.random.randn(2048).astype('float32') mod = SourceModule(""" __global__ void sum(float *dest, float *a, float *b) { const int i = threadIdx.x; // for l from 0 to logn: // __sync_threads() // if the thread is active // sum two elements into where they belong // a[0] should containt the needed sum } """) sum_kernel = mod.get_function("sum") add_kernel( drv.InOut(a), block=(1024,1,1) ) assert np.allclose(dest, a + b), 'WA' # checks that these are equal print('OK') ``` ## Warps and thread blocks Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties. You can actually do the same stuff with 2d and 3d indexing — weird, right? ## Atomics ## Reducing big arrays ## Reducing very big arrays Now, things get harder. It's time to tell how exactly GPU parallelism works. ```python ``` ## Dense Matrix multiplication Let's get to our first example where using GPUs actually makes sense: matrix multiplication. ## Sorting Our last (and hardest task) is to implement sorting. You might notice that we advocated divide-and-conquer approaches most of the time. It's true. They work. But we can't get an algorithm that works already. ```python # we'll use a deep learning library for benchmarking because I'm not familiar with anything else import torch a = torch.randn(10**8) b = a.cuda() ``` ```python # this should run for ~15 secs %time c = torch.sort(a) %time c = torch.sort(b) ``` CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s Wall time: 15.2 s CPU times: user 274 ms, sys: 237 ms, total: 511 ms Wall time: 511 ms So, 30 times speedup. So, we now what we need to compete against. ```python b.sort() ``` (tensor([-5.4567, -5.3551, -5.3288, ..., 5.3529, 5.4484, 5.4486], device='cuda:0'), tensor([55083205, 8383169, 73705953, ..., 79814161, 50474932, 27805828], device='cuda:0')) There are two types of sorting algorithms: data-driven. The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort. It has $O(\log n)$ stages, in total they have $1 + 2 + 3 + \ldots + \log n = O(\log^2 n$ blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has $O(n \log^ n)$ work complexity, but $O(\log^2 n)$ step complexity, which is pretty sweet. It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation: ```python def bitonic_sort(a, up=False): if len(a) <= 1: return a else: l = bitonic_sort(x[:len(a) // 2], True) r = bitonic_sort(x[len(a) // 2:], False) return bitonic_merge(first + second, up) def bitonic_merge(a, up): # assume input a is bitonic, and sorted list is returned if len(a) == 1: return a else: bitonic_compare(a, up) l = bitonic_merge(a[:len(a) // 2], up) r = bitonic_merge(a[len(a) // 2:], up) return l + r def bitonic_compare(a, up): dist = len(a) // 2 for i in range(dist): if (a[i] > a[i + dist]) == up: a[i], a[i + dist] = a[i + dist], x[i] # this is how swap is done in Python ``` ```python bitonic_sort([57, 179, 42, 17, 300, 111]) ``` [300, 179, 111, 57, 42, 17] ```python a = np.random.randn(10**8).astype('float32') ``` --------------------------------------------------------------------------- NameError Traceback (most recent call last) in () ----> 1 a = np.random.randn(10**8).astype('float32') NameError: name 'np' is not defined ## Why CUDA Most of it still applicable. Again, GPU programming is very specific. SSE and tensor cores. ## Kernels Just like C or C++, except that you use some custom built-in functions and specifiers. CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: You need to think of your computer as a heterogenious machine: there is host data and device data. * You move input data to device memory. * You run some computation on device. * You retrieve back this data. In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. What you need to understand about GPUs is that they are extremely specialised for their applications. Intrinsics for that. Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision. First, they introduced "multiply-accumulate" operation (e. g. `x += y * z`) per 1 GPU clock cycle. Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell). Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix). This is a lot of work per Well, you don't really need anything more precise than that for deep learning anyway. It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices. Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores". So, see, this is not exactly fair comparison. * +You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull +* down to int4 (16-valued, you heard correct) You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch. Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply. ## Reducing an array It seems to be simple: you just need to . What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen: 1. Read $x$ into register 2. Read $s$ into register 3. Calculate $s + x$ 4. Write it back to wherever $s$ was initially Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value. Note: Atomics to do that for small data types they are implemented on the hardware level and much more faster than that. std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. ## Memory types If all the various types of device memory were to race, here’s how the race would turn out: Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible). * 1st place: **Register memory**
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread. * 2nd place: **Shared Memory**
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should * 3rd place: **Constant Memory**
* 4th: Texture Memory * Tie for last place: Local Memory and Global Memory What you need to care for now is register For now, you need to care about differe Accessing global memory takes hundreds. ## Problem: dense matrix multiplication A lot of these are actually sparse. You can do stuff with social network graphs or web graphs. Cool. But let's disapploint us for a bit: + + + diff --git a/ru/dp-intro.html b/ru/dp-intro.html new file mode 100644 index 0000000..e4327b4 --- /dev/null +++ b/ru/dp-intro.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Динамическое программирование > Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. Разберем некоторые классические идеи и задачи в динамическом программировании. ## Рюкзак > Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. ## Наибольшая общая подпоследовательность > Есть две строки. Найти длину их наибольшей общей подпоследовательности. ## Расстановка скобок > Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. Можно завести динамику «можно ли ». ## Наибольшая возрастающая подпоследовательность Есть также алгоритм за $O(n \log n)$. ## Мемоизация Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. Первый предполагает ## Число разбиений > ## Замена параметра на ответ > + + + diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html new file mode 100644 index 0000000..acd57c8 --- /dev/null +++ b/ru/dp-optimizations.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +## Оптимизации динамики * Разделяй и властвуй * Оптимизация Кнута * Convex Hull Trick * Лямбда-оптимизация ## Задача > Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. ```c++ // x[] — отсортированный массив координат точек, нумерация с нуля // квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } // TODO: предподсчитать cost for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ``` Заметим, что циклы по `i` и `j` можно поменять местами. Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. ## Разделяй-и-властвуй Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. *Утверждение.* $opt[i, j] \leq opt[i, j+1]$. Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. ```c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); } ``` Вызываться она будет просто последовательно для каждого слоя: ```c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k); ``` Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. Получается, что асимптотика улучшилась до $O(n m \log n)$. ## Оптимизация Кнута Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? $$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: $$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. ```c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } } ``` Сравните с базовым решением — всего 3 новых строчки. ## Convex Hull Trick Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. $$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ f[k, j-1] + x_{i-1}^2 - 2x_{i-1} x_k + x_k^2 \}$$ Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется $ \underbrace{f[k, j-1] + x_k^2}_{a_k} \underbrace{-2x_k}_{b_k} x_{i-1} $. Это теперь можно переписать как $ \min_k (a_k, b_k) \cdot (1, X_{i-1}) $ (имеется в виду скалярное произведение) Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. TODO: иллюстрация. TODO: мем про Скуби-Ду. ### Ли Шао Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. ## Лямбда-оптимизация Не «фольклорное» название — дискретный метод множителей Лагранжа. Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . Заметим следующее: Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. ## Суммируем TODO: сделать табличку * Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. * Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. * CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. * Лагранж: $O(n \log n)$. Функция должна быть выпуклой. ## Другие задачи Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. > Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. + + + diff --git a/ru/dp_intro.html b/ru/dp_intro.html new file mode 100644 index 0000000..e1f20f8 --- /dev/null +++ b/ru/dp_intro.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Динамическое программирование test123 > Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. Разберем некоторые классические идеи и задачи в динамическом программировании. ## Рюкзак > Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. ## Наибольшая общая подпоследовательность > Есть две строки. Найти длину их наибольшей общей подпоследовательности. ## Расстановка скобок > Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. Можно завести динамику «можно ли ». ## Наибольшая возрастающая подпоследовательность Есть также алгоритм за $O(n \log n)$. ## Мемоизация Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. Первый предполагает ## Число разбиений > ## Замена параметра на ответ > + + + diff --git a/ru/dp_optimizations.html b/ru/dp_optimizations.html new file mode 100644 index 0000000..acd57c8 --- /dev/null +++ b/ru/dp_optimizations.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +## Оптимизации динамики * Разделяй и властвуй * Оптимизация Кнута * Convex Hull Trick * Лямбда-оптимизация ## Задача > Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. ```c++ // x[] — отсортированный массив координат точек, нумерация с нуля // квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } // TODO: предподсчитать cost for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ``` Заметим, что циклы по `i` и `j` можно поменять местами. Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. ## Разделяй-и-властвуй Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. *Утверждение.* $opt[i, j] \leq opt[i, j+1]$. Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. ```c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); } ``` Вызываться она будет просто последовательно для каждого слоя: ```c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k); ``` Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. Получается, что асимптотика улучшилась до $O(n m \log n)$. ## Оптимизация Кнута Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? $$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: $$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. ```c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } } ``` Сравните с базовым решением — всего 3 новых строчки. ## Convex Hull Trick Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. $$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ f[k, j-1] + x_{i-1}^2 - 2x_{i-1} x_k + x_k^2 \}$$ Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется $ \underbrace{f[k, j-1] + x_k^2}_{a_k} \underbrace{-2x_k}_{b_k} x_{i-1} $. Это теперь можно переписать как $ \min_k (a_k, b_k) \cdot (1, X_{i-1}) $ (имеется в виду скалярное произведение) Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. TODO: иллюстрация. TODO: мем про Скуби-Ду. ### Ли Шао Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. ## Лямбда-оптимизация Не «фольклорное» название — дискретный метод множителей Лагранжа. Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . Заметим следующее: Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. ## Суммируем TODO: сделать табличку * Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. * Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. * CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. * Лагранж: $O(n \log n)$. Функция должна быть выпуклой. ## Другие задачи Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. > Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. + + + diff --git a/ru/fenwick.html b/ru/fenwick.html new file mode 100644 index 0000000..b584776 --- /dev/null +++ b/ru/fenwick.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + НЯ!
Эта статья полна любви и обожания.
Возможно, стоит добавить ещё больше? +# Дерево Фенвика Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности. ## Определение Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: $$ t_i = \sum_{k=F(i)}^i a_k $$ где $F$ это какая-то функцию, для которой выполнено $F(i) \leq i$. Конкретно её определим потом. Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле: $$ sum(k) = t_k + sum(F(k)-1) $$ Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка. $F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\log n)$. Популярны две функции: * $F_1(x) =$ `x & (x + 1)` * $F_2(x) =$ `x - (x & -x) + 1` Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать. **Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\log n)$ таких отрезков. ## Реализация Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$. ```c++ int t[maxn]; // возвращает сумму на префиксе int sum (int r) { int res = 0; for (; r > 0; r -= r & -r) res += t[r]; return res; } int sum (int l, int r) { return sum(r) - sum(l-1); } // обновляет нужные t void add (int k, int x) { for (; k <= n; k += k & -k) t[k] += x; } ``` Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии. ## Многомерный случай > $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре. `sum` перепишется следующим образом: ```c++ int sum (int r1, int r2) { int res = 0; for (int i = r1; i > 0; i -= i & -i) for (int j = r2; j > 0; j -= j & -j) ans += t[i][j]; return res; } ``` В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах. Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017. ## Бинпоиск Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\log n)$. ```c++ // возвращает индекс, на котором сумма уже больше int lower_bound (int s) { int k = 0; for (int l = logn; l >= 0; l--) { if (k + (1<= k > i - (i & -i)`. Будем перебирать префиксы TODO Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя. Можно представить дерево так: ячейка 2^k содержит все TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию. ## Название Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree». Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев. Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных). В частном случае, когда длина массива равна $2^k$, то дерево будет только одно. + + + diff --git a/ru/games.html b/ru/games.html new file mode 100644 index 0000000..c45c188 --- /dev/null +++ b/ru/games.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Теория игр Начнём с самого баянного примера математической игры, который можно вспомнить: > На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков. Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так: $$ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) $$ то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния). **Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях. ## Игры на графах В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек». Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные* Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*. Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*. ## Ретроанализ Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности: * Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины. * Вершина *выигрышная* — если из неё есть переход в проигрышную вершину. * Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные). Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся. **Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин. Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние. Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось. Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные. **Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали. * При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). * При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше. Асимптотика составит $O(n + m)$, как и у любого bfs-а. ```c++ vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа int cnt[maxn]; // счётчик исходящих рёбер enum StatusType { win, loss, unknown }; StatusType status[maxn]; // выгрышность вершины; // по умолчанию все кроме терминальных считаются unknown // те, кто в итоге остаются unknown -- ничейные queue q = {/* нужно заранее добавить сюда все терминальные вершины*/}; while (!q.empty()) { int v = q.front(); q.pop(); for (int u : t[v]) { cnt[u]--; // удаляем это ребро if (status[v] == unknown) { // из u есть ребро в проигрышную -- значит она выигрышная if (status[v] == loss) status[u] = win; // все ребра u ведут в выигрышные вершины -- значит она проигрышная if (status[v] == win && cnt[u] == 0) { status[u] = loss; // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь if (status[v] != unknown) q.push(u); } } } ``` Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации: ```c++ StatusType dfs(int v) { if (status[v] != unknown) return status[v]; status[v] = loss; // изменим статус, когда найдём переход в проигрышную вершину for (int u : g[v]) if (dfs(u) == loss) status[v] = win; return status[v]; } ``` TODO: можно ли здесь циклы учесть? ## Минимаксные игры Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно. Типичный граф минимаксной игры выглядит следующим образом: В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников. Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей. ## Ретроанализ для больших графов Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго. Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей). Ретроанализ нужно оптимизировать. ### Ограничение перебора Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит. Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли). ### Мемоизация позиций В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает . Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары ### Альфа-бета отсечение Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым. На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым. Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6. Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем. Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации. ### Monte-Carlo Tree Search ## Игры с ненулевой суммой ## Теория Шпрага-Гранди Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые. Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями. **Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \oplus a_2 \oplus \ldots \oplus a_n $ размеров кучек отлична от нуля. **Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы: * Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия. * Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \oplus a_i \oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$. Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \oplus a_i$. Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории. **Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен. ### Ним с увеличениями Пусть у нас ### Эквивалентность игр ниму ## Игры с неполной информацией Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков. Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет. Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло. ### Дилемма заключенного ### Камень-ножницы-бумага ### Покер Рассмотрим такую у + + + diff --git a/ru/geometry.html b/ru/geometry.html new file mode 100644 index 0000000..2e5b407 --- /dev/null +++ b/ru/geometry.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Геометрия и ООП Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали. ![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg) Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь. **Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается: $$ a \cdot b = x_a x_b + y_a y_b $$ ![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg) У него есть полезные свойства: * Скалярное произведение симметрично ($a \cdot b = b \cdot a$). * Геометрически, это проекция вектора b на вектор a, помноженынй на длину а. * Перпендикулярные вектора должны иметь нулевое скалярное произведение. * Если угол острый, то оно положительное * Если угол тупой, то отрицательное **Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах: $$ a \times b = x_a y_b - y_a x_b $$ ![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg) Его свойства: * Скалярное произведение *анти*симметрично ($a \times b = - (b \times a)$). * Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора. * Коллинеарные вектора должны иметь нулевое векторное произведение. * Если b «слева» от a, то оно положительное. * Если b «справа» — то отрицательное. ## Всякие проверки Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями. **Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником. $$ \text{P лежит внутри ABC} \iff \begin{cases} (B-A) \times (P-A) \geq 0 \\ (C-B) \times (P-B) \geq 0 \\ (A-C) \times (P-C) \geq 0 \\ \end{cases} $$ **Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения. $$ V = \frac{1}{2} (B-A) \times (C-A) $$ **Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными). ![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg) Забудьте о формуле Герона и всегда считайте площади через векторное произведение. Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2. **Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\times(c-a) > 0$). **Пересекаются ли отрезки**. ![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png) ## Уравнение прямой Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством. У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \geq 0$ будет указывать в сторону самой полуплоскости. Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой: $$ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} $$ **Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений: $$ \begin{cases} A_1 x + B_1 y + C_1 = 0 \\ A_2 x + B_2 y + C_2 = 0 \end{cases} \implies \begin{cases} -x = \frac{B_1 y + C_1}{A_1} \\ -x = \frac{B_2 y + C_2}{A_2} \end{cases} \implies \frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} \implies y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} $$ Аналогично, $x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки). Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать. # Как это кодить в C++ Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. **Точка $\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно. ```c++ struct r { double x, y; r () {} r (int _x, int _y) { x = _x, y = _y; } }; ``` Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$. ## Операции над векторами Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину: ```c++ double len (r a) { return sqrt(a.x*a.x + a.y*a.y); } ``` ## Операторы В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д. Давайте для будущих нужд определим `+` и `-`: ```c++ r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); } r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); } ``` ## Скалярное произведение ```c++ int operator*(r a, r b){ return a.x*b.x + a.y*b.y; } ``` ## Векторное произведение Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике. ```c++ int operator^(r a, r b){ return a.x*b.y - b.x*a.y; } ``` ### Ввод-вывод Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так: ```c++ istream& operator>>(istream ∈, r &p){ in >> p.x >> p.y; return in; } ostream& operator<<(ostream &out, r &p){ out << p.x << " " << p.y << endl; return out; } ``` ## Алгебра VS Алгоритмы или зачем мы всё это делали Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен. ### Векторное представление прямой ($Ax + By + C = 0 \rightarrow r = at + b$) Тут нужно просто выбрать две любые точки на прямой. ```c++ // даны A, B, C (A^2 + B^2 != 0) r a, b; if (eq(A, 0)) // значит, это горизонтальная прямая a = r(0, -C/B), b = r(1, -C/B); else a = r(-C/A, 0), b = (1, -(C+B)/A, 1) ``` ### Пример: отражение от прямой Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать. $\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} $ Формула имеет смысл: длина на единичный вектор направления. Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить. ```c++ // прямая r = at + b, точка c r pr (r a, r b, r c) { c -= b; // пусть c и a выходят из одной точки return b + (a*b / len(a) / len(a)) * a; } r reflect (r a, r b, r c) { return c + 2*(pr(a, b, c)-c); } ``` ## Типичные баги ### Точность > Первое правило действительных чисел — не использовать действительные числа Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`: ```c++ const double eps = 1e-8; bool eq (double a, double b) { return abs(a-b) < eps } ``` Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре. ### $0 \neq -0$ Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду. ### Область определения обратных функций `acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции. + + + diff --git a/ru/hashing.html b/ru/hashing.html new file mode 100644 index 0000000..665f45b --- /dev/null +++ b/ru/hashing.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Хэширование в строковых задачах Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка. «Хорошая» хэш-функция: * Быстро считается — за линейное от размера объекта время; * Имеет не очень большие значения — влезающие в 64 бита; * «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\frac{1}{n}$. Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*. Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \cdot n \cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки. ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png) ## Применения в реальной жизни * **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить. * **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\frac{k}{n}$ = $O(1)$ при правильном выборе $n$. * **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш. * **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками. * **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить. * **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса. Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы. Сегодня же мы остановимся на строках. # Полиномиальное хэширование **Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами. Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`. Определим *прямой полиномиальный хэш* строки как значение следующего многочлена: $$ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p $$ Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой. Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$: ```c++ const int k = 31, mod = 1e9+7; string s = "abacabadaba"; long long h = 0, m = 1; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h + m * x) % mod; m = (m * k) % mod; } ``` Можем ещё определить *обратный полиномиальный хэш*: $$ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p $$ Его преимущество в том, что можно написать на одну строчку кода меньше: ```c++ long long h = 0; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h * k + x) % mod; } ``` Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$. ## Зачем он нужен? Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций. Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$: $$ h(ab) = h(a) + k^{|a|} \cdot h(b) $$ Удалить префикс строки можно так: $$ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} $$ А суффикс — ещё проще: $$ h(a) = h(ab) - k^{|a|} \cdot h(b) $$ В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве: ```c++ const int maxn = 1e5+5; int p[maxn]; p[0] = 1; for (int i = 1; i < maxn; i++) p[i] = (p[i-1] * k) % mod; ``` Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса: ```c++ int h[maxn]; h[0] = 0; // h[k] -- хэш префикса длины k // будем считать, что s это уже последовательность int-ов for (int i = 0; i < n; i++) h[i+1] = (h[i] + p[i] * s[i]) % mod; ``` Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке: $$ h(s[l:r]) = \frac{h_r-h_l}{k^l} $$ Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим. Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить. $$ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$ ```c++ int hash_substring (int l, int r) { return (h[r+1] - h[l]) * p[n-l] % mod; } ``` Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$. ### Упражнение Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. ## Примеры задач **Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`. **Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа. **Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним. **Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором. **Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно. ### Изменения строки* Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына. Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне. Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова. # Вероятность ошибки и почему это всё вообще работает У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения. Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого? ## Выбор констант Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \cdot n^2$. Обоснование — см. парадокс дней рождений. Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно. Можно также брать модуль $2^{64}$. У него есть несколько преимуществ: * Он большой — второй модуль точно не понадобится. * С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении. * С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`. Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду. В выборе же $k$ ограничения не такие серьезные: * Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. * Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться. Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты. ## Парадокс дней рождений > В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%. Более общее утверждение: в мультимножество нужно добавить $\Theta(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. **Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. $$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ Попытаемся оценить $f$: $$ \begin{align} \begin{aligned} e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ & = e^{-\frac{n(n-1)}{2d}} & \\ & \simeq e^{-\frac{n^2}{2d}} & \\ \end{aligned} \end{align} $$ Из последнего выражения более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется очень быстро. **Второе доказательство** (для любителей теорвера). Введем $\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\frac{1}{d}$. Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\frac{n (n-1)}{2} \cdot \frac{1}{d}$. Отсюда понятно, что если $d = \Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно. *Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1. ### Бонус: «мета-задача» Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу. + + + diff --git a/ru/hld.html b/ru/hld.html new file mode 100644 index 0000000..974ba1f --- /dev/null +++ b/ru/hld.html @@ -0,0 +1,17 @@ + + + + + + + + + + + +# Heavy-light декомпозиция HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid). * +TODO: найти менее уродливую иллюстрацию +* Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого. Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения. **Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер. **Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра. Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер. **Утверждение.** На любом вертикальном пути будет не более $O(\log n)$ блоков. **Доказательство** разбивается на две части: * Лёгких ребер на вертикальном пути будет не более $O(\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего. * Непрерывных путей из тяжелых рёбер будет не более $O(\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\log n)$. **Следствие.** На любом пути будет не более $O(\log n)$ блоков. Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам. ## Реализация Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера. А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь. Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре. ```c++ vector g[maxn]; int s[maxn], p[maxn], tin[maxn], tout[maxn]; int head[maxn]; // «голова» тяжелого пути, которому принадлежит v int t = 0; void sizes (int v = 0) { s[v] = 1; for (int &u : g[v]) { sizes(u); s[v] += s[u]; if (s[u] > s[g[v][0]]) // &u -- это ссылка, так что её легально использовать при swap-е swap(u, g[v][0]); } } void hld (int v = 0) { rin[t] = v; tin[v] = t++; for (int u : g[v]) { // если это тяжелый ребенок -- его next нужно передать // в противном случае он сам является головой нового пути head[u] = (u == g[v][0] ? head[v] : u); hld(u); } tout[v] = t; } ``` ## Как им решать задачи Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов: 1. Узнать минимальное число на пути между $v_i$ и $u_i$. 2. Изменить число у $v_i$-той вершины на $x_i$. Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`. ```c++ int val[maxn]; segtree st(0, n); ``` При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков: ```c++ void upd (int v, int x) { st.upd(tin[v], x); } ``` Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям. ```c++ int ancestor (int a, int b) { return tin[a] <= tin[b] && tin[b] <= tout[a]; } void up (int &a, int &b, int &ans) { while (!ancestor(head[a], b)) { ans = min(ans, st.get_min(tin[head[a]], tin[a])); a = p[head[a]]; } } int get_min (int a, int b) { int ans = inf; up(a, b, ans); up(b, a, ans); if (!ancestor(a, b)) swap(a, b); ans = min(ans, st.get_min(tin[a], tin[b])); return ans; } ``` + + + diff --git a/ru/index.html b/ru/index.html new file mode 100644 index 0000000..750277c --- /dev/null +++ b/ru/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Алгоритмика Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/). Более-менее готово: * [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное * [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое * [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки * [Центроидная декомпозиция](http://sereja.me/a/centroid) * [Heavy-light декомпозиция](http://sereja.me/a/hld) * [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла * [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву * [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость * [Ро-алгоритм Полларда](http://sereja.me/a/pollard) * [Sparse table](http://sereja.me/a/sparse-table) * [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона * [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса * [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера * [Классные задачки](http://sereja.me/a/bayans): пока что без решений Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально. Не очень готово: * [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция * [Суффиксный массив](http://sereja.me/a/suffix-array) * [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++ * [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick * [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала * [Обратное по модулю](http://sereja.me/a/reciprocal) * [Метод отжига](http://sereja.me/a/annealing) * [Стресс-тестирование](http://sereja.me/a/stress-test) * [Бор](http://sereja.me/a/trie) * [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько * Выпуклые оболочки * [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса * CUDA: программирование на GPU (на английском) Вообще не готово: * [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема * [Ахо-Корасик](http://sereja.me/a/aho-corasick) * [Теорвер](http://sereja.me/a/probability) * Корневая декомпозиция * Теория информации * Функции потерь в машинном обучении * Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском) * Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском) Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности. + + + diff --git a/ru/ioi.html b/ru/ioi.html new file mode 100644 index 0000000..12f628c --- /dev/null +++ b/ru/ioi.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# IOI Upsolving Challenge https://contest.yandex.com/ioi/total Я решал их почти 2 года назад и уже не очень хорошо всё помню. # 09-2 12 мая ### Медведь Миша Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за $O(n^2 \log n)$. ### Регионы В ограничениях нам спалили важную идею: задачу можно просто решить в случае, если все регионы небольшие, а также в случае, если регионов немного. Обычно это означает, что решения можно объединить с помощью корневой эвристики. Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так: * Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк * Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос Такое должно работать за $O(n \sqrt{n \log n})$, и TL такое позволяет. ### Коммивояжёр Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать. Подумайте, какой вид будет иметь эта функция, если зафиксировать время. TODO # 05-1 31 мая ### Сад Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно. ### Средняя последовательность Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы: ```c++ int a = -inf, b = inf, _d = 0, d = 0, t; for(int i = 0; i < n; i++){ cin >> t; _d = d; d = 2*t - d; if (i % 2 == 0) b = min(b, (d-_d)/2); else a = max(a, (_d-d)/2); } cout << max(b - a + 1, 0ll); ``` ### Польские горки По сути от нас просят обрабатывать запросы «присвоить всем элементам с $l$ по $r$ значение $x$» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше. # 05-2 ### Прямоугольник Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста. + + + diff --git a/ru/karatsuba.html b/ru/karatsuba.html new file mode 100644 index 0000000..bafc1de --- /dev/null +++ b/ru/karatsuba.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Быстрое умножение В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел). [Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал. Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба. Алгоритм Карацубы имеет довольно значимое место в истории науки. {\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}). У Колмогорова была гипотеза, что нижняя оценка для {\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности {\displaystyle M(n)=O(n^{\log _{2}3})} M(n)=O(n^{{\log _{2}3}}) и тем самым опроверг «гипотезу {\displaystyle n^{2}} n^{2}». (Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.) Можно представить себе какого-то грозного японца. ## Мастер-теорема Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек». **Мастер-теорема.** Пусть имеется рекуррента: $$ T(n) = \begin{cases} a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 \\ \Theta(1), & n \leq n_0 \end{cases} $$ Тогда: * **A.** Если $c > \log_b a$, то $T(n) = \Theta(n^c)$. * **B.** Если $c = \log_b a$, то $T(n) = \Theta(n^c \log n)$. * **C.** Если $c < \log_b a$, то $T(n) = \Theta(n^{\log_b a})$. --- --- **Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням: $$ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k $$ * **A.** Если $c > \log_b a$, то $\sum (\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \Theta(n^c)$. * **B.** Если $c = \log_b a$, то $$\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)$$ * **C.** Если $c < \log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу, $$ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) $$ Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$ ## Алгоритм Карацубы Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат. Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. ## Развитие идеи То же самое можно применить матрицам. Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\epsilon})$ для произвольного $\epsilon$ ещё никто не доказал. + + + diff --git a/ru/lca.html b/ru/lca.html new file mode 100644 index 0000000..dc16a63 --- /dev/null +++ b/ru/lca.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Корневые деревья Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*. Примеры корневых деревьев: * наследование классов в языках программирования (если множественное наследование запрещено), * дерево факторизации числа на простые (в общем случае не уникальное), * иерархия в какой-нибудь компании, * просто множество как-то вложенных объектов (ссылка на крысу-роботягу). Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах. ![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png) ## Напоминание: DFS Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. ```c++ vector g[maxn]; int p[maxn], tin[maxn], tout[maxn]; int t = 0; void dfs (int v) { tin[v] = t++; for (int u : g[v]) dfs(u); tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет } ``` У этих массивов много полезных свойств: * Вершина $u$ является предком $v$ $\iff tin_v \in [tin_u, tout_u) $. Эту проверку можно делать за константу. * Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой. * В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер. * Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$. * Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации. ## Запросы на поддеревьях Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО. > Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$. Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree). ## Запросы на уровнях > Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$). Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам. Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку. ## Наименьший общий предок Очень много задач нам поможет решить следующая вспомогательная задача. > Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких. По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные. ![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png) Для лучшего понимания: медленно (за линейное время) это можно делать так: ```c++ bool a (int u, int v) { return tin[u] <= tin[v] && tin[v] <= tout[u]; } int lca (int u, int v) { while (!ancestor(u, v)) u = p[u]; return u; } ``` ## LCA: двоичные подъемы Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \times \lceil \log n \rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень. Такой препроцессинг можно выполнить за $O(n \log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$: ```c++ int up[maxn][logn]; void dfs (int v) { for (int l = 1; l < logn; l++) up[v][l] = up[up[v][l-1]][l-1]; tin[v] = t++; for (int u : g[v]) { up[u][0] = v; dfs(u); } tout[v] = t++; } ``` Пусть поступил очередной запрос — пара вершин $(u, v)$: * Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. * Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA. Подробнее про второй пункт. Пусть $L = \lceil \log n \rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$. ```c++ int lca (int v, int u) { if (a(v, u)) return v; if (a(u, v)) return u; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l]; return up[v][0]; } ``` ### Асимптотика Препроцессинг — $O(n \log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу. Ответ на запрос — $O(\log n)$, потому что по сути мы делаем один бинпоиск. ## Запросы на путях Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа). Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути. Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA. ```c++ int get_min (int v, int u) { int ans = inf; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l], ans = min(ans, mn[v][l]); for (int l = logn-1; l >= 0; l--) if (!ancestor(up[u][l], v)) u = up[u][l], ans = min(ans, mn[u][l]); return min({ans, mn[v][0], mn[u][0]}) } ``` Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная. ## Сведение к RMQ Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить. Во втором массиве мы по сути выписали наш проход dfs-а. Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути. Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree). ### Разреженная таблица На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива. Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table). ## А наоборот можно?* *Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения. Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя. Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль. Возьмем константу $k = \lfloor \frac{\log n}{2} \rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\frac{2 n}{\log n})$, и построение будет работать за линейное время: $$O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)$$ Также посчитаем для каждой возможной маски размера $\frac{\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\sqrt n$ (ради этого мы и делили логарифм на два). ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу. Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали. Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear). Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики. Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.** + + + diff --git a/ru/linalg.html b/ru/linalg.html new file mode 100644 index 0000000..56cf471 --- /dev/null +++ b/ru/linalg.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Ликбез по линейной алгебре **Определение**. Функция $f: \mathbb{R}^n \to \mathbb{R}^m$ называется *линейной*, если для неё выполнено 1. $ f(x+y) = f(x) + f(y) $ 2. $ f(ax) = a f(x), \; a \in R $ Примеры: * $ f(x) = 0 $ * $ f(x) = x $ * $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\mathbb{R}^3$ в $\mathbb{R}$) * $ f(x) = (x, -x, 0) $ (из $\mathbb{R}$ в $\mathbb{R}^3$) Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: * Сумма линейных функций — линейная функция. * Сумма коммутативна: $f+g = g+f$). * Сумма ассоциативна: $(f+g)+h = f+(g+h)$. * Композиция $f(g(x)) = (f \circ g)(x)$ линейных функций — линейная функция. * Композция ассоциативна: $(f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h$. * Композиция в общем случае не коммутативна.
Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен. Все свойства можно вывести лишь из этих двух пунктов в определении. ## Что такое матрица? Можно показать, что любую линейную функцию $f: \mathbb{R}^n \to \mathbb{R}^m$ можно представить в таком виде: $$ f(x) = \begin{pmatrix} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ \ldots \\ a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ \end{pmatrix} $$ *Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$. $$ A = \begin{pmatrix} a_{11} & a_{12} & \ldots & a_{1n} \\ a_{21} & a_{22} & \ldots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \ldots & a_{mn} \\ \end{pmatrix} $$ Каждой линейной функции из $\mathbb{R}^n$ в $\mathbb{R}^m$ соответствует какая-то матрица размера $n \times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте. Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так: $$ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} $$ Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$. Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \times k$, то вторая должна иметь размер $k \times m$, то есть «средние» размерности обязательно должны совпадать. Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом. К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы. Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы: * Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$. * Сумма коммутативна: $A+B = B+A$) * Сумма ассоциативна: $(A+B)+C = A+(B+C)$ * Умножение ассоциативно: $(AB)C = A(BC) = ABC$. * Умножение в общем случае не коммутативно. **Пример**: матрица поворота в 2d. $$ \begin{pmatrix} \cos \alpha & -\sin \alpha \\ \sin \alpha & \cos \alpha \\ \end{pmatrix} $$ **Пример**: матрица проецирования на $Ox$ в 3d. $$ \begin{pmatrix} 1 & 0 & 0 \\ 0 & 0 & 0 \\ 0 & 0 & 0 \\ \end{pmatrix} $$ **Пример**: матрица «свапни $x$ и $y$». $$ \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ \end{pmatrix} $$ Напишем класс, который реализует матричное умножение. ```c++ struct matrix { int n, m; int t[]; matrix (int _n, int _m) { n = _n, m = _m; t = new int(n*m); memset(t, 0, sizeof t); } int[] operator[] (int k) { return t[k*m]; } } matrix operator* (matrix a, matrix b) { matrix c(a.n, b.m); for (int i = 0; i < a.n; i++) for (int j = 0; j < b.m; j++) for (int k = 0; k < a.m; k++) c[i][j] += a[i][k] * b[i][k]; return c; } ``` ## Динамика Некоторые динамики можно выразить в терминах матричного умножения. Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика? $$ \begin{pmatrix} f_{n+1} \\ f_{n+2} \\ \end{pmatrix} = \begin{pmatrix} 0+f_{n+1} \\ f_{n}+f_{n+1} \\ \end{pmatrix} = \begin{pmatrix} 0 & 1 \\ 1 & 1 \\ \end{pmatrix} \begin{pmatrix} f_{n} \\ f_{n+1} \\ \end{pmatrix} $$ Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$. Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам: $$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$ Это будет работать за $O(n^3 \log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны. ```c++ matrix binpow (matrix a, int p) { matrix b(n, n); for (int i = 0; i < n; i++) b[i][i] = 1; while (p) { if (p&1) b = b*a; a = a*a; p >>= 1; } return b; } ``` Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$. $$ \begin{vmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{vmatrix} $$ В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы. В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}$ имеет такую матрицу перехода: \begin{pmatrix} 0 & 1 & 0 & \ldots & 0 \\ 0 & 0 & 1 & \ldots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \ldots & 1 \\ a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ \end{pmatrix} ## Матрица смежности У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов. Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86)). ## Обобщения Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел. Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два. ## Определитель *Определителем* кадратной матрицы $A$ называется такое выражение: $$ \det A = |A| = \ldots $$ Где $\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например: $$ \begin{vmatrix} a & b \\ c & d \\ \end{vmatrix} = ad - bc $$ У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться. В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем. ## Базис Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. Базисы есть не только в линейной алгебре. Например, $\{1, x, x^2\}$ является базисом всех квадратных трёхчленов. Или $\{\neg, \land, \lor\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом. ## Метод Крамера и easy пересечение прямых Пусть нам надо пересечь две прямые. $$ \begin{cases} a_1 x + b_1 y + c_1 = 0 a_2 x + b_2 y + c_2 = 0 \end{cases} $$ Это то же самое, что найти такие коэффициенты $x$ и $y$, что $$ x \vec{a} + y \vec{b} = -\vec{c} $$ Площадь параллелограмма, натянутого на $\vec{a}$ и $\vec{b}$, равна векторному произведению, или детерминанту. По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$. Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да? ## *Собственные векторы Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление. $ Av = k v $, где $k \neq 0$. $ Av - kv = (A-kI)v = 0 $. Это означает Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике. ## Системы уравнений и метод Гаусса Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. Нас по сути просят решить следующую систему: $$ \begin{cases} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ \ldots \\ a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 \end{cases} $$ Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. Метод Крамера неоптимален — там $O(n^4)$ операций. В таком случае можно значительно ускорить и упростить обычный метод Гаусса: ```c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; } ``` Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81)). + + + diff --git a/ru/matching.html b/ru/matching.html new file mode 100644 index 0000000..5a7e2b5 --- /dev/null +++ b/ru/matching.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Паросочетания Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом. **Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$). Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием. *Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны. Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$. **Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер. **Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию. **Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию. ![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg) *Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \to 8 \to 4 \to 6 \to 3 \to 7$.* Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу. В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу. **Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже. ```c++ const int maxn; vector g[maxn]; // будем хранить только рёбра из левой доли в правую int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой) bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом // dfs возвращает, можно ли найти путь из вершины v // в какую-нибудь вершину правой доли // если можно, то ещё и проводит чередование bool dfs (int v) { if (used[v]) return false; used[v] = true; for (int u : g[v]) { // если вершина свободна, то можно сразу с ней соединиться // если она занята, то с нейможно соединиться только тогда, // когда из её текущей пары можно найти какую-нибудь другую вершину if (mt[u] == -1 || dfs(mt[u])) { mt[u] = v; return true; } } return false; } // где-то в main: memset(mt, -1, sizeof(mt)); for (int i = 0; i < n; i++) { memset(used, 0, sizeof(mt)); if (dfs(i)) cnt++; } ``` ## Корректность Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить. **Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным. **Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие. ## Скорость работы Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$. Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \approx 10^4$), потому что для него есть мощные неасимптотические оптимизации: * Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть). * Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу. * Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются. Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \sqrt m)$. ## Покрытие путями DAG-а Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути. Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\overline{H}$ следующим образом: * В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно. * Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$. * Теперь из графа $H$ сделаем граф $\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$. Если мы рассмотрим любой путь $v_1, v_2, \ldots, v_k$ в исходном графе $G$, то в графе $\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$. Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\overline{H}$ — это паросочетание в $H$ (напомним, это $\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$. Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них. Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин. ## Лемма Холла *Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно. ![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif) **Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли. **Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием. В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу. **База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. **Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием. Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна) Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$). Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно. ## Для ноулайферов: матроиды С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid). Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*. **Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна. + + + diff --git a/ru/matroid.html b/ru/matroid.html new file mode 100644 index 0000000..1f51643 --- /dev/null +++ b/ru/matroid.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Матроиды Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры). **Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства: * Пустое множество является независимым: $\varnothing \in I$ * Любое подмножество независимого множества тоже независимо: $$A \subset B, B \in I \implies A \in I$$ * Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$: $$A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I$$ Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \sum w(a_i)$. Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ: ```python X.sort() s = [] for x in X: if good(s + [x]): s += [x] ``` Здесь под `good` имеется в виду $s \cup x \in I$. Корректность этого алгоритма для любого матроида доказывает следующая теорема: ## Теорема Радо-Эдмондса Пусть $A \in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \cup x \in I,\;x \notin A,\;w(x)$ — минимальна. Тогда $A \cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. *Доказательство: ** Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. Из свойств матроида: $\exists y \in B \setminus A : A \cup y \in I$. Тогда верны два неравенства: $$ \begin{cases} w(A \cup y) = w(A) + w(y) \geq w(B) \implies w(A) \geq w(B) - w(y) \\ w(B \setminus y) = w(B) - w(y) \geq w(A) \implies w(A) \leq w(B) - w(y) \end{cases} $$ Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно, $w(A \cup y) = w(A) + w(y) = w(B)$. Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \cup x \in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым. ## Примеры Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее. ### Минимальный остов Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом: * Граф без ребер является лесом. * Если удалить из леса ребра, он останется лесом. * Пусть есть два леса $|A| \leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \cup \{x\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности. Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова. ### Расписания Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград. Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \geq i$ для всех $i$. Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: * Пустой набор заданий всегда можно сделать. * Если у нас стало меньше заданий, то их сделать мы тоже успеем. * Пусть есть два правильных набора $|A| \leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \cup x$ — тоже правильный набор. Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных. ### Паросочетания Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом: * Любое паросочетание покрывает пустое множество вершин. * Исходное паросочетание покрывает также и любое подмножество исходных вершин. * Пусть есть два множества вершин $|A| \leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$. Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна. ### Линейно независимые вектора (TODO) (Школьники не обязаны знать линал.) Такие штуки будем называть базисами. * Ноль есть в любом базисе. * Подмножество базиса — базис. * ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше. + + + diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html new file mode 100644 index 0000000..9219954 --- /dev/null +++ b/ru/mincost-maxflow.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Поток минимальной стоимости Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна: $$ \sum_{(u, v) \in E} f_{uv} \to \max $$ $$ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min $$ Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет. Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$). ## Критерий оптимальности Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот). **Доказательство:** $\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению. $\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости. ### Отмена циклов Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу. Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли). ## Дополняющие пути Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости. **Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса. Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$. Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз. Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер. ## Потенциалы Джонсона Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами: $$ w_{uv}' = w_{uv} + d_u - d_v $$ **Утверждение 1**. Веса всех рёбер графа неотрицательные. **Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$? Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой: $$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$ **Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими. **Доказательство**. Распишем новую стоимость пути из $a$ в $z$. $$ \begin{align} w_{ab}' + \ldots + w_{yz}' &= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) \\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z \end{align} $$ Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу. Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути. Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа. **Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими). **Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2). ## Итоговый алгоритм * Модифицируем сеть, добавивив обратные рёбра. * Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю. * Пока максимальный поток не найден: * * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$. * * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \leadsto t$, обновить остаточную сеть. ### Асимптотика Алгоритм работает за $O(U m \log n)$ или $O(U n^2)$ в случае плотных графов. В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \leq n$ или $U \leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма. ### Реализация Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию). * `cost`, `cap` — параметры сети * `pot` — потенциалы * `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока) * `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния ```c++ const int maxn = 305, inf = 1e9; int n; int cost[maxn][maxn], cap[maxn][maxn]; int d[maxn], pot[maxn], par[maxn]; bool dijkstra (int s, int t) { used[maxn] = {0}; fill(d, d+n, inf); d[s] = 0; while (1) { int v = -1; for (int u = 0; u < n; u++) if (!used[u] && (v == -1 && d[u] < d[v])) v = u; if (v == -1 || d[v] == inf) break; used[v] = 1; for (int u = 0; u < n; u++) { int w = cost[v][u] + pot[v] - pot[u]; if (cap[v][u] && d[u] > d[v] + w) { d[u] = d[v] + w; par[u] = v; } } } return d[t] < inf; } int mincost_maxflow (int s, int t) { int ans = 0; while (dijkstra(s, t)) { memcpy(pot, d, sizeof(d)); int delta = inf; for (int v = t; v != s; v = par[v]) delta = min(delta, cap[par[v]][v]); for (int v = t; v != s; v = par[v]) { cap[par[v]][v] -= delta; cap[v][par[v]] += delta; ans += cost[par[v]][v]*delta; } } return ans; } ``` + + + diff --git a/ru/mst.html b/ru/mst.html new file mode 100644 index 0000000..f769f21 --- /dev/null +++ b/ru/mst.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Минимальные остовы > Авиакомпания содержит $m$ рейсов между $n$ городами, $i$-ый из них обходится в $w_i$ рублей, причём из любого города можно добраться до любого другого. В стране наступил кризис, и нужно отказаться от как можно большего числа из них таким образом, что содержание оставшиъся рейсов будет наиболее дешевым. Иными словами, нужно найти дерево минимального веса, которое является подграфом данного неориентированного графа. Такие деревья называют *остовами* (*каркас*, *скелет*; ударение на первый слог, но так мало кто произносит). По-английски — *minimum spanning tree* (дословно, минимальное покрывающее дерево). Почему дерево? Потому в противном случае там был бы цикл из которго можно удалить какое-то ребро и получить ответ пооптимальней. А если это больше, чем одно дерево, то какие-то две вершины остаются несвязаны. Вообще, следующие утверждения про деревья являются эквивалентными: * Граф — дерево. * В графе из $n$ вершин $n-1$ рёбер и нет циклов. * Из любой вершины можно дойти в любоую другую единственным образом. ## Лемма о безопасном ребре Назовем подграф $T$ графа $G$ безопасным, если они является подграфом какого-то минимального остова. Назовем ребро безопасным, если при добавлении его в подграф $T$ получившийся граф $T'$ тоже является безопасным, то есть подграфом какого-то минимального остова. Все алгоритмы для поиска минимального остова опираются на следующее утверждение: > Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным. **Доказательство**: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже. ![safe](https://neerc.ifmo.ru/wiki/images/thumb/8/81/%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png/300px-%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png) Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов. # Алгоритм Прима Минимальный остов строится постепенно, добавлением в него рёбер по одному. * Изначально остов — одна произвольная вершина. * Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден. Очень похоже на алгоритм Дейкстры, только мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё. Совсем наивная реализация за $O(nm)$ — каждый раз перебираем все рёбра: ```python const int maxn = 1e5, inf = 1e9; vector from, to, weight; bool used[maxn] // считать все рёбра в массивы used[0] = 1; for (int i = 0; i < n-1; i++) { int opt_w = inf, opt_from, opt_to; for (int j = 0; j < m; j++) if (opt_w > weight[j] && used[from[j]] && !used[to[j]]) opt_w = weight[j], opt_from = from[j], opt_to = to[j] used[opt_to] = 1; cout << opt_from << " " << opt_to << endl; } ``` Реализация за $O(n^2)$: ```python const int maxn = 1e5, inf = 1e9; bool used[maxn]; vector< pair > g[maxn]; int min_edge[maxn] = {inf}, best_edge[maxn]; min_edge[0] = 0; // ... for (int i = 0; i < n; i++) { int v = -1; for (int u = 0; u < n; j++) if (!used[u] && (v == -1 || min_edge[u] < min_edge[v])) v = u; used[v] = 1; if (v != 0) cout << v << " " << best_edge[v] << endl; for (auto e : g[v]) { int u = e.first, w = e.second; if (w < min_edge[u]) { min_edge[u] = w; best_edge[u] = v; } } } ``` Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за $O(m \log n)$: ```python set< pair > q; int d[maxn]; while (q.size()) { v = q.begin()->second; q.erase(q.begin()); for (auto e : g[v]) { int u = e.first, w = e.second; if (w < d[u]) { q.erase({d[u], u}); d[u] = w; q.insert({d[u], u}); } } } ``` Про алгоритм за $O(n^2)$ забывать не стоит — он работает лучше в случае плотных графов. # Алгоритм Крускала Будем добавлять рёбра в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить. Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Однако для этой проверки нам нужна будет целая отдельная структура. # Система непересекающихся множеств Эта структура данных предоставляет следующие возможности. Изначально имеется несколько элементов, каждый из которых находится в отдельном (своём собственном) множестве. Структура поддерживает две операции: * объединить два каких-либо множества * запросить, в каком множестве сейчас находится указанный элемент Обе операции будут выполняться в среднем почти за $O(1)$ (но не совсем — этот сложный вопрос будет разъяснен позже). Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив `_p`, в котором для каждого элемента мы храним номер его предка в дерева. Для корней деревьев будем считать, что их предок — они сами. Наивная реализация, которую мы потом ускорим: ```python int _p[maxn]; int p (int v) { if (_p[v] == v) return v; else return p(_p[v]); } void unite (int a, int b) { a = p(a), b = p(b); _p[a] = b; } for (int i = 0; i < n; i++) _p[i] = i; ``` **Эвристика сжатия пути**. Оптимизируем работу функции `p`. Давайте перед тем, как вернуть ответ, запишем его в `_p` от текущей вершины, то есть переподвесим его за самую высокую. Насколько лучше это сделает асимптотику? Выясняется, что $O(n \log n)$. Тут должен быть мем из опросов. **Ранговая эвристика**. Эта штука напрямую пытается минимизировать высоту дерева. Давайте делать переподвешивание за то, которое менее глубоко. Ну понятно, что тогда любое дерево будет не более логарифма. **Весовая эвристика**. Давайте каждый раз подвешивать за более крупное. Работать будет быстро, так как . Автор предпочитает именно её, потому что часто эти размеры компонент требуются сами по себе. Оказывается, что сжатия + ранговая или сжатия + весовая работает быстро. Асимптотика объединения обеих эвристик (сжатия путей и одной из ранговых) — O(a(n)), где a(n) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4). Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует. ```python int _p[maxn], s[maxn]; int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); } void unite (int a, int b) { a = p(a), b = p(b); if (s[a] > s[b]) swap(a, b); s[b] += s[a]; _p[a] = b; } for (int i = 0; i < n; i++) _p[i] = i; ``` ## Полезные свойства и классические задачи * Если веса всех рёбер различны, то остов будет уникален. * Минимальный остов является также и остовом с минимальным произведением весов рёбер (замените веса всех рёбер на их логарифмы) * Минимальный остов является также и остовом с минимальным весом самого тяжелого ребра. * Если вы решаете задачу, где ребра не добавляются, а удаляются, то можно попробовать решать задачу «с конца» и применить алгоритм Крускала. ## Персистентная СНМ* Как вы уже заметили, автор очень любит персистентные структуры данных. СНМ — тоже структура, и её тоже можно сделать персистентной. В СНМ мы изменяем массивы, а массивы можно сделать персистентными через персистентное ДО (только так, проще не получается — многие пытались). Здесь есть нюанс — амортизированные структуры не очень хорошо дружат с персистентностью. Поэтому нам придется отказаться от эвристики сжатия путей, и поэтому асимптотика составит $O(n \log^2 n)$ времени и памяти — один логарифм от СНМа, другой от персистентного ДО. ## Динамическая связность* *Dynamic Connectivity Problem:* > Даны $n$ запросов добавления ребра (`+`), удаления ребра (`-` и какого-то запроса про граф (`?`), например, о связности двух вершин. О решении этой задачи в online и в offline можете почитать в [этом посте](https://codeforces.com/blog/entry/15296). + + + diff --git a/ru/pollard.html b/ru/pollard.html new file mode 100644 index 0000000..b55cbbc --- /dev/null +++ b/ru/pollard.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Факторизация за $O(\sqrt[4]{n})$ Наука умеет раскладывать целые числа на множители за $O(n^\frac{1}{4})$. Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера. ## Парадокс дней рождений Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. $$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ Попытаемся оценить $f$: $$ \begin{align} \begin{aligned} e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ & = e^{-\frac{n(n-1)}{2d}} & \\ & \simeq e^{-\frac{n^2}{2d}} & \\ \end{aligned} \end{align} $$ Из формулы более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее: **Утверждение**. В мультимножество нужно добавить $O(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. Если вы не доверяете математике, посмотрите на графики: ```python import matplotlib.pyplot as plt %matplotlib inline import seaborn as sns sns.set() ``` ```python def f(n, d): p = 1 for x in range(1, n): p *= 1-x/d return p def g(n, d): return 2.71**(-0.5 * n**2 / d) ``` ```python exact = [f(n, 365) for n in range(40)] approx = [g(n, 365) for n in range(40)] plt.plot(exact) plt.plot(approx) plt.show() ``` ![png](pollard_files/pollard_7_0.png) На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать $n \approx \sqrt{d}$, построим такую же для больших чисел и перейдём в логарифмический масштаб. ```python approx = [g(n, 10**10) for n in range(10**6)] plt.xscale('log') plt.plot(approx) plt.show() ``` ![png](pollard_files/pollard_9_0.png) ## $\rho$-алгоритм Полларда Итак, мы хотим факторизовать число $n$. Предположим, что $n = p q$ и $p \approx q$. Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей. Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например $f(x) = (x+1)^2 \mod n$. Как выглядит граф, в котором из вершины есть единственное ребро $x \to f(x)$? Такой граф называется *функциональным*. Если в нём нарисовать «траекторию» произвольного элемента, — какой-то путь, превращающийся в цикл, — то получится что-то похожее на букву $\rho$ (ро). Алгоритм из-за этого так и назван. Рассмотрим траекторию какого-нибудь элемента $x_0$: {$x_0$, $f(x_0)$, $f(f(x_0))$, $\ldots$}. Теперь сделаем из неё новую последовательность, взяв каждый элемент по модулю $p$ — наименьшего из простых делителей $n$. **Утверждение**. Ожидаемая длина цикла в этой последовательности $O(\sqrt[4]{n})$. *Доказательство:* так как $p$ — меньший делитель, то $p \leq \sqrt{n}$. Теперь просто подставим в предыдущее утверждение: в множество нужно добавить $O(\sqrt{p}) = O(\sqrt[4]{n})$ элементов, чтобы какие-то два совпали, а значит последовательность зациклилась. Если мы найдём цикл в такой последовательности (то есть такие $i$ и $j$, что $f^i(x_0) \equiv f^j(x_0)$), то мы можем восстановить какой-то делитель $n$, а именно $\gcd(|f^i(x_0) - f^j(x_0)|, n)$ — это число меньше $n$ и делится на $p$. Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя $i$ и $j$ ($i = 2j$) и проверять, что $f^i(x_0) \equiv f^j(x_0) \pmod p$, что эквивалентно проверке $\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1$. ```python from math import gcd from random import randint ``` ```python def find_divisor(n, seed = 1, f = lambda x: (x+1)**2): x = y = seed d = 1 while d == 1: x = f(f(x)) % n y = f(y) % n d = gcd(abs(x-y), n) return d ``` ```python n = 3*5*7 # = 105 print([find_divisor(n, seed=i) for i in range(n)]) ``` [3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3] ```python n = (10**9+7)*(10**9+9) print([find_divisor(n, seed=randint(1, n)) for _ in range(20)]) ``` [1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009] Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за $O(\sqrt{n})$ (нужно добавить отсечение по времени). ## Разное Формально, мы показали, что алгоритм работает за $O(\sqrt[4]{n} \log n)$ за счёт поиска $\gcd$, но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет. Пародокс дней рождения также можно использовать при подсчёте вероятности коллизии при хэшировании. Хороший модуль должен быть не менее квадрата количества хэшей, а чего-то вроде $5 n^2$ уже почти всегда достаточно. + + + diff --git a/ru/probability.html b/ru/probability.html new file mode 100644 index 0000000..10d5968 --- /dev/null +++ b/ru/probability.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Ликбез по теорверу * Вероятностные распределения * Матожидание и дисперсия * Парадокс дней рождения * Нормальное распределение * Правдоподобие и оценка качества предсказания * Энтропия и оптимальное кодирование Эта статья представляет собой выжимку самых интересных фактов и «больших идей» теорвера, которые обычно рассказывают на курсах статистики, машинного обучения и теории информации. (Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.) ```python import numpy as np import matplotlib as plt %matplotlib inline import seaborn as sns sns.set() ``` ## Матожидание ## Дисперсия Какие два числа лучше всего описывают распределение? ## Нормальное распределение Центральная предельная теорема названа так пафосно вполне обоснованно. Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию. $$ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} $$ Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса. Трудно даже доказать, что это распределение, т. е. что $\int_{-\inf}^\inf f(x) dx = 1$. ## Применения Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный. Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как ## Линейные рекурренты Чтобы решать следующие задачи, нам нужно будет использовать следующий факт: ... Доказательство мы не приведем. В частности, таким образом получается формула для чисел Фибоначчи. $$ f_n = \ldots $$ Кто бы мог подумать, что все эти иррациональности и степени сократятся и вообще дадут целое число?.. ## Классика Парадокс дней рождения. Это на самом деле очень часто используемый результат. Так можно считать вероятность коллизии хэшей, а также он используется во многих теоретико-числовых алгоритмах, в которых используется предположения (весьма справедливые) о распределении простых чисел. Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт? TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. Казино. Мы приходим в казино с 1000\$ и следующим образом проводим там время: ставим по 1\$, пока не обанкротимся или не выиграем 1100\$. Какая вероятность того, что мы уйдём с деньгами? ## Принцип максимального правдоподобия ## Энтропия Энтропией называется минимальное число бит, которым теоретически возможно сжать сообщение. Эта величина важна, потому что на практике если её можно посчитать, то сжатие с соответствующей кратностью реально достижимо. Шумный канал. Пусть у вас есть 1тб данных и два китайских терабайтника, на каждый из которых можно записать столько данных, но каждый бит имеет вероятность 10% записаться на противоположный. Требуется сохранить данные с первого раза без потерь. Совсем без потерь. Причём это делается почти впритык. + + + diff --git a/ru/reciprocal.html b/ru/reciprocal.html new file mode 100644 index 0000000..9ffaef7 --- /dev/null +++ b/ru/reciprocal.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Обратный элемент по модулю Часто в задачах требуется посчитать что-то по простому модулю (чаще всего $10^9 + 7$). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче. Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например: ```c++ c = (a + b) % mod; c = (mod + a - b) % mod; c = a * b % mod; ``` Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: $\frac{8}{2} = 4$, но $\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4$. Нужно найти некоторый элемент, который будет себя вести как $\frac{1}{a} = a^{-1}$, и вместо «деления» домножать на него. Назовем такой элемент обратным. ## Способ 1: бинарное возведение в степень Если модуль $p$ простой, то решением будет $a^{-1} \equiv a^{p-2}$. Это следует из малой теоремы Ферма: **Теорема.** $a^p \equiv a \pmod p$ для всех $a$, не делящихся на $p$. **Доказательство**. (для понимания несущественно, можно пропустить) $$ \begin{align} a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p \\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} \\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} \\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} \\ &= a \end{align} $$ Здесь $P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}$ это мультиномиальный коеффициент — количество раз, которое элемент $a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}$ появится при раскрытии скобки $(a_1 + a_2 + \ldots + a_n)^k$. Теперь два раза «поделим» наш результат на $a$. $$ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} $$ Получается, что $a^{p-2}$ ведет себя как $a^{-1}$, что нам по сути и нужно. Посчитать $a^{p-2}$ можно за $O(\log p)$ бинарным возведением в степень. Приведем код, который позволяет считает $C_n^k$. ```c++ int t[maxn]; // факториалы, можно предподситать простым циклом // бинарное возведение в степень int bp (int a, int n) { int res = 1; while (n) { if (n & 1) res = res * a % mod; a = a * a % mod; n >>= 1; } return res; } // находит обратный элемент как a^(p-2) int inv (int x) { return bp(x, mod-2); } int c (int n, int k) { return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod; } ``` ## Способ 2: диофантово уравнение Диофантовыми уравнениями называют такие штуки: $$ ax + by = 1 $$ Требуется решить их в целых числах, то есть $a$ и $b$ известны, и нужно найти такие целые (возможно, отрицательные) $x$ и $y$, чтобы равенство выполнялось. Решают такие вещи расширенным алгоритмом Евклида. TODO: описать, как он работает. Подставим в качестве $a$ и $b$ соответственно $a$ и $m$ $$ ax + my = 1 $$ Одним из решений уравнения и будет $a^{-1}$, потому что если взять уравнение по модулю $m$, то получим $$ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m $$ Преимущества этого метода над возведением в степень: * Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. * Алгоритм проще выполнять руками. Сам автор почти всегда использует возведение в степень. ## Почему $10^9+7$? 1. Это выражение довольно легко вбивать (`1e9+7`). 2. Простое число. 3. Достаточно большое. 4. `int` не переполняется при сложении. 5. `long long` не переполняется при умножении. Кстати, $10^9 + 9$ обладает теми же свойствами. Иногда используют и его. ## Предподсчёт обратных факториалов за линейное время Пусть нам нужно зачем-то посчитать все те же $C_n^k$, но для больших $n$ и $k$, поэтому асимптотика $O(n \log m)$ нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам. Если у нас уже написан `inv`, то нам не жалко потратить $O(\log m)$ операций, посчитав $m!^{-1}$. После этого мы будем считать $(m-1)!^{-1}$ как $m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}$. ```c++ int f[maxn]; f[0] = 1; for (int i = 1; i < maxn; i++) f[i] = i*f[i-1] % mod; int r[maxn]; r[maxn-1] = inv(f[maxn-1]) for (int i = maxn-1; i >= 1; i--) r[i-1] = r[i]*i % mod; ``` TODO: техника с сайта емакса. + + + diff --git a/ru/segtree.html b/ru/segtree.html new file mode 100644 index 0000000..e515b68 --- /dev/null +++ b/ru/segtree.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Дерево отрезков **Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля. **Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках. Рассмотрим конкретную задачу: >Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов: >1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`). >2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$. >Оба запроса нужно обрабатывать за время $O(\log n)$. Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции: Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1. Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков». ![alt text](http://i.imgur.com/GGBmcEP.png) Строить его можно рекурсивной функцией: * Если вершина является листом, взять в качестве суммы значение соответствующей ячейки. * Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей. ## Разные свойства Высота такого дерева есть величина $\Theta(\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики. Более того, любой полуинтервал разбивается на $O(\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков. Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} + \ldots + 1 < 2n$. Значит, оно линейное по памяти. При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом. ## Ок, как это нам поможет? Опишем теперь, как с помощью такой структуры решить задачу. **Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$. Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\Theta(\log n)$ — по одной с каждого уровня. Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков. **Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения. Сделаем тоже рекурсивную функцию, рассмотрев три случая: * Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму. * Если отрезки вершины и запроса не пересекаются, то вернуть 0. * Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей. Чтобы разобраться, почему это работает за $O(\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\log n)$ отрезках, а значит и итоговая асимптотика будет такая же. ## Ликбез по C++ Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества. Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел. Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\log n)$. В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса. ```c++ struct A { int param1, param2; // тут можно что-то хранить char param3 = 'k'; A (int var) { // эта часть называется конструктором // ... } void do_something () { // это какой-то другой метод // ... } }; // <- не забудьте точку с запятой ``` Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве. Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется: ```c++ A x(179); x.do_something(); x.param1 = 57; A *y = new A(42); // new возвращает адрес, по которому можно найти объект y->do_something(); y.param3 = '!'; ``` Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались ## Реализация Общий план реализации любых структур данных: 1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc. 2. Формально описать, что должны делать методы и за какую асимптотику. 3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты. ```c++ struct segtree { int lb, rb; // левые и правые границы отрезков int sum = 0; // сумма на текущем отрезке segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { // если не лист, создаем детей int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) // если мы лежим полностью в отрезке запроса, вывести сумму return sum; if (max(lb, lq) >= min(rb, rq)) // если мы не пересекаемся с отрезком запроса, вывести ноль return 0; // иначе всё сложно -- запускаемся от детей и пусть они там сами решают return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` > Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$). Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи: * Запросим сумму от $k$ до $n$ в ДО. * Добавим единичку в $k$-тую позицию в ДО. Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \log n)$ запросов. > Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике. Ответим на все запросы в оффлайн, используя метод сканирующей прямой: * Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \times [y_1, y_2]$. * Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше. * Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$». ## Отложенные операции Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному. Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем. Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено. Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся. Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \log n)$. При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны. ```c++ struct segtree { int lb, rb; int sum = 0, assign = -1; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void push () { if (assign != -1) { sum = (rb-lb) * assign; if (l) { // если дети есть l->assign = assign; r->assign = assign; } } assign = -1; } void upd (int lq, int rq, int x) { push(); if (lq <= lb && rb <= rq) assign = x; else if (l && max(lb, lq) < min(rb, rq)) { // если есть дети и отрезок запроса хоть как-то пересекается с нашим l->upd(lq, rq, x); r->upd(lq, rq, x); // ...дальше они сами разберутся } } int get_sum (int lq, int rq) { push(); if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах. Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни. ## Динамическое построение А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\log_2 10^6 \approx 20$, $\log_2 10^9 \approx 30$), кроме этапа построения. Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation. Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так. ```c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; // а тут ничего нет } void extend () { if (!l && lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { extend(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; extend(); return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так: ```c++ vector compress (vector a) { vector b = a; sort(b.begin(), b.end()); b.erase(unique(b.begin(), b.end()), b.end()); for (int &x : a) x = int(lower_bound(b.begin(), b.end(), x) - b.begin()); return a; } ``` ## Персистентность Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния. Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается. У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \log n)$. ```c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb != rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void copy () { if (l) { l = new segtree(l); r = new segtree(r); } } void add (int k, int x) { copy(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { // этот метод ничего не меняет -- он и так хороший if (lq <= lb && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` > Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике. Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \log n)$ памяти. > Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке. Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу. Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`. Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом. > Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\frac{1}{2^{100}}$, и ей на практике можно пренебречь. Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`. ## Другие реализации Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы: **На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree). **«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051). ## Задачи * [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции. * Второй контест — на отложенные операции. * [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность. + + + diff --git a/ru/sparse-table.html b/ru/sparse-table.html new file mode 100644 index 0000000..7259f55 --- /dev/null +++ b/ru/sparse-table.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Разреженная таблица - Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \log n)$ с малой константой. - Обновления не поддерживает (static RMQ). - Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья. - Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос). - Требует $O(n \log n)$ памяти. - Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ. Определим разреженную таблицу как двумерный массив размера $n \times\log n$: $$ t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} $$ Идея такая: считаем минимум на каждом отрезке длины $2^k$. Такой массив можно посчитать за его размер: $t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования. Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам. ![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png) Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов. ```c++ int a[maxn], lg[maxn], mx[maxn][logn]; int rmq (int l, int r) { int t = lg[r-l+1]; return min(mx[l][t], mx[r-(1<= 0; i--) { mx[i][0] = a[i]; for (int l = 0; l < logn-1; l++) mx[i][l+1] = max(mx[i][l], mx[i+(1< + + + diff --git a/ru/sse.html b/ru/sse.html new file mode 100644 index 0000000..429b4db --- /dev/null +++ b/ru/sse.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Streaming SIMD Extensions This is ``` processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 945.615 cache size : 9216 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: ``` ```c++ !cat /proc/cpuinfo | less -20 ``` processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3422.395 cache size : 9216 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 1 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3702.863 cache size : 9216 KB physical id : 0 siblings : 12 core id : 1 cpu cores : 6 apicid : 2 initial apicid : 2 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 2 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3556.265 cache size : 9216 KB physical id : 0 siblings : 12 core id : 2 cpu cores : 6 apicid : 4 initial apicid : 4 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 3 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3512.268 cache size : 9216 KB physical id : 0 siblings : 12 core id : 3 cpu cores : 6 apicid : 6 initial apicid : 6 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 4 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3642.787 cache size : 9216 KB physical id : 0 siblings : 12 core id : 4 cpu cores : 6 apicid : 8 initial apicid : 8 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 5 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3136.167 cache size : 9216 KB physical id : 0 siblings : 12 core id : 5 cpu cores : 6 apicid : 10 initial apicid : 10 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 6 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3535.684 cache size : 9216 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 1 initial apicid : 1 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 7 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 2251.174 cache size : 9216 KB physical id : 0 siblings : 12 core id : 1 cpu cores : 6 apicid : 3 initial apicid : 3 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 8 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 2202.563 cache size : 9216 KB physical id : 0 siblings : 12 core id : 2 cpu cores : 6 apicid : 5 initial apicid : 5 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 9 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 2413.537 cache size : 9216 KB physical id : 0 siblings : 12 core id : 3 cpu cores : 6 apicid : 7 initial apicid : 7 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 10 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 1974.963 cache size : 9216 KB physical id : 0 siblings : 12 core id : 4 cpu cores : 6 apicid : 9 initial apicid : 9 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 11 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3336.891 cache size : 9216 KB physical id : 0 siblings : 12 core id : 5 cpu cores : 6 apicid : 11 initial apicid : 11 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: + + + diff --git a/ru/stress-test.html b/ru/stress-test.html new file mode 100644 index 0000000..ca8932a --- /dev/null +++ b/ru/stress-test.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Стресс-тестирование * Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений * Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написанно решение на маленькие подгруппы Суть такая: * Есть решение `smart` — быстрое, но в котором есть баг, который хотим найти * Пишем решение `stupid` — медленное, но точно корректное * Пишем генератор `gen` — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт `checker`, который n раз генерирует тест, даёт его на ввод `stupid`-у и `smart`-у, сравнивает выводы и останавливается, когда они отличаются Примерный код скрипта: ```python import os, sys f1, f2, gen, iters = sys.argv for i in range(int(iters)): print('Test', i+1) os.popen('python3 %s > test.txt' % gen) v1 = os.popen('./%s < test.txt' % f1).read() v2 = os.popen('./%s < test.txt' % f2).read() if v1 != v2: print test print("Correct:") print v1 print("Wrong:") print v2 break ``` Автор обычно запускает его командой `python3 checker.py stupid smart gen.py 100`, предварительно скомпилировав `stupid` и `smart` в ту же директорию, что и сам `checker.py`. Скрипт написан под Linux. Для Windows нужно убрать «`./`» во всех системных вызовах. `gen.py` автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через `./gen`. Пример `gen`-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10: ```python from random import randint, choice n = randint(1, 10) print(n) for _ in range(n): print(choice('abc'), end='') ``` + + + diff --git a/ru/strings.html b/ru/strings.html new file mode 100644 index 0000000..a9398c0 --- /dev/null +++ b/ru/strings.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Базовые строковые алгоритмы ## Префикс-функция Рассмотрим задачу, которая возникает каждый раз, когда вы делаете `ctrl+f`: > Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него. Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго. Для решения этой задачи за линейное время придумали **префикс-функцию**. **Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \ldots s_i$, который также является и суффиксом этой строки (не считая всю строку). Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$. ```python def slow_prefix_function(s): n = len(s) p = [0]*n for i in range(n): prefix = s[:i] for l in range(1, i): if prefix[:l] == prefix[-l:]: p[i] = l return p slow_prefix_function('aataataa') ``` [0, 0, 1, 0, 1, 2, 3, 4] (Этот алгоритм работает за $O(n^3)$, но это только пока.) ## Как это поможет решить исходную задачу? Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке. Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\#t$. ```python s = "let it go" t = """let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!""" print((s + '#' + t).replace('\n', ' ')) print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)])) ``` let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door! 00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000 Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$. Такой алгоритм (посчитать префикс-функцию от $s\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**. ## Как её быстро считать Рассмотрим ещё несколько примеров префикс-функций: ```python for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']: print(slow_prefix_function(s)) ``` [0, 0, 1, 2, 3] [0, 0, 0, 0, 0, 0] [0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0] [0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] Можно заметить несколько особенностей: * $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; * $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \ldots s_i$, длина которого будет ровно на единицу меньше. Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения. Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\underbrace{aabaa}t\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$. Но что происходит, когда $s_{p_i}\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$. * $\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. * $\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa». * $\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$. * $\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$. В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым. ```python def fast_prefix_function(s): n = len(s) p = [0]*n for i in range(1, n): cur = p[i - 1] # перебираем префикс-функцию, пока не найдем равный символ while s[i] != s[cur] and cur > 0: cur = p[cur - 1] # если нашли, то значение на единицу больше if s[i] == s[cur]: p[i] = cur + 1 return p fast_prefix_function('abacabadabacabax') ``` [0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0] ### Асимптотика Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой. **Доказательство**: * Как мы ранее заметили, префикс функция возрастает максимум на единицу. * $\implies$ Вырасти она может максимум $n-1$ раз. * Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один. * $\implies$ Количество понижений не больше количества повышений, то есть $O(n)$. * $\implies$ Суммарно итераций цикла `while` — $O(n)$. ## Z-функция Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания. Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$. $$\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)$$ ```python def slow_z_function(s): n = len(s) z = [0]*n for i in range(1, n): suffix = s[i:] for l in range(1, i): if s[:l] == suffix[:l]: z[i] = l return z for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']: print(slow_z_function(s)) ``` [0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1] [0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1] [0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0] Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$. ## Как её быстро считать Заметим, что: * $z_0 = 0$ — из-за договоренности (потому что информации не несет); * $z_i = 0 \iff s_i \neq s_0$; * $z_i > 0 \iff s_i = s_0$. Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$. Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. * Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком. * Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку. ```python def fast_z_function(s): n = len(s) z = [0]*n l = 0 r = 0 for i in range(1, n): if i <= r: z[i] = min(r - i + 1, z[i-l]) while i + z[i] < n and s[z[i]] == s[i+z[i]]: z[i] += 1 if i + z[i] - 1 > r: l = i r = i + z[i] - 1 return z ``` **Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$. ## Зачем тогда люди используют префикс-функцию ![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg) В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов. Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение. + + + diff --git a/ru/suffix-array.html b/ru/suffix-array.html new file mode 100644 index 0000000..891a579 --- /dev/null +++ b/ru/suffix-array.html @@ -0,0 +1,17 @@ + + + + + + + + + + + +# Суффиксный массив Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике. Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём. * +«Паблик с тупыми шутками про проганье» +* ## Мотивация **Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки. **Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно. В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс. Работать такой алгоритм будет за $O(|t| \log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \log |s|)$, что является одним из самых оптимальных алгоритмов поиска. Теперь научимся его строить. ## Построение за $O(n \log n)$ Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара. Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \log^2 n)$. Но это не самый быстрый и удобный алгоритм. Наш алгоритм будет состоять из $\lceil \log n \rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив. Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов). Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4. $$ p_0 = (0, 1, 3, 2) \;\;\; c_0 = (0, 0, 1, 0) \\ p_1 = (0, 3, 1, 2) \;\;\; c_1 = (0, 1, 2, 0) \\ p_2 = (3, 0, 1, 2) \;\;\; c_2 = (1, 2, 3, 0) $$ Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом. Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором. Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \log^2 n)$, потому что каждый этап будет работать за $O(n \log n$). **Оптимизация до $O(n \log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$. ```c++ // строка -- это последовательность чисел от 1 до размера алфавита vector suffix_array (vector &s) { s.push_back(0); // добавляем нулевой символ в конец строки int n = (int) s.size(), cnt = 0, // вспомогательная переменная: счётчик для сортировки cls = 0; // количество классов эквивалентности vector c(n), p(n); map< int, vector > t; for (int i = 0; i < n; i++) t[s[i]].push_back(i); // «нулевой» этап for (auto &x : t) { for (int u : x.second) c[u] = cls, p[cnt++] = u; cls++; } // пока все суффиксы не стали уникальными for (int l = 1; cls < n; l++) { vector< vector > a(cls); // массив для сортировки подсчётом vector _c(n); // новые классы эквивалентности int d = (1<(p.begin()+1, p.end()); } ``` TODO: переписать это ## Наибольшие общие префиксы Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами. TODO + + + diff --git a/ru/treap.html b/ru/treap.html new file mode 100644 index 0000000..3cf38ef --- /dev/null +++ b/ru/treap.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Декартово дерево Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века. Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим. Декартово дерево же определяется и строится так: * Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*. * Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*. * От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня. * Аналогично, запустимся от правой части и добавим корню правого сына. Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно. Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх: ![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png) Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий: * Дерамида (дерево + пирамида) * ПиВо (пирамида + дерево) * КуРево (куча + дерево) * Treap (tree + heap) ## Бинарные деревья С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например: * добавить число $x$ в множество * определить, есть ли в множестве число $x$ * найти первое число, не меньшее $x$ (`lower_bound`) * найти количество чисел в промежутке $[l, r]$ При этом все операции — за $O(\log n)$. На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\log n)$. ## Приоритеты и асимптотика В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска. **Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \log n)$. Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить. **Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*. Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна $$d_i = \sum_{j=1}^n a(j, i)$$ Её матожидание равно $$E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)$$ где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно. Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение. **Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$). **Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. **Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет. У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей: $$p(x, y) = \frac{1}{y-x+1}$$ Теперь, чтобы найти матожидание, эти вероятности надо просуммировать: $$E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)$$ Перед последним переходом мы получили сумму гармонического ряда. Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя. **Упражнение***. Выведите из этого доказательства асимптотику `quicksort`. ## Реализация Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте. Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву. ```c++ struct Node { int key, prior; Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } }; ``` Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся. ### Merge Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча. Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше. Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына. ```c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; if (l->prior > r->prior) { l->r = merge(l->r, r); return l; } else { r->l = merge(l, r->l); return r; } } ``` ### Split Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$. В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо: ```c++ typedef pair Pair; Pair split (Node *p, int x) { if (!p) return {0, 0}; if (p->key <= x) { Pair q = split(p->r, x); p->r = q.first; return {p, q.second}; } else { Pair q = split(p->l, x); p->l = q.second; return {q.first, p}; } } ``` ### Пример: вставка `merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. Вот так, например, будет выглядеть код, добавляющий $x$ в сет. ```c++ Node *root = 0; void insert (int x) { Pair q = split(root, x); Node *t = new Node(x); root = merge(q.first, merge(t, q.second)); } ``` ### Пример: модификация для суммы на отрезке Иногда нам нужно написать какие-то модификации для более продвинутых операций. Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке». ```c++ struct Node { int val, sum; // ... }; ``` При `merge` и `split` надо будет поддерживать эту сумму актуальной. Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины. ```c++ void sum (Node* v) { return v ? v->sum : 0; } // обращаться по пустому указателю нельзя -- выдаст ошибку void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; } ``` В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается: ```c++ Node* merge (Node *l, Node *r) { // ... if (...) { l->r = merge(l->r, r); upd(l); return l; } else { // ... } } ``` ```c++ typedef pair Pair; Pair split (Node *p, int x) { // ... if (...) { // ... upd(p); return {p, q.second}; } else { // ... } } ``` Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму: ```c++ int sum (int l, int r) { Pair rq = split(root, r); Pair lq = split(rq.first, l); int res = sum(lr.second); root = merge(lq.first, merge(lq.second, rq.second)); return res; } ``` # Неявный ключ Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку. Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными. Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева: ```c++ struct Node { int key, prior, size = 1; // ^ размер поддерева Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } }; ``` Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины. ```c++ int size (Node *v) { return v ? v->size : 0; } void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); } ``` `merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа. Про `split` теперь удобнее думать как "вырежи первые `k` элементов". ```c++ typedef pair Pair; Pair split (Node *p, int k) { if (!p) return {0, 0}; if (size(p->l) + 1 <= k) { Pair q = split(p->r, k - size(p->l) - 1); // ^ правый сын не знает количество вершин слева от него p->r = q.first; upd(p); return {p, q.second}; } else { Pair q = split(p->l, k); p->l = q.second; upd(p); return {q.first, p}; } } ``` Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно. ### Пример: ctrl+x, ctrl+v ```c++ Node* ctrlx (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l); root = merge(q2.first, q1.second); return q2.second; } ``` ```c++ void ctrlv (Node *v, int k) { Pair q = split(root, k); root = merge(q.first, merge(v, q.second)); } ``` ### Пример: переворот Нужно за $O(\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д. Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут: ```c++ struct Node { bool rev; // ... }; ``` Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку: ```c++ void push (node *v) { if (v->rev) { swap(v->l, v->r); if (v->l) v->rev ^= 1; if (v->r) v->rev ^= 1; } v->rev = 0; } ``` Аналогично, эту функцию будем вызывать в начале `merge` и `split`. Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг. ```c++ void reverse (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second->rev ^= 1; root = merge(q2.first, merge(q2.second, q1.second)); } ``` # Функциональное программирование* Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно. Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке. ```c++ auto apply (int l, int r, auto f) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second = f(q2.second); root = merge(q2.first, merge(q2.second, q1.second)); } void reverse (Node *v) { if (v) v->rev ^= 1; } ``` Применять её нужно так: ```c++ apply(l, r, reverse); ``` Это работает в плюсах, начиная с `g++14`. Для простых операций можно даже написать лямбду: ```c++ apply(l, r, [](Node *v){ if (v) v->rev ^= 1; }); ``` # Персистентность* Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи. > Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции. Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим. > Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**. Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами. Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ. Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`: ```c++ Node* copy (Node *v) { return new Node(*v); } ``` Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так: ```c++ Pair split (Node *p, int x) { p = copy(p); // ... } ``` В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию. У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\frac{L}{L+R}$, иначе за правое. **Теорема**. Такое переподвешивание эквивалентно приоритетам. **Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции: * Лист имеет вероятность 1 быть корнем себя (база индукции) * Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}$. С вершинами правого дерева аналогично. Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД. ```c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; l = copy(l), r = copy(r); if (rand() % (size(l) + size(r)) < size(l)) { // ... } else { // ... } } ``` Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$? + + + diff --git a/ru/trie.html b/ru/trie.html new file mode 100644 index 0000000..f6ff625 --- /dev/null +++ b/ru/trie.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +# Бор Бор — это структура данных для компактного хранения строк. Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. ![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png) Бор можно удобно использовать для разных задач: * Хранение строк — занимает гораздо меньше места, чем массив или сет строк. * Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке. * Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи. ## Реализация Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: * терминальная ли вершина, * ссылки на детей, * возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет. ```c++ const int k = 26; struct Vertex { Vertex* to[k] = {0}; bool terminal = 0; }; Vertex *root = new Vertex(); ``` Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной. ```c++ void add_string (string &s) { v = root; for (char c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(); v = v->to[c]; } v->terminal = true; } ``` Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть. Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности. ### Как хранить ссылки Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми. В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например. Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями. ## Суффиксные ссылки > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). + + + From f8868e1fa7a8285db5edd62ec3144ecb26be8ac2 Mon Sep 17 00:00:00 2001 From: Sergey Slotin Date: Thu, 6 Jun 2019 22:48:04 +0300 Subject: [PATCH 012/230] Create CNAME --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..0838f43 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +algorithmica.org \ No newline at end of file From b2204e0f1069b4ee338a45ea6105cb192787231a Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 6 Jun 2019 20:18:18 +0000 Subject: [PATCH 013/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- CNAME | 1 - ru/index.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 CNAME diff --git a/CNAME b/CNAME deleted file mode 100644 index 0838f43..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -algorithmica.org \ No newline at end of file diff --git a/ru/index.html b/ru/index.html index 750277c..a93ada9 100644 --- a/ru/index.html +++ b/ru/index.html @@ -9,7 +9,7 @@ -# Алгоритмика Здесь (временно) живут конспекты занятий, которые я последние полтора года готовил для разных курсов и школ — в основном, для второго курса [Tinkoff Generation](http://algocode.ru/b/). Более-менее готово: * [Дерево отрезков](http://sereja.me/a/segtree): на указателях, динамическое, пересистентное * [Декартово дерево](http://sereja.me/a/treap): явное, неявное, персистентое * [Полиномиальное хэширование](http://sereja.me/a/hashing) в задачах на строки * [Центроидная декомпозиция](http://sereja.me/a/centroid) * [Heavy-light декомпозиция](http://sereja.me/a/hld) * [Паросочетания](http://sereja.me/a/matching): алгоритм Куна, покрытие DAG-а, лемма Холла * [Дерево Фенвика](http://sereja.me/a/fenwick): устройство, многомерное дерево, бинпоиск по дереву * [Матроиды](http://sereja.me/a/matroid): алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость * [Ро-алгоритм Полларда](http://sereja.me/a/pollard) * [Sparse table](http://sereja.me/a/sparse-table) * [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow): критерий оптимальности, потенциалы Джонсона * [Битовое сжатие](http://sereja.me/a/bitset): битсет, нахождение цикла длины 3, перемножение матриц, метод Гаусса * [Наименьший общий предок](http://sereja.me/a/lca): свойства tin и tout, двоичные подъемы, сведение к RMQ, алгоритм Фараха-Колтона и Бендера * [Классные задачки](http://sereja.me/a/bayans): пока что без решений Дальше, в некоторых местах могут быть странно структурированные или незавершенные предложения. Это нормально. Не очень готово: * [Поиск строки в строке](http://sereja.me/a/strings): префикс-функция, z-функция * [Суффиксный массив](http://sereja.me/a/suffix-array) * [Геометрия](http://sereja.me/a/geometry): скалярное и векторное произведение, пересечение прямых, классы в C++ * [Оптимизации ДП](http://sereja.me/a/dp-optimizations): Кнут, разделяй-и-властвуй, Convex Hull Trick * [Остовные деревья](http://sereja.me/a/mst): алгоритм Прима и Крускала * [Обратное по модулю](http://sereja.me/a/reciprocal) * [Метод отжига](http://sereja.me/a/annealing) * [Стресс-тестирование](http://sereja.me/a/stress-test) * [Бор](http://sereja.me/a/trie) * [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько * Выпуклые оболочки * [Линейная алгебра](http://sereja.me/a/linalg): линейные операторы, матрицы, применения к динамике, метод Гаусса * CUDA: программирование на GPU (на английском) Вообще не готово: * [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема * [Ахо-Корасик](http://sereja.me/a/aho-corasick) * [Теорвер](http://sereja.me/a/probability) * Корневая декомпозиция * Теория информации * Функции потерь в машинном обучении * Память: кэширование, локальность, префетчинг, cache-oblivious алгоритмы (на английском) * Streaming SIMD Extensions: ассемблер, ускоряем программы в 8 раз (на английском) Спасибо Косте Амеличеву, Глебу Лобанову, Ване Клигунову, Диме Дубровину, Паше Колесникову, Маше Тряпицыной, Шелхонову Булату, Саше Мамаеву, Андрею Гаркавому, Тёме Рябову, Андрею Чулкову, Максу Деб Натху и много кому ещё за фидбэк и указания на ошибки и неточности. +# Алгоритмика ### Структуры данных * [Дерево отрезков](http://sereja.me/a/segtree) отложенные операции, динамическое, персистентное * [Декартово дерево](http://sereja.me/a/treap) treap, дерамида, неявный ключ, персистентное * [Дерево Фенвика](http://sereja.me/a/fenwick) многомерное дерево Фенвика, бинпоиск по дереву Фенвика * [Разреженная таблица](http://sereja.me/a/sparse-table) sparse table, static RMQ * [Битовое сжатие](http://sereja.me/a/bitset) битовые операции, std::bitset, перемножение матриц, метод Гаусса ### Общие техники * Корневая декомпозиция: она где-то есть * [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема ### Дискретная математика * [Модулярная арифметика](http://sereja.me/a/reciprocal) теорема Ферма, нахождение обратного по модулю, бинарное возведение в степень, диофантово уравнение, применения в комбинаторике, предподсчёт обратных факториалов за линейное время * [Ро-алгоритм Полларда](http://sereja.me/a/pollard) парадокс дней рождений, факторизация за $O(\sqrt[4] n)$ * [Матроиды](http://sereja.me/a/matroid) алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость ### Графы * [Остовные деревья](http://sereja.me/a/mst) алгоритм Прима, алгоритм Крускала * [Паросочетания]([http://sereja.me/a/matching](http://sereja.me/a/matching) алгоритм Куна, покрытие ациклического орграфа, лемма Холла ### Потоки * [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow) критерий оптимальности, отмена потока, потенциалы Джонсона, «дейкстра с потенциалами» ### Деревья * [Наименьший общий предок]([http://sereja.me/a/lca](http://sereja.me/a/lca) LCA, двоичные подъемы, сведение LCA к RMQ, алгоритм Фараха-Колтона и Бендера * [Центроидная декомпозиция](http://sereja.me/a/centroid) * [Heavy-light декомпозиция](http://sereja.me/a/hld) ### Строки * [Полиномиальное хэширование](http://sereja.me/a/hashing) хэши, парадокс дней рождений, хранение строк в декартовом дереве * [Поиск строки в строке](http://sereja.me/a/strings) префикс-функция, z-функция * [Бор](http://sereja.me/a/trie) * [Ахо-Корасик](http://sereja.me/a/aho-corasick) * [Суффиксный массив](http://sereja.me/a/suffix-array) построение за $O(n \log n)$, LCP ### Динамическое программирование * [Пересчёт динамики по слоям](http://sereja.me/a/dp-optimizations) оптимизация Кнута, разделяй-и-властвуй, Convex Hull Trick, дискретный метод Лагранжа ### Оптимизация * [Метод отжига](http://sereja.me/a/annealing) задача о ферзях ### Разное * [Стресс-тестирование](http://sereja.me/a/stress-test) ### Теория игр * [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько ### Высшая математика * [Линейная алгебра](http://sereja.me/a/linalg) линейные операторы, матрицы, применения к динамике, метод Гаусса * [Теорвер](http://sereja.me/a/probability) ### Геометрия * [Ликбез по вычислительной геометрии](http://sereja.me/a/geometry) скалярное и векторное произведение, пересечение прямых, классы в C++ * Выпуклые оболочки: они где-то есть From 0a36343fff97865585c903e6cfc297ef4f384987 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 6 Jun 2019 20:25:50 +0000 Subject: [PATCH 014/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..036ff89 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +algorithmica.org From b31d3a8dd6fe76161d4e95d07e832579c7e4e770 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 6 Jun 2019 20:25:55 +0000 Subject: [PATCH 015/230] Deploy algorithmica-org/articles to github.com/algorithmica-org/articles.git:gh-pages --- CNAME | 1 - 1 file changed, 1 deletion(-) delete mode 100644 CNAME diff --git a/CNAME b/CNAME deleted file mode 100644 index 036ff89..0000000 --- a/CNAME +++ /dev/null @@ -1 +0,0 @@ -algorithmica.org From ae1b83b06a29d3fea6a64fa8dd5fb18bb2dc8d52 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 6 Jun 2019 20:28:18 +0000 Subject: [PATCH 016/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 CNAME diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..036ff89 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +algorithmica.org From 5b77270de648320a0d59d2c02dc96774e49340da Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 7 Jun 2019 12:37:11 +0000 Subject: [PATCH 017/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/index.html | 2 ++ ru/aho-corasick.html | 4 +++- ru/annealing.html | 2 ++ ru/bayans.html | 2 ++ ru/bitset.html | 2 ++ ru/centroid.html | 4 +++- ru/convex-hulls.html | 2 ++ ru/cross-entropy.html | 2 ++ ru/cuda.html | 9 +++++++-- ru/dp-intro.html | 2 ++ ru/dp-optimizations.html | 2 ++ ru/dp_intro.html | 2 ++ ru/dp_optimizations.html | 2 ++ ru/fenwick.html | 8 +++++++- ru/games.html | 4 +++- ru/geometry.html | 4 +++- ru/hashing.html | 2 ++ ru/hld.html | 4 +++- ru/index.html | 2 ++ ru/ioi.html | 2 ++ ru/karatsuba.html | 4 +++- ru/lca.html | 4 +++- ru/linalg.html | 5 ++++- ru/matching.html | 4 +++- ru/matroid.html | 2 ++ ru/mincost-maxflow.html | 2 ++ ru/mst.html | 2 ++ ru/pollard.html | 2 ++ ru/probability.html | 2 ++ ru/reciprocal.html | 2 ++ ru/segtree.html | 2 ++ ru/sparse-table.html | 2 ++ ru/sse.html | 2 ++ ru/stress-test.html | 2 ++ ru/strings.html | 2 ++ ru/suffix-array.html | 4 +++- ru/treap.html | 2 ++ ru/trie.html | 2 ++ 38 files changed, 97 insertions(+), 13 deletions(-) diff --git a/en/index.html b/en/index.html index ee802f9..3865303 100644 --- a/en/index.html +++ b/en/index.html @@ -10,6 +10,8 @@ some english index +
+
diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html index 5fb77e1..761ae55 100644 --- a/ru/aho-corasick.html +++ b/ru/aho-corasick.html @@ -9,7 +9,9 @@ -# Алгоритм Ахо-Корасик Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время. Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик. Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы. > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). ## Зачем это нужно > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс. ## Алгоритм Ахо-Корасик* Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой. **Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход. **Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда. ```c++ const int k = 26; struct Vertex { Vertex *to[k] = {0}, *go[k] = {0}; Vertex *link = 0, *p; int pch; Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; } }; Vertex *root = new Vertex(-1, 0); ``` ```c++ void add_string (string s) { Vertex *v = root; for (char _c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(c, v); v = v->to[c]; } } ``` Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться. ```c++ Vertex* go (Vertex *v, int c); Vertex* link (Vertex *v) { if (!v->link) { if (v == root || v->p == root) v->link = root; else v->link = go(link(v->p), v->pch); } return v->link; } Vertex* go (Vertex *v, int c) { if (!v->go[c]) { if (v->to[c]) v->go[c] = v->to[c]; else if (v == root) v->go[c] = root; else v->go[c] = go(link(v), c); } return v->go[c]; } ``` +# Алгоритм Ахо-Корасик Пусть дан набор строк в алфавите размера $k$ суммарной длины $n$. Алгоритм Ахо-Корасик за $O(nk)$ времени и памяти строит [бор](http://sereja.me/a/trie) для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время. Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик. Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы. > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). ## Зачем это нужно > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Помимо суффиксных ссылок, нужно найти ещё *переходы*, чтобы поддерживать самый длинный суффикс. ## Алгоритм Ахо-Корасик* Заметим, что всего суффиксных ссылок нужно найти $O(n)$, а переходов — $O(nk)$. Суффиксные ссылки и переходы можно быстро найти динамикой. **Ссылки**. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход. **Переходы**. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на *одну* суффиксную ссылку и взять уже посчитанный переход оттуда. ```c++ const int k = 26; struct Vertex { Vertex *to[k] = {0}, *go[k] = {0}; Vertex *link = 0, *p; int pch; Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; } }; Vertex *root = new Vertex(-1, 0); ``` ```c++ void add_string (string s) { Vertex *v = root; for (char _c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(c, v); v = v->to[c]; } } ``` Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться. ```c++ Vertex* go (Vertex *v, int c); Vertex* link (Vertex *v) { if (!v->link) { if (v == root || v->p == root) v->link = root; else v->link = go(link(v->p), v->pch); } return v->link; } Vertex* go (Vertex *v, int c) { if (!v->go[c]) { if (v->to[c]) v->go[c] = v->to[c]; else if (v == root) v->go[c] = root; else v->go[c] = go(link(v), c); } return v->go[c]; } ``` +
+
diff --git a/ru/annealing.html b/ru/annealing.html index 33b3b16..0c9e2cc 100644 --- a/ru/annealing.html +++ b/ru/annealing.html @@ -10,6 +10,8 @@ # Метод отжига * Эвристический метод глобальной оптимизации * Работает с дискретными функциями в предположении, что у близких «точек» близкое значение, и есть что-то типа градиента * Очень простой в написании * Никаких гарантий нет, но хорошо работает на практике * Метод настолько мощный, что иногда им можно сдать задачи, у которых автором предполагалось адекватное решение (пример: [Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам](http://codeforces.com/contest/745/submission/23067030)) ![](https://camo.githubusercontent.com/574bf26bf301827efaa18748440fa77fab8c5a4f/68747470733a2f2f75706c6f61642e77696b696d656469612e6f72672f77696b6970656469612f636f6d6d6f6e732f642f64352f48696c6c5f436c696d62696e675f776974685f53696d756c617465645f416e6e65616c696e672e676966) ## Постановка задачи Имеется некоторая функция $f(x)$, которую мы хотим минимизировать. Наука решать эту задачу в общем случае не умеет, и вряд ли когда-либо научится. TODO: описание алгоритма, физическая аналогия, конструирование гладких функций При решении конкретной задачи нужно менять только функцию `fitness` и, возможно, параметры алгоритма. ## Ферзи Рассмотрим такую задачу: дана шахматная доска $n \times n$ и $n$ ферзей. Нужно расставить их так, чтобы они не били друг друга. Будем кодировать состояние перестановкой чисел от $1$ до $n$. Такое представление кодирует не все состояния, но оно точно не учитывает те, где ферзи бьют друг друга по вертикали или горизонтали. Выберем такую функцию: fitness(p) = число пар ферзей, которые бьют друг друга. Важная деталь: скорость одной итерации прямо пропорционально скорости работы `fitness`, поэтому очень важно реализовать её эффективно. Конкретно здесь её можно реализовать за $O(n^2)$ (используется в коде для наглядности), $O(n)$ и даже $O(1)$. Мы будем использовать `python`, потому что в нём удобнее строить графики. Эквивалентный код на C++ приведена сразу после. ```python import random # Эти библиотеки будут нужны, чтобы рисовать графики: import matplotlib.pyplot as plt %matplotlib inline import seaborn as sns sns.set() ``` ```python def fitness(p): s = 0 for i in range(len(p)): d = 1 for j in range(i): if abs(i-j) == abs(p[i]-p[j]): # ферзи стоят на одной диагонали d = 0 s += d return s ``` ```python n = 100 # кол-во ферзей k = 1000 # кол-во итераций p = list(range(n)) # исходная перестановка scores = [] t = 1 for i in range(k): # уменьшаем температуру t *= 0.99 # этот параметр лучше перебрать руками # копируем перестановку и делаем небольшое изменение q = p[:] a, b = random.sample(range(n), 2) q[a], q[b] = q[b], q[a] # считаем разницу fitness и решаем, нужно ли переходить в новое состояние d = fitness(q) - fitness(p) if d > 0 or random.random() < math.exp(d/t): p = q[:] scores += [fitness(q)] print('Best score:', max(scores)) plt.plot(scores) plt.show() ``` Best score: 95 ![png](annealing_files/annealing_11_1.png) Примерно эквивалентный код на C++: ```python const int n = 100, k = 1000; int f(vector p) { int s = 0; for (int i = 0; i < n; i++) { int d = 1; for (int j = 0; j < i; j++) if abs(i-j) == abs(p[i]-p[j]): d = 0; s += d; } return s; } double rnd() { return double(rand()) / RAND_MAX; } int main() { for (int i = 0; i < n; i++) v[i] = i; int ans = 1; double t = 1; for (int i = 0; i < k; i++) { t *= 0.99; vector u = v; swap(u[rand()%n], u[rand()%n]); int val = f(u); if (val > ans || rnd() < exp((val-ans)/t)) v = u, ans = val; if (ans == n) break; } for (int x : v) cout << x+1 << " "; return 0; } ``` +
+
diff --git a/ru/bayans.html b/ru/bayans.html index bad857f..b524d7e 100644 --- a/ru/bayans.html +++ b/ru/bayans.html @@ -10,6 +10,8 @@ # Красивые идейные задачи Везде, где не указано — время работы $O(n)$, а если есть конкретные числа, то TL 1 секунда. Задачи идут в порядке вспоминания, то есть в весьма рандомном. ## Попугаи Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы. ## Минимум и максимум Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый. ## Перестановка Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов. ## Выпуклая оболочка Требуется отвечать на 2 типа запросов: 1. Добавить точку в выпуклую оболочку. 2. Проверить, лежит ли точка внутри выпуклой оболочки. Обе операции онлайн за $O(\log n)$. ## Геометрическая прогрессия Найдите способ посчитать $\frac{1-a^n}{1-a}$ по произвольному модулю за $O(\log n)$. ## Покемоны В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-*турнир* из 1024 вершин и $1023 \times 1022 : 2$ рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из $a \to b$ и $b \to c$ не следует, что $a \to c$. У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш. ## Сортировка Можно ли отсортировать * 5 камней за 8 взвешиваний? * 5 камней за 7 взвешиваний? * 20 камней за 60 взвешиваний? ## Точки в круге Даны $n$ точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за $O(n)$ в среднем сортирует их по удаленности от начала координат. ## Замкнутые ломаные Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии? ## Неубывающий массив Дан массив из $n$ целых чисел. Требуется за $2n$ операций «прибавить к одному элементу любой другой» сделать его неубывающим. ## Чётный цикл Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины. ## $k$-ая порядковая статистика Дан массив из $n$ целых чисел. Найдите его $k$-й наименьший элемент за $O(n)$. ## Доминирующий элемент Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ *доминирующий* элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. Время работы $O((n+m) \log n)$. ## Разрушение дерева Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева. ## $k$-ый элемент на отрезке Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов $k$-ой порядковой статистики на произвольном отрезке. Время работы $O((n+m) \log n)$. ## Различные числа на отрезке Дан массив из $n$ целых чисел. Требуется ответить на $m$ запросов количества различных элементов на произвольном отрезке. Время работы $O(m\sqrt{n})$. ## Физкультура Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится $m$ дней. Деканат последовательно издает $n$ приказов двух типов: 1. Объявить все дни с $l$ по $r$ выходными (физру закрывать нельзя) 2. Объявить все дни с $l$ по $r$ учебными (физру закрывать можно) При этом приказ может частично отменить действие предыдущих приказов. После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика $O(n \log n)$. ## Нулевая сумма Дано мультимножество из $n$ целых чисел. Найдите любое его подмножество, сумма чисел которого делится на $n$. ## Мета-задача В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу. ## Ниточка В плоскую доску вбили $n$ гвоздей радиуса $r$, причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления. ## Пельмени Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено $n$ возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика $O(n^2 \log n)$. ## От нуля до единицы Дан следующий код: ```python x = 0 while x < 1: x += random() ``` Требуется посчитать матожидание `x`. (`random` в питоне возвращает случайное действительное число от 0 до 1.) ## Площадь Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%. ## Окружности Имеется окружность радиуса $R$, назовём её *внешней*. Внутри неё лежит окружность радиуса $r < R$ и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: $k$-я окружность должна * соприкасаться с *внешней*, * соприкасаться с предыдущей (($k-1$)-ой), * иметь при этом максимальный радиус. Найдите (выведите формулу за $O(1)$) радиус $k$-й такой окружности. ## Блеф Катя и Серёжа играют в игру. У Кати есть $n$ карт, у Серёжи — $m$. Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её *особой*. Цель игроков — её отгадать. Все $n+m+1$ карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может: * Попытаться угадать *особую* карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается. * Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом. С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика $O(nm)$. ## Достижимость Дан ориентированный граф без кратных рёбер. Для всех пар вершин $u$ и $v$ определите, можно ли дойти из $u$ в $v$. Вершин меньше 2000. ## Нумизмат Есть $n$ жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть $k$ типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой. Придумайте любой полиномиальный алгоритм. ## Принцесса В некотором королевстве жила принцесса. Она была настольно прекрасна, что каждый юноша в королевстве хотел жениться на ней. Принцесса была привередлива, поэтому твердо решила выйти замуж только за самого красивого юношу в государстве. Она составила список из $n$ самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством. У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми. Помогите принцессе разработать стратегую, которая максимизирует вероятность того, что она выйдет замуж за наиболее красивого юношу. Асимптотика $O(n^2)$. ## Спираль Определим спираль $(2n+1) \times (2n+1)$ как матрицу следующего вида: $$ \begin{matrix} 21 & 22 & 23 & 24 & 25 \\ 20 & 7 & 8 & 9 & 10 \\ 19 & 6 & 1 & 2 & 11 \\ 18 & 5 & 4 & 3 & 12 \\ 17 & 16 & 15 & 14 & 13 \\ \end{matrix} $$ Ваша задача — рассчитать ответы на $q$ запросов суммы чисел в произвольной прямоугольной области (по модулю $10^9+7$). $q \leq 100$, $n \leq 10^9$. ## Польский лабиринт Группа из $n$ туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка $(x, y)$ свободна, только если `x & y == 0`. ![](https://image.ibb.co/cSs7H7/Screenshot_from_2018_03_31_16_34_58.png) Туристы устали блуждать по лабиринту и хотят встретиться в какой-нибудь свободной клетке, сумма расстояний от всех туристов до которой наименьшая. Туристы за один ход могут передвигаться вверх, вниз, влево и вправо по свободным клеткам. $n \leq 10^5$, изначальные координаты туристов до $10^9$. ## Нимные подмножества Есть множество $A$, состоящее из $n$ чисел от 0 до $2^{32}-1$. Требуется выбрать его подмножество $B \subseteq A$ максимальной суммы такое, что нельзя выбрать его подмножество $C \subseteq B$ такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика $O(n \log n)$. ## Баланс степеней Дан неориентированный граф. Требуется ориентировать каждое ребро так, чтобы максимизировать число вершин, у которых степень исхода равна степени захода. ## Два пути Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из $s$ в $t$. ## Пьяница Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью $p$ и влево с вероятностью $1-p$. С какой вероятностью он когда-либо окажется в точке с отрицательной координатой? ## Ксоровый рюкзак Дан массив из $10^5$ целых чисел от $0$ до $(2^{30}-1)$. Найти количество различных подпоследовательностей этого массива, `xor`-сумма которых равна заданному числу $x$. ## Иван Сусанин Польская армия хочет добраться из поселения $s$ в поселение $t$. Ей руководят два гетмана — Камиль и Матеуш. - Камиль руководит армией днём и водит армию по *дорогам*. - Матеуш руководит армией ночью и совершает маневры по *секретным тропам*. Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск. Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу. Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением $t$ и войском по соответствующей карте строго уменьшилось. Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из $s$ в $t$. ## Варенье В ряд стоят $n$ пустых банок из-под варенья. Вместительность $i$-й банки равна $v_i$ грамм. Карлсон наполняет эти банки вареньем в $m$ этапов. На каждом этапе он выбирает числа $l$, $r$, $x$ и $y$, а затем пролетает над банками с $l$ по $r$, выполняя следующие операции: в банку номер $l$ он добавляет $x$ грамм варенья, в банку номер $(l + 1)$ — $(x + y)$ грамм варенья, в банку номер $(l + 2)$ — $(x + 2y)$, и так далее до $r$-той банки, в которую он положит $x + y(r - l)$ грамм варенья. Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной. $n, m \leq 10^5$ ## Лабиринт Серёжа потерялся в лабиринте $n \times m$. Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт. Придумайте любой полиномиальный алгоритм. ## Обезьяна Дана строка из $10^5$ символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт? ## Ожидание минимума Даны $n$ случайных величин, равномерно распределенных на отрезках $[l_i, r_i]$ — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин. Придумайте любой точный полиномиальный алгоритм. ## Шумный ксор Загадано некое число $x$. Вы можете делать запросы следующего типа: назвать число $y$ и получить в ответ **число единичных битов** в ксор-сумме $x$, $y$ и $m$, где $m$ это случайно сгенерированная маска, в которой каждый бит имеет вероятность $p = \frac15$ быть единичным, то есть каждый бит $x \oplus y$ заменяется на противоположный с вероятностью $y$, и вам возвращается количество единичных битов. Для ясности: ```python x = # ... def mask(p=0.2): r = 0 for i in range(32): if random.random() < p: r += 2**i return r def query(y): return bin(x ^ y ^ mask()).count('1') ``` Ваша задача — отгадать число, используя не более 10000 попыток. +
+
diff --git a/ru/bitset.html b/ru/bitset.html index 5d52aff..fd4544f 100644 --- a/ru/bitset.html +++ b/ru/bitset.html @@ -10,6 +10,8 @@ # Битовое сжатие * Из-за него в «асимптотиках» появляется `/64` * На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов * `bitset` есть в stl; говорят, самописный быстрее Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать `&` двух `bool`-ом и двух `long`-ов примерно одинаково по скорости. Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию. Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — `bitset`. Работать с ним нужно вот так: ```c++ const int lim = 1000; bitset b; // создать битсет размера lim (должно быть константой) b.set(); // заполнить единицами b.reset(); // заполнить нулями b.flip(); // заменить единички на нули и наоборот b.count(); // посчитать число единичек cout << b; // вывести битовую строку ``` Также для битсетов работает вся битовая арифметика — `&, |, ^, ~, <<, >>` и их варианты с `[operator]=`. ## Рюкзак Задача: даны $n$ предметов с положительными целыми весами $a_i$ и рюкзак размера $lim$, выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака. Обычно его решают так: ```c++ bool dp[lim] = {}; // так можно его заполнить нулями dp[0] = 1; for (int i = 0; i < n; i++) for (int x = lim - a[i]; x >= 0; x--) dp[x + a[i]] |= dp[x]; ``` …а с битсетом оно разгоняется так: ```c++ bitset b; b[0] = 1; for (int i = 0; i < n; i++) b |= b << a[i]; ``` ## Цикл длины 3 Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из $n$ вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за $O(\frac{n^3}{64})$ следующим образом: ```c++ bitset g[maxn]; // матрица смежности for (int a = 0; a < n; a++) { for (int b = 0; b < n; b++) { if (g[a][b] && (~g[a] & g[b]).any()) { // цикл найден } } } ``` Benchmark: на серверах CodeForces этот код при $n = 5000$ работает за 7 секунд. ## Перемножение матриц Матрица смежности графа, возведенная в степень $n$, имеет комбинаторный смысл: количество способов дойти из $a$ в $b$, используя ровно $n$ переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через $n$ ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения: ```c++ typedef bitset t; typedef array matrix; matrix operator* (matrix a, matrix b) { matrix c; for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) if(a[i][j]) c[i] |= b[j]; return c; } ``` ## Гаусс Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. Нас по сути просят решить следующую систему: $$ \begin{cases} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ \ldots \\ a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 \end{cases} $$ Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. В таком случае можно значительно ускорить и упростить обычный метод Гаусса: ```c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; } ``` Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. +
+
diff --git a/ru/centroid.html b/ru/centroid.html index 5fc150a..8382835 100644 --- a/ru/centroid.html +++ b/ru/centroid.html @@ -9,7 +9,9 @@ -# Центроидная декомпозиция Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути». Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще). ![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700) **Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\frac{n}{2}$. Выясняется, что центр всегда существует — это следует из алгоритма его поиска: ```c++ int s[maxn]; int sizes (int v) { s[v] = 1; for (int u : g[v]) // для простоты считаем, что дерево корневое s[v] += sizes(u); return s[v]; } // второй параметр -- размер дерева int centroid (int v, int n) { for (int u : g[v]) if (s[u] > n/2) return centroid(u, n); return v; } ``` **Утверждение.** `centroid` действительно находит цетроид. **Доказательство:** * `centroid` вернет вершину, у которой размеры всех детей не больше $\frac{n}{2}$ (это явно проверяется в `if`-е). * Мы пришли в эту вершину, когда её размер был больше $\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\frac{n}{2}+1) = \frac{n}{2}-1$ вершин. * Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен. Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид. **Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент». **Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением. Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты. Теперь поймём, зачем мы всё это делали. **Утверждение.** Каждая вершина входит в $O(\log n)$ компонент. **Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\lceil \log_2 n \rceil$ разделений. **Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \log n)$. **Утверждение.** Для любого пути $a \leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$. **Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$. Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний. ## Подсчет путей с заданным свойством Рассмотрим конкретный пример: подсчёт путей заданной длины. Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид. Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз. ```c++ int l = 179; // нужная нам длина int ans = 0; // нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена bool used[maxn]; int s[maxn]; // размеры поддеревьев void sizes (int v, int p) { s[v] = 1; for (int u : g[v]) if (u != p && !used[u]) sizes(u, v), s[v] += s[u]; } int centroid (int v, int p, int n) { for (int u : g[v]) if (u != p && !used[u] && s[u] > n/2) return centroid(u, v, n); return v; } // записывает в t[] глубины вершин void dfs (int v, int p, int d, vector &t) { t.push_back(d); for (int u : g[v]) if (u != p && !used[u]) dfs(u, v, d + 1, t); } void solve (int v) { /* */ size(v); vector d(s[v], 0); d[0] = 1; for (int u : g[v]) { if (!used[u]) { vector t; dfs(u, v, 1, t); for (int x : t) if (x <= l) ans += d[l-x]; for (int x : t) d[x]++; } } /* */ used[v] = 1; for (int u : g[v]) if (!used[u]) solve(centroid(u, v, s[u]/2)); } ``` **Асимптотика** $O(n \log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\log n)$ «уровней» центроидной декомпозиции. ## Запросы на путях — offline Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline. А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем. Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. Таким образом, каждый запрос будет просмотрен $O(\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \log n + n \log n)$. ## Запросы на путях — online Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы. Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \times \log n$, в котором для каждой вершины будем хранить $O(\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути. Тогда, при ответе на запрос, мы за $O(\log n)$ или даже $O(\log \log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида. ## Асимптотика при более долгих пересчётах TODO: сослаться на мастер-теорему Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени? Возьмем худший случай — когда каждый раз компонента разбивается на две равные части. Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень: $$ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} \leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n = n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} < 2 n \log n = O(n \log n) $$ Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \log n)$ — как, впрочем, и во всех остальных «разделяйках». +# Центроидная декомпозиция Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути». Иногда вместо неё можно написать [Heavy-light декомпозицию](http://sereja.me/a/hld) (чуть сложнее) или метод переливаний (чуть проще). ![](https://tanujkhattar.files.wordpress.com/2016/01/1.jpg?w=700) **Определение**. *Центром* или *цетроидом* (англ. *centroid*) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более $\frac{n}{2}$. Выясняется, что центр всегда существует — это следует из алгоритма его поиска: ```c++ int s[maxn]; int sizes (int v) { s[v] = 1; for (int u : g[v]) // для простоты считаем, что дерево корневое s[v] += sizes(u); return s[v]; } // второй параметр -- размер дерева int centroid (int v, int n) { for (int u : g[v]) if (s[u] > n/2) return centroid(u, n); return v; } ``` **Утверждение.** `centroid` действительно находит цетроид. **Доказательство:** * `centroid` вернет вершину, у которой размеры всех детей не больше $\frac{n}{2}$ (это явно проверяется в `if`-е). * Мы пришли в эту вершину, когда её размер был больше $\frac{n}{2}$, а это значит, что в «обратном» направлении есть не более $n - (\frac{n}{2}+1) = \frac{n}{2}-1$ вершин. * Значит, размеры всех соседей не больше половины $n$, и алгоритм корректен. Иногда центров два (пример: 1-**2-3**-4), тогда алгоритм вернёт «нижний» центроид. **Определение**. *Центроидной декомпозицией* будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент». **Определение**. *Компонентой центроида* будем называть множество вершин, достижимых из центроида непосредственно перед его удалением. Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты. Теперь поймём, зачем мы всё это делали. **Утверждение.** Каждая вершина входит в $O(\log n)$ компонент. **Доказательство.** Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более $\lceil \log_2 n \rceil$ разделений. **Следствие.** Центроидная декомпозиция (см. определение выше) работает за $O(n \log n)$. **Утверждение.** Для любого пути $a \leadsto b$ есть единственный центроид $c$, в чьей компоненте были и $a$, и $b$. **Доказательство.** Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила $a$ и $b$. Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний. ## Подсчет путей с заданным свойством Рассмотрим конкретный пример: подсчёт путей заданной длины. Вмешаемся в процесс центроиднодной декомпозиции: для каждого центроида перед его удалением будем прибавлять к ответу число интересующих нас путей, которые проходят через этот центроид. Количество таких путей можно посчитать за размер текущей компоненты: заведём массив `d`, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей `dfs`, который будет возвращать временный массив `t` — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению `x` в нём и добавить к ответу `d[l-x]`, а затем добавить все значения из `t` в `d`. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз. ```c++ int l = 179; // нужная нам длина int ans = 0; // нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена bool used[maxn]; int s[maxn]; // размеры поддеревьев void sizes (int v, int p) { s[v] = 1; for (int u : g[v]) if (u != p && !used[u]) sizes(u, v), s[v] += s[u]; } int centroid (int v, int p, int n) { for (int u : g[v]) if (u != p && !used[u] && s[u] > n/2) return centroid(u, v, n); return v; } // записывает в t[] глубины вершин void dfs (int v, int p, int d, vector &t) { t.push_back(d); for (int u : g[v]) if (u != p && !used[u]) dfs(u, v, d + 1, t); } void solve (int v) { /* */ size(v); vector d(s[v], 0); d[0] = 1; for (int u : g[v]) { if (!used[u]) { vector t; dfs(u, v, 1, t); for (int x : t) if (x <= l) ans += d[l-x]; for (int x : t) d[x]++; } } /* */ used[v] = 1; for (int u : g[v]) if (!used[u]) solve(centroid(u, v, s[u]/2)); } ``` **Асимптотика** $O(n \log n)$, потому что на каждую из $O(n)$ верщин мы потратим $O(1)$ операций на каждом из $O(\log n)$ «уровней» центроидной декомпозиции. ## Запросы на путях — offline Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline. А именно, мы можем хранить вместе с вершиной список относящихся к ней ещё не отвеченных запросов. Этот список мы будем просматривать во время обработки очередной компоненты, и если центроид лежит на этом запросе, то мы ответим на этот запрос и удалим его из списка, а в обратном случае просто проигнорируем. Например, при запросах суммы на пути, мы можем насчитать во внутреннем `dfs` для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины. Таким образом, каждый запрос будет просмотрен $O(\log n)$ раз, пока не будет удален, и асимптотика составит $O(q \log n + n \log n)$. ## Запросы на путях — online Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы. Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив `centroid[][]` размера $n \times \log n$, в котором для каждой вершины будем хранить $O(\log n)$ центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути. Тогда, при ответе на запрос, мы за $O(\log n)$ или даже $O(\log \log n)$ операций находим центроид на нужном нам пути (первые сколько-то значений `centroid[v]` и `centroid[u]` будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида. ## Асимптотика при более долгих пересчётах TODO: сослаться на мастер-теорему Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за $O(n \log n)$ (например, где-то использовав `set`). Сильно ли это хуже по времени? Возьмем худший случай — когда каждый раз компонента разбивается на две равные части. Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень: $$ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} \leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n = n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} < 2 n \log n = O(n \log n) $$ Получается, что мы можем «мердж» в центроидной декомпозиции писать и за $O(n \log n)$ — как, впрочем, и во всех остальных «разделяйках». +
+
diff --git a/ru/convex-hulls.html b/ru/convex-hulls.html index 7029d81..c9f01da 100644 --- a/ru/convex-hulls.html +++ b/ru/convex-hulls.html @@ -10,6 +10,8 @@ # Задачи на многоугольники Очень часто геометрические задачи на контестах - это задачи на многоугольники. Некоторые из таких задач мы уже обсудили на предыдущих занятиях. Так мы уже обсудили задачу проверки многоугольника на выпуклость и научились находить площадь многоугольника. Напомним, что такое полярный угол. ![alt text](https://upload.wikimedia.org/wikipedia/commons/1/13/Polar_graph_paper.svg) # Выпуклые оболочки **Выпуклое множество** - такое множество точек, что все точки отрезка, образуемого любыми двумя точками данного множества, также принадлежат данному множеству **Выпуклая оболочка фигуры** - такое выпуклое множество точек, что все точки фигуры также лежат в нем. **Минимальная выпуклая оболочка фигуры** - это минимальная по площади выпуклая оболочка. ![alt text](http://informatics.letovo.ru/geom/Volgin_Asatrian/images/vup.png) ![alt text](http://grafika.me/files/les_screens/convex_1.png) ![alt text](https://2.bp.blogspot.com/-JngZWPM7wig/WlRz58cIMHI/AAAAAAAADGA/Pyhfngs_GKQMkb9B2_kqZJRQ5ZLEzrVMQCK4BGAYYCw/s1600/convexhullex.jpg) Дано множество точек, требуется построить его минимальную выпуклую оболочку : ## Построение за $O(nh)$ ### Алгоритм Джарвиса(метод заворачивания подарка) Давайте выберем какую-то точку, которая гарантированно попадет в минимальную выпуклую оболочкуо, например обычно берут нижнюю и если таких несколько, то самую левую из них. Теперь давайте по одной набирать точки, как бы заворачивая нашу выпуклую оболочку(отсюда и название). Как же нам найти следующую точку в выпуклую оболочку, давайте пройдемся по точкам, которые мы еще не взяли в МВО и среди них выберем с минимальным полярным углом. ![alt text](http://neerc.ifmo.ru/wiki/images/b/b3/Graham1.png) Корректность алгоритма легко доказывается по индукции, так как на первом шагу мы выбрали точку, точно лежащую в МВО, а на i, взяли такую точку, что все остальные лежат в нужной нам стороне. Асимптотика : для каждой точки выпуклой оболочки мы из всех оставшихся точек будем искать оптимальную - что будет работать за h(размер выпуклой оболочки) * n ### Важно помнить, что именно $O(hn)$, а не $O(n^2)$, так как существуют задачи на это ``` int base = 0; for (int i = 1; i < n; i++) { if (mas[i].y < mas[base].y) { base = i; } else if (mas[i].y == mas[base].y && mas[i].x < mas[base].x) { base = i; } } convex_hull.push_back(base); point first = mas[base]; point cur = first; point prev = point(first.x - 1, first.y); do { double minCosAngle = 1e9; // чем больше угол, тем меньше его косинус double maxLen = 1e9; int next = -1; for (int i = 0; i < n; i++) { double curCosAngle = CosAngle(prev, cur, mas[i]); if (Less(curCosAngle,minCosAngle)) {//если меньше сразу меняем next = i; minCosAngle = curCosAngle; maxLen = dist(cur, mas[i]); } else if (Equal(curCosAngle, minCosAngle)) {// смотрим по длине double curLen = dist(cur,mas[i]); if (More(curLen,maxLen)) { next = i; maxLen = curLen; } } } prev = cur; cur = mas[next]; convex_hull.push_back(next); } while (cur != first); ``` ## Построение за $O(n \log n)$ ### Алгоритм Грэхема Алгоритм Грэхема базируется на следующей идее: Давайте не искать следующую точку каждый раз, а сделаем так, чтобы у нас всегда была оптимальная точка и мы могли бы просто ее достать и проверить. В прошлом алгоритме мы искали точку, оптимальную по полярному углу, тогда давайте сейчас сразу отсортируем точки по полярному углу и сразу возьмем две первые точки в МВО. Теперь будем делать следующий алгоритм, пока все точки не будут просмотрены : 1) Возьмем первую из отсортированных точек. 2) Проверем последние три точки из взятых, если они образуют правый поворот, то удалим предпоследнюю точку Сделать это можно, например, стеком. Код есть ниже. ![alt text](https://cdn.turkaramamotoru.com/ru/algoritm-grehema-4131.jpg) Асимптотика : Мы просмотрим одну точку и либо удалим ее, либо оставим, то есть сам поиск МВО работает за линейное время, но мы еще делаем сортировку, а $\rightarrow$ алгоритм работает за $O(n\log(n))$, при этом его корректность вытекает из предыдущего алгоритма. Красивая визуализация - https://visualgo.net/en/convexhull красивое видео - https://www.youtube.com/watch?v=BTgjXwhoMuI . ``` struct Point { int x, y; }; Point operator -(Point a, Point b) { return {a.x - b.x, a.y - b.y}; } int operator * (Point a, Point b) { return a.x * b.x + b.y * a.y; } int operator ^(Point a, Point b) { return a.x * b.y - b.x * a.y; } bool cw(Point a, Point b, Point c) { return (a - b) * (c - b) > 0; } bool ccw(Point a, Point b, Point c) { return (a - b) * (c - b) < 0; } int main() { sort(all(p2), comp); vector s; s.push_back(p[min_ind]); for (int i = 0; i < n - 1; i++) { if (p2[i].x == s[s.size() - 1].x && p2[i].y == s[s.size() - 1].y) continue; while (s.size() > 1 && (vect(s[s.size() - 1], s[s.size() - 2]) ^ vect(s[s.size() - 1], p2[i])) > 0) s.pop_back(); s.push_back(p2[i]); } } ``` ### Алгоритм Эндрю Алгоритм Эндрю опирается на то, что вещественные числа не точны и предлагает поменять компаратор и строить не одну выпуклую оболочку, а две : Давайте выберем самую нижнюю и самую правую точку, затем отсортируем точки по самому простому из возможных компараторов, теперь будем строить две оболочки от самой правой точки и самой левой, в итоге мы получим верхнюю и нижнюю части выпуклой оболочки ``` bool comp(Point a, Point b) { if(a.x == b.x) { return a.y < b.y; } return a.x < b.x; } int main() { sort(all(p), comp); vector up, down; up.pb(p[0]); down.pb(p[0]); Point p1 = p[0], p2 = p.back(); for(int i = 1; i < n; i++) { if (i == n - 1 || cw(p1, p[i], p2)) { while (up.size() >= 2 && !cw(up[up.size() - 2], up[up.size() - 1], p[i])) { up.pop_back(); } up.pb(p[i]); } if (i == n - 1 || ccw(p1, p[i], p2)) { while (down.size() >= 2 && !ccw(down[down.size() - 2], down[down.size() - 1], p[i])) { down.pop_back(); } down.pb(p[i]); } } } ``` ### Алгоритм Чена Также существует алгоритм, объединяющий Джарвиса и Грэхема(Эндрю) и работающий за $O(n\log(h))$, но он разбираться не будет # Задачи 1) Базовые задачи - достаточно простые, например найти длину забора, чтобы ограничить многоугольник и подобные, но есть достаточно интересные задачи, в которых выпуклая оболочка неочевидна, например следующая : Даны $n$ пунктов в городе и $n$ почтальонов, для каждого пункта известно расстояние от почты $c_{i}$. Требуется каждому пункту доставить почту, $i$-ый почтальон просит $a_{i}$ монет, чтобы проснуться и $b_{i}$, чтобы проехать один километр, требуется для каждого пункта сказать, кто доставит почту наиболее выгодно. (Подсказка : $a_{i} + b_{i} * c_{j}$ - это прямая и стоимость доставки от $i$ почтальона к $j$ пункту). +
+
diff --git a/ru/cross-entropy.html b/ru/cross-entropy.html index 873d4a7..d6036d0 100644 --- a/ru/cross-entropy.html +++ b/ru/cross-entropy.html @@ -10,6 +10,8 @@ # Почему машинное обучение работает b Принцип максимального правдоподобия — ## Функция правдоподобия $$ \hat{\theta} = \argmax_\theta P(X | \theta) $$ ## Принцип максимального правдоподобия ## Логистическая регрессия ## Нормальное распределение и MSE ## Почему сигмоида +
+
diff --git a/ru/cuda.html b/ru/cuda.html index 2d9f174..657283d 100644 --- a/ru/cuda.html +++ b/ru/cuda.html @@ -9,9 +9,14 @@ -# Supercomputing for the Masses This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in [Colab]() or [download]() and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: * `apt-get install nvidia-cuda-dev nvidia-cuda-toolkit` * `pip install pycuda` Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work. ## Subtlties of the Moore's law Here is a graph that roughly represents what is happening in the CPU world: **Moore's law** is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too. You can see that around 2005 there became a shift in design . The cores are more or less independent. Modern GPUs appeared in early 2000s. They exploit the specific area they operate. There are physical limitations to the speed of a core. One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another. Some of them have The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome. ## Why multiprocessing? Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound There are two types of ## General-purpose GPU There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills. There are several. This is like with Windows and Linux. We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning. ## Heterogineous computing CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs. ## Differences from CPUs ### Threads Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive. By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput. ### Memory The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device? You can easily dump 98% of performance of you think this way. ## Installing PyCUDA CUDA is available for many languages. Nice documentation can be found here: https://documen.tician.de/pycuda/index.html If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU". ```python # you may want to clear the output of this cell after installation from IPython.display import clear_output # this might take a while !pip install pycuda clear_output() ``` ```python import numpy as np from pycuda.compiler import SourceModule import pycuda.driver as drv import pycuda.autoinit ``` ## The basics Let's start with a simple example and then dive deeper. ## Kernels Just like C or C++, except that you use some custom built-in functions and specifiers. CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: You need to think of your computer as a heterogenious machine: there is host data and device data. * You move input data to device memory. * You run some computation on device. * You retrieve back this data. In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. ## The famous $A + B$ problem For testing and coordination with host, we will use **NumPy** package. If you don't have it, install it: `pip install numpy`. NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it. ```python # lets generate our test data: two float arrays filled with something random a = numpy.random.randn(100).astype('float32') b = numpy.random.randn(100).astype('float32') # the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it # we need to create space where kernel should write its answers to dest = numpy.zeros_like(a) # this is the kernel itself mod = SourceModule(""" __global__ void add(float *dest, float *a, float *b) { const int i = threadIdx.x; dest[i] = a[i] + b[i]; } """) # you need to specify the source code, and PyCUDA will compile it add_kernel = mod.get_function("add") add_kernel( drv.Out(dest), # specifies that this memory should be accessible for writing drv.In(a), # specifies this should be accessible for reading drv.In(b), block=(100,1,1) # we'll talk about it in a minute ) assert np.allclose(dest, a + b), 'WA' # checks that these are equal print('OK') ``` File "", line 19 %%time ^ SyntaxError: invalid syntax ### Memory management In CUDA C API, you need to allocate memory explicitly. So this is actually really nice. There is also `drv.InOut` function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too. Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough. GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time. You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications ## Synchronization **Reduction** is any array-wise operation. Assume the following problem: ## Dynamic programming Consider the following recurrence: ```python ## Problem: dynamic programming ``` ## Work vs. Latency We actually think about both work and step complexity now. Some tasks, especially in cryptography, cannot be parallelized. But some can. ## Summing arrays in $O(\log n)$ time Assume we want to perform some associative (i. e. $A*(B*C) = (A*B)*C$) operation on an array of $n$ elements. Say, sum it up. Normally, we would do that with a simple loop: ```c++ float s = 0; for (int i = 0; i < n; i++) { s += a[i]; } ``` Its computation graphs looks like this: This is optimal in terms of work complexity, but not in terms of step complexity: it's $O(n)$. We may want something that is a bit worse in terms of work complexity, but can be parallelized. Let's try this divide-and-conquer approach: Now it's still $O(n)$ work complexity (you actually need exactly the same number of additions), but this is $O(\log n)$ step complexity. When you unroll the recursion from top to bottom, you will see that to get each required value, ## Reducing small arrays ```python a = numpy.random.randn(2048).astype('float32') mod = SourceModule(""" __global__ void sum(float *dest, float *a, float *b) { const int i = threadIdx.x; // for l from 0 to logn: // __sync_threads() // if the thread is active // sum two elements into where they belong // a[0] should containt the needed sum } """) sum_kernel = mod.get_function("sum") add_kernel( drv.InOut(a), block=(1024,1,1) ) assert np.allclose(dest, a + b), 'WA' # checks that these are equal print('OK') ``` ## Warps and thread blocks Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties. You can actually do the same stuff with 2d and 3d indexing — weird, right? ## Atomics ## Reducing big arrays ## Reducing very big arrays Now, things get harder. It's time to tell how exactly GPU parallelism works. ```python ``` ## Dense Matrix multiplication Let's get to our first example where using GPUs actually makes sense: matrix multiplication. ## Sorting Our last (and hardest task) is to implement sorting. You might notice that we advocated divide-and-conquer approaches most of the time. It's true. They work. But we can't get an algorithm that works already. ```python # we'll use a deep learning library for benchmarking because I'm not familiar with anything else import torch a = torch.randn(10**8) b = a.cuda() ``` ```python # this should run for ~15 secs %time c = torch.sort(a) %time c = torch.sort(b) ``` CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s Wall time: 15.2 s CPU times: user 274 ms, sys: 237 ms, total: 511 ms Wall time: 511 ms So, 30 times speedup. So, we now what we need to compete against. ```python b.sort() ``` (tensor([-5.4567, -5.3551, -5.3288, ..., 5.3529, 5.4484, 5.4486], device='cuda:0'), tensor([55083205, 8383169, 73705953, ..., 79814161, 50474932, 27805828], device='cuda:0')) There are two types of sorting algorithms: data-driven. The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort. It has $O(\log n)$ stages, in total they have $1 + 2 + 3 + \ldots + \log n = O(\log^2 n$ blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has $O(n \log^ n)$ work complexity, but $O(\log^2 n)$ step complexity, which is pretty sweet. It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation: ```python def bitonic_sort(a, up=False): if len(a) <= 1: return a else: l = bitonic_sort(x[:len(a) // 2], True) r = bitonic_sort(x[len(a) // 2:], False) return bitonic_merge(first + second, up) def bitonic_merge(a, up): # assume input a is bitonic, and sorted list is returned if len(a) == 1: return a else: bitonic_compare(a, up) l = bitonic_merge(a[:len(a) // 2], up) r = bitonic_merge(a[len(a) // 2:], up) return l + r def bitonic_compare(a, up): dist = len(a) // 2 for i in range(dist): if (a[i] > a[i + dist]) == up: a[i], a[i + dist] = a[i + dist], x[i] # this is how swap is done in Python ``` ```python bitonic_sort([57, 179, 42, 17, 300, 111]) ``` [300, 179, 111, 57, 42, 17] ```python a = np.random.randn(10**8).astype('float32') ``` --------------------------------------------------------------------------- NameError Traceback (most recent call last) in () ----> 1 a = np.random.randn(10**8).astype('float32') NameError: name 'np' is not defined ## Why CUDA Most of it still applicable. Again, GPU programming is very specific. SSE and tensor cores. ## Kernels Just like C or C++, except that you use some custom built-in functions and specifiers. CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: You need to think of your computer as a heterogenious machine: there is host data and device data. * You move input data to device memory. * You run some computation on device. * You retrieve back this data. In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. What you need to understand about GPUs is that they are extremely specialised for their applications. Intrinsics for that. Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision. First, they introduced "multiply-accumulate" operation (e. g. `x += y * z`) per 1 GPU clock cycle. Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell). Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix). This is a lot of work per Well, you don't really need anything more precise than that for deep learning anyway. It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices. Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores". So, see, this is not exactly fair comparison. * +# Supercomputing for the Masses This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in [Colab]() or [download]() and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: * `apt-get install nvidia-cuda-dev nvidia-cuda-toolkit` * `pip install pycuda` Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work. ## Subtlties of the Moore's law Here is a graph that roughly represents what is happening in the CPU world: **Moore's law** is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too. You can see that around 2005 there became a shift in design . The cores are more or less independent. Modern GPUs appeared in early 2000s. They exploit the specific area they operate. There are physical limitations to the speed of a core. One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another. Some of them have The default free GPUs available on Google Colab are [rather powerfull](https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/tesla-t4/t4-tensor-core-datasheet-951643.pdf). Author has no idea why Google does this, but this is awesome. ## Why multiprocessing? Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound There are two types of ## General-purpose GPU There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills. There are several. This is like with Windows and Linux. We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning. ## Heterogineous computing CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs. ## Differences from CPUs ### Threads Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive. By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution. In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput. ### Memory The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device? You can easily dump 98% of performance of you think this way. ## Installing PyCUDA CUDA is available for many languages. Nice documentation can be found here: https://documen.tician.de/pycuda/index.html If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU". ```python # you may want to clear the output of this cell after installation from IPython.display import clear_output # this might take a while !pip install pycuda clear_output() ``` ```python import numpy as np from pycuda.compiler import SourceModule import pycuda.driver as drv import pycuda.autoinit ``` ## The basics Let's start with a simple example and then dive deeper. ## Kernels Just like C or C++, except that you use some custom built-in functions and specifiers. CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: You need to think of your computer as a heterogenious machine: there is host data and device data. * You move input data to device memory. * You run some computation on device. * You retrieve back this data. In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. ## The famous $A + B$ problem For testing and coordination with host, we will use **NumPy** package. If you don't have it, install it: `pip install numpy`. NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it. ```python # lets generate our test data: two float arrays filled with something random a = numpy.random.randn(100).astype('float32') b = numpy.random.randn(100).astype('float32') # the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it # we need to create space where kernel should write its answers to dest = numpy.zeros_like(a) # this is the kernel itself mod = SourceModule(""" __global__ void add(float *dest, float *a, float *b) { const int i = threadIdx.x; dest[i] = a[i] + b[i]; } """) # you need to specify the source code, and PyCUDA will compile it add_kernel = mod.get_function("add") add_kernel( drv.Out(dest), # specifies that this memory should be accessible for writing drv.In(a), # specifies this should be accessible for reading drv.In(b), block=(100,1,1) # we'll talk about it in a minute ) assert np.allclose(dest, a + b), 'WA' # checks that these are equal print('OK') ``` File "", line 19 %%time ^ SyntaxError: invalid syntax ### Memory management In CUDA C API, you need to allocate memory explicitly. So this is actually really nice. There is also `drv.InOut` function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too. Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough. GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have *compute capabilities* (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability $x$ is also available at later versions. These can be checked at run-time or compile-time. You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications ## Synchronization **Reduction** is any array-wise operation. Assume the following problem: ## Dynamic programming Consider the following recurrence: ```python ## Problem: dynamic programming ``` ## Work vs. Latency We actually think about both work and step complexity now. Some tasks, especially in cryptography, cannot be parallelized. But some can. ## Summing arrays in $O(\log n)$ time Assume we want to perform some associative (i. e. $A*(B*C) = (A*B)*C$) operation on an array of $n$ elements. Say, sum it up. Normally, we would do that with a simple loop: ```c++ float s = 0; for (int i = 0; i < n; i++) { s += a[i]; } ``` Its computation graphs looks like this: This is optimal in terms of work complexity, but not in terms of step complexity: it's $O(n)$. We may want something that is a bit worse in terms of work complexity, but can be parallelized. Let's try this divide-and-conquer approach: Now it's still $O(n)$ work complexity (you actually need exactly the same number of additions), but this is $O(\log n)$ step complexity. When you unroll the recursion from top to bottom, you will see that to get each required value, ## Reducing small arrays ```python a = numpy.random.randn(2048).astype('float32') mod = SourceModule(""" __global__ void sum(float *dest, float *a, float *b) { const int i = threadIdx.x; // for l from 0 to logn: // __sync_threads() // if the thread is active // sum two elements into where they belong // a[0] should containt the needed sum } """) sum_kernel = mod.get_function("sum") add_kernel( drv.InOut(a), block=(1024,1,1) ) assert np.allclose(dest, a + b), 'WA' # checks that these are equal print('OK') ``` ## Warps and thread blocks Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties. You can actually do the same stuff with 2d and 3d indexing — weird, right? ## Atomics ## Reducing big arrays ## Reducing very big arrays Now, things get harder. It's time to tell how exactly GPU parallelism works. ```python ``` ## Dense Matrix multiplication Let's get to our first example where using GPUs actually makes sense: matrix multiplication. ## Sorting Our last (and hardest task) is to implement sorting. You might notice that we advocated divide-and-conquer approaches most of the time. It's true. They work. But we can't get an algorithm that works already. ```python # we'll use a deep learning library for benchmarking because I'm not familiar with anything else import torch a = torch.randn(10**8) b = a.cuda() ``` ```python # this should run for ~15 secs %time c = torch.sort(a) %time c = torch.sort(b) ``` CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s Wall time: 15.2 s CPU times: user 274 ms, sys: 237 ms, total: 511 ms Wall time: 511 ms So, 30 times speedup. So, we now what we need to compete against. ```python b.sort() ``` (tensor([-5.4567, -5.3551, -5.3288, ..., 5.3529, 5.4484, 5.4486], device='cuda:0'), tensor([55083205, 8383169, 73705953, ..., 79814161, 50474932, 27805828], device='cuda:0')) There are two types of sorting algorithms: data-driven. The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort. It has $O(\log n)$ stages, in total they have $1 + 2 + 3 + \ldots + \log n = O(\log^2 n$ blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has $O(n \log^ n)$ work complexity, but $O(\log^2 n)$ step complexity, which is pretty sweet. It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation: ```python def bitonic_sort(a, up=False): if len(a) <= 1: return a else: l = bitonic_sort(x[:len(a) // 2], True) r = bitonic_sort(x[len(a) // 2:], False) return bitonic_merge(first + second, up) def bitonic_merge(a, up): # assume input a is bitonic, and sorted list is returned if len(a) == 1: return a else: bitonic_compare(a, up) l = bitonic_merge(a[:len(a) // 2], up) r = bitonic_merge(a[len(a) // 2:], up) return l + r def bitonic_compare(a, up): dist = len(a) // 2 for i in range(dist): if (a[i] > a[i + dist]) == up: a[i], a[i + dist] = a[i + dist], x[i] # this is how swap is done in Python ``` ```python bitonic_sort([57, 179, 42, 17, 300, 111]) ``` [300, 179, 111, 57, 42, 17] ```python a = np.random.randn(10**8).astype('float32') ``` --------------------------------------------------------------------------- NameError Traceback (most recent call last) in () ----> 1 a = np.random.randn(10**8).astype('float32') NameError: name 'np' is not defined ## Why CUDA Most of it still applicable. Again, GPU programming is very specific. SSE and tensor cores. ## Kernels Just like C or C++, except that you use some custom built-in functions and specifiers. CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows: You need to think of your computer as a heterogenious machine: there is host data and device data. * You move input data to device memory. * You run some computation on device. * You retrieve back this data. In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results. What you need to understand about GPUs is that they are extremely specialised for their applications. Intrinsics for that. Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision. First, they introduced "multiply-accumulate" operation (e. g. `x += y * z`) per 1 GPU clock cycle. Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell). Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix). This is a lot of work per Well, you don't really need anything more precise than that for deep learning anyway. It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices. Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores". So, see, this is not exactly fair comparison. * You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull -* down to int4 (16-valued, you heard correct) You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch. Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply. ## Reducing an array It seems to be simple: you just need to . What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen: 1. Read $x$ into register 2. Read $s$ into register 3. Calculate $s + x$ 4. Write it back to wherever $s$ was initially Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value. Note: Atomics to do that for small data types they are implemented on the hardware level and much more faster than that. std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. ## Memory types If all the various types of device memory were to race, here’s how the race would turn out: Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible). * 1st place: **Register memory**
This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread. * 2nd place: **Shared Memory**
Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should * 3rd place: **Constant Memory**
* 4th: Texture Memory * Tie for last place: Local Memory and Global Memory What you need to care for now is register For now, you need to care about differe Accessing global memory takes hundreds. ## Problem: dense matrix multiplication A lot of these are actually sparse. You can do stuff with social network graphs or web graphs. Cool. But let's disapploint us for a bit: +* down to int4 (16-valued, you heard correct) You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch. Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply. ## Reducing an array It seems to be simple: you just need to . What actually happens when you do `s += x`? This is not a single operation. Actually, four things happen: 1. Read $x$ into register 2. Read $s$ into register 3. Calculate $s + x$ 4. Write it back to wherever $s$ was initially Two threads may execute it in an interleaved fashion. Say thread A could get $s$, but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value. Note: Atomics to do that for small data types they are implemented on the hardware level and much more faster than that. std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions. ## Memory types If all the various types of device memory were to race, here’s how the race would turn out: Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible). * 1st place: **Register memory**
+This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread. * 2nd place: **Shared Memory**
+Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should * 3rd place: **Constant Memory**
+* 4th: Texture Memory * Tie for last place: Local Memory and Global Memory What you need to care for now is register For now, you need to care about differe Accessing global memory takes hundreds. ## Problem: dense matrix multiplication A lot of these are actually sparse. You can do stuff with social network graphs or web graphs. Cool. But let's disapploint us for a bit: +
+
diff --git a/ru/dp-intro.html b/ru/dp-intro.html index e4327b4..d4f3be0 100644 --- a/ru/dp-intro.html +++ b/ru/dp-intro.html @@ -10,6 +10,8 @@ # Динамическое программирование > Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. Разберем некоторые классические идеи и задачи в динамическом программировании. ## Рюкзак > Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. ## Наибольшая общая подпоследовательность > Есть две строки. Найти длину их наибольшей общей подпоследовательности. ## Расстановка скобок > Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. Можно завести динамику «можно ли ». ## Наибольшая возрастающая подпоследовательность Есть также алгоритм за $O(n \log n)$. ## Мемоизация Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. Первый предполагает ## Число разбиений > ## Замена параметра на ответ > +
+
diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index acd57c8..299cebe 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -10,6 +10,8 @@ ## Оптимизации динамики * Разделяй и властвуй * Оптимизация Кнута * Convex Hull Trick * Лямбда-оптимизация ## Задача > Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. ```c++ // x[] — отсортированный массив координат точек, нумерация с нуля // квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } // TODO: предподсчитать cost for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ``` Заметим, что циклы по `i` и `j` можно поменять местами. Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. ## Разделяй-и-властвуй Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. *Утверждение.* $opt[i, j] \leq opt[i, j+1]$. Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. ```c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); } ``` Вызываться она будет просто последовательно для каждого слоя: ```c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k); ``` Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. Получается, что асимптотика улучшилась до $O(n m \log n)$. ## Оптимизация Кнута Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? $$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: $$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. ```c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } } ``` Сравните с базовым решением — всего 3 новых строчки. ## Convex Hull Trick Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. $$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ f[k, j-1] + x_{i-1}^2 - 2x_{i-1} x_k + x_k^2 \}$$ Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется $ \underbrace{f[k, j-1] + x_k^2}_{a_k} \underbrace{-2x_k}_{b_k} x_{i-1} $. Это теперь можно переписать как $ \min_k (a_k, b_k) \cdot (1, X_{i-1}) $ (имеется в виду скалярное произведение) Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. TODO: иллюстрация. TODO: мем про Скуби-Ду. ### Ли Шао Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. ## Лямбда-оптимизация Не «фольклорное» название — дискретный метод множителей Лагранжа. Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . Заметим следующее: Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. ## Суммируем TODO: сделать табличку * Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. * Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. * CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. * Лагранж: $O(n \log n)$. Функция должна быть выпуклой. ## Другие задачи Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. > Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. +
+
diff --git a/ru/dp_intro.html b/ru/dp_intro.html index e1f20f8..faba50c 100644 --- a/ru/dp_intro.html +++ b/ru/dp_intro.html @@ -10,6 +10,8 @@ # Динамическое программирование test123 > Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать. Разберем некоторые классические идеи и задачи в динамическом программировании. ## Рюкзак > Имеется $n$ предметов, у которых известны их веса и стомости. Есть рюкзак, у которого есть максимальная вместимость. Нужно набрать предметы как можно большей стомости так, чтобы их суммарный вес не превышал вместимость рюкзака. ## Наибольшая общая подпоследовательность > Есть две строки. Найти длину их наибольшей общей подпоследовательности. ## Расстановка скобок > Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «$2+2\times2$». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать. Можно завести динамику «можно ли ». ## Наибольшая возрастающая подпоследовательность Есть также алгоритм за $O(n \log n)$. ## Мемоизация Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы. Первый предполагает ## Число разбиений > ## Замена параметра на ответ > +
+
diff --git a/ru/dp_optimizations.html b/ru/dp_optimizations.html index acd57c8..299cebe 100644 --- a/ru/dp_optimizations.html +++ b/ru/dp_optimizations.html @@ -10,6 +10,8 @@ ## Оптимизации динамики * Разделяй и властвуй * Оптимизация Кнута * Convex Hull Trick * Лямбда-оптимизация ## Задача > Даны $n$ точек на прямой. Нужно найти $m$ отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин. Базовое решение — следующая динамика: $f[i, j]$ — минимальная стоимость покрытия $i$ первых (самых левых) точек, используя не более $j$ отрезков (итоговый ответ будет записан в $f[n, m]$ — прим. К. О.). Переход — перебор всех возможных последних отрезков, то есть $f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}$. ```c++ // x[] — отсортированный массив координат точек, нумерация с нуля // квадрат длины отрезка от i-той до j-той точки int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); } // TODO: предподсчитать cost for (int i = 0; i <= m; i++) f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо // все остальные f предполагаем равными бесконечности for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) for (int k = 0; k < i; k++) f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1)); ``` Заметим, что циклы по `i` и `j` можно поменять местами. Такое решение пока работает за $O(n^2 m)$ — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить. ## Разделяй-и-властвуй Обозначим за $opt[i, j]$ оптимальный $k$, на котором $f[i, j]$ минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший. *Утверждение.* $opt[i, j] \leq opt[i, j+1]$. Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок. Что это нам даёт? Если мы уже знаем $opt[i, l]$ и $opt[i, r]$ и хотим посчитать $opt[i, j]$ для какого-то $j$ между $l$ и $r$, то мы можем сузить отрезок поиска оптимального индекса с $[0, i-1]$ до $[opt[i, l], opt[i][r]]$. Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка $[l, r]$, зная, что их $opt$-ы лежат между $l'$ и $r'$. Она берет середину отрезка $[l, r]$ и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно. ```c++ void solve (int l, int r, int _l, int _r, int k) { if (l > r) return; // отрезок пустой — выходим int t = (l + r) / 2; int opt = _l; for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt = i; } solve(l, t-1, _l, opt, k); solve(t+1, r, opt, _r, k); } ``` Вызываться она будет просто последовательно для каждого слоя: ```c++ for (int k = 1; k <= m; k++) solve(0, n-1, 0, n-1, k); ``` Теперь пересчет одного «слоя» динамики занимает $O(n \log n)$ вместо $O(n^2)$. Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет $O(\log n)$, а значит и каждый элемент будет просмотрен не более $O(\log n)$ раз. Получается, что асимптотика улучшилась до $O(n m \log n)$. ## Оптимизация Кнута Предыдущий метод основывался на том факте, что $opt[i, j] \leq opt[i, j+1]$. А что, если $opt$ монотонен ещё и по первому параметру? $$ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] $$ В задаче это выполняется примерно по той же логике: если наш отрезок стал меньше, то мы можем себе позволить больший последний отрезок. Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем: $$ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) $$ Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из $opt[i, j]$ не более $O(n)$. ```c++ for (int i = 1; i <= n; i++) { for (int j = m; j >= 1; j--) { for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) { int val = f[i+1][k-1] + cost(i, j); if (val < f[t][k]) f[t][k] = val, opt[i][j] = i; } } } ``` Сравните с базовым решением — всего 3 новых строчки. ## Convex Hull Trick Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на `cost` и увидев там скалярное произведение. $$ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ f[k, j-1] + x_{i-1}^2 - 2x_{i-1} x_k + x_k^2 \}$$ Посмотрим внимательнее на минимизируемое выражение. $x_{i-1}^2$ не зависит от $k$, значит его можно вынести. Под минимумом останется $ \underbrace{f[k, j-1] + x_k^2}_{a_k} \underbrace{-2x_k}_{b_k} x_{i-1} $. Это теперь можно переписать как $ \min_k (a_k, b_k) \cdot (1, X_{i-1}) $ (имеется в виду скалярное произведение) Представим $(a_k, b_k)$ как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением. TODO: иллюстрация. TODO: мем про Скуби-Ду. ### Ли Шао Существует другой подход: увидеть здесь не точки и оптимизацию скалярного произведения, и линии и нахождение минимума в точке. ## Лямбда-оптимизация Не «фольклорное» название — дискретный метод множителей Лагранжа. Пусть у нас есть функция $f(x)$, которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = . Заметим следующее: Давайте заменим $f[i, j]$ на $g_\lambda[i]$ — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный $\lambda$, за использование отрезка. Давайте сделаем бинпоиск по $\lambda$. Понятно, что для каких-то лямбд мы будем использовать ровно $j$ отрезков. Тогда ответ на динамику $f[i, j] = g[i] - \lambda k$. ## Суммируем TODO: сделать табличку * Разделяйка: $O(nm \log n)$, если `cost` такой, что `opt` монотонна по одному аргументу. * Кнут: $O(nm)$, если `cost` такой, что `opt` монотонна по обоим аргументам. * CHT: $O(nm)$. В оптимизируемой функции нужно увидеть скалярное произведение. * Лагранж: $O(n \log n)$. Функция должна быть выпуклой. ## Другие задачи Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение. > Есть $n$ ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить $m$ амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар. +
+
diff --git a/ru/fenwick.html b/ru/fenwick.html index b584776..c0116f9 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -9,8 +9,14 @@ - НЯ!
Эта статья полна любви и обожания.
Возможно, стоит добавить ещё больше? +
+ НЯ!
+Эта статья полна любви и обожания.
+Возможно, стоит добавить ещё больше? +
# Дерево Фенвика Дерево Фенвика или бинарно индеквированное дерево (англ. *binary indexed tree*) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности. ## Определение Пусть дан массив $a$ длины $n$. Деревом Фенвика будем называть массив $t$ той же длины, который объявим так: $$ t_i = \sum_{k=F(i)}^i a_k $$ где $F$ это какая-то функцию, для которой выполнено $F(i) \leq i$. Конкретно её определим потом. Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе ($sum(l, r) = sum(r) - sum(l-1)$), каждый из которых будем считать по этой формуле: $$ sum(k) = t_k + sum(F(k)-1) $$ Когда мы изменяем $k$-ю ячейку исходного массива, мы обновляем все $t_i$, в которых учтена эта ячейка. $F$ можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам $t_i$ при обновлении будет будет $O(\log n)$. Популярны две функции: * $F_1(x) =$ `x & (x + 1)` * $F_2(x) =$ `x - (x & -x) + 1` Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать. **Disclaimer**: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на $O(\log n)$ отрезков вида $[F(i), i]$, и любой элемент входит в не более $O(\log n)$ таких отрезков. ## Реализация Из-за того, что $F(0) = 1 > 0$ и поэтому $[0, F(0)]$ не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать $t_0$. ```c++ int t[maxn]; // возвращает сумму на префиксе int sum (int r) { int res = 0; for (; r > 0; r -= r & -r) res += t[r]; return res; } int sum (int l, int r) { return sum(r) - sum(l-1); } // обновляет нужные t void add (int k, int x) { for (; k <= n; k += k & -k) t[k] += x; } ``` Автор отмечает красивую симметрию в формулах `r += r & -r` и `k -= k & -k`, которой нет в «традиционной» версии. ## Многомерный случай > $k$-мерное дерево Фенвика пишется в $(k+1)$ строчку Нужно добавить всего одну такую же строчку в `sum`, `add`, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре. `sum` перепишется следующим образом: ```c++ int sum (int r1, int r2) { int res = 0; for (int i = r1; i > 0; i -= i & -i) for (int j = r2; j > 0; j -= j & -j) ans += t[i][j]; return res; } ``` В $k$-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно $2^k$ запросов суммы на префиксах. Если размерности больше, чем позволяет память, то можно вместо массива `t` использовать хэш-таблицу — так потенциально потребуется $O(q \log^2 A)$ памяти ($A$ — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом [решил](https://pastebin.com/DPemaJeW) какую-то задачу на 2d-сумму с USACO 2017. ## Бинпоиск Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за $O(\log n)$. ```c++ // возвращает индекс, на котором сумма уже больше int lower_bound (int s) { int k = 0; for (int l = logn; l >= 0; l--) { if (k + (1<= k > i - (i & -i)`. Будем перебирать префиксы TODO Мы знаем, что $t_i$ вложены друг в друга. Минимальный подходящий $i$ равен $k$. Какой следующий? Нам нужно для каждого $i$ уметь находить его непосредственного родителя. Можно представить дерево так: ячейка 2^k содержит все TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию. ## Название Потому что $F$ использует битовые операции, по-английски структура называется «Binary Indexed Tree». Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев. Можно показать, что множества элементов, учтенных в $t_i$ и $t_j$, либо не пересекаются, либо одно является подмножеством другого. Значит, между $t_i$ можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных). В частном случае, когда длина массива равна $2^k$, то дерево будет только одно. +
+
diff --git a/ru/games.html b/ru/games.html index c45c188..a84d4e8 100644 --- a/ru/games.html +++ b/ru/games.html @@ -9,7 +9,9 @@ -# Теория игр Начнём с самого баянного примера математической игры, который можно вспомнить: > На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков. Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так: $$ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) $$ то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния). **Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях. ## Игры на графах В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек». Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные* Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*. Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*. ## Ретроанализ Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности: * Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины. * Вершина *выигрышная* — если из неё есть переход в проигрышную вершину. * Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные). Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся. **Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин. Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние. Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось. Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные. **Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали. * При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). * При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше. Асимптотика составит $O(n + m)$, как и у любого bfs-а. ```c++ vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа int cnt[maxn]; // счётчик исходящих рёбер enum StatusType { win, loss, unknown }; StatusType status[maxn]; // выгрышность вершины; // по умолчанию все кроме терминальных считаются unknown // те, кто в итоге остаются unknown -- ничейные queue q = {/* нужно заранее добавить сюда все терминальные вершины*/}; while (!q.empty()) { int v = q.front(); q.pop(); for (int u : t[v]) { cnt[u]--; // удаляем это ребро if (status[v] == unknown) { // из u есть ребро в проигрышную -- значит она выигрышная if (status[v] == loss) status[u] = win; // все ребра u ведут в выигрышные вершины -- значит она проигрышная if (status[v] == win && cnt[u] == 0) { status[u] = loss; // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь if (status[v] != unknown) q.push(u); } } } ``` Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации: ```c++ StatusType dfs(int v) { if (status[v] != unknown) return status[v]; status[v] = loss; // изменим статус, когда найдём переход в проигрышную вершину for (int u : g[v]) if (dfs(u) == loss) status[v] = win; return status[v]; } ``` TODO: можно ли здесь циклы учесть? ## Минимаксные игры Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно. Типичный граф минимаксной игры выглядит следующим образом: В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников. Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей. ## Ретроанализ для больших графов Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго. Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей). Ретроанализ нужно оптимизировать. ### Ограничение перебора Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит. Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли). ### Мемоизация позиций В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает . Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары ### Альфа-бета отсечение Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым. На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым. Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6. Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем. Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации. ### Monte-Carlo Tree Search ## Игры с ненулевой суммой ## Теория Шпрага-Гранди Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые. Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями. **Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \oplus a_2 \oplus \ldots \oplus a_n $ размеров кучек отлична от нуля. **Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы: * Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия. * Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \oplus a_i \oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$. Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \oplus a_i$. Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории. **Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен. ### Ним с увеличениями Пусть у нас ### Эквивалентность игр ниму ## Игры с неполной информацией Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков. Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет. Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло. ### Дилемма заключенного ### Камень-ножницы-бумага ### Покер Рассмотрим такую у +# Теория игр Начнём с самого баянного примера математической игры, который можно вспомнить: > На столе лежит кучка из $n$ спичек. Два игрока по очереди берут спички из кучки, разрешается взять одну, две или три спички. Тот, кто берет последнюю спичку, выигрывает. Требуется определить, кто выигрывает при оптимальной игре обоих игроков. Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет $f_k$ — выиграет ли первый игрок, если спичек сейчас $n$, и ход принадлежит ему. Пересчитывать её нужно так: $$ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) $$ то есть должен найтись переход в какое-то состояние динамики, которое является проигрышным (или, что эквивалентно, не должно быть пере динамики должны вести в проигрышные состояния). **Примечание.** Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли $n$ на 4, но мы всё равно рассмотрим этот пример в педагогических целях. ## Игры на графах В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «$n$ спичек». Любую игру можно описать в виде графа (возможно, бесконечного) *состояний игры*, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как *терминальные* Состояние называется *выигрышным*, если игрок, начинающий в нём, побеждает, и *проигрышным* в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём *ничейными*. Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют *играми с нулевой суммой* (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется *ретроанализом*. ## Ретроанализ Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности: * Вершина *проигрышная* — если все её переходы ведут в выигрышные вершины. * Вершина *выигрышная* — если из неё есть переход в проигрышную вершину. * Вершина *ничейная* — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные). Пользуясь этими критериями, можно разработать общий алгоритм: проверять все вершины на соответствие этим критериям и обновлять их статусы, пока вершины с неизвестным статусом не кончатся. **Корректность.** Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин. Рассмотрим граф неизвестных вершин $U$. Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина $v$, у которой нет исходящих рёбер в $U$ — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины $v$ определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние. Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось. Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф $U$. В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в $U$ — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные. **Асимптотика.** Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали. * При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф). * При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше. Асимптотика составит $O(n + m)$, как и у любого bfs-а. ```c++ vector g[maxn], t[maxn]; // списки смежности прямого и обратного графа int cnt[maxn]; // счётчик исходящих рёбер enum StatusType { win, loss, unknown }; StatusType status[maxn]; // выгрышность вершины; // по умолчанию все кроме терминальных считаются unknown // те, кто в итоге остаются unknown -- ничейные queue q = {/* нужно заранее добавить сюда все терминальные вершины*/}; while (!q.empty()) { int v = q.front(); q.pop(); for (int u : t[v]) { cnt[u]--; // удаляем это ребро if (status[v] == unknown) { // из u есть ребро в проигрышную -- значит она выигрышная if (status[v] == loss) status[u] = win; // все ребра u ведут в выигрышные вершины -- значит она проигрышная if (status[v] == win && cnt[u] == 0) { status[u] = loss; // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь if (status[v] != unknown) q.push(u); } } } ``` Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации: ```c++ StatusType dfs(int v) { if (status[v] != unknown) return status[v]; status[v] = loss; // изменим статус, когда найдём переход в проигрышную вершину for (int u : g[v]) if (dfs(u) == loss) status[v] = win; return status[v]; } ``` TODO: можно ли здесь циклы учесть? ## Минимаксные игры Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются *минимаксными* — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно. Типичный граф минимаксной игры выглядит следующим образом: В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников. Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей. ## Ретроанализ для больших графов Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго. Для некоторых игр титаническими трудами были посчитаны исходы во всех возможных состояниях (шашкки, для шахмат — только база эндшпилей). Ретроанализ нужно оптимизировать. ### Ограничение перебора Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено $K$ ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через $K$ ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая **численно оценивает, насколько первый игрок выигрывает**. Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за $K$ ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит. Ясно, что это дает существенное ускорение по времени: можно подобрать $K$ так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли). ### Мемоизация позиций В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает . Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары ### Альфа-бета отсечение Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым. На картинке изображена игра с нулевой суммой: в ней сумма выигрыша первого и второго игрока равна нулю, поэтому на картинке в вершине написан только выигрыш того игрока, который ходит из нее первым. Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния $X$ (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6. Давайте зайдем в третьего сына, назовем его $Y$, вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына $Y$, обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына $Y$ можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня $X$ в вершину $Y$, то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына $X$ уже дает 6, что больше, и рассматривать вершину $Y$ далее бессмысленно, мы в нее уже точно не пойдем. Отсечение таких лишних веток и называется альфа-бета-отсечением. Понятно, что оно может значительно ускорить перебор, так как мы выкидываем целые ветки перебора, и при этом эта оптимизация все еще находит оптимальный ход, в отличие от прошлой оптимизации. ### Monte-Carlo Tree Search ## Игры с ненулевой суммой ## Теория Шпрага-Гранди Рассмотрим игру «ним»: даны $n$ кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые. Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в $i$-й как $a_i$. Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями. **Теорема.** Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 \oplus a_2 \oplus \ldots \oplus a_n $ размеров кучек отлична от нуля. **Доказательство** проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы: * Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i \oplus b_i $, где $b_i < a_i$ — это число камней в $i$-й кучке после нашего действия. * Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой $b_i < a_i$, что xor-сумма станет нулевой, то есть $S \oplus a_i \oplus b_i = 0$. Для этого посмотрим на старший взведенный бит $S$ и возьмем любой $a_i$, у которого этот бит тоже взведен. Такой $a_i$ найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый $b_i$ равен $S \oplus a_i$, и выясняется, что это корректный новый размер кучки, то есть $b_i < a_i$. Почему так? Потому что все старшие биты в выражении остались нетронутыми, $k$-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем $2^k$. Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех $a_i$, найти такой $a_i$, у которого старший бит взведен, и заменить его на $S \oplus a_i$. Каждый раз, перед тем, как рассказать про решение нима, автор просит кого-нибудь непосвященного сыграть против него в ним, и со скрежетом ксорит в уме несколько бинарных чисел, пока все не убедятся в верности теории. **Зачем это надо?** Есть много игр, в которых присутствует какой-то подобный *цугцванг* (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен. ### Ним с увеличениями Пусть у нас ### Эквивалентность игр ниму ## Игры с неполной информацией Это называется минимаксоной теоремой, а более общее утверждение доказал Джон Нэш. Все это состояние называется равновесием Нэша, или эквилибриумом. Эквилибриум существует и в играх с большим числом игроков. Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет. Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло. ### Дилемма заключенного ### Камень-ножницы-бумага ### Покер Рассмотрим такую у +
+
diff --git a/ru/geometry.html b/ru/geometry.html index 2e5b407..faa571a 100644 --- a/ru/geometry.html +++ b/ru/geometry.html @@ -9,7 +9,9 @@ -# Геометрия и ООП Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали. ![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg) Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь. **Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается: $$ a \cdot b = x_a x_b + y_a y_b $$ ![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg) У него есть полезные свойства: * Скалярное произведение симметрично ($a \cdot b = b \cdot a$). * Геометрически, это проекция вектора b на вектор a, помноженынй на длину а. * Перпендикулярные вектора должны иметь нулевое скалярное произведение. * Если угол острый, то оно положительное * Если угол тупой, то отрицательное **Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах: $$ a \times b = x_a y_b - y_a x_b $$ ![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg) Его свойства: * Скалярное произведение *анти*симметрично ($a \times b = - (b \times a)$). * Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора. * Коллинеарные вектора должны иметь нулевое векторное произведение. * Если b «слева» от a, то оно положительное. * Если b «справа» — то отрицательное. ## Всякие проверки Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями. **Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником. $$ \text{P лежит внутри ABC} \iff \begin{cases} (B-A) \times (P-A) \geq 0 \\ (C-B) \times (P-B) \geq 0 \\ (A-C) \times (P-C) \geq 0 \\ \end{cases} $$ **Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения. $$ V = \frac{1}{2} (B-A) \times (C-A) $$ **Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными). ![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg) Забудьте о формуле Герона и всегда считайте площади через векторное произведение. Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2. **Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\times(c-a) > 0$). **Пересекаются ли отрезки**. ![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png) ## Уравнение прямой Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством. У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \geq 0$ будет указывать в сторону самой полуплоскости. Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой: $$ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} $$ **Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений: $$ \begin{cases} A_1 x + B_1 y + C_1 = 0 \\ A_2 x + B_2 y + C_2 = 0 \end{cases} \implies \begin{cases} -x = \frac{B_1 y + C_1}{A_1} \\ -x = \frac{B_2 y + C_2}{A_2} \end{cases} \implies \frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} \implies y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} $$ Аналогично, $x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки). Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать. # Как это кодить в C++ Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. **Точка $\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно. ```c++ struct r { double x, y; r () {} r (int _x, int _y) { x = _x, y = _y; } }; ``` Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$. ## Операции над векторами Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину: ```c++ double len (r a) { return sqrt(a.x*a.x + a.y*a.y); } ``` ## Операторы В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д. Давайте для будущих нужд определим `+` и `-`: ```c++ r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); } r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); } ``` ## Скалярное произведение ```c++ int operator*(r a, r b){ return a.x*b.x + a.y*b.y; } ``` ## Векторное произведение Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике. ```c++ int operator^(r a, r b){ return a.x*b.y - b.x*a.y; } ``` ### Ввод-вывод Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так: ```c++ istream& operator>>(istream ∈, r &p){ in >> p.x >> p.y; return in; } ostream& operator<<(ostream &out, r &p){ out << p.x << " " << p.y << endl; return out; } ``` ## Алгебра VS Алгоритмы или зачем мы всё это делали Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен. ### Векторное представление прямой ($Ax + By + C = 0 \rightarrow r = at + b$) Тут нужно просто выбрать две любые точки на прямой. ```c++ // даны A, B, C (A^2 + B^2 != 0) r a, b; if (eq(A, 0)) // значит, это горизонтальная прямая a = r(0, -C/B), b = r(1, -C/B); else a = r(-C/A, 0), b = (1, -(C+B)/A, 1) ``` ### Пример: отражение от прямой Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать. $\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} $ Формула имеет смысл: длина на единичный вектор направления. Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить. ```c++ // прямая r = at + b, точка c r pr (r a, r b, r c) { c -= b; // пусть c и a выходят из одной точки return b + (a*b / len(a) / len(a)) * a; } r reflect (r a, r b, r c) { return c + 2*(pr(a, b, c)-c); } ``` ## Типичные баги ### Точность > Первое правило действительных чисел — не использовать действительные числа Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`: ```c++ const double eps = 1e-8; bool eq (double a, double b) { return abs(a-b) < eps } ``` Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре. ### $0 \neq -0$ Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду. ### Область определения обратных функций `acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции. +# Геометрия и ООП Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется **вектором**. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали. ![vector](https://habrastorage.org/getpro/habr/post_images/7aa/2e5/f55/7aa2e5f550583a65de28560234e055e8.jpg) Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь. **Скалярное произведение** (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается: $$ a \cdot b = x_a x_b + y_a y_b $$ ![dot](https://habrastorage.org/getpro/habr/post_images/747/242/79b/74724279b3aa34f4f7c5f0b024fa23da.jpg) У него есть полезные свойства: * Скалярное произведение симметрично ($a \cdot b = b \cdot a$). * Геометрически, это проекция вектора b на вектор a, помноженынй на длину а. * Перпендикулярные вектора должны иметь нулевое скалярное произведение. * Если угол острый, то оно положительное * Если угол тупой, то отрицательное **Векторное произведение** (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах: $$ a \times b = x_a y_b - y_a x_b $$ ![cross](https://habrastorage.org/getpro/habr/post_images/a28/585/04b/a2858504bc917e2b084bf6f3e5827ca3.jpg) Его свойства: * Скалярное произведение *анти*симметрично ($a \times b = - (b \times a)$). * Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора. * Коллинеарные вектора должны иметь нулевое векторное произведение. * Если b «слева» от a, то оно положительное. * Если b «справа» — то отрицательное. ## Всякие проверки Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями. **Принадлежность точки треугольнику**. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником. $$ \text{P лежит внутри ABC} \iff \begin{cases} (B-A) \times (P-A) \geq 0 \\ (C-B) \times (P-B) \geq 0 \\ (A-C) \times (P-C) \geq 0 \\ \end{cases} $$ **Площадь треугольника**. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения. $$ V = \frac{1}{2} (B-A) \times (C-A) $$ **Площадь произвольного многоугольника**. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными). ![any](https://habrastorage.org/getpro/habr/post_images/1b3/b00/8c0/1b3b008c0c074b0e38111a95ad421605.jpg) Забудьте о формуле Герона и всегда считайте площади через векторное произведение. Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2. **Проверка на выпуклость**. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то $(b-a)\times(c-a) > 0$). **Пересекаются ли отрезки**. ![segments](https://habrastorage.org/storage2/a03/5b4/ffa/a035b4ffa74057e35428196b55a4154b.png) ## Уравнение прямой Прямая задается уравнением вида $Ax + By + C = 0$. Полуплоскость можно задать таким же неравенством. У прямой есть *вектор нормали* с координатами $(A, B)$. Он перпендиуклярен прямой, а в случае с полуплоскостью $Ax + By + C \geq 0$ будет указывать в сторону самой полуплоскости. Чтобы найти расстояние от точки $(x_0, y_0)$ до прямой $Ax + By + C = 0$, можно воспользоваться следующей формулой: $$ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} $$ **Точка пересечения**. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений: $$ \begin{cases} A_1 x + B_1 y + C_1 = 0 \\ A_2 x + B_2 y + C_2 = 0 \end{cases} \implies \begin{cases} -x = \frac{B_1 y + C_1}{A_1} \\ -x = \frac{B_2 y + C_2}{A_2} \end{cases} \implies \frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} \implies y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} $$ Аналогично, $x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}$ (обратите внимание на знаки). Заметьте, что знаменатель может оказаться нулем. Это означает, что векторное произведение векторов нормали нулевое, а значит прямые параллельны (в частности, это могут быть совпадающие прямые). Этот случай нужно отдельно обрабатывать. # Как это кодить в C++ Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля *приватные* — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. **Точка $\simeq$ вектор**. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её *радиус-вектор* — вектор из начала координат, ведущий в эту точку. По [принятой](https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B4%D0%B8%D1%83%D1%81-%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80) в математике и физике нотации, будем обозночать вектора как `r`. Вы можете обозвать их как `point`, `pt`, `vec` — как угодно. ```c++ struct r { double x, y; r () {} r (int _x, int _y) { x = _x, y = _y; } }; ``` Функция `r` внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, `r()` вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами $(x, y)$. ## Операции над векторами Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину: ```c++ double len (r a) { return sqrt(a.x*a.x + a.y*a.y); } ``` ## Операторы В C++ можно *перегружать* почти все стандартные операторы, например, `+`, `-`, `<<` и т. д. Давайте для будущих нужд определим `+` и `-`: ```c++ r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); } r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); } ``` ## Скалярное произведение ```c++ int operator*(r a, r b){ return a.x*b.x + a.y*b.y; } ``` ## Векторное произведение Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике. ```c++ int operator^(r a, r b){ return a.x*b.y - b.x*a.y; } ``` ### Ввод-вывод Как вы думаете, как на самом деле работает `cin >> x;`? Это тоже перегрузка оператора `>>`. Делать это нужно так: ```c++ istream& operator>>(istream &in, r &p){ in >> p.x >> p.y; return in; } ostream& operator<<(ostream &out, r &p){ out << p.x << " " << p.y << endl; return out; } ``` ## Алгебра VS Алгоритмы или зачем мы всё это делали Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать `intersect(a, b)`, в корректности которого он точно уверен. ### Векторное представление прямой ($Ax + By + C = 0 \rightarrow r = at + b$) Тут нужно просто выбрать две любые точки на прямой. ```c++ // даны A, B, C (A^2 + B^2 != 0) r a, b; if (eq(A, 0)) // значит, это горизонтальная прямая a = r(0, -C/B), b = r(1, -C/B); else a = r(-C/A, 0), b = (1, -(C+B)/A, 1) ``` ### Пример: отражение от прямой Пусть нам надо отразить точку $(x_0, y_0)$ симметрично относительно заданной прямой $ax+by+c=0$. Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать. $\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} $ Формула имеет смысл: длина на единичный вектор направления. Мы **не** хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить. ```c++ // прямая r = at + b, точка c r pr (r a, r b, r c) { c -= b; // пусть c и a выходят из одной точки return b + (a*b / len(a) / len(a)) * a; } r reflect (r a, r b, r c) { return c + 2*(pr(a, b, c)-c); } ``` ## Типичные баги ### Точность > Первое правило действительных чисел — не использовать действительные числа Все переменные типа `double` хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно **всегда** учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число `eps`: ```c++ const double eps = 1e-8; bool eq (double a, double b) { return abs(a-b) < eps } ``` Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре. ### $0 \neq -0$ Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду. ### Область определения обратных функций `acos`, `asin` и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции. +
+
diff --git a/ru/hashing.html b/ru/hashing.html index 665f45b..b4cc204 100644 --- a/ru/hashing.html +++ b/ru/hashing.html @@ -10,6 +10,8 @@ # Хэширование в строковых задачах Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка. «Хорошая» хэш-функция: * Быстро считается — за линейное от размера объекта время; * Имеет не очень большие значения — влезающие в 64 бита; * «Детерминированно-случайная» — если хэш может принимать $n$ различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно $\frac{1}{n}$. Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют *сюрьективными*. Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны $n$ строк длины $m$, и нас просят $q$ раз проверять произвольные две на равенство. Вместо наивной проверки за $O(q \cdot n \cdot m)$, мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки. ![hash](https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Hash_table_4_1_1_0_0_1_0_LL.svg/300px-Hash_table_4_1_1_0_0_1_0_LL.svg.png) ## Применения в реальной жизни * **Чек-суммы**. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить. * **Хэш-таблица**. Класс `unordered_set` из STL можно реализовать так: заведём $n$ изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию $f$ с областью значений $[0, n)$. При обработке `.insert(x)` мы будем добавлять элемент $x$ в $f(x)$-тый список. При ответе на `.find(x)` мы будем проверять, лежит ли $x$-тый элемент в $f(x)$-том списке. Благодаря «равномерности» хэш-функции, после $k$ добавлений ожидаемое количество сравнений будет равно $\frac{k}{n}$ = $O(1)$ при правильном выборе $n$. * **Мемоизация**. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш. * **Проверка на изоморфизм**. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками. * **Криптография**. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить. * **Поиск в многомерных пространствах**. Детерминированный поиск ближайшей точки среди $m$ точек в $n$-мерном пространстве быстро не решается. Однако можно придумать [хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши](https://ru.wikipedia.org/wiki/Locality-sensitive_hashing), и делать поиск только среди элементов с тем же хэшом, что у запроса. Хэшируемые объекты могут быть самыми разными: строки, изображения, графы, шахматные позиции, просто битовые файлы. Сегодня же мы остановимся на строках. # Полиномиальное хэширование **Лайфхак**: пока вы не выучили все детерминированные строковые алгоритмы, научитесь пользоваться хэшами. Будем считать, что строка — это последовательность чисел от $1$ до $m$ (размер алфавита). В C++ `char` это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: `int x = (int) (c - 'a' + 1)`. Определим *прямой полиномиальный хэш* строки как значение следующего многочлена: $$ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p $$ Здесь $k$ — произвольное число больше размера алфавита, а $p$ — достаточно большой модуль, вообще говоря, не обязательно простой. Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени $k$: ```c++ const int k = 31, mod = 1e9+7; string s = "abacabadaba"; long long h = 0, m = 1; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h + m * x) % mod; m = (m * k) % mod; } ``` Можем ещё определить *обратный полиномиальный хэш*: $$ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p $$ Его преимущество в том, что можно написать на одну строчку кода меньше: ```c++ long long h = 0; for (char c : s) { int x = (int) (c - 'a' + 1); h = (h * k + x) % mod; } ``` Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой $h$. ## Зачем он нужен? Используя тот факт, что хэш это значение многочлена, можно быстро пересчитывать хэш от результата выполнения многих строковых операций. Например, если нужно посчитать хэш от конкатенации строк $a$ и $b$ (т. е. $b$ приписали в конец строки $a$), то можно просто хэш $b$ домножить на $k^{|a|}$ и сложить с хэшом $a$: $$ h(ab) = h(a) + k^{|a|} \cdot h(b) $$ Удалить префикс строки можно так: $$ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} $$ А суффикс — ещё проще: $$ h(a) = h(ab) - k^{|a|} \cdot h(b) $$ В задачах нам часто понадобится домножать $k$ в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве: ```c++ const int maxn = 1e5+5; int p[maxn]; p[0] = 1; for (int i = 1; i < maxn; i++) p[i] = (p[i-1] * k) % mod; ``` Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса: ```c++ int h[maxn]; h[0] = 0; // h[k] -- хэш префикса длины k // будем считать, что s это уже последовательность int-ов for (int i = 0; i < n; i++) h[i+1] = (h[i] + p[i] * s[i]) % mod; ``` Теперь с помощью этих префиксных хэшей мы можем определить функцию, которая будет считать хэш на произвольном подотрезке: $$ h(s[l:r]) = \frac{h_r-h_l}{k^l} $$ Деление по модулю воможно делать только при некоторых `k` и `mod` (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим. Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к $n$-ной. Так проще — нужно будет домножать, а не делить. $$ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) $$ ```c++ int hash_substring (int l, int r) { return (h[r+1] - h[l]) * p[n-l] % mod; } ``` Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за $O(1)$. ### Упражнение Напишите то же самое, но используя *обратный* полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. ## Примеры задач **Количество разных подстрок**. Посчитаем хэши от всех подстрок за $O(n^2)$ и добавим их все в `std::set`. Чтобы получить ответ, просто вызовем `set.size()`. **Поиск подстроки в строке**. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа. **Сравнение строк** (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним. **Палиндромность подстроки**. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений `hash_substring()` на первом массиве и на втором. **Количество палиндромов**. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно. ### Изменения строки* Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в [декартово дерево](http://sereja.me/a/treap). В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в `upd()` пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына. Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне. Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова. # Вероятность ошибки и почему это всё вообще работает У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения. Событие, когда два хэша совпали, а не должны, называется *коллизией*. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в `set` $O(n^2)$ различных случайных значений в промежутке $[0, m)$. Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать $m$, чтобы не бояться такого? ## Выбор констант Практическое правило: если вам нужно хранить $n$ различных хэшей, то безопасный модуль — это число порядка $10 \cdot n^2$. Обоснование — см. парадокс дней рождений. Не всегда такой можно выбрать один — если он будет слишком большой, будут происходить переполнения. Вместо этого можно брать два или даже три модуля и считать много хэшей параллельно. Можно также брать модуль $2^{64}$. У него есть несколько преимуществ: * Он большой — второй модуль точно не понадобится. * С ним ни о каких переполнениях заботиться не нужно — если все хранить в `unsigned long long`, процессор сам автоматически сделает эти взятия остатков при переполнении. * С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию `%`. Всё с этим модулем было прекрасно, пока не придумали [тест против него](http://codeforces.com/blog/entry/4898). Однако, его добавляют далеко не на все контесты — имейте это в виду. В выборе же $k$ ограничения не такие серьезные: * Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. * Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться. Главное — чтобы значения $k$ и модуля не знал человек, который генерирует тесты. ## Парадокс дней рождений > В группе, состоящей из 23 или более человек, вероятность совпадения дней рождения хотя бы у двух людей превышает 50%. Более общее утверждение: в мультимножество нужно добавить $\Theta(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. **Первое доказательство** (для любителей матана). Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. $$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ Попытаемся оценить $f$: $$ \begin{align} \begin{aligned} e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ & = e^{-\frac{n(n-1)}{2d}} & \\ & \simeq e^{-\frac{n^2}{2d}} & \\ \end{aligned} \end{align} $$ Из последнего выражения более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется очень быстро. **Второе доказательство** (для любителей теорвера). Введем $\frac{n(n-1)}{2}$ индикаторов — по одному для каждой пары людей $(i, j)$ — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна $\frac{1}{d}$. Обозначим за $X$ число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть $\frac{n (n-1)}{2} \cdot \frac{1}{d}$. Отсюда понятно, что если $d = \Theta(n^2)$, то ожидание равно константе, а если $d$ асимптотически больше или меньше, то $X$ стремится нулю или бесконечности соответственно. *Примечание*: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1. ### Бонус: «мета-задача» Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу. +
+
diff --git a/ru/hld.html b/ru/hld.html index 974ba1f..48aeca7 100644 --- a/ru/hld.html +++ b/ru/hld.html @@ -9,9 +9,11 @@ -# Heavy-light декомпозиция HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid). * +# Heavy-light декомпозиция HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать [что-нибудь попроще](http://sereja.me/a/centroid). * TODO: найти менее уродливую иллюстрацию * Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины $v$ посмотрим на всех её непосредственных детей $u$, выберем среди них ребёнка $u_{max}$ (с самым большим размером поддерева) и назовём ребро $(v, u)$ *тяжелым* (heavy), а все остальные рёбра — *лёгкими* (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого. Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения. **Утверждение.** Дерево разбивается на непересекающиеся пути из тяжелых рёбер. **Доказательство.** В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра. Назовём *блоком* либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер. **Утверждение.** На любом вертикальном пути будет не более $O(\log n)$ блоков. **Доказательство** разбивается на две части: * Лёгких ребер на вертикальном пути будет не более $O(\log n)$: рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего. * Непрерывных путей из тяжелых рёбер будет не более $O(\log n$: если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего $O(\log n)$. **Следствие.** На любом пути будет не более $O(\log n)$ блоков. Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на $O(\log n)$ запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам. ## Реализация Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера. А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то `tin`-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь. Теперь мы можем построить какую-нибудь структуру поверх массива размера $n$ (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре. ```c++ vector g[maxn]; int s[maxn], p[maxn], tin[maxn], tout[maxn]; int head[maxn]; // «голова» тяжелого пути, которому принадлежит v int t = 0; void sizes (int v = 0) { s[v] = 1; for (int &u : g[v]) { sizes(u); s[v] += s[u]; if (s[u] > s[g[v][0]]) // &u -- это ссылка, так что её легально использовать при swap-е swap(u, g[v][0]); } } void hld (int v = 0) { rin[t] = v; tin[v] = t++; for (int u : g[v]) { // если это тяжелый ребенок -- его next нужно передать // в противном случае он сам является головой нового пути head[u] = (u == g[v][0] ? head[v] : u); hld(u); } tout[v] = t; } ``` ## Как им решать задачи Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов: 1. Узнать минимальное число на пути между $v_i$ и $u_i$. 2. Изменить число у $v_i$-той вершины на $x_i$. Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в [соответствующей статье](http://sereja.me/a/segtree) и имеет методы `upd(k, x)` и `get_min(l, r)`. ```c++ int val[maxn]; segtree st(0, n); ``` При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков: ```c++ void upd (int v, int x) { st.upd(tin[v], x); } ``` Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям. ```c++ int ancestor (int a, int b) { return tin[a] <= tin[b] && tin[b] <= tout[a]; } void up (int &a, int &b, int &ans) { while (!ancestor(head[a], b)) { ans = min(ans, st.get_min(tin[head[a]], tin[a])); a = p[head[a]]; } } int get_min (int a, int b) { int ans = inf; up(a, b, ans); up(b, a, ans); if (!ancestor(a, b)) swap(a, b); ans = min(ans, st.get_min(tin[a], tin[b])); return ans; } ``` +
+
diff --git a/ru/index.html b/ru/index.html index a93ada9..2b53204 100644 --- a/ru/index.html +++ b/ru/index.html @@ -10,6 +10,8 @@ # Алгоритмика ### Структуры данных * [Дерево отрезков](http://sereja.me/a/segtree) отложенные операции, динамическое, персистентное * [Декартово дерево](http://sereja.me/a/treap) treap, дерамида, неявный ключ, персистентное * [Дерево Фенвика](http://sereja.me/a/fenwick) многомерное дерево Фенвика, бинпоиск по дереву Фенвика * [Разреженная таблица](http://sereja.me/a/sparse-table) sparse table, static RMQ * [Битовое сжатие](http://sereja.me/a/bitset) битовые операции, std::bitset, перемножение матриц, метод Гаусса ### Общие техники * Корневая декомпозиция: она где-то есть * [Алгоритм Карацубы](http://sereja.me/a/karatsuba): введение в разделяй-и-властвуй, мастер-теорема ### Дискретная математика * [Модулярная арифметика](http://sereja.me/a/reciprocal) теорема Ферма, нахождение обратного по модулю, бинарное возведение в степень, диофантово уравнение, применения в комбинаторике, предподсчёт обратных факториалов за линейное время * [Ро-алгоритм Полларда](http://sereja.me/a/pollard) парадокс дней рождений, факторизация за $O(\sqrt[4] n)$ * [Матроиды](http://sereja.me/a/matroid) алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость ### Графы * [Остовные деревья](http://sereja.me/a/mst) алгоритм Прима, алгоритм Крускала * [Паросочетания]([http://sereja.me/a/matching](http://sereja.me/a/matching) алгоритм Куна, покрытие ациклического орграфа, лемма Холла ### Потоки * [Поток минимальной стоимости](http://sereja.me/a/mincost-maxflow) критерий оптимальности, отмена потока, потенциалы Джонсона, «дейкстра с потенциалами» ### Деревья * [Наименьший общий предок]([http://sereja.me/a/lca](http://sereja.me/a/lca) LCA, двоичные подъемы, сведение LCA к RMQ, алгоритм Фараха-Колтона и Бендера * [Центроидная декомпозиция](http://sereja.me/a/centroid) * [Heavy-light декомпозиция](http://sereja.me/a/hld) ### Строки * [Полиномиальное хэширование](http://sereja.me/a/hashing) хэши, парадокс дней рождений, хранение строк в декартовом дереве * [Поиск строки в строке](http://sereja.me/a/strings) префикс-функция, z-функция * [Бор](http://sereja.me/a/trie) * [Ахо-Корасик](http://sereja.me/a/aho-corasick) * [Суффиксный массив](http://sereja.me/a/suffix-array) построение за $O(n \log n)$, LCP ### Динамическое программирование * [Пересчёт динамики по слоям](http://sereja.me/a/dp-optimizations) оптимизация Кнута, разделяй-и-властвуй, Convex Hull Trick, дискретный метод Лагранжа ### Оптимизация * [Метод отжига](http://sereja.me/a/annealing) задача о ферзях ### Разное * [Стресс-тестирование](http://sereja.me/a/stress-test) ### Теория игр * [Теория игр](http://sereja.me/a/games): эту статью определенно надо распилить на несколько ### Высшая математика * [Линейная алгебра](http://sereja.me/a/linalg) линейные операторы, матрицы, применения к динамике, метод Гаусса * [Теорвер](http://sereja.me/a/probability) ### Геометрия * [Ликбез по вычислительной геометрии](http://sereja.me/a/geometry) скалярное и векторное произведение, пересечение прямых, классы в C++ * Выпуклые оболочки: они где-то есть +
+
diff --git a/ru/ioi.html b/ru/ioi.html index 12f628c..a9fdbfb 100644 --- a/ru/ioi.html +++ b/ru/ioi.html @@ -10,6 +10,8 @@ # IOI Upsolving Challenge https://contest.yandex.com/ioi/total Я решал их почти 2 года назад и уже не очень хорошо всё помню. # 09-2 12 мая ### Медведь Миша Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за $O(n^2 \log n)$. ### Регионы В ограничениях нам спалили важную идею: задачу можно просто решить в случае, если все регионы небольшие, а также в случае, если регионов немного. Обычно это означает, что решения можно объединить с помощью корневой эвристики. Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так: * Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк * Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос Такое должно работать за $O(n \sqrt{n \log n})$, и TL такое позволяет. ### Коммивояжёр Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать. Подумайте, какой вид будет иметь эта функция, если зафиксировать время. TODO # 05-1 31 мая ### Сад Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно. ### Средняя последовательность Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы: ```c++ int a = -inf, b = inf, _d = 0, d = 0, t; for(int i = 0; i < n; i++){ cin >> t; _d = d; d = 2*t - d; if (i % 2 == 0) b = min(b, (d-_d)/2); else a = max(a, (_d-d)/2); } cout << max(b - a + 1, 0ll); ``` ### Польские горки По сути от нас просят обрабатывать запросы «присвоить всем элементам с $l$ по $r$ значение $x$» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше. # 05-2 ### Прямоугольник Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста. +
+
diff --git a/ru/karatsuba.html b/ru/karatsuba.html index bafc1de..59422e0 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -9,7 +9,9 @@ -# Быстрое умножение В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел). [Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал. Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба. Алгоритм Карацубы имеет довольно значимое место в истории науки. {\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}). У Колмогорова была гипотеза, что нижняя оценка для {\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности {\displaystyle M(n)=O(n^{\log _{2}3})} M(n)=O(n^{{\log _{2}3}}) и тем самым опроверг «гипотезу {\displaystyle n^{2}} n^{2}». (Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.) Можно представить себе какого-то грозного японца. ## Мастер-теорема Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек». **Мастер-теорема.** Пусть имеется рекуррента: $$ T(n) = \begin{cases} a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 \\ \Theta(1), & n \leq n_0 \end{cases} $$ Тогда: * **A.** Если $c > \log_b a$, то $T(n) = \Theta(n^c)$. * **B.** Если $c = \log_b a$, то $T(n) = \Theta(n^c \log n)$. * **C.** Если $c < \log_b a$, то $T(n) = \Theta(n^{\log_b a})$. --- --- **Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням: $$ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k $$ * **A.** Если $c > \log_b a$, то $\sum (\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \Theta(n^c)$. * **B.** Если $c = \log_b a$, то $$\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)$$ * **C.** Если $c < \log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу, $$ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) $$ Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$ ## Алгоритм Карацубы Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат. Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. ## Развитие идеи То же самое можно применить матрицам. Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\epsilon})$ для произвольного $\epsilon$ ещё никто не доказал. +# Быстрое умножение В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел). [Андрей Колмогоров](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BB%D0%BC%D0%BE%D0%B3%D0%BE%D1%80%D0%BE%D0%B2,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%9D%D0%B8%D0%BA%D0%BE%D0%BB%D0%B0%D0%B5%D0%B2%D0%B8%D1%87) и несколько других пионеров компьютер саенса выдвинули «гипотезу $n^2$» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение $n$ $n$-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал. Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба. Алгоритм Карацубы имеет довольно значимое место в истории науки. {\displaystyle M(n)=O(n^{2}).} M(n)=O(n^{2}). У Колмогорова была гипотеза, что нижняя оценка для {\displaystyle M(n)} M(n) при любом методе умножения есть также величина порядка {\displaystyle n^{2}} n^{2}. На правдоподобность «гипотезы {\displaystyle n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности {\displaystyle M(n)=O(n^{\log _{2}3})} M(n)=O(n^{{\log _{2}3}}) и тем самым опроверг «гипотезу {\displaystyle n^{2}} n^{2}». (Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.) Можно представить себе какого-то грозного японца. ## Мастер-теорема Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек». **Мастер-теорема.** Пусть имеется рекуррента: $$ T(n) = \begin{cases} a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 \\ \Theta(1), & n \leq n_0 \end{cases} $$ Тогда: * **A.** Если $c > \log_b a$, то $T(n) = \Theta(n^c)$. * **B.** Если $c = \log_b a$, то $T(n) = \Theta(n^c \log n)$. * **C.** Если $c < \log_b a$, то $T(n) = \Theta(n^{\log_b a})$. --- --- **Доказательство.** Рассмотрим «дерево рекурсии» этого соотношения. В нём будет $log_b n$ уровней. На $k$-том уровне будет $a^k$ вершин, каждая из которых будет стоить $(\frac{n}{b^k})^c$ операций. Просуммируем значения во всех вершинах по всем уровням: $$ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k $$ * **A.** Если $c > \log_b a$, то $\sum (\frac{a}{b^с})^k$ это сумма убывающей геометрической прогрессии, которая не зависит от $n$ и просто равна какой-то константе. Значит, $T(n) = \Theta(n^c)$. * **B.** Если $c = \log_b a$, то $$\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)$$ * **C.** Если $c < \log_b a$, то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу, $$ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) $$ Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает $O(n)$ ## Алгоритм Карацубы Алгоритм Карацубы сводит задачу умножения двух чисел длины $n$ к возведению $n$-значного числа в квадрат. Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи. ## Развитие идеи То же самое можно применить матрицам. Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за $O(n^{1+\epsilon})$ для произвольного $\epsilon$ ещё никто не доказал. +
+
diff --git a/ru/lca.html b/ru/lca.html index dc16a63..bb989af 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -9,7 +9,9 @@ -# Корневые деревья Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*. Примеры корневых деревьев: * наследование классов в языках программирования (если множественное наследование запрещено), * дерево факторизации числа на простые (в общем случае не уникальное), * иерархия в какой-нибудь компании, * просто множество как-то вложенных объектов (ссылка на крысу-роботягу). Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах. ![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png) ## Напоминание: DFS Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. ```c++ vector g[maxn]; int p[maxn], tin[maxn], tout[maxn]; int t = 0; void dfs (int v) { tin[v] = t++; for (int u : g[v]) dfs(u); tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет } ``` У этих массивов много полезных свойств: * Вершина $u$ является предком $v$ $\iff tin_v \in [tin_u, tout_u) $. Эту проверку можно делать за константу. * Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой. * В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер. * Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$. * Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации. ## Запросы на поддеревьях Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО. > Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$. Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree). ## Запросы на уровнях > Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$). Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам. Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку. ## Наименьший общий предок Очень много задач нам поможет решить следующая вспомогательная задача. > Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких. По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные. ![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png) Для лучшего понимания: медленно (за линейное время) это можно делать так: ```c++ bool a (int u, int v) { return tin[u] <= tin[v] && tin[v] <= tout[u]; } int lca (int u, int v) { while (!ancestor(u, v)) u = p[u]; return u; } ``` ## LCA: двоичные подъемы Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \times \lceil \log n \rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень. Такой препроцессинг можно выполнить за $O(n \log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$: ```c++ int up[maxn][logn]; void dfs (int v) { for (int l = 1; l < logn; l++) up[v][l] = up[up[v][l-1]][l-1]; tin[v] = t++; for (int u : g[v]) { up[u][0] = v; dfs(u); } tout[v] = t++; } ``` Пусть поступил очередной запрос — пара вершин $(u, v)$: * Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. * Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA. Подробнее про второй пункт. Пусть $L = \lceil \log n \rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$. ```c++ int lca (int v, int u) { if (a(v, u)) return v; if (a(u, v)) return u; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l]; return up[v][0]; } ``` ### Асимптотика Препроцессинг — $O(n \log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу. Ответ на запрос — $O(\log n)$, потому что по сути мы делаем один бинпоиск. ## Запросы на путях Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа). Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути. Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA. ```c++ int get_min (int v, int u) { int ans = inf; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l], ans = min(ans, mn[v][l]); for (int l = logn-1; l >= 0; l--) if (!ancestor(up[u][l], v)) u = up[u][l], ans = min(ans, mn[u][l]); return min({ans, mn[v][0], mn[u][0]}) } ``` Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная. ## Сведение к RMQ Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить. Во втором массиве мы по сути выписали наш проход dfs-а. Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути. Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree). ### Разреженная таблица На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива. Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table). ## А наоборот можно?* *Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения. Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя. Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль. Возьмем константу $k = \lfloor \frac{\log n}{2} \rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\frac{2 n}{\log n})$, и построение будет работать за линейное время: $$O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)$$ Также посчитаем для каждой возможной маски размера $\frac{\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\sqrt n$ (ради этого мы и делили логарифм на два). ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу. Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали. Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear). Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики. Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.** +# Корневые деревья Дерево называется *корневым*, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется *корнем*. Примеры корневых деревьев: * наследование классов в языках программирования (если множественное наследование запрещено), * дерево факторизации числа на простые (в общем случае не уникальное), * иерархия в какой-нибудь компании, * просто множество как-то вложенных объектов (ссылка на крысу-роботягу). Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах. ![dfs](https://raw.githubusercontent.com/e-maxx-eng/e-maxx-eng/master/img/LCA_Euler.png) ## Напоминание: DFS Посчитаем для каждой вершины времена входа ($tin$) и выхода ($tout$) из неё во время эйлерова прохода. ```c++ vector g[maxn]; int p[maxn], tin[maxn], tout[maxn]; int t = 0; void dfs (int v) { tin[v] = t++; for (int u : g[v]) dfs(u); tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет } ``` У этих массивов много полезных свойств: * Вершина $u$ является предком $v$ $\iff tin_v \in [tin_u, tout_u) $. Эту проверку можно делать за константу. * Два полуинтервала — $[tin_v, tout_v)$ и $[tin_u, tout_u)$ — либо не пересекаются, либо вложены один в другой. * В $tin$ есть все числа из промежутка от 0 до $n-1$. У каждой вершины — свой номер. * Размер поддерева вершины $v$ (включая саму вершину) равен $tout_v - tin_v$. * Если ввести нумерацию вершин, соответствующую $tin$-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации. ## Запросы на поддеревьях Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО. > Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число $x_i$ и найти значение числа у вершины $v_i$. Давайте запишем все числа у вершин в позиции, соответствующие $tin$-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать [какой-нибудь достаточно продвинутой структурой](http://sereja.me/a/segtree). ## Запросы на уровнях > Дано корневое дерево. Требуется отвечать на запросы нахождения $d_i$-того предка вершины $v_i$ (т. е. вершины-предка, находящейся на расстоянии $d_i$). Создадим $h$ векторов, где $h$ — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их $tin$-ам. Теперь заметим, что отрезки их поддеревьев — $[tin_v, tout_v)$ — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять $tin$ вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку. ## Наименьший общий предок Очень много задач нам поможет решить следующая вспомогательная задача. > Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин $u_i$ и $v_i$, то есть вершины $w$, которая лежит на пути от корня до $u_i$, на пути от корня до $v_i$, и при этом самую глубокую (нижнюю) из всех таких. По-английский эта задача называется *Least Common Ancestor*. Есть много разных способов её решать, и мы рассмотрим основные. ![lca](http://homepages.kcbbs.gen.nz/tonyg/pictures/least-common-ancestor.png) Для лучшего понимания: медленно (за линейное время) это можно делать так: ```c++ bool a (int u, int v) { return tin[u] <= tin[v] && tin[v] <= tout[u]; } int lca (int u, int v) { while (!ancestor(u, v)) u = p[u]; return u; } ``` ## LCA: двоичные подъемы Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве `up` размера $n \times \lceil \log n \rceil$ — столько точно хватит. В `up[v][d]` будет храниться предок вершины $v$ на расстоянии $2^d$, а если такой вершины не существует — то корень. Такой препроцессинг можно выполнить за $O(n \log n)$, используя тот факт, что предок на расстоянии $2^{d+1}$ — это предок на расстоянии $2^d$ предка на расстоянии $2^d$: ```c++ int up[maxn][logn]; void dfs (int v) { for (int l = 1; l < logn; l++) up[v][l] = up[up[v][l-1]][l-1]; tin[v] = t++; for (int u : g[v]) { up[u][0] = v; dfs(u); } tout[v] = t++; } ``` Пусть поступил очередной запрос — пара вершин $(u, v)$: * Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. * Иначе, пользуясь массивом `up`, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA. Подробнее про второй пункт. Пусть $L = \lceil \log n \rceil$. Присвоим $i = L$. Будем уменьшать эту переменную на единицу, пока `up[v][i]` не перестанет быть предком $u$ (указатель `up[v][i]` изначально будет корнем, а затем каждую итерацию спускаться на $2^i$). Когда это произойдёт, подвинем указатель на $2^i$-го предка $v$, и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на $2^{i+1}$. ```c++ int lca (int v, int u) { if (a(v, u)) return v; if (a(u, v)) return u; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l]; return up[v][0]; } ``` ### Асимптотика Препроцессинг — $O(n \log n)$. Размер массива `up` ровно такой, и каждый его элемент вычисляется за константу. Ответ на запрос — $O(\log n)$, потому что по сути мы делаем один бинпоиск. ## Запросы на путях Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа). Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером $2^d$-го предка минимум на соответствующем пути. Мы знаем, что минимум на пути от $u$ до $v$ — это минимум от минимума на пути от $u$ до $lca(u, v)$ и от минимума на пути от $v$ до $lca(u, v)$. А каждый минимум — это минимум на всех двоичных подъемах до LCA. ```c++ int get_min (int v, int u) { int ans = inf; for (int l = logn-1; l >= 0; l--) if (!ancestor(up[v][l], u)) v = up[v][l], ans = min(ans, mn[v][l]); for (int l = logn-1; l >= 0; l--) if (!ancestor(up[u][l], v)) u = up[u][l], ans = min(ans, mn[u][l]); return min({ans, mn[v][0], mn[u][0]}) } ``` Аналогичным образом можно считать сумму, `gcd`, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная. ## Сведение к RMQ Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить. Во втором массиве мы по сути выписали наш проход dfs-а. Пусть у нас есть запрос: найти LCA вершин $v$ и $u$. Для определенности положим, что $tin_v < tin_u$. Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из $v$ и в первй раз вошли в $u$. Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути. Получается, что можно найти LCA, просто найдя позицию минимума на отрезке $[tout_v, tin_u]$ в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы [уже умеем](http://sereja.me/a/segtree). ### Разреженная таблица На практике асимптотику мы особо не улучшили — пока что всё равно требуется $O(n \log n)$ времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива. Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за $O(1)$, но использующая $O(n \log n)$ препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в [отдельной статье](http://sereja.me/a/sparse-table). ## А наоборот можно?* *Примечание*: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения. Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до $n$, а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя. Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера $n-1$: единица стоит, если следующее значение больше, единица в противном случае ноль. Возьмем константу $k = \lfloor \frac{\log n}{2} \rfloor$, и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков $O(\frac{2 n}{\log n})$, и построение будет работать за линейное время: $$O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)$$ Также посчитаем для каждой возможной маски размера $\frac{\log n}{2}$ минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего $\sqrt n$ (ради этого мы и делили логарифм на два). ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу. Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали. Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать [у Емакса](http://e-maxx.ru/algo/lca_linear). Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики. Важный вывод такой: RMQ более общая задача, чем LCA. **UPD: это неправда, я глупый.** +
+
diff --git a/ru/linalg.html b/ru/linalg.html index 56cf471..f1aede1 100644 --- a/ru/linalg.html +++ b/ru/linalg.html @@ -9,7 +9,10 @@ -# Ликбез по линейной алгебре **Определение**. Функция $f: \mathbb{R}^n \to \mathbb{R}^m$ называется *линейной*, если для неё выполнено 1. $ f(x+y) = f(x) + f(y) $ 2. $ f(ax) = a f(x), \; a \in R $ Примеры: * $ f(x) = 0 $ * $ f(x) = x $ * $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\mathbb{R}^3$ в $\mathbb{R}$) * $ f(x) = (x, -x, 0) $ (из $\mathbb{R}$ в $\mathbb{R}^3$) Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: * Сумма линейных функций — линейная функция. * Сумма коммутативна: $f+g = g+f$). * Сумма ассоциативна: $(f+g)+h = f+(g+h)$. * Композиция $f(g(x)) = (f \circ g)(x)$ линейных функций — линейная функция. * Композция ассоциативна: $(f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h$. * Композиция в общем случае не коммутативна.
Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен. Все свойства можно вывести лишь из этих двух пунктов в определении. ## Что такое матрица? Можно показать, что любую линейную функцию $f: \mathbb{R}^n \to \mathbb{R}^m$ можно представить в таком виде: $$ f(x) = \begin{pmatrix} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ \ldots \\ a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ \end{pmatrix} $$ *Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$. $$ A = \begin{pmatrix} a_{11} & a_{12} & \ldots & a_{1n} \\ a_{21} & a_{22} & \ldots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \ldots & a_{mn} \\ \end{pmatrix} $$ Каждой линейной функции из $\mathbb{R}^n$ в $\mathbb{R}^m$ соответствует какая-то матрица размера $n \times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте. Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так: $$ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} $$ Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$. Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \times k$, то вторая должна иметь размер $k \times m$, то есть «средние» размерности обязательно должны совпадать. Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом. К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы. Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы: * Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$. * Сумма коммутативна: $A+B = B+A$) * Сумма ассоциативна: $(A+B)+C = A+(B+C)$ * Умножение ассоциативно: $(AB)C = A(BC) = ABC$. * Умножение в общем случае не коммутативно. **Пример**: матрица поворота в 2d. $$ \begin{pmatrix} \cos \alpha & -\sin \alpha \\ \sin \alpha & \cos \alpha \\ \end{pmatrix} $$ **Пример**: матрица проецирования на $Ox$ в 3d. $$ \begin{pmatrix} 1 & 0 & 0 \\ 0 & 0 & 0 \\ 0 & 0 & 0 \\ \end{pmatrix} $$ **Пример**: матрица «свапни $x$ и $y$». $$ \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ \end{pmatrix} $$ Напишем класс, который реализует матричное умножение. ```c++ struct matrix { int n, m; int t[]; matrix (int _n, int _m) { n = _n, m = _m; t = new int(n*m); memset(t, 0, sizeof t); } int[] operator[] (int k) { return t[k*m]; } } matrix operator* (matrix a, matrix b) { matrix c(a.n, b.m); for (int i = 0; i < a.n; i++) for (int j = 0; j < b.m; j++) for (int k = 0; k < a.m; k++) c[i][j] += a[i][k] * b[i][k]; return c; } ``` ## Динамика Некоторые динамики можно выразить в терминах матричного умножения. Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика? $$ \begin{pmatrix} f_{n+1} \\ f_{n+2} \\ \end{pmatrix} = \begin{pmatrix} 0+f_{n+1} \\ f_{n}+f_{n+1} \\ \end{pmatrix} = \begin{pmatrix} 0 & 1 \\ 1 & 1 \\ \end{pmatrix} \begin{pmatrix} f_{n} \\ f_{n+1} \\ \end{pmatrix} $$ Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$. Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам: $$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$ Это будет работать за $O(n^3 \log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны. ```c++ matrix binpow (matrix a, int p) { matrix b(n, n); for (int i = 0; i < n; i++) b[i][i] = 1; while (p) { if (p&1) b = b*a; a = a*a; p >>= 1; } return b; } ``` Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$. $$ \begin{vmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{vmatrix} $$ В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы. В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}$ имеет такую матрицу перехода: \begin{pmatrix} 0 & 1 & 0 & \ldots & 0 \\ 0 & 0 & 1 & \ldots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \ldots & 1 \\ a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ \end{pmatrix} ## Матрица смежности У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов. Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86)). ## Обобщения Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел. Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два. ## Определитель *Определителем* кадратной матрицы $A$ называется такое выражение: $$ \det A = |A| = \ldots $$ Где $\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например: $$ \begin{vmatrix} a & b \\ c & d \\ \end{vmatrix} = ad - bc $$ У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться. В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем. ## Базис Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. Базисы есть не только в линейной алгебре. Например, $\{1, x, x^2\}$ является базисом всех квадратных трёхчленов. Или $\{\neg, \land, \lor\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом. ## Метод Крамера и easy пересечение прямых Пусть нам надо пересечь две прямые. $$ \begin{cases} a_1 x + b_1 y + c_1 = 0 a_2 x + b_2 y + c_2 = 0 \end{cases} $$ Это то же самое, что найти такие коэффициенты $x$ и $y$, что $$ x \vec{a} + y \vec{b} = -\vec{c} $$ Площадь параллелограмма, натянутого на $\vec{a}$ и $\vec{b}$, равна векторному произведению, или детерминанту. По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$. Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да? ## *Собственные векторы Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление. $ Av = k v $, где $k \neq 0$. $ Av - kv = (A-kI)v = 0 $. Это означает Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике. ## Системы уравнений и метод Гаусса Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. Нас по сути просят решить следующую систему: $$ \begin{cases} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ \ldots \\ a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 \end{cases} $$ Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. Метод Крамера неоптимален — там $O(n^4)$ операций. В таком случае можно значительно ускорить и упростить обычный метод Гаусса: ```c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; } ``` Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81)). +# Ликбез по линейной алгебре **Определение**. Функция $f: \mathbb{R}^n \to \mathbb{R}^m$ называется *линейной*, если для неё выполнено 1. $ f(x+y) = f(x) + f(y) $ 2. $ f(ax) = a f(x), \; a \in R $ Примеры: * $ f(x) = 0 $ * $ f(x) = x $ * $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из $\mathbb{R}^3$ в $\mathbb{R}$) * $ f(x) = (x, -x, 0) $ (из $\mathbb{R}$ в $\mathbb{R}^3$) Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: * Сумма линейных функций — линейная функция. * Сумма коммутативна: $f+g = g+f$). * Сумма ассоциативна: $(f+g)+h = f+(g+h)$. * Композиция $f(g(x)) = (f \circ g)(x)$ линейных функций — линейная функция. * Композция ассоциативна: $(f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h$. * Композиция в общем случае не коммутативна.
+Пример: $f = (-x_2, x_1)$ — поворот точки на плоскости на прямой угол, $g = (x_1, 0)$ — проекция на $Ox$. Почти для всех точек порядок этих операций важен. Все свойства можно вывести лишь из этих двух пунктов в определении. ## Что такое матрица? Можно показать, что любую линейную функцию $f: \mathbb{R}^n \to \mathbb{R}^m$ можно представить в таком виде: $$ f(x) = \begin{pmatrix} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ \ldots \\ a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ \end{pmatrix} $$ *Матрицы* ввели просто как очень компактную запись этих коэффициентов $a_{ij}$. $$ A = \begin{pmatrix} a_{11} & a_{12} & \ldots & a_{1n} \\ a_{21} & a_{22} & \ldots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{m1} & a_{m2} & \ldots & a_{mn} \\ \end{pmatrix} $$ Каждой линейной функции из $\mathbb{R}^n$ в $\mathbb{R}^m$ соответствует какая-то матрица размера $n \times m$ (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении $i$-го строки и $j$-го столбца будем обозначать $A_{ij}$. Не перепутайте. Пусть линейной функции $f$ соответствует матрица $A$, а функции $g$ — $B$. Тогда композиции этих функций $h = f \circ g$ будет соответствовать *произведение* $C$ матриц $A$ и $B$, определяемое так: $$ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} $$ Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из $g$ подставить в $f$. Когда перемножаете руками, удобно думать так: элемент на пересечении $i$-го столбца и $j$-той строки — это скалярное произведение $i$-той строки $A$ и $j$-того столбца $B$. Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер $n \times k$, то вторая должна иметь размер $k \times m$, то есть «средние» размерности обязательно должны совпадать. Исходное выражение для $f(x)$ теперь можно компактно записать как $f(x) = Ax$ вместо $m$ уравнений с $n$ слагаемыми в каждом. К матрицам **не** нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы. Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы: * Сумма матриц $A$ и $B$ — матрица $C = A+B: C_{ij} = A_{ij} + B_{ij}$. * Сумма коммутативна: $A+B = B+A$) * Сумма ассоциативна: $(A+B)+C = A+(B+C)$ * Умножение ассоциативно: $(AB)C = A(BC) = ABC$. * Умножение в общем случае не коммутативно. **Пример**: матрица поворота в 2d. $$ \begin{pmatrix} \cos \alpha & -\sin \alpha \\ \sin \alpha & \cos \alpha \\ \end{pmatrix} $$ **Пример**: матрица проецирования на $Ox$ в 3d. $$ \begin{pmatrix} 1 & 0 & 0 \\ 0 & 0 & 0 \\ 0 & 0 & 0 \\ \end{pmatrix} $$ **Пример**: матрица «свапни $x$ и $y$». $$ \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ \end{pmatrix} $$ Напишем класс, который реализует матричное умножение. ```c++ struct matrix { int n, m; int t[]; matrix (int _n, int _m) { n = _n, m = _m; t = new int(n*m); memset(t, 0, sizeof t); } int[] operator[] (int k) { return t[k*m]; } } matrix operator* (matrix a, matrix b) { matrix c(a.n, b.m); for (int i = 0; i < a.n; i++) for (int j = 0; j < b.m; j++) for (int k = 0; k < a.m; k++) c[i][j] += a[i][k] * b[i][k]; return c; } ``` ## Динамика Некоторые динамики можно выразить в терминах матричного умножения. Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика? $$ \begin{pmatrix} f_{n+1} \\ f_{n+2} \\ \end{pmatrix} = \begin{pmatrix} 0+f_{n+1} \\ f_{n}+f_{n+1} \\ \end{pmatrix} = \begin{pmatrix} 0 & 1 \\ 1 & 1 \\ \end{pmatrix} \begin{pmatrix} f_{n} \\ f_{n+1} \\ \end{pmatrix} $$ Обозначим за $A$ эту матрицу перехода. Чтобы посчитать $n$-е число Фибоначчи, нужно применить $n$ раз эту матрицу к вектору $(f_0, f_1) = (0, 1)$. Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам: $$ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) $$ Это будет работать за $O(n^3 \log n)$. Мы делаем $O(n^3)$ операций для одного умножения, а всего их нужно сделать $O(\log n)$. Кстати, наука знает и [более быстрые](https://en.wikipedia.org/wiki/Strassen_algorithm) способы перемножить матрицы, но на контестах они не нужны. ```c++ matrix binpow (matrix a, int p) { matrix b(n, n); for (int i = 0; i < n; i++) b[i][i] = 1; while (p) { if (p&1) b = b*a; a = a*a; p >>= 1; } return b; } ``` Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: $I$. $$ \begin{vmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \\ \end{vmatrix} $$ В плане умножения она действительно ведет себя как единица: $AI = A = IA$. В коде она используется вместо единицы. В общем случае, линейная рекуррента $f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}$ имеет такую матрицу перехода: \begin{pmatrix} 0 & 1 & 0 & \ldots & 0 \\ 0 & 0 & 1 & \ldots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \ldots & 1 \\ a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ \end{pmatrix} ## Матрица смежности У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины $a$ в вершину $b$ за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из $a$ в $b$, используя ровно $n$ переходов. Когда нам нужна только информация, можно ли дойти из $a$ в $b$ (количество способов не важно), то решение можно ускорить (см. [Битсет](http://sereja.me/a/bitset#%D0%9F%D0%B5%D1%80%D0%B5%D0%BC%D0%BD%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BC%D0%B0%D1%82%D1%80%D0%B8%D1%86)). ## Обобщения Всё, что мы сказали, на самом деле работает не только для действительных чисел. Например, ту же логику можно применить для чисел. Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два. ## Определитель *Определителем* кадратной матрицы $A$ называется такое выражение: $$ \det A = |A| = \ldots $$ Где $\epsilon$ — чётность числа инверсий в перестановке ($-1$ или $+1$). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например: $$ \begin{vmatrix} a & b \\ c & d \\ \end{vmatrix} = ad - bc $$ У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться. В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем. ## Базис Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их. Базисы есть не только в линейной алгебре. Например, $\{1, x, x^2\}$ является базисом всех квадратных трёхчленов. Или $\{\neg, \land, \lor\}$ является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом. ## Метод Крамера и easy пересечение прямых Пусть нам надо пересечь две прямые. $$ \begin{cases} a_1 x + b_1 y + c_1 = 0 a_2 x + b_2 y + c_2 = 0 \end{cases} $$ Это то же самое, что найти такие коэффициенты $x$ и $y$, что $$ x \vec{a} + y \vec{b} = -\vec{c} $$ Площадь параллелограмма, натянутого на $\vec{a}$ и $\vec{b}$, равна векторному произведению, или детерминанту. По сути, нам нужно выразить $c$ в другом базисе. Давайте спроецируем её на $a$. Аналогично, напрягите воображение и спроецируйте эту точку в $n$-мерном пространстве. Это уже сложно, да? ## *Собственные векторы Очень часто у матриц есть *собственные вектора* -- те, которые не меняют направление. $ Av = k v $, где $k \neq 0$. $ Av - kv = (A-kI)v = 0 $. Это означает Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике. ## Системы уравнений и метод Гаусса Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем $\mathbb{Z}_2$ — то есть все числа по модулю 2. К примеру: есть $n$ переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из $n$ лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей. Нас по сути просят решить следующую систему: $$ \begin{cases} a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ \ldots \\ a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 \end{cases} $$ Здесь $x$ — состояния переключателей, $b$ — состояния лампочек, $A$ — информация о том, влияет ли переключатель на лампочку. Метод Крамера неоптимален — там $O(n^4)$ операций. В таком случае можно значительно ускорить и упростить обычный метод Гаусса: ```c++ t gauss (matrix a) { for (int i = 0; i < n; i++) { int nonzero = i; for (int j = i+1; j < n; j++) if (a[j][i]) nonzero = j; swap(a[nonzero], a[i]); for (int j = 0; j < n; j++) if (j != i && a[j][i]) a[j] ^= a[i]; } t x; for (int i = 0; i < n; i++) x[i] = a[i][n] ^ a[i][i]; return x; } ``` Код находит вектор $x$ из уравнения $Ax = b$ при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор $b$ приписан справа к матрице $A$. Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. [Битсет](http://sereja.me/a/bitset#%D0%93%D0%B0%D1%83%D1%81%D1%81)). +
+
diff --git a/ru/matching.html b/ru/matching.html index 5a7e2b5..b77dd9d 100644 --- a/ru/matching.html +++ b/ru/matching.html @@ -9,7 +9,9 @@ -# Паросочетания Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом. **Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$). Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием. *Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны. Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$. **Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер. **Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию. **Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию. ![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg) *Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \to 8 \to 4 \to 6 \to 3 \to 7$.* Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу. В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу. **Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже. ```c++ const int maxn; vector g[maxn]; // будем хранить только рёбра из левой доли в правую int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой) bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом // dfs возвращает, можно ли найти путь из вершины v // в какую-нибудь вершину правой доли // если можно, то ещё и проводит чередование bool dfs (int v) { if (used[v]) return false; used[v] = true; for (int u : g[v]) { // если вершина свободна, то можно сразу с ней соединиться // если она занята, то с нейможно соединиться только тогда, // когда из её текущей пары можно найти какую-нибудь другую вершину if (mt[u] == -1 || dfs(mt[u])) { mt[u] = v; return true; } } return false; } // где-то в main: memset(mt, -1, sizeof(mt)); for (int i = 0; i < n; i++) { memset(used, 0, sizeof(mt)); if (dfs(i)) cnt++; } ``` ## Корректность Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить. **Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным. **Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие. ## Скорость работы Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$. Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \approx 10^4$), потому что для него есть мощные неасимптотические оптимизации: * Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть). * Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу. * Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются. Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \sqrt m)$. ## Покрытие путями DAG-а Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути. Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\overline{H}$ следующим образом: * В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно. * Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$. * Теперь из графа $H$ сделаем граф $\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$. Если мы рассмотрим любой путь $v_1, v_2, \ldots, v_k$ в исходном графе $G$, то в графе $\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$. Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\overline{H}$ — это паросочетание в $H$ (напомним, это $\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$. Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них. Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин. ## Лемма Холла *Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно. ![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif) **Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли. **Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием. В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу. **База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. **Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием. Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна) Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$). Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно. ## Для ноулайферов: матроиды С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid). Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*. **Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна. +# Паросочетания Пусть есть $n$ мальчиков и $m$ девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом. **Паросочетанием** $M$ называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из $M$). Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном $M$), назовём *насыщенными* этим паросочетанием. *Мощностью* паросочетания назовём количество рёбер в нём. *Наибольшим* (*максимальным*) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а *совершенным* — где все вершины левой доли им насыщенны. Паросочетания [можно искать](http://e-maxx.ru/algo/matching_edmonds) в любых графах, однако этот алгоритм неприятно кодить, и он работает за $O(n^3)$, так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как $L$, а правую долю как $R$. **Цепью** длины $k$ назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно $k$ рёбер. **Чередующейся цепью** относительно некоторого паросочетания назовём простой путь длины $k$ в которой рёбра поочередно принадлежат/не принадлежат паросочетанию. **Увеличивающей цепью** относительно некоторого паросочетания назовём чередующуюся цепь, у которой начальная и конечная вершины не принадлежат паросочетанию. ![](https://neerc.ifmo.ru/wiki/images/4/4a/Alternating_path.jpg) *Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: $1 \to 8 \to 4 \to 6 \to 3 \to 7$.* Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести **чередование** — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу. В примере добавятся синие рёбра $(1, 8)$, $(3, 7)$ и $(4, 6)$, а удалятся красные $(3, 6)$ и $(4, 8)$. С ребром $(2, 5)$ ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу. **Алгоритм Куна** в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже. ```c++ const int maxn; vector g[maxn]; // будем хранить только рёбра из левой доли в правую int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой) bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом // dfs возвращает, можно ли найти путь из вершины v // в какую-нибудь вершину правой доли // если можно, то ещё и проводит чередование bool dfs (int v) { if (used[v]) return false; used[v] = true; for (int u : g[v]) { // если вершина свободна, то можно сразу с ней соединиться // если она занята, то с нейможно соединиться только тогда, // когда из её текущей пары можно найти какую-нибудь другую вершину if (mt[u] == -1 || dfs(mt[u])) { mt[u] = v; return true; } } return false; } // где-то в main: memset(mt, -1, sizeof(mt)); for (int i = 0; i < n; i++) { memset(used, 0, sizeof(mt)); if (dfs(i)) cnt++; } ``` ## Корректность Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить. **Теорема (Бержа)**. Паросочетание без увеличивающих цепей является максимальным. **Доказательство** проведём от противного: пусть есть два паросочетания вершин $|A| \leq |B|$, и для $A$ нет увеличивающих путей, и покажем, как найти этот путь и увеличить $A$ на единицу. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для $A$, а значит $A$ не оптимальное, и мы получили противоречие. ## Скорость работы Такой алгоритм ровно $n$ раз ищет увеличивающий путь, каждый раз просматривая не более $m$ рёбер, а значит работает за $O(nm)$. Что примечательно, его можно не бояться запускать на ограничениях и побольше ($n, m \approx 10^4$), потому что для него есть мощные неасимптотические оптимизации: * Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть). * Можно не заполнять нулями на каждой итерации массив `used`, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало `true`. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как `false`. В каком-то смысле это позволяет эмулировать очищение массива за константу. * Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы $n \times m$. Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются. Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить [алгоритм Диница](http://e-maxx.ru/algo/dinic) к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за $O(n \sqrt m)$. ## Покрытие путями DAG-а Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф $G$ (англ. *directed acyclic graph*). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути. Построим соответствующие изначальному графу $G$ два двудольных графа $H$ и $\overline{H}$ следующим образом: * В каждой доле графа $H$ будет по $n$ вершин. Обозначим их через $a_i$ и $b_i$ соответственно. * Для каждого ребра $(i, j)$ исходного графа $G$ проведём соответствующее ребро $(a_i, b_j)$ в графе $H$. * Теперь из графа $H$ сделаем граф $\overline{H}$, добавив обратное ребро $(b_i, a_i)$ для каждого $i$. Если мы рассмотрим любой путь $v_1, v_2, \ldots, v_k$ в исходном графе $G$, то в графе $\overline{H}$ ему будет соответствовать путь $a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}$. Обратное тоже верно: любой путь, начинающийся в левой доле $\overline{H}$ и заканчивающийся в правой будет соответствовать какому-то пути в $G$. Итак, есть взаимно однозначное соответствие между путями в $G$ и путями $\overline{H}$, идущими из левой доли в правую. Заметим, что любой такой путь в $\overline{H}$ — это паросочетание в $H$ (напомним, это $\overline{H}$ без обратных рёбер). Получается, любому пути из $G$ можно поставить в соответствие паросочетание в $H$, и наоборот. Более того, непересекающимся путям в $G$ соответствуют непересекающиеся паросочетания в $H$. Заметим, что если есть $p$ непересекающихся путей, покрывающих все $n$ вершин графа, то они вместе содержат $r = n - p$ рёбер. Отсюда получаем, что чтобы минимизировать число путей $p$, мы должны максимизировать число рёбер $r$ в них. Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе $H$. После нахождения этого паросочетания мы должны преобразовать его в набор путей в $G$. Это делается тривиальным алгоритмом: возьмем $a_1$, посмотрим, с какой $b_k$ она соединена, посмотрим на $a_k$ и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин. ## Лемма Холла *Лемма Холла* (или: *теорема о свадьбах*) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно. ![](https://neerc.ifmo.ru/wiki/images/2/2f/Aba.gif) **Лемма Холла**. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли. **Доказательство**. В одну сторону понятно — если совершенное паросочетание есть, то для любого подмножества вершин левой доли можно взять вершины правой, соединенные с ним паросочетанием. В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу. **База индукции**: одна вершина из $L$, которая по условию соединена с хотя бы одной вершиной из $R$. **Индукционный переход**: пусть после $k < n$ шагов построено паросочетание $M$. Докажем, что в $M$ можно добавить вершину $v$ из $L$, не насыщенную паросочетанием. Рассмотрим множество вершин $H$ — все вершины, достижимые из $x$, если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна) Тогда в $H$ найдется вершина $y$ из $R$, не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины $H_L$ (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что $|H_L| \leq |N(H_L)|$ (здесь $N(X)$ — множество вершин, соединенным паросочетанием с $X$). Тогда должен существовать путь из $x$ в $y$, и он будет увеличивающим для паросочетания $M$, потому что из $R$ в $L$ мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно. ## Для ноулайферов: матроиды С весьма большой вероятностью матроиды вам никогда не пригодится в школьных олимпиадах, однако, если вам совсем нечего делать, [можете про них почитать](http://sereja.me/a/matroid). Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию *хороших подмножеств* (паросочетаний) обобщает понятие **матроида**. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше *хорошее* множество, если оно после добавления остается *хорошим*. **Применимо к паросочетаниям**: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна. +
+
diff --git a/ru/matroid.html b/ru/matroid.html index 1f51643..e7124d3 100644 --- a/ru/matroid.html +++ b/ru/matroid.html @@ -10,6 +10,8 @@ # Матроиды Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры). **Матроидом** называется пара $(X, I)$, где $X$ — множество элементов, называемое **носителем матроида**, а $I$ — некоторое множество подмножеств $X$, называемое **семейством независимых множеств**. В матроиде должны выполняться следующие свойства: * Пустое множество является независимым: $\varnothing \in I$ * Любое подмножество независимого множества тоже независимо: $$A \subset B, B \in I \implies A \in I$$ * Если в независимом множестве $A$ меньше элементов, чем в независимом множестве $B$, то будет существовать элемент из $B$, дополняющий $A$ до независимого множества размера $|A|+1$: $$A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I$$ Матроид называется **взвешенным**, если на нем существует аддитивная весовая функция: $w(A) = \sum w(a_i)$. Пусть нам нужно найти независимое множество, которое будет включать как можно больше элементов и при этом иметь как можно меньший вес. Утверждается, что можно просто отсортировать элементы по возрастанию весов и пытаться в таком порядке добавлять их в ответ: ```python X.sort() s = [] for x in X: if good(s + [x]): s += [x] ``` Здесь под `good` имеется в виду $s \cup x \in I$. Корректность этого алгоритма для любого матроида доказывает следующая теорема: ## Теорема Радо-Эдмондса Пусть $A \in I$ — множество минимального веса среди всех независимых подмножеств $X$ мощности $k$. Возьмем $x: A \cup x \in I,\;x \notin A,\;w(x)$ — минимальна. Тогда $A \cup x$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. *Доказательство: ** Рассмотрим $B$ — множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. Из свойств матроида: $\exists y \in B \setminus A : A \cup y \in I$. Тогда верны два неравенства: $$ \begin{cases} w(A \cup y) = w(A) + w(y) \geq w(B) \implies w(A) \geq w(B) - w(y) \\ w(B \setminus y) = w(B) - w(y) \geq w(A) \implies w(A) \leq w(B) - w(y) \end{cases} $$ Величина $w(A)$ с двух сторон ограничивает величину $w(B) - w(y)$. Значит, они равны. Cледовательно, $w(A \cup y) = w(A) + w(y) = w(B)$. Получаем, что если объединить множество $A$ с $x$ — минимальным из таких, что $A \cup x \in I$, — то получим множество минимального веса среди независимых подмножеств $X$ мощности $k + 1$. Иными словами, если у нас есть оптимальное $k$-элементарное независимое множество, то мы можем индуктивно построить оптимальное $(k+1)$-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым. ## Примеры Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее. ### Минимальный остов Рассмотрим неориентированный граф $G = (V, E)$. Пусть $I$ — множество лесов графа (ациклических подмножеств $E$). Тогда $M = (E, I)$ является матроидом: * Граф без ребер является лесом. * Если удалить из леса ребра, он останется лесом. * Пусть есть два леса $|A| \leq |B|$. В $A$ будет $|V| - |A|$ компонент связности, в $B$ будет $|V|-|B|$ компонент связности. Так как в $B$ компонент связности меньше, то будет существовать какое-то ребро $x$, связывающее две компоненты связности из $A$. Его и возьмем: $A \cup \{x\}$ тоже будет лесом, так как $x$ только соединило две разные компоненты связности. Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова. ### Расписания Пусть у нас есть $n$ заданий, на выполнение каждого требуется $1$ час. Награда за выполнение $i$-го задания не позже $d_i$-того часа равна $w_i$. В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград. Назовём *правильными* те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам ($d_i$) и проверить, что $d_i \geq i$ для всех $i$. Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом: * Пустой набор заданий всегда можно сделать. * Если у нас стало меньше заданий, то их сделать мы тоже успеем. * Пусть есть два правильных набора $|A| \leq |B|$. Тогда в $B$ будет существовать задание $x$ с дедлайном позже $|A|$. Все задания $A$ можно сделать не позже $|A|$-го часа, а в $(|A|+1)$-й час будем делать $x$. Значит, $A \cup x$ — тоже правильный набор. Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных. ### Паросочетания Рассмотрим двудольный граф $G = (L, R, E)$. Пусть $I$ — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда $M = (L, I)$ является матроидом: * Любое паросочетание покрывает пустое множество вершин. * Исходное паросочетание покрывает также и любое подмножество исходных вершин. * Пусть есть два множества вершин $|A| \leq |B|$. Раскрасим ребра из паросочетания, соответствующего $A$ в красный цвет, $B$ — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству $A$ добавилась какая-то вершина из левой доли, принадлежавшая ранее $B$. Пусть у вершин левой доли есть веса, и нам нужно найти максимальное паросочетание минимального веса. Согласно теореме, мы можем отсортировать вершины левой доли по их весам, а затем пытаться добавлять их в паросочетание. Сделать это можно стандартным алгоритмом Куна. ### Линейно независимые вектора (TODO) (Школьники не обязаны знать линал.) Такие штуки будем называть базисами. * Ноль есть в любом базисе. * Подмножество базиса — базис. * ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора $B$ лежат в $A$. Значит, размерность $B$ уж точно не больше. +
+
diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html index 9219954..7a5123c 100644 --- a/ru/mincost-maxflow.html +++ b/ru/mincost-maxflow.html @@ -10,6 +10,8 @@ # Поток минимальной стоимости Рассмотрим ориентированный граф $G = (V, E)$ с истоком $s$ и стоком $t$, в котором у каждого ребра $(u, v)$ задана целая стоимость $w_{uv}$ и целая положительная пропускная способность $c_{uv}$. Требуется найти максимальный поток, стоимость которого минимальна: $$ \sum_{(u, v) \in E} f_{uv} \to \max $$ $$ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min $$ Заметим, что рёбра отрицательной стоимости по условию возможны. Мы дополнительно предполагаем, что циклов отрицательного веса нет. Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра $(u, v)$ добавим $(v, u)$, для которого $c_{vu} = 0$ и $w_{vu} = -w_{uv}$. Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая ($c_{uv}-f_{uv} > 0$). ## Критерий оптимальности Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот). **Доказательство:** $\rightarrow$ Рассмотрим произвольный неоптимальный поток $f$ и оптимальный поток $f^*$. Рассмотрим разность $f^*-f$. Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость $f^*$ меньше стоимости $f$, что противоречит предположению. $\leftarrow$ Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости. ### Отмена циклов Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более $mUC$ раз где $U$ — величина потока, $C$ — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу. Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит $O(m^2nUC)$ (предполагая, что какой-нибудь максимальный поток мы уже нашли). ## Дополняющие пути Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости. **Утверждеие.** Алгоритм не создает в остаточной сети циклов отрицательного веса. Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из $s$ в $t$ и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро $(v, u)$ находится в цикле отрицательного веса. Тогда есть путь Из $u$ в $v$ стоимости меньше, чем $w_{uv}$. Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра $(u, v)$. Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит $O(nmU)$ — искать каждый дополняющий путь мы будем не более $U$ раз. Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер. ## Потенциалы Джонсона Потенциалом вершины $v$ будем называть расстояние $d_v$ от вершины $s$. Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами: $$ w_{uv}' = w_{uv} + d_u - d_v $$ **Утверждение 1**. Веса всех рёбер графа неотрицательные. **Доказательство.** Пусть вес какого-то ребра $(u, v)$ отрицателен, то есть $w_{uv}' = w_{uv} + d_u - d_v < 0$. Тогда $d_u + w_{uv} < d_v$, и нарушилось неравенство треугольника: почему мы тогда не использовали ребро $(u, v)$, когда искали кратчайший путь до $v$? Аналогично можно показать, что рёбра на кратчайших путях из $s$ имеют нулевую стоимость. Заметим, что стоимость *обратных* рёбер на кратчайших путях тоже будет нулевой: $$ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 $$ **Утверждение 2**. Кратчайшие пути между любыми вершинами остались кратчайшими. **Доказательство**. Распишем новую стоимость пути из $a$ в $z$. $$ \begin{align} w_{ab}' + \ldots + w_{yz}' &= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) \\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z \end{align} $$ Получаем, что стоимость всех путей из $a$ в $z$ лишь изменилась на константу. Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути. Заметьте, что в доказательстве мы не использовали то, что $d_v$ — кратчайшие расстояния. Это вообще могут быть произвольные числа. **Утверждение 3**. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими). **Доказательство**. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2). ## Итоговый алгоритм * Модифицируем сеть, добавивив обратные рёбра. * Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю. * Пока максимальный поток не найден: * * Посчитать алгоритмом Дейкстры кратчайшие расстояния от $s$, используя для веса формулу с потенциалами, записать их в $d$. * * Протолкнуть максимально возможный поток вдоль кратчайшего пути $s \leadsto t$, обновить остаточную сеть. ### Асимптотика Алгоритм работает за $O(U m \log n)$ или $O(U n^2)$ в случае плотных графов. В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и $U \leq n$ или $U \leq m$. Например, в задаче о назначениях (паросочетание минимального веса) $U = n$ и алгоритм работает за $O(n^3)$, что совпадает с асимптикой венгерского алгоритма. ### Реализация Решение задачи о назначениях за $O(n^3)$. Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию). * `cost`, `cap` — параметры сети * `pot` — потенциалы * `par` — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока) * `d` — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния ```c++ const int maxn = 305, inf = 1e9; int n; int cost[maxn][maxn], cap[maxn][maxn]; int d[maxn], pot[maxn], par[maxn]; bool dijkstra (int s, int t) { used[maxn] = {0}; fill(d, d+n, inf); d[s] = 0; while (1) { int v = -1; for (int u = 0; u < n; u++) if (!used[u] && (v == -1 && d[u] < d[v])) v = u; if (v == -1 || d[v] == inf) break; used[v] = 1; for (int u = 0; u < n; u++) { int w = cost[v][u] + pot[v] - pot[u]; if (cap[v][u] && d[u] > d[v] + w) { d[u] = d[v] + w; par[u] = v; } } } return d[t] < inf; } int mincost_maxflow (int s, int t) { int ans = 0; while (dijkstra(s, t)) { memcpy(pot, d, sizeof(d)); int delta = inf; for (int v = t; v != s; v = par[v]) delta = min(delta, cap[par[v]][v]); for (int v = t; v != s; v = par[v]) { cap[par[v]][v] -= delta; cap[v][par[v]] += delta; ans += cost[par[v]][v]*delta; } } return ans; } ``` +
+
diff --git a/ru/mst.html b/ru/mst.html index f769f21..2ede1b1 100644 --- a/ru/mst.html +++ b/ru/mst.html @@ -10,6 +10,8 @@ # Минимальные остовы > Авиакомпания содержит $m$ рейсов между $n$ городами, $i$-ый из них обходится в $w_i$ рублей, причём из любого города можно добраться до любого другого. В стране наступил кризис, и нужно отказаться от как можно большего числа из них таким образом, что содержание оставшиъся рейсов будет наиболее дешевым. Иными словами, нужно найти дерево минимального веса, которое является подграфом данного неориентированного графа. Такие деревья называют *остовами* (*каркас*, *скелет*; ударение на первый слог, но так мало кто произносит). По-английски — *minimum spanning tree* (дословно, минимальное покрывающее дерево). Почему дерево? Потому в противном случае там был бы цикл из которго можно удалить какое-то ребро и получить ответ пооптимальней. А если это больше, чем одно дерево, то какие-то две вершины остаются несвязаны. Вообще, следующие утверждения про деревья являются эквивалентными: * Граф — дерево. * В графе из $n$ вершин $n-1$ рёбер и нет циклов. * Из любой вершины можно дойти в любоую другую единственным образом. ## Лемма о безопасном ребре Назовем подграф $T$ графа $G$ безопасным, если они является подграфом какого-то минимального остова. Назовем ребро безопасным, если при добавлении его в подграф $T$ получившийся граф $T'$ тоже является безопасным, то есть подграфом какого-то минимального остова. Все алгоритмы для поиска минимального остова опираются на следующее утверждение: > Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным. **Доказательство**: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже. ![safe](https://neerc.ifmo.ru/wiki/images/thumb/8/81/%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png/300px-%D0%9B%D0%B5%D0%BC%D0%BC%D0%B0_%D0%BE_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE%D0%BC_%D1%80%D0%B5%D0%B1%D1%80%D0%B5.png) Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов. # Алгоритм Прима Минимальный остов строится постепенно, добавлением в него рёбер по одному. * Изначально остов — одна произвольная вершина. * Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден. Очень похоже на алгоритм Дейкстры, только мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё. Совсем наивная реализация за $O(nm)$ — каждый раз перебираем все рёбра: ```python const int maxn = 1e5, inf = 1e9; vector from, to, weight; bool used[maxn] // считать все рёбра в массивы used[0] = 1; for (int i = 0; i < n-1; i++) { int opt_w = inf, opt_from, opt_to; for (int j = 0; j < m; j++) if (opt_w > weight[j] && used[from[j]] && !used[to[j]]) opt_w = weight[j], opt_from = from[j], opt_to = to[j] used[opt_to] = 1; cout << opt_from << " " << opt_to << endl; } ``` Реализация за $O(n^2)$: ```python const int maxn = 1e5, inf = 1e9; bool used[maxn]; vector< pair > g[maxn]; int min_edge[maxn] = {inf}, best_edge[maxn]; min_edge[0] = 0; // ... for (int i = 0; i < n; i++) { int v = -1; for (int u = 0; u < n; j++) if (!used[u] && (v == -1 || min_edge[u] < min_edge[v])) v = u; used[v] = 1; if (v != 0) cout << v << " " << best_edge[v] << endl; for (auto e : g[v]) { int u = e.first, w = e.second; if (w < min_edge[u]) { min_edge[u] = w; best_edge[u] = v; } } } ``` Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за $O(m \log n)$: ```python set< pair > q; int d[maxn]; while (q.size()) { v = q.begin()->second; q.erase(q.begin()); for (auto e : g[v]) { int u = e.first, w = e.second; if (w < d[u]) { q.erase({d[u], u}); d[u] = w; q.insert({d[u], u}); } } } ``` Про алгоритм за $O(n^2)$ забывать не стоит — он работает лучше в случае плотных графов. # Алгоритм Крускала Будем добавлять рёбра в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить. Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Однако для этой проверки нам нужна будет целая отдельная структура. # Система непересекающихся множеств Эта структура данных предоставляет следующие возможности. Изначально имеется несколько элементов, каждый из которых находится в отдельном (своём собственном) множестве. Структура поддерживает две операции: * объединить два каких-либо множества * запросить, в каком множестве сейчас находится указанный элемент Обе операции будут выполняться в среднем почти за $O(1)$ (но не совсем — этот сложный вопрос будет разъяснен позже). Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив `_p`, в котором для каждого элемента мы храним номер его предка в дерева. Для корней деревьев будем считать, что их предок — они сами. Наивная реализация, которую мы потом ускорим: ```python int _p[maxn]; int p (int v) { if (_p[v] == v) return v; else return p(_p[v]); } void unite (int a, int b) { a = p(a), b = p(b); _p[a] = b; } for (int i = 0; i < n; i++) _p[i] = i; ``` **Эвристика сжатия пути**. Оптимизируем работу функции `p`. Давайте перед тем, как вернуть ответ, запишем его в `_p` от текущей вершины, то есть переподвесим его за самую высокую. Насколько лучше это сделает асимптотику? Выясняется, что $O(n \log n)$. Тут должен быть мем из опросов. **Ранговая эвристика**. Эта штука напрямую пытается минимизировать высоту дерева. Давайте делать переподвешивание за то, которое менее глубоко. Ну понятно, что тогда любое дерево будет не более логарифма. **Весовая эвристика**. Давайте каждый раз подвешивать за более крупное. Работать будет быстро, так как . Автор предпочитает именно её, потому что часто эти размеры компонент требуются сами по себе. Оказывается, что сжатия + ранговая или сжатия + весовая работает быстро. Асимптотика объединения обеих эвристик (сжатия путей и одной из ранговых) — O(a(n)), где a(n) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4). Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует. ```python int _p[maxn], s[maxn]; int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); } void unite (int a, int b) { a = p(a), b = p(b); if (s[a] > s[b]) swap(a, b); s[b] += s[a]; _p[a] = b; } for (int i = 0; i < n; i++) _p[i] = i; ``` ## Полезные свойства и классические задачи * Если веса всех рёбер различны, то остов будет уникален. * Минимальный остов является также и остовом с минимальным произведением весов рёбер (замените веса всех рёбер на их логарифмы) * Минимальный остов является также и остовом с минимальным весом самого тяжелого ребра. * Если вы решаете задачу, где ребра не добавляются, а удаляются, то можно попробовать решать задачу «с конца» и применить алгоритм Крускала. ## Персистентная СНМ* Как вы уже заметили, автор очень любит персистентные структуры данных. СНМ — тоже структура, и её тоже можно сделать персистентной. В СНМ мы изменяем массивы, а массивы можно сделать персистентными через персистентное ДО (только так, проще не получается — многие пытались). Здесь есть нюанс — амортизированные структуры не очень хорошо дружат с персистентностью. Поэтому нам придется отказаться от эвристики сжатия путей, и поэтому асимптотика составит $O(n \log^2 n)$ времени и памяти — один логарифм от СНМа, другой от персистентного ДО. ## Динамическая связность* *Dynamic Connectivity Problem:* > Даны $n$ запросов добавления ребра (`+`), удаления ребра (`-` и какого-то запроса про граф (`?`), например, о связности двух вершин. О решении этой задачи в online и в offline можете почитать в [этом посте](https://codeforces.com/blog/entry/15296). +
+
diff --git a/ru/pollard.html b/ru/pollard.html index b55cbbc..efdc503 100644 --- a/ru/pollard.html +++ b/ru/pollard.html @@ -10,6 +10,8 @@ # Факторизация за $O(\sqrt[4]{n})$ Наука умеет раскладывать целые числа на множители за $O(n^\frac{1}{4})$. Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера. ## Парадокс дней рождений Пусть $f(n, d)$ это вероятность того, что в группе из $n$ человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от $1$ до $d$. $$f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})$$ Попытаемся оценить $f$: $$ \begin{align} \begin{aligned} e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ & = e^{-\frac{n(n-1)}{2d}} & \\ & \simeq e^{-\frac{n^2}{2d}} & \\ \end{aligned} \end{align} $$ Из формулы более-менее понятно, что вероятность $\frac{1}{2}$ достигается при $n \approx \sqrt{d}$ и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее: **Утверждение**. В мультимножество нужно добавить $O(\sqrt{n})$ случайных чисел от 1 до n, чтобы какие-то два совпали. Если вы не доверяете математике, посмотрите на графики: ```python import matplotlib.pyplot as plt %matplotlib inline import seaborn as sns sns.set() ``` ```python def f(n, d): p = 1 for x in range(1, n): p *= 1-x/d return p def g(n, d): return 2.71**(-0.5 * n**2 / d) ``` ```python exact = [f(n, 365) for n in range(40)] approx = [g(n, 365) for n in range(40)] plt.plot(exact) plt.plot(approx) plt.show() ``` ![png](pollard_files/pollard_7_0.png) На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать $n \approx \sqrt{d}$, построим такую же для больших чисел и перейдём в логарифмический масштаб. ```python approx = [g(n, 10**10) for n in range(10**6)] plt.xscale('log') plt.plot(approx) plt.show() ``` ![png](pollard_files/pollard_9_0.png) ## $\rho$-алгоритм Полларда Итак, мы хотим факторизовать число $n$. Предположим, что $n = p q$ и $p \approx q$. Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей. Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например $f(x) = (x+1)^2 \mod n$. Как выглядит граф, в котором из вершины есть единственное ребро $x \to f(x)$? Такой граф называется *функциональным*. Если в нём нарисовать «траекторию» произвольного элемента, — какой-то путь, превращающийся в цикл, — то получится что-то похожее на букву $\rho$ (ро). Алгоритм из-за этого так и назван. Рассмотрим траекторию какого-нибудь элемента $x_0$: {$x_0$, $f(x_0)$, $f(f(x_0))$, $\ldots$}. Теперь сделаем из неё новую последовательность, взяв каждый элемент по модулю $p$ — наименьшего из простых делителей $n$. **Утверждение**. Ожидаемая длина цикла в этой последовательности $O(\sqrt[4]{n})$. *Доказательство:* так как $p$ — меньший делитель, то $p \leq \sqrt{n}$. Теперь просто подставим в предыдущее утверждение: в множество нужно добавить $O(\sqrt{p}) = O(\sqrt[4]{n})$ элементов, чтобы какие-то два совпали, а значит последовательность зациклилась. Если мы найдём цикл в такой последовательности (то есть такие $i$ и $j$, что $f^i(x_0) \equiv f^j(x_0)$), то мы можем восстановить какой-то делитель $n$, а именно $\gcd(|f^i(x_0) - f^j(x_0)|, n)$ — это число меньше $n$ и делится на $p$. Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя $i$ и $j$ ($i = 2j$) и проверять, что $f^i(x_0) \equiv f^j(x_0) \pmod p$, что эквивалентно проверке $\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1$. ```python from math import gcd from random import randint ``` ```python def find_divisor(n, seed = 1, f = lambda x: (x+1)**2): x = y = seed d = 1 while d == 1: x = f(f(x)) % n y = f(y) % n d = gcd(abs(x-y), n) return d ``` ```python n = 3*5*7 # = 105 print([find_divisor(n, seed=i) for i in range(n)]) ``` [3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3] ```python n = (10**9+7)*(10**9+9) print([find_divisor(n, seed=randint(1, n)) for _ in range(20)]) ``` [1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009] Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за $O(\sqrt{n})$ (нужно добавить отсечение по времени). ## Разное Формально, мы показали, что алгоритм работает за $O(\sqrt[4]{n} \log n)$ за счёт поиска $\gcd$, но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет. Пародокс дней рождения также можно использовать при подсчёте вероятности коллизии при хэшировании. Хороший модуль должен быть не менее квадрата количества хэшей, а чего-то вроде $5 n^2$ уже почти всегда достаточно. +
+
diff --git a/ru/probability.html b/ru/probability.html index 10d5968..882048f 100644 --- a/ru/probability.html +++ b/ru/probability.html @@ -10,6 +10,8 @@ # Ликбез по теорверу * Вероятностные распределения * Матожидание и дисперсия * Парадокс дней рождения * Нормальное распределение * Правдоподобие и оценка качества предсказания * Энтропия и оптимальное кодирование Эта статья представляет собой выжимку самых интересных фактов и «больших идей» теорвера, которые обычно рассказывают на курсах статистики, машинного обучения и теории информации. (Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.) ```python import numpy as np import matplotlib as plt %matplotlib inline import seaborn as sns sns.set() ``` ## Матожидание ## Дисперсия Какие два числа лучше всего описывают распределение? ## Нормальное распределение Центральная предельная теорема названа так пафосно вполне обоснованно. Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию. $$ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} $$ Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса. Трудно даже доказать, что это распределение, т. е. что $\int_{-\inf}^\inf f(x) dx = 1$. ## Применения Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный. Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как ## Линейные рекурренты Чтобы решать следующие задачи, нам нужно будет использовать следующий факт: ... Доказательство мы не приведем. В частности, таким образом получается формула для чисел Фибоначчи. $$ f_n = \ldots $$ Кто бы мог подумать, что все эти иррациональности и степени сократятся и вообще дадут целое число?.. ## Классика Парадокс дней рождения. Это на самом деле очень часто используемый результат. Так можно считать вероятность коллизии хэшей, а также он используется во многих теоретико-числовых алгоритмах, в которых используется предположения (весьма справедливые) о распределении простых чисел. Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт? TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. Казино. Мы приходим в казино с 1000\$ и следующим образом проводим там время: ставим по 1\$, пока не обанкротимся или не выиграем 1100\$. Какая вероятность того, что мы уйдём с деньгами? ## Принцип максимального правдоподобия ## Энтропия Энтропией называется минимальное число бит, которым теоретически возможно сжать сообщение. Эта величина важна, потому что на практике если её можно посчитать, то сжатие с соответствующей кратностью реально достижимо. Шумный канал. Пусть у вас есть 1тб данных и два китайских терабайтника, на каждый из которых можно записать столько данных, но каждый бит имеет вероятность 10% записаться на противоположный. Требуется сохранить данные с первого раза без потерь. Совсем без потерь. Причём это делается почти впритык. +
+
diff --git a/ru/reciprocal.html b/ru/reciprocal.html index 9ffaef7..a43185f 100644 --- a/ru/reciprocal.html +++ b/ru/reciprocal.html @@ -10,6 +10,8 @@ # Обратный элемент по модулю Часто в задачах требуется посчитать что-то по простому модулю (чаще всего $10^9 + 7$). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче. Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например: ```c++ c = (a + b) % mod; c = (mod + a - b) % mod; c = a * b % mod; ``` Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: $\frac{8}{2} = 4$, но $\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4$. Нужно найти некоторый элемент, который будет себя вести как $\frac{1}{a} = a^{-1}$, и вместо «деления» домножать на него. Назовем такой элемент обратным. ## Способ 1: бинарное возведение в степень Если модуль $p$ простой, то решением будет $a^{-1} \equiv a^{p-2}$. Это следует из малой теоремы Ферма: **Теорема.** $a^p \equiv a \pmod p$ для всех $a$, не делящихся на $p$. **Доказательство**. (для понимания несущественно, можно пропустить) $$ \begin{align} a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p \\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} \\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} \\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} \\ &= a \end{align} $$ Здесь $P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}$ это мультиномиальный коеффициент — количество раз, которое элемент $a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}$ появится при раскрытии скобки $(a_1 + a_2 + \ldots + a_n)^k$. Теперь два раза «поделим» наш результат на $a$. $$ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} $$ Получается, что $a^{p-2}$ ведет себя как $a^{-1}$, что нам по сути и нужно. Посчитать $a^{p-2}$ можно за $O(\log p)$ бинарным возведением в степень. Приведем код, который позволяет считает $C_n^k$. ```c++ int t[maxn]; // факториалы, можно предподситать простым циклом // бинарное возведение в степень int bp (int a, int n) { int res = 1; while (n) { if (n & 1) res = res * a % mod; a = a * a % mod; n >>= 1; } return res; } // находит обратный элемент как a^(p-2) int inv (int x) { return bp(x, mod-2); } int c (int n, int k) { return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod; } ``` ## Способ 2: диофантово уравнение Диофантовыми уравнениями называют такие штуки: $$ ax + by = 1 $$ Требуется решить их в целых числах, то есть $a$ и $b$ известны, и нужно найти такие целые (возможно, отрицательные) $x$ и $y$, чтобы равенство выполнялось. Решают такие вещи расширенным алгоритмом Евклида. TODO: описать, как он работает. Подставим в качестве $a$ и $b$ соответственно $a$ и $m$ $$ ax + my = 1 $$ Одним из решений уравнения и будет $a^{-1}$, потому что если взять уравнение по модулю $m$, то получим $$ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m $$ Преимущества этого метода над возведением в степень: * Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. * Алгоритм проще выполнять руками. Сам автор почти всегда использует возведение в степень. ## Почему $10^9+7$? 1. Это выражение довольно легко вбивать (`1e9+7`). 2. Простое число. 3. Достаточно большое. 4. `int` не переполняется при сложении. 5. `long long` не переполняется при умножении. Кстати, $10^9 + 9$ обладает теми же свойствами. Иногда используют и его. ## Предподсчёт обратных факториалов за линейное время Пусть нам нужно зачем-то посчитать все те же $C_n^k$, но для больших $n$ и $k$, поэтому асимптотика $O(n \log m)$ нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам. Если у нас уже написан `inv`, то нам не жалко потратить $O(\log m)$ операций, посчитав $m!^{-1}$. После этого мы будем считать $(m-1)!^{-1}$ как $m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}$. ```c++ int f[maxn]; f[0] = 1; for (int i = 1; i < maxn; i++) f[i] = i*f[i-1] % mod; int r[maxn]; r[maxn-1] = inv(f[maxn-1]) for (int i = maxn-1; i >= 1; i--) r[i-1] = r[i]*i % mod; ``` TODO: техника с сайта емакса. +
+
diff --git a/ru/segtree.html b/ru/segtree.html index e515b68..b0622da 100644 --- a/ru/segtree.html +++ b/ru/segtree.html @@ -10,6 +10,8 @@ # Дерево отрезков **Замечание**. Почти везде мы будем использовать полуинтервалы — обозначаемые как $[l, r)$ — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля. **Дерево отрезков** — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках. Рассмотрим конкретную задачу: >Дан массив $a$ из $n$ целых чисел, нужно уметь отвечать на запросы двух типов: >1. Изменить значение в ячейке (т. е. отреагировать на присвоение `a[k] = x`). >2. Вывести сумму элементов $a_i$ на отрезке с $l$ по $r$. >Оба запроса нужно обрабатывать за время $O(\log n)$. Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции: Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1. Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку $[0, n)$, а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков». ![alt text](http://i.imgur.com/GGBmcEP.png) Строить его можно рекурсивной функцией: * Если вершина является листом, взять в качестве суммы значение соответствующей ячейки. * Если вершина является отрезком, разделить его на два и в качестве суммы взять сумму его детей. ## Разные свойства Высота такого дерева есть величина $\Theta(\log n)$: на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики. Более того, любой полуинтервал разбивается на $O(\log n)$ неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков. Дерево также содержит менее $2n$ вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет $n$. Таким образом, число вершин в худшем случае оценивается суммой $n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} + \ldots + 1 < 2n$. Значит, оно линейное по памяти. При $n$, отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при $n=3$ левый сын корня есть отрезок $[0, 2)$, имеющий двух потомков, в то время как правый сын корня — отрезок $[2, 3)$, являющийся листом. ## Ок, как это нам поможет? Опишем теперь, как с помощью такой структуры решить задачу. **Запрос обновления**. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению $a[k] = x$. Изменим все вершины, в суммах которых участвует $k$-тый элемент. Их будет $\Theta(\log n)$ — по одной с каждого уровня. Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит $k$-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков. **Запрос суммы**. Мы знаем, что во всех вершинах лежат корректные значения. Сделаем тоже рекурсивную функцию, рассмотрев три случая: * Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму. * Если отрезки вершины и запроса не пересекаются, то вернуть 0. * Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей. Чтобы разобраться, почему это работает за $O(\log n)$, нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в $O(\log n)$ отрезках, а значит и итоговая асимптотика будет такая же. ## Ликбез по C++ Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества. Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел. Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за $O(\log n)$. В C++ есть два способа объявлять классы (объект — это экземпляр класса): через `struct` и через `class`. Их основное отличие в том, что по умолчанию в `class` все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально. У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например **конструктор** — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса. ```c++ struct A { int param1, param2; // тут можно что-то хранить char param3 = 'k'; A (int var) { // эта часть называется конструктором // ... } void do_something () { // это какой-то другой метод // ... } }; // <- не забудьте точку с запятой ``` Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (*аллокатор*) выделяет место в массиве (*оперативной памяти*) под этот объект и возвращает позицию (*указатель*) на место в этом массиве. Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется: ```c++ A x(179); x.do_something(); x.param1 = 57; A *y = new A(42); // new возвращает адрес, по которому можно найти объект y->do_something(); y.param3 = '!'; ``` Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем $2^{32}$ байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с `long long` быстрее считались ## Реализация Общий план реализации любых структур данных: 1. Полностью понять все *инварианты* — как должна выглядеть структура, какие значения должны принимать поля, etc. 2. Формально описать, что должны делать методы и за какую асимптотику. 3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты. ```c++ struct segtree { int lb, rb; // левые и правые границы отрезков int sum = 0; // сумма на текущем отрезке segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { // если не лист, создаем детей int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) // если мы лежим полностью в отрезке запроса, вывести сумму return sum; if (max(lb, lq) >= min(rb, rq)) // если мы не пересекаемся с отрезком запроса, вывести ноль return 0; // иначе всё сложно -- запускаемся от детей и пусть они там сами решают return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` > Посчитать число беспорядков в перестановке из $n$ элементов (беспорядок или инверсия — это пара чисел $i < j$, для которых $p_i > p_j$). Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на $n$ элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число $x$, будем делать две вещи: * Запросим сумму от $k$ до $n$ в ДО. * Добавим единичку в $k$-тую позицию в ДО. Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за $O(n \log n)$ запросов. > Даны $n$ точек на плоскости с целыми координатами от 1до $n$. Требуется ответить на $m$ запросов количества точек на прямоугольнике. Ответим на все запросы в оффлайн, используя метод сканирующей прямой: * Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике $[x_1, x_2] \times [y_1, y_2]$ равна сумме на прямоугольнике $[0, x_2] \times [y_1, y_2]$ минус сумма на прямоугольнике $[0, x_1] \times [y_1, y_2]$. * Отсортируем теперь все точки и префиксные запросы по их $x$. При этом, если у точки и запроса одинаковый $x$, то точка должна идти раньше. * Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в $y_i$» и «вывести сумму с $y_1$ по $y_2$». ## Отложенные операции Пусть теперь наш запрос обновления — это присвоение значения $x$ всем элементам некоторого отрезка $[l, r)$, а не только одному. Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем. Например, если пришел запрос «присвой число $x$ на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено. Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся. Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за $O(n \log n)$. При реализации создадим вспомогательную функцию `push`, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны. ```c++ struct segtree { int lb, rb; int sum = 0, assign = -1; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void push () { if (assign != -1) { sum = (rb-lb) * assign; if (l) { // если дети есть l->assign = assign; r->assign = assign; } } assign = -1; } void upd (int lq, int rq, int x) { push(); if (lq <= lb && rb <= rq) assign = x; else if (l && max(lb, lq) < min(rb, rq)) { // если есть дети и отрезок запроса хоть как-то пересекается с нашим l->upd(lq, rq, x); r->upd(lq, rq, x); // ...дальше они сами разберутся } } int get_sum (int lq, int rq) { push(); if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` По-английски эта техника называется *lazy propagation*. **Очень важно научиться её писать** — она часто встречается на олимпиадах. Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни. ## Динамическое построение А что, если у нас все индексы лежать не от в пределах $10^5$, а, например, $10^9$. Все асимптотики нас по прежнему устраивают ($\log_2 10^6 \approx 20$, $\log_2 10^9 \approx 30$), кроме этапа построения. Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation. Реализовать это можно так же, как и с `push`-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так. ```c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; // а тут ничего нет } void extend () { if (!l && lb + 1 < rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void add (int k, int x) { extend(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { if (lb >= lq && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; extend(); return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так: ```c++ vector compress (vector a) { vector b = a; sort(b.begin(), b.end()); b.erase(unique(b.begin(), b.end()), b.end()); for (int &x : a) x = int(lower_bound(b.begin(), b.end(), x) - b.begin()); return a; } ``` ## Персистентность Структуры данных называют **персистентными**, если их можно быстро «откатить» до произвольного предыдущего состояния. Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент $t$ никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается. У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать $O(\log n)$ новых вершин на запрос, что означает общее потребление памяти $O(m \log n)$. ```c++ struct segtree { int lb, rb; int sum = 0; segtree *l = 0, *r = 0; segtree (int _lb, int _rb) { lb = _lb, rb = _rb; if (lb != rb) { int t = (lb + rb) / 2; l = new segtree(lb, t); r = new segtree(t, rb); } } void copy () { if (l) { l = new segtree(l); r = new segtree(r); } } void add (int k, int x) { copy(); sum += x; if (l) { if (k < l->rb) l->add(k, x); else r->add(k, x); } } int get_sum (int lq, int rq) { // этот метод ничего не меняет -- он и так хороший if (lq <= lb && rb <= rq) return sum; if (max(lb, lq) >= min(rb, rq)) return 0; return l->get_sum(lq, rq) + r->get_sum(lq, rq); } }; ``` > Даны $n$ точек на плоскости. Нужно *в онлайн* ответить на $q$ запросов суммы на прямоугольнике. Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения $x_i$ и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с $O(n \log n)$ памяти. > Дан отрезок из $n$ чисел от 1 до $n$. Требуется ответить на $q$ запросов $k$-той порядковой статистики на подотрезке. Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент $k$, добавим единицу к $k$-ому элементу. Дальше определим *разность деревьев* как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо `sum` использовать везде `sum_r` - `sum_l`. Что будет находиться в разности $r$-го и $l$-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает $k$ — она и будет ответом. > Дан массив из $n$ элементов. Требуется ответить на $m$ запросов, есть ли на отрезке $[l, r]$ доминирующий элемент — тот, который встречается на нём хотя бы $\frac{r-l}{2}$ раз. У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за $O(\log n)$, посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна $\frac{1}{2^{100}}$, и ей на практике можно пренебречь. Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее $\frac{n}{2}$ в массиве на $n$ элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем `true`, иначе `false`. ## Другие реализации Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы: **На массивах**. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у [Емакса](http://e-maxx.ru/algo/segment_tree). **«ДО снизу»**. Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с [CodeForces](https://codeforces.com/blog/entry/18051). ## Задачи * [Первый контест](https://informatics.msk.ru/mod/statements/view3.php?id=33853&chapterid=752#1) — на базовые операции. * Второй контест — на отложенные операции. * [Дополнительный контест](https://codeforces.com/group/g92L0id9Yb/contest/228565) — на динамическое построение и персистентность. +
+
diff --git a/ru/sparse-table.html b/ru/sparse-table.html index 7259f55..b233e35 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -10,6 +10,8 @@ # Разреженная таблица - Нужна для нахождения минимума на отрезке за $O(1)$ с препроцессингом за $O(n \log n)$ с малой константой. - Обновления не поддерживает (static RMQ). - Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья. - Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос). - Требует $O(n \log n)$ памяти. - Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ. Определим разреженную таблицу как двумерный массив размера $n \times\log n$: $$ t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} $$ Идея такая: считаем минимум на каждом отрезке длины $2^k$. Такой массив можно посчитать за его размер: $t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])$. Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования. Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам. ![](https://neerc.ifmo.ru/wiki/images/7/75/SparseTableRMQ.png) Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов. ```c++ int a[maxn], lg[maxn], mx[maxn][logn]; int rmq (int l, int r) { int t = lg[r-l+1]; return min(mx[l][t], mx[r-(1<= 0; i--) { mx[i][0] = a[i]; for (int l = 0; l < logn-1; l++) mx[i][l+1] = max(mx[i][l], mx[i+(1< +
+
diff --git a/ru/sse.html b/ru/sse.html index 429b4db..6bd867a 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -10,6 +10,8 @@ # Streaming SIMD Extensions This is ``` processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 945.615 cache size : 9216 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: ``` ```c++ !cat /proc/cpuinfo | less -20 ``` processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3422.395 cache size : 9216 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 0 initial apicid : 0 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 1 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3702.863 cache size : 9216 KB physical id : 0 siblings : 12 core id : 1 cpu cores : 6 apicid : 2 initial apicid : 2 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 2 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3556.265 cache size : 9216 KB physical id : 0 siblings : 12 core id : 2 cpu cores : 6 apicid : 4 initial apicid : 4 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 3 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3512.268 cache size : 9216 KB physical id : 0 siblings : 12 core id : 3 cpu cores : 6 apicid : 6 initial apicid : 6 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 4 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3642.787 cache size : 9216 KB physical id : 0 siblings : 12 core id : 4 cpu cores : 6 apicid : 8 initial apicid : 8 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 5 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3136.167 cache size : 9216 KB physical id : 0 siblings : 12 core id : 5 cpu cores : 6 apicid : 10 initial apicid : 10 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 6 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3535.684 cache size : 9216 KB physical id : 0 siblings : 12 core id : 0 cpu cores : 6 apicid : 1 initial apicid : 1 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 7 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 2251.174 cache size : 9216 KB physical id : 0 siblings : 12 core id : 1 cpu cores : 6 apicid : 3 initial apicid : 3 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 8 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 2202.563 cache size : 9216 KB physical id : 0 siblings : 12 core id : 2 cpu cores : 6 apicid : 5 initial apicid : 5 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 9 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 2413.537 cache size : 9216 KB physical id : 0 siblings : 12 core id : 3 cpu cores : 6 apicid : 7 initial apicid : 7 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 10 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 1974.963 cache size : 9216 KB physical id : 0 siblings : 12 core id : 4 cpu cores : 6 apicid : 9 initial apicid : 9 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: processor : 11 vendor_id : GenuineIntel cpu family : 6 model : 158 model name : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz stepping : 10 microcode : 0x96 cpu MHz : 3336.891 cache size : 9216 KB physical id : 0 siblings : 12 core id : 5 cpu cores : 6 apicid : 11 initial apicid : 11 fpu : yes fpu_exception : yes cpuid level : 22 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4416.00 clflush size : 64 cache_alignment : 64 address sizes : 39 bits physical, 48 bits virtual power management: +
+
diff --git a/ru/stress-test.html b/ru/stress-test.html index ca8932a..b1a86cb 100644 --- a/ru/stress-test.html +++ b/ru/stress-test.html @@ -10,6 +10,8 @@ # Стресс-тестирование * Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений * Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написанно решение на маленькие подгруппы Суть такая: * Есть решение `smart` — быстрое, но в котором есть баг, который хотим найти * Пишем решение `stupid` — медленное, но точно корректное * Пишем генератор `gen` — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт `checker`, который n раз генерирует тест, даёт его на ввод `stupid`-у и `smart`-у, сравнивает выводы и останавливается, когда они отличаются Примерный код скрипта: ```python import os, sys f1, f2, gen, iters = sys.argv for i in range(int(iters)): print('Test', i+1) os.popen('python3 %s > test.txt' % gen) v1 = os.popen('./%s < test.txt' % f1).read() v2 = os.popen('./%s < test.txt' % f2).read() if v1 != v2: print test print("Correct:") print v1 print("Wrong:") print v2 break ``` Автор обычно запускает его командой `python3 checker.py stupid smart gen.py 100`, предварительно скомпилировав `stupid` и `smart` в ту же директорию, что и сам `checker.py`. Скрипт написан под Linux. Для Windows нужно убрать «`./`» во всех системных вызовах. `gen.py` автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через `./gen`. Пример `gen`-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10: ```python from random import randint, choice n = randint(1, 10) print(n) for _ in range(n): print(choice('abc'), end='') ``` +
+
diff --git a/ru/strings.html b/ru/strings.html index a9398c0..b2db1eb 100644 --- a/ru/strings.html +++ b/ru/strings.html @@ -10,6 +10,8 @@ # Базовые строковые алгоритмы ## Префикс-функция Рассмотрим задачу, которая возникает каждый раз, когда вы делаете `ctrl+f`: > Есть большой текст $t$. Нужно найти все вхождения строки $s$ в него. Наивное решение со сравнением всех подстрок $t$ длины $|s|$ со строкой $s$ работает за $O(|t| \cdot |s|)$. Если текст большой, то длинные слова в нем искать становится очень долго. Для решения этой задачи за линейное время придумали **префикс-функцию**. **Определение**. Префикс-функцией от строки $s$ называется массив $p$, где $p_i$ равно длине самого большого префикса строки $s_0 s_1 s_2 \ldots s_i$, который также является и суффиксом этой строки (не считая всю строку). Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна $[0, 1, 0, 1, 2, 3, 4, 5]$. ```python def slow_prefix_function(s): n = len(s) p = [0]*n for i in range(n): prefix = s[:i] for l in range(1, i): if prefix[:l] == prefix[-l:]: p[i] = l return p slow_prefix_function('aataataa') ``` [0, 0, 1, 0, 1, 2, 3, 4] (Этот алгоритм работает за $O(n^3)$, но это только пока.) ## Как это поможет решить исходную задачу? Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке. Соединим подстроки $s$ и $t$ каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки $s\#t$. ```python s = "let it go" t = """let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!""" print((s + '#' + t).replace('\n', ' ')) print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)])) ``` let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door! 00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000 Видно, что все места, где значения равны 9 (длине S) — это концы вхождений $s$ в текст $t$. Такой алгоритм (посчитать префикс-функцию от $s\#t$ и посмотреть, в каких позициях она равна $|s|$) называется **алгоритмом Кнута-Морриса-Пратта**. ## Как её быстро считать Рассмотрим ещё несколько примеров префикс-функций: ```python for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']: print(slow_prefix_function(s)) ``` [0, 0, 1, 2, 3] [0, 0, 0, 0, 0, 0] [0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0] [0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] Можно заметить несколько особенностей: * $p_0 = 0$ для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; * $p_{i+1}$ максимум на единицу превосходит $p_i$: если есть префикс, равный суффиксу строки $s_0 s_1 \ldots s_{i+1}$ длины $p_{i+1}$, то, отбросив последний символ, можно получить и правильный суффикс для строки $s_0 s_1 \ldots s_i$, длина которого будет ровно на единицу меньше. Хочется придумать алгоритм, как считать префикс-функцию за $O(N)$. Логично это делать с помощью динамики: найти формулу для $p_i$ через предыдущие значения. Заметим, что $p_{i+1} = p_i + 1$ в том и только том случае, когда $s_{p_i} =s_{i+1}$. Например, в строке $\underbrace{aabaa}t\overbrace{aabaa}$ выделен максимальный префикс, равный суффиксу: $p_{10} = 5$. Если следующий символ равен будет равен $t$, то $p_{11} = p_{10} + 1 = 6$. Но что происходит, когда $s_{p_i}\neq s_{i+1}$? Пусть следующий символ в этой же строке равен $b$. * $\implies$ Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. * $\implies$ Значит, помимо того, что он является суффиксом «aabaa**b**», префикс является префиксом и подстроки «aabaa». * $\implies$ Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть $p_4 = 2$. * $\implies$ Если $s_2 = s_{11}$ (т. е. новый символ совпадает с идущим после префикса-кандидата), то $p_{11} = p_2 + 1 = 2 + 1 = 3$. В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} \neq p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — $p_{p_{p_i}}$. Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым. ```python def fast_prefix_function(s): n = len(s) p = [0]*n for i in range(1, n): cur = p[i - 1] # перебираем префикс-функцию, пока не найдем равный символ while s[i] != s[cur] and cur > 0: cur = p[cur - 1] # если нашли, то значение на единицу больше if s[i] == s[cur]: p[i] = cur + 1 return p fast_prefix_function('abacabadabacabax') ``` [0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0] ### Асимптотика Почему это работает за $O(N)$? В худшем случае этот `while` может работать $O(N)$ раз за одну итерацию. Но оказывается, что *в среднем* каждый `while` работает за $O(1)$ — это называется амортизированной асимптотикой. **Доказательство**: * Как мы ранее заметили, префикс функция возрастает максимум на единицу. * $\implies$ Вырасти она может максимум $n-1$ раз. * Если мы зашли в `while`, то каждая его итерация понижает значение префикс-функции хотя бы на один. * $\implies$ Количество понижений не больше количества повышений, то есть $O(n)$. * $\implies$ Суммарно итераций цикла `while` — $O(n)$. ## Z-функция Альтернатива префикс-функции — z-функция (*примечание: не «зи», а «зет»*). Она немного проще для понимания. Z-функция от строки $s$ — это такой массив $z$, что $z_i$ равно длине максимальной подстроки, **начинающейся** с $i$-й позиции, которая равна префиксу $s$. $$\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)$$ ```python def slow_z_function(s): n = len(s) z = [0]*n for i in range(1, n): suffix = s[i:] for l in range(1, i): if s[:l] == suffix[:l]: z[i] = l return z for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']: print(slow_z_function(s)) ``` [0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1] [0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1] [0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0] Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c $s$, а не заканчиваться. Осталось научиться её искать за $O(n)$. ## Как её быстро считать Заметим, что: * $z_0 = 0$ — из-за договоренности (потому что информации не несет); * $z_i = 0 \iff s_i \neq s_0$; * $z_i > 0 \iff s_i = s_0$. Будем идти слева направо и хранить *z-блок* — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как $l$ и $r$. Пусть мы сейчас хотим найти $z_i$, а все предыдущие уже нашли. Если новый, $i$-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. * Если правее, то мы просто наивно перебором найдем $z_i$ (максимальный отрезок, начинающийся с $s_i$ и равный префиксу), и объявим его новым z-блоком. * Если $i$-й элемент лежит внутри z-блока, то мы можем посмотреть на значение $z_{i-l}$ и использовать его, чтобы инициализировать $z_i$ чем-то, возможно, отличным от нуля. Если $z_{i-l}$ «не хватает» до границы $z$-блока, то $z_i = z_{i-l}$. Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку. ```python def fast_z_function(s): n = len(s) z = [0]*n l = 0 r = 0 for i in range(1, n): if i <= r: z[i] = min(r - i + 1, z[i-l]) while i + z[i] < n and s[z[i]] == s[i+z[i]]: z[i] += 1 if i + z[i] - 1 > r: l = i r = i + z[i] - 1 return z ``` **Асимптотика**. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это $O(n)$. ## Зачем тогда люди используют префикс-функцию ![hz](https://cs8.pikabu.ru/post_img/2016/12/13/5/1481615023179882832.jpg) В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов. Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение. +
+
diff --git a/ru/suffix-array.html b/ru/suffix-array.html index 891a579..26549be 100644 --- a/ru/suffix-array.html +++ b/ru/suffix-array.html @@ -9,9 +9,11 @@ -# Суффиксный массив Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике. Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём. * +# Суффиксный массив Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике. Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём. * «Паблик с тупыми шутками про проганье» * ## Мотивация **Суффиксным массивом** строки $s$ называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки. **Как это использовать.** Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно. В случае с суффиксным массивом можно сделать следующее: найти бинарным поиском первый суффикс в суффиксном массиве, который меньше искомого слова, а также последний, который меньше. Все суффиксы между этими двумя будут включать искомую строку как префикс. Работать такой алгоритм будет за $O(|t| \log |s|)$, и позже это можно будет оптимизировать до $O(|t| + \log |s|)$, что является одним из самых оптимальных алгоритмов поиска. Теперь научимся его строить. ## Построение за $O(n \log n)$ Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара. Мы могли бы просто взять перестановку от $0$ до $n$, написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в `std::sort`, что будет работать за $O(n^2 \log n)$, потому что внутреннее сравнение работает за $O(n)$. Однако, если сравнивать суффиксы [хэшами](http://sereja.me/a/hashing), то уже тут можно получить $O(n \log^2 n)$. Но это не самый быстрый и удобный алгоритм. Наш алгоритм будет состоять из $\lceil \log n \rceil$ этапов. На $k$-том этапе мы будем рассматривать циклические подстроки длины $2^k$. На последнем этапе мы отсортируем строки длины $\geq n$ (это легально — они ведь циклические), и мы получим нужный суффиксный массив. Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки $p$ индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции $i$ с длиной 2^k, номер $c_i$ класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие $c_i$). Количество классов эквивалентности будем хранить в переменной `cls` (изначально она равна количеству различных символов). Пример: $s = aaba$. Этапов будет 3: для подстрок длины 1, 2 и 4. $$ p_0 = (0, 1, 3, 2) \;\;\; c_0 = (0, 0, 1, 0) \\ p_1 = (0, 3, 1, 2) \;\;\; c_1 = (0, 1, 2, 0) \\ p_2 = (3, 0, 1, 2) \;\;\; c_2 = (1, 2, 3, 0) $$ Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины $2^0 = 1$, то есть просто символы. Это мы сделаем сортировкой подсчётом. Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней `std::sort` со своим компаратором. Как быстро сравнить две подстроки? Мы можем использовать $c_i$ — каждой строке длины $2^k$ сопоставить биграмму (строку из двух символов), а именно строка $s[i..i+2^k-1]$ с точки зрения сортировки будет эквивалентна паре $(c_i, c_{i+2^{k-1}})$. Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за $O(1)$. Однако, это всё ещё будет работать за $O(n \log^2 n)$, потому что каждый этап будет работать за $O(n \log n$). **Оптимизация до $O(n \log n)$**. Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся **цифровой сортировкой** — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив $p$ с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в $p$ отнять $2^{k-1}$. Таким образом, можно проводить этап за $O(n)$. ```c++ // строка -- это последовательность чисел от 1 до размера алфавита vector suffix_array (vector &s) { s.push_back(0); // добавляем нулевой символ в конец строки int n = (int) s.size(), cnt = 0, // вспомогательная переменная: счётчик для сортировки cls = 0; // количество классов эквивалентности vector c(n), p(n); map< int, vector > t; for (int i = 0; i < n; i++) t[s[i]].push_back(i); // «нулевой» этап for (auto &x : t) { for (int u : x.second) c[u] = cls, p[cnt++] = u; cls++; } // пока все суффиксы не стали уникальными for (int l = 1; cls < n; l++) { vector< vector > a(cls); // массив для сортировки подсчётом vector _c(n); // новые классы эквивалентности int d = (1<(p.begin()+1, p.end()); } ``` TODO: переписать это ## Наибольшие общие префиксы Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы [делали](http://sereja.me/a/hashing) с хэшами. TODO +
+
diff --git a/ru/treap.html b/ru/treap.html index 3cf38ef..a78f1fc 100644 --- a/ru/treap.html +++ b/ru/treap.html @@ -10,6 +10,8 @@ # Декартово дерево Рене Декарт (фр. *René Descartes*) — великий французский математик и философ XVII века. Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим. Декартово дерево же определяется и строится так: * Нанесём на плоскость набор из $n$ точек. Их $x$ зачем-то назовем *ключем*, а $y$ *приоритетом*. * Выберем самую верхнюю точку (с наибольшим $y$, а если таких несколько — любую) и назовём её *корнем*. * От всех вершин, лежащих слева (с меньшим $x$) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня. * Аналогично, запустимся от правой части и добавим корню правого сына. Заметим, что если все $y$ и $x$ различны, то дерево строится однозначно. Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх: ![treap](https://hsto.org/storage/habraeffect/a1/0a/a10a744def8f325a1019502ecc175ef6.png) Таким образом, декартово дерево — это одновременно *бинарное дерево* по $x$ и *куча* по $y$. Поэтому ему придумали много альтернативных названий: * Дерамида (дерево + пирамида) * ПиВо (пирамида + дерево) * КуРево (куча + дерево) * Treap (tree + heap) ## Бинарные деревья С небольшими модификациями, декартово дерево умеет всё то же, что и любое [бинарное дерево поиска](https://ru.wikipedia.org/wiki/%D0%94%D0%B2%D0%BE%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0), например: * добавить число $x$ в множество * определить, есть ли в множестве число $x$ * найти первое число, не меньшее $x$ (`lower_bound`) * найти количество чисел в промежутке $[l, r]$ При этом все операции — за $O(\log n)$. На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до $O(\log n)$. ## Приоритеты и асимптотика В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты ($y$) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска. **Теорема**. Ожидание глубины вершины в декартовом дереве равно $O(n \log n)$. Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить. **Доказательство***. Введем функцию $a(x, y)$ равную единице, если $x$ является предком $y$, и нулем в противном случае. Такие функции называются *индикаторами*. Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна $$d_i = \sum_{j=1}^n a(j, i)$$ Её матожидание равно $$E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)$$ где $p(x, y)$ это веряотность, что $a(x, y) = 1$. Здесь мы воспользовались важным свойством [линейности](https://neerc.ifmo.ru/wiki/index.php?title=%D0%9C%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%BE%D0%B6%D0%B8%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D0%BE%D0%B9_%D0%B2%D0%B5%D0%BB%D0%B8%D1%87%D0%B8%D0%BD%D1%8B#.D0.9B.D0.B8.D0.BD.D0.B5.D0.B9.D0.BD.D0.BE.D1.81.D1.82.D1.8C_.D0.BC.D0.B0.D1.82.D0.B5.D0.BC.D0.B0.D1.82.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B3.D0.BE_.D0.BE.D0.B6.D0.B8.D0.B4.D0.B0.D0.BD.D0.B8.D1.8F): матожидание суммы чего угодно равна сумме матожиданий этого чего угодно. Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение. **Лемма**. Вершина $x$ является предком $y$, если у неё приоритет больше, чем у всех вершин из отрезка $(x, y]$ (без ограничения общности, будем считать, что $x < y$). **Необходимость**. Если это не так, то где-то между $x$ и $y$ есть вершина с большим приоритетом, чем $x$. Она не может быть потомком $x$, а значит $x$ и $y$ будут разделены. **Достаточность**. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком $x$. Таким образом, всё, что справа от $y$, ни на что влиять не будет. У всех вершин на любом отрезке одинаковая вероятность иметь наибольший приоритет. Объединяя этот факт с результатом леммы, мы можем получить выражение для искомых вероятностей: $$p(x, y) = \frac{1}{y-x+1}$$ Теперь, чтобы найти матожидание, эти вероятности надо просуммировать: $$E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)$$ Перед последним переходом мы получили сумму гармонического ряда. Примечательно, что ожидаемая глубина вершин зависит от их позиции: вершина из середины должна быть примерно в два раза глубже, чем крайняя. **Упражнение***. Выведите из этого доказательства асимптотику `quicksort`. ## Реализация Декартово дерево удобно писать на указателях и структурах. Поэтому мы [рассказали](http://sereja.me/a/segtree) дерево отрезков на указателях, а не стандартную [рекурсию на 5 параметрах](http://e-maxx.ru/algo/segment_tree). Не знаете, что это такое — посмотрите в предыдущем конспекте. Создадим структуру `Node`, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву. ```c++ struct Node { int key, prior; Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } }; ``` Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся. ### Merge Принимает два дерева (два корня, $L$ и $R$), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча. Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень $L$ или правый $R$ — просто возьмем тот, у кого приоритет больше. Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном $L$. С правым сыном сложнее: возможно, его нужно смерджить с $R$. Поэтому рекурсивно сделаем `merge(l->r, r)` и запишем результат в качестве правого сына. ```c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; if (l->prior > r->prior) { l->r = merge(l->r, r); return l; } else { r->l = merge(l, r->l); return r; } } ``` ### Split Принимает дерево и ключ $x$, по которому его нужно разделить на два: $L$ должно иметь все ключи не больше $x$, а $R$ должно иметь все ключи больше $x$. В этой функции мы сначала решим, в каком из деревьев должен быть корень, а потом рекурсивно разделим его правую или левую половину и присоединим, куда надо: ```c++ typedef pair Pair; Pair split (Node *p, int x) { if (!p) return {0, 0}; if (p->key <= x) { Pair q = split(p->r, x); p->r = q.first; return {p, q.second}; } else { Pair q = split(p->l, x); p->l = q.second; return {q.first, p}; } } ``` ### Пример: вставка `merge` и `split` сами по себе не очень полезные, но помогут написать все остальное. Вот так, например, будет выглядеть код, добавляющий $x$ в сет. ```c++ Node *root = 0; void insert (int x) { Pair q = split(root, x); Node *t = new Node(x); root = merge(q.first, merge(t, q.second)); } ``` ### Пример: модификация для суммы на отрезке Иногда нам нужно написать какие-то модификации для более продвинутых операций. Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке». ```c++ struct Node { int val, sum; // ... }; ``` При `merge` и `split` надо будет поддерживать эту сумму актуальной. Вместо того, чтобы модифицировать и `merge`, и `split` под наши хотелки, напишем вспомогательные функцию `upd`, которую будем вызывать при обновлении детей вершины. ```c++ void sum (Node* v) { return v ? v->sum : 0; } // обращаться по пустому указателю нельзя -- выдаст ошибку void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; } ``` В `merge` и `split` теперь можно просто вызывать `upd` перед тем, как вернуть вершину, и тогда ничего не сломается: ```c++ Node* merge (Node *l, Node *r) { // ... if (...) { l->r = merge(l->r, r); upd(l); return l; } else { // ... } } ``` ```c++ typedef pair Pair; Pair split (Node *p, int x) { // ... if (...) { // ... upd(p); return {p, q.second}; } else { // ... } } ``` Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму: ```c++ int sum (int l, int r) { Pair rq = split(root, r); Pair lq = split(rq.first, l); int res = sum(lr.second); root = merge(lq.first, merge(lq.second, rq.second)); return res; } ``` # Неявный ключ Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку. Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными. Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева: ```c++ struct Node { int key, prior, size = 1; // ^ размер поддерева Node *l = 0, *r = 0; Node (int _key) { key = _key, prior = rand(); } }; ``` Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины. ```c++ int size (Node *v) { return v ? v->size : 0; } void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); } ``` `merge` не меняется, а вот в `split` нужно использовать позицию корня вместо его ключа. Про `split` теперь удобнее думать как "вырежи первые `k` элементов". ```c++ typedef pair Pair; Pair split (Node *p, int k) { if (!p) return {0, 0}; if (size(p->l) + 1 <= k) { Pair q = split(p->r, k - size(p->l) - 1); // ^ правый сын не знает количество вершин слева от него p->r = q.first; upd(p); return {p, q.second}; } else { Pair q = split(p->l, k); p->l = q.second; upd(p); return {q.first, p}; } } ``` Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно. ### Пример: ctrl+x, ctrl+v ```c++ Node* ctrlx (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l); root = merge(q2.first, q1.second); return q2.second; } ``` ```c++ void ctrlv (Node *v, int k) { Pair q = split(root, k); root = merge(q.first, merge(v, q.second)); } ``` ### Пример: переворот Нужно за $O(\log n)$ обрабатывать запросы переворота произвольных подстрок: значение $a_l$ поменять с $a_r$, $a_{l+1}$ поменять с $a_{r-1}$ и т. д. Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут: ```c++ struct Node { bool rev; // ... }; ``` Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку: ```c++ void push (node *v) { if (v->rev) { swap(v->l, v->r); if (v->l) v->rev ^= 1; if (v->r) v->rev ^= 1; } v->rev = 0; } ``` Аналогично, эту функцию будем вызывать в начале `merge` и `split`. Саму функцию `reverse` реализуем так: вырезать нужный отрезок, поменять флаг. ```c++ void reverse (int l, int r) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second->rev ^= 1; root = merge(q2.first, merge(q2.second, q1.second)); } ``` # Функциональное программирование* Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с $l$ по $r$, что-то с ним делаем и склеиваем обратно. Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке. ```c++ auto apply (int l, int r, auto f) { Pair q1 = split(root, r); Pair q2 = split(q1.first, l) q2.second = f(q2.second); root = merge(q2.first, merge(q2.second, q1.second)); } void reverse (Node *v) { if (v) v->rev ^= 1; } ``` Применять её нужно так: ```c++ apply(l, r, reverse); ``` Это работает в плюсах, начиная с `g++14`. Для простых операций можно даже написать лямбду: ```c++ apply(l, r, [](Node *v){ if (v) v->rev ^= 1; }); ``` # Персистентность* Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи. > Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции. Построим персистентное ДД. Тогда просто вызвав два `split`-а, мы можем получить копию любой подстроки (указатель вершину), которую потом можно вставлять куда угодно, при этом оригинальную подстроку мы не изменим. > Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции **и сравнение произвольных подстрок**. Можно в вершинах хранить **полиномиальный хэш** соответствующей подстроки. Тогда мы можем проверять равенство подстрок сравниванием хэшей вершин, полученных теми же двумя сплитами. Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ. Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию `copy`: ```c++ Node* copy (Node *v) { return new Node(*v); } ``` Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный `split` начнётся так: ```c++ Pair split (Node *p, int x) { p = copy(p); // ... } ``` В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию. У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен $L$, а размер правого $R$, то будем подвешивать за левое с вероятностью $\frac{L}{L+R}$, иначе за правое. **Теорема**. Такое переподвешивание эквивалентно приоритетам. **Доказательство**. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции: * Лист имеет вероятность 1 быть корнем себя (база индукции) * Переход индукции — операция `merge`. Любая вершина левого дерева была корнем с вероятностью $\frac{1}{L}$ (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью $\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}$. С вершинами правого дерева аналогично. Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД. ```c++ Node* merge (Node *l, Node *r) { if (!l) return r; if (!r) return l; l = copy(l), r = copy(r); if (rand() % (size(l) + size(r)) < size(l)) { // ... } else { // ... } } ``` Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и $x$, и $y$? +
+
diff --git a/ru/trie.html b/ru/trie.html index f6ff625..760386a 100644 --- a/ru/trie.html +++ b/ru/trie.html @@ -10,6 +10,8 @@ # Бор Бор — это структура данных для компактного хранения строк. Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин. ![trie](https://koenig-media.raywenderlich.com/uploads/2016/10/SwiftAlgClub_TrieData-trie-1.png) Бор можно удобно использовать для разных задач: * Хранение строк — занимает гораздо меньше места, чем массив или сет строк. * Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке. * Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи. ## Реализация Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: * терминальная ли вершина, * ссылки на детей, * возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет. ```c++ const int k = 26; struct Vertex { Vertex* to[k] = {0}; bool terminal = 0; }; Vertex *root = new Vertex(); ``` Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной. ```c++ void add_string (string &s) { v = root; for (char c : s) { c -= 'a'; if (!v->to[c]) v->to[c] = new Vertex(); v = v->to[c]; } v->terminal = true; } ``` Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть. Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности. ### Как хранить ссылки Хранить ссылки на детей не обязательно в массиве. Возможно, наш алфавит большой — у нас тогда просто не хватит памяти инициализировать столько массивов, большинство из которых будут пустыми. В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (`map`) или хэш-таблице (`unordered_map`). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У `map`-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например. Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями. ## Суффиксные ссылки > Пусть заданы $n$ *плохих* слов и большой текст $t$. Нужно найти суммарное количество их вхождений в этот текст. Эту и много других задач помогают решать *суффиксные ссылки*. Суффиксная ссылка для вершины $v$ — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине $v$, и присутствующий в боре. Будем считать, что мы их умеем быстро находить. Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок. Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за $O(nk)$, где $n$ и $k$ это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в [отдельную статью](http://sereja.me/a/aho-corasick). +
+
From a2e401921fdc014408ba6e96dafb05339b45ebf8 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 7 Jun 2019 14:34:28 +0000 Subject: [PATCH 018/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- favicon.png | Bin 0 -> 2544 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 favicon.png diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a4c38b5ff394461cc9e639829c3a031b74489bb GIT binary patch literal 2544 zcmVWFU8GbZ8()Nlj2>E@cM*00~q{L_t(|+U;FSOcYrZ zJyjrI9ex4ZKv>Yj#})+^MuvoiF-8{xqaiLt7Z@{fG^mN98(g^~5I^{XI0!PW3yrMQ zr~%Q$U}EGWaYAMQ5oa>Qj=&7kEdlz@qANWVc2)oC^JwZOwdk(Tt9MSl`|i88r5m7a|ej%brCR7j+B?IUV(XZZ{O8SB0IGb(+ z2>O0#g%9Yz52jz>g8<)RfDZ`$f<@C7AQbq{z+NO9IF9>Gbpi~vqpUjO`~*K4z$XJ| zSxNDyW9Iz;KT@!E43U>^e@s+=I^nA-SaQrfdGdq+ z5CFP-`7*2iqY9HOwDh8=i61!RT_fr!6Zs?E&I4ARolFf}zL`ae889HXP7 zNKH*;m56m5$Bp~j0_s_-MO$0jH{b)n{QNu`8yi_RP3!$?0j#?w#k?GMP*m z8ymx_RjZbjtAdxY`I(ZAjt)ui0bqQ59QW_vXSo&lhN+Ojz?_tpmWnJNPe*ZaG3&8q zi!TH)m$x%AGV+b-DRuPs_p?kuyQ~TlET11)YrCFIPsw(E^(xD1=ttQ!7sk>BiHK-s zW`+_H65L<7$z+mk0kN^MG&MEFax19z*aCzRTP=&G=j7z1`*%%E4YqFGDmzwaZEa<_ z71Vkh9SfPl%t>x;uKVkbi;JV_>1n!m@1ATc$j;6tr_;%DE6DRiKpksu+0oG<0`|m- z6GTKbH#bLVX=$<~Jbn6iw1FDxvu?jI^~(A(R~taFZsYZ5dwa_Q2g|A*;3kE2JA%3dEG9gWJ$%H`@G%4Ev_ zGdem-VPRqJFSo3$EZ{)>;v*&|h9)N`ne+?sa83Vb#`IiRSaAP7cI=o>_hn>cVCT-A zlIQJqJ6c*=SZo0~AV#K-F4g6^xw(Gb|KPy`*?u51Gm}OCP#1`iyq~6~CJ|swO-+8? z?{qrJY&Of1@c8j#)(98^5&grO>6w_ANOrs3e-Bq(oh$*RrKPM9@DGS+PVcvP_UxGm zsMDuU2lSkYi3y61j+P{V=Xn|(9c77tIi5AsGb}93tLZ5v2?+_({Td z)eY;dfN;$2qt+K7aY&Prd zABxi@Bzfk{84(ZpuEH3proWEHvkzK8LEDTRaI58 z1T0N=(NNkKkA^jy2fQ?fTU%SJYIy?#1F{70JWnGdBYF@}p-W;`ety2Y@{y5|WV6{+ zFSD?)P?m&1oqs_}y5^~g&!0aRRs6t#1De#dxw%=EfTW}(ayT6N=zlGfbIZ%iMHTPv z?$)HHGcz+37Z)c>LQ6}Fmf+WEo}T{k<3|b)4|i9%prAn0&+W*OBeDc+-n?1M*YTL1 z&Z}8sO-)TuA;)0Fo0}Q{y?*^#tJmQ*ouV{5J1d%6w0iYw*=#q}KD)d;7W_lia={ss z8YSMkbxTyy%F0Sz4Db5-dRYRZqN3>2r%!6XhDpZt<475+5J0rkB*W$Me}I(15oqPsj{EtM5D6570K}xo4+2T{cCObQuK7anKRI%1T(p3Vsh+e#SLF?A7ldRmf zZQGQroX^tG(BN0pEGsLE-n@CEzU3oBf=|0(%+JqLUtb?xxNw1ri;I1#TtY$u9XfP~ z+S=Nvudh!PLMA6C>FwLMbo1s-+P-~zK%q;HZ`AU5b^orrdZfo~5<;3ZTOODnZk zhlHvLfCyg5%GP7u>N=rp9)`FliKLTjx>UPL>ZL*`gJY%M9Lt0N_zplSTq`?f!L`1f z89*9 Date: Fri, 7 Jun 2019 14:39:39 +0000 Subject: [PATCH 019/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- favicon.png => favicon.ico | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename favicon.png => favicon.ico (100%) diff --git a/favicon.png b/favicon.ico similarity index 100% rename from favicon.png rename to favicon.ico From af1ac3f0f125c34634509e149dfac6e558b0d196 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 7 Jun 2019 15:58:07 +0000 Subject: [PATCH 020/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- favicon.ico | Bin 2544 -> 93062 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/favicon.ico b/favicon.ico index 4a4c38b5ff394461cc9e639829c3a031b74489bb..e53d898480120746d55ffa83fc4456f9089f27e9 100644 GIT binary patch literal 93062 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 literal 2544 zcmVWFU8GbZ8()Nlj2>E@cM*00~q{L_t(|+U;FSOcYrZ zJyjrI9ex4ZKv>Yj#})+^MuvoiF-8{xqaiLt7Z@{fG^mN98(g^~5I^{XI0!PW3yrMQ zr~%Q$U}EGWaYAMQ5oa>Qj=&7kEdlz@qANWVc2)oC^JwZOwdk(Tt9MSl`|i88r5m7a|ej%brCR7j+B?IUV(XZZ{O8SB0IGb(+ z2>O0#g%9Yz52jz>g8<)RfDZ`$f<@C7AQbq{z+NO9IF9>Gbpi~vqpUjO`~*K4z$XJ| zSxNDyW9Iz;KT@!E43U>^e@s+=I^nA-SaQrfdGdq+ z5CFP-`7*2iqY9HOwDh8=i61!RT_fr!6Zs?E&I4ARolFf}zL`ae889HXP7 zNKH*;m56m5$Bp~j0_s_-MO$0jH{b)n{QNu`8yi_RP3!$?0j#?w#k?GMP*m z8ymx_RjZbjtAdxY`I(ZAjt)ui0bqQ59QW_vXSo&lhN+Ojz?_tpmWnJNPe*ZaG3&8q zi!TH)m$x%AGV+b-DRuPs_p?kuyQ~TlET11)YrCFIPsw(E^(xD1=ttQ!7sk>BiHK-s zW`+_H65L<7$z+mk0kN^MG&MEFax19z*aCzRTP=&G=j7z1`*%%E4YqFGDmzwaZEa<_ z71Vkh9SfPl%t>x;uKVkbi;JV_>1n!m@1ATc$j;6tr_;%DE6DRiKpksu+0oG<0`|m- z6GTKbH#bLVX=$<~Jbn6iw1FDxvu?jI^~(A(R~taFZsYZ5dwa_Q2g|A*;3kE2JA%3dEG9gWJ$%H`@G%4Ev_ zGdem-VPRqJFSo3$EZ{)>;v*&|h9)N`ne+?sa83Vb#`IiRSaAP7cI=o>_hn>cVCT-A zlIQJqJ6c*=SZo0~AV#K-F4g6^xw(Gb|KPy`*?u51Gm}OCP#1`iyq~6~CJ|swO-+8? z?{qrJY&Of1@c8j#)(98^5&grO>6w_ANOrs3e-Bq(oh$*RrKPM9@DGS+PVcvP_UxGm zsMDuU2lSkYi3y61j+P{V=Xn|(9c77tIi5AsGb}93tLZ5v2?+_({Td z)eY;dfN;$2qt+K7aY&Prd zABxi@Bzfk{84(ZpuEH3proWEHvkzK8LEDTRaI58 z1T0N=(NNkKkA^jy2fQ?fTU%SJYIy?#1F{70JWnGdBYF@}p-W;`ety2Y@{y5|WV6{+ zFSD?)P?m&1oqs_}y5^~g&!0aRRs6t#1De#dxw%=EfTW}(ayT6N=zlGfbIZ%iMHTPv z?$)HHGcz+37Z)c>LQ6}Fmf+WEo}T{k<3|b)4|i9%prAn0&+W*OBeDc+-n?1M*YTL1 z&Z}8sO-)TuA;)0Fo0}Q{y?*^#tJmQ*ouV{5J1d%6w0iYw*=#q}KD)d;7W_lia={ss z8YSMkbxTyy%F0Sz4Db5-dRYRZqN3>2r%!6XhDpZt<475+5J0rkB*W$Me}I(15oqPsj{EtM5D6570K}xo4+2T{cCObQuK7anKRI%1T(p3Vsh+e#SLF?A7ldRmf zZQGQroX^tG(BN0pEGsLE-n@CEzU3oBf=|0(%+JqLUtb?xxNw1ri;I1#TtY$u9XfP~ z+S=Nvudh!PLMA6C>FwLMbo1s-+P-~zK%q;HZ`AU5b^orrdZfo~5<;3ZTOODnZk zhlHvLfCyg5%GP7u>N=rp9)`FliKLTjx>UPL>ZL*`gJY%M9Lt0N_zplSTq`?f!L`1f z89*9 Date: Fri, 7 Jun 2019 23:30:35 +0000 Subject: [PATCH 021/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/index.html | 17 ----------------- metrica.html => head.html | 2 ++ pandoc.css | 20 ++++++++++++++++---- ru/aho-corasick.html | 17 ----------------- ru/annealing.html | 17 ----------------- ru/bayans.html | 17 ----------------- ru/bitset.html | 17 ----------------- ru/centroid.html | 17 ----------------- ru/convex-hulls.html | 17 ----------------- ru/cross-entropy.html | 17 ----------------- ru/cuda.html | 22 ---------------------- ru/dp-intro.html | 17 ----------------- ru/dp-optimizations.html | 17 ----------------- ru/dp_intro.html | 17 ----------------- ru/dp_optimizations.html | 17 ----------------- ru/fenwick.html | 22 ---------------------- ru/games.html | 17 ----------------- ru/geometry.html | 17 ----------------- ru/hashing.html | 17 ----------------- ru/hld.html | 19 ------------------- ru/index.html | 17 ----------------- ru/ioi.html | 17 ----------------- ru/karatsuba.html | 17 ----------------- ru/lca.html | 17 ----------------- ru/linalg.html | 18 ------------------ ru/matching.html | 17 ----------------- ru/matroid.html | 17 ----------------- ru/mincost-maxflow.html | 17 ----------------- ru/mst.html | 17 ----------------- ru/pollard.html | 17 ----------------- ru/probability.html | 17 ----------------- ru/reciprocal.html | 17 ----------------- ru/segtree.html | 17 ----------------- ru/sparse-table.html | 17 ----------------- ru/sse.html | 17 ----------------- ru/stress-test.html | 17 ----------------- ru/strings.html | 17 ----------------- ru/suffix-array.html | 19 ------------------- ru/treap.html | 17 ----------------- ru/trie.html | 17 ----------------- 40 files changed, 18 insertions(+), 665 deletions(-) delete mode 100644 en/index.html rename metrica.html => head.html (91%) delete mode 100644 ru/aho-corasick.html delete mode 100644 ru/annealing.html delete mode 100644 ru/bayans.html delete mode 100644 ru/bitset.html delete mode 100644 ru/centroid.html delete mode 100644 ru/convex-hulls.html delete mode 100644 ru/cross-entropy.html delete mode 100644 ru/cuda.html delete mode 100644 ru/dp-intro.html delete mode 100644 ru/dp-optimizations.html delete mode 100644 ru/dp_intro.html delete mode 100644 ru/dp_optimizations.html delete mode 100644 ru/fenwick.html delete mode 100644 ru/games.html delete mode 100644 ru/geometry.html delete mode 100644 ru/hashing.html delete mode 100644 ru/hld.html delete mode 100644 ru/index.html delete mode 100644 ru/ioi.html delete mode 100644 ru/karatsuba.html delete mode 100644 ru/lca.html delete mode 100644 ru/linalg.html delete mode 100644 ru/matching.html delete mode 100644 ru/matroid.html delete mode 100644 ru/mincost-maxflow.html delete mode 100644 ru/mst.html delete mode 100644 ru/pollard.html delete mode 100644 ru/probability.html delete mode 100644 ru/reciprocal.html delete mode 100644 ru/segtree.html delete mode 100644 ru/sparse-table.html delete mode 100644 ru/sse.html delete mode 100644 ru/stress-test.html delete mode 100644 ru/strings.html delete mode 100644 ru/suffix-array.html delete mode 100644 ru/treap.html delete mode 100644 ru/trie.html diff --git a/en/index.html b/en/index.html deleted file mode 100644 index 3865303..0000000 --- a/en/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - -some english index -
- -
- - diff --git a/metrica.html b/head.html similarity index 91% rename from metrica.html rename to head.html index 5dbaa4f..51ded08 100644 --- a/metrica.html +++ b/head.html @@ -1,3 +1,5 @@ + + + + + + +

some english index

+ + diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html new file mode 100644 index 0000000..7da3680 --- /dev/null +++ b/ru/aho-corasick.html @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + +

Алгоритм Ахо-Корасик

+

Пусть дан набор строк в алфавите размера \(k\) суммарной длины \(n\). Алгоритм Ахо-Корасик за \(O(nk)\) времени и памяти строит бор для этого набора строк, а затем по этому бору строит автомат, который может использоваться в различных строковых задачах — например, нахождения всех вхождений каждой строки из данного набора в произвольный текст за линейное время.

+

Алгоритм был назван именами создателей — Альфреда Ахо и Маргарет Корасик.

+

+

Автор специализируется на компьютерной лингвистике и когда-то работал над созданием разговорного интеллекта — этот модуль назывался «Болталка», говорящая на произвольные темы.

+
+

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

+
+

Эту и много других задач помогают решать суффиксные ссылки. Суффиксная ссылка для вершины \(v\) — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине \(v\), и присутствующий в боре. Будем считать, что мы их умеем быстро находить.

+

Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

+

Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за \(O(nk)\), где \(n\) и \(k\) это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в отдельную статью.

+

Зачем это нужно

+
+

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

+
+

Добавим все плохие слова в бор, и будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор.

+

Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, по которым можно дойти по суффиксным ссылкам. Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

+

Помимо суффиксных ссылок, нужно найти ещё переходы, чтобы поддерживать самый длинный суффикс.

+

Алгоритм Ахо-Корасик*

+

Заметим, что всего суффиксных ссылок нужно найти \(O(n)\), а переходов — \(O(nk)\). Суффиксные ссылки и переходы можно быстро найти динамикой.

+

Ссылки. Мы можем сделать так: пройти на символ назад, а оттуда пройти по суффиксной ссылке, и уже оттуда вызвать переход.

+

Переходы. Вот к нашей строке прибавился символ — нужно найти самую длинную строку. Для этого можно откатываться по суффиксным ссылкам, пока не придем в вершину, из которой данный переход есть. Рано или поздно мы либо попадем в такую вершину, либо попадем в корень. Но можно поступить лениво, и просто откатиться на одну суффиксную ссылку и взять уже посчитанный переход оттуда.

+

+
const int k = 26;
+
+struct Vertex {
+    Vertex *to[k] = {0}, *go[k] = {0};
+    Vertex *link = 0, *p;
+    int pch;
+    Vertex (int _pch, Vertex *_p) { pch = _pch, p = _p; }
+};
+
+Vertex *root = new Vertex(-1, 0);
+
void add_string (string s) {
+    Vertex *v = root;
+    for (char _c : s) {
+        c -= 'a';
+        if (!v->to[c])
+            v->to[c] = new Vertex(c, v);
+        v = v->to[c];
+    }
+}
+

Нам нужно объявить две функции, которые будут ссылаться друг на друга. В интерпретируемых языках (например, в питоне) можно просто объявить две функции, а вот C++ так не умеет — нужно сначала все объявить, а потом ссылаться.

+
Vertex* go (Vertex *v, int c);
+
+Vertex* link (Vertex *v) {
+    if (!v->link) {
+        if (v == root || v->p == root) v->link = root;
+        else v->link = go(link(v->p), v->pch);
+    }
+    return v->link;
+}
+
+Vertex* go (Vertex *v, int c) {
+    if (!v->go[c]) {
+        if (v->to[c]) v->go[c] = v->to[c];
+        else if (v == root) v->go[c] = root;
+        else v->go[c] = go(link(v), c);
+    }
+    return v->go[c];
+}
+ + diff --git a/ru/annealing.html b/ru/annealing.html new file mode 100644 index 0000000..0b1b3f6 --- /dev/null +++ b/ru/annealing.html @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + +

Метод отжига

+
    +
  • Эвристический метод глобальной оптимизации
  • +
  • Работает с дискретными функциями в предположении, что у близких «точек» близкое значение, и есть что-то типа градиента
  • +
  • Очень простой в написании
  • +
  • Никаких гарантий нет, но хорошо работает на практике
  • +
  • Метод настолько мощный, что иногда им можно сдать задачи, у которых автором предполагалось адекватное решение (пример: Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам)
  • +
+
+ + +
+

Постановка задачи

+

Имеется некоторая функция \(f(x)\), которую мы хотим минимизировать.

+

Наука решать эту задачу в общем случае не умеет, и вряд ли когда-либо научится.

+

TODO: описание алгоритма, физическая аналогия, конструирование гладких функций

+

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

+

Ферзи

+

Рассмотрим такую задачу: дана шахматная доска \(n \times n\) и \(n\) ферзей. Нужно расставить их так, чтобы они не били друг друга.

+

Будем кодировать состояние перестановкой чисел от \(1\) до \(n\). Такое представление кодирует не все состояния, но оно точно не учитывает те, где ферзи бьют друг друга по вертикали или горизонтали.

+

Выберем такую функцию: fitness(p) = число пар ферзей, которые бьют друг друга.

+

Важная деталь: скорость одной итерации прямо пропорционально скорости работы fitness, поэтому очень важно реализовать её эффективно. Конкретно здесь её можно реализовать за \(O(n^2)\) (используется в коде для наглядности), \(O(n)\) и даже \(O(1)\).

+

Мы будем использовать python, потому что в нём удобнее строить графики. Эквивалентный код на C++ приведена сразу после.

+
import random
+
+# Эти библиотеки будут нужны, чтобы рисовать графики:
+import matplotlib.pyplot as plt
+%matplotlib inline
+
+import seaborn as sns
+sns.set()
+
def fitness(p):
+    s = 0
+    for i in range(len(p)):
+        d = 1
+        for j in range(i):
+            if abs(i-j) == abs(p[i]-p[j]): # ферзи стоят на одной диагонали
+                d = 0
+        s += d
+    return s
+
n = 100 # кол-во ферзей
+k = 1000 # кол-во итераций
+p = list(range(n)) # исходная перестановка
+
+scores = []
+
+t = 1
+for i in range(k):
+    # уменьшаем температуру
+    t *= 0.99 # этот параметр лучше перебрать руками
+    
+    # копируем перестановку и делаем небольшое изменение
+    q = p[:]
+    a, b = random.sample(range(n), 2)
+    q[a], q[b] = q[b], q[a]
+    
+    # считаем разницу fitness и решаем, нужно ли переходить в новое состояние
+    d = fitness(q) - fitness(p)
+    if d > 0 or random.random() < math.exp(d/t):
+        p = q[:]
+    scores += [fitness(q)]
+
+print('Best score:', max(scores))
+plt.plot(scores)
+plt.show()
+
Best score: 95
+
+png +

png

+
+

Примерно эквивалентный код на C++:

+
const int n = 100, k = 1000;
+
+int f(vector<int> p) {
+    int s = 0;
+    for (int i = 0; i < n; i++) {
+        int d = 1;
+        for (int j = 0; j < i; j++)
+            if abs(i-j) == abs(p[i]-p[j]):
+                d = 0;
+        s += d;
+    }
+    return s;
+}
+
+double rnd() { return double(rand()) / RAND_MAX; }
+
+int main() {
+
+    for (int i = 0; i < n; i++)
+        v[i] = i;
+
+    int ans = 1;
+
+    double t = 1;
+    for (int i = 0; i < k; i++) {
+        t *= 0.99;
+        vector<int> u = v;
+        swap(u[rand()%n], u[rand()%n]);
+        int val = f(u);
+        if (val > ans || rnd() < exp((val-ans)/t))
+            v = u, ans = val;
+        if (ans == n) break;
+    }
+
+    for (int x : v)
+        cout << x+1 << " ";
+
+    return 0;
+}
+ + diff --git a/ru/bayans.html b/ru/bayans.html new file mode 100644 index 0000000..d5695ab --- /dev/null +++ b/ru/bayans.html @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + +

Красивые идейные задачи

+

Везде, где не указано — время работы \(O(n)\), а если есть конкретные числа, то TL 1 секунда.

+

Задачи идут в порядке вспоминания, то есть в весьма рандомном.

+

Попугаи

+

Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы.

+

Минимум и максимум

+

Даны 68 камней разного веса. Требуется за 100 операций взвешивания определить самый лёгкий и самый тяжелый.

+

Перестановка

+

Есть перестановка из 64 элементов. Вы можете спрашивать, какие элементы находятся на заданном множестве позиций (вам сообщается список элементов в произвольном порядке). Восстановите перестановку за 6 запросов.

+

Выпуклая оболочка

+

Требуется отвечать на 2 типа запросов:

+
    +
  1. Добавить точку в выпуклую оболочку.
  2. +
  3. Проверить, лежит ли точка внутри выпуклой оболочки.
  4. +
+

Обе операции онлайн за \(O(\log n)\).

+

Геометрическая прогрессия

+

Найдите способ посчитать \(\frac{1-a^n}{1-a}\) по произвольному модулю за \(O(\log n)\).

+

Покемоны

+

В турнире участвуют 1024 видов покемонов. Вы, будучи экспертом по покемонам, знаете, кто кого может победить. Иначе говоря, вам полностью известен граф-турнир из 1024 вершин и \(1023 \times 1022 : 2\) рёбер, в котором каждые две вершины соединены ровно одним ориентированным ребром, определяющим победителя в возможном поединке. Обратите внимание, что из \(a \to b\) и \(b \to c\) не следует, что \(a \to c\).

+

У вас есть 10 покеболов. Составьте команду из 10 покемонов такую, что для каждого из 1024 типов покемонов в вашей команде найдется покемон, побеждающий его. Считайте, что в битве одинаковых покемонов побеждает ваш.

+

Сортировка

+

Можно ли отсортировать * 5 камней за 8 взвешиваний? * 5 камней за 7 взвешиваний? * 20 камней за 60 взвешиваний?

+

Точки в круге

+

Даны \(n\) точек, равномерно распределенных в единичном круге с центром в начале координат. Предложите алгоритм, который за \(O(n)\) в среднем сортирует их по удаленности от начала координат.

+

Замкнутые ломаные

+

Даны две замкнутые несамопересекающиеся ломаные. Определите, можно ли перевести их друг в друга с помощью параллельного переноса, поворотов и гомотетии?

+

Неубывающий массив

+

Дан массив из \(n\) целых чисел. Требуется за \(2n\) операций «прибавить к одному элементу любой другой» сделать его неубывающим.

+

Чётный цикл

+

Дан неориентированный граф. Определите, есть ли в нём простой цикл чётной длины.

+

\(k\)-ая порядковая статистика

+

Дан массив из \(n\) целых чисел. Найдите его \(k\)-й наименьший элемент за \(O(n)\).

+

Доминирующий элемент

+

Дан массив из \(n\) элементов. Требуется ответить на \(m\) запросов, есть ли на отрезке \([l, r]\) доминирующий элемент — тот, который встречается на нём хотя бы \(\frac{r-l}{2}\) раз. Время работы \(O((n+m) \log n)\).

+

Разрушение дерева

+

Дано корневое дерево. Каждую итерацию выбирается вершина (равновероятно из всех оставшихся), и удаляется всё поддерево, соответствующее этой вершине. Найти, сколько ходов в среднем будет продолжаться этот процесс, то есть матожидание номера итерации, на которой будет удалён корень дерева.

+

\(k\)-ый элемент на отрезке

+

Дан массив из \(n\) целых чисел. Требуется ответить на \(m\) запросов \(k\)-ой порядковой статистики на произвольном отрезке. Время работы \(O((n+m) \log n)\).

+

Различные числа на отрезке

+

Дан массив из \(n\) целых чисел. Требуется ответить на \(m\) запросов количества различных элементов на произвольном отрезке. Время работы \(O(m\sqrt{n})\).

+

Физкультура

+

Зачёт по физкультуре в одном институте ставится по количеству посещений, поэтому важно знать, сколько учебных дней осталось до конца семестра (особенно когда напропускал пары). Семестр длится \(m\) дней. Деканат последовательно издает \(n\) приказов двух типов:

+
    +
  1. Объявить все дни с \(l\) по \(r\) выходными (физру закрывать нельзя)
  2. +
  3. Объявить все дни с \(l\) по \(r\) учебными (физру закрывать можно)
  4. +
+

При этом приказ может частично отменить действие предыдущих приказов.

+

После каждого приказа нужно посчитать суммарное число учебных дней в семестре. Асимптотика \(O(n \log n)\).

+

Нулевая сумма

+

Дано мультимножество из \(n\) целых чисел. Найдите любое его подмножество, сумма чисел которого делится на \(n\).

+

Мета-задача

+

В задаче дана произвольная строка, по которой известным только авторам способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердикта всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA. «Решите» задачу.

+

Ниточка

+

В плоскую доску вбили \(n\) гвоздей радиуса \(r\), причём так, что соответствующие точки на плоскости образуют вершины выпуклого многоугольника. На эти гвозди натянули ниточку, причём ниточка «огибает» по кругу гвозди. Найдите длину ниточки, то есть периметр этого многоугольника с учётом закругления.

+

Пельмени

+

Компания друзей захотела сделать заготовки для пельменей из огромного прямоугольного куска теста. Для этого в ход пошли всевозможные предметы округлой формы: стаканы, кружки, кострюли… В итоге тесто было разделено \(n\) возможно пересекающимися окружностями произвольных радиусов и центров. Нам интересно посчитать, сколько получилось заготовок, то есть на сколько кусков распалось тесто. Асимптотика \(O(n^2 \log n)\).

+

От нуля до единицы

+

Дан следующий код:

+
x = 0
+while x < 1:
+    x += random()
+

Требуется посчитать матожидание x.

+

(random в питоне возвращает случайное действительное число от 0 до 1.)

+

Площадь

+

Дан единичный квадрат и 100 кругов. Нужно посчитать площадь квадрата, которую покрывают эти круги, с ошибкой менее 1%.

+

Окружности

+

Имеется окружность радиуса \(R\), назовём её внешней. Внутри неё лежит окружность радиуса \(r < R\) и соприкасается с ней. Дальше строятся бесконечное число окружностей по следующему правилу: \(k\)-я окружность должна

+
    +
  • соприкасаться с внешней,
  • +
  • соприкасаться с предыдущей ((\(k-1\))-ой),
  • +
  • иметь при этом максимальный радиус.
  • +
+

Найдите (выведите формулу за \(O(1)\)) радиус \(k\)-й такой окружности.

+

Блеф

+

Катя и Серёжа играют в игру. У Кати есть \(n\) карт, у Серёжи — \(m\). Одна дополнительная карта лежит на столе рубашкой вверх. Назовём её особой. Цель игроков — её отгадать. Все \(n+m+1\) карт различны, игроки изначально знают только свои карты. Игроки ходят по очереди, начинает Катя. Игрок в свой ход может:

+
    +
  • Попытаться угадать особую карту. Если получилось угадать, то игрок победил, иначе проиграл. В любом случае, игра на этом заканчивается.
  • +
  • Назвать любую карту из колоды. Если у соперника есть такая карта — он обязан показать ее и вывести из игры. Если же у него нет такой карты — он сообщает об этом.
  • +
+

С какой вероятностью выиграет Катя при оптимальной игре обоих игроков? Асимптотика \(O(nm)\).

+

Достижимость

+

Дан ориентированный граф без кратных рёбер. Для всех пар вершин \(u\) и \(v\) определите, можно ли дойти из \(u\) в \(v\). Вершин меньше 2000.

+

Нумизмат

+

Есть \(n\) жадных коллекционеров монет, которые согласны меняться, только если взамен монеты, которых у них более одной, они получают монету, которых у них нет вовсе. Всего есть \(k\) типов монет. Известно количество монет каждого типа у каждого коллекционера и у коллекционера Серёжи. Серёжа хочет максимизировать количество различных монет у него путем обмена с жадными коллекционерами. Считайте, что жадные коллекционеры не меняются между собой.

+

Придумайте любой полиномиальный алгоритм.

+

Принцесса

+

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

+

Она составила список из \(n\) самых красивых юношей и вызывает их каждый день в случайном порядке. После того, как к ней приходит очередной юноша, она принимает решение: либо выйти за него замуж, либо нет. Если она выходит за него замуж, то она все равно просматривает всех остальных, чтобы убедиться, что он действительно самый красивый. Если это так, то они живут долго и счастливо. Если нет (если она вышла замуж не за самого красивого, либо не вышла замуж вообще), то принцесса покончит жизнь самоубийством.

+

У принцессы очень хорошая память, поэтому она может сравнивать красоту очередного юноши со всеми предыдущими, а также у неё тонкий вкус, поэтому никакие двое юношей не являются для неё одинаково красивыми.

+

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

+

Асимптотика \(O(n^2)\).

+

Спираль

+

Определим спираль \((2n+1) \times (2n+1)\) как матрицу следующего вида:

+

\[ +\begin{matrix} +21 & 22 & 23 & 24 & 25 \\ +20 & 7 & 8 & 9 & 10 \\ +19 & 6 & 1 & 2 & 11 \\ +18 & 5 & 4 & 3 & 12 \\ +17 & 16 & 15 & 14 & 13 \\ +\end{matrix} +\]

+

Ваша задача — рассчитать ответы на \(q\) запросов суммы чисел в произвольной прямоугольной области (по модулю \(10^9+7\)).

+

\(q \leq 100\), \(n \leq 10^9\).

+

Польский лабиринт

+

Группа из \(n\) туристов гуляет в бесконечном лабиринте. Лабиринт имеет форму треугольника Серпинского: клетка \((x, y)\) свободна, только если x & y == 0.

+
+ + +
+

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

+

\(n \leq 10^5\), изначальные координаты туристов до \(10^9\).

+

Нимные подмножества

+

Есть множество \(A\), состоящее из \(n\) чисел от 0 до \(2^{32}-1\). Требуется выбрать его подмножество \(B \subseteq A\) максимальной суммы такое, что нельзя выбрать его подмножество \(C \subseteq B\) такое, что ним на кучках соответствующего размера — проигрышный. Асимптотика \(O(n \log n)\).

+

Баланс степеней

+

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

+

Два пути

+

Дан ориентированный граф. Найдите два непересекающихся по рёбрам пути из \(s\) в \(t\).

+

Пьяница

+

Пьяница стоит на числовой прямой в точке 0. Каждую секунду он делает единичный шаг вправо (в сторону увеличения координат) с вероятностью \(p\) и влево с вероятностью \(1-p\). С какой вероятностью он когда-либо окажется в точке с отрицательной координатой?

+

Ксоровый рюкзак

+

Дан массив из \(10^5\) целых чисел от \(0\) до \((2^{30}-1)\). Найти количество различных подпоследовательностей этого массива, xor-сумма которых равна заданному числу \(x\).

+

Иван Сусанин

+

Польская армия хочет добраться из поселения \(s\) в поселение \(t\). Ей руководят два гетмана — Камиль и Матеуш.

+
    +
  • Камиль руководит армией днём и водит армию по дорогам.
  • +
  • Матеуш руководит армией ночью и совершает маневры по секретным тропам.
  • +
+

Каждый гетман перед маршем спрашивает дорогу у Ивана Сусанина. Иван хочет задержать наступление польских войск.

+

Карта дорог известна Камилю. Аналогично, карта секретных троп известна Матеушу. Поэтому Ивану не удастся их так просто обмануть — он должен каждый раз выбрать переход так, что минимальное расстояние между поселением \(t\) и войском по соответствующей карте строго уменьшилось.

+

Вы знаете карту дорог и троп вместе с их длинами. Помогите Ивану как можно дольше (желательно, бесконечно) вести армию из \(s\) в \(t\).

+

Варенье

+

В ряд стоят \(n\) пустых банок из-под варенья. Вместительность \(i\)-й банки равна \(v_i\) грамм.

+

Карлсон наполняет эти банки вареньем в \(m\) этапов. На каждом этапе он выбирает числа \(l\), \(r\), \(x\) и \(y\), а затем пролетает над банками с \(l\) по \(r\), выполняя следующие операции: в банку номер \(l\) он добавляет \(x\) грамм варенья, в банку номер \((l + 1)\)\((x + y)\) грамм варенья, в банку номер \((l + 2)\)\((x + 2y)\), и так далее до \(r\)-той банки, в которую он положит \(x + y(r - l)\) грамм варенья.

+

Малышу хочется определить для каждой банки наименьший номер операции, после которой она станет полной.

+

\(n, m \leq 10^5\)

+

Лабиринт

+

Серёжа потерялся в лабиринте \(n \times m\). Каждая клетка либо свободна, либо стена. Все крайние клетки — стены. Вам известно, где находится выход из лабиринта, но не известно, где находится Серёжа. Составьте для него последовательность направлений (вверх, вниз, влево, вправо) такую, после которой он окажется в клетке с выходом, вне зависимости от его стартовой позиции. Если вы прикажете ему идти в стену, то просто ничего не произойдёт.

+

Придумайте любой полиномиальный алгоритм.

+

Обезьяна

+

Дана строка из \(10^5\) символов латинского алфавита. Обезьяна нажимает случайные клавиши на клавиатуре (одну из 26 букв), пока не наберёт её целиком, то есть пока исходная подстрока не станет подстрокой набранной строки. Какое ожидание числа нажатых клавиш перед тем, как это произойдёт?

+

Ожидание минимума

+

Даны \(n\) случайных величин, равномерно распределенных на отрезках \([l_i, r_i]\) — у каждой величины свой отрезок. Найдите математическое ожидание минимума этих случайных величин.

+

Придумайте любой точный полиномиальный алгоритм.

+

Шумный ксор

+

Загадано некое число \(x\). Вы можете делать запросы следующего типа: назвать число \(y\) и получить в ответ число единичных битов в ксор-сумме \(x\), \(y\) и \(m\), где \(m\) это случайно сгенерированная маска, в которой каждый бит имеет вероятность \(p = \frac15\) быть единичным, то есть каждый бит \(x \oplus y\) заменяется на противоположный с вероятностью \(y\), и вам возвращается количество единичных битов. Для ясности:

+
x = # ...
+
+def mask(p=0.2):
+    r = 0
+    for i in range(32):
+        if random.random() < p:
+            r += 2**i
+    return r
+
+def query(y):
+    return bin(x ^ y ^ mask()).count('1')
+

Ваша задача — отгадать число, используя не более 10000 попыток.

+ + diff --git a/ru/bitset.html b/ru/bitset.html new file mode 100644 index 0000000..302a5ea --- /dev/null +++ b/ru/bitset.html @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + +

Битовое сжатие

+
    +
  • Из-за него в «асимптотиках» появляется /64
  • +
  • На Всеросе часто дают задачи, где оно может принести «бесплатные» ~20 баллов
  • +
  • bitset есть в stl; говорят, самописный быстрее
  • +
+

Процессор так устроен, что работает сразу с блоками по 32 или 64 бита (зависит от архитектуры, но получить что-то меньше одного байта он в принципе не может). Иными словами, сделать & двух bool-ом и двух long-ов примерно одинаково по скорости.

+

Часто нам требуется сделать много одинаковых операций над элементами булевого массива. Проксорить два массива, например. Здесь появляется такая идея: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем ксорить сразу все 64 бита за одну операцию.

+

Это всё можно кодить и вручную, но в STL это уже сделали до нас, создав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями — bitset.

+

Работать с ним нужно вот так:

+
const int lim = 1000;
+bitset<lim> b; // создать битсет размера lim (должно быть константой)
+b.set();       // заполнить единицами
+b.reset();     // заполнить нулями
+b.flip();      // заменить единички на нули и наоборот
+b.count();     // посчитать число единичек
+cout << b;     // вывести битовую строку
+

Также для битсетов работает вся битовая арифметика — &, |, ^, ~, <<, >> и их варианты с [operator]=.

+

Рюкзак

+

Задача: даны \(n\) предметов с положительными целыми весами \(a_i\) и рюкзак размера \(lim\), выбрать подмножество предметов с максимальной суммой, не превышающий размер рюкзака.

+

Обычно его решают так:

+
bool dp[lim] = {}; // так можно его заполнить нулями
+dp[0] = 1;
+for (int i = 0; i < n; i++)
+    for (int x = lim - a[i]; x >= 0; x--)
+        dp[x + a[i]] |= dp[x];
+

…а с битсетом оно разгоняется так:

+
bitset<lim> b;
+b[0] = 1;
+for (int i = 0; i < n; i++)
+    b |= b << a[i];
+

Цикл длины 3

+

Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из \(n\) вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за \(O(\frac{n^3}{64})\) следующим образом:

+
bitset<maxn> g[maxn]; // матрица смежности
+for (int a = 0; a < n; a++) {
+    for (int b = 0; b < n; b++) {
+        if (g[a][b] && (~g[a] & g[b]).any()) {
+            // цикл найден
+        }
+    }
+}
+

Benchmark: на серверах CodeForces этот код при \(n = 5000\) работает за 7 секунд.

+

Перемножение матриц

+

Матрица смежности графа, возведенная в степень \(n\), имеет комбинаторный смысл: количество способов дойти из \(a\) в \(b\), используя ровно \(n\) переходов. Иногда нам не нужно знать число способов, и нам просто хватит знания, можно ли вообще через \(n\) ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения:

+
typedef bitset<maxn> t;
+typedef array<t, maxn> matrix;
+
+matrix operator* (matrix a, matrix b) {
+    matrix c;
+    for(int i = 0; i < n; i++)
+        for(int j = 0; j < n; j++)
+            if(a[i][j])
+                c[i] |= b[j];
+    return c;
+}
+

Гаусс

+

Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

+

Нас по сути просят решить следующую систему:

+

\[ +\begin{cases} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ +\ldots \\ +a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 +\end{cases} +\]

+

Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

+

В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

+
t gauss (matrix a) {
+    for (int i = 0; i < n; i++) {
+        int nonzero = i;
+        for (int j = i+1; j < n; j++)
+            if (a[j][i])
+                nonzero = j;
+        swap(a[nonzero], a[i]);
+        for (int j = 0; j < n; j++)
+            if (j != i && a[j][i])
+                a[j] ^= a[i];
+    }
+    t x;
+    for (int i = 0; i < n; i++)
+        x[i] = a[i][n] ^ a[i][i];
+    return x;
+}
+

Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

+ + diff --git a/ru/centroid.html b/ru/centroid.html new file mode 100644 index 0000000..9b6c9c6 --- /dev/null +++ b/ru/centroid.html @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + +

Центроидная декомпозиция

+

Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».

+

Иногда вместо неё можно написать Heavy-light декомпозицию (чуть сложнее) или метод переливаний (чуть проще).

+
+ + +
+

Определение. Центром или цетроидом (англ. centroid) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более \(\frac{n}{2}\).

+

Выясняется, что центр всегда существует — это следует из алгоритма его поиска:

+
int s[maxn];
+
+int sizes (int v) {
+    s[v] = 1;
+    for (int u : g[v])
+        // для простоты считаем, что дерево корневое
+        s[v] += sizes(u);
+    return s[v];
+}
+
+// второй параметр -- размер дерева
+int centroid (int v, int n) {
+    for (int u : g[v])
+        if (s[u] > n/2)
+            return centroid(u, n);
+    return v;
+}
+

Утверждение. centroid действительно находит цетроид.

+

Доказательство:

+
    +
  • centroid вернет вершину, у которой размеры всех детей не больше \(\frac{n}{2}\) (это явно проверяется в if-е).
  • +
  • Мы пришли в эту вершину, когда её размер был больше \(\frac{n}{2}\), а это значит, что в «обратном» направлении есть не более \(n - (\frac{n}{2}+1) = \frac{n}{2}-1\) вершин.
  • +
  • Значит, размеры всех соседей не больше половины \(n\), и алгоритм корректен.
  • +
+

Иногда центров два (пример: 1-2-3-4), тогда алгоритм вернёт «нижний» центроид.

+

Определение. Центроидной декомпозицией будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».

+

Определение. Компонентой центроида будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.

+

Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты.

+

Теперь поймём, зачем мы всё это делали.

+

Утверждение. Каждая вершина входит в \(O(\log n)\) компонент.

+

Доказательство. Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более \(\lceil \log_2 n \rceil\) разделений.

+

Следствие. Центроидная декомпозиция (см. определение выше) работает за \(O(n \log n)\).

+

Утверждение. Для любого пути \(a \leadsto b\) есть единственный центроид \(c\), в чьей компоненте были и \(a\), и \(b\).

+

Доказательство. Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила \(a\) и \(b\).

+

Это очень важные выводы. Получается, что в процессе центроидной декомпозиции и для каждого простого пути будет ровно одна ситуация, когда этот путь содержится в какой-то компоненте и проходит через центроид. Ровно в этот момент мы можем обработать какой-то запрос, пользуясь техникой, похожей на метод переливаний.

+

Подсчет путей с заданным свойством

+

Рассмотрим конкретный пример: подсчёт путей заданной длины.

+

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

+

Количество таких путей можно посчитать за размер текущей компоненты: заведём массив d, в котором будем хранить количество вершин на каждом расстоянии от 0 до размера компоненты. Подвесим наше дерево-компоненту за центроид и будем запускать от его непосредственных детей dfs, который будет возвращать временный массив t — список глубин вершин в этом поддереве. Мы можем пройтись по каждому значению x в нём и добавить к ответу d[l-x], а затем добавить все значения из t в d. Можно убедиться, что таким образом каждый интересующий нас путь будет учтён ровно один раз.

+
int l = 179; // нужная нам длина
+int ans = 0;
+
+// нам лень явно удалять вершины: заведем массив used -- была ли вершина удалена
+bool used[maxn];
+int s[maxn]; // размеры поддеревьев
+
+void sizes (int v, int p) {
+    s[v] = 1;
+    for (int u : g[v])
+        if (u != p && !used[u])
+            sizes(u, v), s[v] += s[u];
+}
+
+int centroid (int v, int p, int n) {
+    for (int u : g[v])
+        if (u != p && !used[u] && s[u] > n/2)
+            return centroid(u, v, n);
+    return v;
+}
+
+// записывает в t[] глубины вершин
+void dfs (int v, int p, int d, vector<int> &t) {
+    t.push_back(d);
+    for (int u : g[v])
+        if (u != p && !used[u])
+            dfs(u, v, d + 1, t);
+} 
+
+void solve (int v) {
+    /* <единственный зависящий от конкретной задачи код> */
+    size(v);
+    vector<int> d(s[v], 0);
+    d[0] = 1;
+    for (int u : g[v]) {
+        if (!used[u]) {
+            vector<int> t;
+            dfs(u, v, 1, t);
+            for (int x : t)
+                if (x <= l)
+                    ans += d[l-x];
+            for (int x : t)
+                d[x]++;
+        }
+    }
+    /* </единственный зависящий от конкретной задачи код> */
+
+    used[v] = 1;
+    for (int u : g[v])
+        if (!used[u])
+            solve(centroid(u, v, s[u]/2));
+}
+

Асимптотика \(O(n \log n)\), потому что на каждую из \(O(n)\) верщин мы потратим \(O(1)\) операций на каждом из \(O(\log n)\) «уровней» центроидной декомпозиции.

+

Запросы на путях — offline

+

Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.

+

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

+

Например, при запросах суммы на пути, мы можем насчитать во внутреннем dfs для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины.

+

Таким образом, каждый запрос будет просмотрен \(O(\log n)\) раз, пока не будет удален, и асимптотика составит \(O(q \log n + n \log n)\).

+

Запросы на путях — online

+

Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.

+

Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив centroid[][] размера \(n \times \log n\), в котором для каждой вершины будем хранить \(O(\log n)\) центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.

+

Тогда, при ответе на запрос, мы за \(O(\log n)\) или даже \(O(\log \log n)\) операций находим центроид на нужном нам пути (первые сколько-то значений centroid[v] и centroid[u] будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида.

+

Асимптотика при более долгих пересчётах

+

TODO: сослаться на мастер-теорему

+

Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за \(O(n \log n)\) (например, где-то использовав set). Сильно ли это хуже по времени?

+

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

+

+

Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:

+

\[ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} +\leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n += n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} +< 2 n \log n += O(n \log n) \]

+

Получается, что мы можем «мердж» в центроидной декомпозиции писать и за \(O(n \log n)\) — как, впрочем, и во всех остальных «разделяйках».

+ + diff --git a/ru/convex-hulls.html b/ru/convex-hulls.html new file mode 100644 index 0000000..33340fe --- /dev/null +++ b/ru/convex-hulls.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + +

Задачи на многоугольники

+

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

+
+alt text +

alt text

+
+

Выпуклые оболочки

+

Выпуклое множество - такое множество точек, что все точки отрезка, образуемого любыми двумя точками данного множества, также принадлежат данному множеству

+

Выпуклая оболочка фигуры - такое выпуклое множество точек, что все точки фигуры также лежат в нем.

+

Минимальная выпуклая оболочка фигуры - это минимальная по площади выпуклая оболочка.

+
+alt text +

alt text

+
+
+alt text +

alt text

+
+
+alt text +

alt text

+
+

Дано множество точек, требуется построить его минимальную выпуклую оболочку :

+

Построение за \(O(nh)\)

+

Алгоритм Джарвиса(метод заворачивания подарка)

+

Давайте выберем какую-то точку, которая гарантированно попадет в минимальную выпуклую оболочкуо, например обычно берут нижнюю и если таких несколько, то самую левую из них. Теперь давайте по одной набирать точки, как бы заворачивая нашу выпуклую оболочку(отсюда и название). Как же нам найти следующую точку в выпуклую оболочку, давайте пройдемся по точкам, которые мы еще не взяли в МВО и среди них выберем с минимальным полярным углом.

+
+alt text +

alt text

+
+

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

+

Асимптотика : для каждой точки выпуклой оболочки мы из всех оставшихся точек будем искать оптимальную - что будет работать за h(размер выпуклой оболочки) * n

+

Важно помнить, что именно \(O(hn)\), а не \(O(n^2)\), так как существуют задачи на это

+
int base = 0;
+for (int i = 1; i < n; i++) {
+    if (mas[i].y < mas[base].y) {
+        base = i;
+    }
+    else if (mas[i].y == mas[base].y && mas[i].x < mas[base].x) {
+        base = i;
+    }
+}
+convex_hull.push_back(base);
+point first = mas[base];
+point cur = first;
+point prev = point(first.x - 1, first.y);
+do {
+    double minCosAngle = 1e9; // чем больше угол, тем меньше его косинус
+    double maxLen = 1e9;
+    int next = -1;
+    for (int i = 0; i < n; i++) {
+        double curCosAngle = CosAngle(prev, cur, mas[i]);
+        if (Less(curCosAngle,minCosAngle)) {//если меньше сразу меняем
+            next = i;
+            minCosAngle = curCosAngle;
+            maxLen = dist(cur, mas[i]);
+        }
+        else if (Equal(curCosAngle, minCosAngle)) {// смотрим по длине
+            double curLen = dist(cur,mas[i]);
+            if (More(curLen,maxLen)) {
+                next = i;
+                maxLen = curLen;
+            }
+        }
+    }
+    prev = cur;
+    cur = mas[next];
+    convex_hull.push_back(next);
+}
+while (cur != first);
+

Построение за \(O(n \log n)\)

+

Алгоритм Грэхема

+

Алгоритм Грэхема базируется на следующей идее: Давайте не искать следующую точку каждый раз, а сделаем так, чтобы у нас всегда была оптимальная точка и мы могли бы просто ее достать и проверить.

+

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

+

Теперь будем делать следующий алгоритм, пока все точки не будут просмотрены :

+
    +
  1. Возьмем первую из отсортированных точек.

  2. +
  3. Проверем последние три точки из взятых, если они образуют правый поворот, то удалим предпоследнюю точку

  4. +
+

Сделать это можно, например, стеком. Код есть ниже.

+
+alt text +

alt text

+
+

Асимптотика : Мы просмотрим одну точку и либо удалим ее, либо оставим, то есть сам поиск МВО работает за линейное время, но мы еще делаем сортировку, а \(\rightarrow\) алгоритм работает за \(O(n\log(n))\), при этом его корректность вытекает из предыдущего алгоритма.

+

Красивая визуализация - https://visualgo.net/en/convexhull

+

красивое видео - https://www.youtube.com/watch?v=BTgjXwhoMuI .

+
struct Point {
+    int x, y;
+};
+
+Point operator -(Point a, Point b)
+{
+    return {a.x - b.x, a.y - b.y};
+}
+
+int operator * (Point a, Point b)
+{
+    return a.x * b.x + b.y * a.y;
+}
+
+int operator ^(Point a, Point b)
+{
+    return a.x * b.y - b.x * a.y;
+}
+
+bool cw(Point a, Point b, Point c)
+{
+    return (a - b) * (c - b) > 0;
+}
+
+bool ccw(Point a, Point b, Point c)
+{
+    return (a - b) * (c - b) < 0;
+}
+
+int main()
+{
+    sort(all(p2), comp);
+    vector<Point> s;
+    s.push_back(p[min_ind]);
+    for (int i = 0; i < n - 1; i++) {
+        if (p2[i].x == s[s.size() - 1].x && p2[i].y == s[s.size() - 1].y)
+            continue;
+        while (s.size() > 1 && (vect(s[s.size() - 1], s[s.size() - 2]) ^
+               vect(s[s.size() - 1], p2[i])) > 0)
+            s.pop_back();
+        s.push_back(p2[i]);
+    }
+}
+

Алгоритм Эндрю

+

Алгоритм Эндрю опирается на то, что вещественные числа не точны и предлагает поменять компаратор и строить не одну выпуклую оболочку, а две :

+

Давайте выберем самую нижнюю и самую правую точку, затем отсортируем точки по самому простому из возможных компараторов, теперь будем строить две оболочки от самой правой точки и самой левой, в итоге мы получим верхнюю и нижнюю части выпуклой оболочки

+
bool comp(Point a, Point b) {
+    if(a.x == b.x) {
+        return a.y < b.y;
+    }
+    return a.x < b.x;
+}
+
+int main() {
+    sort(all(p), comp);
+    vector<Point> up, down;
+    up.pb(p[0]);
+    down.pb(p[0]);
+    Point p1 = p[0], p2 = p.back();
+    for(int i = 1; i < n; i++) {
+        if (i == n - 1 || cw(p1, p[i], p2)) {
+            while (up.size() >= 2 && !cw(up[up.size() - 2], up[up.size() - 1], p[i])) {
+                up.pop_back();
+            }
+            up.pb(p[i]);
+        }
+        if (i == n - 1 || ccw(p1, p[i], p2)) {
+            while (down.size() >= 2 && !ccw(down[down.size() - 2], down[down.size() - 1], p[i])) {
+                down.pop_back();
+            }
+            down.pb(p[i]);
+        }
+    }
+}
+

Алгоритм Чена

+

Также существует алгоритм, объединяющий Джарвиса и Грэхема(Эндрю) и работающий за \(O(n\log(h))\), но он разбираться не будет

+

Задачи

+
    +
  1. Базовые задачи - достаточно простые, например найти длину забора, чтобы ограничить многоугольник и подобные, но есть достаточно интересные задачи, в которых выпуклая оболочка неочевидна, например следующая : Даны \(n\) пунктов в городе и \(n\) почтальонов, для каждого пункта известно расстояние от почты \(c_{i}\). Требуется каждому пункту доставить почту, \(i\)-ый почтальон просит \(a_{i}\) монет, чтобы проснуться и \(b_{i}\), чтобы проехать один километр, требуется для каждого пункта сказать, кто доставит почту наиболее выгодно. (Подсказка : \(a_{i} + b_{i} * c_{j}\) - это прямая и стоимость доставки от \(i\) почтальона к \(j\) пункту).
  2. +
+ + diff --git a/ru/cross-entropy.html b/ru/cross-entropy.html new file mode 100644 index 0000000..5d7af41 --- /dev/null +++ b/ru/cross-entropy.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + +

Почему машинное обучение работает

+

b

+

Принцип максимального правдоподобия —

+

Функция правдоподобия

+

\[ \hat{\theta} = \argmax_\theta P(X | \theta) \]

+

Принцип максимального правдоподобия

+

Логистическая регрессия

+

Нормальное распределение и MSE

+

Почему сигмоида

+ + diff --git a/ru/cuda.html b/ru/cuda.html new file mode 100644 index 0000000..189eba6 --- /dev/null +++ b/ru/cuda.html @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + +

Supercomputing for the Masses

+

This is an html-rendered Jupyter notebook. If you want to do the exercises right here, open it in Colab or download and edit it locally. In former case, you need to complete a little quest and install CUDA and PyCuda, its Python binding. On a Debian-based machine, this will probably be enough: * apt-get install nvidia-cuda-dev nvidia-cuda-toolkit * pip install pycuda

+

Prerequisites: basic knowledge of Python and C, basic algorithms, and generally how computers work.

+

Subtlties of the Moore's law

+

Here is a graph that roughly represents what is happening in the CPU world:

+

+

Moore's law is the observation that the number of transistors in a microprocessor doubles about every two years. This roughly means that the performance doubles too.

+

You can see that around 2005 there became a shift in design .

+

The cores are more or less independent.

+

Modern GPUs appeared in early 2000s. They exploit the specific area they operate.

+

There are physical limitations to the speed of a core.

+

One solid one: the speed of light. You at least need the time for electromagnetic wave (this is light too) to pass from one side of the motherboard to another.

+

Some of them have

+

The default free GPUs available on Google Colab are rather powerfull. Author has no idea why Google does this, but this is awesome.

+

Why multiprocessing?

+

Clock frequencies — for example, Intel Core i7 can have. This gives an upper bound

+

There are two types of

+

General-purpose GPU

+

There was a period in time when hedge funds hired computer graphics guys from game companies because of their computing skills.

+

There are several.

+

This is like with Windows and Linux.

+

We will stick with CUDA, because it more spread, especially in fields where noone cares, like deep learning.

+

Heterogineous computing

+

CUDA programming involves running code on two different platforms concurrently: a host system with one or more CPUs and one or more GPUs.

+

Differences from CPUs

+

Threads

+

Threads on a CPU are generally heavyweight entities. The operating system must swap threads on and off CPU execution channels to provide multithreading capability. Context switches are therefore slow and expensive.

+

By comparison, threads on GPUs are extremely lightweight. In a typical system, thousands of threads are queued up for work — in warps of 32 threads each. If the GPU must wait on one warp of threads, it simply begins executing work on another. Because separate registers are allocated to all active threads, no swapping of registers or other state need occur when switching among GPU threads. Resources stay allocated to each thread until it completes its execution.

+

In short, CPU cores are designed to minimize latency for one or two threads at a time each, whereas GPUs are designed to handle a large number of concurrent, lightweight threads in order to maximize throughput.

+

Memory

+

The host system and the device each have their own distinct attached physical memories. As the host and device memories are separated by the PCI Express (PCIe) bus, items in the host memory must occasionally be communicated across the bus to the device memory or vice versa as described in What Runs on a CUDA-Enabled Device?

+

You can easily dump 98% of performance of you think this way.

+

Installing PyCUDA

+

CUDA is available for many languages.

+

Nice documentation can be found here: https://documen.tician.de/pycuda/index.html

+

If you are on Colab, go to Runtime -> Change runtime type -> Hardware accelerator and set it to "GPU".

+
# you may want to clear the output of this cell after installation
+from IPython.display import clear_output
+ 
+# this might take a while
+!pip install pycuda
+
+clear_output()
+
import numpy as np
+
+from pycuda.compiler import SourceModule
+import pycuda.driver as drv
+import pycuda.autoinit
+

The basics

+

Let's start with a simple example and then dive deeper.

+

Kernels

+

Just like C or C++, except that you use some custom built-in functions and specifiers.

+

CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:

+

You need to think of your computer as a heterogenious machine: there is host data and device data.

+
    +
  • You move input data to device memory.
  • +
  • You run some computation on device.
  • +
  • You retrieve back this data.
  • +
+

In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results.

+

The famous \(A + B\) problem

+

For testing and coordination with host, we will use NumPy package. If you don't have it, install it: pip install numpy.

+

NumPy is a package for linear algebra and array manupulation in Python. It is written in C and is very efficient, but runs solely on CPU, so we will benchmark against it.

+
# lets generate our test data: two float arrays filled with something random
+a = numpy.random.randn(100).astype('float32')
+b = numpy.random.randn(100).astype('float32')
+# the type needs to be specified in this case, because randn's default type is float64, but CUDA knows nothing about it
+
+# we need to create space where kernel should write its answers to
+dest = numpy.zeros_like(a)
+
+# this is the kernel itself
+mod = SourceModule("""
+    __global__ void add(float *dest, float *a, float *b) {
+        const int i = threadIdx.x;
+        dest[i] = a[i] + b[i];
+    }
+""")
+
+# you need to specify the source code, and PyCUDA will compile it
+add_kernel = mod.get_function("add")
+
+add_kernel(
+    drv.Out(dest),  # specifies that this memory should be accessible for writing
+    drv.In(a),  # specifies this should be accessible for reading
+    drv.In(b),
+    block=(100,1,1)  # we'll talk about it in a minute
+)
+
+assert np.allclose(dest, a + b), 'WA'  # checks that these are equal
+print('OK')
+
  File "<ipython-input-27-afc857479fe4>", line 19
+    %%time
+    ^
+SyntaxError: invalid syntax
+

Memory management

+

In CUDA C API, you need to allocate memory explicitly. So this is actually really nice.

+

There is also drv.InOut function, which makes it available for both reading and writing, but we won't use it in this tutorial because we need to test our code too.

+

Most of the operations here are memory operations, so measuring performance here is useless. Don't worry, we will get to more complex examples soon enough.

+

GPUs have very specific operations. However, in case of NVIDIA GPUs managing it is quite simple: the cards have compute capabilities (1.0, 1.1, 1.2, 1.3, 2.0, etc.) and all features added at capability \(x\) is also available at later versions. These can be checked at run-time or compile-time.

+

You can check differences in this Wikipedia article: https://en.wikipedia.org/wiki/CUDA#Version_features_and_specifications

+

Synchronization

+

Reduction is any array-wise operation.

+

Assume the following problem:

+

Dynamic programming

+

Consider the following recurrence:

+
## Problem: dynamic programming
+

Work vs. Latency

+

We actually think about both work and step complexity now.

+

Some tasks, especially in cryptography, cannot be parallelized. But some can.

+

Summing arrays in \(O(\log n)\) time

+

Assume we want to perform some associative (i. e. \(A*(B*C) = (A*B)*C\)) operation on an array of \(n\) elements. Say, sum it up.

+

Normally, we would do that with a simple loop:

+
float s = 0;
+for (int i = 0; i < n; i++) {
+     s += a[i]; 
+}
+

Its computation graphs looks like this:

+

+

This is optimal in terms of work complexity, but not in terms of step complexity: it's \(O(n)\). We may want something that is a bit worse in terms of work complexity, but can be parallelized.

+

Let's try this divide-and-conquer approach:

+

+

Now it's still \(O(n)\) work complexity (you actually need exactly the same number of additions), but this is \(O(\log n)\) step complexity.

+

When you unroll the recursion from top to bottom, you will see that to get each required value,

+

+

Reducing small arrays

+
a = numpy.random.randn(2048).astype('float32')
+
+mod = SourceModule("""
+    __global__ void sum(float *dest, float *a, float *b) {
+        const int i = threadIdx.x;
+        // for l from 0 to logn:
+        //   __sync_threads()
+        //   if the thread is active
+        //     sum two elements into where they belong
+        // a[0] should containt the needed sum
+    }
+""")
+
+sum_kernel = mod.get_function("sum")
+
+add_kernel(
+    drv.InOut(a),
+    block=(1024,1,1)
+)
+
+assert np.allclose(dest, a + b), 'WA'  # checks that these are equal
+print('OK')
+

Warps and thread blocks

+

Threads are bandled in groups of 32. All threads in a group must be either waiting or performing the same operation. This is caused by architectural difficulties.

+

+

You can actually do the same stuff with 2d and 3d indexing — weird, right?

+

Atomics

+

Reducing big arrays

+

Reducing very big arrays

+

Now, things get harder. It's time to tell how exactly GPU parallelism works.

+
+

Dense Matrix multiplication

+

Let's get to our first example where using GPUs actually makes sense: matrix multiplication.

+

Sorting

+

Our last (and hardest task) is to implement sorting.

+

You might notice that we advocated divide-and-conquer approaches most of the time.

+

It's true. They work. But we can't get an algorithm that works already.

+
# we'll use a deep learning library for benchmarking because I'm not familiar with anything else 
+import torch
+
+a = torch.randn(10**8)
+b = a.cuda()
+
# this should run for ~15 secs
+%time c = torch.sort(a)
+%time c = torch.sort(b)
+
CPU times: user 15.2 s, sys: 177 µs, total: 15.2 s
+Wall time: 15.2 s
+CPU times: user 274 ms, sys: 237 ms, total: 511 ms
+Wall time: 511 ms
+

So, 30 times speedup. So, we now what we need to compete against.

+
b.sort()
+
(tensor([-5.4567, -5.3551, -5.3288,  ...,  5.3529,  5.4484,  5.4486],
+        device='cuda:0'),
+ tensor([55083205,  8383169, 73705953,  ..., 79814161, 50474932, 27805828],
+        device='cuda:0'))
+

There are two types of sorting algorithms: data-driven.

+

The second can be represented and analuzed with sorting networks. Here is the one that we'll use, it's called bitonic sort.

+

+

It has \(O(\log n)\) stages, in total they have \(1 + 2 + 3 + \ldots + \log n = O(\log^2 n\) blocks of comparisons that can't be parallelized and invonve every element of the array. So, in total it has \(O(n \log^ n)\) work complexity, but \(O(\log^2 n)\) step complexity, which is pretty sweet.

+

It is actually not that hard to implement. To make it clear, here is a slow recursive Python implementation:

+
def bitonic_sort(a, up=False):
+    if len(a) <= 1:
+        return a
+    else: 
+        l = bitonic_sort(x[:len(a) // 2], True)
+        r = bitonic_sort(x[len(a) // 2:], False)
+        return bitonic_merge(first + second, up)
+
+def bitonic_merge(a, up): 
+    # assume input a is bitonic, and sorted list is returned 
+    if len(a) == 1:
+        return a
+    else:
+        bitonic_compare(a, up)
+        l = bitonic_merge(a[:len(a) // 2], up)
+        r = bitonic_merge(a[len(a) // 2:], up)
+        return l + r
+
+def bitonic_compare(a, up):
+    dist = len(a) // 2
+    for i in range(dist):  
+        if (a[i] > a[i + dist]) == up:
+            a[i], a[i + dist] = a[i + dist], x[i]  # this is how swap is done in Python
+
bitonic_sort([57, 179, 42, 17, 300, 111])
+
[300, 179, 111, 57, 42, 17]
+
a = np.random.randn(10**8).astype('float32')
+
---------------------------------------------------------------------------
+
+NameError                                 Traceback (most recent call last)
+
+<ipython-input-27-58a927c14aae> in <module>()
+----> 1 a = np.random.randn(10**8).astype('float32')
+
+
+NameError: name 'np' is not defined
+

Why CUDA

+

Most of it still applicable.

+

Again, GPU programming is very specific.

+

SSE and tensor cores.

+

Kernels

+

Just like C or C++, except that you use some custom built-in functions and specifiers.

+

CUDA is pretty much like normal C, except that you can specify some functions to be run as. Depending on implementation, the workflow goes as follows:

+

You need to think of your computer as a heterogenious machine: there is host data and device data.

+
    +
  • You move input data to device memory.
  • +
  • You run some computation on device.
  • +
  • You retrieve back this data.
  • +
+

In fact, kernel runs are concurrent — you program does not block until kernel run is complete. Newer devices can even run multiple kernels concurrently this way and wait for their results.

+

What you need to understand about GPUs is that they are extremely specialised for their applications.

+

Intrinsics for that.

+

Now, a lot of value comes from cryptocurrency and deep learning. The latter relies on two specific operations: matrix multiplications for linear layers and convolutions for convolutional layers used in computer vision.

+

First, they introduced "multiply-accumulate" operation (e. g. x += y * z) per 1 GPU clock cycle.

+

Google uses Tensor Processing Units. Nobody really knows how they work (proprietary hardware that they rent, not sell).

+

Each tensor core perform operations on small matrices with size 4x4. Each tensor core can perform 1 matrix multiply-accumulate operation per 1 GPU clock. It multiplies two fp16 matrices 4x4 and adds the multiplication product fp32 matrix (size: 4x4) to accumulator (that is also fp32 4x4 matrix).

+

This is a lot of work per

+

Well, you don't really need anything more precise than that for deep learning anyway.

+

It is called mixed precision because input matrices are fp16 but multiplication result and accumulator are fp32 matrices.

+

Probably, the proper name would be "4x4 matrix cores", however NVIDIA marketing team decided to use "tensor cores".

+

So, see, this is not exactly fair comparison.

+

+* +
+You need to extend this graph just a little bit: last November, NVIDIA's stock dropped 30% following Bitcoin crash, so I wouldn't be so hopefull +
+
    +
  • +
+

down to int4 (16-valued, you heard correct)

+

You need to know a lot of this specialised stuff to write efficient code. So this is a bad idea to write libraries from scratch.

+

Anyway, for pedagogical and recreational reasons, today we will reinvent the wheel and do a matrix multiply.

+

Reducing an array

+

It seems to be simple: you just need to .

+

What actually happens when you do s += x? This is not a single operation. Actually, four things happen:

+
    +
  1. Read \(x\) into register
  2. +
  3. Read \(s\) into register
  4. +
  5. Calculate \(s + x\)
  6. +
  7. Write it back to wherever \(s\) was initially
  8. +
+

Two threads may execute it in an interleaved fashion. Say thread A could get \(s\), but a nanosecond later thread B will be writing here, but thread A doesn't know about it and will re-write unchanged value.

+

Note: Atomics to do that

+

for small data types they are implemented on the hardware level and much more faster than that.

+

std::atomic is introduced to handle atomic operations in multi-thread context. In multi-thread environment, when two threads operating on the same variable, you must be extra careful to avoid race conditions.

+

Memory types

+

If all the various types of device memory were to race, here’s how the race would turn out:

+

Register size (= machine word width) is 32 bits, but they also contain 64bit capability (otherwise having more than 4gb of memory would not be possible).

+
    +
  • 1st place: Register memory
    This is the data visible only to the thread that wrote it. It lasts only for the lifetime of that thread.
  • +
  • 2nd place: Shared Memory
    Shared to all threads within thread block. Lasts only for the lifetime of that block. This type of memory allows communication (data sharing) between threads. This is why you should
  • +
  • 3rd place: Constant Memory
  • +
  • 4th: Texture Memory
  • +
  • Tie for last place: Local Memory and Global Memory
  • +
+

What you need to care for now is register

+

For now, you need to care about differe

+

Accessing global memory takes hundreds.

+

Problem: dense matrix multiplication

+

A lot of these are actually sparse. You can do stuff with social network graphs or web graphs.

+

Cool. But let's disapploint us for a bit:

+ + diff --git a/ru/dp-intro.html b/ru/dp-intro.html new file mode 100644 index 0000000..50098e4 --- /dev/null +++ b/ru/dp-intro.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + +

Динамическое программирование

+
+

Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

+
+

Разберем некоторые классические идеи и задачи в динамическом программировании.

+

Рюкзак

+
+

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

+
+

Наибольшая общая подпоследовательность

+
+

Есть две строки. Найти длину их наибольшей общей подпоследовательности.

+
+

Расстановка скобок

+
+

Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

+
+

Можно завести динамику «можно ли ».

+

Наибольшая возрастающая подпоследовательность

+

Есть также алгоритм за \(O(n \log n)\).

+

Мемоизация

+

Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

+

Первый предполагает

+

Число разбиений

+
+ +
+

Замена параметра на ответ

+
+ +
+ + diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html new file mode 100644 index 0000000..40606e7 --- /dev/null +++ b/ru/dp-optimizations.html @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + +

Оптимизации динамики

+
    +
  • Разделяй и властвуй
  • +
  • Оптимизация Кнута
  • +
  • Convex Hull Trick
  • +
  • Лямбда-оптимизация
  • +
+

Задача

+
+

Даны \(n\) точек на прямой. Нужно найти \(m\) отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.

+
+

Базовое решение — следующая динамика: \(f[i, j]\) — минимальная стоимость покрытия \(i\) первых (самых левых) точек, используя не более \(j\) отрезков (итоговый ответ будет записан в \(f[n, m]\) — прим. К. О.).

+

Переход — перебор всех возможных последних отрезков, то есть \(f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}\).

+
// x[] — отсортированный массив координат точек, нумерация с нуля
+
+// квадрат длины отрезка от i-той до j-той точки
+int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }
+
+// TODO: предподсчитать cost
+
+for (int i = 0; i <= m; i++)
+    f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо
+// все остальные f предполагаем равными бесконечности
+
+for (int i = 1; i <= n; i++)
+    for (int j = 1; j <= m; j++)
+        for (int k = 0; k < i; k++)
+            f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1));
+

Заметим, что циклы по i и j можно поменять местами.

+

Такое решение пока работает за \(O(n^2 m)\) — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить.

+

Разделяй-и-властвуй

+

Обозначим за \(opt[i, j]\) оптимальный \(k\), на котором \(f[i, j]\) минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.

+

Утверждение. \(opt[i, j] \leq opt[i, j+1]\).

+

Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.

+

Что это нам даёт? Если мы уже знаем \(opt[i, l]\) и \(opt[i, r]\) и хотим посчитать \(opt[i, j]\) для какого-то \(j\) между \(l\) и \(r\), то мы можем сузить отрезок поиска оптимального индекса с \([0, i-1]\) до \([opt[i, l], opt[i][r]]\).

+

Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка \([l, r]\), зная, что их \(opt\)-ы лежат между \(l'\) и \(r'\). Она берет середину отрезка \([l, r]\) и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно.

+
void solve (int l, int r, int _l, int _r, int k) {
+    if (l > r) return; // отрезок пустой — выходим
+    int t = (l + r) / 2;
+    int opt = _l;
+    for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы 
+        int val = f[i+1][k-1] + cost(i, j);
+        if (val < f[t][k])
+            f[t][k] = val, opt = i;
+    }
+    solve(l, t-1, _l, opt, k);
+    solve(t+1, r, opt, _r, k);
+}
+

Вызываться она будет просто последовательно для каждого слоя:

+
for (int k = 1; k <= m; k++)
+    solve(0, n-1, 0, n-1, k);
+

Теперь пересчет одного «слоя» динамики занимает \(O(n \log n)\) вместо \(O(n^2)\). Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет \(O(\log n)\), а значит и каждый элемент будет просмотрен не более \(O(\log n)\) раз.

+

Получается, что асимптотика улучшилась до \(O(n m \log n)\).

+

Оптимизация Кнута

+

Предыдущий метод основывался на том факте, что \(opt[i, j] \leq opt[i, j+1]\). А что, если \(opt\) монотонен ещё и по первому параметру?

+

\[ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] \]

+

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

+

Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:

+

\[ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) \]

+

Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из \(opt[i, j]\) не более \(O(n)\).

+
for (int i = 1; i <= n; i++) {
+    for (int j = m; j >= 1; j--) {
+        for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) {
+            int val = f[i+1][k-1] + cost(i, j);
+            if (val < f[t][k])
+                f[t][k] = val, opt[i][j] = i;
+        }
+    }
+}
+

Сравните с базовым решением — всего 3 новых строчки.

+

Convex Hull Trick

+

Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на cost и увидев там скалярное произведение.

+

\[ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ +f[k, j-1] + x_{i-1}^2 +- 2x_{i-1} x_k ++ x_k^2 +\}\]

+

Посмотрим внимательнее на минимизируемое выражение. \(x_{i-1}^2\) не зависит от \(k\), значит его можно вынести. Под минимумом останется $ {a_k} {b_k} x_{i-1} $.

+

Это теперь можно переписать как $ k (a_k, b_k) (1, X{i-1}) $ (имеется в виду скалярное произведение)

+

Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

+

TODO: иллюстрация.

+

TODO: мем про Скуби-Ду.

+

Ли Шао

+

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

+

Лямбда-оптимизация

+

Не «фольклорное» название — дискретный метод множителей Лагранжа.

+

Пусть у нас есть функция \(f(x)\), которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .

+

Заметим следующее:

+

Давайте заменим \(f[i, j]\) на \(g_\lambda[i]\) — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный \(\lambda\), за использование отрезка.

+

Давайте сделаем бинпоиск по \(\lambda\). Понятно, что для каких-то лямбд мы будем использовать ровно \(j\) отрезков. Тогда ответ на динамику \(f[i, j] = g[i] - \lambda k\).

+

Суммируем

+

TODO: сделать табличку

+
    +
  • Разделяйка: \(O(nm \log n)\), если cost такой, что opt монотонна по одному аргументу.
  • +
  • Кнут: \(O(nm)\), если cost такой, что opt монотонна по обоим аргументам.
  • +
  • CHT: \(O(nm)\). В оптимизируемой функции нужно увидеть скалярное произведение.
  • +
  • Лагранж: \(O(n \log n)\). Функция должна быть выпуклой.
  • +
+

Другие задачи

+

Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.

+
+

Есть \(n\) ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить \(m\) амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар.

+
+ + diff --git a/ru/dp_intro.html b/ru/dp_intro.html new file mode 100644 index 0000000..26b85f0 --- /dev/null +++ b/ru/dp_intro.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + +

Динамическое программирование

+

test123

+
+

Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

+
+

Разберем некоторые классические идеи и задачи в динамическом программировании.

+

Рюкзак

+
+

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

+
+

Наибольшая общая подпоследовательность

+
+

Есть две строки. Найти длину их наибольшей общей подпоследовательности.

+
+

Расстановка скобок

+
+

Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

+
+

Можно завести динамику «можно ли ».

+

Наибольшая возрастающая подпоследовательность

+

Есть также алгоритм за \(O(n \log n)\).

+

Мемоизация

+

Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

+

Первый предполагает

+

Число разбиений

+
+ +
+

Замена параметра на ответ

+
+ +
+ + diff --git a/ru/dp_optimizations.html b/ru/dp_optimizations.html new file mode 100644 index 0000000..40606e7 --- /dev/null +++ b/ru/dp_optimizations.html @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + +

Оптимизации динамики

+
    +
  • Разделяй и властвуй
  • +
  • Оптимизация Кнута
  • +
  • Convex Hull Trick
  • +
  • Лямбда-оптимизация
  • +
+

Задача

+
+

Даны \(n\) точек на прямой. Нужно найти \(m\) отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.

+
+

Базовое решение — следующая динамика: \(f[i, j]\) — минимальная стоимость покрытия \(i\) первых (самых левых) точек, используя не более \(j\) отрезков (итоговый ответ будет записан в \(f[n, m]\) — прим. К. О.).

+

Переход — перебор всех возможных последних отрезков, то есть \(f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}\).

+
// x[] — отсортированный массив координат точек, нумерация с нуля
+
+// квадрат длины отрезка от i-той до j-той точки
+int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }
+
+// TODO: предподсчитать cost
+
+for (int i = 0; i <= m; i++)
+    f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо
+// все остальные f предполагаем равными бесконечности
+
+for (int i = 1; i <= n; i++)
+    for (int j = 1; j <= m; j++)
+        for (int k = 0; k < i; k++)
+            f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1));
+

Заметим, что циклы по i и j можно поменять местами.

+

Такое решение пока работает за \(O(n^2 m)\) — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить.

+

Разделяй-и-властвуй

+

Обозначим за \(opt[i, j]\) оптимальный \(k\), на котором \(f[i, j]\) минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.

+

Утверждение. \(opt[i, j] \leq opt[i, j+1]\).

+

Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.

+

Что это нам даёт? Если мы уже знаем \(opt[i, l]\) и \(opt[i, r]\) и хотим посчитать \(opt[i, j]\) для какого-то \(j\) между \(l\) и \(r\), то мы можем сузить отрезок поиска оптимального индекса с \([0, i-1]\) до \([opt[i, l], opt[i][r]]\).

+

Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка \([l, r]\), зная, что их \(opt\)-ы лежат между \(l'\) и \(r'\). Она берет середину отрезка \([l, r]\) и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно.

+
void solve (int l, int r, int _l, int _r, int k) {
+    if (l > r) return; // отрезок пустой — выходим
+    int t = (l + r) / 2;
+    int opt = _l;
+    for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы 
+        int val = f[i+1][k-1] + cost(i, j);
+        if (val < f[t][k])
+            f[t][k] = val, opt = i;
+    }
+    solve(l, t-1, _l, opt, k);
+    solve(t+1, r, opt, _r, k);
+}
+

Вызываться она будет просто последовательно для каждого слоя:

+
for (int k = 1; k <= m; k++)
+    solve(0, n-1, 0, n-1, k);
+

Теперь пересчет одного «слоя» динамики занимает \(O(n \log n)\) вместо \(O(n^2)\). Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет \(O(\log n)\), а значит и каждый элемент будет просмотрен не более \(O(\log n)\) раз.

+

Получается, что асимптотика улучшилась до \(O(n m \log n)\).

+

Оптимизация Кнута

+

Предыдущий метод основывался на том факте, что \(opt[i, j] \leq opt[i, j+1]\). А что, если \(opt\) монотонен ещё и по первому параметру?

+

\[ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] \]

+

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

+

Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:

+

\[ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) \]

+

Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из \(opt[i, j]\) не более \(O(n)\).

+
for (int i = 1; i <= n; i++) {
+    for (int j = m; j >= 1; j--) {
+        for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) {
+            int val = f[i+1][k-1] + cost(i, j);
+            if (val < f[t][k])
+                f[t][k] = val, opt[i][j] = i;
+        }
+    }
+}
+

Сравните с базовым решением — всего 3 новых строчки.

+

Convex Hull Trick

+

Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на cost и увидев там скалярное произведение.

+

\[ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ +f[k, j-1] + x_{i-1}^2 +- 2x_{i-1} x_k ++ x_k^2 +\}\]

+

Посмотрим внимательнее на минимизируемое выражение. \(x_{i-1}^2\) не зависит от \(k\), значит его можно вынести. Под минимумом останется $ {a_k} {b_k} x_{i-1} $.

+

Это теперь можно переписать как $ k (a_k, b_k) (1, X{i-1}) $ (имеется в виду скалярное произведение)

+

Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

+

TODO: иллюстрация.

+

TODO: мем про Скуби-Ду.

+

Ли Шао

+

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

+

Лямбда-оптимизация

+

Не «фольклорное» название — дискретный метод множителей Лагранжа.

+

Пусть у нас есть функция \(f(x)\), которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .

+

Заметим следующее:

+

Давайте заменим \(f[i, j]\) на \(g_\lambda[i]\) — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный \(\lambda\), за использование отрезка.

+

Давайте сделаем бинпоиск по \(\lambda\). Понятно, что для каких-то лямбд мы будем использовать ровно \(j\) отрезков. Тогда ответ на динамику \(f[i, j] = g[i] - \lambda k\).

+

Суммируем

+

TODO: сделать табличку

+
    +
  • Разделяйка: \(O(nm \log n)\), если cost такой, что opt монотонна по одному аргументу.
  • +
  • Кнут: \(O(nm)\), если cost такой, что opt монотонна по обоим аргументам.
  • +
  • CHT: \(O(nm)\). В оптимизируемой функции нужно увидеть скалярное произведение.
  • +
  • Лагранж: \(O(n \log n)\). Функция должна быть выпуклой.
  • +
+

Другие задачи

+

Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.

+
+

Есть \(n\) ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить \(m\) амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар.

+
+ + diff --git a/ru/fenwick.html b/ru/fenwick.html new file mode 100644 index 0000000..dfa7b9a --- /dev/null +++ b/ru/fenwick.html @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + +
+

НЯ!
Эта статья полна любви и обожания.
Возможно, стоит добавить ещё больше?

+
+

Дерево Фенвика

+

Дерево Фенвика или бинарно индеквированное дерево (англ. binary indexed tree) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности.

+

Определение

+

Пусть дан массив \(a\) длины \(n\). Деревом Фенвика будем называть массив \(t\) той же длины, который объявим так:

+

\[ t_i = \sum_{k=F(i)}^i a_k \]

+

где \(F\) это какая-то функцию, для которой выполнено \(F(i) \leq i\). Конкретно её определим потом.

+

Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе (\(sum(l, r) = sum(r) - sum(l-1)\)), каждый из которых будем считать по этой формуле:

+

\[ sum(k) = t_k + sum(F(k)-1) \]

+

Когда мы изменяем \(k\)-ю ячейку исходного массива, мы обновляем все \(t_i\), в которых учтена эта ячейка.

+

\(F\) можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам \(t_i\) при обновлении будет будет \(O(\log n)\). Популярны две функции:

+
    +
  • \(F_1(x) =\) x & (x + 1)
  • +
  • \(F_2(x) =\) x - (x & -x) + 1
  • +
+

Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.

+

Disclaimer: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на \(O(\log n)\) отрезков вида \([F(i), i]\), и любой элемент входит в не более \(O(\log n)\) таких отрезков.

+

Реализация

+

Из-за того, что \(F(0) = 1 > 0\) и поэтому \([0, F(0)]\) не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать \(t_0\).

+
int t[maxn];
+
+// возвращает сумму на префиксе
+int sum (int r) {
+    int res = 0;
+    for (; r > 0; r -= r & -r)
+        res += t[r];
+    return res;
+}
+
+int sum (int l, int r) {
+    return sum(r) - sum(l-1);
+}
+
+// обновляет нужные t
+void add (int k, int x) {
+    for (; k <= n; k += k & -k)
+        t[k] += x;
+}
+

Автор отмечает красивую симметрию в формулах r += r & -r и k -= k & -k, которой нет в «традиционной» версии.

+

Многомерный случай

+
+

\(k\)-мерное дерево Фенвика пишется в \((k+1)\) строчку

+
+

Нужно добавить всего одну такую же строчку в sum, add, а также при подсчете суммы на прямоугольнике вместо двух запросов к префиксным суммам использовать четыре.

+

sum перепишется следующим образом:

+
int sum (int r1, int r2) {
+    int res = 0;
+    for (int i = r1; i > 0; i -= i & -i)
+        for (int j = r2; j > 0; j -= j & -j)
+            ans += t[i][j];
+    return res;
+}
+

В \(k\)-мерном случае, в соответствии с принципом включений-исключений, для запроса суммы нужно \(2^k\) запросов суммы на префиксах.

+

Если размерности больше, чем позволяет память, то можно вместо массива t использовать хэш-таблицу — так потенциально потребуется \(O(q \log^2 A)\) памяти (\(A\) — максимальная координата), но это всё равно один из самых безболезненных способов решать достаточно простые задачи на двумерные структуры. Автор в своё время таким образом решил какую-то задачу на 2d-сумму с USACO 2017.

+

Бинпоиск

+

Оказывается, можно производить бинарный поиск (точнее, спуск) по префиксным суммам за \(O(\log n)\).

+
// возвращает индекс, на котором сумма уже больше
+int lower_bound (int s) {
+    int k = 0;
+    for (int l = logn; l >= 0; l--) {
+        if (k + (1<<l) <= n && t[k + (1<<l)] < s) {
+            k += (1<<l);
+            s -= t[k];
+        }
+    }
+    return k;
+}
+

Если знать, что \(F(x)\) удаляет последний бит \(x\), то принцип понятен: просто делаем спуск по бинарному дереву, как в ДО. Чем-то похоже на генерацию \(k\)-го лексикографического комбинаторного объекта: пытаемся увеличить следующий символ всегда, когда это возможно.

+

Отметим, что в «традиционной» индексации такое делать нельзя.

+

Ограничения

+

Дерево Фенвика можно использовать, когда наша операция обратима, а также когда трюк с префиксными суммами работает. Это обычно простые операции типа суммы, xor, умножения по модулю (если гарантируется, что на этот модуль ничего не делится). Минимум и gcd, отложенные операции и персистентность прикрутить в общем случае уже не получится — тогда уже нужно писать ДО.

+

Почему работает

+

Итак, мы выбрали вариант с \(F(x)\) = x - (x & -x) + 1. Поймем, что означает x & -x.

+

Лемма. x & -x возвращает последний единичный бит в двоичной записи x.

+

Доказательство потребует знания, как в компьютерах хранятся целые числа. Чтобы процессор не сжигал лишние такты, проверяя знак числа при арифметических операциях, их хранят как бы по модулю \(2^k\), а первый бит отвечает за знак (0 для положительных и 1 для отрицательных). Поэтому когда мы хотим узнать, как выглядит отрицательное число, нужно его вычесть из нуля: \(-x = 0-x = 2^k-x\).

+

Например,

+

\[ +\begin{align} + \begin{aligned} + +90 = 2+8+16+64 & = 0 \, 10110_2 \\ + -90 = 00000_2 - 10110_2 & = 1 \, 01010_2 \\ + \implies (+90) \text{ & } (-90) & = 0 \, 00010_2 \\ + \end{aligned} +\end{align} +\]

+

Вернёмся к доказательству леммы. Когда мы вычитаем, мы идем справа налево. В ответе можно мысленно разделить на три блока: * Первые сколько-то (возможно, нисколько) нулей с конца ими же и останутся. * Потом, ровно на самом младшем единичном бите, мы «займём» много единиц, так что весь префикс станет единицами. В ответе на этом месте точно будет единица. Потом отменятся ровно те из них, которые были единицами в исходном числе.

+

Делаем &: в каком-то префиксе все биты будут противоположными, младший единичный бит останется, а на суффиксе все как было нулями, так и осталось. Выживет только этот самый младший единичный бит.

+

Теперь сразу понятно, почему sum будет работать за логарифм — каждый раз мы делаем x -= x & -x, то есть удаляем один бит.

+

Теперь сложная (для понимания) часть — как делать add. Какие \(t_i\) содержат \(k\)-й элемент? Для них должно выполняться i >= k > i - (i & -i).

+

Будем перебирать префиксы TODO

+

Мы знаем, что \(t_i\) вложены друг в друга. Минимальный подходящий \(i\) равен \(k\). Какой следующий? Нам нужно для каждого \(i\) уметь находить его непосредственного родителя.

+

Можно представить дерево так: ячейка 2^k содержит все

+

TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию.

+

Название

+

Потому что \(F\) использует битовые операции, по-английски структура называется «Binary Indexed Tree».

+

Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.

+

Можно показать, что множества элементов, учтенных в \(t_i\) и \(t_j\), либо не пересекаются, либо одно является подмножеством другого. Значит, между \(t_i\) можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).

+

В частном случае, когда длина массива равна \(2^k\), то дерево будет только одно.

+ + diff --git a/ru/games.html b/ru/games.html new file mode 100644 index 0000000..458dd68 --- /dev/null +++ b/ru/games.html @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + +

Теория игр

+

Начнём с самого баянного примера математической игры, который можно вспомнить:

+
+

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

+
+

Все задачи такого типа решаются одним и тем же способом: посмотреть на решения для маленьких чисел и применить динамическое программирование. В данном случае состоянием динамики будет \(f_k\) — выиграет ли первый игрок, если спичек сейчас \(n\), и ход принадлежит ему. Пересчитывать её нужно так:

+

\[ f_k = \lnot f_{k-1} \lor \lnot f_{k-2} \lor \lnot f_{k-3} = \lnot (f_{k-1} \land f_{k-2} \land f_{k-3}) \]

+

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

+

Примечание. Конечно, здесь не нужна была никакая динамика, и ответ зависит только от того, делится ли \(n\) на 4, но мы всё равно рассмотрим этот пример в педагогических целях.

+

Игры на графах

+

В общем случае состояния игры могут представлять из себя гораздо более сложную структуру, чем «\(n\) спичек».

+

Любую игру можно описать в виде графа (возможно, бесконечного) состояний игры, между которыми есть переходы. Игроки поочередно выбирают, по какому переходу пройти. Некоторые состояния в этом графе помечены как терминальные

+

Состояние называется выигрышным, если игрок, начинающий в нём, побеждает, и проигрышным в противном случае. В графе могут быть циклы, и иногда обоим игрокам выгодно по ним ходить. Например, в шахматах бывают ситуации, когда выгодно повторение ходов (сама игра при этом всё равно не может длиться бесконечно, потому что троекратное повторение позиции приводит к ничье — подобные правила есть в большинстве стратегических игр). Такие вершины — в которых оптимальной стратегией будет хождение по циклу — назовём ничейными.

+

Подобные игры — где чтобы сделать одному игроку лучше, нужно сделать другому на столько же хуже — называют играми с нулевой суммой (почему такое название и что такое игры с ненулевой суммой — ниже прочитаете). Они почти всегда решаются определенным видом динамики на графе, который называется ретроанализом.

+

Ретроанализ

+

Обобщим пример со спичками и сформулируем критерии выгрышности и проигрышности:

+
    +
  • Вершина проигрышная — если все её переходы ведут в выигрышные вершины.
  • +
  • Вершина выигрышная — если из неё есть переход в проигрышную вершину.
  • +
  • Вершина ничейная — если она не выиграшная, и не проигрышная (у неё есть хотя бы один переход в другую ничейную и, возможно, сколько-то переходов в выигрышные).
  • +
+

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

+

Корректность. Рассмотрим случай ациклической игры. Он проще: раз циклов нет, значит нет и ничейных вершин.

+

Рассмотрим граф неизвестных вершин \(U\). Будучи подграфом исходного, он тоже ациклический, а значит должна быть вершина \(v\), у которой нет исходящих рёбер в \(U\) — если рёбра есть, то они ведут в вершины с известным состоянием. Значит, на данном этапе мы и для вершины \(v\) определим выигрышность, которая будет зависеть только от того, ведут ли эти рёбра в какое-то проигрышное состояние.

+

Вернёмся к общему случаю — когда в графе могут быть циклы. Наш алгоритм не пометит ничейные вершины ничейными, потому что здесь возникает проблема курицы и яйца — изначально ничейных терминальных вершин нет, и вершину нельзя по определению пометить ничейной. Но выясняется, что алгоритм менять не надо — ничейными можно просто объявить все вершины, выигрышность которых определить не удалось.

+

Почему так? Пусть мы провели сколько-то итераций алгоритма, и он остановился, когда в графе остались ещё какие-то вершины с невыясненным состоянием, которые образуют подграф \(U\). В этом графе ни одна вершина не ведет в проигрышную — иначе она была бы помечена выигрышной, и алгоритм пошел бы дальше. Также, нет вершин без исходящих рёбер в \(U\) — иначе такая вершина была бы помечена проигрышной. Получается, оптимальной стратегией в этом подграфе будет всегда оставаться в нём — выходы есть только в выигрышные вершины — и, значит, все вершины в нём ничейные.

+

Асимптотика. Можно добавить все терминальные вершины в очередь, и поддерживать в ней список вершин, у которых мы определили выигрышность, но ещё не обработали.

+
    +
  • При обработке проигрышной вершины, надо пометить выигрышными все вершины, у которых есть ребро, ведущее в текущее (для этого нам надо хранить обратный граф).
  • +
  • При обработке выигрышной вершины, надо удалить все ребра в обратном графи и пометить проигрышными все вершины, у которых больше не осталось рёбер в неизвестные вершины, и которые ещё не были помечены раньше.
  • +
+

Асимптотика составит \(O(n + m)\), как и у любого bfs-а.

+
vector<int> g[maxn], t[maxn]; // списки смежности прямого и обратного графа
+int cnt[maxn]; // счётчик исходящих рёбер
+
+enum StatusType { win, loss, unknown };
+StatusType status[maxn]; // выгрышность вершины;
+// по умолчанию все кроме терминальных считаются unknown
+// те, кто в итоге остаются unknown -- ничейные
+
+queue<int> q = {/* нужно заранее добавить сюда все терминальные вершины*/};
+
+while (!q.empty()) {
+    int v = q.front();
+    q.pop();
+    for (int u : t[v]) {
+        cnt[u]--; // удаляем это ребро
+        if (status[v] == unknown) {
+            // из u есть ребро в проигрышную -- значит она выигрышная
+            if (status[v] == loss)
+                status[u] = win;
+            // все ребра u ведут в выигрышные вершины -- значит она проигрышная
+            if (status[v] == win && cnt[u] == 0) {
+                status[u] = loss;
+            // если после проверок у вершины определилась выигрышность, то её можно добавить в очередь
+            if (status[v] != unknown)
+                q.push(u);
+        }
+    }
+}
+

Как обычно это бывает с динамиками, иногда проще написать рекурсивный подсчёт динамики, а не итеративный. Однако это работает только с ациклическими динамиками, а в общем графе могут быть циклы. Тем не менее, именно рекурсивную реализацию ретроанализа мы будем считать каноничной, потому что она в дальнейшем позволит делать некоторые отсечения и оптимизации:

+
StatusType dfs(int v) {
+    if (status[v] != unknown)
+        return status[v];
+    status[v] = loss;
+    // изменим статус, когда найдём переход в проигрышную вершину
+    for (int u : g[v])
+        if (dfs(u) == loss)
+            status[v] = win;
+    return status[v];
+}
+

TODO: можно ли здесь циклы учесть?

+

Минимаксные игры

+

Иногда у нас более богатая градация терминальных состояний, чем просто проигрышные и выигрышные. Такие игры в общем случае называются минимаксными — в терминальных вершинах могут быть записаны произвольные числа, задача первого игрока — придти в наибольшее, а второго — в наименьшее. Игроков будем называть Макс и Мин соответственно.

+

Типичный граф минимаксной игры выглядит следующим образом:

+

+

В не-листовых вершинах записаны значения игр в поддеревьях при оптимальной игре обоих соперников.

+

Решение таких задач ничем принципиально не отличается от ретроанализа, только теперь у нас «хорошесть» вершины — это минимум / максимум из переходов в детей.

+

Ретроанализ для больших графов

+

Иногда наша игра сложная, и все её состояния не вмещаются в память — например, шахматы или го. Это конечные ациклические игры, и в них однозначно определен победитель при оптимальной игре, которого в теории можно найти ретроанализом. Однако возникает проблема: алгоритм работает очень долго.

+

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

+

Ретроанализ нужно оптимизировать.

+

Ограничение перебора

+

Для этого надо идти по состояниям графа BFS-ом, а не DFS-ом, и останавливаться в момент, когда пройдено \(K\) ходов. При этом возникает только одна проблемы: как понять результат игры на рассматриваемой доске через \(K\) ходов, ведь игра еще не закончена. Для этого надо придумать приближенную функцию которая численно оценивает, насколько первый игрок выигрывает.

+

Для шахмат можно просто присвоить каждой фигуре сколько-то баллов и считать для каждого игрока эту сумму, а за функцию взять разность сумм у первого и второго игрока. Тогда если у первого игрока есть стратегия, с помощью которой он за \(K\) ходов гарантированно ест ферзя, не теряя много своих фигур, ретроанализ это обнаружит.

+

Ясно, что это дает существенное ускорение по времени: можно подобрать \(K\) так, чтобы время было достаточно маленьким. А если есть какой-то определенный лимит по времени, можно прекращать перебирать, когда закончится этот лимит. Но ход, который предлагает ретроанализ с такой оптимизацией, конечно, перестает быть оптимальным. Впрочем, оптимальную стратегию для шахмат пока найти не смогли (а для шашек, кстати, нашли).

+

Мемоизация позиций

+

В некоторые позиции можно придти более чем одним способом, а также есть группы позиций, про которые нам заранее известно, что их исход один и тот же (например, симметричные позиции в крестиках-ноликах). Нам не обязательно обрабатывать каждую из них каждый раз, когда попадём в неё — эффективнее работает .

+

Для шахмат (и не только) был разработан интересный способ хэширования. Вместо полиномиального хэша. Для каждой пары

+

Альфа-бета отсечение

+

+

Рассмотрим стандартный минимакс. На картинке в не-листовых вершинах написан выигрыш игрока, который ходит из неё первым.

+

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

+

Идею оптимизации рассмотрим на частном примере: мы перебираем куда идти из состояния \(X\) (например, корень). Мы уже зашли в первого и второго сына, увидели, что если пойти в первого, то выигрыш второго игрока составит -3, а если во второго, то выигрыш составит -6. Наша задача - минимизровать его выигрыш, так как это максимизирует наш выигрыш. Пока что лучшим ходом из корня дерева является второй сын, и выигрыш будет равен 6.

+

Давайте зайдем в третьего сына, назовем его \(Y\), вдруг там второй сможет набрать больше, чем -6. Зайдем в первого сына \(Y\), обработаем полностью и заметим, что при таком ходе второй при оптимальной игре получит -5. Утверждается, что тогда ход во второго сына \(Y\) можно вообще не проверять. Почему? Потому что уже понятно, что если пойти из корня \(X\) в вершину \(Y\), то выигрыш второго будет хотя бы -5 (а возможно даже больше), а значит выигрыш первого будет не более, чем 5. Но ход во второго сына \(X\) уже дает 6, что больше, и рассматривать вершину \(Y\) далее бессмысленно, мы в нее уже точно не пойдем.

+

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

+ +

Игры с ненулевой суммой

+

Теория Шпрага-Гранди

+

Рассмотрим игру «ним»: даны \(n\) кучек, в каждой из них по несколько камней. За один ход игрок может выбрать кучку и выбросить оттуда любое ненулевое число камней. Проигрыш наступпает, когда ходов больше не осталось, то есть когда все кучки пустые.

+

Немного переформулируем условие. Состояние нима однозначно описывается неупорядоченным набором неотрицательных чисел — как-нибудь пронумеруем их и обозначим количество камней в \(i\)-й как \(a_i\). Теперь, за один ход разрешается строго уменьшить любое из чисел. Терминальное состояние — когда все числа стали нулями.

+

Теорема. Состояние игры выигрышное тогда и только тогда, когда xor-сумма $ S = a_1 a_2 a_n $ размеров кучек отлична от нуля.

+

Доказательство проведём по индукции. Для терминального состояния xor-сумма равна нулю, и оно действительно проигрышное — база доказана. Теперь докажем переходы:

+
    +
  • Из состояния с нулевой xor-суммой все переходы ведут в выигрышные состояния, то есть в состояния с ненулевой суммой. В самом деле, достаточно убрать сколько угодно спичек из любой кучки — xor сумма изменится с нуля на $a_i b_i $, где \(b_i < a_i\) — это число камней в \(i\)-й кучке после нашего действия.
  • +
  • Второй переход сложнее — нужно показать, что если xor-сумма ненулевая, то всегда существует такой \(b_i < a_i\), что xor-сумма станет нулевой, то есть \(S \oplus a_i \oplus b_i = 0\). Для этого посмотрим на старший взведенный бит \(S\) и возьмем любой \(a_i\), у которого этот бит тоже взведен. Такой \(a_i\) найдётся хотя бы один — свойства ксора говорят, что их должно быть нечетное число. Из предыдущего равнства вытекает, что искомый \(b_i\) равен \(S \oplus a_i\), и выясняется, что это корректный новый размер кучки, то есть \(b_i < a_i\). Почему так? Потому что все старшие биты в выражении остались нетронутыми, \(k\)-й бит изменился на единицу, а что происходило с дальнейшими битами нам не важно, потому что эти изменения точно не больше, чем \(2^k\).
  • +
+

Автор любит конструктивные доказательства — из них сразу же вытекают алгоритмы, которые остается только реализовать. Получается, что оптимальная стратегия: посчитать xor-сумму всех \(a_i\), найти такой \(a_i\), у которого старший бит взведен, и заменить его на \(S \oplus a_i\).

+

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

+

Зачем это надо? Есть много игр, в которых присутствует какой-то подобный цугцванг (шахматный термин — когда у соперника кончились хорошие ходы, и он бы просто стоял на месте, но правила это запрещают). Выясняется, что они все эквивалентны ниму — их графы с точки зрения выигрышности суммы отдельных игр работают также, как графы нима. Но чтобы доказать это, нам потребуется немного получше разобраться в том, как ним устроен.

+

Ним с увеличениями

+

Пусть у нас

+

Эквивалентность игр ниму

+

Игры с неполной информацией

+

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

+

Любое доказательство использует слишком большое количество матана, чтобы его включать в блог про программирование, так что автор приводить его не будет.

+

Вы могли знать это имя по «Играм Разума». Не описано, чем он занимался, и какой вклад это внесло.

+

Дилемма заключенного

+

Камень-ножницы-бумага

+

Покер

+

Рассмотрим такую у

+ + diff --git a/ru/geometry.html b/ru/geometry.html new file mode 100644 index 0000000..356ee0f --- /dev/null +++ b/ru/geometry.html @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + +

Геометрия и ООП

+

Напомним, что отрезок, для которого указано, какой из его концов считается началом, а какой — концом, называется вектором. Вектор на плоскости можно задать двумя числами — его координатами по горизонтали и вертикали.

+
+vector +

vector

+
+

Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь.

+

Скалярное произведение (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:

+

\[ a \cdot b = x_a x_b + y_a y_b \]

+
+dot +

dot

+
+

У него есть полезные свойства:

+
    +
  • Скалярное произведение симметрично (\(a \cdot b = b \cdot a\)).
  • +
  • Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.
  • +
  • Перпендикулярные вектора должны иметь нулевое скалярное произведение.
  • +
  • Если угол острый, то оно положительное
  • +
  • Если угол тупой, то отрицательное
  • +
+

Векторное произведение (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:

+

\[ a \times b = x_a y_b - y_a x_b \]

+
+cross +

cross

+
+

Его свойства:

+
    +
  • Скалярное произведение антисимметрично (\(a \times b = - (b \times a)\)).
  • +
  • Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.
  • +
  • Коллинеарные вектора должны иметь нулевое векторное произведение.
  • +
  • Если b «слева» от a, то оно положительное.
  • +
  • Если b «справа» — то отрицательное.
  • +
+

Всякие проверки

+

Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.

+

Принадлежность точки треугольнику. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.

+

\[ +\text{P лежит внутри ABC} \iff \begin{cases} +(B-A) \times (P-A) \geq 0 \\ +(C-B) \times (P-B) \geq 0 \\ +(A-C) \times (P-C) \geq 0 \\ +\end{cases} +\]

+

Площадь треугольника. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.

+

\[ V = \frac{1}{2} (B-A) \times (C-A) \]

+

Площадь произвольного многоугольника. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).

+
+any +

any

+
+

Забудьте о формуле Герона и всегда считайте площади через векторное произведение.

+

Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.

+

Проверка на выпуклость. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то \((b-a)\times(c-a) > 0\)).

+

Пересекаются ли отрезки.

+
+segments +

segments

+
+

Уравнение прямой

+

Прямая задается уравнением вида \(Ax + By + C = 0\). Полуплоскость можно задать таким же неравенством.

+

У прямой есть вектор нормали с координатами \((A, B)\). Он перпендиуклярен прямой, а в случае с полуплоскостью \(Ax + By + C \geq 0\) будет указывать в сторону самой полуплоскости.

+

Чтобы найти расстояние от точки \((x_0, y_0)\) до прямой \(Ax + By + C = 0\), можно воспользоваться следующей формулой:

+

\[ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} \]

+

Точка пересечения. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:

+

\[ +\begin{cases} +A_1 x + B_1 y + C_1 = 0 \\ +A_2 x + B_2 y + C_2 = 0 +\end{cases} +\implies +\begin{cases} +-x = \frac{B_1 y + C_1}{A_1} \\ +-x = \frac{B_2 y + C_2}{A_2} +\end{cases} +\implies +\frac{B_1 y + C_1}{A_1} = \frac{B_2 y + C_2}{A_2} +\implies +y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} +\]

+

Аналогично, \(x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}\) (обратите внимание на знаки).

+

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

+

Как это кодить в C++

+

Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

+

Точка \(\simeq\) вектор. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её радиус-вектор — вектор из начала координат, ведущий в эту точку. По принятой в математике и физике нотации, будем обозночать вектора как r. Вы можете обозвать их как point, pt, vec — как угодно.

+
struct r {
+    double x, y;
+    r () {}
+    r (int _x, int _y) { x = _x, y = _y; }
+};
+

Функция r внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, r() вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами \((x, y)\).

+

Операции над векторами

+

Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:

+
double len (r a) { return sqrt(a.x*a.x + a.y*a.y); }
+

Операторы

+

В C++ можно перегружать почти все стандартные операторы, например, +, -, << и т. д.

+

Давайте для будущих нужд определим + и -:

+
r operator+(r a, r b){ return r(a.x+b.x, a.y+b.y); }
+r operator-(r a, r b){ return r(a.x-b.x, a.y-b.y); }
+

Скалярное произведение

+
int operator*(r a, r b){ return a.x*b.x + a.y*b.y; }
+

Векторное произведение

+

Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике.

+
int operator^(r a, r b){ return a.x*b.y - b.x*a.y; }
+

Ввод-вывод

+

Как вы думаете, как на самом деле работает cin >> x;? Это тоже перегрузка оператора >>. Делать это нужно так:

+
istream& operator>>(istream &in, r &p){ 
+    in >> p.x >> p.y;
+    return in;
+}
+
+ostream& operator<<(ostream &out, r &p){ 
+    out << p.x << " " << p.y << endl;
+    return out;            
+}
+

Алгебра VS Алгоритмы или зачем мы всё это делали

+

Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать intersect(a, b), в корректности которого он точно уверен.

+

Векторное представление прямой (\(Ax + By + C = 0 \rightarrow r = at + b\))

+

Тут нужно просто выбрать две любые точки на прямой.

+
// даны A, B, C (A^2 + B^2 != 0)
+r a, b;
+if (eq(A, 0)) // значит, это горизонтальная прямая
+    a = r(0, -C/B), b = r(1, -C/B);
+else
+    a = r(-C/A, 0), b = (1, -(C+B)/A, 1)
+

Пример: отражение от прямой

+

Пусть нам надо отразить точку \((x_0, y_0)\) симметрично относительно заданной прямой \(ax+by+c=0\). Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.

+

$_a b = = = |b| $

+

Формула имеет смысл: длина на единичный вектор направления.

+

Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить.

+
// прямая r = at + b, точка c
+r pr (r a, r b, r c) {
+    c -= b; // пусть c и a выходят из одной точки
+    return b + (a*b / len(a) / len(a)) * a;
+}
+
+r reflect (r a, r b, r c) {
+    return c + 2*(pr(a, b, c)-c);
+}
+

Типичные баги

+

Точность

+
+

Первое правило действительных чисел — не использовать действительные числа

+
+

Все переменные типа double хранятся в компьютере неточно (ну а как вы представите ⅓ в двоичной системе счисления?). Поэтому при работе с даблами нужно всегда учитывать эту погрешность. Например, чтобы сравнить два дабла, надо проверить, что они отличаются по модулю меньше, чем на очень маленькое число eps:

+
const double eps = 1e-8;
+
+bool eq (double a, double b) { return abs(a-b) < eps }
+

Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре.

+

\(0 \neq -0\)

+

Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду.

+

Область определения обратных функций

+

acos, asin и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции.

+ + diff --git a/ru/hashing.html b/ru/hashing.html new file mode 100644 index 0000000..f4984c3 --- /dev/null +++ b/ru/hashing.html @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + +

Хэширование в строковых задачах

+

Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.

+

«Хорошая» хэш-функция: * Быстро считается — за линейное от размера объекта время; * Имеет не очень большие значения — влезающие в 64 бита; * «Детерминированно-случайная» — если хэш может принимать \(n\) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно \(\frac{1}{n}\).

+

Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюрьективными.

+

Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\), и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

+
+hash +

hash

+
+

Применения в реальной жизни

+
    +
  • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
  • +
  • Хэш-таблица. Класс unordered_set из STL можно реализовать так: заведём \(n\) изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию \(f\) с областью значений \([0, n)\). При обработке .insert(x) мы будем добавлять элемент \(x\) в \(f(x)\)-тый список. При ответе на .find(x) мы будем проверять, лежит ли \(x\)-тый элемент в \(f(x)\)-том списке. Благодаря «равномерности» хэш-функции, после \(k\) добавлений ожидаемое количество сравнений будет равно \(\frac{k}{n}\) = \(O(1)\) при правильном выборе \(n\).
  • +
  • Мемоизация. В динамическом программировании нам иногда надо работать с состояниями, которые непонятно как кодировать, чтобы «разгладить» в массив. Пример: шахматные позиции. В таком случае нужно писать динамику рекурсивно и хранить подсчитанные значения в хэш-таблице, а для идентификации состояния использовать его хэш.
  • +
  • Проверка на изоморфизм. Если нам нужно проверить, что какие-нибудь сложные структуры (например, деревья) совпадают, то мы можем придумать для них хэш-функцию и сравнивать их хэши аналогично примеру со строками.
  • +
  • Криптография. Правильнее и безопаснее хранить хэши паролей в базе данных вместо самих паролей — хэш-функцию нельзя однозначно восстановить.
  • +
  • Поиск в многомерных пространствах. Детерминированный поиск ближайшей точки среди \(m\) точек в \(n\)-мерном пространстве быстро не решается. Однако можно придумать хэш-функцию, присваивающую лежащим рядом элементам одинаковые хэши, и делать поиск только среди элементов с тем же хэшом, что у запроса.
  • +
+

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

+

Сегодня же мы остановимся на строках.

+

Полиномиальное хэширование

+

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

+

Будем считать, что строка — это последовательность чисел от \(1\) до \(m\) (размер алфавита). В C++ char это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: int x = (int) (c - 'a' + 1).

+

Определим прямой полиномиальный хэш строки как значение следующего многочлена:

+

\[ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p \]

+

Здесь \(k\) — произвольное число больше размера алфавита, а \(p\) — достаточно большой модуль, вообще говоря, не обязательно простой.

+

Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени \(k\):

+
const int k = 31, mod = 1e9+7;
+
+string s = "abacabadaba";
+long long h = 0, m = 1;
+for (char c : s) {
+    int x = (int) (c - 'a' + 1);
+    h = (h + m * x) % mod;
+    m = (m * k) % mod;
+}
+

Можем ещё определить обратный полиномиальный хэш:

+

\[ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p \]

+

Его преимущество в том, что можно написать на одну строчку кода меньше:

+
long long h = 0;
+for (char c : s) {
+    int x = (int) (c - 'a' + 1);
+    h = (h * k + x) % mod;
+}
+

Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой \(h\).

+

Зачем он нужен?

+

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

+

Например, если нужно посчитать хэш от конкатенации строк \(a\) и \(b\) (т. е. \(b\) приписали в конец строки \(a\)), то можно просто хэш \(b\) домножить на \(k^{|a|}\) и сложить с хэшом \(a\):

+

\[ h(ab) = h(a) + k^{|a|} \cdot h(b) \]

+

Удалить префикс строки можно так:

+

\[ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} \]

+

А суффикс — ещё проще:

+

\[ h(a) = h(ab) - k^{|a|} \cdot h(b) \]

+

В задачах нам часто понадобится домножать \(k\) в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:

+
const int maxn = 1e5+5;
+
+int p[maxn];
+p[0] = 1;
+
+for (int i = 1; i < maxn; i++)
+    p[i] = (p[i-1] * k) % mod;
+

Как это использовать в реальных задачах? Пусть нам надо отвечать на запросы проверки на равенство произвольных подстрок одной большой строки. Подсчитаем значение хэш-функции для каждого префикса:

+
int h[maxn];
+h[0] = 0; // h[k] -- хэш префикса длины k
+
+// будем считать, что s это уже последовательность int-ов
+
+for (int i = 0; i < n; i++) 
+    h[i+1] = (h[i] + p[i] * s[i]) % mod;
+

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

+

\[ h(s[l:r]) = \frac{h_r-h_l}{k^l} \]

+

Деление по модулю воможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

+

Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к \(n\)-ной. Так проще — нужно будет домножать, а не делить.

+

\[ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) \]

+
int hash_substring (int l, int r) {
+    return (h[r+1] - h[l]) * p[n-l] % mod;
+}
+

Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за \(O(1)\).

+

Упражнение

+

Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще.

+

Примеры задач

+

Количество разных подстрок. Посчитаем хэши от всех подстрок за \(O(n^2)\) и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

+

Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

+

Сравнение строк (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.

+

Палиндромность подстроки. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений hash_substring() на первом массиве и на втором.

+

Количество палиндромов. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно.

+

Изменения строки*

+

Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в декартово дерево. В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в upd() пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.

+

Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.

+

Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова.

+

Вероятность ошибки и почему это всё вообще работает

+

У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.

+

Событие, когда два хэша совпали, а не должны, называется коллизией. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в set \(O(n^2)\) различных случайных значений в промежутке \([0, m)\). Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать \(m\), чтобы не бояться такого?

+

Выбор констант

+

Практическое правило: если вам нужно хранить \(n\) различных хэшей, то безопасный модуль — это число порядка \(10 \cdot n^2\). Обоснование — см. парадокс дней рождений.

+

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

+

Можно также брать модуль \(2^{64}\). У него есть несколько преимуществ: * Он большой — второй модуль точно не понадобится. * С ним ни о каких переполнениях заботиться не нужно — если все хранить в unsigned long long, процессор сам автоматически сделает эти взятия остатков при переполнении. * С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию %.

+

Всё с этим модулем было прекрасно, пока не придумали тест против него. Однако, его добавляют далеко не на все контесты — имейте это в виду.

+

В выборе же \(k\) ограничения не такие серьезные: * Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. * Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.

+

Главное — чтобы значения \(k\) и модуля не знал человек, который генерирует тесты.

+

Парадокс дней рождений

+
+

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

+
+

Более общее утверждение: в мультимножество нужно добавить \(\Theta(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

+

Первое доказательство (для любителей матана). Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

+

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

+

Попытаемся оценить \(f\):

+

\[ +\begin{align} + \begin{aligned} + e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ + & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ + e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ + f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ + & = e^{-\frac{n(n-1)}{2d}} & \\ + & \simeq e^{-\frac{n^2}{2d}} & \\ + \end{aligned} +\end{align} +\]

+

Из последнего выражения более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется очень быстро.

+

Второе доказательство (для любителей теорвера). Введем \(\frac{n(n-1)}{2}\) индикаторов — по одному для каждой пары людей \((i, j)\) — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна \(\frac{1}{d}\).

+

Обозначим за \(X\) число совпавших дней рождений. Его ожидание равно сумме ожиданий этих индикаторов, то есть \(\frac{n (n-1)}{2} \cdot \frac{1}{d}\).

+

Отсюда понятно, что если \(d = \Theta(n^2)\), то ожидание равно константе, а если \(d\) асимптотически больше или меньше, то \(X\) стремится нулю или бесконечности соответственно.

+

Примечание: формально, из этого явно не следует, что вероятности тоже стремятся к 0 и 1.

+

Бонус: «мета-задача»

+

Дана произвольная строка, по которой известным только авторам задачи способом генерируется ответ yes/no. В задаче 100 тестов. У вас есть 20 попыток отослать решение. В качестве фидбэка вам доступны вердикты на каждом тесте. Вердиктов всего два: OK (ответ совпал) и WA. Попытки поделить на ноль, выделить терабайт памяти и подобное тоже считаются как WA.

+

«Решите» задачу.

+ + diff --git a/ru/hld.html b/ru/hld.html new file mode 100644 index 0000000..568c3f8 --- /dev/null +++ b/ru/hld.html @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + +

Heavy-light декомпозиция

+

HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать что-нибудь попроще.

+

+* +
+TODO: найти менее уродливую иллюстрацию +
+
    +
  • +
+

Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины \(v\) посмотрим на всех её непосредственных детей \(u\), выберем среди них ребёнка \(u_{max}\) (с самым большим размером поддерева) и назовём ребро \((v, u)\) тяжелым (heavy), а все остальные рёбра — лёгкими (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.

+

Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.

+

Утверждение. Дерево разбивается на непересекающиеся пути из тяжелых рёбер.

+

Доказательство. В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.

+

Назовём блоком либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.

+

Утверждение. На любом вертикальном пути будет не более \(O(\log n)\) блоков.

+

Доказательство разбивается на две части:

+
    +
  • Лёгких ребер на вертикальном пути будет не более \(O(\log n)\): рассмотрим самую нижнюю вершину и будем идти по вертикальному пути снизу вверх. Каждый раз, когда мы переходим по лёгкому ребру, размер поддерева текущей вершины увеличивается в два раза, потому что если вершина связана с родителем лёгким ребром, то у него есть какой-то другой ребёнок, который не легче текущего.
  • +
  • Непрерывных путей из тяжелых рёбер будет не более \(O(\log n\): если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего \(O(\log n)\).
  • +
+

Следствие. На любом пути будет не более \(O(\log n)\) блоков.

+

Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на \(O(\log n)\) запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам.

+

Реализация

+

Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.

+

А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то tin-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.

+

Теперь мы можем построить какую-нибудь структуру поверх массива размера \(n\) (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре.

+
vector<int> g[maxn];
+int s[maxn], p[maxn], tin[maxn], tout[maxn];
+int head[maxn]; // «голова» тяжелого пути, которому принадлежит v
+int t = 0;
+
+void sizes (int v = 0) {
+    s[v] = 1;
+    for (int &u : g[v]) {
+        sizes(u);
+        s[v] += s[u];
+        if (s[u] > s[g[v][0]])
+            // &u -- это ссылка, так что её легально использовать при swap-е
+            swap(u, g[v][0]);
+    }
+}
+
+void hld (int v = 0) {
+    rin[t] = v;
+    tin[v] = t++;
+    for (int u : g[v]) {
+        // если это тяжелый ребенок -- его next нужно передать
+        // в противном случае он сам является головой нового пути
+        head[u] = (u == g[v][0] ? head[v] : u);
+        hld(u);
+    }
+    tout[v] = t;
+}
+

Как им решать задачи

+

Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:

+
    +
  1. Узнать минимальное число на пути между \(v_i\) и \(u_i\).
  2. +
  3. Изменить число у \(v_i\)-той вершины на \(x_i\).
  4. +
+

Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в соответствующей статье и имеет методы upd(k, x) и get_min(l, r).

+
int val[maxn];
+segtree st(0, n);
+

При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:

+
void upd (int v, int x) {
+    st.upd(tin[v], x);
+}
+

Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям.

+
int ancestor (int a, int b) {
+    return tin[a] <= tin[b] && tin[b] <= tout[a];
+}
+
+void up (int &a, int &b, int &ans) {
+    while (!ancestor(head[a], b)) {
+        ans = min(ans, st.get_min(tin[head[a]], tin[a]));
+        a = p[head[a]];
+    }
+}
+
+int get_min (int a, int b) {
+    int ans = inf;
+    up(a, b, ans);
+    up(b, a, ans);
+    if (!ancestor(a, b))
+        swap(a, b);
+    ans = min(ans, st.get_min(tin[a], tin[b]));
+    return ans;
+}
+

+ + diff --git a/ru/index.html b/ru/index.html new file mode 100644 index 0000000..44f69b5 --- /dev/null +++ b/ru/index.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + +

Алгоритмика

+

Структуры данных

+ +

Общие техники

+
    +
  • Корневая декомпозиция: она где-то есть

  • +
  • Алгоритм Карацубы: введение в разделяй-и-властвуй, мастер-теорема

  • +
+

Дискретная математика

+
    +
  • Модулярная арифметика теорема Ферма, нахождение обратного по модулю, бинарное возведение в степень, диофантово уравнение, применения в комбинаторике, предподсчёт обратных факториалов за линейное время

  • +
  • Ро-алгоритм Полларда парадокс дней рождений, факторизация за \(O(\sqrt[4] n)\)

  • +
  • Матроиды алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость

  • +
+

Графы

+ +

Потоки

+ +

Деревья

+ +

Строки

+ +

Динамическое программирование

+ +

Оптимизация

+ +

Разное

+ +

Теория игр

+
    +
  • Теория игр: эту статью определенно надо распилить на несколько
  • +
+

Высшая математика

+ +

Геометрия

+ + + diff --git a/ru/ioi.html b/ru/ioi.html new file mode 100644 index 0000000..1a130a8 --- /dev/null +++ b/ru/ioi.html @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + +

IOI Upsolving Challenge

+

https://contest.yandex.com/ioi/total

+

Я решал их почти 2 года назад и уже не очень хорошо всё помню.

+

09-2

+

12 мая

+

Медведь Миша

+

Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за \(O(n^2 \log n)\).

+

Регионы

+

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

+

Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так:

+
    +
  • Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк
  • +
  • Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос
  • +
+

Такое должно работать за \(O(n \sqrt{n \log n})\), и TL такое позволяет.

+

Коммивояжёр

+

Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать.

+

Подумайте, какой вид будет иметь эта функция, если зафиксировать время.

+

TODO

+

05-1

+

31 мая

+

Сад

+

Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно.

+

Средняя последовательность

+

Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы:

+
int a = -inf, b = inf, _d = 0, d = 0, t;
+for(int i = 0; i < n; i++){
+    cin >> t;
+    _d = d;
+    d = 2*t - d;
+    if (i % 2 == 0)
+        b = min(b, (d-_d)/2);
+    else
+        a = max(a, (_d-d)/2);
+}
+
+cout << max(b - a + 1, 0ll);
+

Польские горки

+

По сути от нас просят обрабатывать запросы «присвоить всем элементам с \(l\) по \(r\) значение \(x\)» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше.

+

05-2

+

Прямоугольник

+

Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста.

+ + diff --git a/ru/karatsuba.html b/ru/karatsuba.html new file mode 100644 index 0000000..7e00b9c --- /dev/null +++ b/ru/karatsuba.html @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + +

Быстрое умножение

+

В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).

+

Андрей Колмогоров и несколько других пионеров компьютер саенса выдвинули «гипотезу \(n^2\)» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

+

Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.

+

Алгоритм Карацубы имеет довольно значимое место в истории науки.

+

{M(n)=O(n^{2}).} M(n)=O(n^{2}). У Колмогорова была гипотеза, что нижняя оценка для {M(n)} M(n) при любом методе умножения есть также величина порядка {n^{2}} n^{2}. На правдоподобность «гипотезы {n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности {M(n)=O(n^{{2}3})} M(n)=O(n^{{{2}3}}) и тем самым опроверг «гипотезу {n^{2}} n^{2}».

+

(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)

+

Можно представить себе какого-то грозного японца.

+

Мастер-теорема

+

Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек».

+

Мастер-теорема. Пусть имеется рекуррента:

+

\[ T(n) = \begin{cases} +a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 +\\ \Theta(1), & n \leq n_0 +\end{cases} \]

+

Тогда:

+
    +
  • A. Если \(c > \log_b a\), то \(T(n) = \Theta(n^c)\).
  • +
  • B. Если \(c = \log_b a\), то \(T(n) = \Theta(n^c \log n)\).
  • +
  • C. Если \(c < \log_b a\), то \(T(n) = \Theta(n^{\log_b a})\).
  • +
+
+

+
+

Доказательство. Рассмотрим «дерево рекурсии» этого соотношения. В нём будет \(log_b n\) уровней. На \(k\)-том уровне будет \(a^k\) вершин, каждая из которых будет стоить \((\frac{n}{b^k})^c\) операций. Просуммируем значения во всех вершинах по всем уровням:

+

\[ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k \]

+
    +
  • A. Если \(c > \log_b a\), то \(\sum (\frac{a}{b^с})^k\) это сумма убывающей геометрической прогрессии, которая не зависит от \(n\) и просто равна какой-то константе. Значит, \(T(n) = \Theta(n^c)\).
  • +
  • B. Если \(c = \log_b a\), то \[\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)\]
  • +
  • C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,
  • +
+

\[ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) \]

+

Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает \(O(n)\)

+

Алгоритм Карацубы

+

Алгоритм Карацубы сводит задачу умножения двух чисел длины \(n\) к возведению \(n\)-значного числа в квадрат.

+

Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи.

+

Развитие идеи

+

То же самое можно применить матрицам.

+

Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за \(O(n^{1+\epsilon})\) для произвольного \(\epsilon\) ещё никто не доказал.

+ + diff --git a/ru/lca.html b/ru/lca.html new file mode 100644 index 0000000..f8c14b3 --- /dev/null +++ b/ru/lca.html @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + +

Корневые деревья

+

Дерево называется корневым, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется корнем.

+

Примеры корневых деревьев: * наследование классов в языках программирования (если множественное наследование запрещено), * дерево факторизации числа на простые (в общем случае не уникальное), * иерархия в какой-нибудь компании, * просто множество как-то вложенных объектов (ссылка на крысу-роботягу).

+

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

+
+dfs +

dfs

+
+

Напоминание: DFS

+

Посчитаем для каждой вершины времена входа (\(tin\)) и выхода (\(tout\)) из неё во время эйлерова прохода.

+
vector<int> g[maxn];
+int p[maxn], tin[maxn], tout[maxn];
+int t = 0;
+
+void dfs (int v) {
+    tin[v] = t++;
+    for (int u : g[v])
+        dfs(u);
+    tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет 
+}
+

У этих массивов много полезных свойств: * Вершина \(u\) является предком \(v\) $tin_v [tin_u, tout_u) $. Эту проверку можно делать за константу. * Два полуинтервала — \([tin_v, tout_v)\) и \([tin_u, tout_u)\) — либо не пересекаются, либо вложены один в другой. * В \(tin\) есть все числа из промежутка от 0 до \(n-1\). У каждой вершины — свой номер. * Размер поддерева вершины \(v\) (включая саму вершину) равен \(tout_v - tin_v\). * Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации.

+

Запросы на поддеревьях

+

Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.

+
+

Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число \(x_i\) и найти значение числа у вершины \(v_i\).

+
+

Давайте запишем все числа у вершин в позиции, соответствующие \(tin\)-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать какой-нибудь достаточно продвинутой структурой.

+

Запросы на уровнях

+
+

Дано корневое дерево. Требуется отвечать на запросы нахождения \(d_i\)-того предка вершины \(v_i\) (т. е. вершины-предка, находящейся на расстоянии \(d_i\)).

+
+

Создадим \(h\) векторов, где \(h\) — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их \(tin\)-ам.

+

Теперь заметим, что отрезки их поддеревьев — \([tin_v, tout_v)\) — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять \(tin\) вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку.

+

Наименьший общий предок

+

Очень много задач нам поможет решить следующая вспомогательная задача.

+
+

Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин \(u_i\) и \(v_i\), то есть вершины \(w\), которая лежит на пути от корня до \(u_i\), на пути от корня до \(v_i\), и при этом самую глубокую (нижнюю) из всех таких.

+
+

По-английский эта задача называется Least Common Ancestor. Есть много разных способов её решать, и мы рассмотрим основные.

+
+lca +

lca

+
+

Для лучшего понимания: медленно (за линейное время) это можно делать так:

+
bool a (int u, int v) {
+    return tin[u] <= tin[v] && tin[v] <= tout[u];
+}
+
+int lca (int u, int v) {
+    while (!ancestor(u, v))
+        u = p[u];
+    return u;
+}
+

LCA: двоичные подъемы

+

Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве up размера \(n \times \lceil \log n \rceil\) — столько точно хватит. В up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует — то корень.

+

Такой препроцессинг можно выполнить за \(O(n \log n)\), используя тот факт, что предок на расстоянии \(2^{d+1}\) — это предок на расстоянии \(2^d\) предка на расстоянии \(2^d\):

+
int up[maxn][logn];
+
+void dfs (int v) {
+    for (int l = 1; l < logn; l++)
+        up[v][l] = up[up[v][l-1]][l-1];
+    tin[v] = t++;
+    for (int u : g[v]) {
+        up[u][0] = v;
+        dfs(u);
+    }
+    tout[v] = t++;
+}
+

Пусть поступил очередной запрос — пара вершин \((u, v)\): * Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. * Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.

+

Подробнее про второй пункт. Пусть \(L = \lceil \log n \rceil\). Присвоим \(i = L\). Будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком \(u\) (указатель up[v][i] изначально будет корнем, а затем каждую итерацию спускаться на \(2^i\)). Когда это произойдёт, подвинем указатель на \(2^i\)-го предка \(v\), и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на \(2^{i+1}\).

+
int lca (int v, int u) {
+    if (a(v, u)) return v;
+    if (a(u, v)) return u;
+    for (int l = logn-1; l >= 0; l--)
+        if (!ancestor(up[v][l], u))
+            v = up[v][l];
+    return up[v][0];
+}
+

Асимптотика

+

Препроцессинг — \(O(n \log n)\). Размер массива up ровно такой, и каждый его элемент вычисляется за константу.

+

Ответ на запрос — \(O(\log n)\), потому что по сути мы делаем один бинпоиск.

+

Запросы на путях

+

Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).

+

Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером \(2^d\)-го предка минимум на соответствующем пути.

+

Мы знаем, что минимум на пути от \(u\) до \(v\) — это минимум от минимума на пути от \(u\) до \(lca(u, v)\) и от минимума на пути от \(v\) до \(lca(u, v)\). А каждый минимум — это минимум на всех двоичных подъемах до LCA.

+
int get_min (int v, int u) {
+    int ans = inf;
+    for (int l = logn-1; l >= 0; l--)
+        if (!ancestor(up[v][l], u))
+            v = up[v][l], ans = min(ans, mn[v][l]);
+    for (int l = logn-1; l >= 0; l--)
+        if (!ancestor(up[u][l], v))
+            u = up[u][l], ans = min(ans, mn[u][l]);
+    return min({ans, mn[v][0], mn[u][0]})
+}
+

Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная.

+

Сведение к RMQ

+

Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.

+

+

Во втором массиве мы по сути выписали наш проход dfs-а.

+

Пусть у нас есть запрос: найти LCA вершин \(v\) и \(u\). Для определенности положим, что \(tin_v < tin_u\). Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из \(v\) и в первй раз вошли в \(u\). Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.

+

Получается, что можно найти LCA, просто найдя позицию минимума на отрезке \([tout_v, tin_u]\) в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы уже умеем.

+

Разреженная таблица

+

На практике асимптотику мы особо не улучшили — пока что всё равно требуется \(O(n \log n)\) времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.

+

Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за \(O(1)\), но использующая \(O(n \log n)\) препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в отдельной статье.

+

А наоборот можно?*

+

Примечание: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

+

Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.

+

Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера \(n-1\): единица стоит, если следующее значение больше, единица в противном случае ноль.

+

Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

+

\[O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)\]

+

Также посчитаем для каждой возможной маски размера \(\frac{\log n}{2}\) минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего \(\sqrt n\) (ради этого мы и делили логарифм на два).

+

ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.

+

Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.

+

Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать у Емакса.

+

Впрочем, этот алгоритм на практике использовать нецелесообразно: у него слишком большая константа. Слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.

+

Важный вывод такой: RMQ более общая задача, чем LCA. UPD: это неправда, я глупый.

+ + diff --git a/ru/linalg.html b/ru/linalg.html new file mode 100644 index 0000000..a66b197 --- /dev/null +++ b/ru/linalg.html @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + +

Ликбез по линейной алгебре

+

+

Определение. Функция \(f: \mathbb{R}^n \to \mathbb{R}^m\) называется линейной, если для неё выполнено

+
    +
  1. $ f(x+y) = f(x) + f(y) $
  2. +
  3. $ f(ax) = a f(x), ; a R $
  4. +
+

Примеры: * $ f(x) = 0 $ * $ f(x) = x $ * $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из \(\mathbb{R}^3\) в \(\mathbb{R}\)) * $ f(x) = (x, -x, 0) $ (из \(\mathbb{R}\) в \(\mathbb{R}^3\))

+

Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: * Сумма линейных функций — линейная функция. * Сумма коммутативна: \(f+g = g+f\)). * Сумма ассоциативна: \((f+g)+h = f+(g+h)\). * Композиция \(f(g(x)) = (f \circ g)(x)\) линейных функций — линейная функция. * Композция ассоциативна: \((f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h\). * Композиция в общем случае не коммутативна.
Пример: \(f = (-x_2, x_1)\) — поворот точки на плоскости на прямой угол, \(g = (x_1, 0)\) — проекция на \(Ox\). Почти для всех точек порядок этих операций важен.

+

Все свойства можно вывести лишь из этих двух пунктов в определении.

+

Что такое матрица?

+

Можно показать, что любую линейную функцию \(f: \mathbb{R}^n \to \mathbb{R}^m\) можно представить в таком виде:

+

\[ +f(x) = +\begin{pmatrix} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \\ +\ldots \\ +a_{m1} x_1 + a_{m2} x_2 + \ldots + a_{mn} x_n \\ +\end{pmatrix} +\]

+

Матрицы ввели просто как очень компактную запись этих коэффициентов \(a_{ij}\).

+

\[ +A = +\begin{pmatrix} +a_{11} & a_{12} & \ldots & a_{1n} \\ +a_{21} & a_{22} & \ldots & a_{2n} \\ +\vdots & \vdots & \ddots & \vdots \\ +a_{m1} & a_{m2} & \ldots & a_{mn} \\ +\end{pmatrix} +\]

+

Каждой линейной функции из \(\mathbb{R}^n\) в \(\mathbb{R}^m\) соответствует какая-то матрица размера \(n \times m\) (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении \(i\)-го строки и \(j\)-го столбца будем обозначать \(A_{ij}\). Не перепутайте.

+

Пусть линейной функции \(f\) соответствует матрица \(A\), а функции \(g\)\(B\). Тогда композиции этих функций \(h = f \circ g\) будет соответствовать произведение \(C\) матриц \(A\) и \(B\), определяемое так:

+

\[ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} \]

+

Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из \(g\) подставить в \(f\).

+

Когда перемножаете руками, удобно думать так: элемент на пересечении \(i\)-го столбца и \(j\)-той строки — это скалярное произведение \(i\)-той строки \(A\) и \(j\)-того столбца \(B\). Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер \(n \times k\), то вторая должна иметь размер \(k \times m\), то есть «средние» размерности обязательно должны совпадать.

+

+

Исходное выражение для \(f(x)\) теперь можно компактно записать как \(f(x) = Ax\) вместо \(m\) уравнений с \(n\) слагаемыми в каждом.

+

К матрицам не нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.

+

Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:

+
    +
  • Сумма матриц \(A\) и \(B\) — матрица \(C = A+B: C_{ij} = A_{ij} + B_{ij}\).
  • +
  • Сумма коммутативна: \(A+B = B+A\))
  • +
  • Сумма ассоциативна: \((A+B)+C = A+(B+C)\)
  • +
  • Умножение ассоциативно: \((AB)C = A(BC) = ABC\).
  • +
  • Умножение в общем случае не коммутативно.
  • +
+

Пример: матрица поворота в 2d. \[ +\begin{pmatrix} +\cos \alpha & -\sin \alpha \\ +\sin \alpha & \cos \alpha \\ +\end{pmatrix} +\]

+

Пример: матрица проецирования на \(Ox\) в 3d. \[ +\begin{pmatrix} +1 & 0 & 0 \\ +0 & 0 & 0 \\ +0 & 0 & 0 \\ +\end{pmatrix} +\]

+

Пример: матрица «свапни \(x\) и \(y\)». \[ +\begin{pmatrix} +0 & 1 \\ +1 & 0 \\ +\end{pmatrix} +\]

+

Напишем класс, который реализует матричное умножение.

+
struct matrix {
+    int n, m;
+    int t[];
+    matrix (int _n, int _m) {
+        n = _n, m = _m;
+        t = new int(n*m);
+        memset(t, 0, sizeof t);
+    }
+    int[] operator[] (int k) {
+        return t[k*m];
+    }
+}
+
+matrix operator* (matrix a, matrix b) {
+    matrix c(a.n, b.m);
+    for (int i = 0; i < a.n; i++)
+        for (int j = 0; j < b.m; j++)
+            for (int k = 0; k < a.m; k++)
+                c[i][j] += a[i][k] * b[i][k];
+     return c;
+}
+

Динамика

+

Некоторые динамики можно выразить в терминах матричного умножения.

+

Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?

+

\[ +\begin{pmatrix} +f_{n+1} \\ +f_{n+2} \\ +\end{pmatrix} += +\begin{pmatrix} +0+f_{n+1} \\ +f_{n}+f_{n+1} \\ +\end{pmatrix} += +\begin{pmatrix} +0 & 1 \\ +1 & 1 \\ +\end{pmatrix} +\begin{pmatrix} +f_{n} \\ +f_{n+1} \\ +\end{pmatrix} +\]

+

Обозначим за \(A\) эту матрицу перехода. Чтобы посчитать \(n\)-е число Фибоначчи, нужно применить \(n\) раз эту матрицу к вектору \((f_0, f_1) = (0, 1)\).

+

Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:

+

\[ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) \]

+

Это будет работать за \(O(n^3 \log n)\). Мы делаем \(O(n^3)\) операций для одного умножения, а всего их нужно сделать \(O(\log n)\). Кстати, наука знает и более быстрые способы перемножить матрицы, но на контестах они не нужны.

+
matrix binpow (matrix a, int p) {
+    matrix b(n, n);
+    for (int i = 0; i < n; i++)
+        b[i][i] = 1;
+    while (p) {
+        if (p&1) b = b*a;
+        a = a*a;
+        p >>= 1;
+    }
+    return b;
+}
+

Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: \(I\).

+

\[ +\begin{vmatrix} +1 & 0 & 0 \\ +0 & 1 & 0 \\ +0 & 0 & 1 \\ +\end{vmatrix} +\]

+

В плане умножения она действительно ведет себя как единица: \(AI = A = IA\). В коде она используется вместо единицы.

+

В общем случае, линейная рекуррента \(f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}\) имеет такую матрицу перехода:

+\begin{pmatrix} +0 & 1 & 0 & \ldots & 0 \\ +0 & 0 & 1 & \ldots & 0 \\ +\vdots & \vdots & \vdots & \ddots & \vdots \\ +0 & 0 & 0 & \ldots & 1 \\ +a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ +\end{pmatrix} +

Матрица смежности

+

У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины \(a\) в вершину \(b\) за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из \(a\) в \(b\), используя ровно \(n\) переходов.

+

Когда нам нужна только информация, можно ли дойти из \(a\) в \(b\) (количество способов не важно), то решение можно ускорить (см. Битсет).

+

Обобщения

+

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

+

Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два.

+

Определитель

+

Определителем кадратной матрицы \(A\) называется такое выражение:

+

\[ \det A = |A| = \ldots \]

+

Где \(\epsilon\) — чётность числа инверсий в перестановке (\(-1\) или \(+1\)). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:

+

\[ +\begin{vmatrix} +a & b \\ +c & d \\ +\end{vmatrix} += +ad - bc +\]

+

У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.

+

В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем.

+

Базис

+

Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их.

+

Базисы есть не только в линейной алгебре. Например, \(\{1, x, x^2\}\) является базисом всех квадратных трёхчленов. Или \(\{\neg, \land, \lor\}\) является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом.

+

Метод Крамера и easy пересечение прямых

+

Пусть нам надо пересечь две прямые.

+

\[ +\begin{cases} +a_1 x + b_1 y + c_1 = 0 +a_2 x + b_2 y + c_2 = 0 +\end{cases} +\]

+

Это то же самое, что найти такие коэффициенты \(x\) и \(y\), что

+

\[ x \vec{a} + y \vec{b} = -\vec{c} \]

+

Площадь параллелограмма, натянутого на \(\vec{a}\) и \(\vec{b}\), равна векторному произведению, или детерминанту.

+

По сути, нам нужно выразить \(c\) в другом базисе. Давайте спроецируем её на \(a\).

+

Аналогично, напрягите воображение и спроецируйте эту точку в \(n\)-мерном пространстве. Это уже сложно, да?

+

*Собственные векторы

+

Очень часто у матриц есть собственные вектора -- те, которые не меняют направление.

+

$ Av = k v $, где \(k \neq 0\).

+

$ Av - kv = (A-kI)v = 0 $. Это означает

+

Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике.

+

Системы уравнений и метод Гаусса

+

Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

+

Нас по сути просят решить следующую систему:

+

\[ +\begin{cases} +a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n \equiv b_1 \pmod 2\\ +a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n \equiv b_2 \pmod 2\\ +\ldots \\ +a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n \equiv b_n \pmod 2 +\end{cases} +\]

+

Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

+

Метод Крамера неоптимален — там \(O(n^4)\) операций.

+

В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

+
t gauss (matrix a) {
+    for (int i = 0; i < n; i++) {
+        int nonzero = i;
+        for (int j = i+1; j < n; j++)
+            if (a[j][i])
+                nonzero = j;
+        swap(a[nonzero], a[i]);
+        for (int j = 0; j < n; j++)
+            if (j != i && a[j][i])
+                a[j] ^= a[i];
+    }
+    t x;
+    for (int i = 0; i < n; i++)
+        x[i] = a[i][n] ^ a[i][i];
+    return x;
+}
+

Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

+

Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. Битсет).

+ + diff --git a/ru/matching.html b/ru/matching.html new file mode 100644 index 0000000..03986bf --- /dev/null +++ b/ru/matching.html @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + +

Паросочетания

+

Пусть есть \(n\) мальчиков и \(m\) девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.

+

+

Паросочетанием \(M\) называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из \(M\)).

+

Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном \(M\)), назовём насыщенными этим паросочетанием.

+

Мощностью паросочетания назовём количество рёбер в нём. Наибольшим (максимальным) паросочетанием назовём паросочетание, мощность которого максимальна среди всех возможных паросочетаний в данном графе, а совершенным — где все вершины левой доли им насыщенны.

+

Паросочетания можно искать в любых графах, однако этот алгоритм неприятно кодить, и он работает за \(O(n^3)\), так что сегодня мы сфокусируемся только на двудольных графах. Будем в дальнейшем обозначать левую долю графа как \(L\), а правую долю как \(R\).

+

Цепью длины \(k\) назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно \(k\) рёбер.

+

Чередующейся цепью относительно некоторого паросочетания назовём простой путь длины \(k\) в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.

+

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

+
+ + +
+

Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: \(1 \to 8 \to 4 \to 6 \to 3 \to 7\).

+

Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести чередование — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.

+

В примере добавятся синие рёбра \((1, 8)\), \((3, 7)\) и \((4, 6)\), а удалятся красные \((3, 6)\) и \((4, 8)\). С ребром \((2, 5)\) ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.

+

Алгоритм Куна в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже.

+
const int maxn;
+
+vector<int> g[maxn]; // будем хранить только рёбра из левой доли в правую
+int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой)
+bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом
+
+// dfs возвращает, можно ли найти путь из вершины v
+// в какую-нибудь вершину правой доли
+// если можно, то ещё и проводит чередование
+bool dfs (int v) {
+    if (used[v])
+        return false;
+    used[v] = true;
+    for (int u : g[v]) {
+        // если вершина свободна, то можно сразу с ней соединиться
+        // если она занята, то с нейможно соединиться только тогда,
+        // когда из её текущей пары можно найти какую-нибудь другую вершину
+        if (mt[u] == -1 || dfs(mt[u])) {
+            mt[u] = v;
+            return true;
+        }
+    }
+    return false;
+}
+
+
+// где-то в main:
+
+memset(mt, -1, sizeof(mt));
+for (int i = 0; i < n; i++) {
+    memset(used, 0, sizeof(mt));
+    if (dfs(i))
+        cnt++;
+}
+

Корректность

+

Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.

+

Теорема (Бержа). Паросочетание без увеличивающих цепей является максимальным.

+

Доказательство проведём от противного: пусть есть два паросочетания вершин \(|A| \leq |B|\), и для \(A\) нет увеличивающих путей, и покажем, как найти этот путь и увеличить \(A\) на единицу.

+

Раскрасим ребра из паросочетания, соответствующего \(A\) в красный цвет, \(B\) — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из только красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих рёбер, а так как всего синих рёбер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром — он и будет увеличивающей цепью для \(A\), а значит \(A\) не оптимальное, и мы получили противоречие.

+

Скорость работы

+

Такой алгоритм ровно \(n\) раз ищет увеличивающий путь, каждый раз просматривая не более \(m\) рёбер, а значит работает за \(O(nm)\).

+

Что примечательно, его можно не бояться запускать на ограничениях и побольше (\(n, m \approx 10^4\)), потому что для него есть мощные неасимптотические оптимизации:

+
    +
  • Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).

  • +
  • Можно не заполнять нулями на каждой итерации массив used, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало true. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как false. В каком-то смысле это позволяет эмулировать очищение массива за константу.

  • +
  • Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы \(n \times m\). Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.

  • +
+

Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить алгоритм Диница к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за \(O(n \sqrt m)\).

+

Покрытие путями DAG-а

+

Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф \(G\) (англ. directed acyclic graph). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.

+

Построим соответствующие изначальному графу \(G\) два двудольных графа \(H\) и \(\overline{H}\) следующим образом: * В каждой доле графа \(H\) будет по \(n\) вершин. Обозначим их через \(a_i\) и \(b_i\) соответственно. * Для каждого ребра \((i, j)\) исходного графа \(G\) проведём соответствующее ребро \((a_i, b_j)\) в графе \(H\). * Теперь из графа \(H\) сделаем граф \(\overline{H}\), добавив обратное ребро \((b_i, a_i)\) для каждого \(i\).

+

Если мы рассмотрим любой путь \(v_1, v_2, \ldots, v_k\) в исходном графе \(G\), то в графе \(\overline{H}\) ему будет соответствовать путь \(a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}\). Обратное тоже верно: любой путь, начинающийся в левой доле \(\overline{H}\) и заканчивающийся в правой будет соответствовать какому-то пути в \(G\).

+

Итак, есть взаимно однозначное соответствие между путями в \(G\) и путями \(\overline{H}\), идущими из левой доли в правую. Заметим, что любой такой путь в \(\overline{H}\) — это паросочетание в \(H\) (напомним, это \(\overline{H}\) без обратных рёбер). Получается, любому пути из \(G\) можно поставить в соответствие паросочетание в \(H\), и наоборот. Более того, непересекающимся путям в \(G\) соответствуют непересекающиеся паросочетания в \(H\).

+

Заметим, что если есть \(p\) непересекающихся путей, покрывающих все \(n\) вершин графа, то они вместе содержат \(r = n - p\) рёбер. Отсюда получаем, что чтобы минимизировать число путей \(p\), мы должны максимизировать число рёбер \(r\) в них.

+

Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе \(H\). После нахождения этого паросочетания мы должны преобразовать его в набор путей в \(G\). Это делается тривиальным алгоритмом: возьмем \(a_1\), посмотрим, с какой \(b_k\) она соединена, посмотрим на \(a_k\) и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин.

+

Лемма Холла

+

Лемма Холла (или: теорема о свадьбах) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.

+
+ + +
+

Лемма Холла. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.

+

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

+

В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.

+

База индукции: одна вершина из \(L\), которая по условию соединена с хотя бы одной вершиной из \(R\).

+

Индукционный переход: пусть после \(k < n\) шагов построено паросочетание \(M\). Докажем, что в \(M\) можно добавить вершину \(v\) из \(L\), не насыщенную паросочетанием.

+

Рассмотрим множество вершин \(H\) — все вершины, достижимые из \(x\), если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)

+

Тогда в \(H\) найдется вершина \(y\) из \(R\), не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины \(H_L\) (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что \(|H_L| \leq |N(H_L)|\) (здесь \(N(X)\) — множество вершин, соединенным паросочетанием с \(X\)).

+

Тогда должен существовать путь из \(x\) в \(y\), и он будет увеличивающим для паросочетания \(M\), потому что из \(R\) в \(L\) мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно.

+

Для ноулайферов: матроиды

+

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

+

Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию хороших подмножеств (паросочетаний) обобщает понятие матроида. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше хорошее множество, если оно после добавления остается хорошим.

+

Применимо к паросочетаниям: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна.

+ + diff --git a/ru/matroid.html b/ru/matroid.html new file mode 100644 index 0000000..2ba869f --- /dev/null +++ b/ru/matroid.html @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + +

Матроиды

+

Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры).

+

Матроидом называется пара \((X, I)\), где \(X\) — множество элементов, называемое носителем матроида, а \(I\) — некоторое множество подмножеств \(X\), называемое семейством независимых множеств. В матроиде должны выполняться следующие свойства:

+
    +
  • Пустое множество является независимым: \(\varnothing \in I\)

  • +
  • Любое подмножество независимого множества тоже независимо: \[A \subset B, B \in I \implies A \in I\]

  • +
  • Если в независимом множестве \(A\) меньше элементов, чем в независимом множестве \(B\), то будет существовать элемент из \(B\), дополняющий \(A\) до независимого множества размера \(|A|+1\): \[A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I\]

  • +
+

Матроид называется взвешенным, если на нем существует аддитивная весовая функция: \(w(A) = \sum w(a_i)\).

+

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

+
X.sort()
+s = []
+for x in X:
+    if good(s + [x]):
+        s += [x]
+

Здесь под good имеется в виду \(s \cup x \in I\).

+

Корректность этого алгоритма для любого матроида доказывает следующая теорема:

+

Теорема Радо-Эдмондса

+

Пусть \(A \in I\) — множество минимального веса среди всех независимых подмножеств \(X\) мощности \(k\). Возьмем \(x: A \cup x \in I,\;x \notin A,\;w(x)\) — минимальна. Тогда \(A \cup x\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

+

*Доказательство: **

+

Рассмотрим \(B\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

+

Из свойств матроида: \(\exists y \in B \setminus A : A \cup y \in I\).

+

Тогда верны два неравенства:

+

\[ +\begin{cases} +w(A \cup y) = w(A) + w(y) \geq w(B) \implies w(A) \geq w(B) - w(y) \\ +w(B \setminus y) = w(B) - w(y) \geq w(A) \implies w(A) \leq w(B) - w(y) +\end{cases} +\]

+

Величина \(w(A)\) с двух сторон ограничивает величину \(w(B) - w(y)\). Значит, они равны. Cледовательно, \(w(A \cup y) = w(A) + w(y) = w(B)\).

+

Получаем, что если объединить множество \(A\) с \(x\) — минимальным из таких, что \(A \cup x \in I\), — то получим множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

+

Иными словами, если у нас есть оптимальное \(k\)-элементарное независимое множество, то мы можем индуктивно построить оптимальное \((k+1)\)-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым.

+

Примеры

+

Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.

+

Минимальный остов

+

Рассмотрим неориентированный граф \(G = (V, E)\). Пусть \(I\) — множество лесов графа (ациклических подмножеств \(E\)). Тогда \(M = (E, I)\) является матроидом:

+
    +
  • Граф без ребер является лесом.
  • +
  • Если удалить из леса ребра, он останется лесом.
  • +
  • Пусть есть два леса \(|A| \leq |B|\). В \(A\) будет \(|V| - |A|\) компонент связности, в \(B\) будет \(|V|-|B|\) компонент связности. Так как в \(B\) компонент связности меньше, то будет существовать какое-то ребро \(x\), связывающее две компоненты связности из \(A\). Его и возьмем: \(A \cup \{x\}\) тоже будет лесом, так как \(x\) только соединило две разные компоненты связности.
  • +
+

Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.

+

Расписания

+

Пусть у нас есть \(n\) заданий, на выполнение каждого требуется \(1\) час. Награда за выполнение \(i\)-го задания не позже \(d_i\)-того часа равна \(w_i\). В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.

+

Назовём правильными те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам (\(d_i\)) и проверить, что \(d_i \geq i\) для всех \(i\).

+

Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом:

+
    +
  • Пустой набор заданий всегда можно сделать.
  • +
  • Если у нас стало меньше заданий, то их сделать мы тоже успеем.
  • +
  • Пусть есть два правильных набора \(|A| \leq |B|\). Тогда в \(B\) будет существовать задание \(x\) с дедлайном позже \(|A|\). Все задания \(A\) можно сделать не позже \(|A|\)-го часа, а в \((|A|+1)\)-й час будем делать \(x\). Значит, \(A \cup x\) — тоже правильный набор.
  • +
+

Значит, можно отсортировать задания по убыванию их стоимости и пытаться в таком порядке добавлять в ответ, проверяя правильность какой-нибудь структурой данных.

+

Паросочетания

+

Рассмотрим двудольный граф \(G = (L, R, E)\). Пусть \(I\) — множество наборов вершин левой доли, которых можно покрыть каким-нибудь паросочетанием. Тогда \(M = (L, I)\) является матроидом:

+
    +
  • Любое паросочетание покрывает пустое множество вершин.
  • +
  • Исходное паросочетание покрывает также и любое подмножество исходных вершин.
  • +
  • Пусть есть два множества вершин \(|A| \leq |B|\). Раскрасим ребра из паросочетания, соответствующего \(A\) в красный цвет, \(B\) — в синий, а ребра из обоих паросочетаний — в пурпурный. Рассмотрим граф из красных и синих ребер. Любая компонента связности в нём представляет собой либо путь, либо цикл, состоящий из чередующихся красных и синих ребер. В любом цикле будет равное число красных и синих ребер, а так как всего синих ребер больше, то должен существовать путь, начинающийся и оканчивающийся синим ребром. Поменяем в этом пути красный и синий цвета и сделаем пурпурные ребра обратно красными. Теперь в графе из красных ребер на одно ребро больше, а значит к множеству \(A\) добавилась какая-то вершина из левой доли, принадлежавшая ранее \(B\).
  • +
+

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

+

Линейно независимые вектора

+

(TODO) (Школьники не обязаны знать линал.)

+

Такие штуки будем называть базисами.

+
    +
  • Ноль есть в любом базисе.
  • +
  • Подмножество базиса — базис.
  • +
  • ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора \(B\) лежат в \(A\). Значит, размерность \(B\) уж точно не больше.
  • +
+ + diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html new file mode 100644 index 0000000..e87a940 --- /dev/null +++ b/ru/mincost-maxflow.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + +

Поток минимальной стоимости

+

Рассмотрим ориентированный граф \(G = (V, E)\) с истоком \(s\) и стоком \(t\), в котором у каждого ребра \((u, v)\) задана целая стоимость \(w_{uv}\) и целая положительная пропускная способность \(c_{uv}\). Требуется найти максимальный поток, стоимость которого минимальна:

+

\[ \sum_{(u, v) \in E} f_{uv} \to \max \] \[ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min \]

+

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

+

Модифицируем сеть, добавив стандартным образом обратные рёбра, позволяющие «отменять» операции: для каждого ребра \((u, v)\) добавим \((v, u)\), для которого \(c_{vu} = 0\) и \(w_{vu} = -w_{uv}\). Напомним, что остаточной сетью называется граф из рёбер, остаточная пропускная способность которых ненулевая (\(c_{uv}-f_{uv} > 0\)).

+

Критерий оптимальности

+

Если в остаточной сети нет циклов отрицательного веса, то поток оптимален (и наоборот).

+

Доказательство:

+

\(\rightarrow\) Рассмотрим произвольный неоптимальный поток \(f\) и оптимальный поток \(f^*\). Рассмотрим разность \(f^*-f\). Она является циркуляцией, а любая циркуляция может быть разложена на сумму простых циклов. Хотя бы один из этих циклов будет иметь отрицательную стоимость, так как стоимость \(f^*\) меньше стоимости \(f\), что противоречит предположению.

+

\(\leftarrow\) Пусть цикл существует, тогда мы можем пропустить поток по этому циклу и получить поток меньшей стоимости.

+

Отмена циклов

+

Этот критерий сразу даёт нам относительно простой алгоритм: найдем какой-нибудь максимальный поток и будем «отменять» циклы отрицательного веса в остаточной цепи, пока такие циклы существуют. Искать цикл нам придётся не более \(mUC\) раз где \(U\) — величина потока, \(C\) — максимальная пропускная способность ребра. Этой величиной ограничен модуль минимальной стоимости ответа, а каждый отмененный цикл уменьшает ответ хотя бы на единицу.

+

Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит \(O(m^2nUC)\) (предполагая, что какой-нибудь максимальный поток мы уже нашли).

+

Дополняющие пути

+

Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.

+

Утверждеие. Алгоритм не создает в остаточной сети циклов отрицательного веса.

+

Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из \(s\) в \(t\) и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро \((v, u)\) находится в цикле отрицательного веса. Тогда есть путь Из \(u\) в \(v\) стоимости меньше, чем \(w_{uv}\). Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра \((u, v)\).

+

Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит \(O(nmU)\) — искать каждый дополняющий путь мы будем не более \(U\) раз.

+

Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер.

+

Потенциалы Джонсона

+

Потенциалом вершины \(v\) будем называть расстояние \(d_v\) от вершины \(s\). Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:

+

\[ w_{uv}' = w_{uv} + d_u - d_v \]

+

Утверждение 1. Веса всех рёбер графа неотрицательные.

+

Доказательство. Пусть вес какого-то ребра \((u, v)\) отрицателен, то есть \(w_{uv}' = w_{uv} + d_u - d_v < 0\). Тогда \(d_u + w_{uv} < d_v\), и нарушилось неравенство треугольника: почему мы тогда не использовали ребро \((u, v)\), когда искали кратчайший путь до \(v\)?

+

Аналогично можно показать, что рёбра на кратчайших путях из \(s\) имеют нулевую стоимость. Заметим, что стоимость обратных рёбер на кратчайших путях тоже будет нулевой: \[ w_{vu}' = w_{vu} + d_v - d_u = -w_{uv} - d_u + d_v = -(w_{uv}) = 0 \]

+

Утверждение 2. Кратчайшие пути между любыми вершинами остались кратчайшими.

+

Доказательство. Распишем новую стоимость пути из \(a\) в \(z\).

+

\[ +\begin{align} +w_{ab}' + \ldots + w_{yz}' +&= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) +\\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z +\end{align} +\]

+

Получаем, что стоимость всех путей из \(a\) в \(z\) лишь изменилась на константу.

+

Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.

+

Заметьте, что в доказательстве мы не использовали то, что \(d_v\) — кратчайшие расстояния. Это вообще могут быть произвольные числа.

+

Утверждение 3. Когда мы проталкиваем поток вдоль кратчайшего пути, удаляя ребра и возможно добавляя обратные, веса в изменённом графе тоже остались корректными (все рёбра неотрицательного веса и все кратчайшие пути остались кратчайшими).

+

Доказательство. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2).

+

Итоговый алгоритм

+
    +
  • Модифицируем сеть, добавивив обратные рёбра.
  • +
  • Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.
  • +
  • Пока максимальный поток не найден:
  • +
    • +
    • Посчитать алгоритмом Дейкстры кратчайшие расстояния от \(s\), используя для веса формулу с потенциалами, записать их в \(d\).
    • +
  • +
    • +
    • Протолкнуть максимально возможный поток вдоль кратчайшего пути \(s \leadsto t\), обновить остаточную сеть.
    • +
  • +
+

Асимптотика

+

Алгоритм работает за \(O(U m \log n)\) или \(O(U n^2)\) в случае плотных графов.

+

В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и \(U \leq n\) или \(U \leq m\). Например, в задаче о назначениях (паросочетание минимального веса) \(U = n\) и алгоритм работает за \(O(n^3)\), что совпадает с асимптикой венгерского алгоритма.

+

Реализация

+

Решение задачи о назначениях за \(O(n^3)\). Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).

+
    +
  • cost, cap — параметры сети
  • +
  • pot — потенциалы
  • +
  • par — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)
  • +
  • d — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния
  • +
+
const int maxn = 305, inf = 1e9;
+
+int n;
+int cost[maxn][maxn], cap[maxn][maxn];
+int d[maxn], pot[maxn], par[maxn];
+
+bool dijkstra (int s, int t) {
+    used[maxn] = {0};
+
+    fill(d, d+n, inf);
+    d[s] = 0;
+
+    while (1) {
+        int v = -1;
+        for (int u = 0; u < n; u++)
+            if (!used[u] && (v == -1 && d[u] < d[v]))
+                v = u;
+        if (v == -1 || d[v] == inf)
+            break;
+        used[v] = 1;
+        for (int u = 0; u < n; u++) {
+            int w = cost[v][u] + pot[v] - pot[u];
+            if (cap[v][u] && d[u] > d[v] + w) {
+                d[u] = d[v] + w;
+                par[u] = v;
+            }
+        }
+    }
+
+    return d[t] < inf;
+}
+
+int mincost_maxflow (int s, int t) {
+    int ans = 0;
+    while (dijkstra(s, t)) {
+        memcpy(pot, d, sizeof(d));
+        int delta = inf;
+        for (int v = t; v != s; v = par[v])
+            delta = min(delta, cap[par[v]][v]);
+        for (int v = t; v != s; v = par[v]) {
+            cap[par[v]][v] -= delta;
+            cap[v][par[v]] += delta;
+            ans += cost[par[v]][v]*delta;
+        }
+    }
+    return ans;
+}
+ + diff --git a/ru/mst.html b/ru/mst.html new file mode 100644 index 0000000..328cd24 --- /dev/null +++ b/ru/mst.html @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + +

Минимальные остовы

+
+

Авиакомпания содержит \(m\) рейсов между \(n\) городами, \(i\)-ый из них обходится в \(w_i\) рублей, причём из любого города можно добраться до любого другого. В стране наступил кризис, и нужно отказаться от как можно большего числа из них таким образом, что содержание оставшиъся рейсов будет наиболее дешевым.

+
+

Иными словами, нужно найти дерево минимального веса, которое является подграфом данного неориентированного графа. Такие деревья называют остовами (каркас, скелет; ударение на первый слог, но так мало кто произносит). По-английски — minimum spanning tree (дословно, минимальное покрывающее дерево).

+

Почему дерево? Потому в противном случае там был бы цикл из которго можно удалить какое-то ребро и получить ответ пооптимальней. А если это больше, чем одно дерево, то какие-то две вершины остаются несвязаны.

+

Вообще, следующие утверждения про деревья являются эквивалентными: * Граф — дерево. * В графе из \(n\) вершин \(n-1\) рёбер и нет циклов. * Из любой вершины можно дойти в любоую другую единственным образом.

+

Лемма о безопасном ребре

+

Назовем подграф \(T\) графа \(G\) безопасным, если они является подграфом какого-то минимального остова.

+

Назовем ребро безопасным, если при добавлении его в подграф \(T\) получившийся граф \(T'\) тоже является безопасным, то есть подграфом какого-то минимального остова.

+

Все алгоритмы для поиска минимального остова опираются на следующее утверждение:

+
+

Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным.

+
+

Доказательство: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже.

+
+safe +

safe

+
+

Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов.

+

Алгоритм Прима

+

Минимальный остов строится постепенно, добавлением в него рёбер по одному.

+
    +
  • Изначально остов — одна произвольная вершина.
  • +
  • Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден.
  • +
+

Очень похоже на алгоритм Дейкстры, только мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё.

+

Совсем наивная реализация за \(O(nm)\) — каждый раз перебираем все рёбра:

+
const int maxn = 1e5, inf = 1e9;
+vector<int> from, to, weight;
+bool used[maxn]
+
+// считать все рёбра в массивы
+
+used[0] = 1;
+for (int i = 0; i < n-1; i++) {
+    int opt_w = inf, opt_from, opt_to;
+    for (int j = 0; j < m; j++)
+        if (opt_w > weight[j] && used[from[j]] && !used[to[j]])
+            opt_w = weight[j], opt_from = from[j], opt_to = to[j]
+    used[opt_to] = 1;
+    cout << opt_from << " " << opt_to << endl;
+}
+

Реализация за \(O(n^2)\):

+
const int maxn = 1e5, inf = 1e9;
+bool used[maxn];
+vector< pair<int, int> > g[maxn];
+int min_edge[maxn] = {inf}, best_edge[maxn];
+min_edge[0] = 0;
+
+// ...
+
+for (int i = 0; i < n; i++) {
+    int v = -1;
+    for (int u = 0; u < n; j++)
+        if (!used[u] && (v == -1 || min_edge[u] < min_edge[v]))
+            v = u;
+ 
+    used[v] = 1;
+    if (v != 0)
+        cout << v << " " << best_edge[v] << endl;
+ 
+    for (auto e : g[v]) {
+        int u = e.first, w = e.second;
+        if (w < min_edge[u]) {
+            min_edge[u] = w;
+            best_edge[u] = v;
+        }
+    }
+}
+

Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за \(O(m \log n)\):

+
set< pair<int, int> > q;
+int d[maxn];
+
+while (q.size()) {
+    v = q.begin()->second;
+    q.erase(q.begin());
+ 
+    for (auto e : g[v]) {
+        int u = e.first, w = e.second;
+        if (w < d[u]) {
+            q.erase({d[u], u});
+            d[u] = w;
+            q.insert({d[u], u});
+        }
+    }
+}
+

Про алгоритм за \(O(n^2)\) забывать не стоит — он работает лучше в случае плотных графов.

+

Алгоритм Крускала

+

Будем добавлять рёбра в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить.

+

Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Однако для этой проверки нам нужна будет целая отдельная структура.

+

Система непересекающихся множеств

+

Эта структура данных предоставляет следующие возможности. Изначально имеется несколько элементов, каждый из которых находится в отдельном (своём собственном) множестве. Структура поддерживает две операции: * объединить два каких-либо множества * запросить, в каком множестве сейчас находится указанный элемент

+

Обе операции будут выполняться в среднем почти за \(O(1)\) (но не совсем — этот сложный вопрос будет разъяснен позже).

+

Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив _p, в котором для каждого элемента мы храним номер его предка в дерева. Для корней деревьев будем считать, что их предок — они сами.

+

Наивная реализация, которую мы потом ускорим:

+
int _p[maxn];
+
+int p (int v) {
+    if (_p[v] == v)
+        return v;
+    else
+        return p(_p[v]);
+}
+
+void unite (int a, int b) {
+    a = p(a), b = p(b);
+    _p[a] = b;
+}
+
+for (int i = 0; i < n; i++)
+    _p[i] = i;
+

Эвристика сжатия пути. Оптимизируем работу функции p. Давайте перед тем, как вернуть ответ, запишем его в _p от текущей вершины, то есть переподвесим его за самую высокую.

+

Насколько лучше это сделает асимптотику? Выясняется, что \(O(n \log n)\).

+

Тут должен быть мем из опросов.

+

Ранговая эвристика. Эта штука напрямую пытается минимизировать высоту дерева. Давайте делать переподвешивание за то, которое менее глубоко. Ну понятно, что тогда любое дерево будет не более логарифма.

+

Весовая эвристика. Давайте каждый раз подвешивать за более крупное. Работать будет быстро, так как .

+

Автор предпочитает именно её, потому что часто эти размеры компонент требуются сами по себе.

+

Оказывается, что сжатия + ранговая или сжатия + весовая работает быстро.

+

Асимптотика объединения обеих эвристик (сжатия путей и одной из ранговых) — O(a(n)), где a(n) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4). Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует.

+
int _p[maxn], s[maxn];
+
+int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); }
+
+void unite (int a, int b) {
+    a = p(a), b = p(b);
+    if (s[a] > s[b]) swap(a, b);
+    s[b] += s[a];
+    _p[a] = b;
+}
+
+for (int i = 0; i < n; i++)
+    _p[i] = i;
+

Полезные свойства и классические задачи

+
    +
  • Если веса всех рёбер различны, то остов будет уникален.
  • +
  • Минимальный остов является также и остовом с минимальным произведением весов рёбер (замените веса всех рёбер на их логарифмы)
  • +
  • Минимальный остов является также и остовом с минимальным весом самого тяжелого ребра.
  • +
  • Если вы решаете задачу, где ребра не добавляются, а удаляются, то можно попробовать решать задачу «с конца» и применить алгоритм Крускала.
  • +
+

Персистентная СНМ*

+

Как вы уже заметили, автор очень любит персистентные структуры данных.

+

СНМ — тоже структура, и её тоже можно сделать персистентной. В СНМ мы изменяем массивы, а массивы можно сделать персистентными через персистентное ДО (только так, проще не получается — многие пытались).

+

Здесь есть нюанс — амортизированные структуры не очень хорошо дружат с персистентностью. Поэтому нам придется отказаться от эвристики сжатия путей, и поэтому асимптотика составит \(O(n \log^2 n)\) времени и памяти — один логарифм от СНМа, другой от персистентного ДО.

+

Динамическая связность*

+

Dynamic Connectivity Problem:

+
+

Даны \(n\) запросов добавления ребра (+), удаления ребра (- и какого-то запроса про граф (?), например, о связности двух вершин.

+
+

О решении этой задачи в online и в offline можете почитать в этом посте.

+ + diff --git a/ru/pollard.html b/ru/pollard.html new file mode 100644 index 0000000..e18ff2c --- /dev/null +++ b/ru/pollard.html @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + +

Факторизация за \(O(\sqrt[4]{n})\)

+

Наука умеет раскладывать целые числа на множители за \(O(n^\frac{1}{4})\). Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера.

+

Парадокс дней рождений

+

Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

+

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

+

Попытаемся оценить \(f\):

+

\[ +\begin{align} + \begin{aligned} + e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ + & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ + e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ + f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ + & = e^{-\frac{n(n-1)}{2d}} & \\ + & \simeq e^{-\frac{n^2}{2d}} & \\ + \end{aligned} +\end{align} +\]

+

Из формулы более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее:

+

Утверждение. В мультимножество нужно добавить \(O(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

+

Если вы не доверяете математике, посмотрите на графики:

+
import matplotlib.pyplot as plt
+%matplotlib inline
+
+import seaborn as sns
+sns.set()
+
def f(n, d):
+    p = 1
+    for x in range(1, n):
+        p *= 1-x/d
+    return p
+
+def g(n, d):
+    return 2.71**(-0.5 * n**2 / d)
+
exact = [f(n, 365) for n in range(40)]
+approx = [g(n, 365) for n in range(40)]
+plt.plot(exact)
+plt.plot(approx)
+plt.show()
+
+png +

png

+
+

На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать \(n \approx \sqrt{d}\), построим такую же для больших чисел и перейдём в логарифмический масштаб.

+
approx = [g(n, 10**10) for n in range(10**6)]
+plt.xscale('log')
+plt.plot(approx)
+plt.show()
+
+png +

png

+
+

\(\rho\)-алгоритм Полларда

+

Итак, мы хотим факторизовать число \(n\). Предположим, что \(n = p q\) и \(p \approx q\). Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей.

+

Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например \(f(x) = (x+1)^2 \mod n\).

+

Как выглядит граф, в котором из вершины есть единственное ребро \(x \to f(x)\)? Такой граф называется функциональным. Если в нём нарисовать «траекторию» произвольного элемента, — какой-то путь, превращающийся в цикл, — то получится что-то похожее на букву \(\rho\) (ро). Алгоритм из-за этого так и назван.

+

Рассмотрим траекторию какого-нибудь элемента \(x_0\): {\(x_0\), \(f(x_0)\), \(f(f(x_0))\), \(\ldots\)}. Теперь сделаем из неё новую последовательность, взяв каждый элемент по модулю \(p\) — наименьшего из простых делителей \(n\).

+

Утверждение. Ожидаемая длина цикла в этой последовательности \(O(\sqrt[4]{n})\).

+

Доказательство: так как \(p\) — меньший делитель, то \(p \leq \sqrt{n}\). Теперь просто подставим в предыдущее утверждение: в множество нужно добавить \(O(\sqrt{p}) = O(\sqrt[4]{n})\) элементов, чтобы какие-то два совпали, а значит последовательность зациклилась.

+

Если мы найдём цикл в такой последовательности (то есть такие \(i\) и \(j\), что \(f^i(x_0) \equiv f^j(x_0)\)), то мы можем восстановить какой-то делитель \(n\), а именно \(\gcd(|f^i(x_0) - f^j(x_0)|, n)\) — это число меньше \(n\) и делится на \(p\).

+

Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя \(i\) и \(j\) (\(i = 2j\)) и проверять, что \(f^i(x_0) \equiv f^j(x_0) \pmod p\), что эквивалентно проверке \(\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1\).

+
from math import gcd
+from random import randint
+
def find_divisor(n, seed = 1, f = lambda x: (x+1)**2):
+    x = y = seed
+    d = 1
+    while d == 1:
+        x = f(f(x)) % n
+        y = f(y) % n
+        d = gcd(abs(x-y), n)
+    return d
+
n = 3*5*7 # = 105
+print([find_divisor(n, seed=i) for i in range(n)])
+
[3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3]
+
n = (10**9+7)*(10**9+9)
+print([find_divisor(n, seed=randint(1, n)) for _ in range(20)])
+
[1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009]
+

Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за \(O(\sqrt{n})\) (нужно добавить отсечение по времени).

+

Разное

+

Формально, мы показали, что алгоритм работает за \(O(\sqrt[4]{n} \log n)\) за счёт поиска \(\gcd\), но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет.

+

Пародокс дней рождения также можно использовать при подсчёте вероятности коллизии при хэшировании. Хороший модуль должен быть не менее квадрата количества хэшей, а чего-то вроде \(5 n^2\) уже почти всегда достаточно.

+ + diff --git a/ru/probability.html b/ru/probability.html new file mode 100644 index 0000000..476a5f3 --- /dev/null +++ b/ru/probability.html @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + +

Ликбез по теорверу

+
    +
  • Вероятностные распределения
  • +
  • Матожидание и дисперсия
  • +
  • Парадокс дней рождения
  • +
  • Нормальное распределение
  • +
  • Правдоподобие и оценка качества предсказания
  • +
  • Энтропия и оптимальное кодирование
  • +
+

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

+

(Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.)

+
import numpy as np
+
+import matplotlib as plt
+%matplotlib inline
+
+import seaborn as sns
+sns.set()
+

Матожидание

+

Дисперсия

+

Какие два числа лучше всего описывают распределение?

+

Нормальное распределение

+

Центральная предельная теорема названа так пафосно вполне обоснованно.

+

Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию.

+

\[ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} \]

+

Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса.

+

Трудно даже доказать, что это распределение, т. е. что \(\int_{-\inf}^\inf f(x) dx = 1\).

+

Применения

+

Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный.

+

Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как

+

Линейные рекурренты

+

Чтобы решать следующие задачи, нам нужно будет использовать следующий факт:

+

...

+

Доказательство мы не приведем.

+

В частности, таким образом получается формула для чисел Фибоначчи.

+

\[ f_n = \ldots \]

+

Кто бы мог подумать, что все эти иррациональности и степени сократятся и вообще дадут целое число?..

+

Классика

+

Парадокс дней рождения.

+

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

+

Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт?

+

TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. Казино. Мы приходим в казино с 1000$ и следующим образом проводим там время: ставим по 1$, пока не обанкротимся или не выиграем 1100$. Какая вероятность того, что мы уйдём с деньгами?

+

Принцип максимального правдоподобия

+

Энтропия

+

Энтропией называется минимальное число бит, которым теоретически возможно сжать сообщение. Эта величина важна, потому что на практике если её можно посчитать, то сжатие с соответствующей кратностью реально достижимо.

+

Шумный канал.

+

Пусть у вас есть 1тб данных и два китайских терабайтника, на каждый из которых можно записать столько данных, но каждый бит имеет вероятность 10% записаться на противоположный. Требуется сохранить данные с первого раза без потерь. Совсем без потерь.

+

Причём это делается почти впритык.

+ + diff --git a/ru/reciprocal.html b/ru/reciprocal.html new file mode 100644 index 0000000..e212c1c --- /dev/null +++ b/ru/reciprocal.html @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + +

Обратный элемент по модулю

+

Часто в задачах требуется посчитать что-то по простому модулю (чаще всего \(10^9 + 7\)). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче.

+

Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например:

+
c = (a + b) % mod;
+c = (mod + a - b) % mod;
+c = a * b % mod;
+

Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: \(\frac{8}{2} = 4\), но \(\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4\).

+

Нужно найти некоторый элемент, который будет себя вести как \(\frac{1}{a} = a^{-1}\), и вместо «деления» домножать на него. Назовем такой элемент обратным.

+

Способ 1: бинарное возведение в степень

+

Если модуль \(p\) простой, то решением будет \(a^{-1} \equiv a^{p-2}\). Это следует из малой теоремы Ферма:

+

Теорема. \(a^p \equiv a \pmod p\) для всех \(a\), не делящихся на \(p\).

+

Доказательство. (для понимания несущественно, можно пропустить)

+

\[ +\begin{align} +a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p +\\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} +\\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} +\\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} +\\ &= a +\end{align} +\]

+

Здесь \(P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}\) это мультиномиальный коеффициент — количество раз, которое элемент \(a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}\) появится при раскрытии скобки \((a_1 + a_2 + \ldots + a_n)^k\).

+

Теперь два раза «поделим» наш результат на \(a\).

+

\[ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} \]

+

Получается, что \(a^{p-2}\) ведет себя как \(a^{-1}\), что нам по сути и нужно. Посчитать \(a^{p-2}\) можно за \(O(\log p)\) бинарным возведением в степень.

+

Приведем код, который позволяет считает \(C_n^k\).

+
int t[maxn]; // факториалы, можно предподситать простым циклом
+
+// бинарное возведение в степень
+int bp (int a, int n) {
+    int res = 1;
+    while (n) {
+        if (n & 1) res = res * a % mod;
+        a = a * a % mod;
+        n >>= 1;
+    }
+    return res;
+}
+
+// находит обратный элемент как a^(p-2)
+int inv (int x) {
+    return bp(x, mod-2);
+}
+
+int c (int n, int k) {
+    return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod;
+}
+

Способ 2: диофантово уравнение

+

Диофантовыми уравнениями называют такие штуки:

+

\[ ax + by = 1 \]

+

Требуется решить их в целых числах, то есть \(a\) и \(b\) известны, и нужно найти такие целые (возможно, отрицательные) \(x\) и \(y\), чтобы равенство выполнялось. Решают такие вещи расширенным алгоритмом Евклида. TODO: описать, как он работает.

+

Подставим в качестве \(a\) и \(b\) соответственно \(a\) и \(m\)

+

\[ ax + my = 1 \]

+

Одним из решений уравнения и будет \(a^{-1}\), потому что если взять уравнение по модулю \(m\), то получим

+

\[ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m \]

+

Преимущества этого метода над возведением в степень: * Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. * Алгоритм проще выполнять руками.

+

Сам автор почти всегда использует возведение в степень.

+

Почему \(10^9+7\)?

+
    +
  1. Это выражение довольно легко вбивать (1e9+7).
  2. +
  3. Простое число.
  4. +
  5. Достаточно большое.
  6. +
  7. int не переполняется при сложении.
  8. +
  9. long long не переполняется при умножении.
  10. +
+

Кстати, \(10^9 + 9\) обладает теми же свойствами. Иногда используют и его.

+

Предподсчёт обратных факториалов за линейное время

+

Пусть нам нужно зачем-то посчитать все те же \(C_n^k\), но для больших \(n\) и \(k\), поэтому асимптотика \(O(n \log m)\) нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам.

+

Если у нас уже написан inv, то нам не жалко потратить \(O(\log m)\) операций, посчитав \(m!^{-1}\).

+

После этого мы будем считать \((m-1)!^{-1}\) как \(m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}\).

+
int f[maxn];
+f[0] = 1;
+for (int i = 1; i < maxn; i++)
+    f[i] = i*f[i-1] % mod;
+
+int r[maxn];
+r[maxn-1] = inv(f[maxn-1])
+for (int i = maxn-1; i >= 1; i--)
+    r[i-1] = r[i]*i % mod;
+

TODO: техника с сайта емакса.

+ + diff --git a/ru/segtree.html b/ru/segtree.html new file mode 100644 index 0000000..1fe8cb7 --- /dev/null +++ b/ru/segtree.html @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + +

Дерево отрезков

+

Замечание. Почти везде мы будем использовать полуинтервалы — обозначаемые как \([l, r)\) — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля.

+

Дерево отрезков — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках.

+

Рассмотрим конкретную задачу:

+
+

Дан массив \(a\) из \(n\) целых чисел, нужно уметь отвечать на запросы двух типов:

+
+
+
    +
  1. Изменить значение в ячейке (т. е. отреагировать на присвоение a[k] = x).
  2. +
  3. Вывести сумму элементов \(a_i\) на отрезке с \(l\) по \(r\).
  4. +
+
+
+

Оба запроса нужно обрабатывать за время \(O(\log n)\).

+
+

Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:

+

Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.

+

Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку \([0, n)\), а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков».

+
+alt text +

alt text

+
+

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

+

Разные свойства

+

Высота такого дерева есть величина \(\Theta(\log n)\): на каждом новом уровне длина отрезка уменьшается вдвое. Этот факт будет ключевым для оценки асимптотики.

+

Более того, любой полуинтервал разбивается на \(O(\log n)\) неперекрывающихся полуинтервалов, соответствующих в вершинам дерева: с каждого уровня нам достаточно не более двух отрезков.

+

Дерево также содержит менее \(2n\) вершин: первый уровень дерева отрезков содержит одну вершину (корень), второй уровень — в худшем случае две вершины, на третьем уровне в худшем случае будет четыре вершины, и так далее, пока число вершин не достигнет \(n\). Таким образом, число вершин в худшем случае оценивается суммой \(n + \frac{n}{2} + \frac{n}{4} + \frac{n}{8} + \ldots + 1 < 2n\). Значит, оно линейное по памяти.

+

При \(n\), отличных от степеней двойки, не все уровни дерева отрезков будут полностью заполнены. Например, при \(n=3\) левый сын корня есть отрезок \([0, 2)\), имеющий двух потомков, в то время как правый сын корня — отрезок \([2, 3)\), являющийся листом.

+

Ок, как это нам поможет?

+

Опишем теперь, как с помощью такой структуры решить задачу.

+

Запрос обновления. Нам нужно обновить значения в вершинах таким образом, чтобы они соответствовали новому значению \(a[k] = x\).

+

Изменим все вершины, в суммах которых участвует \(k\)-тый элемент. Их будет \(\Theta(\log n)\) — по одной с каждого уровня.

+

Это можно реализовать как рекурсивную функцию: ей передаётся текущая вершина дерева отрезков, и эта функция выполняет рекурсивный вызов от одного из двух своих сыновей (от того, который содержит \(k\)-ый элемент в своём отрезке), а после этого — пересчитывает значение суммы в текущей вершине точно таким же образом, как мы это делали при построении дерева отрезков.

+

Запрос суммы. Мы знаем, что во всех вершинах лежат корректные значения.

+

Сделаем тоже рекурсивную функцию, рассмотрев три случая:

+
    +
  • Если отрезок вершины лежит целиком в отрезке запроса, то вернуть записанную в ней сумму.
  • +
  • Если отрезки вершины и запроса не пересекаются, то вернуть 0.
  • +
  • Иначе разделиться рекурсивно на 2 и вернуть сумму этой функции от обоих детей.
  • +
+

Чтобы разобраться, почему это работает за \(O(\log n)\), нужно оценить количество «интересных» отрезков — тех, которые порождают новые вызовы рекурсии. Это будут только те, которые содержат границу запросов — остальные сразу завершатся. Обе границы отрезка содержатся в \(O(\log n)\) отрезках, а значит и итоговая асимптотика будет такая же.

+

Ликбез по C++

+

Наша реализация будет на указателях. Никто не говорит, что она самая лучшая (см. раздел «Другие реализации»), но она самая понятная. Вам может поначалу показаться, что она слишком сложная, но позже вы поймёте её преимущества.

+

Но сначала нам нужно рассказать про объектно-ориентированное программирование и некоторые фишки C++. Если вы их уже знаете, то можете пропускать этот раздел.

+

Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за \(O(\log n)\).

+

В C++ есть два способа объявлять классы (объект — это экземпляр класса): через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

+

У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например конструктор — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса.

+
struct A {
+    int param1, param2; // тут можно что-то хранить
+    char param3 = 'k';
+    A (int var) {
+        // эта часть называется конструктором
+        // ...
+    }
+    void do_something () {
+        // это какой-то другой метод
+        // ...
+    }
+}; // <- не забудьте точку с запятой
+

Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (аллокатор) выделяет место в массиве (оперативной памяти) под этот объект и возвращает позицию (указатель) на место в этом массиве.

+

Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:

+
A x(179);
+x.do_something();
+x.param1 = 57;
+
+A *y = new A(42); // new возвращает адрес, по которому можно найти объект
+y->do_something();
+y.param3 = '!';
+

Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем \(2^{32}\) байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с long long быстрее считались

+

Реализация

+

Общий план реализации любых структур данных: 1. Полностью понять все инварианты — как должна выглядеть структура, какие значения должны принимать поля, etc. 2. Формально описать, что должны делать методы и за какую асимптотику. 3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты.

+
struct segtree {
+    int lb, rb; // левые и правые границы отрезков
+    int sum = 0; // сумма на текущем отрезке
+    segtree *l = 0, *r = 0;
+    segtree (int _lb, int _rb) {
+        lb = _lb, rb = _rb;
+        if (lb + 1 < rb) {
+            // если не лист, создаем детей
+            int t = (lb + rb) / 2;
+            l = new segtree(lb, t);
+            r = new segtree(t, rb);
+        }
+    }
+    void add (int k, int x) {
+        sum += x;
+        if (l) {
+            if (k < l->rb)
+                l->add(k, x);
+            else
+                r->add(k, x);
+        }
+    }
+    int get_sum (int lq, int rq) {
+        if (lb >= lq && rb <= rq)
+            // если мы лежим полностью в отрезке запроса, вывести сумму
+            return sum;
+        if (max(lb, lq) >= min(rb, rq))
+            // если мы не пересекаемся с отрезком запроса, вывести ноль
+            return 0;
+        // иначе всё сложно -- запускаемся от детей и пусть они там сами решают
+        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
+    }
+};
+
+

Посчитать число беспорядков в перестановке из \(n\) элементов (беспорядок или инверсия — это пара чисел \(i < j\), для которых \(p_i > p_j\)).

+
+

Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на \(n\) элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число \(x\), будем делать две вещи: * Запросим сумму от \(k\) до \(n\) в ДО. * Добавим единичку в \(k\)-тую позицию в ДО.

+

Так мы для каждой инверсии учтём её, когда запросим сумму для её правого элемента. Таким образом, мы решили эту задачу за \(O(n \log n)\) запросов.

+
+

Даны \(n\) точек на плоскости с целыми координатами от 1до \(n\). Требуется ответить на \(m\) запросов количества точек на прямоугольнике.

+
+

Ответим на все запросы в оффлайн, используя метод сканирующей прямой:

+
    +
  • Разобьем запросы суммы на прямоугольнике на два запроса суммы на префиксах — сумма на прямоугольнике \([x_1, x_2] \times [y_1, y_2]\) равна сумме на прямоугольнике \([0, x_2] \times [y_1, y_2]\) минус сумма на прямоугольнике \([0, x_1] \times [y_1, y_2]\).
  • +
  • Отсортируем теперь все точки и префиксные запросы по их \(x\). При этом, если у точки и запроса одинаковый \(x\), то точка должна идти раньше.
  • +
  • Пройдёмся по ним в таком порядке и будем решать задачу для одномерной суммы: у нас есть операция «сделать +1 в \(y_i\)» и «вывести сумму с \(y_1\) по \(y_2\)».
  • +
+

Отложенные операции

+

Пусть теперь наш запрос обновления — это присвоение значения \(x\) всем элементам некоторого отрезка \([l, r)\), а не только одному.

+

Мы не хотим спускаться до каждого элемента, где меняется сумма — их может быть очень много. Мы схитрим, и при запросе присваивания будем, по возможности, помечать некоторые вершины, что они и все их дети «покрашены» в какое-то число. Непосредственно спускаться до листьев мы не будем.

+

Например, если пришел запрос «присвой число \(x\) на всем массиве», то мы вообще фактических присвоений делать не будем — только оставим пометку в корне дерева, что оно покрашено.

+

Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.

+

Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за \(O(n \log n)\).

+

При реализации создадим вспомогательную функцию push, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.

+
struct segtree {
+    int lb, rb;
+    int sum = 0, assign = -1;
+    segtree *l = 0, *r = 0;
+    segtree (int _lb, int _rb) {
+        lb = _lb, rb = _rb;
+        if (lb + 1 < rb) {
+            int t = (lb + rb) / 2;
+            l = new segtree(lb, t);
+            r = new segtree(t, rb);
+        }
+    }
+    void push () {
+        if (assign != -1) {
+            sum = (rb-lb) * assign;
+            if (l) { // если дети есть
+                l->assign = assign;
+                r->assign = assign;
+            }
+        }
+        assign = -1;
+    }
+    void upd (int lq, int rq, int x) {
+        push();
+        if (lq <= lb && rb <= rq)
+            assign = x;
+        else if (l && max(lb, lq) < min(rb, rq)) {
+            // если есть дети и отрезок запроса хоть как-то пересекается с нашим
+            l->upd(lq, rq, x);
+            r->upd(lq, rq, x);
+            // ...дальше они сами разберутся
+        }
+    }
+    int get_sum (int lq, int rq) {
+        push();
+        if (lb >= lq && rb <= rq)
+            return sum;
+        if (max(lb, lq) >= min(rb, rq))
+            return 0;
+        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
+    }
+};
+

По-английски эта техника называется lazy propagation. Очень важно научиться её писать — она часто встречается на олимпиадах.

+

Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни.

+

Динамическое построение

+

А что, если у нас все индексы лежать не от в пределах \(10^5\), а, например, \(10^9\). Все асимптотики нас по прежнему устраивают (\(\log_2 10^6 \approx 20\), \(\log_2 10^9 \approx 30\)), кроме этапа построения.

+

Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.

+

Реализовать это можно так же, как и с push-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так.

+
struct segtree {
+    int lb, rb;
+    int sum = 0;
+    segtree *l = 0, *r = 0;
+    segtree (int _lb, int _rb) {
+        lb = _lb, rb = _rb;
+        // а тут ничего нет
+    }
+    void extend () {
+        if (!l && lb + 1 < rb) {
+            int t = (lb + rb) / 2;
+            l = new segtree(lb, t);
+            r = new segtree(t, rb);
+        }
+    }
+    void add (int k, int x) {
+        extend();
+        sum += x;
+        if (l) {
+            if (k < l->rb)
+                l->add(k, x);
+            else
+                r->add(k, x);
+        }
+    }
+    int get_sum (int lq, int rq) {
+        if (lb >= lq && rb <= rq)
+            return sum;
+        if (max(lb, lq) >= min(rb, rq))
+            return 0;
+        extend();
+        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
+    }
+};
+

Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:

+
vector<int> compress (vector<int> a) {
+    vector<int> b = a;
+    sort(b.begin(), b.end());
+    b.erase(unique(b.begin(), b.end()), b.end());
+    for (int &x : a) 
+        x = int(lower_bound(b.begin(), b.end(), x) - b.begin());
+    return a;
+}
+

Персистентность

+

Структуры данных называют персистентными, если их можно быстро «откатить» до произвольного предыдущего состояния.

+

Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент \(t\) никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.

+

У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать \(O(\log n)\) новых вершин на запрос, что означает общее потребление памяти \(O(m \log n)\).

+
struct segtree {
+    int lb, rb;
+    int sum = 0;
+    segtree *l = 0, *r = 0;
+    segtree (int _lb, int _rb) {
+        lb = _lb, rb = _rb;
+        if (lb != rb) {
+            int t = (lb + rb) / 2;
+            l = new segtree(lb, t);
+            r = new segtree(t, rb);
+        }
+    }
+    void copy () {
+        if (l) {
+            l = new segtree(l);
+            r = new segtree(r);
+        }
+    }
+    void add (int k, int x) {
+        copy();
+        sum += x;
+        if (l) {
+            if (k < l->rb) l->add(k, x);
+            else r->add(k, x);
+        }
+    }
+    int get_sum (int lq, int rq) {
+        // этот метод ничего не меняет -- он и так хороший
+        if (lq <= lb && rb <= rq)
+            return sum;
+        if (max(lb, lq) >= min(rb, rq))
+            return 0;
+        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
+    }
+};
+
+

Даны \(n\) точек на плоскости. Нужно в онлайн ответить на \(q\) запросов суммы на прямоугольнике.

+
+

Если бы можно было отвечать в оффлайн, мы бы воспользовались методом сканирующей прямой — но так делать мы не можем. Вместо этого мы будем таким же образом добавлять точки в порядке увеличения \(x_i\) и декомпозировать запрос суммы на два, но при ответе на эти запросы мы будем доставать соответствующую версию ДО, которую мы получили, обработав нужное количество точек. Таким образом, можно отвечать на запросы в онлайн, но с \(O(n \log n)\) памяти.

+
+

Дан отрезок из \(n\) чисел от 1 до \(n\). Требуется ответить на \(q\) запросов \(k\)-той порядковой статистики на подотрезке.

+
+

Сделаем такой стандартный препроцессинг: пройдёмся с персистентным деревом отрезков для суммы по массиву. Когда будем обрабатывать элемент \(k\), добавим единицу к \(k\)-ому элементу.

+

Дальше определим разность деревьев как дерево отрезков, которое соответствует разности массивов. Заметим, что он неотрицательный. Его можно получить неявно, спускаясь одновременно в двух ДО и вместо sum использовать везде sum_r - sum_l.

+

Что будет находиться в разности \(r\)-го и \(l\)-го дерева? Там будут количества вхождений чисел на этом отрезке. В таком ДО не составить труда сделать спуск, который находит последнюю позицию, у которой сумма на соответствующем префиксе не превышает \(k\) — она и будет ответом.

+
+

Дан массив из \(n\) элементов. Требуется ответить на \(m\) запросов, есть ли на отрезке \([l, r]\) доминирующий элемент — тот, который встречается на нём хотя бы \(\frac{r-l}{2}\) раз.

+
+

У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за \(O(\log n)\), посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна \(\frac{1}{2^{100}}\), и ей на практике можно пренебречь.

+

Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее \(\frac{n}{2}\) в массиве на \(n\) элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем true, иначе false.

+

Другие реализации

+

Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:

+

На массивах. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у Емакса.

+

«ДО снизу». Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с CodeForces.

+

Задачи

+ + + diff --git a/ru/sparse-table.html b/ru/sparse-table.html new file mode 100644 index 0000000..efc8cfc --- /dev/null +++ b/ru/sparse-table.html @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + +

Разреженная таблица

+
    +
  • Нужна для нахождения минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) с малой константой.
  • +
  • Обновления не поддерживает (static RMQ).
  • +
  • Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.
  • +
  • Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).
  • +
  • Требует \(O(n \log n)\) памяти.
  • +
  • Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ.
  • +
+

Определим разреженную таблицу как двумерный массив размера \(n \times\log n\):

+

\[ +t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} +\]

+

Идея такая: считаем минимум на каждом отрезке длины \(2^k\).

+

Такой массив можно посчитать за его размер: \(t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])\). Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования.

+

Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.

+
+ + +
+

Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

+
int a[maxn], lg[maxn], mx[maxn][logn];
+
+int rmq (int l, int r) {
+    int t = lg[r-l+1];
+    return min(mx[l][t], mx[r-(1<<t)+1][t]);
+}
+
+// Это считается уже где-то в первых строчках main:
+
+for (int l = 1; l < logn; l++)
+    for (int i = (1<<l); i < maxn; i++)
+        lg[i] = l;
+
+for (int i = n-1; i >= 0; i--) {
+    mx[i][0] = a[i];
+    for (int l = 0; l < logn-1; l++)
+        mx[i][l+1] = max(mx[i][l], mx[i+(1<<l)][l]);
+}
+

2d Static RMQ

+

Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

+

В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

+ + diff --git a/ru/sse.html b/ru/sse.html new file mode 100644 index 0000000..15ebfc8 --- /dev/null +++ b/ru/sse.html @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + +

Streaming SIMD Extensions

+

This is

+
processor   : 0
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 945.615
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 0
+cpu cores   : 6
+apicid      : 0
+initial apicid  : 0
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
!cat /proc/cpuinfo | less -20
+
processor   : 0
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3422.395
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 0
+cpu cores   : 6
+apicid      : 0
+initial apicid  : 0
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 1
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3702.863
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 1
+cpu cores   : 6
+apicid      : 2
+initial apicid  : 2
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 2
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3556.265
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 2
+cpu cores   : 6
+apicid      : 4
+initial apicid  : 4
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 3
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3512.268
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 3
+cpu cores   : 6
+apicid      : 6
+initial apicid  : 6
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 4
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3642.787
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 4
+cpu cores   : 6
+apicid      : 8
+initial apicid  : 8
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 5
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3136.167
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 5
+cpu cores   : 6
+apicid      : 10
+initial apicid  : 10
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 6
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3535.684
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 0
+cpu cores   : 6
+apicid      : 1
+initial apicid  : 1
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 7
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 2251.174
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 1
+cpu cores   : 6
+apicid      : 3
+initial apicid  : 3
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 8
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 2202.563
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 2
+cpu cores   : 6
+apicid      : 5
+initial apicid  : 5
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 9
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 2413.537
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 3
+cpu cores   : 6
+apicid      : 7
+initial apicid  : 7
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 10
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 1974.963
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 4
+cpu cores   : 6
+apicid      : 9
+initial apicid  : 9
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+
+processor   : 11
+vendor_id   : GenuineIntel
+cpu family  : 6
+model       : 158
+model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
+stepping    : 10
+microcode   : 0x96
+cpu MHz     : 3336.891
+cache size  : 9216 KB
+physical id : 0
+siblings    : 12
+core id     : 5
+cpu cores   : 6
+apicid      : 11
+initial apicid  : 11
+fpu     : yes
+fpu_exception   : yes
+cpuid level : 22
+wp      : yes
+flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
+bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips    : 4416.00
+clflush size    : 64
+cache_alignment : 64
+address sizes   : 39 bits physical, 48 bits virtual
+power management:
+ + diff --git a/ru/stress-test.html b/ru/stress-test.html new file mode 100644 index 0000000..074dd67 --- /dev/null +++ b/ru/stress-test.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + +

Стресс-тестирование

+
    +
  • Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений
  • +
  • Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написанно решение на маленькие подгруппы
  • +
+

Суть такая: * Есть решение smart — быстрое, но в котором есть баг, который хотим найти * Пишем решение stupid — медленное, но точно корректное * Пишем генератор gen — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются

+

Примерный код скрипта:

+
import os, sys
+
+f1, f2, gen, iters = sys.argv
+
+for i in range(int(iters)):
+    print('Test', i+1)
+    os.popen('python3 %s > test.txt' % gen)
+    v1 = os.popen('./%s < test.txt' % f1).read()
+    v2 = os.popen('./%s < test.txt' % f2).read()
+    if v1 != v2:
+        print test
+        print("Correct:")
+        print v1
+        print("Wrong:")
+        print v2
+        break
+

Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py.

+

Скрипт написан под Linux. Для Windows нужно убрать «./» во всех системных вызовах.

+

gen.py автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через ./gen. Пример gen-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10:

+
from random import randint, choice
+
+n = randint(1, 10)
+
+print(n)
+
+for _ in range(n):
+    print(choice('abc'), end='')
+ + diff --git a/ru/strings.html b/ru/strings.html new file mode 100644 index 0000000..1252980 --- /dev/null +++ b/ru/strings.html @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + +

Базовые строковые алгоритмы

+

Префикс-функция

+

Рассмотрим задачу, которая возникает каждый раз, когда вы делаете ctrl+f:

+
+

Есть большой текст \(t\). Нужно найти все вхождения строки \(s\) в него.

+
+

Наивное решение со сравнением всех подстрок \(t\) длины \(|s|\) со строкой \(s\) работает за \(O(|t| \cdot |s|)\). Если текст большой, то длинные слова в нем искать становится очень долго.

+

Для решения этой задачи за линейное время придумали префикс-функцию.

+

Определение. Префикс-функцией от строки \(s\) называется массив \(p\), где \(p_i\) равно длине самого большого префикса строки \(s_0 s_1 s_2 \ldots s_i\), который также является и суффиксом этой строки (не считая всю строку).

+

Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна \([0, 1, 0, 1, 2, 3, 4, 5]\).

+
def slow_prefix_function(s):
+    n = len(s)
+    p = [0]*n
+    for i in range(n):
+        prefix = s[:i]
+        for l in range(1, i):
+            if prefix[:l] == prefix[-l:]:
+                p[i] = l
+    return p
+
+slow_prefix_function('aataataa')
+
[0, 0, 1, 0, 1, 2, 3, 4]
+

(Этот алгоритм работает за \(O(n^3)\), но это только пока.)

+

Как это поможет решить исходную задачу?

+

Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.

+

Соединим подстроки \(s\) и \(t\) каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки \(s\#t\).

+
s = "let it go"
+t = """let it go, let it go
+can't hold it back anymore
+let it go, let it go
+turn away and slam the door!"""
+
+print((s + '#' + t).replace('\n', ' '))
+print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))
+
let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!
+00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000
+

Видно, что все места, где значения равны 9 (длине S) — это концы вхождений \(s\) в текст \(t\).

+

Такой алгоритм (посчитать префикс-функцию от \(s\#t\) и посмотреть, в каких позициях она равна \(|s|\)) называется алгоритмом Кнута-Морриса-Пратта.

+

Как её быстро считать

+

Рассмотрим ещё несколько примеров префикс-функций:

+
for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:
+    print(slow_prefix_function(s))
+
[0, 0, 1, 2, 3]
+[0, 0, 0, 0, 0, 0]
+[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]
+[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
+

Можно заметить несколько особенностей: * \(p_0 = 0\) для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; * \(p_{i+1}\) максимум на единицу превосходит \(p_i\): если есть префикс, равный суффиксу строки \(s_0 s_1 \ldots s_{i+1}\) длины \(p_{i+1}\), то, отбросив последний символ, можно получить и правильный суффикс для строки \(s_0 s_1 \ldots s_i\), длина которого будет ровно на единицу меньше.

+

Хочется придумать алгоритм, как считать префикс-функцию за \(O(N)\). Логично это делать с помощью динамики: найти формулу для \(p_i\) через предыдущие значения.

+

Заметим, что \(p_{i+1} = p_i + 1\) в том и только том случае, когда \(s_{p_i} =s_{i+1}\). Например, в строке \(\underbrace{aabaa}t\overbrace{aabaa}\) выделен максимальный префикс, равный суффиксу: \(p_{10} = 5\). Если следующий символ равен будет равен \(t\), то \(p_{11} = p_{10} + 1 = 6\).

+

Но что происходит, когда \(s_{p_i}\neq s_{i+1}\)? Пусть следующий символ в этой же строке равен \(b\). * \(\implies\) Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. * \(\implies\) Значит, помимо того, что он является суффиксом «aabaab», префикс является префиксом и подстроки «aabaa». * \(\implies\) Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть \(p_4 = 2\). * \(\implies\) Если \(s_2 = s_{11}\) (т. е. новый символ совпадает с идущим после префикса-кандидата), то \(p_{11} = p_2 + 1 = 2 + 1 = 3\).

+

В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — \(p_{p_{p_i}}\). Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым.

+
def fast_prefix_function(s):
+    n = len(s)
+    p = [0]*n
+    for i in range(1, n):
+        cur = p[i - 1]
+        # перебираем префикс-функцию, пока не найдем равный символ
+        while s[i] != s[cur] and cur > 0:
+            cur = p[cur - 1]
+         # если нашли, то значение на единицу больше
+        if s[i] == s[cur]:
+            p[i] = cur + 1
+    return p
+
+fast_prefix_function('abacabadabacabax')
+
[0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]
+

Асимптотика

+

Почему это работает за \(O(N)\)? В худшем случае этот while может работать \(O(N)\) раз за одну итерацию. Но оказывается, что в среднем каждый while работает за \(O(1)\) — это называется амортизированной асимптотикой.

+

Доказательство: * Как мы ранее заметили, префикс функция возрастает максимум на единицу. * \(\implies\) Вырасти она может максимум \(n-1\) раз. * Если мы зашли в while, то каждая его итерация понижает значение префикс-функции хотя бы на один. * \(\implies\) Количество понижений не больше количества повышений, то есть \(O(n)\). * \(\implies\) Суммарно итераций цикла while\(O(n)\).

+

Z-функция

+

Альтернатива префикс-функции — z-функция (примечание: не «зи», а «зет»). Она немного проще для понимания.

+

Z-функция от строки \(s\) — это такой массив \(z\), что \(z_i\) равно длине максимальной подстроки, начинающейся с \(i\)-й позиции, которая равна префиксу \(s\).

+

\[\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)\]

+
def slow_z_function(s):
+    n = len(s)
+    z = [0]*n
+    for i in range(1, n):
+        suffix = s[i:]
+        for l in range(1, i):
+            if s[:l] == suffix[:l]:
+                z[i] = l
+    return z
+
+for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:
+    print(slow_z_function(s))
+
[0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]
+[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]
+[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]
+

Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c \(s\), а не заканчиваться. Осталось научиться её искать за \(O(n)\).

+

Как её быстро считать

+

Заметим, что: * \(z_0 = 0\) — из-за договоренности (потому что информации не несет); * \(z_i = 0 \iff s_i \neq s_0\); * \(z_i > 0 \iff s_i = s_0\).

+

Будем идти слева направо и хранить z-блок — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как \(l\) и \(r\).

+

Пусть мы сейчас хотим найти \(z_i\), а все предыдущие уже нашли. Если новый, \(i\)-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. * Если правее, то мы просто наивно перебором найдем \(z_i\) (максимальный отрезок, начинающийся с \(s_i\) и равный префиксу), и объявим его новым z-блоком. * Если \(i\)-й элемент лежит внутри z-блока, то мы можем посмотреть на значение \(z_{i-l}\) и использовать его, чтобы инициализировать \(z_i\) чем-то, возможно, отличным от нуля. Если \(z_{i-l}\) «не хватает» до границы \(z\)-блока, то \(z_i = z_{i-l}\). Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку.

+
def fast_z_function(s):
+    n = len(s)
+    z = [0]*n
+    l = 0
+    r = 0
+    for i in range(1, n):
+        if i <= r:
+            z[i] = min(r - i + 1, z[i-l])
+        while i + z[i] < n and s[z[i]] == s[i+z[i]]:
+            z[i] += 1
+        if i + z[i] - 1 > r:
+            l = i
+            r = i +  z[i] - 1
+    return z
+

Асимптотика. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это \(O(n)\).

+

Зачем тогда люди используют префикс-функцию

+
+hz +

hz

+
+

В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.

+

Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение.

+ + diff --git a/ru/suffix-array.html b/ru/suffix-array.html new file mode 100644 index 0000000..ff15d94 --- /dev/null +++ b/ru/suffix-array.html @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + +

Суффиксный массив

+

Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.

+

Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.

+

+* +
Паблик с тупыми шутками про проганье» +
+
    +
  • +
+

Мотивация

+

Суффиксным массивом строки \(s\) называется перестановка индексов начал её суффиксов, которая задаёт их порядок в порядке лексикографической сортировки. Иными словами, чтобы его построить, нужно выполнить сортировку всех суффиксов заданной строки.

+

+

Как это использовать. Пусть вы решили основать ООО «Ещё Один Поисковик», и чтобы получить финансирование, вы хотите сделат хоть что-то минимально работающие — просто научиться искать по ключевому слову документы, включающие его, а также позиции их вхождения (в 90-е это был бы уже довольно сильный MVP). Простыми алгоритмами (полиномиальными хэшами, z- и префикс-функцией и даже Ахо-Корасиком) это сделать быстро нельзя, а суффиксными структурами — можно.

+

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

+

Работать такой алгоритм будет за \(O(|t| \log |s|)\), и позже это можно будет оптимизировать до \(O(|t| + \log |s|)\), что является одним из самых оптимальных алгоритмов поиска.

+

Теперь научимся его строить.

+

Построение за \(O(n \log n)\)

+

Для удобства мы допишем в конец строки какой-нибудь символ, который лексикографически меньше любого другого, и будем выполнять сортировку не суффиксов, а циклических сдвигов. Строки мы, соответственно, тоже будем рассматривать циклические. Для стандартной ASCII обычно выбирают либо «$» либо «#», либо просто нулевой символ, если это c-string (в языке Си все строки и так заканчиваются нулевым символом). Легко убедиться, что сортировка таких циклических сдвигов эквивалентна сортировке суффиксов — можно просто убрать всё, что идёт после доллара.

+

Мы могли бы просто взять перестановку от \(0\) до \(n\), написать компаратор, который сравнивает соответствующие суффиксы, и скормить это в std::sort, что будет работать за \(O(n^2 \log n)\), потому что внутреннее сравнение работает за \(O(n)\). Однако, если сравнивать суффиксы хэшами, то уже тут можно получить \(O(n \log^2 n)\). Но это не самый быстрый и удобный алгоритм.

+

Наш алгоритм будет состоять из \(\lceil \log n \rceil\) этапов. На \(k\)-том этапе мы будем рассматривать циклические подстроки длины \(2^k\). На последнем этапе мы отсортируем строки длины \(\geq n\) (это легально — они ведь циклические), и мы получим нужный суффиксный массив.

+

Заметим, что, в отличие сортировки суффиксов, сортировка подстрок не всегда однозначная — они могут быть одинаковыми. Поэтому на каждой фазе алгоритм помимо перестановки \(p\) индексов циклических подстрок мы будем поддерживать для каждой циклической подстроки, начинающейся в позиции \(i\) с длиной 2^k, номер \(c_i\) класса эквивалентности, которому эта подстрока принадлежит (давать их будем таким образом, чтобы они сохраняли порядок: меньшим подстрокам соответствуют меньшие \(c_i\)). Количество классов эквивалентности будем хранить в переменной cls (изначально она равна количеству различных символов).

+

Пример: \(s = aaba\). Этапов будет 3: для подстрок длины 1, 2 и 4.

+

\[ + p_0 = (0, 1, 3, 2) \;\;\; c_0 = (0, 0, 1, 0) +\\ p_1 = (0, 3, 1, 2) \;\;\; c_1 = (0, 1, 2, 0) +\\ p_2 = (3, 0, 1, 2) \;\;\; c_2 = (1, 2, 3, 0) +\]

+

Как настоящие программисты, мы нумеруем этапы с нуля, поэтому на нулевом этапе мы отсортируем строки длины \(2^0 = 1\), то есть просто символы. Это мы сделаем сортировкой подсчётом.

+

Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней std::sort со своим компаратором.

+

Как быстро сравнить две подстроки? Мы можем использовать \(c_i\) — каждой строке длины \(2^k\) сопоставить биграмму (строку из двух символов), а именно строка \(s[i..i+2^k-1]\) с точки зрения сортировки будет эквивалентна паре \((c_i, c_{i+2^{k-1}})\). Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за \(O(1)\). Однако, это всё ещё будет работать за \(O(n \log^2 n)\), потому что каждый этап будет работать за \(O(n \log n\)).

+

Оптимизация до \(O(n \log n)\). Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся цифровой сортировкой — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив \(p\) с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в \(p\) отнять \(2^{k-1}\). Таким образом, можно проводить этап за \(O(n)\).

+
// строка -- это последовательность чисел от 1 до размера алфавита
+vector<int> suffix_array (vector<int> &s) {
+    s.push_back(0);  // добавляем нулевой символ в конец строки
+    int n = (int) s.size(),
+        cnt = 0,  // вспомогательная переменная: счётчик для сортировки 
+        cls = 0;  // количество классов эквивалентности
+    vector<int> c(n), p(n);
+    
+    map< int, vector<int> > t;
+    for (int i = 0; i < n; i++)
+        t[s[i]].push_back(i);
+    
+    // «нулевой» этап
+    for (auto &x : t) {
+        for (int u : x.second)
+            c[u] = cls, p[cnt++] = u;
+        cls++;
+    }
+    
+    // пока все суффиксы не стали уникальными
+    for (int l = 1; cls < n; l++) {
+        vector< vector<int> > a(cls);  // массив для сортировки подсчётом
+        vector<int> _c(n);  // новые классы эквивалентности
+        int d = (1<<l)/2;
+        int _cls = cnt = 0;  // новое количество классов
+        
+        for (int i = 0; i < n; i++) {
+            int k = (p[i]-d+n)%n;
+            a[c[k]].push_back(k);
+        }
+        
+        for (int i = 0; i < cls; i++) {
+            for (size_t j = 0; j < a[i].size(); j++) {
+                // если суффикс начинает новый класс эквивалентности
+                if (j == 0 || c[(a[i][j]+d)%n] != c[(a[i][j-1]+d)%n])
+                    _cls++;
+                _c[a[i][j]] = _cls-1;
+                p[cnt++] = a[i][j];
+            }
+        }
+        
+        c = _c;
+        cls = _cls;
+    }
+    
+    return vector<int>(p.begin()+1, p.end());
+}
+

TODO: переписать это

+

Наибольшие общие префиксы

+

Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы делали с хэшами.

+

TODO

+ + diff --git a/ru/treap.html b/ru/treap.html new file mode 100644 index 0000000..9e24716 --- /dev/null +++ b/ru/treap.html @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + +

Декартово дерево

+

Рене Декарт (фр. René Descartes) — великий французский математик и философ XVII века.

+

Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.

+

Декартово дерево же определяется и строится так:

+
    +
  • Нанесём на плоскость набор из \(n\) точек. Их \(x\) зачем-то назовем ключем, а \(y\) приоритетом.
  • +
  • Выберем самую верхнюю точку (с наибольшим \(y\), а если таких несколько — любую) и назовём её корнем.
  • +
  • От всех вершин, лежащих слева (с меньшим \(x\)) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.
  • +
  • Аналогично, запустимся от правой части и добавим корню правого сына.
  • +
+

Заметим, что если все \(y\) и \(x\) различны, то дерево строится однозначно.

+

Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:

+
+treap +

treap

+
+

Таким образом, декартово дерево — это одновременно бинарное дерево по \(x\) и куча по \(y\). Поэтому ему придумали много альтернативных названий:

+
    +
  • Дерамида (дерево + пирамида)
  • +
  • ПиВо (пирамида + дерево)
  • +
  • КуРево (куча + дерево)
  • +
  • Treap (tree + heap)
  • +
+

## Бинарные деревья

+

С небольшими модификациями, декартово дерево умеет всё то же, что и любое бинарное дерево поиска, например:

+
    +
  • добавить число \(x\) в множество
  • +
  • определить, есть ли в множестве число \(x\)
  • +
  • найти первое число, не меньшее \(x\) (lower_bound)
  • +
  • найти количество чисел в промежутке \([l, r]\)
  • +
+

При этом все операции — за \(O(\log n)\).

+

На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до \(O(\log n)\).

+

Приоритеты и асимптотика

+

В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты (\(y\)) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска.

+

Теорема. Ожидание глубины вершины в декартовом дереве равно \(O(n \log n)\).

+

Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.

+

Доказательство. Введем функцию \(a(x, y)\) равную единице, если \(x\) является предком \(y\), и нулем в противном случае. Такие функции называются индикаторами*.

+

Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна

+

\[d_i = \sum_{j=1}^n a(j, i)\]

+

Её матожидание равно

+

\[E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)\]

+

где \(p(x, y)\) это веряотность, что \(a(x, y) = 1\). Здесь мы воспользовались важным свойством линейности: матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.

+

Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.

+

Лемма. Вершина \(x\) является предком \(y\), если у неё приоритет больше, чем у всех вершин из отрезка \((x, y]\) (без ограничения общности, будем считать, что \(x < y\)).

+

Необходимость. Если это не так, то где-то между \(x\) и \(y\) есть вершина с большим приоритетом, чем \(x\). Она не может быть потомком \(x\), а значит \(x\) и \(y\) будут разделены.

+

Достаточность. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком \(x\). Таким образом, всё, что справа от \(y\), ни на что влиять не будет.

+

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

+

\[p(x, y) = \frac{1}{y-x+1}\]

+

Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:

+

\[E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)\]

+

Перед последним переходом мы получили сумму гармонического ряда.

+

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

+

Упражнение*. Выведите из этого доказательства асимптотику quicksort.

+

Реализация

+

Декартово дерево удобно писать на указателях и структурах. Поэтому мы рассказали дерево отрезков на указателях, а не стандартную рекурсию на 5 параметрах. Не знаете, что это такое — посмотрите в предыдущем конспекте.

+

Создадим структуру Node, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву.

+
struct Node {
+    int key, prior;
+    Node *l = 0, *r = 0;
+    Node (int _key) { key = _key, prior = rand(); }
+};
+

Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся.

+

Merge

+

Принимает два дерева (два корня, \(L\) и \(R\)), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.

+

Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень \(L\) или правый \(R\) — просто возьмем тот, у кого приоритет больше.

+

Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном \(L\). С правым сыном сложнее: возможно, его нужно смерджить с \(R\). Поэтому рекурсивно сделаем merge(l->r, r) и запишем результат в качестве правого сына.

+
Node* merge (Node *l, Node *r) {
+    if (!l) return r;
+    if (!r) return l;
+    if (l->prior > r->prior) {
+        l->r = merge(l->r, r);
+        return l;
+    }
+    else {
+        r->l = merge(l, r->l);
+        return r;
+    }
+}
+

Split

+

Принимает дерево и ключ \(x\), по которому его нужно разделить на два: \(L\) должно иметь все ключи не больше \(x\), а \(R\) должно иметь все ключи больше \(x\).

+

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

+
typedef pair<Node*, Node*> Pair;
+
+Pair split (Node *p, int x) {
+    if (!p) return {0, 0};
+    if (p->key <= x) {
+        Pair q = split(p->r, x);
+        p->r = q.first;
+        return {p, q.second};
+    }
+    else {
+        Pair q = split(p->l, x);
+        p->l = q.second;
+        return {q.first, p};
+    }
+}
+

Пример: вставка

+

merge и split сами по себе не очень полезные, но помогут написать все остальное.

+

Вот так, например, будет выглядеть код, добавляющий \(x\) в сет.

+
Node *root = 0;
+
+void insert (int x) {
+    Pair q = split(root, x);
+    Node *t = new Node(x);
+    root = merge(q.first, merge(t, q.second));
+}
+

Пример: модификация для суммы на отрезке

+

Иногда нам нужно написать какие-то модификации для более продвинутых операций.

+

Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке».

+
struct Node {
+    int val, sum;
+    // ...
+};
+

При merge и split надо будет поддерживать эту сумму актуальной.

+

Вместо того, чтобы модифицировать и merge, и split под наши хотелки, напишем вспомогательные функцию upd, которую будем вызывать при обновлении детей вершины.

+
void sum (Node* v) { return v ? v->sum : 0; }
+// обращаться по пустому указателю нельзя -- выдаст ошибку
+
+void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; }
+

В merge и split теперь можно просто вызывать upd перед тем, как вернуть вершину, и тогда ничего не сломается:

+
Node* merge (Node *l, Node *r) {
+    // ...
+    if (...) {
+        l->r = merge(l->r, r);
+        upd(l);
+        return l;
+    }
+    else {
+        // ...
+    }
+}
+
typedef pair<Node*, Node*> Pair;
+
+Pair split (Node *p, int x) {
+    // ...
+    if (...) {
+        // ...
+        upd(p);
+        return {p, q.second};
+    }
+    else {
+        // ...
+    }
+}
+

Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:

+
int sum (int l, int r) {
+    Pair rq = split(root, r);
+    Pair lq = split(rq.first, l);
+    int res = sum(lr.second);
+    root = merge(lq.first, merge(lq.second, rq.second));
+    return res;
+}
+

Неявный ключ

+

Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.

+

Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.

+

Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:

+
struct Node {
+    int key, prior, size = 1;
+    //              ^ размер поддерева
+    Node *l = 0, *r = 0;
+    Node (int _key) { key = _key, prior = rand(); }
+};
+

Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины.

+
int size (Node *v) { return v ? v->size : 0; }
+
+void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); }
+

merge не меняется, а вот в split нужно использовать позицию корня вместо его ключа.

+

Про split теперь удобнее думать как "вырежи первые k элементов".

+
typedef pair<Node*, Node*> Pair;
+
+Pair split (Node *p, int k) {
+    if (!p) return {0, 0};
+    if (size(p->l) + 1 <= k) {
+        Pair q = split(p->r, k - size(p->l) - 1);
+        //                   ^ правый сын не знает количество вершин слева от него
+        p->r = q.first;
+        upd(p);
+        return {p, q.second};
+    }
+    else {
+        Pair q = split(p->l, k);
+        p->l = q.second;
+        upd(p);
+        return {q.first, p};
+    }
+}
+

Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно.

+

Пример: ctrl+x, ctrl+v

+
Node* ctrlx (int l, int r) {
+    Pair q1 = split(root, r);
+    Pair q2 = split(q1.first, l);
+    root = merge(q2.first, q1.second);
+    return q2.second;
+}
+
void ctrlv (Node *v, int k) {
+    Pair q = split(root, k);
+    root = merge(q.first, merge(v, q.second));
+}
+

Пример: переворот

+

Нужно за \(O(\log n)\) обрабатывать запросы переворота произвольных подстрок: значение \(a_l\) поменять с \(a_r\), \(a_{l+1}\) поменять с \(a_{r-1}\) и т. д.

+

Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:

+
struct Node {
+    bool rev;
+    // ...
+};
+

Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:

+
void push (node *v) {
+    if (v->rev) {
+        swap(v->l, v->r);
+        if (v->l)
+            v->rev ^= 1;
+        if (v->r)
+            v->rev ^= 1;
+    }
+    v->rev = 0;
+}
+

Аналогично, эту функцию будем вызывать в начале merge и split.

+

Саму функцию reverse реализуем так: вырезать нужный отрезок, поменять флаг.

+
void reverse (int l, int r) {
+    Pair q1 = split(root, r);
+    Pair q2 = split(q1.first, l)
+    q2.second->rev ^= 1;
+    root = merge(q2.first, merge(q2.second, q1.second));
+}
+

Функциональное программирование*

+

Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с \(l\) по \(r\), что-то с ним делаем и склеиваем обратно.

+

Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке.

+
auto apply (int l, int r, auto f) {
+    Pair q1 = split(root, r);
+    Pair q2 = split(q1.first, l)
+    q2.second = f(q2.second);
+    root = merge(q2.first, merge(q2.second, q1.second));
+}
+
+void reverse (Node *v) {
+    if (v)
+        v->rev ^= 1;
+}
+

Применять её нужно так:

+
apply(l, r, reverse);
+

Это работает в плюсах, начиная с g++14.

+

Для простых операций можно даже написать лямбду:

+
apply(l, r, [](Node *v){
+    if (v)
+        v->rev ^= 1;
+});
+

Персистентность*

+

Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.

+
+

Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.

+
+

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

+
+

Дана строка. Требуется выполнять в ней копирования, удаления, вставки в произвольные позиции и сравнение произвольных подстрок.

+
+

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

+

Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ.

+

Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию copy:

+
Node* copy (Node *v) { return new Node(*v); }
+

Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный split начнётся так:

+
Pair split (Node *p, int x) {
+    p = copy(p);
+    // ...
+}
+

В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.

+

У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен \(L\), а размер правого \(R\), то будем подвешивать за левое с вероятностью \(\frac{L}{L+R}\), иначе за правое.

+

Теорема. Такое переподвешивание эквивалентно приоритетам.

+

Доказательство. Покажем, что все вершины всё так же имеют равную вероятность быть корнем. Докажем по индукции:

+
    +
  • Лист имеет вероятность 1 быть корнем себя (база индукции)
  • +
  • Переход индукции — операция merge. Любая вершина левого дерева была корнем с вероятностью \(\frac{1}{L}\) (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью \(\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}\). С вершинами правого дерева аналогично.
  • +
+

Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД.

+
Node* merge (Node *l, Node *r) {
+    if (!l) return r;
+    if (!r) return l;
+    l = copy(l), r = copy(r);
+    if (rand() % (size(l) + size(r)) < size(l)) {
+        // ...
+    }
+    else {
+        // ...
+    }
+}
+

Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и \(x\), и \(y\)?

+ + diff --git a/ru/trie.html b/ru/trie.html new file mode 100644 index 0000000..0a97980 --- /dev/null +++ b/ru/trie.html @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + +

Бор

+

Бор — это структура данных для компактного хранения строк.

+

Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин.

+
+trie +

trie

+
+

Бор можно удобно использовать для разных задач: * Хранение строк — занимает гораздо меньше места, чем массив или сет строк. * Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.

+
    +
  • Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи.
  • +
+

Реализация

+

Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: * терминальная ли вершина, * ссылки на детей, * возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет.

+
const int k = 26;
+
+struct Vertex {
+    Vertex* to[k] = {0};
+    bool terminal = 0;
+};
+
+Vertex *root = new Vertex();
+

Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной.

+
void add_string (string &s) {
+    v = root;
+    for (char c : s) {
+        c -= 'a';
+        if (!v->to[c]) 
+            v->to[c] = new Vertex();
+        v = v->to[c];
+    }
+    v->terminal = true;
+}
+

Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.

+

Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности.

+

Как хранить ссылки

+

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

+

В этом случае можно придумать какой-нибудь другой способ хранить отображение из символа в ссылку на вершину, например бинарном дереве (map) или хэш-таблице (unordered_map). Они будут работать дольше (но лишь в константу раз), но зато потребление памяти в них будет линейным. У map-а есть ещё одно преимущество, что он хранит ссылки уже отсортированными по символам — так можно отсортировать строки, например.

+

Учитывайте, что писать бор можно по-разному, особенно когда решаете задачи с жестокими ограничениями.

+

Суффиксные ссылки

+
+

Пусть заданы \(n\) плохих слов и большой текст \(t\). Нужно найти суммарное количество их вхождений в этот текст.

+
+

Эту и много других задач помогают решать суффиксные ссылки. Суффиксная ссылка для вершины \(v\) — это вершина, которой соответствует наидлиннейший суффикс строки, соответствующей вершине \(v\), и присутствующий в боре. Будем считать, что мы их умеем быстро находить.

+

Добавим все плохие слова в бор. Будем считывать строку и с помощью суффиксных ссылок поддерживать самый длинный суффикс текущей строки, который принимает бор. Тогда, для конкретной позиции, мы можем быстро посчитать, какие плохие слова на нём заканчиваются — ровно те, до которых можно дойти по суффиксным ссылкам (по определению, суффиксная ссылка ведёт в наидлиннейший суффикс, присутствующий в боре). Информацию о количестве таких слов можно посчитать заранее динамикой в графе из суффиксных ссылок.

+

Алгоритм Ахо-Корасик позволяет строить суффиксные ссылки для произвольного бора за \(O(nk)\), где \(n\) и \(k\) это суммарный размер строк и размер алфавита соответственно. Его описание вынесенов в отдельную статью.

+ + From 92f96dfe3461b604b5bcf0e5aaa7d80c25130edc Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 7 Jun 2019 23:38:08 +0000 Subject: [PATCH 023/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- 404.html | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 404.html diff --git a/404.html b/404.html new file mode 100644 index 0000000..76b2739 --- /dev/null +++ b/404.html @@ -0,0 +1,11 @@ + + + 404 — Not found + + +
+

404

+

The article you were trying to find does not seem to exist yet.

+

If you got here by following some link on Algorithmica, we would really appreciate if you helped us fix it.

+
+ From 36af0c7cf29a6175c54f4d233b7a55355cc2d988 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 8 Jun 2019 14:14:46 +0000 Subject: [PATCH 024/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 4 ++++ ru/aho-corasick.html | 5 ++++- ru/annealing.html | 5 ++++- ru/bayans.html | 5 ++++- ru/bitset.html | 5 ++++- ru/centroid.html | 5 ++++- ru/convex-hulls.html | 5 ++++- ru/cross-entropy.html | 5 ++++- ru/cuda.html | 5 ++++- ru/dp-intro.html | 5 ++++- ru/dp-optimizations.html | 5 ++++- ru/dp_intro.html | 5 ++++- ru/dp_optimizations.html | 5 ++++- ru/fenwick.html | 5 ++++- ru/games.html | 5 ++++- ru/geometry.html | 5 ++++- ru/hashing.html | 5 ++++- ru/hld.html | 5 ++++- ru/index.html | 5 ++++- ru/ioi.html | 5 ++++- ru/karatsuba.html | 5 ++++- ru/lca.html | 5 ++++- ru/linalg.html | 5 ++++- ru/matching.html | 5 ++++- ru/matroid.html | 5 ++++- ru/mincost-maxflow.html | 5 ++++- ru/mst.html | 5 ++++- ru/pollard.html | 5 ++++- ru/probability.html | 5 ++++- ru/reciprocal.html | 5 ++++- ru/segtree.html | 38 +++++++++++++++++++++++++------------- ru/sparse-table.html | 5 ++++- ru/sse.html | 5 ++++- ru/stress-test.html | 5 ++++- ru/strings.html | 5 ++++- ru/suffix-array.html | 5 ++++- ru/treap.html | 5 ++++- ru/trie.html | 5 ++++- 38 files changed, 173 insertions(+), 49 deletions(-) diff --git a/pandoc.css b/pandoc.css index 5cebc56..122a81b 100644 --- a/pandoc.css +++ b/pandoc.css @@ -55,6 +55,10 @@ img { max-width: 100%; } +.title { + display: none; +} + h1, h2, h3, h4, h5, h6 { color: #111; line-height: 125%; diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html index 7da3680..664b754 100644 --- a/ru/aho-corasick.html +++ b/ru/aho-corasick.html @@ -4,7 +4,7 @@ - + Алгоритм Ахо-Корасик @@ -27,6 +27,9 @@ +

Задачи на многоугольники

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

diff --git a/ru/cross-entropy.html b/ru/cross-entropy.html index 5d7af41..581d650 100644 --- a/ru/cross-entropy.html +++ b/ru/cross-entropy.html @@ -4,7 +4,7 @@ - + Почему машинное обучение работает @@ -27,6 +27,9 @@ +

Почему машинное обучение работает

b

Принцип максимального правдоподобия —

diff --git a/ru/cuda.html b/ru/cuda.html index 189eba6..6ef5b53 100644 --- a/ru/cuda.html +++ b/ru/cuda.html @@ -4,7 +4,7 @@ - + Supercomputing for the Masses @@ -27,6 +27,9 @@ +

Динамическое программирование

Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index 40606e7..ff1c68a 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -4,7 +4,7 @@ - + # Оптимизации динамики @@ -27,6 +27,9 @@ +

Динамическое программирование

test123

diff --git a/ru/dp_optimizations.html b/ru/dp_optimizations.html index 40606e7..ff1c68a 100644 --- a/ru/dp_optimizations.html +++ b/ru/dp_optimizations.html @@ -4,7 +4,7 @@ - + # Оптимизации динамики @@ -27,6 +27,9 @@ +

Алгоритмика

Структуры данных

    diff --git a/ru/ioi.html b/ru/ioi.html index 1a130a8..4656c3e 100644 --- a/ru/ioi.html +++ b/ru/ioi.html @@ -4,7 +4,7 @@ - + IOI Upsolving Challenge @@ -27,6 +27,9 @@ +

    Быстрое умножение

    В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).

    Андрей Колмогоров и несколько других пионеров компьютер саенса выдвинули «гипотезу \(n^2\)» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

    diff --git a/ru/lca.html b/ru/lca.html index f8c14b3..7fee799 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -4,7 +4,7 @@ - + Корневые деревья - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + - + + + + + Algorithmica + + + + + + diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html index aaa5a8c..f537837 100644 --- a/ru/aho-corasick.html +++ b/ru/aho-corasick.html @@ -45,8 +45,8 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - + - - - - - - - - - - -

    Почему машинное обучение работает

    -

    b

    -

    Принцип максимального правдоподобия —

    -

    Функция правдоподобия

    -

    \[ \hat{\theta} = \argmax_\theta P(X | \theta) \]

    -

    Принцип максимального правдоподобия

    -

    Логистическая регрессия

    -

    Нормальное распределение и MSE

    -

    Почему сигмоида

    - - diff --git a/ru/dp-intro.html b/ru/dp-intro.html deleted file mode 100644 index 4a4a2b3..0000000 --- a/ru/dp-intro.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - Динамическое программирование - - - - - - - - - - - - -

    Динамическое программирование

    -
    -

    Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

    -
    -

    Разберем некоторые классические идеи и задачи в динамическом программировании.

    -

    Рюкзак

    -
    -

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

    -
    -

    Наибольшая общая подпоследовательность

    -
    -

    Есть две строки. Найти длину их наибольшей общей подпоследовательности.

    -
    -

    Расстановка скобок

    -
    -

    Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

    -
    -

    Можно завести динамику «можно ли ».

    -

    Наибольшая возрастающая подпоследовательность

    -

    Есть также алгоритм за \(O(n \log n)\).

    -

    Мемоизация

    -

    Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

    -

    Первый предполагает

    -

    Число разбиений

    -
    - -
    -

    Замена параметра на ответ

    -
    - -
    - - diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index 2523b32..e940d02 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -4,7 +4,7 @@ - # Оптимизации динамики + Оптимизации динамики - - - - - - - - - - - -

    Динамическое программирование

    -

    test123

    -
    -

    Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.

    -
    -

    Разберем некоторые классические идеи и задачи в динамическом программировании.

    -

    Рюкзак

    -
    -

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

    -
    -

    Наибольшая общая подпоследовательность

    -
    -

    Есть две строки. Найти длину их наибольшей общей подпоследовательности.

    -
    -

    Расстановка скобок

    -
    -

    Имеется какое-то арифметическое выражение из чисел и знаков умножения и сложения. Например, «\(2+2\times2\)». Нужно задать порядок операций (т. е. расставить в нём скобки), чтобы его максимизировать.

    -
    -

    Можно завести динамику «можно ли ».

    -

    Наибольшая возрастающая подпоследовательность

    -

    Есть также алгоритм за \(O(n \log n)\).

    -

    Мемоизация

    -

    Есть два подхода к подсчету динамики: итеративный и рекурсивный. Почти всегда они взаимозаменимы.

    -

    Первый предполагает

    -

    Число разбиений

    -
    - -
    -

    Замена параметра на ответ

    -
    - -
    - - diff --git a/ru/dp_optimizations.html b/ru/dp_optimizations.html deleted file mode 100644 index 2523b32..0000000 --- a/ru/dp_optimizations.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - # Оптимизации динамики - - - - - - - - - - - - - -

    Оптимизации динамики

    -
      -
    • Разделяй и властвуй
    • -
    • Оптимизация Кнута
    • -
    • Convex Hull Trick
    • -
    • Лямбда-оптимизация
    • -
    -

    Задача

    -
    -

    Даны \(n\) точек на прямой. Нужно найти \(m\) отрезков, покрывающих все точки, минимизировав при этом сумму квадратов их длин.

    -
    -

    Базовое решение — следующая динамика: \(f[i, j]\) — минимальная стоимость покрытия \(i\) первых (самых левых) точек, используя не более \(j\) отрезков (итоговый ответ будет записан в \(f[n, m]\) — прим. К. О.).

    -

    Переход — перебор всех возможных последних отрезков, то есть \(f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \}\).

    -
    // x[] — отсортированный массив координат точек, нумерация с нуля
    -
    -// квадрат длины отрезка от i-той до j-той точки
    -int cost(int i, int j) { return (x[j]-x[i])*(x[j]-x[i]); }
    -
    -// TODO: предподсчитать cost
    -
    -for (int i = 0; i <= m; i++)
    -    f[0][k] = 0; // если нам не нужно ничего покрывать, то всё и так хорошо
    -// все остальные f предполагаем равными бесконечности
    -
    -for (int i = 1; i <= n; i++)
    -    for (int j = 1; j <= m; j++)
    -        for (int k = 0; k < i; k++)
    -            f[i][j] = min(f[i][j], f[k][j-1] + cost(k, i-1));
    -

    Заметим, что циклы по i и j можно поменять местами.

    -

    Такое решение пока работает за \(O(n^2 m)\) — для каждого состояния линейный перебор. Сейчас мы покажем 4 разных метода, как его ускорить.

    -

    Разделяй-и-властвуй

    -

    Обозначим за \(opt[i, j]\) оптимальный \(k\), на котором \(f[i, j]\) минимизируется. Для однозначности, из всех оптимальных индексов веберем набольший.

    -

    Утверждение. \(opt[i, j] \leq opt[i, j+1]\).

    -

    Интуиция такая: чем меньше доступно отрезков, тем больше нам нужно делать самый последний отрезок.

    -

    Что это нам даёт? Если мы уже знаем \(opt[i, l]\) и \(opt[i, r]\) и хотим посчитать \(opt[i, j]\) для какого-то \(j\) между \(l\) и \(r\), то мы можем сузить отрезок поиска оптимального индекса с \([0, i-1]\) до \([opt[i, l], opt[i][r]]\).

    -

    Давайте делать следующее: заведем рекурсивную функцию, которая считает динамики для отрезка \([l, r]\), зная, что их \(opt\)-ы лежат между \(l'\) и \(r'\). Она берет середину отрезка \([l, r]\) и линейным проходом считает ответ для неё, а затем просто спускается дальше рекурсивно.

    -
    void solve (int l, int r, int _l, int _r, int k) {
    -    if (l > r) return; // отрезок пустой — выходим
    -    int t = (l + r) / 2;
    -    int opt = _l;
    -    for (int i = max(t, _l); i <= _r; i++) { // TODO: неправильные границы 
    -        int val = f[i+1][k-1] + cost(i, j);
    -        if (val < f[t][k])
    -            f[t][k] = val, opt = i;
    -    }
    -    solve(l, t-1, _l, opt, k);
    -    solve(t+1, r, opt, _r, k);
    -}
    -

    Вызываться она будет просто последовательно для каждого слоя:

    -
    for (int k = 1; k <= m; k++)
    -    solve(0, n-1, 0, n-1, k);
    -

    Теперь пересчет одного «слоя» динамики занимает \(O(n \log n)\) вместо \(O(n^2)\). Почему? Потому что каждый раз рекурсивная функция уменьшает в два раза хотя бы один из отрезков, так что её глубина будет \(O(\log n)\), а значит и каждый элемент будет просмотрен не более \(O(\log n)\) раз.

    -

    Получается, что асимптотика улучшилась до \(O(n m \log n)\).

    -

    Оптимизация Кнута

    -

    Предыдущий метод основывался на том факте, что \(opt[i, j] \leq opt[i, j+1]\). А что, если \(opt\) монотонен ещё и по первому параметру?

    -

    \[ opt[i-1, j] \leq opt[i, j] \leq opt[i, j+1] \]

    -

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

    -

    Давайте теперь просто для каждого состояния перебирать элементы непосредственно от $ opt[i-1, j] $ до $ opt[i, j+1] $. Выясняется, что это работает быстро. Чтобы понять почему, распишем количество элементов, которые мы просмотрим для каждого состояния, и просуммируем:

    -

    \[ \sum_{i, j} (opt[i, j+1] - opt[i-1, j] + 1) = nm + \sum_{ij} (opt[i, j+1] - opt[i-1, j]) = O((n+m)n) = O(n^2) \]

    -

    Здесь мы заметили, что все элементы, кроме граничных, учитываются в сумме ровно два раза — один раз с плюсом, другой с минусом. Каждый из \(opt[i, j]\) не более \(O(n)\).

    -
    for (int i = 1; i <= n; i++) {
    -    for (int j = m; j >= 1; j--) {
    -        for (int k = opt[i-1][j]; k <= opt[i][j+1]; k++) {
    -            int val = f[i+1][k-1] + cost(i, j);
    -            if (val < f[t][k])
    -                f[t][k] = val, opt[i][j] = i;
    -        }
    -    }
    -}
    -

    Сравните с базовым решением — всего 3 новых строчки.

    -

    Convex Hull Trick

    -

    Этот метод призывает думать об оптимизируемой функции геометрически — посмотрев на cost и увидев там скалярное произведение.

    -

    \[ f[i, j] = \min_{k < i} \{f[k, j-1] + (x_{i-1}-x_k)^2 \} = \min_{k < i} \{ -f[k, j-1] + x_{i-1}^2 -- 2x_{i-1} x_k -+ x_k^2 -\}\]

    -

    Посмотрим внимательнее на минимизируемое выражение. \(x_{i-1}^2\) не зависит от \(k\), значит его можно вынести. Под минимумом останется $ {a_k} {b_k} x_{i-1} $.

    -

    Это теперь можно переписать как $ k (a_k, b_k) (1, X{i-1}) $ (имеется в виду скалярное произведение)

    -

    Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

    -

    TODO: иллюстрация.

    -

    TODO: мем про Скуби-Ду.

    -

    Ли Шао

    -

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

    -

    Лямбда-оптимизация

    -

    Не «фольклорное» название — дискретный метод множителей Лагранжа.

    -

    Пусть у нас есть функция \(f(x)\), которую сложно посчитать. Но пусть при этом нам легко посчитать g(x) = .

    -

    Заметим следующее:

    -

    Давайте заменим \(f[i, j]\) на \(g_\lambda[i]\) — та же задача, но теперь у нас нет лимита на количество отрезков. Вместо этого мы платим фиксированный «штраф», равный \(\lambda\), за использование отрезка.

    -

    Давайте сделаем бинпоиск по \(\lambda\). Понятно, что для каких-то лямбд мы будем использовать ровно \(j\) отрезков. Тогда ответ на динамику \(f[i, j] = g[i] - \lambda k\).

    -

    Суммируем

    -

    TODO: сделать табличку

    -
      -
    • Разделяйка: \(O(nm \log n)\), если cost такой, что opt монотонна по одному аргументу.
    • -
    • Кнут: \(O(nm)\), если cost такой, что opt монотонна по обоим аргументам.
    • -
    • CHT: \(O(nm)\). В оптимизируемой функции нужно увидеть скалярное произведение.
    • -
    • Лагранж: \(O(n \log n)\). Функция должна быть выпуклой.
    • -
    -

    Другие задачи

    -

    Как мы увидели, часто оптимизации динамики взаимо заменяемые. Сведение оставляется читателю как упражнение.

    -
    -

    Есть \(n\) ферм на длинной прямой дороге. Каждая ферма каждую осень производит сколько-то мешков зерна. Требуеттся так поставить \(m\) амбаров в некоторых фермах, чтобы суммарная стоимость транспортировки зерна была минимальной. Перевезти 1 мешок зерна на 1 метр стоит 1 доллар.

    -
    - - diff --git a/ru/fenwick.html b/ru/fenwick.html index 415b260..2807170 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -146,15 +146,17 @@

    Почему работает

    Доказательство потребует знания, как в компьютерах хранятся целые числа. Чтобы процессор не сжигал лишние такты, проверяя знак числа при арифметических операциях, их хранят как бы по модулю \(2^k\), а первый бит отвечает за знак (0 для положительных и 1 для отрицательных). Поэтому когда мы хотим узнать, как выглядит отрицательное число, нужно его вычесть из нуля: \(-x = 0-x = 2^k-x\).

    Например,

    \[ -\begin{align} - \begin{aligned} - +90 = 2+8+16+64 & = 0 \, 10110_2 \\ +\begin{aligned} + +90 = 2+8+16+64 & = 0 \, 10110_2 \\ -90 = 00000_2 - 10110_2 & = 1 \, 01010_2 \\ - \implies (+90) \text{ & } (-90) & = 0 \, 00010_2 \\ - \end{aligned} -\end{align} + \implies (+90) \text{\&} (-90) & = 0 \, 00010_2 \\ +\end{aligned} \]

    -

    Вернёмся к доказательству леммы. Когда мы вычитаем, мы идем справа налево. В ответе можно мысленно разделить на три блока: * Первые сколько-то (возможно, нисколько) нулей с конца ими же и останутся. * Потом, ровно на самом младшем единичном бите, мы «займём» много единиц, так что весь префикс станет единицами. В ответе на этом месте точно будет единица. Потом отменятся ровно те из них, которые были единицами в исходном числе.

    +

    Вернёмся к доказательству леммы. Когда мы вычитаем, мы идем справа налево. В ответе можно мысленно разделить на три блока:

    +
      +
    • Первые сколько-то (возможно, нисколько) нулей с конца ими же и останутся.
    • +
    • Потом, ровно на самом младшем единичном бите, мы «займём» много единиц, так что весь префикс станет единицами. В ответе на этом месте точно будет единица. Потом отменятся ровно те из них, которые были единицами в исходном числе.
    • +

    Делаем &: в каком-то префиксе все биты будут противоположными, младший единичный бит останется, а на суффиксе все как было нулями, так и осталось. Выживет только этот самый младший единичный бит.

    Теперь сразу понятно, почему sum будет работать за логарифм — каждый раз мы делаем x -= x & -x, то есть удаляем один бит.

    Теперь сложная (для понимания) часть — как делать add. Какие \(t_i\) содержат \(k\)-й элемент? Для них должно выполняться i >= k > i - (i & -i).

    diff --git a/ru/geometry.html b/ru/geometry.html index 8c62aeb..7f55904 100644 --- a/ru/geometry.html +++ b/ru/geometry.html @@ -195,7 +195,9 @@

    0), b = (1, -(C+B)/A, 1)

Пример: отражение от прямой

Пусть нам надо отразить точку \((x_0, y_0)\) симметрично относительно заданной прямой \(ax+by+c=0\). Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.

-

$_a b = = = |b| $

+

\[ +\Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} +\]

Формула имеет смысл: длина на единичный вектор направления.

Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить.

// прямая r = at + b, точка c
diff --git a/ru/hashing.html b/ru/hashing.html
index ddcd108..2869306 100644
--- a/ru/hashing.html
+++ b/ru/hashing.html
@@ -69,7 +69,12 @@ 

Хэширование в строковых задачах

Хэширование в строковых задачах

Хэш — это какая-то функция, сопоставляющая объектам какого-то множества числовые значения из ограниченного промежутка.

-

«Хорошая» хэш-функция: * Быстро считается — за линейное от размера объекта время; * Имеет не очень большие значения — влезающие в 64 бита; * «Детерминированно-случайная» — если хэш может принимать \(n\) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно \(\frac{1}{n}\).

+

«Хорошая» хэш-функция:

+
    +
  • Быстро считается — за линейное от размера объекта время;
  • +
  • Имеет не очень большие значения — влезающие в 64 бита;
  • +
  • «Детерминированно-случайная» — если хэш может принимать \(n\) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно \(\frac{1}{n}\).
  • +

Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюрьективными.

Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\), и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

@@ -137,7 +142,9 @@

Зачем он нужен?

for (int i = 0; i < n; i++) h[i+1] = (h[i] + p[i] * s[i]) % mod;

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

-

\[ h(s[l:r]) = \frac{h_r-h_l}{k^l} \]

+

\[ +h(s[l:r]) = \frac{h_r-h_l}{k^l} +\]

Деление по модулю воможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к \(n\)-ной. Так проще — нужно будет домножать, а не делить.

\[ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) \]

@@ -163,9 +170,18 @@

Выбор констант

Практическое правило: если вам нужно хранить \(n\) различных хэшей, то безопасный модуль — это число порядка \(10 \cdot n^2\). Обоснование — см. парадокс дней рождений.

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

-

Можно также брать модуль \(2^{64}\). У него есть несколько преимуществ: * Он большой — второй модуль точно не понадобится. * С ним ни о каких переполнениях заботиться не нужно — если все хранить в unsigned long long, процессор сам автоматически сделает эти взятия остатков при переполнении. * С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию %.

+

Можно также брать модуль \(2^{64}\). У него есть несколько преимуществ:

+
    +
  • Он большой — второй модуль точно не понадобится.
  • +
  • С ним ни о каких переполнениях заботиться не нужно — если все хранить в unsigned long long, процессор сам автоматически сделает эти взятия остатков при переполнении.
  • +
  • С ним хэширование будет быстрее — раз переполнение происходит на уровне процессора, можно не выполнять долгую операцию %.
  • +

Всё с этим модулем было прекрасно, пока не придумали тест против него. Однако, его добавляют далеко не на все контесты — имейте это в виду.

-

В выборе же \(k\) ограничения не такие серьезные: * Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию. * Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.

+

В выборе же \(k\) ограничения не такие серьезные:

+
    +
  • Она должна быть чуть больше размера словаря — иначе можно изменить две соседние буквы и получить коллизию.
  • +
  • Она должна быть взаимно проста с модулем — иначе в какой-то момент всё может занулиться.
  • +

Главное — чтобы значения \(k\) и модуля не знал человек, который генерирует тесты.

Парадокс дней рождений

@@ -176,16 +192,14 @@

Парадокс дней р

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

Попытаемся оценить \(f\):

\[ -\begin{align} - \begin{aligned} +\begin{aligned} e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ & = e^{-\frac{n(n-1)}{2d}} & \\ & \simeq e^{-\frac{n^2}{2d}} & \\ - \end{aligned} -\end{align} +\end{aligned} \]

Из последнего выражения более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется очень быстро.

Второе доказательство (для любителей теорвера). Введем \(\frac{n(n-1)}{2}\) индикаторов — по одному для каждой пары людей \((i, j)\) — каждый будет равен единице, если дни рождения совпали. Ожидание и вероятность каждого индикатора равна \(\frac{1}{d}\).

diff --git a/ru/index.html b/ru/index.html index b0bc036..db466d4 100644 --- a/ru/index.html +++ b/ru/index.html @@ -33,53 +33,83 @@

Алгоритмика

Алгоритмика

Структуры данных

Общие техники

Дискретная математика

    -
  • Модулярная арифметика теорема Ферма, нахождение обратного по модулю, бинарное возведение в степень, диофантово уравнение, применения в комбинаторике, предподсчёт обратных факториалов за линейное время

  • -
  • Ро-алгоритм Полларда парадокс дней рождений, факторизация за \(O(\sqrt[4] n)\)

  • -
  • Матроиды алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость

  • +
  • Модулярная арифметика

  • +
    • +
    • Малая теорема Ферма
    • +
  • +
  • Нахождение обратного по модулю

  • +
  • Бинарное возведение в степень

  • +
  • Диофантово уравнение

  • +
  • Предподсчёт обратных факториалов за линейное время

  • +
  • Ро-алгоритм Полларда

  • +
    • +
    • Парадокс дней рождений
    • +
  • +
  • Факторизация целых чисел за \(O(\sqrt[4] n)\)

  • +
  • Матроиды: алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость

Графы

    -
  • Остовные деревья алгоритм Прима, алгоритм Крускала

  • -
  • [Паросочетания](https://algorithmica.org/ru/matching алгоритм Куна, покрытие ациклического орграфа, лемма Холла

  • +
  • Остовные деревья: алгоритм Прима, алгоритм Крускала

  • +
  • [Паросочетания](https://algorithmica.org/ru/matching](https://algorithmica.org/ru/matching): алгоритм Куна, покрытие ациклического орграфа, лемма Холла

Потоки

Деревья

Строки

Динамическое программирование

Оптимизация

Разное

    @@ -91,12 +121,22 @@

    Теория игр

Высшая математика

    -
  • Линейная алгебра линейные операторы, матрицы, применения к динамике, метод Гаусса

  • +
  • Линейная алгебра

  • +
    • +
    • Линейные операторы и матрицы
    • +
  • +
  • Применения к динамике

  • +
  • Метод Гаусса

  • Теорвер

  • +
    • +
    • Математическое ожидание
    • +
  • +
  • Дисперсия

  • +
  • Линейность математического ожидания

Геометрия

diff --git a/ru/ioi.html b/ru/ioi.html deleted file mode 100644 index ccb1299..0000000 --- a/ru/ioi.html +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - IOI Upsolving Challenge - - - - - - - - - - - - - -

IOI Upsolving Challenge

-

https://contest.yandex.com/ioi/total

-

Я решал их почти 2 года назад и уже не очень хорошо всё помню.

-

09-2

-

12 мая

-

Медведь Миша

-

Можно сделать бинарный поиск по ответу: понятно, что дать фору медведю всегда можно. Когда мы проверяем очередное время, можно поддерживать текущие множества достижимых медведем и пчелами ячеек с помощью чего-то вроде двух параллельных bfs-ов. Работать такое будет за \(O(n^2 \log n)\).

-

Регионы

-

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

-

Назовем крупными регионы те, у которых больше 500 представителей, а остальные будем называть мелкими. Обрабатывать запросы будем так:

-
    -
  • Крупный -> *: сделаем предподсчет для всех таких запросов, пройдясь dfs-ом по дереву и для каждой вершины из крупного региона добавив до 500 значений в прекальк
  • -
  • Мелкий -> *: для каждого региона заранее составим массив tin-ов его вершин; теперь можно для каждой вершины мелкого региона сделать бинпоиск по этому массиву, чтобы узнать, сколько вершин она покрывает, сумма этих чисел будет являться ответом на запрос
  • -
-

Такое должно работать за \(O(n \sqrt{n \log n})\), и TL такое позволяет.

-

Коммивояжёр

-

Это на самом деле задача на подсчет динамики через ДО. Нужно моделировать события с конца, поддерживая (неявно) такую динамику: находимся здесь-то в такое-то время (по этому слою мы и будем пересчитывать), сколько денег можно отсюда заработать.

-

Подумайте, какой вид будет иметь эта функция, если зафиксировать время.

-

TODO

-

05-1

-

31 мая

-

Сад

-

Тут вроде нужно насчитать динамику «какой оптимальный ответ, если нам нужно поставить ровно один участок на этом префиксном прямоугольнике», а дальше смерджить её. Но это не точно.

-

Средняя последовательность

-

Понятно, что если зафиксировать какой-нибудь элемент последовательности, то остальные можно однозначно восстановить. Будем считывать среднюю последовательность и поддерживать множество возможных первых элементов, поочередно обрезая верхние и нижние границы:

-
int a = -inf, b = inf, _d = 0, d = 0, t;
-for(int i = 0; i < n; i++){
-    cin >> t;
-    _d = d;
-    d = 2*t - d;
-    if (i % 2 == 0)
-        b = min(b, (d-_d)/2);
-    else
-        a = max(a, (_d-d)/2);
-}
-
-cout << max(b - a + 1, 0ll);
-

Польские горки

-

По сути от нас просят обрабатывать запросы «присвоить всем элементам с \(l\) по \(r\) значение \(x\)» и уметь находить (делать спуск) максимум префиксных сумм. Решается это с ДО с отложенными операциями. Конкретно, нам нужно хранить сумму на отрезке, максимум префиксных сумм и информацию, нужно ли проталкивать запрос дальше.

-

05-2

-

Прямоугольник

-

Я не шарю, как её нормально решать. Я помню, что два года назад анализировал числа Гранди для состояний и придумал какую-то битовую магию на 30 строк, а в разборе какая-то индукция была. Расскажите, пожалуйста.

- - diff --git a/ru/karatsuba.html b/ru/karatsuba.html index 2dff36a..d94ef4e 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -34,8 +34,6 @@

Быстрое умножение

В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).

Андрей Колмогоров и несколько других пионеров компьютер саенса выдвинули «гипотезу \(n^2\)» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.

-

Алгоритм Карацубы имеет довольно значимое место в истории науки.

-

{M(n)=O(n^{2}).} M(n)=O(n^{2}). У Колмогорова была гипотеза, что нижняя оценка для {M(n)} M(n) при любом методе умножения есть также величина порядка {n^{2}} n^{2}. На правдоподобность «гипотезы {n^{2}} n^{2}» указывал тот факт, что метод умножения «в столбик» известен не менее четырёх тысячелетий (например, этим методом пользовались шумеры), и если бы был более быстрый метод умножения, то он, вероятно, уже был бы найден. Однако, в 1960 году Анатолий Карацуба[4][5][6][7] нашёл новый метод умножения двух n-значных чисел с оценкой сложности {M(n)=O(n^{{2}3})} M(n)=O(n^{{{2}3}}) и тем самым опроверг «гипотезу {n^{2}} n^{2}».

(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)

Можно представить себе какого-то грозного японца.

Мастер-теорема

diff --git a/ru/lca.html b/ru/lca.html index 3a524e9..54f3755 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -69,7 +69,13 @@

Корневые деревья

Корневые деревья

Дерево называется корневым, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется корнем.

-

Примеры корневых деревьев: * наследование классов в языках программирования (если множественное наследование запрещено), * дерево факторизации числа на простые (в общем случае не уникальное), * иерархия в какой-нибудь компании, * просто множество как-то вложенных объектов (ссылка на крысу-роботягу).

+

Примеры корневых деревьев:

+
    +
  • наследование классов в языках программирования (если множественное наследование запрещено),
  • +
  • дерево факторизации числа на простые (в общем случае не уникальное),
  • +
  • иерархия в какой-нибудь компании,
  • +
  • просто множество как-то вложенных объектов (ссылка на крысу-роботягу).
  • +

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

dfs @@ -87,7 +93,14 @@

Напоминание: DFS

dfs(u); tout[v] = t; // при выходе из вершины можно тоже счётчик увеличить, но автор не хочет }
-

У этих массивов много полезных свойств: * Вершина \(u\) является предком \(v\) $tin_v [tin_u, tout_u) $. Эту проверку можно делать за константу. * Два полуинтервала — \([tin_v, tout_v)\) и \([tin_u, tout_u)\) — либо не пересекаются, либо вложены один в другой. * В \(tin\) есть все числа из промежутка от 0 до \(n-1\). У каждой вершины — свой номер. * Размер поддерева вершины \(v\) (включая саму вершину) равен \(tout_v - tin_v\). * Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации.

+

У этих массивов много полезных свойств:

+
    +
  • Вершина \(u\) является предком \(v\) \(\iff tin_v \in [tin_u, tout_u)\). Эту проверку можно делать за константу.
  • +
  • Два полуинтервала — \([tin_v, tout_v)\) и \([tin_u, tout_u)\) — либо не пересекаются, либо вложены один в другой.
  • +
  • В \(tin\) есть все числа из промежутка от 0 до \(n-1\). У каждой вершины — свой номер.
  • +
  • Размер поддерева вершины \(v\) (включая саму вершину) равен \(tout_v - tin_v\).
  • +
  • Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации.
  • +

Запросы на поддеревьях

Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.

@@ -121,7 +134,7 @@

Наименьший об return u; }

LCA: двоичные подъемы

-

Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве up размера \(n \times \lceil \log n \rceil\) — столько точно хватит. В up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует — то корень.

+

Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве up размера \(n \times \lceil \log n \rceil\) — столько точно хватит. В up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует — то корень.

Такой препроцессинг можно выполнить за \(O(n \log n)\), используя тот факт, что предок на расстоянии \(2^{d+1}\) — это предок на расстоянии \(2^d\) предка на расстоянии \(2^d\):

int up[maxn][logn];
 
@@ -135,7 +148,11 @@ 

LCA: двоичные подъем } tout[v] = t++; }

-

Пусть поступил очередной запрос — пара вершин \((u, v)\): * Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом. * Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.

+

Пусть поступил очередной запрос — пара вершин \((u, v)\):

+
    +
  • Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.
  • +
  • Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.
  • +

Подробнее про второй пункт. Пусть \(L = \lceil \log n \rceil\). Присвоим \(i = L\). Будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком \(u\) (указатель up[v][i] изначально будет корнем, а затем каждую итерацию спускаться на \(2^i\)). Когда это произойдёт, подвинем указатель на \(2^i\)-го предка \(v\), и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на \(2^{i+1}\).

int lca (int v, int u) {
     if (a(v, u)) return v;
diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html
index bd10383..3f96d60 100644
--- a/ru/mincost-maxflow.html
+++ b/ru/mincost-maxflow.html
@@ -95,11 +95,11 @@ 

Потенциалы Джонсо

Утверждение 2. Кратчайшие пути между любыми вершинами остались кратчайшими.

Доказательство. Распишем новую стоимость пути из \(a\) в \(z\).

\[ -\begin{align} +\begin{aligned} w_{ab}' + \ldots + w_{yz}' &= (w_{ab} + \ldots + w_{yz}) + (d_a + \ldots + d_y) - (d_b + \ldots + d_z) \\&= (w_{ab} + \ldots + w_{yz}) + d_a - d_z -\end{align} +\end{aligned} \]

Получаем, что стоимость всех путей из \(a\) в \(z\) лишь изменилась на константу.

Более того, если мы добавим или удалим некоторые рёбра из графа, потенциалы тоже никак не повлияют на кратчайшие пути.

diff --git a/ru/pollard.html b/ru/pollard.html index 70591a3..061741d 100644 --- a/ru/pollard.html +++ b/ru/pollard.html @@ -74,16 +74,14 @@

Парадокс дней р

\[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

Попытаемся оценить \(f\):

\[ -\begin{align} - \begin{aligned} +\begin{aligned} e^x & = 1 + x + \frac{x^2}{2!} + \ldots & \text{(ряд Тейлора для экспоненты)} \\ & \simeq 1 + x & \text{(аппроксимация для $|x| \ll 1$)} \\ e^{-\frac{n}{d}} & \simeq 1 - \frac{n}{d} & \text{(подставим $\frac{n}{d} \ll 1$)} \\ f(n, d) & \simeq e^{-\frac{1}{d}} \times e^{-\frac{2}{d}} \times \ldots \times e^{-\frac{n-1}{d}} & \\ & = e^{-\frac{n(n-1)}{2d}} & \\ & \simeq e^{-\frac{n^2}{2d}} & \\ - \end{aligned} -\end{align} +\end{aligned} \]

Из формулы более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее:

Утверждение. В мультимножество нужно добавить \(O(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

@@ -106,19 +104,13 @@

Парадокс дней р plt.plot(exact) plt.plot(approx) plt.show()

-
-png -

png

-
+

TODO: график

На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать \(n \approx \sqrt{d}\), построим такую же для больших чисел и перейдём в логарифмический масштаб.

approx = [g(n, 10**10) for n in range(10**6)]
 plt.xscale('log')
 plt.plot(approx)
 plt.show()
-
-png -

png

-
+

TODO: график

\(\rho\)-алгоритм Полларда

Итак, мы хотим факторизовать число \(n\). Предположим, что \(n = p q\) и \(p \approx q\). Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей.

Возьмём произвольную «достаточно случайную» с точки зрения ТЧ функцию. Например \(f(x) = (x+1)^2 \mod n\).

diff --git a/ru/probability.html b/ru/probability.html index cb8419b..23f8578 100644 --- a/ru/probability.html +++ b/ru/probability.html @@ -93,7 +93,7 @@

Нормальное р

Она говорит, что нам достаточно про каждое слагаемое знать всего два числа — ожидание и дисперсию.

\[ f(x) = \frac{1}{2\sqrt{\pi}} e^{-\frac{(x-\mu)^2}{\sigma^2}} \]

Доказать это очень трудно. Даже со ссылками на мощные теоремы оно займёт не одну страницу. Обычно доказательство рассказывают в середине второго курса.

-

Трудно даже доказать, что это распределение, т. е. что \(\int_{-\inf}^\inf f(x) dx = 1\).

+

Трудно даже доказать, что это распределение, т. е. что.

Применения

Пусть в некоторой стране есть два кандидата в президенты, назовём их Путин и Навальный.

Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как

diff --git a/ru/reciprocal.html b/ru/reciprocal.html index 24b7fb1..823b309 100644 --- a/ru/reciprocal.html +++ b/ru/reciprocal.html @@ -80,13 +80,13 @@

Теорема. \(a^p \equiv a \pmod p\) для всех \(a\), не делящихся на \(p\).

Доказательство. (для понимания несущественно, можно пропустить)

\[ -\begin{align} +\begin{aligned} a^p &= (\underbrace{1+1+\ldots+1+1}_\text{$a$ раз})^p \\ &= \sum_{x_1+x_2+\ldots+x_a = p} P(x_1, x_2, \ldots, x_a) & \text{(раскладываем по определению)} \\ &= \sum_{x_1+x_2+\ldots+x_a = p} \frac{p!}{x_1! x_2! \ldots x_a!} & \text{(какие слагаемые не делятся на $p$?)} \\ &\equiv P(p, 0, \ldots, 0) + \ldots + P(0, 0, \ldots, p) & \text{(все остальные не убьют $p$ в знаменателе)} \\ &= a -\end{align} +\end{aligned} \]

Здесь \(P(x_1, x_2, \ldots, x_n) = \frac{k}{\prod (x_i!)}\) это мультиномиальный коеффициент — количество раз, которое элемент \(a_1^{x_1} a_2^{x_2} \ldots a_n^{x_n}\) появится при раскрытии скобки \((a_1 + a_2 + \ldots + a_n)^k\).

Теперь два раза «поделим» наш результат на \(a\).

@@ -122,7 +122,11 @@

Способ 2:

\[ ax + my = 1 \]

Одним из решений уравнения и будет \(a^{-1}\), потому что если взять уравнение по модулю \(m\), то получим

\[ ax + by = 1 \iff ax \equiv 1 \iff x \equiv a^{-1} \pmod m \]

-

Преимущества этого метода над возведением в степень: * Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее. * Алгоритм проще выполнять руками.

+

Преимущества этого метода над возведением в степень:

+
    +
  • Если обратное существует, то оно найдется даже если модуль не простой. Способ с бинарным возведением тоже можно заставить работать с произвольным модулем, но это будет намного труднее.
  • +
  • Алгоритм проще выполнять руками.
  • +

Сам автор почти всегда использует возведение в степень.

Почему \(10^9+7\)?

    diff --git a/ru/segtree.html b/ru/segtree.html index d896e7d..ad34836 100644 --- a/ru/segtree.html +++ b/ru/segtree.html @@ -77,7 +77,7 @@

    Дерево отрезков

  1. Изменить значение в ячейке (т. е. отреагировать на присвоение a[k] = x).

  2. Вывести сумму элементов \(a_i\) на отрезке с \(l\) по \(r\).

-

Оба запроса нужно обрабатывать за время O(n).

+

Оба запроса нужно обрабатывать за время \(O(\log n)\).

Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:

Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.

diff --git a/ru/sse.html b/ru/sse.html deleted file mode 100644 index 372128d..0000000 --- a/ru/sse.html +++ /dev/null @@ -1,422 +0,0 @@ - - - - - - - Streaming SIMD Extensions - - - - - - - - - - - - -

Streaming SIMD Extensions

-

This is

-
processor   : 0
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 945.615
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 0
-cpu cores   : 6
-apicid      : 0
-initial apicid  : 0
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
!cat /proc/cpuinfo | less -20
-
processor   : 0
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3422.395
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 0
-cpu cores   : 6
-apicid      : 0
-initial apicid  : 0
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 1
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3702.863
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 1
-cpu cores   : 6
-apicid      : 2
-initial apicid  : 2
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 2
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3556.265
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 2
-cpu cores   : 6
-apicid      : 4
-initial apicid  : 4
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 3
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3512.268
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 3
-cpu cores   : 6
-apicid      : 6
-initial apicid  : 6
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 4
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3642.787
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 4
-cpu cores   : 6
-apicid      : 8
-initial apicid  : 8
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 5
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3136.167
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 5
-cpu cores   : 6
-apicid      : 10
-initial apicid  : 10
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 6
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3535.684
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 0
-cpu cores   : 6
-apicid      : 1
-initial apicid  : 1
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 7
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 2251.174
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 1
-cpu cores   : 6
-apicid      : 3
-initial apicid  : 3
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 8
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 2202.563
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 2
-cpu cores   : 6
-apicid      : 5
-initial apicid  : 5
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 9
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 2413.537
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 3
-cpu cores   : 6
-apicid      : 7
-initial apicid  : 7
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 10
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 1974.963
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 4
-cpu cores   : 6
-apicid      : 9
-initial apicid  : 9
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
-
-processor   : 11
-vendor_id   : GenuineIntel
-cpu family  : 6
-model       : 158
-model name  : Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
-stepping    : 10
-microcode   : 0x96
-cpu MHz     : 3336.891
-cache size  : 9216 KB
-physical id : 0
-siblings    : 12
-core id     : 5
-cpu cores   : 6
-apicid      : 11
-initial apicid  : 11
-fpu     : yes
-fpu_exception   : yes
-cpuid level : 22
-wp      : yes
-flags       : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hwp_notify hwp_act_window hwp_epp flush_l1d
-bugs        : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
-bogomips    : 4416.00
-clflush size    : 64
-cache_alignment : 64
-address sizes   : 39 bits physical, 48 bits virtual
-power management:
- - From 74aeb57466781fa79815e71787764755a7c11a7f Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 8 Jun 2019 19:47:39 +0000 Subject: [PATCH 033/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 6 ++-- ru/index.html | 94 +++++++++++++-------------------------------------- 2 files changed, 28 insertions(+), 72 deletions(-) diff --git a/pandoc.css b/pandoc.css index d5a6ab9..fb71aa7 100644 --- a/pandoc.css +++ b/pandoc.css @@ -75,7 +75,7 @@ h1 { } h1, h2, h3, h4, h5, h6 { - font-family: Garamond, serif; + font-family: 'Garamond', serif; color: #111; line-height: 125%; margin-top: 1em; @@ -373,6 +373,7 @@ a[href^='http://e-maxx.ru']:before { background-size: 18px; } +/* a[href^='https://algorithmica.org']:before { content: ""; display: inline-block; @@ -380,9 +381,10 @@ a[href^='https://algorithmica.org']:before { top: 4px; width: 20px; height: 20px; - background: url('http://e-maxx.ru/favicon.ico') no-repeat; + background: url('https://algorithmica.org/favicon.ico') no-repeat; background-size: 18px; } +*/ /* https://habrahabr.ru/favicon.ico */ diff --git a/ru/index.html b/ru/index.html index db466d4..e850d4f 100644 --- a/ru/index.html +++ b/ru/index.html @@ -33,79 +33,43 @@

Алгоритмика

Алгоритмика

Структуры данных

Общие техники

    -
  • Корневая декомпозиция: она где-то есть

  • -
  • Разделяй-и-властвуй:

  • -
    • -
    • Мастер-теорема
    • -
  • -
  • Алгоритм Карацубы

  • +
  • Корневая эвристика

  • +
  • Алгоритм Карацубы, асимптотики «разделяй-и-властвуй» и мастер-теорема

Дискретная математика

    -
  • Модулярная арифметика

  • -
    • -
    • Малая теорема Ферма
    • -
  • -
  • Нахождение обратного по модулю

  • -
  • Бинарное возведение в степень

  • -
  • Диофантово уравнение

  • -
  • Предподсчёт обратных факториалов за линейное время

  • -
  • Ро-алгоритм Полларда

  • -
    • -
    • Парадокс дней рождений
    • -
  • -
  • Факторизация целых чисел за \(O(\sqrt[4] n)\)

  • -
  • Матроиды: алгоритм Радо-Эдмондса, расписания, паросочетания, линейная независимость

  • +
  • Модулярная арифметика, малая теорема Ферма, нахождение обратного по модулю бинарным возведением в степень и алгоритмом Евклида, применения в комбинаторике

  • +
  • Ро-алгоритм Полларда, парадокс дней рождений и фаторизация целых чисел за \(O(\sqrt[4] n)\)

  • +
  • Матроиды и жадный алгоритм Радо-Эдмондса

Графы

    -
  • Остовные деревья: алгоритм Прима, алгоритм Крускала

  • -
  • [Паросочетания](https://algorithmica.org/ru/matching](https://algorithmica.org/ru/matching): алгоритм Куна, покрытие ациклического орграфа, лемма Холла

  • -
-

Потоки

- -

Деревья

-
    -
  • [Наименьший общий предок](https://algorithmica.org/ru/lca](https://algorithmica.org/ru/lca): LCA, двоичные подъемы, сведение LCA к RMQ, алгоритм Фараха-Колтона и Бендера

  • +
  • Остовные деревья, лемма о безопасном ребре, система непересекающихся множеств, алгоритмы Прима, Крускала и Борувки

  • +
  • [Паросочетания](https://algorithmica.org/ru/matching](https://algorithmica.org/ru/matching), теорема Бержа, алгоритм Куна и лемма Холла

  • +
  • Поток минимальной стоимости, критерий оптимальности потока, алгоритм отмены потока, потенциалы Джонсона и «Дейкстра с потенциалами»

  • +
  • [Наименьший общий предок](https://algorithmica.org/ru/lca](https://algorithmica.org/ru/lca), двоичные подъемы, сведение LCA к RMQ, алгоритм Фараха-Колтона и Бендера

  • Центроидная декомпозиция

  • Heavy-light декомпозиция

Строки

Динамическое программирование

    -
  • Пересчёт динамики по слоям
  • -
    • -
    • Оптимизация Кнута
    • -
  • -
  • Оптимизацпия динамики через «разделяй-и-властвуй»

  • -
  • Convex Hull Trick

  • -
  • Дискретный метод Лагранжа («лямбда-оптимизация»)

  • +
  • Пересчёт динамики по слоям, оптимизация Кнута, оптимизация через «разделяй-и-властвуй», Convex Hull Trick, дискретный метод Лагранжа («лямбда-оптимизация»)

Оптимизация

    @@ -117,27 +81,17 @@

    Разное

Теория игр

    -
  • Теория игр: эту статью определенно надо распилить на несколько
  • +
  • Теория игр, игры на графах, ретроанализ, альфа-бета отсечение, ним, теория шпрага-гранди, игры с неполной информацией

Высшая математика

    -
  • Линейная алгебра

  • -
    • -
    • Линейные операторы и матрицы
    • -
  • -
  • Применения к динамике

  • -
  • Метод Гаусса

  • -
  • Теорвер

  • -
    • -
    • Математическое ожидание
    • -
  • -
  • Дисперсия

  • -
  • Линейность математического ожидания

  • +
  • Линейная алгебра, матрицы, матричное умножение и быстрое возведение в степень, применения к динамике, метод Гаусса

  • +
  • Теория вероятностей, математическое ожидание, дисперсия, линейность математического ожидания, нормальное распределение и метод Монте-Карло

Геометрия

From 77462ee9bc3500af22b2e1142c6780157b3c28cd Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 8 Jun 2019 21:46:48 +0000 Subject: [PATCH 034/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- img/divide-and-conquer.png | Bin 0 -> 290262 bytes ru/karatsuba.html | 67 +++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 img/divide-and-conquer.png diff --git a/img/divide-and-conquer.png b/img/divide-and-conquer.png new file mode 100644 index 0000000000000000000000000000000000000000..6222d0bf6d44ac15cf9ca176064d2365b48ffd87 GIT binary patch literal 290262 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 &psBbKLUDA^`DwKgB{F zxRs;TerQ6BOW0l{E#Ko(8(HTWH40rs2n6*)Dthq@)K_11WUdR_u1rGqS#nIT)_3wi z4~{R^QIfGPL~7j$ z8#a`&WVw2CV&m(H4_~qjTHt4{ACbWK&BFC87;E8)$HpcQWE=F<07Q}z)awSIhaBt} zQcBAoy(US_-C@@&35g_R^c+UJwrO{~jHPeKKb?FB6Rq2yam((TdVW>3#|ttVb_(KP zWG2pI&oLzE@=;!DuWykVvW>vN8ng3U&`Po>D2&oHT9zcEk7_SqU*e|AQ;v80kk)DBVf-}v%GLdjZ>m3y; zHX`!P>VHc$a=M^G3o~?~ z1~)Xn-nm3<{yVyV@O+hGCRdFEC8SXAB5)Mt zVe!K#s`dMje0#AgPa_~|Nk#T-!p)W*&1H`H;nJQcCm;U^uPz?3GLNS!rg={FB?P3Ce}gU&ZWaUW=Ekk5Sf^=t~%m&37?`< z-fB2Q4?vmm@5~H`F8izG?<3p<5ys1_d=-ax8h8ZE-2}=txCH6@s)~Ad#3|du;mE*x zL%3$e-k3CcMz7xC$j{ken+GhyLGSa=-(l+<&a&ldh*vZk$%o^gh} z#AOjD(GQpOkS#oDxA3#?XxKj)5%BpTtVSVE>Gzgyd(IX9x~R07Q-0x?n0Kh!?`=-$ zn@(}AtmsiyzS28_uM3D9;Ma5~<2*H>s8?N!XU4w@bf7-kG?Sr#Na|Y(!SJG6y;E=U zBddaL25z2Avh(-x_f_1e>&@SE?04(SLe_6Op1nVNj+1aR;>MfOvtr~zY9~|bgXd&= zN$t#DO4?4&x*2WnKj@l`wMmOq@&nHm<$_pXZ0lCxPl6Oz{;DWWtWcrTQEI5}dSUmi z2)CyJM$uIVCtGh=Z=^@A7C;vftBzlyA`+7QqVc7`6Hy&J$k_Dhh+OY&*h5OM2*_&a z*6pkQ8d{Qjy@7b&Ao9{k^E0Eb>SbQYj)8|)^RpQJ((_d78%69=PCd*$ndkZg8!G5s zob>PgE-3#8qA}m#Q@lF&s?meikl#a_?hx|u`&u^@PyBrrvPsJNmnDar0G)%TAebl# z4ro}rbfxpVJzl|0mhdgdnxWa^2&$d$H19zLZ5vT?KPao8Z8izq_UwLK`AWZor+mUr z0_gKH{y|c)5x*^Weo>2eU3nz2eblzD*2r z32|1q^9*jBVE`YGGaNa5VNpIha5rlLq3r-2DRv2uWTmugthX`0NMimzWaUu%88c;=d#)BAxI8at(WKd60YkIM9n3j4_Rp*9uncH+(JP7f|9 zBH_xxwciT|ZqoBY@nTlreDzL|J21U%)P-PYf;z;*Ms76-vc-u@A`Ks#<>}hF?$iE2 zd+n13yuz#U{jNqx#}gZ!OQ=obw?skrI+P$5VD(~UVWx-vQn5I#LK<-0tV#6-W0(2A zKD?Y)usx1_PEDZW()w`Id_G1-g`a-5NCmnJG=E;s+l zI0}|S{+i1edqe3e?yD}H6K%KNpI`(TErNi0XqbBnRNNG(>FTq-fp#leWnIg-s)f9V z;iHAy@xy!t?4}wXszLsgbzBdm-iFWwwfiMm{A8B3Gs|wB3j|#*{~8?tjt5SrXo>}1 z9X$H7&Tg8%eJz3OA;ggJiny{~`KWSW?;xqPT=kvq)b%Tc{MU$AH^@~0y*absDg@^8 zYSG~Jaau~XnJH3H$JSi+e*p?%p${M)KvZ>+uPYe=s|7OtEhE4nV$FsSk;~H~DGecT z5v`0*RjL^q$Bi}tz{gpAJ41|sb8-;ZksM8)Y$SgXzm?N=Jf`~gX{#RsXGC%gwXU)8Xh=)Hk~Tzjd{vltMC-?K3!rLdoceMTh!BSJ2;@ z%O2K4BCWk{H==jRa_^DV5a$igLN{Y3NRDT0enz<7lYOH4LL&pj>u2Zkciy!>R2m92 z-xTJdg=v&4@3_|i(RKbUn1c5-5`t-$dlC*Pv`<3RIO#+g1+708PTwCnnXF+0czxSj zTRaIV%_?5<6Zv|gY~T*CY~tRTTCm-@u8;7PWI5z4(oX`t@AHHCnml=CdZ&O}DKdls z62O_LH#C~mcToOgk9|n)Bo=19TxjE}ts8u3aI-@XJAY5FJUH|F%)MVRC9S#N7RY>| zBl2DBz~MWjj8`)KScjvgH+Jo=%e=_X)yl%)j0I66#WxrO=Wc|2i;0Hbnp@GY@P7 z^5zf|Z4$DeSw&pBsb|pG+uLjf#OH9M3E06UvOM&nfJ#)1jzBypy2t=_^kM_D4adzp z%!!Jd%;kuepGCS(I zpX~_MBMI8FUFyv5=&BzBOi*Q<@?ArCx}Ro{B*=x&|A_efw0F_YSo3~S@w$}X%$+eY z1&);=XAa>OAKGp^lf~PXy^75U3;&>(rKtr`j(;mX*3ahISZ#{v`!`1&riO2U9;P0!Nu%v0W>bP+7~ z?Qy}L*sIA=W&p9p&XH7E!RxCbM62}7*#PAXcAZS$qpW3~hufftffxn62z&+K1Fr?q z_OI0pC_p@58p{cstK4DEV>lv<2wOIkf3#?_zWSavz+HTDanC{lq{77ZT?+vt2(+lk zJj?#hpzg@=^al3jQ(IY;gjAJ47K3ZE8+s)^NEKsJi^r9Rlmi4@^0S{7$a#=*>_~Mw zQ|x&2$$l*AnXFl}z0?9uasP_vzEaTn)aT5xd5|zIw(X(MaQ3onUYH=>Q)5lxBBdjw zvnTiE=HyYtL>FG(q-qgp2LgQQQZC&LxVV3QI8P3m0YTRbbi_*r>=8hDPTRve0fw)I zcU97)?uJ?5XV+CX*uJm1&qGsG4Kl<{e}NQHnx^)>&<5}8%+9SMf3Y)aBSAC?pj+pf zU{PaFn7LVA>*If(QwIZ{!; z?(w?|cJ~M9p@_;B+^(&d1ufLuw+8msq+hosbZJWQX;~B`#1&D+pM<>dI)Y`_?Nk+X z5_-9I!`hvW(q7TKC*RVW968XMzGP6PEJvVk=|ST{^GKE9;sb3dp#=gr<6D}Go?f?+ zEDcz4*CUdzzgP8PY{!q1(NUaM{t%y!)ZIB>ur(t!SaLEmE89E)DHWs?&Ei%ODkb3* z@svfs1E~-To$MmG56?_AEF14KLoYuNV1A{LSTd-K--U@_ZOWA_uvV7?#~Xovt)H~5 z%Yp)j>P)J1bMOH5=tgL3+XCpKOR{k6Sbx45T6oSCy;O+Va0ztFDeUvR)5}PGEWI+e zU#Q{Z+*pV&9SK@*P74agmv$;t$BobuP>Yya@^aEU(l9~sps$^~`M-F?ilk}U_ZDIY z1+))@*L6RfbIinT<`iGbg8m%6M0I{|?|LCdYmtgtmee^A2;QKWCbxZjpV9)?&an=; z9f>9J)1*Zg`uGpgtks{SH9k=2x(jHamJq$86 z@qz1Tr(33@{!)dsj(!r4+v9SalgXp6@N}qPQMNF4Faqc?sjHwc)^j7rE6^@Seh78G z&BtN_*r>?9v5+O`x}Wn5l&R@?Rt2|8sAk?d{w`qM05+fvp{0lD(+S!NMI0zimhx@9nMwve>Ioqh=#~eKWGb(49?>K$Vk}8D zhhzubSc99ffOrIf0QGhI)I`FI1%_9|9g9JtTr{0$WY==R9Hh!($fYQNoRsy$$hk2_ zYl)~Q-jCEj`^2x>Z7B+xmjJ5)o3^XMQ~yrVN+0(qewl-47es8*vXqCEZ5sI7 zSxGXx3IA=CzggGnMnI|pLuiU=UwqpJog_o%CA01Gj$?kj_2xD!^R~`fmcehk5`xjO zj+X@W4UF}R-@5;i2c@$ysHO-*(0dLfM3V5<=uX;l`yR2tWX0c1Xh|Ru0i+Am<*U|{ zkeZ%Fd!3P~#Xy0mYrvoaAo@x-lG83(^+k^F&0GVT3EC&$F(iDrrj}0kzF#B$N9zL2 ze)9`R!Ro3GZn5ox*MLr1QN=p62n#fUQzuRRg{OuQsmt5PSdD*G1#RMTsj846!q&h# z(KPDc8hmSZH+uXPx1Tjk58e|ya<7WEHAQ@5D6rLWdkYX4a7X9nQE^y-PLgdvk|zNM zqXg;q4(*Jp#9l!$fu5!w(CR$5ZJm|9F^&tbU zlVJ^I#)OY<&GKieSBvarXc16MPvn4Oi9zy41=R(&$IQwp=q+x1h=Zv(ez7?bI2imn zGk>3#<`VxpYU<+bCy@0t#Hu;h$K1F4B57Do>o+^S zhhtx=$nD-{CxjL9#al>8q?5||9f$Bn38nX#q|u4}lseIW{JJh1Y3Uq$ygiP2iL}Aq zK3;{_L9_V-tNE7~J2+GDzMjP6}~MPVYwW-gL-lLZ(}*YT;t4YX6GL_i{!o0^(4m3y2; zzlD$E+2HXDc6hY)eqL(CjVMrDt1WzqF}ZN-xx=pv8X>pPLjL$RVYZFU%Ym&wUn0{B zJ6=gI=I+gvp==Os!2G3)=E}7S=kJ;$U_U=94QQ=1_J6bNQ^rpn_GSB|Bqt1x;|4Wr zjHcK$U&|$2VaFaf9|`UB`(1M; z#g4?Bg8UJYosLSvOLmvskL)a@ltN47rl*KAKv+xrLUoB&ncSe*ZlRCrTl4FD_>=E& zT1VyDwaozH`;hjlP6Kvzm>ReFXe{Z2;Eqy-hRrxY zXWD-E4a<=|7KkFz{?^{w$^tBN%5cthC-2bWVy)jyV5}vTwM<`dd+sbdMUXCHpFpQy zXzL*u-<~ZEIAQ)Wc$;K?SHm4T@-gROQionI{p3yHj({nExg&1Z-vtvL_W?p$&iD{- z?9}*_QSW;dCzO zSwY^uB+$BdqCrN)#zuE|xI)dwwxJCX54x%NmMQH^c2w=D%tU;P)qT?=sbJ>MNci)l zcArgmzYKZaJ`u$_){W)s^%q-|AYY4Q<_vhaf%!@3w`LbalW#+ld!rO;jG!4;Bw zHTQrS@Q^}``<~UyLH65{0qW4l@u0`$Mw)~dq@_a5)5v^Q+J>e?vb%I{B1z&B;~qSH z+&A~Lt|YyB!z-4heU_=;#>(yaT&aHF%c z-VTu2=}i$<#BYFVEmHE4k&b^N?2GM>Um$XQPDcXH3C?$KTttH1$v)01gg|y&=wk)X zF(Bt3iejseq~R+;9uC?_#)iHz$A*M@(wo*yaj28^lj4L~u=l&^Rg)S$-Gw=}yX*)2 zj4ZMV6j`R~>b@Vo)_F|Ts^5@5t#t{qQ1hy>LMyBFGE18k2b!!M#kXDv+>?h{@3GcD zKXhQ;=!yKu@AAqzYVj2N;&A37_QL;yO(lW-C-K@mgAoOK=6ro-Oa5Z{qRC(|#3Gf4 zbRG2fe7@}t$bsC{+{OlW^bi%&0l2o!VkN`E7T@xk6g5jbFj4EY#9cb1`-2)@Uz{*# z;xz(~>$+H-UwR2{cA4?hUycu%e}Ai`@YrIspM|{2fkRN0W`%8Hk>~V0`$F*iVCDj+ z@q6Q=msq+rn_G?NdlwHRQ#n{P)1*HROCZHrkw&c*V7du!5G$dTtj=$exE0S16>2>J zc0N*4NBywUW^Y*+9_eT$`J%T%RwUG+E)< ztR-@6s;8QFWmNT22mIt*%70a#9C;06Jr(E#k|h|tCBLz`y_|hxduuC@cl_fI17TP9 zw~c3A7cKI(pzH;0*z{KsyFnimy(z@6C~mU+-t5?T76$q>sFn0hWU`vR*`*|rwu3=!vm`Zn*_D4U#gCVc z<+1xf#h@igsRo~+wZNEb6-*v*{^sHkCBFrF6wDCQE$%X>>&76K_KYuRtLIH-z1E+Y zmLQVXj~K(=+}xTYWofG;JeeY^wrHuoJ2}~)KtTq6K{aET>-n!6hefnFmph4E!K2S~ zs!n<8?`jU!aDRHZK)~qlz!wUS$gIQ*E0mKUqwtZ#vznRhgNs8=5F08fzMNJh22oDX%tTNv&!7%A9c*>;a!cZr;G z4;E9Oi{o~Zy-^MPIQ2ha_+1i0#IkB(a(0g%SHwAAtYC*1w^({Sc!A+{W&Ql6*(wgN zBrIp7%pRPU(k`@BHZu;+vD6bJ9<9b1toWGDeXWSmrnMjMDE&;_g2!IBwbr&ame?y| zjcY^Nc>MGIs__^I3yM8Zz+WbWy1A>A( z9|hvMgqIEu4(GuWLOEW9YI=|dTAODt9B#^|#cPCN*fZ3GSmJHzXDtd841MiZqH8J5gh=|2)oQ<$v0IC0eekCZLu)Ur%CDdz*Xh(px2~`;0Gdsf}{4@Vx zvA6N`TpP=_48w$T3#|1wL+mj^-zo-3wQ~50*Q$lVc?es}CkH9={LXGZ!QhYp2F=%H zP!VfEmoLo-K@<)^!qY2d3&7E_bv&u#x=m5GGipBW#J;MLNV ziYjh0ygZSO}2TO~r19S<$AmEp8+_7GZndp)Y65UHqasyR_e{ zlUO|Gw0YY<@v;|S+fA>OSmoNRSZxcU4S9bn?s-wWTAb3iIact*863o@>{*i^?Ukw4 z6e&9Y>4~l49FxUv?XPcnyoOZ?MMjSXpFX&Q^}e9nmfX^x_Eosz4nnKpK+BtO_`r^3p;>P=SvNVV0 zGG^gx(poDeyv5cfkvTzdoQn4N`Z#(wou0ME1 zo+Uw6FnkDmQR*yaY%R`|Bc_nm5s@ptA?Me0a&V(;n&llmEvPuAX`?sDMmP3HJBY}v zBa5**)2hl~ANk&WR`=O&J&`X@6Vx7QvI9lxbTM1_ds>qVf0Gp{bX|PK>i&u=soQ7P zP-Ev1Cm&-my3cx5pChLYU5H4PiY)%DPni(g!s^DW<@l_4wRNR-{s#JeYd1r*agR4U z;WPBWDM=oejYmEvooIU{tq7UCISwWe`JL_=&uRS;vZqhFA2{8iu+{LaOs>PzJ#^(S@dtRT6U(13p*4-CLEEmK zd360zC6Ls6r0*B6d1T9(G^Dh$#5nIR-Gw{ADRJf0x*k$XX^W%F$R0TI9KSQrW=h*F zeN#9*&L+*;KPvvsLM8Nzu9htm&z=fr@RqLyo)zR8BvM4RJgz#T+Tgts9*XfMVK`xn z>7Y-6HP+KVFG)R2T6du->;059(2-qPy;!22er;>oPNa>GoNda>!H94C)TXnySV_yF z@ON$Yb7D1=8n3q=o7XB2VhxC|WHfiiCfm|v8*C!FvVDB7)Ud^Ji#F8{eyF`e$Ebs< z*Qs85;`uXr;}SLoC?_V=;omvU)1z~8mVKg0laR4BChAsY*B|c|r2LQevV~Hi&CXMK zyES~;ONv1qCTb-*^;}yE0eah=?t4@9&z1Rk-jJE9GvL6mN>wTVii?KSU(!E3Ua+} z_3}+&_q#7Xis@W*yO!q}h1G!T8G4XF1)ZKePpJ;S=jcd5BCF@lEdEJ+n}nB|a{&Ja zK(QdP1Qz~sf}P>l+^b=D8}0-p=pRt#DI;1+PqbSww%jQUop`amC+_|o)sib*y`ntN zG(6F(IY-AbaZ(ZpP41DyT=Q~o0^t4ov`N4dVMMB#`x(BG;;+TJW_Qdcyl^tI+T@p51XwFipwN!OMI2A$9i9L%oEh9G^2KR0(<&t=h55_pS`r6 zH1=pne9jZIwHiuyZIUM7UzdzxS}P{Pov~t=<0?mM<3fx-q5e zKPqBIrSU?y8tikn;pI;mZ3NukE5&-!vyVG~mzZhmh+EDP4*sNhv)m-n-~D<(;<$+8 zF>-=Ew)zPfkyuaDEAP;;J@s4fY7X?o2(}`Fv|2^(#ymD>L6f`*0B5|%4VjUtj|exU zwUFcd2_t~s)3|Wx`56WJ2QeWapD=~Jq|+qMhy{l<21HzKR* zW2Qu3skz##9&bhiK?hsXB{Or_KJD z{b{k`D|-gr@Gu5XI+98|%^bye7K)M=hll5ylT$s;yZerZ`2pv5930?tlH4DO>`#zO zebdNcElmCF{py(`sA*%@!S|I<#SZm-c@nQzY?m?hltgY4|ATxg$CJECP(Esov?y?|$2+ep1h1 z>@d@a0ig%35yAnUfx9Q0WisTkC=RYPk!qV02-nek(d7(IJ=Fr=_u3HI;#UMVJxI#2 z)=B|z#wt*?=(@NP_=7F(Z=GW0ZQ~j-JP>sq#i7TU3Ew17i_~3x;a$Q%6Aw2-+m*o z3vpK~#Gi_n^dbFnejUP?D9q^h_CMNlYCv^$=ed%-D@X)F@7wM2F1Y92~7vWP6R@Tn>`LI<;CV^jSeY71UQ~xV&hdz zyJhg64N(f6rQ@`jx8Lh?HYm!;lyUI;c@Li{@WPZ3&r)wqmfYGKQ{OCm{ z>lZDIAzQvT;FzXYsWK(%%7dxWq&kqu3V!Vuv^IIE+0PwSMK}2I4QiAjAA01*A3h(! zCYAnJuHR;Bwcbx65mUC}W$#T^#P>L>EyClzT^{Sc3VxSuMQC^)rA*%&`-;G`B^L1V zTe)$wDkJlnIA>9>-~rE=H}?Zy*s5#tk1lX~8x)J9g18>`WFsMcg0>V% z8FIlBB$@TX`8}E9f>z40#J^1Dr&t9%h(Q|aag~X0mET4nkw}4e7Ao_9UY3-<-Tx6q zoFw`{lI0d5Xi$EfqK|K414e(6zSi7|9go+Cyb)6eK9KW;5O{|wP;oo#^Q#HE#7wsI zecHM^nIO;{c-j$b;Zw)^W7v-zUquws@|*|kPun5lIeUJc>J07UVJZBo(G#htcv-0G zt-N%WZT8AiV6_7F*2yD5&;B0t_+ih3cN?i6xSoBW956?Pexk-D6vZj%I_e9El{dmb zOdfJv`{Fja`0eay`xHWW@Aeox!ine4NjxeaC$0`BuUgSr1M#w)k2pby^8tnDF;2*< z-(mUY?XBl(`K+cH<i&)8d>KfA?x%~m1$XP9Z{Zu8qGw<$!La>{+Ij(1TZ!u+U=*()@GylVJUAXBwb+|Kl9hTKTf9sc0y4hMLLVT;g%*%Hwb zr@%D+Xc?K*GcV-54~-+{ z@UHuBA2PQF&595jMa9);A+ror1ui^Ve0It6j7S(?*OkCJ2`tZh{zoYlT`6Di6-t3w#0gmmA~)} z=E8fMLPMhwm3vJotqr`olj-q~=jv|K;1E9WV(@bPQfKVvEh~*DWO`*L%|VypaBVvc z%U^{!nqD5}F#cB)Lw%3zPkEly4XL#(@Ct?B>Rl!*1r(Hu4e_H249W#hyb>OCcbZ>k zh+*?ywM}rwt zU^ip1d1yqU;@L+Zc~5Ps>#7)!Ks*n?~`7Me(BRFnOkJn z9%8eTaXlbbDZNX6LLG-9zI!cKj3S-8!mku>6bbv3pDfe5YtvLt z*-4g=oieHp>11dS^e&5@2_^hLJiUcmlwH(4taNt_-3Civ*B(3Y=8&%@>$$dk_1SyP z0F#9Cw*AbXdNbKS)dHLCzo+24gnL*9;Lem0tLU z2w0(~`^}nLgPtDs?pGw}e*2D7*uzh8Put<}1GR+*22JTO?bq56xm22)=rkp^H);`5 z?~*cO$`B<(f(j`(9ee0ZhzA_i@KmDC^hqa;xZ#V+I|)fc-~PoCz0Z*QZcmrhEI~SI z^ik5DQd_X|PdzhX?+-EdDW?Jjv~>I2aq7f_5lvR0)nvXhjS&ZZ zju9y*$}406n3}|F$5ptcNUK9jpMy5$h7rjrkYc$9km-|J+!TN9)4*ms0$TRPUf<_c^tR^tb z&Hw#JAuTtz#g!FpA|~l?Dd|6>9K9OgC=MWnrIwqVycxVG9<4Nj=+`QSe(?l{Ub&tQaG+ zS?|V_`Wsuzep88&)vh)PZ+ngN9`xc30{7#;H&Tmu!f&we{zPwT*OQ+!qE*S?ak^i@ z5L1RK@p6?ql}|0ChrQ~suOPenFL#pIGc!ERhW)EcTkwyMZu|n+|_2kW39m&2~yhi(*%#<)c&Ut8+q z?T}eQ@#Ho!wSRX{V{5o}_FH`&OoXYbQ)a(X} zd5U~>j7gmp=^v%vu7yzaw*4btusOFd`N74}2?WUUTM@+@<9Pn{N7L6nCmpH--Bq{~ z#dVEe@+a~T=7kbl5|?HjW#P#_L2^p584UUmC208x$rb{lGz3Ey!PnJxzqg|t2HVG9 z<@ecR(7;U&em>yMmk_U8r*Rxg3Ee*!3hy}ho`XZIw8bsQ&f^={R#|UwdAlEH>1x$- zMA1+v6-_|*w10T>$8G8K!FqaVS^(Q@VZ-CQ6n+$)Hn_OH8N_SVWJifo)uKe?nUS)K zY?-;e^{OmSG@-1ly{_Vm+h!cmNla?RNx;gnFy$0c>uKQ4HCIjpqxNimx;H%ZV{th} zQVij+j{NfVfGq$v*2>z-(OeX3rIh002@9>{v+RV*vvM-*qr$D4*%w{(2Q2%_%Y~`b z|K6Zpykm%yjk0R!3l6pKeqNVEz~t$A|Klz3N3hSCAkD?!ilZ90e>zF-u33>2eHsdO zlnzrY*PT>i!K4OmIklG`x|RI#R{Z1rW&Up(b-tnv&yQQf$++hHoV2krvy3t*KN6^U5*0M` ziOS${S`-nC3@SQXd`xJO3CBn}WuN(nK9J`pQTn4!4#$^L)zQj83h?98ih6V~HeeQ#9j zJ)grbpPDuluOuBN&4Y~KtZHx*9Q^yz@#D}49&uUIQ=i+7oHYf)Y8X|jrIKOIsGy0B zM*@Z{rB{-p%t#ow=0E{s25n+#HOJ&0v>OTVw zJtlPe_v>b9ANe{>*d>;$FwL;R59mwDdHE zU{)z5CPE1`-s$%S$qZ%--W3#n!@R!Bm&|ViygyC_>ER)7X{-587Jk|kD~~=Z zwVo!ieh29G$7IW<~mijtuE+?T^e$1H7_Ig}x<03C>hWnbP6 z0B%&*&ycx_fVI=kw+mP=N}K~{5y-tkZKM`89BEUkA>8R!hNi}pl>PU3Ed@6@VZ$+~ z63_odYe{dRxq)nVFxi#VoUZP_?5+twpocXP?XwI<%47k{pvU?(pReL_G8%E}A12F; zzKkw(?}x9plp=?R1D36O=Z?d0c=&34sheVyN?T{|_KBY?WrT;g0`8UB@ZE~1SZnSi zm!vkdxy+V%u~1y8W^A2uY8F4Zp6O9J*1Y6t4K{Paej6e z-r-1Qt0L+!2_7!5FkIjd+W%pkAC>fS7TyP~cW0?xAyh{z*p1m8I+?tjMn0)8|w|5Z8llGpEHG{$iq4xB6^UZp~g5=gtEKhoW zk28YZ7;FT*ZL~m>=0tT58up(KEzLF!rCmYi%(JT#1>U|Bn?AcM zO(4`jEsxGs?OnD40wqCWis(TQ@1l}!(`ugsWvVY$(6<_A^;8)SZrQQ(00lWV8kZ<+ zjGw+qvBdi5Sz>7=VXSCM=~;ZAv{Yt)@yEczft_cb0EsID_pcwXTaZfEZN!7r`Hz5IyR^+-@)$N^Hb zeYq#@YcG^Xao@A|++w3M@W&RxRyogK=w^iG<%%@v<6SB(2V3h+lx1H>Y}Xea9pq{9 zD86T7v~XV%@kBoG*txEEjd8A;QbI)6xDq^tTUWx@w`P|-lI$@`bB@1=Cze)To=zF+ z;oV4&3&b&MYQM*W=3}P3gy?OJ9VQ(7`qwypX6;-xai#ag?JT4Tkse;eM<`_B_a~itQ+WTO? z*4@4Nd@ZbHwk22~VYK@GligpD3V3Qqg*0m#A5$W|4Q+RAK^-XvnrI+Lo(4)pIQNs<~>b?YC* zBoOoFK|cNzGIbuXj8UXA5{LC!B;daYHbKtW}@;ZD{Q{FyTJ7@bdYt($Vu-n&m&_NipZ_Zs%^ zYb%Uf^DDVT=-TCW85Pj=IG#@b!?VL&Hcm(cIXpU)hwX-!Nii`iKw{r*^oaAy(+Jpn?P6lavJ+^ z;LS+>^DWEu91HoEcRw>uKi*1FR=<+m1OGx)*;IZSF$uXcQ}mU9-@t6e&+e(Nz5l+D zS_S-wus}CoAg^^!e_pQqVA>U(cl6bYo_z0wl%>oB78b0xH9!Gv*>nSJ-30jj{#>uN zKJK>m?)bkvQuI9SCLMRsezFG0bSiOP2$o!i2^9RY3xI8ttmIZMZ6ZM$A4@zvO+WK) z+_Y77DUHZ|&!CZ{G^19mc6`fmz{=Md^|U>*RzZ$! z#8YHNz8`YLt35<1BXZQ&cAIx`cfZ|*Q3x1~{MP+#`RApa{rL<|NdI$W!MTB+X^LKk zsrx(>PYO@XkGQ9G)?ti0@#-mT24#Ukw(~J7u(1L7zFQ z9Ki60)$kx|>jd%TasB1<%Q@gm)3vz!M_G!96@lrEWxK@mNEq3Stj6*$AJ@nfHm%%m z{e7qn6E?^?>QU7D4@srvWDiL~Sfg+(wy3FOhbi5!cFpFOyDi16swea4M&#Z?xr;O+ zp^rWrWi*o7H_(@7$!FXv2M?_?t6{d?KYpB(hvi)D|KiSvd52WC_=<7`PPITm5kH*X zBUzFShg<8N`+OJDr7HSd<<`;__>$MzX|lGme(}aXaX9SyYGf}4_4(p}L1SX%$-mdy z`v#|kC>*0Sg|(L&r!a*NCoZEO3)*u;c8@RaRHZjny=mGUW1+n-hR#^i_Wp*rF^=>W z_xB(d5UgmzUw+fq_P8VJ4wd%*}j0u!DLeO22nX}Ce(z80^K z_%^pyB5X5mI>FdbjHbw&#F~G|0ZENmIqp?c3)Yg=SE*?9z#gl{cFXQJ=m_E86qFs7 zq1y*idJ9)Y3MMgM^ORzRk>?3#a1cqxf9uTgKUfd8AI~W1+l~fxGEM0G`DHx%C(^!Z z5DO7(*7#uwDEDIFi4X2Qo1H*mxccxxLn$Un%i%((FOgvw2I!Sh-(H;!Je;n1?gg`J(7$>fruJRT|2Q-E&{}Q(2BElg_v~sQ`Qvly z`pfy$oOii8tn@WAaadO$823VFF@Dw`7A#}IG zw)=aK1GzjOZ+G%g0un<`>nRYx1-k;hr>^b5iMvgz_)^P!eEBpZEx5TAnEkX;Y|+$wA?roWf%;epl{W)(qUSXK*Kb6-x$nf3euxM8t%>xbGxuK^TPC(D z*R5Tx87r|QCj_-5L{`7bl zKv70%HDiSaj8OT~*&!VP*<3l$DjZjQqBx9f7@scMfQQ4^(V(j- zJ$BS#)G^mLF=Y6_$Lgbq&J8)f0?`rw(J`QNb9cCAI_isKk0raHlg|O%72j3Ou8`py z$;Uqb-n%5ho7O+&QrS2xoZ)BbW7D{~r5>Fv6x{PX58_k##YEpnmnX@Fg`9@897qceU;tMcJ=F60e{)KQQRM^anCL3S)$F9@%Lu-kt zeQ~yoa@}Oi<&{|s|E_j!zQ#Wz%Lh((kK2~jo|}QQXOBXs5B)+#D=Pp4WTY{JRa1#i z=C^A7M^I3nA1Y9~tD{u83j{wL`#vw#z6XqAM?QBO9q) zB0QCND{xw=oaIL3LRobc3nT<|u;WloD(>h?rYKn>@2RG>YZE9#G|J`x2TK=RwjI$N zH4Umg=(#4g()C+L%PMJVe=E-7q{?nXdFk9@WmF}UV|5S#FSK6CCS$11A7T6Z`vZtX zxe;^_lz18by=Xqsn9z6+vrMI+I5*Fpq>Cq?Z-aYj+LvnhGCO89Txag=*nRE4sNy2v zTuWjnu;^o0$s?;_u#k(Rc-TRQ)$M-~gS&nK?3f4~5qlC9CjK+)4I|_KR4z=d!RI>V zKsjVl;aKtSvYIG@R+p8DXNV9slx2V3_Oa-(%J9Dw^W56Up?y2|iudKuyEW50oC*Ky z0S^tG*2JZ65n)1L<%UGoq?Fm#>29_0;6SW-L-)w{kB!P~S}ODviVqw?9kM5@nAj)-~)Qo{VS+vwTedjnr~we!l{x?ei=U+Q8n0VZ=3L4$bmZEK_z%cZs;l!kwhPC@^b?si)sSd^}P&=yIc z4pJXREQ6!t6)Ve7BBu``8zg87vbjHrj2QL?)*eE20EQ7XM{m+8gtz)AQ-ws{1|D2~|?$0+v&Wv4;8hb&X?u_qC<1P*xSj@`pQB5bfr;Dm&CtTjbZ^B&Fps)qWB8=-6;Kpu1m9n!O1|v8{d;XM z*Hp@?4`|B;*4}9rM75$FcILHENk@K<>@?1(n%=^8<7Vt>0_Mc^csDM?VH^v z37Nlwg%JYk2Z5I-RLjd;Mwxg~7(;oM`;Iw~nqyG2LPZ#B5_&&g`5)pho^vHUY`&lV z(Rl%LN7%WXB!J2WpPne<8>GO=q$E+zLPg!H^2^$K*ddZ>#lo%r?roi&*4@{kH!?yr zp(089p6@9?1ns%LY34D<+5=`q#Qc8;^w3jZda~Z6+hG&?= z>xy`x@?61Wx{t5!X5FlwXZTcwW9A?|$BxU?&Xm#w_+@OK&~N=IUpPLTr#bkAo_XsW zL0}pf+IK&HyWpR9z24gLB~qz-ilycYqI86kJ5xDkdNy|NTm#MDsgMs?IMw_Bs$G5V z`woP5z%@yJQ*XjMp;T7vp;y>r{v8gYE8IimtIz_rKH7y?nIW-3a(M1=h+C~4lc)AM@S#a>>fh^J>l zQ}7@c#qsf-*oHC0JcSFzw=KGiG)A%Nnp2)_{VF4|-~8MK*3r=e9D6r&zp?69E%d0W zsTTy1l42YY`-LoD-nLo$ixot~P5@w~#m1~uE)~I9s?UMncX|33c98&*x@nR$f=a+U z5T>E1Q4TV&v@i2P{`~R$^TK7bjE7O>fAt3cvoW34#_)a3sSs7Nm>AG{-VCovchP49 zHrE6MDH=GXl_MLWo`1_L;xF5O6tX7p+Zv^nqFdsf_5WULY%;}&p5H2clTmu$@bG$@ z7L8$+@n5!-!B^_bHV(V`&$>e8U16(i((nUM{wq{(>GZ@sAyMJzB!#_zMX?L<+Qp+PI+7P{v&S@Klz`9YC`5;8y}8uIwxaO6BBV|6Kn9*dNC^PFb;>(!>y@ zv0g|hZvL@cXyF3GX^Vk9HXYj|*b@<+nXcL9>p{Y5!`m&Yjy4IbaCl%x7Qfk?QZUhZ*7FAISxx1)V1}G_~kRx zZmd&G|fq>Bf6CTXlBRP4Q-2 z^z;T%0zcf}Ytf||S(MjCsX7@PTa4L{8E?OwziYicnCH0?$4yQS4h@h&8=de2jJfH? zy6NwPV;tCrwjvLgS_+Ot5}OB}wNxytOI#@`F0GB&?WLY-6O53BbkuWb9!f97;dn zzfl8r!Q5y71>MEH#l|rrSIPcIW8JXv9t-+WnLB@ZRY}TJ`r(wA^8YXZ{>$0cSs|Df z8_ly<6|9dz6|M8LepJ5j*<=P8l?EU?e3tV}%;M@WcZuIqsM{Ni#Oq(b=@7i_;21SK zs4kXci^d`%QPf+=ow84ym(!9ns$n(Y(4bb+uuCkcVaMqd*GSd?l+pbApVFApLhn}(ur^6xpy(q15r|wct3`rbzk9qE^)j{=o zH<1X|&0Rayyd5-3ecY+Mi%Ham<#vm?!03wkAbN->$}t{0y|VYk3WsNOTOn#KnH38Q z@>!!^@m@!n+h%3l-+O}FU!t!IBu-z!ydg&VVjUQfn` zd;37o_XsqEAM=*c&PY9%5h=WtzhNjql}V(h4#F)xxf_+zv^=#|C1}54C$dbc)+iw zk_M&QX4j5@^-;fIPpakh1gQYD8V~`Pn14^9>R*q`kZ~1Zk9f53YhBvn0pTCq_sF5E zO;|7`UDgXRrYY+!IsB*|`I1wV$ueGzWKm!&$jNbg21tEubMC?Mo9wasKK1f?Li}`XO=q{gme#yG*ZjNd+k+Bqm40qo4A|(Nq1fsC|Dh4&UJ+BDhPS%i zVjQ1yJ1-0aAoH0Wj=~GrMJ?ZH>6Llie-NXXFM2P=Sa3v@xm7EdaD1jur0?gJi*&c} zl}NhX;t3X>kNcfLV2!2cTPk88gz5-wF#en&uS}$FrYSRv%aBh^eS5I2xYR|wEMhQcY4pFOI-_J^Y+mBn{BqD5qNv5q7p{@1tTuO9n5SeeS^G-C7v z5aJ@NGN1rtgB&R>{=AwurV;ZIJ3nhGsxZ0uhq~6)zIR<(JOcEJ-#%wsbXh<4~sB?ayp`|2^mzh_lS?(xN4`=i9^0@}pnIO*{_Au#vQKQT~b9 zQI#3O5%8*q7^N`vln-o#8EjZBh-9g>EC3Ru&0tg~7{E+h2yX^LzTaxo-ibI5r5vM? zx;fE+R`5aIQz8D2mqY|N8XD|Xlzh$%YEs8S$j?hnAzoY(YwRa(j~XZUXiG-rJe@yG zfdc+$qKVW8PzWp(xb*-kgi#qZ9Db>64UgrFL(f|7}IhU7ZK2}-F;5- zcVE{eX=nLrp^L{P!{@I6n5!p=woT zOO7^pX*aO7uLy=;|2ppb#>E~#(vZj)_vQ!PRQiDL`p_jr;pZqh{U>w;^1 zC@F20Ui-C7zwx%ia-O#yyoqZFRLEL5!rQ<{pM)e$>rqtVN{;h@2>5x`90h!MfB>uM znj|IeFAh$Bh45C_8q{_E$YGhdPOnOte2~4Xq41DNA>H}xpB^BLAK{*zjp7|ekH%n= zhBrJ)9PZpD=rwnCdp6+K%7RAZ?sf{S({s76t!V#Y3s2GMq#t+9Evio(+CY-{`!)%1 zc`N^wVBoh2eChuboE9`InCK~;_v3S=8Dlo5c<+&yK$E@zxJQ-Fz+*x8BqC(zFHbc0 zN}Iy#88*1D%V7j7r)4!ZCT@rEqil>}|Mlr|TQba7x1ZFDPDvA+VjD~297F4WFJdUY zjQw~EX9;~gU>GviKOzR)(yM-^d2;{tSFGY?RF}jjBBBfdQoIx!%?M*JBizFLIs1xqLGTR_&OVwv>WQC?bq2GM zhW2=}H^d#tBFD4FDeeo<*Nu-J4i8g6upauf~`H`ExYi*y4C&ruC~tHN=R>Mzp41o-k28*-*dRa19M zGD=ZbnUEr6{yt#U$4`aqY3+n+s|vfJX{m-oz_*{28Q|W{R$<_rS@xld;tQNd^<8kg;jpzQo3TaWrj9c(z z#rRwE9}9Yuf!w6p1-eS0K{?Ne`0jYyYvkFb>96d5$)b7G{#US()h8ehY5fR znka;!Y30V8y-WALDRag!>({&|QQ*hI@@AVwo&N(fy>8=vTHJ&QThW4Bm}=5JL09o* zI&iC0IY*VW(g9C7)*YKP-{!kex`VAk5}d*;zA@#rjP1qgR<7;hSSxK}%$s2D`5^b< zmU3ZA+}uiDZRPlPH`MW-QA7ksXVtB%eJ~iIMyI$oo11{3ORJ!b<9KD1U9c*tQx(@VkP<#@sw*zcShk7BR7)5s-l#hVPpIS9TgPW z3~edDyS*dKr_SS6nqM~QdD%n_y`8wJ8jCQ*KwwFkRR77-%PBBF^9l09&;}2miAQUn zYwk_dD9753b6R5H>XSh;Y-{f8OZUTng(+$DUF`$A?;bt;-nk9UpptRV{0*G^?d zcO*}ZN|O>l6|GsQ>!-kEsG`w?v0hMoVWh`moKM6uOQ1sa*t8|^xy6nKb_kpfmke+_ zrHdi}%}m2ueH&#jN5}I`PoM!Zb0aPzl`F@n95uu6B*c9so(koptAZ)-MQRO&m(~d9 zsq6YiYSIllh1ed_(6U&FAS(&_-zjBaDbGf9nWpa$3nT{9`z_4=dRZ+ru-Y6$CByo)Uh zVJBHV!6q{e%3&f~>D(wo`|n4D$J6yV3v%JXC*xk~8|TIe&=4 z(hScXTH+0I(e(O6a5N?{Yovk2Vfi-ndI)%?W+$rClIJ*(%}t1 z6pez5m5lJtGNL#iAltHAPM$9cwAI^HpQ~wt1;h1Z>jDJ=P`1 zBKMvH=s=v{1AER-^J6dhMg5%mY$)j4b8Al(6FywVA`u!|ypUOC3J72?0yRQ<7OBlZ zu(=Gqt`t!jP=9Bgx?uM6Tv3HA%=}7g5jg|caZj7E^+~|Uq3h)twdd|^ZI4R6;HI7Q zq`%eh2!tZL{HZBYL6o&&1r=qe-XIG0wm;nAe}I!-fA{@{({B5Ipm-`rUd*L^^k_!k z-{3PNNzl?7H+i`3+VGnx@7dd*DqEk2r%Mob3n?=JJefNLP-Y2G4aKMD?eBX^JlK*p z*cqk_Xg+!RJnbJ-<cX~c}OiNPD(z9Z2Z)|*hZe3yUoQnP zVM?pmz_Fq5IGU$T7PQLH%>a1BMtKZEyjS9~l7)&1JV{dG>HQmV zQJ$0s9Z4;&GYaV}=sOjRV8TKveAYMuk~o>4c*H|Uo@%)&#c8=yEC4z67dTosO-z+F zA(vG*{Yw$9i3p`SWfF0uA5uN8$x_wAfDT8Kb;EiZf+dq7qB^avQd(`m!!u$v3NO}Q z8k*mdRno0ZbE(5qk{4%JNGj(~xGw`axa*fN+}_RS69`|tX1_o6So^1diyO+O0zn)W zBIQi23}I!!JtHr%S6^|w;(f1zcbAQ9+v8P-|7oRruEi_5|GHyj7DumdO>RP4&^o9i zcXswnpC`pX9^`RB@Rf-1hS3L|CRU%sC(slIeOzHMm^~3gIbTx@s`~I!^0K8;nANoS zFg%>LPD#N7+439O9!|{5;98Dv>nBdfP7SL(AzbM3(g**W`}w<EbUv>Y}RsP?Z z{HnPuczx}!2$1Mf(#9G%zg(7=6v{R5sus#~liiAqIz8w2iBjKpmajmpmw%VW)i~Z^n~(3A znwolr2F6W;JDy~r$0SLvMb7svNQBw&+5V4j1$or*S`g$7yil7`v1W)JBp% z6SiE5Ij^PGxxBr9o6*?&A{sLs;?(n1UpO750+~m~iL{nscN2PGCEuog7W}q4$@6O$ zs#Z`ZyA78GJ~2q2bxnl$GIgY==|YhxaV&n5F_lnPz)-ong^@YQDzlRRat?|{1wTJ~ zB=0%3G|?9+XVg0?JK$9&X(EC`+@L~9vemnUw{OmaMgwm9g6CP{p++~(*}YwN_%g%X zt`Wvm1W@RFp*kflC3 zp#_cv5fAVGaRF=wwoYE-5BUTsUg6*hJr7InQ_^hRQg`H`MF0ECtF)yS3YfZC=Gv72 zhSuA~6PmN5rZu%HT3Fp@wwlsb(J}>^tZVG@u9kEIer5jJjz}#o97cnm@f7gK^hR9j zt=iS6T}j`1uW|7n+29~|FE@y03tvFP-4+9+zkbt)y>o{TiCv})1`jT^a5t;f8t(2f zJ2DhUjSG~|IruI$8H0nT2H*LreHzc6<8l3>P$}E`G0bwplQQSPr5`W=qY|4c(ZWST zz=coqR*w@W+=;=t^CG7`yTgKt)HQE-z`U5OM2w7h*;jgT%H|A=P z;nKvy$Z&5KuuyNRwxjII#zqf5B&n}?d;Bf~f-fY`IG_HBE?%e8rQNCr%V>~iA0>1{ zrxMlZz2P^x^69li?Lk=l{?Hk<1XaC=OA&a5|@`N2qQtag! z&%e59)8o5nX2rza#sp<)C`Vbgpn5|{lvd(_=&wUxu_^f}cCC0p+KF4@gOd1hcTy3k z+k0W0Pj=~c-IMI5JYBuE5@d8+p)L77(z=W}p1kU_P9$S~S2~L zzMN{cDM5-N+xgtb#3+cJaa-soJC5Ro{Q6iq_I4gJZ*Ob{9eV+?hOEii%L>8Uozstd zw|~XWi{dPg&{CC^7_)5HUxN;q$Nd;?5D)qgL(``Kt!ag&iYfiLXXxy@sl6k*wf+6*g*W_8Hvej!_8~aFb^l5O+ zc6t*dtiF2X^*gLcXb3gA{ znJAEFOr+@w_Ecj@k^Dub~xLWa9%)(0rJ|(icxk+1zhqC}3##(Oqs{?zbG0e>oza zlv{?(5g^$Gw1}{vZuO#>KUY`cUr%1w(7fL-_sJSROry*HCt@X%!<)@D%RFuuw0`*S z)dElBVf!cIE@Gjd?|NHZZ7_OIae&Ja-zu*iwDM;{A30*ogm}(|`Yi%F-LS)jwkep` z!!%i;5~jt;-5s{cX+su1jRaxlj_SiXuTqv74^zaIT?A-{7iEZxh#~R}mCuA^YK>$me zt9!s*jjm>$KAsCp6o?8DK~j~((@wu;6HUxy6RZn8CY*X~_$a+XJ`BI-+uZ;1(ZeHH zrFw1L_FW6KYloPI9BOAJtm)1tERruNXia_yzR1PlnWj%c%pG)_>VZp5>n++x+ z#df2W%9u~Il~tMpn4nr|iz%fJOSZI;Y5OkCx6o4LO{nG=A}mLytx!65rP!VxD5{p+N7IYm|_n)_h& zK0aLHEj47fp?yC#>EdGdsjj62>myLqt3Qzzt82U`Gc=AMiN=eLx)d(jjQ!!zFP1p1 zMj`oKr*^@UlI1Aqt4;em&--+jr7Lvsa1+(x1c^OVx?9}7E7qt6S($TM*+VYEJ*NrE z|ELhQX^dv}N^9-6JYzYmImvpc5gE4#P_#+Ildd!`nseKHcmv6{!`!Qc-f5gS>a!AI zM9Z9KDsPR~;2HNwX{6aq1a5Jf=>ax4o|N{|n@o+1xd$$jE@TY~Fi%9MPYO^fTe7U| zN4*;5EYwU}O+#aBgjMeINb(8&1iK<>zFgiM^?!OJ)91kc; z>9MvmT9oA`lCSX?e*(8Fks{HiLqs3nxfv;62U?I_ESC zD2;@Ic*xdxDX`JMV5W^AbAdbKl}UlQXtnvlHGluemPbeF>cPyZ{b$N-NjEYSh3cdN zihT&Uk>9dj$8b zaL$mov2k|c!9Jw#A96P$rlGo6q=Pu)u+Sl{=19$6RiRP1afh4_6S(k|W!V&Nbk>4Xfg_yTztq2oh#XXua$|8$ z6|B0=95<1H?Z(RphhX^dQNa3mhLg~&*y`^AZ3VDeT<4O^9gh!9auqd_Z38!o(IJ&) zXMU!Pjs1p?haPU%4ss(L!=5eWBzN#cn}G_M)|-yE(_pnzB*x90A5AnJ1ZKE1R3A;CP_fnYO-rKeV4691}^1VIjn z)DBTtxOC(@1fZf_W!9q4_|p^D@Gw=6K^($#<%tQr|?9cg-0GE4^Ye)HMjBg1vDNyXSKF~;CcS+h;MNJ$*xa0CK3SZ-6=Da=IZO)Njz(xk| zz@l$#RfHZIHmt#+b(!+&L-1|z=!8cw)NQmTxinkvdhb172*A!KD`#@Xg%wLl!>I0HGQ9q?*o|C`GJB#L;#!%xiB zehJ20ed9qS#23x22KP4eb#J&`uq}9bTw;v5XicN#Dv9OrWx{O9+wu~QMGnN_NDcRj zgq{)$C2@A>l{@iAIihYSC|OCWmF8uu2OD?Pkb*L2WHhDUNY(i_&B31klfT9qM%C%U zSxtGUk^|a0ZMnOw+o9Hr0$No(lwkA)^vrD?_a;Za**RGgf&9poi_gNN+6iw)<*t0` z@E{P!WjM8Zxuxlq(MNsyQM>m3Z5 z$rAl^@!g6`*AyBt1E#P#5~ME(ec;RFRd`#uguIot2|^BSe;x6Y;V0OtVe2|lG6+x2 z+f1r;k@vgv#~sGD{nu(Y;_P&bmx#rX$lI?HT~Rm+>zGWtC3`8asP<+R3(x2YMvyuN;~TSVfIzk~lT@ydwG z2ljmL51gKbXRKKo9o%S^ zid!+=No%%42M4@_-GX6lgKH?NO}7Wg`n?lPQWG|@v{0pcdo#}YnEXT7WycldM8fsn z_oW5zs`SjiGwK&Wdcgh%SP)mH{$N8v=j1}&M5d#<)m_9t$P-nTKHXRp!i zVSD){8@nz`t7*=}a117*ptc)NBX zYxkf7&sZRS&nS$LF#QP^t1%+#=3=m&_wN(OhRh|dka0}tjEhH)CBJ1MdprE7W;}B+ zU`WW!Civ!oUA94q{B!SW9$J)<{@lFA>ev4W6#Z+Kl=!C;W@UfAGgGEevX^@>`lLocV6Xe}!W;4X+ z%k_)eu~nj+gPHMA67=Iok&0;imNa(9E~JAc>Ei3)}yu})k;POjs??0cJuFOZ^U-6I(DHIASA4~=J3WHhltR!E09@3I_e3Vk=RQtuj5~<@ zGkc(btQU&7^Wok*7g<(~MQQ8SgHGvq*bZ5Yn8x~Invie5p7JS<_m*5MIh^f=WB z`cJV^4aJz7*ypKM^)7p6^7s6s(dXfgKRhHGi(fgJYt;lvX%G;!#I5T7D5DZXLLD1S zMW}gwn5X766ipnH-`)O{GRO!S*%WRXR%BCvkQUtlMvatD>bpN791Afplr-$YRU{r> zdy9a|?wst|=O|~S_-{M)r?vKJyV zp?;k39?iAF!K_Fk-x>*SKQsKMOd3f@h#zJ1L%Ykfp?|H#zcy<`#8f={&&0CG`;|S& z<(0q3r9OrWF{0An|K#O(f8dkA{9{-z7=3yg116-$x10sLAPau{VXD0`TsgUR_=le6 zc@?t0h~_hn<`DU4*GSb5o|NF#d)Hvn2acB+58CtHk~s65=@3 z>8V8*{${a#kSsx@Dr!vbpLIhuCrQyWDBVOd8jn}&o2JxPC&v^LTj7@SxAwAGLR9!4 zG>dz1s4GM--T;LZyVBX3GTUXLLhBJq-QvHrF(>PY)%+yPb$l>^iDFNJkPvHBE_Cr| zKFmfIT0#Z4c)5ZELYl$$ON%P4wz&?`5ov__vJ}rxcA>^3d+uSSO+CEESvTSAsBd<7 ztHuVqo0~U=MJFR1AM@MkKnQAzOJo>hln46Q)M2ZNOm%A{@6Ejh5pg0oEhg2OBb4{C z)!8R&2hE?C%sKqN8lh5bf@3b{?iZ}w0i_FG%qb$SeVkz1RHZn|Yyv};vR*zQtIdocvU6vvM#62r6sk*lwXNU1<4 zpNFGrjK3KVKTNlKXsca^`{8ppVG% z`r>QM8BO{&^!|RFU0H+`r9;+Zt*3QcE4;ke6w8ZNhwauRc|;%An(>vAC~mT6)9^8J zt60Ai=3Qj5t~2ro5ANSQ0C&GG+Akq3QjDf5>AI-0g6m-4XZPn> zREe~LZA*fO^M=nOF5VVlpIjCo1glcA<8-S%bP>s>p*aNqKjCuwXK`L-Dr9N3GWton z&A?3|`H$IO*ermg04dyA3mUK$SzWlyv#W@#z{N*C5#Hz7wB{Dcw)?{bi<@Ria~?1x z5ni&ad2!n$c3xj!-a1}`^w7d&TE63-h~CmTtBS&x{2gz zamH~`gIfbPL>xs8I*rsw-cLVEhfKOP;2?2R*5b0y7{T3&U`*`mj@}~|AGJc`ZtJDD z?13MR)s8Y;7RS9tB;&E%VL6Ic)%3PJV%F7WLUf5u;Q=p1vx&bu?Vrl5*D;J3asSeb zm`-EIA7B?P(m$zR^Xu)XbMa|CNo3(;S?gu->GpVCxnmD)MuCuGA3zBNqXzk z#?Q)Oup$dVBOz2mlp~F8O+|B!jPhW%oB;0|uNg7C>S82sAz|Kfw82T};Bsby%uDoB<@EM$QT*X7$)WN^lOW!l({{kB#qQ8^Jmd49)qJ+WOKE}?3Ly*9BW z$r8d&VT&vJ)urk;Kk;m{*Qc+ymlY`{7Eq~HG1cWivFg;@irgt zY!v#yIQ?mTX^E*(-p8rU>NpYQ*vyp|LoPyA;*`HlB@-+ZkUAPgx8GB3d>E~I%KlQ3 zPlHKxxL^RKW;akc72`#cg5n+)ShNyZuo7X?v<{K`XmRv8kf7etmb+kW2(tB?#HvZ6K3z1oVDl znC-9j^?}9ZyM&r;ar+mpzz1DePvUOOe#dcQ+n{8dq_WESCy*sTM@W^}FG4LF`E^YF zRpnquW;&G79l6QFt2nlgQ9wG+P*8aDEEM6u4%#W{XTxM0d_ryq=oCg*eA$+m@C;VRe1{i!sJZ0Q7s#3qVI2 z;jNt_wD{eMlZvHKGndMAGT10gRSDZg4AqDVut>uCbuX0VH4u!&vvvh&suM`1=S!ws zfRy^r<)OkmjYXrdS6v*swflL zr17zN47rFLK}B0QXPc`o$)ZMEvlcLRkYik?Xj*@rtB7sr8<%glDM zH^KKFT1JU@nKOAfW@g2MK%&dD`C zI!nU0$2a`nJsrc3=SVQ-wNsD8QokiKubinYFUwfB{dVl$t6!T-Y9$tjFobXnUMyN0 ze@}nFr^%Ww5=WtA(iIE@A`aL}-^;rqV;)lt%j_ag$G6m$R;Xq@V0e_l==!3;tOCpw zjw{h!yxq$1an5C8%MV;P5Rjg`&i3GS*X~oHUn+u|y3$_rq(pPY->eXDy659F(o>Bk*~WZ{xJnt@${|+4j$E>y+&?tqM{`BP?-zAZz5`{d0s(& zfhhokqwc#$h#ojLMee!J@4U@&IlH$g4 zj1?YHw$fG`d>L~07MC&eV1CUB1Cd*IRt8IMr0$C;wD-7mXqNP|>D zonFNjl?3%?2di2^^8+#sRqjNR$IvHRI~ZJ=hlTEyjrrhDe=6P(hie`EPU47p`e*@P zWL8m(2fzEV*uG2B=n&OzGOp;WaTS$RR@w=*8;m48(;t6`Nc4d+L}URFxJi{5H#k@l z#2}%&;$AvWv3gGpIPC&+`P~Ur*u)qo(D}E2;Ic$Uo0cs!(3-Rdd_da8r ze3nX}t?!pbryRw?$hHxCBQssK2KCqhToNOkjSzfZi6WVq71 zRlbs z`J_iO!KmEet!EB)1aFF4l}f=>&t8bSIbcpvtc9}~^cH$F@fEP;{RN4`Yz(<-==c$2 zxOUPXzG*^oS`#*%@WLyC|M)lrW@e%B!iYK^bS8)f9t~%)j0;0eR3;zcPj`LA7vbF= zOW86fB|p==`hnMO-x;B9X`z6F)h{eA;l1PRDjRbAt@{Q3P7H@xvZAx1^{2s0nLr3{ z>g3BJ8e(ZWHSd#1#mgdM_SgD3FCE9_Bzg(g_k+#Nbod&;C}@?xJ> z`;}G1Vnq#T&9(l@bzAzC^zScg)WyX%CVp1IU+%C&f70;KHY~ON)qL8?eyVwXm2VW;g9zG&_0vzImA|9NXI9hzGMcnl|MHZ7JE=YEVC;8b) zk85U&4e-Y9GeffqBZY)Icx$_M#LoU##$jfpJIS3`5W&B%AjW;G^22?dIOSdOnXkfs zGCvShf16>Qjj!Bjtjx9}+5nYzCPGT7N}jVvL{0a1zZY%(va^Y0!~N5&`rO?!oIW&nPQ8*YFM#%R;^gsp|OB#gHW^@KRTR6ddOpR(?X+Lgx74IotQd|*Y_tV1tAZ3 z6f~DvyRyw)Tpl^{rz2QLC2^ulJG$nAi-2nPKxsSh>azhc*_?!J=wQ3+NkYsEZ`oDy z^Zi9j)2b4i9J>#h#VrRRp4)5C>3)u&-@ypI4mLuu*@DW40eSIb>OTPJBz2iCHaPO+ zVV+FV^12G<=HPj}E@%+xP{ZEvu)74;sMvAR#a|+Jf#uo1H!t6GW4R3X{LJIoJMjuu zo(+DVSpO^Jv?x%WGa;Wh==XF@hST z!$RXVadG0BSluUr%Y}@>PhsdqaT)feT$;yAF8M##pEq07m{j6)C%yIQg1O)g*G^26 zmE@Y<6fqw*(5Sf}uMD0fKm2mW%O8A3$BvSy>V6d4`&wMpgXZoqOf(yE^gzN9X3RRv zh1Tx!y`}@Lfq6?7tj4{%uoV`AZzn}f?L#C_huX$aa zvCNTpeh9XYt!+o4^-tITmDm|4TiVdD_jvXMwXvB+Fx;-+e36A6NP!&v`0o%`kG{<| zIL)=@2{R+3PW*xZJ}%NZpW_MA*JZj-KoqSTNKiy8Cplk8M9KH_D_F4p-?;o|@1bph zR%j{s7JmID8M#&lcJCtP!5{0MAKiR+{hj?2HTh9tb$WTkO2lRQ13|Q|Bivc(y+rq- zjk7p-Uh3osFRzi;L=9dXj%2({8U!!`A%UQvr$$kajDI}#@J51~3R|?A@ZCy-R9`8VUc? z5cpOk%wWR=)4kB#vlVy>3koJvPcen(_Z)@I%|9AGvIA`aD;?6P%8f*e=|{)IO9l{xbbmG1 z!*zY6wqHw`4P~#k zA5m;)@A-{9bho;EjtQ3RFDw~+=WiV7P}TIt#qD-n;||QhH4j0+aZ55i&3xlB z3TbZeOF8J5MGRGAo+)Cn3Xx$)eD&cXqpW# z{67t^uQBY+Gq#tdk2lmL@mdUy{qE#Jb=`CxecTc{jGay$<|s=r_LYn2zZ6d0?yQXa zvs<02``>CM4tlTt3&oq^_a)FrRkjFK#cZFL#ORP~$z1VL^baHT@X7R07Av2fpy5+5xTWFsrIbojua}&!g$c`?P;TEU!GC}8dML2E z^|_;34jbH=II9NI=%M%cwj~i&pGsZqkaZ6l!tquF{xdjuTxh-FNwo`UNSR9|Iu*wd z_1rR;z>^03%2;F4>m+hUHMH6cSf8cqjzQI}jg^n#lV-6MT+ko%x`IMA= zvSAe$2RZ5@fwE_^RiWr=_nVdwB0vUv!{jc%czPQ~2udIBw3meR@a4r=k%jXkBC6v5 z+bk_e-WE4w$d?X>v^mWk%Cns0#T|dhF@>;u8B8lL#j$mm^MpF)jLVtVP{L;D)&Go0 zK5cVvMrk`k{Ls7f>(F6Zt%e1cO`}8h;LGZewtyVu=k!uo)8${mm}JWNEx2Wq~Z&ERH$X@;W-0}6v4yiWX=b@X^TzBQ&j&%C@ zx3uW0xJ(Phk24b6xm$|XTVpjf16#~dLWn`3lL_a_U*RV-vHdz!J=Ys|ew<2+GVdJ7 z7)s@(GnCz`u|Gtq@DEo0{R2DPZm17G9Q4z7HHDCFol3_J;<^h4YhVgntSwwtP~nol zdz-PIuSz9$EAB+aCZ4U%A~W~$Vy_nocZHnCc=2wF?`+{K(^T1GYB-Bjv6D55wf(v5 zk+tZMH_WRH|1<;V-gbVDpcxNi;NZvF|7ro-2;En|VH$h9u9dsa^If0#rgIR3)YJMh zhxUQ%Cbrmnx`zgWW2Uv7{;a%zC)6~(b=BW0aTg_0o=E5x zhO~0ouR3#Th4k9=VJJ6K4Aa}EkG7W)rkTsv_3`H+RA&_MCGpYlnApQetB}18j~z4k zz6?Ui!agncWQJtA>XVCe+Gf?F%}KznaCH8=DW^M0)3&>NsFi;hGedmyK1YY9HLrxq zJhIrv>w$|}3T?ydYy~hKt(S`|s!u2*c_{GgYN)Iz^Bt)ZmcWfo2}{yWM7u@uKjeZf zcB^ybTe!W8&u3%8c(&`?czB*I z0>d2$=&k(OQR%dZET;K3_uxjD+TL4?c~mK;S;2=H!lG@+g=yJRaJv_h25q~5dIjTg zsspzHTN(c-==dt#;NJGSZkQ@S)7VBrywzC<)sR@aaY2D>c1}*FQcz6>(~KwyqQC{& zi{hhl1wm~I=s=_Nh{}!NVRgSy=%M*!hFcZJ`+6;N(Le1V4{f0%ESvEMK_gyGwvxF* z)bXGoTaI+b?`FWcwSFL(4zbEw6>G{v|5V}EXFarcnQ?b~5U!eTJRSxIlTNCQUQlAg zwV5Fg_ZS zI(&y!pY_fe4PKMRPIY5^Dl7PHd~7T}e*UJmHb|41brxH^GC)1JufdJ2W4>*>A?*hp zb==v7Ljm&-I?9EPp^|g5=s%|A=|f&a=4dh{V$kEavEi&-K&+ZP<{hK8Bh*=mcb9qGiSC7aU=*$M+Z#GY4l z^lsRT@B%4_J11~LuCu;|MWeIt`LmUwZ3iC({zCmP|1PdIC}P;K`?t!tAEhL3xY!F@ zt4h%^*!E;fz`DWHgAwCqmX_XOVWSrpGFiprE}}c3a&pQaVBIxUGh5C-8>&KYe&?S> z>Z1QqN1jxw(+1|k@U@J;E%tebuh=Z~sgTZA&Nat2qvxaQRy zf4ts)L=WGY^E|VQEiU-f-ifqjd3$3cxUeW|%1NosqTiF|Em7ch)M6OikKl6RsC_@@ zsz~!njW#^v{@(XX$ZllnPO^^Wo6Z~N$PXiS>TIM52?>9EB%93bDEkU7wqaK>RcTjY zWsp7)QFmzIJ>lnBf?K8|wEUfke-y5_e<<~SAJ^L3QrE*aHfRQlLrhEzbjmM(=&HfU zl65BW24zYuv&ATv#pbQ_xE|fJt6v+Tc+=sVXYf^{mALoTs80~%?;Aj9Cy4E3;x}(? zZnl<%?*04sxP884ej5#mQ$kngtiZM<{oDACn6aj`R1GoE<;jkr%4qaIaqq@+GnAca z6;i!qQd;f!^i;A6KPDds>FZc$;v-4>nxO+${=hsb+*y7Bx#;YwV2%U?Vg6|CAP58r zaC2D7;M>^PSm~3+fB%x+yutdlDOzW8f4ZLjPDlaA&{5McJR^MAJD{TH4luR=)jTP; zw!@i2laOtRBj@Y5oA%St!U%tz{7tYv6<043!WQF_%&0~t7k>He z*!GcxxcKkizhBzbM7vI#zFOG5B)8N$3EOI}N{5U))!hKs7$bc#E+b6`X1#Sgh**n& zrUvz0yGQVCG)hS;)a@+6@*F)p%U+y>Zu@P6h-C z9Bi664Uq4~MdCF}fWr0(xiP-!=h!eEh{|`#+R^KSEh2PVGkVLJRkgF9J58bLH7X1W z49Yx%gR|{JEiEjBXb75GT9&{j055Ewb*q#)Pi1c6ci@>f{r*%>^9v?{hA9svoAjcG zV!x~$-Qb~!o#ErY`&KGf)1qz4tpRK|Xd^d6j;!F3%|CD?1iw&&>H%$Q!%R50UNii@ zyp%=Uh9c15Y>=0aPtW)`aeqIMvEwUB1yr>q%U#?<@lc}I0VVIvbsX=hR39m+uee7& zte(2!HRvF+xQ9lZ@AT7$oF5A!N!&!J1N}T2ri;cMKtXmVlzvi#JATakm2>Z~PMX#2 z7g=nPRxhSS;oG{P$wh=ZAg_OFiW~$4aI?2CxHPE5w%TZH)9=d^puKfz@;s;qGyMGc z1L~}SdH;bI7d@2xN<D{$@Rvr5qNT!UH$;Q&;az~qR)fPfbQl*{_&u$#4*gi^;k z%-YU=cB^aM8{<^H&delf1fQRW==!bUWHfNL8~mV6LEVALqddexyGYF5o;L{Um@ffA zof3P*E+woT0ag+ij7dc#Fc|MRbIIBjp+T?1uR;b_(k((Yu?di!%Sxm(NZ25#FL(@W zZn|1@iBsb5ZvEW!%LbdBkeCSk;^Vl3o*hk^+#ak}F%4fyJO6*(6A@Ku$v;`q`gQXx zs(`SAR609Li$-Hk<9Glcv99hA8EK7r`4_Y^*X?##89yuMQZU|W)?2$8db#Z)3NF7} zAz=ic2buK8hA$u958#DTQp0l2{hqs_gB>1CT5k*2TmFls4T=y<0`IBh`BsuOJlOF` zxG4tikqiV2QWab$dI2a&pCz%T~sQ_JeC&6j9*_a=gqvjGQf8I*cA|zKT zw3|okNaB+T91-;0>1V3-hs-74z08Td#>D=*z{{)X)E-3i;umX z?54A~IjxNIUd?}y!LwleZq#9xm`sk@OE$f;RRI430v)H!47}xJvtb_0b-)5S{l@-TsHUc7 zz|j`%MVOpB5Bo~%vTT{ir98^kJ)$(?+`3!7TYwG)X|v#O7?BM5(ziyL$Hn7O$* z#)d$=np`8B1a<4|oZQ@ zz6=oG_CVuI7#L8?$HzyBj~#em&@jCMf)qF<7*ah~@9JY7&hk*3mG!VF`gV-F8cn~V zcIuUR>MEDh?O`~$rSxr7)LmfAC&xR?{{f*JAb-wp?=`ev@M^7ob(PYZ;}HGAO^1Qy zWHIjXLx5CRM4NZQcQ{Th>1bM==-j=KPIN8^ed&LK?eS|x7hXS zJTF!F^>9Mz^+t#5=Q-)H3oCXD*fyC7QQ{wir)|Hwlxn{?dp&e zd}#gp;g~bS*h=>lYd;S`n^sC9KBj}HqUDTl4Di<0M<-bNcCZFSg!t=0(+r~&Xoi-8W(g#LEgP<02dHw<5WWX|#L@~J-WVG68*O2*s z!2X14OI?=D69y|k)_Iv&MCOZN{e@TcA~fM8In|~l**l$4Z!32AwnB`N&J8SV7fDKNxYuMhBxdw;$0Z9UwpjRdIU!Q7n{uvCGO zoguO%;|?A{K|{ceRa8^}(gW&R=|!} z@l2bWCDhb!RR~SR;7%4GcbZfHYSC1P1cl4+y|qbpfSU5q7#Epq4~I@eAQDxJG$vCD zA4}U`Dz2S{hx^@eC`*=!xf$jN{ZI_h-);FML%-1Df3Gm13Wsi?U3 z_<$B;+VpSQUk?{ygd#CJ!!a%Dq>Y0tkN`mpq?Zl(4U*D;6!mm{u00MkCkI4rfHImE zjL3=7&#EM&%!RO9V0x*wJ;Pts)EbY2-dp<(@%4Y45ztH*G{FD`fEG5Ifwh2Fry_{L z;2N;29)PN}_-`MtWLSE|ORc@tqt%jN$85&&2crrDGY}WR2k67k-#g>tF^YpcD*P6b zV)$Xx_rKI zbMbi+Y%(?vce*Qr!AY>HCFj4dWA5zm%br*Ivz%P^7F@Vll*#f|*Uj%EHR|2Sl*+r3 zR;&jvE@(kSFs+=G%hd!}FDEZ=cl+vc8^lx4G+A%U95W^61zodhc+=Ut+A&>H(uZnz zUE3LxDpzGbk`sG2Cc=9ab_|C)6CRy~UV_UwcY&>8cs1bc;xvHegIxjPeVOWy(%^g8 z#W5?gWO-?wjimCccAAXYX5YQqlU$ z4$9^+wH@v^YS>D5#xj)$D;kCddUl%gF#yQ2(d^S=E(_cr;88cIxuqqzIwSaUCoz0% z&1d(Sz=v06N}iwO`K13gr!OA-Ewj^YWtsQq(dQtu?8?AXV2SV_{P=J0>Q4K8#(Nl1 zEAQj8vx*^(@*u78fedPEVB7%RjErai@Ca}-;Nm5d3mKMS3m_xh1+nh?#sTk}e@)8M zx)WH4Pz|bR{%+*lJpT#J(1$)96>*$fKNu*3vP5FqI2=rYy8`9$F^fkNcQ z3e|JS#kv>8cfQ;n^U;b^*$WB3EWE*b;$0=FvViVs0X2{}m{@ZaU@r}mu_JYFKSf3O zC?qciG}=_6cs!nvu=fo5VIhG?0+yhs{qlgHPd41H$&BdsO?!L$^9yi(_V3>~uw_f) ziKPvts~w?e*!H7GhH#C%Qy!>I@Nn;$Ukl(@%Ye-SC3oTR%Aw~+Qc_YX{#p&gUOMGkr9pOsQioH*_Lne7O1JP8 zKSOkM3X2921rvfSJ$5VmWqRc2&o59e*|ulIj+_oT0@)+pybIs?TI_^A06|ejC5ncY z7DO-OJl4N~7Vvg+$*$f#epv&c4xq{BGfHGL%yF4|BjFS2Vj9AhA!7Jg0IZuoK^UX0k5|UfX$}%l4G|(eS-cJdtuD(QT_UfSB(rN82(-5G{!v^4B@)A zvO&IrcCG@WOIjsjZ8C7;1L#8-?j9DFXOD{on8{pQaR2<}p%{SP^;yH5-Re7a1KUMb zlz|@`!07=%$m>j+w!VJT;j|;7r=bDIFLv?Myl`#w?f!W~gfLL!)6b{@FSkWS3P(3J z!QMO3%tSb@4SKo&qj_dA@^ClW)nFuz^V5y0b3jIcxJ4St2vEb)5(YKMu>TK#AU0u} zFZOLVf1vr6xx?seUp9U=&tbv6RyjOqq72GY;*ye!%gdSF-B>0gYza#)cOqiFXQA(7o}V9Z zp5jdKb70%XdJ*Zk|Ag^?BCfAF@b-fs4AFCQbMvxX1c!G4eF|!$lP6lfP+7hl5;fFk z^R7m#?nsx8T3*wPyBLlm{l<$qN@!v%c@)e`0-*uKh(@6NEcGV>5&%R7)#SIDCEX~L zG2cVR>|xdZg7Vo?e34!?Myy7i;6{t8$&G_QM`!1Vt8+pOL&nAeco0M`5L|&07N8V>A~1DmgEHX5O&*qi@v5ujhAJ-!d)b24 zTgreqWC4HPx09B5RBG(k-JPdTb3XYiw84-X$7 z{LEFPKR;Wwq_j0B?3XXg3M}Q2D7(KahlyhVjDY%~?xq{g=_-uc_s<-GJa}M;ku1W) z5nfF_N1@HLe8A98Xpj!yzq;OajWV0a5IxpE&mn6U(~redTFwtO_WyYl8ZwUtE&;+c zfcRJ7wjzwM_va5s!-(x;W4e-aX5p0K`!Qt{2eDZL7(?~m@y3pj$7s_0#SY?W5azxm zK|rAbMZpQ3M+M_J8k0Z|XiE2JpGRPI^;Z5+!m13AUh&|tNM5v#EOr!6NU_HN4G=vr zj1}XcK(IRT4;IY^v1aY)Oe9+a2yBnGzWeXT?=kzyV+gO<6WMnv@^!ot1>)vZK zW-0-axx0Yx@%^wg`*&4yR6HBMJdm}DHQyA=RHv+Hr*8l;^hJEQ6!O2cyy%ma33Z!o z)~Cv*(3$Z+jXb##HM!=c6N95<*P{`>r})h4?#)J0fy_2Lf&HM+z9n2w8qho41c$Fi z(TDv8xfLjt=%%0bbq$)uvx93+iOr^!Z&#Lkf$u zw}8qV&oJlK08$jtoa;<@ea|1{hUVis{3PyxBrK4yK1=0bv~8kJ<=Ej_)@mPB#;r2o z2J4Th6wJ&NT73q=v zutsw*H_j8MI>}tOsIoF=fD+gIT12u(dW@@(DpS!S77~+wq9nrMgn19F%of{4cZpj% z@<=a|;Lxi$J~425Fd*PJNKDhyrr2+8?w!yuDz1MI-&@7-Oz19 zs|gGUNT=@`+~+|;Pf^AN@%Qq$Uv75~Ay@LgOH!q*l6#{Q`RpfS@86@(AJYP;Y-t}x z(GUcE`%f&Mb(?e$Nz#GMmSP33?nz{iL~i;4Ncq;vNIYzzTHY*SKBIrqU61UF6l(2} z+T#&T!b-r%Z}}TiPW5E~RldVQr?R<4P(<2OEotT^nq4%i5!%=NLVy8Z0fb+0)Q0(L z6AeA-roZd}p!Fb#cDi0r!^VG%R;uKJ(Mzo52kkFEOOoz(OBydS2bzhr-KKW{_4=9v zd<+ucwO;PhF#VkX2tS~9xP2DHtTqbcGi#*iNQ%nR zM^XIg5WK%5SoGtewW&F5DM+J z?sO0VPvaRLu2qHnuR;itlv_jgy4|ds;Tj|%mIyoModF^z*}9qk&fVyhDm1g~WP@(9 zbGx&xKXsRlwRm_y6+BPru$c}GR2`rZ0HgU&(E%XVpsa|q6TI7+k7VL##Mpoe&PILM zHo=9KL6K}(L>0TK8gRfn;-Zi6ikBV?$7;E3JLi8BG|TTH9Uy^7g7N(ySfVm#z|$c- z#MKD}fq?mYBwMUC?7}41$-Kha_(w{K$wocQ^g``Teh>2T_$Ov+RJDDU9ktBNnh&Tb zh5+&nOcBU|lVqe;l*JNT0I&jz_JugVKTsk8{JM62PswA_x-5qGpF_H0$k;SE-^=Vb zAUBFQlK|aMhj@aFPk;FjOmkeg3tnW>$D$a*>YVa)^7-Q@Pw?6dYsC>B@97NbD9o4= znV1>}I-eJ^lNV(OXaH-GFVgumxdwonX3OmA#TmGNc1D0Magx>o@04M%zDrmR92?3k6vmBZ~mn1(XBSnLTqvNLD}@ zOutJSp*E@9vGh5)-;{|SUCyn&Sd^4F$17yCzkLdqrJaVA?=yfPq*{e609FS@ocvo* zpe{N%J#Y>?-%$ZYm=8AKcR)Z4q0=$Ptre_O+j;Rh6#vQBLotlX2lO?7QI>Wxd#zuk zCL2~+U}B*#9SEuiL80430*s154!KsJIjr>C-TeHqdl*$p=d=yB5;(QrmJo1#OWVXCPJ8@cJpr8M6`=+NSx}Dz=No_!2!cJ|%%HyF?TlrNQ$Le_F#n?K zH{!7>Sd$%fQ1P{{{y4u~a`zjg!L=>y0#oV$TRF+GfKKu_aXz8>%KEJJHbHy|lg;C+ zZ`5tqDD%Tc|}3{G>>j6skhp zxI$s`Na@cE$!9FxvkO`9pP;%x9Lbolgwdfea?A#@O1JHsXHHwi{ujF9=mq94i7`4s zD}?{J25U(IX`~9DGeea@yT$LFOzvkR`d{>bnDG=50j8-g&9?Z1`oMM50!hDMvEzhW z!x!X6;;G?c!(&c%kdk&>Ke0rrr3SF=-!uc@Ry+ax83@86i5ap}zpmRLY9Db!rT!ZE zcsLrR9%l`t!29!aU=zD<~Lx zUh3WFsCo|=8dEPaZB}clvS?g0W5}gJ>j!x#k%!;N;3^5cvGL=q+fv|M!P7yPECI&b z%6zYN=zr_iu3U1mUw+Dgv4%m#>q)yDV^QU==q$ad;VtLuh;wEFKiBE z#F`h>$fgx8f)g@Mzo2=zjz~5iyl;N;dGqac84M`;05Jnw0R#u280Dw3Y4&_j(I*)r zVfBj9Rng}&{6m2m7gE6^A|oQFr9cD%o(JG1*!dpM9O_5>#3z@WdkbT&VQYu61WZ9| ze!>++(7MLXY~zO}b@u6t@K-?X%8_lym4NIMGciV7{2IhWvwUf90P zPaEpN0sVt?srn3O^4d4bKH-=~AM(@B?&!70Y?-F_4h{lvMSA;Yz3ZflC=h@;eP?}! z4d368Ba_KVyv;%Wio_BRB^cn4Bcrl222r2v5zzo0IKJjwY4<>45KjQrUJ$XqH=ay* z=ElzqasNbYo@Fr1r*9#uGT0Q@Sl-2C3FF_iZAbK9JQqqVgeu(|)xD?D#M-PF&EG3H z%YL?JYIy~<`AYkuk|S^S1vY29u<#jn_~Ony0hcX2)OVHqoT9f$qXMF|DioyLV*bXX z=gpWuhb6s$FEJ|OOf zT0al9IFm|N8p;$u;hcDrCuPpb1c8|vgcCgr5V6o9j ze0#}~9o`#(nm!|fB_VmsvfqDr?|(!m=>#h9#pJ65eXybozWa^5&ihYO;=zu@yw;cQ zw1Gv5=4Zrt&SErN^h(;NT8R4^l=Nj+l+np`1u!ULbVRFq94-nA<%9(%h88<$ZkXqu zG}qF7j{aR9G8DEob~fCQ-9WDvVj18B4|yh|O(w%cLKvTx@4MTO7p^3E+P7g>4$8 zl=Yu=Ozs2dBzemiCIzPXs&k5V8y19jYPMG3sKUWoYpHSa1(9(YulMJ9C62V0B>d3` zS}mxg1pslLIS8kXqSNFIsuPLh_&=oub@!6Y6l z@3?@`t6bKMVTG(K=S5cdWV!3jL}ojy2%FI z6ep)5syXbDDLU(PB%XzY>&;>PwnCKQXZP#@;O8PBCOQ$KZZCf2UtS?2f$cz;!ocht zPva&JXk7UCCSI2y>;OMxuN#oc}#DUiPhG zLTkV+d%YeP%H6Yh$144U)i$4d^VfII8#O)uw)Rl%j|9u9WeVk1T|1d_*SxQ@3_)N- z-r33nQrswo$r?y|I+|_%kTKC&+j5ug7kZz6+iJMrX_Lf{?Z5L;>lYPwyrW63^poc6 zOTt$l1g}`lCK?6et}yf4`$+Xm*BTSn&HXx;Ao|R!!XT)V=q1`b`gpQ&O;m+bepN>W zN2%*wj#rAIWGbIdawotgWV@%&Eb^9p-(^hD!fWYV|8ew_;3$rs6DP(MG3DE~k6N3n z53;={>1~-dUeX|)upPoxYjqFA`)iz^hVNJn?iVC>+@js%tJ=9hr?RQ@4eB|ZKz3Iy+LATh6(V)}mQhV(W( zA+Xn0#OX$EI$vHun5*{ogr=iHX}bp2#8KN;DvQaU#@jh))ggJ7u7U6d<4~2qQw;d; zFL!8kjIJ(pw!gAOwqw!khOEy+6~t+%1^QdNEU}^na{q?r7JT7w-)1B{_!LIJOF|w9 zuQ`w=?@WP~M@a`q<+WhaX%v&r!r1vR*OFxc;mMo$-HTymvT(i$KazCAEhFlS_Bb>3 zA@-nN3!=LJqv@=|;^?|4{RM~M+Guc>#vOt?1lQmm+?^yq;|`5Of(Ca89%$Semk=Ph zy9J;6XP&v_imK{Ur}o)t~r; zV?3H>4QxB_(QVIzR|b~HBIT3$^k%lJwuo3oi{$AY<)_8Ivhj#cN_QuYS*x25AC#x< zLOv0S6HqV>n^kZG@4ioNJaU^_=e}ZU<>iehgNg>TI2)^Z5cQ|NBcR1vh)x}oX3P`YQtrWM=I1u(bN5|FHLL5Jz_CPW+BO3m$-IE@Vk=z zwBLmR`7!VPV^m+DNM69(MSwlg1?Xb8-2#*dS?=X{7*!}jmDa?+|Cv!_nPsL7UNZ4` zTT)CfKg{`rq)B-Hn5jQ0rjWa~(zQn^tPN~c8Zb}7f38M?xEd2L{dIVc5WSbStNvC+ z33^4Ju;DzZ5}B{*ajA+dNm_EVBXJk>KItcmV`K7DKn2>h!7vn(ETLPR8m!iJ5-uQADXKlee>I~t@r>Ij>Vp%EW5=BMP z%~wij7nQMJQa~@0ZuZ|o6Z8*1%&jg)nT&A#T0Zi(*%Zd>xIbF{kz;kTzus8Db_pSQ z7Ar>Eku;XC0xrCPndkD37VgpiN#LemrMbYhasTYe`{e0?XUA55k8`oEmouWjKIGkL zPIqlOVac)mJcF?mOOJAzA3DX1bGI&~l$dA=hmv42nc~o)SmBC89vcS}L6+>~59Moi z=T?TnS<_ti?|+_K6f5x=x@{;Lxe6M}x{eG($IekR$avO+)MOfmVN(_0b4Q*YiRo8P%3gLi)4Tc3Vl| zU5+oxfyZx$=O5}HZENB8Y>&>{lDTF6HjJMYRxHag@g6%GLO9ZZ%^tF+ULm8|v>dg% z7x1au>jSaBO@e9o><90F?($8%D0M(_~O@%qO@II(Ue9C_1q zJQ<#0-=o+&yjM0OFOQ4#-%`snLX6?1s$zHuv6M9a+xK|=(#Ni1qp2c>5i+COsaki& zc%l&ShkVDZ$}-^H;`$;pxm`B;Ocp#s7r&iOI>VpLCfb$*Qt%*|2>fwcWm@crWCmW|L@}@!sAz0SkjLaJnxmiUJM|SXbLBP|&m1>XAy)S!i+%l&fqObb9)Z zF{r6XBEfR_AS=K(V?pw6<%1~Q?MOV7M@d5!7@-7p}<27 z*Pw~ONf+Lo5Z|SI!6%eklPK{byg3q1-6*d{42YGx4N3lQLZeD%&^Q?!=wR6e^y+vM zMyh45jy@C*s$9!0=A?hR_ql9~=n;yMS4N^0|Mgy3iDCeV)aId@gK{TF)352X-&U}O zzo!qc!-kDXcshIoR;sX2={t({{CHb`C9#ap>N~{8$$j7GOkxh#5|<_xx_VNT8Ld6y z;&czmCOFHURnXIe%d%4(l=J11Rc*9eGJ)D zYb|23A9i+*)e8`dm-I6mfC#(((Y6Reb2Pqa0*Hn3`R|rIm%T(A4=@;a2p=|eX8W2) ziLu6d5xm7x^X-R|1cK0+9}7mzBeCK}P(eP7Eg0keoJuh*das#*n2SzIKiR%5{FrX1~%z0?xFC1e2go`U6taz*B&##}|e zEFb{I>=}^}4FX2dV{B4JgqhWg&0esc6oIt4q29xz3P^!=6`Cn~K7cQIZpcm?ZV~Pv z0(e6ICwz>@HF6kdCC@e(Nfza~(zYffyhcUto2pO&PW$<>v_;kX-SWvHS2;)6n_0Y) z+Ai>_k>oyff+}k|U_@#`$dY0(7C-~Y;}*TSpvNrl+{GIv3k0|hLa8T7JWowEuY?k^;Md737kIYC2QfbG;D=w`- zu9-Y|F&LKvp~H{2z9RQ_f3@fVyBTFyXBYIf%X+J%eqsb4!PGkKQb3w z8W$?|jEIUS90+qMVN344Xp87ODtr%;s&Fc;QSCk#N{B9EPG_y2=vP$!q15@sl>-{?0#^)#pH=g#_+k9=k#j{gi^u~)}#>+QKE zuXFvmj6e`1sb98@7G0)|fM!0p0E`L9sTaM=byxwpW0%}hd59fRahXRnRFe(p8%g9J z{U=KN?6d!p@i&P;kpR~I%=Jl-ZVZ8;HAvFD%?MC8FFgPx3E+|zJEy<&PwYoQ2B_p8u*-y& zc}^YyxZ>BF0JSubX7?H08~D|a`9qybKobm8p1C|mGYfl$@$)9|DhF2rpTXX;B;BD9 zcuLhe0=68+C;R(PAbB!yT4~w`{^CoH=ErMsfYJvjv8r0wfXJ?+8$G=D-HB=VD=qcT z#>-jrtn-dUvrQgnY5i{)HOqUydG&|B3bnjY&|O~(X}5R7fk)JARPqNoDS-vJ4jop& z^6M$_bbGW;6L{Qay{mG;7`d&FSC%%C~$GU3)0-83-zN!E37?Lzemlm_vB703Fr;`pe(ulVPO}TD6P7xgF4> zxCaC%muU4iRE0?!zhigX|7cTC|A$zous?%Fbp84XD1oDyJ&1$R8$6U2b6h3YGq_y! z=ZR1xV()W-`I?uARkuN%35y&Z8tqr=ejLQ?31Z2Y4M3W~>CjB(SA*U*Y_Xj{!TD|R zE(GYTZeF>o7Km8?BRt^mA#NBPOrTgOKIgpK%WA6P{&Nx}x4jYu zM)(KT(u8tAug|drjd-lZLBJT8y#U4vz=Yp_lXg*D6z<)>%Bf%PSn|Iw5RP-?j!m|N zzLe-d=os?`t+lgsb;pixsclgquOKdcGVHTzpN2&8_?QSFsjyjxHr+5=FuJFD^lIMF+Q-fB?mamF1Mx6MBJ7b9_ zp#W`9bjWKPpfg{E2a+`!u%q`SQKKJ67ox%H?D?`g18%Y#V?7<41IXUwW37!8*;8~C zcN^ft;8N3K#Bv!n4|L?B_4=G;KOMc>e!R)zsIPpOHusk`Wki&WvD$IR2g=&84?`!;N>FDg`O8k_(UA&xeCjeNjrgY)Iyu~So2G!A=RC5|+6yFOJyTvhjP~SofM0{`JwLvX-E` z%a|Iv1nVb{FR~sc|9>q2N4xYqg#w_1b@+a@e>UIJM}okKQ3HqSrArV%VjE4`^>lIm5hv!WfyG9h1*hvAq+{rR7P zagDLmG{pils6wt?i3*vUHt|)R%Z|TwPe`jFfau_MidmULwKQ2|085kJm^V~Rul-hq z0%viRrqSyCa~}63eq<0Xq)HG72kmcArC1zj;i#JPFPJ9q{O!bgJK$1O9LJyusxz$y zh$EPhh$vw+V>>)bSTtDxO~sBESdcaP7WD>ra>{$6+-gq7M}EpZSYhwwc7<7aT+c4# z)RQF0k+~{lqg~&i9#1n3-A%8cvQJx|N?>|^2bq;LBLF(>rIw;7EZ8u;Hw(Vr z<4NUMB2n>BqHkwVH;uPqu65%opnk)Hd}zDX$5Gq&-c35r$u2# zcbd9_0O9BDi7}u8fjBS-6!tm^6fJhxwV7Bf3v7)L&U)ZMf#ewRf?0E70dsTnrXpPlHbrz^3|`6f|2Uo}d<%Ht*>J?u1Uw_pxG&NGl|D-PgTLvSrc1+@Bt>H*RgZw{B+Dn{&140X)U z?(WiMM~f^Jt-*g%nWaUz->jtvcY}|IM|<*UN7(Kj&8YP!06mXuC|l42{*b%sy}E1c z5VBmi2{dWqh2Wq^!54`zCC*Y16HwryAquQuRnBJv8>mw~HKrB&%3#yD>H}hP{J`)w zZ?e`-z0M8Q?eAi~L_pr)E%zKbcQg1t&a|#|1?xHT&6#A7H5gR?+bIeEIQ%m|=9gso zFzXphG&;kkRA0JOpCnm}VV_0|9;&PJDLXKz&Y=2-PGxRh!R$#(`68>V=5UaJjf7d9 zsVgo-sab2FQpeo{71ya1&>C<9Fjim^Ce}96$AQ1WHe1W*zj^#Pb5{FTT%wISw` zD}ukweWq9vIyXB`2xr%$eUKzHc65Zn(6t3~$ck}`1-L6P0EvIQ(%?(4K+)Lh#e^}e zU2f{pM(FXZW)1xl3iVPiTeOT~DB4JReSUp@w-)+O5h)WI;k*KHAVwEqt{TT(89G_f z(ttGc_2MqH%y%8uK#^TdFbU_}_v&ae@}}nwi$s+#LQfsZ1;-uuz#aIqK`7xC!5%3gqLmhEkau#iY%cq^4DHEniEQT(ev_(`n?|UR?hR4$`*`v3=Ru{;W#DtVO6I z<}G(177(}}&}CC^z>@Te2ivh&D#LWd*D8Abe#-^x>IG3W)R2 z19Y=RD+eE{_3(ZH1{I#XzmGH^M$;1I5+hiXYM}wCY3y>^g?)dJgaVqI6Pvi2$??HQ z(vLUGON%g-d;QIv0XpfwAnYTXuvzgw+@gL!@xRj@#WUx z^<}=4vyBZIx6!oj#Xapo`2rK8Ys}Y%T!2Xb?tZUljRzF}9!+(uzeP<|yBJ<CnwyYIa{Za$Z5>F*#xkS!7ZD6 zo5a~`rGe{jrcB~~@qt-N*a&0+G_9z<4+n9#34dp|wQmo6e|x-O+4x6&z0>=h&J`o` z5EOEPE8L-YG~Yov2v9}w=@Xy;_OWMu=q^}nap!tFe9|E&)s|qT9;n?hAu_~30YkW{ zSHWk@w3LvskNZHq@aKsoH}P^mSp0Iks1pCez_IQomMd z6o*l%iqKun597uTSKdj0CIlC6fLoJ-uH;vPvlzVB+leT`HcGZ<5heCg59tfsvDsF{ z3=Aw#*C2Y7dcJagS`&1}hyQTscx$Z(`;-W++FaH5+X5DNw(7=JjZ5%r0(`ehVii?6bQwLtfNOw$EZvJXx(BVs_ zVhzG`5n#9Dl@9|zg=mr~O`;zsYdM7+7M7}ZGij~4DY*;ONjU`lO!H2c<8BWi0YwR5 zB&ly`>Al}jd2G2|-F^#iHF-jgbZ5?VapIg1Jb3!(B-v#jOz*^9Sb91;8UqWm)`7Nj z`xGKi2bhu9P>d>C`tVPJD8B3UP`U6c9nFH?nQ7%bJUcRxcwih!RlJ%uS*3$XsX*me zNdlc775Nyi1fRyrnh*~~qNo?~^(ge2Yqyf4&!=3_m^FqGI58!eA2q6ZM967|gE5^} zU_Rm$aF_?XjVdYYN^TnKSM&Nzz+VzrI*Tm%tW#v_>=Aqu6VRoMTjS>Rv*U5to^RLe z^(g|(cMpiyTsdp$AuLhW;qRObIQFVCVhBy!scz)MZV%84vz-Xkx_hhq)8$wKm7g5_ zQuc;Pk^&1^E6axq=Xqhxwr8h~^9iLO7Tu<<9`y588cpVChH(61AmEBK;b>EcSdZ)r z7apzrC@u@#S8B`V%dCk7RXb0;BP@uG=BGJ4Xv9goffl7(ZQh?i9+hDQKpB|GQ{*)w z{*~V9*GXS^GUz}?rTkixG$Onk9wf4{6^YZ?(N;F?gg1ac1f1$jR1r79=8Z;7krlH2 zXvH>G9h~Wo5ixs~ZxOYXZ(gJe+&Tb*H`s4~_UO)G?#N4BqpkDj-|gZ7+ph12{(zqt zQpEinNdq-5TNU0b_Jr&RffNzUPSD6DuFqSIzn}zS^S{2)J?;MI&Upu zCpcT=jJ_dMsL!TDzlhZ-e;|%!zYI&-^JXVsn^tqXi#mrl-g1-}$fQd*QvkkCR+nNj#Pv zhO;iZhPAtZfbVr+;#quc1S>t2v&_zJ?vS6IB|3~FXCQ6FuMbEn*ZTD1V*})ukBKUe zK)@{vaz7~te`PvG*2jzfwI9Pq2egtww+1B&Jx?y@_LU(4d2t^Lma!TIF`C~L)F)~U z(2C+vz00leoUMVX$iryDBHtt&%>ZW&apzfa@m@Zx-zSW*_i>Le;=Wyh2rR+}`JT_a zsH~)^YG_0o`A_LCoonJB;&4X~@Z`Cra5o~q{AQr6Ok;*vSQrYloLC4a#59oL%g}qO z%G*CO$I-9E!|la(wk9ZNmva|J&`VGL>WZ28c=8n_Xrm2CGSo84Q81mcr_*$hcGuRO zSRVPlHZc@k3rQjIFk-Z@mja%bv_|{pwe#H}LBDyjc_SiGp5&0vXRb?5E&oCkao?2w z_BOi^GZ`9eQN>Jd4x}qoEJ^lz-9Qv3M~o#y>YmSu;^{Tp}zzL#G-o^{}Xu< z!|ME=x5znbYJsu_x8ldeK+vFVUvLmG0ygW;it_Ec;Y+7wE+1b8(XT`*t?-SV zrOp&kh`{ao&00x#f}(4QSp=g&>SaCImBaPdB<^9{&HtjL6s@Jh;~?4+YuBV2yH%}k zLU3s#zAcMv01C~KPk^C*7TgN&>XW8+Qn=0{V*V#0A9GD}qF_z7#_N&866yILy zORip!Z+H|^xN1JL1MN!aO{Nlk9GoZ&C;FfaGWO-V*+AJrSR+qB2bGy_x?9q zJ|&bMjz)_lMYvH*f*IKQr!!QgXVFT;jJ6u1`jklW>4GWbVfLK^e>Nx}R)fN-pIe>~ z&v}6zc=^I;azI9_*nNEEIusPJlGO^gnJPC|Mfq6d#^tZ`qju!7{rF%^paO7bSH

S@ggsR(vrZaM+sXm^K_`R}MyTRYNw81v&JEG( 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 mss5*tqrHUBrvGV - Быстрое умножение + Разделяй-и-властвуй @@ -28,21 +28,21 @@

-

Быстрое умножение

-

В конце 50-х годов, а быстрое преобразование Фурье только изобрели, но использовали только «по назначению» (для обработки сигналов, а не для умножения чисел).

-

Андрей Колмогоров и несколько других пионеров компьютер саенса выдвинули «гипотезу \(n^2\)» — что . Основания на то были — шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

-

Колмогоров уже собрал научный семинар с повесткой дня «давайте это всё уже докажем», на котором его аспирант. Аспиранта звали Анатолий Карацуба.

-

(Быстрое преобразование Фурье тогда уже изобрели, но к умножению чисел применять ещё не додумались.)

-

Можно представить себе какого-то грозного японца.

+

Разделяй-и-властвуй

+

К концу 50-х годов быстрое преобразование Фурье уже изобрели, но использовали только «по назначению»: для обработки сигналов, а не для умножения чисел.

+

В это время Андрей Колмогоров и несколько других пионеров информатики выдвинули «гипотезу \(n^2\)»: невозможно перемножить два \(n\)-значных числа, быстрее, чем за \(O(n^2)\). Основания на то были: шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

+

На очередном научном семинаре с повесткой «давайте это уже докажем» один из его аспирантов неожиданно предъявил новый метод с оценкой сложности \(O(n^{\log_2 3})\), тем самым опровергая гипотезу. Аспиранта звали Анатолий Карацуба.

Мастер-теорема

Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек».

Мастер-теорема. Пусть имеется рекуррента:

-

\[ T(n) = \begin{cases} +

\[ +T(n) = \begin{cases} a T(\frac{n}{b}) + \Theta(n^c), & n > n_0 -\\ \Theta(1), & n \leq n_0 -\end{cases} \]

+\\ \Theta(1), & n \leq n_0 +\end{cases} +\]

Тогда:

  • A. Если \(c > \log_b a\), то \(T(n) = \Theta(n^c)\).
  • @@ -50,22 +50,45 @@

    Мастер-теорема

  • C. Если \(c < \log_b a\), то \(T(n) = \Theta(n^{\log_b a})\).

-

+
+divide-and-conquer +

divide-and-conquer

+

Доказательство. Рассмотрим «дерево рекурсии» этого соотношения. В нём будет \(log_b n\) уровней. На \(k\)-том уровне будет \(a^k\) вершин, каждая из которых будет стоить \((\frac{n}{b^k})^c\) операций. Просуммируем значения во всех вершинах по всем уровням:

-

\[ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k \]

+

\[ +T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k +\]

  • A. Если \(c > \log_b a\), то \(\sum (\frac{a}{b^с})^k\) это сумма убывающей геометрической прогрессии, которая не зависит от \(n\) и просто равна какой-то константе. Значит, \(T(n) = \Theta(n^c)\).
  • -
  • B. Если \(c = \log_b a\), то \[\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n)\]
  • -
  • C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,
  • +
  • B. Если \(c = \log_b a\), то
-

\[ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) \]

-

Для более точных оценок асимптотики «мерджа» теорема ничего не говорит работает. Например, если мердж занимает \(O(n)\)

+

\[ + \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n) + \] * C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,

+

\[ +n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) +\]

+

Примечание. Для более точных оценок асимптотики «мерджа» теорема ничего не говорит. Например, если мердж занимает \(\Theta(n \log n)\) и задача разбивается каждый раз на две части, то асимптотика будет равна:

+

\[ +\sum_{k=0}^{\log n} n \log \frac{n}{2^k} += \sum_{k=0}^{\log n} n (\log n - k) += n \sum_{k=0}^{\log n} k += \Theta (n \log^2 n) +\]

+

В то же время эта рекуррента под условия теоремы не попадает. Можно лишь получить неточные границы \(\Omega (n \log n)\) и \(O(n^{1+\epsilon})\), если подставтиь \(c = 1\) и \(c = 1 + \epsilon\) соответственно. Заметим, что \(n \log n\) и \(n \log^2 n\) асимптотически меньше \(n^{1+\epsilon}\), каким бы маленьким \(\epsilon\) ни был.

Алгоритм Карацубы

-

Алгоритм Карацубы сводит задачу умножения двух чисел длины \(n\) к возведению \(n\)-значного числа в квадрат.

-

Зачем нам возведение в квадрат, если мы хотим умножать числа? Оказывается, это эквивалентные задачи.

-

Развитие идеи

-

То же самое можно применить матрицам.

-

Чемпионат идёт на пятые знаки после запятой. Возможность или невозможность умножения чисел за \(O(n^{1+\epsilon})\) для произвольного \(\epsilon\) ещё никто не доказал.

+

Алгоритм Карацубы сводит задачу умножения двух чисел длины \(n\) к возведению \(n\)-значного числа в квадрат. Для простоты будем считать, что числа бинарные, а n = 2k является степенью двойки. На практике же лучше брать достаточно большое основание, а не двойку — \(10^9\), например.

+

Оказывается, что умножение и возведение в квадрат, вообще говоря, эквивалентные задачи: так как \(4ab=(a+b)^{2}-(a-b)^{2}\), то вместо умножения двух чисел \(a\) и \(b\) можно возвести два числа такого же порядка в квадрат и за линейное время сделать несколько сложений и вычитаний.

+

Итак, нам нужно возвести число x в квадрат. Разобьём его на две части, то есть представим в виде \(x = a + 2^k b\), где числа \(a\) и \(b\) являются \(k\)-значными. Внимание, алгебра:

+

\[ +x^2 = (a + 2^k b)^2 = a^2 + 2^k ((a+b)^2 - a^2 - b^2) + b^2 +\]

+

Выяснилось, что мы можем вместо возведения \(n\)-значного числа в квадрат возвести три \(\frac{n}{2}\)-значных значных числа в квадрат — \(a^2\), \(b^2\) и \((a+b)^2\) — и сделать константное количество сложений, вычитаний и сдвигов.

+

Пролистав пол-экрана выше, можно убедиться, что асимптотика будет \(\Theta (n^{\log_2 3}) \approx \Theta (n^{1.58})\): в данном случае наша задача разбивается на \(a = 3\) части в \(b = 2\) раз меньшего размера, а объединение происходит за \(O(n)\).

+

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

+

Развитие идеи

+

То же самое можно применить к матричному умножению — это называется [алгоритмом Штрассена](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A8%D1%82%D1%80%D0%B0%D1%81%D1%81%D0%B5%D0%BD%D0%B0. В нём две матрицы разбиваются на \(8 = 4 + 4\) частей, перемножаются блочно, и сложной алгеброй от одного из 8 умножений получается избавиться, что даёт асимптотику \(O(n^{\log_7 8}) \approx O(n^{2.81})\).

+

И в алгоритме Штрассена, и в алгоритме Карацубы можно достичь и лучшей асимптотики, если разбивать объекты на большее число частей. Однако, на практике это не применяется, потому что константа получается слишком высокой.

From 5d00a8cef4e5d072377e57aca372598cf6099080 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 8 Jun 2019 21:48:09 +0000 Subject: [PATCH 035/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/karatsuba.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ru/karatsuba.html b/ru/karatsuba.html index 9309f62..f34c9c3 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -88,7 +88,7 @@

Алгоритм Карацубы

Пролистав пол-экрана выше, можно убедиться, что асимптотика будет \(\Theta (n^{\log_2 3}) \approx \Theta (n^{1.58})\): в данном случае наша задача разбивается на \(a = 3\) части в \(b = 2\) раз меньшего размера, а объединение происходит за \(O(n)\).

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

Развитие идеи

-

То же самое можно применить к матричному умножению — это называется [алгоритмом Штрассена](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A8%D1%82%D1%80%D0%B0%D1%81%D1%81%D0%B5%D0%BD%D0%B0. В нём две матрицы разбиваются на \(8 = 4 + 4\) частей, перемножаются блочно, и сложной алгеброй от одного из 8 умножений получается избавиться, что даёт асимптотику \(O(n^{\log_7 8}) \approx O(n^{2.81})\).

+

То же самое можно применить к матричному умножению — это называется [алгоритмом Штрассена](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A8%D1%82%D1%80%D0%B0%D1%81%D1%81%D0%B5%D0%BD%D0%B0](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A8%D1%82%D1%80%D0%B0%D1%81%D1%81%D0%B5%D0%BD%D0%B0). В нём две матрицы разбиваются на \(8 = 4 + 4\) частей, перемножаются блочно, и сложной алгеброй от одного из 8 умножений получается избавиться, что даёт асимптотику \(O(n^{\log_7 8}) \approx O(n^{2.81})\).

И в алгоритме Штрассена, и в алгоритме Карацубы можно достичь и лучшей асимптотики, если разбивать объекты на большее число частей. Однако, на практике это не применяется, потому что константа получается слишком высокой.

From 9602cb7627584207e618a2340dd4f788b9b22de6 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 8 Jun 2019 21:53:49 +0000 Subject: [PATCH 036/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 15 +++--------- ru/index.html | 57 +++++++++++++++++++++++++++------------------ ru/karatsuba.html | 4 ++-- ru/probability.html | 9 ------- 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/pandoc.css b/pandoc.css index fb71aa7..0132dd5 100644 --- a/pandoc.css +++ b/pandoc.css @@ -27,7 +27,7 @@ html { body { /* color: #444; */ font-family: 'Times New Roman', Palatino, 'Palatino Linotype', Times, serif; - font-size: 14px; + font-size: 16px; line-height: 1.7; padding: 15m; margin: auto; @@ -219,7 +219,8 @@ img { .figure img { border: none; - margin: auto; + margin: auto; + max-width: 85%; } figcaption { @@ -280,16 +281,6 @@ pre { font-size: 0.9em; } -@media only screen and (min-width: 480px) { - body { - font-size: 14px; - } -} -@media only screen and (min-width: 768px) { - body { - font-size: 16px; - } -} @media print { * { background: transparent !important; diff --git a/ru/index.html b/ru/index.html index e850d4f..4d8e74b 100644 --- a/ru/index.html +++ b/ru/index.html @@ -7,7 +7,6 @@ Алгоритмика - @@ -36,44 +35,61 @@

Структуры данных

  • Дерево отрезков
  • Декартово дерево
  • Дерево Фенвика
  • -
  • Разреженная таблица и задача static RMQ
  • -
  • Битовое сжатие, std::bitset, матричное умножение и метод Гаусса над \(Z_2\)
  • +
  • Разреженная таблица
  • Общие техники

    Дискретная математика

    +

    Переборные задачи

    + +

    Высшая математика

    +

    Графы

    Строки

    Динамическое программирование

    Оптимизация

    Разное

      @@ -81,16 +97,11 @@

      Разное

    Теория игр

      -
    • Теория игр, игры на графах, ретроанализ, альфа-бета отсечение, ним, теория шпрага-гранди, игры с неполной информацией
    • -
    -

    Высшая математика

    -
      -
    • Линейная алгебра, матрицы, матричное умножение и быстрое возведение в степень, применения к динамике, метод Гаусса

    • -
    • Теория вероятностей, математическое ожидание, дисперсия, линейность математического ожидания, нормальное распределение и метод Монте-Карло

    • +
    • Теория игр

    Геометрия

    diff --git a/ru/karatsuba.html b/ru/karatsuba.html index f34c9c3..e9fda5e 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -51,8 +51,8 @@

    Мастер-теорема


    -divide-and-conquer -

    divide-and-conquer

    + +

    Доказательство. Рассмотрим «дерево рекурсии» этого соотношения. В нём будет \(log_b n\) уровней. На \(k\)-том уровне будет \(a^k\) вершин, каждая из которых будет стоить \((\frac{n}{b^k})^c\) операций. Просуммируем значения во всех вершинах по всем уровням:

    diff --git a/ru/probability.html b/ru/probability.html index 23f8578..7ba2083 100644 --- a/ru/probability.html +++ b/ru/probability.html @@ -68,16 +68,7 @@

    Ликбез по теорверу

    Ликбез по теорверу

    -
      -
    • Вероятностные распределения
    • -
    • Матожидание и дисперсия
    • -
    • Парадокс дней рождения
    • -
    • Нормальное распределение
    • -
    • Правдоподобие и оценка качества предсказания
    • -
    • Энтропия и оптимальное кодирование
    • -

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

    -

    (Эти строчки позволят нам генерировать распределения и рисовать графики, не обращайте внимание.)

    import numpy as np
     
     import matplotlib as plt
    
    From a479e355c4c190f0aab33c7bd2302398f51e0538 Mon Sep 17 00:00:00 2001
    From: "Deployment Bot (from Travis CI)" 
    Date: Sat, 8 Jun 2019 23:29:10 +0000
    Subject: [PATCH 037/230] Deploy algorithmica.org to
     github.com/algorithmica-org/articles.git:gh-pages
    
    ---
     img/centroid.jpg  | Bin 0 -> 16507 bytes
     ru/centroid.html  |  36 +++++++++++++++++-------------------
     ru/karatsuba.html |   4 ++--
     3 files changed, 19 insertions(+), 21 deletions(-)
     create mode 100644 img/centroid.jpg
    
    diff --git a/img/centroid.jpg b/img/centroid.jpg
    new file mode 100644
    index 0000000000000000000000000000000000000000..79fd12f0e8f9f23769ae99cb32bcb69d1576b49e
    GIT binary patch
    literal 16507
    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
    Gk^c{abR+Zt
    
    literal 0
    HcmV?d00001
    
    diff --git a/ru/centroid.html b/ru/centroid.html
    index 591e898..94e8f44 100644
    --- a/ru/centroid.html
    +++ b/ru/centroid.html
    @@ -68,14 +68,14 @@
     

    Центроидная декомпозиция

    Центроидная декомпозиция

    -

    Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья (массив можно представить бамбуком, так что это действительно обобщение). Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».

    -

    Иногда вместо неё можно написать Heavy-light декомпозицию (чуть сложнее) или метод переливаний (чуть проще).

    +

    Центроидная декомпозиция — обобщение метода «разделяй-и-властвуй» на деревья. Обычно её используют для решения двух типов задач: «сколько есть путей с такими-то свойствами» и «есть ли такое-то свойство у этого пути».

    +

    Иногда вместо неё можно написать heavy-light декомпозицию, что чуть сложнее, или метод переливаний, что чуть проще.

    - +
    -

    Определение. Центром или цетроидом (англ. centroid) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более \(\frac{n}{2}\).

    -

    Выясняется, что центр всегда существует — это следует из алгоритма его поиска:

    +

    Определение. Центром или центроидом (англ. centroid) дерева будем называть вершину, при удалении которой размеры оставшихся компонент будут не более \(\frac{n}{2}\).

    +

    Центроид всегда существует — это следует из алгоритма его поиска:

    int s[maxn];
     
     int sizes (int v) {
    @@ -100,7 +100,7 @@ 

    Центроидная
  • Мы пришли в эту вершину, когда её размер был больше \(\frac{n}{2}\), а это значит, что в «обратном» направлении есть не более \(n - (\frac{n}{2}+1) = \frac{n}{2}-1\) вершин.
  • Значит, размеры всех соседей не больше половины \(n\), и алгоритм корректен.
  • -

    Иногда центров два (пример: 1-2-3-4), тогда алгоритм вернёт «нижний» центроид.

    +

    Иногда центроидов два (пример: 1-2-3-4), и тогда алгоритм вернёт «нижний» центроид.

    Определение. Центроидной декомпозицией будем называть рекурсивный процесс «выделить центроид, удалить, запуститься от компонент».

    Определение. Компонентой центроида будем называть множество вершин, достижимых из центроида непосредственно перед его удалением.

    Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты.

    @@ -168,26 +168,24 @@

    Под solve(centroid(u, v, s[u]/2)); }

    Асимптотика \(O(n \log n)\), потому что на каждую из \(O(n)\) верщин мы потратим \(O(1)\) операций на каждом из \(O(\log n)\) «уровней» центроидной декомпозиции.

    -

    Запросы на путях — offline

    +

    Запросы на путях — offline

    Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.

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

    Например, при запросах суммы на пути, мы можем насчитать во внутреннем dfs для каждой вершины сумму на пути от её текущего центроида, и если при обработке текущей компоненты встретим вторую вершину запроса, в какой-то другой ветке, то просто возьмем её сумму и уже посчитанной суммой на пути до первой вершины.

    Таким образом, каждый запрос будет просмотрен \(O(\log n)\) раз, пока не будет удален, и асимптотика составит \(O(q \log n + n \log n)\).

    -

    Запросы на путях — online

    +

    Запросы на путях — online

    Альтернативно, можно сначала «построить» центроидную декомпозицию, а потом отвечать на запросы.

    Пусть мы хотим посчитать отвечать на запросы минимума на путях и не знаем, как решать LCA двоичными подъемами. Создадим двумерный массив centroid[][] размера \(n \times \log n\), в котором для каждой вершины будем хранить \(O(\log n)\) центроидов, в чьи компоненты она когда-то входила. Помимо этого, в таком же массиве будем хранить информацию, которая нам будем нужна для ответа на запросы — в данном случае для каждой пары вершина-центроид будем хранить минимум на соответствующем пути.

    Тогда, при ответе на запрос, мы за \(O(\log n)\) или даже \(O(\log \log n)\) операций находим центроид на нужном нам пути (первые сколько-то значений centroid[v] и centroid[u] будут совпадать — нас интересует последнее) и берём в качестве ответа минимум от минимумов на двух путях до центроида.

    Асимптотика при более долгих пересчётах

    -

    TODO: сослаться на мастер-теорему

    -

    Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за \(O(n \log n)\) (например, где-то использовав set). Сильно ли это хуже по времени?

    -

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

    -

    -

    Оценим сверху, сколько всего выполняется операций, рассмотрев дерево рекурсии нашего алгоритма и просуммировав каждый уровень:

    -

    \[ \sum_{k=0}^{\log n} \frac{n}{2^k} \log \frac{n}{2^k} -\leq \sum_{k=0}^{\log n} \frac{n}{2^k} \log n -= n \log n \sum_{k=0}^{\log n} \frac{1}{2^k} -< 2 n \log n -= O(n \log n) \]

    -

    Получается, что мы можем «мердж» в центроидной декомпозиции писать и за \(O(n \log n)\) — как, впрочем, и во всех остальных «разделяйках».

    +

    Представим, что мы написали «мердж» (ту часть, которая отвечает за ответ на запросы или подсчёт путей, идущих через центроид) не за линейное время, а за \(O(n \log n)\), например, где-то использовав set. Сильно ли это хуже по времени?

    +

    В худшем случае — бамбуке — каждый раз компонента будет разбиваться на две равные части. Просуммируем общее количество операций в дереве рекурсии (см. мастер-теорему:

    +

    \[ +\sum_{k=0}^{\log n} n \log \frac{n}{2^k} += \sum_{k=0}^{\log n} n (\log n - k) += n \sum_{k=0}^{\log n} k += \Theta (n \log^2 n) +\]

    +

    Мораль: используйте хэш-таблицы.

    diff --git a/ru/karatsuba.html b/ru/karatsuba.html index e9fda5e..b5e6247 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -32,7 +32,7 @@

    Разделяй-и-властвуй

    Разделяй-и-властвуй

    К концу 50-х годов быстрое преобразование Фурье уже изобрели, но использовали только «по назначению»: для обработки сигналов, а не для умножения чисел.

    -

    В это время Андрей Колмогоров и несколько других пионеров информатики выдвинули «гипотезу \(n^2\)»: невозможно перемножить два \(n\)-значных числа, быстрее, чем за \(O(n^2)\). Основания на то были: шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значиных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

    +

    В это время Андрей Колмогоров и несколько других пионеров информатики выдвинули «гипотезу \(n^2\)»: невозможно перемножить два \(n\)-значных числа, быстрее, чем за \(O(n^2)\). Основания на то были: шумеры научились умножать в «в столбик» (сложение \(n\) \(n\)-значных чисел) как минимум четыре тысячи лет назад, и за это время никто ничего быстрее не придумал.

    На очередном научном семинаре с повесткой «давайте это уже докажем» один из его аспирантов неожиданно предъявил новый метод с оценкой сложности \(O(n^{\log_2 3})\), тем самым опровергая гипотезу. Аспиранта звали Анатолий Карацуба.

    Мастер-теорема

    Алгоритм Карацубы основывается на парадигме «разделяй-и-властвуй». Чтобы понять, почему его асимптотика именно такая, нам понадобится более мощная теорема, которая говорит об асимптотике большого класса «разделяек».

    @@ -88,7 +88,7 @@

    Алгоритм Карацубы

    Пролистав пол-экрана выше, можно убедиться, что асимптотика будет \(\Theta (n^{\log_2 3}) \approx \Theta (n^{1.58})\): в данном случае наша задача разбивается на \(a = 3\) части в \(b = 2\) раз меньшего размера, а объединение происходит за \(O(n)\).

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

    Развитие идеи

    -

    То же самое можно применить к матричному умножению — это называется [алгоритмом Штрассена](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A8%D1%82%D1%80%D0%B0%D1%81%D1%81%D0%B5%D0%BD%D0%B0](https://ru.wikipedia.org/wiki/%D0%90%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC_%D0%A8%D1%82%D1%80%D0%B0%D1%81%D1%81%D0%B5%D0%BD%D0%B0). В нём две матрицы разбиваются на \(8 = 4 + 4\) частей, перемножаются блочно, и сложной алгеброй от одного из 8 умножений получается избавиться, что даёт асимптотику \(O(n^{\log_7 8}) \approx O(n^{2.81})\).

    +

    То же самое можно применить к матричному умножению — это называется алгоритмом Штрассена. В нём две матрицы разбиваются на \(8 = 4 + 4\) частей, перемножаются блочно, и сложной алгеброй от одного из 8 умножений получается избавиться, что даёт асимптотику \(O(n^{\log_7 8}) \approx O(n^{2.81})\).

    И в алгоритме Штрассена, и в алгоритме Карацубы можно достичь и лучшей асимптотики, если разбивать объекты на большее число частей. Однако, на практике это не применяется, потому что константа получается слишком высокой.

    From a5c876b148952a1ff43ca478dec5ec05da22107d Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 8 Jun 2019 23:58:04 +0000 Subject: [PATCH 038/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 1 + ru/karatsuba.html | 11 +++++------ ru/segtree.html | 14 +++++--------- ru/treap.html | 48 +++++++++++++++++++++++++++-------------------- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/pandoc.css b/pandoc.css index 0132dd5..aede3fe 100644 --- a/pandoc.css +++ b/pandoc.css @@ -34,6 +34,7 @@ body { max-width: 48em; background: #fefefe; text-align: justify; + padding-bottom: 25px; } a { diff --git a/ru/karatsuba.html b/ru/karatsuba.html index b5e6247..a05fc54 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -59,13 +59,12 @@

    Мастер-теорема

    \[ T(n) = \sum_{k=0}^{\log_b n} a^k (\frac{n}{b^k})^c = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k \]

    -
      -
    • A. Если \(c > \log_b a\), то \(\sum (\frac{a}{b^с})^k\) это сумма убывающей геометрической прогрессии, которая не зависит от \(n\) и просто равна какой-то константе. Значит, \(T(n) = \Theta(n^c)\).
    • -
    • B. Если \(c = \log_b a\), то
    • -
    +

    A. Если \(c > \log_b a\), то \(\sum (\frac{a}{b^с})^k\) это сумма убывающей геометрической прогрессии, которая не зависит от \(n\) и просто равна какой-то константе. Значит, \(T(n) = \Theta(n^c)\).

    +

    B. Если \(c = \log_b a\), то

    \[ - \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n) - \] * C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,

    +\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n) +\]

    +

    C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,

    \[ n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) \]

    diff --git a/ru/segtree.html b/ru/segtree.html index ad34836..f7d6e0f 100644 --- a/ru/segtree.html +++ b/ru/segtree.html @@ -136,8 +136,9 @@

    Ликбез по C++

    A *y = new A(42); // new возвращает адрес, по которому можно найти объект y->do_something(); y.param3 = '!';
    -

    Кстати, вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем \(2^{32}\) байтами памяти — ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций — это операции с памятью, и размерность повысили именно из-за этого, а не чтобы операции с long long быстрее считались

    +

    Оффтоп: вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем \(2^{32}\) байтами памяти — это ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций в любом компьютере — это операции с памятью, и разрядность повысили именно из-за этого, а не чтобы операции с long long быстрее считались

    Реализация

    +

    Примечание. Данная реализация неэффективна по времени и памяти (см. раздел «другие реализации»). Мы её приводим в педагогических целях.

    Общий план реализации любых структур данных:

    1. Полностью понять все инварианты — как должна выглядеть структура, какие значения должны принимать поля, etc.
    2. @@ -348,14 +349,9 @@

      Персистентность

      У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за \(O(\log n)\), посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна \(\frac{1}{2^{100}}\), и ей на практике можно пренебречь.

      Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее \(\frac{n}{2}\) в массиве на \(n\) элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем true, иначе false.

      Другие реализации

      -

      Эта реализация проста и легко расширяема, но весьма медленная и неэффективная по памяти. Есть альтернативы:

      +

      Реализация на указателях проста и легко расширяема, но очень медленная и неэффективная по памяти: нужно хранить сами указатели, структура перестаёт помещаться в кэш, нужно много лишних раз ходить по ссылкам в память, только чтобы получить нужные вершины.

      +

      Если динамическое построение и персистентность для решения задачи не нужны, есть альтернативы, которые в несколько раз быстрее:

      На массивах. Можно ввести несложную нумерацию вершин, позволяющую при спуске в ребёнка пересчитывать его номер. Это позволит не хранить границы текущего отрезка. Подробнее у Емакса.

      -

      «ДО снизу». Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в одном посте с CodeForces.

      -

      Задачи

      - +

      «ДО снизу». Можно делать все операции итеративно — так получится раз в 7 быстрее, но писать что-либо нетривиальное (например, массовые операции) так будет намного труднее. Подробнее смотрите в посте с CodeForces.

      diff --git a/ru/treap.html b/ru/treap.html index d80c196..5fa77b0 100644 --- a/ru/treap.html +++ b/ru/treap.html @@ -85,18 +85,18 @@

      Декартово дерево

      Таким образом, декартово дерево — это одновременно бинарное дерево по \(x\) и куча по \(y\). Поэтому ему придумали много альтернативных названий:

        -
      • Дерамида (дерево + пирамида)
      • -
      • ПиВо (пирамида + дерево)
      • -
      • КуРево (куча + дерево)
      • -
      • Treap (tree + heap)
      • +
      • Дерамида (дерево + пирамида)

      • +
      • ПиВо (пирамида + дерево)

      • +
      • КуРево (куча + дерево)

      • +
      • Treap (tree + heap)

      -

      ## Бинарные деревья

      +

      Бинарные деревья

      С небольшими модификациями, декартово дерево умеет всё то же, что и любое бинарное дерево поиска, например:

        -
      • добавить число \(x\) в множество
      • -
      • определить, есть ли в множестве число \(x\)
      • -
      • найти первое число, не меньшее \(x\) (lower_bound)
      • -
      • найти количество чисел в промежутке \([l, r]\)
      • +
      • добавить число \(x\) в множество

      • +
      • определить, есть ли в множестве число \(x\)

      • +
      • найти первое число, не меньшее \(x\) (lower_bound)

      • +
      • найти количество чисел в промежутке \([l, r]\)

      При этом все операции — за \(O(\log n)\).

      На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до \(O(\log n)\).

      @@ -104,25 +104,33 @@

      Приоритеты и

      В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты (\(y\)) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска.

      Теорема. Ожидание глубины вершины в декартовом дереве равно \(O(n \log n)\).

      Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.

      -

      Доказательство. Введем функцию \(a(x, y)\) равную единице, если \(x\) является предком \(y\), и нулем в противном случае. Такие функции называются индикаторами*.

      +

      Доказательство. Введем функцию \(a(x, y)\) равную единице, если \(x\) является предком \(y\), и нулем в противном случае. Такие функции называются индикаторами.

      Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна

      -

      \[d_i = \sum_{j=1}^n a(j, i)\]

      +

      \[ +d_i = \sum_{j=1}^n a(j, i) +\]

      Её матожидание равно

      -

      \[E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i)\]

      +

      \[ +E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i) +\]

      где \(p(x, y)\) это веряотность, что \(a(x, y) = 1\). Здесь мы воспользовались важным свойством линейности: матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.

      Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.

      -

      Лемма. Вершина \(x\) является предком \(y\), если у неё приоритет больше, чем у всех вершин из отрезка \((x, y]\) (без ограничения общности, будем считать, что \(x < y\)).

      +

      Лемма. Вершина \(x\) является предком \(y\), если у неё приоритет больше, чем у всех вершин из полуинтервала \((x, y]\) (без ограничения общности, будем считать, что \(x < y\)).

      Необходимость. Если это не так, то где-то между \(x\) и \(y\) есть вершина с большим приоритетом, чем \(x\). Она не может быть потомком \(x\), а значит \(x\) и \(y\) будут разделены.

      Достаточность. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком \(x\). Таким образом, всё, что справа от \(y\), ни на что влиять не будет.

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

      -

      \[p(x, y) = \frac{1}{y-x+1}\]

      +

      \[ +p(x, y) = \frac{1}{y-x+1} +\]

      Теперь, чтобы найти матожидание, эти вероятности надо просуммировать:

      -

      \[E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n)\]

      +

      \[ +E[d_i] = \sum_{j \neq i} p(j, i) = \sum_{j \neq i} \frac{1}{|i-j|+1} \leq \sum_{i=1}^n \frac{1}{n} = O(\log n) +\]

      Перед последним переходом мы получили сумму гармонического ряда.

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

      -

      Упражнение*. Выведите из этого доказательства асимптотику quicksort.

      +

      Упражнение. Выведите из этого доказательства асимптотику quicksort.

      Реализация

      -

      Декартово дерево удобно писать на указателях и структурах. Поэтому мы рассказали дерево отрезков на указателях, а не стандартную рекурсию на 5 параметрах. Не знаете, что это такое — посмотрите в предыдущем конспекте.

      +

      Декартово дерево удобно писать на указателях и структурах.

      Создадим структуру Node, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву.

      struct Node {
           int key, prior;
      @@ -220,7 +228,7 @@ 

      return res; }

      -

      Неявный ключ

      +

      Неявный ключ

      Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.

      Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.

      Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:

      @@ -292,7 +300,7 @@

      Пример: переворот

      q2.second->rev ^= 1; root = merge(q2.first, merge(q2.second, q1.second)); } -

      Функциональное программирование*

      +

      Небольшой рефакторинг

      Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с \(l\) по \(r\), что-то с ним делаем и склеиваем обратно.

      Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке.

      auto apply (int l, int r, auto f) {
      @@ -314,7 +322,7 @@ 

      Функ if (v) v->rev ^= 1; });

      -

      Персистентность*

      +

      Персистентность

      Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.

      Дана строка. Требуется выполнять в ней копирования, удаления и вставки в произвольные позиции.

      From 69afbff6eefcb3dedece73a772b228930af8b949 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 9 Jun 2019 00:39:10 +0000 Subject: [PATCH 039/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/matroid.html | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/ru/matroid.html b/ru/matroid.html index 2140c50..80d3cdc 100644 --- a/ru/matroid.html +++ b/ru/matroid.html @@ -68,14 +68,25 @@

      Матроиды

      Матроиды

      -

      Матроиды нужны, чтобы уметь применять некоторые жадники, в которых нужно набрать какое-то множество объектов максимального веса. Например, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени (см. примеры).

      +

      Понятие матроида нужно, чтобы придумывать и доказывтать некоторые жадные алгоритмы в задачах, где нужно набрать какое-то множество объектов максимального веса. Например, минимальный остов, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени.

      +

      Если вы не специалист по абстрактной алгебре, рекомендуется сначала посмотреть примеры матроидов в конце статьи.

      +

      Определение

      Матроидом называется пара \((X, I)\), где \(X\) — множество элементов, называемое носителем матроида, а \(I\) — некоторое множество подмножеств \(X\), называемое семейством независимых множеств. В матроиде должны выполняться следующие свойства:

      • Пустое множество является независимым: \(\varnothing \in I\)

      • -
      • Любое подмножество независимого множества тоже независимо: \[A \subset B, B \in I \implies A \in I\]

      • -
      • Если в независимом множестве \(A\) меньше элементов, чем в независимом множестве \(B\), то будет существовать элемент из \(B\), дополняющий \(A\) до независимого множества размера \(|A|+1\): \[A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I\]

      • +
      • Любое подмножество независимого множества тоже независимо:

      -

      Матроид называется взвешенным, если на нем существует аддитивная весовая функция: \(w(A) = \sum w(a_i)\).

      +

      \[ + A \subset B, B \in I \implies A \in I + \]

      +
        +
      • Если в независимом множестве \(A\) меньше элементов, чем в независимом множестве \(B\), то будет существовать элемент из \(B\), дополняющий \(A\) до независимого множества размера \(|A|+1\):
      • +
      +

      \[ + A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I + \]

      +

      Матроид называется взвешенным, если на нем есть аддитивная весовая функция: \(w(A) = \sum w(a_i)\).

      +

      Алгоритм Радо-Эдмондса

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

      X.sort()
       s = []
      @@ -83,10 +94,9 @@ 

      Матроиды

      if good(s + [x]): s += [x]

      Здесь под good имеется в виду \(s \cup x \in I\).

      -

      Корректность этого алгоритма для любого матроида доказывает следующая теорема:

      -

      Теорема Радо-Эдмондса

      -

      Пусть \(A \in I\) — множество минимального веса среди всех независимых подмножеств \(X\) мощности \(k\). Возьмем \(x: A \cup x \in I,\;x \notin A,\;w(x)\) — минимальна. Тогда \(A \cup x\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

      -

      *Доказательство: **

      +

      Корректность этого алгоритма для произвольного матроида следует из следующего утверждения:

      +

      Теорема Радо-Эдмондса. Пусть \(A \in I\) — множество минимального веса среди всех независимых подмножеств \(X\) мощности \(k\). Возьмем \(x: A \cup x \in I,\;x \notin A,\;w(x)\) — минимальна. Тогда \(A \cup x\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

      +

      Доказательство:

      Рассмотрим \(B\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

      Из свойств матроида: \(\exists y \in B \setminus A : A \cup y \in I\).

      Тогда верны два неравенства:

      @@ -100,7 +110,7 @@

      Теорема Радо-Эдм

      Получаем, что если объединить множество \(A\) с \(x\) — минимальным из таких, что \(A \cup x \in I\), — то получим множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

      Иными словами, если у нас есть оптимальное \(k\)-элементарное независимое множество, то мы можем индуктивно построить оптимальное \((k+1)\)-элементарное множество, добавив к нему минимальный элемент, оставляющий построенное множество независимым.

      Примеры

      -

      Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий посложнее.

      +

      Для доказательства жадников нужно просто (а иногда не просто) показать, что рассматриваемое множество является матроидом. Первые два критерия доказывать тривиально, третий сложнее.

      Минимальный остов

      Рассмотрим неориентированный граф \(G = (V, E)\). Пусть \(I\) — множество лесов графа (ациклических подмножеств \(E\)). Тогда \(M = (E, I)\) является матроидом:

        @@ -128,12 +138,14 @@

        Паросочетания

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

      Линейно независимые вектора

      -

      (TODO) (Школьники не обязаны знать линал.)

      -

      Такие штуки будем называть базисами.

      +

      Множество \(n\)-мерных векторов называется линейно независимым, если никакой его вектор нельзя получить линейной комбинацией (взвешенной суммой) других.

      +

      Рассмотрим какое-то множество векторов \(V\). Пусть \(I\) — множество всех его линейно независимых подмножеств. Тогда \(M = (V, I)\) является матроидом:

        -
      • Ноль есть в любом базисе.
      • -
      • Подмножество базиса — базис.
      • -
      • ??? Пусть нельзя выбрать новый вектор. Получается, что все вектора \(B\) лежат в \(A\). Значит, размерность \(B\) уж точно не больше.
      • +
      • В пустом множестве нельзя получить какой-либо вектор.
      • +
      • Подмножество линейно независимого множество тоже линейно независимо: способов набрать вектора стало ещё меньше.
      • +
      • Пусть есть два линейно независимых множетва \(A\) и \(B\), и \(B\) больше. Пусть в нём нельзя выбрать вектор, совместимый с \(A\). Тогда получается, что все вектора \(B\) можно выразить из \(A\). Значит, размерность \(B\) уж точно не больше — противоречие.
      +

      Пример задачи: набрать базис максимального веса, если каждый вектор сколько-то стоит.

      +

      Примечание: \(Z_2\) — это тоже пространство, и там тоже можно ввести линейную независимость. Это значит, что то же самое распространяется на задачи в духе «набрать максимальное множество чисел, из которых ксором нельзя получить ноль».

      From 7e19e935aa0595b271a728f4539c91f88f3a8a2c Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 9 Jun 2019 01:19:38 +0000 Subject: [PATCH 040/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sparse-table.html | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/ru/sparse-table.html b/ru/sparse-table.html index 1089505..807c1b9 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -68,31 +68,27 @@

      Разреженная таблица

      Разреженная таблица

      -
        -
      • Нужна для нахождения минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) с малой константой.
      • -
      • Обновления не поддерживает (static RMQ).
      • -
      • Так как LCA эквивалентна RMQ, может быть применена в разных задачах на деревья.
      • -
      • Её можно строить на ходу. Это может быть полезно при подсчете всяких динамик (см. последний Всерос).
      • -
      • Требует \(O(n \log n)\) памяти.
      • -
      • Также можно считать, например, gcd на отрезке за сложность одного gcd, но в основном её используют для RMQ.
      • -
      -

      Определим разреженную таблицу как двумерный массив размера \(n \times\log n\):

      +

      Разреженная таблица (англ. sparse table) — структура данных, позволяющая отвечать на запросы минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) времени и памяти.

      +

      Разреженная таблица — это следующий двумерный массив размера \(n \times\log n\):

      \[ t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} \]

      -

      Идея такая: считаем минимум на каждом отрезке длины \(2^k\).

      -

      Такой массив можно посчитать за его размер: \(t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1])\). Считать его можно как итерируясь как по i, так и по k, причём какой-то из этих вариантов в несколко раз быстрее из-за кэширования.

      -

      Имея таком массив, мы можем в принципе для любого отрезка быстро посчитать минимум на нём. Нужно заметить, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.

      +

      По-русски: считаем минимумы на каждом отрезке длины \(2^k\).

      +

      Такой массив можно посчитать за его размер, итерируясь либо по \(i\), либо по \(k\):

      +

      \[ +t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1]) +\]

      +

      Имея таком массив, мы можем для любого отрезка быстро посчитать минимум на нём. Заметим, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.

      Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

      -
      int a[maxn], lg[maxn], mx[maxn][logn];
      +
      int a[maxn], lg[maxn], mn[maxn][logn];
       
       int rmq (int l, int r) {
           int t = lg[r-l+1];
      -    return min(mx[l][t], mx[r-(1<<t)+1][t]);
      +    return min(mn[l][t], mn[r-(1<<t)+1][t]);
       }
       
       // Это считается уже где-то в первых строчках main:
      @@ -102,12 +98,18 @@ 

      Разреженная табли lg[i] = l; for (int i = n-1; i >= 0; i--) { - mx[i][0] = a[i]; + mn[i][0] = a[i]; for (int l = 0; l < logn-1; l++) - mx[i][l+1] = max(mx[i][l], mx[i+(1<<l)][l]); + mn[i][l+1] = min(mn[i][l], mn[i+(1<<l)][l]); }

      2d Static RMQ

      Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

      В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

      +

      Примечания

      +

      В отличие от дерева отрезков, разреженная таблица является статической, то есть её нельзя дёшево обновлять (но не всегда — см. задачу «Антиматерия.

      +

      Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.

      +

      Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (\(a ∘ (b ∘ c) = (a ∘ b) ∘ c\)), коммутативность (\(a ∘ b = b ∘ a\)) и идемпотентность (\(a ∘ a = a\)). Например, её можно применять для нахождения gcd.

      +

      Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

      +

      Упражнение. Какой из 4 вариантов итерирования и layout-а самый эффективный? (Подсказка: не тот, который приведен в этой статье.)

      From ebfd257b02ca24de8c0a40b6189b2cf89d03d910 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 9 Jun 2019 01:26:08 +0000 Subject: [PATCH 041/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sparse-table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ru/sparse-table.html b/ru/sparse-table.html index 807c1b9..a6a4015 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -106,7 +106,7 @@

      2d Static RMQ

      Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

      В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

      Примечания

      -

      В отличие от дерева отрезков, разреженная таблица является статической, то есть её нельзя дёшево обновлять (но не всегда — см. задачу «Антиматерия.

      +

      В отличие от дерева отрезков, разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но не всегда — см. задачу «Антиматерия» с РОИ-2017).

      Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.

      Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (\(a ∘ (b ∘ c) = (a ∘ b) ∘ c\)), коммутативность (\(a ∘ b = b ∘ a\)) и идемпотентность (\(a ∘ a = a\)). Например, её можно применять для нахождения gcd.

      Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

      From ca7c426ee99bda0a4c2d7ba29b2cdb4bcddfba74 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 9 Jun 2019 12:12:37 +0000 Subject: [PATCH 042/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/dp-optimizations.html | 5 +++- ru/hashing.html | 45 +++++++++++++++++++++------------- ru/matroid.html | 2 +- ru/mst.html | 52 ++++++++++++++++++++++++---------------- 4 files changed, 65 insertions(+), 39 deletions(-) diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index e940d02..64296d4 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -148,7 +148,10 @@

      Convex Hull Trick

      Это теперь можно переписать как \(\min_k (a_k, b_k) \cdot (1, X_{i-1})\) (тут имеется в виду скалярное произведение).

      Представим \((a_k, b_k)\) как точки на плоскости. Тогда мы можем построить их нижнюю огибающие и бинарным поиском находить оптимальную, с минимальным скалярным произведением.

      TODO: иллюстрация.

      -

      TODO: мем про Скуби-Ду.

      +
      +Паблик «Mathematical bullshit» +

      Паблик «Mathematical bullshit»

      +

      Ли Шао

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

      Лямбда-оптимизация

      diff --git a/ru/hashing.html b/ru/hashing.html index 2869306..aeef7e1 100644 --- a/ru/hashing.html +++ b/ru/hashing.html @@ -78,10 +78,10 @@

      Хэшир

      Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюрьективными.

      Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\), и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

      -hash -

      hash

      + +
      -

      Применения в реальной жизни

      +

      Применения в реальной жизни

      • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
      • Хэш-таблица. Класс unordered_set из STL можно реализовать так: заведём \(n\) изначально пустых односвязных списков. Возьмем какую-нибудь хэш-функцию \(f\) с областью значений \([0, n)\). При обработке .insert(x) мы будем добавлять элемент \(x\) в \(f(x)\)-тый список. При ответе на .find(x) мы будем проверять, лежит ли \(x\)-тый элемент в \(f(x)\)-том списке. Благодаря «равномерности» хэш-функции, после \(k\) добавлений ожидаемое количество сравнений будет равно \(\frac{k}{n}\) = \(O(1)\) при правильном выборе \(n\).
      • @@ -92,11 +92,13 @@

        Применени

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

      Сегодня же мы остановимся на строках.

      -

      Полиномиальное хэширование

      +

      Полиномиальное хэширование

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

      Будем считать, что строка — это последовательность чисел от \(1\) до \(m\) (размер алфавита). В C++ char это на самом деле тоже число, поэтому можно вычитать из символов минимальный код и кастовать в число: int x = (int) (c - 'a' + 1).

      Определим прямой полиномиальный хэш строки как значение следующего многочлена:

      -

      \[ h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p \]

      +

      \[ +h_f = (s_0 + s_1 k + s_2 k^2 + \ldots + s_n k^n) \mod p +\]

      Здесь \(k\) — произвольное число больше размера алфавита, а \(p\) — достаточно большой модуль, вообще говоря, не обязательно простой.

      Его можно посчитать за линейное время, поддерживая переменную, равную нужной в данный момент степени \(k\):

      const int k = 31, mod = 1e9+7;
      @@ -109,7 +111,9 @@ 

      Полиномиа m = (m * k) % mod; }

      Можем ещё определить обратный полиномиальный хэш:

      -

      \[ h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p \]

      +

      \[ +h_b = (s_0 k^n + s_1 k^{n-1} + \ldots + s_n) \mod p +\]

      Его преимущество в том, что можно написать на одну строчку кода меньше:

      long long h = 0;
       for (char c : s) {
      @@ -117,14 +121,20 @@ 

      Полиномиа h = (h * k + x) % mod; }

      Автору проще думать об обычных многочленах, поэтому он будет везде использовать прямой полиномиальный хэш и обозначать его просто буквой \(h\).

      -

      Зачем он нужен?

      +

      Зачем это нужно?

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

      Например, если нужно посчитать хэш от конкатенации строк \(a\) и \(b\) (т. е. \(b\) приписали в конец строки \(a\)), то можно просто хэш \(b\) домножить на \(k^{|a|}\) и сложить с хэшом \(a\):

      -

      \[ h(ab) = h(a) + k^{|a|} \cdot h(b) \]

      +

      \[ +h(ab) = h(a) + k^{|a|} \cdot h(b) +\]

      Удалить префикс строки можно так:

      -

      \[ h(b) = \frac{h(ab) - h(a)}{k^{|a|}} \]

      +

      \[ +h(b) = \frac{h(ab) - h(a)}{k^{|a|}} +\]

      А суффикс — ещё проще:

      -

      \[ h(a) = h(ab) - k^{|a|} \cdot h(b) \]

      +

      \[ +h(a) = h(ab) - k^{|a|} \cdot h(b) +\]

      В задачах нам часто понадобится домножать \(k\) в какой-то степени, поэтому имеет смысл предпосчитать все нужные степени и сохранить в массиве:

      const int maxn = 1e5+5;
       
      @@ -147,24 +157,25 @@ 

      Зачем он нужен?

      \]

      Деление по модулю воможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

      Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к \(n\)-ной. Так проще — нужно будет домножать, а не делить.

      -

      \[ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) \]

      +

      \[ +\hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) +\]

      int hash_substring (int l, int r) {
           return (h[r+1] - h[l]) * p[n-l] % mod;
       }

      Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за \(O(1)\).

      -

      Упражнение

      -

      Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще.

      +

      Упражнение. Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще.

      Примеры задач

      Количество разных подстрок. Посчитаем хэши от всех подстрок за \(O(n^2)\) и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

      Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

      Сравнение строк (больше-меньше, а не только равенство). У любых двух строк есть какой-то общий префикс (возможно, пустой). Сделаем бинпоиск по его длине, а дальше сравним два символа, идущие за ним.

      Палиндромность подстроки. Можно посчитать два массива — обратные хэши и прямые. Проверка на палиндром будет заключаться в сравнении значений hash_substring() на первом массиве и на втором.

      Количество палиндромов. Можно перебрать центр палиндрома, а для каждого центра — бинпоиском его размер. Проверять подстроку на палиндромность мы уже умеем. Как и всегда в задачах на палиндромы, случаи четных и нечетных палиндромов нужно обрабатывать отдельно.

      -

      Изменения строки*

      +

      Хранение строк в декартовом дереве

      Если для вас всё вышеперечисленное тривиально: можно делать много клёвых вещей, если «оборачивать» строки в декартово дерево. В вершине дерева можно хранить символ, а также хэш подстроки, соответствующей её поддереву. Чтобы поддерживать хэш, нужно просто добавить в upd() пересчёт хэша от конкатенации трёх строк — левого сына, своего собственного символа и правого сына.

      Имея такое дерево, мы можем обрабатывать запросы, связанные с изменением строки: удаление и вставка символа, перемещение и переворот подстрок, а если дерево персистентное — то и копирование подстрок. При запросе хэша подстроки нам, как обычно, нужно просто вырезать нужную подстроку и взять хэш, который будет лежать в вершине-корне.

      Если нам не нужно обрабатывать запросы вставки и удаления символов, а, например, только изменения, то можно использовать и дерево отрезков вместо декартова.

      -

      Вероятность ошибки и почему это всё вообще работает

      +

      Вероятность ошибки и почему это всё вообще работает

      У алгоритмов, использующих хэширование, есть один неприятный недостаток: недетерминированность. Если мы сгенерируем бесконечное количество примеров, то когда-нибудь нам не повезет, и программа отработает неправильно. На CodeForces даже иногда случаются взломы решений, использующих хэширование — можно в оффлайне сгенерировать тест против конкретного решения.

      Событие, когда два хэша совпали, а не должны, называется коллизией. Пусть мы решаем задачу определения количества различных подстрок — мы добавляем в set \(O(n^2)\) различных случайных значений в промежутке \([0, m)\). Понятно, что если произойдет коллизия, то мы какую-то строку не учтем и получим WA. Насколько большим следует делать \(m\), чтобы не бояться такого?

      Выбор констант

      @@ -189,7 +200,9 @@

      Парадокс дней р

      Более общее утверждение: в мультимножество нужно добавить \(\Theta(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

      Первое доказательство (для любителей матана). Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

      -

      \[f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d})\]

      +

      \[ +f(n, d) = (1-\frac{1}{d}) \times (1-\frac{2}{d}) \times ... \times (1-\frac{n-1}{d}) +\]

      Попытаемся оценить \(f\):

      \[ \begin{aligned} diff --git a/ru/matroid.html b/ru/matroid.html index 80d3cdc..c7d0432 100644 --- a/ru/matroid.html +++ b/ru/matroid.html @@ -122,7 +122,7 @@

      Минимальный остов

      Расписания

    Пусть у нас есть \(n\) заданий, на выполнение каждого требуется \(1\) час. Награда за выполнение \(i\)-го задания не позже \(d_i\)-того часа равна \(w_i\). В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.

    Назовём правильными те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам (\(d_i\)) и проверить, что \(d_i \geq i\) для всех \(i\).

    -

    Тогда $M = $ (множество всех заданий, множество правильных наборов заданий) является матроидом:

    +

    Тогда \(M =\) (множество всех заданий, множество правильных наборов заданий) является матроидом:

    По-английский эта задача называется Least Common Ancestor. Есть много разных способов её решать, и мы рассмотрим основные.

    -
    -lca -

    lca

    -
    +
    +lca
    lca
    +

    Для лучшего понимания: медленно (за линейное время) это можно делать так:

    -
    bool a (int u, int v) {
    -    return tin[u] <= tin[v] && tin[v] <= tout[u];
    -}
    -
    -int lca (int u, int v) {
    -    while (!ancestor(u, v))
    -        u = p[u];
    -    return u;
    -}
    +

    LCA: двоичные подъемы

    Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве up размера \(n \times \lceil \log n \rceil\) — столько точно хватит. В up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует — то корень.

    Такой препроцессинг можно выполнить за \(O(n \log n)\), используя тот факт, что предок на расстоянии \(2^{d+1}\) — это предок на расстоянии \(2^d\) предка на расстоянии \(2^d\):

    -
    int up[maxn][logn];
    -
    -void dfs (int v) {
    -    for (int l = 1; l < logn; l++)
    -        up[v][l] = up[up[v][l-1]][l-1];
    -    tin[v] = t++;
    -    for (int u : g[v]) {
    -        up[u][0] = v;
    -        dfs(u);
    -    }
    -    tout[v] = t++;
    -}
    +

    Пусть поступил очередной запрос — пара вершин \((u, v)\):

    • Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.
    • Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.

    Подробнее про второй пункт. Пусть \(L = \lceil \log n \rceil\). Присвоим \(i = L\). Будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком \(u\) (указатель up[v][i] изначально будет корнем, а затем каждую итерацию спускаться на \(2^i\)). Когда это произойдёт, подвинем указатель на \(2^i\)-го предка \(v\), и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на \(2^{i+1}\).

    -
    int lca (int v, int u) {
    -    if (a(v, u)) return v;
    -    if (a(u, v)) return u;
    -    for (int l = logn-1; l >= 0; l--)
    -        if (!ancestor(up[v][l], u))
    -            v = up[v][l];
    -    return up[v][0];
    -}
    +

    Асимптотика

    Препроцессинг — \(O(n \log n)\). Размер массива up ровно такой, и каждый его элемент вычисляется за константу.

    Ответ на запрос — \(O(\log n)\), потому что по сути мы делаем один бинпоиск.

    @@ -169,16 +201,16 @@

    Запросы на путях

    Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).

    Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером \(2^d\)-го предка минимум на соответствующем пути.

    Мы знаем, что минимум на пути от \(u\) до \(v\) — это минимум от минимума на пути от \(u\) до \(lca(u, v)\) и от минимума на пути от \(v\) до \(lca(u, v)\). А каждый минимум — это минимум на всех двоичных подъемах до LCA.

    -
    int get_min (int v, int u) {
    -    int ans = inf;
    -    for (int l = logn-1; l >= 0; l--)
    -        if (!ancestor(up[v][l], u))
    -            v = up[v][l], ans = min(ans, mn[v][l]);
    -    for (int l = logn-1; l >= 0; l--)
    -        if (!ancestor(up[u][l], v))
    -            u = up[u][l], ans = min(ans, mn[u][l]);
    -    return min({ans, mn[v][0], mn[u][0]})
    -}
    +

    Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная.

    Сведение к RMQ

    Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.

    diff --git a/ru/linalg.html b/ru/linalg.html index 9e75736..99bbbf4 100644 --- a/ru/linalg.html +++ b/ru/linalg.html @@ -1,50 +1,84 @@ - - + + - - + + Ликбез по линейной алгебре - - - - + + + + @@ -64,13 +98,13 @@ - +

    Ликбез по линейной алгебре

    Определение. Функция \(f: \mathbb{R}^n \to \mathbb{R}^m\) называется линейной, если для неё выполнено

    -
      +
      1. $ f(x+y) = f(x) + f(y) $
      2. $ f(ax) = a f(x), ; a R $
      @@ -134,27 +168,27 @@

      Что такое матрица?

      \end{pmatrix} \]

      Напишем класс, который реализует матричное умножение.

      -
      struct matrix {
      -    int n, m;
      -    int t[];
      -    matrix (int _n, int _m) {
      -        n = _n, m = _m;
      -        t = new int(n*m);
      -        memset(t, 0, sizeof t);
      -    }
      -    int[] operator[] (int k) {
      -        return t[k*m];
      -    }
      -}
      -
      -matrix operator* (matrix a, matrix b) {
      -    matrix c(a.n, b.m);
      -    for (int i = 0; i < a.n; i++)
      -        for (int j = 0; j < b.m; j++)
      -            for (int k = 0; k < a.m; k++)
      -                c[i][j] += a[i][k] * b[i][k];
      -     return c;
      -}
      +

      Динамика

      Некоторые динамики можно выразить в терминах матричного умножения.

      Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?

      @@ -182,17 +216,17 @@

      Динамика

      Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:

      \[ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) \]

      Это будет работать за \(O(n^3 \log n)\). Мы делаем \(O(n^3)\) операций для одного умножения, а всего их нужно сделать \(O(\log n)\). Кстати, наука знает и более быстрые способы перемножить матрицы, но на контестах они не нужны.

      -
      matrix binpow (matrix a, int p) {
      -    matrix b(n, n);
      -    for (int i = 0; i < n; i++)
      -        b[i][i] = 1;
      -    while (p) {
      -        if (p&1) b = b*a;
      -        a = a*a;
      -        p >>= 1;
      -    }
      -    return b;
      -}
      +

      Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: \(I\).

      \[ \begin{vmatrix} @@ -203,13 +237,7 @@

      Динамика

      \]

      В плане умножения она действительно ведет себя как единица: \(AI = A = IA\). В коде она используется вместо единицы.

      В общем случае, линейная рекуррента \(f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}\) имеет такую матрицу перехода:

      -\begin{pmatrix} -0 & 1 & 0 & \ldots & 0 \\ -0 & 0 & 1 & \ldots & 0 \\ -\vdots & \vdots & \vdots & \ddots & \vdots \\ -0 & 0 & 0 & \ldots & 1 \\ -a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ -\end{pmatrix} +

      \begin{pmatrix} 0 & 1 & 0 & & 0 \ 0 & 0 & 1 & & 0 \ & & & & \ 0 & 0 & 0 & & 1 \ a_k & a_{k-1} & a_{k-2} & & a_1 \ \end{pmatrix}

      Матрица смежности

      У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины \(a\) в вершину \(b\) за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из \(a\) в \(b\), используя ровно \(n\) переходов.

      Когда нам нужна только информация, можно ли дойти из \(a\) в \(b\) (количество способов не важно), то решение можно ускорить (см. Битсет).

      @@ -247,7 +275,7 @@

      По сути, нам нужно выразить \(c\) в другом базисе. Давайте спроецируем её на \(a\).

      Аналогично, напрягите воображение и спроецируйте эту точку в \(n\)-мерном пространстве. Это уже сложно, да?

      *Собственные векторы

      -

      Очень часто у матриц есть собственные вектора -- те, которые не меняют направление.

      +

      Очень часто у матриц есть собственные вектора – те, которые не меняют направление.

      $ Av = k v $, где \(k \neq 0\).

      $ Av - kv = (A-kI)v = 0 $. Это означает

      Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике.

      @@ -265,22 +293,22 @@

      Систе

      Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

      Метод Крамера неоптимален — там \(O(n^4)\) операций.

      В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

      -
      t gauss (matrix a) {
      -    for (int i = 0; i < n; i++) {
      -        int nonzero = i;
      -        for (int j = i+1; j < n; j++)
      -            if (a[j][i])
      -                nonzero = j;
      -        swap(a[nonzero], a[i]);
      -        for (int j = 0; j < n; j++)
      -            if (j != i && a[j][i])
      -                a[j] ^= a[i];
      -    }
      -    t x;
      -    for (int i = 0; i < n; i++)
      -        x[i] = a[i][n] ^ a[i][i];
      -    return x;
      -}
      +

      Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

      Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. Битсет).

      diff --git a/ru/matching.html b/ru/matching.html index 4de87a0..1942b48 100644 --- a/ru/matching.html +++ b/ru/matching.html @@ -1,50 +1,84 @@ - - + + - - + + Паросочетания - - - - + + + + @@ -64,9 +98,9 @@ - +

      Паросочетания

      Пусть есть \(n\) мальчиков и \(m\) девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.

      @@ -77,48 +111,45 @@

      Паросочетания

      Цепью длины \(k\) назовём некоторый простой путь (т.е. не содержащий повторяющихся вершин или рёбер), содержащий ровно \(k\) рёбер.

      Чередующейся цепью относительно некоторого паросочетания назовём простой путь длины \(k\) в которой рёбра поочередно принадлежат/не принадлежат паросочетанию.

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

      -
      - - -
      +

      Здесь красными помечены вершины паросочетания, а в графе есть увеличивающая цепь: \(1 \to 8 \to 4 \to 6 \to 3 \to 7\).

      Зачем нужны увеличивающие цепи? Оказывается, можно с их помощью увеличивать паросочетание на единицу (отсюда и название). Можно взять такой путь и провести чередование — убрать из паросочетания все рёбра, принадлежащие цепи, и, наоборот, добавить все остальные. Всего в увеличивающей цепи нечетное число рёбер, а первое и последнее были не в паросочетании. Значит, мощность паросочетания увеличилась ровно на единицу.

      В примере добавятся синие рёбра \((1, 8)\), \((3, 7)\) и \((4, 6)\), а удалятся красные \((3, 6)\) и \((4, 8)\). С ребром \((2, 5)\) ничего не случится — оно не в увеличивающей цепи. Таким образом, размер паросочетания увеличится на единицу.

      Алгоритм Куна в этом и заключается — будем искать увеличивающую цепь, пока ищется, и проводить чередование в ней. Увеличивающие цепи удобны тем, что их легко искать: можно просто запустить поиск пути из произвольной свободной вершины из левой доли в какую-нибудь свободную вершину правой доли в том же графе, но в котором из правой доли можно идти только по рёбрам паросочетания (то есть у вершин правой доли будет либо одно ребро, либо ноль). Это можно делать как угодно (для упражнения автор рекомендует явно строить такой граф, искать путь и явно проводить чередования), однако устоялась эффективная реализация в виде dfs на 20 строчек кода, приведённая ниже.

      -
      const int maxn;
      -
      -vector<int> g[maxn]; // будем хранить только рёбра из левой доли в правую
      -int mt[maxn]; // с какой вершиной сматчена вершина правой доли (-1, если ни с какой)
      -bool used[maxn]; // вспомогательный массив для поиска пути dfs-ом
      -
      -// dfs возвращает, можно ли найти путь из вершины v
      -// в какую-нибудь вершину правой доли
      -// если можно, то ещё и проводит чередование
      -bool dfs (int v) {
      -    if (used[v])
      -        return false;
      -    used[v] = true;
      -    for (int u : g[v]) {
      -        // если вершина свободна, то можно сразу с ней соединиться
      -        // если она занята, то с нейможно соединиться только тогда,
      -        // когда из её текущей пары можно найти какую-нибудь другую вершину
      -        if (mt[u] == -1 || dfs(mt[u])) {
      -            mt[u] = v;
      -            return true;
      -        }
      -    }
      -    return false;
      -}
      -
      -
      -// где-то в main:
      -
      -memset(mt, -1, sizeof(mt));
      -for (int i = 0; i < n; i++) {
      -    memset(used, 0, sizeof(mt));
      -    if (dfs(i))
      -        cnt++;
      -}
      +

      Корректность

      Для доказательства алгоритма нам будет достаточно ещё доказать, что если увеличивающие цепи уже не ищутся, то паросочетание в принципе нельзя увеличить.

      Теорема (Бержа). Паросочетание без увеличивающих цепей является максимальным.

      @@ -129,7 +160,7 @@

      Скорость работы

      Что примечательно, его можно не бояться запускать на ограничениях и побольше (\(n, m \approx 10^4\)), потому что для него есть мощные неасимптотические оптимизации:

      • Eго можно жадно инициализировать (просто заранее пройтись по вершинам левой доли и сматчить их со свободной вершиной правой, если она есть).

      • -
      • Можно не заполнять нулями на каждой итерации массив used, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно -- номер итерации, на которой это значение стало true. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как false. В каком-то смысле это позволяет эмулировать очищение массива за константу.

      • +
      • Можно не заполнять нулями на каждой итерации массив used, а использовать следующий трюк: хранить в нём вместо булева флага версию последнего изменения, а конкретно – номер итерации, на которой это значение стало true. Если этот номер меньше текущего номера итерации, то мы можем воспринимать это значение как false. В каком-то смысле это позволяет эмулировать очищение массива за константу.

      • Очень часто граф приходит из какой-то другой задачи, природа которой накладывает ограничения на его вид. Например, в задачах на решетках (когда есть двумерный массив, и соседние клетки связаны друг с другом) граф двудольный, но степень каждой вершины маленькая, и граф имеет очень специфичную структуру, и на нём алгоритм Куна работает быстрее, чем ожидается из формулы \(n \times m\). Контрпримеры в таких задачах на самом деле почти всегда можно сгенерировать, но авторы редко так заморачиваются.

      Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить алгоритм Диница к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за \(O(n \sqrt m)\).

      @@ -142,10 +173,7 @@

      Покрытие путями DAG-

      Мы теперь можем свести задачу к нахождению максимального паросочетания в двудольном графе \(H\). После нахождения этого паросочетания мы должны преобразовать его в набор путей в \(G\). Это делается тривиальным алгоритмом: возьмем \(a_1\), посмотрим, с какой \(b_k\) она соединена, посмотрим на \(a_k\) и так далее. Некоторые вершины могут остаться ненасыщенными — в таком случае в ответ надо добавить пути нулевой длины из каждой из этих вершин.

      Лемма Холла

      Лемма Холла (или: теорема о свадьбах) — очень удобный критерий в задачах, где нужно проверить, что паросочетание существует, но при этом не требуется строить его явно.

      -
      - - -
      +

      Лемма Холла. Полное паросочетание существует тогда и только тогда, когда любая группа вершин левой доли соединена с не меньшим количеством вершин правой доли.

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

      В другую сложнее — нужно воспользоваться индукцией. Будем доказывать, что если паросочетание не полное, то можно в таком графе найти увеличивающую цепь, и с её помощью увеличить паросочетание на единицу.

      diff --git a/ru/matroid.html b/ru/matroid.html index c7d0432..959779a 100644 --- a/ru/matroid.html +++ b/ru/matroid.html @@ -1,50 +1,84 @@ - - + + - - + + Матроиды - - - - + + + + @@ -64,9 +98,9 @@ - +

      Матроиды

      Понятие матроида нужно, чтобы придумывать и доказывтать некоторые жадные алгоритмы в задачах, где нужно набрать какое-то множество объектов максимального веса. Например, минимальный остов, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени.

      Если вы не специалист по абстрактной алгебре, рекомендуется сначала посмотреть примеры матроидов в конце статьи.

      @@ -74,25 +108,23 @@

      Определение

      Матроидом называется пара \((X, I)\), где \(X\) — множество элементов, называемое носителем матроида, а \(I\) — некоторое множество подмножеств \(X\), называемое семейством независимых множеств. В матроиде должны выполняться следующие свойства:

      • Пустое множество является независимым: \(\varnothing \in I\)

      • -
      • Любое подмножество независимого множества тоже независимо:

      • -
      +
    1. Любое подмножество независимого множества тоже независимо:

      \[ - A \subset B, B \in I \implies A \in I - \]

      -
        -
      • Если в независимом множестве \(A\) меньше элементов, чем в независимом множестве \(B\), то будет существовать элемент из \(B\), дополняющий \(A\) до независимого множества размера \(|A|+1\):
      • -
      +A \subset B, B \in I \implies A \in I +\]

    2. +
    3. Если в независимом множестве \(A\) меньше элементов, чем в независимом множестве \(B\), то будет существовать элемент из \(B\), дополняющий \(A\) до независимого множества размера \(|A|+1\):

      \[ - A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I - \]

      +A, B \in I, |A| < |B| \implies \exists x \in B \setminus A: A \cup \{x\} \in I +\]

    4. +

      Матроид называется взвешенным, если на нем есть аддитивная весовая функция: \(w(A) = \sum w(a_i)\).

      Алгоритм Радо-Эдмондса

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

      -
      X.sort()
      -s = []
      -for x in X:
      -    if good(s + [x]):
      -        s += [x]
      +

      Здесь под good имеется в виду \(s \cup x \in I\).

      Корректность этого алгоритма для произвольного матроида следует из следующего утверждения:

      Теорема Радо-Эдмондса. Пусть \(A \in I\) — множество минимального веса среди всех независимых подмножеств \(X\) мощности \(k\). Возьмем \(x: A \cup x \in I,\;x \notin A,\;w(x)\) — минимальна. Тогда \(A \cup x\) — множество минимального веса среди независимых подмножеств \(X\) мощности \(k + 1\).

      diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html index 3f96d60..14a1833 100644 --- a/ru/mincost-maxflow.html +++ b/ru/mincost-maxflow.html @@ -1,50 +1,84 @@ - - + + - - + + Поток минимальной стоимости - - - - + + + + @@ -64,9 +98,9 @@ - +

      Поток минимальной стоимости

      Рассмотрим ориентированный граф \(G = (V, E)\) с истоком \(s\) и стоком \(t\), в котором у каждого ребра \((u, v)\) задана целая стоимость \(w_{uv}\) и целая положительная пропускная способность \(c_{uv}\). Требуется найти максимальный поток, стоимость которого минимальна:

      \[ \sum_{(u, v) \in E} f_{uv} \to \max \] \[ \sum_{(u, v) \in E} f_{uv} w_{uv} \to \min \]

      @@ -129,52 +163,52 @@

      Реализация

    5. par — предок вершины в алгоритме Дейкстры (нужен для проталкивания потока)
    6. d — временный массив для алгоритма Дейкстры, куда будут записаны новые расстояния
    7. -
      const int maxn = 305, inf = 1e9;
      -
      -int n;
      -int cost[maxn][maxn], cap[maxn][maxn];
      -int d[maxn], pot[maxn], par[maxn];
      -
      -bool dijkstra (int s, int t) {
      -    used[maxn] = {0};
      -
      -    fill(d, d+n, inf);
      -    d[s] = 0;
      -
      -    while (1) {
      -        int v = -1;
      -        for (int u = 0; u < n; u++)
      -            if (!used[u] && (v == -1 && d[u] < d[v]))
      -                v = u;
      -        if (v == -1 || d[v] == inf)
      -            break;
      -        used[v] = 1;
      -        for (int u = 0; u < n; u++) {
      -            int w = cost[v][u] + pot[v] - pot[u];
      -            if (cap[v][u] && d[u] > d[v] + w) {
      -                d[u] = d[v] + w;
      -                par[u] = v;
      -            }
      -        }
      -    }
      -
      -    return d[t] < inf;
      -}
      -
      -int mincost_maxflow (int s, int t) {
      -    int ans = 0;
      -    while (dijkstra(s, t)) {
      -        memcpy(pot, d, sizeof(d));
      -        int delta = inf;
      -        for (int v = t; v != s; v = par[v])
      -            delta = min(delta, cap[par[v]][v]);
      -        for (int v = t; v != s; v = par[v]) {
      -            cap[par[v]][v] -= delta;
      -            cap[v][par[v]] += delta;
      -            ans += cost[par[v]][v]*delta;
      -        }
      -    }
      -    return ans;
      -}
      + diff --git a/ru/mst.html b/ru/mst.html index 61c1536..aed65ed 100644 --- a/ru/mst.html +++ b/ru/mst.html @@ -1,50 +1,84 @@ - - + + - - + + Минимальные остовы - - - - + + + + @@ -64,9 +98,9 @@ - +

      Минимальные остовы

      Рассмотрим следующую задачу:

      @@ -86,10 +120,7 @@

      Лемма о безоп

      Все алгоритмы для поиска минимального остова опираются на следующее утверждение:

      Лемма о безопасном ребре. Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным.

      Доказательство: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить, то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже. Противоречие.

      -
      - - -
      +

      Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов.

      Алгоритм Прима

      Один из подходов — строить минимальный остов остепенно, добавленяя в него рёбра по одному.

      @@ -99,65 +130,65 @@

      Алгоритм Прима

      Этот алгоритм очень похож на алгоритм Дейкстры, только тут мы выбираем следующую вершину с другой весовой функцией — вес соединяющего ребра вместо суммарного расстояния до неё.

      Совсем наивная реализация за \(O(nm)\) — каждый раз перебираем все рёбра:

      -
      const int maxn = 1e5, inf = 1e9;
      -vector from, to, weight;
      -bool used[maxn]
      -
      -// считать все рёбра в массивы
      -
      -used[0] = 1;
      -for (int i = 0; i < n-1; i++) {
      -    int opt_w = inf, opt_from, opt_to;
      -    for (int j = 0; j < m; j++)
      -        if (opt_w > weight[j] && used[from[j]] && !used[to[j]])
      -            opt_w = weight[j], opt_from = from[j], opt_to = to[j]
      -    used[opt_to] = 1;
      -    cout << opt_from << " " << opt_to << endl;
      -}
      +

      Реализация за \(O(n^2)\):

      -
      const int maxn = 1e5, inf = 1e9;
      -bool used[maxn];
      -vector< pair<int, int> > g[maxn];
      -int min_edge[maxn] = {inf}, best_edge[maxn];
      -min_edge[0] = 0;
      -
      -// ...
      -
      -for (int i = 0; i < n; i++) {
      -    int v = -1;
      -    for (int u = 0; u < n; j++)
      -        if (!used[u] && (v == -1 || min_edge[u] < min_edge[v]))
      -            v = u;
      -
      -    used[v] = 1;
      -    if (v != 0)
      -        cout << v << " " << best_edge[v] << endl;
      -
      -    for (auto e : g[v]) {
      -        int u = e.first, w = e.second;
      -        if (w < min_edge[u]) {
      -            min_edge[u] = w;
      -            best_edge[u] = v;
      -        }
      -    }
      -}
      +

      Можно не делать линейный поиск оптимальной вершины, а поддерживать его в приоритетной очереди, как в алгоритме Дейкстры. Получается реализация за \(O(m \log n)\):

      -
      set< pair<int, int> > q;
      -int d[maxn];
      -
      -while (q.size()) {
      -    v = q.begin()->second;
      -    q.erase(q.begin());
      -
      -    for (auto e : g[v]) {
      -        int u = e.first, w = e.second;
      -        if (w < d[u]) {
      -            q.erase({d[u], u});
      -            d[u] = w;
      -            q.insert({d[u], u});
      -        }
      -    }
      -}
      +

      Про алгоритм за \(O(n^2)\) забывать не стоит — он работает лучше в случае плотных графов.

      Система непересекающихся множеств

      Система непересекающихся множеств (англ. disjoint set union) — структура данных, которая используется для хранения информации о связности компонент. Она нам потребуется для описания следующего подхода — алгоритма Крускала.

      @@ -169,74 +200,73 @@

      Сис

      Обе операции выполняются в среднем почти за \(O(1)\) (но не совсем — этот сложный вопрос будет разъяснен позже).

      Множества элементов мы будем хранить в виде деревьев: одно дерево соответствует одному множеству. Корень дерева — это представитель (лидер) множества. Заведём массив _p, в котором для каждого элемента мы храним номер его предка в дереве. Для корней деревьев будем считать, что их предки — они сами.

      Наивная реализация, которую мы потом ускорим:

      -
      int _p[maxn];
      -
      -int p (int v) {
      -    if (_p[v] == v)
      -        return v;
      -    else
      -        return p(_p[v]);
      -}
      -
      -void unite (int a, int b) {
      -    a = p(a), b = p(b);
      -    _p[a] = b;
      -}
      -
      -for (int i = 0; i < n; i++)
      -    _p[i] = i;
      +

      Эвристика сжатия пути. Оптимизируем работу функции p. Давайте перед тем, как вернуть ответ, запишем его в _p от текущей вершины, то есть переподвесим его за самую высокую.

      -
      -Паблик «Странные опросы для спортивных программистов» -

      Паблик «Странные опросы для спортивных программистов»

      -
      +
      +Паблик «Странные опросы для спортивных программистов»
      Паблик «Странные опросы для спортивных программистов»
      +

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

      Ранговая эвристика. Будем хранить для каждой вершины её ранг — высоту её поддереа. При объединении деревьев будем делать корнем нового дерева ту вершину, у которой ранг больше, и пересчитывать ранги (ранг у лидера должен увеличиться на единицу, если он совпадал с рангом другой вершины). Эта эвристика оптимизирует высоту дерева напрямую.

      Весовая эвристика. Будем вместо ранга хранить размеры поддеревьев для каждой вершины, а при объединении — подвешивать за более «тяжелую».

      Финальная реализация, использующая весовую эвристику и эвристику сжатия путей:

      -
      int _p[maxn], s[maxn];
      -
      -int p (int v) { return (_p[v] == v) ? v : _p[v] = p(_p[v]); }
      -
      -void unite (int a, int b) {
      -    a = p(a), b = p(b);
      -    if (s[a] > s[b])
      -        swap(a, b);
      -    s[b] += s[a];
      -    _p[a] = b;
      -}
      -
      -// где-то в main:
      -
      -for (int i = 0; i < n; i++)
      -    _p[i] = i;
      +

      Автор предпочитает именно весовую эвристику, потому что часто в задачах размеры компонент требуются сами по себе.

      Асимптотика

      -

      Эвристика сжатия путей улучшает асимптотику до \(O(\log n)\) в среднем. Здесь используется именно амортизированная оценка — понятно, что в худшем случае нужно будет сжимать весь бамбук за \($O(n)\).

      +

      Эвристика сжатия путей улучшает асимптотику до \(O(\log n)\) в среднем. Здесь используется именно амортизированная оценка — понятно, что в худшем случае нужно будет сжимать весь бамбук за $\(O(n)\).

      Индукцией несложно показать, что весовая и ранговая эвристики ограничивают высоту дерева до \(O(\log n)\), а соответственно и асимптотику тоже.

      При использовании эвристики сжатия плюс весовой или ранговой асимптотика будет \(O(a(n))\), где \(a(n)\) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4).

      Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует.

      Алгоритм Крускала

      Отсортируем рёбра и будем пытаться добавлять их в остов в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить.

      Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Наивная проверка будет работать за \(O(m \log m + n^2)\), но асимптотику можно улучшить до \(O(n \log m)\) (до стоимости сортировки), если для проверок использовать систему непересекающихся множеств.

      -
      // (w, (a, b))
      -vector< pair< int, pair<int, int> > > edges;
      -
      -sort(edges.begin(), edges.end());
      -
      -for (auto e : edges) {
      -    int a = e.first.first, b = e.first.second;
      -    // компоненты разные, если лидеры разные
      -    if (p(a) != p(b)) {
      -        // добавим ребро (a, b)
      -        unite(a, b);
      -    }
      -}
      +

      Алгоритм Борувки

      Лемма. Для любой вершины минимальное инцидентное ей реборо является безопасным.

      Доказательство. Пусть есть минимальный остов, в котором для какой-то врешины \(v\) нет её минимального инцидентного ребра. Тогда, если добавить это ребро, образуется цикл, из которого можно удалить другое ребро, тоже инцидентное \(v\), но имеющее не меньший вес.

      Алгоритм Борувки опирается на этот факт и заключается в следующем:

      -
        +
        1. Для каждой вершины найдем минимальное инцидентное ей ребро.
        2. Добавим все такие рёбра в остов (это безопасно — см. лемму) и сожмем получившиеся компоненты, то есть объединим списки смежности вершин, которые эти рёбра соединяют.
        3. Повторяем шаги 1-2, пока в графе не останется только одна вершина-компонента.
        4. diff --git a/ru/pollard.html b/ru/pollard.html index 061741d..5034467 100644 --- a/ru/pollard.html +++ b/ru/pollard.html @@ -1,50 +1,84 @@ - - + + - - + + Факторизация за $O(\sqrt[4]{n})$ - - - - + + + + @@ -64,9 +98,9 @@ - +

          Факторизация за \(O(\sqrt[4]{n})\)

          Наука умеет раскладывать целые числа на множители за \(O(n^\frac{1}{4})\). Алгоритм рандомизированный, поэтому нам сначала понадобится один факт из теорвера.

          Парадокс дней рождений

          @@ -86,30 +120,30 @@

          Парадокс дней р

          Из формулы более-менее понятно, что вероятность \(\frac{1}{2}\) достигается при \(n \approx \sqrt{d}\) и в этой точке изменяется быстро. Для самого алгоритма нам понадобится следующее:

          Утверждение. В мультимножество нужно добавить \(O(\sqrt{n})\) случайных чисел от 1 до n, чтобы какие-то два совпали.

          Если вы не доверяете математике, посмотрите на графики:

          -
          import matplotlib.pyplot as plt
          -%matplotlib inline
          -
          -import seaborn as sns
          -sns.set()
          -
          def f(n, d):
          -    p = 1
          -    for x in range(1, n):
          -        p *= 1-x/d
          -    return p
          -
          -def g(n, d):
          -    return 2.71**(-0.5 * n**2 / d)
          -
          exact = [f(n, 365) for n in range(40)]
          -approx = [g(n, 365) for n in range(40)]
          -plt.plot(exact)
          -plt.plot(approx)
          -plt.show()
          + + +

          TODO: график

          На графике две линии, если не видно, — аппроксимация настолько хороша. Чтобы продемонстрировать \(n \approx \sqrt{d}\), построим такую же для больших чисел и перейдём в логарифмический масштаб.

          -
          approx = [g(n, 10**10) for n in range(10**6)]
          -plt.xscale('log')
          -plt.plot(approx)
          -plt.show()
          +

          TODO: график

          \(\rho\)-алгоритм Полларда

          Итак, мы хотим факторизовать число \(n\). Предположим, что \(n = p q\) и \(p \approx q\). Понятно, что труднее случая, наверное, нет. Алгоритм итеративно ищет наименьший делитель и сводит задачу к как минимум в два раза меньшей.

          @@ -120,21 +154,21 @@

          \(\rho\

          Доказательство: так как \(p\) — меньший делитель, то \(p \leq \sqrt{n}\). Теперь просто подставим в предыдущее утверждение: в множество нужно добавить \(O(\sqrt{p}) = O(\sqrt[4]{n})\) элементов, чтобы какие-то два совпали, а значит последовательность зациклилась.

          Если мы найдём цикл в такой последовательности (то есть такие \(i\) и \(j\), что \(f^i(x_0) \equiv f^j(x_0)\)), то мы можем восстановить какой-то делитель \(n\), а именно \(\gcd(|f^i(x_0) - f^j(x_0)|, n)\) — это число меньше \(n\) и делится на \(p\).

          Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющиеся друг от друга указателя \(i\) и \(j\) (\(i = 2j\)) и проверять, что \(f^i(x_0) \equiv f^j(x_0) \pmod p\), что эквивалентно проверке \(\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1\).

          -
          from math import gcd
          -from random import randint
          -
          def find_divisor(n, seed = 1, f = lambda x: (x+1)**2):
          -    x = y = seed
          -    d = 1
          -    while d == 1:
          -        x = f(f(x)) % n
          -        y = f(y) % n
          -        d = gcd(abs(x-y), n)
          -    return d
          -
          n = 3*5*7 # = 105
          -print([find_divisor(n, seed=i) for i in range(n)])
          + + +
          [3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3, 3, 21, 7, 21, 21, 21, 3, 3, 7, 21, 21, 7, 3, 3, 21, 21, 21, 7, 21, 3, 3]
          -
          n = (10**9+7)*(10**9+9)
          -print([find_divisor(n, seed=randint(1, n)) for _ in range(20)])
          +
          [1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009, 1000000007, 1000000009, 1000000009, 1000000009, 1000000009, 1000000009, 1000000007, 1000000009]

          Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за \(O(\sqrt{n})\) (нужно добавить отсечение по времени).

          Разное

          diff --git a/ru/probability.html b/ru/probability.html index 7ba2083..0ed3bdf 100644 --- a/ru/probability.html +++ b/ru/probability.html @@ -1,50 +1,84 @@ - - + + - - + + Ликбез по теорверу - - - - + + + + @@ -64,18 +98,18 @@ - +

          Ликбез по теорверу

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

          -
          import numpy as np
          -
          -import matplotlib as plt
          -%matplotlib inline
          -
          -import seaborn as sns
          -sns.set()
          +

          Матожидание

          Дисперсия

          Какие два числа лучше всего описывают распределение?

          @@ -90,7 +124,7 @@

          Применения

          Мы спросили у 1000 случайных избирателей бинарный вопрос, и 510 из них сказали, что будут голосовать за Путина. С какой вероятностью он победит? Теорема говорит, что число голосов, как

          Линейные рекурренты

          Чтобы решать следующие задачи, нам нужно будет использовать следующий факт:

          -

          ...

          +

          Доказательство мы не приведем.

          В частности, таким образом получается формула для чисел Фибоначчи.

          \[ f_n = \ldots \]

          @@ -98,7 +132,7 @@

          Линейные рекуррен

          Классика

          Парадокс дней рождения.

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

          -

          Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт?

          +

          Пьяница. Человек стоит на краю обрава и идёт в его сторону с вероятностью p. С какой вероятностью он когда-либо в него упадёт?

          TODO: история про эстетическое удовольствие, азарт и смысл посещения казино. Казино. Мы приходим в казино с 1000$ и следующим образом проводим там время: ставим по 1$, пока не обанкротимся или не выиграем 1100$. Какая вероятность того, что мы уйдём с деньгами?

          Принцип максимального правдоподобия

          Энтропия

          diff --git a/ru/reciprocal.html b/ru/reciprocal.html index 823b309..59ab273 100644 --- a/ru/reciprocal.html +++ b/ru/reciprocal.html @@ -1,50 +1,84 @@ - - + + - - + + Обратный элемент по модулю - - - - + + + + @@ -64,15 +98,15 @@ - +

          Обратный элемент по модулю

          Часто в задачах требуется посчитать что-то по простому модулю (чаще всего \(10^9 + 7\)). Это делают для того, чтобы участникам не приходилось использовать длинную арифметику, и они могли сосредоточиться на самой задаче.

          Обычные арифметические операции выполняются не сильно сложнее — просто нужно брать модули и заботиться о переполнении. Например:

          -
          c = (a + b) % mod;
          -c = (mod + a - b) % mod;
          -c = a * b % mod;
          +

          Но вот с делением возникают проблемы — мы не можем просто взять и поделить. Пример: \(\frac{8}{2} = 4\), но \(\frac{8 \% 5 = 3}{2 \% 5 = 2} \neq 4\).

          Нужно найти некоторый элемент, который будет себя вести как \(\frac{1}{a} = a^{-1}\), и вместо «деления» домножать на него. Назовем такой элемент обратным.

          Способ 1: бинарное возведение в степень

          @@ -93,27 +127,27 @@

          \[ a^p \equiv a \implies a^{p-1} \equiv 1 \implies a^{p-2} \equiv a^{-1} \]

          Получается, что \(a^{p-2}\) ведет себя как \(a^{-1}\), что нам по сути и нужно. Посчитать \(a^{p-2}\) можно за \(O(\log p)\) бинарным возведением в степень.

          Приведем код, который позволяет считает \(C_n^k\).

          -
          int t[maxn]; // факториалы, можно предподситать простым циклом
          -
          -// бинарное возведение в степень
          -int bp (int a, int n) {
          -    int res = 1;
          -    while (n) {
          -        if (n & 1) res = res * a % mod;
          -        a = a * a % mod;
          -        n >>= 1;
          -    }
          -    return res;
          -}
          -
          -// находит обратный элемент как a^(p-2)
          -int inv (int x) {
          -    return bp(x, mod-2);
          -}
          -
          -int c (int n, int k) {
          -    return t[n] * inv(t[k]) % mod * inv(t[n-k]) % mod;
          -}
          +

          Способ 2: диофантово уравнение

          Диофантовыми уравнениями называют такие штуки:

          \[ ax + by = 1 \]

          @@ -129,7 +163,7 @@

          Способ 2:

          Сам автор почти всегда использует возведение в степень.

          Почему \(10^9+7\)?

          -
            +
            1. Это выражение довольно легко вбивать (1e9+7).
            2. Простое число.
            3. Достаточно большое.
            4. @@ -141,15 +175,15 @@

              \(C_n^k\), но для больших \(n\) и \(k\), поэтому асимптотика \(O(n \log m)\) нас не устроит. Оказывается, мы можем сразу предподсчитать все обратные ко всем факториалам.

              Если у нас уже написан inv, то нам не жалко потратить \(O(\log m)\) операций, посчитав \(m!^{-1}\).

              После этого мы будем считать \((m-1)!^{-1}\) как \(m!^{-1} m = \frac{1}{1 \cdot 2 \cdot \ldots \cdot (m-1)}\).

              -
              int f[maxn];
              -f[0] = 1;
              -for (int i = 1; i < maxn; i++)
              -    f[i] = i*f[i-1] % mod;
              -
              -int r[maxn];
              -r[maxn-1] = inv(f[maxn-1])
              -for (int i = maxn-1; i >= 1; i--)
              -    r[i-1] = r[i]*i % mod;
              +

              TODO: техника с сайта емакса.

              diff --git a/ru/segtree.html b/ru/segtree.html index f7d6e0f..19b9ff3 100644 --- a/ru/segtree.html +++ b/ru/segtree.html @@ -1,50 +1,84 @@ - - + + - - + + Дерево отрезков - - - - + + + + @@ -64,16 +98,16 @@ - +

              Дерево отрезков

              Замечание. Почти везде мы будем использовать полуинтервалы — обозначаемые как \([l, r)\) — вместо отрезков. Несмотря на контринтуитивность, это немного упростит код и вообще является хорошей практикой в программировании, подобно нумерации с нуля.

              Дерево отрезков — очень мощная и гибкая структура данных, позволяющая быстро отвечать на самые разные запросы на отрезках.

              Рассмотрим конкретную задачу:

              Дан массив \(a\) из \(n\) целых чисел, нужно уметь отвечать на запросы двух типов:

              -
                +
                1. Изменить значение в ячейке (т. е. отреагировать на присвоение a[k] = x).

                2. Вывести сумму элементов \(a_i\) на отрезке с \(l\) по \(r\).

                @@ -82,10 +116,7 @@

                Дерево отрезков

                Чтобы решить задачу, сделаем с исходным массивом следующие манипуляции:

                Посчитаем сумму всего массива и где-нибудь запишем. Потом разделим его пополам и посчитаем сумму на половинах и тоже где-нибудь запишем. Каждую половину потом разделим пополам ещё раз, и так далее, пока не придём к отрезкам длины 1.

                Эту последовательность разбиений можно представить в виде дерева. Корень этого дерева соответствует отрезку \([0, n)\), а каждая вершина (не считая листьев) имеет ровно двух сыновей, которые тоже соответствуют каким-то отрезкам. Отсюда и название — «дерево отрезков».

                -
                - - -
                +

                Строить его можно рекурсивной функцией:

                • Если вершина является листом, взять в качестве суммы значение соответствующей ячейки.
                • @@ -115,69 +146,69 @@

                  Ликбез по C++

                  Объект — это сущность, которой можно посылать сообщения и которая может на них реагировать, используя свои данные. Инкапсулировать логику в объекты на самом деле очень удобно. Дереву отрезков не важно знать, как устроен окружающий мир, а миру не важно, как внутри устроено дерево отрезков — это просто какая-то структура, которая умеет делать нужные операции за \(O(\log n)\).

                  В C++ есть два способа объявлять классы (объект — это экземпляр класса): через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

                  У классов есть поля (переменные) и методы (функции, привязанные к объектам). Среди них есть особые, например конструктор — он вызывается при создании объекта. Чтобы объявить конструктор класса в C++, нужно объявить внутри него метод с тем же названием, что и у самого класса.

                  -
                  struct A {
                  -    int param1, param2; // тут можно что-то хранить
                  -    char param3 = 'k';
                  -    A (int var) {
                  -        // эта часть называется конструктором
                  -        // ...
                  -    }
                  -    void do_something () {
                  -        // это какой-то другой метод
                  -        // ...
                  -    }
                  -}; // <- не забудьте точку с запятой
                  +

                  Другое важное понятие — указатель. Память можно представлять как просто очень большой массив. На самом деле, когда мы создаем какой-то объект, отдельная программа (аллокатор) выделяет место в массиве (оперативной памяти) под этот объект и возвращает позицию (указатель) на место в этом массиве.

                  Указатели нам нужны для того, чтобы хранить ссылки на детей. Имея указатель на объект, можно делать всё то же, что и имея сам объект, только синтаксис немного поменяется:

                  -
                  A x(179);
                  -x.do_something();
                  -x.param1 = 57;
                  -
                  -A *y = new A(42); // new возвращает адрес, по которому можно найти объект
                  -y->do_something();
                  -y.param3 = '!';
                  +

                  Оффтоп: вы не задумывались, почему мы перешли с 32-битных процессоров на 64-битные? Каждый указатель ссылается на байт — более точный адрес менеджер памяти выделять не умеет. Поэтому 32-битный компьютер умеет работать только с не более, чем \(2^{32}\) байтами памяти — это ровно 4 гигабайта — что с какого-то момента начало нехватать. Большинство операций в любом компьютере — это операции с памятью, и разрядность повысили именно из-за этого, а не чтобы операции с long long быстрее считались

                  Реализация

                  Примечание. Данная реализация неэффективна по времени и памяти (см. раздел «другие реализации»). Мы её приводим в педагогических целях.

                  Общий план реализации любых структур данных:

                  -
                    +
                    1. Полностью понять все инварианты — как должна выглядеть структура, какие значения должны принимать поля, etc.
                    2. Формально описать, что должны делать методы и за какую асимптотику.
                    3. Решить много отдельных задач, реализуя методы, не нарушающие инварианты.
                    -
                    struct segtree {
                    -    int lb, rb; // левые и правые границы отрезков
                    -    int sum = 0; // сумма на текущем отрезке
                    -    segtree *l = 0, *r = 0;
                    -    segtree (int _lb, int _rb) {
                    -        lb = _lb, rb = _rb;
                    -        if (lb + 1 < rb) {
                    -            // если не лист, создаем детей
                    -            int t = (lb + rb) / 2;
                    -            l = new segtree(lb, t);
                    -            r = new segtree(t, rb);
                    -        }
                    -    }
                    -    void add (int k, int x) {
                    -        sum += x;
                    -        if (l) {
                    -            if (k < l->rb)
                    -                l->add(k, x);
                    -            else
                    -                r->add(k, x);
                    -        }
                    -    }
                    -    int get_sum (int lq, int rq) {
                    -        if (lb >= lq && rb <= rq)
                    -            // если мы лежим полностью в отрезке запроса, вывести сумму
                    -            return sum;
                    -        if (max(lb, lq) >= min(rb, rq))
                    -            // если мы не пересекаемся с отрезком запроса, вывести ноль
                    -            return 0;
                    -        // иначе всё сложно -- запускаемся от детей и пусть они там сами решают
                    -        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
                    -    }
                    -};
                    +

                    Посчитать число беспорядков в перестановке из \(n\) элементов (беспорядок или инверсия — это пара чисел \(i < j\), для которых \(p_i > p_j\)).

                    @@ -203,136 +234,136 @@

                    Отложенные операц

                    Когда нам позже понадобятся правильные значения таких вершин и их детей, мы будем делать «проталкивание» информации из текущей вершины в её сыновей: если метка стоит, пересчитаем сумму текущего отрезка и передадим эту метку сыновьям. Когда нам потом понадобятся сыновья, мы будем делать то же самое. Подобная операция будет гарантировать корректность данных в вершине ровно к тому моменту, когда они нам понадобятся.

                    Понятно, что от использования таких «запаздывающих» обновлений асимптотика никак не уходшается, и мы можем всё так же решить задачу за \(O(n \log n)\).

                    При реализации создадим вспомогательную функцию push, которая будет производить проталкивание информации из этой вершины в обоих её сыновей. Вызывать её стоит в самом начале обработки любого запроса — тогда она гарантирует, что в текущей вершине и её сыновьях все значения корректны.

                    -
                    struct segtree {
                    -    int lb, rb;
                    -    int sum = 0, assign = -1;
                    -    segtree *l = 0, *r = 0;
                    -    segtree (int _lb, int _rb) {
                    -        lb = _lb, rb = _rb;
                    -        if (lb + 1 < rb) {
                    -            int t = (lb + rb) / 2;
                    -            l = new segtree(lb, t);
                    -            r = new segtree(t, rb);
                    -        }
                    -    }
                    -    void push () {
                    -        if (assign != -1) {
                    -            sum = (rb-lb) * assign;
                    -            if (l) { // если дети есть
                    -                l->assign = assign;
                    -                r->assign = assign;
                    -            }
                    -        }
                    -        assign = -1;
                    -    }
                    -    void upd (int lq, int rq, int x) {
                    -        push();
                    -        if (lq <= lb && rb <= rq)
                    -            assign = x;
                    -        else if (l && max(lb, lq) < min(rb, rq)) {
                    -            // если есть дети и отрезок запроса хоть как-то пересекается с нашим
                    -            l->upd(lq, rq, x);
                    -            r->upd(lq, rq, x);
                    -            // ...дальше они сами разберутся
                    -        }
                    -    }
                    -    int get_sum (int lq, int rq) {
                    -        push();
                    -        if (lb >= lq && rb <= rq)
                    -            return sum;
                    -        if (max(lb, lq) >= min(rb, rq))
                    -            return 0;
                    -        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
                    -    }
                    -};
                    +

                    По-английски эта техника называется lazy propagation. Очень важно научиться её писать — она часто встречается на олимпиадах.

                    Идея «давайте будем всё делать в последний момент» применима не только в ДО, но и в других структурах и в реальной жизни.

                    Динамическое построение

                    А что, если у нас все индексы лежать не от в пределах \(10^5\), а, например, \(10^9\). Все асимптотики нас по прежнему устраивают (\(\log_2 10^6 \approx 20\), \(\log_2 10^9 \approx 30\)), кроме этапа построения.

                    Можно решить эту проблему так: откажемся от явного создания всех вершин дерева изначально. Изначально создадим только лишь корень, а остальные вершины будем создавать на ходу, когда в них потребуется записать что-то не дефолтное — как в lazy propagation.

                    Реализовать это можно так же, как и с push-ем: в начале всех методов будем проверять, что дети-вершины созданы, и создавать их, если это не так.

                    -
                    struct segtree {
                    -    int lb, rb;
                    -    int sum = 0;
                    -    segtree *l = 0, *r = 0;
                    -    segtree (int _lb, int _rb) {
                    -        lb = _lb, rb = _rb;
                    -        // а тут ничего нет
                    -    }
                    -    void extend () {
                    -        if (!l && lb + 1 < rb) {
                    -            int t = (lb + rb) / 2;
                    -            l = new segtree(lb, t);
                    -            r = new segtree(t, rb);
                    -        }
                    -    }
                    -    void add (int k, int x) {
                    -        extend();
                    -        sum += x;
                    -        if (l) {
                    -            if (k < l->rb)
                    -                l->add(k, x);
                    -            else
                    -                r->add(k, x);
                    -        }
                    -    }
                    -    int get_sum (int lq, int rq) {
                    -        if (lb >= lq && rb <= rq)
                    -            return sum;
                    -        if (max(lb, lq) >= min(rb, rq))
                    -            return 0;
                    -        extend();
                    -        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
                    -    }
                    -};
                    +

                    Но вообще, в большинстве случаев, использовать динамическое построение — это как стрелять из пушки по воробьям. Если все запросы известны заранее, то их координаты можно просто сжать перед обработкой запросов. Автор обычно делает это так:

                    -
                    vector<int> compress (vector<int> a) {
                    -    vector<int> b = a;
                    -    sort(b.begin(), b.end());
                    -    b.erase(unique(b.begin(), b.end()), b.end());
                    -    for (int &x : a) 
                    -        x = int(lower_bound(b.begin(), b.end(), x) - b.begin());
                    -    return a;
                    -}
                    +

                    Персистентность

                    Структуры данных называют персистентными, если их можно быстро «откатить» до произвольного предыдущего состояния.

                    Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент \(t\) никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.

                    У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать \(O(\log n)\) новых вершин на запрос, что означает общее потребление памяти \(O(m \log n)\).

                    -
                    struct segtree {
                    -    int lb, rb;
                    -    int sum = 0;
                    -    segtree *l = 0, *r = 0;
                    -    segtree (int _lb, int _rb) {
                    -        lb = _lb, rb = _rb;
                    -        if (lb != rb) {
                    -            int t = (lb + rb) / 2;
                    -            l = new segtree(lb, t);
                    -            r = new segtree(t, rb);
                    -        }
                    -    }
                    -    void copy () {
                    -        if (l) {
                    -            l = new segtree(l);
                    -            r = new segtree(r);
                    -        }
                    -    }
                    -    void add (int k, int x) {
                    -        copy();
                    -        sum += x;
                    -        if (l) {
                    -            if (k < l->rb) l->add(k, x);
                    -            else r->add(k, x);
                    -        }
                    -    }
                    -    int get_sum (int lq, int rq) {
                    -        // этот метод ничего не меняет -- он и так хороший
                    -        if (lq <= lb && rb <= rq)
                    -            return sum;
                    -        if (max(lb, lq) >= min(rb, rq))
                    -            return 0;
                    -        return l->get_sum(lq, rq) + r->get_sum(lq, rq);
                    -    }
                    -};
                    +

                    Даны \(n\) точек на плоскости. Нужно в онлайн ответить на \(q\) запросов суммы на прямоугольнике.

                    diff --git a/ru/sparse-table.html b/ru/sparse-table.html index a6a4015..54581cd 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -1,50 +1,84 @@ - - + + - - + + Разреженная таблица - - - - + + + + @@ -64,9 +98,9 @@ - +

                    Разреженная таблица

                    Разреженная таблица (англ. sparse table) — структура данных, позволяющая отвечать на запросы минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) времени и памяти.

                    Разреженная таблица — это следующий двумерный массив размера \(n \times\log n\):

                    @@ -79,29 +113,26 @@

                    Разреженная табли t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1]) \]

                    Имея таком массив, мы можем для любого отрезка быстро посчитать минимум на нём. Заметим, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.

                    -
                    - - -
                    +

                    Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

                    -
                    int a[maxn], lg[maxn], mn[maxn][logn];
                    -
                    -int rmq (int l, int r) {
                    -    int t = lg[r-l+1];
                    -    return min(mn[l][t], mn[r-(1<<t)+1][t]);
                    -}
                    -
                    -// Это считается уже где-то в первых строчках main:
                    -
                    -for (int l = 1; l < logn; l++)
                    -    for (int i = (1<<l); i < maxn; i++)
                    -        lg[i] = l;
                    -
                    -for (int i = n-1; i >= 0; i--) {
                    -    mn[i][0] = a[i];
                    -    for (int l = 0; l < logn-1; l++)
                    -        mn[i][l+1] = min(mn[i][l], mn[i+(1<<l)][l]);
                    -}
                    +

                    2d Static RMQ

                    Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

                    В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

                    diff --git a/ru/sqrt.html b/ru/sqrt.html index 4de30c6..4ad55f8 100644 --- a/ru/sqrt.html +++ b/ru/sqrt.html @@ -1,50 +1,84 @@ - - + + - - + + Корневая эвристика - - - - + + + + @@ -64,9 +98,9 @@ - +

                    Корневая эвристика

                    Корневая эвристика — обобщённое название методов и структур данных, опирающихся

                    Центральное равенство этой статьи: \(\sqrt x = \frac{x}{\sqrt x}\).

                    @@ -74,99 +108,99 @@

                    Кор

                    Сделаем вид, что про дерево отрезков мы не знаем, и рассмотрим следующую задачу:

                    Дан массив \(a\) длины \(n\) и \(q\) запросов одного из двух типов:

                    -
                      +
                      1. Найти сумму на отрезке \([l, r]\).

                      2. Увеличить все элементы на отрезке [l, r] на \(x\).

                    Разделим весь массив на блоки по \(c \approx \sqrt{n}\) элементов и посчитаем сумму на каждом блоке. Так как блоки не пересекаются, суммарно это будет работать за \(O(n)\).

                    -
                    // c это и количество блоков, и их размер -- оно должно быть чуть больше корня
                    -const int maxn = 1e5, c = 330;
                    -int a[maxn], b[c];
                    -int add[c];
                    -
                    -for (int i = 0; i < n; i++)
                    -    b[i / c] += a[i];
                    +

                    Заведем также массив add размера \(\sqrt n\), который будем использовать для отложенной операции прибавления на блоке. Будем считать, что реальное значение \(i\)-го элемента равно a[i] + add[i / c].

                    Теперь мы можем отвечать на запросы первого типа за \(O(\sqrt n)\) на запрос:

                    -
                      +
                      1. Для всех блоков, лежащих целиком внутри запроса, просто возьмём уже посчитанные суммы и прибавим к ответу.

                      2. Для блоков, пересекающихся с запросом только частично (их два — правый и левый), проитерируемся по нужным элементам и прибавим к ответу.

                      -
                      int sum(int l, int r) {
                      -    int res = 0;
                      -    while (l <= r) {
                      -        // если мы находимся в начале блока и он целиком в запросе
                      -        if (l % c == 0 && l + c - 1 <= r) {
                      -            res += b[l / c];
                      -            l += c; // мы можем прыгнуть сразу на блок
                      -        }
                      -        else {
                      -            res += a[l] + add[l / c];
                      -            l += 1;
                      -        }
                      -    }
                      -    return res;
                      -}
                      +

                      Обновление пишется примерно так же:

                      -
                      void upd(int l, int r, int x) {
                      -    while (l <= r) {
                      -        if (l % c == 0 && l + c - 1 <= r) {
                      -            b[l / c] += c * x;
                      -            add[l / c] += x;
                      -            l += c;
                      -        }
                      -        else {
                      -            a[l] += x;
                      -            l++;
                      -        }
                      -    }
                      - }
                      +

                      Обе операции будут работать за \(O(\sqrt n)\), потому что нужных «центральных» блоков всегда не более \(\sqrt n\), а в граничных блоках суммарно не более \(2 \sqrt n\) элементов.

                      Корневая эвристика на запросах

                      Сделаем вид, что запросов обновления нет. Тогда бы мы решали это задачу просто массивом префиксных сумм.

                      -
                      int s[maxn]; // [0, r)
                      -s[0] = 0
                      -
                      -for (int i = 0; i < n; i++)
                      -    s[i+1] = s[i] + a[i];
                      +

                      Подойдём теперь к задаче с другой стороны: разобьём на корневые блоки не массив, а запросы к нему. Будем обрабатывать каждый блок запросов независимо от других с помощью массива префикных сумм, который мы будем честно пересчитывать каждые \(\sqrt q\) запросов.

                      На каждый запрос суммы мы можем потратить \(O(1)\) времени на запрос к префиксным суммам, плюс \(\sqrt q\) времени на поправку на всех запросах из буфера.

                      -
                      struct query { int l, r, x; };
                      -vector<query> buffer; // запросы, не учтенные в префиксных суммах
                      -
                      -int sum(int l, int r) {
                      -    int res = s[r+1] - s[l];
                      -    for (query q : buffer)
                      -        // пересечем запрос суммы со всеми запросами
                      -        res += q.x * max(0, min(r, q.r) - max(l, q.l));
                      -    return res;
                      -}
                      -
                      -void rebuild() {
                      -    vector<int> d(n, 0);
                      -    // массив дельт
                      -    
                      -    for (query q : buffer) {
                      -        d[q.l] += x;
                      -        d[q.r + 1] -= x;
                      -    }
                      -    buffer.clear();
                      -    
                      -    int delta = 0, running_sum = 0;
                      -    for (int i = 1; i <= n; i++) {
                      -        p[i] += running_sum;
                      -        delta += d[i];
                      -        running_sum += delta;
                      -    }
                      -}
                      -
                      -void upd(int l, int r, int x) {
                      -    buffer.push_back({l, r, x});
                      -    if (buffer.size() == c)
                      -        // буфер слишком большой; надо пересчитать префиксные суммы и очистить его
                      -        rebuild();
                      -}
                      +

                      Такое решение будет работать за \(O(n \sqrt q + q \sqrt q)\).

                      Превращение статических структур в динамические

                      Эту технику можно применять не только к массиву префиксных сумм, но и к любым статическим структурам данных.

                      diff --git a/ru/stress-test.html b/ru/stress-test.html index de90290..140dffc 100644 --- a/ru/stress-test.html +++ b/ru/stress-test.html @@ -1,49 +1,83 @@ - - + + - - + + Стресс-тестирование - - - + + + @@ -63,9 +97,9 @@ - +

                      Стресс-тестирование

                      • Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений
                      • @@ -73,32 +107,32 @@

                        Стресс-тестирован

                      Суть такая: * Есть решение smart — быстрое, но в котором есть баг, который хотим найти * Пишем решение stupid — медленное, но точно корректное * Пишем генератор gen — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются

                      Примерный код скрипта:

                      -
                      import os, sys
                      -
                      -f1, f2, gen, iters = sys.argv
                      -
                      -for i in range(int(iters)):
                      -    print('Test', i+1)
                      -    os.popen('python3 %s > test.txt' % gen)
                      -    v1 = os.popen('./%s < test.txt' % f1).read()
                      -    v2 = os.popen('./%s < test.txt' % f2).read()
                      -    if v1 != v2:
                      -        print test
                      -        print("Correct:")
                      -        print v1
                      -        print("Wrong:")
                      -        print v2
                      -        break
                      +

                      Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py.

                      Скрипт написан под Linux. Для Windows нужно убрать «./» во всех системных вызовах.

                      -

                      gen.py автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через ./gen. Пример gen-а, генерирующего случайную строку из символов "a", "b" и "c" длины от 1 до 10:

                      -
                      from random import randint, choice
                      -
                      -n = randint(1, 10)
                      -
                      -print(n)
                      -
                      -for _ in range(n):
                      -    print(choice('abc'), end='')
                      +

                      gen.py автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через ./gen. Пример gen-а, генерирующего случайную строку из символов “a”, “b” и “c” длины от 1 до 10:

                      + diff --git a/ru/strings.html b/ru/strings.html index dff915a..c7eab46 100644 --- a/ru/strings.html +++ b/ru/strings.html @@ -1,50 +1,84 @@ - - + + - - + + Базовые строковые алгоритмы - - - - + + + + @@ -64,9 +98,9 @@ - +

                      Базовые строковые алгоритмы

                      Префикс-функция

                      Рассмотрим задачу, которая возникает каждый раз, когда вы делаете ctrl+f:

                      @@ -77,38 +111,38 @@

                      Префикс-функция

                      Для решения этой задачи за линейное время придумали префикс-функцию.

                      Определение. Префикс-функцией от строки \(s\) называется массив \(p\), где \(p_i\) равно длине самого большого префикса строки \(s_0 s_1 s_2 \ldots s_i\), который также является и суффиксом этой строки (не считая всю строку).

                      Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна \([0, 1, 0, 1, 2, 3, 4, 5]\).

                      -
                      def slow_prefix_function(s):
                      -    n = len(s)
                      -    p = [0]*n
                      -    for i in range(n):
                      -        prefix = s[:i]
                      -        for l in range(1, i):
                      -            if prefix[:l] == prefix[-l:]:
                      -                p[i] = l
                      -    return p
                      -
                      -slow_prefix_function('aataataa')
                      +
                      [0, 0, 1, 0, 1, 2, 3, 4]

                      (Этот алгоритм работает за \(O(n^3)\), но это только пока.)

                      Как это поможет решить исходную задачу?

                      Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.

                      Соединим подстроки \(s\) и \(t\) каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки \(s\#t\).

                      -
                      s = "let it go"
                      -t = """let it go, let it go
                      -can't hold it back anymore
                      -let it go, let it go
                      -turn away and slam the door!"""
                      -
                      -print((s + '#' + t).replace('\n', ' '))
                      -print(''.join([str(x) for x in slow_prefix_function(s + '#' + t)]))
                      +
                      let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!
                       00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000

                      Видно, что все места, где значения равны 9 (длине S) — это концы вхождений \(s\) в текст \(t\).

                      Такой алгоритм (посчитать префикс-функцию от \(s\#t\) и посмотреть, в каких позициях она равна \(|s|\)) называется алгоритмом Кнута-Морриса-Пратта.

                      Как её быстро считать

                      Рассмотрим ещё несколько примеров префикс-функций:

                      -
                      for s in ['aaaaa', 'abcdef', 'abacabadava', 'antananarivuantananarivu']:
                      -    print(slow_prefix_function(s))
                      +
                      [0, 0, 1, 2, 3]
                       [0, 0, 0, 0, 0, 0]
                       [0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]
                      @@ -118,20 +152,20 @@ 

                      Как её быстро счи

                      Заметим, что \(p_{i+1} = p_i + 1\) в том и только том случае, когда \(s_{p_i} =s_{i+1}\). Например, в строке \(\underbrace{aabaa}t\overbrace{aabaa}\) выделен максимальный префикс, равный суффиксу: \(p_{10} = 5\). Если следующий символ равен будет равен \(t\), то \(p_{11} = p_{10} + 1 = 6\).

                      Но что происходит, когда \(s_{p_i}\neq s_{i+1}\)? Пусть следующий символ в этой же строке равен \(b\). * \(\implies\) Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. * \(\implies\) Значит, помимо того, что он является суффиксом «aabaab», префикс является префиксом и подстроки «aabaa». * \(\implies\) Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть \(p_4 = 2\). * \(\implies\) Если \(s_2 = s_{11}\) (т. е. новый символ совпадает с идущим после префикса-кандидата), то \(p_{11} = p_2 + 1 = 2 + 1 = 3\).

                      В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — \(p_{p_{p_i}}\). Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым.

                      -
                      def fast_prefix_function(s):
                      -    n = len(s)
                      -    p = [0]*n
                      -    for i in range(1, n):
                      -        cur = p[i - 1]
                      -        # перебираем префикс-функцию, пока не найдем равный символ
                      -        while s[i] != s[cur] and cur > 0:
                      -            cur = p[cur - 1]
                      -         # если нашли, то значение на единицу больше
                      -        if s[i] == s[cur]:
                      -            p[i] = cur + 1
                      -    return p
                      -
                      -fast_prefix_function('abacabadabacabax')
                      +
                      [0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]

                      Асимптотика

                      Почему это работает за \(O(N)\)? В худшем случае этот while может работать \(O(N)\) раз за одну итерацию. Но оказывается, что в среднем каждый while работает за \(O(1)\) — это называется амортизированной асимптотикой.

                      @@ -140,18 +174,18 @@

                      Z-функция

                      Альтернатива префикс-функции — z-функция (примечание: не «зи», а «зет»). Она немного проще для понимания.

                      Z-функция от строки \(s\) — это такой массив \(z\), что \(z_i\) равно длине максимальной подстроки, начинающейся с \(i\)-й позиции, которая равна префиксу \(s\).

                      \[\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)\]

                      -
                      def slow_z_function(s):
                      -    n = len(s)
                      -    z = [0]*n
                      -    for i in range(1, n):
                      -        suffix = s[i:]
                      -        for l in range(1, i):
                      -            if s[:l] == suffix[:l]:
                      -                z[i] = l
                      -    return z
                      -
                      -for s in ['abacabadaba', 'aabcaabaabca', 'antananarivuantananarivu']:
                      -    print(slow_z_function(s))
                      +
                      [0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]
                       [0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]
                       [0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]
                      @@ -160,26 +194,25 @@

                      Как её быстро сч

                      Заметим, что: * \(z_0 = 0\) — из-за договоренности (потому что информации не несет); * \(z_i = 0 \iff s_i \neq s_0\); * \(z_i > 0 \iff s_i = s_0\).

                      Будем идти слева направо и хранить z-блок — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как \(l\) и \(r\).

                      Пусть мы сейчас хотим найти \(z_i\), а все предыдущие уже нашли. Если новый, \(i\)-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. * Если правее, то мы просто наивно перебором найдем \(z_i\) (максимальный отрезок, начинающийся с \(s_i\) и равный префиксу), и объявим его новым z-блоком. * Если \(i\)-й элемент лежит внутри z-блока, то мы можем посмотреть на значение \(z_{i-l}\) и использовать его, чтобы инициализировать \(z_i\) чем-то, возможно, отличным от нуля. Если \(z_{i-l}\) «не хватает» до границы \(z\)-блока, то \(z_i = z_{i-l}\). Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку.

                      -
                      def fast_z_function(s):
                      -    n = len(s)
                      -    z = [0]*n
                      -    l = 0
                      -    r = 0
                      -    for i in range(1, n):
                      -        if i <= r:
                      -            z[i] = min(r - i + 1, z[i-l])
                      -        while i + z[i] < n and s[z[i]] == s[i+z[i]]:
                      -            z[i] += 1
                      -        if i + z[i] - 1 > r:
                      -            l = i
                      -            r = i +  z[i] - 1
                      -    return z
                      +

                      Асимптотика. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это \(O(n)\).

                      Зачем тогда люди используют префикс-функцию

                      -
                      -hz -

                      hz

                      -
                      +
                      +hz
                      hz
                      +

                      В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.

                      Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение.

                      diff --git a/ru/suffix-array.html b/ru/suffix-array.html index 3ee90f9..dc1c2ad 100644 --- a/ru/suffix-array.html +++ b/ru/suffix-array.html @@ -1,50 +1,84 @@ - - + + - - + + Суффиксный массив - - - - + + + + @@ -64,9 +98,9 @@ - +

                      Суффиксный массив

                      Суффиксный массив, автомат и дерево обобщённо называют суффиксными структурами данных. Они применяются в множестве различных задач, встречающихся как на олимпиадах, так и на практике.

                      Суффиксные структуры часто (но не всегда) взаимозаменяемые, и более того, конвертируются друг в друга за линейное время. Суффиксный массив — самый простой из них, поэтому мы с него и начнём.

                      @@ -100,53 +134,53 @@

                      Построение за Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней std::sort со своим компаратором.

                      Как быстро сравнить две подстроки? Мы можем использовать \(c_i\) — каждой строке длины \(2^k\) сопоставить биграмму (строку из двух символов), а именно строка \(s[i..i+2^k-1]\) с точки зрения сортировки будет эквивалентна паре \((c_i, c_{i+2^{k-1}})\). Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за \(O(1)\). Однако, это всё ещё будет работать за \(O(n \log^2 n)\), потому что каждый этап будет работать за \(O(n \log n\)).

                      Оптимизация до \(O(n \log n)\). Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся цифровой сортировкой — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив \(p\) с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в \(p\) отнять \(2^{k-1}\). Таким образом, можно проводить этап за \(O(n)\).

                      -
                      // строка -- это последовательность чисел от 1 до размера алфавита
                      -vector<int> suffix_array (vector<int> &s) {
                      -    s.push_back(0);  // добавляем нулевой символ в конец строки
                      -    int n = (int) s.size(),
                      -        cnt = 0,  // вспомогательная переменная: счётчик для сортировки 
                      -        cls = 0;  // количество классов эквивалентности
                      -    vector<int> c(n), p(n);
                      -    
                      -    map< int, vector<int> > t;
                      -    for (int i = 0; i < n; i++)
                      -        t[s[i]].push_back(i);
                      -    
                      -    // «нулевой» этап
                      -    for (auto &x : t) {
                      -        for (int u : x.second)
                      -            c[u] = cls, p[cnt++] = u;
                      -        cls++;
                      -    }
                      -    
                      -    // пока все суффиксы не стали уникальными
                      -    for (int l = 1; cls < n; l++) {
                      -        vector< vector<int> > a(cls);  // массив для сортировки подсчётом
                      -        vector<int> _c(n);  // новые классы эквивалентности
                      -        int d = (1<<l)/2;
                      -        int _cls = cnt = 0;  // новое количество классов
                      -        
                      -        for (int i = 0; i < n; i++) {
                      -            int k = (p[i]-d+n)%n;
                      -            a[c[k]].push_back(k);
                      -        }
                      -        
                      -        for (int i = 0; i < cls; i++) {
                      -            for (size_t j = 0; j < a[i].size(); j++) {
                      -                // если суффикс начинает новый класс эквивалентности
                      -                if (j == 0 || c[(a[i][j]+d)%n] != c[(a[i][j-1]+d)%n])
                      -                    _cls++;
                      -                _c[a[i][j]] = _cls-1;
                      -                p[cnt++] = a[i][j];
                      -            }
                      -        }
                      -        
                      -        c = _c;
                      -        cls = _cls;
                      -    }
                      -    
                      -    return vector<int>(p.begin()+1, p.end());
                      -}
                      +
                      // строка -- это последовательность чисел от 1 до размера алфавита
                      +vector<int> suffix_array (vector<int> &s) {
                      +    s.push_back(0);  // добавляем нулевой символ в конец строки
                      +    int n = (int) s.size(),
                      +        cnt = 0,  // вспомогательная переменная: счётчик для сортировки 
                      +        cls = 0;  // количество классов эквивалентности
                      +    vector<int> c(n), p(n);
                      +    
                      +    map< int, vector<int> > t;
                      +    for (int i = 0; i < n; i++)
                      +        t[s[i]].push_back(i);
                      +    
                      +    // «нулевой» этап
                      +    for (auto &x : t) {
                      +        for (int u : x.second)
                      +            c[u] = cls, p[cnt++] = u;
                      +        cls++;
                      +    }
                      +    
                      +    // пока все суффиксы не стали уникальными
                      +    for (int l = 1; cls < n; l++) {
                      +        vector< vector<int> > a(cls);  // массив для сортировки подсчётом
                      +        vector<int> _c(n);  // новые классы эквивалентности
                      +        int d = (1<<l)/2;
                      +        int _cls = cnt = 0;  // новое количество классов
                      +        
                      +        for (int i = 0; i < n; i++) {
                      +            int k = (p[i]-d+n)%n;
                      +            a[c[k]].push_back(k);
                      +        }
                      +        
                      +        for (int i = 0; i < cls; i++) {
                      +            for (size_t j = 0; j < a[i].size(); j++) {
                      +                // если суффикс начинает новый класс эквивалентности
                      +                if (j == 0 || c[(a[i][j]+d)%n] != c[(a[i][j-1]+d)%n])
                      +                    _cls++;
                      +                _c[a[i][j]] = _cls-1;
                      +                p[cnt++] = a[i][j];
                      +            }
                      +        }
                      +        
                      +        c = _c;
                      +        cls = _cls;
                      +    }
                      +    
                      +    return vector<int>(p.begin()+1, p.end());
                      +}

                      TODO: переписать это

                      Наибольшие общие префиксы

                      Для многих применений очень часто требуется искать длину общих префиксов строк. Например, просто чтобы сравнить две подстроки, мы можем найти их общий префикс и посмотреть на следующий символ — так же, как мы делали с хэшами.

                      diff --git a/ru/treap.html b/ru/treap.html index 5fa77b0..8b9ef87 100644 --- a/ru/treap.html +++ b/ru/treap.html @@ -1,50 +1,84 @@ - - + + - - + + Декартово дерево - - - - + + + + @@ -64,9 +98,9 @@ - +

                      Декартово дерево

                      Рене Декарт (фр. René Descartes) — великий французский математик и философ XVII века.

                      Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.

                      @@ -79,10 +113,9 @@

                      Декартово дерево

                Заметим, что если все \(y\) и \(x\) различны, то дерево строится однозначно.

                Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:

                -
                -treap -

                treap

                -
                +
                +treap
                treap
                +

                Таким образом, декартово дерево — это одновременно бинарное дерево по \(x\) и куча по \(y\). Поэтому ему придумали много альтернативных названий:

                • Дерамида (дерево + пирамида)

                • @@ -132,196 +165,196 @@

                  Приоритеты и

                  Реализация

                  Декартово дерево удобно писать на указателях и структурах.

                  Создадим структуру Node, в которой будем хранить ключ и приоритет, а также указатели на левого и правого сына. Указателя на корень дерева достаточно для идентификации всего дерева. Поэтому, когда мы будем говорить «функция принимает два дерева» на самом деле будут иметься в виду указатели на их корни. К нулевому указателю же мы будем относиться, как к «пустому» дереву.

                  -
                  struct Node {
                  -    int key, prior;
                  -    Node *l = 0, *r = 0;
                  -    Node (int _key) { key = _key, prior = rand(); }
                  -};
                  +

                  Объявим две вспомогательные функции, изменяющие структуру деревьев: одна будет разделять деревья, а другая объединять. Как мы увидим, через них можно легко выразить почти все функции, которые нам потом понадобятся.

                  Merge

                  Принимает два дерева (два корня, \(L\) и \(R\)), про которые известно, что в левом все вершины имеют меньший ключ, чем все в правом. Их нужно объединить в одно дерево так, чтобы ничего не сломалось: по ключам это всё ещё дерево, а по приоритетами — куча.

                  Сначала выберем, какая вершина будет корнем. Здесь всего два кандидата — левый корень \(L\) или правый \(R\) — просто возьмем тот, у кого приоритет больше.

                  Пусть, для однозначности, это был левый корень. Тогда левый сын корня итогового дерева должен быть левым сыном \(L\). С правым сыном сложнее: возможно, его нужно смерджить с \(R\). Поэтому рекурсивно сделаем merge(l->r, r) и запишем результат в качестве правого сына.

                  -
                  Node* merge (Node *l, Node *r) {
                  -    if (!l) return r;
                  -    if (!r) return l;
                  -    if (l->prior > r->prior) {
                  -        l->r = merge(l->r, r);
                  -        return l;
                  -    }
                  -    else {
                  -        r->l = merge(l, r->l);
                  -        return r;
                  -    }
                  -}
                  +

                  Split

                  Принимает дерево и ключ \(x\), по которому его нужно разделить на два: \(L\) должно иметь все ключи не больше \(x\), а \(R\) должно иметь все ключи больше \(x\).

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

                  -
                  typedef pair<Node*, Node*> Pair;
                  -
                  -Pair split (Node *p, int x) {
                  -    if (!p) return {0, 0};
                  -    if (p->key <= x) {
                  -        Pair q = split(p->r, x);
                  -        p->r = q.first;
                  -        return {p, q.second};
                  -    }
                  -    else {
                  -        Pair q = split(p->l, x);
                  -        p->l = q.second;
                  -        return {q.first, p};
                  -    }
                  -}
                  +

                  Пример: вставка

                  merge и split сами по себе не очень полезные, но помогут написать все остальное.

                  Вот так, например, будет выглядеть код, добавляющий \(x\) в сет.

                  -
                  Node *root = 0;
                  -
                  -void insert (int x) {
                  -    Pair q = split(root, x);
                  -    Node *t = new Node(x);
                  -    root = merge(q.first, merge(t, q.second));
                  -}
                  +

                  Пример: модификация для суммы на отрезке

                  Иногда нам нужно написать какие-то модификации для более продвинутых операций.

                  Например, нам может быть интересно иногда считать сумму чисел на отрезке. Для этого в вершине нужно хранить также своё число и сумму на своем «отрезке».

                  -
                  struct Node {
                  -    int val, sum;
                  -    // ...
                  -};
                  +

                  При merge и split надо будет поддерживать эту сумму актуальной.

                  Вместо того, чтобы модифицировать и merge, и split под наши хотелки, напишем вспомогательные функцию upd, которую будем вызывать при обновлении детей вершины.

                  -
                  void sum (Node* v) { return v ? v->sum : 0; }
                  -// обращаться по пустому указателю нельзя -- выдаст ошибку
                  -
                  -void upd (Node* v) { v->sum = sum(v->l) + sum(v->r) + v->val; }
                  +

                  В merge и split теперь можно просто вызывать upd перед тем, как вернуть вершину, и тогда ничего не сломается:

                  -
                  Node* merge (Node *l, Node *r) {
                  -    // ...
                  -    if (...) {
                  -        l->r = merge(l->r, r);
                  -        upd(l);
                  -        return l;
                  -    }
                  -    else {
                  -        // ...
                  -    }
                  -}
                  -
                  typedef pair<Node*, Node*> Pair;
                  -
                  -Pair split (Node *p, int x) {
                  -    // ...
                  -    if (...) {
                  -        // ...
                  -        upd(p);
                  -        return {p, q.second};
                  -    }
                  -    else {
                  -        // ...
                  -    }
                  -}
                  + +

                  Тогда при запросе суммы нужно просто вырезать нужный отрезок и запросить эту сумму:

                  -
                  int sum (int l, int r) {
                  -    Pair rq = split(root, r);
                  -    Pair lq = split(rq.first, l);
                  -    int res = sum(lr.second);
                  -    root = merge(lq.first, merge(lq.second, rq.second));
                  -    return res;
                  -}
                  +

                  Неявный ключ

                  Обычное декартово дерево — это структура для множеств, каждый элемент которых имеет какой-то ключ. Эти ключи задают на этом множестве какой-то порядок, и все запросы к ДД обычно как-то привязаны к этому порядку.

                  Но что, если у нас есть запросы, которые этот порядок как-то нетривиально меняют? Например, если у нас есть массив, в котором нужно уметь выводить сумму на произвольном отрезке и «переворачивать» произвольный отрезок. Если бы не было второй операции, мы бы просто использовали индекс элемента в качестве ключа, но с операцией переворота нет способа их быстро поддерживать актуальными.

                  Решение такое: выкинем ключи, а вместо них будем поддерживать информацию, которая поможет неявно восстановить ключ, когда он нам будет нужен. А именно, будем хранить вместе с каждой вершиной размер её поддерева:

                  -
                  struct Node {
                  -    int key, prior, size = 1;
                  -    //              ^ размер поддерева
                  -    Node *l = 0, *r = 0;
                  -    Node (int _key) { key = _key, prior = rand(); }
                  -};
                  +

                  Размеры поддеревьев будем поддерживать по аналогии с суммой — напишем вспомогательную функцию, которую будем вызывать после каждого структурного изменения вершины.

                  -
                  int size (Node *v) { return v ? v->size : 0; }
                  -
                  -void upd (Node *v) { v->size = 1 + size(v->l) + size(v->r); }
                  +

                  merge не меняется, а вот в split нужно использовать позицию корня вместо его ключа.

                  -

                  Про split теперь удобнее думать как "вырежи первые k элементов".

                  -
                  typedef pair<Node*, Node*> Pair;
                  -
                  -Pair split (Node *p, int k) {
                  -    if (!p) return {0, 0};
                  -    if (size(p->l) + 1 <= k) {
                  -        Pair q = split(p->r, k - size(p->l) - 1);
                  -        //                   ^ правый сын не знает количество вершин слева от него
                  -        p->r = q.first;
                  -        upd(p);
                  -        return {p, q.second};
                  -    }
                  -    else {
                  -        Pair q = split(p->l, k);
                  -        p->l = q.second;
                  -        upd(p);
                  -        return {q.first, p};
                  -    }
                  -}
                  +

                  Про split теперь удобнее думать как “вырежи первые k элементов”.

                  +

                  Всё. Теперь у нас есть клёвая гибкая структура, которую можно резать как угодно.

                  Пример: ctrl+x, ctrl+v

                  -
                  Node* ctrlx (int l, int r) {
                  -    Pair q1 = split(root, r);
                  -    Pair q2 = split(q1.first, l);
                  -    root = merge(q2.first, q1.second);
                  -    return q2.second;
                  -}
                  -
                  void ctrlv (Node *v, int k) {
                  -    Pair q = split(root, k);
                  -    root = merge(q.first, merge(v, q.second));
                  -}
                  + +

                  Пример: переворот

                  Нужно за \(O(\log n)\) обрабатывать запросы переворота произвольных подстрок: значение \(a_l\) поменять с \(a_r\), \(a_{l+1}\) поменять с \(a_{r-1}\) и т. д.

                  Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:

                  -
                  struct Node {
                  -    bool rev;
                  -    // ...
                  -};
                  +

                  Поступим по аналогии с ДО — когда мы когда-либо встретим такую вершину, мы поменяем ссылки на её детей, а им самим передадим эту метку:

                  -
                  void push (node *v) {
                  -    if (v->rev) {
                  -        swap(v->l, v->r);
                  -        if (v->l)
                  -            v->rev ^= 1;
                  -        if (v->r)
                  -            v->rev ^= 1;
                  -    }
                  -    v->rev = 0;
                  -}
                  +

                  Аналогично, эту функцию будем вызывать в начале merge и split.

                  Саму функцию reverse реализуем так: вырезать нужный отрезок, поменять флаг.

                  -
                  void reverse (int l, int r) {
                  -    Pair q1 = split(root, r);
                  -    Pair q2 = split(q1.first, l)
                  -    q2.second->rev ^= 1;
                  -    root = merge(q2.first, merge(q2.second, q1.second));
                  -}
                  +

                  Небольшой рефакторинг

                  Реализация большинства операций всегда примерно одинаковая — вырезаем отрезок с \(l\) по \(r\), что-то с ним делаем и склеиваем обратно.

                  Дублирующийся код — это плохо. Давайте используем всю мощь плюсов и определим функцию, которая принимает другую функцию, которая уже делает полезные вещи на нужном отрезке.

                  -
                  auto apply (int l, int r, auto f) {
                  -    Pair q1 = split(root, r);
                  -    Pair q2 = split(q1.first, l)
                  -    q2.second = f(q2.second);
                  -    root = merge(q2.first, merge(q2.second, q1.second));
                  -}
                  -
                  -void reverse (Node *v) {
                  -    if (v)
                  -        v->rev ^= 1;
                  -}
                  +

                  Применять её нужно так:

                  -
                  apply(l, r, reverse);
                  +

                  Это работает в плюсах, начиная с g++14.

                  Для простых операций можно даже написать лямбду:

                  -
                  apply(l, r, [](Node *v){
                  -    if (v)
                  -        v->rev ^= 1;
                  -});
                  +

                  Персистентность

                  Так же, как и с ДО, персистентной версией ДД можно решать очень интересные задачи.

                  @@ -334,12 +367,12 @@

                  Персистентность

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

                  Чтобы полноценно сравнивать стоки лексикографически, можно применить бинарный поиск: перебрать длину совпадающего суффикса, и, когда она найдется, посмотреть на следующий символ.

                  Реализация почти такая же, как и для всех персистентных структур на ссылках — перед тем, как идти в какую-то вершину, нужно создать её копию и идти в неё. Создадим для этого вспомогательную функцию copy:

                  -
                  Node* copy (Node *v) { return new Node(*v); }
                  +

                  Во всех методах мы будем начинать с копирования всех упоминаемых в ней вершин. Например, персистентный split начнётся так:

                  -
                  Pair split (Node *p, int x) {
                  -    p = copy(p);
                  -    // ...
                  -}
                  +

                  В ДО просто создавать копии вершин было достаточно. Этого обычно достаточно для всех детерминированных структур данных, но в ДД всё сложнее. Оказывается существует тест, который «валит» приоритеты: можно раскопировать много версий одной вершины, а все остальные — удалить. Тогда у всех вершин будет один и тот же приоритет, и дерево превратится в «бамбук», в котором все операции будут работать за линию.

                  У этой проблемы есть очень элегантное решение — избавиться от приоритетов, и делать теперь следующее переподвешивание: если размер левого дерева равен \(L\), а размер правого \(R\), то будем подвешивать за левое с вероятностью \(\frac{L}{L+R}\), иначе за правое.

                  Теорема. Такое переподвешивание эквивалентно приоритетам.

                  @@ -349,17 +382,17 @@

                  Персистентность

                • Переход индукции — операция merge. Любая вершина левого дерева была корнем с вероятностью \(\frac{1}{L}\) (по предположению индукции), а после слияния она будет корнем всего дерева с вероятностью \(\frac{1}{L} \cdot \frac{L}{L+R} = \frac{1}{L+R}\). С вершинами правого дерева аналогично.

                Получается, что при таком переподвешивании всё так же каждая вершина любого поддерева равновероятно могла быть его корнем, а на этом основывалось наше доказательство асимптотики ДД.

                -
                Node* merge (Node *l, Node *r) {
                -    if (!l) return r;
                -    if (!r) return l;
                -    l = copy(l), r = copy(r);
                -    if (rand() % (size(l) + size(r)) < size(l)) {
                -        // ...
                -    }
                -    else {
                -        // ...
                -    }
                -}
                +

                Философский вопрос: можно ли декартово дерево называть декартовым, если из него удалить и \(x\), и \(y\)?

                diff --git a/ru/trie.html b/ru/trie.html index ec2b68b..ac27ccd 100644 --- a/ru/trie.html +++ b/ru/trie.html @@ -1,50 +1,84 @@ - - + + - - + + Бор - - - - + + + + @@ -64,41 +98,40 @@ - +

                Бор

                Бор — это структура данных для компактного хранения строк.

                Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин.

                -
                -trie -

                trie

                -
                +
                +trie
                trie
                +

                Бор можно удобно использовать для разных задач: * Хранение строк — занимает гораздо меньше места, чем массив или сет строк. * Сортировка строк — по бору можно пройтись dfs-ом и вывести все строки в лексикографическом порядке.

                • Сет строк — как мы увидим, в нем легко добавлять и удалять слова, а также проверять, если ли слово в бореи.

                Реализация

                Бор состоит из ссылающихся друг на друга вершин. В вершине обычно хранится такая информация: * терминальная ли вершина, * ссылки на детей, * возможно, какая-нибудь дополнительная зависящая от задачи информация о слове, если вершина терминальная. Например, количество таких слов — так можно реализовать мультисет.

                -
                const int k = 26;
                -
                -struct Vertex {
                -    Vertex* to[k] = {0};
                -    bool terminal = 0;
                -};
                -
                -Vertex *root = new Vertex();
                +

                Чтобы добавить слово в бор, нужно пройти от корня по символам слова. Если перехода по для очередного символа нет — создать его, иначе пройти по уже существующему. Последнюю вершину нужно пометить терминальной.

                -
                void add_string (string &s) {
                -    v = root;
                -    for (char c : s) {
                -        c -= 'a';
                -        if (!v->to[c]) 
                -            v->to[c] = new Vertex();
                -        v = v->to[c];
                -    }
                -    v->terminal = true;
                -}
                +

                Чтобы проверить, есть ли слово в боре, нужно пройти от корня по символам слова. Если в конце оказались в терминальной вершине — то есть. Если оказались в нетерминальной или когда-нибудь потребовалось пройтись по несуществущей ссылке — то есть.

                Удалить слово можно лениво, просто дойдя до него и убрав флаг терминальности.

                Как хранить ссылки

                From 298c6a6be05842cf6085df2eddbddd3ace04ed16 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Mon, 10 Jun 2019 20:55:25 +0000 Subject: [PATCH 056/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- img/chernobyl.jpg | Bin 0 -> 87738 bytes ru/fft.html | 531 +++++++++++++++++++++++++++++++++++++++ ru/palindromes.html | 124 +++++++++ ru/suffix_automaton.html | 116 +++++++++ ru/trie.html | 2 +- 5 files changed, 772 insertions(+), 1 deletion(-) create mode 100644 img/chernobyl.jpg create mode 100644 ru/fft.html create mode 100644 ru/palindromes.html create mode 100644 ru/suffix_automaton.html diff --git a/img/chernobyl.jpg b/img/chernobyl.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f4f66df5659a1516f72f4e30b31680bb2e78cf1 GIT binary patch literal 87738 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°F!t#dGrvS2 z5gH8^{ZhA0i=$XY|EE<5*hiNSUdehUKSJukh3mgciL`1pdgqKgfq*O|hL%x|i9!Y!%Q#FDf-wb(_^u zm(jl|;h(q@!sV*Wrml#~>=d711@VQC_jq49d+#6yr{m79AMxZ;mXEv@7XRTra;8Bx zF_Ft(9VX;$ZqohG+IXxJY(XkWQx@7A0Ts$qb_CIC%i$5yFrd%(6=wX2&S@%~Q2{D( z$7EB1d(TAGH)=CyEaBwoHM`04yhyrj?CVO|zH~L{?)ud|M(as}%e7Ufea0l|DI5m} z(L_!T_sg8~Gnh>pUefU}V&ANk@MXksDm(%2@{!bjAo7tvh_J`-TD#&4MA%5^a2iv& zpjS17*08d|v|+Ia9~&oRLPxPXLCYhS6G1r(AD1%7^D|dZ4h{;!Rt<;_hzDNaFcZr? zFqNM|@2cnBk8PJn27SZC7yr3q6HlM`({@co9S`7R�ySo_s`MS*J*~58O>>aaDpd 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 U_v$G{HY=d%ii0R*Z{ + + + + + + БПФ и многочлены + + + + + + + + + + + + + +
                +

                БПФ и многочлены

                +
                +

                БПФ и многочлены

                +

                Рассмотрим такую распространённую операцию как умножение двух целых чисел. Квадратичный алгоритм — умножения в столбик — все знают со школы. Долгое время предполагалось, что ничего быстрее придумать нельзя.

                +
                +

                \[ +T(n)=3T\left(\dfrac n 2\right)+O(n)=O\left(n^{\log_2 3}\right)\approx O(n^{1.58}) +\]

                +

                Чтобы перейти к алгоритму с лучшей оценкой, нам нужно сначала установить несколько фактов о многочленах.

                +

                Умножение многочленов

                +

                Обратим внимание на то, что любое число можно представить многочленом:

                +

                \[ +\begin{aligned} +A(x) &= a_0 + a_1\cdot x + a_2 \cdot x^2 + \dots + a_n \cdot x^n +\\ &= a_0 + a_1\cdot 2 + a_2 \cdot 2^2 + \dots + a_n \cdot 2^n +\end{aligned} +\]

                +

                Основание x при этом может быть выбрано произвольно.

                +

                Чтобы перемножить два числа, мы можем перемножить соответствующие им многочлены, а затем произвести каррирование: пройтись от нижних разрядов получившегося многочлена и «сдвинуть» переполнившиеся разряды:

                + +

                Прямая формула для произведения многочленов имеет вид

                +

                \[ +\left(\sum_{i=0}^n a_i x^i\right)\cdot\left(\sum_{j=0}^m b_j x^j\right)=\sum_{k=0}^{n+m}x^k\sum_{i+j=k}a_i b_j +\]

                +

                Её подсчёт требует \(O(n^2)\) операций, что нас не устраивает. Подойдём к этой задаче с другой стороны.

                +

                Интерполяция

                +

                Теорема. Пусть есть набор различных точек \(x_0, x_1, \dots, x_{n}\). Многочлен степени \(n\) однозначно задаётся своими значениями в этих точках. (Коэффициентов у этого многочлена столько же, сколько и точек — прим. К. О.)

                +

                Доказательство будет конструктивным — можно явным образом задать многочлен, который принимает заданные значения \(y_0, y_1, \ldots, y_n\) в этих точках:

                +

                \[ +y(x)=\sum\limits_{i=0}^{n}y_i\prod\limits_{j\neq i}\dfrac{x-x_j}{x_i-x_j} +\]

                +

                Корректность. Проверим, что в точке \(x_i\) значение действительно будет равно \(y\):

                +
                  +
                1. Для \(i\)-го слагаемого внутреннее произведение будет равно единице, если вместо \(x\) подставить \(x_i\): в этом случае просто перемножается \((n-1)\) единица. Эта единица помножится на \(y_i\) и войдёт в сумму.

                2. +
                3. Для всех остальных слагаемых произведение занулится: один из множетелей будет равен \((x_i - x_i)\).

                4. +
                +

                Уникальность. Предположим, есть два подходящих многочлена степени \(n\)\(A(x)\) и \(B(x)\). Рассмотрим их разность. В точках \(x_i\) значение получившегося многочлена \(A(x) - B(x)\) будет равняться нулю. Если так, то точки \(x_i\) должны являться его корнями, и тогда разность можно записать так:

                +

                \[ +A(x) - B(x) = \alpha \prod_{i=0}^n (x-x_i) +\]

                +

                для какого-то числа \(\alpha\). Тут мы получаем противоречие: если раскрыть это произведение, то получится многочлен степени \(n+1\), который нельзя получить разностью двух многочленов степени \(n\).

                +

                Этот многочлен называется интерполяционным многочленом Лагранжа, а сама задача проведения многочлена через точки — интерполяцией.

                +

                +

                Примечание. На практике интерполяцию решают методом Гаусса: её можно свести к решению линейного уравнения \(aX = y\), где \(X\) это матрица следующего вида:

                +

                \[ +\begin{pmatrix} + 1 & x_0 & x_0^2 & \ldots & x_0^n \\ + 1 & x_1 & x_1^2 & \ldots & x_1^n \\ + \vdots & \vdots & \vdots & \ddots & \vdots \\ + 1 & x_n & x_n^2 & \ldots & x_n^n \\ +\end{pmatrix} +\]

                +

                Важный факт: многочлен можно однозначно задать не только своими коэффициентами, но также корнями и значениями хотя бы в \((n+1)\)-ой точке.

                +

                Умножение через интерполяцию

                +

                Что происходит со значениями многочлена-произведения \(A(x) B(x)\) в конкретной точке \(x_i\)? Оно просто становится равным \(A(x_i) B(x_i)\).

                +

                Основная идея алгоритма: если мы знаем значения в каких-то различных \(n + m\) точках для обоих многочленов \(A\) и \(B\), то, попарно перемножив их, мы за \(O(n + m)\) операций можем получить значения в тех же точках для многочлена \(A(x) B(x)\) — а с их помощью можно интерполяцией получить исходный многочлен и решить задачу.

                + +

                Если притвориться, что evaluate и interpolate работают за линейное время, то умножение тоже будет работать за линейное время.

                +

                К сожалению, непосредственное вычисление значений требует \(O(n^2)\) операций, а интерполяция — как методом Гаусса, так и через символьное вычисление многочлена Лагранжа — и того больше, \(O(n^3)\).

                +

                Но что, если бы мы могли вычислять значения в точках и делать интерполяцию быстрее?

                +

                Ликбез: комплексные числа

                +

                Определение. Комплексные числа — это числа вида \(a + bi\), где \(a\) и \(b\) это обычные вещественные числа, а \(i\) это так называемая мнимая единица: это число, для которого выполняется равенство \(i^2 = -1\).

                +

                Комплексные числа ввели в алгебре, чтобы работать с корнями из отрицательных чисел: \(i\) в каком-то смысле равно \(\sqrt{-1}\). Так же, как и отрицательные числа, они как бы «не существуют» в реальном мире, а только в сознании математиков.

                +

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

                +

                Комплексная плоскость

                +

                Комплексные числа удобно изображать на плоскости в виде вектора \((a, b)\) и считать через них всякую геометрию.

                +

                +

                Модулем комплексного числа называется действительное число \(r = \sqrt{a^2 + b^2}\) . Геометрически, это длина вектора \((a, b)\).

                +

                Аргументом комплексного числа называется действительное число \(\phi \in (-\pi, \pi]\), для которого выполнено \(\tg \phi = \frac{b}{a}\). Геометрически, это значение угла между \((a, 0)\) и \((a, b)\). Для нуля — вектора \((0, 0)\) — аргумент не определён.

                +

                Таким образом комплексное число можно представить в полярных координатах:

                +

                \[ +a+bi = r ( \cos \phi + i \sin \phi ) +\]

                +

                Подобное представление удобно по следующей причине: чтобы перемножить два комплексных числа, нужно перемножить их модули и сложить аргументы.

                +

                Упражнение. Докажите это.

                +

                Формула эйлера

                +

                Определим число Эйлера \(e\) как число со следующим свойством:

                +

                \[ +e^{i\phi} = \cos \phi + i \sin \phi +\]

                +

                Просто введём такую нотацию для выражения \(\cos \phi + i \sin \phi\). Не надо думать, почему это так.

                +

                Геометрически, все такие точки живут на единичном круге:

                +

                +

                Такая нотация удобна, потому что можно обращаться с \(e^{i\phi}\) как с обычной экспонентой. Пусть мы, например, хотим перемножить два числа на единичном круге с аргументами \(a\) и \(b\). Тогда это можно записать так:

                +

                \[ +(\cos a + i \sin a) \cdot (\cos b + i \sin b) = e^{i (a+b)} +\]

                +

                Упражнение. Проверьте это: раскройте скобки и проделайте немного алгебры.

                +

                Корни из единицы

                +

                У комплексных чисел есть много других замечательных свойств, но нам для алгоритма на самом деле потребуется только следующее:

                +

                Утверждение. Для любого натурального \(n\) есть ровно \(n\) комплексных «корней из единицы», то есть чисел \(w_k\), для которых выполнено:

                +

                \[ +w_k^n = 1 +\]

                +

                А именно, это будут числа вида:

                +

                \[ +w_k = e^{i \tau \frac{k}{n}} +\]

                +

                где \(\tau\) обозначает \(2 \pi\), «целый круг». Это довольно новая нотация.

                +

                На комплексной плоскости эти числа располагаются на единичном круге на равном расстоянии друг от друга:

                +
                +Все 9 комплексных корней степени 9 из единицы
                Все 9 комплексных корней степени 9 из единицы
                +
                +

                Первый корень \(w_1\) (точнее второй — единицу считаем нулевым корнем) называют образующим корнем степени \(n\) из единицы. Возведение его в нулевую, первую, вторую и так далее степени порождает последовательность нужных корней единицы, при этом на \(n\)-ном элементе последовательность зацикливается:

                +

                \[ +w_n = e^{i \tau \frac{n}{n}} = e^{i \tau} = e^{i \cdot 0} = w_0 = 1 +\]

                +

                Упражнение. Докажите, что других корней быть не может.

                +

                Дискретное преобразование Фурье

                +

                Дискретным преобразованием Фурье называется вычисление значений многочлена в комплексных корнях из единицы:

                +

                \[ +X_j = \sum_{k=0}^{n-1} x_n e^{i\tau \frac{kj}{n}} +\]

                +

                Пусть в поле, в котором мы работаем, есть элемент \(w\) такой что

                +

                Будем называть его образующим корнем степени \(n\) из единицы. Такой элемент обладает очень полезным свойством, на которое мы будем опираться в дальнейшем. Во-первых, все \(w^i\) различны для \(i\) от \(0\) до \(k-1\), во-вторых \(w^m=w^{m \bmod n}\). Значит, степени \(w\) образуют группу остатков целых чисел от деления на \(n\). Вычисление значений многочлена в таких точках и называется дискретным преобразованием Фурье.

                +

                Чаще всего, используют такие корни из поля комплексных чисел. Исходя из формулы Эйлера

                +

                \[e^{i\varphi}=\cos\varphi+i\sin\varphi\]

                +

                можно заключить, что все они имеют вид \(w^k=e^{i\tfrac{2\pi}{n}k}\). Кроме этого при умножении многочленов с целыми коэффициентами можно использовать корни из единицы в полях остатков по простым модулям, что будет рассмотрено позже.

                +

                Схема Кули-Тьюки

                +

                Представим многочлен в виде \(P(x)=A(x^2)+xB(x^2)\), где \(A(x)\) состоит из коэффициентов при чётных степенях \(x\), а \(B(x)\) – из коэффициентов при нечётных. Пусть \(n=2k\). Тогда

                +

                \[w^{2t}=w^{2t \bmod 2k}=w^{2(t \bmod k)}\]

                +

                Кроме того, что нетрудно проверить, \(w^2\) является образующим корнем степени \(k\) из единицы. Значит,

                +

                \[P(w^t)=A\left(w^{2(t\bmod k)}\right)+w^tB\left(w^{2(t\bmod k)}\right)\]

                +

                Данная формула за \(O(n)\) сводит дискретное преобразование размера \(n\) к двум дискретным преобразованиям размера \(\dfrac n 2\), следовательно, общее время вычислений с использованием данной формулы составит

                +

                \[T(n)=2T\left(\dfrac n 2\right)+O(n)=O(n\log n)\]

                +

                Заметим, что в данной формуле существенную роль играло предположение о делимости \(n\) на \(2\). Значит, \(n\) должно быть чётным на каждом уровне, кроме последнего, из чего необходимо следует, что \(n\) – степень двойки. Приведём код, производящий требуемые вычисления.

                + +

                Обратное преобразование

                +

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

                +

                Но есть ещё более простой способ. При вычислении мы фактически применяем матрицу к вектору:

                +

                \[ +\begin{pmatrix} +w^0 & w^0 & w^0 & w^0 & \dots & w^0 \\ +w^0 & w^1 & w^2 & w^3 & \dots & w^{-1} \\ +w^0 & w^2 & w^4 & w^6 & \dots & w^{-2} \\ +w^0 & w^3 & w^6 & w^9 & \dots & w^{-3} \\ +\vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ +w^0 & w^{-1} & w^{-2} & w^{-3} & \dots & w^1 +\end{pmatrix}\begin{pmatrix} a_0 \\ a_1 \\ a_2 \\ a_3 \\ \vdots \\ a_{n-1} +\end{pmatrix} = \begin{pmatrix} y_0 \\ y_1 \\ y_2 \\ y_3 \\ \vdots \\ y_{n-1} +\end{pmatrix} +\]

                +

                Рассмотрим сумму \(\sum\limits_{k=0}^{n-1}(w^i w^j)^k=\sum\limits_{k=0}^{n-1} w^{(i+j)k}\). Любое число вида \(w^i\) удовлетворяет

                +

                \[ +w^n=1 \implies 1-w^n=(1-w)(1+w+w^2+\dots+w^{n-1})=0 +\]

                +

                Значит,

                +

                \[ +\sum\limits_{i=0}^{n-1}(w^{i})^k=\let\@ifnextchar\new@ifnextchar + \left\lbrace\def\arraystretch{1.2}% + \array{@{}l*{2}{@{\quad}l}@{}} +n,& i=0 \\0,&i\neq 0 +\endarray\right.\kern-\nulldelimiterspace +\]

                +

                Поэтому означенная сумма равна \(n\) если \(i+j=0\) или \(0\) в противном случае. Отсюда следует, что

                +

                \[ +\dfrac 1 n\begin{pmatrix} +w^0 & w^0 & w^0 & w^0 & \dots & w^0 \\ +w^0 & w^{-1} & w^{-2} & w^{-3} & \dots & w^{1} \\ +w^0 & w^{-2} & w^{-4} & w^{-6} & \dots & w^{2} \\ +w^0 & w^{-3} & w^{-6} & w^{-9} & \dots & w^{3} \\ +\vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ +w^0 & w^{1} & w^{2} & w^{3} & \dots & w^{-1} +\end{pmatrix} +\]

                +

                Это обратная матрицей к той, на которую мы умножаем при прямом преобразовании. Значит, при обратном преобразовании мы должны посчитать преобразование Фурье от \(w^{-1}\) и разделить на \(n\).

                + +

                Пройденный к этому моменту путь позволяет перемножить два числа за \(O(n \log n)\).

                +

                Примечание. Приведённый выше код, являясь корректным и имея асимптотику \(O(n\log n)\), едва ли пригоден для использования на реальных контестах. Он имеет большую константу и далеко не так численно устойчивый, чем оптимальные варианты написания быстрого преобразования Фурье. Мы его приводим, потому что он относительно простой.

                +

                Читателю рекомендуется самостоятельно задуматься о том, как можно улучшить время работы и точность вычислений. Из наиболее важных недостатков:

                +
                  +
                • внутри преобразования не должно происходить выделений памяти

                • +
                • работать желательно с указателями, а не векторами

                • +
                • корни из единицы должны быть посчитаны наперёд

                • +
                • Следует избавиться от операций взятия остатка по модулю

                • +
                • Вместо вычисления преобразования с \(w^{-1}\) можно вычислить преобразование с \(w\), а затем развернуть элементы массива со второго по последний.

                • +
                +

                Здесь приведена одна из условно пригодных реализаций.

                +

                Применения и вариации преобразования

                +

                Свёртки и корреляции

                +

                Пусть есть \(\{a_i\}_{i=0}^{n}\) и \(\{b_j\}_{j=0}^{m}\). Тогда свёрткой называют \(\{c_k\}_{k=0}^{m+n}\):

                +

                \[c_k=\sum\limits_{i=0}^{n} a_i b_{k-i}\]

                +

                Как мы видим, это просто \(k\)-ый коэффициент из произведения. Корреляцией же называют \(\{d_k\}_{-n}^{m}\):

                +

                \[d_k=\sum\limits_{i=0}^{n} a_i b_{k+i}\]

                +

                В обоих случаях мы предполагаем, что вне допустимых индексов последовательности равны нулям. Корреляцию можно интерпретировать двумя способами. С одной стороны, это коэффициент в произведении \(A(x) \cdot B(x^{-1})\), т.е. сдвинутая свёртка первой последовательности и развёрнутой второй. С другой стороны, \(d_k\) – в точности скалярное произведение последовательности \(a_i\) и отрезка последовательности \(b_j\), начинающегося в позиции \(k\). Именно свёртка и корреляция являются теми величинами, которые чаще всего нужно считать в задачах на преобразование Фурье.

                +

                Преобразование в кольце по модулю

                +

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

                +

                Значит, для любого простого \(p\) в поле остатков от деления на него есть корень \(g\) степени \(p-1\) из единицы. Если при этом \((p-1)=c\cdot 2^k\), то \(g^{c}\) будет корнем степени \(2^k\), что позволяет применять метод Кули-Тьюки. Отсюда следует, что \(p=c\cdot2^k+1\). Практика показывает, что чисел такого вида очень много.

                +

                Chirp Z-transform

                +

                Пусть нам дано некоторое число \(z\) и мы хотим вычислить значение многочлена в числах вида \(\{z^i\}_{i=0}^{n-1}\), т.е. множество чисел \(y_k=\sum\limits_{i=0}^{n-1} a_i z^{ik}\). Для этого сделаем замену \(ik=\dfrac{i^2+k^2-(i-k)^2}{2}\), после которой получим, что нам нужно вычислить

                +

                \[y_k=z^{\tfrac{k^2}{2}}\sum\limits_{i=0}^{n-1}\left(a_i z^{\tfrac{i^2}{2}}\right)z^{-\tfrac{(i-k)^2}{2}}\]

                +

                Что с точностью до множителя \(z^{\tfrac{k^2}{2}}\) является свёрткой двух последовательностей

                +

                \[u_i=a_i z^{\tfrac{i^2}{2}},~v_i=z^{-\tfrac{i^2}{2}}\]

                +

                Которая считается через произведение многочленов с такими коэффициентами. Но следует учесть, что здесь \(v_i\) определена также для отрицательных номеров. Данный метод среди прочего позволяет за \(O(n \log n)\) посчитать преобразование Фурье произвольной длины.

                +

                Одновременное преобразование вещественных многочленов

                +

                Пусть есть два многочлена

                +

                \[A(x)=\sum\limits_{i=0}^{n-1} a_i x^i,~B(x)=\sum\limits_{i=0}^{n-1} b_i x^i\]

                +

                С вещественными коэффициентами. Рассмотрим \(P(x)=A(x)+iB(x)\) и сопряжённый к нему.

                +

                \[\overline{P(w^k)}=A(\overline{w^k})-iB(\overline{w^k})=A(w^{n-k})-iB(w^{n-k})\]

                +

                Отсюда следует выражение для преобразования Фурье \(A(x)\) и \(B(x)\):

                +

                \[\begin{dcases} +A(w^k)=\dfrac{P(w^k)+P(w^{n-k})}{2},\\ +B(w^k)=\dfrac{P(w^k)-P(w^{n-k})}{2i} +\end{dcases}\]

                +

                Одновременное преобразование можно произвести и в обратную сторону, рассматривая последовательность \(P(w^k)=A(w^k)+iB(w^k)\). После обратного преобразования мы получим \(P(x)=A(x)+iB(x)\).

                +

                Умножение по произвольному модулю

                +

                Нам нужно перемножить два многочлена, а затем вывести коэффициенты результата по модулю \(M\), не являющимся подходящим для быстрого преобразования Фурье. При этом достаточно большому, чтобы обычному преобразованию не хватало точности. Для разрешения данной ситуации представим многочлены в виде \[A(x)=A_1(x)+A_2(x)\cdot 2^k\]\[B(x)=B_1(x)+B_2(x) \cdot 2^k\]

                +

                где \(2^k \approx \sqrt M\). Тогда все коэффициенты будет \(O(\sqrt M)\), а произведение разложится как

                +

                \[A \cdot B = A_1 B_1 + (A_1 B_2+A_2 B_1) \cdot 2^k + A_2 B_2 \cdot 2^{2k}\]

                +

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

                +

                Многомерное преобразование Фурье

                +

                Ранее мы работали с многочленами от одной переменной. Но аналогичные конструкции работают для многочлена от нескольких переменных. Считать значения многочлена теперь нужно в точках \((w_1^{k_1},w_2^{k_2}, \dots, w_m^{k_m})\). Оказывается, для такого преобразования достаточно поочерёдно сделать одномерное преобразование Фурье вдоль каждой координаты. В двумерном случае, например, нужно сначала сделать одномерное преобразование каждой строки, а затем каждого столбца.

                +

                Докажем это для двумерного случая. Мы хотим получить набор чисел

                +

                \[P_{uv}=P(w_1^u,w_2^v)=\sum_{i=0}^{n-1}\sum_{j=0}^{m-1} a_{ij} w_1^uw_2^v\]

                +

                Изначально мы имеем таблицу \(A_{uv}=a_{uv}\), после преобразования строк, мы получим

                +

                \[A'_{uv}=P_u(w_2^v)=\sum_{j=0}^{m-1}A_{uj}w_2^v=\sum_{j=0}^{m-1}a_{uj}w_2^v\]

                +

                После последующего преобразования столбцов же мы получим

                +

                \[A''_{uv}=P'_v(w_1^u)=\sum_{i=0}^{n-1} A'_{iv}w_1^u=\sum_{i=0}^{n-1}P_i(w_2^v) w_1^u=\sum_{i=0}^{n-1}\sum_{j=0}^{m-1} w_1^u w_2^v\]

                +

                Такое преобразование позволяет быстро вычислять двумерные свёртки \(C(x,y)=A(x,y)\cdot B(x,y)\) вида

                +

                \[c_{uv}=\mathop{\sum\sum}_{\substack{i_1+j_1=u\\i_2+j_2=v}} a_{i_1 i_2} b_{j_1 j_2}\]

                +

                Преобразование Уолша-Адамара и другие свёртки

                +

                Вычисляя значения многомерного многочлена в некоторых особых точках, мы можем научиться считать свёртки с другими условиями суммирования:

                +

                \[c_k=\sum\limits_{i | j=k} a_i b_j,~~~c_k=\sum\limits_{i \oplus j=k} a_i b_j,~~~c_k=\sum\limits_{i \& j=k} a_i b_j\]

                +

                Здесь \(|\), \(\&\) и \(\oplus\) соответствуют операциям побитового \(or\), \(and\) и \(xor\) соответственно.

                +
                  +
                1. \(xor\). Рассмотрим значения многочлена в точках гиперкуба

                  +

                  \(x \in \{-1,1\}^k\). Для таких точек верно соотношение

                  +

                  \(x_i^a x_i^b=x_i^{a~xor~b}\), поэтому произведения значений

                  +

                  многочленов в этих точках будут равны значениям многочлена, в

                  +

                  котором мономы умножаются с учётом данного условия.

                  +

                  Иначе говоря, если рассматривать степень \(x_i\) в мономе, как \(i\)-ый

                  +

                  бит номера данного коэффициента, мы можем считать, что при

                  +

                  произведении двух мономов мы получаем моном, чьему номеру

                  +

                  соответствует \(xor\) номеров исходных мономов.

                  +

                  Заметим, что такое вычисление есть ни что иное как вычисление

                  +

                  многомерного преобразования Фурье в корнях степени \(2\) из единицы.

                  +

                  Оно также называется преобразованием Уолша-Адамара. Здесь есть

                  +

                  некоторое упрощение по сравнению с обычным преобразованием Фурье:

                  +

                  во-первых, все вычисления можно производить в целых числах,

                  +

                  во-вторых, \(w^{-1}=w=-1\), поэтому для обратного преобразования можно

                  +

                  просто применить прямое и разделить всё на \(n\).

                  +
                2. +
                3. \(or\). Теперь рассмотрим значения в точках \(x \in \{0,1\}^k\). Для них

                  +

                  имеет место \(x_i^a x_i^b=x_i^{a~or~b}\), из чего следует, что при

                  +

                  произведении мономов можно трактовать результат как моном с номером,

                  +

                  равным побитовому \(or\) их номеров. Отдельно заметим, что посчитанное

                  +

                  значение многочлена в точке это сумма его коэффициентов по всем

                  +

                  подмаскам номера данной точки.

                  +
                4. +
                5. \(and\). Чтобы посчитать свёртку по данной операции, нужно либо

                  +

                  поменять все маски на их дополнения, посчитать свёртку по \(or\), а

                  +

                  потом вернуться, либо воспользоваться идеей из прошлого пункта и

                  +

                  провести суммирование по всем надмаскам. Это будет соответствовать

                  +

                  значению многочлена в тех же точках, но с неявной перенумерацией,

                  +

                  соответствующей переходу к дополнениям.

                6. +
                +

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

                +

                Метод Ньютона для функций над многочленами

                +

                Хотим решить уравнение \(f(x)=0\). \(f(x)\) можно представить в виде \(f(x)=f(x_0)+f'(x_0)\Delta x+O(\Delta x^2)\). Будем последовательно искать её нули, приближая линейной \(g(x_{n+1})=f(x_n)+f'(x_n)(x_{n+1}-x_n)\) на каждом шаге. Решая \(g(x_{n+1})=0\), приходим к \[x_{n+1}=x_n-\dfrac{f(x_n)}{f'(x_n)}\]

                +

                При этом \(f(x_{n+1})=O((x_{n+1}-x_n)^2)=O\left(\dfrac{f(x_n)^2}{f'(x_n)^2}\right)\). В случае обратимой производной это \(O(f(x_n)^2)\). Если \(x\) – многочлен, это значит, что используя метод Ньютона, мы будем на каждом шаге удваивать число точно известных коэффициентов. Наиболее распространённые функции от многочленов:

                +
                  +
                1. Обратный ряд. Надо решить \(PQ=1 \Rightarrow f(P)=Q-P^{-1}\) и

                  +

                  \(P_{n+1}=P_n-\dfrac{Q-P_n^{-1}}{P_n^{-2}}=P_n(2-QP_n)\).

                2. +
                3. Экспонента. \(Q = \ln P \Rightarrow f(P)=Q-\ln P\) и

                  +

                  \(P_{n+1}=P_n-\dfrac{Q-\ln P_n}{-P_n^{-1}}=P_n(1+Q-\ln P_n)\).

                4. +
                5. Корень. \(Q=P^k \Rightarrow f(P)=Q-P^k\) и

                  +

                  \(P_{n+1}=P_n+\dfrac{Q-P_n^k}{kP_n^{k-1}}=P_n\left(\dfrac{k-1}{k}+\dfrac{Q}{kP_n^k}\right)\).

                6. +
                +

                В выражении для экспоненты есть логарифм, для его вычисления следует воспользоваться тем, что \(\left(\log P\right)'=P'P^{-1}\), что позволит восстановить коэффициенты при положительных степенях, а коэффициент при нулевой степени можно посчитать встроенными методами. Отметим, что все указанные алгоритмы работают за \(O(n \log n)\).

                +

                Разделяй и властвуй

                +

                Дано уравнение \(AX=B\) и мы хотим посчитать первые \(n\) коэффициентов \(X\). При этом \(B\) нам дан не весь, а подаётся по мере того, как мы узнаём коэффициенты \(X\) (например, \(B\) может зависеть от \(X\), см. пример ниже). Пусть мы нашли первые \(m \approx n/2\) коэффициентов и хотим найти следующие \(m\), тогда

                +

                \[\begin{cases} +X=X_1+x^m X_2,\\ +A=A_1+x^m A_2,\\ +B=B_1+x^m B_2,\\ +AX_1=B_1+x^m B_2' +\end{cases} \implies AX\bmod x^{2m} = B_1+x^m(A_1X_2+B_2')=B_1+x^m B_2\] \[A_1X_2=B_2-B_2'\mod x^{m}\]

                +

                Значит, если запомнить \(B_2'\), который потом нужно будет вычесть из \(B_2\), можно свести вычисление оставшихся коэффициентов к этой же задаче размера \(\approx n/2\). Что даст алгоритм за \(O(n \log^2 n)\).

                +

                В качестве примера, этим методом также можно считать экспоненту, так как \[e^A=X \implies X'=A'X\]

                +

                На практике это проще, чем по схеме Ньютона и скрытая константа должна быть меньше, т.к. не нужно вызывать такие затратные функции как логарифм, который требует подсчёта обратного ряда. Заметим, что аналогичная схема может быть применена для подсчёта \(X=AB\), где \(A\) задан заранее, а \(B\) зависит от \(X\).

                +

                \[AB_1 = X_1+x^m X_2'\implies AB \bmod x^{2m}=X_1+x^m(A_1B_2+X_2')\]

                +

                \[X_2 = A_1 B_2 + X_2' \mod x^{m}\]

                +

                Деление и интерполяция

                +

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

                +
                  +
                1. Деление с остатком. Нам нужно представить

                  +

                  \(A(x)=B(x)D(x)+R(x),~\deg R(x) < \deg B(x)\). Пусть

                  +

                  \(\deg A = n,~\deg B=m\). Тогда \(\deg D = n-m\). При этом с учётом

                  +

                  \(\deg R < m\) приходим к выводу, что коэффициенты при

                  +

                  \(\{x^k\}_{k=m}^n\) не зависят от \(R(x)\). Получается, мы имеем систему

                  +

                  из \(n-m+1\) линейных уравнений на \(n-m+1\) неизвестных (коэффициенты

                  +

                  \(D\)).

                  +

                  Рассмотрим

                  +

                  \(A^r(x)=x^nA\left(x^{-1}\right),~B^r(x)=x^mB\left(x^{-1}\right),~~D^r(x)=x^{n-m}D\left(x^{-1}\right)\)

                  +

                  – многочлены, в которых коэффициенты идут в обратном порядке. Для

                  +

                  \(n-m+1\) старших коэффициентов исходных многочленов с учётом того,

                  +

                  что \(P(x) \mod z^k\) – первые \(k\) коэффициентов \(P(x)\) имеем систему

                  +

                  \[A^r(x)= B^r(x) D^r(x)\mod z^{n-m+1}\]

                  +

                  Её решением будет \(D^r(x)=A^r(x)[B^r(x)]^{-1}\bmod z^{n-m+1}\), что

                  +

                  позволяет найти \(D(x)\) и из него \(R(x)\).

                2. +
                3. Многоточечное вычисление. Нужно вычислить \(P(x_i)\) для

                  +

                  \(\{x_i\}_{i=1}^n\). Учитывая \(P(x_i)=P \mod (x - x_i)\), вычислим

                  +

                  \(P \mod \prod\limits_{i=1}^{n/2-1}(x-x_i)\) и

                  +

                  \(P \mod \prod\limits_{i=n/2}^{n} (x-x_i)\) и запустимся рекурсивно.

                  +

                  Получим \(O(n \log^2 n)\).

                4. +
                5. Интерполяция. Дан набор \(\{(x_i, y_i)\}_{i=0}^{n-1}\), нужно найти

                  +

                  \(P: P(x_i)=y_i\). Пусть мы нашли многочлен \(P_1\) для первых \(n/2\)

                  +

                  точек. Тогда \(P=P_1+P_2 \prod\limits_{i=0}^{n/2-1}(x-x_i)=P_1+P_2Q\).

                  +

                  Нахождение \(P_2\) сведём к интерполяции и многоточечному вычислению:

                  +

                  \(P_2(x_i)=\dfrac{y_i-P_1(x_i)}{Q(x_i)}\) для \(i>n/2\). Получим

                  +

                  \(O(n \log^3 n)\).

                6. +
                + +

                Упражнения

                +

                Рюкзак

                +

                Есть \(n\) типов предметов. Предмет \(i\)-го типа имеет стоимость \(s_i\). Пусть \(s=\sum\limits_{i=1}^n s_i\). Предложите алгоритм, который для каждого \(w\leq s\) находит число способов выбрать подмножество предметов ровно с таким весом за \(O(s \log s \log n)\).

                +

                Степенной ряд

                +

                Даны числа \(k\) и \(n\). Найдите \(\sum\limits_{m=0}^n m^k\) за \(O(k \log k)\).

                +

                Общая схема Кули-Тьюки

                +

                Пусть \(n=pq\). Придумайте алгоритм, сводящий преобразование Фурье размера \(n\) к \(p\) преобразованиям Фурье размера \(q\) за \(O(n)\) дополнительных операций.

                +

                Арифметические прогрессии

                +

                Дано множество из \(n\) чисел от \(0\) до \(m\). Найдите число арифметических прогрессий длины \(3\) в этом множестве за \(O(m \log m)\).

                +

                Расстояние между точками

                +

                Даны \(n\) точек в прямоугольнике \(A \times B\). Для каждой возможной пары \((\Delta x,\Delta y)\) посчитайте сколько есть пар точек, таких что разность по \(x\)-координате между ними равна \(\Delta x\), а по \(y\)-координате соответственно \(\Delta y\) за \(O(AB \log AB)\).

                +

                Сопоставление шаблонов

                +

                Даны две строки \(s\) и \(t\). В них могут встречаться символы из множества \(\Sigma\), а также знаки вопроса. Найдите все позиции \(i\) такие, что если приложить строку \(t\) к строке \(s\) начиная с \(i\), то в любой позиции соответствующие символы в \(s\) и \(t\) должны либо совпадать, либо хотя бы один из них должен быть знаком вопроса за \(O(\Sigma n \log n)\) и за \(O(n \log n)\)*.

                +

                Линейные рекурренты*

                +

                Последовательность \(F_n\) задана как \(F_n = \sum\limits_{i=1}^k a_{k-i} F_{n-i}\). Даны коэффициенты \(\{a_i\}_{i=0}^{k-1}\) и начальные величины \(\{F_i\}_{i=0}^{k-1}\). Предложите алгоритм, вычисляющий \(F_n\) за \(O(k \log k \log n)\).

                +

                Степень многочлена*

                +

                Дан \(P(x)=\sum\limits_{i=0}^n a_i x^i\). Нужно найти первые \(n\) коэффициентов \(P^k(x)\) за \(O(n \log n)\).

                + + diff --git a/ru/palindromes.html b/ru/palindromes.html new file mode 100644 index 0000000..ac691e7 --- /dev/null +++ b/ru/palindromes.html @@ -0,0 +1,124 @@ + + + + + + + palindromes + + + + + + + + + + + + + +

                Алгоритм Ахо-Корасик

                +

                Рассмотрим следующую задачу (): Дан набор строк \(S_1, \dots, S_n\), называемый словарём и текст \(T\). Необходимо найти все позиции где строки из словаря входят в текст. Пусть строки из словаря не являются подстроками друг друга (позже мы увидим, что это требование избыточно)

                +

                Будем приписывать символы к тексту по одному и отмечать новый символ, если в нём заканчивается вхождение какой-нибудь из \(S_i\). Для решения данной задачи логично действовать следующим образом: Будем поддерживать наибольшую строку, являющуюся префиксом строки из словаря и при этом также суффиксом считанного на данный момент текста. Тогда мы должны отметить текущее состояние если поддерживаемая строка является одной из \(S_i\).

                +

                Все префиксы \(S_i\) удобно хранить в единой структуре – префиксном дереве. На его рёбрах написаны символы, а пути из корня в листья при конкатенации символов на рёбрах дают строки из \(S_i\). простейший алгоритм построения префиксного дерева имеет следующий вид:

                +

                ``` {.c++ language=“C++”} const int maxn = 1e5 + 42, sigma = 26; // Наибольшая суммарная длина строк словаря и размер алфавита. int to[maxn][sigma]; // to[v][c] – ребро из v с пометкой c. int sz = 1; // Количество использованных вершин.

                +

                void add_str(string s) { int v = 0; // Корень дерева. for(char c: s) { c -= ‘a’; // Переводим в [0; 26) if(!to[v][c]) // Если ещё нет перехода по данному символу, создаём его в вершину sz. to[v][c] = sz++; v = to[v][c]; } }

                +
                
                +Теперь нам нужно научиться при добавлении очередного символа находить
                +вершину в префиксном дереве, которая получена приписыванием этого
                +символа к какому-то суффиксу текущей вершины, а из всех таких вершин --
                +наибольшую. Будем записывать эти вершины в переходы по недостающим
                +символам, рассматривая префиксы в порядке увеличения их длины (то есть,
                +в порядке $bfs$ вершин дерева). Введём понятие суффиксной ссылки
                +$link_v$ -- Она ведёт из вершины $v$ в вершину $u$ с наибольшей длиной,
                +являющуюся её собственным суффиксом.
                +
                +Алгоритм аналогичен принципу математической индукции -- мы считаем, что
                +когда мы пришли в вершину $v$, все вершины с меньшей длиной уже
                +обаботаны корректно, то есть, их переходы выставлены в нужные места, а
                +для вершины $v$ известна суффиксная ссылка $u$. Рассмотрим переходы по
                +всем возможным символам $c$ из $v$. Если по данному символу уже
                +выставлен переход, то мы можем сказать, что суффиксной ссылкой вершины
                +$to_{v,c}$, в которую ведёт переход будет вершина $to_{u,c}$. Если же
                +перехода нет, то выставим вместо него $to_{u,c}$. Корректность данного
                +построения очевидна -- если бы искомый переход шёл из суффикса большего,
                +чем $u$, то в него бы и вела суффиксная ссылка, а не в $u$, а переходы
                +из суффиксов меньших, чем $u$ учтены в ней по предположению индукции.
                +
                +\newpage
                +В реализации ниже есть один нюанс -- суффиксная ссылка из корня ведёт в
                +него же, что заставляет рассматривать его отдельно, но в целом упрощает
                +реализацию.
                +
                +``` {.c++ language="C++"}
                +int link[maxn];
                +void push_links()
                +{
                +    int que[sz];
                +    int st = 0, fi = 1;
                +    que[st] = 0; 
                +    while(st < fi)
                +    {
                +        int v = que[st++];
                +        int u = link[v];
                +        for(int c = 0; c < sigma; c++)
                +        {
                +            if(to[v][c])
                +            {
                +                link[to[v][c]] = v ? to[u][c] : 0;
                +                que[fi++] = to[v][c];
                +            }
                +            else
                +            {
                +                to[v][c] = to[u][c];
                +            }
                +        }
                +    }
                +}
                +

                Таким образом, чтобы решить задачу, нам следует поддерживать требуемую вершину \(v\) и при каждом приписывании нового символа совершать переход по нему. В общем случае если строки словаря могут быть подстроками друг друга, всё, что нам нужно будет сделать – проверить, что по пути из суффиксных ссылок текущей вершины встречается какая-то из строк словаря.

                +

                Данный алгоритм работает за \(O(|S|\Sigma)\), где \(|S|\) – размер словаря, а \(\Sigma\) – размер алфавита. Заметим, что существуют также алгоритмы построения подобной структуры за \(O(|S| \log \Sigma)\) с помощью персистентного массива или через стандартные амортизационные трюки. Второй вариант очень похож на алгоритм префикс-функции, приведём его здесь без подробных комментариев.

                +

                {.c++ language="C++"} map<int, int> to[maxn]; ... void push_links() { int que[sz]; int st = 0, fi = 1; que[st] = 0; while(st < fi) { int v = que[st++]; for(auto it: to[v]) { int c = it.first; int u = it.second; int j = link[v]; while(j != 0 && to[j][c] == 0) j = link[j]; if(to[j][c] != u) link[u] = to[j][c]; que[fi++] = u; } } }

                + +

                Алгоритм Манакера

                +

                Напомним, что палиндром – это строка, которая читается одинаково слева направо и справа налево.

                +

                Пусть есть строка \(S\) и мы хотим найти в ней все подпалиндромы. Мы сразу сталкиваемся с очевидной тудностью – их в строке может быть \(O(n^2)\), что можно видеть на примере строки \(S = aa\dots a\). Поэтому будет использован следующий формат: Для каждой позиции \(S_i\) мы найдём наибольший палиндром, центр которого совпадает с \(S_i\). При этом пока что не будем учитывать палиндромы чётной длины ().

                +

                Рассмотрим наиболее наивный алгоритм: Перебираем \(S_i\), а для него вторым циклом находим наибольшую искомую длину.

                +

                {.c++ language="C++"} vector<int> pal_array(string s) { int n = s.size(); s = "@" + s + "$"; // Окружим спецсимволами, чтобы не рассматривать выход за границы vector<int> len(n + 1); for(int i = 1; i <= n; i++) { len[i] = 0; while(s[i - len[i]] == s[i + len[i]]) len[i]++; } len.erase(begin(len)); return len; }

                +

                \(len_i\) здесь обозначает не совсем искомую длину, а расстояние от центра до границы палиндрома, которое в дальнейшем будем называть радиусом палиндрома. Тот же пример \(S = aa\dots a\) показывает, что данная реализация работает за \(O(n^2)\).

                +

                Применим идею, которая, возможно, знакома вам по алгоритму z-функции: При инициализации \(len_i\) будем пользоваться посчитанными \(len\). Будем хранить \((l, r)\) – интервал, соответствующий самому правому из найденных подпалиндромов. Тогда мы можем сказать, что та часть наибольшего палиндрома с центром в \(S_i\), которая лежит в \(S_{(l, r)}\) имеет радиус \(\min(r-i,len_{l+r-i})\). \(r-i\) означает длину при которой произошёл бы выход за \(S_{(l, r)}\), а \(len_{l+r-i}\) – значение \(len\) в позиции, зеркальной относительно центра \(S_{(l, r)}\).

                +

                {.c++ language="C++"} vector<int> pal_array(string s) { int n = s.size(); s = "@" + s + "$"; vector<int> len(n + 1); int l = 1, r = 1; for(int i = 1; i <= n; i++) { len[i] = min(r - i, len[l + (r - i)]); while(s[i - len[i]] == s[i + len[i]]) len[i]++; if(i + len[i] > r) { l = i - len[i]; r = i + len[i]; } } len.erase(begin(len)); return len; }

                +

                Покажем, что алгоритм линейный. Очевидно, что цикл \(while\) запускается только если \(len_i = r-i\) (иначе палиндром уже во что-то упёрся). Значит, каждая его итерация сдвигает \(r\) на единицу вправо. Таким образом, так как \(r \leq n\), получаем, что суммарно эти циклы отработают за \(O(n)\).

                +

                Чтобы учесть чётные палиндромы, сделаем замену: \(S = s_1 s_2 \dots s_n \to S^* = s_1 \# s_2 \# \dots \# s_n\). Теперь нечётные палиндромы с центром в \(s_i\) соответствуют нечётным палиндромам исходной строки, а нечётные палиндромы с центром в \(\#\) – чётным.

                + +

                Дерево палиндромов

                +

                Данная структура, предложенная Михаилом Рубинчиком, представляет собой другой формат хранения информации обо всех подпалиндромах строки, более мощный по своей сути. Сперва докажем, что у любой строки есть не больше, чем \(n\) палиндромов. Пусть мы наращиваем строку по одному символу и в данный момент имеем наибольший суффикс-палиндром \(S_{l,r}\). Пусть у него есть суффикс-палиндром \(S_{l',r}\). Тогда он уже имеет вхождение в позиции \(S_{l,l+r-l'}\). Таким образом, с каждым новым символом у строки появляется не более одного нового палиндрома и если таковой есть, то это всегда наибольший суффикс-палиндром.

                +

                Всем палиндромам строки удобно сопоставить следующую структуру, которая и называется деревом палиндромов: возьмём от каждого палиндрома его правую половинку (например, \(caba\) для \(abacaba\) или \(ba\) для \(abba\)) и добавим в префиксное дерево. Чётные и нечётные палиндромы при этом рассматриваем отдельно. Таким образом будет получено взаимооднозначное соответствие между вершинами дерева и подпалиндромами.

                +

                Пусть мы построили дерево палиндромов для первых \(n\) символов строки и хотим добавить очередной символ. Для этого будем поддерживать наибольший суффикс-палиндром. Когда к нам придёт очередной символ, нам нужно будет найти наибольший суффикс указанного палиндрома, который может быть дополнен новым символом до палиндрома, это и будет новый суффикс-палиндром. Для этих целей будем поддерживать суффиксную ссылку, аналогичную таковой в алгоритме Ахо-Корасик, она будет вести из \(v\) в наибольший собственный суффикс-палиндром \(v\). При добавлении очередного символа, будем подниматься по суффиксным ссылкам, пока не найдём место, где можно совершить переход. Там нам, возможно, придётся создать новую вершину в дереве и для неё понадобится суффиксная ссылка. Чтобы найти её, будем продолжать подниматься по суффиксным ссылкам предыдущего суффикс-палиндрома, пока не найдём второе место, которое можно дополнить очередным символом.

                +

                Покажем линейность. Рассмотрим длину наибольшего суффикс-палиндрома строки. Каждый новый символ увеличивает её не более, чем на \(2\). При этом каждый подъём по суффиксной ссылке в цикле уменьшает её, поэтому основной цикл работает линейное время. Аналогичными рассуждениями о втором суффикс-палиндроме получаем, что пересчёт суффиксных ссылок при создании новых вершин также работает за линейное время суммарно.

                +

                Наконец, приведём реализацию указанного алгоритма:

                +

                ``` {.c++ language=“C++”} const int maxn = 1e5 + 42; map<char, int> to[maxn]; int len[maxn], link[maxn], s[maxn]; int sz, n, last;

                +

                void init() // Начальные значения, чтобы не возиться с границами { link[0] = 1; len[1] = -1; s[n++] = -1; sz = 2; }

                +

                int get_link(int v) // Суффикс v, который дополняется до палиндрома { while(s[n - len[v] - 2] != s[n - 1]) v = link[v]; return v; }

                +

                void add_letter(char c) { s[n++] = c; last = get_link(last); if(!to[last][c]) { len[sz] = len[last] + 2; link[sz] = to[get_link(link[last])][c]; to[last][c] = sz++; } last = to[last][c]; } ```

                +

                Как и в случае с Ахо-Корасик, существуют неамортизированные \(O(n \Sigma)\) и \(O(n \log \Sigma)\) версии.

                + + diff --git a/ru/suffix_automaton.html b/ru/suffix_automaton.html new file mode 100644 index 0000000..4050b9b --- /dev/null +++ b/ru/suffix_automaton.html @@ -0,0 +1,116 @@ + + + + + + + suffix_automaton + + + + + + + + + + + + +

                Введение

                +

                Рассмотрим задачу поиска образцов в тексте. Дан текст \(T\) и \(n\) образцов \(S_i\), для каждого из образцов нужно узнать, встречается ли он в тексте. Есть два основных подхода к решению этой задачи. Первый — алгоритм Ахо-Корасик, который строит по набору образцов автомат, распознающий строки, в которых встречаются данные образцы. Второй — использование суффиксных структур, под которыми обычно подразумевают суффиксный массив, суффиксный автомат или суффиксное дерево. Данная лекция по большей части будет посвящена последним двум из них, их связи и применении в решении задач.

                +

                Наивное решение

                +

                Возьмём все суффиксы \(T\) и объединим их в бор, в нём каждой подстроке \(T\) будет соответствовать ровно одна вершина. Теперь мы можем за \(O(|T|^2)\) времени на построение бора и \(O(|S_i|)\) на запрос узнавать, входит ли \(S_i\) в \(T\). От квадратичной сложности можно уйти двумя способами, ведущими к суффиксному дереву и суффиксному автомату соответственно.

                + +
                +Суффиксное дерево cbcbba
                Суффиксное дерево \(cbcbba\)
                +
                + +
                +Суффиксное дерево cbcbba
                Суффиксное дерево \(cbcbba\)
                +
                +

                Суффиксное дерево

                +

                Любой нисходящий путь в этом боре будет подстрокой \(T\), значит если из вершины \(v\) нет других исходящих рёбер, можно заменить путь \(u \topto{\al} v \topto{\bb} w\) на ребро \(u \topto{\al \bb} w\), храня при этом строку \(\al \bb\) в виде пары чисел \([l;r]\), указывающей на её местоположение в \(T\). Такая сжатая структура называется суффиксным деревом и занимает \(O(|T|)\) памяти.

                +

                Суффиксный автомат

                +

                Детерминированный конечный автомат (далее автомат) — это пятёрка \(A = (Q, \Sigma, \delta, q_0, F)\), где \(Q\) — множество состояний, \(\Sigma\) — алфавит, \(\delta \subset Q \times \Sigma \times Q\) — множество переходов, \(q_0 \in Q\) — начальное состояние, \(F \subset Q\) — множество финальных состояний.

                +

                Всякому автомату можно сопоставить ориентированный граф такой что состояния автомата — это вершины графа, а переходы — это дуги помеченные символами из алфавита. Соответственно, в таком графе должна быть выделена некоторая вершина \(q_0\) (начальное состояние) и набор вершин \(F\) (финальные состояния). Кроме того, из любого состояния не может быть двух переходов по одному и тому же символу (детерминированность).

                +

                Состояние \(q\) принимает строку \(s\), если есть путь из \(q_0\) в \(q\) такой что если выписать все символы, которые мы встретили на этом пути, мы получим строку \(s\). Автомат принимает строку \(s\), если её принимает хотя бы одно из финальных состояний. Множество строк принимаемых автоматом будем называть его языком.

                +

                Суффиксным автоматом строки \(s\) будем называть минимальный автомат, который принимает все суффиксы строки и только их. Под минимальностью подразумевается, что число состояний в нём должно быть наименьшим возможным.

                +

                Суффиксный автомат не только ориентированный, но и ациклический граф, ведь множество принимаемых им строк конечно, а цикл позволил бы принять сколь угодно длинную строку. Рассмотрим факты, позволяющие нам уточнить природу суффиксного автомата.

                +

                Пусть \(a\) и \(b\) — строки, которые принимает состояние \(q\) некоторого автомата \(A\). Тогда для любой строки \(x\) строки \(ax\) и \(bx\) принимаются или не принимаются \(A\) одновременно.

                +

                Независимо от того, как мы “пришли” в состояние \(q\), если мы пройдём из него по пути, соответствующему строке \(x\), мы сможем точно сказать, в какое состояние мы попадём и, в частности, будет ли оно финальным.

                +

                Правым контекстом \(X(q)\) состояния \(q\) назовём множество строк \(x\), переводящих \(q\) в одно из финальных состояний. Соответственно, назовём правым контекстом строки \(a\) множество \(X(a)\) строк \(x\) таких что \(ax\) принимается автоматом.

                +

                Правый контекст строки совпадает с правым контекстом состояния, которое её принимает, поэтому правые контексты языка можно рассматривать независимо от конкретного автомата:

                +

                Рассмотрим множество \(R\) непустых правых контекстов всех возможных слов над \(\Sigma\). В любом автомате, задающем данный язык будет хотя бы \(|R|\) состояние.

                +

                Рассмотрим любой правый контекст в \(R\). Пусть он порождён строкой \(a\), тогда в автомате должно быть некоторое состояние \(q\), принимающее строку \(a\) и имеющее такой правый контекст, иначе автомат бы не смог принять строку \(ax\). При этом все эти состояния будут различными, так как у них будут различный правые контексты.

                +

                Существует автомат, на котором эта оценка достигается.

                +

                Если в автомате есть состояние с пустым правым контекстом, мы можем удалить его, не изменим принимаемый язык. Иначе пусть в автомате есть два состояния \(q_1\), \(q_2\) такие что \(X(q_1) = X(q_2)\). Мы можем удалить состояние \(q_2\) и перевести переходы, ведущие в него в состояние \(q_1\). Множество принимаемых строк от этого не изменится, следовательно, мы можем продолжать эту процедуру, пока число состояний не будет равно числу различных непустных правых контекстов.

                +

                Таким образом, мы доказали:

                +

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

                +

                В случае суффиксного автомата правый контекст \(X(a)\) строки \(a\) взаимно однозначно соответствует множеству правых позиций вхождений строки \(a\) в строку \(s\). Действительно, если \(ax\) принимается автоматом, то есть, является суффиксом, то \(s = yax\), а строке \(x\) мы можем сопоставить позицию \(|s|-|x|-1\). Таким образом, каждое состояние автомата принимает строки с одинаковым множеством правых позиций их вхождений и обратно, все строки с таким множеством позиций принимаются этим состоянием.

                +

                Связь между суффиксным автоматом и суффиксным деревом

                +

                Рассмотрим ребро в суффиксном дереве строки \(T\), а точнее все подстроки \(T\), которым соответствует или “внутренняя” вершина ребра, или вершина, в которой ребро заканчивается. Для любой строки \(x\) и любой пары строк \(a\), \(b\) из рассматриваемого множества строк, строки \(xa\) и \(xb\) являются или не являются префиксами строки \(s\) одновременно.

                +

                Пусть \(|a| < |b|\). Тогда \(a\) является предком \(b\) в боре, то есть, её префиксом, значит, её множество вхождений точно содержит множество вхождений строки \(b\). Допустим, существует позиция \(|x|\), в которой есть вхождение строки \(a\), но не строки \(b\).

                +

                Рассмотрим строку \(a'\), которая является максимальным префиксом строки \(b\), который можно встретить в той позиции. Если \(a'\) не упирается в конец строки, то её можно продолжить, как минимум, двумя различными символами чтобы она осталась подстрокой \(T\). Значит, соответствующая ей вершина в дереве имеет степень больше двух и должна разбивать ребро, на котором находится, что конфликтует с предположением о том, что \(a\) и \(b\) взяты с одного ребра.

                +

                Если же \(a'\) нельзя продолжить, то она всё ещё должна разбивать ребро, т.к. является суффиксом строки и её вершина не будет удалена при сжатии рёбер.

                +

                Аналогичным образом можно показать, что для любой строки \(a\) все строки \(b\) с таким же множеством строк \(x\) находятся на соответствующем строке \(a\) ребре, то есть, есть биекция между рёбрами суффиксного дерева и множествами левых позиций вхождений строк в \(T\). Отсюда:

                +

                Для любого состояния \(q\) суффиксного автомата строки \(T\) найдётся вершина \(q'\) суффиксного дерева развернутой строки \(T\) такая, что множество строк, принимаемых состоянием \(q\), совпадает с развёрнутым множеством строк, таких что соответствующая им вершина в дереве лежит на ребре, ведущем в \(q'\) (включая строку, соответствующую \(q'\)).

                +

                Это целиком описывает состояния автомата и позволяет разработать алгоритм его построения.

                +

                Построение суффиксного автомата

                +

                Пусть длина самой короткой строки, которая принимается состоянием \(q\) равна \(k\). Тогда суффиксная ссылка \(link(q)\) будет вести из этого состояния в состояние, которое принимает эту же строку без первого символа.

                +

                Обратившись к суффиксному дереву мы поймём, что в нём она будет вести в предка вершины \(q'\). Таким образом, суффиксные ссылки образуют дерево, которое соответствует суффиксному дереву развернутой строки. Также будем обозначать длину самой длинной строки, которая принимается состоянием \(q\) как \(len(q)\). Длина самой короткой строки из \(q\) будет равна \(len(link(q)) + 1\).

                +

                Будем дописывать символы в конец строки \(T\) по одному и при этом поддерживать для неё корректный автомат с деревом суффиксных ссылок. Пусть у нас есть автомат для строки \(T\) и она принимается его состоянием \(last\). Мы хотим получить автомат для строки \(Tc\). Нам нужно, чтобы для каждого суффикса новой строки существовало состояние, которое его примет. При этом нам нужно сохранить минимальность автомата.

                +

                Добавим состояние, которое принимает всю строку \(Tc\) и назовём его \(new\). Правый контекст \(Tc\) состоит из единственной строки — пустой, значит, в \(new\) будут входить те и только те суффиксы, которые мы встретили в строке впервые. Все такие строки можно получить дописыванием символа \(c\) к суффиксам \(T\), которые принимаются состоянием, из которого ещё нет перехода по данному символу. Таким образом, чтобы новые суффиксы принимались, нам необходимо будет “попрыгать” по суффиксным ссылкам и добавить переходы по символу \(c\) в состояние \(new\), пока не придём в корень или не обнаружим, что переход по символу \(c\) из состояния уже есть.

                +

                Если мы пришли в корень, значит, все непустые суффиксы строки \(Tc\) принимаются состоянием \(new\) и мы можем положить \(link(new) = q_0\) и завершить работу.

                +

                Иначе мы нашли состояние \(q'\), из которого переход по символу \(c\) уже есть. Значит, суффиксы длины \(\leq len(q') + 1\) уже встречались в строке, и новых переходов в состояние \(new\) мы проводить не будем. Однако, для состояния \(new\) ещё нужно посчитать суффиксную ссылку. Наибольшей строкой в ней будет суффикс строки \(Tc\) длины \(len(q') + 1\). В данный момент он находится в состоянии \(t\), в которое ведёт переход по символу \(c\) из состояния \(q'\), но в нём могуть быть также строки большей длины. Таким образом, если \(len(t) = len(q') + 1\), то \(t\) и есть искомая суффиксная ссылка. Проведя её, мы завершим обновление автомата.

                +

                Иначе \(t\) — состояние, которое принимает как строки, являющиеся суффиксами строки, так и строки, которые ими не являются, из-за этого мы не можем корректно определить его финальность. Чтобы решить данный конфликт мы должны будем отщепить от \(t\) состояние \(t'\), которое примет все строки, которые принимаются \(t\), но имеют длину \(\leq len(q') + 1\), то есть, тот самый кусок с суффиксами. Для этого скопируем в \(t'\) переходы и суффиксную ссылку из \(t\), а длину установим равной \(len(q') + 1\). Затем установим \(link(new) = link(t) = t'\). Наконец, чтобы “перебросить” в него нужные строки из \(t\), пройдёмся по суффиксным ссылкам состояния \(q'\) пока переходы по \(c\) ведут в \(t\) и переправим эти переходы в \(t'\). Таким образом, мы перенаправим все пути интересующей нас длины, которые ранее проходили через вершину \(t\) в вершину \(t'\).

                +

                В некотором смысле эта процедура соответствует построению суффиксного дерева добавлением суффиксов в возрастающем (по длине) порядке, приведём код на языке C++, выполняющий её:

                +

                ``` {.c++ language=“C++”} const int maxn = 2e5 + 42; // Максимальное число состояний map<char, int> to[maxn]; // Переходы int link[maxn]; // Суффиксные ссылки int len[maxn]; // Длины максимальных строк в состояниях int last = 0; // Состояние, соответствующее всей строке int sz = 1; // Общее число состояний

                +

                void add_letter(char c) // Дописываем символ в конец { int p = last; // Записываем в p состояние строки s last = sz++; // Создаём для строки sc новое состояние len[last] = len[p] + 1; for(; to[p][c] == 0; p = link[p]) // (1) to[p][c] = last; // Прыгаем по ссылкам, создавая переходы if(to[p][c] == last) { // Если мы оказались здесь, то символ c встречен впервые link[last] = 0; return; } int q = to[p][c]; if(len[q] == len[p] + 1) { // Если переход сплошной, то q - суфф ссылка link[last] = q; return; } // Расщепляем q на два состояния, одно из которых cl, // А второе получит тот же номер, что и q имело ранее int cl = sz++; to[cl] = to[q]; // (2) link[cl] = link[q]; len[cl] = len[p] + 1; link[last] = link[q] = cl; for(; to[p][c] == q; p = link[p]) // (3) to[p][c] = cl; // Перенаправляем переходы там, где нужно } ```

                +

                Время работы

                +

                Покажем линейность работы данного алгоритма. На каждом шаге есть три места, которые выполняются не за \(O(1)\):

                +
                  +
                1. Прыжки по ссылкам \(last\) для создания переходов в \(new\).

                2. +
                3. Копирование переходов из \(t\) в \(t'\).

                4. +
                5. Прыжки по ссылкам \(q'\) для перенаправления переходов из \(t\) в \(t'\).

                6. +
                +

                В первых двух пунктах происходит создание очередного перехода в автомате. Покажем, что всего переходов будет \(O(n)\). Разделим все переходы \(\delta(v, c) = u\) из \(v\) в \(u\) по символу \(c\) на два класса — “сплошные”, для которых \(len(v) + 1 = len(u)\) и все остальные.

                +

                Т.к. в каждое состояние, кроме начального, ведёт ровно один сплошной переход, сплошных переходов будет не больше, чем состояний, а их \(O(n)\).

                +

                Рассмотрим теперь несплошные переходы. Каждому такому переходу можно поставить в соответствие строку \(acb\), где \(a\) — длиннейшая строка, которую принимает состояние \(v\), а \(b\) — длиннейшая строка, которую можно вывести из состояния \(u\). Данная строка является суффиксом \(s\) (иначе мы могли бы продлить \(b\) вправо). Кроме того \(|a| = len(v)\), отсюда можно сделать вывод, что \(|a|\) составлена исключительно из сплошных переходов. Значит, по произвольному суффиксу мы можем определить рассмотренный несплошной переход, как первый, который встретим, “скармливая” его строке. Значит, такое отображение взаимооднозначное и сплошных переходов не больше, чем суффиксов в строке. Отсюда следует, что суммарно переходов в автомате \(O(n)\).

                +

                Наконец, докажем линейность третьего пункта. Для удобства назовём \(link(link(q))\) второй суффиксной ссылкой состояния \(q\). Также будем использовать такие обозначения: \(last\) — состояние, соответствующее строке \(s\), \(new\) — состояние строки \(sc\), \(p\) — состояние, которое “прыгает” в циклах (можно видеть в коде). Т.к. \(sc\) не могла встречаться в суффиксном автомате \(s\), из состояния \(last\) изначально нет перехода по \(c\), поэтому в цикле (1) мы сделаем хотя бы один шаг. Отсюда \(len(link(p)) \leq len(link(link(last)))\) (действительно, изначально \(p = last\), после первой итерации \(p = link(last) \rightarrow len(link(p)) = len(link(link(last)))\), на всех последующих итерациях длина \(len(link(p))\) не увеличивается, значит, упомянутое неравенство верно.

                +

                Когда мы вышли из цикла (1), мы имеем \(\delta(p, c) = q\). Очевидно, если есть переход из состояния \(p\) в состояние \(q\), то если мы допишем символ \(c\) к самой короткой строке из \(p\), длина которой равна \(len(link(p)) + 1\), то получим строку, которая будет не короче, чем самая короткая строка, принимаемая состоянием \(q\), длина которой равна \(len(link(q)) + 1\). То есть, \(len(link(p)) + 2 \geq len(link(q)) + 1 \rightarrow len(link(p)) + 1 \geq len(link(q))\).

                +

                После выхода из цикла (1) куда бы мы ни поставили суффиксную ссылку из \(new\), вторая суффиксная ссылка точно будет \(link(q)\). Отсюда \(link(q) = link(link(new)) \rightarrow len(link(q)) = len(link(link(new)))\).

                +

                Теперь посмотрим на цикл (3). Он будет выполняться пока \(\delta(p, c) = q\), то есть, как было упомянуто выше \(len(link(p)) + 1 \geq len(link(q))\). При этом изначально \(len(link(link(last))) \geq len(link(p))\). Так как \(len(link(q)) = len(link(link(new)))\), а также на каждом шаге \(len(link(p))\) уменьшается, получаем, что весь цикл отработает не дольше, чем за \(len(link(link(last))) - len(link(link(new)))\), то есть, за разность максимальных длин, принимаемых вторыми суффиксными ссылками состояний, соответствующих всей строке.

                +

                Наконец, собирая всё полученное воедино, получим такое неравенство: \(len(link(link(last))) + 1 \geq len(link(p)) + 1 \geq len(link(q)) = len(link(link(new)),\) отсюда \(len(link(link(last))) + 1 \geq len(link(link(new)))\), которое означает, что на каждом шаге длина второй суффиксной ссылки либо уменьшилась, либо увеличилась не больше, чем на единицу (а значит, её суммарное уменьшение не превосходит \(O(n)\)). Линейность алгоритма доказана!

                +

                Применение в решении задач

                +
                  +
                1. Число различных подстрок. Дана строка \(s\), необходимо посчитать количество её различных подстрок. В каждом состоянии встречаются строки длины от \(len(link(q)) + 1\) до \(len(q)\). Всего \(len(q) - len(link(q))\) строк. Просуммировав эту величину по всем состояниям, получим ответ.

                  +

                  Упражнение: решите эту же задачу за \(O(n)\), учитывая, что к строке \(s\) символы дописываются по одному и после каждого нового символа необходимо сказать текущее число различных подстрок строки \(s\).

                  +

                  Упражнение*: Возьмём задачу из предыдущего упражнения и скажем, что теперь мы можем не только дописывать символы в конец, но и удалять их с начала строки. Вам требуется отвечать на те же запросы. Время работы решения всё ещё должно линейно зависеть от размера входа. Подсказка: иногда алгоритм Укконена также бывает полезен.

                2. +
                3. Поиск подстрок в тексте. Пропустив строку через автомат мы сможем сказать, входит ли она в текст. Допустим, мы хотим узнать какую-то информацию о её вхождениях. Например, нам нужно выдать любое конкретное вхождение. Как мы уже знаем, каждому вхождению соответствует строка \(x\) такая что \(ax\) — суффикс \(s\). Или, проще говоря, путь из состояния \(q\) в какое-то финальное состояние. Динамикой по автомату как ациклическому ориентированному графу мы можем найти длину какого-нибудь такого пути (например, для определённости минимального или максимального). Отметим, что аналогичной динамикой считаются многие другие полезные значения, например, количество строк в правом контексте состояния (или, что то же самое, количество вхождений строк из состояния в \(s\)).

                  +

                  Альтернативным решением будет обратиться к дереву суффиксных ссылок, которое, как мы помним, является суффиксным деревом для \(s^T\). Как мы упоминали в самом начале, любая подстрока строки \(s\) является префиксом одного из суффиксов исходной строки. Таким образом, если мы запишем в каждую “суффиксную” вершину индекс соответствующего ей суффикса, то все позиции вхождений строки \(t\) в \(s\) можно будет обнаружить в поддереве вершины, которая соответствует строке \(t\). Значит, в частности, динамикой можно будет найти самое первое или самое последнее вхождение.

                  +

                  Более того, учитывая, что в последнем случае мы работали с деревом, мы можем обойти его таким образом, чтобы на каждом шаге иметь в вершине множество возможных позиций, в которых встречаются строки из соответствующего состояния. Для этого нужно применить идею быстрого слияния множеств, когда мы всегда добавляем элементы из меньшего множества в большее, а не наоборот. Тогда такой обход потребует \(O(n \log n)\) операций добавления в множество, т.к. каждый раз когда мы переносим между множествами элемент \(k\), размер нового множества будет как минимум, в два раза больше старого, в котором он хранился.

                  +

                  Упражнение*: дана строка \(s\). Найти число строк \(t\) таких, что они имеют хотя бы \(3\) непересекающихся вхождения в строку \(s\).

                4. +
                5. Наибольшая общая подстрока. Нам дано \(k\) строк \(s_1, s_2, \dots, s_k\). Нужно найти наибольшую строку \(t\), которая встречается в каждой из строк \(s_i\). Одно из возможных решений — построить автомат для строки \(s_1 t_1 s_2 t_2 \dots s_n t_n\), где \(t_i\) — уникальный для каждой строки символ-разделитель. Теперь мы можем завести динамику \(dp[q][i]\), в которой хранить \(1\), если из состояния \(q\) можно добраться до состояния, из которого есть переход по \(t_i\), не проходя при этом через другие символы-разделители. Это будет равносильно тому, что строки из \(q\) входят в \(s_i\). Как и в прошлый раз, динамику можно пересчитывать по топологической сортировке автомата как ориентированного ациклического графа. Итого решение будет работать за \(O(k \cdot \sum |s_i|)\).

                  +

                  Упражнение*: решить указанную задачу за \(O(\sum |s_i|)\).

                6. +
                + + diff --git a/ru/trie.html b/ru/trie.html index ac27ccd..c03285a 100644 --- a/ru/trie.html +++ b/ru/trie.html @@ -102,7 +102,7 @@

                Бор

                Бор

                -

                Бор — это структура данных для компактного хранения строк.

                +

                Префиксное дерево или бор — это структура данных для компактного хранения строк.

                Он устроен в виде дерева, где на ребрах между вершинами написана символы, а некоторые вершины помечены терминальными. Бор хранит ровно те строки, которые получаются, если выписать подряд все буквы на путях от корня до терминальных вершин.

                trie
                trie
                From f39c05f35401db73bd8373a600a30a39e9c987f4 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 12 Jun 2019 18:30:14 +0000 Subject: [PATCH 057/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- img/complex-circle.png | Bin 0 -> 129615 bytes pandoc.css | 8 +- ru/fft.html | 253 ++++++++++++++++++--------------- ru/geometry.html | 6 +- ru/imperferct-information.html | 100 +++++++++++++ ru/pollard.html | 96 +++++-------- 6 files changed, 285 insertions(+), 178 deletions(-) create mode 100644 img/complex-circle.png create mode 100644 ru/imperferct-information.html diff --git a/img/complex-circle.png b/img/complex-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..3a95f48964f64397b521e4d1fbc33b56758ce4b9 GIT binary patch literal 129615 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%0ASM)akRV`SpaH>AkWrF>FepKZ5*u`s5d+wb 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 literal 0 HcmV?d00001 diff --git a/pandoc.css b/pandoc.css index 5dc70a8..263ea7e 100644 --- a/pandoc.css +++ b/pandoc.css @@ -31,7 +31,7 @@ body { line-height: 1.7; padding: 15m; margin: auto; - max-width: 48em; + width: 750px; background: #fefefe; text-align: justify; padding-bottom: 25px; @@ -224,16 +224,18 @@ img { vertical-align: middle; } -.figure { +figure { display: block; text-align: center; margin: 1em 0; } -.figure img { +img { border: none; margin: auto; + display: block; max-width: 85%; + max-height: 400px; } figcaption { diff --git a/ru/fft.html b/ru/fft.html index 4e182bd..ce12e27 100644 --- a/ru/fft.html +++ b/ru/fft.html @@ -4,7 +4,7 @@ - БПФ и многочлены + Быстрое преобразование Фурье + + + + + + + +

                + + + +
                +

                Игры с неполной информацией

                +
                +

                Игры с неполной информацией

                +

                Рассмотрим следующую ситуацию, известную как дилемма заключённого:

                +
                +

                Двое преступников — А и Б — попались примерно в одно и то же время на сходных преступлениях. Полиция предполагает, что они действовали по сговору, и, изолировав их друг от друга, предлагает им одну и ту же сделку:

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

                • +
                • Если оба молчат, их деяние проходит по более лёгкой статье, и каждый из них приговаривается к году тюрьмы.

                • +
                • Если оба свидетельствуют друг против друга, они получают по 2 года.

                • +
                +

                Каждый заключённый выбирает, молчать или свидетельствовать против другого. Однако ни один из них не знает точно, что сделает другой. Что произойдёт?

                +
                +

                Игры, в которых у игроков (в данном случае — узников) есть некоторая неопределенность относительно действий других игроков (например, из-за одновременности действий), называются играми с неполной информацией.

                +

                Данную «игру» можно представить в виде следующей табилцы:

                +
                                                   | Б хранит молчание | Б даёт показания |
                +А хранит молчание | (1, 1) | (10, 0) |
                +А даёт показания | (0, 10) | (2, 2) |
                +

                Пары чисел в ячейках означают сроки, которые получают заключенные А и Б соответственно.

                +

                Предположим, что оба преступника заботятся только о минимизации собственного срока заключения. Тогда рассуждения каждого узника будут следующими:

                +
                  +
                • Если партнёр молчит, то лучше его предать и выйти на свободу (иначе — полгода тюрьмы).

                • +
                • Если партнёр свидетельствует, то лучше тоже свидетельствовать против него, чтобы получить 2 года (иначе — 10 лет) тюрьмы.

                • +
                +

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

                +

                С точки зрения группы лучше всего хранить молчание и получить по году, так как это уменьшит срок заключения каждого. Такие состояния называются Парето-эффективными, но, как мы видим, они не всегда являются стабильными.

                +

                Игры с нулевой суммой

                +

                Рассмотрим другой пример игры с неполной информацией: «камень-ножницы-бумага». Эта игра в каком-то смысле более «человечная»: игроки явно соревнуются друг с другом, и им не нужно делать сделки с совестью и предавать друг друга.

                +

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

                +

                Примечание. Дилемма заключенного — пример игры с ненулевой суммой.

                +

                В играх с нулевой суммой исходы можно записывать разницей между выгодой первого и второго игрока:

                +
                    | К2 | Н2 | Б2|
                +К1 | 0 | +1 | -1|
                +Н1 | -1 | 0 | +1|
                +Б1 | +1 | -1 | 0 |
                +

                Каждая фиксированная стратегия (в данном случае выбор действия — камень, ножницы или бумага), которую может выбрать игрок, называется его чистой стратегией.

                +

                Смешанной стратегией игрока называется набор чистых стратегий, задаваемый вероятностями (относительными частотами) выбора соответствующих чистых стратегий. Задача обоих игроков — выбрать смешанную стратегию, максимизирующую свой выигрыш.

                +

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

                +

                Обозначим смешанную стратегию первого игрока за \(x\), а второго игрока за \(y\). Тогда ожидаемый счёт можно записать так:

                +

                \[ +V = xAy^T +\]

                +

                Действительно, эта формула раскрывается в \(\sum A_{ij} \cdot x(i) \cdot y(j)\).

                +

                Минимаксная теорема. Первый игрок хочет минимизировать это значение, а второй — максимизировать. Обозначим за \(V_a\) и \(V_b\) оптимальные значения, которые удаётся достичь игрокам вне зависимости от действий оппонента, то есть:

                +

                \[ +V_a = \min_x \max_y xAy^T +\\ V_b = \max_y \min_x xAy^T +\]

                +

                Утверждение: \(V_a = V_b\).

                +

                Доказательство. Запишем матрицу \(A'\), приписав к \(A\) справа единичную матрицу:

                +

                Будем доказывать от противного: покажем, что ситуация \(V_a < 0 < V_b\) невозможна, а значит для любого \(t\) ситуация невозможна.

                +

                Рассмотрим выпуклую оболочку \(C\) её столбцов.

                +

                Предположим, что \(\vec{0} \in C\).

                +

                Тогда существуют \(z_1, z_2, \ldots z_{n+m}\) такие что \(0 \leq z_i \leq 1\) и \(\sum z_i = 1\) (то есть вероятностное распределение).

                +

                $sum_{j=1}^n a_{ij} z_{ij} $

                +

                В этой игре оптимальной

                +

                Несимметричные игры

                +

                Минимаксная теорема

                +

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

                +

                Пусть имеется матрица \(A\) размера \(n \times m\). Задача

                + + diff --git a/ru/pollard.html b/ru/pollard.html index 5034467..3a4c854 100644 --- a/ru/pollard.html +++ b/ru/pollard.html @@ -4,7 +4,7 @@ - Факторизация за $O(\sqrt[4]{n})$ + Ро-алгоритм Полларда + + + + + + + + + + + +
                +

                Игры с неполной информацией

                +
                +

                Игры с неполной информацией

                +

                Рассмотрим следующую ситуацию, известную как дилемма заключённого:

                +
                +

                Двое преступников — А и Б — попались примерно в одно и то же время на сходных преступлениях. Полиция предполагает, что они действовали по сговору, и, изолировав их друг от друга, предлагает им одну и ту же сделку:

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

                • +
                • Если оба молчат, их деяние проходит по более лёгкой статье, и каждый из них приговаривается к году тюрьмы.

                • +
                • Если оба свидетельствуют друг против друга, они получают по 2 года.

                • +
                +

                Каждый заключённый выбирает, молчать или свидетельствовать против другого. Однако ни один из них не знает точно, что сделает другой. Что произойдёт?

                +
                +

                Игры, в которых у игроков (в данном случае — узников) есть некоторая неопределенность относительно действий других игроков (например, из-за одновременности действий), называются играми с неполной информацией.

                +

                Данную «игру» можно представить в виде следующей табилцы:

                +
                                                   | Б хранит молчание | Б даёт показания |
                +А хранит молчание | (1, 1) | (10, 0) |
                +А даёт показания | (0, 10) | (2, 2) |
                +

                Пары чисел в ячейках означают сроки, которые получают заключенные А и Б соответственно.

                +

                Предположим, что оба преступника заботятся только о минимизации собственного срока заключения. Тогда рассуждения каждого узника будут следующими:

                +
                  +
                • Если партнёр молчит, то лучше его предать и выйти на свободу (иначе — полгода тюрьмы).

                • +
                • Если партнёр свидетельствует, то лучше тоже свидетельствовать против него, чтобы получить 2 года (иначе — 10 лет) тюрьмы.

                • +
                +

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

                +

                С точки зрения группы лучше всего хранить молчание и получить по году, так как это уменьшит срок заключения каждого. Такие состояния называются Парето-эффективными, но, как мы видим, они не всегда являются стабильными.

                +

                Игры с нулевой суммой

                +

                Рассмотрим другой пример игры с неполной информацией: «камень-ножницы-бумага». Эта игра в каком-то смысле более «человечная»: игроки явно соревнуются друг с другом, и им не нужно делать сделки с совестью и предавать друг друга.

                +

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

                +

                Примечание. Дилемма заключенного — пример игры с ненулевой суммой.

                +

                В играх с нулевой суммой исходы можно записывать разницей между выгодой первого и второго игрока:

                +
                    | К2 | Н2 | Б2|
                +К1 | 0 | +1 | -1|
                +Н1 | -1 | 0 | +1|
                +Б1 | +1 | -1 | 0 |
                +

                Каждая фиксированная стратегия (в данном случае выбор действия — камень, ножницы или бумага), которую может выбрать игрок, называется его чистой стратегией.

                +

                Смешанной стратегией игрока называется набор чистых стратегий, задаваемый вероятностями (относительными частотами) выбора соответствующих чистых стратегий. Задача обоих игроков — выбрать смешанную стратегию, максимизирующую свой выигрыш.

                +

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

                +

                Обозначим смешанную стратегию первого игрока за \(x\), а второго игрока за \(y\). Тогда ожидаемый счёт можно записать так:

                +

                \[ +V = xAy^T +\]

                +

                Действительно, эта формула раскрывается в \(\sum A_{ij} \cdot x(i) \cdot y(j)\).

                +

                Минимаксная теорема. Первый игрок хочет минимизировать это значение, а второй — максимизировать. Обозначим за \(V_a\) и \(V_b\) оптимальные значения, которые удаётся достичь игрокам вне зависимости от действий оппонента, то есть:

                +

                \[ +V_a = \min_x \max_y xAy^T +\\ V_b = \max_y \min_x xAy^T +\]

                +

                Утверждение: \(V_a = V_b\).

                +

                Доказательство. Запишем матрицу \(A'\), приписав к \(A\) справа единичную матрицу:

                +

                Будем доказывать от противного: покажем, что ситуация \(V_a < 0 < V_b\) невозможна, а значит для любого \(t\) ситуация невозможна.

                +

                Рассмотрим выпуклую оболочку \(C\) её столбцов.

                +

                Предположим, что \(\vec{0} \in C\).

                +

                Тогда существуют \(z_1, z_2, \ldots z_{n+m}\) такие что \(0 \leq z_i \leq 1\) и \(\sum z_i = 1\) (то есть вероятностное распределение).

                +

                $sum_{j=1}^n a_{ij} z_{ij} $

                +

                В этой игре оптимальной

                +

                Несимметричные игры

                +

                Минимаксная теорема

                +

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

                +

                Пусть имеется матрица \(A\) размера \(n \times m\). Задача

                + + From 24c26dd6ebbbe72e077931923821b758ce88b6f1 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 12 Jun 2019 18:34:50 +0000 Subject: [PATCH 059/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/palindromes.html | 6 +- ru/suffix_automaton.html | 116 --------------------------------------- 2 files changed, 4 insertions(+), 118 deletions(-) delete mode 100644 ru/suffix_automaton.html diff --git a/ru/palindromes.html b/ru/palindromes.html index ac691e7..653925e 100644 --- a/ru/palindromes.html +++ b/ru/palindromes.html @@ -4,7 +4,7 @@ - palindromes + Алгоритм Ахо-Корасик - - - - - - - - - - - -

                Введение

                -

                Рассмотрим задачу поиска образцов в тексте. Дан текст \(T\) и \(n\) образцов \(S_i\), для каждого из образцов нужно узнать, встречается ли он в тексте. Есть два основных подхода к решению этой задачи. Первый — алгоритм Ахо-Корасик, который строит по набору образцов автомат, распознающий строки, в которых встречаются данные образцы. Второй — использование суффиксных структур, под которыми обычно подразумевают суффиксный массив, суффиксный автомат или суффиксное дерево. Данная лекция по большей части будет посвящена последним двум из них, их связи и применении в решении задач.

                -

                Наивное решение

                -

                Возьмём все суффиксы \(T\) и объединим их в бор, в нём каждой подстроке \(T\) будет соответствовать ровно одна вершина. Теперь мы можем за \(O(|T|^2)\) времени на построение бора и \(O(|S_i|)\) на запрос узнавать, входит ли \(S_i\) в \(T\). От квадратичной сложности можно уйти двумя способами, ведущими к суффиксному дереву и суффиксному автомату соответственно.

                - -
                -Суффиксное дерево cbcbba
                Суффиксное дерево \(cbcbba\)
                -
                - -
                -Суффиксное дерево cbcbba
                Суффиксное дерево \(cbcbba\)
                -
                -

                Суффиксное дерево

                -

                Любой нисходящий путь в этом боре будет подстрокой \(T\), значит если из вершины \(v\) нет других исходящих рёбер, можно заменить путь \(u \topto{\al} v \topto{\bb} w\) на ребро \(u \topto{\al \bb} w\), храня при этом строку \(\al \bb\) в виде пары чисел \([l;r]\), указывающей на её местоположение в \(T\). Такая сжатая структура называется суффиксным деревом и занимает \(O(|T|)\) памяти.

                -

                Суффиксный автомат

                -

                Детерминированный конечный автомат (далее автомат) — это пятёрка \(A = (Q, \Sigma, \delta, q_0, F)\), где \(Q\) — множество состояний, \(\Sigma\) — алфавит, \(\delta \subset Q \times \Sigma \times Q\) — множество переходов, \(q_0 \in Q\) — начальное состояние, \(F \subset Q\) — множество финальных состояний.

                -

                Всякому автомату можно сопоставить ориентированный граф такой что состояния автомата — это вершины графа, а переходы — это дуги помеченные символами из алфавита. Соответственно, в таком графе должна быть выделена некоторая вершина \(q_0\) (начальное состояние) и набор вершин \(F\) (финальные состояния). Кроме того, из любого состояния не может быть двух переходов по одному и тому же символу (детерминированность).

                -

                Состояние \(q\) принимает строку \(s\), если есть путь из \(q_0\) в \(q\) такой что если выписать все символы, которые мы встретили на этом пути, мы получим строку \(s\). Автомат принимает строку \(s\), если её принимает хотя бы одно из финальных состояний. Множество строк принимаемых автоматом будем называть его языком.

                -

                Суффиксным автоматом строки \(s\) будем называть минимальный автомат, который принимает все суффиксы строки и только их. Под минимальностью подразумевается, что число состояний в нём должно быть наименьшим возможным.

                -

                Суффиксный автомат не только ориентированный, но и ациклический граф, ведь множество принимаемых им строк конечно, а цикл позволил бы принять сколь угодно длинную строку. Рассмотрим факты, позволяющие нам уточнить природу суффиксного автомата.

                -

                Пусть \(a\) и \(b\) — строки, которые принимает состояние \(q\) некоторого автомата \(A\). Тогда для любой строки \(x\) строки \(ax\) и \(bx\) принимаются или не принимаются \(A\) одновременно.

                -

                Независимо от того, как мы “пришли” в состояние \(q\), если мы пройдём из него по пути, соответствующему строке \(x\), мы сможем точно сказать, в какое состояние мы попадём и, в частности, будет ли оно финальным.

                -

                Правым контекстом \(X(q)\) состояния \(q\) назовём множество строк \(x\), переводящих \(q\) в одно из финальных состояний. Соответственно, назовём правым контекстом строки \(a\) множество \(X(a)\) строк \(x\) таких что \(ax\) принимается автоматом.

                -

                Правый контекст строки совпадает с правым контекстом состояния, которое её принимает, поэтому правые контексты языка можно рассматривать независимо от конкретного автомата:

                -

                Рассмотрим множество \(R\) непустых правых контекстов всех возможных слов над \(\Sigma\). В любом автомате, задающем данный язык будет хотя бы \(|R|\) состояние.

                -

                Рассмотрим любой правый контекст в \(R\). Пусть он порождён строкой \(a\), тогда в автомате должно быть некоторое состояние \(q\), принимающее строку \(a\) и имеющее такой правый контекст, иначе автомат бы не смог принять строку \(ax\). При этом все эти состояния будут различными, так как у них будут различный правые контексты.

                -

                Существует автомат, на котором эта оценка достигается.

                -

                Если в автомате есть состояние с пустым правым контекстом, мы можем удалить его, не изменим принимаемый язык. Иначе пусть в автомате есть два состояния \(q_1\), \(q_2\) такие что \(X(q_1) = X(q_2)\). Мы можем удалить состояние \(q_2\) и перевести переходы, ведущие в него в состояние \(q_1\). Множество принимаемых строк от этого не изменится, следовательно, мы можем продолжать эту процедуру, пока число состояний не будет равно числу различных непустных правых контекстов.

                -

                Таким образом, мы доказали:

                -

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

                -

                В случае суффиксного автомата правый контекст \(X(a)\) строки \(a\) взаимно однозначно соответствует множеству правых позиций вхождений строки \(a\) в строку \(s\). Действительно, если \(ax\) принимается автоматом, то есть, является суффиксом, то \(s = yax\), а строке \(x\) мы можем сопоставить позицию \(|s|-|x|-1\). Таким образом, каждое состояние автомата принимает строки с одинаковым множеством правых позиций их вхождений и обратно, все строки с таким множеством позиций принимаются этим состоянием.

                -

                Связь между суффиксным автоматом и суффиксным деревом

                -

                Рассмотрим ребро в суффиксном дереве строки \(T\), а точнее все подстроки \(T\), которым соответствует или “внутренняя” вершина ребра, или вершина, в которой ребро заканчивается. Для любой строки \(x\) и любой пары строк \(a\), \(b\) из рассматриваемого множества строк, строки \(xa\) и \(xb\) являются или не являются префиксами строки \(s\) одновременно.

                -

                Пусть \(|a| < |b|\). Тогда \(a\) является предком \(b\) в боре, то есть, её префиксом, значит, её множество вхождений точно содержит множество вхождений строки \(b\). Допустим, существует позиция \(|x|\), в которой есть вхождение строки \(a\), но не строки \(b\).

                -

                Рассмотрим строку \(a'\), которая является максимальным префиксом строки \(b\), который можно встретить в той позиции. Если \(a'\) не упирается в конец строки, то её можно продолжить, как минимум, двумя различными символами чтобы она осталась подстрокой \(T\). Значит, соответствующая ей вершина в дереве имеет степень больше двух и должна разбивать ребро, на котором находится, что конфликтует с предположением о том, что \(a\) и \(b\) взяты с одного ребра.

                -

                Если же \(a'\) нельзя продолжить, то она всё ещё должна разбивать ребро, т.к. является суффиксом строки и её вершина не будет удалена при сжатии рёбер.

                -

                Аналогичным образом можно показать, что для любой строки \(a\) все строки \(b\) с таким же множеством строк \(x\) находятся на соответствующем строке \(a\) ребре, то есть, есть биекция между рёбрами суффиксного дерева и множествами левых позиций вхождений строк в \(T\). Отсюда:

                -

                Для любого состояния \(q\) суффиксного автомата строки \(T\) найдётся вершина \(q'\) суффиксного дерева развернутой строки \(T\) такая, что множество строк, принимаемых состоянием \(q\), совпадает с развёрнутым множеством строк, таких что соответствующая им вершина в дереве лежит на ребре, ведущем в \(q'\) (включая строку, соответствующую \(q'\)).

                -

                Это целиком описывает состояния автомата и позволяет разработать алгоритм его построения.

                -

                Построение суффиксного автомата

                -

                Пусть длина самой короткой строки, которая принимается состоянием \(q\) равна \(k\). Тогда суффиксная ссылка \(link(q)\) будет вести из этого состояния в состояние, которое принимает эту же строку без первого символа.

                -

                Обратившись к суффиксному дереву мы поймём, что в нём она будет вести в предка вершины \(q'\). Таким образом, суффиксные ссылки образуют дерево, которое соответствует суффиксному дереву развернутой строки. Также будем обозначать длину самой длинной строки, которая принимается состоянием \(q\) как \(len(q)\). Длина самой короткой строки из \(q\) будет равна \(len(link(q)) + 1\).

                -

                Будем дописывать символы в конец строки \(T\) по одному и при этом поддерживать для неё корректный автомат с деревом суффиксных ссылок. Пусть у нас есть автомат для строки \(T\) и она принимается его состоянием \(last\). Мы хотим получить автомат для строки \(Tc\). Нам нужно, чтобы для каждого суффикса новой строки существовало состояние, которое его примет. При этом нам нужно сохранить минимальность автомата.

                -

                Добавим состояние, которое принимает всю строку \(Tc\) и назовём его \(new\). Правый контекст \(Tc\) состоит из единственной строки — пустой, значит, в \(new\) будут входить те и только те суффиксы, которые мы встретили в строке впервые. Все такие строки можно получить дописыванием символа \(c\) к суффиксам \(T\), которые принимаются состоянием, из которого ещё нет перехода по данному символу. Таким образом, чтобы новые суффиксы принимались, нам необходимо будет “попрыгать” по суффиксным ссылкам и добавить переходы по символу \(c\) в состояние \(new\), пока не придём в корень или не обнаружим, что переход по символу \(c\) из состояния уже есть.

                -

                Если мы пришли в корень, значит, все непустые суффиксы строки \(Tc\) принимаются состоянием \(new\) и мы можем положить \(link(new) = q_0\) и завершить работу.

                -

                Иначе мы нашли состояние \(q'\), из которого переход по символу \(c\) уже есть. Значит, суффиксы длины \(\leq len(q') + 1\) уже встречались в строке, и новых переходов в состояние \(new\) мы проводить не будем. Однако, для состояния \(new\) ещё нужно посчитать суффиксную ссылку. Наибольшей строкой в ней будет суффикс строки \(Tc\) длины \(len(q') + 1\). В данный момент он находится в состоянии \(t\), в которое ведёт переход по символу \(c\) из состояния \(q'\), но в нём могуть быть также строки большей длины. Таким образом, если \(len(t) = len(q') + 1\), то \(t\) и есть искомая суффиксная ссылка. Проведя её, мы завершим обновление автомата.

                -

                Иначе \(t\) — состояние, которое принимает как строки, являющиеся суффиксами строки, так и строки, которые ими не являются, из-за этого мы не можем корректно определить его финальность. Чтобы решить данный конфликт мы должны будем отщепить от \(t\) состояние \(t'\), которое примет все строки, которые принимаются \(t\), но имеют длину \(\leq len(q') + 1\), то есть, тот самый кусок с суффиксами. Для этого скопируем в \(t'\) переходы и суффиксную ссылку из \(t\), а длину установим равной \(len(q') + 1\). Затем установим \(link(new) = link(t) = t'\). Наконец, чтобы “перебросить” в него нужные строки из \(t\), пройдёмся по суффиксным ссылкам состояния \(q'\) пока переходы по \(c\) ведут в \(t\) и переправим эти переходы в \(t'\). Таким образом, мы перенаправим все пути интересующей нас длины, которые ранее проходили через вершину \(t\) в вершину \(t'\).

                -

                В некотором смысле эта процедура соответствует построению суффиксного дерева добавлением суффиксов в возрастающем (по длине) порядке, приведём код на языке C++, выполняющий её:

                -

                ``` {.c++ language=“C++”} const int maxn = 2e5 + 42; // Максимальное число состояний map<char, int> to[maxn]; // Переходы int link[maxn]; // Суффиксные ссылки int len[maxn]; // Длины максимальных строк в состояниях int last = 0; // Состояние, соответствующее всей строке int sz = 1; // Общее число состояний

                -

                void add_letter(char c) // Дописываем символ в конец { int p = last; // Записываем в p состояние строки s last = sz++; // Создаём для строки sc новое состояние len[last] = len[p] + 1; for(; to[p][c] == 0; p = link[p]) // (1) to[p][c] = last; // Прыгаем по ссылкам, создавая переходы if(to[p][c] == last) { // Если мы оказались здесь, то символ c встречен впервые link[last] = 0; return; } int q = to[p][c]; if(len[q] == len[p] + 1) { // Если переход сплошной, то q - суфф ссылка link[last] = q; return; } // Расщепляем q на два состояния, одно из которых cl, // А второе получит тот же номер, что и q имело ранее int cl = sz++; to[cl] = to[q]; // (2) link[cl] = link[q]; len[cl] = len[p] + 1; link[last] = link[q] = cl; for(; to[p][c] == q; p = link[p]) // (3) to[p][c] = cl; // Перенаправляем переходы там, где нужно } ```

                -

                Время работы

                -

                Покажем линейность работы данного алгоритма. На каждом шаге есть три места, которые выполняются не за \(O(1)\):

                -
                  -
                1. Прыжки по ссылкам \(last\) для создания переходов в \(new\).

                2. -
                3. Копирование переходов из \(t\) в \(t'\).

                4. -
                5. Прыжки по ссылкам \(q'\) для перенаправления переходов из \(t\) в \(t'\).

                6. -
                -

                В первых двух пунктах происходит создание очередного перехода в автомате. Покажем, что всего переходов будет \(O(n)\). Разделим все переходы \(\delta(v, c) = u\) из \(v\) в \(u\) по символу \(c\) на два класса — “сплошные”, для которых \(len(v) + 1 = len(u)\) и все остальные.

                -

                Т.к. в каждое состояние, кроме начального, ведёт ровно один сплошной переход, сплошных переходов будет не больше, чем состояний, а их \(O(n)\).

                -

                Рассмотрим теперь несплошные переходы. Каждому такому переходу можно поставить в соответствие строку \(acb\), где \(a\) — длиннейшая строка, которую принимает состояние \(v\), а \(b\) — длиннейшая строка, которую можно вывести из состояния \(u\). Данная строка является суффиксом \(s\) (иначе мы могли бы продлить \(b\) вправо). Кроме того \(|a| = len(v)\), отсюда можно сделать вывод, что \(|a|\) составлена исключительно из сплошных переходов. Значит, по произвольному суффиксу мы можем определить рассмотренный несплошной переход, как первый, который встретим, “скармливая” его строке. Значит, такое отображение взаимооднозначное и сплошных переходов не больше, чем суффиксов в строке. Отсюда следует, что суммарно переходов в автомате \(O(n)\).

                -

                Наконец, докажем линейность третьего пункта. Для удобства назовём \(link(link(q))\) второй суффиксной ссылкой состояния \(q\). Также будем использовать такие обозначения: \(last\) — состояние, соответствующее строке \(s\), \(new\) — состояние строки \(sc\), \(p\) — состояние, которое “прыгает” в циклах (можно видеть в коде). Т.к. \(sc\) не могла встречаться в суффиксном автомате \(s\), из состояния \(last\) изначально нет перехода по \(c\), поэтому в цикле (1) мы сделаем хотя бы один шаг. Отсюда \(len(link(p)) \leq len(link(link(last)))\) (действительно, изначально \(p = last\), после первой итерации \(p = link(last) \rightarrow len(link(p)) = len(link(link(last)))\), на всех последующих итерациях длина \(len(link(p))\) не увеличивается, значит, упомянутое неравенство верно.

                -

                Когда мы вышли из цикла (1), мы имеем \(\delta(p, c) = q\). Очевидно, если есть переход из состояния \(p\) в состояние \(q\), то если мы допишем символ \(c\) к самой короткой строке из \(p\), длина которой равна \(len(link(p)) + 1\), то получим строку, которая будет не короче, чем самая короткая строка, принимаемая состоянием \(q\), длина которой равна \(len(link(q)) + 1\). То есть, \(len(link(p)) + 2 \geq len(link(q)) + 1 \rightarrow len(link(p)) + 1 \geq len(link(q))\).

                -

                После выхода из цикла (1) куда бы мы ни поставили суффиксную ссылку из \(new\), вторая суффиксная ссылка точно будет \(link(q)\). Отсюда \(link(q) = link(link(new)) \rightarrow len(link(q)) = len(link(link(new)))\).

                -

                Теперь посмотрим на цикл (3). Он будет выполняться пока \(\delta(p, c) = q\), то есть, как было упомянуто выше \(len(link(p)) + 1 \geq len(link(q))\). При этом изначально \(len(link(link(last))) \geq len(link(p))\). Так как \(len(link(q)) = len(link(link(new)))\), а также на каждом шаге \(len(link(p))\) уменьшается, получаем, что весь цикл отработает не дольше, чем за \(len(link(link(last))) - len(link(link(new)))\), то есть, за разность максимальных длин, принимаемых вторыми суффиксными ссылками состояний, соответствующих всей строке.

                -

                Наконец, собирая всё полученное воедино, получим такое неравенство: \(len(link(link(last))) + 1 \geq len(link(p)) + 1 \geq len(link(q)) = len(link(link(new)),\) отсюда \(len(link(link(last))) + 1 \geq len(link(link(new)))\), которое означает, что на каждом шаге длина второй суффиксной ссылки либо уменьшилась, либо увеличилась не больше, чем на единицу (а значит, её суммарное уменьшение не превосходит \(O(n)\)). Линейность алгоритма доказана!

                -

                Применение в решении задач

                -
                  -
                1. Число различных подстрок. Дана строка \(s\), необходимо посчитать количество её различных подстрок. В каждом состоянии встречаются строки длины от \(len(link(q)) + 1\) до \(len(q)\). Всего \(len(q) - len(link(q))\) строк. Просуммировав эту величину по всем состояниям, получим ответ.

                  -

                  Упражнение: решите эту же задачу за \(O(n)\), учитывая, что к строке \(s\) символы дописываются по одному и после каждого нового символа необходимо сказать текущее число различных подстрок строки \(s\).

                  -

                  Упражнение*: Возьмём задачу из предыдущего упражнения и скажем, что теперь мы можем не только дописывать символы в конец, но и удалять их с начала строки. Вам требуется отвечать на те же запросы. Время работы решения всё ещё должно линейно зависеть от размера входа. Подсказка: иногда алгоритм Укконена также бывает полезен.

                2. -
                3. Поиск подстрок в тексте. Пропустив строку через автомат мы сможем сказать, входит ли она в текст. Допустим, мы хотим узнать какую-то информацию о её вхождениях. Например, нам нужно выдать любое конкретное вхождение. Как мы уже знаем, каждому вхождению соответствует строка \(x\) такая что \(ax\) — суффикс \(s\). Или, проще говоря, путь из состояния \(q\) в какое-то финальное состояние. Динамикой по автомату как ациклическому ориентированному графу мы можем найти длину какого-нибудь такого пути (например, для определённости минимального или максимального). Отметим, что аналогичной динамикой считаются многие другие полезные значения, например, количество строк в правом контексте состояния (или, что то же самое, количество вхождений строк из состояния в \(s\)).

                  -

                  Альтернативным решением будет обратиться к дереву суффиксных ссылок, которое, как мы помним, является суффиксным деревом для \(s^T\). Как мы упоминали в самом начале, любая подстрока строки \(s\) является префиксом одного из суффиксов исходной строки. Таким образом, если мы запишем в каждую “суффиксную” вершину индекс соответствующего ей суффикса, то все позиции вхождений строки \(t\) в \(s\) можно будет обнаружить в поддереве вершины, которая соответствует строке \(t\). Значит, в частности, динамикой можно будет найти самое первое или самое последнее вхождение.

                  -

                  Более того, учитывая, что в последнем случае мы работали с деревом, мы можем обойти его таким образом, чтобы на каждом шаге иметь в вершине множество возможных позиций, в которых встречаются строки из соответствующего состояния. Для этого нужно применить идею быстрого слияния множеств, когда мы всегда добавляем элементы из меньшего множества в большее, а не наоборот. Тогда такой обход потребует \(O(n \log n)\) операций добавления в множество, т.к. каждый раз когда мы переносим между множествами элемент \(k\), размер нового множества будет как минимум, в два раза больше старого, в котором он хранился.

                  -

                  Упражнение*: дана строка \(s\). Найти число строк \(t\) таких, что они имеют хотя бы \(3\) непересекающихся вхождения в строку \(s\).

                4. -
                5. Наибольшая общая подстрока. Нам дано \(k\) строк \(s_1, s_2, \dots, s_k\). Нужно найти наибольшую строку \(t\), которая встречается в каждой из строк \(s_i\). Одно из возможных решений — построить автомат для строки \(s_1 t_1 s_2 t_2 \dots s_n t_n\), где \(t_i\) — уникальный для каждой строки символ-разделитель. Теперь мы можем завести динамику \(dp[q][i]\), в которой хранить \(1\), если из состояния \(q\) можно добраться до состояния, из которого есть переход по \(t_i\), не проходя при этом через другие символы-разделители. Это будет равносильно тому, что строки из \(q\) входят в \(s_i\). Как и в прошлый раз, динамику можно пересчитывать по топологической сортировке автомата как ориентированного ациклического графа. Итого решение будет работать за \(O(k \cdot \sum |s_i|)\).

                  -

                  Упражнение*: решить указанную задачу за \(O(\sum |s_i|)\).

                6. -
                - - From ea6cb1beb2b5921cb3eeb9b82cff59d38c51c34e Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 12 Jun 2019 19:29:44 +0000 Subject: [PATCH 060/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/index.html | 7 ++ head.html | 7 ++ pandoc.css | 16 +++-- ru/aho-corasick.html | 7 ++ ru/annealing.html | 7 ++ ru/bayans.html | 7 ++ ru/bitset.html | 7 ++ ru/centroid.html | 7 ++ ru/convex-hulls.html | 7 ++ ru/cuda.html | 7 ++ ru/dp-optimizations.html | 7 ++ ru/fenwick.html | 7 ++ ru/fft.html | 7 ++ ru/games.html | 7 ++ ru/geometry.html | 7 ++ ru/hashing.html | 7 ++ ru/hld.html | 7 ++ ru/imperfect-information.html | 7 ++ ru/imperferct-information.html | 7 ++ ru/index.html | 72 +++++++++---------- ru/karatsuba.html | 7 ++ ru/lca.html | 7 ++ ru/linalg.html | 7 ++ ru/matching.html | 7 ++ ru/matroid.html | 7 ++ ru/mincost-maxflow.html | 7 ++ ru/mst.html | 7 ++ ru/palindromes.html | 7 ++ ru/pollard.html | 7 ++ ru/probability.html | 7 ++ ru/reciprocal.html | 7 ++ ru/segtree.html | 7 ++ ru/sparse-table.html | 7 ++ ru/sqrt.html | 7 ++ ru/stress-test.html | 7 ++ ru/strings.html | 7 ++ ru/suffix-array.html | 7 ++ ru/suffix-automaton.html | 126 +++++++++++++++++++++++++++++++++ ru/treap.html | 7 ++ ru/trie.html | 7 ++ 40 files changed, 432 insertions(+), 41 deletions(-) create mode 100644 ru/suffix-automaton.html diff --git a/en/index.html b/en/index.html index 9eac44c..278968b 100644 --- a/en/index.html +++ b/en/index.html @@ -32,6 +32,13 @@ + +
                diff --git a/head.html b/head.html index c4dee70..795ed65 100644 --- a/head.html +++ b/head.html @@ -15,3 +15,10 @@ + + diff --git a/pandoc.css b/pandoc.css index 263ea7e..7ce97ed 100644 --- a/pandoc.css +++ b/pandoc.css @@ -40,10 +40,16 @@ body { .contents { position: inline-block; float: left; - width: 33.3%; + position: relative; + left: 55px; + width: 275px; text-align: left; } +.contents h3 { + +} + .contents ul { padding-left: 22px; margin-top: 5px; @@ -133,11 +139,13 @@ blockquote { hr { display: block; - height: 2px; + width: 100%; + height: 0px; border: 0; - border-top: 1px solid #aaa; + /* border-top: 1px solid #aaa; border-bottom: 1px solid #eee; - margin: 1em 0; + */ + margin: 0 0; padding: 0; } diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html index 593ee53..b2567b3 100644 --- a/ru/aho-corasick.html +++ b/ru/aho-corasick.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/annealing.html b/ru/annealing.html index 37d79e2..5898660 100644 --- a/ru/annealing.html +++ b/ru/annealing.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/bayans.html b/ru/bayans.html index a1510a6..f4a9891 100644 --- a/ru/bayans.html +++ b/ru/bayans.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/bitset.html b/ru/bitset.html index e4fdb1a..941d553 100644 --- a/ru/bitset.html +++ b/ru/bitset.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/centroid.html b/ru/centroid.html index 978980d..24ca801 100644 --- a/ru/centroid.html +++ b/ru/centroid.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/convex-hulls.html b/ru/convex-hulls.html index ea014bf..d59a285 100644 --- a/ru/convex-hulls.html +++ b/ru/convex-hulls.html @@ -33,6 +33,13 @@ + +
                diff --git a/ru/cuda.html b/ru/cuda.html index 6775c0a..e26eb2d 100644 --- a/ru/cuda.html +++ b/ru/cuda.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index 69af5d7..af676e7 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/fenwick.html b/ru/fenwick.html index ef71723..2b036b5 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/fft.html b/ru/fft.html index ce12e27..a3cac42 100644 --- a/ru/fft.html +++ b/ru/fft.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/games.html b/ru/games.html index 308d755..2e7f0c9 100644 --- a/ru/games.html +++ b/ru/games.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/geometry.html b/ru/geometry.html index 332348e..65ad658 100644 --- a/ru/geometry.html +++ b/ru/geometry.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/hashing.html b/ru/hashing.html index 7da2662..7ff1074 100644 --- a/ru/hashing.html +++ b/ru/hashing.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/hld.html b/ru/hld.html index 5dfa7c9..1e75e0c 100644 --- a/ru/hld.html +++ b/ru/hld.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/imperfect-information.html b/ru/imperfect-information.html index fcd7b67..b97f651 100644 --- a/ru/imperfect-information.html +++ b/ru/imperfect-information.html @@ -33,6 +33,13 @@ + +
                diff --git a/ru/imperferct-information.html b/ru/imperferct-information.html index fcd7b67..b97f651 100644 --- a/ru/imperferct-information.html +++ b/ru/imperferct-information.html @@ -33,6 +33,13 @@ + +
                diff --git a/ru/index.html b/ru/index.html index 2b88150..ea3e9ba 100644 --- a/ru/index.html +++ b/ru/index.html @@ -32,6 +32,13 @@ + +
                @@ -52,45 +59,40 @@

                Общие техники

                +
                - @@ -99,42 +101,38 @@

                Строки

                +
                -
                -

                Теория игр

                - -
                -
                -

                Геометрия

                - +
                + diff --git a/ru/karatsuba.html b/ru/karatsuba.html index 501d5d5..d6dd64b 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -33,6 +33,13 @@ + +
                diff --git a/ru/lca.html b/ru/lca.html index f9738ea..d6a2635 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/linalg.html b/ru/linalg.html index 99bbbf4..16e2b56 100644 --- a/ru/linalg.html +++ b/ru/linalg.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/matching.html b/ru/matching.html index 1942b48..4483cd8 100644 --- a/ru/matching.html +++ b/ru/matching.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/matroid.html b/ru/matroid.html index 959779a..5903797 100644 --- a/ru/matroid.html +++ b/ru/matroid.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html index 14a1833..2c9d578 100644 --- a/ru/mincost-maxflow.html +++ b/ru/mincost-maxflow.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/mst.html b/ru/mst.html index aed65ed..f0c78d4 100644 --- a/ru/mst.html +++ b/ru/mst.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/palindromes.html b/ru/palindromes.html index 653925e..2171bd7 100644 --- a/ru/palindromes.html +++ b/ru/palindromes.html @@ -33,6 +33,13 @@ + +
                diff --git a/ru/pollard.html b/ru/pollard.html index 3a4c854..a9d9aeb 100644 --- a/ru/pollard.html +++ b/ru/pollard.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/probability.html b/ru/probability.html index 0ed3bdf..0853129 100644 --- a/ru/probability.html +++ b/ru/probability.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/reciprocal.html b/ru/reciprocal.html index 59ab273..f4072a3 100644 --- a/ru/reciprocal.html +++ b/ru/reciprocal.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/segtree.html b/ru/segtree.html index 19b9ff3..80efe21 100644 --- a/ru/segtree.html +++ b/ru/segtree.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/sparse-table.html b/ru/sparse-table.html index 54581cd..6b60f92 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/sqrt.html b/ru/sqrt.html index 4ad55f8..134e4a0 100644 --- a/ru/sqrt.html +++ b/ru/sqrt.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/stress-test.html b/ru/stress-test.html index 140dffc..ec24a25 100644 --- a/ru/stress-test.html +++ b/ru/stress-test.html @@ -95,6 +95,13 @@ + +
                diff --git a/ru/strings.html b/ru/strings.html index c7eab46..ea3f1a7 100644 --- a/ru/strings.html +++ b/ru/strings.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/suffix-array.html b/ru/suffix-array.html index dc1c2ad..81404e6 100644 --- a/ru/suffix-array.html +++ b/ru/suffix-array.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/suffix-automaton.html b/ru/suffix-automaton.html new file mode 100644 index 0000000..da7ce1c --- /dev/null +++ b/ru/suffix-automaton.html @@ -0,0 +1,126 @@ + + + + + + + Введение {#введение .unnumbered .unnumbered} + + + + + + + + + + + + + + +
                +

                Введение {#введение .unnumbered .unnumbered}

                +
                +

                Введение

                +

                Рассмотрим задачу поиска образцов в тексте. Дан текст \(T\) и \(n\) образцов \(S_i\), для каждого из образцов нужно узнать, встречается ли он в тексте. Есть два основных подхода к решению этой задачи. Первый — алгоритм Ахо-Корасик, который строит по набору образцов автомат, распознающий строки, в которых встречаются данные образцы. Второй — использование суффиксных структур, под которыми обычно подразумевают суффиксный массив, суффиксный автомат или суффиксное дерево. Данная лекция по большей части будет посвящена последним двум из них, их связи и применении в решении задач.

                +

                Наивное решение

                +

                Возьмём все суффиксы \(T\) и объединим их в бор, в нём каждой подстроке \(T\) будет соответствовать ровно одна вершина. Теперь мы можем за \(O(|T|^2)\) времени на построение бора и \(O(|S_i|)\) на запрос узнавать, входит ли \(S_i\) в \(T\). От квадратичной сложности можно уйти двумя способами, ведущими к суффиксному дереву и суффиксному автомату соответственно.

                + +
                +Суффиксное дерево cbcbba
                Суффиксное дерево \(cbcbba\)
                +
                + +
                +Суффиксное дерево cbcbba
                Суффиксное дерево \(cbcbba\)
                +
                +

                Суффиксное дерево

                +

                Любой нисходящий путь в этом боре будет подстрокой \(T\), значит если из вершины \(v\) нет других исходящих рёбер, можно заменить путь \(u \topto{\al} v \topto{\bb} w\) на ребро \(u \topto{\al \bb} w\), храня при этом строку \(\al \bb\) в виде пары чисел \([l;r]\), указывающей на её местоположение в \(T\). Такая сжатая структура называется суффиксным деревом и занимает \(O(|T|)\) памяти.

                +

                Суффиксный автомат

                +

                Детерминированный конечный автомат (далее автомат) — это пятёрка \(A = (Q, \Sigma, \delta, q_0, F)\), где \(Q\) — множество состояний, \(\Sigma\) — алфавит, \(\delta \subset Q \times \Sigma \times Q\) — множество переходов, \(q_0 \in Q\) — начальное состояние, \(F \subset Q\) — множество финальных состояний.

                +

                Всякому автомату можно сопоставить ориентированный граф такой что состояния автомата — это вершины графа, а переходы — это дуги помеченные символами из алфавита. Соответственно, в таком графе должна быть выделена некоторая вершина \(q_0\) (начальное состояние) и набор вершин \(F\) (финальные состояния). Кроме того, из любого состояния не может быть двух переходов по одному и тому же символу (детерминированность).

                +

                Состояние \(q\) принимает строку \(s\), если есть путь из \(q_0\) в \(q\) такой что если выписать все символы, которые мы встретили на этом пути, мы получим строку \(s\). Автомат принимает строку \(s\), если её принимает хотя бы одно из финальных состояний. Множество строк принимаемых автоматом будем называть его языком.

                +

                Суффиксным автоматом строки \(s\) будем называть минимальный автомат, который принимает все суффиксы строки и только их. Под минимальностью подразумевается, что число состояний в нём должно быть наименьшим возможным.

                +

                Суффиксный автомат не только ориентированный, но и ациклический граф, ведь множество принимаемых им строк конечно, а цикл позволил бы принять сколь угодно длинную строку. Рассмотрим факты, позволяющие нам уточнить природу суффиксного автомата.

                +

                Пусть \(a\) и \(b\) — строки, которые принимает состояние \(q\) некоторого автомата \(A\). Тогда для любой строки \(x\) строки \(ax\) и \(bx\) принимаются или не принимаются \(A\) одновременно.

                +

                Независимо от того, как мы “пришли” в состояние \(q\), если мы пройдём из него по пути, соответствующему строке \(x\), мы сможем точно сказать, в какое состояние мы попадём и, в частности, будет ли оно финальным.

                +

                Правым контекстом \(X(q)\) состояния \(q\) назовём множество строк \(x\), переводящих \(q\) в одно из финальных состояний. Соответственно, назовём правым контекстом строки \(a\) множество \(X(a)\) строк \(x\) таких что \(ax\) принимается автоматом.

                +

                Правый контекст строки совпадает с правым контекстом состояния, которое её принимает, поэтому правые контексты языка можно рассматривать независимо от конкретного автомата:

                +

                Рассмотрим множество \(R\) непустых правых контекстов всех возможных слов над \(\Sigma\). В любом автомате, задающем данный язык будет хотя бы \(|R|\) состояние.

                +

                Рассмотрим любой правый контекст в \(R\). Пусть он порождён строкой \(a\), тогда в автомате должно быть некоторое состояние \(q\), принимающее строку \(a\) и имеющее такой правый контекст, иначе автомат бы не смог принять строку \(ax\). При этом все эти состояния будут различными, так как у них будут различный правые контексты.

                +

                Существует автомат, на котором эта оценка достигается.

                +

                Если в автомате есть состояние с пустым правым контекстом, мы можем удалить его, не изменим принимаемый язык. Иначе пусть в автомате есть два состояния \(q_1\), \(q_2\) такие что \(X(q_1) = X(q_2)\). Мы можем удалить состояние \(q_2\) и перевести переходы, ведущие в него в состояние \(q_1\). Множество принимаемых строк от этого не изменится, следовательно, мы можем продолжать эту процедуру, пока число состояний не будет равно числу различных непустных правых контекстов.

                +

                Таким образом, мы доказали:

                +

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

                +

                В случае суффиксного автомата правый контекст \(X(a)\) строки \(a\) взаимно однозначно соответствует множеству правых позиций вхождений строки \(a\) в строку \(s\). Действительно, если \(ax\) принимается автоматом, то есть, является суффиксом, то \(s = yax\), а строке \(x\) мы можем сопоставить позицию \(|s|-|x|-1\). Таким образом, каждое состояние автомата принимает строки с одинаковым множеством правых позиций их вхождений и обратно, все строки с таким множеством позиций принимаются этим состоянием.

                +

                Связь между суффиксным автоматом и суффиксным деревом

                +

                Рассмотрим ребро в суффиксном дереве строки \(T\), а точнее все подстроки \(T\), которым соответствует или “внутренняя” вершина ребра, или вершина, в которой ребро заканчивается. Для любой строки \(x\) и любой пары строк \(a\), \(b\) из рассматриваемого множества строк, строки \(xa\) и \(xb\) являются или не являются префиксами строки \(s\) одновременно.

                +

                Пусть \(|a| < |b|\). Тогда \(a\) является предком \(b\) в боре, то есть, её префиксом, значит, её множество вхождений точно содержит множество вхождений строки \(b\). Допустим, существует позиция \(|x|\), в которой есть вхождение строки \(a\), но не строки \(b\).

                +

                Рассмотрим строку \(a'\), которая является максимальным префиксом строки \(b\), который можно встретить в той позиции. Если \(a'\) не упирается в конец строки, то её можно продолжить, как минимум, двумя различными символами чтобы она осталась подстрокой \(T\). Значит, соответствующая ей вершина в дереве имеет степень больше двух и должна разбивать ребро, на котором находится, что конфликтует с предположением о том, что \(a\) и \(b\) взяты с одного ребра.

                +

                Если же \(a'\) нельзя продолжить, то она всё ещё должна разбивать ребро, т.к. является суффиксом строки и её вершина не будет удалена при сжатии рёбер.

                +

                Аналогичным образом можно показать, что для любой строки \(a\) все строки \(b\) с таким же множеством строк \(x\) находятся на соответствующем строке \(a\) ребре, то есть, есть биекция между рёбрами суффиксного дерева и множествами левых позиций вхождений строк в \(T\). Отсюда:

                +

                Для любого состояния \(q\) суффиксного автомата строки \(T\) найдётся вершина \(q'\) суффиксного дерева развернутой строки \(T\) такая, что множество строк, принимаемых состоянием \(q\), совпадает с развёрнутым множеством строк, таких что соответствующая им вершина в дереве лежит на ребре, ведущем в \(q'\) (включая строку, соответствующую \(q'\)).

                +

                Это целиком описывает состояния автомата и позволяет разработать алгоритм его построения.

                +

                Построение суффиксного автомата

                +

                Пусть длина самой короткой строки, которая принимается состоянием \(q\) равна \(k\). Тогда суффиксная ссылка \(link(q)\) будет вести из этого состояния в состояние, которое принимает эту же строку без первого символа.

                +

                Обратившись к суффиксному дереву мы поймём, что в нём она будет вести в предка вершины \(q'\). Таким образом, суффиксные ссылки образуют дерево, которое соответствует суффиксному дереву развернутой строки. Также будем обозначать длину самой длинной строки, которая принимается состоянием \(q\) как \(len(q)\). Длина самой короткой строки из \(q\) будет равна \(len(link(q)) + 1\).

                +

                Будем дописывать символы в конец строки \(T\) по одному и при этом поддерживать для неё корректный автомат с деревом суффиксных ссылок. Пусть у нас есть автомат для строки \(T\) и она принимается его состоянием \(last\). Мы хотим получить автомат для строки \(Tc\). Нам нужно, чтобы для каждого суффикса новой строки существовало состояние, которое его примет. При этом нам нужно сохранить минимальность автомата.

                +

                Добавим состояние, которое принимает всю строку \(Tc\) и назовём его \(new\). Правый контекст \(Tc\) состоит из единственной строки — пустой, значит, в \(new\) будут входить те и только те суффиксы, которые мы встретили в строке впервые. Все такие строки можно получить дописыванием символа \(c\) к суффиксам \(T\), которые принимаются состоянием, из которого ещё нет перехода по данному символу. Таким образом, чтобы новые суффиксы принимались, нам необходимо будет “попрыгать” по суффиксным ссылкам и добавить переходы по символу \(c\) в состояние \(new\), пока не придём в корень или не обнаружим, что переход по символу \(c\) из состояния уже есть.

                +

                Если мы пришли в корень, значит, все непустые суффиксы строки \(Tc\) принимаются состоянием \(new\) и мы можем положить \(link(new) = q_0\) и завершить работу.

                +

                Иначе мы нашли состояние \(q'\), из которого переход по символу \(c\) уже есть. Значит, суффиксы длины \(\leq len(q') + 1\) уже встречались в строке, и новых переходов в состояние \(new\) мы проводить не будем. Однако, для состояния \(new\) ещё нужно посчитать суффиксную ссылку. Наибольшей строкой в ней будет суффикс строки \(Tc\) длины \(len(q') + 1\). В данный момент он находится в состоянии \(t\), в которое ведёт переход по символу \(c\) из состояния \(q'\), но в нём могуть быть также строки большей длины. Таким образом, если \(len(t) = len(q') + 1\), то \(t\) и есть искомая суффиксная ссылка. Проведя её, мы завершим обновление автомата.

                +

                Иначе \(t\) — состояние, которое принимает как строки, являющиеся суффиксами строки, так и строки, которые ими не являются, из-за этого мы не можем корректно определить его финальность. Чтобы решить данный конфликт мы должны будем отщепить от \(t\) состояние \(t'\), которое примет все строки, которые принимаются \(t\), но имеют длину \(\leq len(q') + 1\), то есть, тот самый кусок с суффиксами. Для этого скопируем в \(t'\) переходы и суффиксную ссылку из \(t\), а длину установим равной \(len(q') + 1\). Затем установим \(link(new) = link(t) = t'\). Наконец, чтобы “перебросить” в него нужные строки из \(t\), пройдёмся по суффиксным ссылкам состояния \(q'\) пока переходы по \(c\) ведут в \(t\) и переправим эти переходы в \(t'\). Таким образом, мы перенаправим все пути интересующей нас длины, которые ранее проходили через вершину \(t\) в вершину \(t'\).

                +

                В некотором смысле эта процедура соответствует построению суффиксного дерева добавлением суффиксов в возрастающем (по длине) порядке, приведём код на языке C++, выполняющий её:

                +

                ``` {.c++ language=“C++”} const int maxn = 2e5 + 42; // Максимальное число состояний map<char, int> to[maxn]; // Переходы int link[maxn]; // Суффиксные ссылки int len[maxn]; // Длины максимальных строк в состояниях int last = 0; // Состояние, соответствующее всей строке int sz = 1; // Общее число состояний

                +

                void add_letter(char c) // Дописываем символ в конец { int p = last; // Записываем в p состояние строки s last = sz++; // Создаём для строки sc новое состояние len[last] = len[p] + 1; for(; to[p][c] == 0; p = link[p]) // (1) to[p][c] = last; // Прыгаем по ссылкам, создавая переходы if(to[p][c] == last) { // Если мы оказались здесь, то символ c встречен впервые link[last] = 0; return; } int q = to[p][c]; if(len[q] == len[p] + 1) { // Если переход сплошной, то q - суфф ссылка link[last] = q; return; } // Расщепляем q на два состояния, одно из которых cl, // А второе получит тот же номер, что и q имело ранее int cl = sz++; to[cl] = to[q]; // (2) link[cl] = link[q]; len[cl] = len[p] + 1; link[last] = link[q] = cl; for(; to[p][c] == q; p = link[p]) // (3) to[p][c] = cl; // Перенаправляем переходы там, где нужно } ```

                +

                Время работы

                +

                Покажем линейность работы данного алгоритма. На каждом шаге есть три места, которые выполняются не за \(O(1)\):

                +
                  +
                1. Прыжки по ссылкам \(last\) для создания переходов в \(new\).

                2. +
                3. Копирование переходов из \(t\) в \(t'\).

                4. +
                5. Прыжки по ссылкам \(q'\) для перенаправления переходов из \(t\) в \(t'\).

                6. +
                +

                В первых двух пунктах происходит создание очередного перехода в автомате. Покажем, что всего переходов будет \(O(n)\). Разделим все переходы \(\delta(v, c) = u\) из \(v\) в \(u\) по символу \(c\) на два класса — “сплошные”, для которых \(len(v) + 1 = len(u)\) и все остальные.

                +

                Т.к. в каждое состояние, кроме начального, ведёт ровно один сплошной переход, сплошных переходов будет не больше, чем состояний, а их \(O(n)\).

                +

                Рассмотрим теперь несплошные переходы. Каждому такому переходу можно поставить в соответствие строку \(acb\), где \(a\) — длиннейшая строка, которую принимает состояние \(v\), а \(b\) — длиннейшая строка, которую можно вывести из состояния \(u\). Данная строка является суффиксом \(s\) (иначе мы могли бы продлить \(b\) вправо). Кроме того \(|a| = len(v)\), отсюда можно сделать вывод, что \(|a|\) составлена исключительно из сплошных переходов. Значит, по произвольному суффиксу мы можем определить рассмотренный несплошной переход, как первый, который встретим, “скармливая” его строке. Значит, такое отображение взаимооднозначное и сплошных переходов не больше, чем суффиксов в строке. Отсюда следует, что суммарно переходов в автомате \(O(n)\).

                +

                Наконец, докажем линейность третьего пункта. Для удобства назовём \(link(link(q))\) второй суффиксной ссылкой состояния \(q\). Также будем использовать такие обозначения: \(last\) — состояние, соответствующее строке \(s\), \(new\) — состояние строки \(sc\), \(p\) — состояние, которое “прыгает” в циклах (можно видеть в коде). Т.к. \(sc\) не могла встречаться в суффиксном автомате \(s\), из состояния \(last\) изначально нет перехода по \(c\), поэтому в цикле (1) мы сделаем хотя бы один шаг. Отсюда \(len(link(p)) \leq len(link(link(last)))\) (действительно, изначально \(p = last\), после первой итерации \(p = link(last) \rightarrow len(link(p)) = len(link(link(last)))\), на всех последующих итерациях длина \(len(link(p))\) не увеличивается, значит, упомянутое неравенство верно.

                +

                Когда мы вышли из цикла (1), мы имеем \(\delta(p, c) = q\). Очевидно, если есть переход из состояния \(p\) в состояние \(q\), то если мы допишем символ \(c\) к самой короткой строке из \(p\), длина которой равна \(len(link(p)) + 1\), то получим строку, которая будет не короче, чем самая короткая строка, принимаемая состоянием \(q\), длина которой равна \(len(link(q)) + 1\). То есть, \(len(link(p)) + 2 \geq len(link(q)) + 1 \rightarrow len(link(p)) + 1 \geq len(link(q))\).

                +

                После выхода из цикла (1) куда бы мы ни поставили суффиксную ссылку из \(new\), вторая суффиксная ссылка точно будет \(link(q)\). Отсюда \(link(q) = link(link(new)) \rightarrow len(link(q)) = len(link(link(new)))\).

                +

                Теперь посмотрим на цикл (3). Он будет выполняться пока \(\delta(p, c) = q\), то есть, как было упомянуто выше \(len(link(p)) + 1 \geq len(link(q))\). При этом изначально \(len(link(link(last))) \geq len(link(p))\). Так как \(len(link(q)) = len(link(link(new)))\), а также на каждом шаге \(len(link(p))\) уменьшается, получаем, что весь цикл отработает не дольше, чем за \(len(link(link(last))) - len(link(link(new)))\), то есть, за разность максимальных длин, принимаемых вторыми суффиксными ссылками состояний, соответствующих всей строке.

                +

                Наконец, собирая всё полученное воедино, получим такое неравенство: \(len(link(link(last))) + 1 \geq len(link(p)) + 1 \geq len(link(q)) = len(link(link(new)),\) отсюда \(len(link(link(last))) + 1 \geq len(link(link(new)))\), которое означает, что на каждом шаге длина второй суффиксной ссылки либо уменьшилась, либо увеличилась не больше, чем на единицу (а значит, её суммарное уменьшение не превосходит \(O(n)\)). Линейность алгоритма доказана!

                +

                Применение в решении задач

                +
                  +
                1. Число различных подстрок. Дана строка \(s\), необходимо посчитать количество её различных подстрок. В каждом состоянии встречаются строки длины от \(len(link(q)) + 1\) до \(len(q)\). Всего \(len(q) - len(link(q))\) строк. Просуммировав эту величину по всем состояниям, получим ответ.

                  +

                  Упражнение: решите эту же задачу за \(O(n)\), учитывая, что к строке \(s\) символы дописываются по одному и после каждого нового символа необходимо сказать текущее число различных подстрок строки \(s\).

                  +

                  Упражнение*: Возьмём задачу из предыдущего упражнения и скажем, что теперь мы можем не только дописывать символы в конец, но и удалять их с начала строки. Вам требуется отвечать на те же запросы. Время работы решения всё ещё должно линейно зависеть от размера входа. Подсказка: иногда алгоритм Укконена также бывает полезен.

                2. +
                3. Поиск подстрок в тексте. Пропустив строку через автомат мы сможем сказать, входит ли она в текст. Допустим, мы хотим узнать какую-то информацию о её вхождениях. Например, нам нужно выдать любое конкретное вхождение. Как мы уже знаем, каждому вхождению соответствует строка \(x\) такая что \(ax\) — суффикс \(s\). Или, проще говоря, путь из состояния \(q\) в какое-то финальное состояние. Динамикой по автомату как ациклическому ориентированному графу мы можем найти длину какого-нибудь такого пути (например, для определённости минимального или максимального). Отметим, что аналогичной динамикой считаются многие другие полезные значения, например, количество строк в правом контексте состояния (или, что то же самое, количество вхождений строк из состояния в \(s\)).

                  +

                  Альтернативным решением будет обратиться к дереву суффиксных ссылок, которое, как мы помним, является суффиксным деревом для \(s^T\). Как мы упоминали в самом начале, любая подстрока строки \(s\) является префиксом одного из суффиксов исходной строки. Таким образом, если мы запишем в каждую “суффиксную” вершину индекс соответствующего ей суффикса, то все позиции вхождений строки \(t\) в \(s\) можно будет обнаружить в поддереве вершины, которая соответствует строке \(t\). Значит, в частности, динамикой можно будет найти самое первое или самое последнее вхождение.

                  +

                  Более того, учитывая, что в последнем случае мы работали с деревом, мы можем обойти его таким образом, чтобы на каждом шаге иметь в вершине множество возможных позиций, в которых встречаются строки из соответствующего состояния. Для этого нужно применить идею быстрого слияния множеств, когда мы всегда добавляем элементы из меньшего множества в большее, а не наоборот. Тогда такой обход потребует \(O(n \log n)\) операций добавления в множество, т.к. каждый раз когда мы переносим между множествами элемент \(k\), размер нового множества будет как минимум, в два раза больше старого, в котором он хранился.

                  +

                  Упражнение*: дана строка \(s\). Найти число строк \(t\) таких, что они имеют хотя бы \(3\) непересекающихся вхождения в строку \(s\).

                4. +
                5. Наибольшая общая подстрока. Нам дано \(k\) строк \(s_1, s_2, \dots, s_k\). Нужно найти наибольшую строку \(t\), которая встречается в каждой из строк \(s_i\). Одно из возможных решений — построить автомат для строки \(s_1 t_1 s_2 t_2 \dots s_n t_n\), где \(t_i\) — уникальный для каждой строки символ-разделитель. Теперь мы можем завести динамику \(dp[q][i]\), в которой хранить \(1\), если из состояния \(q\) можно добраться до состояния, из которого есть переход по \(t_i\), не проходя при этом через другие символы-разделители. Это будет равносильно тому, что строки из \(q\) входят в \(s_i\). Как и в прошлый раз, динамику можно пересчитывать по топологической сортировке автомата как ориентированного ациклического графа. Итого решение будет работать за \(O(k \cdot \sum |s_i|)\).

                  +

                  Упражнение*: решить указанную задачу за \(O(\sum |s_i|)\).

                6. +
                + + diff --git a/ru/treap.html b/ru/treap.html index 8b9ef87..77af76d 100644 --- a/ru/treap.html +++ b/ru/treap.html @@ -96,6 +96,13 @@ + +
                diff --git a/ru/trie.html b/ru/trie.html index c03285a..26649e1 100644 --- a/ru/trie.html +++ b/ru/trie.html @@ -96,6 +96,13 @@ + +
                From 8fb16edac92bf2221d2c61ecd140cdffff7f7c22 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 13 Jun 2019 22:33:35 +0000 Subject: [PATCH 061/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- {ru => en}/cuda.html | 0 ru/fenwick.html | 2 +- ru/geometry.html | 111 +++++++++++++++++++-------------- ru/imperferct-information.html | 107 ------------------------------- 4 files changed, 64 insertions(+), 156 deletions(-) rename {ru => en}/cuda.html (100%) delete mode 100644 ru/imperferct-information.html diff --git a/ru/cuda.html b/en/cuda.html similarity index 100% rename from ru/cuda.html rename to en/cuda.html diff --git a/ru/fenwick.html b/ru/fenwick.html index 2b036b5..8a30b51 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -108,7 +108,7 @@

                Дерево Фенвика

                -
                +

                НЯ!
                Эта статья полна любви и обожания.
                Возможно, стоит добавить ещё больше?

                Дерево Фенвика

                diff --git a/ru/geometry.html b/ru/geometry.html index 65ad658..3fd732a 100644 --- a/ru/geometry.html +++ b/ru/geometry.html @@ -114,35 +114,41 @@

                Вычислитель vector
                vector

                Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь.

                -

                Скалярное произведение (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива формула, которая муторно и чисто технически доказывается:

                -

                \[ a \cdot b = x_a x_b + y_a y_b \]

                +

                Скалярное произведение (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива следующая формула:

                +

                \[ +a \cdot b = x_a x_b + y_a y_b +\]

                +

                Она доказывается муторно и чисто технически, так что мы это делать не будем.

                +

                Геометрически, она равна проекции вектора \(b\) на вектор \(a\), помноженный на длину \(а\):

                dot
                dot

                У него есть полезные свойства:

                • Скалярное произведение симметрично (\(a \cdot b = b \cdot a\)).
                • -
                • Геометрически, это проекция вектора b на вектор a, помноженынй на длину а.
                • Перпендикулярные вектора должны иметь нулевое скалярное произведение.
                • -
                • Если угол острый, то оно положительное
                • -
                • Если угол тупой, то отрицательное
                • +
                • Если угол острый, то скалярное произведение положительное.
                • +
                • Если угол тупой, то скалярное произведение отрицательное.
                -

                Векторное произведение (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:

                -

                \[ a \times b = x_a y_b - y_a x_b \]

                +

                Векторное произведение (англ. cross product) — произведение длин векторов на синус угла между ними, причём знак этого синуса зависит от порядка операндов. Оно тоже удобно выражается в координатах:

                +

                \[ +a \times b = x_a y_b - y_a x_b +\]

                +

                Геометрически, это ориентированный объем параллелограмма, натянутого на вектора \(a\) и \(b\):

                cross
                cross

                Его свойства:

                  -
                • Скалярное произведение антисимметрично (\(a \times b = - (b \times a)\)).
                • -
                • Геометрически, это ориентированный объем параллелограмма, натянутого на эти вектора.
                • +
                • Скалярное произведение антисимметрично: \(a \times b = - (b \times a)\).
                • Коллинеарные вектора должны иметь нулевое векторное произведение.
                • -
                • Если b «слева» от a, то оно положительное.
                • -
                • Если b «справа» — то отрицательное.
                • +
                • Если \(b\) «слева» от \(a\), то векторное произведение положительное.
                • +
                • Если \(b\) «справа» от \(a\), то векторное произведение отрицательное.
                +

                Вообще говоря, векторное произведение определяется не так. Оно определено как вектор такой же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в трёхмерной геометрии и физике, но нам об этом думать не надо.

                Всякие проверки

                -

                Из-за этих классных свойств, почти все проверки можно описать через них, а не уравнениями.

                -

                Принадлежность точки треугольнику. Пусть у нас есть треугольник ABC (заданный против часовой стрелки) и точка P. Тогда она должна лежать слева от всех трёх векторов AB, BC и CA. Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.

                +

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

                +

                Принадлежность точки треугольнику. Пусть у нас есть треугольник \(ABC\) (заданный против часовой стрелки) и точка \(P\). Тогда она должна лежать слева от всех трёх векторов \(AB\), \(BC\) и \(CA\). Это условие задаст пересечение трёх полуплоскостей, которое и будет нужным треугольником.

                \[ \text{P лежит внутри ABC} \iff \begin{cases} (B-A) \times (P-A) \geq 0 \\ @@ -151,23 +157,27 @@

                Всякие проверки

                \end{cases} \]

                Площадь треугольника. Можно пользоваться готовыми формулами, а можно и свойством векторного произведения.

                -

                \[ V = \frac{1}{2} (B-A) \times (C-A) \]

                +

                \[ +V = \frac{1}{2} (B-A) \times (C-A) +\]

                Площадь произвольного многоугольника. Если многоугольник задан последовательностью вершин в каком-то порядке, то можно считать так: для каждого ребра добавим его ориентированную площадь от начала координат. Какие-то слагаемые будут положительными (которые на последнем слое, а какие-то — отрицательными).

                any
                any

                Забудьте о формуле Герона и всегда считайте площади через векторное произведение.

                -

                Кстати, из формулы тря треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Чтобы оставаться в целых числах, иногда имеет смысл умножить все входные данные на 2.

                -

                Проверка на выпуклость. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону (то есть если у нас последовательные точки a, b и c, то \((b-a)\times(c-a) > 0\)).

                +

                Кстати, из формулы для площади треугольника следует, что площадь любой фигуры будет либо целым числом, либо рациональным с двойкой в знаменателе. Часто в в задачах входные данные целочисленные, и, чтобы оставаться в целых числах, когда мы считаем какую-нибудь площадь, иногда имеет смысл умножить все входные числа на \(2\) (см. «точность»).

                +

                Проверка на выпуклость. Можно пройтись по сторонам многоугольника и проверять векторным произведением, что мы поворачиваем всегда в одну сторону, то есть для всех последовательных точек \(a\), \(b\), \(c\) проверить, что \((b-a)\times(c-a) > 0\).

                Пересекаются ли отрезки.

                segments
                segments

                Уравнение прямой

                -

                Прямая задается уравнением вида \(Ax + By + C = 0\). Полуплоскость можно задать таким же неравенством.

                +

                Прямую можно задать уравнением вида \(Ax + By + C = 0\). Полуплоскость можно задать таким же неравенством.

                У прямой есть вектор нормали с координатами \((A, B)\). Он перпендиуклярен прямой, а в случае с полуплоскостью \(Ax + By + C \geq 0\) будет указывать в сторону самой полуплоскости.

                Чтобы найти расстояние от точки \((x_0, y_0)\) до прямой \(Ax + By + C = 0\), можно воспользоваться следующей формулой:

                -

                \[ d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} \]

                +

                \[ +d = \frac{|Ax_0+By_0+C|}{\sqrt{A^2+B^2}} +\]

                Точка пересечения. По сути, найти точку пересечения двух прямых — это то же самое, что и найти точку, которая удовлетворяет обоим условиям их уравнений:

                \[ \begin{cases} @@ -185,57 +195,62 @@

                Уравнение прямой

                y = - \frac{A_1 C_2 - A_2 C_1}{A_1 B_2 - A_2 B_1} \]

                Аналогично, \(x = \frac{B_1 C_2 - B_2 C_1}{A_1 B_2 - A_2 B_1}\) (обратите внимание на знаки).

                -

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

                -

                Как это кодить в C++

                -

                Сначала мы создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

                -

                Точка \(\simeq\) вектор. Мы будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её радиус-вектор — вектор из начала координат, ведущий в эту точку. По принятой в математике и физике нотации, будем обозночать вектора как r. Вы можете обозвать их как point, pt, vec — как угодно.

                +

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

                +

                Как это кодить в C++

                +

                Небольшой ликбез по объектно-ориентированному программированию в C++. Создадим класс, который будет отвечать за все операции с точками. В C++ есть два способа это сделать: через struct и через class. Их основное отличие в том, что по умолчанию в class все поля приватные — к ним нет прямого доступа снаружи. Это нужно для дополнительной защиты, чтобы в крупных промышленных проектах никто случайно ничего не поломал, но на олимпиадах это не очень актуально.

                +

                Точка \(\simeq\) вектор. Будем считать точка и вектор это один и тот же объект, так как они оба — это просто пара чисел. Будем сопоставлять точке её радиус-вектор — вектор из начала координат, ведущий в эту точку. По принятой в математике и физике нотации, будем обозночать вектора как r. Вы можете обозвать их как point, pt, vec — как угодно.

                -

                Функция r внутри класса вызывается при инциализации объекта. Она называется конструктор, и её можно указывать разную для разных параметров. Таким образом, r() вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами \((x, y)\).

                -

                Операции над векторами

                +

                Функция r внутри класса вызывается при инциализации объекта. Её называют конструктором, и её можно указывать разную для разных параметров. Здесь r()вернёт точку с неопределенными (какие оказались в памяти в тот момент) координатами, а r(x, y) вернет точку с координатами \((x, y)\).

                +

                Операции над векторами

                Давайте напишем функцию, которая принимает вектора и что-то с ними делает. Например, считает длину:

                - -

                Операторы

                + +

                Операторы

                В C++ можно перегружать почти все стандартные операторы, например, +, -, << и т. д.

                -

                Давайте для будущих нужд определим + и -:

                - -

                Скалярное произведение

                - -

                Векторное произведение

                -

                Формально оно определяется не так. Оно определяется как вектор той же длины, но перпендикулярный обоим исходным векторам. Это имеет применение в 3d геометрии (ещё не разу не встречавшейся на школьных олимпиадах) и физике.

                - +

                Переобрадилим для будущих нужд + и -:

                + +

                Скалярное произведение:

                + +

                Векторное произведение:

                +

                Ввод-вывод

                -

                Как вы думаете, как на самом деле работает cin >> x;? Это тоже перегрузка оператора >>. Делать это нужно так:

                -
                istream& operator>>(istream &in, r &p){ 
                +

                Как вы думаете, как на самом деле работает cin >> x? Это тоже перегрузка оператора — >>. Делается это так:

                + -

                Алгебра VS Алгоритмы или зачем мы всё это делали

                -

                Мы могли не создавать никаких структур и работать с уравнениями, описывающими математические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать. Программист же хочет абстрагироваться и просто написать intersect(a, b), в корректности которого он точно уверен.

                -

                Векторное представление прямой (\(Ax + By + C = 0 \rightarrow r = at + b\))

                -

                Тут нужно просто выбрать две любые точки на прямой.

                +

                Почему алгебра это плохо

                +

                Мы могли не создавать никаких структур и работать с уравнениями, описывающими геометрические объекты. Такой подход будет популярен на олимпиадах по математике, а не по программированию. Когда математик говорит «пересечем две прямые», он представляет громоздкое уравнение, с которым он потом будет работать.

                +

                Программист же хочет абстрагироваться и просто написать intersect(a, b), в корректности которого он точно уверен. Программист хочет разбить задачу на много маленьких кусочков и делать по отдельности, а не возиться с формулами.

                +

                Приведем несколько примеров конструктивного подхода.

                +

                Векторное представление прямой

                +

                Прямую можно задать не через уравнение, а через два вектора \(a\) и \(b\):

                +

                \[ +Ax + By + C = 0 \rightarrow r = at + b +\]

                +

                Чтобы это сделать, достаточно выбрать две любые точки на прямой:

                -

                Пример: отражение от прямой

                -

                Пусть нам надо отразить точку \((x_0, y_0)\) симметрично относительно заданной прямой \(ax+by+c=0\). Чисто в педагогических целях, решим эту задачу как математики, чтобы никогда потом так не делать.

                +

                Отражение от прямой

                +

                Пусть нам надо отразить точку \((x_0, y_0)\) симметрично относительно заданной прямой \(ax+by+c=0\). Чисто в педагогических целях, начнём решать эту задачу как математики, чтобы никогда потом так не делать.

                \[ \Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} \]

                -

                Формула имеет смысл: длина на единичный вектор направления.

                -

                Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так более наглядна логика алгоритма, и как следствие его проще дебажить.

                +

                Геометрический смысл: длина на единичный вектор направления.

                +

                Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так становится более наглядной логика алгоритма, и, как следствие, его проще дебажить.

                Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре.

                \(0 \neq -0\)

                -

                Действительные числа так хрянятся, что 0 и -0 могут быть разными числами. Имейте это ввиду.

                +

                Действительные числа так хрянятся, что \(0\) и \(-0\) могут быть разными числами. Имейте это ввиду.

                Область определения обратных функций

                acos, asin и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции.

                diff --git a/ru/imperferct-information.html b/ru/imperferct-information.html deleted file mode 100644 index b97f651..0000000 --- a/ru/imperferct-information.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - Игры с неполной информацией - - - - - - - - - - - - - - -
                -

                Игры с неполной информацией

                -
                -

                Игры с неполной информацией

                -

                Рассмотрим следующую ситуацию, известную как дилемма заключённого:

                -
                -

                Двое преступников — А и Б — попались примерно в одно и то же время на сходных преступлениях. Полиция предполагает, что они действовали по сговору, и, изолировав их друг от друга, предлагает им одну и ту же сделку:

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

                • -
                • Если оба молчат, их деяние проходит по более лёгкой статье, и каждый из них приговаривается к году тюрьмы.

                • -
                • Если оба свидетельствуют друг против друга, они получают по 2 года.

                • -
                -

                Каждый заключённый выбирает, молчать или свидетельствовать против другого. Однако ни один из них не знает точно, что сделает другой. Что произойдёт?

                -
                -

                Игры, в которых у игроков (в данном случае — узников) есть некоторая неопределенность относительно действий других игроков (например, из-за одновременности действий), называются играми с неполной информацией.

                -

                Данную «игру» можно представить в виде следующей табилцы:

                -
                                                   | Б хранит молчание | Б даёт показания |
                -А хранит молчание | (1, 1) | (10, 0) |
                -А даёт показания | (0, 10) | (2, 2) |
                -

                Пары чисел в ячейках означают сроки, которые получают заключенные А и Б соответственно.

                -

                Предположим, что оба преступника заботятся только о минимизации собственного срока заключения. Тогда рассуждения каждого узника будут следующими:

                -
                  -
                • Если партнёр молчит, то лучше его предать и выйти на свободу (иначе — полгода тюрьмы).

                • -
                • Если партнёр свидетельствует, то лучше тоже свидетельствовать против него, чтобы получить 2 года (иначе — 10 лет) тюрьмы.

                • -
                -

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

                -

                С точки зрения группы лучше всего хранить молчание и получить по году, так как это уменьшит срок заключения каждого. Такие состояния называются Парето-эффективными, но, как мы видим, они не всегда являются стабильными.

                -

                Игры с нулевой суммой

                -

                Рассмотрим другой пример игры с неполной информацией: «камень-ножницы-бумага». Эта игра в каком-то смысле более «человечная»: игроки явно соревнуются друг с другом, и им не нужно делать сделки с совестью и предавать друг друга.

                -

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

                -

                Примечание. Дилемма заключенного — пример игры с ненулевой суммой.

                -

                В играх с нулевой суммой исходы можно записывать разницей между выгодой первого и второго игрока:

                -
                    | К2 | Н2 | Б2|
                -К1 | 0 | +1 | -1|
                -Н1 | -1 | 0 | +1|
                -Б1 | +1 | -1 | 0 |
                -

                Каждая фиксированная стратегия (в данном случае выбор действия — камень, ножницы или бумага), которую может выбрать игрок, называется его чистой стратегией.

                -

                Смешанной стратегией игрока называется набор чистых стратегий, задаваемый вероятностями (относительными частотами) выбора соответствующих чистых стратегий. Задача обоих игроков — выбрать смешанную стратегию, максимизирующую свой выигрыш.

                -

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

                -

                Обозначим смешанную стратегию первого игрока за \(x\), а второго игрока за \(y\). Тогда ожидаемый счёт можно записать так:

                -

                \[ -V = xAy^T -\]

                -

                Действительно, эта формула раскрывается в \(\sum A_{ij} \cdot x(i) \cdot y(j)\).

                -

                Минимаксная теорема. Первый игрок хочет минимизировать это значение, а второй — максимизировать. Обозначим за \(V_a\) и \(V_b\) оптимальные значения, которые удаётся достичь игрокам вне зависимости от действий оппонента, то есть:

                -

                \[ -V_a = \min_x \max_y xAy^T -\\ V_b = \max_y \min_x xAy^T -\]

                -

                Утверждение: \(V_a = V_b\).

                -

                Доказательство. Запишем матрицу \(A'\), приписав к \(A\) справа единичную матрицу:

                -

                Будем доказывать от противного: покажем, что ситуация \(V_a < 0 < V_b\) невозможна, а значит для любого \(t\) ситуация невозможна.

                -

                Рассмотрим выпуклую оболочку \(C\) её столбцов.

                -

                Предположим, что \(\vec{0} \in C\).

                -

                Тогда существуют \(z_1, z_2, \ldots z_{n+m}\) такие что \(0 \leq z_i \leq 1\) и \(\sum z_i = 1\) (то есть вероятностное распределение).

                -

                $sum_{j=1}^n a_{ij} z_{ij} $

                -

                В этой игре оптимальной

                -

                Несимметричные игры

                -

                Минимаксная теорема

                -

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

                -

                Пусть имеется матрица \(A\) размера \(n \times m\). Задача

                - - From d2e9852466c7ab01c9edb6913b333b1ab9e82352 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 13 Jun 2019 23:48:26 +0000 Subject: [PATCH 062/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/fenwick.html | 68 +++++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/ru/fenwick.html b/ru/fenwick.html index 8a30b51..a967677 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -112,23 +112,27 @@

                Дерево Фенвика

                НЯ!
                Эта статья полна любви и обожания.
                Возможно, стоит добавить ещё больше?

                Дерево Фенвика

                -

                Дерево Фенвика или бинарно индеквированное дерево (англ. binary indexed tree) — структура данных, которая на многих (но не всех) задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, скоько массив такой же длины), намного быстрее пишется и легче обобщается на большие размерности.

                +

                Дерево Фенвика или бинарно индеквированное дерево (англ. binary indexed tree) — структура данных, которая на многих задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, сколько и массив той же длины), намного быстрее пишется и легче обобщается на большие размерности.

                Определение

                -

                Пусть дан массив \(a\) длины \(n\). Деревом Фенвика будем называть массив \(t\) той же длины, который объявим так:

                -

                \[ t_i = \sum_{k=F(i)}^i a_k \]

                +

                Пусть дан массив \(a\) длины \(n\). Деревом Фенвика будем называть массив \(t\) той же длины, объявленный следующим образом:

                +

                \[ +t_i = \sum_{k=F(i)}^i a_k +\]

                где \(F\) это какая-то функцию, для которой выполнено \(F(i) \leq i\). Конкретно её определим потом.

                -

                Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе (\(sum(l, r) = sum(r) - sum(l-1)\)), каждый из которых будем считать по этой формуле:

                -

                \[ sum(k) = t_k + sum(F(k)-1) \]

                -

                Когда мы изменяем \(k\)-ю ячейку исходного массива, мы обновляем все \(t_i\), в которых учтена эта ячейка.

                +

                Запрос суммы. Когда нам нужна сумма на отрезке, мы будем сводить этот запрос к двум суммам на префиксе: \(sum(l, r) = sum(r) - sum(l-1)\). Оба этих запроса будем считать по формуле:

                +

                \[ +sum(k) = t_k + sum(F(k)-1) +\]

                +

                Запрос обновления. Когда мы изменяем \(k\)-ю ячейку исходного массива, мы обновляем все \(t_i\), в которых учтена эта ячейка.

                \(F\) можно выбрать так, что и «спусков» при подсчете суммы, и интересных нам \(t_i\) при обновлении будет будет \(O(\log n)\). Популярны две функции:

                • \(F_1(x) =\) x & (x + 1)
                • \(F_2(x) =\) x - (x & -x) + 1
                -

                Первый вариант описан на викиконспектах и емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.

                -

                Disclaimer: наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце, а пока абстрагируйтесь и примите на веру, что любой префикс разбивается на \(O(\log n)\) отрезков вида \([F(i), i]\), и любой элемент входит в не более \(O(\log n)\) таких отрезков.

                +

                Первый вариант описан на Викиконспектах и Емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.

                +

                Примечание. Наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце. Рекумендуется пока что абстрагироваться и принять на веру, что любой префикс разбивается на \(O(\log n)\) отрезков вида \([F(i), i]\), а также что любой элемент входит в не более \(O(\log n)\) таких отрезков.

                Реализация

                -

                Из-за того, что \(F(0) = 1 > 0\) и поэтому \([0, F(0)]\) не является корректным отрезком, нам будет удобнее хранить массив в 1-индексации и не использовать \(t_0\).

                +

                Так как \(F(0) = 1 > 0\), то \([0, F(0)]\) не является корректным отрезком. Поэтому нам будет удобнее хранить массив в 1-индексации и не использовать \(t_0\).

                Если знать, что \(F(x)\) удаляет последний бит \(x\), то принцип понятен: просто делаем спуск по бинарному дереву, как в ДО. Чем-то похоже на генерацию \(k\)-го лексикографического комбинаторного объекта: пытаемся увеличить следующий символ всегда, когда это возможно.

                Отметим, что в «традиционной» индексации такое делать нельзя.

                -

                Ограничения

                -

                Дерево Фенвика можно использовать, когда наша операция обратима, а также когда трюк с префиксными суммами работает. Это обычно простые операции типа суммы, xor, умножения по модулю (если гарантируется, что на этот модуль ничего не делится). Минимум и gcd, отложенные операции и персистентность прикрутить в общем случае уже не получится — тогда уже нужно писать ДО.

                -

                Почему работает

                +

                Ограничения на операцию

                +

                Дерево Фенвика можно использовать, когда наша операция обратима, а также когда трюк с префиксными суммами работает. Это обычно простые операции типа суммы, xor, умножения по модулю (если гарантируется, что на этот модуль ничего не делится). Минимум и gcd, отложенные операции и персистентность прикрутить в общем случае уже не получится — тогда уже нужно писать дерево отрезков.

                +

                Почему это работает

                Итак, мы выбрали вариант с \(F(x)\) = x - (x & -x) + 1. Поймем, что означает x & -x.

                Лемма. x & -x возвращает последний единичный бит в двоичной записи x.

                Доказательство потребует знания, как в компьютерах хранятся целые числа. Чтобы процессор не сжигал лишние такты, проверяя знак числа при арифметических операциях, их хранят как бы по модулю \(2^k\), а первый бит отвечает за знак (0 для положительных и 1 для отрицательных). Поэтому когда мы хотим узнать, как выглядит отрицательное число, нужно его вычесть из нуля: \(-x = 0-x = 2^k-x\).

                -

                Например,

                +

                Как будет выглядеть -x в битовой записи? Ответ можно мысленно разделить на три блока:

                +
                  +
                • Первые сколько-то (возможно, нисколько) нулей с конца числа x ими же в ответе и останутся.
                • +
                • Потом, ровно на самом младшем единичном бите x, мы «займём» много единиц, так что весь префикс станет единицами. В ответе на этом месте точно будет единица.
                • +
                • Потом отменятся ровно те биты из этого префикса, которые были единицами в исходном числе.
                • +
                +

                Пример:

                \[ \begin{aligned} +90 = 2+8+16+64 & = 0 \, 10110_2 \\ @@ -193,22 +203,24 @@

                Почему работает

                \implies (+90) \text{\&} (-90) & = 0 \, 00010_2 \\ \end{aligned} \]

                -

                Вернёмся к доказательству леммы. Когда мы вычитаем, мы идем справа налево. В ответе можно мысленно разделить на три блока:

                +

                Теперь мы можем доказать нашу лемму. Когда мы сделаем &, в префиксе до младшего единичного бита все биты x и -x будут противоположными, младший единичный бит останется единичным, а на суффиксе все как было нулями, так и осталось. Следовательно, «выживет» только этот самый младший единичный бит, что мы и доказывали.

                +

                Следствие 1. sum будет работать за логарифм, а точнее за количество единичных битов в записи \(x\): на каждой итерации мы делаем x -= x & -x, то есть удаляем младший бит.

                +

                Следствие 2. add тоже будет работать за логарифм: каждую итерацию количество нулей на конце \(x\) увеличивается хотя бы на единицу.

                +

                Следствие 3. (Почему дерево Фенвика — дерево.)

                  -
                • Первые сколько-то (возможно, нисколько) нулей с конца ими же и останутся.
                • -
                • Потом, ровно на самом младшем единичном бите, мы «займём» много единиц, так что весь префикс станет единицами. В ответе на этом месте точно будет единица. Потом отменятся ровно те из них, которые были единицами в исходном числе.
                • +
                • Длина отрезка, соответстующего любому \(t_i\) — степень двойки, причём начинается этот отрезок на индексе, кратном этой же степени двойки.

                • +
                • \(\implies\) Множества элементов, учтённых в произвольных \(t_i\) и \(t_j\), либо не пересекаются, либо одно является подмножеством другого.

                • +
                • \(\implies\) На \(t_i\) можно ввести отношение вложенности.

                -

                Делаем &: в каком-то префиксе все биты будут противоположными, младший единичный бит останется, а на суффиксе все как было нулями, так и осталось. Выживет только этот самый младший единичный бит.

                -

                Теперь сразу понятно, почему sum будет работать за логарифм — каждый раз мы делаем x -= x & -x, то есть удаляем один бит.

                -

                Теперь сложная (для понимания) часть — как делать add. Какие \(t_i\) содержат \(k\)-й элемент? Для них должно выполняться i >= k > i - (i & -i).

                -

                Будем перебирать префиксы TODO

                -

                Мы знаем, что \(t_i\) вложены друг в друга. Минимальный подходящий \(i\) равен \(k\). Какой следующий? Нам нужно для каждого \(i\) уметь находить его непосредственного родителя.

                -

                Можно представить дерево так: ячейка 2^k содержит все

                -

                TODO: сначала объявить дерево как дерево и описать его структуру словами, а потом уже показывать реализацию.

                -

                Название

                -

                Потому что \(F\) использует битовые операции, по-английски структура называется «Binary Indexed Tree».

                -

                Почему дерево Фенвика — дерево? Тут нужно немного воображения. На самом деле BIT в общем случае — набор деревьев.

                -

                Можно показать, что множества элементов, учтенных в \(t_i\) и \(t_j\), либо не пересекаются, либо одно является подмножеством другого. Значит, между \(t_i\) можно ввести отношение вложенности, и тогда дерево Фенвика можно представить как набор деревьев (не обязательно бинарных).

                -

                В частном случае, когда длина массива равна \(2^k\), то дерево будет только одно.

                +

                То есть, если напрячь воображение, то \(t\) можно рассматривать как лес деревьев. В частном случае, когда \(n\) является степенью двойки, дерево будет одно.

                +

                Теперь единственное, что осталось доказать ­— это корректность add. На самом деле, в add мы делаем ни что иное, как подъём от вершины до корня по всем предкам.

                +

                Как для \(x\) найти непосредственного родителя, то есть минимального предка? Нужно найти минимальное число \(y > x\), у которого \(t_y\) будет включать \(x\). Иными словами, должно выполняться y >= x > y - (y & -y).

                +

                Дальше, читателю предлагается самостоятельно попялиться в пример, чтобы понять, что x + (x & -x) — минимальное такое число:

                +

                \[ +\begin{aligned} + x = 90 = 2+8+16+64 & = 10110_2 \\ + y = 96 = 32 + 64 & = 11000_2 \\ + \end{aligned} +\]

                From bc24377416884fecff555bd94db893c9d1840a36 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 13 Jun 2019 23:51:35 +0000 Subject: [PATCH 063/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/fenwick.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ru/fenwick.html b/ru/fenwick.html index a967677..7034549 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -200,7 +200,7 @@

                Почему это работае \begin{aligned} +90 = 2+8+16+64 & = 0 \, 10110_2 \\ -90 = 00000_2 - 10110_2 & = 1 \, 01010_2 \\ - \implies (+90) \text{\&} (-90) & = 0 \, 00010_2 \\ + \implies (+90) \& (-90) & = 0 \, 00010_2 \\ \end{aligned} \]

                Теперь мы можем доказать нашу лемму. Когда мы сделаем &, в префиксе до младшего единичного бита все биты x и -x будут противоположными, младший единичный бит останется единичным, а на суффиксе все как было нулями, так и осталось. Следовательно, «выживет» только этот самый младший единичный бит, что мы и доказывали.

                @@ -214,8 +214,8 @@

                Почему это работае

                То есть, если напрячь воображение, то \(t\) можно рассматривать как лес деревьев. В частном случае, когда \(n\) является степенью двойки, дерево будет одно.

                Теперь единственное, что осталось доказать ­— это корректность add. На самом деле, в add мы делаем ни что иное, как подъём от вершины до корня по всем предкам.

                -

                Как для \(x\) найти непосредственного родителя, то есть минимального предка? Нужно найти минимальное число \(y > x\), у которого \(t_y\) будет включать \(x\). Иными словами, должно выполняться y >= x > y - (y & -y).

                -

                Дальше, читателю предлагается самостоятельно попялиться в пример, чтобы понять, что x + (x & -x) — минимальное такое число:

                +

                Как для \(x\) найти непосредственного родителя? Нужно найти минимальное число \(y > x\), у которого \(t_y\) будет включать \(x\). Иными словами, должно выполняться y >= x > y - (y & -y).

                +

                Дальше читателю предлагается самостоятельно попялиться в пример, чтобы понять, что x + (x & -x) — минимальное такое число:

                \[ \begin{aligned} x = 90 = 2+8+16+64 & = 10110_2 \\ From 1567526f3b02fd18a55f544cb3548ed3d584c38e Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 13 Jun 2019 23:55:56 +0000 Subject: [PATCH 064/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/pollard.html | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ru/pollard.html b/ru/pollard.html index a9d9aeb..9483f18 100644 --- a/ru/pollard.html +++ b/ru/pollard.html @@ -135,28 +135,28 @@

                Первым эту гипотезу опроверг Анатолий Карацуба. Его алгоритм сводит умножение двух \(n\)-значиных чисел к трём умножениям \(\frac{n}{2}\)-значных чисел, что даёт оценку времени работы

                \(\rho\

                Рассмотрим траекторию какого-нибудь элемента \(x_0\): {\(x_0\), \(f(x_0)\), \(f(f(x_0))\), \(\ldots\)}. Сделаем из неё новую последовательность, мысленно взяв каждый элемент по модулю \(p\) — наименьшего из простых делителей \(n\).

                Утверждение. Ожидаемая длина цикла в этой последовательности \(O(\sqrt[4]{n})\).

                -

                Доказательство: так как \(p\) — меньший делитель, то \(p \leq \sqrt{n}\). Теперь просто подставляем в следствие из парадокса дней рождений: в множество нужно добавить \(O(\sqrt{p}) = O(\sqrt[4]{n})\) элементов, чтобы какие-то два совпали, а значит последовательность зациклилась.

                +

                Доказательство: так как \(p\) — меньший делитель, то \(p \leq \sqrt n\). Теперь просто подставлим в следствие из парадокса дней рождений: в множество нужно добавить \(O(\sqrt{p}) = O(\sqrt[4]{n})\) элементов, чтобы какие-то два совпали, а значит последовательность зациклилась.

                Если мы найдём цикл в такой последовательности — то есть такие \(i\) и \(j\), что \(f^i(x_0) \equiv f^j(x_0) \pmod p\) — то мы сможем найти и какой-то делитель \(n\), а именно \(\gcd(|f^i(x_0) - f^j(x_0)|, n)\) — это число меньше \(n\) и делится на \(p\).

                Алгоритм по сути находит цикл в этой последовательности, используя для этого стандартный алгоритм («черепаха и заяц»): будем поддерживать два удаляющихся друг от друга указателя \(i\) и \(j\) (\(i = 2j\)) и проверять, что \(f^i(x_0) \equiv f^j(x_0) \pmod p\), что эквивалентно проверке \(\gcd(|f^i(x_0) - f^j(x_0)|, n) > 1\).

                -
                typedef long long ll;
                +
                -

                Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за \(O(\sqrt{n})\) (нужно добавить отсечение по времени).

                -

                Разное

                +

                Так как алгоритм рандомизированный, при полной реализации нужно учитывать разные детали. Например, что иногда делитель не находится (нужно запускать несколько раз), или что при попытке факторизовать простое число он будет работать за \(O(\sqrt n)\) (нужно добавить отсечение по времени).

                +

                Примечания

                Формально, мы показали, что алгоритм работает за \(O(\sqrt[4]{n} \log n)\) за счёт поиска \(\gcd\), но сложной теорией чисел можно доказать, что этого логарифма в асимптотике на самом деле нет.

                Факторизация больших чисел интересна в контексте криптографии — на предположение невозможности факторизации за линейное время опирается, например, алгоритм RSA.

                Существуют также субэкспоненциальные, но не полиномиальные алгоритмы факторизации. Человечество умеет факторизовывать числа порядка \(2^{200}\).

                From 8e0596537b272fca47fcb2ff8ee90329930f0295 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 14 Jun 2019 00:08:16 +0000 Subject: [PATCH 065/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/pollard.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ru/pollard.html b/ru/pollard.html index 9483f18..4bb4fb0 100644 --- a/ru/pollard.html +++ b/ru/pollard.html @@ -109,7 +109,7 @@

                Ро-алгоритм Полларда

                Ро-алгоритм Полларда

                -

                Наука умеет раскладывать целые числа на множители за \(O(n^\frac{1}{4})\). Описываемый в этой статье алгоритм рандомизированный, поэтому нам сначала понадобится доказать одно утверждение относящееся к теории вероятностей.

                +

                Наука умеет раскладывать целые числа на множители за \(O(n^\frac{1}{4})\). Описываемый в этой статье алгоритм рандомизированный, поэтому нам сначала понадобится доказать одно утверждение из теории вероятностей.

                Парадокс дней рождений

                Пусть \(f(n, d)\) это вероятность того, что в группе из \(n\) человек ни у кого не совпали дни рождения. Будем считать, что дни рождения распределены независимо и равномерно в промежутке от \(1\) до \(d\).

                \[ @@ -142,7 +142,7 @@

                \(\rho\ inline ll f(ll x) { return (x+1)*(x+1); } -ll find_divisor(ll n, ll seed = 1): +ll find_divisor(ll n, ll seed = 1) { ll x = seed, y = seed; ll divisor = 1; while (d == 1) { From 705b3f2255e6ee70bfe502deeb68cc717106ec95 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 14 Jun 2019 00:25:00 +0000 Subject: [PATCH 066/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 5 +++++ ru/mincost-maxflow.html | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pandoc.css b/pandoc.css index 7ce97ed..3934abc 100644 --- a/pandoc.css +++ b/pandoc.css @@ -205,6 +205,11 @@ li p:last-child { margin-bottom: 0; } +li ul { + padding-left: 18px; + padding-top: 1px; +} + ul ul, ol ol { margin: .3em 0; } diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html index 2c9d578..2d7c3f8 100644 --- a/ru/mincost-maxflow.html +++ b/ru/mincost-maxflow.html @@ -126,7 +126,7 @@

                Дополняющие пути

                Утверждеие. Алгоритм не создает в остаточной сети циклов отрицательного веса.

                Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из \(s\) в \(t\) и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро \((v, u)\) находится в цикле отрицательного веса. Тогда есть путь Из \(u\) в \(v\) стоимости меньше, чем \(w_{uv}\). Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра \((u, v)\).

                Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит \(O(nmU)\) — искать каждый дополняющий путь мы будем не более \(U\) раз.

                -

                Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась. Но, оказывается, есть способ это пофиксить: дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер.

                +

                Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась: это можно сделать, если дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер.

                Потенциалы Джонсона

                Потенциалом вершины \(v\) будем называть расстояние \(d_v\) от вершины \(s\). Рассмотрим граф из всех достижимых вершин и тех же рёбер, только с изменёнными весами:

                \[ w_{uv}' = w_{uv} + d_u - d_v \]

                @@ -159,11 +159,8 @@

                Итоговый алгоритм

                Протолкнуть максимально возможный поток вдоль кратчайшего пути \(s \leadsto t\), обновить остаточную сеть. -

                Асимптотика

                -

                Алгоритм работает за \(O(U m \log n)\) или \(O(U n^2)\) в случае плотных графов.

                -

                В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и \(U \leq n\) или \(U \leq m\). Например, в задаче о назначениях (паросочетание минимального веса) \(U = n\) и алгоритм работает за \(O(n^3)\), что совпадает с асимптикой венгерского алгоритма.

                Реализация

                -

                Решение задачи о назначениях за \(O(n^3)\). Используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищем минимум за линию).

                +

                Ниже приведено решение задачи о назначениях (паросочетание минимального веса). Для нахождения дополняющего пути используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищется минимум за линию).

                • cost, cap — параметры сети
                • pot — потенциалы
                • @@ -217,5 +214,8 @@

                  Реализация

                  } return ans; }

                +

                Асимптотика

                +

                В общем случае, алгоритм работает за \(O(U m \log n)\) или \(O(U n^2)\) в случае плотных графов.

                +

                В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и \(U \leq n\) или \(U \leq m\). Например, в задаче о назначениях \(U = n\), и алгоритм работает за \(O(n^3)\), что совпадает с асимптикой венгерского алгоритма.

                From c12e428e0b95d118757110130530275da579ca65 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 14 Jun 2019 17:51:24 +0000 Subject: [PATCH 067/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/mst.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ru/mst.html b/ru/mst.html index f0c78d4..9c4ab5b 100644 --- a/ru/mst.html +++ b/ru/mst.html @@ -227,7 +227,7 @@

                Сис
                Паблик «Странные опросы для спортивных программистов»
                Паблик «Странные опросы для спортивных программистов»
                -

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

                +

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

                Ранговая эвристика. Будем хранить для каждой вершины её ранг — высоту её поддереа. При объединении деревьев будем делать корнем нового дерева ту вершину, у которой ранг больше, и пересчитывать ранги (ранг у лидера должен увеличиться на единицу, если он совпадал с рангом другой вершины). Эта эвристика оптимизирует высоту дерева напрямую.

                Весовая эвристика. Будем вместо ранга хранить размеры поддеревьев для каждой вершины, а при объединении — подвешивать за более «тяжелую».

                Финальная реализация, использующая весовую эвристику и эвристику сжатия путей:

                @@ -249,7 +249,7 @@

                Сис _p[i] = i;

                Автор предпочитает именно весовую эвристику, потому что часто в задачах размеры компонент требуются сами по себе.

                Асимптотика

                -

                Эвристика сжатия путей улучшает асимптотику до \(O(\log n)\) в среднем. Здесь используется именно амортизированная оценка — понятно, что в худшем случае нужно будет сжимать весь бамбук за $\(O(n)\).

                +

                Эвристика сжатия путей улучшает асимптотику до \(O(\log n)\) в среднем. Здесь используется именно амортизированная оценка — понятно, что в худшем случае нужно будет сжимать весь бамбук за \(O(n)\).

                Индукцией несложно показать, что весовая и ранговая эвристики ограничивают высоту дерева до \(O(\log n)\), а соответственно и асимптотику тоже.

                При использовании эвристики сжатия плюс весовой или ранговой асимптотика будет \(O(a(n))\), где \(a(n)\) — обратная функция Аккермана (очень медленно растущая функция, для всех адекватных чисел не превосходящая 4).

                Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует.

                From 36b25f21719a1fd7c2a95be6dbb1f7225838d4b4 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 14 Jun 2019 18:25:18 +0000 Subject: [PATCH 068/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.html b/index.html index 64dec14..921945d 100644 --- a/index.html +++ b/index.html @@ -19,4 +19,6 @@ +

                If you're seeing this, your JavaScript is probably disabled.

                +

                Go to one of those then: algorithmica.org/en/ or algorithmica.org.ru. From ba2c103528c845e855d5317adda3009358587ebe Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 14 Jun 2019 18:51:05 +0000 Subject: [PATCH 069/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- sitemap.xml | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 sitemap.xml diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..e99ee9a --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,192 @@ + + + + + + + https://algorithmica.org/ + 2019-06-14T18:25:26+00:00 + 1.00 + + + https://algorithmica.org/en/ + 2019-06-14T18:25:26+00:00 + 0.80 + + + https://algorithmica.org/ru/ + 2019-06-14T18:25:26+00:00 + 0.80 + + + https://algorithmica.org/ru/segtree + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/treap + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/fenwick + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/sparse-table + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/sqrt + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/bitset + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/dp-optimizations + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/linalg + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/probability + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/karatsuba + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/fft + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/reciprocal + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/matroid + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/pollard + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/mst + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/matching + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/mincost-maxflow + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/lca + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/centroid + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/hld + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/hashing + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/strings + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/trie + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/aho-corasick + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/suffix-array + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/suffix-automaton + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/palindromes + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/geometry + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/convex-hulls + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/games + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/imperfect-information + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/annealing + 2019-06-14T18:25:26+00:00 + 0.64 + + + https://algorithmica.org/ru/stress-test + 2019-06-14T18:25:26+00:00 + 0.64 + + + + \ No newline at end of file From 8f4421a1e690e57a875c017ddb8af49811f3818b Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 14 Jun 2019 19:04:56 +0000 Subject: [PATCH 070/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/treap.html | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ru/treap.html b/ru/treap.html index 77af76d..ed958b8 100644 --- a/ru/treap.html +++ b/ru/treap.html @@ -120,9 +120,7 @@

                Декартово дерево

                Заметим, что если все \(y\) и \(x\) различны, то дерево строится однозначно.

                Если нарисовать получившуюся структуру на плоскости, то получится действительно дерево — по традиции, корнем вверх:

                -
                -treap
                treap
                -
                +

                Таким образом, декартово дерево — это одновременно бинарное дерево по \(x\) и куча по \(y\). Поэтому ему придумали много альтернативных названий:

                • Дерамида (дерево + пирамида)

                • @@ -133,10 +131,10 @@

                  Декартово дерево

                  Бинарные деревья

                  С небольшими модификациями, декартово дерево умеет всё то же, что и любое бинарное дерево поиска, например:

                    -
                  • добавить число \(x\) в множество

                  • -
                  • определить, есть ли в множестве число \(x\)

                  • -
                  • найти первое число, не меньшее \(x\) (lower_bound)

                  • -
                  • найти количество чисел в промежутке \([l, r]\)

                  • +
                  • Добавить число \(x\) в множество.

                  • +
                  • Определить, есть ли в множестве число \(x\).

                  • +
                  • Найти первое число, не меньшее \(x\) (lower_bound).

                  • +
                  • Найти количество чисел в промежутке \([l, r]\).

                  При этом все операции — за \(O(\log n)\).

                  На самом деле, бинарных деревьев очень много. В большинстве из них время выполнения операций пропорционально высоте дерева, поэтому в них придумываются разные инварианты, позволяющие эту высоту минимизировать до \(O(\log n)\).

                  @@ -153,8 +151,8 @@

                  Приоритеты и

                  \[ E[d_i] = E[\sum_{j \neq i} a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} E[a(j, i)] = \sum_{j \neq i} p(j, i) \]

                  -

                  где \(p(x, y)\) это веряотность, что \(a(x, y) = 1\). Здесь мы воспользовались важным свойством линейности: матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.

                  -

                  Ок, теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.

                  +

                  где \(p(x, y)\) это вероятность, что \(a(x, y) = 1\). Здесь мы воспользовались важным свойством линейности: матожидание суммы чего угодно равна сумме матожиданий этого чего угодно.

                  +

                  Теперь осталось посчитать эти вероятности и сложить. Но сначала нам понадобится вспомогательное утверждение.

                  Лемма. Вершина \(x\) является предком \(y\), если у неё приоритет больше, чем у всех вершин из полуинтервала \((x, y]\) (без ограничения общности, будем считать, что \(x < y\)).

                  Необходимость. Если это не так, то где-то между \(x\) и \(y\) есть вершина с большим приоритетом, чем \(x\). Она не может быть потомком \(x\), а значит \(x\) и \(y\) будут разделены.

                  Достаточность. Если справа будет какая-то вершина с большим приоритетом, то её левым сыном будет какая-то вершина, которая будет являться предком \(x\). Таким образом, всё, что справа от \(y\), ни на что влиять не будет.

                  From e8ea01a5e622135a85ede97b7ebd82f972ccd013 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 14 Jun 2019 20:23:58 +0000 Subject: [PATCH 071/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/convex-hulls.html | 158 ++++++++++++++----------------------------- 1 file changed, 52 insertions(+), 106 deletions(-) diff --git a/ru/convex-hulls.html b/ru/convex-hulls.html index d59a285..82f7ecd 100644 --- a/ru/convex-hulls.html +++ b/ru/convex-hulls.html @@ -4,7 +4,7 @@ - Задачи на многоугольники + Выпуклые оболочки + - - - - - - - - - - -
                  -

                  Базовые строковые алгоритмы

                  -
                  -

                  Базовые строковые алгоритмы

                  -

                  Префикс-функция

                  -

                  Рассмотрим задачу, которая возникает каждый раз, когда вы делаете ctrl+f:

                  -
                  -

                  Есть большой текст \(t\). Нужно найти все вхождения строки \(s\) в него.

                  -
                  -

                  Наивное решение со сравнением всех подстрок \(t\) длины \(|s|\) со строкой \(s\) работает за \(O(|t| \cdot |s|)\). Если текст большой, то длинные слова в нем искать становится очень долго.

                  -

                  Для решения этой задачи за линейное время придумали префикс-функцию.

                  -

                  Определение. Префикс-функцией от строки \(s\) называется массив \(p\), где \(p_i\) равно длине самого большого префикса строки \(s_0 s_1 s_2 \ldots s_i\), который также является и суффиксом этой строки (не считая всю строку).

                  -

                  Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна \([0, 1, 0, 1, 2, 3, 4, 5]\).

                  - -
                  [0, 0, 1, 0, 1, 2, 3, 4]
                  -

                  (Этот алгоритм работает за \(O(n^3)\), но это только пока.)

                  -

                  Как это поможет решить исходную задачу?

                  -

                  Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки — как это делать, поясним дальше — и научимся с помощью нее искать подстроку в строке.

                  -

                  Соединим подстроки \(s\) и \(t\) каким-нибудь символом, который не встречается ни там, ни там. Посмотрим на префикс-функцию получившейся строки \(s\#t\).

                  - -
                  let it go#let it go, let it go can't hold it back anymore let it go, let it go turn away and slam the door!
                  -00000000000123456789001234567890000000001000000000000000000123456789001234567890000000000000000100000000000
                  -

                  Видно, что все места, где значения равны 9 (длине S) — это концы вхождений \(s\) в текст \(t\).

                  -

                  Такой алгоритм (посчитать префикс-функцию от \(s\#t\) и посмотреть, в каких позициях она равна \(|s|\)) называется алгоритмом Кнута-Морриса-Пратта.

                  -

                  Как её быстро считать

                  -

                  Рассмотрим ещё несколько примеров префикс-функций:

                  - -
                  [0, 0, 1, 2, 3]
                  -[0, 0, 0, 0, 0, 0]
                  -[0, 0, 0, 1, 0, 1, 2, 3, 0, 1, 0]
                  -[0, 0, 0, 0, 1, 2, 1, 2, 1, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
                  -

                  Можно заметить несколько особенностей: * \(p_0 = 0\) для всех строк: префикс подстроки из первого символа, который не совпадает со всей строкой — это только пустой префикс; * \(p_{i+1}\) максимум на единицу превосходит \(p_i\): если есть префикс, равный суффиксу строки \(s_0 s_1 \ldots s_{i+1}\) длины \(p_{i+1}\), то, отбросив последний символ, можно получить и правильный суффикс для строки \(s_0 s_1 \ldots s_i\), длина которого будет ровно на единицу меньше.

                  -

                  Хочется придумать алгоритм, как считать префикс-функцию за \(O(N)\). Логично это делать с помощью динамики: найти формулу для \(p_i\) через предыдущие значения.

                  -

                  Заметим, что \(p_{i+1} = p_i + 1\) в том и только том случае, когда \(s_{p_i} =s_{i+1}\). Например, в строке \(\underbrace{aabaa}t\overbrace{aabaa}\) выделен максимальный префикс, равный суффиксу: \(p_{10} = 5\). Если следующий символ равен будет равен \(t\), то \(p_{11} = p_{10} + 1 = 6\).

                  -

                  Но что происходит, когда \(s_{p_i}\neq s_{i+1}\)? Пусть следующий символ в этой же строке равен \(b\). * \(\implies\) Тогда длина префикса, равного суффиксу новой строки, будет точно меньше 5. * \(\implies\) Значит, помимо того, что он является суффиксом «aabaab», префикс является префиксом и подстроки «aabaa». * \(\implies\) Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть \(p_4 = 2\). * \(\implies\) Если \(s_2 = s_{11}\) (т. е. новый символ совпадает с идущим после префикса-кандидата), то \(p_{11} = p_2 + 1 = 2 + 1 = 3\).

                  -

                  В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, $p_{i+1} p_{p_i+1} $? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — \(p_{p_{p_i}}\). Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым.

                  - -
                  [0, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6, 7, 0]
                  -

                  Асимптотика

                  -

                  Почему это работает за \(O(N)\)? В худшем случае этот while может работать \(O(N)\) раз за одну итерацию. Но оказывается, что в среднем каждый while работает за \(O(1)\) — это называется амортизированной асимптотикой.

                  -

                  Доказательство: * Как мы ранее заметили, префикс функция возрастает максимум на единицу. * \(\implies\) Вырасти она может максимум \(n-1\) раз. * Если мы зашли в while, то каждая его итерация понижает значение префикс-функции хотя бы на один. * \(\implies\) Количество понижений не больше количества повышений, то есть \(O(n)\). * \(\implies\) Суммарно итераций цикла while\(O(n)\).

                  -

                  Z-функция

                  -

                  Альтернатива префикс-функции — z-функция (примечание: не «зи», а «зет»). Она немного проще для понимания.

                  -

                  Z-функция от строки \(s\) — это такой массив \(z\), что \(z_i\) равно длине максимальной подстроки, начинающейся с \(i\)-й позиции, которая равна префиксу \(s\).

                  -

                  \[\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3)\]

                  - -
                  [0, 0, 1, 0, 3, 0, 1, 0, 3, 0, 1]
                  -[0, 0, 0, 0, 3, 1, 0, 5, 1, 0, 0, 1]
                  -[0, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0, 11, 0, 0, 2, 0, 2, 0, 1, 0, 0, 0, 0]
                  -

                  Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c \(s\), а не заканчиваться. Осталось научиться её искать за \(O(n)\).

                  -

                  Как её быстро считать

                  -

                  Заметим, что: * \(z_0 = 0\) — из-за договоренности (потому что информации не несет); * \(z_i = 0 \iff s_i \neq s_0\); * \(z_i > 0 \iff s_i = s_0\).

                  -

                  Будем идти слева направо и хранить z-блок — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как \(l\) и \(r\).

                  -

                  Пусть мы сейчас хотим найти \(z_i\), а все предыдущие уже нашли. Если новый, \(i\)-й символ не лежит левее z-блока, значит он лежит либо в z-блоке, либо правее. * Если правее, то мы просто наивно перебором найдем \(z_i\) (максимальный отрезок, начинающийся с \(s_i\) и равный префиксу), и объявим его новым z-блоком. * Если \(i\)-й элемент лежит внутри z-блока, то мы можем посмотреть на значение \(z_{i-l}\) и использовать его, чтобы инициализировать \(z_i\) чем-то, возможно, отличным от нуля. Если \(z_{i-l}\) «не хватает» до границы \(z\)-блока, то \(z_i = z_{i-l}\). Если он упирается, в границу, то «обрежем» его до известной границы и будем увеличивать на единичку.

                  - -

                  Асимптотика. В алгоритме мы делаем столько действий, сколько раз сдвигается правая граница z-блока — а это \(O(n)\).

                  -

                  Зачем тогда люди используют префикс-функцию

                  -
                  -hz
                  hz
                  -
                  -

                  В целом они очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про Z-функция почему-то мало кто знает кроме олимпиадных программистов.

                  -

                  Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение.

                  - - From c03347967f9e2b5dda184e96ab0a5447dbd2739c Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Mon, 17 Jun 2019 23:57:16 +0000 Subject: [PATCH 085/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/string-searching.html | 244 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 ru/string-searching.html diff --git a/ru/string-searching.html b/ru/string-searching.html new file mode 100644 index 0000000..8b6776e --- /dev/null +++ b/ru/string-searching.html @@ -0,0 +1,244 @@ + + + + + + + Поиск строки в строке + + + + + + + + + + + + + + + +
                  +

                  Поиск строки в строке

                  +
                  +

                  Поиск строки в строке

                  +

                  Рассмотрим задачу, которая возникает каждый раз, когда вы делаете ctrl+f:

                  +
                  +

                  Есть большой текст \(t\). Нужно найти все вхождения строки \(s\) в него.

                  +
                  +

                  Наивное решение со сравнением всех подстрок \(t\) длины \(|s|\) со строкой \(s\) работает за \(O(|t| \cdot |s|)\). Если текст большой, то длинные слова в нем искать становится очень долго.

                  +

                  Однако существует множество способов решить эту задачу за \(O(|s| + |t|)\), два самых распространённых и простых из них: через префикс-функцию и через z-функцию (примечание: не «зи», а «зет»).

                  +

                  Префикс-функция

                  +

                  Определение. Префикс-функцией от строки \(s\) называется массив \(p\), где \(p_i\) равно длине самого большого префикса строки \(s_0 s_1 s_2 \ldots s_i\), который также является и суффиксом \(i\)-того префика (не считая весь \(i\)-й префикс).

                  +

                  Например, самый большой префикс, который равен суффиксу для строки «aataataa» — это «aataa»; префикс-функция для этой строки равна \([0, 1, 0, 1, 2, 3, 4, 5]\).

                  + +

                  Этот алгоритм пока что работает за \(O(n^3)\), но позже мы его ускорми.

                  +

                  Как это поможет решить исходную задачу?

                  +

                  Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки, и научимся с помощью нее искать подстроку в строке.

                  +

                  Соединим подстроки \(s\) и \(t\) каким-нибудь символом, который не встречается ни там, ни там — обозначим пусть этот символ #. Посмотрим на префикс-функцию получившейся строки \(s\#t\).

                  + + +

                  Видно, что все места, где значения равны 6 (длине \(s\)) — это концы вхождений \(s\) в текст \(t\).

                  +

                  Такой алгоритм (посчитать префикс-функцию от \(s\#t\) и посмотреть, в каких позициях она равна \(|s|\)) называется алгоритмом Кнута-Морриса-Пратта.

                  +

                  Как её быстро считать

                  +

                  Рассмотрим ещё несколько примеров префикс-функций и попытаемся найти закономерности:

                  + +

                  Можно заметить следующую особенность: \(p_{i+1}\) максимум на единицу превосходит \(p_i\).

                  +

                  Доказательство. Если есть префикс, равный суффиксу строки \(s_{:i+1}\), длины \(p_{i+1}\), то, отбросив последний символ, можно получить правильный суффикс для строки \(s_{:i}\), длина которого будет ровно на единицу меньше.

                  +

                  Попытаемся решить задачу с помощью динамики: найдём формулу для \(p_i\) через предыдущие значения.

                  +

                  Заметим, что \(p_{i+1} = p_i + 1\) в том и только том случае, когда \(s_{p_i} =s_{i+1}\). В этом случае мы можем просто обновить \(p_{i+1}\) и пойти дальше.

                  +

                  Например, в строке \(\underbrace{aabaa}t\overbrace{aabaa}\) выделен максимальный префикс, равный суффиксу: \(p_{10} = 5\). Если следующий символ равен будет равен \(t\), то \(p_{11} = p_{10} + 1 = 6\).

                  +

                  Но что происходит, когда \(s_{p_i}\neq s_{i+1}\)? Пусть следующий символ в этом же примере равен не \(t\), а \(b\).

                  +
                    +
                  • \(\implies\) Длина префикса, равного суффиксу новой строки, будет точно меньше 5.
                  • +
                  • \(\implies\) Помимо того, что искомый новый супрефикс является суффиксом «aabaab», он ещё является префиксом подстроки «aabaa».
                  • +
                  • \(\implies\) Значит, следующий кандидат на проверку — это значение префикс-функции от «aabaa», то есть \(p_4 = 2\), которое мы уже посчитали.
                  • +
                  • \(\implies\) Если \(s_2 = s_{11}\) (т. е. новый символ совпадает с идущим после префикса-кандидата), то \(p_{11} = p_2 + 1 = 2 + 1 = 3\).
                  • +
                  +

                  В данном случае это действительно так (нужный префикс — «aab»). Но что делать, если, в общем случае, \(p_{i+1} \neq p_{p_i+1}\)? Тогда мы проводим такое же рассуждение и получаем нового кандидата, меньшей длины — \(p_{p_{p_i}}\). Если и этот не подошел — аналогично проверяем меньшего, пока этот индекс не станет нулевым.

                  + +

                  Асимптотика. В худшем случае этот while может работать \(O(n)\) раз за одну итерацию, но в среднем каждый while работает за \(O(1)\).

                  +

                  Префикс-функция каждый шаг возрастает максимум на единицу и после каждой итерации while уменьшается хотя бы на единицу. Значит, суммарно операций будет не более \(O(n)\).

                  +

                  Z-функция

                  +

                  Немногого более простая для понимания альтернатива префикс-функции — z-функция.

                  +

                  Z-функция от строки \(s\) определяется как массив \(z\), такой что \(z_i\) равно длине максимальной подстроки, начинающейся с \(i\)-й позиции, которая равна префиксу \(s\).

                  +

                  \[ +\underbrace{aba}c\overbrace{aba}daba \hspace{1em} (z_4 = 3) +\]

                  + + +

                  Z-функцию можно использовать вместо префикс-функции в алгоритме Кнута-Морриса-Пратта — только теперь нужные позиции будут начинаться c \(|s|\), а не заканчиваться. Осталось только научиться её искать за \(O(n)\).

                  +

                  Как её быстро считать

                  +

                  Будем идти слева направо и хранить z-блок — самую правую подстроку, равную префиксу, которую мы успели обнаружить. Будем обозначать его границы как \(l\) и \(r\) включительно.

                  +

                  Пусть мы сейчас хотим найти \(z_i\), а все предыдущие уже нашли. Новый \(i\)-й символ может лежать либо правее z-блока, либо внутри него:

                  +
                    +
                  • Если правее, то мы просто наивно перебором найдем \(z_i\) (максимальный отрезок, начинающийся с \(s_i\) и равный префиксу), и объявим его новым z-блоком.
                  • +
                  • Если \(i\)-й элемент лежит внутри z-блока, то мы можем посмотреть на значение \(z_{i-l}\) и использовать его, чтобы инициализировать \(z_i\) чем-то, возможно, отличным от нуля. Если \(z_{i-l}\) левее правой границы \(z\)-блока, то \(z_i = z_{i-l}\) — больше \(z_i\) быть не может. Если он упирается в границу, то «обрежем» его до неё и будем увеличивать на единичку.
                  • +
                  + +

                  Асимптотика. В алгоритме мы делаем столько же действий, сколько раз сдвигается правая граница z-блока — а это \(O(n)\).

                  +

                  Сравнение

                  +

                  В целом они зет- и префикс-функции очень похожи, но алгоритм Кнута-Морриса-Пратта есть во всех классических учебниках по программированию, а про z-функцию почему-то мало кто знает кроме олимпиадных программистов.

                  +

                  Про префикс-функцию важно ещё знать, что она онлайновая — достаточно считать следующий символ, и сразу можно узнать значение.

                  +

                  Упражнение 1. Дан массив префикс-функции. Исходная строка не дана. Вычислите за \(O(n)\) зет-функцию этой строки.

                  +

                  Упражнение 2. Дан массив зет-функции. Исходная строка не дана. Вычислите за \(O(n)\) префикс-функцию этой строки.

                  + + From 20c6a00a1832560faa901d60e3dba8ecd6d58275 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 18 Jun 2019 00:02:06 +0000 Subject: [PATCH 086/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ru/index.html b/ru/index.html index 71ac66f..03aebea 100644 --- a/ru/index.html +++ b/ru/index.html @@ -66,7 +66,7 @@

                  Общие техники

                  Математика

                  @@ -98,7 +98,7 @@

                  Строки

                • Префикс- и z-функция
                • Алгоритм Ахо-Корасик
                • Суффиксный массив
                • -
                • Суффиксный автомат
                • +
                • Суффиксный автомат
                • Палиндромы
                @@ -113,8 +113,8 @@

                Геометрия

                Теория игр

                From f9be115ded785c9bedaa5942f02a31d799ce2291 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 18 Jun 2019 11:22:41 +0000 Subject: [PATCH 087/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/dp-optimizations.html | 76 ++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index af676e7..5a99025 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -4,7 +4,7 @@ - Оптимизации динамики + Пересчёт динамики по слоям + @@ -48,45 +49,45 @@

                Алгоритмика

                Структуры данных

                Общие техники

                Математика


                Комбинаторика и ТЧ

                Графы

                @@ -94,33 +95,33 @@

                Графы

                Строки


                Геометрия

                Теория игр

                Разное

                From 24e4a7f00dde39bf4492e02ecac67117859ceec2 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 19 Jun 2019 06:34:08 +0000 Subject: [PATCH 093/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/hld.html | 29 ++++++++++++----------------- ru/lca.html | 14 +++++++------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/ru/hld.html b/ru/hld.html index 1e75e0c..ad759fd 100644 --- a/ru/hld.html +++ b/ru/hld.html @@ -111,13 +111,9 @@

                Heavy-light декомпозиция

                Heavy-light декомпозиция

                HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать что-нибудь попроще.

                -*
                -TODO: найти менее уродливую иллюстрацию +TODO: найти менее уродливую иллюстрацию
                -
                  -
                • -

                Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины \(v\) посмотрим на всех её непосредственных детей \(u\), выберем среди них ребёнка \(u_{max}\) (с самым большим размером поддерева) и назовём ребро \((v, u)\) тяжелым (heavy), а все остальные рёбра — лёгкими (light). При этом, если самых «тяжелых» детей будет больше одного, то выберем любого.

                Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.

                Утверждение. Дерево разбивается на непересекающиеся пути из тяжелых рёбер.

                @@ -134,7 +130,7 @@

                Heavy-light декомпозици

                Реализация

                Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.

                А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то tin-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.

                -

                Теперь мы можем построить какую-нибудь структуру поверх массива размера \(n\) (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре.

                +

                Теперь мы можем построить какую-нибудь структуру поверх массива размера \(n\) (дерево отрезков) и при запросе к какому-нибудь тяжелому пути делать запрос к отрезку в структуре.

                + tin[v] = t++; + for (int u : g[v]) { + // если это тяжелый ребенок -- его next нужно передать + // в противном случае он сам является головой нового пути + head[u] = (u == g[v][0] ? head[v] : u); + hld(u); + } + tout[v] = t; +}

                Как им решать задачи

                Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:

                  @@ -177,7 +172,7 @@

                  Как им решать зада }

                Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям.

                int ancestor (int a, int b) {
                -    return tin[a] <= tin[b] && tin[b] <= tout[a];
                +    return tin[a] <= tin[b] && tin[b] < tout[a];
                 }
                 
                 void up (int &a, int &b, int &ans) {
                diff --git a/ru/lca.html b/ru/lca.html
                index 0cb5980..1dcc2a6 100644
                --- a/ru/lca.html
                +++ b/ru/lca.html
                @@ -142,7 +142,7 @@ 

                Напоминание: DFS

              1. Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации.
              2. Запросы на поддеревьях

                -

                Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через ДО.

                +

                Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через дерево отрезков.

                Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число \(x_i\) и найти значение числа у вершины \(v_i\).

                @@ -164,7 +164,7 @@

                Наименьший об

                Для лучшего понимания: медленно (за линейное время) это можно делать так:

                Пусть поступил очередной запрос — пара вершин \((u, v)\):

                -

                Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная.

                +

                Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная.

                Сведение к RMQ

                Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.

                Во втором массиве мы по сути выписали наш проход dfs-а.

                -

                Заметим, что размер полученного массива - \(O(n)\). Доказательство - каждое ребро мы пройдем дважды (заходя в и выходя из поддерева), при просмотре каждого ребра мы выпишем не более, чем две вершины (потому что у каждого ребра всего два конца). Тогда размер массива не более, чем в четыре раза больше числа ребер, которых \(n - 1\).

                +

                Заметим, что размер полученного массива — \(O(n)\). Доказательство — каждое ребро мы пройдем дважды (заходя в и выходя из поддерева), при просмотре каждого ребра мы выпишем не более, чем две вершины (потому что у каждого ребра всего два конца). Тогда размер массива не более, чем в четыре раза больше числа ребер, которых \(n-1\).

                Пусть у нас есть запрос: найти LCA вершин \(v\) и \(u\). Для определенности положим, что \(tin_v < tin_u\). Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из \(v\) и в первй раз вошли в \(u\). Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.

                Получается, что можно найти LCA, просто найдя позицию минимума на отрезке \([tout_v, tin_u]\) в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы уже умеем.

                Разреженная таблица

                @@ -231,9 +231,9 @@

                Разреженная табли

                Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за \(O(1)\), но использующая \(O(n \log n)\) препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в отдельной статье.

                А наоборот можно?*

                Примечание: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

                -

                Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — любые два элемента отличаются не более, чем на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.

                +

                Интересный с теоретической точки зрения факт: LCA сводится к RMQ без изменения асимптотики, но не наоборот. Почему это так? Потому что на самом деле мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — любые два элемента отличаются ровно на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос. Наоборот, к несчатью, свести нельзя.

                Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера \(n-1\): единица стоит, если следующее значение больше, единица в противном случае ноль.

                -

                Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

                +

                Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

                \[O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n)\]

                Также посчитаем для каждой возможной маски размера \(\frac{\log n}{2}\) минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего \(\sqrt n\) (ради этого мы и делили логарифм на два).

                ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.

                From 7c4b7c82a9fc524cca13215a480aeab0eac91700 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 19 Jun 2019 09:33:04 +0000 Subject: [PATCH 094/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 6 +++--- ru/index.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pandoc.css b/pandoc.css index 734d8d6..bf6adfc 100644 --- a/pandoc.css +++ b/pandoc.css @@ -57,9 +57,9 @@ body { .contents li { font-size: 8px; - color: #CCC; + color: lightgrey; font-family: 'Open Sans', sans; - height: 28px; + height: 30px; line-height: 8px; } @@ -81,7 +81,7 @@ body { font-family: 'Times New Roman', sans-serif; display: block; font-size: 16px; - margin-bottom: 6px; + margin-bottom: 5px; } a { diff --git a/ru/index.html b/ru/index.html index 27a05ef..99e43ee 100644 --- a/ru/index.html +++ b/ru/index.html @@ -88,7 +88,7 @@

                Графы

              3. Паросочетания лемма бержа, алгоритм куна, лемма холла, покрытие dag-а
              4. Поток минимальной стоимости критерий оптимальности, потенциалы джонсона
              5. Наименьший общий предок запросы на путях и уровнях, двоичные подъемы, lca \(\leftrightarrow\) rmq
              6. -
              7. Центроидная декомпозиция
              8. +
              9. Центроидная декомпозиция подсчёт путей с заданным свойством, запросы на путях
              10. Heavy-light декомпозиция
              11. @@ -96,7 +96,7 @@

                Графы

                Строки


                -
                + From c89a17cecf323ce5221a0de5a048edd95eb31453 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 19 Jun 2019 13:49:40 +0000 Subject: [PATCH 095/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/eratosphenes.html | 49 +++++++++++ ru/euclid.html | 190 +++++++++++++++++++++++++++++++++++++++++++ ru/index.html | 2 + 3 files changed, 241 insertions(+) create mode 100644 ru/eratosphenes.html create mode 100644 ru/euclid.html diff --git a/ru/eratosphenes.html b/ru/eratosphenes.html new file mode 100644 index 0000000..48a3d91 --- /dev/null +++ b/ru/eratosphenes.html @@ -0,0 +1,49 @@ + + + + + + + Решето Эратосфена + + + + + + + + + + + + + +
                +

                Решето Эратосфена

                +
                +

                Решето Эратосфена

                + + diff --git a/ru/euclid.html b/ru/euclid.html new file mode 100644 index 0000000..d4602cf --- /dev/null +++ b/ru/euclid.html @@ -0,0 +1,190 @@ + + + + + + + Алгоритм Евклида + + + + + + + + + + + + + + + +
                +

                Алгоритм Евклида

                +
                +

                Алгоритм Евклида

                +

                Наименьшим общим делителем (англ. greatest common divisor) целых неотрицательных чисел \(a\) и \(b\) называется наибольшое число \(x\), которое делит одновременно и \(a\), и \(b\).

                +

                \[ +\gcd(a, b) = \max_{k: \; k|a \, \land \, k | b} k +\]

                +

                Когда оба числа равны нулю, результат не определён — подойдёт сколько угодно большое число. Однако в этом случае мы положим в этом случае \(\gcd\) равным тоже нулю, чтобы можно было использовать следующее правило: если одно из чисел равно нулю, то их \(\gcd\) равен второму числу.

                +

                Алгоритм Евклида находит \(gcd\) двух чисел \(a\) и \(b\) за \(O(\log \min(a, b))\). Он известен с IV века до нашей эры, а возможно и ранее.

                +

                Сам алгоритм основывается на следующей формуле, которую мы докажем позже:

                +

                \[ +\gcd(a, b) = \begin{cases} +a, & b = 0 \\ +\gcd(b,\, a \bmod b), & b > 0 +\end{cases} +\]

                +

                Эту формулу очень просто реализовать:

                + +

                Чуть более быстрая итеративная форма:

                + +

                В компиляторе gcc для этого уже есть встроенная функция __gcd.

                +

                Корректность

                +

                Из того, что второй аргумент \(\gcd\) всегда уменьшается и не может стать отрицательным, будет следовать, что алгоритм Евклида всегда хотя бы завершается.

                +

                Чтобы доказать корректность алгоритма, нам нужно доказать равенство \(g = \gcd(a, b) = \gcd(b, a \bmod b)\) для любых \(a \geq 0\), \(b > 0\).

                +

                Разложим остаток от деления \(a\) на \(b\) через их частное:

                +

                \[ +a \bmod b = a - b \cdot \Big \lfloor \frac{a}{b} \Big \rfloor +\]

                +

                По определению, \(g = \gcd(a, b)\) делит \(a\) и \(b\). Следовательно, \(g\) делит и эту разность, а значит и \((a \bmod b)\).

                +

                Так как \(g\) делит числа \(b\) и \((a \bmod b)\), то оно должно также делить \(\gcd(b, a \bmod b)\).

                +

                Мы получили, что \(g = \gcd(a, b)\) делит \(\gcd(b, a \bmod b)\). Можно провести симметричные рассуждения и показать, что \(\gcd(b, a \bmod b)\) делит \(\gcd(a, b)\), из чего будет следовать, что они равны.

                +

                Асимптотика

                +

                Заметим, что в алгоритме поддерживается инвариант \(a > b\), кроме, возможно, первой итерации. Так как \(a \bmod b < b\) и \(a > b\), то \(a\) уменьшится хотя бы в два раза.

                +

                Самые худшие входные данные для алгоритма — последовательность Фибоначчи.

                +

                Также по этой причине нахождение \(\gcd\) от \(n\) чисел будет работать за \(O(n + \log x)\), где \(x\) это минимальное из них.

                +

                Решение диофантовых уравнений

                +

                Для обычного использования \(gcd\) не нужно даже знать, как алгоритм устроен — он есть даже в компиляторе.

                +

                Расширенный алгоритм Евклида находит помимо \(g = \gcd(a, b)\) также коэффициенты \(x\) и \(y\) такие, что

                +

                \[ +a \cdot x + b \cdot y = g +\]

                +

                Эта версия значима, потому что с помощью неё можно искать обратный элемент по модулю: нужно найти такой элемент \(a^{-1}\), что \(a \cdot a^{1} \equiv 1\), что то же самое, что:

                +

                \[ +a^{-1} \cdot a + k \cdot m = 1 +\]

                +

                Модифицируем стандартный алгоритм Евклида, чтобы учесть в нём нахождение этих коэффициентов.

                +

                Пусть мы посчитали эти коэффициенты когда рекурсивно считали \(gcd(b, a \bmod b)\). Иными словами, у нас есть решение \((x, y)\) для пары \((b, a \bmod b)\):

                +

                $$

                +

                $$

                +

                Чтобы получить решение для исходной пары, преобразуем величину \((a \bmod b)\) как \(todo\) и подставим в преведённое выше выражение:

                +

                $$

                +

                $$

                +

                Если выполнить перегруппировку слагаемых, можно получить:

                +

                $$

                +

                $$

                +

                Реализация

                + +

                Эта рекурсивная функция по прежнему возвращает значение \(gcd(a, b)\), но помимо этого записывает в \(x\) и \(y\) искомые коэффициенты.

                + + diff --git a/ru/index.html b/ru/index.html index 99e43ee..5dd4dc6 100644 --- a/ru/index.html +++ b/ru/index.html @@ -76,6 +76,8 @@

                Математика

                Комбинаторика и ТЧ

                  +
                • Алгоритм Евклида расширенный алгоритм евклида, диофантовы уравнения
                • +
                • Решето Эратосфена линейное решето, факторизация всех чисел от 1 до n
                • Нахождение обратного по модулю малая теорема ферма, алгоритм евклида, возведение в степень
                • Жадный алгоритм Радо-Эдмондса матроиды, жадный набор паросочетаний, расписаний, базисов
                • Ро-алгоритм Полларда быстрая факторизация, пародокс дней рождений
                • From 9058bf576d832916db725b1ee0d555283a8cff1b Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 19 Jun 2019 23:03:58 +0000 Subject: [PATCH 096/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/linalg.html | 314 +++++++++++++++++++++++++++++-------------------- 1 file changed, 184 insertions(+), 130 deletions(-) diff --git a/ru/linalg.html b/ru/linalg.html index 16e2b56..c7cad48 100644 --- a/ru/linalg.html +++ b/ru/linalg.html @@ -109,17 +109,29 @@

                  Ликбез по линейной алгебре

                  Ликбез по линейной алгебре

                  -

                  Определение. Функция \(f: \mathbb{R}^n \to \mathbb{R}^m\) называется линейной, если для неё выполнено

                    -
                  1. $ f(x+y) = f(x) + f(y) $
                  2. -
                  3. $ f(ax) = a f(x), ; a R $
                  4. +
                  5. $ f(x+y) = f(x) + f(y)$
                  6. +
                  7. \(f(ax) = a f(x), \; a \in \mathbb{R}\)
                  -

                  Примеры: * $ f(x) = 0 $ * $ f(x) = x $ * $ f(x) = 2 x_1 - 2 x_2 - 8 x_3 $ (из \(\mathbb{R}^3\) в \(\mathbb{R}\)) * $ f(x) = (x, -x, 0) $ (из \(\mathbb{R}\) в \(\mathbb{R}^3\))

                  -

                  Линейная алгебра занимается изучением линейных функций. Некоторые полезные свойства: * Сумма линейных функций — линейная функция. * Сумма коммутативна: \(f+g = g+f\)). * Сумма ассоциативна: \((f+g)+h = f+(g+h)\). * Композиция \(f(g(x)) = (f \circ g)(x)\) линейных функций — линейная функция. * Композция ассоциативна: \((f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h\). * Композиция в общем случае не коммутативна.
                  Пример: \(f = (-x_2, x_1)\) — поворот точки на плоскости на прямой угол, \(g = (x_1, 0)\) — проекция на \(Ox\). Почти для всех точек порядок этих операций важен.

                  -

                  Все свойства можно вывести лишь из этих двух пунктов в определении.

                  -

                  Что такое матрица?

                  -

                  Можно показать, что любую линейную функцию \(f: \mathbb{R}^n \to \mathbb{R}^m\) можно представить в таком виде:

                  +

                  Примеры:

                  +
                    +
                  • Функция, которая «растягивает» вектор в \(k\) раз: \(f(x) = k x\).
                  • +
                  • Функция, которая поворачиват вектор на плоскости на угол \(\theta\).
                  • +
                  • Функция, которая проецирует трёхмерный вектор на какую-нибудь плоскость.
                  • +
                  • Скалярное произведение \(f(x, y) = x \cdot y = \sum x_ky_k\) также линейно по обоим параметрам.
                  • +
                  +

                  Линейная алгебра занимается изучением линейных функций. Из одних лишь двух пунктов в определении можно вывести много полезных свойств:

                  +
                    +
                  • Сумма линейных функций является линейной функцией.
                  • +
                  • Композиция линейных функций \(f(g(x)) = (f \circ g)(x)\) является линейной функцией.
                  • +
                  • Сумма линейных функций коммутативна: \(f+g = g+f\).
                  • +
                  • Сумма линейных функций ассоциативна: \((f+g)+h = f+(g+h)\).
                  • +
                  • Композция линейных функций ассоциативна: \((f \circ g) \circ h = f \circ (g \circ h) = f \circ g \circ h\).
                  • +
                  • Композиция в общем случае не коммутативна. Пример: \(f(x) = (-x_2, x_1)\) — поворот точки на плоскости на прямой угол, \(g(x) = (x_1, 0)\) — проекция на \(Ox\). Почти для всех точек плоскости важен порядок этих двух операций.
                  • +
                  +

                  Матрицы

                  +

                  Можно показать, что любую линейную функцию \(f: \mathbb{R}^n \to \mathbb{R}^m\) можно представить в следующем виде:

                  \[ f(x) = \begin{pmatrix} @@ -139,66 +151,154 @@

                  Что такое матрица?

                  a_{m1} & a_{m2} & \ldots & a_{mn} \\ \end{pmatrix} \]

                  -

                  Каждой линейной функции из \(\mathbb{R}^n\) в \(\mathbb{R}^m\) соответствует какая-то матрица размера \(n \times m\) (первое число это количество строк, второе — столбцов), и наоборот. Элемент на пересечении \(i\)-го строки и \(j\)-го столбца будем обозначать \(A_{ij}\). Не перепутайте.

                  -

                  Пусть линейной функции \(f\) соответствует матрица \(A\), а функции \(g\)\(B\). Тогда композиции этих функций \(h = f \circ g\) будет соответствовать произведение \(C\) матриц \(A\) и \(B\), определяемое так:

                  -

                  \[ C = AB: C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} \]

                  -

                  Можете убедиться в этом, расписав, какие коэффициенты получаются, если формулы из \(g\) подставить в \(f\).

                  -

                  Когда перемножаете руками, удобно думать так: элемент на пересечении \(i\)-го столбца и \(j\)-той строки — это скалярное произведение \(i\)-той строки \(A\) и \(j\)-того столбца \(B\). Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер \(n \times k\), то вторая должна иметь размер \(k \times m\), то есть «средние» размерности обязательно должны совпадать.

                  -

                  +

                  Каждой линейной функции \(f\) из \(\mathbb{R}^n\) в \(\mathbb{R}^m\) соответствует какая-то матрица \(A\) размера \(n \times m\) (первое число это количество строк, второе — столбцов) и наоборот. Элемент на пересечении \(i\)-ой строки и \(j\)-го столбца будем обозначать \(A_{ij}\). Не перепутайте.

                  +

                  Если вектор — это упорядоченный набор скаляров, то матрицу можно рассматривать как вектор векторов. Вектор, в частности, можно представить как матрицу, у которой одна из размерностей равна единице — тогда его называют вектор-столбец либо вектор-строка.

                  +

                  Напишем класс для работы с матрицами, который пока что будет просто оборачивать двумерные массивы:

                  + +

                  Ещё есть тензоры — ими называют все объекты ещё более высокого порядка: векторы матриц (трёхмерный тензор), матрицы матриц (четырёхмерный тензор) и векторы матриц матриц и так далее.

                  +

                  +

                  У тензоров есть своя интересная алгебра, но в контекстах, с которыми с ними может столкнуться программист, никакая алгебра обычно не подразумневается, и термин используется лишь потому, что в словосочетании «многомерный массив» слишком много букв.

                  +

                  Матричное умножение

                  +

                  Пусть линейной функции \(f\) соответствует матрица \(A\), а функции \(g\) соответствует матрица \(B\). Тогда композиции этих функций \(h = f \circ g\) будет соответствовать произведение \(C\) матриц \(A\) и \(B\), определяемое следующим образом:

                  +

                  \[ +C = AB: \; C_{ij} = \sum_{i=1}^{k} A_{ik} B_{kj} +\]

                  +

                  Читатель может убедиться в этом, расписав, какие коэффициенты получаются, если формулы из \(g\) подставить в \(f\).

                  +

                  При перемножении матриц на белковых процессорах удобно думать так: элемент на пересечении \(i\)-го столбца и \(j\)-той строки — это скалярное произведение \(i\)-той строки \(A\) и \(j\)-того столбца \(B\). Заметим, что это накладывает ограничение на размерности перемножаемых матриц: если первая матрица имеет размер \(n \times k\), то вторая должна иметь размер \(k \times m\), то есть «средние» размерности обязательно должны совпадать.

                  +

                  Исходное выражение для \(f(x)\) теперь можно компактно записать как \(f(x) = Ax\) вместо \(m\) уравнений с \(n\) слагаемыми в каждом.

                  -

                  К матрицам не нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.

                  -

                  Из-за такого взаимно однозначного соотношения все ранее упомянутые свойства линейных функций переносятся и на матрицы:

                  +

                  Напишем внешнюю функцию, реализующую матричное умножение:

                  + +

                  Эта реализация очень проста, но неоптимальна.

                  +

                  BLAS. Кэш-линии.

                  +

                  Транспонирование.

                  +

                  Кстати, наука знает и более быстрые способы перемножить матрицы, но на контестах они не нужны.

                  +

                  Свойства матриц

                  +

                  К матрицам не нужно относиться как к табличкам, в которых стоят какие-то числа. Каждой матрице соответствует какая-то линейная функция, как-то преобразующая вектора. Центральными объектами линейной алгебры являются именно линейные функции, а не матрицы.

                  +

                  Благодаря этому взаимно однозначному соотношению все ранее упомянутые свойства линейных функций переносятся и на матрицы:

                    -
                  • Сумма матриц \(A\) и \(B\) — матрица \(C = A+B: C_{ij} = A_{ij} + B_{ij}\).
                  • -
                  • Сумма коммутативна: \(A+B = B+A\))
                  • -
                  • Сумма ассоциативна: \((A+B)+C = A+(B+C)\)
                  • -
                  • Умножение ассоциативно: \((AB)C = A(BC) = ABC\).
                  • -
                  • Умножение в общем случае не коммутативно.
                  • +
                  • Сумма матриц \(A\) и \(B\) тоже является матрицей: \(C = A+B: \; C_{ij} = A_{ij} + B_{ij}\).
                  • +
                  • Сумма матриц коммутативна: \(A+B = B+A\).
                  • +
                  • Сумма матриц ассоциативна: \((A+B)+C = A+(B+C)\).
                  • +
                  • Умножение матриц ассоциативно: \((AB)C = A(BC) = ABC\).
                  • +
                  • Умножение матриц в общем случае не коммутативно.
                  -

                  Пример: матрица поворота в 2d. \[ +

                  Есть специальная матрица \(I\), которая называется единичной и относительно умножения ничего не делает, то есть ведёт себя как единица. У неё единицы на главной диагонали и нули вне неё:

                  +

                  \[ +I_3 = \begin{pmatrix} +1 & 0 & 0 \\ +0 & 1 & 0 \\ +0 & 0 & 1 \\ +\end{pmatrix} +\]

                  +

                  Некоторые линейные функции обратимы (например, поворот на угол), некоторые — необратимы (например, проекция). Понятие обратимости можно продолжить и на матрицы.

                  +

                  Определение. Матрица \(A\) является обратимой, если существует матрица \(A^{-1}\) такая, что \(A \cdot A^{-1} = I\).

                  +

                  Из формулы следует, что матрица \(A\) должна быть хотя бы квадратной. Обратную матрицу можно находить за \(O(n^3)\) методом Гаусса, который мы рассмотрим дальше. TODO

                  +

                  Примеры матриц

                  +

                  Матрица «увеличь всё в два раза»:

                  +

                  \[ +\begin{pmatrix} +2 & 0 & 0 \\ +0 & 2 & 0 \\ +0 & 0 & 2 \\ +\end{pmatrix} +\]

                  +

                  Матрица «поменяй \(x\) и \(y\) местами»:

                  +

                  \[ +\begin{pmatrix} +0 & 1 \\ +1 & 0 \\ +\end{pmatrix} +\]

                  +

                  Матрица поворота на угол \(\alpha\) на плоскости:

                  +

                  \[ \begin{pmatrix} \cos \alpha & -\sin \alpha \\ \sin \alpha & \cos \alpha \\ \end{pmatrix} \]

                  -

                  Пример: матрица проецирования на \(Ox\) в 3d. \[ +

                  Матрица проецирования на плоскость \(xy\) в трёхмерном пространстве:

                  +

                  \[ \begin{pmatrix} 1 & 0 & 0 \\ -0 & 0 & 0 \\ +0 & 1 & 0 \\ 0 & 0 & 0 \\ \end{pmatrix} \]

                  -

                  Пример: матрица «свапни \(x\) и \(y\)». \[ -\begin{pmatrix} -0 & 1 \\ -1 & 0 \\ -\end{pmatrix} +

                  Применения вне геометрии

                  +

                  Пока что все примеры были про геометрию. Так просто проще думать: представлять в уме повороты и растяжения векторов на плоскости легче, чем рассуждать о свойствах абстрактных n-мерных пространств.

                  +

                  Однако мы программисты, и нас в основном интересуют задачи вне геометрии.

                  +

                  Динамическое программирование

                  +

                  Переходы в некоторых динамиках иногда можно выразить в терминах матричного умножения.

                  +

                  Рассмотирм конкретный пример. Пусть у нас есть ориентированный граф \(G\), заданный своей матрицей смежности, и нам нужно найти количество путей из \(s\) в \(t\) через ровно \(k\) переходов. Размер графа маленький по сравнению с \(k\).

                  +

                  Обычно, мы бы ввели динамику \(f_{i,v}\), равную количеству способов дойти до \(v\)-той вершины через ровно \(i\) переходов. Пересчёт — перебор предпоследней вершины в пути:

                  +

                  \[ +f_{i,v} = \sum_u g_{uv} \cdot f_{i-1,u} \]

                  -

                  Напишем класс, который реализует матричное умножение.

                  - -

                  Динамика

                  -

                  Некоторые динамики можно выразить в терминах матричного умножения.

                  -

                  Расмотрим конкретный пример — Фибоначчи. Ну а чем не динамика?

                  +

                  Выясняется, что \(f_{i}\) зависит от \(f_{i-1}\) линейно: \(v\)-тый элемент получается скалярным умножением \(f_{i-1}\) и \(v\)-того столбца матрицы смежности \(G\).

                  +

                  Значит, имеет место следующее равенство:

                  +

                  \[ +f_i = f_{i-1} G +\]

                  +

                  Поэтому \(G\) и называют матрицей смежности.

                  +

                  Получается, \(f_k\) можно раскрыть как \(f_k = f_0 \cdot G \cdot G \cdot \ldots \cdot G\). Вспоминая, что умножение матриц коммутативно, по аналогии со скалярами для вычисления \(G^k\) мы можем применить бинарное возведение в степень:

                  +

                  \[ +A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) +\]

                  +

                  По этой формуле нам нужно \(\log k\) раз перемножить две матрицы размера \(n\), что суммарно будет работать за \(O(n^3 \log k)\).

                  + +

                  В получившейся матрице в ячейке \(g_{ij}\) будет находиться количество способов дойти из \(i\)-той вершины в \(j\)-тую, используя ровно \(k\) переходов. Ответом нужно просто вывести \(g_{st}\).

                  +

                  Практически не меняя алгоритм, можно решить задачу «с какой вероятностью я попаду из вершины \(s\) в вершину \(t\)», если вместо матрицы смежности даны вероятности, с которыми мы переходим из вершины в вершину в марковской цепи.

                  +

                  Если нам не нужно количество способов, а только сам факт, можно ли дойти за ровно \(k\) переходов, то можно обернуть матрицу в битсеты и сильно ускорить решение.

                  +

                  Если нам нужно «за не более \(k\) переходов», то можно вместо \(k\)-той степени считать сумму геометрической прогрессии.

                  +

                  Подобную технику можно применить и к другим динамикам, где нужно посчитать количество способов что-то сделать — иногда очень неочевидными способами. Например, можно найти количество строк длины большого \(k\), не содержащих данные маленькие подстроки, построив граф легальных переходов в Ахо-Корасике.

                  +

                  Внутри матричного умножения не обязательно использовать сложение и умножение — это могут быть и какие-нибудь другие операции. Например, для нахождения минимального пути. TODO: более адекватный пример

                  +

                  Линейные рекурренты

                  +

                  Расмотрим другой пример динамики — последовательность Фибоначчи. Ну а чем не динамика?

                  +

                  \[ +f_{n} = f_{n-1} + f_{n-2} +\]

                  +

                  Как мы видим, \(n\)-ный элемент линейно выражается через два предыдущих.

                  +

                  Вообще, последовательностей Фибоначчи много — им не обязательно начинаться с 0 и 1. Для однозначной идентификации достаточно знать позиции и значения любых двух различных элементов. Поэтому в качестве состояния динамики введём.

                  \[ \begin{pmatrix} f_{n+1} \\ @@ -219,73 +319,27 @@

                  Динамика

                  f_{n+1} \\ \end{pmatrix} \]

                  +

                  Также можно «шагать» в обратном направлении, если обратить эту матрицу.

                  Обозначим за \(A\) эту матрицу перехода. Чтобы посчитать \(n\)-е число Фибоначчи, нужно применить \(n\) раз эту матрицу к вектору \((f_0, f_1) = (0, 1)\).

                  -

                  Вспомним, что умножение коммутативно. Значит, мы можем применить бинарное возведение в степень к матрицам:

                  -

                  \[ A^8 = A^{2^{2^{2}}} = ((AA)(AA))((AA)(AA)) \]

                  -

                  Это будет работать за \(O(n^3 \log n)\). Мы делаем \(O(n^3)\) операций для одного умножения, а всего их нужно сделать \(O(\log n)\). Кстати, наука знает и более быстрые способы перемножить матрицы, но на контестах они не нужны.

                  - -

                  Единичной называется матрица, у которой единицы стоят на главной диагонали. Обозначение: \(I\).

                  -

                  \[ -\begin{vmatrix} -1 & 0 & 0 \\ -0 & 1 & 0 \\ -0 & 0 & 1 \\ -\end{vmatrix} -\]

                  -

                  В плане умножения она действительно ведет себя как единица: \(AI = A = IA\). В коде она используется вместо единицы.

                  В общем случае, линейная рекуррента \(f_n = a_1 f_{n-1} + a_2 f_{n-2} + \ldots + a_k f_{n-k}\) имеет такую матрицу перехода:

                  -

                  \begin{pmatrix} 0 & 1 & 0 & & 0 \ 0 & 0 & 1 & & 0 \ & & & & \ 0 & 0 & 0 & & 1 \ a_k & a_{k-1} & a_{k-2} & & a_1 \ \end{pmatrix}

                  -

                  Матрица смежности

                  -

                  У перемножения матриц смежности есть комбинаторный смысл — количество способов попасть из вершины \(a\) в вершину \(b\) за определенное количество переходов. Иными словами, (G^n)_{ab} будет равно количеству способов попасть из \(a\) в \(b\), используя ровно \(n\) переходов.

                  -

                  Когда нам нужна только информация, можно ли дойти из \(a\) в \(b\) (количество способов не важно), то решение можно ускорить (см. Битсет).

                  -

                  Обобщения

                  -

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

                  -

                  Полями и кольцами в общей алгебры называют множества, на которых определены какие-то операции для которых выполняются особые свойства. Например есть поле комплексных чисел, или кольцо многочленов, или кольцо вычетов по модулю два.

                  -

                  Определитель

                  -

                  Определителем кадратной матрицы \(A\) называется такое выражение:

                  -

                  \[ \det A = |A| = \ldots \]

                  -

                  Где \(\epsilon\) — чётность числа инверсий в перестановке (\(-1\) или \(+1\)). То есть, чтобы его посчитать «в лоб», нужно перебрать все перестановки элементов, взять соответствующие элементы со столбцов и добавить в сумму с нужным знаком. Например:

                  -

                  \[ -\begin{vmatrix} -a & b \\ -c & d \\ -\end{vmatrix} -= -ad - bc -\]

                  -

                  У него есть куча полезных свойств. Например, если определитель матрицы равен нулю, то соответствующая система не решается. Определитель произведения равен произведению определителей, и так далее. Много полезных (особенно для доказательств) свойств, которые рассказываются в университетских программах, но на них мы не будем останавливаться.

                  -

                  В двумерном случае его значение равно ориентированной площади (проверьте — сравните с формулой для векторного произведения). Для трехмерного — объему, в общем случае это какой-то «гипер-объем». Доказывать это утверждение мы не будем.

                  -

                  Базис

                  -

                  Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их.

                  -

                  Базисы есть не только в линейной алгебре. Например, \(\{1, x, x^2\}\) является базисом всех квадратных трёхчленов. Или \(\{\neg, \land, \lor\}\) является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом.

                  -

                  Метод Крамера и easy пересечение прямых

                  -

                  Пусть нам надо пересечь две прямые.

                  \[ -\begin{cases} -a_1 x + b_1 y + c_1 = 0 -a_2 x + b_2 y + c_2 = 0 -\end{cases} +\begin{pmatrix} +0 & 1 & 0 & \ldots & 0 \\ +0 & 0 & 1 & \ldots & 0 \\ +\vdots & \vdots & \vdots & \ddots & \vdots \\ +0 & 0 & 0 & \ldots & 1 \\ +a_k & a_{k-1} & a_{k-2} & \ldots & a_1 \\ +\end{pmatrix} \]

                  -

                  Это то же самое, что найти такие коэффициенты \(x\) и \(y\), что

                  -

                  \[ x \vec{a} + y \vec{b} = -\vec{c} \]

                  -

                  Площадь параллелограмма, натянутого на \(\vec{a}\) и \(\vec{b}\), равна векторному произведению, или детерминанту.

                  -

                  По сути, нам нужно выразить \(c\) в другом базисе. Давайте спроецируем её на \(a\).

                  -

                  Аналогично, напрягите воображение и спроецируйте эту точку в \(n\)-мерном пространстве. Это уже сложно, да?

                  -

                  *Собственные векторы

                  -

                  Очень часто у матриц есть собственные вектора – те, которые не меняют направление.

                  +

                  TODO

                  +

                  Собственные векторы

                  +

                  Очень часто у матриц есть собственные векторы — те, которые не меняют направление при применении этой матрицы к ним.

                  $ Av = k v $, где \(k \neq 0\).

                  $ Av - kv = (A-kI)v = 0 $. Это означает

                  Вообще, собственные вектора в линейной алгебре имеют почти такое же значение, как простые числа в арифметике.

                  +

                  Линейная независимость

                  +

                  Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их.

                  +

                  Базисы есть не только в линейной алгебре. Например, \(\{1, x, x^2\}\) является базисом всех квадратных трёхчленов. Или \(\{\neg, \land, \lor\}\) является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом.

                  Системы уравнений и метод Гаусса

                  Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

                  Нас по сути просят решить следующую систему:

                  @@ -300,22 +354,22 @@

                  Систе

                  Здесь \(x\) — состояния переключателей, \(b\) — состояния лампочек, \(A\) — информация о том, влияет ли переключатель на лампочку.

                  Метод Крамера неоптимален — там \(O(n^4)\) операций.

                  В таком случае можно значительно ускорить и упростить обычный метод Гаусса:

                  - +

                  Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода, предполагается, что вектор \(b\) приписан справа к матрице \(A\).

                  Часто эту систему нужно решить по модулю 2. Тогда код значительно упрощается и ускоряется (опять же, см. Битсет).

                  From 291bc24cd2242a9120b6b52c559147a3c64cfb96 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 19 Jun 2019 23:10:15 +0000 Subject: [PATCH 097/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/{games.html => games-on-graphs.html} | 0 ru/index.html | 5 ++- ru/sprague-grundy.html | 50 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) rename ru/{games.html => games-on-graphs.html} (100%) create mode 100644 ru/sprague-grundy.html diff --git a/ru/games.html b/ru/games-on-graphs.html similarity index 100% rename from ru/games.html rename to ru/games-on-graphs.html diff --git a/ru/index.html b/ru/index.html index 5dd4dc6..bd75719 100644 --- a/ru/index.html +++ b/ru/index.html @@ -87,9 +87,9 @@

                  Комбинаторика и ТЧГрафы

                  @@ -116,7 +116,8 @@

                  Геометрия

                  Теория игр

                  diff --git a/ru/sprague-grundy.html b/ru/sprague-grundy.html new file mode 100644 index 0000000..bd1f4c2 --- /dev/null +++ b/ru/sprague-grundy.html @@ -0,0 +1,50 @@ + + + + + + + Теория Шпрага-Гранди + + + + + + + + + + + + + +
                  +

                  Теория Шпрага-Гранди

                  +
                  +

                  Теория Шпрага-Гранди

                  +

                  TODO

                  + + From 355e864ed5a043bf4490831896784456781a31f6 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 22 Jun 2019 13:02:06 +0000 Subject: [PATCH 098/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- img/fenwick_ranges.png | Bin 0 -> 14161 bytes ru/centroid.html | 4 ++-- ru/fenwick.html | 1 + ru/hashing.html | 5 ++++- ru/karatsuba.html | 4 ++-- ru/matching.html | 1 + ru/matroid.html | 2 +- ru/mincost-maxflow.html | 2 +- ru/mst.html | 2 +- ru/sparse-table.html | 2 +- ru/string-searching.html | 2 +- 11 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 img/fenwick_ranges.png diff --git a/img/fenwick_ranges.png b/img/fenwick_ranges.png new file mode 100644 index 0000000000000000000000000000000000000000..cb3a38bcaf92d035f1ab10dc83dacc874cef113d GIT binary patch literal 14161 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 jЦентроидная

                  Заметим, что в конце всё дерево будет удалено, а значит каждая вершина ровно один раз побывала центроидом своей компоненты.

                  Теперь поймём, зачем мы всё это делали.

                  Утверждение. Каждая вершина входит в \(O(\log n)\) компонент.

                  -

                  Доказательство. Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «выжить» более \(\lceil \log_2 n \rceil\) разделений.

                  +

                  Доказательство. Центроид разбивает дерево на компоненты в хотя бы два раза меньшего размера. Значит, никакая вершина не может «прожить» более \(\lceil \log_2 n \rceil\) разделений.

                  Следствие. Центроидная декомпозиция (см. определение выше) работает за \(O(n \log n)\).

                  Утверждение. Для любого пути \(a \leadsto b\) есть единственный центроид \(c\), в чьей компоненте были и \(a\), и \(b\).

                  Доказательство. Каждая вершина когда-то была центроидом. Какая-то из вершин на пути была центроидом первой, и она навсегда разъединила \(a\) и \(b\).

                  @@ -203,7 +203,7 @@

                  Под used[v] = 1; for (int u : g[v]) if (!used[u]) - solve(centroid(u, v, s[u]/2)); + solve(centroid(u, v, s[u])); }

                Асимптотика \(O(n \log n)\), потому что на каждую из \(O(n)\) верщин мы потратим \(O(1)\) операций на каждом из \(O(\log n)\) «уровней» центроидной декомпозиции.

                Запросы на путях — offline

                diff --git a/ru/fenwick.html b/ru/fenwick.html index 7034549..1a5f308 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -131,6 +131,7 @@

                Определение

                Первый вариант описан на Викиконспектах и Емаксе и поэтому более известен. Второй, как мы дальше увидим, более простой для запоминания и кодинга, а так же более гибкий — например, там можно делать бинпоиск по префиксным суммам. Его мы и будем использовать.

                Примечание. Наверное, меньше четверти умеющих писать эту структуру полностью понимают, как она работает. Анализ действительно весьма сложный, поэтому мы приведём его в конце. Рекумендуется пока что абстрагироваться и принять на веру, что любой префикс разбивается на \(O(\log n)\) отрезков вида \([F(i), i]\), а также что любой элемент входит в не более \(O(\log n)\) таких отрезков.

                +

                Реализация

                Так как \(F(0) = 1 > 0\), то \([0, F(0)]\) не является корректным отрезком. Поэтому нам будет удобнее хранить массив в 1-индексации и не использовать \(t_0\).

                int t[maxn];
                diff --git a/ru/hashing.html b/ru/hashing.html
                index 7ff1074..63f392b 100644
                --- a/ru/hashing.html
                +++ b/ru/hashing.html
                @@ -202,7 +202,10 @@ 

                Зачем это нужно?

                return (h[r+1] - h[l]) * p[n-l] % mod; }

                Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за \(O(1)\).

                -

                Упражнение. Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще.

                +

                Упражнение. Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. Обратный хэш подстроки принято считать и использовать в стандартном виде из определения, поскольку там нет необходимости в делении.

                +

                Лайфхак. Если взять обратный полиномиальный хэш короткой строки на небольшом алфавите с \(k=10\), то числовое значение хэша строки будет красиво соотноситься с самой строкой (подумайте о десятичной системе счисления). \[ +h(abacaba)=1213121 +\] Этим очень удобно пользоваться при дебаге.

                Примеры задач

                Количество разных подстрок. Посчитаем хэши от всех подстрок за \(O(n^2)\) и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

                Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

                diff --git a/ru/karatsuba.html b/ru/karatsuba.html index d6dd64b..29eaeab 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -74,11 +74,11 @@

                Мастер-теорема

                A. Если \(c > \log_b a\), то \(\sum (\frac{a}{b^с})^k\) это сумма убывающей геометрической прогрессии, которая не зависит от \(n\) и просто равна какой-то константе. Значит, \(T(n) = \Theta(n^c)\).

                B. Если \(c = \log_b a\), то

                \[ -\sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n) +T(n) = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = n^c \sum_{k=0}^{\log_b n} 1^k = \Theta(n^c \log_b n) \]

                C. Если \(c < \log_b a\), то так как сумма прогрессии асимптотически эквивалентна своему старшему элементу,

                \[ -n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) +T(n) = n^c \sum_{k=0}^{\log_b n} (\frac{a}{b^c})^k = \Theta(n^c (\frac{a}{b^c})^{\log_b n}) = \Theta(n^c \cdot \frac{a^{\log_b n}}{n^c}) = \Theta(a^{\log_b n}) = \Theta(n^{\log_b a}) \]

                Примечание. Для более точных оценок асимптотики «мерджа» теорема ничего не говорит. Например, если мердж занимает \(\Theta(n \log n)\) и задача разбивается каждый раз на две части, то асимптотика будет равна:

                \[ diff --git a/ru/matching.html b/ru/matching.html index 4483cd8..8956416 100644 --- a/ru/matching.html +++ b/ru/matching.html @@ -109,6 +109,7 @@

                Паросочетания

                Паросочетания

                +

                Задача

                Пусть есть \(n\) мальчиков и \(m\) девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.

                Паросочетанием \(M\) называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из \(M\)).

                diff --git a/ru/matroid.html b/ru/matroid.html index 5903797..1047987 100644 --- a/ru/matroid.html +++ b/ru/matroid.html @@ -110,7 +110,7 @@

                Матроиды

                Матроиды

                Понятие матроида нужно, чтобы придумывать и доказывтать некоторые жадные алгоритмы в задачах, где нужно набрать какое-то множество объектов максимального веса. Например, минимальный остов, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени.

                -

                Если вы не специалист по абстрактной алгебре, рекомендуется сначала посмотреть примеры матроидов в конце статьи.

                +

                Если вы не специалист по абстрактной алгебре, рекомендуется подсматривать примеры матроидов в конце статьи.

                Определение

                Матроидом называется пара \((X, I)\), где \(X\) — множество элементов, называемое носителем матроида, а \(I\) — некоторое множество подмножеств \(X\), называемое семейством независимых множеств. В матроиде должны выполняться следующие свойства:

                  diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html index 2d7c3f8..66c9d28 100644 --- a/ru/mincost-maxflow.html +++ b/ru/mincost-maxflow.html @@ -160,7 +160,7 @@

                  Итоговый алгоритм

                Реализация

                -

                Ниже приведено решение задачи о назначениях (паросочетание минимального веса). Для нахождения дополняющего пути используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищется минимум за линию).

                +

                Ниже приведено решение задачи о назначениях (паросочетание минимального веса). Для нахождения дополняющего пути используется алгоритм Дейкстры для плотных графов (без приоритетной очереди — каждую итерацию ищется минимум за \(O(n)\)).

                • cost, cap — параметры сети
                • pot — потенциалы
                • diff --git a/ru/mst.html b/ru/mst.html index 9c4ab5b..1bee19b 100644 --- a/ru/mst.html +++ b/ru/mst.html @@ -255,7 +255,7 @@

                  Асимптотика

                  Тратить время на изучения доказательства или даже чтения статьи на Википедии про функцию Аккермана автор не рекомендует.

                  Алгоритм Крускала

                  Отсортируем рёбра и будем пытаться добавлять их в остов в порядке возрастания их весов. Если ребро соединяет какие-то две уже соединенные вершины, то проигнорируем его, иначе оно является безопасным, и его можно добавить.

                  -

                  Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Наивная проверка будет работать за \(O(m \log m + n^2)\), но асимптотику можно улучшить до \(O(n \log m)\) (до стоимости сортировки), если для проверок использовать систему непересекающихся множеств.

                  +

                  Звучит очень просто — отсортировать все рёбра, пройтись по ним циклом и делать проверку, что вершины в разных компонентах. Наивная проверка будет работать за \(O(m \log m + n^2)\), но асимптотику можно улучшить до \(O(m \log m)\) (до стоимости сортировки), если для проверок использовать систему непересекающихся множеств.

                  // (w, (a, b))
                   vector< pair< int, pair<int, int> > > edges;
                   
                  diff --git a/ru/sparse-table.html b/ru/sparse-table.html
                  index 6b60f92..2c14bda 100644
                  --- a/ru/sparse-table.html
                  +++ b/ru/sparse-table.html
                  @@ -144,7 +144,7 @@ 

                  2d Static RMQ

                  Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

                  В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

                  Примечания

                  -

                  В отличие от дерева отрезков, разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но не всегда — см. задачу «Антиматерия» с РОИ-2017).

                  +

                  В отличие от дерева отрезков, разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но можно достраивать на ходу — см. задачу «Антиматерия» с РОИ-2017).

                  Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.

                  Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (\(a ∘ (b ∘ c) = (a ∘ b) ∘ c\)), коммутативность (\(a ∘ b = b ∘ a\)) и идемпотентность (\(a ∘ a = a\)). Например, её можно применять для нахождения gcd.

                  Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

                  diff --git a/ru/string-searching.html b/ru/string-searching.html index 8b6776e..5050019 100644 --- a/ru/string-searching.html +++ b/ru/string-searching.html @@ -218,7 +218,7 @@

                  Как её быстро сч vector<int> z(n, 0); int l = 0, r = 0; for (int i = 1; i < n; i++) { - // если мы уже видили этот символ + // если мы уже видели этот символ if (i <= r) // то мы можем попробовать его инициализировать z[i - l], // но не дальше правой границы: там мы уже ничего не знаем From 50e698ab1d5b8fba569af5d1e355baf279ac67ed Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 22 Jun 2019 16:30:07 +0000 Subject: [PATCH 099/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/eratosphenes.html | 94 ++++++++++++++++++++++++++++++++++++++++++++ ru/hashing.html | 6 ++- ru/matching.html | 10 +++-- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/ru/eratosphenes.html b/ru/eratosphenes.html index 48a3d91..1acf262 100644 --- a/ru/eratosphenes.html +++ b/ru/eratosphenes.html @@ -11,7 +11,71 @@ span.underline{text-decoration: underline;} div.column{display: inline-block; vertical-align: top; width: 50%;} + + @@ -45,5 +109,35 @@

                  Решето Эратосфена

                  Решето Эратосфена

                  +

                  Определение. Целое положительное число называется простым, если оно имеет ровно два различных натуральных делителя — единицу и самого себя. Единица простым числом не считается.

                  +

                  Решето Эратосфена (англ. slieve of Eratosphenes) — алгоритм нахождения всех простых чисел от \(1\) до \(n\).

                  +

                  Основная идея соответствует названию алгоритма: запишем ряд чисел \(1, 2,\ldots, n\), а затем будем вычеркивать

                  +
                    +
                  • сначала числа, делящиеся на \(2\), кроме самого числа \(2\),

                  • +
                  • потом числа, делящиеся на \(3\), кроме самого числа \(3\),

                  • +
                  • с числами, делящимися на \(4\), ничего делать не будем — мы их уже вычеркивали,

                  • +
                  • потом числа, делящиеся на \(5\), кроме самого числа \(5\)

                  • +
                  +

                  …и так далее.

                  +

                  Самая простая реализация может выглядеть так:

                  + +

                  Этот код сначала помечает все числа, кроме нуля и единицы, как простые, а затем начинает процесс отсеивания составных чисел. Для этого мы перебираем в цикле все числа от \(2\) до \(n\), и, если текущее число простое, то помечаем все числа, кратные ему, как составные.

                  +

                  Асимптотика

                  +

                  Выясняется, что даже такой способ работает достаточно быстро — асимптотика алгоритма состовляет \(O(n \log \log n)\).

                  +

                  Алгоритм уже работает за \(O(n \log \log n)\)

                  +

                  Про простые числа полезно знать два факта:

                  +
                    +
                  1. Простые числа распределены почти равномерно.

                  2. +
                  3. Простых чисел от \(1\) до \(n\) примерно \(\frac{n}{\ln n}\) .

                  4. +
                  +

                  Линейное решето

                  +

                  Этот алгоритм на самом деле интересен не своей асимптотикой (на самом деле, он требует больше времеи из-за константы)

                  diff --git a/ru/hashing.html b/ru/hashing.html index 63f392b..cde0986 100644 --- a/ru/hashing.html +++ b/ru/hashing.html @@ -203,9 +203,11 @@

                  Зачем это нужно?

                  }

                  Теперь мы можем просто вызывать эту функцию от двух отрезков и сравнивать числовое значение, отвечая на запрос за \(O(1)\).

                  Упражнение. Напишите то же самое, но используя обратный полиномиальный хэш — этот способ тоже имеет право на существование, и местами он даже проще. Обратный хэш подстроки принято считать и использовать в стандартном виде из определения, поскольку там нет необходимости в делении.

                  -

                  Лайфхак. Если взять обратный полиномиальный хэш короткой строки на небольшом алфавите с \(k=10\), то числовое значение хэша строки будет красиво соотноситься с самой строкой (подумайте о десятичной системе счисления). \[ +

                  Лайфхак. Если взять обратный полиномиальный хэш короткой строки на небольшом алфавите с \(k=10\), то числовое значение хэша строки будет наглядно соотноситься с самой строкой:

                  +

                  \[ h(abacaba)=1213121 -\] Этим очень удобно пользоваться при дебаге.

                  +\]

                  +

                  Этим удобно пользоваться при дебаге.

                  Примеры задач

                  Количество разных подстрок. Посчитаем хэши от всех подстрок за \(O(n^2)\) и добавим их все в std::set. Чтобы получить ответ, просто вызовем set.size().

                  Поиск подстроки в строке. Можно посчитать хэши от шаблона (строки, которую ищем) и пройтись «окном» размера шаблона по тексту, поддерживая хэш текущей подстроки. Если хэш какой-то из этих подстрок совпал с хэшом шаблона, то мы нашли нужную подстроку. Это называется алгоритмом Рабина-Карпа.

                  diff --git a/ru/matching.html b/ru/matching.html index 8956416..5951e76 100644 --- a/ru/matching.html +++ b/ru/matching.html @@ -109,8 +109,7 @@

                  Паросочетания

                  Паросочетания

                  -

                  Задача

                  -

                  Пусть есть \(n\) мальчиков и \(m\) девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.

                  +

                  Задача. Пусть есть \(n\) мальчиков и \(m\) девочек. Про каждого мальчика и про каждую девочку известно, с кем они не против танцевать. Нужно составить как можно больше пар, в которых партнёры хотят танцевать друг с другом.

                  Паросочетанием \(M\) называется набор попарно несмежных рёбер графа (иными словами, любой вершине графа должно быть инцидентно не более одного ребра из \(M\)).

                  Все вершины, у которых есть смежное ребро из паросочетания (т.е. которые имеют степень ровно один в подграфе, образованном \(M\)), назовём насыщенными этим паросочетанием.

                  @@ -174,7 +173,12 @@

                  Скорость работы

                  Вообще говоря, увлекаться ускорением алгоритма Куна не стоит — существует более асимптотически быстрый алгоритм. Задача нахождения максимального паросочетания — частный случай задачи о максимальном потоке, и если применить алгоритм Диница к двудольным графам с единичной пропускной способностью, то выясняется, что работать он будет за \(O(n \sqrt m)\).

                  Покрытие путями DAG-а

                  Сводить задачи к поиску максимального паросочетания обычно не очень трудно, но в некоторых случаях самому додуматься сложно. Разберём одну такую известную задачу. Дан ориентированный ациклический граф \(G\) (англ. directed acyclic graph). Требуется покрыть его наименьшим числом путей, то есть найти наименьшее множество простых путей, где каждая вершина принадлежит ровно одному пути.

                  -

                  Построим соответствующие изначальному графу \(G\) два двудольных графа \(H\) и \(\overline{H}\) следующим образом: * В каждой доле графа \(H\) будет по \(n\) вершин. Обозначим их через \(a_i\) и \(b_i\) соответственно. * Для каждого ребра \((i, j)\) исходного графа \(G\) проведём соответствующее ребро \((a_i, b_j)\) в графе \(H\). * Теперь из графа \(H\) сделаем граф \(\overline{H}\), добавив обратное ребро \((b_i, a_i)\) для каждого \(i\).

                  +

                  Построим соответствующие изначальному графу \(G\) два двудольных графа \(H\) и \(\overline{H}\) следующим образом:

                  +
                    +
                  • В каждой доле графа \(H\) будет по \(n\) вершин. Обозначим их через \(a_i\) и \(b_i\) соответственно.
                  • +
                  • Для каждого ребра \((i, j)\) исходного графа \(G\) проведём соответствующее ребро \((a_i, b_j)\) в графе \(H\).
                  • +
                  • Теперь из графа \(H\) сделаем граф \(\overline{H}\), добавив обратное ребро \((b_i, a_i)\) для каждого \(i\).
                  • +

                  Если мы рассмотрим любой путь \(v_1, v_2, \ldots, v_k\) в исходном графе \(G\), то в графе \(\overline{H}\) ему будет соответствовать путь \(a_{v_1}, b_{v_2}, a_{v_2}, b_{v_3}, \ldots, a_{v_{k-1}}, b_{v_k}\). Обратное тоже верно: любой путь, начинающийся в левой доле \(\overline{H}\) и заканчивающийся в правой будет соответствовать какому-то пути в \(G\).

                  Итак, есть взаимно однозначное соответствие между путями в \(G\) и путями \(\overline{H}\), идущими из левой доли в правую. Заметим, что любой такой путь в \(\overline{H}\) — это паросочетание в \(H\) (напомним, это \(\overline{H}\) без обратных рёбер). Получается, любому пути из \(G\) можно поставить в соответствие паросочетание в \(H\), и наоборот. Более того, непересекающимся путям в \(G\) соответствуют непересекающиеся паросочетания в \(H\).

                  Заметим, что если есть \(p\) непересекающихся путей, покрывающих все \(n\) вершин графа, то они вместе содержат \(r = n - p\) рёбер. Отсюда получаем, что чтобы минимизировать число путей \(p\), мы должны максимизировать число рёбер \(r\) в них.

                  From 66235e05116313bb785c8ad3ddd1a05839aa6dc0 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 22 Jun 2019 16:41:51 +0000 Subject: [PATCH 100/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/annealing.html | 1 + ru/treap.html | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ru/annealing.html b/ru/annealing.html index 3746b3f..1c218e9 100644 --- a/ru/annealing.html +++ b/ru/annealing.html @@ -139,6 +139,7 @@

                  Описание алгоритма
                • В случае с перестановками этим минимальным изменением может быть, например, своп двух случайных элементов.

                • Если \(y\) не хуже, то есть \(f(y) \leq f(x)\), то переходим в него в любом случае. Иначе делаем переход в \(y\), с вероятностью \(p = e^\frac{f(x)-f(y)}{t_k}\) — это экспонента отрицательного числа, и она даст вероятность в промежутке \((0, 1)\).

              +

              Вообще, в выборе конкретных эвристик не существует «золотого правила». Все компоненты алгоритма сильно зависят друг от друга и от задачи.

              Реализация

              На практике применим алгоритм к другой задаче:

              diff --git a/ru/treap.html b/ru/treap.html index ed958b8..2332823 100644 --- a/ru/treap.html +++ b/ru/treap.html @@ -141,7 +141,6 @@

              Бинарные деревья

              Приоритеты и асимптотика

              В декартовом дереве логарифмическая высота дерева гарантируется не инвариантами и эвристиками, а законами теории вероятностей: оказывается, что если все приоритеты (\(y\)) выбирать случайно, то средняя глубина вершины будет логарифмической. Поэтому ДД ещё называют рандомизированным деревом поиска.

              Теорема. Ожидание глубины вершины в декартовом дереве равно \(O(n \log n)\).

              -

              Если не знаете, что такое ожидание, то на доказательство забейте: это будет на занятии по теорверу в конце года. Сейчас его можно пропустить.

              Доказательство. Введем функцию \(a(x, y)\) равную единице, если \(x\) является предком \(y\), и нулем в противном случае. Такие функции называются индикаторами.

              Глубина вершины равна количеству её предков — прим. К. О. Таким образом, она равна

              \[ From 71d35f088edcb96cdcf2ac1b2459ef1a5fe19738 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 22 Jun 2019 19:10:48 +0000 Subject: [PATCH 101/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/eratosphenes.html | 66 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/ru/eratosphenes.html b/ru/eratosphenes.html index 1acf262..d742d6f 100644 --- a/ru/eratosphenes.html +++ b/ru/eratosphenes.html @@ -115,29 +115,71 @@

              Решето Эратосфена

            5. сначала числа, делящиеся на \(2\), кроме самого числа \(2\),

            6. потом числа, делящиеся на \(3\), кроме самого числа \(3\),

            7. -
            8. с числами, делящимися на \(4\), ничего делать не будем — мы их уже вычеркивали,

            9. -
            10. потом числа, делящиеся на \(5\), кроме самого числа \(5\)

            11. +
            12. с числами, делящимися на \(4\), ничего делать не будем — мы их уже вычёркивали,

            13. +
            14. потом продолжим вычеркивать числа, делящиеся на \(5\), кроме самого числа \(5\),

            15. …и так далее.

              Самая простая реализация может выглядеть так:

              Этот код сначала помечает все числа, кроме нуля и единицы, как простые, а затем начинает процесс отсеивания составных чисел. Для этого мы перебираем в цикле все числа от \(2\) до \(n\), и, если текущее число простое, то помечаем все числа, кратные ему, как составные.

              -

              Асимптотика

              -

              Выясняется, что даже такой способ работает достаточно быстро — асимптотика алгоритма состовляет \(O(n \log \log n)\).

              -

              Алгоритм уже работает за \(O(n \log \log n)\)

              -

              Про простые числа полезно знать два факта:

              +

              Если память позволяет, то для оптимизации скорости лучше использовать не вектор bool, а вектор char — но он займёт в 8 раз больше места. Компьютер не умеет напрямую оперировать с битами, и поэтому при индексации к vector<bool> он сначала достаёт нужный байт, а затем битовыми операциями получает нужное значение, что занимает приличное количество времени.

              +

              Время работы

              +

              Довольно легко показать, что асимптотическое время работы алгоритма хотя бы не хуже, чем \(O(n \log n)\): даже если бы мы входили в цикл вычёркиваний для каждого числа, не проверяя его сначала на простоту, суммарно итераций было бы

              +

              \[ +\sum_k \frac{n}{k} = \frac{n}{2} + \frac{n}{3} + \frac{n}{4} + \ldots + \frac{n}{n} = O(n \log n) +\]

              +

              Здесь мы воспользовались асимптотикой гармонического ряда.

              +

              У исходного алгоритма асимптотика должна быть ещё лучше. Чтобы найти её точнее, нам понадобятся два факта про простые числа:

                -
              1. Простые числа распределены почти равномерно.

              2. Простых чисел от \(1\) до \(n\) примерно \(\frac{n}{\ln n}\) .

              3. +
              4. Простые числа распределены без больших «разрывов» и «скоплений», то есть \(k\)-тое постое число примерно равно \(k \ln k\).

              +

              Мы можем упрощённо считать, что число \(k\) является простым с «вероятностью» \(\frac{1}{\ln n}\). Тогда, время работы алгоритма божно более точнее оценить как

              +

              \[ +\sum_k \frac{1}{\ln k} \frac{n}{k} +\approx n \int \frac{1}{k \ln k} += n \ln \ln k \Big |_2^n += O(n \log \log n) +\]

              +

              Асимптотику алгоритма можно улучшить и дальше, до \(O(n)\).

              Линейное решето

              -

              Этот алгоритм на самом деле интересен не своей асимптотикой (на самом деле, он требует больше времеи из-за константы)

              +

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

              +

              Обозначим за \(d(k)\) минимальный простой делитель числа \(k\) и заметим следующий факт: у составного числа \(k\) есть единственное представление \(k = d(k) \cdot r\), и при этом у числа \(r\) нет простых делителей меньше \(d(k)\).

              +

              Идея оптимизации состоит в том, чтобы перебирать этот \(r\), и для каждого перебирать только нужные множители — а именно все от \(2\) до \(d(r)\) включительно.

              +

              Алгоритм

              +

              Немного обобщим задачу — теперь мы хотим посчитать для каждого числа \(k\) на отрезке \([2, n]\) его минимальный простой делитель \(d_k\), а не только определить его простоту.

              +

              Изначально массив \(d\) заполним нулями, что означает, что все числа предполагаются простыми. В ходе работы алгортима этот массив будет постепенно заполняться. Помимо этого, будем поддерживать список \(p\) всех найденных на текущий момент простых чисел.

              +

              Теперь будем перебирать число \(k\) от \(2\) до \(n\). Если это число простое, то есть \(d_k = 0\), то просвоим \(d_k = k\) и добавим \(k\) в список \(p\).

              +

              Дальше, вне зависимости от простоты \(k\), начнём процесс расстановки значений в массиве \(d\) — переберем найденные простые числа \(p_i\), не превосходящие \(d_k\), и сделаем присвоение \(d_{p_i k} = p_i\).

              + +

              Алгоритм требует как минимум в 32 раза больше памяти, чем обычное решето, потому что требуется хранить делитель (int, 4 байта) вместо одного бита на каждое число. Линейное решето хоть и имеет лучшую асимптотику, но на практике проигрывает также и по скорости оптимизированному варианту решета Эратосфена.

              +

              Применения

              +

              Массив \(d\) он позволяет искать факторизацию любого числа \(k\) за время порядка размера этой факторизации:

              +

              \[ +factor(k) = \{d(k)\} \cup factor(k / d(k)) +\]

              +

              Знание факторизации всех чисел — очень полезная информация для некоторых задач. Ленейное решето интересно не своим временем работы, а именно этим массивом минимальных простых делителей.

              From 184b6892b61aa8764a66d4bf1149ec8a9aa40db7 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 22 Jun 2019 22:51:48 +0000 Subject: [PATCH 102/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/euclid.html | 131 ++++++++++++++++++++++++++++--------------------- ru/index.html | 4 +- 2 files changed, 77 insertions(+), 58 deletions(-) diff --git a/ru/euclid.html b/ru/euclid.html index d4602cf..6661191 100644 --- a/ru/euclid.html +++ b/ru/euclid.html @@ -114,77 +114,96 @@

              Алгоритм Евклида

              \gcd(a, b) = \max_{k: \; k|a \, \land \, k | b} k \]

              Когда оба числа равны нулю, результат не определён — подойдёт сколько угодно большое число. Однако в этом случае мы положим в этом случае \(\gcd\) равным тоже нулю, чтобы можно было использовать следующее правило: если одно из чисел равно нулю, то их \(\gcd\) равен второму числу.

              -

              Алгоритм Евклида находит \(gcd\) двух чисел \(a\) и \(b\) за \(O(\log \min(a, b))\). Он известен с IV века до нашей эры, а возможно и ранее.

              -

              Сам алгоритм основывается на следующей формуле, которую мы докажем позже:

              +

              Алгоритм Евклида находит \(\gcd\) двух чисел \(a\) и \(b\) за \(O(\log \min(a, b))\). Он известен ещё с IV века до нашей эры, а возможно и ранее.

              +

              Алгоритм основывается на следующей несложной формуле:

              \[ \gcd(a, b) = \begin{cases} a, & b = 0 \\ -\gcd(b,\, a \bmod b), & b > 0 +\gcd(b,\, a - b), & b > 0 \end{cases} \]

              -

              Эту формулу очень просто реализовать:

              +

              Здесь предполагается, что \(a > b\).

              +

              Докажем корректность этой формулы:

              +
                +
              • Если \(g = \gcd(a, b)\) делит и \(a\), и \(b\), то их разность \((a-b)\) тоже будет делиться на \(g\).

              • +
              • Никакой больший делитель \(d\) числа \(b\) не может делить число \((a-b)\): если \(d > g\), то \(d\) не может делить \(a\), а значит и не делит \((a - b)\).

              • +
              +

              Прямая рекурсивная реализация:

              -

              Чуть более быстрая итеративная форма:

              - -

              В компиляторе gcc для этого уже есть встроенная функция __gcd.

              -

              Корректность

              -

              Из того, что второй аргумент \(\gcd\) всегда уменьшается и не может стать отрицательным, будет следовать, что алгоритм Евклида всегда хотя бы завершается.

              -

              Чтобы доказать корректность алгоритма, нам нужно доказать равенство \(g = \gcd(a, b) = \gcd(b, a \bmod b)\) для любых \(a \geq 0\), \(b > 0\).

              -

              Разложим остаток от деления \(a\) на \(b\) через их частное:

              +    if (a < b) +        swap(a, b); +    if (b == 0) +        return a; +    else +        return gcd(b, a - b); +} +

              Этот алгоритм может работать долго — например, на паре \((10^9, 1)\) он сделает миллиард итераций. Идея дальнейшей оптимизации в том, чтобы вычитать из \(a\) не одно \(b\) за раз, а столько, чтобы в следующий раз \(a\) и \(b\) уже поменялись местами — чтобы новое \(b\) стало меньше нового \(a\).

              +

              Простой способ этого достичь — просто вычесть \(b\) из \(a\) сразу максимально возможное число раз, то есть взять вместо нового \(b\) остаток от деления \(a\) на \(b\):

              \[ -a \bmod b = a - b \cdot \Big \lfloor \frac{a}{b} \Big \rfloor +\gcd(a, b) = \begin{cases} +a, & b = 0 \\ +\gcd(b,\, a \bmod b), & b > 0 +\end{cases} \]

              -

              По определению, \(g = \gcd(a, b)\) делит \(a\) и \(b\). Следовательно, \(g\) делит и эту разность, а значит и \((a \bmod b)\).

              -

              Так как \(g\) делит числа \(b\) и \((a \bmod b)\), то оно должно также делить \(\gcd(b, a \bmod b)\).

              -

              Мы получили, что \(g = \gcd(a, b)\) делит \(\gcd(b, a \bmod b)\). Можно провести симметричные рассуждения и показать, что \(\gcd(b, a \bmod b)\) делит \(\gcd(a, b)\), из чего будет следовать, что они равны.

              -

              Асимптотика

              -

              Заметим, что в алгоритме поддерживается инвариант \(a > b\), кроме, возможно, первой итерации. Так как \(a \bmod b < b\) и \(a > b\), то \(a\) уменьшится хотя бы в два раза.

              -

              Самые худшие входные данные для алгоритма — последовательность Фибоначчи.

              -

              Также по этой причине нахождение \(\gcd\) от \(n\) чисел будет работать за \(O(n + \log x)\), где \(x\) это минимальное из них.

              +

              Можно показать, что каждые две итерации меньшее число уменьшится хотя бы в два раза, а следовательно алгоритм работает за \(O(\log \min (a, b))\).

              +

              Реализация:

              + +

              Чуть более быстрая итеративная форма:

              + +

              В компиляторе gcc для этого уже есть встроенная функция __gcd, которая, впрочем, может непредсказуемо себя вести на отрицательных числах и \((0, 0)\).

              +

              Время работы

              +

              Оценка \(O(\log n)\) относится к худшему случаю. Асимптотика в среднем это более интересный вопрос, играющий существенную роль в некоторых алгоритмах.

              +

              +

              Примечательно, что худшие входные данные для алгоритма — это соседние числа Фибоначчи. На графике они видны как синие точки в пропорциях золотого сечения.

              +

              Также иногда полезно знать, что нахождение \(\gcd\) группы из \(n\) чисел от \(1\) до \(A\) будет работать не за \(O(n \log A)\), а за \(O(n + \log A)\) — это несложно доказать по индукции.

              Решение диофантовых уравнений

              Для обычного использования \(gcd\) не нужно даже знать, как алгоритм устроен — он есть даже в компиляторе.

              -

              Расширенный алгоритм Евклида находит помимо \(g = \gcd(a, b)\) также коэффициенты \(x\) и \(y\) такие, что

              +

              Расширенный алгоритм Евклида находит, помимо \(g = \gcd(a, b)\), такие целые коэффициенты \(x\) и \(y\), что

              \[ a \cdot x + b \cdot y = g \]

              -

              Эта версия значима, потому что с помощью неё можно искать обратный элемент по модулю: нужно найти такой элемент \(a^{-1}\), что \(a \cdot a^{1} \equiv 1\), что то же самое, что:

              +

              Эта модификация алгоритма интересна, потому что с помощью неё можно искать обратный элемент по модулю: такой элемент \(a^{-1}\), что \(a \cdot a^{1} \equiv 1\), что то же самое, что найти решение в целых числах:

              \[ a^{-1} \cdot a + k \cdot m = 1 \]

              -

              Модифицируем стандартный алгоритм Евклида, чтобы учесть в нём нахождение этих коэффициентов.

              -

              Пусть мы посчитали эти коэффициенты когда рекурсивно считали \(gcd(b, a \bmod b)\). Иными словами, у нас есть решение \((x, y)\) для пары \((b, a \bmod b)\):

              -

              $$

              -

              $$

              -

              Чтобы получить решение для исходной пары, преобразуем величину \((a \bmod b)\) как \(todo\) и подставим в преведённое выше выражение:

              -

              $$

              -

              $$

              -

              Если выполнить перегруппировку слагаемых, можно получить:

              -

              $$

              -

              $$

              -

              Реализация

              - -

              Эта рекурсивная функция по прежнему возвращает значение \(gcd(a, b)\), но помимо этого записывает в \(x\) и \(y\) искомые коэффициенты.

              +

              Заметим также, что решений бесконечно много: можно \(x\) увеличить на \(b \cdot y\), а \(y\) уменьшить на \(a \cdot x\), и равенство при этом не изменится.

              +

              Алгоритм будет тоже рекурсивный. Пусть мы посчитали нужные коэффициенты \(x'\) и \(y'\), когда рекурсивно считали \(\gcd(b, a \bmod b)\). Иными словами, у нас есть решение \((x', y')\) для пары \((b, a \bmod b)\):

              +

              \[ +b \cdot x' + (a \bmod b) \cdot y' = g +\]

              +

              Чтобы получить решение для исходной пары, запишем выражение \((a \bmod b)\) как \((a - \lfloor \frac{a}{b} \rfloor \cdot b)\) и подставим в преведенное выше равенство:

              +

              \[ +b \cdot x' + (a - \Big \lfloor \frac{a}{b} \Big \rfloor \cdot b) \cdot y' = g +\]

              +

              Теперь выполним перегруппировку слагаемых (сгруппируем по исходным \(a\) и \(b\)) и получим:

              +

              \[ +a \cdot \underbrace{y'}_x + b \cdot \underbrace{(x' - \Big \lfloor \frac{a}{b} \Big \rfloor \cdot y')}_y = g +\]

              +

              Сравнивая это с исходным выражением, получаем, что для иходных \(x\) и \(y\) подходят коэффициенты при \(a\) и \(b\).

              +

              Реализация:

              + +

              Эта рекурсивная функция по прежнему возвращает значение \(\gcd(a, b)\), но помимо этого записывает в переданные по ссылке переменные \(x\) и \(y\) искомые коэффициенты.

              diff --git a/ru/index.html b/ru/index.html index bd75719..7c5007a 100644 --- a/ru/index.html +++ b/ru/index.html @@ -76,8 +76,8 @@

              Математика

              Комбинаторика и ТЧ

                -
              • Алгоритм Евклида расширенный алгоритм евклида, диофантовы уравнения
              • -
              • Решето Эратосфена линейное решето, факторизация всех чисел от 1 до n
              • +
              • Алгоритм Евклида расширенный алгоритм евклида, диофантовы уравнения
              • +
              • Решето Эратосфена линейное решето, факторизация всех чисел от 1 до n
              • Нахождение обратного по модулю малая теорема ферма, алгоритм евклида, возведение в степень
              • Жадный алгоритм Радо-Эдмондса матроиды, жадный набор паросочетаний, расписаний, базисов
              • Ро-алгоритм Полларда быстрая факторизация, пародокс дней рождений
              • From 90d706c841414acc6991a3f55c6ed1e7a04ba481 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 22 Jun 2019 22:58:56 +0000 Subject: [PATCH 103/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/euclid.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ru/euclid.html b/ru/euclid.html index 6661191..6974a61 100644 --- a/ru/euclid.html +++ b/ru/euclid.html @@ -173,7 +173,7 @@

                Решение

                \[ a \cdot x + b \cdot y = g \]

                -

                Эта модификация алгоритма интересна, потому что с помощью неё можно искать обратный элемент по модулю: такой элемент \(a^{-1}\), что \(a \cdot a^{1} \equiv 1\), что то же самое, что найти решение в целых числах:

                +

                Эта модификация алгоритма интересна, потому что с помощью неё можно искать обратный элемент по модулю: такой элемент \(a^{-1}\), что \(a \cdot a^{1} \equiv 1\), что то же самое, что найти решение в целых числах:

                \[ a^{-1} \cdot a + k \cdot m = 1 \]

                From d9eaf750c80d365dee2aa8f1669f32811157dd69 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Mon, 24 Jun 2019 10:07:19 +0000 Subject: [PATCH 104/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/eratosphenes.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ru/eratosphenes.html b/ru/eratosphenes.html index d742d6f..f340df1 100644 --- a/ru/eratosphenes.html +++ b/ru/eratosphenes.html @@ -139,7 +139,7 @@

                Время работы

                У исходного алгоритма асимптотика должна быть ещё лучше. Чтобы найти её точнее, нам понадобятся два факта про простые числа:

                1. Простых чисел от \(1\) до \(n\) примерно \(\frac{n}{\ln n}\) .

                2. -
                3. Простые числа распределены без больших «разрывов» и «скоплений», то есть \(k\)-тое постое число примерно равно \(k \ln k\).

                4. +
                5. Простые числа распределены без больших «разрывов» и «скоплений», то есть \(k\)-тое простое число примерно равно \(k \ln k\).

                Мы можем упрощённо считать, что число \(k\) является простым с «вероятностью» \(\frac{1}{\ln n}\). Тогда, время работы алгоритма божно более точнее оценить как

                \[ From be899e70a68b6814f1d48988edf653e37ba40434 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Mon, 24 Jun 2019 11:01:10 +0000 Subject: [PATCH 105/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/dp-optimizations.html | 1 + ru/hld.html | 5 +++++ ru/stress-test.html | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index ef0a3f1..97df386 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -247,6 +247,7 @@

                Дерево Ли Шао

                else return max(f(ln[v], x), get(x, 2 * v + 1, m, r)); }

              +

              Достаточно полезно сравнить между собой CHT и дерево Ли-Шао и понимать, в какой из ситуаций стоит применять каждую из этих структур. Адекватные реализации CHT требуют особых условий — точки должны быть отсортированы по \(x\). Если это выполнено, то работать алгоритм будет значительно быстрее, чем дерево Ли Шао, которое, в свою очередь, решает более общую задачу, но работает за \(O(\log MAXC)\) на запрос, а зачастую еще и требует неявную реализацию, если \(MAXC\) достаточно большое (неявная реализация схожа с неявным деревом отрезков).

              Лямбда-оптимизация

              Примечание. В научной литературе метод известен как дискретный метод множителей Лагранжа.

              Рассмотрим немного другую задачу. Пусть нам нужно покрыть те же точки, но теперь нас не ограничивают жёстко в количестве отрезков, а просто штрафуют на какую-то константу \(\lambda\) за использование каждого. Нашу оптимизируемую функцию \(g\) можно выразить через \(f\) следующим образом:

              diff --git a/ru/hld.html b/ru/hld.html index ad759fd..4311871 100644 --- a/ru/hld.html +++ b/ru/hld.html @@ -192,5 +192,10 @@

              Как им решать зада return ans; }

              +

              Замечания

              +
                +
              • Если в рамках задачи нет запросов обновления, то можно прокэшировать запросы — заметим, что большинство запросов к структуре, которые надо сделать, находятся на префиксах тяжелых путей, и на них можно отвечать за \(O(1)\), и таких будет \(O(\log n)\) на запрос. Также придется сделать \(O(1)\) запросов к структуре, которые будут работать за \(O(\log n)\). Получаем \(O(\log n)\) на запрос.
              • +
              • Так как наша реализация HLD строит структуру данных на эйлеровом обходе дерева, то мы можем добавить запросы к поддеревьям (ведь поддеревья — это подотрезки эйлерова обхода).
              • +
              diff --git a/ru/stress-test.html b/ru/stress-test.html index ec24a25..1040edd 100644 --- a/ru/stress-test.html +++ b/ru/stress-test.html @@ -110,7 +110,7 @@

              Стресс-тестирование

              Стресс-тестирование

              • Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений
              • -
              • Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написанно решение на маленькие подгруппы
              • +
              • Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написано решение на маленькие подгруппы

              Суть такая: * Есть решение smart — быстрое, но в котором есть баг, который хотим найти * Пишем решение stupid — медленное, но точно корректное * Пишем генератор gen — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются

              Примерный код скрипта:

              From 5bf169b481768f96dd8b29d8f22c3e8f2e8b23b7 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 25 Jun 2019 08:22:53 +0000 Subject: [PATCH 106/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- sitemap.xml | 103 ++++++++++++++++++++++------------------------------ 1 file changed, 44 insertions(+), 59 deletions(-) diff --git a/sitemap.xml b/sitemap.xml index e99ee9a..86a518d 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -9,182 +9,167 @@ https://algorithmica.org/ - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 1.00 https://algorithmica.org/en/ - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.80 https://algorithmica.org/ru/ - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.80 https://algorithmica.org/ru/segtree - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/treap - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/fenwick - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/sparse-table - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/sqrt - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/bitset - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/dp-optimizations - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/linalg - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 - https://algorithmica.org/ru/probability - 2019-06-14T18:25:26+00:00 + https://algorithmica.org/ru/karatsuba + 2019-06-24T11:01:16+00:00 0.64 - https://algorithmica.org/ru/karatsuba - 2019-06-14T18:25:26+00:00 + https://algorithmica.org/ru/fft + 2019-06-24T11:01:16+00:00 0.64 - https://algorithmica.org/ru/fft - 2019-06-14T18:25:26+00:00 + https://algorithmica.org/ru/euclid + 2019-06-24T11:01:16+00:00 + 0.64 + + + https://algorithmica.org/ru/eratosphenes + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/reciprocal - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/matroid - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/pollard - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/mst - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 - https://algorithmica.org/ru/matching - 2019-06-14T18:25:26+00:00 + https://algorithmica.org/ru/lca + 2019-06-24T11:01:16+00:00 0.64 - https://algorithmica.org/ru/mincost-maxflow - 2019-06-14T18:25:26+00:00 + https://algorithmica.org/ru/matching + 2019-06-24T11:01:16+00:00 0.64 - https://algorithmica.org/ru/lca - 2019-06-14T18:25:26+00:00 + https://algorithmica.org/ru/mincost-maxflow + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/centroid - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/hld - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/hashing - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 - https://algorithmica.org/ru/strings - 2019-06-14T18:25:26+00:00 - 0.64 - - - https://algorithmica.org/ru/trie - 2019-06-14T18:25:26+00:00 + https://algorithmica.org/ru/string-searching + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/aho-corasick - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/suffix-array - 2019-06-14T18:25:26+00:00 - 0.64 - - - https://algorithmica.org/ru/suffix-automaton - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/palindromes - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/geometry - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/convex-hulls - 2019-06-14T18:25:26+00:00 - 0.64 - - - https://algorithmica.org/ru/games - 2019-06-14T18:25:26+00:00 - 0.64 - - - https://algorithmica.org/ru/imperfect-information - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/annealing - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 https://algorithmica.org/ru/stress-test - 2019-06-14T18:25:26+00:00 + 2019-06-24T11:01:16+00:00 0.64 From d03efd286cbdfec8a00af451e7e9eb8277c7e648 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 25 Jun 2019 12:23:20 +0000 Subject: [PATCH 107/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/dp-optimizations.html | 37 ++++++++++++++++++++++++++++++++++++- ru/matching.html | 13 ++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index 97df386..7a84ccb 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -196,7 +196,42 @@

              Convex Hull Trick

              Пусть мы хотим найти оптимальное \(k\) для \(f[i][j]\). Представим все уже посчитанные релевантные динамики с предыдущего слоя как точки \((a_k, b_k)\) на плоскости. Чтобы эффективно находить среди них точку с минимальным скалярным произведением, можно поддерживать их нижнюю огибающую (вектор \((1, x_{i-1})\) «смотрит» всегда вверх, поэтому нам интересна только она) и просто бинпоиском будем находить оптимальную точку.

              Хранить нижнюю огибающую можно просто в стэке. Так как добавляемые точки отсортированы по \(x\), её построение будет занимать линейное время, а асимптотика всего алгоритму будет упираться в асимптотику бинарного поиска, то есть будет равна \(O(n m \log n)\)

              - +

              В случае нашей конкретной задачи, алгоритм можно и дальше соптимизировать, если вспомнить, что \(opt[i, j] \leq opt[i][j+1]\), то есть что оптимальная точка всегда будет «правее». Это позволяет вместо бинпоиска применить метод двух указателей:

              Мы избавились от бинпоиска, и теперь алгоритм работает за \(O(n \cdot m)\).

              diff --git a/ru/matching.html b/ru/matching.html index 5951e76..a788ae0 100644 --- a/ru/matching.html +++ b/ru/matching.html @@ -194,8 +194,19 @@

              Лемма Холла

              Рассмотрим множество вершин \(H\) — все вершины, достижимые из \(x\), если можно ходить из правой доли в левую только по рёбрам паросочетания, а из левой в правую — по любым (мы такой граф по сути строим, когда ищем увеличивающую цепь в алгоритме Куна)

              Тогда в \(H\) найдется вершина \(y\) из \(R\), не насыщенная паросочетанием. Иначе, если такой вершины нет, то получается, что если рассмотреть вершины \(H_L\) (вершины левой доли, насыщенные паросочетанием), то для них не будет выполнено условие, что \(|H_L| \leq |N(H_L)|\) (здесь \(N(X)\) — множество вершин, соединенным паросочетанием с \(X\)).

              Тогда должен существовать путь из \(x\) в \(y\), и он будет увеличивающим для паросочетания \(M\), потому что из \(R\) в \(L\) мы всегда шли только по ребрам паросочетания. Проведя чередование вдоль этого пути, получим большее паросочетание, следовательно предположение индукции верно.

              +

              Минимальное вершинное покрытие

              +

              Задача. Дан граф. Назовем вершинным покрытием такое множество вершин, что каждое ребро графа инцидентно хотя бы одной вершине из множества. Необходимо найти вершинное покрытие наименьшего размера.

              +

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

              +

              Теорема. \(\mid V_{min} \mid \le \mid M \mid\), где \(V_{min}\) — минимальное вершинное покрытие, а \(M\) — максимальное паросочетание. Доказательство. \(\mid V_{min} \mid \ge \mid M \mid\), поскольку \(M\) — множество независимых ребер. Теперь приведем алгоритм, который строит вершинное покрытие размера \(\mid M \mid\). Очевидно, оно будет минимальным. Алгоритм. Мысленно ориентируем ребра графа — ребра из \(M\) проведем из правой доли в левую, остальные ­— из левой в правую, после чего запустим обход в глубину из всех вершин левой доли, не включенных в \(M\).

              +

              +

              Заметим, что граф разбился на несколько множеств: \(L^+, L^-, R^+, R^-\), где “плюсовые” множества — это множества посещенных в процессе обхода вершин. В графе такого вида не бывает ребер \(L^+ \rightarrow R^-\), \(L^- \leftarrow R^+\) по очевидным соображениям. Ребер \(L^+ \leftarrow R^-\) не бывает, потому что в противном случае паросочетание \(M\) не максимальное — его можно дополнить ребрами такого типа.

              +

              \[ + L^- \cup R^+ = V_{min} +\]

              +

              Понятно, что данное множество покроет все ребра. Осталось выяснить, почему \(L^- \cup R^+\). Это верно потому, что \(L^- \cup R^+\) покрывает все ребра \(M\) ровно один раз (ведь ребра \(L^- \rightarrow R^+\) не принадлежат \(M\)), а также потому, что в нем нет вершин, не принадлежащих \(M\) (для \(L^-\) это справедливо по определению, для \(R^+\) можно провести доказательство от противного с использованием чередующихся цепей).

              +

              Упражнение. Подумайте, как это можно применить к решению задачи о максимальном независимом множестве.

              Для ноулайферов: матроиды

              -

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

              +

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

              Математика вообще занимается тем, что обобщает всякие объекты и старается формулировать все теоремы максимально абстрактно. Так, концепцию хороших подмножеств (паросочетаний) обобщает понятие матроида. Практическое применение — жадный алгоритм Радо-Эдмондса, который нужен для обоснования большого числа жадников, где нужно набрать какое-то подмножество минимального / максимального веса. Сам алгоритм максимально простой: давайте отсортируем эти объекты по весу и будем добавлять их в таком порядке в наше хорошее множество, если оно после добавления остается хорошим.

              Применимо к паросочетаниям: пусть у вершин левой доли есть вес, и нам нужно набрать максимальное паросочетание минимального веса. Тогда выясняется, что можно просто отсортировать вершины левой доли по весу и пытаться в таком порядке добавлять их в паросочетание стандартным алгоритмом Куна.

              From 11459143e904cd8b09abd18a23a393f15e4804a6 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 26 Jun 2019 11:16:30 +0000 Subject: [PATCH 108/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/stress-test.html | 119 +++++++++++++++++++++++++++++++++----------- 1 file changed, 91 insertions(+), 28 deletions(-) diff --git a/ru/stress-test.html b/ru/stress-test.html index 1040edd..43799b0 100644 --- a/ru/stress-test.html +++ b/ru/stress-test.html @@ -75,6 +75,7 @@ code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */ + @@ -112,34 +113,96 @@

              Стресс-тестирован
            16. Метод поиска багов, заключающийся в генерации случайных тестов и сравнивании результатов двух решений
            17. Очень полезен на школьных олимпиадах, когда есть много времени, или когда уже написано решение на маленькие подгруппы
            18. -

              Суть такая: * Есть решение smart — быстрое, но в котором есть баг, который хотим найти * Пишем решение stupid — медленное, но точно корректное * Пишем генератор gen — печатает какой-то корректный тест, сгенерированный случайно * Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются

              -

              Примерный код скрипта:

              - -

              Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py.

              +

              Суть такая:

              +
                +
              • Есть решение smart — быстрое, но в котором есть баг, который хотим найти
              • +
              • Пишем решение stupid — медленное, но точно корректное
              • +
              • Пишем генератор gen — печатает какой-то корректный тест, сгенерированный случайно
              • +
              • Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются
              • +
              +

              Как это выглядит в реальной жизни

              +

              Задача. Есть два числа \(a\) и \(b\), \(1 \le a, b \le 2 * 10^9\). Вычислите \(a *b\).

              +

              Примечание. На пробных турах всех школьных олимпиад автор проверял свои стресс-тесты именно на этой задаче.

              +

              Приведем код решения smart, которое не учитывает проблему переполнения 32-битного типа данных int:

              + +

              Приведем код решения stupid:

              + +

              Если программист начнёт тестировать код руками, то проверить так называемый макстест ему придет в голову далеко не сразу. При этом, если сказать, что решение не работает на тесте 1234567890 1987654321, то программист сразу поймет, в чем ошибка.

              +

              Стресс-тестирование inline

              +

              Примечание. Автор не рекомендует так делать, но многим такой подход кажется проще для понимания.

              +

              Суть в следующем:

              +
                +
              • Все решения и генераторы помещаются в отдельные методы.
              • +
              • Тесты рекомендуется передавать либо строками, либо через файл, но особо уверенные в себе могут использовать глобальные переменные.
              • +
              • Быть аккуратным с очищением глобальных переменных.
              • +
              • Запустить и получить тест.
              • +
              • Profit.
              • +
              + +

              Script-based стресс-тестирование

              +

              Суть в следующем:

              +
                +
              • Все решения и генераторы помещаются в отдельные файлы.
              • +
              • Тесты рекомендуется передавать через перенаправление потоков ввода-вывода.
              • +
              • Быть аккуратным не надо — мы работаем с тем же самым решением, которое отправим в тестирующую систему.
              • +
              • Запустить и получить тест.
              • +
              • Если вы не работаете под Linux, то начните уже наконец работать под Linux.
              • +
              • Если вы не знаете Python, то выучите уже наконец Python.
              • +
              • Profit.
              • +
              +

              Файлы stupid.cpp, smart.cpp и gen.py содержат уже понятный нам код.

              +

              Вот примерный код скрипта checker.py:

              + +

              Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py. При желании можно компилировать прямо внутри скрипта.

              Скрипт написан под Linux. Для Windows нужно убрать «./» во всех системных вызовах.

              -

              gen.py автор тоже обычно пишет на питоне, но вообще его тоже можно писать на чём угодно, сделать исполняемым и вызывать через ./gen. Пример gen-а, генерирующего случайную строку из символов “a”, “b” и “c” длины от 1 до 10:

              - +

              Примечание. Ну такой вот примерно рецепт усредненный, потому что вариаций масса. Берется неправильное решение, оно не работает, рабочий код — это не про код моего бати. Он берет это решение, вываливает его в скрипт и начинает запускать. Добавляет огромное количество тестов, крайних случаев, рандома и МАКСТЕСТОВ! для проверки. Все это прогоняется вместе с медленным решением. Потом скрипт находит баг и системный блок остужается на балконе. Потом батя заносит тест и щедро заполнив код отладочным выводом начинает дебажить. При этом параллельно ест и засыпает крошками клавиатуру. Ест и приговаривает полушепотом ух ###. При этом у него на лбу аж пот выступает. Любезно мне иногда предлагает подебажить, но я отказываюсь. Надо ли говорить о том какой код получается потом? Вонища такая, что тестирующая система падает.

              From a202e9e15d59aacad7f09204a8a9e0bed0ed55ce Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 28 Jun 2019 13:03:26 +0000 Subject: [PATCH 109/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/stress-test.html | 120 +++++++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 41 deletions(-) diff --git a/ru/stress-test.html b/ru/stress-test.html index 43799b0..6fcd181 100644 --- a/ru/stress-test.html +++ b/ru/stress-test.html @@ -121,21 +121,34 @@

              Стресс-тестирован
            19. Кормим всё в скрипт checker, который n раз генерирует тест, даёт его на ввод stupid-у и smart-у, сравнивает выводы и останавливается, когда они отличаются
            20. Как это выглядит в реальной жизни

              -

              Задача. Есть два числа \(a\) и \(b\), \(1 \le a, b \le 2 * 10^9\). Вычислите \(a *b\).

              -

              Примечание. На пробных турах всех школьных олимпиад автор проверял свои стресс-тесты именно на этой задаче.

              -

              Приведем код решения smart, которое не учитывает проблему переполнения 32-битного типа данных int:

              - -

              Приведем код решения stupid:

              - -

              Если программист начнёт тестировать код руками, то проверить так называемый макстест ему придет в голову далеко не сразу. При этом, если сказать, что решение не работает на тесте 1234567890 1987654321, то программист сразу поймет, в чем ошибка.

              +

              Задача. Есть массив чисел \(1 \le a_1 ... a_n \le 10^9\). Найдите значение минимального элемента.

              +

              Приведем код решения stupid, который будем использовать в качестве эталонного:

              + +

              Пусть у нас есть решение smart, которое содержит ошибку в границах цикла:

              + +

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

              Стресс-тестирование inline

              Примечание. Автор не рекомендует так делать, но многим такой подход кажется проще для понимания.

              Суть в следующем:

              @@ -146,32 +159,53 @@

              Стресс-тестир
            21. Запустить и получить тест.
            22. Profit.
            23. -

              Script-based стресс-тестирование

              Суть в следующем:

              -

              Палиндром — не самый часто встречающийся в реальной жизни объект, однако задачи на палиндромы любят давать на соревнованиях по спортивному программированию.

              +

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

              Алгоритм Манакера

              Пусть есть строка \(s\) и мы хотим найти в ней все подпалиндромы.

              -

              Мы сразу сталкиваемся с очевидной тудностью: их в строке может быть \(O(n^2)\), что можно видеть на примере строки \(s = aa \ldots a\). Поэтому будем использовать следующий формат: для каждой позиции \(s_i\) найдём наибольший палиндром, центр которого совпадает с \(s_i\). При этом палиндромы чётной длины учитывать пока что не будем.

              +

              Мы сразу сталкиваемся с очевидной тудностью: их в строке может быть \(O(n^2)\), что можно видеть на примере строки \(s = aa \ldots a\). Поэтому будем использовать следующий формат: для каждой позиции \(s_i\) найдём наибольший палиндром, центр которого совпадает с \(s_i\). Половину его длины назовём радиусом. Чётные и нечётные палиндромы будем рассматривать отдельно.

              Наивное решение — перебрать \(s_i\), а для него вторым циклом находить наибольшую искомую длину:

              -

              Значение в массиве r в дальнейшем будем называть радиусом палиндрома.

              -

              Тот же пример \(s = aa\dots a\) показывает, что данная реализация работает за \(O(n^2)\). Для оптимизации, применим идею, знакомую из алгоритма z-функции: при инициализации \(r\) будем пользоваться уже посчитанными \(r\).

              -

              Будем поддерживать \((l, r)\) — интервал, соответствующий самому правому из найденных подпалиндромов. Тогда мы можем сказать, что часть наибольшего палиндрома с центром в \(s_i\), которая лежит в \(s_{l:r}\) имеет радиус \(\min(r-i, len_{l+r-i})\).

              +

              Тот же пример \(s = aa\dots a\) показывает, что данная реализация работает за \(O(n^2)\). Для оптимизации применим идею, знакомую из алгоритма z-функции: при инициализации \(t_i\) будем пользоваться уже посчитанными \(t\).

              +

              Будем поддерживать \((l, r)\) — интервал, соответствующий самому правому из найденных подпалиндромов. Тогда мы можем сказать, что часть наибольшего палиндрома с центром в \(s_i\), которая лежит в \(s_{l:r}\) имеет радиус хотя бы: \(\min(r-i, t{l+r-i})\).

              \((r-i)\) означает длину при которой произошёл бы выход за \(S_{(l, r)}\), а \(len_{l+r-i}\) – значение \(len\) в позиции, зеркальной относительно центра \(S_{(l, r)}\).

              - +

              Так же, как и z-функция, алгоритм работает за линейное время.

              Цикл \(while\) запускается только если \(len_i = r-i\) (иначе палиндром уже во что-то упёрся). Значит, каждая его итерация сдвигает \(r\) на единицу вправо. Таким образом, так как \(r \leq n\), получаем, что суммарно эти циклы отработают за \(O(n)\).

              Чтобы учесть чётные палиндромы, сделаем замену: \(S = s_1 s_2 \dots s_n \to S^* = s_1 \# s_2 \# \dots \# s_n\). Теперь нечётные палиндромы с центром в \(s_i\) соответствуют нечётным палиндромам исходной строки, а нечётные палиндромы с центром в \(\#\) – чётным.

              -

              Дерево палиндромов

              -

              Данная структура, предложенная Михаилом Рубинчиком, представляет собой другой формат хранения информации обо всех подпалиндромах строки, более мощный по своей сути. чем \(n\) палиндромов. Пусть мы наращиваем строку по одному символу и в

              -

              Сперва докажем, что у любой строки есть не больше, данный момент имеем наибольший суффикс-палиндром \(S_{l,r}\). Пусть у него есть суффикс-палиндром \(S_{l',r}\). Тогда он уже имеет вхождение в позиции \(S_{l,l+r-l'}\). Таким образом, с каждым новым символом у строки появляется не более одного нового палиндрома и если таковой есть, то это всегда наибольший суффикс-палиндром.

              -

              Всем палиндромам строки удобно сопоставить следующую структуру, которая и называется деревом палиндромов: возьмём от каждого палиндрома его правую половинку (например, \(caba\) для \(abacaba\) или \(ba\) для \(abba\)) и добавим в префиксное дерево. Чётные и нечётные палиндромы при этом рассматриваем отдельно. Таким образом будет получено взаимооднозначное соответствие между вершинами дерева и подпалиндромами.

              -

              Пусть мы построили дерево палиндромов для первых \(n\) символов строки и хотим добавить очередной символ. Для этого будем поддерживать наибольший суффикс-палиндром. Когда к нам придёт очередной символ, нам нужно будет найти наибольший суффикс указанного палиндрома, который может быть дополнен новым символом до палиндрома, это и будет новый суффикс-палиндром. Для этих целей будем поддерживать суффиксную ссылку, аналогичную таковой в алгоритме Ахо-Корасик, она будет вести из \(v\) в наибольший собственный суффикс-палиндром \(v\). При добавлении очередного символа, будем подниматься по суффиксным ссылкам, пока не найдём место, где можно совершить переход. Там нам, возможно, придётся создать новую вершину в дереве и для неё понадобится суффиксная ссылка. Чтобы найти её, будем продолжать подниматься по суффиксным ссылкам предыдущего суффикс-палиндрома, пока не найдём второе место, которое можно дополнить очередным символом.

              -

              Покажем линейность. Рассмотрим длину наибольшего суффикс-палиндрома строки. Каждый новый символ увеличивает её не более, чем на \(2\). При этом каждый подъём по суффиксной ссылке в цикле уменьшает её, поэтому основной цикл работает линейное время. Аналогичными рассуждениями о втором суффикс-палиндроме получаем, что пересчёт суффиксных ссылок при создании новых вершин также работает за линейное время суммарно.

              -

              Наконец, приведём реализацию указанного алгоритма:

              -
              const int maxn = 1e5 + 42;
              -map<char, int> to[maxn];
              -int len[maxn], link[maxn], s[maxn];
              -int sz, n, last;
              -
              +

              Дерево палиндромов (англ. palindromic tree, EERTREE) — структура данных, использующая другой, более мощный формат хранения информации обо всех подпалиндромах, чем размеры \(n\) палиндромов. Она была предложена Михаилом Рубинчиком на летних петрозаводских сборах в 2014-м году.

              +

              Лемма. В строке есть не более \(n\) различных подпалиндромов.

              +

              Доказательство. Пусть мы дописываем к строке по одному символу и в данный момент, записав \(r\) символов, имеем наибольший суффикс-палиндром \(s_{l:r}\). Пусть у него, в свою очередь, есть суффикс-палиндром \(s_{l':r} = t\). Тогда он также имеет более раннее вхождение в строку как \(s_{l:l+r-l'} = t\). Таким образом, с каждым новым символом у строки появляется не более одного нового палиндрома, и если таковой есть, то это всегда наибольший суффикс-палиндром.

              +

              Этот факт позволяет сопоставить всем палиндромам строки сопоставить следующую структуру: возьмём от каждого плаиндрома его правую половину (например, \(caba\) для \(abacaba\) или \(ba\) для \(abba\); будем рассматривать пока что только чётные палиндромы) и добавим все эти половины в префиксное дерево — получившуюся структуру и будем называть деревом палиндромов.

              +

              Построение за линейное время

              +

              Научимся его строить эффективно. Будем поддерживать наибольший суффикс-палиндром. Когда мы будем дописывать очередной символ \(c\), нужно найти наибольший суффикс этого палиндрома, который может быть дополнен символом \(c\) — это и будет новый наидлиннейший суффикс-палиндром.

              +

              Для этого поступим аналогично алгоритму Ахо-Корасик: будем поддерживать для каждого палиндрома суффиксную ссылку \(l(v)\), ведущую из \(v\) в её наибольший суффиксный палиндром. При добавлении очередного символа, будем подниматься по суффиксным ссылкам, пока не найдём вершину, из которой можно совершить нужный переход.

              +

              Если в подходящей вершине этого перехода не существовало, то нужно создать новую вершину, а для неё тоже понадобится своя суффиксная ссылка. Чтобы найти её, будем продолжать подниматься по суффиксным ссылкам предыдущего суффикс-палиндрома, пока не найдём второе такое место, которое мы можем дополнить символом \(c\).

              +

              Асимптотика. Покажем линейность алгоритма. Рассмотрим длину наибольшего суффикс-палиндрома строки. Каждый новый символ увеличивает её не более, чем на 2. При этом каждый переход по суффиксной ссылке уменьшает её, поэтому нахождение первого суффикс-палиндрома амортизированно работает за линеейное время.

              +

              Аналогичными рассуждениями о длине второго суффикс-палиндрома получаем, что пересчёт суффиксных ссыдлк при создании новых вершин тоже суммарно работает за линейное время.

              + +int get_link(int v) // Суффикс v, который дополняется до палиндрома +{ + while(s[n - len[v] - 2] != s[n - 1]) + v = link[v]; + return v; +} +void add_letter(char c) +{ + s[n++] = c; + last = get_link(last); + if(!to[last][c]) + { + len[sz] = len[last] + 2; + link[sz] = to[get_link(link[last])][c]; + to[last][c] = sz++; + } + last = to[last][c]; +}
              +

              Здесь мы использовали массив для хранения переходов.

              +

              Как и в случае с Ахо-Корасик, существуют неамортизированные O(n ) и O(n ) версии.

              +

              TODO

              +

              Покажем линейность. Рассмотрим длину наибольшего суффикс-палиндрома строки. Каждый новый символ увеличивает её не более, чем на \(2\). При этом каждый подъём по суффиксной ссылке в цикле уменьшает её, поэтому основной цикл работает линейное время. Аналогичными рассуждениями о втором суффикс-палиндроме получаем, что пересчёт суффиксных ссылок при создании новых вершин также работает за линейное время суммарно.

              +

              Наконец, приведём реализацию указанного алгоритма:

              +

              Как и в случае с Ахо-Корасик, существуют неамортизированные \(O(n \Sigma)\) и \(O(n \log \Sigma)\) версии.

              diff --git a/ru/suffix-automaton.html b/ru/suffix-automaton.html index da7ce1c..b6745e5 100644 --- a/ru/suffix-automaton.html +++ b/ru/suffix-automaton.html @@ -4,13 +4,76 @@ - Введение {#введение .unnumbered .unnumbered} + Суффиксный автомат + + + +

              If you're seeing this, your JavaScript is probably disabled.

              Go to one of those then: algorithmica.org/en/ or algorithmica.org.ru. From c41703a286298a8e9ed8be055c053ee15b08c3f9 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 7 Jul 2019 18:47:16 +0000 Subject: [PATCH 128/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/fenwick.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ru/fenwick.html b/ru/fenwick.html index 1a5f308..a8f1184 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -112,7 +112,7 @@

              Дерево Фенвика

              НЯ!
              Эта статья полна любви и обожания.
              Возможно, стоит добавить ещё больше?

              Дерево Фенвика

              -

              Дерево Фенвика или бинарно индеквированное дерево (англ. binary indexed tree) — структура данных, которая на многих задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, сколько и массив той же длины), намного быстрее пишется и легче обобщается на большие размерности.

              +

              Дерево Фенвика или бинарно индексированное дерево (англ. binary indexed tree) — структура данных, которая на многих задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, сколько и массив той же длины), намного быстрее пишется и легче обобщается на большие размерности.

              Определение

              Пусть дан массив \(a\) длины \(n\). Деревом Фенвика будем называть массив \(t\) той же длины, объявленный следующим образом:

              \[ From 5a0b04d34751878cc500e51f7b135f86247fe8ea Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 9 Jul 2019 04:32:11 +0000 Subject: [PATCH 129/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sparse-table.html | 58 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/ru/sparse-table.html b/ru/sparse-table.html index 2c14bda..afbba93 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -124,14 +124,14 @@

              Разреженная табли

              Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

              int a[maxn], lg[maxn], mn[maxn][logn];
               
              -int rmq (int l, int r) {
              -    int t = lg[r-l+1];
              -    return min(mn[l][t], mn[r-(1<<t)+1][t]);
              +int rmq (int l, int r) { // полуинтервал [l; r)
              +    int t = lg[r-l];
              +    return min(mn[l][t], mn[r-(1<<t)][t]);
               }
               
               // Это считается уже где-то в первых строчках main:
               
              -for (int l = 1; l < logn; l++)
              +for (int l = 0; l < logn; l++)
                   for (int i = (1<<l); i < maxn; i++)
                       lg[i] = l;
               
              @@ -149,5 +149,55 @@ 

              Примечания

              Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (\(a ∘ (b ∘ c) = (a ∘ b) ∘ c\)), коммутативность (\(a ∘ b = b ∘ a\)) и идемпотентность (\(a ∘ a = a\)). Например, её можно применять для нахождения gcd.

              Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

              Упражнение. Какой из 4 вариантов итерирования и layout-а самый эффективный? (Подсказка: не тот, который приведен в этой статье.)

              +

              Disjoint Sparse Table

              +

              Представим себе, что мы очень-очень хотим написать sparse table для нахождения функции f на отрезке, но f не удовлетворяет идемпотентности. Тогда мы не можем испольовать наш стандартный трюк — нельзя взять \(O(1)\) пересекающихся отрезков. Но можно придумать способ разбивать запрос на \(O(1)\) непересекающихся отрезков. Реализация окажется медленнее и сложнее, но будет более гибкой, чем обычный sparse table.

              +

              Мысленно построим на массиве дерево отрезков. Пусть корень дерева отвечает за отрезок длины \(n\), его сыновья за отрезки длины \(\frac{n}{2}\), итд. Введем понятие центрального элемента — для полуинтервала \([l; r)\) назовем центральным элемент с индексом \(\frac{l + r}{2}\).

              +

              +

              Для каждого отрезка преподсчитаем f на всех подотрезках с правыми концами в центральном элементе и с левыми концами в его правом соседе: \(T([l; r)) = \{[l; \frac{l + r}{2} + 1), ..., [\frac{l + r}{2} + 1, r) \}\).

              +

              Утверждение. Любой полуинтервал запроса \([l, r)\) разбивается на \(O(1)\) непересекающихся преподсчитанных интервалов.

              +

              Доказательство. Возьмем самый высокий центральный элемент, принадлежащий запросу. Если он разбивает запрос на три и более интервалов, то тогда он не самый высокий. Противоречие.

              +

              Тогда отвечать на запрос достаточно просто — надо найти нужный центральный элемент, и сделать два запроса для него. Осталось найти центральный элемент за \(O(1)\). Тут мы подходим к тонкостям реализации:

              +
                +
              • Будем писать код так, как будто говорим про дерево отрезков, причем дополним его размер до степени двойки
              • +
              • Будем хранить всю структуру данных так же, как и обычный sparse table, в массиве \(t[logn][maxn]\), где первым параметром мы будем хранить глубину центрального элемента, а вторым — правую границу соответствующего полуинтервала
              • +
              • Если предыдущий пункт непонятен, то представьте себе разрезанное по слоям дерево отрезков.
              • +
              • Для ответа на запрос нам нужно найти глубину оптимального центрального элемента. Посмотрим на двоичную запись \(l\) и \(r\). \(h_{[l, r)} = maxlog - 1 - maxbit(l \oplus r)\), если \(n = 2^{maxlog}\), а \(maxbit(x) = \lfloor{\log_2{x}}\rfloor\). Доказательство методом пристального изучения картинки.
              • +
              + From 944e83b7474e2418f5bd1be9017ef9687ce0ffdc Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 9 Jul 2019 09:01:13 +0000 Subject: [PATCH 130/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sparse-table.html | 155 ++++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 70 deletions(-) diff --git a/ru/sparse-table.html b/ru/sparse-table.html index afbba93..87a64c7 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -110,7 +110,7 @@

              Разреженная таблица

              Разреженная таблица

              Разреженная таблица (англ. sparse table) — структура данных, позволяющая отвечать на запросы минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) времени и памяти.

              -

              Разреженная таблица — это следующий двумерный массив размера \(n \times\log n\):

              +

              Определение. Разреженная таблица — это следующий двумерный массив размера \(n \times\log n\):

              \[ t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} \]

              @@ -124,80 +124,95 @@

              Разреженная табли

              Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

              + +// Это считается где-то в первых строчках main: + +for (int l = 0; l < logn; l++) + for (int i = (1<<l); i < maxn; i++) + lg[i] = l; + +for (int i = n-1; i >= 0; i--) { + mn[i][0] = a[i]; + for (int l = 0; l < logn-1; l++) + mn[i][l+1] = min(mn[i][l], mn[i+(1<<l)][l]); +}

              +

              Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

              +

              Упражнение. Какой из 4 вариантов итерирования и layout-а самый эффективный? (Подсказка: не тот, который приведен выше.)

              +

              Применения

              +

              Разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но можно достраивать на ходу — см. задачу «Антиматерия» с РОИ-2017).

              +

              Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.

              2d Static RMQ

              Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

              -

              В общем же случае от нас просят минимум тоже на прямоугольниках. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет.

              -

              Примечания

              -

              В отличие от дерева отрезков, разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но можно достраивать на ходу — см. задачу «Антиматерия» с РОИ-2017).

              -

              Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.

              -

              Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (\(a ∘ (b ∘ c) = (a ∘ b) ∘ c\)), коммутативность (\(a ∘ b = b ∘ a\)) и идемпотентность (\(a ∘ a = a\)). Например, её можно применять для нахождения gcd.

              -

              Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

              -

              Упражнение. Какой из 4 вариантов итерирования и layout-а самый эффективный? (Подсказка: не тот, который приведен в этой статье.)

              +

              В общем же случае от нас просят минимум на прямоугольниках \(d\)-мерного массива. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет — нужно хранить минимумы на всех гипер-прямоугольниках со сторонами степени двойки.

              +

              Ограничения на операцию

              +

              Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (\(a ∘ (b ∘ c) = (a ∘ b) ∘ c\)), коммутативность (\(a ∘ b = b ∘ a\)) и идемпотентность (\(a ∘ a = a\)). Например, её можно применять для нахождения \(\gcd\).

              +

              Если операция не идемпотентна, то для нахождения её результата можно действовать так: возьмём самый длинный упирающийся в левую границу запроса отрезок, прибавим его к ответу, сдвинем указатель на его правый конец и будем так продолжать, пока не обработаем весь запрос целиком.

              + +

              Это работает быстрее, чем, например, дерево отрезков, но тоже асимптотически за \(O(\log n)\), да ещё и с дополнительной памятью. Но есть способ это ускорить.

              Disjoint Sparse Table

              -

              Представим себе, что мы очень-очень хотим написать sparse table для нахождения функции f на отрезке, но f не удовлетворяет идемпотентности. Тогда мы не можем испольовать наш стандартный трюк — нельзя взять \(O(1)\) пересекающихся отрезков. Но можно придумать способ разбивать запрос на \(O(1)\) непересекающихся отрезков. Реализация окажется медленнее и сложнее, но будет более гибкой, чем обычный sparse table.

              -

              Мысленно построим на массиве дерево отрезков. Пусть корень дерева отвечает за отрезок длины \(n\), его сыновья за отрезки длины \(\frac{n}{2}\), итд. Введем понятие центрального элемента — для полуинтервала \([l; r)\) назовем центральным элемент с индексом \(\frac{l + r}{2}\).

              -

              -

              Для каждого отрезка преподсчитаем f на всех подотрезках с правыми концами в центральном элементе и с левыми концами в его правом соседе: \(T([l; r)) = \{[l; \frac{l + r}{2} + 1), ..., [\frac{l + r}{2} + 1, r) \}\).

              -

              Утверждение. Любой полуинтервал запроса \([l, r)\) разбивается на \(O(1)\) непересекающихся преподсчитанных интервалов.

              -

              Доказательство. Возьмем самый высокий центральный элемент, принадлежащий запросу. Если он разбивает запрос на три и более интервалов, то тогда он не самый высокий. Противоречие.

              -

              Тогда отвечать на запрос достаточно просто — надо найти нужный центральный элемент, и сделать два запроса для него. Осталось найти центральный элемент за \(O(1)\). Тут мы подходим к тонкостям реализации:

              -
                -
              • Будем писать код так, как будто говорим про дерево отрезков, причем дополним его размер до степени двойки
              • -
              • Будем хранить всю структуру данных так же, как и обычный sparse table, в массиве \(t[logn][maxn]\), где первым параметром мы будем хранить глубину центрального элемента, а вторым — правую границу соответствующего полуинтервала
              • -
              • Если предыдущий пункт непонятен, то представьте себе разрезанное по слоям дерево отрезков.
              • -
              • Для ответа на запрос нам нужно найти глубину оптимального центрального элемента. Посмотрим на двоичную запись \(l\) и \(r\). \(h_{[l, r)} = maxlog - 1 - maxbit(l \oplus r)\), если \(n = 2^{maxlog}\), а \(maxbit(x) = \lfloor{\log_2{x}}\rfloor\). Доказательство методом пристального изучения картинки.
              • -
              - +

              Мы хотим иметь какую-то структуру, которая может считать функцию \(f\) на отрезке, при том что \(f\) не удовлетворяет условию идемпотентности. Стандартная разреженная таблица тут не подойдёт — в ней нельзя найти \(O(1)\) непересекающихся отрезков.

              +

              Сделаем сделующее: мысленно построим на массиве дерево отрезков и (уже не мысленно) для каждого его отрезка \([l, r)\) посчитаем \(f\) на всех отрезках от его центрального элемента — то есть от элемента с индексом \(m = \lfloor \frac{l + r}{2} \rfloor\) — до всех остальных элементов \(k \in [l, r)\). Для каждого элемента массива будет \(O(\log n)\) центральных, а значит суммарно на это потребуются те же \(O(n \log n)\) времени и памяти.

              +

              Утверждение. Любой запрос \([l, r)\) разбивается на \(O(1)\) непересекающихся преподсчитанных интервалов.

              +

              Доказательство. Возьмем самый высокий центральный элемент \(m\), принадлежащий запросу. Его отрезок полностью покрыает запрос — если бы это было не так, то самым высоким был бы не \(m\), а какая-то из его границ . Раз отрезок запроса \([l, r)\) полностью покрыт, и \(m\) лежит внутри него, то \([l, r)\) можно разбить на предпосчитанные \([l, m)\) и \([m, r)\).

              +

              Решать задачу мы так и будем: найдём нужный центральный элемент и сделаем два запроса от него.

              +

              Реализация

              +

              Сложная часть — найти этот центральный элемент за константное время — станет чуть проще, если мы будем работать только с массивами длины степени двойки и, соответственно, полными деревьями отрезков. Массивы неподходящей длины дополним до ближайшей степени двойки специальным нейтральным элементом, зависящим от самой операции (например, \(0\) для сложения или \(1\) для умножения).

              +

              Будем хранить всю структуру (предпосчитанные значения на отрезках) в массиве t[logn][maxn], в котором первым параметром будет уровень в дереве отрезков (число \(d\) для отрезков размера \(2^d\)), а вторым — граница соответствующего интервала (число \(k\)). Этой информации достаточно, чтобы однозначно восстановить отрезок.

              +

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

              +

              Заметим, что любая вершина \(k\)-того уровня соответствует какому-то отрезку \([l, l + 2^k)\), причём \(l\) делится на \(2^k\). Двоичное представление всех индексов на этом отрезке будет иметь какой-то общий префикс, а последние \(k\) знаков будут различными.

              +

              Нам нужно найти уровень нужного центрального элемента — это то же самое, что и уровень наименьшего общего отрезка для элементов \(l\) и \(r\). Используя предыдущий факт, получаем, что искомый уровень будет равен позиции самого значимого бита, который отличается у чисел \(l\) и \(r\). Его можно найти за константное время выражением \(h_{[l,r)]} = \lfloor \log_2 (l \oplus r) \rfloor\), если заранее предпосчитать логарифмы.

              +

              Для примера, построим DST для умножения по составному модулю:

              + +

              TODO: скорее всего, тут есть баги.

              From df249e2d2d75087456af476352bb1ef455a8f329 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 9 Jul 2019 09:03:36 +0000 Subject: [PATCH 131/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ru/index.html b/ru/index.html index 8bfb761..efddef0 100644 --- a/ru/index.html +++ b/ru/index.html @@ -52,7 +52,7 @@

              Структуры данных

            24. Дерево отрезков ленивые операции, динамическое ДО, персистентноcть
            25. Декартово дерево неявный ключ, персистентность
            26. Дерево Фенвика почему работает, многомерный случай, бинарный поиск
            27. -
            28. Разреженная таблица static rmq, многомерный случай
            29. +
            30. Разреженная таблица static rmq, многомерный случай, disjoint st
            31. From 721a2edc11d60d7c40b435acda31e081c3a82d74 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Wed, 17 Jul 2019 21:13:25 +0000 Subject: [PATCH 132/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 ru/sse.html diff --git a/ru/sse.html b/ru/sse.html new file mode 100644 index 0000000..6d28051 --- /dev/null +++ b/ru/sse.html @@ -0,0 +1,279 @@ + + + + + + + Векторизация + + + + + + + + + + + + + + + +
              +

              Векторизация

              +
              +

              Векторизация

              +

              Рассмотрим следующую программу, в которой складывают два массива:

              + +

              Если скомпилировать этот код под GCC без всяких дополнительных настроек и запустить, он отработает за 1.65 секунды.

              +

              Добавим теперь следующую магическую директиву в самое начало программы:

              + +

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

              +

              Чтобы понять, что здесь происходит, нам нужно сначала разъяснить некоторые особенности работы современных компьютеров.

              +

              Complex Instruction Set Computing

              +

              Раньше, во времена, когда компьютеры назывались ЭВМ-ами и занимали целую комнату, увеличение производительности происходило в основном за счёт увеличения тактовой частоты. Тактовая частота условно равна количеству инструкций, выполняемому процессором за единицу времени. (На современных процессорах это не так — разные инструкции занимают разное время, которое ещё и может зависеть от разных обстоятельств.)

              +

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

              +

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

              +
                +
              • RISC (англ. reduced instruction set computer), в которых длина кода (идентификатора) самой инструкции ограничена, а значит ограничено и само количество инструкций. Самые первые компьютеры относились к этому типу и могли не иметь даже отдельных инструкций для умножения и деления. Такие процессоры требует меньше транзисторов, и как следствие сами меньше, дешевле и потребляют меньше энергии. Самое популярное семейство архитектур называется arm и используется почти на всех современных мобильных устройствах.

              • +
              • CISC (англ. complex instruction set computer), к которому относят всё, что не RISC — в них длина команды не фиксирована, что позволяет поддерживать практически произовльное количество инструкций. Самоя популярное семейство архитектур называется x86 и используется почти на всех современных декстопах и серверах.

              • +
              +

              +

              Новые инструкции стали добавлять постепенно, причём разные в зависимости от области применения.

              +
                +
              • В общеприменимых CPU довольно быстро добавили инструкцию, которая принимает числа \((x, y)\) и загружает в регистр данные по адресу \(a \cdot x + y\). Это полезно при индексации массивов — не нужно отдельно индекс считать.

              • +
              • На графических сопроцессорах появилась отдельная инструкция, которую называют «saxpy» (сокращенно от выражения s += a * x + y), которая полезна, например, при перемножении матриц.

              • +
              • В последние GPU от Nvidia добавили «tensor core» — отдельную схему, которая перемножает две матрицы \(4 \times 4\) и прибавляет к третьей, как бы производя \(4 \times 4 \times 4 = 64\) умножений и \(4 \times 4 = 16\) сложений за раз, что сильно ускоряет алгоритмы блочного матричного умножения.

              • +
              +

              В этой статье мы сфокусируемся на отдельным виде инструкций, которые позволяют выполнять одну и ту же операцию сразу на какой-то последовательности данных. Эта концепция называется SIMD-параллелизмом (англ. single instruction, multiple data).

              +

              Streaming SIMD Extensions

              +

              SSE — это обобщённое называние всех SIMD-инструкций для x86.

              +

              Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательный блок из памяти, над ним производится какая-то последовательность операций, а итоговый результат записывается обратно в память. Сами операции обычно разбивают эту булеву последовательность на блоки, например, по 32 бит, и логически работают уже с ними.

              +

              Довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

              +

              Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует).

              +

              Очень часто SSE используют для работы с действительными числами, и вы этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходых данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

              +

              +

              Конкретный набор инструкций и размеры регистров зависит от вендора и поколения архитектуры. На данный момент (лето 2019 года) большинство процессоров архитектуры x86 производит Intel, поэтому мы сконцентрируемся именно на их наборе инструкций.

              +

              Поддержка SIMD-инструкций добавлялись постепенно, сохраняя обратную совместимость. Если третий пентиум в 1999-м году умел работать с регистрами размера 128, то в самых современных i7 есть 512-битные регистры.

              +

              +

              Чтобы разработчикам не нужно было предоставлять отдельные оптимизированные бинарники под каждую конкретную архитектуру, информация о поддержке наборов инструкций процессором зашита в ассемблерную инструкцию cpuid, которую можно просто вызвать в рантайме и всё узнать: например так.

              +

              В GCC есть встроенная функция __builtin_cpu_supports, которая берёт строчку-название набора инструкций (“sse”, “avx2”, “avx512f” и т. п.) и возвращает целое число — ноль или какую-то степень двойки. Эта функция работает так: входная строка во время компиляции переводится в нужную степень двойки, которая в рантайме просто AND-ится с маской из cpuid и возвращается — всё ради эффективности.

              +

              Экономя время читателю: сервера CodeForces и большинство онлайн джаджей на момент написания статьи поддерживают AVX2, то есть умеют работать с 256-битными регистрами.

              +

              C++ intrinsics

              +

              SSE это те же чистые ассемблерные инструкции. Языки с каким-либо более верхним уровнем абстракции напрямую работать с ними уже не могут. Однако не обязательно писать на чистом ассемблере, чтобы их использовать — разработчики компиляторов уже позаботились об этом за вас и сделали встроенные функции-обёртки, которые называют интринзиками (англ. intrinsic — «внутренний»).

              +

              Чтобы их подключить, нужно указать include на соответствующий заголовочный файл, а также сказать компилятору о том, что мы хотим использовать конкретный набор или наборы инструкций. В примере из начала статьи мы сделали именно это, указав target("avx2") — компилятор получил доступ к более широким регистрам и продвинутым инструкциям для них, и смог соптимизировать программу примерно в два раза (по умолчанию включены 128-битные sse и sse2, поэтому в 2, а не в \(\frac{256}{32} = 8\)).

              +

              По аналогии с <bits/stdc++.h>, в GCC такой же заголовочный файл <x86intrin.h>, включающий в себя сразу все SSE-интринзики. Шаблон любителя засоренных неймспейсов и избыточно долгой компиляции может начинаться так:

              + +

              Простой цикл, в котором складывают два массива 64-битных действительных чисел, на SSE-интринзиках будет выглядеть так:

              + +

              Конвенция именования интринзиков такая же, как самих инструкций, а она такая же, как в ассемблере — то есть максимально короткая и непонятная.

              +

              Большинство команд кодируются как _mm<размерность>_<действие>_<тип>. Например:

              +
                +
              • _mm_add_epi16 — складывает две пачки 16-битных extended packed integer, проще говоря \(\frac{128}{16} = 8\) short-ов (в инструкциях, где размер регистра не указан, он равен 128).

              • +
              • _mm256_acos_pd — принимает один регистр, содержащий 4 double-ов, и возвращает их арк-косинусы.

              • +
              • _mm256_broadcast_sd — бродкастит (копирует) double из памяти во все четыре слота в регистре.

              • +
              • _mm256_ceil_pd — округляет double к ближайшему int-у вверх.

              • +
              +

              Полная документация по SSE-интринзикам — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              +

              Выравнивание. У операций чтения и записи есть по две версии: load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первая корректно работает только тогда, когда весь считываемый блок помещается на одну кэш-линию (в противном случае она вызывает segfault в рантайме), в то время как unaligned версия работает всегда и везде.

              +

              Это имело очень большое значение на старых компьютерах — если не получалось «выровнять» память, то производительность могла резко упасть (в два и более раз), потому что нарушался паттерн последовательного доступа. На современных компьютерах это не так значительно: будет медленне, но в пределах 5%. Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

              + +

              Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и это просто нужно для проверки типов и избежания связанных ошибок.

              +

              Автовекторизация

              +

              В самом начале статьи мы увидели пример, когда оптимальный результат был получен без какого-либо переписывания. Зачем вообще

              +

              Иногда — очень редко — программист всё-таки умнее компилятора и знает что-то больше.

              +

              На высоких уровнях оптимизации

              +

              Как мы убедились раньше, компилятор и без нас справляется оптимизировать циклы. Зачем вообще программисту что-либо делать, кроме подключения нужного таргета компиляции?

              +

              Обратите внимание, что . На старых компьютерах это было существенно (потеря производительности разница в более чем два раза), на современных же

              +

              Выравнивание

              +

              Ликбез о том, как работают кэши:

              +

              На самом деле, очень часто основным ограничением является работа памяти. Когда массив не будет помещаться в кэш, существенного ускорения уже не будет.

              +

              Ещё одна вещь, про которую нужно думать — это выравнивание (англ. alignment) в памяти.

              +

              Автовекторизация

              +

              Почему компилятор сам это не делает?

              +

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

              +

              Иногда компилятор просто-напросто не имеет достаточно информации, чтобы убедиться в корректности. Например, складываемые отрезки могут на самом деле пересекаться. При достаточно больших циклах на высоком уровне оптимизации компилятор сделает эту проверку прямо в рантайме, но в некоторых случаев этого не хватит.

              +

              От Intel есть гайд как оставлять намёки компилятору.

              +

              На контестах

              +

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

              +

              Бинарное возведение в степень

              + +

              Возведём миллион (точнее, \(2^{20}\)) чисел в степень:

              + +

              Этот код работает за X секунд.

              +

              Для оптимизации, напишем sse-версию:

              + +

              mixed precision

              + + From 63084bcca92bb6536c98419006a8f97cbfcdec08 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 18 Jul 2019 10:50:30 +0000 Subject: [PATCH 133/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 120 ++++++++++++++++++++++++++-------------------------- 1 file changed, 59 insertions(+), 61 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index 6d28051..f0903fc 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -155,13 +155,13 @@

              Complex Instruction Set Computing

              В этой статье мы сфокусируемся на отдельным виде инструкций, которые позволяют выполнять одну и ту же операцию сразу на какой-то последовательности данных. Эта концепция называется SIMD-параллелизмом (англ. single instruction, multiple data).

              Streaming SIMD Extensions

              SSE — это обобщённое называние всех SIMD-инструкций для x86.

              -

              Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательный блок из памяти, над ним производится какая-то последовательность операций, а итоговый результат записывается обратно в память. Сами операции обычно разбивают эту булеву последовательность на блоки, например, по 32 бит, и логически работают уже с ними.

              +

              Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно разбивают эту булеву последовательность на блоки, например, по 32 бит, и логически работают уже с ними.

              Довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

              Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует).

              Очень часто SSE используют для работы с действительными числами, и вы этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходых данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

              Конкретный набор инструкций и размеры регистров зависит от вендора и поколения архитектуры. На данный момент (лето 2019 года) большинство процессоров архитектуры x86 производит Intel, поэтому мы сконцентрируемся именно на их наборе инструкций.

              -

              Поддержка SIMD-инструкций добавлялись постепенно, сохраняя обратную совместимость. Если третий пентиум в 1999-м году умел работать с регистрами размера 128, то в самых современных i7 есть 512-битные регистры.

              +

              Поддержка SIMD-инструкций добавлялись постепенно, сохраняя обратную совместимость. Если третий пентиум в 1999-м году умел работать с регистрами размера 128, то в самых современных i7 есть 512-битные регистры. Автор не является специалистом в проектировании микропроцессоров, но предполагает, то регистры больше 64 байт (512 бит) появятся очень не скоро, потому что это уже больше размера кэш-линии

              Чтобы разработчикам не нужно было предоставлять отдельные оптимизированные бинарники под каждую конкретную архитектуру, информация о поддержке наборов инструкций процессором зашита в ассемблерную инструкцию cpuid, которую можно просто вызвать в рантайме и всё узнать: например так.

              В GCC есть встроенная функция __builtin_cpu_supports, которая берёт строчку-название набора инструкций (“sse”, “avx2”, “avx512f” и т. п.) и возвращает целое число — ноль или какую-то степень двойки. Эта функция работает так: входная строка во время компиляции переводится в нужную степень двойки, которая в рантайме просто AND-ится с маской из cpuid и возвращается — всё ради эффективности.

              @@ -201,7 +201,7 @@

              C++ intrinsics

            32. _mm256_broadcast_sd — бродкастит (копирует) double из памяти во все четыре слота в регистре.

            33. _mm256_ceil_pd — округляет double к ближайшему int-у вверх.

            34. -

              Полная документация по SSE-интринзикам — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              +

              Комбинаторно получается огромное количество различных функций. Полная документация по SSE-интринзикам — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              Выравнивание. У операций чтения и записи есть по две версии: load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первая корректно работает только тогда, когда весь считываемый блок помещается на одну кэш-линию (в противном случае она вызывает segfault в рантайме), в то время как unaligned версия работает всегда и везде.

              Это имело очень большое значение на старых компьютерах — если не получалось «выровнять» память, то производительность могла резко упасть (в два и более раз), потому что нарушался паттерн последовательного доступа. На современных компьютерах это не так значительно: будет медленне, но в пределах 5%. Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

              alignas(32) int a;
              @@ -213,67 +213,65 @@ 

              C++ intrinsics

              // ... }

              Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и это просто нужно для проверки типов и избежания связанных ошибок.

              -

              Автовекторизация

              -

              В самом начале статьи мы увидели пример, когда оптимальный результат был получен без какого-либо переписывания. Зачем вообще

              -

              Иногда — очень редко — программист всё-таки умнее компилятора и знает что-то больше.

              -

              На высоких уровнях оптимизации

              -

              Как мы убедились раньше, компилятор и без нас справляется оптимизировать циклы. Зачем вообще программисту что-либо делать, кроме подключения нужного таргета компиляции?

              -

              Обратите внимание, что . На старых компьютерах это было существенно (потеря производительности разница в более чем два раза), на современных же

              -

              Выравнивание

              -

              Ликбез о том, как работают кэши:

              -

              На самом деле, очень часто основным ограничением является работа памяти. Когда массив не будет помещаться в кэш, существенного ускорения уже не будет.

              -

              Ещё одна вещь, про которую нужно думать — это выравнивание (англ. alignment) в памяти.

              -

              Автовекторизация

              -

              Почему компилятор сам это не делает?

              -

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

              -

              Иногда компилятор просто-напросто не имеет достаточно информации, чтобы убедиться в корректности. Например, складываемые отрезки могут на самом деле пересекаться. При достаточно больших циклах на высоком уровне оптимизации компилятор сделает эту проверку прямо в рантайме, но в некоторых случаев этого не хватит.

              -

              От Intel есть гайд как оставлять намёки компилятору.

              -

              На контестах

              -

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

              -

              Бинарное возведение в степень

              - +

              Трудности автовекторизации

              +

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

              +

              Зачем тогда вообще программисту делать что-либо ещё? Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

              +

              Рассмотрим этот же пример, убрав из него всё лишнее:

              + +

              Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

              +

              Во-первых, потому что это не всегда корректно. Предположим, что a[] и c[] пересекаются, причём так, что указатели на начало массивов отличаются на 1-2 позиции. Ну, мало ли — может, мы такой изощрённой свёрткой хотели посчитать последовательность Фибоначчи. Тогда в simd-блоках данные будут пересекаться, и наблюдаемое поведение будет совсем не то, которое мы хотели.

              +

              Во-вторых, мы ничнго не знаем ничего про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

              +

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

              +

              Иногда мы можем считать внутри цикла что-то сложнее суммы — например, что-то, использующее if-ы или какие-нибудь нетривиальные внешние функции, для которых, тем не менее, есть SSE-аналог.

              +

              Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но часто проще написать всё самому.

              +

              Нетривиальный пример

              +

              Пусть мы хотим возвести \(10^7\) чисел в какие-то степени и перемножить.

              +

              Возведём миллион (точнее, \(2^{20}\)) чисел в степень:

              - +

              Этот код работает за X секунд.

              Для оптимизации, напишем sse-версию:

              - +

              mixed precision

              From 9da179223edbd703c0af37a1945d79a74cd1dd96 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 21 Jul 2019 16:11:37 +0000 Subject: [PATCH 134/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 102 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index f0903fc..4791028 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -155,7 +155,7 @@

              Complex Instruction Set Computing

              В этой статье мы сфокусируемся на отдельным виде инструкций, которые позволяют выполнять одну и ту же операцию сразу на какой-то последовательности данных. Эта концепция называется SIMD-параллелизмом (англ. single instruction, multiple data).

              Streaming SIMD Extensions

              SSE — это обобщённое называние всех SIMD-инструкций для x86.

              -

              Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно разбивают эту булеву последовательность на блоки, например, по 32 бит, и логически работают уже с ними.

              +

              Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно разбивают эту булеву последовательность на блоки, например, по 32 бит, и логически работают уже с ними.

              Довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

              Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует).

              Очень часто SSE используют для работы с действительными числами, и вы этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходых данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

              @@ -185,8 +185,8 @@

              C++ intrinsics

              __m256d x = _mm256_loadu_pd(&a[i]); __m256d y = _mm256_loadu_pd(&b[i]); // - 256 означает размер регистров -    // - d означает "double" -    // - pd озанчает "packed double" + // - d означает "double" + // - pd озанчает "packed double" // просуммируем числа и положим результат в другой регистр: __m256d z = _mm256_add_pd(x, y); @@ -200,6 +200,8 @@

              C++ intrinsics

            35. _mm256_acos_pd — принимает один регистр, содержащий 4 double-ов, и возвращает их арк-косинусы.

            36. _mm256_broadcast_sd — бродкастит (копирует) double из памяти во все четыре слота в регистре.

            37. _mm256_ceil_pd — округляет double к ближайшему int-у вверх.

            38. +
            39. _mm256_cmpeq_epi32 — сравнивает запакованные int-ы и возвращает вектор-маску, в которой для полностью совпавших элементов будет по 32 единицы.

            40. +
            41. _mm256_blendv_ps — по заданной маске берёт значения либо из первого массива, либо из второго. Часто применяется для замены if

            42. Комбинаторно получается огромное количество различных функций. Полная документация по SSE-интринзикам — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              Выравнивание. У операций чтения и записи есть по две версии: load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первая корректно работает только тогда, когда весь считываемый блок помещается на одну кэш-линию (в противном случае она вызывает segfault в рантайме), в то время как unaligned версия работает всегда и везде.

              @@ -272,6 +274,98 @@

              Нетривиальный пр } return res; }

              -

              mixed precision

              +

              Почему мы для примера взяли unsigned, а не обычное умножение по какому-нибудь модулю, например \(10^9+7\)?

              +
              #pragma GCC optimize("O3")
              +#pragma GCC target("avx2")
              +
              +#include <x86intrin.h>
              +#include <bits/stdc++.h>
              +
              +using namespace std;
              +
              +typedef unsigned long long ull;
              +typedef __m256i reg;
              +
              +const int n = 1e8;
              +alignas(32) unsigned bases[n], results[n], powers[n];
              +
              +void binpow_simple() {
              +    for (int i = 0; i < n; i++) {
              +        unsigned a = bases[i], p = powers[i];
              +
              +        unsigned res = 1;
              +        while (p > 0) {
              +            if (p & 1)
              +                res = (res * a);
              +            a = (a * a);
              +            p >>= 1;
              +        }
              +
              +        results[i] = res;
              +    }
              +}
              +
              +template<typename T>
              +void print(T var) {
              +    unsigned *val = (unsigned*) &var;
              +    for (int i = 0; i < 3; i++)
              +        cout << bitset<32>(val[i]) << " ";
              +    cout << endl;
              +}
              +
              +void binpow_sse() {
              +    const reg ones = _mm256_set_epi32(1, 1, 1, 1, 1, 1, 1, 1);
              +    for (int i = 0; i < n; i += 8) {
              +        reg a = _mm256_load_si256((__m256i*) &bases[i]);
              +        reg p = _mm256_load_si256((__m256i*) &powers[i]);
              +        reg res = ones;
              +        
              +        // на самом деле, здесь никакого цикла не будет
              +        // -- компилятор это развернёт в 30 отдельных операций
              +        for (int i = 0; i < 32; i++) {
              +            // чтобы не писать лишний if, посчитаем для каждого элемента его множитель:
              +            // это будет либо единица, либо a, в зависимости от значения нижнего бита p
              +
              +            // маски элементов, которые нужно домножать на a:
              +            reg mask = _mm256_cmpeq_epi32(_mm256_and_si256(p, ones), ones);
              +            // эта операция смешивает два вектора по маске:
              +            reg mul = _mm256_blendv_epi8(ones, a, mask);
              +
              +            // res *= mul:
              +            res = _mm256_mullo_epi32(res, mul);
              +            // (mullo = mul low -- оставляет только нижние 32 бита)
              +
              +            // a *= a:
              +            a = _mm256_mullo_epi32(a, a);
              +
              +            // p >>= 1:
              +            p = _mm256_srli_epi32(p, 1);
              +        }
              +
              +        _mm256_store_si256((__m256i*) &results[i], res);
              +    }
              +}
              +
              +void timeit(void (*f)()) {
              +    clock_t start = clock();
              +    f();
              +    cout << double(clock() - start) / CLOCKS_PER_SEC << endl;
              +
              +    for (int i = 0; i < 10; i++)
              +        cout << results[i] << " ";
              +    cout << endl;
              +}
              +
              +int main() {
              +    for (int i = 0; i < n; i++) {
              +        bases[i] = rand();
              +        powers[i] = rand();
              +    }
              +
              +    timeit(binpow_simple);
              +    timeit(binpow_sse);
              +    
              +    return 0;
              +}
              From dec03bd9b73b3b2892684f95454b1dc655873e8a Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 21 Jul 2019 17:19:32 +0000 Subject: [PATCH 135/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 2 +- ru/sse.html | 273 ++++++++++++++++++++++++---------------------------- 2 files changed, 125 insertions(+), 150 deletions(-) diff --git a/pandoc.css b/pandoc.css index bb02bee..1e6a8c4 100644 --- a/pandoc.css +++ b/pandoc.css @@ -298,7 +298,7 @@ img { margin: auto; display: block; max-width: 85%; - max-height: 400px; + max-height: 320px; } figcaption { diff --git a/ru/sse.html b/ru/sse.html index 4791028..6d63623 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -196,16 +196,16 @@

              C++ intrinsics

              Конвенция именования интринзиков такая же, как самих инструкций, а она такая же, как в ассемблере — то есть максимально короткая и непонятная.

              Большинство команд кодируются как _mm<размерность>_<действие>_<тип>. Например:

                -
              • _mm_add_epi16 — складывает две пачки 16-битных extended packed integer, проще говоря \(\frac{128}{16} = 8\) short-ов (в инструкциях, где размер регистра не указан, он равен 128).

              • +
              • _mm_add_epi16 — складывает две группы 16-битных extended packed integer, проще говоря \(\frac{128}{16} = 8\) short-ов (в инструкциях, где размер регистра не указан, он равен 128).

              • _mm256_acos_pd — принимает один регистр, содержащий 4 double-ов, и возвращает их арк-косинусы.

              • _mm256_broadcast_sd — бродкастит (копирует) double из памяти во все четыре слота в регистре.

              • _mm256_ceil_pd — округляет double к ближайшему int-у вверх.

              • _mm256_cmpeq_epi32 — сравнивает запакованные int-ы и возвращает вектор-маску, в которой для полностью совпавших элементов будет по 32 единицы.

              • -
              • _mm256_blendv_ps — по заданной маске берёт значения либо из первого массива, либо из второго. Часто применяется для замены if

              • +
              • _mm256_blendv_ps — по заданной маске берёт значения либо из первого массива, либо из второго. Часто применяется для замены if-а.

              -

              Комбинаторно получается огромное количество различных функций. Полная документация по SSE-интринзикам — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              -

              Выравнивание. У операций чтения и записи есть по две версии: load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первая корректно работает только тогда, когда весь считываемый блок помещается на одну кэш-линию (в противном случае она вызывает segfault в рантайме), в то время как unaligned версия работает всегда и везде.

              -

              Это имело очень большое значение на старых компьютерах — если не получалось «выровнять» память, то производительность могла резко упасть (в два и более раз), потому что нарушался паттерн последовательного доступа. На современных компьютерах это не так значительно: будет медленне, но в пределах 5%. Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

              +

              Комбинаторно получается огромное количество различных SSE-интринзиков. Полная документация по ним — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              +

              Выравнивание. Отдельно стоит отметить одну делать: операции чтения и записи имеют по две версии: load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первая корректно работает только тогда, когда весь считываемый блок помещается на одну кэш-линию (если это не так, то в рантаеме вызвется segfault), в то время как unaligned версия работает всегда и везде.

              +

              Это отличие имело очень большое значение на старых компьютерах — если не получалось «выровнять» память, то производительность могла резко упасть (в два и более раз), так как нарушался паттерн последовательного доступа. На современных компьютерах это не так существенно: unaligned версия будет медленне, но в пределах 5%. Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

              -

              Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и это просто нужно для проверки типов и избежания связанных ошибок.

              +

              Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

              +

              Некоторые операции есть только для какого-то одного типа, например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов, однако будет работать с ними абсолютно так же. Поэтому to make compiler happy можно применять к ним преобразания типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

              Трудности автовекторизации

              -

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

              -

              Зачем тогда вообще программисту делать что-либо ещё? Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

              +

              В самом начале статьи мы приводили пример кода, в котором уже оптимизированный бинарник получается без каких-либо изменений, кроме подключения нужного таргета компиляции. Зачем тогда вообще программисту делать что-либо ещё?

              +

              Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

              Рассмотрим этот же пример, убрав из него всё лишнее:

              void sum(int a[], int b[], int c[], int n) {
                   for (int i = 0)
              @@ -225,147 +226,121 @@ 

              Трудности }

              Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

              Во-первых, потому что это не всегда корректно. Предположим, что a[] и c[] пересекаются, причём так, что указатели на начало массивов отличаются на 1-2 позиции. Ну, мало ли — может, мы такой изощрённой свёрткой хотели посчитать последовательность Фибоначчи. Тогда в simd-блоках данные будут пересекаться, и наблюдаемое поведение будет совсем не то, которое мы хотели.

              -

              Во-вторых, мы ничнго не знаем ничего про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

              -

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

              -

              Иногда мы можем считать внутри цикла что-то сложнее суммы — например, что-то, использующее if-ы или какие-нибудь нетривиальные внешние функции, для которых, тем не менее, есть SSE-аналог.

              -

              Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но часто проще написать всё самому.

              +

              Во-вторых, мы ничего не знаем про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

              +

              На самом деле, для достаточно больших циклов на высоких уровнях оптимизации компилятор вставит проверки на эти случаи и сгенерирует два разных варианта: через SSE и обычный. Но эти проверки выполняются в рантайме и стоят лишних инструкций.

              +

              Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

              Нетривиальный пример

              -

              Пусть мы хотим возвести \(10^7\) чисел в какие-то степени и перемножить.

              - -

              Возведём миллион (точнее, \(2^{20}\)) чисел в степень:

              - -

              Этот код работает за X секунд.

              -

              Для оптимизации, напишем sse-версию:

              - -

              Почему мы для примера взяли unsigned, а не обычное умножение по какому-нибудь модулю, например \(10^9+7\)?

              -
              #pragma GCC optimize("O3")
              -#pragma GCC target("avx2")
              -
              -#include <x86intrin.h>
              -#include <bits/stdc++.h>
              -
              -using namespace std;
              -
              -typedef unsigned long long ull;
              -typedef __m256i reg;
              -
              -const int n = 1e8;
              -alignas(32) unsigned bases[n], results[n], powers[n];
              -
              -void binpow_simple() {
              -    for (int i = 0; i < n; i++) {
              -        unsigned a = bases[i], p = powers[i];
              -
              -        unsigned res = 1;
              -        while (p > 0) {
              -            if (p & 1)
              -                res = (res * a);
              -            a = (a * a);
              -            p >>= 1;
              -        }
              -
              -        results[i] = res;
              -    }
              -}
              -
              -template<typename T>
              -void print(T var) {
              -    unsigned *val = (unsigned*) &var;
              -    for (int i = 0; i < 3; i++)
              -        cout << bitset<32>(val[i]) << " ";
              -    cout << endl;
              -}
              -
              -void binpow_sse() {
              -    const reg ones = _mm256_set_epi32(1, 1, 1, 1, 1, 1, 1, 1);
              -    for (int i = 0; i < n; i += 8) {
              -        reg a = _mm256_load_si256((__m256i*) &bases[i]);
              -        reg p = _mm256_load_si256((__m256i*) &powers[i]);
              -        reg res = ones;
              -        
              -        // на самом деле, здесь никакого цикла не будет
              -        // -- компилятор это развернёт в 30 отдельных операций
              -        for (int i = 0; i < 32; i++) {
              -            // чтобы не писать лишний if, посчитаем для каждого элемента его множитель:
              -            // это будет либо единица, либо a, в зависимости от значения нижнего бита p
              -
              -            // маски элементов, которые нужно домножать на a:
              -            reg mask = _mm256_cmpeq_epi32(_mm256_and_si256(p, ones), ones);
              -            // эта операция смешивает два вектора по маске:
              -            reg mul = _mm256_blendv_epi8(ones, a, mask);
              -
              -            // res *= mul:
              -            res = _mm256_mullo_epi32(res, mul);
              -            // (mullo = mul low -- оставляет только нижние 32 бита)
              -
              -            // a *= a:
              -            a = _mm256_mullo_epi32(a, a);
              -
              -            // p >>= 1:
              -            p = _mm256_srli_epi32(p, 1);
              -        }
              -
              -        _mm256_store_si256((__m256i*) &results[i], res);
              -    }
              -}
              -
              -void timeit(void (*f)()) {
              -    clock_t start = clock();
              -    f();
              -    cout << double(clock() - start) / CLOCKS_PER_SEC << endl;
              -
              -    for (int i = 0; i < 10; i++)
              -        cout << results[i] << " ";
              -    cout << endl;
              -}
              -
              -int main() {
              -    for (int i = 0; i < n; i++) {
              -        bases[i] = rand();
              -        powers[i] = rand();
              -    }
              -
              -    timeit(binpow_simple);
              -    timeit(binpow_sse);
              -    
              -    return 0;
              -}
              +

              Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

              +

              В SSE весьма сложно делить int-ы (см. примечания ниже), поэтому будем считать всё по модулю \(2^{32}\), то есть просто переполняя естественным образом unsigned int.

              + +

              Напишем стандартное итеративное бинарное возведение в степень:

              + +

              Этот код работает за 9.47 секунды.

              +

              Теперь попробуем векторизованную версию:

              + +

              Она уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

              +

              Разное

              +

              С++ в ассемблер. Посмотреть на генерируемые инструкции можно так:

              +
              g++ -S program.cpp -o program.s
              +

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

              +

              Распечатать вектор. Для дебага помогает такой код:

              + +

              В данном случае он выводит 4 группы по 32 бита из 128-битного вектора.

              +

              Деление. В SSE нет операции деления int-ов, но есть для float-ов и производных. Также нет взятия остатка от деления, что осложняет вычисления в комбинаторике.

              +

              Для деления 32-битных целых чисел их можно аккуратно скастовать к даблу, поделить так, и скастовать обратно — точности хватит, хоть это и будет медленно.

              +

              Умножение работает в несколько раз быстрее деления, и поэтому для ускорения деления на известную константу \(d\) есть следующий трюк: заменить выражение \(x / d\) на \(x \cdot \frac{1}{d}\), и при этом \(\frac{1}{d}\) посчитать во время компиляции. Это уже идеально работает с float-ами, а для int-ов нужно приблизить \(\frac{1}{d}\), подобрав «магическое» число \(m\) и степень двойки \(s\), такое что x / d == (x * m*) >> s для всех x, то есть мы заменяем деление на умножение и битовый сдвиг.

              +

              Можно показать, что такая пара чисел всегда существует, и компилятор сам оптимизирует деление на константу подобным образом. Вот, например, сгенерированные инструкции для деления unsigned long long на \(10^9 + 7\):

              + +

              Здесь для умножения используется «mixed precision» инструкция mulq, которая берёт два 64-битных числа и записывает 128-битный результат их умножения в два 64-битных регистра (lo, hi).

              +

              Для деления long-ов на SSE такой способ пока что не работает: аналогичная инструкция добавилась только в AVX512.

              From 0c50b90942245e337d05d2f1dd2b4f29e5b6e9a0 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 21 Jul 2019 17:43:13 +0000 Subject: [PATCH 136/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 306 +++++++++++++++++++++++++++------------------------- 1 file changed, 160 insertions(+), 146 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index 6d63623..55d7cad 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -136,63 +136,77 @@

              Векторизация

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

              -

              Чтобы понять, что здесь происходит, нам нужно сначала разъяснить некоторые особенности работы современных компьютеров.

              +

              Чтобы понять, что здесь происходит, нам нужно сначала разъяснить некоторые особенности работы современных компьютеров. (Знающие ассемблер могут отмотать примерно до ⅓.)

              Complex Instruction Set Computing

              Раньше, во времена, когда компьютеры назывались ЭВМ-ами и занимали целую комнату, увеличение производительности происходило в основном за счёт увеличения тактовой частоты. Тактовая частота условно равна количеству инструкций, выполняемому процессором за единицу времени. (На современных процессорах это не так — разные инструкции занимают разное время, которое ещё и может зависеть от разных обстоятельств.)

              -

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

              -

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

              +

              Помимо жесткого физического ограничения на максимально возможную тактовую частоту, такой такой подход в какой-то момент просто перестал быть экономически оправданным: прямое увеличение тактовой частоты приводит к в более чем линейному потреблению энергии, и, следовательно, выделению тепла, которое к тому же нужно ещё как-то выводить.

              +

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

              • RISC (англ. reduced instruction set computer), в которых длина кода (идентификатора) самой инструкции ограничена, а значит ограничено и само количество инструкций. Самые первые компьютеры относились к этому типу и могли не иметь даже отдельных инструкций для умножения и деления. Такие процессоры требует меньше транзисторов, и как следствие сами меньше, дешевле и потребляют меньше энергии. Самое популярное семейство архитектур называется arm и используется почти на всех современных мобильных устройствах.

              • -
              • CISC (англ. complex instruction set computer), к которому относят всё, что не RISC — в них длина команды не фиксирована, что позволяет поддерживать практически произовльное количество инструкций. Самоя популярное семейство архитектур называется x86 и используется почти на всех современных декстопах и серверах.

              • +
              • CISC (англ. complex instruction set computer), к которому относят всё, что не RISC — в них длина команды не фиксирована, что позволяет поддерживать практически произовльное количество инструкций. Самое популярное семейство архитектур называется x86 и используется почти на всех современных декстопах и серверах.

              Новые инструкции стали добавлять постепенно, причём разные в зависимости от области применения.

                -
              • В общеприменимых CPU довольно быстро добавили инструкцию, которая принимает числа \((x, y)\) и загружает в регистр данные по адресу \(a \cdot x + y\). Это полезно при индексации массивов — не нужно отдельно индекс считать.

              • +
              • В обычных CPU довольно быстро добавили инструкцию, которая принимает числа \((x, y)\) и загружает в регистр данные по адресу \(a \cdot x + y\). Это полезно при индексации массивов — не нужно отдельно индекс считать.

              • На графических сопроцессорах появилась отдельная инструкция, которую называют «saxpy» (сокращенно от выражения s += a * x + y), которая полезна, например, при перемножении матриц.

              • В последние GPU от Nvidia добавили «tensor core» — отдельную схему, которая перемножает две матрицы \(4 \times 4\) и прибавляет к третьей, как бы производя \(4 \times 4 \times 4 = 64\) умножений и \(4 \times 4 = 16\) сложений за раз, что сильно ускоряет алгоритмы блочного матричного умножения.

              В этой статье мы сфокусируемся на отдельным виде инструкций, которые позволяют выполнять одну и ту же операцию сразу на какой-то последовательности данных. Эта концепция называется SIMD-параллелизмом (англ. single instruction, multiple data).

              Streaming SIMD Extensions

              SSE — это обобщённое называние всех SIMD-инструкций для x86.

              -

              Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно разбивают эту булеву последовательность на блоки, например, по 32 бит, и логически работают уже с ними.

              -

              Довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

              +

              Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно логически разбивают эту булеву последовательность на блоки, например, по 32 бит, и работают уже с ними.

              +

              Подобным способом довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

              Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует).

              -

              Очень часто SSE используют для работы с действительными числами, и вы этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходых данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

              +

              Очень часто SSE используют для работы с действительными числами, и в этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходых данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

              Конкретный набор инструкций и размеры регистров зависит от вендора и поколения архитектуры. На данный момент (лето 2019 года) большинство процессоров архитектуры x86 производит Intel, поэтому мы сконцентрируемся именно на их наборе инструкций.

              -

              Поддержка SIMD-инструкций добавлялись постепенно, сохраняя обратную совместимость. Если третий пентиум в 1999-м году умел работать с регистрами размера 128, то в самых современных i7 есть 512-битные регистры. Автор не является специалистом в проектировании микропроцессоров, но предполагает, то регистры больше 64 байт (512 бит) появятся очень не скоро, потому что это уже больше размера кэш-линии

              +

              Поддержка SIMD-инструкций добавлялись постепенно, сохраняя обратную совместимость. Если третий пентиум в 1999-м году умел работать с регистрами размера 128, то в самых современных i7 есть 512-битные регистры. Автор не является специалистом в проектировании микропроцессоров, но предполагает, то регистры больше 64 байт (512 бит) появятся не скоро, потому что это уже больше размера кэш-линии

              Чтобы разработчикам не нужно было предоставлять отдельные оптимизированные бинарники под каждую конкретную архитектуру, информация о поддержке наборов инструкций процессором зашита в ассемблерную инструкцию cpuid, которую можно просто вызвать в рантайме и всё узнать: например так.

              -

              В GCC есть встроенная функция __builtin_cpu_supports, которая берёт строчку-название набора инструкций (“sse”, “avx2”, “avx512f” и т. п.) и возвращает целое число — ноль или какую-то степень двойки. Эта функция работает так: входная строка во время компиляции переводится в нужную степень двойки, которая в рантайме просто AND-ится с маской из cpuid и возвращается — всё ради эффективности.

              -

              Экономя время читателю: сервера CodeForces и большинство онлайн джаджей на момент написания статьи поддерживают AVX2, то есть умеют работать с 256-битными регистрами.

              +

              В компиляторе GCC есть встроенная функция __builtin_cpu_supports, которая берёт строчку-название набора инструкций (“sse”, “avx2”, “avx512f” и т. п.) и возвращает целое число — ноль или какую-то степень двойки. Эта функция работает так: входная строка во время компиляции переводится в нужную степень двойки, которая в рантайме просто AND-ится с маской из cpuid и возвращается — всё ради эффективности.

              + +

              Экономя время читателю: сервера CodeForces и большинство онлайн джаджей на момент написания статьи поддерживают AVX2, то есть умеют полноценно работать с 256-битными регистрами.

              C++ intrinsics

              SSE это те же чистые ассемблерные инструкции. Языки с каким-либо более верхним уровнем абстракции напрямую работать с ними уже не могут. Однако не обязательно писать на чистом ассемблере, чтобы их использовать — разработчики компиляторов уже позаботились об этом за вас и сделали встроенные функции-обёртки, которые называют интринзиками (англ. intrinsic — «внутренний»).

              -

              Чтобы их подключить, нужно указать include на соответствующий заголовочный файл, а также сказать компилятору о том, что мы хотим использовать конкретный набор или наборы инструкций. В примере из начала статьи мы сделали именно это, указав target("avx2") — компилятор получил доступ к более широким регистрам и продвинутым инструкциям для них, и смог соптимизировать программу примерно в два раза (по умолчанию включены 128-битные sse и sse2, поэтому в 2, а не в \(\frac{256}{32} = 8\)).

              +

              Чтобы их подключить, нужно указать include на соответствующий заголовочный файл, а также сказать компилятору о том, что мы хотим использовать конкретный набор или наборы инструкций. В примере из начала статьи мы сделали именно это, прописав target("avx2") — компилятор получил доступ к более широким регистрам и продвинутым инструкциям для них, и смог соптимизировать программу примерно в два раза (по умолчанию включены 128-битные sse и sse2, поэтому в 2, а не в \(\frac{256}{32} = 8\)).

              По аналогии с <bits/stdc++.h>, в GCC такой же заголовочный файл <x86intrin.h>, включающий в себя сразу все SSE-интринзики. Шаблон любителя засоренных неймспейсов и избыточно долгой компиляции может начинаться так:

              - + +

              Перейдём теперь к самому синтаксису.

              Простой цикл, в котором складывают два массива 64-битных действительных чисел, на SSE-интринзиках будет выглядеть так:

              - +

              Конвенция именования интринзиков такая же, как самих инструкций, а она такая же, как в ассемблере — то есть максимально короткая и непонятная.

              Большинство команд кодируются как _mm<размерность>_<действие>_<тип>. Например:

                @@ -203,143 +217,143 @@

                C++ intrinsics

              • _mm256_cmpeq_epi32 — сравнивает запакованные int-ы и возвращает вектор-маску, в которой для полностью совпавших элементов будет по 32 единицы.

              • _mm256_blendv_ps — по заданной маске берёт значения либо из первого массива, либо из второго. Часто применяется для замены if-а.

              -

              Комбинаторно получается огромное количество различных SSE-интринзиков. Полная документация по ним — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              -

              Выравнивание. Отдельно стоит отметить одну делать: операции чтения и записи имеют по две версии: load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первая корректно работает только тогда, когда весь считываемый блок помещается на одну кэш-линию (если это не так, то в рантаеме вызвется segfault), в то время как unaligned версия работает всегда и везде.

              +

              Комбинаторно получается огромное количество различных функций. Полная документация по ним — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

              +

              Выравнивание. Отдельно стоит отметить одну делать: операции чтения и записи имеют по две версии — load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первые корректно работают только тогда, когда весь считываемый блок помещается на одну кэш-линию (если это не так, то в рантаеме вызвется segfault), в то время как unaligned версия работает всегда и везде.

              Это отличие имело очень большое значение на старых компьютерах — если не получалось «выровнять» память, то производительность могла резко упасть (в два и более раз), так как нарушался паттерн последовательного доступа. На современных компьютерах это не так существенно: unaligned версия будет медленне, но в пределах 5%. Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

              - +

              Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

              Некоторые операции есть только для какого-то одного типа, например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов, однако будет работать с ними абсолютно так же. Поэтому to make compiler happy можно применять к ним преобразания типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

              Трудности автовекторизации

              В самом начале статьи мы приводили пример кода, в котором уже оптимизированный бинарник получается без каких-либо изменений, кроме подключения нужного таргета компиляции. Зачем тогда вообще программисту делать что-либо ещё?

              Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

              Рассмотрим этот же пример, убрав из него всё лишнее:

              - +

              Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

              Во-первых, потому что это не всегда корректно. Предположим, что a[] и c[] пересекаются, причём так, что указатели на начало массивов отличаются на 1-2 позиции. Ну, мало ли — может, мы такой изощрённой свёрткой хотели посчитать последовательность Фибоначчи. Тогда в simd-блоках данные будут пересекаться, и наблюдаемое поведение будет совсем не то, которое мы хотели.

              Во-вторых, мы ничего не знаем про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

              -

              На самом деле, для достаточно больших циклов на высоких уровнях оптимизации компилятор вставит проверки на эти случаи и сгенерирует два разных варианта: через SSE и обычный. Но эти проверки выполняются в рантайме и стоят лишних инструкций.

              -

              Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

              +

              На самом деле, когда компилятор подозревает, что функция будет использована для достаточно больших циклов, то на высоких уровнях оптимизации он сам вставит runtime-проверки на эти случаи и сгенерирует два разных варианта: через SSE и «безопасный».

              +

              Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или вызываются какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

              Нетривиальный пример

              Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

              В SSE весьма сложно делить int-ы (см. примечания ниже), поэтому будем считать всё по модулю \(2^{32}\), то есть просто переполняя естественным образом unsigned int.

              - +

              Напишем стандартное итеративное бинарное возведение в степень:

              - +

              Этот код работает за 9.47 секунды.

              Теперь попробуем векторизованную версию:

              - -

              Она уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

              + +

              Эта реализация уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

              Разное

              С++ в ассемблер. Посмотреть на генерируемые инструкции можно так:

              -
              g++ -S program.cpp -o program.s
              +

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

              Распечатать вектор. Для дебага помогает такой код:

              - +

              В данном случае он выводит 4 группы по 32 бита из 128-битного вектора.

              Деление. В SSE нет операции деления int-ов, но есть для float-ов и производных. Также нет взятия остатка от деления, что осложняет вычисления в комбинаторике.

              Для деления 32-битных целых чисел их можно аккуратно скастовать к даблу, поделить так, и скастовать обратно — точности хватит, хоть это и будет медленно.

              Умножение работает в несколько раз быстрее деления, и поэтому для ускорения деления на известную константу \(d\) есть следующий трюк: заменить выражение \(x / d\) на \(x \cdot \frac{1}{d}\), и при этом \(\frac{1}{d}\) посчитать во время компиляции. Это уже идеально работает с float-ами, а для int-ов нужно приблизить \(\frac{1}{d}\), подобрав «магическое» число \(m\) и степень двойки \(s\), такое что x / d == (x * m*) >> s для всех x, то есть мы заменяем деление на умножение и битовый сдвиг.

              Можно показать, что такая пара чисел всегда существует, и компилятор сам оптимизирует деление на константу подобным образом. Вот, например, сгенерированные инструкции для деления unsigned long long на \(10^9 + 7\):

              - +

              Здесь для умножения используется «mixed precision» инструкция mulq, которая берёт два 64-битных числа и записывает 128-битный результат их умножения в два 64-битных регистра (lo, hi).

              Для деления long-ов на SSE такой способ пока что не работает: аналогичная инструкция добавилась только в AVX512.

              From 8340d25784d35d962745ef2ac98e76f013eeeb61 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 21 Jul 2019 18:20:52 +0000 Subject: [PATCH 137/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/dp-optimizations.html | 39 +++++++++++++++++++++++++++++++-- ru/geometry.html | 4 ++-- ru/sqrt.html | 47 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index 7a84ccb..cb4f5a1 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -303,8 +303,43 @@

              Лямбда-оптимизация

              \[ f[i, j] - f[i, j-1] \leq f[i, j+1] - f[i, j] \]

              -

              Иными словами, «выгода» добавления следующего отрезка с каждым разом не увеличивается.

              -

              TODO

              +

              Иными словами, «выгода» добавления следующего отрезка с каждым разом не увеличивается. Тогда если мы найдем минимальную \(\lambda\) такую, что \(k \ge j\), то \(f[i, k] = f[i, j]\).

              +

              Суммируем

              TODO: сделать табличку

                diff --git a/ru/geometry.html b/ru/geometry.html index 3fd732a..01d5e87 100644 --- a/ru/geometry.html +++ b/ru/geometry.html @@ -140,7 +140,7 @@

                Вычислитель

                Его свойства:

                  -
                • Скалярное произведение антисимметрично: \(a \times b = - (b \times a)\).
                • +
                • Векторное произведение антисимметрично: \(a \times b = - (b \times a)\).
                • Коллинеарные вектора должны иметь нулевое векторное произведение.
                • Если \(b\) «слева» от \(a\), то векторное произведение положительное.
                • Если \(b\) «справа» от \(a\), то векторное произведение отрицательное.
                • @@ -210,7 +210,7 @@

                  Операции над ве

                  Операторы

                  В C++ можно перегружать почти все стандартные операторы, например, +, -, << и т. д.

                  -

                  Переобрадилим для будущих нужд + и -:

                  +

                  Переопределим для будущих нужд + и -:

                  Скалярное произведение:

                  diff --git a/ru/sqrt.html b/ru/sqrt.html index 826aff0..8e868d4 100644 --- a/ru/sqrt.html +++ b/ru/sqrt.html @@ -287,8 +287,51 @@

                  Де
                • В цикле две тяжелые вершины — обозначим их как \(a\) и \(b\), а лёгкую как \(c\). Зафиксируем пару \((a, c)\) — способов это сделать \(O(m)\), потому что всего столько рёбер. Для этого ребра будет не более \(O(\sqrt n)\) рёбер \((a, b)\), потому что столько всего тяжелых вершин. Получается, что всего таких циклов может быть не более \(O(m \sqrt n)\).

                • Все вершины тяжелые. Аналогично — тип третьей вершины в разборе предыдущего случая нигде не использовался; важно лишь то, что тяжелых вершин \(b\) немного.

                @@ -201,8 +235,12 @@

                Script-based стр print("Wrong:") print v2 break -

                Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py. При желании можно компилировать прямо внутри скрипта.

                -

                Скрипт написан под Linux. Для Windows нужно убрать «./» во всех системных вызовах.

                +
                  +
                • Автор обычно запускает его командой python3 checker.py stupid smart gen.py 100, предварительно скомпилировав stupid и smart в ту же директорию, что и сам checker.py.
                • +
                • При желании можно компилировать прямо внутри скрипта.
                • +
                • Если задача подразумевает неоднозначный вывод (к примеру, вывести индекс минимума — таких может быть несколько), то вместо v1 != v2 следует использовать сторонний скрипт compare.py.
                • +
                • Скрипт написан под Linux. Для Windows нужно убрать «./» во всех системных вызовах.
                • +

                Примечание. Ну такой вот примерно рецепт усредненный, потому что вариаций масса. Берется неправильное решение, оно не работает, рабочий код — это не про код моего бати. Он берет это решение, вываливает его в скрипт и начинает запускать. Добавляет огромное количество тестов, крайних случаев, рандома и МАКСТЕСТОВ! для проверки. Все это прогоняется вместе с медленным решением. Потом скрипт находит баг и системный блок остужается на балконе. Потом батя заносит тест и щедро заполнив код отладочным выводом начинает дебажить. При этом параллельно ест и засыпает крошками клавиатуру. Ест и приговаривает полушепотом ух ###. При этом у него на лбу аж пот выступает. Любезно мне иногда предлагает подебажить, но я отказываюсь. Надо ли говорить о том какой код получается потом? Вонища такая, что тестирующая система падает.

                From 7cb9d76badee1b07c91a0fd2134cb828b530f769 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 30 Jun 2019 16:26:26 +0000 Subject: [PATCH 110/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/fft.html | 29 +++-- ru/palindromes.html | 170 +++++++++++++++++------------ ru/suffix-automaton.html | 225 +++++++++++++++++++++++++++++++++------ 3 files changed, 319 insertions(+), 105 deletions(-) diff --git a/ru/fft.html b/ru/fft.html index 7a7c1a2..69863df 100644 --- a/ru/fft.html +++ b/ru/fft.html @@ -298,8 +298,12 @@

                Схема Кули-Тьюки

                \]

                Зная это, исходную формулу для значения многочлена в точке \(w^t\) можно записать так:

                \[ -P(w^t)=A\left(w^{2(t\bmod k)}\right)+w^tB\left(w^{2(t\bmod k)}\right) +P(w^t) += A(w^{2t}) + w^t B(w^{2t}) += A\left(w^{2(t\bmod k)}\right)+w^tB\left(w^{2(t\bmod k)}\right) \]

                +

                Ключевое замечание: корней вида \(w^{2t}\) в два раза меньше, потому что \(w^n = w^0\), и можно сказать, что.

                +

                У нас по сути в два раза меньше корней (но они так же равномерно распределены на единичной окружности) и в два раза меньше коэффициентов — мы только что успешно уменьшили нашу задачу в два раз.

                Сам алгоритм заключается в следующем: рекурсивно посчитаем БПФ для многочленов \(A\) и \(B\) и объединим ответы с помощью формулы выше. При этом в рекурсии нам нужно считать значения на корнях степени не \(n\), а \(k = \frac{n}{2}\), то есть на всех «чётных» корнях степени \(n\) (вида \(w^{2t}\)).

                Заметим, что если \(w\) это образующий корень степени \(n = 2k\) из единицы, то \(w^2\) будет образующим корнем степени \(k\), то есть в рекурсию мы можем просто передать другое значение образующего корня.

                Таким образом, мы свели преобразование размера \(n\) к двум преобразованиям размера \(\dfrac n 2\). Следовательно, общее время вычислений составит

                @@ -308,7 +312,7 @@

                Схема Кули-Тьюки

                \]

                Заметим, что предположение о делимости \(n\) на \(2\) имело существенную роль. Значит, \(n\) должно быть чётным на каждом уровне, кроме последнего, из чего следует, что \(n\) должно быть степенью двойки.

                Реализация

                -

                Приведём код, считающий БПФ по схеме Кули-Тьюки:

                +

                Приведём код, вычисляющий БПФ по схеме Кули-Тьюки:

                typedef complex<double> ftype;
                 const double pi = acos(-1);
                 
                @@ -350,11 +354,11 @@ 

                Реализация

                }

                Теперь мы умеем перемножать два многочлена за \(O(n \log n)\):

                Примечание. Приведённый выше код, являясь корректным и имея асимптотику \(O(n\log n)\), едва ли пригоден для использования на реальных контестах. Он имеет большую константу и далеко не так численно устойчивый, чем оптимальные варианты написания быстрого преобразования Фурье. Мы его приводим, потому что он относительно простой.

                Читателю рекомендуется самостоятельно задуматься о том, как можно улучшить время работы и точность вычислений. Из наиболее важных недостатков:

                @@ -368,6 +372,15 @@

                Реализация

                Здесь приведена одна из условно пригодных реализаций.

                Но главная проблема в численной стабильности — мы нарушили первое правило действительных чисел. Однако, от неё можно избавиться.

                Number-theoretic transform

                +

                Для любого модуля такие штуки всегда находятся.

                +

                \[ +m = 998244353 = 7 \cdot 17 \cdot 2^{23} + 1 +\]

                +

                Заметим, что \(m\) простое. У простых чисел существует первообразный корень: такое число, что \(a^{p-1} = 1\), но \(a^l \neq 1\) для всех \(l < p - 1\), то есть степени \(g\) проходятся по всем возможным остаткам, кроме нуля.

                +

                Проблемсеттеры иногда не хотят выдавать, про FFT задача или нет.

                +

                Нам нужно просто найти такую пару \(p\) и \(g\) (играющую роль \(w_n^1\)), такую что \(g^n \equiv 1 \pmod p\) и при этом все остальные степени не различны, что означает, что \(n\) это минимальняя такая штука.

                +

                Очень удобное выбрать какое-то число, такое что . Тогда можно в качестве \(g\) выбрать любое число,потому что

                +

                Очень удобно выбрать какое-нибудь простое число плюс один, потому что

                Нам от комплексных чисел на самом деле нужно было только одно свойство: что у единицы есть \(n\) комплексных корней. На самом деле, это не единственные алгебраические объекты, обладающие таким свойством.

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

                Значит, для любого простого p в поле остатков от деления на него есть корень \(g\) степени \(p-1\) из единицы. Если при этом \((p-1)=c\cdot 2^k\), то \(g^{c}\) будет корнем степени \(2^k\), что позволяет применять метод Кули-Тьюки. Отсюда следует, что \(p=c\cdot2^k+1\). Практика показывает, что чисел такого вида очень много.

                @@ -376,6 +389,8 @@

                Применения

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

                Каждый раз, когда вы открываете джипег или проигрываете .mp3, где-то в компьютере считается БПФ.

                Почти во всех алгоритмах обработки звука и эффективных алгоритмах обработки изображений оно используется.

                +

                Комбинаторика

                +

                632E — Вор в магазине.

                Свёртки и корреляции

                Пусть есть \(\{a_i\}_{i=0}^{n}\) и \(\{b_j\}_{j=0}^{m}\). Тогда свёрткой называют \(\{c_k\}_{k=0}^{m+n}\):

                \[c_k=\sum\limits_{i=0}^{n} a_i b_{k-i}\]

                diff --git a/ru/palindromes.html b/ru/palindromes.html index 3b4350e..326b51e 100644 --- a/ru/palindromes.html +++ b/ru/palindromes.html @@ -116,93 +116,127 @@

                Палиндромы

              • «was it a car or a cat I saw?»

              • «а роза упала на лапу Азора»

            -

            Само решение максимально простое:

            - +

            Само решение максимально простое: отсортируем вершины графа по их степени, ориентируем ребра \(v \rightarrow u, v \le u\). Теперь внутренним циклом будем считать пути \(v \rightarrow u \rightarrow w, v \le u \le w\), а потом проверять существование ребра \(v \rightarrow w\).

            +

            Бакеты

            Пусть нам нужно находить всё ту же сумму, но теперь у нас запрос обновления выглядит так: \(i\)-тый элемент прыгает на \(d\) позиций вправо или влево, смещая, соответственно, \(d\) элементов между начальной и конечной позицией на единицу вправо или влево. Сумму нужно находить уже в новой индексации. Про декартово дерево автор снова просит читателя на время забыть.

            Объединим предыдущие подходы: теперь не только будем делить массив на блоки примерно по корню, но и каждый корень запросов будем всё перестраивать.

            From 3a4e1ccc65bc152a6e8e6ed738f035d31fb45464 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 21 Jul 2019 18:26:50 +0000 Subject: [PATCH 138/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ru/index.html b/ru/index.html index efddef0..04f2ae2 100644 --- a/ru/index.html +++ b/ru/index.html @@ -52,7 +52,7 @@

            Структуры данных

          1. Дерево отрезков ленивые операции, динамическое ДО, персистентноcть
          2. Декартово дерево неявный ключ, персистентность
          3. Дерево Фенвика почему работает, многомерный случай, бинарный поиск
          4. -
          5. Разреженная таблица static rmq, многомерный случай, disjoint st
          6. +
          7. Разреженная таблица static rmq, многомерный случай, disjoint sparse table
          8. @@ -124,6 +124,7 @@

            Теория игр

            Разное

            From 47d09f86d7d44d98ba85e15f5b21bf1261be4d1e Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 21 Jul 2019 20:30:26 +0000 Subject: [PATCH 139/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 196 ++++++++++++++++++++++++++-------------------------- 1 file changed, 99 insertions(+), 97 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index 55d7cad..0043c27 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -169,13 +169,13 @@

            Streaming SIMD Extensions

            using namespace std; int main() { - + cout << __builtin_cpu_supports("sse") << endl; cout << __builtin_cpu_supports("sse2") << endl; cout << __builtin_cpu_supports("avx") << endl; cout << __builtin_cpu_supports("avx2") << endl; cout << __builtin_cpu_supports("avx512f") << endl; - + return 0; }

            Экономя время читателю: сервера CodeForces и большинство онлайн джаджей на момент написания статьи поддерживают AVX2, то есть умеют полноценно работать с 256-битными регистрами.

            @@ -230,108 +230,109 @@

            C++ intrinsics

            }

            Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

            Некоторые операции есть только для какого-то одного типа, например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов, однако будет работать с ними абсолютно так же. Поэтому to make compiler happy можно применять к ним преобразания типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

            +

            Нетривиальный пример

            +

            Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

            +

            Код для тестирования:

            + +

            В SSE весьма сложно делить int-ы (см. примечания ниже), поэтому будем считать всё по модулю \(2^{32}\), то есть просто переполняя естественным образом unsigned int.

            +

            Напишем стандартное итеративное бинарное возведение в степень:

            + +

            Этот код работает за 9.47 секунды.

            +

            Теперь попробуем векторизованную версию:

            + +

            Эта реализация уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

            Трудности автовекторизации

            В самом начале статьи мы приводили пример кода, в котором уже оптимизированный бинарник получается без каких-либо изменений, кроме подключения нужного таргета компиляции. Зачем тогда вообще программисту делать что-либо ещё?

            Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

            Рассмотрим этот же пример, убрав из него всё лишнее:

            - +

            Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

            Во-первых, потому что это не всегда корректно. Предположим, что a[] и c[] пересекаются, причём так, что указатели на начало массивов отличаются на 1-2 позиции. Ну, мало ли — может, мы такой изощрённой свёрткой хотели посчитать последовательность Фибоначчи. Тогда в simd-блоках данные будут пересекаться, и наблюдаемое поведение будет совсем не то, которое мы хотели.

            Во-вторых, мы ничего не знаем про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

            На самом деле, когда компилятор подозревает, что функция будет использована для достаточно больших циклов, то на высоких уровнях оптимизации он сам вставит runtime-проверки на эти случаи и сгенерирует два разных варианта: через SSE и «безопасный».

            Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или вызываются какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

            -

            Нетривиальный пример

            -

            Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

            -

            В SSE весьма сложно делить int-ы (см. примечания ниже), поэтому будем считать всё по модулю \(2^{32}\), то есть просто переполняя естественным образом unsigned int.

            - -

            Напишем стандартное итеративное бинарное возведение в степень:

            - -

            Этот код работает за 9.47 секунды.

            -

            Теперь попробуем векторизованную версию:

            - -

            Эта реализация уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

            Разное

            С++ в ассемблер. Посмотреть на генерируемые инструкции можно так:

            @@ -347,10 +348,11 @@

            Разное

            В данном случае он выводит 4 группы по 32 бита из 128-битного вектора.

            Деление. В SSE нет операции деления int-ов, но есть для float-ов и производных. Также нет взятия остатка от деления, что осложняет вычисления в комбинаторике.

            Для деления 32-битных целых чисел их можно аккуратно скастовать к даблу, поделить так, и скастовать обратно — точности хватит, хоть это и будет медленно.

            -

            Умножение работает в несколько раз быстрее деления, и поэтому для ускорения деления на известную константу \(d\) есть следующий трюк: заменить выражение \(x / d\) на \(x \cdot \frac{1}{d}\), и при этом \(\frac{1}{d}\) посчитать во время компиляции. Это уже идеально работает с float-ами, а для int-ов нужно приблизить \(\frac{1}{d}\), подобрав «магическое» число \(m\) и степень двойки \(s\), такое что x / d == (x * m*) >> s для всех x, то есть мы заменяем деление на умножение и битовый сдвиг.

            +

            Умножение работает в несколько раз быстрее деления, и поэтому для ускорения деления float-ов на известную константу \(d\) есть следующий трюк: заменить выражение \(x / d\) на \(x \cdot \frac{1}{d}\), и при этом \(\frac{1}{d}\) посчитать во время компиляции.

            +

            Для целочисленных типов такое сделать немного сложнее — нужно заменить деление на умножение и битовый сдвиг. Для этого нужно приблизить \(\frac{1}{d} \approx \frac{m}{2^s}\), подобрав «магическое» число \(m\) и степень двойки \(s\), такие что что x / d == (x * m) >> s для всех x.

            Можно показать, что такая пара чисел всегда существует, и компилятор сам оптимизирует деление на константу подобным образом. Вот, например, сгенерированные инструкции для деления unsigned long long на \(10^9 + 7\):

            From c1fb393636cb0975686e804da58d3e64dbeac57c Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 23 Jul 2019 11:03:14 +0000 Subject: [PATCH 140/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 292 ++++++++++++++++++++++++++++------------------------ 1 file changed, 156 insertions(+), 136 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index 0043c27..4b5cdb9 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -109,34 +109,30 @@

            Векторизация

            Векторизация

            -

            Рассмотрим следующую программу, в которой складывают два массива:

            +

            Рассмотрим следующую программу, в которой считается сумма одномерного целочисленного массива:

            -

            Если скомпилировать этот код под GCC без всяких дополнительных настроек и запустить, он отработает за 1.65 секунды.

            + +#include <iostream> +using namespace std; + +const int n = 1e5; +int a[n], s = 0; + +int main() { + for (int t = 0; t < 100000; t++) + for (int i = 0; i < n; i++) + s += a[i]; + + return 0; +} +

            Если скомпилировать этот код под GCC без всяких дополнительных настроек и запустить, он отработает за 2.43 секунды.

            Добавим теперь следующую магическую директиву в самое начало программы:

            -

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

            -

            Чтобы понять, что здесь происходит, нам нужно сначала разъяснить некоторые особенности работы современных компьютеров. (Знающие ассемблер могут отмотать примерно до ⅓.)

            +

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

            +

            Чтобы понять, что здесь происходит, нам нужно сначала разъяснить некоторые особенности работы современных компьютеров. (Знающие ассемблер могут отмотать примерно до ⅓ статьи.)

            Complex Instruction Set Computing

            Раньше, во времена, когда компьютеры назывались ЭВМ-ами и занимали целую комнату, увеличение производительности происходило в основном за счёт увеличения тактовой частоты. Тактовая частота условно равна количеству инструкций, выполняемому процессором за единицу времени. (На современных процессорах это не так — разные инструкции занимают разное время, которое ещё и может зависеть от разных обстоятельств.)

            Помимо жесткого физического ограничения на максимально возможную тактовую частоту, такой такой подход в какой-то момент просто перестал быть экономически оправданным: прямое увеличение тактовой частоты приводит к в более чем линейному потреблению энергии, и, следовательно, выделению тепла, которое к тому же нужно ещё как-то выводить.

            @@ -182,7 +178,7 @@

            Streaming SIMD Extensions

            C++ intrinsics

            SSE это те же чистые ассемблерные инструкции. Языки с каким-либо более верхним уровнем абстракции напрямую работать с ними уже не могут. Однако не обязательно писать на чистом ассемблере, чтобы их использовать — разработчики компиляторов уже позаботились об этом за вас и сделали встроенные функции-обёртки, которые называют интринзиками (англ. intrinsic — «внутренний»).

            Чтобы их подключить, нужно указать include на соответствующий заголовочный файл, а также сказать компилятору о том, что мы хотим использовать конкретный набор или наборы инструкций. В примере из начала статьи мы сделали именно это, прописав target("avx2") — компилятор получил доступ к более широким регистрам и продвинутым инструкциям для них, и смог соптимизировать программу примерно в два раза (по умолчанию включены 128-битные sse и sse2, поэтому в 2, а не в \(\frac{256}{32} = 8\)).

            -

            По аналогии с <bits/stdc++.h>, в GCC такой же заголовочный файл <x86intrin.h>, включающий в себя сразу все SSE-интринзики. Шаблон любителя засоренных неймспейсов и избыточно долгой компиляции может начинаться так:

            +

            По аналогии с <bits/stdc++.h>, в GCC есть такой же заголовочный файл <x86intrin.h>, включающий в себя сразу все SSE-интринзики. Шаблон любителя засоренных неймспейсов и избыточно долгой компиляции может начинаться так:

            -

            Перейдём теперь к самому синтаксису.

            Простой цикл, в котором складывают два массива 64-битных действительных чисел, на SSE-интринзиках будет выглядеть так:

            -

            Конвенция именования интринзиков такая же, как самих инструкций, а она такая же, как в ассемблере — то есть максимально короткая и непонятная.

            +

            Когда размер массива не кратен размеру регистра, нужно поступить одним из двух способов:

            +
              +
            1. Добавить в конец массива «нейтральные» элементы, дополнив его до удобной длины.

            2. +
            3. Обработать через SSE столько элементов, сколько получается, а оставшиеся обработать отдельно.

            4. +
            +

            Например, вот так можно считать сумму на массиве произвольного размера:

            +

            Большинство команд кодируются как _mm<размерность>_<действие>_<тип>. Например:

            • _mm_add_epi16 — складывает две группы 16-битных extended packed integer, проще говоря \(\frac{128}{16} = 8\) short-ов (в инструкциях, где размер регистра не указан, он равен 128).

            • @@ -219,115 +238,113 @@

              C++ intrinsics

            Комбинаторно получается огромное количество различных функций. Полная документация по ним — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

            Выравнивание. Отдельно стоит отметить одну делать: операции чтения и записи имеют по две версии — load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первые корректно работают только тогда, когда весь считываемый блок помещается на одну кэш-линию (если это не так, то в рантаеме вызвется segfault), в то время как unaligned версия работает всегда и везде.

            -

            Это отличие имело очень большое значение на старых компьютерах — если не получалось «выровнять» память, то производительность могла резко упасть (в два и более раз), так как нарушался паттерн последовательного доступа. На современных компьютерах это не так существенно: unaligned версия будет медленне, но в пределах 5%. Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

            - +

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

            +

            Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

            +

            Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

            -

            Некоторые операции есть только для какого-то одного типа, например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов, однако будет работать с ними абсолютно так же. Поэтому to make compiler happy можно применять к ним преобразания типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

            +

            Для каждой размерности регистра есть 3 типа данных. На примере AVX: __m256 для float, __m256d для double и __m256i для разных int-ов.

            +

            Некоторые операции есть только для какого-то одного типа (например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов), однако будут работать с другими типами абсолютно так же. Поэтому, to make compiler happy, нужно применять к ним преобразания типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

            Нетривиальный пример

            Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

            -

            Код для тестирования:

            - +

            Тестировать и сравнивать решения будем таким кодом:

            +

            В SSE весьма сложно делить int-ы (см. примечания ниже), поэтому будем считать всё по модулю \(2^{32}\), то есть просто переполняя естественным образом unsigned int.

            Напишем стандартное итеративное бинарное возведение в степень:

            - +

            Этот код работает за 9.47 секунды.

            Теперь попробуем векторизованную версию:

            - +

            Эта реализация уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

            Трудности автовекторизации

            В самом начале статьи мы приводили пример кода, в котором уже оптимизированный бинарник получается без каких-либо изменений, кроме подключения нужного таргета компиляции. Зачем тогда вообще программисту делать что-либо ещё?

            Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

            Рассмотрим этот же пример, убрав из него всё лишнее:

            - +

            Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

            Во-первых, потому что это не всегда корректно. Предположим, что a[] и c[] пересекаются, причём так, что указатели на начало массивов отличаются на 1-2 позиции. Ну, мало ли — может, мы такой изощрённой свёрткой хотели посчитать последовательность Фибоначчи. Тогда в simd-блоках данные будут пересекаться, и наблюдаемое поведение будет совсем не то, которое мы хотели.

            Во-вторых, мы ничего не знаем про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

            @@ -335,27 +352,30 @@

            Трудности

            Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или вызываются какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

            Разное

            С++ в ассемблер. Посмотреть на генерируемые инструкции можно так:

            - -

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

            + +

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

            +

            Если указать флаг -fopt-info-vec-optimized, то компилятор прямо укажет на операции, которые он смог векторизовать:

            + +

            Можно поменять optimized на missed или all , чтобы посмотреть причины, почему не получилось векторизовать другие.

            Распечатать вектор. Для дебага помогает такой код:

            - +

            В данном случае он выводит 4 группы по 32 бита из 128-битного вектора.

            Деление. В SSE нет операции деления int-ов, но есть для float-ов и производных. Также нет взятия остатка от деления, что осложняет вычисления в комбинаторике.

            Для деления 32-битных целых чисел их можно аккуратно скастовать к даблу, поделить так, и скастовать обратно — точности хватит, хоть это и будет медленно.

            Умножение работает в несколько раз быстрее деления, и поэтому для ускорения деления float-ов на известную константу \(d\) есть следующий трюк: заменить выражение \(x / d\) на \(x \cdot \frac{1}{d}\), и при этом \(\frac{1}{d}\) посчитать во время компиляции.

            Для целочисленных типов такое сделать немного сложнее — нужно заменить деление на умножение и битовый сдвиг. Для этого нужно приблизить \(\frac{1}{d} \approx \frac{m}{2^s}\), подобрав «магическое» число \(m\) и степень двойки \(s\), такие что что x / d == (x * m) >> s для всех x.

            Можно показать, что такая пара чисел всегда существует, и компилятор сам оптимизирует деление на константу подобным образом. Вот, например, сгенерированные инструкции для деления unsigned long long на \(10^9 + 7\):

            - +

            Здесь для умножения используется «mixed precision» инструкция mulq, которая берёт два 64-битных числа и записывает 128-битный результат их умножения в два 64-битных регистра (lo, hi).

            Для деления long-ов на SSE такой способ пока что не работает: аналогичная инструкция добавилась только в AVX512.

            From b7bf19759ba0caa42984905e756f6248d0524754 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 23 Jul 2019 11:38:31 +0000 Subject: [PATCH 141/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 213 ++++++++++++++++++++++++++++------------------------ 1 file changed, 114 insertions(+), 99 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index 4b5cdb9..611d7e5 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -179,14 +179,14 @@

            C++ intrinsics

            SSE это те же чистые ассемблерные инструкции. Языки с каким-либо более верхним уровнем абстракции напрямую работать с ними уже не могут. Однако не обязательно писать на чистом ассемблере, чтобы их использовать — разработчики компиляторов уже позаботились об этом за вас и сделали встроенные функции-обёртки, которые называют интринзиками (англ. intrinsic — «внутренний»).

            Чтобы их подключить, нужно указать include на соответствующий заголовочный файл, а также сказать компилятору о том, что мы хотим использовать конкретный набор или наборы инструкций. В примере из начала статьи мы сделали именно это, прописав target("avx2") — компилятор получил доступ к более широким регистрам и продвинутым инструкциям для них, и смог соптимизировать программу примерно в два раза (по умолчанию включены 128-битные sse и sse2, поэтому в 2, а не в \(\frac{256}{32} = 8\)).

            По аналогии с <bits/stdc++.h>, в GCC есть такой же заголовочный файл <x86intrin.h>, включающий в себя сразу все SSE-интринзики. Шаблон любителя засоренных неймспейсов и избыточно долгой компиляции может начинаться так:

            -
            #pragma GCC optimize("O3")
            -#pragma GCC target("avx2")
            +
            -

            Простой цикл, в котором складывают два массива 64-битных действительных чисел, на SSE-интринзиках будет выглядеть так:

            +

            Пример. Простой цикл, в котором складывают два массива 64-битных действительных чисел, на SSE-интринзиках будет выглядеть так:

            double a[100], b[100], c[100];
             
             for (int i = 0; i < 100; i += 4) {
            @@ -251,100 +251,115 @@ 

            C++ intrinsics

            Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

            Для каждой размерности регистра есть 3 типа данных. На примере AVX: __m256 для float, __m256d для double и __m256i для разных int-ов.

            Некоторые операции есть только для какого-то одного типа (например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов), однако будут работать с другими типами абсолютно так же. Поэтому, to make compiler happy, нужно применять к ним преобразания типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

            +

            Loop unrolling. Добавление флага unroll-loops (#pragma GCC optimize("unroll-loops")) позволяет компилятору делать «размотку» циклов, то есть преобразовывать код вида:

            + +

            во что-то такое:

            + +

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

            Нетривиальный пример

            Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

            Тестировать и сравнивать решения будем таким кодом:

            - +

            В SSE весьма сложно делить int-ы (см. примечания ниже), поэтому будем считать всё по модулю \(2^{32}\), то есть просто переполняя естественным образом unsigned int.

            Напишем стандартное итеративное бинарное возведение в степень:

            - +

            Этот код работает за 9.47 секунды.

            Теперь попробуем векторизованную версию:

            - +

            Эта реализация уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

            Трудности автовекторизации

            В самом начале статьи мы приводили пример кода, в котором уже оптимизированный бинарник получается без каких-либо изменений, кроме подключения нужного таргета компиляции. Зачем тогда вообще программисту делать что-либо ещё?

            Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

            Рассмотрим этот же пример, убрав из него всё лишнее:

            - +

            Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

            Во-первых, потому что это не всегда корректно. Предположим, что a[] и c[] пересекаются, причём так, что указатели на начало массивов отличаются на 1-2 позиции. Ну, мало ли — может, мы такой изощрённой свёрткой хотели посчитать последовательность Фибоначчи. Тогда в simd-блоках данные будут пересекаться, и наблюдаемое поведение будет совсем не то, которое мы хотели.

            Во-вторых, мы ничего не знаем про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

            @@ -352,30 +367,30 @@

            Трудности

            Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или вызываются какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

            Разное

            С++ в ассемблер. Посмотреть на генерируемые инструкции можно так:

            - +

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

            Если указать флаг -fopt-info-vec-optimized, то компилятор прямо укажет на операции, которые он смог векторизовать:

            - +

            Можно поменять optimized на missed или all , чтобы посмотреть причины, почему не получилось векторизовать другие.

            Распечатать вектор. Для дебага помогает такой код:

            - +

            В данном случае он выводит 4 группы по 32 бита из 128-битного вектора.

            Деление. В SSE нет операции деления int-ов, но есть для float-ов и производных. Также нет взятия остатка от деления, что осложняет вычисления в комбинаторике.

            Для деления 32-битных целых чисел их можно аккуратно скастовать к даблу, поделить так, и скастовать обратно — точности хватит, хоть это и будет медленно.

            Умножение работает в несколько раз быстрее деления, и поэтому для ускорения деления float-ов на известную константу \(d\) есть следующий трюк: заменить выражение \(x / d\) на \(x \cdot \frac{1}{d}\), и при этом \(\frac{1}{d}\) посчитать во время компиляции.

            Для целочисленных типов такое сделать немного сложнее — нужно заменить деление на умножение и битовый сдвиг. Для этого нужно приблизить \(\frac{1}{d} \approx \frac{m}{2^s}\), подобрав «магическое» число \(m\) и степень двойки \(s\), такие что что x / d == (x * m) >> s для всех x.

            Можно показать, что такая пара чисел всегда существует, и компилятор сам оптимизирует деление на константу подобным образом. Вот, например, сгенерированные инструкции для деления unsigned long long на \(10^9 + 7\):

            - +

            Здесь для умножения используется «mixed precision» инструкция mulq, которая берёт два 64-битных числа и записывает 128-битный результат их умножения в два 64-битных регистра (lo, hi).

            Для деления long-ов на SSE такой способ пока что не работает: аналогичная инструкция добавилась только в AVX512.

            From d0fb7d0964a2790f093d9431d524f112aab955ec Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 23 Jul 2019 11:53:53 +0000 Subject: [PATCH 142/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index 611d7e5..28e389c 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -165,15 +165,14 @@

            Streaming SIMD Extensions

            using namespace std; int main() { - - cout << __builtin_cpu_supports("sse") << endl; - cout << __builtin_cpu_supports("sse2") << endl; - cout << __builtin_cpu_supports("avx") << endl; - cout << __builtin_cpu_supports("avx2") << endl; - cout << __builtin_cpu_supports("avx512f") << endl; - - return 0; -}
            + cout << __builtin_cpu_supports("sse") << endl; + cout << __builtin_cpu_supports("sse2") << endl; + cout << __builtin_cpu_supports("avx") << endl; + cout << __builtin_cpu_supports("avx2") << endl; + cout << __builtin_cpu_supports("avx512f") << endl; + + return 0; +}

            Экономя время читателю: сервера CodeForces и большинство онлайн джаджей на момент написания статьи поддерживают AVX2, то есть умеют полноценно работать с 256-битными регистрами.

            C++ intrinsics

            SSE это те же чистые ассемблерные инструкции. Языки с каким-либо более верхним уровнем абстракции напрямую работать с ними уже не могут. Однако не обязательно писать на чистом ассемблере, чтобы их использовать — разработчики компиляторов уже позаботились об этом за вас и сделали встроенные функции-обёртки, которые называют интринзиками (англ. intrinsic — «внутренний»).

            @@ -265,7 +264,7 @@

            C++ intrinsics

            for (; i < n; i++) a[i] = (i % b[i]); -

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

            +

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

            Нетривиальный пример

            Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

            Тестировать и сравнивать решения будем таким кодом:

            @@ -371,7 +370,7 @@

            Разное

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

            Если указать флаг -fopt-info-vec-optimized, то компилятор прямо укажет на операции, которые он смог векторизовать:

            -

            Можно поменять optimized на missed или all , чтобы посмотреть причины, почему не получилось векторизовать другие.

            +

            Можно поменять optimized на missed или all, чтобы посмотреть причины, почему не получилось векторизовать другие.

            Распечатать вектор. Для дебага помогает такой код:

            template<typename T>
             void print(T var) {
            
            From 683d6e5063fee076a96caef517bec282e87dbb39 Mon Sep 17 00:00:00 2001
            From: "Deployment Bot (from Travis CI)" 
            Date: Tue, 23 Jul 2019 11:59:45 +0000
            Subject: [PATCH 143/230] Deploy algorithmica.org to
             github.com/algorithmica-org/articles.git:gh-pages
            
            ---
             ru/sse.html | 12 ++++++------
             1 file changed, 6 insertions(+), 6 deletions(-)
            
            diff --git a/ru/sse.html b/ru/sse.html
            index 28e389c..6a0283f 100644
            --- a/ru/sse.html
            +++ b/ru/sse.html
            @@ -139,7 +139,7 @@ 

            Complex Instruction Set Computing

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

            • RISC (англ. reduced instruction set computer), в которых длина кода (идентификатора) самой инструкции ограничена, а значит ограничено и само количество инструкций. Самые первые компьютеры относились к этому типу и могли не иметь даже отдельных инструкций для умножения и деления. Такие процессоры требует меньше транзисторов, и как следствие сами меньше, дешевле и потребляют меньше энергии. Самое популярное семейство архитектур называется arm и используется почти на всех современных мобильных устройствах.

            • -
            • CISC (англ. complex instruction set computer), к которому относят всё, что не RISC — в них длина команды не фиксирована, что позволяет поддерживать практически произовльное количество инструкций. Самое популярное семейство архитектур называется x86 и используется почти на всех современных декстопах и серверах.

            • +
            • CISC (англ. complex instruction set computer), к которому относят всё, что не RISC — в них длина команды не фиксирована, что позволяет поддерживать практически произовльное количество инструкций. Самое популярное семейство архитектур называется x86 и используется почти на всех современных десктопах и серверах.

            Новые инструкции стали добавлять постепенно, причём разные в зависимости от области применения.

            @@ -148,13 +148,13 @@

            Complex Instruction Set Computing

            На графических сопроцессорах появилась отдельная инструкция, которую называют «saxpy» (сокращенно от выражения s += a * x + y), которая полезна, например, при перемножении матриц.

          9. В последние GPU от Nvidia добавили «tensor core» — отдельную схему, которая перемножает две матрицы \(4 \times 4\) и прибавляет к третьей, как бы производя \(4 \times 4 \times 4 = 64\) умножений и \(4 \times 4 = 16\) сложений за раз, что сильно ускоряет алгоритмы блочного матричного умножения.

          10. -

            В этой статье мы сфокусируемся на отдельным виде инструкций, которые позволяют выполнять одну и ту же операцию сразу на какой-то последовательности данных. Эта концепция называется SIMD-параллелизмом (англ. single instruction, multiple data).

            +

            В этой статье мы сфокусируемся на отдельном виде инструкций, которые позволяют выполнять одну и ту же операцию сразу на какой-то последовательности данных. Эта концепция называется SIMD-параллелизмом (англ. single instruction, multiple data).

            Streaming SIMD Extensions

            -

            SSE — это обобщённое называние всех SIMD-инструкций для x86.

            +

            SSE — это обобщённое название всех SIMD-инструкций для x86.

            Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно логически разбивают эту булеву последовательность на блоки, например, по 32 бит, и работают уже с ними.

            Подобным способом довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

            Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует).

            -

            Очень часто SSE используют для работы с действительными числами, и в этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходых данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

            +

            Очень часто SSE используют для работы с действительными числами, и в этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходных данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

            Конкретный набор инструкций и размеры регистров зависит от вендора и поколения архитектуры. На данный момент (лето 2019 года) большинство процессоров архитектуры x86 производит Intel, поэтому мы сконцентрируемся именно на их наборе инструкций.

            Поддержка SIMD-инструкций добавлялись постепенно, сохраняя обратную совместимость. Если третий пентиум в 1999-м году умел работать с регистрами размера 128, то в самых современных i7 есть 512-битные регистры. Автор не является специалистом в проектировании микропроцессоров, но предполагает, то регистры больше 64 байт (512 бит) появятся не скоро, потому что это уже больше размера кэш-линии

            @@ -194,7 +194,7 @@

            C++ intrinsics

            __m256d y = _mm256_loadu_pd(&b[i]); // - 256 означает размер регистров // - d означает "double" - // - pd озанчает "packed double" + // - pd означает "packed double" // просуммируем числа и положим результат в другой регистр: __m256d z = _mm256_add_pd(x, y); @@ -249,7 +249,7 @@

            C++ intrinsics

            }

            Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

            Для каждой размерности регистра есть 3 типа данных. На примере AVX: __m256 для float, __m256d для double и __m256i для разных int-ов.

            -

            Некоторые операции есть только для какого-то одного типа (например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов), однако будут работать с другими типами абсолютно так же. Поэтому, to make compiler happy, нужно применять к ним преобразания типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

            +

            Некоторые операции есть только для какого-то одного типа (например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов), однако будут работать с другими типами абсолютно так же. Поэтому, to make compiler happy, нужно применять к ним преобразования типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

            Loop unrolling. Добавление флага unroll-loops (#pragma GCC optimize("unroll-loops")) позволяет компилятору делать «размотку» циклов, то есть преобразовывать код вида:

            From 0da0c33b2dcc5cb540e61119ffad2f808b310c06 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 23 Jul 2019 12:44:07 +0000 Subject: [PATCH 144/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/aho-corasick.html | 2 +- ru/annealing.html | 4 ++-- ru/bitset.html | 2 +- ru/centroid.html | 2 +- ru/convex-hulls.html | 12 ++++++------ ru/dp-optimizations.html | 2 +- ru/eratosphenes.html | 4 ++-- ru/euclid.html | 4 ++-- ru/fenwick.html | 4 ++-- ru/fft.html | 8 ++++---- ru/geometry.html | 8 ++++---- ru/hashing.html | 4 ++-- ru/hld.html | 2 +- ru/index.html | 2 +- ru/karatsuba.html | 2 +- ru/lca.html | 14 +++++++------- ru/linalg.html | 2 +- ru/matroid.html | 6 +++--- ru/mincost-maxflow.html | 6 +++--- ru/mst.html | 8 ++++---- ru/palindromes.html | 6 +++--- ru/segtree.html | 6 +++--- ru/sparse-table.html | 2 +- ru/sqrt.html | 10 +++++----- ru/string-searching.html | 2 +- ru/suffix-array.html | 2 +- ru/treap.html | 4 ++-- 27 files changed, 65 insertions(+), 65 deletions(-) diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html index a258e81..2c1d4cb 100644 --- a/ru/aho-corasick.html +++ b/ru/aho-corasick.html @@ -158,7 +158,7 @@

            Суффиксные ссылки

            Пусть дан набор строк \(s_1, s_2, \ldots, s_n\), называемый cловарём и большой текст \(t\). Необходимо найти все позиции, где строки словаря входят в текст. Для простоты дополнительно предположим, что строки из словаря не являются подстроками друг друга (позже мы увидим, что это требование избыточно).

            Решать эту задачу будем следующим образом. Будем обрабатывать символы текста по одному и поддерживать наибольшую строку, являющуюся префиксом строки из словаря, и при этом также суффиксом считанного на данный момент текста. Если эта строка совпадает с каким-то \(s_i\), то отметим текущий символ — в нём заканчивается какая-то строка из словаря.

            Для этой задачи нам нужно как-то эффективно хранить и работать со всеми префиксами слов из словаря — для этого нам и понадобится префиксное дерево.

            -

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

            +

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

            Анти-формализм. Чтобы не писать везде громоздкое «строка \(s\), которая соответствуют вершине \(v\)», условимся дальше отождествлять вершину и соответствующую ей строку в боре.

            Определение. Суффиксная ссылка \(l(v)\) ведёт в вершину \(u \neq v\), которая соответствует наидлиннейшему принемаемому бором суффиксу \(v\).

            Определение. Автоматный переход \(\delta(v, c)\) ведёт в вершину, соответствующую минимальному принемаемому бором суффиксу строки \(v + c\). Если переход и так существует в боре, то автоматный переход будет вести туда же.

            diff --git a/ru/annealing.html b/ru/annealing.html index 1c218e9..aa22ade 100644 --- a/ru/annealing.html +++ b/ru/annealing.html @@ -109,7 +109,7 @@

            Метод отжига

            Метод отжига

            -

            Алгоритм иммитации отжига (англ. simulated annealing) — эвристический алгоритм глобальной оптимизации, особенно эффективный при решении дискретных и комбинаторных задач.

            +

            Алгоритм имитации отжига (англ. simulated annealing) — эвристический алгоритм глобальной оптимизации, особенно эффективный при решении дискретных и комбинаторных задач.

            Алгоритм вдохновлён процессом отжига в металлургии — техники, заключающейся в нагревании и контролируемом охлаждении металла, чтобы увеличить его кристаллизованность и уменьшить дефекты. Симулированние отжига в переборных задачах может быть использовано для приближённого нахождения глобального минимума функций с большим количеством свободных переменных.

            Алгоритм вероятностный и не даёт почти никаких гарантий сходимости, однако хорошо работает на практике при решении NP-полных задач. Иногда на контестах им удаётся сдать сложные комбинаторные задачи, у которых есть нормальное решение: Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам.

            @@ -169,7 +169,7 @@

            Реализация

            double rnd() { return double(rand()) / RAND_MAX; } int main() { - // генерируем начальную пререстановку + // генерируем начальную перестановку vector<int> v(n); iota(v.begin(), v.end(), 0); shuffle(v.begin(), v.end()); diff --git a/ru/bitset.html b/ru/bitset.html index 0eafaa4..d84363d 100644 --- a/ru/bitset.html +++ b/ru/bitset.html @@ -113,7 +113,7 @@

            Битовое сжатие

            Когда от нас требуется делать какие-то теоретико-множественные операции, их часто можно свести к большому количеству одинаковых операций над элементами булевого массива. Например:

            • Объединение множеств — побитовое ИЛИ.

            • -
            • Пересечение множест — побитовое И.

            • +
            • Пересечение множеств — побитовое И.

            • Симметрическая разность — побитовый XOR.

            Здесь появляется следующая идея оптимизации: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем применить эту битовую операцию сразу к 64 элементам и потратить на это один такт вместо 64-х.

            diff --git a/ru/centroid.html b/ru/centroid.html index c55b74f..99c1331 100644 --- a/ru/centroid.html +++ b/ru/centroid.html @@ -205,7 +205,7 @@

            Под if (!used[u]) solve(centroid(u, v, s[u])); } -

            Асимптотика \(O(n \log n)\), потому что на каждую из \(O(n)\) верщин мы потратим \(O(1)\) операций на каждом из \(O(\log n)\) «уровней» центроидной декомпозиции.

            +

            Асимптотика \(O(n \log n)\), потому что на каждую из \(O(n)\) вершин мы потратим \(O(1)\) операций на каждом из \(O(\log n)\) «уровней» центроидной декомпозиции.

            Запросы на путях — offline

            Мы находим всё, что нам надо, сразу после того, как получили новую компоненту. Если у нас есть список запросов «посчитать что-то на пути», то мы часто можем обработать их в offline.

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

            diff --git a/ru/convex-hulls.html b/ru/convex-hulls.html index 55edf2f..882280f 100644 --- a/ru/convex-hulls.html +++ b/ru/convex-hulls.html @@ -152,7 +152,7 @@

            Алгоритм Джарвиса

            Важно помнить, что асимптотика именно \(O(nh)\), а не \(O(n^2)\): существуют задачи, где оболочка маленькая, и это существенно.

            Алгоритм Грэхема

            Алгоритм Грэхема — это оптимизация алгоритма Джарвиса, основанная на следующем наблюдении: если отсортировать все точки по полярному углу относительно точки \(p_0\), то выпуклая оболочка будет какой-то подпоследовательностью такого отсортированного массива точек.

            -

            Алгоритм последовательно строит выпуклые оболочки для каждого префикса этого отсортированного масива. При добавлении \(i\)-й точки в оболочку нужно удалить сколько-то последних добавленных точек, которые не будут входить в новую оболочку. Чтобы это делать эффективно, мы можем хранить выпуклую оболочку в стэке и в цикле while смотреть на три последние точки и проверять, образуют ли они правый поворот. Если это так, то среднюю следует удалить — мы нашли треугольник \((p_0, p_i, p_{i-2})\), который содержит \(p_{i-1}\), значит её можно удалить.

            +

            Алгоритм последовательно строит выпуклые оболочки для каждого префикса этого отсортированного масива. При добавлении \(i\)-й точки в оболочку нужно удалить сколько-то последних добавленных точек, которые не будут входить в новую оболочку. Чтобы это делать эффективно, мы можем хранить выпуклую оболочку в стеке и в цикле while смотреть на три последние точки и проверять, образуют ли они правый поворот. Если это так, то среднюю следует удалить — мы нашли треугольник \((p_0, p_i, p_{i-2})\), который содержит \(p_{i-1}\), значит её можно удалить.

            alt text
            alt text
            @@ -166,7 +166,7 @@

            Алгоритм Грэхема

            // TODO: удалить p0 - // сортируем точки по полярному углу + // сортируем точки по полярному углу sort(points.begin(), points.end(), [&](r a, r b){ return (a - p0) ^ (b - p0) > 0; }); @@ -184,7 +184,7 @@

            Алгоритм Грэхема

            }

            Верхние и нижние огибающие

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

            -

            Огибающие строить немного проще: можно отсортировать точки по \(x\) и пройтись по ним в таком порядке, поддерживая на стэке верхнюю огибающую для текущего префикса. При добавлении очередной точки нам нужно аналогичным образом проверить и удалить сколько-то верхних точек на стэке:

            +

            Огибающие строить немного проще: можно отсортировать точки по \(x\) и пройтись по ним в таком порядке, поддерживая на стеке верхнюю огибающую для текущего префикса. При добавлении очередной точки нам нужно аналогичным образом проверить и удалить сколько-то верхних точек на стеке:

            vector<r> upper_envelope(vector<r> points) {
                 sort(points.begin(), points.end(), [](r a, r b){
                     return a.x < b.x;
            @@ -231,16 +231,16 @@ 

            Пересечен

            Это диаграмма Вороного — для каждой точки выделено множество точек плоскости, которые являются к ней ближайшими. Чтобы построить это множество для данной точки \(p_i\), можно перебрать все осталньные точки \(p_j\), посчитать коэффициенты прямой, которая делит плоскость между ними пополам, и пересечь все ориентированные в нужную сторону полуплоскости, соответствующие этим прямым.

            Результатом пересечения множества полуплоскостей тоже будет выпуклая оболочка — полуплоскость это выпуклое множество, а пересечение любых выпуклых множеств тоже выпуклое. Возможно, эта оболочка будет бесконечной по каким-то направлениям. Чтобы не обрабатывать это отдельно, часто добавляют bounding box — «коробку» из четырёх полуплоскостей на большом удалении.

            -

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

            +

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

            -

            Алгоритм почти совпадает с алгоритмом алгоритмом Грэхэма и тоже имеет асимптотику \(O(n \log n)\). На самом деле, разделять полуплоскости на верхние и нижние и потом объединять не требуется — можно пройтись таким же образом по сортированному набору полуплоскостей два раза, и какой-то подотрезок получившегося стэка будет нужным ответом. Автор не в состоянии внятно объяснять, почему.

            +

            Алгоритм почти совпадает с алгоритмом алгоритмом Грэхэма и тоже имеет асимптотику \(O(n \log n)\). На самом деле, разделять полуплоскости на верхние и нижние и потом объединять не требуется — можно пройтись таким же образом по сортированному набору полуплоскостей два раза, и какой-то подотрезок получившегося стека будет нужным ответом. Автор не в состоянии внятно объяснять, почему.

            Нахождение касательной

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

            • Если это так, то она останется той же, и ничего дальше считать не надо.

            • Если это не так, то новая точка должна лежать на пересечении новой полуплоскости и какой-то из старых. Важный факт здесь в том, что она лежит на границе новой полуплоскости. Чтобы её найти, пересечём все старые полуплоскости с этой границей и решим одномерную задачу пересечения множества интервалов, бесконечных в одну из сторон. Если получившийся интервал пуст, то и пересечение полуплоскостей пустое, а в противном случае один из его концов будет искомой точкой, если его положить обратно на границу новой полуплоскости.

            -

            Теперь интересная часть — асимптотика. Казалось бы, такой алгоритм в худшем случае может работать за квадратичное время — мы могли каждый раз на \(k\)-том шаге пересекать все имеющиейся полуплоскости с новой за \(O(k)\).

            +

            Теперь интересная часть — асимптотика. Казалось бы, такой алгоритм в худшем случае может работать за квадратичное время — мы могли каждый раз на \(k\)-том шаге пересекать все имеющиеся полуплоскости с новой за \(O(k)\).

            Выясняется, что за счёт рандомизированности нам нужно выполнять это пересечение не так уж и часто. В случайном множестве из \(k\) полуплоскостей только две из них дают в пересечении оптимальную точку. Из этого следует, что вероятность того, что \(k\)-тая полуплоскость станет новой граничной (и понадобится всё пересчитывать) равна всего \(\frac{2}{k}\).

            Таким образом, в среднем алгоритм совершит линейное количество операций:

            \[ diff --git a/ru/dp-optimizations.html b/ru/dp-optimizations.html index cb4f5a1..902f455 100644 --- a/ru/dp-optimizations.html +++ b/ru/dp-optimizations.html @@ -195,7 +195,7 @@

            Convex Hull Trick

            Это выражение можно переписать как \(\min_k (a_k, b_k) \cdot (1, x_{i-1})\), где под «\(\cdot\)» имеется в виду скалярное произведение.

            Пусть мы хотим найти оптимальное \(k\) для \(f[i][j]\). Представим все уже посчитанные релевантные динамики с предыдущего слоя как точки \((a_k, b_k)\) на плоскости. Чтобы эффективно находить среди них точку с минимальным скалярным произведением, можно поддерживать их нижнюю огибающую (вектор \((1, x_{i-1})\) «смотрит» всегда вверх, поэтому нам интересна только она) и просто бинпоиском будем находить оптимальную точку.

            -

            Хранить нижнюю огибающую можно просто в стэке. Так как добавляемые точки отсортированы по \(x\), её построение будет занимать линейное время, а асимптотика всего алгоритму будет упираться в асимптотику бинарного поиска, то есть будет равна \(O(n m \log n)\)

            +

            Хранить нижнюю огибающую можно просто в стеке. Так как добавляемые точки отсортированы по \(x\), её построение будет занимать линейное время, а асимптотика всего алгоритму будет упираться в асимптотику бинарного поиска, то есть будет равна \(O(n m \log n)\)

            struct line {
                 int k, b;
                 line() {}
            diff --git a/ru/eratosphenes.html b/ru/eratosphenes.html
            index f340df1..0a2538e 100644
            --- a/ru/eratosphenes.html
            +++ b/ru/eratosphenes.html
            @@ -141,7 +141,7 @@ 

            Время работы

          11. Простых чисел от \(1\) до \(n\) примерно \(\frac{n}{\ln n}\) .

          12. Простые числа распределены без больших «разрывов» и «скоплений», то есть \(k\)-тое простое число примерно равно \(k \ln k\).

          -

          Мы можем упрощённо считать, что число \(k\) является простым с «вероятностью» \(\frac{1}{\ln n}\). Тогда, время работы алгоритма божно более точнее оценить как

          +

          Мы можем упрощённо считать, что число \(k\) является простым с «вероятностью» \(\frac{1}{\ln n}\). Тогда, время работы алгоритма можно более точнее оценить как

          \[ \sum_k \frac{1}{\ln k} \frac{n}{k} \approx n \int \frac{1}{k \ln k} @@ -156,7 +156,7 @@

          Линейное решето

          Алгоритм

          Немного обобщим задачу — теперь мы хотим посчитать для каждого числа \(k\) на отрезке \([2, n]\) его минимальный простой делитель \(d_k\), а не только определить его простоту.

          Изначально массив \(d\) заполним нулями, что означает, что все числа предполагаются простыми. В ходе работы алгортима этот массив будет постепенно заполняться. Помимо этого, будем поддерживать список \(p\) всех найденных на текущий момент простых чисел.

          -

          Теперь будем перебирать число \(k\) от \(2\) до \(n\). Если это число простое, то есть \(d_k = 0\), то просвоим \(d_k = k\) и добавим \(k\) в список \(p\).

          +

          Теперь будем перебирать число \(k\) от \(2\) до \(n\). Если это число простое, то есть \(d_k = 0\), то присвоим \(d_k = k\) и добавим \(k\) в список \(p\).

          Дальше, вне зависимости от простоты \(k\), начнём процесс расстановки значений в массиве \(d\) — переберем найденные простые числа \(p_i\), не превосходящие \(d_k\), и сделаем присвоение \(d_{p_i k} = p_i\).

          const int n = 1e6;
           
          diff --git a/ru/euclid.html b/ru/euclid.html
          index 6974a61..53461df 100644
          --- a/ru/euclid.html
          +++ b/ru/euclid.html
          @@ -109,7 +109,7 @@
           

          Алгоритм Евклида

          Алгоритм Евклида

          -

          Наименьшим общим делителем (англ. greatest common divisor) целых неотрицательных чисел \(a\) и \(b\) называется наибольшое число \(x\), которое делит одновременно и \(a\), и \(b\).

          +

          Наименьшим общим делителем (англ. greatest common divisor) целых неотрицательных чисел \(a\) и \(b\) называется наибольшее число \(x\), которое делит одновременно и \(a\), и \(b\).

          \[ \gcd(a, b) = \max_{k: \; k|a \, \land \, k | b} k \]

          @@ -182,7 +182,7 @@

          Решение

          \[ b \cdot x' + (a \bmod b) \cdot y' = g \]

          -

          Чтобы получить решение для исходной пары, запишем выражение \((a \bmod b)\) как \((a - \lfloor \frac{a}{b} \rfloor \cdot b)\) и подставим в преведенное выше равенство:

          +

          Чтобы получить решение для исходной пары, запишем выражение \((a \bmod b)\) как \((a - \lfloor \frac{a}{b} \rfloor \cdot b)\) и подставим в приведенное выше равенство:

          \[ b \cdot x' + (a - \Big \lfloor \frac{a}{b} \Big \rfloor \cdot b) \cdot y' = g \]

          diff --git a/ru/fenwick.html b/ru/fenwick.html index a8f1184..8c1bec0 100644 --- a/ru/fenwick.html +++ b/ru/fenwick.html @@ -112,7 +112,7 @@

          Дерево Фенвика

          НЯ!
          Эта статья полна любви и обожания.
          Возможно, стоит добавить ещё больше?

          Дерево Фенвика

          -

          Дерево Фенвика или бинарно индексированное дерево (англ. binary indexed tree) — структура данных, которая на многих задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, сколько и массив той же длины), намного быстрее пишется и легче обобщается на большие размерности.

          +

          Дерево Фенвика или двоичное индексированное дерево (англ. binary indexed tree) — структура данных, которая на многих задачах заменяет собой дерево отрезков, но при этом работает в 3-4 раза быстрее, занимает минимально возможное количество памяти (столько же, сколько и массив той же длины), намного быстрее пишется и легче обобщается на большие размерности.

          Определение

          Пусть дан массив \(a\) длины \(n\). Деревом Фенвика будем называть массив \(t\) той же длины, объявленный следующим образом:

          \[ @@ -209,7 +209,7 @@

          Почему это работае

          Следствие 2. add тоже будет работать за логарифм: каждую итерацию количество нулей на конце \(x\) увеличивается хотя бы на единицу.

          Следствие 3. (Почему дерево Фенвика — дерево.)

            -
          • Длина отрезка, соответстующего любому \(t_i\) — степень двойки, причём начинается этот отрезок на индексе, кратном этой же степени двойки.

          • +
          • Длина отрезка, соответствующего любому \(t_i\) — степень двойки, причём начинается этот отрезок на индексе, кратном этой же степени двойки.

          • \(\implies\) Множества элементов, учтённых в произвольных \(t_i\) и \(t_j\), либо не пересекаются, либо одно является подмножеством другого.

          • \(\implies\) На \(t_i\) можно ввести отношение вложенности.

          diff --git a/ru/fft.html b/ru/fft.html index 3204e8b..7ad717d 100644 --- a/ru/fft.html +++ b/ru/fft.html @@ -110,7 +110,7 @@

          Быстрое преобразование Фурье

          Быстрое преобразование Фурье

          Рассмотрим такую распространённую операцию как умножение двух целых чисел. Квадратичный алгоритм — умножения в столбик — все знают со школы. Долгое время предполагалось, что ничего быстрее придумать нельзя.

          -

          Первым эту гипотезу опроверг Анатолий Карацуба. Его алгоритм сводит умножение двух \(n\)-значиных чисел к трём умножениям \(\frac{n}{2}\)-значных чисел, что даёт оценку времени работы

          +

          Первым эту гипотезу опроверг Анатолий Карацуба. Его алгоритм сводит умножение двух \(n\)-значных чисел к трём умножениям \(\frac{n}{2}\)-значных чисел, что даёт оценку времени работы

          \[ T(n)=3T\left(\dfrac n 2\right)+O(n)=O\left(n^{\log_2 3}\right)\approx O(n^{1.58}) \]

          @@ -244,7 +244,7 @@

          Дискр

          \[ y_j = \sum_{k=0}^{n-1} x_n e^{i\tau \frac{kj}{n}} = \sum_{k=0}^{n-1} x_n w_1^{kj} \]

          -

          Обратным дискретным преобразованием Фурье назвается, как можно догадаться, обратная операция — интерполяция коэффициентов \(x_i\) по значениям \(X_i\).

          +

          Обратным дискретным преобразованием Фурье называется, как можно догадаться, обратная операция — интерполяция коэффициентов \(x_i\) по значениям \(X_i\).

          \[ x_j = \frac{1}{n} \sum_{k=0}^{n-1} y_n e^{-i\tau \frac{kj}{n}} = \frac{1}{n} \sum_{k=0}^{n-1} y_n w_{n-1}^{kj} \]

          @@ -379,7 +379,7 @@

          Number-theoretic transform

          \]

          Это число простое, и при этом является ровно на единицу больше числа, делящегося на большую степень двойки. Можно в качестве \(g\) взять \(7 \cdot 17 \cdot 2^k\) для какого-нибудь \(k\), и тогда по модулю \(n = 2^{23-k}\) это \(g\) будет подходить вследствие малой теоремы Ферма.

          Реализация никак не отличается.

          - -

          С недавнего врмеени некоторые проблемсеттеры начали использовать именно этот модулю вместо стандартного \(10^9+7\), чтобы намекнуть (или сбить с толку), что задача на FFT.

          + +

          С недавнего времени некоторые проблемсеттеры начали использовать именно этот модулю вместо стандартного \(10^9+7\), чтобы намекнуть (или сбить с толку), что задача на FFT.

          diff --git a/ru/geometry.html b/ru/geometry.html index 01d5e87..0f2eac0 100644 --- a/ru/geometry.html +++ b/ru/geometry.html @@ -113,7 +113,7 @@

          Вычислитель
          vector
          vector
          -

          Помимо очевидных сложения, вычетания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь.

          +

          Помимо очевидных сложения, вычитания и умножения на константу (скаляр — одно число), у векторов можно ввести и свои особенные операции, которые нам упростят жизнь.

          Скалярное произведение (англ. dot product) — произведение длин векторов на косинус угла между ними. Для него справедлива следующая формула:

          \[ a \cdot b = x_a x_b + y_a y_b @@ -250,7 +250,7 @@

          Отражение от прямо \Pr_a b = \frac{a \cdot b}{|a|} \frac{a}{|a|} = \frac{|a| |b| \cos \alpha}{|a|} \frac{a}{|a|} = |b| \cos \alpha \frac{a}{|a|} \]

          Геометрический смысл: длина на единичный вектор направления.

          -

          Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жално посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так становится более наглядной логика алгоритма, и, как следствие, его проще дебажить.

          +

          Мы не хотим раскрывать эти формулы покоординатно и предъявлять готовый ответ. Мы знаем, что он получится громоздким. Нам не жалко посчитать всё по частям — здесь нет смысла заниматься оптимизациями. Также мы хотим делать всё по частям, потому что так становится более наглядной логика алгоритма, и, как следствие, его проще дебажить.

          Чтобы так не делать, старайтесь по возможности использовать только инты и абсолютную точность. Иногда есть трюки, позволяющие так делать: например, если в задаче все входные точки целочисленные и нас просят посчитать какую-то площадь, то можно все координаты домножить на два, и тогда ответ тоже будет целым (см. векторное произведение), который только при выводе нужно будет поделить на четыре.

          \(0 \neq -0\)

          -

          Действительные числа так хрянятся, что \(0\) и \(-0\) могут быть разными числами. Имейте это ввиду.

          +

          Действительные числа так хранятся, что \(0\) и \(-0\) могут быть разными числами. Имейте это ввиду.

          Область определения обратных функций

          -

          acos, asin и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, масштабируйте числа, перед тем как брать от них эти функции.

          +

          acos, asin и прочие обратные тригонометрические функций требуют, чтобы им на вход подавалось число от -1 до 1. Для безопасности, отмасштабируйте числа, перед тем как брать от них эти функции.

          diff --git a/ru/hashing.html b/ru/hashing.html index cde0986..0b65607 100644 --- a/ru/hashing.html +++ b/ru/hashing.html @@ -116,7 +116,7 @@

          Хэшир
        5. Имеет не очень большие значения — влезающие в 64 бита;
        6. «Детерминированно-случайная» — если хэш может принимать \(n\) различных значений, то вероятность того, что хэши от двух случайных объектов совпадут, равна примерно \(\frac{1}{n}\).
        7. -

          Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюрьективными.

          +

          Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюръективными.

          Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\), и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

          Применения в реальной жизни

          @@ -193,7 +193,7 @@

          Зачем это нужно?

          \[ h(s[l:r]) = \frac{h_r-h_l}{k^l} \]

          -

          Деление по модулю воможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

          +

          Деление по модулю возможно делать только при некоторых k и mod (а именно — при взаимно простых). В любом случае, писать его долго, и мы это делать не хотим.

          Для нашей задачи не важно получать именно полиномиальный хэш — главное, чтобы наша функция возвращала одинаковый многочлен от одинаковых подстрок. Вместо приведения к нулевой степени приведём многочлен к какой-нибудь достаточно большой — например, к \(n\)-ной. Так проще — нужно будет домножать, а не делить.

          \[ \hat{h}(s[l:r]) = k^{n-l} (h_r-h_l) diff --git a/ru/hld.html b/ru/hld.html index 4311871..d9d6d4a 100644 --- a/ru/hld.html +++ b/ru/hld.html @@ -126,7 +126,7 @@

          Heavy-light декомпозици
        8. Непрерывных путей из тяжелых рёбер будет не более \(O(\log n\): если это не конец или начало пути, то каждый такой путь окружают два лёгких ребра, а их всего \(O(\log n)\).
        9. Следствие. На любом пути будет не более \(O(\log n)\) блоков.

          -

          Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобъём его на \(O(\log n)\) запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам.

          +

          Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобьем его на \(O(\log n)\) запросов либо к подотрезкам тяжелых путей, либо к лёгким рёбрам.

          Реализация

          Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.

          А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то tin-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.

          diff --git a/ru/index.html b/ru/index.html index 04f2ae2..002f2eb 100644 --- a/ru/index.html +++ b/ru/index.html @@ -124,7 +124,7 @@

          Теория игр

          Разное

          diff --git a/ru/karatsuba.html b/ru/karatsuba.html index 29eaeab..b7db067 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -87,7 +87,7 @@

          Мастер-теорема

          = n \sum_{k=0}^{\log n} k = \Theta (n \log^2 n) \]

          -

          В то же время эта рекуррента под условия теоремы не попадает. Можно лишь получить неточные границы \(\Omega (n \log n)\) и \(O(n^{1+\epsilon})\), если подставтиь \(c = 1\) и \(c = 1 + \epsilon\) соответственно. Заметим, что \(n \log n\) и \(n \log^2 n\) асимптотически меньше \(n^{1+\epsilon}\), каким бы маленьким \(\epsilon\) ни был.

          +

          В то же время эта рекуррента под условия теоремы не попадает. Можно лишь получить неточные границы \(\Omega (n \log n)\) и \(O(n^{1+\epsilon})\), если подставить \(c = 1\) и \(c = 1 + \epsilon\) соответственно. Заметим, что \(n \log n\) и \(n \log^2 n\) асимптотически меньше \(n^{1+\epsilon}\), каким бы маленьким \(\epsilon\) ни был.

          Алгоритм Карацубы

          Алгоритм Карацубы сводит задачу умножения двух чисел длины \(n\) к возведению \(n\)-значного числа в квадрат. Для простоты будем считать, что числа бинарные, а n = 2k является степенью двойки. На практике же лучше брать достаточно большое основание, а не двойку — \(10^9\), например.

          Оказывается, что умножение и возведение в квадрат, вообще говоря, эквивалентные задачи: так как \(4ab=(a+b)^{2}-(a-b)^{2}\), то вместо умножения двух чисел \(a\) и \(b\) можно возвести два числа такого же порядка в квадрат и за линейное время сделать несколько сложений и вычитаний.

          diff --git a/ru/lca.html b/ru/lca.html index 1b604b0..0edd621 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -115,7 +115,7 @@

          Корневые деревья

        10. наследование классов в языках программирования (если множественное наследование запрещено),
        11. дерево факторизации числа на простые (в общем случае не уникальное),
        12. иерархия в какой-нибудь компании,
        13. -
        14. просто множество как-то вложенных объектов (ссылка на крысу-роботягу).
        15. +
        16. просто множество как-то вложенных объектов.
        17. Сегодня мы поговорим о разных задачах на корневых деревьях. Они все бесполезны в реальной жизни, но очень красивые, и поэтому часто встречаются на олимпиадах.

          @@ -221,23 +221,23 @@

          Сведение к RMQ

          Во втором массиве мы по сути выписали наш проход dfs-а.

          Заметим, что размер полученного массива — \(O(n)\). Доказательство — каждое ребро мы пройдем дважды (заходя в и выходя из поддерева), при просмотре каждого ребра мы выпишем не более, чем две вершины (потому что у каждого ребра всего два конца). Тогда размер массива не более, чем в четыре раза больше числа ребер, которых \(n-1\).

          -

          Пусть у нас есть запрос: найти LCA вершин \(v\) и \(u\). Для определенности положим, что \(tin_v < tin_u\). Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из \(v\) и в первй раз вошли в \(u\). Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.

          +

          Пусть у нас есть запрос: найти LCA вершин \(v\) и \(u\). Для определенности положим, что \(tin_v < tin_u\). Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из \(v\) и в первый раз вошли в \(u\). Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.

          Получается, что можно найти LCA, просто найдя позицию минимума на отрезке \([tout_v, tin_u]\) в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы уже умеем.

          Разреженная таблица

          На практике асимптотику мы особо не улучшили — пока что всё равно требуется \(O(n \log n)\) времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.

          Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за \(O(1)\), но использующая \(O(n \log n)\) препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в отдельной статье.

          А наоборот можно?

          -

          Примечание: алгоритм (Фарах-Колтона и Бендера), описаный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

          +

          Примечание: алгоритм (Фарах-Колтона и Бендера), описанный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

          На самом деле, мы решаем не совсем полноценную задачу RMQ: мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — любые два элемента отличаются ровно на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос.

          -

          Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходнуму массиву глубин булевый массив размера \(n-1\): единица стоит, если следующее значение больше, единица в противном случае ноль.

          -

          Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоек посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

          +

          Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходному массиву глубин булевый массив размера \(n-1\): единица стоит, если следующее значение больше, единица в противном случае ноль.

          +

          Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоке посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

          \[ O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n) \]

          Также посчитаем для каждой возможной маски размера \(\frac{\log n}{2}\) минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего \(\sqrt n\) (ради этого мы и делили логарифм на два).

          ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.

          -

          Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Мы возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Тогда минимум на не-блочной части — это минимум на левом блочном элементе минум минимум на не-блочной маски, которую мы предпосчитали.

          -

          Наоборот. Этот алгоритм очень важен с теоретической точки зрения, потому что позволяет решать и static RMQ за линейное время. А именно, построим декартово дерево, в котром в качестве ключей \(x_i\) возьмём индексы элементов, а в качестве приоритетов \(y_i\) возьмём сами значения. Декартово дерево могло получиться несбалансированным (нет рандомизации приоритетов), но это нам и не нужно: дальше мы просто применим описанный выше алгоритм к этому дереву, и теперь для нахождения минимума просто запросим общего предка \(l\)-той и \(r\)-той вершины. Его приоритет в декартовом дереве и будет минимумом на нужном отрезке в исходном массиве.

          +

          Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Теперь по этой маске просто возьмём минимум, который мы посчитали.

          +

          Наоборот. Этот алгоритм очень важен с теоретической точки зрения, потому что позволяет решать и static RMQ за линейное время. А именно, построим декартово дерево, в котором в качестве ключей \(x_i\) возьмём индексы элементов, а в качестве приоритетов \(y_i\) возьмём сами значения. Декартово дерево могло получиться несбалансированным (нет рандомизации приоритетов), но это нам и не нужно: дальше мы просто применим описанный выше алгоритм к этому дереву, и теперь для нахождения минимума просто запросим общего предка \(l\)-той и \(r\)-той вершины. Его приоритет в декартовом дереве и будет минимумом на нужном отрезке в исходном массиве.

          Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать у Емакса. Впрочем, на практике этот алгоритм использовать нецелесообразно: у него слишком большая константа, слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.

          diff --git a/ru/linalg.html b/ru/linalg.html index 80c8caf..0f938b6 100644 --- a/ru/linalg.html +++ b/ru/linalg.html @@ -341,7 +341,7 @@

          Линейная незав

          Базисом множества называется набор векторов, через который можно выразить все вектора этого множества и только их.

          Базисы есть не только в линейной алгебре. Например, \(\{1, x, x^2\}\) является базисом всех квадратных трёхчленов. Или \(\{\neg, \land, \lor\}\) является базисом всех логических выражений (то есть всё можно выразить через И, ИЛИ и НЕ). В произвольном языке программирования можно выделить какой-то набор команд, через который можно будет написать всё что угодно, и он тоже в этом смысле будет базисом.

          Системы уравнений и метод Гаусса

          -

          Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключаетелей.

          +

          Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большая часть из них на самом деле над полем \(\mathbb{Z}_2\) — то есть все числа по модулю 2. К примеру: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно состояние всех лампочек, нужно восстановить состояние переключателей.

          Нас по сути просят решить следующую систему:

          \[ \begin{cases} diff --git a/ru/matroid.html b/ru/matroid.html index 1047987..fad1170 100644 --- a/ru/matroid.html +++ b/ru/matroid.html @@ -109,7 +109,7 @@

          Матроиды

          Матроиды

          -

          Понятие матроида нужно, чтобы придумывать и доказывтать некоторые жадные алгоритмы в задачах, где нужно набрать какое-то множество объектов максимального веса. Например, минимальный остов, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени.

          +

          Понятие матроида нужно, чтобы придумывать и доказывать некоторые жадные алгоритмы в задачах, где нужно набрать какое-то множество объектов максимального веса. Например, минимальный остов, максимальное вершинно-взвешенное паросочетание или выполнение заказов с ограничениями по времени.

          Если вы не специалист по абстрактной алгебре, рекомендуется подсматривать примеры матроидов в конце статьи.

          Определение

          Матроидом называется пара \((X, I)\), где \(X\) — множество элементов, называемое носителем матроида, а \(I\) — некоторое множество подмножеств \(X\), называемое семейством независимых множеств. В матроиде должны выполняться следующие свойства:

          @@ -160,7 +160,7 @@

          Минимальный остов

          Применив к этому матроиду теорему Радо-Эдмондса, мы получаем обоснование алгоритма Крускала для нахождения минимального остова.

          Расписания

          Пусть у нас есть \(n\) заданий, на выполнение каждого требуется \(1\) час. Награда за выполнение \(i\)-го задания не позже \(d_i\)-того часа равна \(w_i\). В один час разрешено сделать только одно задание. Цель — максимизировать сумму наград.

          -

          Назовём правильными те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированый набор можно так: отсортировать по дедлайнам (\(d_i\)) и проверить, что \(d_i \geq i\) для всех \(i\).

          +

          Назовём правильными те наборы заданий, для которых существует порядок, при котором можно их всех успеть сделать до их дедлайнов. Проверять фиксированный набор можно так: отсортировать по дедлайнам (\(d_i\)) и проверить, что \(d_i \geq i\) для всех \(i\).

          Тогда \(M =\) (множество всех заданий, множество правильных наборов заданий) является матроидом:

          • Пустой набор заданий всегда можно сделать.
          • @@ -182,7 +182,7 @@

            Линейно н
            • В пустом множестве нельзя получить какой-либо вектор.
            • Подмножество линейно независимого множество тоже линейно независимо: способов набрать вектора стало ещё меньше.
            • -
            • Пусть есть два линейно независимых множетва \(A\) и \(B\), и \(B\) больше. Пусть в нём нельзя выбрать вектор, совместимый с \(A\). Тогда получается, что все вектора \(B\) можно выразить из \(A\). Значит, размерность \(B\) уж точно не больше — противоречие.
            • +
            • Пусть есть два линейно независимых множества \(A\) и \(B\), и \(B\) больше. Пусть в нём нельзя выбрать вектор, совместимый с \(A\). Тогда получается, что все вектора \(B\) можно выразить из \(A\). Значит, размерность \(B\) уж точно не больше — противоречие.

            Пример задачи: набрать базис максимального веса, если каждый вектор сколько-то стоит.

            Примечание: \(Z_2\) — это тоже пространство, и там тоже можно ввести линейную независимость. Это значит, что то же самое распространяется на задачи в духе «набрать максимальное множество чисел, из которых ксором нельзя получить ноль».

            diff --git a/ru/mincost-maxflow.html b/ru/mincost-maxflow.html index 66c9d28..95c8e3a 100644 --- a/ru/mincost-maxflow.html +++ b/ru/mincost-maxflow.html @@ -123,7 +123,7 @@

            Отмена циклов

            Если искать цикл алгоритмом Форда-Беллмана, то асимптотика алгоритма составит \(O(m^2nUC)\) (предполагая, что какой-нибудь максимальный поток мы уже нашли).

            Дополняющие пути

            Вспомним общий алгоритм поиска максимального потока, основанный на теореме Форда-Фалкерсона: найти какой-нибудь дополняющий путь, пропустить по нему поток и модифицировать сеть, снова найти дополняющий путь и так далее, пока путь из истока в сток существует. Что будет, если мы каждый раз будем искать не произвольный путь, а путь минимальной стоимости? Утверждается, что такой алгоритм найдет максимальный поток минимальной стоимости.

            -

            Утверждеие. Алгоритм не создает в остаточной сети циклов отрицательного веса.

            +

            Утверждение. Алгоритм не создает в остаточной сети циклов отрицательного веса.

            Изначально в остаточной сети нет циклов отрицательного веса. Мы нашли минимальный путь из \(s\) в \(t\) и модифицировали сеть, возможно добавив какие-то обратные рёбра. Могли ли из-за этих рёбер появиться циклы отрицательного веса? Пусть какое-нибудь обратное ребро \((v, u)\) находится в цикле отрицательного веса. Тогда есть путь Из \(u\) в \(v\) стоимости меньше, чем \(w_{uv}\). Но такое не могло произойти: если бы такой путь существовал, то на этапе поиска дополняющего пути мы выбрали бы его вместо ребра \((u, v)\).

            Для поиска дополняющего пути можно использовать алгоритм Форда-Беллмана. Асимптотика в данном случае составит \(O(nmU)\) — искать каждый дополняющий путь мы будем не более \(U\) раз.

            Почему мы не использовали алгоритм Дейкстры? Проблема в рёбрах отрицательного веса. Даже если в исходном графе их нет, они могут в процессе алгоритма появиться как обратные. Покажем, как изменить веса рёбер так, чтобы они стали неотрицательными, но информация о кратчайших путях не утратилась: это можно сделать, если дать каждой вершине так называемый «потенциал», который будет учитываться при пересчете стоимостей ребер.

            @@ -149,7 +149,7 @@

            Потенциалы Джонсо

            Доказательство. Все добавленные обратные рёбра на кратчайшем пути будут иметь нулевую стомость (утверждение 1), а добавления или удаления рёбер на кратчайшие пути не повлияли (утверждение 2).

            Итоговый алгоритм

              -
            • Модифицируем сеть, добавивив обратные рёбра.
            • +
            • Модифицируем сеть, добавив обратные рёбра.
            • Если в исходном графе есть рёбра отрицательного веса (но нет циклов отрицательного веса), то посчитать изначальные потенциалы (расстояния) алгоритмом Форда-Беллмана. Иначе достаточно положить потенциалы изначально равными нулю.
            • Пока максимальный поток не найден:
              • @@ -216,6 +216,6 @@

                Реализация

                }

          Асимптотика

          В общем случае, алгоритм работает за \(O(U m \log n)\) или \(O(U n^2)\) в случае плотных графов.

          -

          В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и \(U \leq n\) или \(U \leq m\). Например, в задаче о назначениях \(U = n\), и алгоритм работает за \(O(n^3)\), что совпадает с асимптикой венгерского алгоритма.

          +

          В наиболее популярных задачах рёбра обычно с единичной пропускной способностью, и \(U \leq n\) или \(U \leq m\). Например, в задаче о назначениях \(U = n\), и алгоритм работает за \(O(n^3)\), что совпадает с асимптотикой венгерского алгоритма.

          diff --git a/ru/mst.html b/ru/mst.html index 81b87d2..a4cd1c3 100644 --- a/ru/mst.html +++ b/ru/mst.html @@ -130,7 +130,7 @@

          Лемма о безоп

          Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов.

          Алгоритм Прима

          -

          Один из подходов — строить минимальный остов остепенно, добавленяя в него рёбра по одному.

          +

          Один из подходов — строить минимальный остов постепенно, добавляя в него рёбра по одному.

          • Изначально остов — одна произвольная вершина.
          • Пока минимальный остов не найден, выбирается ребро минимального веса, исходящее из какой-нибудь вершины текущего остова в вершину, которую мы ещё не добавили. Добавляем это ребро в остов и начинаем заново, пока остов не будет найден.
          • @@ -271,7 +271,7 @@

            Алгоритм Крускала

            }

            Алгоритм Борувки

            Лемма. Для любой вершины минимальное инцидентное ей реборо является безопасным.

            -

            Доказательство. Пусть есть минимальный остов, в котором для какой-то врешины \(v\) нет её минимального инцидентного ребра. Тогда, если добавить это ребро, образуется цикл, из которого можно удалить другое ребро, тоже инцидентное \(v\), но имеющее не меньший вес.

            +

            Доказательство. Пусть есть минимальный остов, в котором для какой-то вершины \(v\) нет её минимального инцидентного ребра. Тогда, если добавить это ребро, образуется цикл, из которого можно удалить другое ребро, тоже инцидентное \(v\), но имеющее не меньший вес.

            Алгоритм Борувки опирается на этот факт и заключается в следующем:

            1. Для каждой вершины найдем минимальное инцидентное ей ребро.
            2. @@ -285,8 +285,8 @@

              Асимптотика

              Зачем это нужно?

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

              Утверждение. В случае планарных графов алгоритм работает за \(O(n)\).

              -

              Доказательство. Из формулы Эйлера нам извество, что рёбер в планарном графе \(O(n)\). Так как подграф планарного графа тоже всегда планарен, то после каждой итерации размер нашей задачи уменьшается в честные 2 раза — меньше становится не только вершин, но и рёбер тоже. Значит, алгоритм будет работать за \(O(n) + O(\frac{n}{2}) + O(\frac{n}{4}) + \ldots = O(n)\).

              -

              Также, в отличие от алгоритмов Прима и Крускала, его можно лего распараллелить. «Параллельная сложность» у него \(O(\log v)\): нужно каждую итерацию просто искать минимум по оставшимся рёбрам.

              +

              Доказательство. Из формулы Эйлера нам известно, что рёбер в планарном графе \(O(n)\). Так как подграф планарного графа тоже всегда планарен, то после каждой итерации размер нашей задачи уменьшается в честные 2 раза — меньше становится не только вершин, но и рёбер тоже. Значит, алгоритм будет работать за \(O(n) + O(\frac{n}{2}) + O(\frac{n}{4}) + \ldots = O(n)\).

              +

              Также, в отличие от алгоритмов Прима и Крускала, его можно легко распараллелить. «Параллельная сложность» у него \(O(\log v)\): нужно каждую итерацию просто искать минимум по оставшимся рёбрам.

              Полезные свойства и классические задачи

              • Если веса всех рёбер различны, то остов будет уникален.
              • diff --git a/ru/palindromes.html b/ru/palindromes.html index 194cf0f..22784d9 100644 --- a/ru/palindromes.html +++ b/ru/palindromes.html @@ -120,7 +120,7 @@

                Палиндромы

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

                Алгоритм Манакера

                Пусть есть строка \(s\) и мы хотим найти в ней все подпалиндромы.

                -

                Мы сразу сталкиваемся с очевидной тудностью: их в строке может быть \(O(n^2)\), что можно видеть на примере строки \(s = aa \ldots a\). Поэтому будем использовать следующий формат: для каждой позиции \(s_i\) найдём наибольший палиндром, центр которого совпадает с \(s_i\) (чётные и нечётные палиндромы будем рассматривать отдельно). Половину его длины, округлённую вниз, будем называть радиусом.

                +

                Мы сразу сталкиваемся с очевидной трудностью: их в строке может быть \(O(n^2)\), что можно видеть на примере строки \(s = aa \ldots a\). Поэтому будем использовать следующий формат: для каждой позиции \(s_i\) найдём наибольший палиндром, центр которого совпадает с \(s_i\) (чётные и нечётные палиндромы будем рассматривать отдельно). Половину его длины, округлённую вниз, будем называть радиусом.

                Наивное решение — перебрать \(s_i\), а для него вторым циклом находить наибольшую искомую длину:

                vector<int> pal_array(string s) {
                     int n = s.size();
                @@ -179,7 +179,7 @@ 

                Дерево палиндромов

                Дерево палиндромов (англ. palindromic tree, EERTREE) — структура данных, использующая другой, более мощный формат хранения информации обо всех подпалиндромах, чем размеры \(n\) палиндромов. Она была предложена Михаилом Рубинчиком на летних петрозаводских сборах в 2014-м году.

                Лемма. В строке есть не более \(n\) различных подпалиндромов.

                Доказательство. Пусть мы дописываем к строке по одному символу и в данный момент, записав \(r\) символов, имеем наибольший суффикс-палиндром \(s_{l:r}\). Пусть у него, в свою очередь, есть суффикс-палиндром \(s_{l':r} = t\). Тогда он также имеет более раннее вхождение в строку как \(s_{l:l+r-l'} = t\). Таким образом, с каждым новым символом у строки появляется не более одного нового палиндрома, и если таковой есть, то это всегда наибольший суффикс-палиндром.

                -

                Этот факт позволяет сопоставить всем палиндромам строки сопоставить следующую структуру: возьмём от каждого плаиндрома его правую половину (например, \(caba\) для \(abacaba\) или \(ba\) для \(abba\); будем рассматривать пока что только чётные палиндромы) и добавим все эти половины в префиксное дерево — получившуюся структуру и будем называть деревом палиндромов.

                +

                Этот факт позволяет сопоставить всем палиндромам строки сопоставить следующую структуру: возьмём от каждого палиндрома его правую половину (например, \(caba\) для \(abacaba\) или \(ba\) для \(abba\); будем рассматривать пока что только чётные палиндромы) и добавим все эти половины в префиксное дерево — получившуюся структуру и будем называть деревом палиндромов.

                Наивный алгоритм построения будет в худшем случае работать за \(O(n^2)\), но это можно делать и более эффективно.

                Построение за линейное время

                Будем поддерживать наибольший суффикс-палиндром. Когда мы будем дописывать очередной символ \(c\), нужно найти наибольший суффикс этого палиндрома, который может быть дополнен символом \(c\) — это и будет новый наидлиннейший суффикс-палиндром.

                @@ -216,7 +216,7 @@

                Построен }

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

                Асимптотика

                -

                Покажем линейность алгоритма. Рассмотрим длину наибольшего суффикс-палиндрома строки. Каждый новый символ увеличивает её не более, чем на 2. При этом каждый переход по суффиксной ссылке уменьшает её, поэтому нахождение первого суффикс-палиндрома амортизированно работает за линеейное время.

                +

                Покажем линейность алгоритма. Рассмотрим длину наибольшего суффикс-палиндрома строки. Каждый новый символ увеличивает её не более, чем на 2. При этом каждый переход по суффиксной ссылке уменьшает её, поэтому нахождение первого суффикс-палиндрома амортизировано работает за линейное время.

                Аналогичными рассуждениями о длине второго суффикс-палиндрома (его длина увеличивается тоже не более, чем на 2) получаем, что пересчёт суффиксных ссылок при создании новых вершин тоже суммарно работает за линейное время.

                diff --git a/ru/segtree.html b/ru/segtree.html index b50fe87..3369b1b 100644 --- a/ru/segtree.html +++ b/ru/segtree.html @@ -219,7 +219,7 @@

                Реализация

                Посчитать число беспорядков в перестановке из \(n\) элементов (беспорядок или инверсия — это пара чисел \(i < j\), для которых \(p_i > p_j\)).

                -

                Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на \(n\) элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направа. Когда обрабатываем очередное число \(x\), будем делать две вещи:

                +

                Эта задача решается просто, если уметь писать сортировку слиянием вручную. Но мы пойдем по другому пути. Создадим ДО для суммы на \(n\) элементов, изначально заполненное нулями. Теперь будем проходить по этому массиву слева направо. Когда обрабатываем очередное число \(x\), будем делать две вещи:

                • Запросим сумму от \(k\) до \(n\) в ДО.
                • Добавим единичку в \(k\)-тую позицию в ДО.
                • @@ -334,7 +334,7 @@

                  Динамическое }

                  Персистентность

                  Структуры данных называют персистентными, если их можно быстро «откатить» до произвольного предыдущего состояния.

                  -

                  Известны персистентные версии многих структур: стэка, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент \(t\) никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.

                  +

                  Известны персистентные версии многих структур: стека, очереди, СНМ, ДО. В случае со структурами данных на ссылках есть следующий общий подход: во всех методах, меняющих значения в вершинах, будем копировать ссылки на детей перед тем, как в них переходить и что-либо менять. Таким образом, мы всегда будем делать копию вершины перед тем, как что-либо менять в ней самой или её потомках. Вершины в момент \(t\) никогда не будут ссылаться на вершины, измененные после этого, и поэтому ничего не сломается.

                  У персистентных структур есть один минус: они обычно требуют больше памяти. В случае ДО мы будем создавать \(O(\log n)\) новых вершин на запрос, что означает общее потребление памяти \(O(m \log n)\).

                  struct segtree {
                       int lb, rb;
                  @@ -385,7 +385,7 @@ 

                  Персистентность

                  Дан массив из \(n\) элементов. Требуется ответить на \(m\) запросов, есть ли на отрезке \([l, r]\) доминирующий элемент — тот, который встречается на нём хотя бы \(\frac{r-l}{2}\) раз.

      У этой задачи есть удивительно простое решение — взять около 100 случайных элементов и каждый проверить, является ли он доминирующим (это можно проверить за \(O(\log n)\), посчитав для каждого значения отсортированный список позиций, на которых он встречается, и сделав два бинпоиска). Вероятность ошибки в худшем случае равна \(\frac{1}{2^{100}}\), и ей на практике можно пренебречь.

      -

      Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее \(\frac{n}{2}\) в массиве на \(n\) элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возврашаем true, иначе false.

      +

      Но проверять 100 сэмплов — долго. Можно построить такое же ДО, как в прошлой задаче, и решать задачу «найти число, большее \(\frac{n}{2}\) в массиве на \(n\) элементов». Это тоже будет спуском по ДО: каждый раз идём в того сына, где сумма больше. Если в листе, куда мы пришли, значение больше нужного, возвращаем true, иначе false.

      Другие реализации

      Реализация на указателях проста и легко расширяема, но очень медленная и неэффективная по памяти: нужно хранить сами указатели, структура перестаёт помещаться в кэш, нужно много лишних раз ходить по ссылкам в память, только чтобы получить нужные вершины.

      Если динамическое построение и персистентность для решения задачи не нужны, есть альтернативы, которые в несколько раз быстрее:

      diff --git a/ru/sparse-table.html b/ru/sparse-table.html index 87a64c7..9156261 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -165,7 +165,7 @@

      Ограничения н

      Это работает быстрее, чем, например, дерево отрезков, но тоже асимптотически за \(O(\log n)\), да ещё и с дополнительной памятью. Но есть способ это ускорить.

      Disjoint Sparse Table

      Мы хотим иметь какую-то структуру, которая может считать функцию \(f\) на отрезке, при том что \(f\) не удовлетворяет условию идемпотентности. Стандартная разреженная таблица тут не подойдёт — в ней нельзя найти \(O(1)\) непересекающихся отрезков.

      -

      Сделаем сделующее: мысленно построим на массиве дерево отрезков и (уже не мысленно) для каждого его отрезка \([l, r)\) посчитаем \(f\) на всех отрезках от его центрального элемента — то есть от элемента с индексом \(m = \lfloor \frac{l + r}{2} \rfloor\) — до всех остальных элементов \(k \in [l, r)\). Для каждого элемента массива будет \(O(\log n)\) центральных, а значит суммарно на это потребуются те же \(O(n \log n)\) времени и памяти.

      +

      Сделаем следующее: мысленно построим на массиве дерево отрезков и (уже не мысленно) для каждого его отрезка \([l, r)\) посчитаем \(f\) на всех отрезках от его центрального элемента — то есть от элемента с индексом \(m = \lfloor \frac{l + r}{2} \rfloor\) — до всех остальных элементов \(k \in [l, r)\). Для каждого элемента массива будет \(O(\log n)\) центральных, а значит суммарно на это потребуются те же \(O(n \log n)\) времени и памяти.

      Утверждение. Любой запрос \([l, r)\) разбивается на \(O(1)\) непересекающихся преподсчитанных интервалов.

      Доказательство. Возьмем самый высокий центральный элемент \(m\), принадлежащий запросу. Его отрезок полностью покрыает запрос — если бы это было не так, то самым высоким был бы не \(m\), а какая-то из его границ . Раз отрезок запроса \([l, r)\) полностью покрыт, и \(m\) лежит внутри него, то \([l, r)\) можно разбить на предпосчитанные \([l, m)\) и \([m, r)\).

      Решать задачу мы так и будем: найдём нужный центральный элемент и сделаем два запроса от него.

      diff --git a/ru/sqrt.html b/ru/sqrt.html index 8e868d4..2aa54f6 100644 --- a/ru/sqrt.html +++ b/ru/sqrt.html @@ -219,7 +219,7 @@

      \(\sqrt q\), проанализировав историю текущего блока.

      +

      Разобьём запросы на блоки явно, и будем обрабатывать их по отдельности. На каждом блоке построим выпуклую оболочку только тех точек, которые существуют на всём её блоке, а остальные запросы сохраним в буфер. При ответе на запрос касательной помимо кандидата из построенной оболочки будем рассматривать все точки, которые существуют на момент данного запроса — найти все такие точки можно за \(\sqrt q\), проанализировав историю текущего блока.

      Часто, если удалений в задаче нет или их можно как-то эмулировать, можно применить похожую, но более мощную технику: поддерживать \(O(\log n)\) структур (например, выпуклых оболочек) размеров степени двойки, причём так, что нет двух структур одинакового размера. При добавлении новой точки мы создаем для неё новую структуру размера 1. Дальше, если структура размера 1 уже существует, то мы объединяем эти две структуры и создаём структуру размера 2. Если структура размера 2 уже существовала, то мы объединяем её и создаём структуру размера 4, и так далее. Запрос мы будем передавать во все \(O(\log n)\) базовых структур и объединять ответы.

      «Алгоритм Мо»

      Обсудим еще одно решение этой же задачи. На этот раз будем группировать запросы и по временным, и по пространственным признакам.

      @@ -260,7 +260,7 @@

      «Алгоритм Мо»

      }

      Конкретно для этой задачи этот алгоритм не нужен, однако он полезен для ответа на более сложные вопросы: найти \(k\)-ую порядковую статистику на отрезке, его медиану, количество различных элементов, наиболее часто встречающийся элемент и так далее. Во всех этих случаях нужно просто вместо суммы поддерживать какую-нибудь структуру — например, хэш-таблицу — отвечающую за множество элементов на отрезке, и пересоздавать её между блоками.

      Деление на тяжелые и легкие объекты

      -

      Всем известный алгортим факторизации за корень операется на тот факт, что каждому «большому» делителю \(d \geq \sqrt n\) числа \(n\) соответствует какой-то «маленький» делитель \(\frac{n}{d} \leq n\).

      +

      Всем известный алгоритм факторизации за корень операется на тот факт, что каждому «большому» делителю \(d \geq \sqrt n\) числа \(n\) соответствует какой-то «маленький» делитель \(\frac{n}{d} \leq n\).

      Подобное полезное свойство (что маленькие объекты маленькие, а больших объектов не много) можно найти и у других объектов.

      710F. Операции над множеством строк. Требутся в онлайне обрабатывать три типа операций над множеством строк:

        @@ -282,7 +282,7 @@

        Де

        Будем называть вершину тяжелой, если она соединена с более чем \(\sqrt n\) другими вершинами, и лёгкой в противном случае.

        Попытаемся оценить количество соединённых вместе троек вершин, рассмотрев все возможные 4 варианта:

          -
        1. В цикле нет тяжелых вершин. Рассмотрим какое-нибудь ребро \((a, b)\) цикла. Третяя вершина \(c\) должна лежать в объединении списков смежности \(g_a\) и \(g_b\), а раз обе эти вершины лёгкие, то таких вершин найдётся не более \(\sqrt n\). Значит, всего циклов этого типа может быть не более \(O(m \sqrt n)\).

        2. +
        3. В цикле нет тяжелых вершин. Рассмотрим какое-нибудь ребро \((a, b)\) цикла. Третья вершина \(c\) должна лежать в объединении списков смежности \(g_a\) и \(g_b\), а раз обе эти вершины лёгкие, то таких вершин найдётся не более \(\sqrt n\). Значит, всего циклов этого типа может быть не более \(O(m \sqrt n)\).

        4. В цикле одна тяжелая вершина. Аналогично — есть одно «лёгкое» ребро, а значит таких циклов тоже \(O(m \sqrt n)\).

        5. В цикле две тяжелые вершины — обозначим их как \(a\) и \(b\), а лёгкую как \(c\). Зафиксируем пару \((a, c)\) — способов это сделать \(O(m)\), потому что всего столько рёбер. Для этого ребра будет не более \(O(\sqrt n)\) рёбер \((a, b)\), потому что столько всего тяжелых вершин. Получается, что всего таких циклов может быть не более \(O(m \sqrt n)\).

        6. Все вершины тяжелые. Аналогично — тип третьей вершины в разборе предыдущего случая нигде не использовался; важно лишь то, что тяжелых вершин \(b\) немного.

        7. @@ -335,8 +335,8 @@

          Де

          Бакеты

          Пусть нам нужно находить всё ту же сумму, но теперь у нас запрос обновления выглядит так: \(i\)-тый элемент прыгает на \(d\) позиций вправо или влево, смещая, соответственно, \(d\) элементов между начальной и конечной позицией на единицу вправо или влево. Сумму нужно находить уже в новой индексации. Про декартово дерево автор снова просит читателя на время забыть.

          Объединим предыдущие подходы: теперь не только будем делить массив на блоки примерно по корню, но и каждый корень запросов будем всё перестраивать.

          -

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

          - +

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

          +

          Подбор константы

          На скорость работы очень сильно влияет размер блока. Мы для простоты использовали одну и ту же константу и для количества блоков, и для их размера, но на практике их часто нужно подбирать.

          diff --git a/ru/string-searching.html b/ru/string-searching.html index 5050019..ddcf7b0 100644 --- a/ru/string-searching.html +++ b/ru/string-searching.html @@ -128,7 +128,7 @@

          Префикс-функция

          p[i] = len; return p; } -

          Этот алгоритм пока что работает за \(O(n^3)\), но позже мы его ускорми.

          +

          Этот алгоритм пока что работает за \(O(n^3)\), но позже мы его ускорим.

          Как это поможет решить исходную задачу?

          Давайте пока поверим, что мы умеем считать префикс-функцию за линейное от размера строки, и научимся с помощью нее искать подстроку в строке.

          Соединим подстроки \(s\) и \(t\) каким-нибудь символом, который не встречается ни там, ни там — обозначим пусть этот символ #. Посмотрим на префикс-функцию получившейся строки \(s\#t\).

          diff --git a/ru/suffix-array.html b/ru/suffix-array.html index 88013ed..db9ac44 100644 --- a/ru/suffix-array.html +++ b/ru/suffix-array.html @@ -137,7 +137,7 @@

          Построение за Следующие этапы нужно проводить, используя информацию с предыдущих. Можем снова создать перестановку и применить к ней std::sort со своим компаратором.

          Как быстро сравнить две подстроки? Мы можем использовать \(c_i\) — каждой строке длины \(2^k\) сопоставить биграмму (строку из двух символов), а именно строка \(s[i..i+2^k-1]\) с точки зрения сортировки будет эквивалентна паре \((c_i, c_{i+2^{k-1}})\). Соответственно, можно просто написать компаратор, который смотрит только на эти пары, и работает за \(O(1)\). Однако, это всё ещё будет работать за \(O(n \log^2 n)\), потому что каждый этап будет работать за \(O(n \log n\)).

          Примечание. Зачастую этот способ построения работает быстро, поскольку имеет небольшую константу.

          -

          Оптимизация до \(O(n \log n)\). Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортиремые значения — в нашем случае это не так. Воспользуемся цифровой сортировкой — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив \(p\) с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в \(p\) отнять \(2^{k-1}\). Таким образом, можно проводить этап за \(O(n)\).

          +

          Оптимизация до \(O(n \log n)\). Сортировка слиянием оптимальна только тогда, когда мы ничего не знаем про сортируемые значения — в нашем случае это не так. Воспользуемся цифровой сортировкой — сначала отсортируем биграммы по второму символу, а потом по первому. Все вторые элементы уже упорядочены (массив \(p\) с предыдущего этапа). Теперь, чтобы упорядочить их по первому, нам надо просто от каждого элемента в \(p\) отнять \(2^{k-1}\). Таким образом, можно проводить этап за \(O(n)\).

          // строка -- это последовательность чисел от 1 до размера алфавита
           vector<int> suffix_array (vector<int> &s) {
               s.push_back(0);  // добавляем нулевой символ в конец строки
          diff --git a/ru/treap.html b/ru/treap.html
          index 2332823..ce8940f 100644
          --- a/ru/treap.html
          +++ b/ru/treap.html
          @@ -113,7 +113,7 @@ 

          Декартово дерево

          Рене Декарт не является создателем декартова дерева, но он является создателем декартовой системы координат, которую мы все знаем и любим.

          Декартово дерево же определяется и строится так:

            -
          • Нанесём на плоскость набор из \(n\) точек. Их \(x\) зачем-то назовем ключем, а \(y\) приоритетом.
          • +
          • Нанесём на плоскость набор из \(n\) точек. Их \(x\) зачем-то назовем ключом, а \(y\) приоритетом.
          • Выберем самую верхнюю точку (с наибольшим \(y\), а если таких несколько — любую) и назовём её корнем.
          • От всех вершин, лежащих слева (с меньшим \(x\)) от корня, рекурсивно запустим этот же процесс. Если слева была хоть одна вершина, то присоединим корень левой части в качестве левого сына текущего корня.
          • Аналогично, запустимся от правой части и добавим корню правого сына.
          • @@ -313,7 +313,7 @@

            Пример: ctrl+x, ctrl+v

            }

          Пример: переворот

          Нужно за \(O(\log n)\) обрабатывать запросы переворота произвольных подстрок: значение \(a_l\) поменять с \(a_r\), \(a_{l+1}\) поменять с \(a_{r-1}\) и т. д.

          -

          Будем хранить в кажой вершине флаг, который будет означать, что её подотрезок перевернут:

          +

          Будем хранить в каждой вершине флаг, который будет означать, что её подотрезок перевернут:

          struct Node {
               bool rev;
               // ...
          
          From af06f7291b8f5d03e1f5a657dece87bedbedab3d Mon Sep 17 00:00:00 2001
          From: "Deployment Bot (from Travis CI)" 
          Date: Tue, 23 Jul 2019 20:50:29 +0000
          Subject: [PATCH 145/230] Deploy algorithmica.org to
           github.com/algorithmica-org/articles.git:gh-pages
          
          ---
           ru/sse.html | 266 ++++++++++++++++++++++++++++------------------------
           1 file changed, 145 insertions(+), 121 deletions(-)
          
          diff --git a/ru/sse.html b/ru/sse.html
          index 6a0283f..1678799 100644
          --- a/ru/sse.html
          +++ b/ru/sse.html
          @@ -208,7 +208,7 @@ 

          C++ intrinsics

        Например, вот так можно считать сумму на массиве произвольного размера:

        int sum(int a[], int n) {
        -    // создадим регистр, в котором будем хранить 8 текущих сумм
        +    // создадим регистр, в котором будем хранить 8 текущих сумм
             __m256i x = _mm256_setzero_si256();
             for (int i = 0; i + 8 <= n; i += 8) {
                 __m256i y = _mm256_loadu_si256((__m256i*) &a[i]);
        @@ -237,159 +237,183 @@ 

        C++ intrinsics

        Комбинаторно получается огромное количество различных функций. Полная документация по ним — Intel Intrinsics Guide — лежит в закладках браузера у каждого уважающего себя performance engineer-а.

        Выравнивание. Отдельно стоит отметить одну делать: операции чтения и записи имеют по две версии — load / loadu и store / storeu. Буква «u» здесь означает «unaligned» (англ. невыровненный). Первые корректно работают только тогда, когда весь считываемый блок помещается на одну кэш-линию (если это не так, то в рантаеме вызвется segfault), в то время как unaligned версия работает всегда и везде.

        -

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

        -

        Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

        -
        alignas(32) int a;
        -// указатель на начало массива теперь будет кратен 32 байтам,
        -// то есть размеру sse-блока; теперь любое чтение будет внутри кэш-линии
        -
        -for (int i = 0; i < n; i += 8) {
        -    __m256i x = _mm256_load_epi32(&a[i]);
        -    // ...
        +

        Иногда, особенно когда операция «лёгкая», это отличие имеет большое значение — если не получается «выровнять» память, то производительность может резко упасть (хотя бы потому, что нужно загружать две кэш-линии вместо одной).

        +

        Например, так складывать два массива:

        + +

        …будет на 30% медленнее, чем так:

        + +

        Если предположить, что в первом варианте начало массива совпадает с началом кэш-линии, а её размер 64 байта, то примерно половина loadu и storeu будут «плохими».

        +

        Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

        +

        Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

        Для каждой размерности регистра есть 3 типа данных. На примере AVX: __m256 для float, __m256d для double и __m256i для разных int-ов.

        Некоторые операции есть только для какого-то одного типа (например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов), однако будут работать с другими типами абсолютно так же. Поэтому, to make compiler happy, нужно применять к ним преобразования типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

        Loop unrolling. Добавление флага unroll-loops (#pragma GCC optimize("unroll-loops")) позволяет компилятору делать «размотку» циклов, то есть преобразовывать код вида:

        - +

        во что-то такое:

        - +

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

        Нетривиальный пример

        Пусть нам зачем-то понадобилось возвести \(10^8\) чисел в какие-то степени.

        Тестировать и сравнивать решения будем таким кодом:

        - +

        В SSE весьма сложно делить int-ы (см. примечания ниже), поэтому будем считать всё по модулю \(2^{32}\), то есть просто переполняя естественным образом unsigned int.

        Напишем стандартное итеративное бинарное возведение в степень:

        - +

        Этот код работает за 9.47 секунды.

        Теперь попробуем векторизованную версию:

        - +

        Эта реализация уже работает за 0.7 секунды — в 13.5 раз быстрее. При этом там и дальше есть, что оптимизировать.

        Трудности автовекторизации

        В самом начале статьи мы приводили пример кода, в котором уже оптимизированный бинарник получается без каких-либо изменений, кроме подключения нужного таргета компиляции. Зачем тогда вообще программисту делать что-либо ещё?

        Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

        Рассмотрим этот же пример, убрав из него всё лишнее:

        - +

        Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

        Во-первых, потому что это не всегда корректно. Предположим, что a[] и c[] пересекаются, причём так, что указатели на начало массивов отличаются на 1-2 позиции. Ну, мало ли — может, мы такой изощрённой свёрткой хотели посчитать последовательность Фибоначчи. Тогда в simd-блоках данные будут пересекаться, и наблюдаемое поведение будет совсем не то, которое мы хотели.

        -

        Во-вторых, мы ничего не знаем про выравнивание этих массивов, и можем потерять производительность здесь (компилятор скорее всего сгенерирует инструкции с loadu).

        -

        На самом деле, когда компилятор подозревает, что функция будет использована для достаточно больших циклов, то на высоких уровнях оптимизации он сам вставит runtime-проверки на эти случаи и сгенерирует два разных варианта: через SSE и «безопасный».

        -

        Существуют различные способы намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или вызываются какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

        +

        Во-вторых, мы ничего не знаем про выравнивание этих массивов, и можем потерять производительность здесь (для больших циклов неактуально — компилятор оба «края» обрабатывает отдельными циклами).

        +

        На самом деле, когда компилятор подозревает, что функция будет использована для больших циклов, то на высоких уровнях оптимизации он сам вставит runtime-проверки на эти случаи и сгенерирует два разных варианта: через SSE и «безопасный».

        +

        Но выполнять эти проверки в рантайме не хочется, поэтому можно сообщить компилятору, что мы уверены, что ничего не сломается:

        + +

        Здесь «ivdep» означает ignore vector dependencies — данные внутри цикла ни от чего не зависят.

        +

        Существует много других способов намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или вызываются какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

        Разное

        С++ в ассемблер. Посмотреть на генерируемые инструкции можно так:

        - +

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

        Если указать флаг -fopt-info-vec-optimized, то компилятор прямо укажет на операции, которые он смог векторизовать:

        - +

        Можно поменять optimized на missed или all, чтобы посмотреть причины, почему не получилось векторизовать другие.

        Распечатать вектор. Для дебага помогает такой код:

        - +

        В данном случае он выводит 4 группы по 32 бита из 128-битного вектора.

        Деление. В SSE нет операции деления int-ов, но есть для float-ов и производных. Также нет взятия остатка от деления, что осложняет вычисления в комбинаторике.

        Для деления 32-битных целых чисел их можно аккуратно скастовать к даблу, поделить так, и скастовать обратно — точности хватит, хоть это и будет медленно.

        Умножение работает в несколько раз быстрее деления, и поэтому для ускорения деления float-ов на известную константу \(d\) есть следующий трюк: заменить выражение \(x / d\) на \(x \cdot \frac{1}{d}\), и при этом \(\frac{1}{d}\) посчитать во время компиляции.

        Для целочисленных типов такое сделать немного сложнее — нужно заменить деление на умножение и битовый сдвиг. Для этого нужно приблизить \(\frac{1}{d} \approx \frac{m}{2^s}\), подобрав «магическое» число \(m\) и степень двойки \(s\), такие что что x / d == (x * m) >> s для всех x.

        Можно показать, что такая пара чисел всегда существует, и компилятор сам оптимизирует деление на константу подобным образом. Вот, например, сгенерированные инструкции для деления unsigned long long на \(10^9 + 7\):

        - +

        Здесь для умножения используется «mixed precision» инструкция mulq, которая берёт два 64-битных числа и записывает 128-битный результат их умножения в два 64-битных регистра (lo, hi).

        Для деления long-ов на SSE такой способ пока что не работает: аналогичная инструкция добавилась только в AVX512.

        From 5a51e25c4a5c74ae28138ace030e03f78e08d469 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 23 Jul 2019 21:04:20 +0000 Subject: [PATCH 146/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index 1678799..a7d76bf 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -259,20 +259,19 @@

        C++ intrinsics

        Если предположить, что в первом варианте начало массива совпадает с началом кэш-линии, а её размер 64 байта, то примерно половина loadu и storeu будут «плохими».

        Вручную «выровнять» память для последовательного чтения через load в случае с массивами можно так:

        + +for (int i = 0; i < n; i += 8) { + __m256 x = _mm256_load_ps(&a[i]); + // ... +}
        +

        Указатель на начало массива теперь будет кратен 32 байтам, то есть размеру sse-блока. Теперь любое чтение и запись гарантированно будет внутри кэш-линии.

        Типизация. Вообще, загружать и сохранять данные интринзиками и вообще использовать __m-типы на самом деле не обязательно — всё можно сделать и обычным reinterpret_cast-ом. Все данные хранятся в одном и том же формате, и разные типы нужны просто для проверки типов и избежания связанных ошибок.

        Для каждой размерности регистра есть 3 типа данных. На примере AVX: __m256 для float, __m256d для double и __m256i для разных int-ов.

        Некоторые операции есть только для какого-то одного типа (например, тот же _mm256_blendv_ps не имеет аналога для 32-битных int-ов), однако будут работать с другими типами абсолютно так же. Поэтому, to make compiler happy, нужно применять к ним преобразования типов, которые не будут стоить дополнительных инструкций в рантайме. Они все имеют такой формат: _mm<размерность>_cast<откуда>_<куда>.

        Loop unrolling. Добавление флага unroll-loops (#pragma GCC optimize("unroll-loops")) позволяет компилятору делать «размотку» циклов, то есть преобразовывать код вида:

        -

        во что-то такое:

        +

        …во что-то такое:

        int i;
         for (i = 1; i < n - 3; i += 4) {
             a[i] = (i % b[i]);
        @@ -356,7 +355,7 @@ 

        Нетривиальный пр // это либо единица, либо a, в зависимости от значения нижнего бита p // маски элементов, которые нужно домножать на a: reg mask = _mm256_cmpeq_epi32(_mm256_and_si256(p, ones), ones); - // эта операция смешивает два вектора по маске: + // смешаем два вектора по маске: reg mul = _mm256_blendv_epi8(ones, a, mask); // res *= mul: res = _mm256_mullo_epi32(res, mul); @@ -375,7 +374,7 @@

        Трудности

        Дело в том, что иногда — очень редко — программист всё-таки умнее компилятора, потому что знает про задачу чуть больше.

        Рассмотрим этот же пример, убрав из него всё лишнее:

        Почему эту функцию нельзя заменить на векторизованный-вариант автоматически?

        @@ -385,7 +384,7 @@

        Трудности

        Но выполнять эти проверки в рантайме не хочется, поэтому можно сообщить компилятору, что мы уверены, что ничего не сломается:

        + // ...

        Здесь «ivdep» означает ignore vector dependencies — данные внутри цикла ни от чего не зависят.

        Существует много других способов намекнуть компилятору, что конкретно мы имели в виду, но в сложных случаях — когда внутри цикла используются if-ы или вызываются какие-нибудь внешние функции — проще спуститься до уровня интринзиков и написать всё самому.

        Разное

        From cb94e205ddc98533df9916228083b84f48e67666 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 23 Jul 2019 21:33:25 +0000 Subject: [PATCH 147/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sse.html | 55 ++++++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/ru/sse.html b/ru/sse.html index a7d76bf..de2a5d2 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -131,11 +131,11 @@

        Векторизация

        Добавим теперь следующую магическую директиву в самое начало программы:

        -

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

        +

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

        Чтобы понять, что здесь происходит, нам нужно сначала разъяснить некоторые особенности работы современных компьютеров. (Знающие ассемблер могут отмотать примерно до ⅓ статьи.)

        Complex Instruction Set Computing

        Раньше, во времена, когда компьютеры назывались ЭВМ-ами и занимали целую комнату, увеличение производительности происходило в основном за счёт увеличения тактовой частоты. Тактовая частота условно равна количеству инструкций, выполняемому процессором за единицу времени. (На современных процессорах это не так — разные инструкции занимают разное время, которое ещё и может зависеть от разных обстоятельств.)

        -

        Помимо жесткого физического ограничения на максимально возможную тактовую частоту, такой такой подход в какой-то момент просто перестал быть экономически оправданным: прямое увеличение тактовой частоты приводит к в более чем линейному потреблению энергии, и, следовательно, выделению тепла, которое к тому же нужно ещё как-то выводить.

        +

        Помимо жесткого физического ограничения на максимально возможную тактовую частоту, такой подход в какой-то момент просто перестал быть экономически оправданным: прямое увеличение тактовой частоты приводит к в более чем линейному потреблению энергии, и, следовательно, выделению тепла, которое к тому же нужно ещё как-то выводить.

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

        • RISC (англ. reduced instruction set computer), в которых длина кода (идентификатора) самой инструкции ограничена, а значит ограничено и само количество инструкций. Самые первые компьютеры относились к этому типу и могли не иметь даже отдельных инструкций для умножения и деления. Такие процессоры требует меньше транзисторов, и как следствие сами меньше, дешевле и потребляют меньше энергии. Самое популярное семейство архитектур называется arm и используется почти на всех современных мобильных устройствах.

        • @@ -152,11 +152,11 @@

          Complex Instruction Set Computing

          Streaming SIMD Extensions

        SSE — это обобщённое название всех SIMD-инструкций для x86.

        Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно логически разбивают эту булеву последовательность на блоки, например, по 32 бит, и работают уже с ними.

        +

        Подобным способом довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

        Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует).

        Очень часто SSE используют для работы с действительными числами, и в этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходных данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

        -

        -

        Конкретный набор инструкций и размеры регистров зависит от вендора и поколения архитектуры. На данный момент (лето 2019 года) большинство процессоров архитектуры x86 производит Intel, поэтому мы сконцентрируемся именно на их наборе инструкций.

        +

        Конкретный набор инструкций и размеры регистров зависят от вендора и поколения архитектуры. На данный момент (лето 2019 года) большинство процессоров архитектуры x86 производит Intel, поэтому мы сконцентрируемся именно на их наборе инструкций.

        Поддержка SIMD-инструкций добавлялись постепенно, сохраняя обратную совместимость. Если третий пентиум в 1999-м году умел работать с регистрами размера 128, то в самых современных i7 есть 512-битные регистры. Автор не является специалистом в проектировании микропроцессоров, но предполагает, то регистры больше 64 байт (512 бит) появятся не скоро, потому что это уже больше размера кэш-линии

        Чтобы разработчикам не нужно было предоставлять отдельные оптимизированные бинарники под каждую конкретную архитектуру, информация о поддержке наборов инструкций процессором зашита в ассемблерную инструкцию cpuid, которую можно просто вызвать в рантайме и всё узнать: например так.

        @@ -201,32 +201,35 @@

        C++ intrinsics

        // запишем содержимое регистра в память: _mm256_storeu_pd(&c[i], z); }
        -

        Когда размер массива не кратен размеру регистра, нужно поступить одним из двух способов:

        +

        Если размер массива не кратен размеру регистра, то программа может отработать некорректно. В этом случае можно поступить одним из двух способов:

        1. Добавить в конец массива «нейтральные» элементы, дополнив его до удобной длины.

        2. Обработать через SSE столько элементов, сколько получается, а оставшиеся обработать отдельно.

        Например, вот так можно считать сумму на массиве произвольного размера:

        -

        Большинство команд кодируются как _mm<размерность>_<действие>_<тип>. Например:

        +    int res = 0; +     + // создадим регистр, в котором будем хранить 8 текущих сумм + __m256i x = _mm256_setzero_si256(); + for (int i = 0; i + 8 <= n; i += 8) { + __m256i y = _mm256_loadu_si256((__m256i*) &a[i]); + x = _mm256_add_epi32(x, y); + } + + // сложим все 8 чисел в регистре в одно + int *b = (int*) &x; + for (int i = 0; i < 8; i++) + res += b[i]; + + // добавим остаток массива + for (int i = (n / 8) * 8; i < n; i++) + res += a[i]; + + return res; +} +

        Имена команд. Большинство команд кодируются как _mm<размерность>_<действие>_<тип>.

        +

        Несколько других примеров:


        diff --git a/ru/links.html b/ru/links.html new file mode 100644 index 0000000..6b909fc --- /dev/null +++ b/ru/links.html @@ -0,0 +1,123 @@ + + + + + + + Ссылки - Алгоритмика + + + + + + + + + + + + + + +

        Ссылки

        +

        Русскоязычные ресурсы:

        + +

        Англоязычные ресурсы:

        + +

        Платформы для соревнований:

        +
          +
        • CodeForces
        • +
        • AtCoder
        • +
        • TopCoder
        • +
        • HackerRank
        • +
        • CodeChef
        • +
        +

        Архивы задач:

        +
          +
        • Timus
        • +
        • Informatics
        • +
        +

        Студенческие сборы:

        +
          +
        • Петрозаводские сборы
        • +
        • Moscow Workshops ICPC
        • +
        +

        Олимпиадные школы:

        +
          +
        • Летняя Компьютерная Школа
        • +
        • Летняя Олимпиадная Школа (МФТИ)
        • +
        • Школа Олимпиадной Подготовки (Иннополис)
        • +
        • Летняя школа по компьютерным наукам (ВШЭ)
        • +
        • Зимняя Компьютерная Школа (МФТИ)
        • +
        +

        Кружки для школьников в Москве:

        +
          +
        • Tinkoff Generation
        • +
        • Московская школа программистов
        • +
        • Яндекс.Лицей (8-9 класс, Python)
        • +
        • Школа Летово (5-8 класс)
        • +
        • Кружки в ЦПМ
        • +
        • 1С: Клуб Программистов
        • +
        • Комбалг
        • +
        +

        Школьные олимпиады:

        +
          +
        • Всероссийская олимпиада школьников по программированию
        • +
        • Открытая олимпиада школьников по программированию
        • +
        • Московская олимпиада школьников
        • +
        • Олимпиада Университета Иннополис
        • +
        • Технокубок
        • +
        +

        Студенческие олимпиады:

        +
          +
        • International Collegiate Programming Contest
        • +
        +

        Документы:

        +
          +
        • IOI Syllabus
        • +
        • Перечень олимпиад РСОШ
        • +
        +

        Паблики с мемами:

        + + + From 694e34f1c1a0ad3c6f30c1f41f4848eceebec8c8 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 1 Aug 2019 16:49:43 +0000 Subject: [PATCH 161/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/link_cut.html | 258 ++++++++++++++++++++++++++++++++++++++++++++ ru/probability.html | 3 + 2 files changed, 261 insertions(+) create mode 100644 ru/link_cut.html diff --git a/ru/link_cut.html b/ru/link_cut.html new file mode 100644 index 0000000..3ac1008 --- /dev/null +++ b/ru/link_cut.html @@ -0,0 +1,258 @@ + + + + + + + Splay-дерево - Алгоритмика + + + + + + + + + + + + + + + + +

        Splay-дерево

        +

        Splay-дерево (дерево Тарьяна-Слейтора) - двоичное дерево поиска, созданное Робертом Тарьяном и Даниелем Слейтером специально для ускорения работы другой структуры данных, которую мы рассмотрим позже. Splay-дерево позволяет быстро получать доступ к недавно использованным данным (вершинам) засчёт их “поднятия” к корню и имеет амортизированное время работы \(\hat{O}(n\,log\,n)\).

        +

        Свойства

        +

        Основная операция в splay-дереве - это операция \(expose(v)\), выполняющая балансировку дерева. Она при помощи серии операций \(rotate\) делает вершину \(v\) корнем дерева. Поворот вокруг ребра \((v, u)\) (где \(u\) - предок \(v\); будем обозначать эту операцию за \(rotate(v)\)) “поднимает” \(v\) на уровень вверх, при этом “опуская” \(u\) и не нарушая свойств двоичного дерева поиска. По сути, splay-дерево - это детерминированное декартово дерево с амортизированным временем работы (из-за чего нельзя эффективно сделать splay-дерево персистентным). Базовая функция для работы splay-дерева - \(find(x)\) - находит вершину \(u\) с максимальным ключом, не большим \(x\). Реализуется \(find(x)\) так же, как и в других деревьях поиска - обычным спуском, но после обязательно нужно выполнить \(expose(u)\). Через \(find(x)\) реализуются операции \(split\) и \(merge\): + \(split(x, k)\) - так же, как и в декартовом дереве, принимает исходное дерево \(x\) и возвращает два дерева \(l\) и \(r\), причём все ключи в \(l\) меньше \(k\), а все ключи в \(r\) - не меньше \(k\). Выполним операцию \(find(k)\) в дереве \(x\), после которой найденная вершина \(u\) окажется корнем \(x\); заметим, что тогда ключи всех вершин в поддереве левого сына \(u\) (обозначим его за \(l\)) будут меньше \(k\), то есть мы можем “отрезать” левое поддерево от \(u\) и вернуть пару \({l, u}\). + \(merge(l, r)\) - принимает два дерева \(l\) и \(r\) (причём все ключи в \(l\) меньше всех ключей в \(r\)) и возвращает новое дерево \(x\), состоящее из всех вершин \(l\) и \(r\). При помощи операции \(find\) найдём в \(r\) вершину с минимальным ключом, после чего она станет корнем \(r\); заметим, что у неё не будет левого сына, тогда мы просто сделаем этим левым сыном всё дерево \(l\). Остальные операции реализуются через эти три (например, \(add(x)\), \(remove(x)\) и т.д).

        +

        Реализация \(expose(v)\)

        +

        Если реализовать \(expose(v)\) простой серией поворотов \(rotate(v)\) (пока \(v\) не станет корнем), то можно подобрать пример, когда она всегда будет отрабатывать за \(O(n)\) (например, если дерево - это один путь, подвешенный за конец, и \(find\) каждый раз затрагивает другой конец пути). Для достижения асимптотики \(\hat{O}(n\,log\,n)\) при реализации \(expose(v)\) используются три вспомогательные операции - zig, zig-zig и zig-zag - которые являются комбинациями операций \(rotate\).

        +

        \(zig-zig(v)\)

        +

        Обозначим за \(p(x)\) предка вершины \(x\), а за \(L(x)\) - функцию, возвращающую 0, если \(x\) - левый сын \(p(x)\) или \(p(x)\) не существует, и 1, если \(x\) - правый сын \(p(x)\). Операция \(zig-zig(v)\) применяется в том случае, если \(v\) не является непосредственным сыном корня, и \(L(x)=L(p(x))\). \(zig-zig(v)\) состоит из двух операций - \(rotate(v)\) и \(rotate(w)\) (где \(w\) - предок \(v\) до выполнения \(zig-zig(v)\)) - которые выполняются именно в таком порядке.

        +

        \(zig-zag(v)\)

        +

        Операция \(zig-zag(v)\) применяется в том случае, если \(v\) не является непосредственным сыном корня, и \(L(x)\neq L(p(x))\). \(zig-zig(v)\) состоит из двух операций - \(rotate(w)\) и \(rotate(v)\) (где \(w\) - предок \(v\) до выполнения \(zig-zag(v)\)) - которые выполняются именно в таком порядке.

        +

        \(zig(v)\)

        +

        Операция \(zig(v)\) применяется в том случае, если \(v\) является непосредственным сыном корня, и представляет собой одну операцию \(rotate(v)\).

        +

        Итоговый алгоритм

        +

        Таким образом, для реализации \(expose(v)\) мы просто в цикле до тех пор, пока \(v\) не станет корнем, рассматриваем три случая: 1. \(v\) - непосредственный сын корня. Тогда выполняем \(zig(v)\). 2. \(L(v) = L(p(v))\) - тогда выполняем \(zig-zig(v)\). 3. \(L(v) \neq L(p(v)\) - тогда выполняем \(zig-zag(v)\).

        +

        Неявное splay-дерево

        +

        Давайте считать, что splay-дерево хранит некоторый массив, причём все элементы массива, за которые отвечают вершины в левом поддереве \(x\), находятся левее элемента \(x\), а все элементы в правом поддереве - правее \(x\) - как в декартовом дереве по неявному ключу. Тогда \(find(k)\) будет искать в дереве вершину, отвечающую за \(k\)-й слева элемент этого массива. Таким образом, неявное splay-дерево решает те же задачи, что и неявное декартово дерево. На практике splay-дерево обычно оказывается в несколько раз быстрее, чем декартово дерево, из-за малой константы (см. время работы), поэтому если не получается упихать декартово дерево, есть много времени и опыт написания splay-дерева, то можно попытаться использовать его (однако, опять же, персистентность прикрутить не получится).

        +

        Реализация

        +
        struct node {
        +    int x, sz = 1;
        +    int p = -1, l = -1, r = -1;
        +    bool lft = 0;
        +
        +    node() {}
        +
        +    node(int x) : x(x) {}
        +};
        +
        +node v[maxn];
        +int root = -1, mx = 0;
        +
        +int gsz(int x) {
        +    return (x == -1 ? 0 : v[x].sz);
        +}
        +
        +void upd(int x) {
        +    v[x].sz = 1 + gsz(v[x].l) + gsz(v[x].r);
        +}
        +
        +// Отсоединяет вершину от предка, обновляя необходимые параметры
        +
        +void disconnect(int x) {
        +    if (x == -1 || v[x].p == -1) return;
        +    if (v[v[x].p].l == x) v[v[x].p].l = -1;
        +    else v[v[x].p].r = -1;
        +    upd(v[x].p); v[x].p = -1; v[x].lft = 0;
        +}
        +
        +// Делает вершину x левым или правым (в зависимости от lft) сыном y
        +
        +void connect(int x, int y, bool lft) {
        +    if (x == -1 || y == -1) return;
        +    v[x].lft = lft; v[x].p = y;
        +    if (lft) v[y].l = x;
        +    else v[y].r = x;
        +    upd(y);
        +}
        +
        +void rotate(int x) {
        +    int y = v[x].p, z = v[y].p, yl = v[y].lft, xl = v[x].lft;
        +    disconnect(x); disconnect(y);
        +    if (xl) {
        +        int b = v[x].r;
        +        disconnect(b); connect(b, y, 1); connect(y, x, 0);
        +    } else {
        +        int b = v[x].l;
        +        disconnect(b); connect(b, y, 0); connect(y, x, 1);
        +    }
        +    connect(x, z, yl);
        +}
        +
        +// Если вместо expose написать splay, то больше людей поймут, что вы написали splay-дерево, посмотрев код посылки на кф 
        +
        +void splay(int x) {
        +    if (x == -1) return;
        +    while (v[x].p != -1) {
        +        int y = v[x].p;
        +        if (v[y].p == -1) {
        +            rotate(x);
        +            break;
        +        }
        +        if (v[x].lft == v[y].lft) {
        +            rotate(y);
        +            rotate(x);
        +        } else {
        +            rotate(x);
        +            rotate(x);
        +        }
        +    }
        +    root = x;
        +}
        +
        +// Функция find, которую я зачем-то назвал get
        +
        +int get(int x, int k) {
        +    if (x == -1) return -1;
        +    while (1) {
        +        if (gsz(v[x].l) == k)
        +            break;
        +        if (k < gsz(v[x].l)) {
        +            x = v[x].l;
        +        } else {
        +            k -= (gsz(v[x].l) + 1);
        +            x = v[x].r;
        +        }
        +    }
        +    splay(x);
        +    return x;
        +}
        +
        +pair<int, int> split(int x, int k) {
        +    if (x == -1) return {-1, -1};
        +    if (k == 0) return {-1, x};
        +    int aut = get(x, k - 1);
        +    int gg = v[aut].r;
        +    disconnect(gg);
        +    return {aut, gg};
        +}
        +
        +int merge(int l, int r) {
        +    if (l == -1) return r;
        +    if (r == -1) return l;
        +    int bs = gsz(l);
        +    int aut = get(l, bs - 1);
        +    connect(r, aut, 0);
        +    return aut;
        +}
        +

        Это первая и последняя реализация splay-дерева, которую я писал, поэтому код такой длинный. Самое болезненное - дебагать обновление параметров вершин при переподвешивании.

        +

        Время работы

        +

        Сперва введём необходимые обозначения: \(t_i\) - это время работы splay-дерева на \(i\)-м запросе, \(T\) - это суммарное время работы дерева на всех запросах. Доказательство времени работы будем проводить при помощи метода потенциалов. Заключается он в следующем: пусть есть структура данных размера \(n\), и мы хотим оценить амортизированное время её работы на \(q\) запросах. Для каждого из \(q+1\) состояний структуры введём величину \(\Phi_{i}\) - потенциал. Также введём величину \(a_i=t_i+\Phi_{i}-\Phi_{i-1}\) - стоимость \(i\)-й операции. Утверждается, что если \(a_i=O(f(n,q))\) для некоторой функции \(f\) и \(\Phi_{i}=O(n\cdot f(n,q))\), то \(T=O(f(n,q))\). Доказательство этого факта довольно простое, мы не будем его приводить, и перейдём непосредственно к доказательству времени работы splay-дерева. Обозначим за \(C(v)\) размер поддерева вершины \(v\). Тогда потенциалом \(\Phi\) нашего дерева назовём сумму двоичных логарифомв \(C(v)\) по всем вершинам \(v\) дерева (для краткости обозначим \(r(v)=log_2\,C(v)\)). Заметим, что эта величина неотрицательная и не больше \(O(n\,log\,n)\). ааааааа что за жесть интересно зачем я вообще это пишу если есть викиконспекты (видимо потому что прилагать чистый код было бы тупо)

        +

        Link-cut tree

        +

        Рассмотрим следующую задачу:

        +
        +

        Дан лес из \(n\) вершин. Приходят запросы трёх типов: + \(link(u,\,v)\) - соединить вершины \(u\) и \(v\) ребром + \(cut(u,\,v)\) - удалить ребро между вершинами \(u\) и \(v\) + \(get(u,\,v)\) - найти длину пути между \(u\) и \(v\) Нужно ответить на все запросы третьего типа. Гарантируется, что после каждого запроса граф остаётся лесом.

        +
        +

        Существует структура данных, которая решает эту задачу, с амортизированным временем работы \(O(n\,log\,n)\) - Link-Cut Tree, изобретённая Тарьяном и Слейтором (именно для достижения такой оценки и создавалось splay-дерево).

        +

        Основная идея

        +

        Подвесим каждое дерево нашего леса за некоторую вершину и ориентируем рёбра в сторону корня. Введём операцию \(expose(x)\), которая делает вершину \(x\) корнем своего дерева. Пусть старым корнем была вершина \(y\), и мы применили операцию \(expose(x)\). После этого нам нужно сменить ориентацию каждого ребра на пути между \(x\) и \(y\) на противоположную, чтобы ничего не сломалось. Рассмотрим, как мы тогда будем обрабатывать наши запросы: + \(link(u, v)\) - в дереве вершины \(u\) выполним операцию \(expose(u)\), в дереве вершины \(v\) - операцию \(expose(v)\), и затем просто соединим \(u\) с \(v\) ребром, и вершина, в сторону которой будет ориентировано новое ребро, станет новым корнем дерева. + \(cut(u, v)\) - просто удалим ребро между вершинами \(u\) и \(v\); вершина, в сторону которой было ориентировано ребро, останется в старом дереве, а другая вершина окажется в новом, её и следует назначить корнем нового дерева. + \(get(u,\,v)\) - выполним операцию \(expose(u)\) и найдём глубину вершины \(v\). Если реализовать операцию \(expose\) напрямую, то каждый запрос в худшем случае будет обрабатыватся за \(O(n)\); однако при помощи структур данных можно добиться амортизированного \(O(n\,log^2\,n)\) или даже \(O(n\,log\,n)\).

        +

        Ускорение работы

        +

        Мы описали в общих чертах алгоритм; теперь нужно понять, как выполнять эти операции быстро. Оказывается, что если мы разобьём каждое дерево на набор вершинно непересекающихся вертикальных путей и будем поддерживать эти пути при помощи декартовых деревьев по неявному ключу, то суммарная асимптотика алгоритма будет равна \(O(n\,log^2\,n)\), что будет доказано в конце. Однако если вместо декартовых деревьев использовать splay-деревья, то асимптотика будет даже \(O(n\,log\,n)\), и доказательство этого факта я не знаю. Разберём, как же работает быстрый алгоритм. Изначально мы строим декомпозицию каждого дерева на пути, причём не важно, какие именно, поэтому можно просто сделать \(N\) путей длины 1. Пути мы храним в виде множества splay-деревьев, и для каждого пути мы храним указатель на предка самой высокой вершины. Когда нам нужно выполнить операцию \(expose(x)\), мы “прыгаем” вверх по путям, попутно сливая их в один (то есть из вершины \(x\) мы перемещаемся в верхнюю вершину пути, в котором лежит \(x\); затем мы перемещаемся на одну вершину вверх и новый путь со старым (причём может оказаться так, что мы попали не на конец пути, а на середину, и тогда придётся “обрезать” путь в этом месте), затем прыгаем вверх по этому пути и т.д.). Затем просто выполняем разворот сегмента полученного объединённого пути, отвечающего за путь \((x; y)\). Казалось бы - совсем не ясно, почему этот алгоритм работает быстрее предыдущего. Однако выясняется очень любопытный факт: какие бы запросы не приходили, структура как бы сама оптимизирует себя, и итоговая асимптотика в любом случае оказывается \(O(n\,log^{(2)}\,n)\).

        +

        TODO

        + + diff --git a/ru/probability.html b/ru/probability.html index 352d125..3c0372e 100644 --- a/ru/probability.html +++ b/ru/probability.html @@ -123,7 +123,10 @@

        Ликбез по теорверу import seaborn as sns sns.set() +

        Плотность распределения

        +

        Пусть есть некоторая случайная величина \(X\). Плотностью распределения \(X\) называется функция \(p(x)\), отражающая относительную вероятность наступления события \(X=x\) (то есть относительно всех остальных возможных событий). Например, для случайной величины с конечным множеством элементарных исходов \(\Omega\) \(p(x)=P(X=x)\) (где \(P(A)\) - вероятность наступления события \(A\)). Обычно функция плотности вероятности предполагается нормированной на единицу (то есть площадь под графиком равна 1). Функцией распределения случайной численной величины \(F(x)\) называется вероятность попадания \(X\) в луч \((-\infty;\,x]\) (или \(\int_{-\infty}^{x}p(x)\,dx\)).

        Матожидание

        +

        Предположим, у нас есть случайная численная величина \(X\) с функцией плотности \(p(x)\). Тогда математическое ожидание случайной величины равно сумме \(P(A) \cdot A\) для всех возможных исходов \(A\). TODO

        Дисперсия

        Какие два числа лучше всего описывают распределение?

        Нормальное распределение

        From b1b40142308f3f2bd3431607c942faff6b18ba2c Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 1 Aug 2019 16:50:26 +0000 Subject: [PATCH 162/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/lca.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ru/lca.html b/ru/lca.html index 2ba8d10..c641230 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -158,6 +158,7 @@

        Запросы на уровнях

        Создадим \(h\) векторов, где \(h\) — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их \(tin\)-ам.

        Теперь заметим, что отрезки их поддеревьев — \([tin_v, tout_v)\) — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять \(tin\) вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку.

        +

        Также существует другой способ, требующий \(O(n log n)\) памяти и \(O(1)\) времени на запрос. Лестничной декомпозицией леса корневых деревьев (чаще всего состоящего из одного дерева) называется его разбиение на вертикальные пути, получаемое следующим образом: в лесу ищется самый длинный вертикальный путь и добавляется в декомпозицию; затем все вершины этого пути и связанные с ними рёбра удаляются из леса, и алгоритм повторяется, пока в лесу есть хотя бы одна вершина. После того, как мы построили изначальный набор путей и для каждой вершины запомнили её номер и позицию на этом пути, мы для каждого пути \(P\) (обозначим его длину \(l\)) берём самую высокую вершину \(x\) этого пути (пусть её глубина \(d_x\)) и всех предков \(x\) с глубинами \([d_x-1; max(0, d_x-l)]\) (глубину корня считаем за 0) последовательно добавляем в \(P\) (при этом позиция в пути и номер пути для каждой вершины остаются прежними). После того, как мы построили эту декомпозицию, осталось построить обычные двоичные подъёмы, а также для каждого \(k\) от 1 до \(n\) найти максимальное \(i\) такое, что \(2^i \leq n\). Затем можно приступить к ответу на запросы. Пусть пришёл запрос \((v, d)\). Обозначим за \(u\) \(2^{\lfloor{log d}\rfloor}\)-го предка вершины \(v\). Если \(v\) - это и есть нужный предок, то ответ найден. Иначе заметим, что длина пути \(P\), на котором изначально лежала \(v\), хотя бы \(2^{\lfloor{log d}\rfloor}\), и при этом расстояние от \(v\) до искомой вершины \(x\) меньше \(2^{\lfloor{log d}\rfloor}\), а это значит, что после удвоения \(P\) вершина \(x\) также оказалась в \(P\), то есть чтобы получить её, достаточно взять вершину \(P\), лежащую на расстоянии \(d-2^{\lfloor{log d}\rfloor}\) от \(v\). Потребление памяти можно оптимизировать и до \(O(n \cdot \alpha(n))\), где \(\alpha(n)\) - обратная функция Аккермана (условно - минимальное k такое, что \(\underbrace{log_2 \,{log_2\,{\dots\,{log_2\,{n}}}}}_k \leq 1\)). TODO

        Наименьший общий предок

        Очень много задач нам поможет решить следующая вспомогательная задача.

        From 7dd11d750e80ba7f0462dabf2c68c0164691c2c1 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 2 Aug 2019 12:09:52 +0000 Subject: [PATCH 163/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/lca.html | 124 ++++++++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/ru/lca.html b/ru/lca.html index c641230..34de788 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -115,18 +115,18 @@

        Корневые деревья

        -

        Дерево называется корневым, если оно ориентированно и из какой-то вершины можно попасть во все остальные — такая вершина единственная, и она называется корнем.

        +

        Дерево называется корневым, если оно ориентированно, и из какой-то вершины (называемой корнем) можно попасть во все остальные.

        Примеры корневых деревьев:

        • наследование классов в языках программирования (если множественное наследование запрещено),
        • дерево факторизации числа на простые (в общем случае не уникальное),
        • -
        • иерархия в какой-нибудь компании,
        • -
        • просто множество как-то вложенных объектов.
        • +
        • иерархия в какой-нибудь организации,
        • +
        • дерево парсинга математичеких выражений.
        -

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

        +

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

        -

        Напоминание: DFS

        -

        Посчитаем для каждой вершины времена входа (\(tin\)) и выхода (\(tout\)) из неё во время эйлерова прохода.

        +

        Свойства dfs

        +

        Посчитаем для каждой вершины времена входа (\(tin\)) и выхода (\(tout\)) из неё во время эйлерова прохода:

        + tout[v] = t; // иногда счётчик тут тоже увеличивают +}

        У этих массивов много полезных свойств:

        • Вершина \(u\) является предком \(v\) \(\iff tin_v \in [tin_u, tout_u)\). Эту проверку можно делать за константу.
        • -
        • Два полуинтервала — \([tin_v, tout_v)\) и \([tin_u, tout_u)\) — либо не пересекаются, либо вложены один в другой.
        • -
        • В \(tin\) есть все числа из промежутка от 0 до \(n-1\). У каждой вершины — свой номер.
        • +
        • Два полуинтервала — \([tin_v, tout_v)\) и \([tin_u, tout_u)\) — либо не пересекаются, либо один вложен в другой.
        • +
        • В массиве \(tin\) есть все числа из промежутка от 0 до \(n-1\), причём у каждой вершины свой номер.
        • Размер поддерева вершины \(v\) (включая саму вершину) равен \(tout_v - tin_v\).
        • -
        • Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то поддерево вершины — всегда какой-то промежуток в этой нумерации.
        • +
        • Если ввести нумерацию вершин, соответствующую \(tin\)-ам, то индексы любого поддерева всегда будут каким-то промежутком в этой нумерации.
        -

        Запросы на поддеревьях

        -

        Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезкам, которые уже можно решать стандартными методами — например, через дерево отрезков.

        +

        Запросы на поддеревьях

        +

        Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезках, которые уже можно решать стандартными методами — например, через дерево отрезков.

        +

        Пример задачи:

        -

        Есть корневое дерево. Рядом с каждой вершиной записано число. Два типа запросов: прибавить ко всем вершинам на каком-то поддереве число \(x_i\) и найти значение числа у вершины \(v_i\).

        +

        Есть корневое дерево. Рядом с каждой вершиной записано число. Поступают два типа запросов: прибавить ко всем вершинам на каком-то поддереве число \(x_i\) и найти значение числа у вершины \(v_i\).

        -

        Давайте запишем все числа у вершин в позиции, соответствующие \(tin\)-ам их вершин. Что такое «прибавить на поддереве» с точки зрения этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать какой-нибудь достаточно продвинутой структурой.

        -

        Запросы на уровнях

        +

        Выпишем все числа у вершин в один массив, в позиции, соответствующие их \(tin\)-ам. Что такое «прибавить на поддереве» относительно этого массива? Это то же, самое, что прибавить какую-то константу на каком-то подотрезке, а это можно делать любой достаточно продвинутой структурой.

        +

        Запросы на уровнях

        -

        Дано корневое дерево. Требуется отвечать на запросы нахождения \(d_i\)-того предка вершины \(v_i\) (т. е. вершины-предка, находящейся на расстоянии \(d_i\)).

        +

        Дано корневое дерево. Требуется отвечать на запросы нахождения \(d_i\)-того предка вершины \(v_i\), то есть вершины-предка, находящейся на расстоянии \(d_i\) от \(v_i\).

        -

        Создадим \(h\) векторов, где \(h\) — высота дерева. В каждый вектор добавим все вершины на этой глубине в том порядке, в котором мы в них заходили в dfs. Как следствие, они будут отсортированы по их \(tin\)-ам.

        -

        Теперь заметим, что отрезки их поддеревьев — \([tin_v, tout_v)\) — тоже не пересекаются, а значит ещё и отсортированы. Тогда мы можем просто взять \(tin\) вершины-запроса, посмотреть на вектор нужного уровня и сделать бинпоиск по нужному отрезку.

        -

        Также существует другой способ, требующий \(O(n log n)\) памяти и \(O(1)\) времени на запрос. Лестничной декомпозицией леса корневых деревьев (чаще всего состоящего из одного дерева) называется его разбиение на вертикальные пути, получаемое следующим образом: в лесу ищется самый длинный вертикальный путь и добавляется в декомпозицию; затем все вершины этого пути и связанные с ними рёбра удаляются из леса, и алгоритм повторяется, пока в лесу есть хотя бы одна вершина. После того, как мы построили изначальный набор путей и для каждой вершины запомнили её номер и позицию на этом пути, мы для каждого пути \(P\) (обозначим его длину \(l\)) берём самую высокую вершину \(x\) этого пути (пусть её глубина \(d_x\)) и всех предков \(x\) с глубинами \([d_x-1; max(0, d_x-l)]\) (глубину корня считаем за 0) последовательно добавляем в \(P\) (при этом позиция в пути и номер пути для каждой вершины остаются прежними). После того, как мы построили эту декомпозицию, осталось построить обычные двоичные подъёмы, а также для каждого \(k\) от 1 до \(n\) найти максимальное \(i\) такое, что \(2^i \leq n\). Затем можно приступить к ответу на запросы. Пусть пришёл запрос \((v, d)\). Обозначим за \(u\) \(2^{\lfloor{log d}\rfloor}\)-го предка вершины \(v\). Если \(v\) - это и есть нужный предок, то ответ найден. Иначе заметим, что длина пути \(P\), на котором изначально лежала \(v\), хотя бы \(2^{\lfloor{log d}\rfloor}\), и при этом расстояние от \(v\) до искомой вершины \(x\) меньше \(2^{\lfloor{log d}\rfloor}\), а это значит, что после удвоения \(P\) вершина \(x\) также оказалась в \(P\), то есть чтобы получить её, достаточно взять вершину \(P\), лежащую на расстоянии \(d-2^{\lfloor{log d}\rfloor}\) от \(v\). Потребление памяти можно оптимизировать и до \(O(n \cdot \alpha(n))\), где \(\alpha(n)\) - обратная функция Аккермана (условно - минимальное k такое, что \(\underbrace{log_2 \,{log_2\,{\dots\,{log_2\,{n}}}}}_k \leq 1\)). TODO

        +

        Создадим \(h\) векторов, где \(h\) — высота дерева. Для каждой вершины, во время прохода в dfs, добавим её \(tin\) в вектор, соответствующей её глубине. Получаем \(h\) отсортированных векторов.

        +

        Теперь заметим, что внутри одного вектора все отрезки поддеревьев вершин — \([tin_v, tout_v)\) — тоже не пересекаются, а значит ещё и отсортированы. Тогда ответа на запрос мы можем просто взять \(tin\) вершины-запроса, посмотреть на вектор нужного уровня и за \(O(\log n)\) сделать бинпоиск по нужному отрезку.

        +

        Также существует другой способ, требующий \(O(1)\) времени на запрос, но \(O(n \log n)\) памяти на предпосчёт — лестничная декомпозиция.

        Наименьший общий предок

        -

        Очень много задач нам поможет решить следующая вспомогательная задача.

        +

        Для большого класса задач требуется решить следующую вспомогательную:

        Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин \(u_i\) и \(v_i\), то есть вершины \(w\), которая лежит на пути от корня до \(u_i\), на пути от корня до \(v_i\), и при этом самую глубокую (нижнюю) из всех таких.

        -

        По-английский эта задача называется Least Common Ancestor. Есть много разных способов её решать, и мы рассмотрим основные.

        +

        По-английский эта задача называется Least Common Ancestor. Есть много разных способов её решать, и мы рассмотрим только основные.

        -

        Для лучшего понимания: медленно (за линейное время) это можно делать так:

        +

        Для лучшего понимания — медленно (за линейное время) наименьшего общего предка можно искать так:

        -

        LCA: двоичные подъемы

        -

        Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го, и т.д. Сохраним это всё в двумерном массиве up размера \(n \times \lceil \log n \rceil\) — столько точно хватит. В up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует — то корень.

        +

        Двоичные подъемы

        +

        Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го и так далее. Сохраним их в двумерном массиве up размера \(n \times \lceil \log n \rceil\): в up[v][d] будет храниться предок вершины \(v\) на расстоянии \(2^d\), а если такой вершины не существует, то корень.

        Такой препроцессинг можно выполнить за \(O(n \log n)\), используя тот факт, что предок на расстоянии \(2^{d+1}\) — это предок на расстоянии \(2^d\) предка на расстоянии \(2^d\):

        int up[maxn][logn];
         
        @@ -191,12 +191,12 @@ 

        LCA: двоичные подъем } tout[v] = t; }

        -

        Пусть поступил очередной запрос — пара вершин \((u, v)\):

        +

        Пусть теперь поступил запрос нахождения LCA — пара вершин \((u, v)\):

        • Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.
        • Иначе, пользуясь массивом up, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.
        -

        Подробнее про второй пункт. Пусть \(L = \lceil \log n \rceil\). Присвоим \(i = L\). Будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком \(u\) (указатель up[v][i] изначально будет корнем, а затем каждую итерацию спускаться на \(2^i\)). Когда это произойдёт, подвинем указатель на \(2^i\)-го предка \(v\), и продолжим дальше. Мы можем это делать, потому что два раза прыгать на {2^i} — можно один раз прыгнуть на \(2^{i+1}\).

        +

        Подробнее про второй пункт. Присвоим \(i = \lceil \log n \rceil\) и будем уменьшать эту переменную на единицу, пока up[v][i] не перестанет быть предком \(u\). Когда это произойдёт, подвинем указатель на \(2^i\)-го предка \(v\) и продолжим дальше:

        -

        Асимптотика

        -

        Препроцессинг — \(O(n \log n)\). Размер массива up ровно такой, и каждый его элемент вычисляется за константу.

        -

        Ответ на запрос — \(O(\log n)\), потому что по сути мы делаем один бинпоиск.

        -

        Запросы на путях

        +

        Указатель up[v][i] изначально явдяеься корнем дерева, а затем будет каждую итерацию спускаться на \(2^i\). Когда он станет потомком lca, нам достаточно подняться всего лишь один раз, потому что два раза прыгнуть на расстояние \(2^i\) это то же самое, что один раз прыгнуть на \(2^{i+1}\), а мы могли это сделать на предыдущем шаге.

        +

        Асимптотика. Препроцессинг занимает \(O(n \log n)\), потому что таков размер массива up, и каждый его элемент вычисляется за константу. Ответ на произвольный запрос будет работать за \(O(\log n)\), потому что фактически мы делаем один бинпоиск.

        +

        Запросы на путях

        Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).

        -

        Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером \(2^d\)-го предка минимум на соответствующем пути.

        -

        Мы знаем, что минимум на пути от \(u\) до \(v\) — это минимум от минимума на пути от \(u\) до \(lca(u, v)\) и от минимума на пути от \(v\) до \(lca(u, v)\). А каждый минимум — это минимум на всех двоичных подъемах до LCA.

        +

        Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером \(2^d\)-ого предка минимум на соответствующем пути.

        +

        Заметим, что минимум на пути от \(u\) до \(v\) — это минимум от минимума на пути от \(u\) до \(lca(u, v)\) и от минимума на пути от \(v\) до \(lca(u, v)\). В свою очередь, оба этих минимума — это минимум на всех двоичных подъемах до LCA.

        -

        Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статичном случае (т. е. когда у нас нет обновлений). Для динамического случая существует heavy-light декомпозиция, но она очень сложная.

        -

        Сведение к RMQ

        -

        Другая идея — это пройтись dfs-ом и выписать два массива: глубины вершин и их номера. Выписывать их мы будем как когда будем входить в вершину, так и когда выходить.

        -

        -

        Во втором массиве мы по сути выписали наш проход dfs-а.

        -

        Заметим, что размер полученного массива — \(O(n)\). Доказательство — каждое ребро мы пройдем дважды (заходя в и выходя из поддерева), при просмотре каждого ребра мы выпишем не более, чем две вершины (потому что у каждого ребра всего два конца). Тогда размер массива не более, чем в четыре раза больше числа ребер, которых \(n-1\).

        -

        Пусть у нас есть запрос: найти LCA вершин \(v\) и \(u\). Для определенности положим, что \(tin_v < tin_u\). Посмотрим на наш выписанный путь между тем моментом, когда мы вышли из \(v\) и в первый раз вошли в \(u\). Где-то на выписанном пути мы должны были прийти в наименьший общий предок, потому что любой простой путь между двумя вершинами в дереве единственный. При этом, мы не поднимались никогда из LCA куда-то выше, а значит LCA — это самая высокая вершина на этом пути.

        -

        Получается, что можно найти LCA, просто найдя позицию минимума на отрезке \([tout_v, tin_u]\) в массиве глубин, и посмотрев, какой вершине она соответствует в эйлеровом обходе. Получается, задачу LCA можно свести к RMQ (нахождению минимума на отрезке), что мы уже умеем.

        +

        Аналогичным образом можно считать сумму, gcd, полиномиальный хэш и много других странных функций на пути, но только в статическом случае (когда у нас нет обновлений). Для динамического случая существует весьма сложный метод, называемый heavy-light декомпозицией.

        +

        Сведение LCA к RMQ

        +

        Подойдём к задаче нахождения наименьшего общего предка с другой стороны.

        +

        Пройдёмся по дереву dfs-ом и выпишем два массива: глубины вершин и номера вершин. Записывать мы их будем как когда при входе в вершину, так и при выходе.

        +

        Пусть теперь поступил запрос: найти LCA вершин \(v\) и \(u\). Для определенности предположим, что \(tin_v < tin_u\), то есть \(v\) в обходе встретилась раньше. Посмотрим на часть выписанного пути между моментом, когда мы вышли из \(v\) и моментом, когда мы в первый раз вошли в \(u\). Где-то на этом отрезке мы должны были прийти в наименьший общий предок, так как любой простой путь между двумя вершинами в дереве единственный. При этом мы на этом пути не поднимались куда-то выше LCA, а значит LCA — это самая высокая вершина на этом пути.

        +

        Получается, что чтобы найти LCA, можно найти позицию минимума на отрезке \([tout_v, tin_u]\) в массиве глубин (первый выписанный массив) и посмотреть, какой вершине она соответствует в эйлеровом обходе (второй выписанный массив). Таким образом, задачу LCA можно свести к задаче RMQ (нахождению минимума на отрезке), что можно сделать тем же деревом отрезков.

        Разреженная таблица

        -

        На практике асимптотику мы особо не улучшили — пока что всё равно требуется \(O(n \log n)\) времени на запрос в ДО, хоть преподсчёт и будет уже линейным. Асимптотику можно улучшить, используя тот факт, что мы решаем static RMQ, то есть у нас нет изменений этого массива.

        -

        Помимо ДО, есть более крутая структура, позволяющая отвечать на запрос минимума за \(O(1)\), но использующая \(O(n \log n)\) препроцессинга (с очень маленькой константой). Подробнее вы можете почитать в отдельной статье.

        -

        А наоборот можно?

        -

        Примечание: алгоритм (Фарах-Колтона и Бендера), описанный в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

        -

        На самом деле, мы решаем не совсем полноценную задачу RMQ: мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — любые два элемента отличаются ровно на единицу, потому что они соответствуют либо спуску, либо подъему в dfs. Выясняется, что это ограничение позволяет находить минимум на подобных массивах за константное время работы как на препроцессинг, так и на запрос.

        -

        Сделаем следующее: раз каждые два элемента отличаются на единицу, сопоставим исходному массиву глубин булевый массив размера \(n-1\): единица стоит, если следующее значение больше, единица в противном случае ноль.

        -

        Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим на блоки по столько элементов. На каждом блоке посчитаем минимум, а над всеми такими блоками построим sparse table. Всего блоков \(O(\frac{2 n}{\log n})\), и построение будет работать за линейное время:

        +

        На практике асимптотику мы особо не улучшили — пока что всё равно требуется \(O(\log n)\) времени на запрос в дерево отрезков, хоть преподсчёт и будет уже линейным.

        +

        Асимптотику времени запроса можно улучшить, используя тот факт, что мы на самом деле решаем задачу static RMQ, то есть у нас нет изменений этого массива. Для этого есть более подходящая структура — разреженная таблица. Она позволяет отвечать на запрос минимума за \(O(1)\), но использует \(O(n \log n)\) операций на препроцессинг (с малой константой). Подробнее про неё можно почитать в отдельной статье.

        +

        Алгоритм Фарах-Колтона и Бендера

        +

        Примечание: алгоритм нахождения RMQ, описываемый в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.

        +

        Оказывается, что и LCA, и static RMQ можно считать за \(O(1)\) времени на запрос и \(O(n)\) времени на предпосчёт.

        +

        На самом деле, мы решаем не совсем полноценную задачу RMQ. Мы работаем не со всеми массивами целых чисел от 1 до \(n\), а только с некоторыми — с теми, в которых любые два элемента отличаются ровно на единицу, потому что каждый переход это либо спуск, либо подъём в dfs. Это ограничение позволяет находить минимум на подотрезках подобных массивов быстрее.

        +

        Сделаем следующее: раз каждые два элемента отличаются на единицу, то сопоставим исходному массиву глубин булевый массив размера \((n-1)\): на \(i\)-той позиции будет стоять единица, если следующее значение больше, и ноль в противном случае. Этот массив нужно будет хранить в бинарном виде, чтобы можно было за константу получать булеву маску небольших подотрезков.

        +

        Первая часть предподсчёта. Возьмем константу \(k = \lfloor \frac{\log n}{2} \rfloor\), и разделим исходный массив на блоки по \(k\) элементов. На каждом блоке посчитаем минимум, а над этими минимумами построим sparse table.

        +

        Всего блоков таких блоков \(O(\frac{2 n}{\log n})\), и поэтому построение работает за линейное время:

        \[ O(\frac{2 n}{\log n} \log \frac{2 n}{\log n}) = O(\frac{2 n}{\log n} (\log 2n - \log \log n)) = O(n) \]

        -

        Также посчитаем для каждой возможной маски размера \(\frac{\log n}{2}\) минимум на ней — это можно сделать за их количество, помноженное на длину маски а их немного: всего \(\sqrt n\) (ради этого мы и делили логарифм на два).

        -

        ОК. Что теперь можно сделать во время запроса? Запрос — это какой-то отрезок. Он включает в себя какие-то последовательные блоки, и сколько-то ячеек слева и справа, не вошедшие ни в какой цельный блок. Для блочной части мы можем сделать запрос в sparse table — он будет работать за константу.

        -

        Не-блочная часть хоть и маленькая, но мы всё же заявили, что будем обрабатывать запросы за константу, и сдержим свое слово. Возмем левую не-блочуню часть запроса и возьмем оттуда маску — это можно сделать за константу. Теперь по этой маске просто возьмём минимум, который мы посчитали.

        -

        Наоборот. Этот алгоритм очень важен с теоретической точки зрения, потому что позволяет решать и static RMQ за линейное время. А именно, построим декартово дерево, в котором в качестве ключей \(x_i\) возьмём индексы элементов, а в качестве приоритетов \(y_i\) возьмём сами значения. Декартово дерево могло получиться несбалансированным (нет рандомизации приоритетов), но это нам и не нужно: дальше мы просто применим описанный выше алгоритм к этому дереву, и теперь для нахождения минимума просто запросим общего предка \(l\)-той и \(r\)-той вершины. Его приоритет в декартовом дереве и будет минимумом на нужном отрезке в исходном массиве.

        -

        Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать у Емакса. Впрочем, на практике этот алгоритм использовать нецелесообразно: у него слишком большая константа, слишком много чего нужно считать, чтобы выкинуть этот логарифм из асимптотики.

        +

        Вторая часть предподсчёта. Посчитаем для каждой возможной маски подъёмов / спусков размера \(k\) максимальный спуск на ней — то есть пройдёмся по ней, поддерживая разницу встретившихся нулей и единиц, и запомним минимальное значение этого баланса. Это можно сделать за длину маски, помоноженное на их количество:

        +

        \[ +O(k \cdot 2^k) = O(\frac{\log n}{2} 2^{\frac{\log n}{2}}) = O(\sqrt n \log n) +\]

        +

        Возможных масок получается немного — ради этого мы и делили логарифм на два при определении \(k\)

        +

        Запрос. Нам нужно с помощью посчитанных структур найти RMQ на каком-то отрезке \([l, r]\). Он включает в себя какие-то последовательные блоки и сколько-то оставшихся ячеек слева и справа, не вошедших ни в какой целый блок.

        +
          +
        • Для блочной части мы можем просто сделать запрос в sparse table — он будет работать за константу.

        • +
        • Для обеих не-блочных частей посчитаем ещё по кандидату на ответ. Для этого нужно прибавить к граничному элементу предподсчитанное значение минимума на маске оставшихся неблочных элементов — её можно за константу получить битовыми операциями над булевым массивом.

        • +
        +

        Просто посчитать все суффиксные и префиксные минимумы для всех блоков, чтобы обрабатывать второй случай, к сожалению, нельзя — есть один частный случай, когда запрос маленький и не накрывает никакой блок целиком. В данном случае нужно просто взять граничный элемент и прибавить к нему минимум от нужной маски из массива подъёмов.

        +

        Наоборот. Этот алгоритм очень важен с теоретической точки зрения, потому что позволяет решать, не только LCA, но и в общем случае static RMQ за линейное время.

        +

        Построим декартово дерево, в котором в качестве ключей \(x_i\) возьмём индексы элементов, а в качестве приоритетов \(y_i\) возьмём сами значения. Декартово дерево могло получиться несбалансированным (так как нет рандомизации приоритетов), но это нам и не нужно. Дальше просто применим описанный выше алгоритм к этому дереву, и теперь для нахождения минимума в исходном массиве можно просто запросить общего предка \(l\)-той и \(r\)-той вершины в дереве — его приоритет в декартовом дереве и будет искомым минимумом.

        +

        Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать у Емакса. Впрочем, на практике этот алгоритм использовать нецелесообразно из-за большой константы: слишком много чего нужно считать, чтобы избавиться от логарифма в асимптотике.

        From 1646d63128ae6b3185ebbf6cb10e3b75b2461ce4 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 3 Aug 2019 14:46:50 +0000 Subject: [PATCH 164/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/suffix-automaton.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ru/suffix-automaton.html b/ru/suffix-automaton.html index 6cbb819..d31fea4 100644 --- a/ru/suffix-automaton.html +++ b/ru/suffix-automaton.html @@ -134,9 +134,9 @@

        Наивное решение

        Суффиксным автоматом строки \(s\) называется минимальный (с наименьшим количеством вершин) детерминированный (нет двух различных путей, соответствующих одинаковой строке) автомат, принимающий все различные подстроки \(s\) и только их.

        Выясняется, что он тоже маленький, и если хранить переходы в std::map, то автомат можно построить за \(O(n log k)\) времени и \(O(n)\) памяти, где \(k\) — размер алфавита (обычно \(k\) считают константой, и тогда построение происходит вообще за \(O(n)\)).

        Сперва обозначим основные свойства суффиксного автомата и его состояний, затем опишем алгоритм, а затем поймём, как и почему он работает.

        -

        Любое состояние \(V\) автомата будет принимать некоторый набор строк \(T\), причём если отсортировать эти строки по возрастанию длины, то для любой строки \(s_i \in T (i < |T|)\), следующая по порядку строка \(s_{i+1} = cs_i\) для некоторого символа \(c\). Исходя из определения суффиксного автомата, для любого префикса исходной строки \(S\) существуют несколько состояний, принимающих его суффиксы: состояние \(V_1\), принимающее суффиксы длин от 1 до некоторого \(k_1\), состояние \(V_2\), принимающее суффиксы длин от \(k_1+1\) до \(k_2\), и т.д. до состояния, принимающего самые длинные суффиксы данного префикса, включая сам префикс. Из каждого состояния \(X\), кроме корневого, существует суффиксная ссылка: пусть \(X\) принимает некоторую строку \(p\), тогда суффиксная ссылка из \(X\) ведёт в отличное от \(X\) состояние, принимающее суффикс \(p\) максимальной длины. Также из каждого состояния \(X\), кроме корневого, ведёт ещё одна ссылка, мы назовём её префиксной: пусть самая длинная принимаемая \(X\) строка - это \(p\), тогда префиксная ссылка из \(X\) указывает на состояние, принимающее префикс \(p\) на единицу меньшей длины.

        +

        Любое состояние \(V\) автомата будет принимать некоторый набор строк \(T\), причём если отсортировать эти строки по возрастанию длины, то для любой строки \(s_i \in T (i < |T|)\) следующая по порядку строка - это \(s_{i+1} = cs_i\) для некоторого символа \(c\). Исходя из определения суффиксного автомата, для любого префикса исходной строки \(S\) существуют несколько состояний, принимающих его суффиксы: состояние \(V_1\), принимающее суффиксы длин от 1 до некоторого \(k_1\), состояние \(V_2\), принимающее суффиксы длин от \(k_1+1\) до \(k_2\), и т.д. до состояния, принимающего самые длинные суффиксы данного префикса, включая сам префикс. Из каждого состояния \(X\), кроме корневого, существует суффиксная ссылка: пусть \(X\) принимает некоторую строку \(p\), тогда суффиксная ссылка из \(X\) ведёт в отличное от \(X\) состояние, принимающее суффикс \(p\) максимальной длины. Также из каждого состояния \(X\), кроме корневого, ведёт ещё одна ссылка, мы назовём её префиксной: пусть самая длинная принимаемая \(X\) строка - это \(p\), тогда префиксная ссылка из \(X\) указывает на состояние, принимающее префикс \(p\) на единицу меньшей длины.

        Теперь можем перейти к алгоритму построения суффиксного автомата. Наш алгоритм будет “индуктивным”: он будет перестраивать автомат, коррекно построенный для строки \(s\), получая из него автомат для строки \(sc\).

        -

        Предположим, что мы построили суффиксный автомат для строки \(s\), удовлетворяющий всем требованиям, и теперь добавляем символ \(c\). Пусть \(X\) - состояние, которое принимало строку \(s\) до добавления \(c\). Сперва мы обязаны создать новое состояние \(U\), так как появилась строка \(sc\), которая ранее не входила в \(s\). Затем, мы должны добавить переход из \(X\) в \(U\) по символу \(c\): мы можем добавить его, так как такого перехода не могло быть по определению, и должны добавить, потому что должно быть состояние, принимающее \(sc\). Таким образом, наше новое состояние \(U\) теперь принимает некоторый набор суффиксов \(sc\). Перейдём по суффиксной ссылке из \(X\). Тогда есть два случая: 1. Перехода по \(c\) из текущего состояния нет. Тогда мы можем и должны добавить переход по символу \(c\), ведущий в \(U\): теперь \(U\) принимает ещё больший набор суффиксов. Снова перейдём по суффиксной ссылке из текущего состояния и продолжим пытаться строить переходы. 2. Переход по \(c\) из текущего состояния есть. Таким образом, для оставшихся суффиксов \(sc\) уже существовуют свои состояния, на этом моменте следует остановиться и перейти к следующей части алгоритма.

        +

        Предположим, что мы построили суффиксный автомат для строки \(s\), удовлетворяющий всем требованиям, и теперь добавляем символ \(c\). Пусть \(X\) - состояние, которое принимало строку \(s\) до добавления \(c\). Сперва мы обязаны создать новое состояние \(U\), так как появилась строка \(sc\), которая ранее не входила в \(s\). Затем, мы должны добавить переход из \(X\) в \(U\) по символу \(c\) (потому что должно быть состояние, принимающее \(sc\)). Таким образом, наше новое состояние \(U\) теперь принимает некоторый набор суффиксов \(sc\). Перейдём по суффиксной ссылке из \(X\). Тогда есть два случая: 1. Перехода по \(c\) из текущего состояния нет. Тогда мы можем и должны добавить переход по символу \(c\), ведущий в \(U\): теперь \(U\) принимает ещё больший набор суффиксов. Снова перейдём по суффиксной ссылке из текущего состояния и продолжим пытаться строить переходы. 2. Переход по \(c\) из текущего состояния есть. Таким образом, для оставшихся суффиксов \(sc\) уже существовуют свои состояния, на этом моменте следует остановиться и перейти к следующей части алгоритма.

        Опять есть два случая: первый - ни из одного состояния, в котором мы побывали, перехода по \(c\) не нашлось, и в итоге мы перешли по суффиксной ссылке из корня “в никуда”. Тогда все суффиксы \(sc\) принимаются одним состоянием \(U\), так что суффиксную ссылку из \(U\) мы проведём в корень, префиксную - в \(X\) - и завершим алгоритм.

        Второй случай: в какой-то момент мы попали в состояние \(P\), из которого уже был переход по \(c\). Этот случай сложнее, и он также предполагает два возможных варианта. Обозначим за \(Q\) состояние, в которое ведёт переход по символу \(c\) из \(P\). Пусть самая длинная строка, принимаемая \(P\) - это \(l\). Снова рассмотрим два случая: 1. Префиксная ссылка из \(Q\) ведёт в \(P\). Это означает, что состояние \(Q\), как и все достижимые по суфф. ссылкам из \(Q\) состояния, принимает только суффиксы \(sc\): исходя из определения префиксной ссылки, \(l\) является самой длинной строкой, принимаемой \(Q\), с приписанным в конце символом \(c\), то есть суффиксом \(sc\), а все остальные строки, принимаемыми \(P\), являются суффиксами самой длинной. 2. Префиксная ссылка из \(Q\) ведёт не в \(P\). Это означает, что существует строка, принимаемая \(Q\), длина которой больше, чем \(lc\); но такая строка если и является суффиксом \(sc\), то из одного из ранее посещённых (в первой части алгоритма) состояний есть переход по \(c\) - противоречие; значит, \(Q\) помимо суффиксов \(sc\) также принимает строки, не являющиеся суффиксами \(sc\), что недопустимо. Поэтому мы должны сделать две версии \(Q\): одна принимает строки старого \(Q\), являющиеся суффиксами \(sc\) - назовём их основным набором, а вторая - все остальные строки \(Q\) - назовём их дополнительным набором. Для этого клонируем состояние \(Q\): создадим новое состояние \(Q'\), которое будет отвечать за строки основного набора, скопируем в него все переходы, которые были в \(Q\) и скопируем суффиксную ссылку из \(Q\). Префиксную ссылку из \(Q'\) проведём в \(P\). Состояние \(Q\) теперь будет отвечать только за строки дополнительного набора, поэтому мы должны перенаправить его суффссылку в \(Q'\). Суффиксная ссылка из \(U\) также должна указывать на \(Q'\), т.к. все суффиксы \(sc\) большей длины уже принимаются \(U\). Осталось только перенаправить некоторые переходы, ведущие в \(Q\), на состояние \(Q'\): заметим, что это будут в точности все переходы по \(c\) в \(Q'\) из всех состояний, достижимых по суффиксным ссылкам из \(P\), включая \(P\).

        Для строки длины 0 суффиксный автомат - это единственное, корневое состояние. Таким образом, мы получили корректный алгоритм построения суффиксного автомата.

        @@ -149,7 +149,7 @@

        \(q\) суффиксного автомата строки \(T\) найдётся вершина \(q'\) суффиксного дерева развернутой строки \(T\) такая, что множество строк, принимаемых состоянием \(q\), совпадает с развёрнутым множеством строк, таких что соответствующая им вершина в дереве лежит на ребре, ведущем в \(q'\) (включая строку, соответствующую \(q'\)).

        Это целиком описывает состояния автомата и позволяет разработать алгоритм его построения. Код суффиксного автомата можно найти в конце статьи.

        Время работы

        -

        Достаточно очевидно, что вершин в автомате (и, соответственно, префиксных и суффиксных ссылок) не более \(2n\) - действительно, для каждого символа добавляется ровно одно состояние и клонируется максимум одно. Максимальное число состояний достигается, когда строка имеет вид \(abbb...bbb\). Переходов, очевидно, не более \(nk\), однако можем дать более строгую оценку сверху в \(3n\). Сплошных переходов - т.е. ведущих в состояние \(X\) из состояния, достижимого по префиксной ссылке \(X\) - не более \(2n\). Рассмотрим переходы, не являющиеся сплошными. Состояния, принимающие суффиксы строки, назовём терминальными. Каждому из оставшихся переходов поставим в соответствие строку \(t_{1}ct_{2}\), где \(c\) - символ на ребре, \(t_1\) - самая длинная строка, принимаемая “истоком” перехода, а \(t_2\) - самая длинная строка, ведущая из “стока” перехода до некоторого терминального состояния. Заметим, что \(t_1\) и \(t_2\) образованы только сплошными переходами (т.к. если от одного состояния до другого есть некоторый путь, то есть не менее короткий путь, проходящий только по сплошным переходам). Также заметим, что во-первых, полученная строка будет являться некоторым суффиксом исходной, а во-вторых, так как все переходы кроме одного в таком пути сплошные, все такие строки будут различными. Из этих двух фактов следует, что количество несплошных переходов не более \(n\), так как у строки ровно \(n\) суффиксов. Таким образом, переходов в автомате не более \(3n\), а ссылок не более \(4n\), то есть автомат линеен по памяти. По сути, доказав асимптотику по памяти, мы почти доказали время работы, и неочевидным остаётся только то, за сколько работает перенаправление переходов после клонирования. Заметим, что если мы добавляли символ \(c\) и клонировали состояние \(Q\) и получили \(Q'\), то не может быть такого, что через какое-то время мы добавим символ \(c\) и нам пришлось клонировать \(Q'\) и, соответственно, перенаправлять переходы по \(c\), ведущие в \(Q'\). Доказательство: из \(P\) обязательно есть переход по символу \(c\) в \(Q'\), причём этот переход сплошной, и такие переходы есть только из \(P\) и некоторых достижимых из него по суффиксным ссылкам состояний. Также заметим, что для некоторого \(k \geq 0\) и только для него из всех состояний, достижимых из \(P\) за \(k\) переходов по суффиксным ссылкам, будет существовать переход в \(Q'\). Таким образом, ни один клон не будет клонирован повторно, а все остальные вершины будут клонированы максимум один раз, то есть каждый переход будет перенаправлен максимум один раз, а переходов линейное число, поэтому суммарно будет совершено \(O(n)\) действий для перенаправления. Итак, мы доказали, что суффиксный автомат линеен по памяти и требует \(O(n log k)\) времени на построение.

        +

        Достаточно очевидно, что вершин в автомате (и, соответственно, префиксных и суффиксных ссылок) не более \(2n\) - действительно, для каждого символа добавляется ровно одно состояние и клонируется максимум одно. Переходов, очевидно, не более \(nk\), однако можем дать более строгую оценку сверху в \(3n\). Сплошных переходов - т.е. ведущих в состояние \(X\) из состояния, достижимого по префиксной ссылке \(X\) - не более \(2n\). Рассмотрим переходы, не являющиеся сплошными. Состояния, принимающие суффиксы строки, назовём терминальными. Каждому из оставшихся переходов поставим в соответствие строку \(t_{1}ct_{2}\), где \(c\) - символ на ребре, \(t_1\) - самая длинная строка, принимаемая “истоком” перехода, а \(t_2\) - самая длинная строка, ведущая из “стока” перехода до некоторого терминального состояния. Заметим, что \(t_1\) и \(t_2\) образованы только сплошными переходами (т.к. если от одного состояния до другого есть некоторый путь, то есть не менее короткий путь, проходящий только по сплошным переходам). Также заметим, что во-первых, полученная строка будет являться некоторым суффиксом исходной, а во-вторых, так как все переходы кроме одного в таком пути сплошные, все такие строки будут различными. Из этих двух фактов следует, что количество несплошных переходов не более \(n\), так как у строки ровно \(n\) суффиксов. Таким образом, переходов в автомате не более \(3n\), а ссылок не более \(4n\), то есть автомат линеен по памяти. По сути, доказав асимптотику по памяти, мы почти доказали время работы, и неочевидным остаётся только то, за сколько работает перенаправление переходов после клонирования. Заметим, что если мы добавляли символ \(c\) и клонировали состояние \(Q\) и получили \(Q'\), то не может быть такого, что через какое-то время мы добавим символ \(c\) и нам пришлось клонировать \(Q'\) и, соответственно, перенаправлять переходы по \(c\), ведущие в \(Q'\). Доказательство: из \(P\) обязательно есть переход по символу \(c\) в \(Q'\), причём этот переход сплошной, и такие переходы есть только из \(P\) и некоторых достижимых из него по суффиксным ссылкам состояний. Также заметим, что для некоторого \(k \geq 0\) и только для него из всех состояний, достижимых из \(P\) за \(k\) переходов по суффиксным ссылкам, будет существовать переход в \(Q'\). Таким образом, ни один клон не будет клонирован повторно, а все остальные вершины будут клонированы максимум один раз, то есть каждый переход будет перенаправлен максимум один раз, а переходов линейное число, поэтому суммарно будет совершено \(O(n)\) действий для перенаправления. Итак, мы доказали, что суффиксный автомат линеен по памяти и требует \(O(n log k)\) времени на построение. TODO доказательство неверное

        Применение в решении задач

        1. Число различных подстрок. Дана строка \(s\), необходимо посчитать

          From 825452bf73dea4fd77ba5f58b9b507f217bdec0c Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 4 Aug 2019 03:46:01 +0000 Subject: [PATCH 165/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/bitset.html | 25 +++++++++++++++---------- ru/sse.html | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ru/bitset.html b/ru/bitset.html index 5dd4070..62c5d1c 100644 --- a/ru/bitset.html +++ b/ru/bitset.html @@ -115,15 +115,15 @@

          Битовое сжатие

          -

          Процессор устроен так, что работает не с индивидуальными битами, а сразу с блоками по 32 или 64 бита — эта величина называется машинным словом. Как следствие, операции, затрагивающие лишь один бит, на самом деле «стоят» столько же, сколько и операции над целым int-ом или даже long long-ом.

          -

          Когда от нас требуется делать какие-то теоретико-множественные операции, их часто можно свести к большому количеству одинаковых операций над элементами булевого массива. Например:

          +

          Процессор устроен так, что работает не с индивидуальными битами, а сразу с блоками по 32 или 64 бита — эта величина называется машинным словом. Как следствие, операции, затрагивающие лишь один бит, на самом деле «стоят» столько же, сколько и операции над целым int-ом или long-ом.

          +

          Когда требуется делать какие-то теоретико-множественные операции, их часто можно свести к большому количеству одинаковых операций над элементами булевого массива. Например:

            -
          • Объединение множеств — побитовое ИЛИ.

          • +
          • Объединение множеств — побитовое ИЛИ.

          • Пересечение множеств — побитовое И.

          • Симметрическая разность — побитовый XOR.

          -

          Здесь появляется следующая идея оптимизации: сгруппировать элементы массива в блоки по 64 и каждый блок считать двоичным числом. Тогда мы можем применить эту битовую операцию сразу к 64 элементам и потратить на это один такт вместо 64-х.

          -

          Это всё можно кодить и вручную, но в STL это уже сделали до нас, написав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями: bitset.

          +

          Здесь появляется следующая идея оптимизации: сгруппировать элементы массива в блоки размера 64 и каждый такой блок считать двоичным числом. Тогда можно применить эту битовую операцию сразу к 64 элементам и потратить на это один такт вместо 64-х.

          +

          Это всё несложно кодить и вручную, но в STL это уже сделали до нас, написав структуру, ведущую себя как большое двоичное число со всеми стандартными битовыми операциями: bitset.

          Работать с ним нужно вот так:

          Цикл длины 3

          -

          Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из \(n\) вершин, заданном своей матрицей смежности. Обернем матрицу в битсет, и тогда задача решается за \(O(n^3 / w)\) следующим образом:

          +

          Пусть нам нужно узнать, есть ли цикл длины 3 в ориентированном графе из \(n\) вершин, заданном своей матрицей смежности. Обернем эту матрицу в битсет, и тогда задача решается за \(O(n^3 / w)\) следующим образом:

          Бенчмарк: на серверах CodeForces этот код при \(n = 5000\) работает за 7 секунд.

          Перемножение матриц

          -

          Зачем это может быть нужно: матрица смежности графа, возведенная в степень \(n\), имеет комбинаторный смысл: количество способов дойти из \(a\) в \(b\), используя ровно \(n\) переходов. Иногда нам не нужно знать число способов — нам хватит знания, можно ли вообще через \(n\) ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения, то есть &:

          +

          Основное применение в олимпиадах: матрица смежности графа, возведенная в степень \(n\), имеет комбинаторный смысл — на пересечении \(a\)-ой строки и \(b\)-того столбца матрицы \(G^n\) будет записано количество способов дойти из вершины \(a\) в вершину \(b\), используя ровно \(n\) переходов.

          +

          В некоторых задачах нам не нужно знать именно число способов — нам хватит знания, можно ли вообще через \(n\) ходов там оказаться. Тогда вместо числового умножения нам хватит битового умножения, то есть &:

          Метод Гаусса

          -

          Иногда встречаются задачи, требующие решения системы линейных уравнений. Очень большую часть из них на самом деле можно решить над полем \(\mathbb{Z}_2\) — то есть взять все числа по модулю 2.

          -

          Например: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно текущее состояние всех лампочек, нужно восстановить по нему состояние переключаетелей.

          +

          Иногда встречаются задачи, требующие решения системы линейных уравнений. Большую часть из них на самом деле можно решить над полем \(\mathbb{Z}_2\) — то есть взять все числа по модулю 2.

          +

          Пример: есть \(n\) переключателей лампочек, каждый активированный переключатель меняет состояние (включает или выключает) какого-то подмножества из \(n\) лампочек. Известно текущее состояние всех лампочек, нужно восстановить по нему состояние переключаетелей.

          Нас по сути просят решить следующую систему:

          \[ \begin{cases} @@ -204,5 +205,9 @@

          Метод Гаусса

          return x; }

          Код находит вектор \(x\) из уравнения \(Ax = b\) при условии, что решение существует и единственно. Для простоты кода предполагается, что вектор \(b\) приписан справа к матрице \(A\).

          +

          Самописный bitset

          +

          На самом деле, реализация из STL очень медленная. Простой for, в котором and-ят int-ы, будет работать в несколько раз быстрее аналогичной операции битсета. Ускорение получается за счёт векторизации — компилятор перестраивает цикл, используя специальные инструкции, работающие с блоками по 128 / 256 бит, а не 64.

          +

          Нужно просто написать цикл с достаточно простым телом, и компилятор сделает всё сам. Трудности возникают только при реализации битовых сдвигов и .count(). Последнюю проще всего реализовать через встроенную в GCC быструю функцию __builtin_popcount, но вообще в STL для этого используется предпосчитанный массив размера 256 с числами от 0 до 8 — количеста единичных бит в записи каждого возможного значения байта.

          +

          Автор предполагает, что битсет из STL реализовывали во времена, когда векторизация и отдельная инструкция popcnt ещё не поддерживались процессорами, поэтому всё устроено так, как устроено.

          diff --git a/ru/sse.html b/ru/sse.html index 107b6f0..2d45e9f 100644 --- a/ru/sse.html +++ b/ru/sse.html @@ -160,7 +160,7 @@

          Streaming SIMD Extensions

          Работают они следующим образом. Помимо обычных регистров (самых близких к процессору ячеек памяти, с которыми он непосредственно работает), есть дополнительные, вмещающие не 64, а 128, 256 или даже 512 бит — в зависимости от поддерживаемой версии SSE. В эти регистры загружается последовательные блоки из памяти, над ним производится какая-то последовательность операций, и итоговый результат записывается обратно в память. Сами операции обычно логически разбивают эту булеву последовательность на блоки, например, по 32 бит, и работают уже с ними, причём одновременно.

          Подобным способом довольно легко получается оптимизировать простые циклы, производящие какие-нибудь независимые друг от друга операции над векторами (массивами) — поэтому сам такой подход называют векторизацией.

          -

          Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует).

          +

          Например, какое-нибудь сложение двух int-овых массивов удаётся таким образом соптимизировать в \(\frac{512}{32} = 16\) раз, если процессор поддерживает AVX512, а операции битсета — в 512 раз (реализация из STL, по всей видимости, SSE не использует, поэтому даже c = a | b работает примерно в три раза дольше, чем просто пройтись for-ом по массиву int-ов).

          Очень часто SSE используют для работы с действительными числами, и в этой ситуации возникает прямой trade-off между точностью вычислений и скоростью работы: например, вместо double можно использовать float, и тогда в один и тот же регистр поместится в два раза больше чисел. По этой причине в последнее время стали развиваться различные методы квантизации: перевода исходных данных в какой-то более дискретизированный формат на входе какой-нибудь процедуры (например, матричного умножения) и восстановления в исходный формат на выходе.

          Конкретный набор инструкций и размеры регистров зависят от вендора и поколения архитектуры. На данный момент (лето 2019 года) большинство процессоров архитектуры x86 производит Intel, поэтому мы сконцентрируемся именно на их наборе инструкций.

          From 7a7178e2cdd6f3e2e860f6325a14a1d01fba8356 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 13 Aug 2019 12:47:37 +0000 Subject: [PATCH 166/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/linalg.html | 138 ++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 59 deletions(-) diff --git a/ru/linalg.html b/ru/linalg.html index 4d056a2..aa392d8 100644 --- a/ru/linalg.html +++ b/ru/linalg.html @@ -115,12 +115,12 @@

          Ликбез по линейной алгебре

          -

          Определение. Функция \(f: \mathbb{R}^n \to \mathbb{R}^m\) называется линейной, если для неё выполнено

          +

          Определение. Функция \(f: \mathbb{R}^n \to \mathbb{R}^m\) называется линейной, если для неё выполнено:

          1. \(f(x+y) = f(x) + f(y)\)
          2. \(f(ax) = a f(x), \; a \in \mathbb{R}\)
          -

          Примеры:

          +

          Например, линейными являются:

          Обычно хэш-функция не является взаимно однозначной: одному хэшу может соответствовать много объектов. Такие функции называют сюръективными.

          Для некоторых задач удобнее работать с хэшами, чем с самими объектами. Пусть даны \(n\) строк длины \(m\), и нас просят \(q\) раз проверять произвольные две на равенство. Вместо наивной проверки за \(O(q \cdot n \cdot m)\), мы можем посчитать хэши всех строк, сохранить, и во время ответа на запрос сравнивать два числа, а не две строки.

          -

          +

          Применения в реальной жизни

          • Чек-суммы. Простой и быстрый способ проверить целостность большого передаваемого файла — посчитать хэш-функцию на стороне отправителя и на стороне получателя и сравнить.
          • diff --git a/ru/karatsuba.html b/ru/karatsuba.html index feabab9..08b2d56 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -71,7 +71,7 @@

            Мастер-теорема

          • C. Если \(c < \log_b a\), то \(T(n) = \Theta(n^{\log_b a})\).

          -

          +


          Доказательство. Рассмотрим «дерево рекурсии» этого соотношения. В нём будет \(log_b n\) уровней. На \(k\)-том уровне будет \(a^k\) вершин, каждая из которых будет стоить \((\frac{n}{b^k})^c\) операций. Просуммируем значения во всех вершинах по всем уровням:

          \[ diff --git a/ru/lca.html b/ru/lca.html index 34de788..ccb1962 100644 --- a/ru/lca.html +++ b/ru/lca.html @@ -124,7 +124,7 @@

          Корневые деревья

        2. дерево парсинга математичеких выражений.
        3. Задачи на корневые деревья весьма бесполезны в реальной жизни, но зато очень интересны с алгоритмической точки зрения, и поэтому часто встречаются на олимпиадах по программированию.

          -

          +

          Свойства dfs

          Посчитаем для каждой вершины времена входа (\(tin\)) и выхода (\(tout\)) из неё во время эйлерова прохода:

          vector<int> g[maxn];
          diff --git a/ru/mst.html b/ru/mst.html
          index 27763f8..f10e69d 100644
          --- a/ru/mst.html
          +++ b/ru/mst.html
          @@ -133,7 +133,7 @@ 

          Лемма о безоп

          Все алгоритмы для поиска минимального остова опираются на следующее утверждение:

          Лемма о безопасном ребре. Рассмотрим произвольный разрез (удалили некоторые рёбра так, что граф распался на две части) какого-то подграфа минимального остова. Тогда ребро минимального веса, пересекающее этот разрез (то есть соединяющее их при добавлении) является безопасным.

          Доказательство: Рассмотрим какой-то минимальный остов, в котором этого ребра нет. Если его добавить, то образуется цикл, из которого можно выкинуть ребро не меньшего веса, получив ответ точно не хуже. Противоречие.

          -

          +

          Получается, что мы можем действовать жадно — на каждом шаге добавлять ребро минимального веса, которое увеличивает наш остов.

          Алгоритм Прима

          Один из подходов — строить минимальный остов постепенно, добавляя в него рёбра по одному.

          @@ -231,7 +231,7 @@

          Сис _p[i] = i;

          Эвристика сжатия пути. Оптимизируем работу функции p. Давайте перед тем, как вернуть ответ, запишем его в _p от текущей вершины, то есть переподвесим его за самую высокую.

          -Паблик «Странные опросы для спортивных программистов»
          Паблик «Странные опросы для спортивных программистов»
          +Паблик «Странные опросы для спортивных программистов»
          Паблик «Странные опросы для спортивных программистов»

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

          Ранговая эвристика. Будем хранить для каждой вершины её ранг — высоту её поддереа. При объединении деревьев будем делать корнем нового дерева ту вершину, у которой ранг больше, и пересчитывать ранги (ранг у лидера должен увеличиться на единицу, если он совпадал с рангом другой вершины). Эта эвристика оптимизирует высоту дерева напрямую.

          From efcf1adfc7769c97bacd172eeee2e38b740c01cc Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 13 Aug 2019 20:00:14 +0000 Subject: [PATCH 170/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- tg/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tg/index.html b/tg/index.html index 860e913..fe3a406 100644 --- a/tg/index.html +++ b/tg/index.html @@ -64,7 +64,7 @@

          Tinkoff Generation

        4. Метод сканирующей прямой
        5. Нахождение кратчайших путей
        6. Сортировки
        7. -
        8. STL
        9. +
        10. C++ STL
        11. From ea2407eef0b052b926028cc7629b68f0e4e1a844 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Tue, 13 Aug 2019 20:02:04 +0000 Subject: [PATCH 171/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- img/centroid.jpg | Bin 0 -> 16507 bytes img/chernobyl.jpg | Bin 0 -> 87738 bytes img/complex-circle.png | Bin 0 -> 129615 bytes img/divide-and-conquer.png | Bin 0 -> 290262 bytes img/fenwick_ranges.png | Bin 0 -> 14161 bytes img/hash-table.png | Bin 0 -> 8577 bytes img/safe-edge.png | Bin 0 -> 8617 bytes img/scooby-doo.jpg | Bin 0 -> 115865 bytes img/stasyan.jpg | Bin 0 -> 54364 bytes 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/centroid.jpg create mode 100644 img/chernobyl.jpg create mode 100644 img/complex-circle.png create mode 100644 img/divide-and-conquer.png create mode 100644 img/fenwick_ranges.png create mode 100644 img/hash-table.png create mode 100644 img/safe-edge.png create mode 100644 img/scooby-doo.jpg create mode 100644 img/stasyan.jpg diff --git a/img/centroid.jpg b/img/centroid.jpg new file mode 100644 index 0000000000000000000000000000000000000000..79fd12f0e8f9f23769ae99cb32bcb69d1576b49e GIT binary patch literal 16507 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 Gk^c{abR+Zt literal 0 HcmV?d00001 diff --git a/img/chernobyl.jpg b/img/chernobyl.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f4f66df5659a1516f72f4e30b31680bb2e78cf1 GIT binary patch literal 87738 zcmb5VbyQqS&@Vc;6Ck*|yEC}M;O-FIb&#MZ!3pjv!F@$WPD)HpL`*{b zZy`u1FL_YWu+Y%3i1Bdni2r}%?+Abp6A6xjjDkc6Kqf>&Aw>E+2A}}|02u$a_WuSF z@=F`hF)*=S#FQ_h|M345|J?@Qq96f~2~Y?C03>)#t_B%~95Q+ou{c3}TlP0U@)Mux zX?1m?=sKosGJ#9_D*ls;2eB5Iv9+0wY>NnVjnRQa_{zWpnViJF`s z4ZXSy3rpYX#|L~8Rki2@Tz{Zn)#z3tHF0%yG!e7Xl4zFJtmfJmRl`n&^x5$7i^rb+ zZ!wnMwD*C~g2d!clZG(gD#<#uyp7GzmkL9AyT8BX&_iF51$EoneN*H2MQ6jjX;SSU@2LM-GWm zDc*2Nfu}*O$6*mq@|BHPlXQtB8H;F;9<)J=Ng6%G_@F5e2;w<$Dc^(`WJGurU5n95 z^)+6D0=2f@K)kK* z_bqjnfL5|@FU(4l6({vruX?!kd%ZSJ@OCKoOtqRB>&xkfjS7z{V-wnyf;a?eqa!gn zIkltF)eM&+;{pK4cv=7-B&@~5_JesXR9$3J zZj{a=USeM5kxxh<1qDoIP7%h=$mQIn@ZOMrR=!sER&l_W``wt^gsS0-HA?ehDJk7> z!;?hrdg8V33JZ`z%fdfJVagO+?Tcl?efWk;rgDn!2$El`U^5%Y13+1G92~gZBXYDogH|MuQlyF1{Q&rA*b8HwXqWw^65?e$}>pg zl!$mwp8F+>W|(2}ERW)4@eX+zOVwx0;QJCDe; z7*qr59V>pMK&ir_Vtz3T^C?M4<#Mm2XyfQ#k0m8##>$Daf1h-f@dRJy<09!sA=yJS z6yjL;rWI4b)g0Jl&He!{9)0R_E&q-cbAw|)?rmp{iQDd}kA@zOV zx2ta09@-r5phE!sF4=ee%1XG-N?}9l4tD6_z41jLVfU`|6)VK>B-a0cW%^4ZJzy%w z$>k6gzQhD8*647uof%)It@dHE|yENI?rYxd~}j21xHr}FJ- z1IrQcK2${8afO^E#K8;}RZsr9w}|;_a=tNeE)Kv+Kq%rooA(+!`_ptx7V$D`wz@pM zawq5=nq4WU2Z%*I%d!dkY$;G`hz>(_*B zPXxtw4h?4XKjyECky%NWxbHy7yv11a;KpJX_|1mFHM?=E>MCFWc6v%9;Y$NWrRnNnwxI-)OzCSW?Yn=wTo@It zM~vEc5sST@J%ugqsuWa0RXSSLlW`Hmd;h$h4~YQx&rLf!SH;meT!h%({eTbu1&D1* zckK71{Z}=zgs#oI|ET}@LxQ^(cW@fIr3q4^<#JPF8FQOHIzm_s8>om9^#KGNk@#uC z^vW|`*@`MV(oY*DJCKbC+1m7KUXjRSc^5f!Hlc?4g->`fXvCyCR}+YN!zhI|); zB&|o2T(==PqIYTN9ghy*^ashyzC)S2t9;bnlG<>Jz#HVtW=AeD~G$AVysTsUAgF5h3d4)^uxSAwrS}}gyoD$f2ZdJ_Ker*0}f}mrMOYq&bHqDnFr(~t7MzD2Jn9k@@ z9s~8?Z zV6I`S>#V_^y_lbDf0!HCBTrF`toKU?|IG1rX!Rci1c~J0mR00u9sX3-U*KX$&$MEneH(U!_7F(izCLOy_&lDEmHk_wV5#9&fj zhWUG_q1Nl!zaewQ&F-;tv|_U;-vEY9Rq72+b4UWyISVChazNwWLc?YYe?*^b zt9jZNHJ7dldWfqN2&OPaMAO9CIQUC%y{2dckx7x>b1Gkf(drCp!f$^^6ejs`L0>Hg zPFnv%VKy6FoSQ$mVR;qM%yahmVcnuOinOr{ellwGL6I~k15K?>LFJPay9W{$avdcy zq%xk9xBKV$vcyx`rBara#IX};Vu6G;%QTb7@v3I0hE`qi=}3>IgSCr_yK>RSwLlPg z^_Bv?uO_I<2tR&49r>``tK5GYRh-^IbcJPiPTj#OIz1B9~QGT7H)hY$r^lLItk#xZA%9!#3T?QeVuqmz(fD7U$-8u8*72bcPrCZ(t)%i)}7=xH1QSx^($D`eqV!}iIp@7kM>8}Vz4 zsAy4bdur*5%Ez>HP0cYn5mUD{I+j~EodLrYm7t7)D+F2|P($f=#_LJ@sCzyCs)oHs^wE^j>y10182xTtDUpV;iR1UDh<88kZ{t>P z=xmtSuM(LYKJXK9XBV2Nx5lo^VSMr!+$)B|9yppk-M9?$?Iwg;N@AV{70oErv%9Z; zK0QyCv|z7RA;RtwarqZX8NcFS0syw}H1S**P&rcwor6wGbo^6Dq>Ak4@5l=$rWKia zRF;sDRO$-Sg}bvMDmnOlll#TjHN2sXni>02D*(>14=&u#@;E{xH$+~*VycZ;=`Al^jY$#JECA0 zV?Iw3<+0!xAD_KsbxJ~iuh^C3Vs1ytxQCGe%T7oy7!f;bo?S5Ko{1|VSHQp8p=QKc ziLGSc6Z+#%f&Sy<{-d$C7{`Q}>l#Zg8Ch-_f;Su(_sZCN{`~W45rnq=_6HPk@cH?1 zb6Zm+^OUAi^iSYIpVkC&FFOGLIKpj&l--b>Q&$cPw4%`j>pBaP$+-Gdu9V=^lPiVO zg+UjHliHS0bbxoxS7|}MP~fpzV0psg>ZCc@S9N-HN7gbu>Mdjc-fd&IO&iOAm?cB5 zPCwhfE6hlBA~$7rHE|eI16e}^so_JNwAMyk1$g&3k=GGEoXare4Fu;7`@)O0R^nJM zr`GHQ!(7DN@oCONf7X0wu)@a$lE?~t8pn=`(JcqG8t(*OTd^5!Tej`)F5Xx;iTd4F z1xd`pPgd_IiGTe5Fo?HjRD!+SQcrQ$~B@<;M z!^p0!CLKpIwZQ=Xrx@ofOnrrcloJ;y;Z#vRN(QhZz-e-B^@m!ocW?7%qzO%DwGX&U zgc>nR`L!I6_pdyR%>^A=^R<{j^et7^t=r|2+(i9HwZwS3W|^4JMP*t_f}3T^5IzGE zi88vGuY{{u_|c;UnS*U8X>sH5%Inm=6TF`hvKh8s>2XaNNzqTS`u;?16IUN`1Kucn zNG%TidGkEYF?A(+vR*-)Z#;G-DTzmd&MsPAQStr*G5e~w^mBE{-o+Yeo@Seb5R;Kn zk(HK2d6$ux$!v7}iTgn{wg8rWy& zb(s>;Ts@!hBD4~LBLg72+E+B#GJFHYh7*)l`khhCj_@0O%^Mo~npbbcN}=|h094D~ z)WV&GUwegr2AcwA>GsEALxo0%tR>kH$3p#?X-kgHvZ(qgvCNGWD2UR zWnw(=v6xSXXk}@*m`eJ)2XKjniqWr&62VvbH@;g#|oOG$LGmK%lU(|6FM>kbp?VhTcv|9!R3e- zSHR)UN4EherhX=qX;4qhXzDm4y7Jwd zMtnc&+)>~(w%PQ6cP7d60+9|R_4Kw55B*qDY(w2>qZlfVn76h6@NRKVFqlzdLil|U zPpO-nC$~VfBZX?6;bC#c>Fi2J$BNUxdk*YmUl^^om5xjSoegx=y(F`3kZI4?_NqA$g#&>6@ zx#rHbXU`MK5)7sD<}Ez6hT=`MsiqV)jNb_jhBxgN03}mCEPN@919u~QwgPV9ykXod z*VfG;P3kON*~&vBtT|QASshY;0qLk^*2F7igiDp4w8|zv=aZXnuP!!A0jT5v!q*XD z7Yok0p$o-+DgQWU{0ztJTiWw*7g%j%Zv9r3NKEe19@c+(u>A$@-qiCp+Fm?WFgt>* zf!3UwQHzU+Ht=%3`1U|BfU7Idc!4OdIATF#a>H|fc74d298cf=Bh8QB9x&R|R;I1p zq`PE?lp;yG2!qhK!QT1a4Li>oP0Uq0yqgp0PQ@B}u16Ga|4H4_JH5>wi_veAC)Z@8 z?h8EF4i66a6{5WeK#h0wB+s!+yJim;@5p*hnpM<;iw zTKCRv4H~r>l4#Ho*K)%5pZe9p6};yaP+8EBu<&Mhuvv^rL)I)>iPfDGgmro-V^fo} z!P|H7!`lBFR{x$%3eWea{6E%NyTyTl8~*V7WM`{W`08NMUqGD3y!H9E#g&DV{s>PA zvxf7Trh5FZjS?A*lV4@`5m^3540y(6ddb$m{sLOI{sKgvtf`(pPHa36doOAa-n~V< z1E6?tn6@rUQe5mJ77AOi&@@+1xJqcickp4Evf%M`dzVs8=(iqK1vC!om5nchzwJ%m z7p-$`jTOY8@`=#_om#YrT$|>3vc7qP1&hsh&vr~>osYUt3-OMd{SRey>g0J+y=ATM ze*HQy+UG09?745|8VR3?Zug2S>@at!JBt?j5*ED%lgt4 zsx3KnP?|%5;aJ9Hgb-QiO4;L|i<*^3-TcZs3zq3U#h#z=RbTNyy=_2 zdNi>xfSfqdu1mF)sFJ~-ql!Eodwubl?X~f-@W**GYh)vyBv$P+!{CYTCVoq*IQ9A6 zp8AsJ5N54oaABNeNTHWWabvSPXS>^9fLYJ^hsS2m*$soqrM!XDh>wtpeagc6`!XMd zFCX1n1FU>cGA<&h@cYY#l8+pI_6*D#VyXykY-i=>;&r;|273nxQ;rY?cFYDyB|~*M zRo5`5X3Da+T25#8FEuA^@x&-S3=zNQC>Im|LhM3FIZ?KUr-mFrV2T?pu?ZX zW<6Ta=(8?;bPI8)xcVeX8^Qa(% z-5BOfMCm@KF^1a#J>J%O5lS5MGbj9{5C zyW>;Qm8)fIEeTlgRs4oGg_O7NFlb*^OfvXj_R=yz`IsHKy2#;@*{GpKH}E#V1o+F? ztM1YD(w-*Bz$!Pa)ItX`Q4m>31J~@y49+SE&K`v32D|4kv{OH+ju}^=WkYQdb|bsz z-2+Ro!r)y2-PLEouyAer&MJG$8#5S2uHihXq~XmSs1T&lJbwOoc`d-Aw60nLn%eh2 z3LXyVuzuTnIQrp-10i=4aloIwf83q>JG#u#uUSMO6Mnm0cp<6|4FzoM=Tm|utQqc( z8&S_N7}oxRjE#HzDAp>n-Ds||X+NicdGYG`L3e&8#V^D(%-*!TpluHbvo@sY%0I;c zHi~?a07XHl3MFH#HD9@{9aR5AL((Wei!$37azW#_&=htuBw{sq+ZCrjK3^R1!7E~b zsvz07X~F&RLF%lBwm_S^rxKK$nbZu=N}pLtA<>>NU)K$j=QR0*y{4!Oa#|MR2?x9&4cSY}M(^-cf!J zc%3-&?lZ(u<28+K4|rH>_xKS3;o{tB;3$Av;P)-v!LP4bd0pN<=fk$FBm`EVLwUzi zLjn>(l@Q*lQrJn|L{7l92{fU?K*v}p)uUQ$5$HGDqOebEUJPFKd5h$ybCAmq{1Orz z>bz8QoNRafo~iFN|A_rDsHWz%-U@J`=^27-p#;ijBrwbcHkO} zEi9QFs+JjOaSRs@KDB3|3UoIRqjgI)e&}I7&*|d~8$NVaM+Dyizj+flTek2S(-q%S z&Rudy1jc*yKc1_qHqH2+N5M912lWc^;8X6o1zp)%GUO?Kqwjt2dTL5~!YQ(gOB09j zri#uQa@v1AjdmO6xY`d|Jiw+OW=gkyoYtH!jMnfvdMAZ_RI2it@K6vn`*Cu{Cx?0Q zMLdMb+R??C_vj?D3;Po4czN7tnG)32R|W;ig!A_M4V=!nQc(p-b5#{K>mbIYu2=(j zr*lO*bY`>N^@Pya1 z5V>{GNI-elNtJWWh7Bc;j+(%hJKxef@+H{S7bX23SPvWTz2I@p>RHZ4gLg?s>yF(N z$;@FbL)5j;eieLu77M+Ql^k9uVQ@sWCcP&+o>JGG%W7FJlx)$UWKZ1e7o??qbx`%>UlnZ-mKGFiS+YsSLGT#paeT-%GUBtUb|JA3aSn@S|VqR_IV zi$T*+orVcsa^bN} zyRQ^~T$@Pbze|wrGx5$%>kaKId1dIB#972G1tV~al5dWfKRw6>t&ShmNPfFW&KIkZ z=F=A4hRVQJGzR@?^PuzJ`tArlhfU1Wy6SEfsS5nuPW*&?DdIAa`vSbdL}Oi|u0>bq z0XD&#*Ts>u^DJ5huo^*lsI!%&w`h{ws%hhuSKkC15vb_;HMC+~F1~9y#P9ZK9*bU0 z(N4KPpn3QoGxwOmCi~@YHVM$5XA$jJ0m(4V$bvsPOPlHCS5IO5!qT z@@19`2OePXdM!k5a5`)DV0Xgs)xGO`%!{mwH&>00xDk%M5-;xR;1P!_<2pgj3(H<- zI%m(*Vzq#o?-=3jgU2_^BmD21PYF6%PPL7;Duf7UUdb7v%%`6T6*z~hnp)+BCT`WX zFCQcD=!tjDHLt*^lEyu3ER)tIzs(-7e)laS4!%SA7N>m?1IbUJaM_BB2tL7MnTUwV z?+tS7mjm!8s+PP9*G4v(Eqv*(pjpx15*q8StPeC27h;oWB|+s@60UiY(+kNyOLU5$fMAVN%j8$su&`zktx;U$yhY7{B8_8^_r~{sL-p`vbnC zGvPs-byCZ5bhiSJp*6x=t^3kWH}9c6OWMgrrk6D|l34IBsbxIDf$+kx=dfo!T%w~? zLlRXIpsO(a{kfu4@Ux)n_T{Z}N!288U-Pe_wf#and^r$s>B#iVHlKR?7h>=3P;Zju z?y3em8P8jtEeC2wf{_iiDUOgvHlX%j&59M&(KW#r?U0;E6V5VKp5W=3aaP;VG&yJU zSuGAOgnukNaT~N=Shdc#l55RrUUJZ!OE*D& z0q^Z^3&BROsTS5$HuZI&XPH)B9Y<9s@w?aT4o8NI^*Q|c*LWpl!82_u;Fa7);G;rg z9ILT6&FqR%GR?G*4!GkI`qEM@v0|tul(zhA*0Pf(*xX$xgSfF=q%v8HfNmV8fDz*sLRB7LGFxz2l{E*a{ZD09J-Iwj4-k{|uG8OjwH8vlY??x`} zx`b3qy)jb(!JQoA0&K(B;BjECz%u>&^7OrSJ2ZIsH2&Vks*m$seP`Oy?AzaM0(+kN z$2zCc-ZHpvS5s*;TIduJa2EZxLnzD)-@0GMakoJ~XCZMTCfP*HhKEn4{mP>&8N=Fy zDuakrvknm7R()jIx{RT^w&v41xRI}3s5-gwI<;Kp%E=V*rvqGDpDkjQJ>K0f>+oF2 zfq!hB^u-q{^YX3`n+VugSBf%sjHj(cqERh!qOmj>Ty*=yXdi?lop1PCk_qul>6Pc? z)yx}^I9&G-;GqH4=7C2vn4N zrN?!vLD`qMJMovOIy4mImykac)R%z0f5Y?sCoGSUfC%F?g8(rZJtGsppr||vsSq=Z zu$Y_ztD>%m(o0kx=F81UXvkA9SctV}qEB*Gy2GT977TH4@S>ogidFe~*3UPr!{nQk z{8>V%%k@aHy9AuqKRT9^Q6ZceE0ITiN5J7E|2i3&#fl3PT)&#&QnyL$7T;_tLJ54u zEC@_Gc`53D8>eg*~09J^Xa#{p{IFk zg`(nv8^%#PJ1NbN*myC_M7pIO7pT?iBv^pVSwq)Z##F96qPK)Ob_2G{SI8}!0YIX8 zR`Zfy27dvWY1SD$YVAaF!}2E~4eigxeOT&E#xZ?QJot`!y_fyBIpiYT%g2lAyR<1F zY`sbfmalGp5>4aiMWE<_%2BVn*14hB{L&isnKmzdcdWik7TwvEokQ5F4IF3bk#UVa zRz<}hT60tXEdFX~Gz812frvRhE`QfAetFclyKF2U?}|R>`2!nqYt3rf+;?5o!Gy{@ zVM@H*2bPX0b@bfTMGd7hb2b$SiI1a;bqD!hz=wZie^|GpmC?rBu9DTE*04G1%Bq$A zWR2(ER;z6iw6ai=*L$J{Gqfq;_DhoI=qMd=B#>i<BBKz zyWx%3>Z^N7{e%C{*MmhxnR0sb+*$t8uhMUWz5eu=$wZ$S+upzO8l|tIpcus3O^H?^ z>th=0{2JWu0)MyioH2VS1+);|(KaG;QIu2+wc%`H5utRyq6GxI(8KoXIjqkOXJN3DWmpDFb(W$oD)LgxJU^Si67}?>C zh~74NaddQD2L+3mFChu|_{~d^s6)zi$Sbn!@9%@GNw@bHVAR{%UOqXg;F@f%TD_QB zFs9ekb`-g9YrYwdf(;wTjIGPt#by)Xq7Rkv8cJC+(H`P?EtHJY2B3FW_svX9O}~Eq z^CO;2OEN@5A5l^|T3}$yTP1mi9bToK88Z8Jp-Df@kMuDk#lZfKW3Eb!I}6ov$=Fkt z(u{}8<@INrPczi$BU9pZ%fNoS5n#>a=^W# zw=PObI!z1jZ4okzXmdciYXRuhEk&zir<<;uMFGswZfE5szrpYVtVHCS1mR&P=D|6p|g zV@Up!=q_Sp2_qfh0fH6fX#c+$G(fQYi$oVAi}?S#?S*;C^CI~_^B9+xtfmJLHS2E-~^E4fMDQo&|pIn*6Cr~OKG*~cRcXCe%(yvy^8432#`Up3* z4Mq|>5UZQU-<0W>WsZ%^qj=q>`~?_&-K=fNmyMG3{&lyfOC%dAM>kaZYM(&pc^mNi z5)g6!?td zd8u(ruv14zPigD^`D$vqm3Hc1ZB(gmSz~x$yfw4v$w5SHqt7bhZmYVq^0|s%4 z^+T`7{qKhW$PoH$_e>_H^}?U1FcVH?^-7;7tjSUir3}RcUlxWcz7_31&wh0d(N(VI z1`Jg#8XCXj@x$zVC=-|y@PK+S)vU4w@4SQgcA|1UR6Ybg15V+UY+6LlZ1d9l_nn^P zf=ZY$5Nsd;R6_s12oq2O%xZ2x=++s9Irsd;&v#}vDkpydm_U9rCaVj9sVH!h`l z#sQM;Ches}#v1eQc(iEf*_ytSuG_nJECzM{=5DUUGjk$q=f=VkB5^;XG!wjqKi6sE z==@F$-6#PTc=Sssce?Y*^|}-7_VVp9EZHr-*$G6j($vcIE_(HXDpiu*RMVii4&vVU z^d)i4FDmL3Kr2uv-@NEYrLxrNL8Y=7LZB8P%`^CRdRYo+8B`;*Hxln5W6}IoJT%!9 zM_3fw3^^?Z#oNcxDTZvQD=k3UV3ns#r7syNw^dWq^B}lGO^SnB>0E`H@;NO1>DBA; zH9sLQgWgbgX%b6am))+~h1aWzk-i4CgWQoGy1a174~Qs3FNgDLEn7{W?f&{EV%0x? z0se{I|D1NiaKFBlwCj0UCg0$f7%8i$b^Zlx_CyC5{CvlLL8Fmymxlp-D)d<8HA#!~ zL~i;GPw0s;L4<}th0X74zf*wE8IPYckGuA7nuU1;PWV)o-oKNF`c#Ip7;cF)tM_JC zH8RvlIIT!>bjYS_qe-s*o)LQmW@od|J~?M(iM14@Gq+L`;pk#j`EEcJ^U>N$M8|Io ztLU%`;fp(quzh)fS2#{I6Qjg0&e_v@e-}8~XRY7X$)l%#c)#?zjN4S{pI$E4-BD?b z8Q1xDy9y}E{xl)*0+lV-8B&_*ohlrcW>0EH5fU)Wc8aaLcCNefpZRRaFi`_F&5VG| zZeyt1Yv|kMi{-lZvKqYfyQ(AIi^e?KLCM4oM)i(Kmf6WHRK%tHA~G?Kfc6(lz5C;jp56;28PDA8V8uleXg&U6nkx zRA`Hho7?4j&LWNFoW=cOz!j(;N;yd!CCiQN2gTnTzy`%d?*9U+l{ucI!7F1=d?^*% znZ*N#zxvh-K2#o3r6{nI9=eB@C_5?D{%AU*WY<-G8S(S(AHPBkC>E3G^xnUJpDG`< zIMBDf>OF46A{N6dQOiQ5c)qziEt&Kb<;}<}n&TS3zLAfkf)1>cz!eU5WQply6+?82R+ILd9X(uU}BZkgW|CvxM2*jHK9gE?PPSX7Gh)_H~U;(uZ7d7Jo;n+5+h0vA90r4rUi z-q^KxwZByqe)Uj7NHy4Kt~1Ol`T`NOeUA<_mkP$)Ee>myw1-tINoPcKMU@2(!%}9K z4)eSsOrcys^-%gm`gj75Smew!-wY>p))|eNV0T9~!U$^P+0nK;+NI8yx|Ul1t+3T^ zlAFptN8_wn37Gc@T%o2X)ihX>gOk@YD9ru_o_o~yJoppUmo^RztVCw5uRidA!mLetv%3&V_tbGmJ$hRbhwN5ghe5a}%2L-C`J2 z+hUQXyF|g%z?z@iH~uVYAO&kCoUjZMc2FZnW~v{9Q^I_Ie#*7-t&+|a#gt4`oNHSP!~4`;HIt{y6CJT-y>8cq@zj=nh1RSJ&jIib(C$ z94Sh_>Lc~5&2}Z!U$JqD#?M%aj&);U5S7=SDYd7KBR-z}!%T9k#?%+MLP=Zs=JT23 zIrvol&9Ztnvb!=tT936XmJsiFUZ#zB+!&P=zr~33l#ZR+r%&|iXR4i*1M8sQ(`Uik zOtEQBxdXpq2)3!XS;Ch#iGnZ;B3C10RlcP~hZXK| zgqx$`hjdy(kXMUKp)^mMPjwn*W6>k&L<1a%7QgW>Rm)m3%A)tI)VVpn%Ci}S z)J6h^B=jk9JH34bMDh!&_uBKRZ|oAMds-3>fw<=G;*B~|}^%E($uj=^0{OQ2)+RK#?rp4UpXXbEZcRMz)C`Dr_yF*fptH&bjnGsL!l zc!B`aS9C@(q{uYNa3>b8_`4)bw}CS$%h;v9_w|?^Dfwxexg6ci06)kvUL(E z2rkw3P^lliPRS>|II%5fK@(KTW{$ToRBSflq!x{FGI9NgK-^ zx1z1>bE#?T1I1>h>ZW;oB%XItM@?tg(*S$%nr8Co=a@(6bnnZD;PeOZ8HZCK@))UQ zIg2vZ6FaHd#+HP} zSgGt=cU;LQF{szKUI&&?oiP++%eUV= z;ohmc$Sdpxxu@-ICWs7!SPZGcr{L**RafNGCe^RGn?iR#>G;?sst0z7ToLgQe5o8> z87{vnQ3>G7x7)%HXJC%bD>h5jbonl6{CR*O?STFu${}_RFV;Q>v#rd336 z!dvZ*%x`hXbcMF|#wzQGEmo+ix^Q$`ONs?GI&bRjAFBDxCd2-M{!9o`?Ppu1Mgsok z$0^<@`p6{qA5=OfCSAhW@WuJ|zBWAc<*Z}PbtjsY7u%3@W={6T>Vm>|ar{mc==uSj zAcow4RW25N(W-VesKH?Co1(*vxZT1DX(54WqtXwDvmTc3Z|5WZg*Tf&&e*Q(b>%9d!ocM&VAKPD=i?_RdN(6~(pRr$;&8qd(4cn5Txl3y%gtuq}!Ib=EKn>w?&} zXLZx}NZk45NzV1z;}vXmj>#dj^7q(MwA!-0$S$?L{Nj4@Ho~a zLBUxr)+IBNxGv`Q`aD>yWvU%+Mge+1J#`|?KwJF}!)d^|d}~{$|ZC|5F(Ar)EtvKOO!GV;dI4o3c&lj)jH zggz(k_0p<;0AQ%1z!eMZVmK8?4L;HK zKsYO2LWuZr*;;KHD0jMd3nQ%tJd}D4LU`jbV*CvlIuE7xM|um)9ZuwdwPY1CsWXxU z(CSn#ttJW8Z;j$rluZ2F2y)h`tmo}sm$TVtHIRz2Xxz4Iukh9>&*C6^Sl&;#Bj{Aj5sy>HIoI( zVhi_b3K`YORdrj7KZiu9vHu_nVH>Kmn{JU%t57K)M+}cRi23Y*K6nng80a7zzretH zVH$kx-K2_0(yx?}1xn0N0}+bRh=*EgQP*}mAzzCPCLha4HJBHpPds~qqBjp}@RD|> zd>@DIKiHa;j`ce#8a-}(n#may4F+pi5Bk(61R(^%ht4i64$HV|U^~yB)v?ViU{?N6 zGDSTJitgntrsgi76lj2q4TtUnGykE9h{8PUL3KMG26Cv&a1;Y8E+MXi8MUn8T^kKy z;a&e0@WT;?LuS0Imc+;S+WE%j0k)#ic}~e>6^mMZJZu8Sw_Ez2Z!2e$Z+91mSiVSi zGEVh))Mc;f@tkClKOIsef1ako5G&|HfHqo%*yk2uA{8>`Iaf+?C>rPAQD%3Is{b&P z1?y9SE03wN=0QYX`yX_b>{+o?FkdOZKIR^#B$#F=5OP{P}m6%r>$l{(yJy|M$<oFhBQ^Ou-ijLwIg`&3DUY($Ycm|weN*)-WZI@OQE$}`y|4rZ zkWQI1%0C_QyN+t*z|7qDJ$?yK)ivf)uis|fua-H_U8r=>$Ms+l7G}-gjE)<;&p}u5 z_uyu0J87R%cd?Jnx#cv-$WRZfL(5Wg4)tZCq|1sFAns!eH3;)?rVzFKmB;@kHr(-x zbkp|hwJpSnhujCe@E{Z)s?Ye5#wuS=Gf6CQXohXHGU5gs`(b@z{B$ZAe zza;i~Xu5cJeFAeq23)IuTRPHL@QJgtYQ?1JB>Rm6^H|({HYvq?HjPw!{*O0fXN)yq zMD(Sr2fF}+s|?tUxm}%&?h{)NYAu7jI!jGFDdDL3sXnE{R3KhV9XRteFl}<5VE+U7~uk|A+0>_cE3DY!WPo z^uLG3)raP%?L)Tp%RiF(RkioKO?T&c;u}hMLyU9k!2_}K>{NL?_2Jtdudpbx;P^+f zuSkpj!RD~D;%<$R2^$g32Nf|1>jV;Mx#VmcbuI>Dfsy!Ps(8yUE2}fA?o8b~AS*jR zqOWXX6jsMcsR$bg_3$fbB=@`X5B0+(J~2)C&m{&Tgtw;*OYdx>JnzUI z2Z6UjvyeyrCZRIq#XFq{THP=-`Wu?w!mJ~6rzIVpq^1#u1?Fg=65f1WuFvHwl}vuQ z=qW3jTq+CJJ2JP5qtU50e)Iz zJO@5sTy_IJCA!AymUpX%Lnsa{{D057k}FWTtbm6ddh32%8fbu5vyX_j_F^l`+c~w(WBhSb))6?BEJ>3&FhdR(Pm06|bXOPn*RumZV{?yqI z30+p7sJVcXs1NsGZ*NDf2(pMWaeLnB=0Ic|U9QqEtDy52?g2l;j{C#Mb|wQ+s3Yr) zEC2oH?DtW%*5kFGwRNser>)R9;)!Eu8tVVT)!8}aefHFLG4^%UxIC9ODRNke7oX6P zOCOJeK@jGWJM~0=;6McPyOJ=z#RxzX7k(Fr*EnF5cv^pp83rT|^k`Cj;|n+8^f%nI zbpl18C5|wcJG2f5;3f+T(TYYN^GW*eh2Z>6ng1N*G%~Xor^^RcRBGGh( z%uUwhdE)9NO^wr^8U#oi-{sW!xCaYTEuoHR=ytT8XsnO!ej@UwvA}7;H`fB55~3aN z_L#YvrVI1x!gQ%Q+CI(TFH0uM_istbG~kS!%BpY?$I8lWZpm0kM-ERNsVoh>>eXRA z3w)KLMixY@?e%BdTi0dRQ_)#e+DzhQb+_~;tCRNVyO(k~N^Oo&#uG6aP?S5c(gb>A zkuKB-TXQ3cdO1;>-{vOx0eB(;pC&j+j+j4j4vKufR$;@rm7`l`YA3OiE35gO;vgOJ z$dAl|7+GyEmnu))Ewraw7X%@PAgk(+y9gRhJ6kG zrX^=r#(DLfwufE}c8?tSN!0^#Ld)rAV#*{>JSkU9Y9ga;T-UHs^<+sZ!fHsN$Bi1c z;tOAZX5{{z|8UR$k^n@-Wx@IvPL@tZM;tOLpgf`Q8?9Yj(;jbLPig+xfpLZ1h9PZN zl3EpmSx?WUk4WA!Q{kwrG{3i)LslmrakzIlfcmX)<90lGWGp{g5joM#6bVh4LK&h7 zHt2Igij3K$!1raay$XIjsk?nMuMUuIc^v=}DpZc@T$WIe z6&-_gX}sXJA!|V8hE3FC{>%U7bNO2B`j~dLr&Ssw^IGab@Ck{6Sfpb826vtm2cx63 z^_Y9)7IH{^ZQJOVs~>qmvHw+C3WE|xlr)W$b48BU@HA2<04C>4fkrr*93|p=RT2)uEWaYLvWoZvB)Z6& zj-)`LTu+demh@}ybC@i0o6f<~yZMg10o@4Lh0+K(bG=JRT)f~|fMS5A`Nc{g*ws1S zLRE6^ff2RFLY_|{Z|8!BpsTpySLaEH}Lp3e*^)!QEhPFm!*8~ zFvXz?Aq)R}-J1dZ?L`}B_p=1u+oq{Bq6KOu8UqDabfa{e-+>+jMC+!Nx2Dh~*j0RJ zbh1qMcO9KnZZdU|h!7tv>}0O^`II8)$3-mT+6Pju6#L=r0q?C2aku;K4FT(dflrn> z@DdK~aqZ#{SOnYBY0RG;1?Z=7v6<%7^)7YZ%2^$6sBWNEx-XReZ z65@Ltm$E%+^RyxihXS4}l0vVY+m17|9MNi)WM%jFwd^odapF=uX(WUVq4%XdEIYU$ z!Via48+RpPv)sh0F>Z6+1zb&lCowrzHhKO8ySgd}^pDF}aE6l|`J0n3q3*I~k-7dQ z-y+BA8o1xFI^*#cF+4wn20}5V-6x4=5B=*$O93+VjoKv3rfhJTc zpY0RV~(aZPP2Vka$S3xB+ehNqO6`@C+P#9Yqsf^T1YV^`~sJVwXW&1p*=~-}iRwz)L0DPQE#KRqKUOnAh-FFWE&MOjGnb4p$Rk0>1A$nB)6HW9C zH)4e?W2xRQGEqzoq8Na~8#ugjr>a`7rJ37C$`LkHfNAbO?P*%EA^t*uVHJDR1f|~U zNi0pFZ?b+B!c1-P!W_nOWd2BBX-=ko@Z>6edrKz)(RjlfX^E{X>tLe5ZLF$NpZ%ir zV^dLj%OO|ZDYe)Tn<#+H&n5^7L&72nhn7Eb2Diu&J94u`CDmkVz(}}n{CDhMIEWs8 zdKQ()V+jPH;?wVQPw2EZmAoUW@;Os4l#qI+?=Rd9p+GHdtA{D$3}&YiY9$R8GeSoL zeNt`QN4tJ%R33Bv?&bxM%=^D^<*et(lV1It`S&g#tD+jsM6W84+O!kZIfk=eS3qg~d0|z{ zKTb;_nwJXtSj?;5Y`hK;n+fCT2eoTJ_K))cgkKJPl$q%N!dV-~rhfS1tB)=71xe!# zL#!aopSE@qfW3ud(zu`jG>rH_6<^AAdD!jMxD{YcTcp6e8aQEwRX;hPJ@B=|^~>Jx zBKmk?=G!kA-Yz*qvDtS)CoXwjrfo3Gm4%3{7QzM8stR=FXD0*MHZk*suVZh;?s?@e zV?M=wHn$&M{46=1b`Y=y)Ssp=Qdd3t>4UW5Z$(es4%(i2d&TmSObSlWpdv&kN6t39 zTq<4TJ^^CU4i`!<*_Al%OZS8K3;S&bW3!70r$(Sh$5+TApzCCar4$-(@A_`imEaF;+X{0=6aY)ocuPi zY9TLI*hQ;!z=kwiY(1a~h@z8O_Dv4^1F`)RJO6$vr%2+_SV5DDOKuu0L=ih_JfhF~ zu{g?2GInzCJ6JBJPAlJqF$R4gU%`+`4;Zb3L%h>h*eCaW5yDqE`ldAS0$Hdf7~2sb zZ3B!Vufviyxe_2N0UFO2^+qzfXc*-!267pxC3>jau5?JZ3S zTQD@dvx-l-o>2Q^vGnQ2Yaxm{P#qovK=Rd;YGImSFyyl$ z^4=aFX}frTlJ&k{an`*l zv+J_eSh*9|1K{fq1Xv50TT~YKj#USXc`ow9Sr}#Uy=9j3*xc2gigF@8nP-iXPAoJE z{bhVnaq z5Bb-MC!N)&Bu}dIV;&Ew(9ViB6_$dO_L;NKngoh+Lel&cvL`I+Xw^Q!Qu?>vnfJ4& zw}y4g5sPyksTQp)0_(c=7+EiJbT7Psava7%HU#tDv6N-4^=$%db`R5-Ql7OhS>q;A zsibB?v@#N#<_*PIh716 zM%I#`nd&?!Hao$#zJk3CK4x~u@Y_0G}P8xm_N z8LBUIELO3TX0xoeZgLItaAU?ksbE>in!klM*eo%&zFtc}38Vn)xiswgGrQuDKz#?w ze%mkV+88qCgv>sM>TurUSw`TD%Tza-+z8{#x}g8Dit!A*i(=djkf=jS#3~G{P5SKd zM()w|+ZQ8QNxy_VzX2QH*iPjKDD`<3I#EMlfA72Ba^t69 zSY1G+9nT<25+3zmI7tklLnINJyO`T^-5r=gENC_)qM`-+;()wvGu zA&u9KOX66csxp7~EbIfb=6_qv9M2Ju;91$yeu0SkDaorO^)Nh@K>hy0Z7=&+P2?x< zhjBKZY%F4aqRIRVcXk$YG|KoKuGd&-aLKDrS!KSf2AHyiQUUGK&Xw|#hBayff@ z{TcxcCEza{STV1+2ofEFYfGZ8(9kU+o7bBovPtXHBZF6QOXw#Jm3M=3^2)*7!LMV3 zR}bmhV8KUqXS~Xz zZmMpweLlCo3i$Et&UipKal4w&2mxd(UY;?}>CA(rkh+ympM$JZz8nitEnV7#cxS04 zxH`PouKiNK=cOO6kKy-w>ve3DVq?Wjb!7RgJQJ~K>2&P=!eJo3jBHfQZ7+tHrQ`&x znmLFaJ-lu!;$HP9Q2vR^9*yEYFVLS8*QkYcVN{*Uq$YzUq3XV>D|U=kaQC zkzb)k8xNw6+_Tq7v*5-0NCNE(`GBsGz{-YvTe{3v)-N5Pc=ai^5Qh*UiZzF0n}Sa| zxSM6ckpe>5u5%>=n;_#Uo@067Weh)IIaT(NeE3nJ#rA6xVGhXo(6zkI8kpen<227fhF8$c-sk@0 zcU>v6n&q?UW1YoJ9s8Qbo#;r>N#p7ZIjS1wHv0Z+ec9-gQb5s@b)Weq#YdsQ-J3;U z7K_&(=MOP3Ju5g_+eKww@B9l_1&TKE(E)n4%O_)N1lNsx?sPk%g-|MmVaoWLZxY-^x2~ z=1IyEeayzHm2-At;!+->PNj2Vz6{%ZHZ=EA=IddYLv|2mx6?b8u79X3i znInZ^XXunKM$*odG;m0Txtpe%^@tl)=+|XUoeWM7ASAKa)F@rDZ=pG$Z+q>zEC#OT z>2cTDsyuC#2RHP{koE17IV-tGsW*Jm6ueb#G4vSH!?qiYZHXDx-UCCgcbAIFTU;gc zXAftVg>R21(AOIk%F93MA)&6!3e;2=73EA0h#Sh%;2%Zj<}p%_g73nU+ys1r`;`huv*N#K%bku0Bu(r2c)F3r7@>~$io z#cV%IuW_XZEZ1V?m%5weLma6391sprJ5`J)Da`_a1~LhE4>pvFitQ}etQ$S57a}71 z$(A?r!6$d5!=F%S;i*4nyVwX+LXf}OW*&&C#2zfgOWX3Bp4YAd>;PM4a{GLlHmxJ_ zvfPZwB8*KZwr)HPJX~Qp=2a;#l@a+bC2ns}7E-e0v5)kR&Ba=}a3YHL(zO-oG3-HKuiYBg zLlOin#S|rQ8ec>%V#~!h@v{3cm|Xc;$LE7>@SOj`Ntl%hAAak@lzj}^Ju4p+Y7cSJ z9f$&xD3PB8p&=|~CR`vHBlbHZYQRRn@Jy;K&`hft77fzIUMlzLpLruSTwEnU;*Cdj zDma;?xDKT4rNc&X340g$r7-(+ool3mi5^wJFNXp#8=5m;X3we=K{2LANiX7K+d@YK zqAZ1<$Aw03J_Dx>RDZVkoqT(wul|wEHjgKXOHHoZgsq$GI2KvqXqpSLct||J)RaSy;_r!)c%TZrX(+K)8hR?VF@>3|Q3bWoxEad#G>Uynpll z?W>X2|FDGm<}DWHJM532*>EVoD!muMrD8XPSN?|g@BQl6>)66??qstrz&CV963JDU z*;PJwIw#X{Xq)mA-18owc9HaH)NSe-0tl?Smq zw>^OCSzdg52D`16c&E zUS-j(sQQ7ftWfdxFmLAZd{)QC`PP+Bh~khx#e4sxQE{c1Lov@Twd4W`dxcOvd?eb{ zag|>sY1Q}QDqZ4%0L~zP;T(fEZZKWUKuSdOcxx@uF7Y5Jl{5z8>$g#e_PFo;1Lcjv zvLRaGE=VTqi~|;2JTnxqf)v&=FRhIbY;0m;QW=IU)NoT-Tn0>R$t}^!mjN$54h?W1 zyKZ#fUdAs7k|Fp)xDO3fyI{kR9m16&pFe2<#8s|cP?q>Npyik|^}5^4arz6FHBvF2 z=R-cU@5RvwsDg&PmJbEuvh&O)T%WXSrm#Ze2wIi1qmBbh%W`SG4>$Hrhlb;^cObYH zS&vlDBOJnDeOHjl&JZh?K}SMsz`z~OEzXHv1FRrkYv+4Vp;nUD-c@&DN_E&54QJWa zfP~JWz3L%A^$@GS_x9M;kAAO&X;g0Ud|N>4stG|I@kG6F5*1Tg!~m9;=V4H)d_>&2 zE|I(3^c3NpL6?^?5Jk=&KP?@y7c;pay+QTBkdZAhX;@jRp`lbglrx>Joe*>h%ESI1 zYiI3edd{v~qp8qGmir^j19LotAv-J&m&I-#yX&UwzC@x2EoZXIBA2Gh-FV$}EJs`h zo5GS#2xG0iR_sY4$X`k;$AaMzV+=+q1#M;9ct{PR++KhBFcU`qERD;dT)P;n_~$8B zb6lUJC$LIa6JO{roP4?ver%~xrYpLDrc|QuGf`Oay1{MnG?*`dWpHxq12lWNCEk-(g;M*rEc&Us`90 zw+1LL7UT67j$8&XJ)C-qR0BAS;B~z{7iw{Dp%NUMK`x z61YcGy!Xit5qvCd=d$I-*ShNk=_7SR7e>(Jd(K|hlXTend8q-SbjN&A`8A$vOJt57 z-*;crBBRK}Ois!8A6yY7e4N6Yn}nbu0wOOGZnT71qmR;P#HtGx2lOFKsY#UUSMy*3 zz45m0=G6LR?fjC~FY=K;y64wmGgs*wJ{SBs61lhSE$gQ1L-|en(c1kk!q#F7T6qXFwSN5;!PRpIm-@VkVU(M(S!m0Eru-6WY;Q}(W1eT z8Sla_*4G{%<_(MFSwW@05F;J@zf00(}+gDZ&Vm_EkGOs&Yp+$8ETDY=p z8*XTcc9LTT3X}>^oz>8Auu;n!iMjubcpI5-NB0c9UB`-G%{O+U6F1seA;KO&og~n` z0|*GI40Ztx4bOp)2-J{|>~?I_ft2iuq4wQUMtg~sy&;3^VuV$ckr(vGvqD%}rEDff zAh}j)Lq{N#6%oQEqa`ZJ>bKD-71>PJzadwB(KujKMHw6v-W2Q}+$)QM8cy+^GMs9w zvU(6Ri4tNYKp;v8!N?a^gwfG3M4*H=N|_x6h2v`C%lbc9^;``H_xE8i#nw=6N*3!< z2eONH5s|`xcs=N}Mz2`0kwY11#Nco4rF1_J-jkXN$E*>f9$1ei93iC=4*NvWZT>?4 zxG98*ZwwF+c_ku?Ax!Ky*dw^gKwJ$gTn>gsT-WTO!&i)6U_UP=x`}mGW4}Vq0Mn8P z{hh@u%?dk^4ZV}}0~%&jlk*u=E<@KuIV`>LwhhF1#hx%k9R-ojGZ4=_%#I@D zx>@c3Ld|!FF>8Y#J%lM_r>G0|Lo_tV%{m3h$%T!*oQH#3VBiA-O2-8br5U0(U&M4K zXzx=Ze=|nxzBU9{rI@E6o$NkqN7-rQ_7_ePgVdT!r_hW(WKwM3lz!0Xic#wO$MLejnor8lRE+Cir z-Q5#?2coVHmZ)ggo*s81^>y-*(Z&lJt_>3szAY=hGNLDWtfB;6H&kl5(@Uvw1PL|V zKy=?@LbtSC&Lj*9Jura=ybk&TWp?E`-ie5(snk^UDw2b^2OE?%%ft~B#5u4X>_wqU z#S+jF>^^zHN@QC9k1`y?Km6H09NIsMaPR)HfAp#hhlTkO`|~?WwpaEH=bH#FJe9-0 z%-P$2m@~2aJt;AIm{B}K#a3nwjl(M+z@*J!mFI(nD%;%q0*`k)E-EH=Qn3jmZ7aEV zhnU9Cmz(VF!B; zE{jzmgaSqdDy4B?!YGI`>JEl(%Rf4w zhpx|m&k8qsy|wIw+Y@wEl6T_oz{Ir4h9Z?NDef&pBnAkI4q&2~5GsI?Y5vif`CSbe z?O`M>ZNnl`9@;1Fxe}b9Vlx$fD;PbEm+d3<@)jG*WQX>)uUb}zRSLSd2PUw@KXhH_ zx>Hfli_l`tkuq)>Z)u%$Ac%GqS?#nOU|h!f8NbwSqy;|so@JdNnapl>5Vffm5YTot z#-Z`8%4ElJ^N6?B>E6O5Lgzqq-dR|s!6{4Okv@f$O~-@!C1(|uQL&{AH17?Xxt5b< zTDxaBpOq22BB2W^#y|M<)CU1 zNgy5<6W|I&6?ZKwDT9_fbcRqG#0Zl5NvXFfyXKDO8HP*65U(jiEU+3uyY&}t%D3?0m`-tkA!q3Rhl1qB z*p291Ti;B#e)X}ao|gixb1Azlrcn<%K18&-GBSb=GpH;fg`uqQSw_mo4~bvk2j_dS zr3}li;o2=nR`*hFm^OPe&hS~V`MPnAD3)kHYhV0X|wDO+&c%&=Lx z^^Oy11+M!_ITz1(lqfl%N@~kV@(2e8*?Gto^1m_y7-2qw6SC(m&*k#jFKV!_#HaUG z7@|yCsd&W$poc$iz&JD2{<1PmE-9PW|V;AvST3=~vvhaniE2bek`E@=Fs3QScC9yz(vIp-mj0+d$k(1F}R z3EHr2Kq*_)Ji%O`{>ZwsQeWIO)<&%-9e~Z$+lmz!D2ajMQiHt`P_thZ!Q_&NVcL5G z?ZWp;d!@lf?pj7&3oYrY^9eI&-7JZyVk2SE(@{p9odRwDWMfuWK3HX0S627-%ma~} z=QqC(Y(VgesRR5y?B|6CLlqy5d=^Wc!JYUT6hpTDN3X~>KV-jDsmfC-y9S6gh_%%n z(XV!^EekBUmy10*e$}%0o?A!C${gV-s)Lo_# zp&k%4(T*AyD_Qikz{I6hxoMWsGaRuVajtlGtu3e#k>^FspeHAbY9MGd=Je~&dNzKA zNg&ANsPd-zX3G(W1c#Rt`ik#xy72$Sfv=lflhc0?AwJ6x%Mfoy<#RK0dI>92)~WVD z1ipb0c}Z153Sf)QKyLTIi8r<9z+r-4P}zuFv#a`hd95H8)^!`871Y1Qn;JlAsyH5V z4X7gL(12Q4){EY;;^0FWyfMTnud<@2Unlf9=@Da(J7#D?I2I^b){2AVhWGSL zvBjZY8WU(0ox`Nh(H25W$z~OId!B?ljX=QtqWjbK$x0SRN>e7`3@QgftU{K&ctg{r z>CLf?65e{IT2ID(zsLKmogo+(g6+F#(k@hI zoA5;@R#JCKvX2kHH&(Z2m!9UC0wku}TBeU(pvd*vrA{dJ+Al5{yySebK>FJ#Cm9?d zy|YT=BL2*)>Xu*QzNCBpF8OjWbPF<*3dhYj4kfY_vIUq14VLU{u3lz{s$LtE4tt zXM&hG#XftTA&B#7p5LbD?6G?)lm+d~s;uCIvBAejOA9!3ix5?4SQ$9XJ)59_=?U`oo zBkkjyGE>$We1e_jgJW)qrCDb2c_$6o!XwQKg`ThgJxzpzO}!Un0Vbu80}T8snA!WmsvbEAB#?tdVM-r*8u`A z{@kl=L^Q6vWJwN_>FsriO><>smY|Ywi-WHYff6S_%(-mO>7GNL=}w6~I*bBl$t^?* z^tXE@><77Rv~)kGWtO)=p;7o`c)!}nIf_FrcqHijpOgO&AD#WrW&g+5|F3{$(QEzx6#d^D`nTx6`Tw+!=fa`6 zv7UBb|L9ltfFI6Q{rN{48Fn+*u^39}asNgPhxJjftPyWOjiRSV*G`%g+w+E9W!XB& zg{AEf<=PHJ?t1O38?0x}u9%qZ2G8HnT09mElk9Yll^Zu9I|B7DqFXHJf*`)C|kuqO2c{@E|GP?Q1uC^#jIk`}$dvD;VF9iRcMK7>5_LfxvS&%@F_RjZC@JTBuLtW`h*SVy@uMLa8aQr21Scr%GYPeNb zV4D^X^t9TGX{_f|suM!&sm^n55lZyWcscsk#ZSWzu!F=5y zZs;&$JW-5|3{u(&^4IP&ssfP_jC&z@(H$*x)fok^Cs>%_mprO8TN%rndVl6QX^*DoEZtAk!f63AL`ER9FA zt9soUkK6Mct-0eM;pikCTJJ+yFYPiIvs-s%#h&%i8}4nN2Tv$-JhYP6c1}@HjKOJ* zWCE{egulyjpqy9^>9%a%1auvw*qk#P%CGbi6`l z(Nr~q5$W+EN(~7zI#o<{OJ=Q25KMYgP{B*5Ymy*KxJhXi-S(b;}($Gr4FmYtH#pVp!DSOY5U2E=>G zdW*JVjj0C(0&>)wc<@YKfje$#c_Q)xwlAT9)Ew>B;d84ijiPBW?c@m74FJR86LbEz zYx|9SLfa*qj?QN6IjsBf(3`#W-_Oq`l7E~X35^S>!=jr1@DpI0CmyvL?%ynH{7{@4 zG&MH9!DR zK#skC2787Z#PmylSS)ZiCNx^-60&?1*JuCx`k?{ZHYPB0&0(#}tT5X&)~HETpe* zNlg@{3lwVo=+RLwc`q?=&9Xl=sCS8)fN&<}r6#a1ba}(rtd(d#F`qA=uvARB*6*I# z4_#K-tk`HMBc${22C{WZ&q$15E0)Ljc`kDEsg}O-G`nrVp>=Hqv?Tqz3Y%0pS+npI zyno?*&eWrNy8@G*42Y`wY{NAe1otfjz0m*^Y;k_SuR*J`2R#y&Ib{rO#5eDFaL9l5HcGvciNs2sIp+P9v6jmV=wX ztMwmwQN7%_a|dstQ#5sylGE1DTDTek^xF1ceG)k|ymBAPWG^v zXJ2nM0cH@H*n^5+lHQQk%Im{4vFO)JN9zYsvL)zm!dFWhlQp#On@&c?1TAZumIu%< zJi0r^zPAc|QYB46UMEMYNNG0C;g!v9@xR-ZWmf3qW0c`Nc_qG|nmd%F)in-gRk=~$ zjC9Vu*gUX>y9gslCf)5yB0rTI=)U-qF2*h&BS!bFZj>}q3{R~AmB72< z*(75aP}3&q?H_j@JCGGsFmLEJ9~Ky8=UePuhKKw^P+D_3)lk_*R86N<8)ixKXC(NK zKeg*yW~aN}rlUvj-#gho2vFZi`W}pZnEu_1Vic}0-q>ItMuWz;6^H{pR>UHZ^GIGcS?dct(?BggHat+7-50L0}`Z|+}pVtP~m>dJ4@LIN~>G~CX zZ_ln;+DEsl)$l5OC_9S=e(dfGrJE@fQD{9eEje6A zbiM{b{{}PuKL;6C$GZ+G0??T*oC7mHO;ZGThEdG;AfHY}iOn&5V*TW;B`wQo~ z+Jvw8=cmi9r4o9bQ9LhYt2U;OKD;#`7MC{1#+ z)_xI>x7Thci*V)`S%b63ekAXIWx23hBm0Q&=xjS9EYdTZlTyW{$!+SYiLe)FGNTy^ z+EH>uctKxL*dE1KaC9!;CzEYhE&Gwb_=ku7RS7+F|CMvZ%a8;qVEsI5$Ucnvp@6d% zzE<`Ab6^re&nf~!SjZ+rZ^e&39GAv<5k&kNr?U@NG#@toReSDxv=6$C%fCo=;w7&H zsW2P%+uU9v5ulC%>o0hlN(8h@d_(U4!in_S6m<}!{vlAHA7s)T^<8N|Bd*1GW6}yZ zfr57$fRVRblj+^m7Q5scGTmDhF1EkN-D4E`stl`lUWoyBH$gD7?z#=%tiex#pVhOc zu*%fQyeQLc(BrXh`*e3QRL&K*9t@;H^IqC>fJtIgdEH9T$B8<>9983$Vaad%n^ z{F!9O>0k#OA>E>`pPIR*$L5H0R(^gzw`oh?I0pyawq+cU6fP+eM(?wj3A+Tj%f(x% zEd8{q0p_CWJm2>Sl=Ny~E5(M%%!-EtXR!K(UWT+uX|+cmyj00*$^@<%gIB&QHXctV z{|>60`N=oaf-7I{M*TzA`L>dtJ^j|BikJJ^Y};Q4^$j;cU=5s1P4vc^*!C{5;TUy zscFg`Ya@#F!k>!!s8937-kY4z8~J8fNw`iZlcqR!xaKMdh=+--Xjx=>rG5K&u;BPxcwEo>m6N^12+L2|eU=PE=r$U>WHf=?LE-w^wvByP8Mkzj zI|0OmJ=2TH(>8RBg*wzfrtTYgmz;f;(+ml>4Z*#Y9t3B@C1tjR5{4<0;IOPRk`F(b zU9+KycyW>7sCddz#H!A2rbSvK4lbtk*S$l)hOBc;N=s*uPH@2@wQt8e-WRnX(AhiS zb$PB$kpVZL37)@Ts+HAZe0{Hy74YX7$loRtzZ7HEr0=tqjTk?mmP@$`6uDr)ldVv* zb~Y(FvT#{Q3HS>)Q_S6>1Tq$Ydt=1-(8>SS%C*TDYj&{NUQ1d_K^D>xN>(DUId}Tz zwV^Rr6QPCD^nghXd^k*JsNs>u79Xz`P1wSMY6%0ef#SAgT|42_@RD4F5@VO9KR--E z6KS;C34IK`KgZ6~*3R`04y7*}DcJ4TOdU!7Pxeol*sPVzAUn@^M~>!Ye3Wu$lh9Y& z-~)QW;UXgaMgkLlrKMjF7^6N$7BlfMgsqN=h6N zsn(7ZDzZ%01$p|S8~eo9#t{2FRxr4kIA>wrZ+NBGbMm2sGhX9K@(L^9?{=3V9hrNT z+T|MSIXJ=ZMjboNdQUHW`_*`E=t3-c^=z~T;V+S0@2d`VFYXm?o16$q<|@3iNKbPx zxA1ySwG`^(NQi&YIt3aMu8eDG=LIEqOa7j6>Y#J=H$AMx(0*5n`O$2=0_f3K^H z65_Je<>KZx#~o&)YpsAzrW-vuYN4WdrWq!6vJIC!%r{B3L43BJQkowk1XYms^HAb0 z`J)Sn>a{ILp3U#4N6+&?TV}QMDIBXRDdRoS)lvA4%r&DPSSvlnU5aaS0ulHNM+0z) zA@C9@awaC3tCE1Z7~@;fz}MkH?#;QTN|lI7{hDFSvt@=30|rLCblC$404ZNqqP><# zOLBH1PSeVj9xkSpA@rwlK6Z_!%7x(HJa!<=lWqeA`$j9F$v{;xyKZ6OD!B0sDYcb1 zuhf`>*1wk?ldu}Lv`TAGb#2VF)EDCtT{hrDGw+0IuhQwGHzKV$m;adh_;4i*gDCHoqQ}Eb(+tduZNNX~)FrE98 zrYg+4A~mj|`4U5PuJEa8{oQV`t(OC`T4bgvohM_#biI0t{{fbea_WY1AGVEX4=Nn1zg z;X>)?B}VFZAi`&_gDx7^oXn5q&C_XF9pQhDgZps3p0(ratM|^s9Vl?iotEnMtG~5l zQX_xl0-&FpDKF=w)^kvO-q6G4ZFJgqq3pTmg=@-5Z949}IhBX@@AG~O7fxdveJ8l5 zy);=9Ltz@7?5E3u8E~?b4$+Y3A5Zi#U-i&)*=HPj!+`tRIavM7jsfrrzt*Vlw+c=A z=Tj!_?$YKav~f&VY-D0qL#T-Ej%j)Mh?RP$dHvL-%}=_@1ACx@-F}@})JhmbH*89;>CAZjavO2awB(F@HHBr)RWIkIw z7%aLNP``T9==e zwSvI0GAa!uqjvSnwG|UjOUI6XNOX86^;JDd`E48eglVq<`yoF$ree{}k9T7Uo2g9q zt1};Psu|#T=ih!`1Wpm)4FZU+6%~h<`)R81$3PwB9k|d*aL|uxCLw6FWayTRD`GJZ z!DnoAH;(KhTG<|4^SK|LTdD+H?)V_B{l`}q$c~1*7(YH4*j(@BMiS!CQvFBG!Ir3mrFMtklBZ6>It`~q@}mV~ ze}M7HRFB#!x9~e+J2Qr5ofxCAxN$MD7TlbQjwv~xTJQES0!gs}y`n;@AQ}q>3ltWK z$2f<|0tma){fs8eh3xB;OQvley?SZhpG_}8?50f|95M=#q)4|i%SZf{Skg{n4fdU1 zALknX$OkSz{Tcw*@3~o;Sr*!*c1CFaw6rq~W!7}iuEm6GABdo#t3-T|;c7G*R~{FI zM=yg1q47PzNk;h}$U6^XOM0c2E@jo%f-Tx=4IQIWu9^4;6j9$te+8ZnZmR5}`mzvj zjY%LdZ2&9Gqaw(-j(Tcf|~hVG^~+&E<~`9@z?p-Pp3$YEf;3Z7raMNh-dTTynEvKU^1D8>E5XWDHZ7)981b_qD9wm{t3uO`6SwSMlTlf4j4D@k6zvE5RvA-chBrn@hf1-uV2v6 z00=wMrIgpC83{*t3&Sa^(~uXVRW1%^zw63v>mMDoY@TQ2=~W{P!hb|Dw0J&<&JK}2 zmuC5ZMB!DMQ?Z_uXGOCsDNEGAy-Bh3`?H&!v-H!T%nM37sE8;vav&v<+pm_9c;NNS z&@7;uAGJw0yl2zOo4ZO^7PQ|6_5b96Lnbg5mbY+(7F#=YaVtn8#OLk0^g;Wrwuhxb zy)?b|*tlp&Vc@CGn^EbN2kUe=RlXiLa{hU}scNoE@Lpe-PN~-nUAH zaVDQGc@9R;Gi^KvtF-g|mOWBBLs{jvmmdRHIYI4lZhdjmX-gX^RvrnQB)PE^F%c}T z7wyT+xwL4DeQu*i4>Mk*ke|qCS}Ay@$5<_n;qA*lpf;QDWdC8tIWE6{9_pQWZS%aY zJ!P(ui;tJi$@1N~LwQf}YL9)WOSBw`0#Q(iOWo=nD~ZnQQ1)tkH!#Y}v99xyi=7xe zxldczc~i5Ax5*^PTBSMNMXN~yFA&{;+oa2pHW=PqteyU9F0+F+P?7qUdeutX4I+6_ z!0&r#dJ?!pq-sPlZ4VzOkAYaPftcwuq;22G!<%zk9%qn8(LsvmGR)6%6LpX!|K)CP zg$?)_zgIY9R(;gP@@#NWjDag)r=sbcI!shmOsL#@4MxNIHqk=E`aKSp)g8HW@Q zxut#Fqk0ymdo5}KJ?$AVjOl+Kj4lqb^u3)54vv?F8ag}=YBM&`V%8@Ptt?xgbNsme z7iKhOL^usd-m}Iv<~24FEakAw+UlE^n0qK3jCZ=cO<9E-aPQgVLPWxPSI`d z!-;yFv!jDL>VMectxy>oSL_1vlH?$hV6jXc4-^m#~mzR#XK4=qc&b$nx{Gluf&@HKb$ zN;z+1HL#^B{urjBPd(LLnowQKU<0xJvFcEXy3py!0>>MZ9-Glss^i(xx;uVp-dyPm zkVtIATjbB_Zt}HAyc>HaaIiK<3>3o|BZ&?3Gg!2(2%0eQW68QuymUvyH@(Z>sE7GD|lh{ z%LgkcvHv`t{7%pk=`lI8{Ww}3CR7H;|I@X)*1p`0)oJvbgLVJOGr>nyUbk8|Nw?9W z*q?(JS_T8S@P&Uc09#rudHrb#&92ecMWH>2tYhMj{cGsC<;RAPS0VLdWW7bdvJv2%IE+pw?$#HwiY1BbsX3pW-BVm0| zsAgJV>5aPRYkleV&93-AV+V`cunXEa*?7Zr{T8pT#5U^@WZl<#9F?_TaM;{uv`)QU z3_WrtMknK)Oa8_~hLvW1;u-!A@b>N*Pn7McYG)XGIN<@3q?P{|DT{rqD4y8UVj^OWi38BHEc=f{v_*Zn;GshWvyhlW--5-PVECs+_( zSCYjp;IhT|>yiLbezFGQ_hJx6Ke~1N53u1m@f~)9mcp>NP||~VOI9PHTOIx;Yu0ZN z))v+!r5%#-I;*?Yma)Q8b^GOUTj5v5@&7jKZgy`*^7qtlR?DLQ(v-4Q+hTud*ezP@ zbpE|{C$Z;Uc?!Cp{R7gV3@YTKYF7g0(v2dXgGc^Q4iTZRn4shpoC1!CS3O!~b$z`q zbT78hJwiLzYPz_;d>=;K3of^7#!nmVb6FU&pp)=jV=1rt41Z+1YbP+k$gyijHp6!6 zL86z9YZL78lfMD$AHe_BfCF3N0dhlG`&R50d1jW!>cVtLfpI==zT!0}^uof}Yu0Ap z!^&>jk^nDPsOX?gF~OR5mYj#H#NM;k=e2hc9Waqmejw}~*)e8&IyNfN>aiJ}?yH)V z`o||qVa+#8Exfu_&g0SYX1y#jOpmIbSMJh2^iOAOC3u@yCPKto1*@fyHZGobY|o zQv>k>`DTe^*Dnqy6i1)@z3&Hejvq6NYIP;r!#C>&jU`N}Yvf*${{b9WJl2%{X6ZaB zzIc`%Yv2BFQ}6NOaa-+@G3a=~Oo7uo-kburEpT1`W_a-QA7E;KsG%}*V*EUlYw`l* zW$L(nu;Y&8FZ40aD5+>rjzG<{(mBosD{$Qbr%SvHJJj{r&|k~xl~fRY&*3y*VV{Ce zwo>uX`Ft$=1KIv92a(GfIU;bY@SH<@zDwX`?t#4et!D8KxExdj*L6zt)&+J7nHnR6 zQS_!(kx&+X-c4wMv1#&k*ShuFT34N1&CsffJ?xGkek@_vZ6(ozA%!NZj8wSW-5L6a zwaV{*ChVILJg)5iN=Cj>t6k$7R#LXr9oOs)&o0W6GW^oIab(M$!~nAtW@E|~%;K7R zi=^)K+_q=4cEioLkb7-?=IR%Dvo$~LuMNlpYoLCsQqBzEnRjqdmK%P-mSr`^=!GfI zlT4NZve(Ak`1Tpd%Xk*q0FpOcQ#yU=a_jL_P9??`> z?Fl*JCGbwIESfQ4&Nr*oYmvzH2t5XR6;0~U`p2{BN%?=L20K1qdqdDG7hnf^EI+$U zQj^HfGelJM!V)uNSG}U7u?P1-D&v7^^NF4p-v$Y`+ zBOf5!L8?!#RvB;9x#?f)#Jsl2Q*aV2^}|!IHzjIe@L_(N^j401I3hb9hyALsP1*zH z-1V!PoueLG0xD&-n0!>49%RfgChDFrRv&sS*SELH4zV#&KMoi> zT~Aidh;l4L(Z2yG(hlxAPNy=k&@ZpC7rVKg7iY7`h`BId~$1Up-CnkWz z{@}y1UK>6!i&572W@-M0D8}U0pD||3M>Ycj1;trRm-fjp?tcLOxn@zp9L3X&l*L>H zIW?sZm>;58$<^(j$P465G?d7(s59^>LXvO0`SLS%H+$0)3)7YT@P6Cbd~Ox^wf9Im z6{Pq(5CH`Gef)Nfa7RqqBx@PBv87`B@GtrOhXUlX@T+>U38{mv=OZla^2+jbVh#NGGDy!{u1Up0o1oR%(S}pR-kzA^ z8Ks;!EdQQbM{?~G=~A=HK9-|NjsZ2hX|iduvGCHSTf!=0^3L9wsSOW}Pvm!R*IXCZ zpFMOMka#IY`BR;6gCTIx{PEr%uZs+Rw(`D<{vcC(FJZsvf_D${!Jg1lH<3bC^4iGH zZ_5Q!q7l;rU#~|t+wEcUW32vlCRxOrdt^UbC*2JC)8^T9cqt29(a_M8x2m4oOK9MN7)Pkv^MYEzg3XKLbm?3)4`M2w*8X z&WUEQv86cGs?X|5)ct;g37JSOHOdbHc;5%YYrLWPnQ`9Gk8fc9|KR!m8_W;DrQw#s z(X#T2_)eeZeL9|Fmo*XxdNi5JRSSKfZ;|S8aQ<@4|KhpRyxQ$Am1Oa(zz; z^p<^nM3Xvu0kfJqAiHjt_4npBVTKQ|U=5Ma36lxz{yubUgUncagMKD`lI$I|$gAIh zZ(M$RjCeovm8qi*B_V=mX1|lrk36{KFvVdm(6SF#gxP+$56j%m`!6NCu=jvjInh4) zDP6TBEyzCm=6-Wkphuwl8FbJOyO(n_JTUtV!TV**{2zeJ#3cLbj$q^S8N12zG)Wi7 zh1qJDWiO;V`_3*UoGWxS?6`FfCg5(;H|T$A(M!eber^T}o(^u=yxiE|?;Z5s_*LWn zw%a@C$z%Waxja{*Dv7$!Rp6OO19hiP<*}js1ATUUrJU+#Q*xLEs3-EfO>W^Q6^)2j zncJvp!=B=tGUULN?0G5IMO4dURZN_(8mvBJb7Tw)zj4&wpc6>q2^*3AIR}}eun2KY zG^vm82h(k%r8Y$KX)&4kc`7UAEp;_;wv_++B9V!vke51p^vmn%~G-@B2rSCozjd<;r?u>c%5NXctW^xf|B6D?62RA=q zNL#m!n3N$Vel|~{ftlH!23f_2%~Ksy>b?dPhk$%A!N&}3Suo;Gp0^! za9|TJ@JOuu)j+-{!$$&Fo~LU2Cj(Yh{RnOe62*cyZj#ZIskN>iiAH(-(6dwrmWv#W z62%*E&_5>rV7a{X{dc_L#~M?_RD3hl@7RBs=wxMCB7vkH-hFX}icv`$?dWI)-&L}x z4@39fb$!Hle9LC*FtlT>#L~zsozL2tPNbA8I#d=A;ORC06eiAjq}r+&&f)l#-Xn&5 zC~Tl_NWm;Z%uM*QMfhj9*RILS@Ny<7e?z*U_hwRHrN7OX}sxr+H3x_@9O zTQgYp`Vsd^V^MQhiF~l5+c@ux;U8Oh_4qE^YL;4mJExdh4L=;m1wwnWc*RlLjN-+0 z2zIhOBd|h7?ll`FMLTUiSKCDSg0yf`UN;RnS@``x9W=0=6?H59QFm_@L5G98C92aJ zr>+miabkhN2~Cxn1Q=DeaNShpavXTe1N7FlTRP)IJO^O=I)c-DNSqLub9c)23nLKp z=MjV2cOdnZ^aVpgg+&W{LS48ML)t5zyT)@_he?_cfqyzB$cIlbPr(dxsOwGY?e>`l zmP}xLDbHJDqbJ(~J`1Bx%kc=@EWM*^*cvZ=HrpuiUWJgA!1T#ltKq13j;Ix#^Hxu> z-Fk2skup==dTJh|cW%C)8r==24j@93FeSX`2(j#Ul(TX*p`IU24m0gbuo@2})CbS$ zs@-)VFWX(*D{8c#AY3OL7(AmC@Cn`)qo13*r7*|26Q?=rxEt zYa#B?inV0L^AOz76K%ypmLr%%J)Be*Givlghw6P$qkZH?PV>TZK}SJn@K?Eu<4@qM zIQ0Z$z-M?^U}lFJ(}D>x!EwVd%(!-KsM5;>ni&j&bXd{{XQ^nO@L5)!Vs-|kSa@($ z(|F+7L~I{$olkE6<&v48|MGrEgGh-nQsaFgswiGS`GwMU#;8D~2DTbwq6V^BRjkMR zLXH+S)(gLrKKARdJTQIhqHqzlT!bJUl*P=PN@FRM)K-~3jayrLZU4kFY+?n>_2CvEQQofHX5II?DYgw z^)ZwOqQot;UKy+Lr!c1>kG@>;$WrrGHDOjr`$5fiqdQAddLv=$C|E!}R?f^(P;a)` zYJ!om?@4uH9RC%tQ^A_2U$=P!&MF5qO_1lHy+}orsh(Tcyqzm8ORhoeMNxUQm?fW!>{sJrmC>6^@Jo3-^;K~ zeEDSU#xh%^a)B+u`*L$Ionl_z1!2K~gpL>slRQjIahcj%LLh}ch°F!t#dGrvS2 z5gH8^{ZhA0i=$XY|EE<5*hiNSUdehUKSJukh3mgciL`1pdgqKgfq*O|hL%x|i9!Y!%Q#FDf-wb(_^u zm(jl|;h(q@!sV*Wrml#~>=d711@VQC_jq49d+#6yr{m79AMxZ;mXEv@7XRTra;8Bx zF_Ft(9VX;$ZqohG+IXxJY(XkWQx@7A0Ts$qb_CIC%i$5yFrd%(6=wX2&S@%~Q2{D( z$7EB1d(TAGH)=CyEaBwoHM`04yhyrj?CVO|zH~L{?)ud|M(as}%e7Ufea0l|DI5m} z(L_!T_sg8~Gnh>pUefU}V&ANk@MXksDm(%2@{!bjAo7tvh_J`-TD#&4MA%5^a2iv& zpjS17*08d|v|+Ia9~&oRLPxPXLCYhS6G1r(AD1%7^D|dZ4h{;!Rt<;_hzDNaFcZr? zFqNM|@2cnBk8PJn27SZC7yr3q6HlM`({@co9S`7R�ySo_s`MS*J*~58O>>aaDpd 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%0ASM)akRV`SpaH>AkWrF>FepKZ5*u`s5d+wb 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 literal 0 HcmV?d00001 diff --git a/img/divide-and-conquer.png b/img/divide-and-conquer.png new file mode 100644 index 0000000000000000000000000000000000000000..6222d0bf6d44ac15cf9ca176064d2365b48ffd87 GIT binary patch literal 290262 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 &psBbKLUDA^`DwKgB{F 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

          S@ggsR(vrZaM+sXm^K_`R}MyTRYNw81v&JEG( zEMt5{td&DK0bPj(F7RnsjFVS+zrj) zNPpBaR=Bn=l2W9Mqb7=GXQxNuR_-S4AFa#M@ZZ1NzX`yUw<_MLn@{KxV*KNhsU{!f z_Xxc^lLY6EFUcb`hDh&SW=lTb12L-)F_0s3TsrGw#q24&4ny8Nbx=Htti}7sVq~7u z&2}Bi$eYb@0K06H$u4Nj*oZtk0GSskrcz;7`P)9qyixEa_8K{j0sB4NO;b8 zIqa_aEoC@8kT1BpysE4F1`qyY(cHArH1{JmojkHLG)#VO(n)>{`Q_7SBbMvt=?k~# zu^~X}heT*sPdnwg`7D%p&J^p`Zh{aRwg-2YD%uhN6bY+s3MK)RSV~n@<_i10>%^$+be&9UW2LWGZ5k(iU-S z*p37USX`g~icGxYQ|}1x>+>?%6atLF;DR{+pJ$%Fo`8EZXwA$^t| zw}1~f;qKk3A4ns)RIY1v&_`TVJT#>>3BPm11!(}s!7gG`Rcn?)GAxl&LiA<X`_RZ8Ck>>~hh1boM$)Wav5d{5gi8%nnv*h4 z$cOzeBX60#)DyrHHx9(azcXh@_~F>x=hJ9CRnU;j^&`N$rHot^*4OY+m1+5Vx`#gx zulJp|)P&k-91y#aLAl4k{X{`n)h4=;%`bBO+6-Qk`>Kq;$=6ncBoiJb6D30}O~(D@ z9k`$tpjL04g@&q}S@by))EPLR!;%`W-o8BOzoWr4fe*A$s;2qklG4~Hf_^an3F#S{ zIH3%?Nb}jFXw@oN_+ce{J30Fe3#sdG!_}9QlFw7C>IJA;S_T#6WlCAAM>Ezjsz++# zQap^-7XxYxbQ(tK_FOYHAb)yZBYIw+CI7jt(19y+U(b1y z4?KK=)vDjLD2C_hxCgg5{52r1vD#|KcE^O+l)F*LSY~tASp98dETxJ18buuyZ7Ic` zK1>LS-dUP+Bsy%ycwXTMdqGU*`@yGt%gd)*^(aiKDBMtA}t%zj&w*hmb0 ziT*IC;6-N+DAfZla**oc@6n(;(9*vQb~aK?=mO$6k8!5%m2@vsI{q_JSrG!AED3zePt{m+T3GRr;QC>`zCOXD|LRuXfu5 z75U1~)izhVWs(GFTE44I_ zoeJt~1)>i7eB#)uj4+9qjEeF@G2GS_|xvmKIGr7H!H4N0!{$(M7w_k$TQ!wreRvEwy!c;gTr9+ z4kI4Yc)(Qx1r~3?A1p{!Skw}jyOVW~B;)C64_78rhZ1$A-gl5F1&q1m(h8W707Dr^ z(X?!d{5k8-bc~iG%gg|#uZmYLUr#F)A2Z~6OJ~&-ni!@hL}Ymx=C)n*kwl+$Xrl+z z?KH84cc!lNJxhz;q)unefPY8NjwnBb?pNEVk)>dai_W}Dl zS|9FE`OJao#cTmq9T})J-T_S&&ngPd`kRjoL?ZPf4)~((WcUj9m#XburYLtM=vP{b zu!`GJ9Q6~@t_HDq>sUa|ReULq^AVHxIRe7psx{c<{WUVOABygOe__@CCLFKVcECx@ z;u>txwq)*4)uP?2;!~mUl6_-4Xzw@HVfHd9`RWZ*a}CoK-U(&5#OzRSUi)Tr^T8{d z7NYyvF3uur4oDTAATHg(J$TxN{Nf9rad#TnK!;JMq)^A87lP0Y5+a=E(4L9JX@K<8 zDm%75=kO!F`j0sJI@}Zl<=TwWr}s)j=7k7*`nI)mscUpV9M!a`x>zDA*U;wlbxI6YAi_Xl$i}^3FmSVk6ZZjf)6#v8B`|2J? zHcvw2e@8^A8&N>NuC$S~M*sV~#CQLhZ(28CRf4WsDc4v%V^CAsTioSq{5CrcD%rv4 z+RG_2y42w=A%tW!(XJC}?Bp+SpEhgmoCc#BoeIn$SmnGM8xi|Tp8$g&_Nq_p&JOAG zN@6D#0`!RJY6p`34qve?l@1~zr)O>%NR`*f87hF;uD|FMAyqkBs&_TFfNt5xOs$B` zRqup4RQ^|psUhIH$gq$YeNs%#3)DFB_-0?Q{AI_jKD$r?pH6AXX{-yV1vJU+5bR)Br)qs zd5>ch8t1n=S7@L&d3}!W_fg09R8YF800UIq9#9tPd%?Nv^O}D6ahBrZGMEO0S&PzF zC>(sJL`VIqN__70%5V7IB?aC4gI2MRM`r1GvixHY2K~`|b`H+F{kv25KN!s}8Ni}D zSQ?RnGB!I#_*4!I7)BnLzd!4S2Q*u>nN*4u(GJ$7NfD_48mF@s8^c_)+^g58m7aPd zWWG<7zfU;xk66}ze>MQRWuLxQCpxd_uo9Up4mPB~sKP~@YK*+2jj;r-ka5IE9EHmk zk7)2WhrIsq!5w^p*=#;UyOKl_epKT+_TsW>ita>Py6^6{@+XFnLtuex&zEn{Lj5&f zT~t8^P9<_Zl7&6L|Gtep-67hub8ctS{iRp1sD6F}m?Vo^a3rdJ%!zunlK?$26S!`h z`>eu93Ju)PC+mKg&O+}!xJ(m9TOyZQ6&fX^1>ltl06QM}`jD4N#SLS#Ji+2kii{ik zVLVxFEmv0JP*7)=U5KD;l*ozqFV?6F8~mPB+1X5}REg2X@l$RO?az=Kj74|*f+qXs zdE#ZDJ+D zImh%rQe*yZXea?4yDI|E9f8MNPj&23$`K6w!YFXoN-JPTemN0!ad8E~_0d+W z2nJNoP}%j!jhL9ceml1?VX-FUZJ+9WkzF~-?u8`*mQ@rOji%FidtmSV4v}edj727? zaK*8zOy+*rrj{n#ylDh1Fa>{;ona1NT4~XJ4n@Dkmg0fkR{PY|B{PocXF6GRDnVyO z>WOyD)fW^ShueF4vL^qCm)d+`*WBejgsgcjmtS&loUp+t2R8g{qm8 zCU*CpWDP_{&KFHPP60tH^4hzzEK5Xx0>~|d z=MJeL&gQ3F9}*#Qx8l~?`cw^EDnVooV@LTeRT&FGhIDo=_K}LExr}KMX`sGem3{yx z;N_B}cV~yc#o2SlX-c@nfVQ2_IqkE_PG9B6v5FQUv+6I<`byV#X6MQwM=kq#lf zK<}-`SBDZigC7%K-1>vs%$toG0J}vPdZYjQk`SrAW1{!~WO#E@sqEPtW+71SO+Kq7 zcI1feHVTuthWx!bTJ-8D8_7~b(aaTN!=IhH4~W;%V}=Bo;Sr1!uV)aj28i0~d)Zx- zKZ<~~3okKbklf$rx@k0Cq}@Ax!4+uER=zL8E+#}kBqnKxQ+XK@FCF3-XA_{949djOH!c%0EW}xT+z%5I22;aQf^mD`)mA`R<#X!HgBI% zD^5jQ`ueKp>cqI{kh~vbKrIv*sk7M4wyC&Lss!wau(bYZ=uN;_P`73MZ|VrSqIn?Y zb;`cwT#RdV%>wPq(Gp)a)+Brq7%d0=B){Bv7Nvl1Rju@Tin9Y>@4>ZJh8W$xW%l z{ncl@LHn~ET~-n~w3vyr>KUM8tRKxfwzdUbA>IUtM?(--c-$R{HfISI78iklGKrv0 zI+K3}En(>t0+!420d*?u3@GNmvGde30;=UG&{bhDJ||TeJqDum0dD~pz>4{QY=^cG zZ>2`V0e4kQ8Sx^E?4Jp-?lb0HCR(-nOR9{Rs5s_Eb4JcitzVBs9F0X8{4d)go z1S6S3!o@t~8--ejP6p$4aq?D-sVpn+r>X@3Gi|ZwZ@tLUou;pS{$)G*tY5b$6K3Ac z=s9K&I{K=JXIGdnDmT~|Cmq_ZOc+tV2ow+~D>dttEjwz~3c8t~^3wQ|->}{uefm0| z9r~IWYT12eK`&c#(5`54obZ;InjLZRTF6`MFuqh|d{Sr9cZ?4WvEbga*IK};ZH+qO z+k(Bv?~^Hj?P;Qa0#!s~T=W#~5n{q_1^nXY;9R#*-^5v=0XLoEbmz>#G{uU@5|n^x zO?Ef~&`I-ipdj}(k|2tQ(|*zkl}Dx-_^wg63eHIY4&8|FTwSszGN{?Js|D0S-0Roq zMM;XR_hmRVg5$DW$uwj4xhnv;t)s(kE0WK+EY}Qx^SDb5c+O9Lrchx_F)tE|Mq4{s zTe=aAuS+}hEkTXGt|z*eQARpI+n}7MA-HkR(BsBN*qsI1b^vbl)9;hT+&r@nRGfk z@+18f;nZ9CzGjan$7&AeL4tm8AP45?R|C2CmI?3v>3q_i&6=n6bN3uJFK24hwD0Aj;VDIVzD!);SS$EK_5Pj1TUKiH-Y*w%WAZ)E#3$ zc4$;%b;1<0_1*|!=KkT~C1HUc4YcO$f@W}@DVRs)N^%5=3SaL_in5n@;omc^Dp6QXi-tX!We*!Uy)EMKQ zBb6n}3XC}ZO7HNlr7OmjM41C)A_H&^7a%}ftepO_SibnlFF(wN3?v^8#tL`dU>MXU zgedBViY=7HiFuAUo859T@_zzhW6)?;MH1{dW0A3iX%KH zTQi2P4U1Ip#TgKw29UPD^Vs|^#wvYe0?b1v?2a+Oqdv1d-qzE#_41M*|L#M1-c;Am z!Wk!;PKouK-ICr`GYB-+3mzv$F5MmE8mbc z`Jb8xGqK=3TQp(Iz*ZXC;}x7bg;g*u4S$x88$9o$2v{k~BS|+S4V=eJIia0_cVE_E zNsFP+1yAQ&i9nNrod%qoIU? zelIRrZ{2LflOOl<-fsv{^#CFFaLLz**W2}1(N!13A>vH=logF^5Hwyhc`k?F=mh2J zCS}7(o?ca59qzu$I;f;AC!CRAiN7w+!%a^b#(0FQMZ%YbK_7`wiMWT*0$_5dGecg# z+*`^G2=5dJsy7Bkd(V`J=efT;5Y?J}1b$InjBo?tUKq7?mk}?8ItX(RiH6VES-qpI zN&Rq6jOe>%j1WyEfkGf{N7HI*$^>Mgr)ksGx`3HqTKAQJQFs}Fk|`2Inw0BQ-l$!H z2+(I~>Pgt-XN6}6FdPErat@o0fhEcFhE#gnN}eX4=G=*Q8H*TTs~&j4e=L$`Mn9>> zugRA2VWlb$sC%iP^Jr;`RT2T>k{9#g-**=k-s%9j6cFW|%o3Qu`@lY^w;)LLf$k6f z9Ip;L%pu;vBD~(v7ber{Nu?tsxKC8XFTxS2z~3Jf-hId(;V?TiW9UZTcIam3VgH-2 zuwUPmMS0Xuqq*I<>XJEG`se196jPypX192CXCg6Fc1d93lr-HaqB7PqJ3iphfOzB{ zryh7wHT>>wDZ3)MeM$xnmW8V)(Qo9Dfe7$;kalae%maRdm1|?GqR)imP&Z$coMRs2 zCC%~}E|c9b{Uuutn_&FcxrCbSq$?q8^j3-3<*$*Ecq_j)CzeS`GD!Sta^^}DO~?4S zIaHDzAxi83_qOlK#ALCTM=rZjmr>Lf%fej979Yz{3jaT2YU(&z##GXtpr1vO`CEGA zx!yJRTRCi|d3jpCl&biT2EWmaH|NQ6bN|IL@%F=_b}R?oriMJkSbhYw ztO`j(J_%HEz%~%yNd^HZT7>^$a|%LVvW_ZM#1=IVn70#>7BpeMalY5aGE1XKyaWtb za@zqaC8O$Oh*Hs7@wC(CZ|oek&kk+qBgU{R)hg?)wm1fN+=g5Zz`hRkedp5a*7h@C zVEG;y;5ak6e+8vjUp8!I;3p`hX4kK&)iSkD?&Q*}-h`7t6w#)SNm_amJ~mP3j`5|++`I=NtblbKC4V(IQLqBy6X8mgl7#=VF6|W& zSNQ538O>Qmxe}7<;X%#Yr!NUOf`V~no~mP8mk(Hy&j%V_8lpd2B3RO@H0c}Fx~8{_ zyS}dhU7E=(Re8m+;sxQ78OIG$6iB&;dF4kC?TN~*bCkl_kIZP+=#9O?_jZ5GLV%Mj z`*DDltwhzV5K_o5$9hYitok*l@`c0PrWHU4YavXu_H+)Eckz`#(8dWP5xfsqM4I9M z5*|r`aVaK*fSx#h5Is*_@M4L%a2A)DK|(35rrnMn1yZkoDbsMiKWsU_vE7bcL^rQ{ z`}C{)8=n@OOo`Nn-PvF~gPoYo68(Q9??&~m!%bmETgM**tRt^JV*y>ZN2o&liRyQ` zwjJ>nh6m*9&$E3i|H&kG(BJ36CLqgR%4ZhiZta5^*f*(+i?uJmDg|o+n#Fe`_j;s5 z^YVTs=idT1wy~4Tkbo09cfTQPzQWMOBCT=@!_earTr{jcz~K>m?r zYs2h<3inMh&}Z3(n(XDQ$miEkYuT2-_KpUgH%Q*Lx#fovKe9T)ti4@hRduU%Oz@~_ zb7g<#u(bu8w(WHeuCsUBZwOlz66}NHPu!8GvcDgd$9yd-vujxq|EU7aVXIm1vr2e% z_sMoaPbUO81^N3%6CTaGp#Z{Z^4)8#WSz-`+v@}YhMV$$kkTod|7ivNW z9T(iV&`%y0;%EB#=lfYR9Mam0J6vsI_jI=`zQ4w*@B!EypAJ55D=b@+N-D{P`0zqk2($rd*)_al~G) z5S}=Q`Jer~Ha~x4=ielKos;)dz#R9P4w@dEL*;cvxCu?O*iw_RgZCx;-kiH)AQ43-_GDW$h?OSLiwa{=`&>r z$ip*m>?QN+A6Mw^mTQl@_amg$bq)WXO8xKo4W~eS^cZI=UQ%DK{L)eO9a(4&*eoal z{(fV&YuyNOs6MbTC1H^4b7N;@=Ze56x1}Q3wubQvG~BFqTE-e;8gM(#nYXpL=%#@#-6z$;&TgpCn(pu9&{? zQ8!85#1vL7*$EHPmMz9Ci|sx*s-?(o<<1Set9=O8(1p(FV)ZFoM7XQsA_>*Nkiy*W z?-R#&5A0CGW@1{(g(BnHJ}0tgycN^W4aUYqW4Q=RvW7Mo zag5xeo)(yb@-4d7dDl%g{PI5%#C;^%Rz&&dNSxCc2HG-us`f*My!P~m{?rRWWTkrZ zmN(t8^gHdqfgREhsp&Zme^U=JqZof4PsNn*3EPRFHWUjqnF22;?gNm4m@aKTRZ(_i zbQ_8IPGZ$_*myU`7}&oG>03lnx4tMKNdG2QDpII6-}ve#Pk`^~lJWVck>r4-GJm%5!mrt%}Fmi zT=_vNS(dwU0&*YpWiKy&U7eLQHx&23fcKSyBEz<%@I)>#*F>-17Tw(zUENzJD|c13 z0^!O0cu90&L!h@9N-P(!BrgfOE+ZZd%ij%uVXi&+4Csvm6p|QtOT3G7QR2F&uU6(e zE8ou9sISOESL#M^RFA$8!6hwNvSYVHoYu($vfC|sB$G1u7hQX-JblOJSBY9(U4H6) zqF~9Kn5jqOb(oM^#MvH-JMn?jj5&0CQNvAQM0ejN_=m3_^A@J&94l$bYMDn;OOWjM zNFNN~?G$*qAeyPF~=cHH&`n>xn9|7nT16K$kbpB;|FTO$*IEu()HR4mAz`m3v=}xOG}C z=247AL);1CaVR;k3ZCfSE6HVs(<`a&xA}+@=-pO27-L7qm6dUq zIkDi2+hADi3Dr?HVPd~4GT0z2oJ*^9fU;J0u&#?a}im2%(Vw|_w z!J@lj{c(NIng@oNWcqr3AFtJmyZtXM6@LB! zoA(#dVqADqqnC$}`5P>CTq6Sd?|45hT;DpWR0X5Uqjl6M$xrQVItkUVdC)SWy9*3Z zEQj@>3DK{f8~g8uzeoW*9>)Bm&9wYS=F z_s4~}_*4kBm>fa?7L#(b?Ds#Wny}dvhmxJFeY$STQ5Yt;fxWDa zWq@C3s);@>Y7!%_tmdEiD9m~})zoAG>?Qv@@yeYTFxt61&&ZsAf75HoJ<1?S6}MO3 z>RoK-HwTfF5`bv9al4z4bHpaLhz#t0QQ*sSlW00ZOL{)4p(oo=_Jd|>L-wETUA6)R z@mIDf(0YF;vKRbUi7+~{Mhe=c?4PWqZadv76}Q99ql0gP)xo!}AuR!9K)w|rMS?_#pn1<0T!3JCDMfW|^GD*vDWT6n^7 zt0;6)p-+hZyfAg<+*)369vQwkkx<7w{4(GNLjB7Ielo*y_s<$4=L`{%39!@IET$H8 z`7Dhw^$q21kgeh5MpCj3gIduqG_ggx7-?uMsp(%s5@iL0(mJX^)=GISakW{4)b=ut zFigKzlSY&3>Y}Q@VoMRE%byU99GV)ggZZJJ;c3J5&xW7k1rD#Ns=Di?ML27&@h0j0 z#H$PT1SA6+N!L57$H}aaTaVf;LdQp9Qe39`8k3&q^QmWUxVNXR!qy;}KvVwk(qGzPENwYQSLSl#;R^4YD9S|my7II+Idg5WqY z+e*6y`}wBGuk!Md5Vr(2Aavu#aRey;hBV+o59e%G^VzC3k|1)IPdbId22f|I4Z*IJgsqMj+;9LZPB(#*5SH=`Fk2HL(aBv5{jrH`(+M9+V z*`975%i{NJ8t7t_#;s&l9w-N<`A7>3O&oQ5#6{zD2`S&Vps3x<;2djFO zm}D8txRd{;$O79x=n?3kxrw_`g!7JU1!YyKUHPbL4Z@)BXXp5F5);M5U8UNA;~aF1 zsF82LclF{MPfcrkD_N+&y6GGC$7QGix{|`-99ZvWA_oq#F zcT7)rcTF?h-Aw0nJ7R|E8m2oA4wKWS+c>(r-zoWzjNWb&-;DfuiVT`p+r3d z)2fhna{P|R31c*GNw|=56A^qw`y~5upuj(_!K_2PL3mr+D+!7)mX4ar^V@x(eCU^& zCfY3)U6s^$W;SO-Ttr%C3a8^>peo2LR)eHi5{xnJ#i5x8FUVe-5Xw#D{%DOdRbKOx z5#@F$2TYo5qQI36;P)}Te-3{ZAN!4AqYc)LqEO`dYry1zdRQ_;KEB!%DS%wOqV1>D#ssNv>mFWV`VQy1Jw` zjd*$9WVeQS6lXvJtc^X-2%Pb*wxtQ*?(W{^WtLyp;D%oy$UDa-qRSOlPGH=*Iv@)2 zv!=Rilyj7+GVAw!grX9cDONwHJIS{gWF}{p4wj+Q>3ZMKI`}~GhDdj3L7EK#1M57? z^Z@KZdpQ$3$-=kT;#yv`xD?l2SQgFpQ2 zR3Fl(;~H4~eEorO>g2(W&@!A}0R z8!$tVhB1G!`T+U<1I4JC7u@l6s}(a}zy@Ce{19Q$lixPWVf-5saF# z;LO9uy3^GcIU~!!{rgGS&^=+f$M;GrstWcI^uBh^E3XU$kNrdxqA1%j`^3c>27jB` zbW$40tHA~@*Lm^J)}b(l1OrqOa(-Z2^^WyZaubzTVZY;&>|@xYm+z4-*gF@qGU5!G_CM9J-~ zAJv;YhO;{wL5F@W@_Y7&eS+K#oby^TrAJ)RyLKuisUo_5RAa39f)aVr0gFmA`H2YGtD;L#53#`ojP zu)fWn;IC#7Bdnp&?^v5xjYkJ@Mkci8|F{>QuZFr|``!lHM52t`)Wo7+N~d>sDmdvI zqw0Cf%6_qcp5WBf*gKst)2yXys{@Lsv&@M&0NE`@9yG_`=TAHVz1{T!#0v0-QB;tu zq5x_pTY5=60_Z3)x=5o{2Gh(x;gB=jhNwU%?lViWSk2jH$47|e4GcQF4hcbpK>Gpjoc*VL$0IWAWSwqLc&I;kqQ_tt4hB+E=^4{!lP8P z-F)p3)%;8jBK-GAvCIgDlvMu?fKvbUna(p6%Q zB!r%eJsVy~-g8Bk^NU5oK7t#z&tQg+cXEYoaz!+k3*KExDEnI}x5y+Z_DFrq4srxv z%Z_|=le2OqkLbtc_ikP9Ca-?GV-IpPk%P~$N{2FGoOycPQ_RDzf2z!WP+_@xFzU(~ z?xk>-AQ}PxQ|o^2EsK^J7Q^iu!lrrwl@qycRM{VwE-AL0Ebt#7zz=38A){ zIte&ib-hYZ&96Tg^0ysuUZVfns3nQ8BgAUFl4ne&N}gxbM{JF|H|14T{PdGT&J+(#hpa2e9U-A~zNg-)AFiKzllmU6;& zos85!#;d98DF3|=<#2%4#(SBnpZt<{OMF6gUYsU?cFwV1Q^tmb2A4b4AF8|`AOMmf zC5j5Is1P{4JI>2rf^@2<xYk^VjZTc)3bx-n1R96+)#@)eKHqF9$Ni;$ru#r1HF?Ty zDw22A$!C)`S;=@w+mS(-m!DtVVzXjdqQ+0OysqAw)y91fse>T*CJ6mTF=yRgX+l?1 zQ#-yx5}9Pkos3OIW4cBt>J8WVJa-1sG0-{kya2KO$Bo$L~E60^N4 zcyxCcWSvDdE8$(GVH~l?=0>&$@36$9MkAU;1^U58AoCYh34PXDGQ~E{T6Tv0LMppB z53H#sQ6eabW^1)|b?tISOVxJdU4AGEJhM0@d=*E zA+QulE+ zqH%28aw3yd;N~rG{(Rpbjd>>`21zEwpMJj&4lS)2ZcJEmn$1rW1Hm&TzA#Im4<0V! z3+kIpHIZM8(Ek{ZnW>M&=n&8&@{zzKuW;PY0##^Oq2uND{|Vs$DHeqYB=18CnZA75 z2>pu|q$bX|%JIPfEy6yI@?L>yh>xmoCyGbM<79;|)4afyG>)}(SkW?c-ILF11;6mc&kn zeSd2fhsaodGm_OqVa=9Pu}fS6y#lb^wf^%K)yJbxgDQ{)~eMU%59VTyHMGECsmrp(po!KesX`kXO%2gL#Xm_2j16H(j*jCJ3N+#6%LkzBv~Q||Ba`^ALLMh zkVP=#$Y(HU@W-Fw&!jaJJCib5!;$std#^1GwF zBgnVWw0D?E_vRs#@Db)Q3_0JBb5(5EaEC68!l>#7eNIyvD$3)+Dk|)<5f>7B++UaB zPI+H{Lq~JZM*EKUBK^ePP8)?^i(jS#Ki~5%k1%-3n8jo;_S&;IgwLja1NP zqNh2leQqWj&7!)w2%KzrXFx5?0Vn6>=`8Cl(y&3U0oQ+#L#J{PxF_SwX}Q9Umj1mI zad{${5Z%w@uQs|rhm89tk^D_L>gCXu%7Y`6B`<-Xq*KHu>^5^L4nwSQ&1V6!tM03v zY@vF!xDFdwd7;Xy?451DNWN4$MMYcgR63e-W23_4-`A13IbDyUHdVobgT&KWeNTCWAEF#r{`;#U=fQZn|i%XA&d&jKaI3q1jv)N zKhbKA5(R&dVICE0N8%xqiEP-4sVPE}jZH2uuNAz<_7P>xdPGB+W{?}+ARiaI)3;5b zXVefc#QT>apmTsbOhQy8r9Qy_-{uTl{wby3vI|oM;t(d|A;|J?o%p=3I@qnLzR^#bi`fp6W(cvPT=3F>J?q9aPjKpHR|L zS|dPl3t(wi*Z2KbSGX!Obw;f2YwxP5M_GyM$niAW#%xhxlQzr?ml~I+TAOrKApx5Y zo4A;F`M&=CTbVmN;nHijX*$v-X=ABGOjKxOB5I(uTog}CxxaU!9c!T(Cm6VFKdKjG z8j0I3MRXI1gQ}~}q&8Z#b)@zAW>wVq?UJV*?gpu+zZJV%4NT2(Fh&p*z}Mg=Pwe$y z&Env#{oS2*<0}zSVeoi-GO%B+VPJPO!5|)LuxQb5Xz%*)qfB8T>xZ&){EY)qd3=@# z%dgR>8qz6f&S^s^zqCAZVq4+hjeBTzBS&)n?uA%UVH)YQv z`#oe<;_VA!zQ%~__=&FBy*O?+_wR|(y%I{6dy&oAz1rUPcTv@WG7CIbODP!{wqii3 z5_|XaNOTe=8AwpluDl|y@8S=11}^yD<^x5|HyV+ZC}$Q<#A2uTN$-xmOlvd)D1z7p z>xVcvGIBRwA4fNxlmI2NtPBdvX9eo>*blq!|BK;93F$NWyQN|S5 z-)2i92E{{r#~^xZ(Sk_F$^|b;@4!N4Bkeh}%ea1@r7FVJ&UTA-eJ(Wej+f9U@t5AR zuP^-GlkD1EQgo=D^L4d|WUfh57!(+~qX?m$76gihY@}WnV+SgVCD2qE&QU+9eZgyI zmAoCYB>JO@0k5Eh0KxIn@Sz=^Z61r)FM&S}yMUYZcS~5avTfvv=y&~$&gxoL0eO^X z4=0mfcU#5Zdb{7A@|R9QJcDIU1{kuxXGSp=t}Y^|GlzU3O(ZIVT=QdQ&C^#!B~^wI zq*(n-l2+gB7E)|m7VnpN@pU@20xi_*kRYGs@2V1av+4F#)Zg~N z{Td4Y0?Hx9tY0o!iz}3h{zxMy0W?6jS0E~x*v!%E^!okNoeJa!tmBs=`G@G1YNq&{ zHE-cvn{Iu};m$!7hnDDs?HvSuH21*sPGZpcxhPg|SZBf*w1`oF0yV)sb8=y9y8TAB zjgil?3bCC+XxWB}Mc6w!Z0#eoZgT%Hk0VY|SGjip4ci|rNZJ%%Kqax9Wx1&21dl80 zo6BjNAAs2Zi|g@%Y;~DP6-gmGS}is4$Vzgyd{bhl&Yk=*j#D%>K&f(BYC)yM$-_2e z`TJH~-6xADwY;!c9q z1#`?g$4}p(8T^CCZk$6L@=#2T`IAAgNv@KpCXn)O$V&klLxkILN#$jTEkRVjV%Skf3qqjP zp^8w2o%BKzS(0E!(BP;TG?`y}Jmd(8RTp*FUIqt@yr%VCP(^2Ymd&_~*zF>fcz8GK z<_2-W7%Go}%R(T48>OZ@nCe=UBt7i%^`CtQT#8%JT{^$(26m#^kz#KzV-%!}nFTbI zbMpF!x)RwowSjrb)f5XX4eKyf4wD|wyU`UCuY{XGA#mIpcz?qGdVR$H{LtrmEj<|s zkpPQb0^=@8&GRMXH+2Gz$fUXYHyNn)LY%SL&!esA!V^|^dgD1Qe0pp1&`lHK5?a-F z6P^$eW&YkJv?Fn5U_(K8r2_qq#P3IZK1AjIF}4tzGsP=byG-Fr(@fJDzjd?fSkbbQ z@1NRyKgx_hZ7#7~LpwyDKm+tV^Hm}k3~PMz9)k9XcE3EbVBRHZdnKk}ba6F>{|;C$ zPAqT{77?dY)QmFGm#xr{8*#`0?U;!s-bFS10S>`7GS0ZehhMmGl3YO}5)EmY_OaGaA;fp@{Gg!O$szM61+WI@JI*=^fS(6YLM1(_X4y`aZcn zH90NACX?Xl%+*bz-0Dlt-K|b&^0hAXH}=EU_EP;En7iiiQ`QDhqZx0a?w|H74NjAz z@ya_Pl|r93a2}|ATF=@b-q_QUUTiBvNWm{N-Iv{O4{w{V50NI1@Qu6{t;G)bS{kG7 zbfW_abl1Pfe)QuH?7yeR`llegCko7u_d!Wj_QMT16)p$G=aJFR%V9h>#&NR?jonmJJwnbe${2tk^4qUJw= zk9lWMh1mHx^ZmagE#j!q1n`$zAIECo(Yxj%pH4{CgBcXI*oVIAPNOELOk!D8;B8E> z9SzrU5*nQm7A>t@O>ds|rP;(ip!tI*0=PQ4V&J;E+T`SRHk?k6>^>M^9t6$twv^mm z1o^(%9uC{SE9Ny`&`hWIGg0Sb6Zm*w@f%n=C_zWh%yQ%;JWA-6%}rMM-|=$gOaAwfhmOVHbO+W~g6nPIb?A@u(K8)1vHsmfy%aeZve0wdi z=_976YPFhF|9#o|gwJTcLJ&U15KG(J1Lqw2@Inm*S1D)`MvQJtJXWjRPzDqSCBB(P zZZ<~9Bl~Gg*TqI@l1pz)-?6C}*KbfRo^WTG&`ME?$6bVBMV68ZA@Z7WC@@t?{?&V@ z8ScFKaWt;tKrw?mjB(%!j%=u&b#?gJ?$hB)i+T6P#pUJLh3`MNF~8+KZMe2g+QxyX zqa%Jg_k@FhPr9_rsigt{&S#g{FFNle6rNuqqmghnyfI!@25;m=zH`yz-U{^{Q%9_E zacK{D0x`y^b%b#srpevGIj?Ab#*Z$ds&yn|C)pw4*{Ar4=cjXd-!O0?YN&DQU&HUH z4#&O%G+zd90fmbji>|FD2WUUdvF_SBL{V0X*1XyB_6&!S1;fs^&?3y}=AG~=e-cK* zL*hS+syOe^jWk%b=TLvOMGpYD)1!eiN*8fkk(k<3fi-rB!e;5pkWpWz#@9fusacet zh!qW6ZcUFp3WNub$V28FSvLFoh{CRkKi=u_j?8*q2TA|Pu=0z9jw?(fxTosjw&HfU zSS|j7S!ywO2DoGWfC7`_@~FIOP(um0Q6A$g1^aywZ(-zMky z?z?j4%HNPhBd9Nh^g_{hlAAXHpc(Hfz=J%RC-w(Dw$1*ZMmSmhzg}^Y#AJ6l?eX7g zuA4pt^KwBrAtq4X*I+2i;5l7lu~1Y^VU1FQwA^RyYq$td5}}+g1hdy0LxEH2A2Dy3 zvE@a#eR*YEL&X7NB&D2;d&H5@r^C>17>sfj2?f=TEt5$A_-tj}cdLHhIj%0pWy-o( zMQG$qAOjhlYV~wh)0)GxmNjYqH&mh$otny{5(7u+Ga}DG|wcfeO{pB1&{GU?sQ zP-{TI!b0mVN^-f}=`_tB6->maY*;65g4D74)tRgdGU6*yMD)Q@5zn?J%= z=S&*aQ(YWY6(R=_L1rZ17;wniWD~ZTYv5bGLUCdyU1G8qZOkv4W-(z?=Tc=Y{ zo&s&@ra%#y45=j6V4BLLcZRvj4jl*zgxnEKwLAHeEnm2Hae*$xB_;ouz;1I^`#!by2Ykr@18uhmgYG zgJc9uwNCmoO@GvTPz&!KWBj1!2Y+nb37fN##gi{)I?YtJi#!xQoaTy(ZzyyQSe>Q= zH5YP<1ZWzVmBB57Y|{L!BESJ zxI=o7ud>`nF9gjZTV z<)7ru5&=zt=d^M9?n&-u^d1yW&`Tnb_}xujZwBT7>>H|vb?;kK;!INc!$`ZT76tdz z9rU;BXz}xn*P*Ypx#K^|IIr|&u-c#>>~5G757n0Q{PZ>F(T7I{Zlp_~xt9MvLH_g%rvcthdNaO!?YOJj<Hqvg$H~ zPZd=O?9zL!H0D(6v1S;VI{OCrB;tjYoq(eE0HrH=I3JI5yeC~=$cfA3rd!Ebz{;%{ z+N_ssF^>Tt^P>H!j3@7#m$c{G1`6kP2qG@#kW2gr4hjd5)hL+IGc!;56`~QEvKrfvAQptg*0~BS6gAKb$boV7V(3csDco z{2VRr_H-*uP$@X7qeP@{lYysov6dyu>m{p!m*ywSmA~1JT|=6X@PfQwe!6HiuEqdDdz&(GK+eDly@*rDEPlY~*q)A-`DX~#X^Qc2p=tMBINE`k#t zLr-*vN$o|1U!$vZ%-Ox8f;0k}I;NREiqI%P;}&rfNHm_6#y9Ic$yvK-LqF6Y*)d=4 z(romlOTR%u3jAS9vQ=bTIM18?_%^_RKGWG{ahEr`fxb3iq!ejw=8oK03yN&`?=l=& z-Uv6cWnliQ%0+na(Up-`&oAch-z6{d-Nv+?z$ghV0$v^K@e;)9iv$s$uf(cm(~J-I zIxe?Q&tv~R>dFM*LNZ3P({LEh6?VB#PhEh=j49=p-a7Mol=k2!a`CrK+u+x}q_I8D zQGFS*CtnInpOBmMzkL$Iuzj*Rf6+D@I_GDUUWHI7_&!v8H?A?jE`gSX?M{vZ?1O``<)S2H&TAeJsmw@6fN$ksZ@Q zl@Y@IEu*Ix)6y_lZ>=$X2M0RCrb|JHiku_lM=WvUU}X83VJKyxV$;sc{iT;)vhL*V_sSk6VQCHDm7G9}%)EJgq9K9(Rd3#GP zTzgKZAKQ|y~$mEgeFyEnEQE^vj+2`HY&lyO)P-R)oL= zofEoduSBCzyQZnPYHP5w3~I+&OVXh|VmJGcJhD@XJYJS+c#;LCfD+^Y*H z+{m4N@Lk@{9LZ*M$&sVWdXO5)}Ysw>Q6SJtn z(}8Ho76ik4Ex()Ge8yp5XBh#@LhFp2eSwZxSC?8pAD_g%WsIb+4?dG5+~4unIO>+1 zGRruT6;n$AAwI~zzSo1?jVvt<4blIndl%q-9v{DHo^$f4&ilpNL%NfgU~HdQ_Y`&( zaGw`0@%JvP^p}0Mx0Mt~c;$%Xn&j)vJ$uOSz?Q2o_5D5l7`a3?#r7ZNET{fp8+jIC zEquRz=?R3;s1l52gi)DX0*C7YsFihp*#!x_SD?*c#A=%!(TMpKZ*d%4UYP|xjnND} zLTIZI_cji^s2DGQTWG!x;-+{sD#2@V9^G`4Y5!=E-T+fXXC2hM&FoEuY3FT71~X)- zs)6F-EvQqu-~8JH?R5Bg$4Z!=t+?DIWe2}QT6OKV&E$`hm}Rq3@*p+@Pe6{k3ARfA zlRocpL#nj-Qg8fDPId=BP&N4_z+>D7duD-CI>_p&V97bZ94Qoeuk-yL(GYDGyZ()+ zBvHtrTSCLzlmg7K(?oU$X-zHP*dCMlzr?7Vg70if4)!n{q1G$??lMA$G~UkAYfYXn z616;`K1)kJDH8Fc?ziA6KyYQBb5X|bQU|d1pQ|=v_fp4jw#V_Kr4`9Uapc%4Ikr!H zHZGu(;^%#88`$@$eVW-M2Y=u-=8zN=jZ=))#kmST`+XI=r|beenb+((`T6XX-Q<6TthhTH zvD$pr!W+zaG%Co7F#z%JGnk>qYc+l^YWj+pAg zzu}2`;bf@wkAP~Wp@(a>e4B2}9k$N`F3?z-)3+qy@sc!dZsZw^0ApO!*3o6onZMaF zVf!T@11qeD3c)v2{!5Lj@uX+%)W+RTip?BIVs@4I`xzDhExwUvR?acSa|)RnG$ zmL>*+e~u_;Rmd?#Lc#DFG0*@aCe;WbYC6C@#!=&#RbfRWw6%x?h&zUy)Rx{0owz;i zvT5YFc{ST$cc1}?7q4b;K{@j_=N^X=Bbri@;Ob(Zt6b-x8S3e>8KSIVYp2MW@6suD z%_Jam2fpTU(nbrB7hS~F5;tfk$Y_N0UDNmG{SNJ0<0ouq>#M*j`M^V-WDKG(Fv-ia z3&1>>{J}oYn)+@CHFgGp-^Vr6i!B^lwDyk8DB}FwN`j=!$hAhV_A{NkRpOtWy#Zp& zK`)afAVLkbv#{|Fi%ngiO^?TB)MMYUF!K87{**qgwr%^b*BSSZ&2C%`MTG?l3Z> z0CC??K|CvwHiq0c)zbJ0zxv{g)R~+qabAJ?TqNV&@Z0EmwwGcmt!@_OyEPr>E8h7r zW&%I(-rHOHLD>OCL&cYPWK*ZVax$-*s7>tcq)2NTT6**6KzpVsw;P3N&+#HmdF;Clq-~N5L)2@{ zO@QoIHby)Zo6>v-`Hlnrva8fB;biQpH`~)osO6ePjK1mG?4p4D&UG{v_EK!vDu~W5xU+E>A zURI#}r_-Oq&PDtS#dQW?UQZe16y->Og9=zkKg7COxL9MsnhJI)P(6DGR2%v=Z!E~{ zdu1DXl?FLD;P*b
          From f6d89e64fc00a866e7a9aeebe4ac6063a347a7f9 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 8 Sep 2019 00:46:40 +0000 Subject: [PATCH 194/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ru/index.html b/ru/index.html index 653218c..9a367cc 100644 --- a/ru/index.html +++ b/ru/index.html @@ -79,6 +79,7 @@

          Комбинаторика и ТЧНахождение обратного по модулю малая теорема ферма, алгоритм евклида, возведение в степень
        12. Жадный алгоритм Радо-Эдмондса матроиды, жадный набор паросочетаний, расписаний, базисов
        13. Ро-алгоритм Полларда быстрая факторизация, пародокс дней рождений
        14. +
        15. α Ликбез по комбинаторике
        16. @@ -89,7 +90,7 @@

          Графы

        17. Паросочетания лемма бержа, алгоритм куна, лемма холла, покрытие dag-а
        18. Поток минимальной стоимости критерий оптимальности, потенциалы джонсона
        19. Центроидная декомпозиция подсчёт путей с заданным свойством, запросы на путях
        20. -
        21. α Обходы графов dfs, мосты, эйлеров путь, конденсация, 2-sat
        22. +
        23. α Обходы графов dfs, мосты, эйлеров путь, конденсация, 2-sat
        24. α Heavy-light декомпозиция
        25. From 657d1301e029aa46653a8344501a66e19af45401 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 14 Sep 2019 01:21:36 +0000 Subject: [PATCH 195/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/index.html | 76 +++++--- ru/shortest-paths.html | 162 ++++++++++++++++ ru/sorting.html | 393 +++++++++++++++++++++++++++++++++++++++ ru/stl.html | 410 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1013 insertions(+), 28 deletions(-) create mode 100644 ru/shortest-paths.html create mode 100644 ru/sorting.html create mode 100644 ru/stl.html diff --git a/ru/index.html b/ru/index.html index 9a367cc..1f5aec2 100644 --- a/ru/index.html +++ b/ru/index.html @@ -47,6 +47,7 @@

          Алгоритмика

          Структуры данных

          +

          Графы

          + +
          +
          +

          Строки

          + +
          +
          +

          Общие техники

          -

          Математика

          +

          Деревья

          + +
          +
          +

          Высшая математика


          -

          Комбинаторика и ТЧ

          +

          Теория чисел

          -

          Графы

          +

          Теория игр

          -

          Строки

          +

          Динамика


          -

          Геометрия

          +

          Комбинаторика

          -

          Теория игр

          +

          Геометрия

          @@ -126,7 +147,6 @@

          Разное

          diff --git a/ru/shortest-paths.html b/ru/shortest-paths.html new file mode 100644 index 0000000..3a27996 --- /dev/null +++ b/ru/shortest-paths.html @@ -0,0 +1,162 @@ + + + + + + + Кратчайшие пути в графе - Алгоритмика + + + + + + + + + + + + + + + +

          Кратчайшие пути в графе

          +

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

          +

          Как его хранить? Давайте просто в списке смежности вместо номеров вершин соседа хранить пару (номер соседа, вес ребра до него).

          +

          Давайте решать задачу посика кратчайшего пути в графе - мы хотим за наименьшую стоимость проехать из вершины \(A\) в \(B\).

          +

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

          +

          Для данной задачи есть несколько алгоритмов решения.

          +

          Алгоритм Флойда

          +

          Мы с вами уже знаем динамическое программирование, давайте рассмотри следующую динамику:

          +

          \(d_{i j}^k\) - это длина кратчайшего пути от вершины \(i\) до \(j\), используя как промежуточные только вершины из первых \(k\) = \(d_{i j}^k\) (напоминает рюкзак).

          +

          База динамики (\(k = 0\)) определяется только путями из одного ребра. Если есть ребро из \(i\) в \(j\) стоимостью \(c\), то \(d_{i j}^{0}\) = \(c\). Если таких ребер несколько, то, конечно, надо взять минимум.

          +

          Если мы хотим посчитать \(d_{i j}^{k}\), то у нас есть два варианта

          +
            +
          1. Не брать на пути нигде \(k\)-ую вершину, тогда \(d_{i j}^{k}\) = \(d_{i j}^{k - 1}\)

          2. +
          3. Взять где-нибудь в пути k-ую вершину, тогда путь разбивается на две части - от \(i\) до \(k\) и от \(k\) до \(j\), раз итоговый путь кратчайший, то и и эти два пути должны быть кратчайшми, а значит формула \(d_{i j}^{k}\) = \(d_{i k}^{k - 1}\) + \(d_{k j}^{k - 1}\)

          4. +
          +

          В результате ответ - \(d_{A B}^{n}\),

          +

          Можете подумать, как по такой динамике восстановить сам путь.

          +

          Также заметим, что вместо трехмерной динамики в этом алгоритме можно использовать двухмерную, храня в \(dp_{ij}\) последнее известное значение из \(dp_{ij}^0\), \(dp_{ij}^1\) \(dp_{ij}^2\), \(\ldots\), \(dp_{ij}^n\).

          +
          for(int k = 0; k < n; k++){
          +    for(int i = 0; i < n; i++){
          +        for(int j = 0; j < n; j++){
          +            a[i][j] = min(a[i][j], a[i][k] + a[k][j]);
          +        }
          +    }
          +}
          +

          Теоретическое задание

          +

          Подумайте, как находить циклы отрицательного веса с помощью Флойда.

          +

          Плюсы алгоритма в том, что он находит расстояние сразу от всех вершин графа до остальных, а минус - алгоритм работает за \(O(N^3)\);

          +

          Практическое задание

          +

          Первые две задачи на алгоритм Флойда.

          +

          Алгоритм Дейкстры

          +

          Для этого алгоритма придется рассматривать только графы без отрицательных рёбер.

          +

          Алгоритм Дейкстры решает немного другую задачу: он находит расстояние от одной вершины \(A\) до каждой вершины. Давайте для каждой вершины хранить расстояние до нее из вершины \(A\) в массиве \(d\). Например: * \(d[A] = 0\) * \(d[x] = \infty\), если x не достижима из A

          +

          Назовем релаксацией обновление ответа для вершины \(b\) через ребро \((a, b, c)\) таким способом: \(d[b] = \min(d[b], d[a] + c)\).

          +
            +
          • При таком действии ответ не может стать лучше, чем кратчайшее расстояние до \(b\), так как мы просто пользуемся путем для вершины \(a\) и продлеваем его в \(b\).
          • +
          • Если мы прорелаксировали все ребра в кратчайшем пути в правильном порядке до вершины \(b\), то мы получим кратчайший путь до \(b\).
          • +
          +

          Теперь давайте каждый раз доставать вершину, для которой расстояние от А сейчас минимально, и мы еще ее не смотрели и затем обновлять всех ее соседей. Допустим вершина с минимальным расстоянием - x, тогда надо прорелаксировать все ребра из нее.

          +

          Давайте докажем корректность алгоритма по индукции:

          +

          База) Первой вершиной мы всегда рассмотрим \(A\), но для нее верно, что расстояние от \(А\) до \(А\) = 0;

          +

          Шаг) Мы достали вершину \(i\), нам известно, что для всех ее предков ответ - корректен, но тогда, допустим, что для \(i\)-ой вершины мы нашли ответ больший, чем надо, тогда это значит, что мы должны прийти из еще не рассмотренной вершины, но так как ребер отрицательного веса в графе нет, то такое невозможно \(\Rightarrow\) мы доказали

          +

          https://visualgo.net/en/sssp?slide=1

          +

          Есть две возможные реализации алгоритма

          +
            +
          1. реализация помощью нахождения минимального расстояния внутри массива за линию. Так как мы для каждого шага находим минимальную вершину, то мы сделаем не более \(N\) шагов, но при этом на каждом шаге мы находим минимум за линию, то есть алгоритм работает за \(O(N^2)\).
          2. +
          +
          for (int i = 0; i < n; ++i){
          +    int uk = -1;
          +    for (int j = 0; j < n; j++){
          +        if ((mark[j] == 0) && ((uk == -1) || (d[j] < d[uk]))){
          +                uk = j;
          +        }
          +    }
          +    for (int j = 0; j < n; j++){
          +        if ((j != uk) && (g[uk][j] != -1)) d[j] = min(d[j], g[uk][j] + d[uk]);
          +    }
          +    mark[uk] = 1;
          +}
          +
            +
          1. Реализация за \(O(MlognN + NlogN)\) с помощью нахождения минимального расстояния внутри кучи/сета за логарифм. Так как для каждой вершины мы сделаем не более одного извлекания из структуры + каждое ребро мы используем максимум два раза. Для этого давайте в выбранной структуре хранить пару (расстояние, вершина); Первый код реализован с set, второй с кучей. Так как в куче нет компаратора на возрастание, то надо либо сделать свой, либо домножить расстояние на -1;
          2. +
          +
          while (used.size()) {
          +    int v = (*(used.begin())).second;
          +    for (int i = 0; i < g[v].size(); i++) {
          +        int to = g[v][i].first, c = g[v][i].second;
          +        if (dist[v] + c < dist[to]) {
          +            used.erase({dist[to], to});
          +            dist[to] = dist[v] + to;
          +            used.insert({dist[to], to});
          +        }
          +    }
          +    used.erase({dist[v], v});
          +}
          +
          while (!q.empty()) {
          +        int v = q.top().second;
          +        q.pop();
          +    for (int j = 0; j < g[v].size(); ++j) {
          +        int to = g[v][j].first, c = g[v][j].second;
          +        if (d[v] + c < d[to]) {
          +            d[to] = d[v] + c;
          +            q.push({-d[to], to});
          +        }
          +    }
          +}
          +

          Из асимптотики понятно, что при большом количестве ребер первый алгоритм писать лучше, иначе второй.

          +

          Теоретическое задание

          +

          Приведите примеры, когда каждый из алгоритмов работает лучше.

          +

          Недостаток алгоритма Дейкстры всего один - он не работает, если в графе есть ребра отрицательного веса.

          +

          Практическое задание

          +

          3-5 Задача на алгоритм Дейкстры.

          +

          Форд-Беллман

          +

          Этот алгоритм решает ту же задачу, что и Дейкстра, но зато может работать с отрицательными ребрами!

          +

          Давайте заведем массив расстояний, как и в дейкстре, для стартовой вершины расстояние = 0. Алгоритм состоит в релаксации каждого ребра в графе \(N-1\) раз.

          +

          Это работает потому, что в кратчайшем пути не больше, чем \(N-1\) ребро, и если мы прорелаксируем их в таком порядке, этот путь найдется. После \(N-1\) прохода по всем ребрам и их релаксации мы точно это сделаем и найдем кратчайший путь.

          +

          Также в этом случае удобнее хранить список ребер явно вместо списка смежности.

          +
          int from[m], to[m], cost[m];
          +for (int i = 0; i < n - 1; ++i) {
          +        for (int j = 0; j < m; ++j) {
          +                d[to[j]] = min(d[to[j]], d[from[j]] + cost[j]);
          +    }
          +}
          +

          Теоретическое задание

          +

          Подумайте, как найти цикл отрицательного веса с помощью этого алгоритма.

          +

          Практическое задание

          +

          Решите задачи 6 и 7.

          +

          Ссылка на контест

          +

          https://informatics.msk.ru/mod/statements/view.php?id=33380#1

          + + diff --git a/ru/sorting.html b/ru/sorting.html new file mode 100644 index 0000000..2ec0f20 --- /dev/null +++ b/ru/sorting.html @@ -0,0 +1,393 @@ + + + + + + + Сортировки и O-нотация - Алгоритмика + + + + + + + + + + + + + + + + +

          Сортировки и O-нотация

          +

          Задача сортировки массива заключается в том, чтобы расставить его элементы в определённом порядке (чаще всего — по неубыванию: каждый элемент должен быть больше или равен предыдущему).

          +

          Будет полезно вместе с описанем алгоритмов смотреть их визуализацию.

          +

          Сортировка пузырьком

          +

          Наш первый подход будет заключаться в следующем: обозначим за \(n\) длину массива и \(n\) раз пройдёмся раз пройдемся по нему, меняя два соседних элемента, если первый больше второго.

          +

          Как каждую итерацию максимальный элемент «всплывает» словно пузырек к концу массива — отсюда и название.

          + +

          После \(i\) шагов алгоритма сортировки пузырьком последние \((i + 1)\) чисел всегда отсортированы, а значит алгоритм работает корректно.

          +

          Упражнение. Алгоритм можно немного ускорить. Подумайте, какие лишние элементы мы перебираем. Как нужно изменить границы в двух циклах for, чтобы не делать никаких бесполезных действий?

          +

          Сортировка выбором

          +

          Другим способом является сортировка выбором минимума (или максимума).

          +

          Чтобы отсортировать массив, просто \(n\) раз выберем минимум среди еще неотсортированных чисел и поставим его на свое место. На \(i\)-ом шаге будем искать минимум на отрезке \([i, n - 1]\) и менять его местами с \(i\)-тым элементом, после чего отрезок \([0, i]\) будет отсортирован.

          +

          Содержательная часть будет выглядеть так:

          + +

          Сортировка вставками

          +

          Определение. Префиксом длины \(i\) будем называть первые \(i\) элементов массива.

          +

          Тогда пусть на \(i\)-ом шаге у нас уже будет отсортирован префикс до \(i\)-го элемента. Чтобы этот префикс увеличить, нужно взять элемент, идущий после него, и менять с левым соседом, пока этот элемент наконец не окажется больше своего левого соседа. Если в конце он больше левого соседа, но меньше правого, то это будет означать, что мы правильно вставили этот элемент в отсортированную часть массива.

          +
          for (int i = 1; i < n; i++) {
          +    for (int j = i; j > 0; j--) {
          +        if (a[j - 1] < a[j]) {
          +            break;
          +        }
          +        swap(a[j], a[j - 1]);
          +    }
          +}
          +

          Сортировка подсчетом

          +

          Предыдущие три алгоритма работали с массивами, в которых лежат абсолютно любые объекты, которые можно сравнивать: любые числа, строки, пары, другие массивы — почти все что угодно.

          +

          Но особых случаях, когда элементы принадлежат какому-то маленькому множеству, можно использовать другой алгоритм — сортировку подсчетом.

          +

          Пусть, например, нам гарантируется, что все числа натуральные и лежат в промежутке от \(1\) до \(100\). Тогда есть такой простой алгоритм:

          +
            +
          • Создадим массив размера \(100\), в котором будем хранить на \(k\)-ом месте, сколько раз число \(k\) встретилось в этом массиве.

          • +
          • Пройдемся по всем числам исходного массива и увеличим соответствующее значение массива на \(1\).

          • +
          • После того, как мы посчитали, сколько раз каждое число встретилось, можно просто пройтись по этому массиву и вывести \(1\) столько раз, сколько встретилась \(1\), вывести \(2\) столько раз, сколько встретилась \(2\), и так далее.

          • +
          +

          Время работы такого алгоритма составляет \(O(M+N)\), где \(M\) — число возможных значений, \(N\) — число элементов в массиве. Сейчас мы расскажем, что же это означает.

          +

          О-нотация

          +

          Часто требуется оценить, сколько времени работает алгоритм. Но тут возникают проблемы:

          +
            +
          • на разных компьютерах время работы всегда будет слегка отличаться;
          • +
          • чтобы измерить время, придётся запустить сам алгоритм, но иногда приходится оценивать алгоритмы, требующие часы или даже дни работы.
          • +
          +

          Зачастую основной задачей программиста становится оптимизировать алгоритм, выполнение которого займёт тысячи лет, до какого-нибудь адекватного времени работы. Поэтому хотелось бы уметь предсказывать, сколько времени займёт выполнение алгоритма ещё до того, как мы его запустим.

          +

          Для этого сначала попробуем оценить число операций в алгоритме. Возникает вопрос: какие именно операции считать. Как один из вариантов — учитывать любые элементарные операции:

          +
            +
          • арифметические операции с числами: +, -, *, /
          • +
          • сравнение чисел: <, >, <=, >=, ==, !=
          • +
          • присваивание: a[0] = 3
          • +
          +

          При этом надо учитывать, как реализованы некоторые отдельные вещи в самом языке. Например, в питоне срезы массива (array[3:10]) копируют этот массив, то есть этот срез работает за 7 элементарных действий. А swap, например, может работать за 3 присваивания.

          +

          Упражнение. Попробуйте посчитать точное число сравнений и присваиваний в сортировках пузырьком, выбором, вставками и подсчетом в худшем случае (это должна быть какая формула, зависящая от \(n\) — длины массива).

          +

          Чтобы учесть вообще все элементарные операции, ещё надо посчитать, например, сколько раз прибавилась единичка внутри цикла for. А ещё, например, строчка n = len(array) — это тоже действие. Поэтому даже посчитав их, сразу очевидно, какой из этих алгоритмов работает быстрее — сравнивать формулы сложно. Хочется придумать способ упростить эти формулы так, чтобы:

          +
            +
          • не нужно было учитывать много информации, не очень сильно влияющей на итоговое время;
          • +
          • легко было оценивать время работы разных алгоритмов для больших чисел;
          • +
          • легко было сравнивать алгоритмы на предмет того, какой из них лучше подходит для тех или иных входных данных.
          • +
          +

          Для этого придумали \(O\)-нотацию — асимптотическое время работы вместо точного (часто его ещё называют асимптотикой).

          +

          Определение. Пусть \(f(n)\) - это какая-то функция. Говорят, что алгоритм работает за \(O(f(n))\), если существует число \(C\), такое что алгоритм работает не более чем за \(C \cdot f(n)\) операций.

          +

          В таких обозначениях можно сказать, что

          +
            +
          • сортировка пузырьком работает за \(O(n^2)\);
          • +
          • сортировка выбором работает за \(O(n^2)\);
          • +
          • сортировка вставками работает за \(O(n^2)\);
          • +
          • сортировка подсчетом работает за \(O(n + m)\).
          • +
          +

          Это обозначение удобно тем, что оно короткое и понятное, а также оно не зависит от умножения на константу или прибавления константы. Например, если алгоритм работает за \(O(n^2)\), то это может значить, что он работает за \(n^2\), за \(n^2 + 3\), за \(\frac{n(n-1)}{2}\) или даже за \(1000 \cdot n^2 + 1\) действие. Главное — что функция ведет себя как \(n^2\), то есть при увеличении \(n\) (в данном случае это длина массива) он увеличивается как некоторая квадратичная функция. Например, если увеличить \(n\) в 10 раз, время работы программы увеличится приблизительно в 100 раз.

          +

          Все рассуждения про то, сколько операций в swap или считать ли отдельно присваивания, сравнения и циклы — отпадают. Каков бы ни был ответ на эти вопросы, они меняют ответ лишь на константу, а значит асимптотическое время работы алгоритма никак не меняется.

          +

          Первые три сортировки именно поэтому называют квадратичными — они работают за \(O(n^2)\). Сортировка подсчетом может работать намного быстрее — она работает за \(O(n + m)\), а если в задаче \(M \leq N\), то это вообще линейная функция \(O(n)\).

          +

          Упражнение. Найдите асимптотику данных функций, маскимально упростив ответ (например, до \(O(n)\), \(O(n^2)\) и т. д.):

          +
            +
          • \(\frac{N}{3}\)
          • +
          • \(\frac{N(N-1)(N-2)}{6}\)
          • +
          • \(1 + 2 + 3 + \ldots + N\)
          • +
          • \(1^2 + 2^2 + 3^2 + \ldots + N^2\)
          • +
          • \(\log{N} + 3\)
          • +
          • \(179\)
          • +
          • \(10^{100}\)
          • +
          +

          Упражнение. Найдите асимптотическое время работы данных функций:

          + + + +

          Упражнение. Найдите лучшее время работы алгоритмов, решающих данные задачи:

          +
            +
          • Написать числа от \(1\) до \(n\).
          • +
          • Написать все тройки чисел от \(1\) до \(n\).
          • +
          • Найти разницу между максимумом и минимумом в массиве.
          • +
          • Найти число единиц в бинарной записи числа \(n\).
          • +
          +

          Сортировки за \(O(n \log n)\)

          +

          Сортировка очень часто применяется как часть решения олимпиадных задач. В таких случаях обычно используют встроенную сортировку sort. В разных языках она может быть реализована по-разному, но везде она работает за \(O(n \log n)\), и, обычно, неплохо оптимизирована.

          + + +

          Количество пар с разницей, больше чем K

          +
          +

          Найти количество пар элементов \(a\) и \(b\) в отсортированном массиве, такие что \(b - a > K\).

          +
          +

          Наивное решение: бинарный поиск. Будем считать, что массив уже отсортирован. Для каждого элемента \(a\) найдем первый справа элемент \(b\), который входит в ответ в паре с \(a\). Нетрудно заметить, что все элементы, большие \(b\), также входят в ответ. Итоговая асимптотика \(O(n\log n)\).

          +

          А можно ли быстрее?

          +

          Да, давайте перебирать два указателя — два индекса \(first\) и \(second\). Будем перебирать \(first\) просто слева направо и поддерживать для каждого \(first\) первый элемент справа от него, такой что \(a[second] - a[first] > K\) как \(second\). Тогда в пару к \(a=a[first]\) подходят ровно \(n-second\) элементов массив начиная с \(second\).

          +
          int second = 0, ans = 0;
          +for (int first = 0; first < n; ++first) {
          +    while (second != n && a[second] - a[first] <= r) {
          +        second++;
          +    }
          +
          +    ans += n - second;
          +}
          +

          За сколько же работает это решение? С виду может показаться, что за \(O(n^2)\), но давайте посмотрим сколько раз меняется значение переменной \(second\). Так как оно изначально равняется нулю, только увеличивается и не может превысить \(n\), то суммарно операций мы сделаем \(O(n)\).

          +

          Это называется метод двух указателей — так как мы двигаем два указателя first и second одновременно слева направо по каким-то правилам. Обычно его используют на одном отсортированном массиве.

          +

          Давайте разберем еще примеры.

          +

          Слияние

          +

          Еще пример двух указателей на нескольких массивах.

          +

          Пусть у нас есть два отсортированных по неубыванию массива размера \(n\) и \(m\). Хотим получить отсортированный массив размера \(n + m\) из исходных.

          +

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

          +
          int a[n + 1], b[m + 1], res[n + m];
          +
          +a[n] = INF; // Создаем в конце массива фиктивный элемент, который заведомо больше остальных
          +b[m] = INF; // Чтобы избежать лишних случаев
          +
          +for (int i = 0; i < n; ++i) {
          +    cin >> a[i];
          +}
          +
          +for (int j = 0; j < m; ++j) {
          +    cin >> a[j];
          +}
          +
          +int i = 0, j = 0;
          +for (int k = 0; k < n + m; ++k) {
          +    if (a[i] < b[j]) {
          +        res[k] = a[i];
          +        i++;
          +    } else {
          +        res[k] = b[j];
          +        j++;
          +    }
          +}
          +

          Итоговая асимптотика: \(O(n + m)\).

          +

          Сортировка слиянием

          +

          Давайте подробно опишем как использовать операцию слияния для сортировки за \(O(n\log n)\).

          +

          Пусть у нас есть какой-то массив.

          +
          int a[8] = {7, 2, 5, 6, 1, 3, 4, 8};
          +

          Сделаем такое предположение. Пусть мы уже умеем как-то сортировать массив размера \(n\). Тогда научимся сортировать массив размера \(2n\). Давайте разобьем наш массив на две половины, отсортируем каждую из них, а после это сделаем слияние двух массивов, которое мы научились делать за \(O(n)\) в данных условиях. Также заметим, что массив размера \(1\) уже отсортирован, тогда мы можем делать это процедуру рекурсивно. Тогда для данного массива \(a\) это будет выглядеть следующим образом:

          +
          // (7 2 5 6 1 3 4 8)
          +// (7 2 5 6) (1 3 4 8)
          +// (7 2) (5 6) (1 3) (4 8)
          +// (2 7) (5 6) (1 3) (4 8)
          +// (2 5 6 7) (1 3 4 8)
          +// (1 2 3 4 5 6 7 8)
          +
          +#include  // Воспользуемся встроенной функцией merge
          +
          +void merge_sort(vector &v, int l, int r) { // v - вектор, который хотим отсортировать
          +    if (r - l == 1) {                            // l и r - полуинтервал, который хотим отсортировать
          +        return;
          +    }
          +
          +    int mid = (l + r) / 2;
          +    merge_sort(v, l, mid);
          +    merge_sort(v, mid, r);
          +    vector temp(r - l); // временный вектор
          +    merge(v.begin() + l, v.begin() + mid, v.begin() + mid, v.begin() + r, c.begin());
          +    for (int i = 0; i < r - l; ++i) {
          +        v[i + l] = temp[i];
          +    }
          +    return;
          +}
          +

          Так сколько же работает это решение?

          +

          Пускай \(T(n)\) — время сортировки массива длины \(n\), тогда для сортировки слиянием справедливо \(T(n)=2T(n/2)+O(n)\) \(O(n)\) — время, необходимое на то, чтобы слить два массива длины n. Распишем это соотношение:

          +

          \(T(n)=2T(n/2)+O(n)=4T(n/4)+2O(n)=\ldots=T(1)+\log(n)O(n)=O(n\log(n)).\)

          +

          Количество инверсий

          +

          Пусть у нас есть некоторая перестановка \(a\). Инверсией называется пара индексов \(i\) и \(j\) такая, что \(i < j\) и \(a[i] > a[j]\).

          +
          +

          Найти количество инверсий в данной перестановке.

          +
          +

          Очевидно, что эта задача легко решается обычным перебором двух индексов за \(O(n^2)\):

          +
          int a[n], ans = 0;
          +
          +for (int i = 0; i < n; ++i) {
          +    for (int j = i + 1; j < n; ++j) {
          +        if (a[i] > a[j]) {
          +            ans++;
          +        }
          +    }
          +}
          +
          +cout << ans << endl;
          +

          Внезапно эту задачу можно решить используя сортировку слиянием, слегка модифицируя её. Оставим ту же идею. Пусть мы умеем находить количество инверсий в массиве размера \(n\), научимся находить количество инверсий в массиве размера \(2n\).

          +

          Заметим, что мы уже знаем количество инверсий в левой половине и в правой половине массива. Осталось лишь посчитать число инверсий, где одно число лежит в левой половине, а второе в правой половине. Как же их посчитать?

          +

          Давайте подробнее рассмотрим операцию merge левой и правой половины (которую мы ранее заменили на вызов встроенной функции merge). Первый указатель указывает на элемент левой половины, второй указатель указывает на элемент второй половины, мы смотрим на минимум из них и этот указатель вдигаем вправо.

          +

          Рассмотрим число \(A\) в левой половине. В скольки инверсиях между половинами оно участвует? В стольки, сколько чисел в правой половине меньше, чем оно. Знаем ли мы это количество? Да! Ровно в тот момент, когда мы число \(A\) вносим в слитый массив, второй указатель указывает на первое число в правой половине, которое больше чем \(A\).

          +

          Значит в тот момент, когда мы добавляем число \(A\) из левой половины, к ответу достаточно прибавить индекс второго указателя (минус начало правой половины). Так мы учтем все инверсии между половинами.

          +

          Быстрая сортировка

          +

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

          +

          https://visualgo.net/en/sorting

          +
          void quicksort(int l, int r){
          +    if (l < r){
          +        int index = (l + r) / 2; /* index - индекс опорного элемента для 
          +        начала сделаем его равным середине отрезка*/
          +        index = divide(l, r, index); /* divide - функция разбивающие элементы 
          +        на меньшие и больше/равные a[index], 
          +        при этом функция возвращает границу разбиения*/
          +        quicksort(l, index);
          +        quicksort(index + 1, r);
          +    }
          +}
          +

          Давайте оценим асимптотику данной сортировки. На случайных данных она работает за \(O(NlogN)\) , так как каждый раз мы будем делить массив на две примерно равные части, то есть суммарно размер рекурсии будет около логарифма и при этом на каждом этапе рекурсии мы просмотрим не более, чем размер массива. Однако можно легко найти две проблемы, одна - одинаковые числа, а вторая - если вдруг середина - минимум или максимум.

          +

          Существуют несколько выходов из этой ситуации :

          +
            +
          1. Давайте если быстрая сортировка работает долго, то запустим любую другую сортировку за \(NlogN\).

          2. +
          3. Давайте делить массив не на две, а на три части(меньше, равны, больше).

          4. +
          5. Чтобы избавиться от проблемы с максимумом/минимумом в середине, давайте брать случайный элемент.

          6. +
          +

          Поиск \(k\)-ой порядковой статистики за \(O(N)\)

          +

          Пусть дан массив \(A\) длиной \(N\) и пусть дано число \(K\). Задача заключается в том, чтобы найти в этом массиве \(K\)-ое по величине число, т.е. \(K\)-ую порядковую статистику.

          +

          Давайте поймем, что в быстрой сортировке мы можем узнать, сколько элементов меньше данного, тогда рассмотрим три случая

          +
            +
          1. количество чисел, меньше данного = \(k - 1\), тогда наше число - ответ.

          2. +
          3. количество чисел, меньше данного >= \(k\), тогда спускаемся рекурсивно в левую часть и ищем там ответ.

          4. +
          5. количество чисел, меньше данного < \(k\), спускаемся в правую ищем (\(k\) - левая - 1) - ое число.

          6. +
          +

          За сколько же это работает, из быстрой сортировки мы имеем, что размер убывает приблизительно в 2 раза, то есть мы имеем сумму \(\sum_{k=1}^n {2 ^ k} = {2^{k+1}-1}\) что в нашем случае это максимум равно \(2 * N - 1\), то есть \(O(N)\).

          +

          Также в с++ эта функция уже реализована :

          +
          nth_element(указатель на начало, указатель на нужный элемент, указатель на конец);
          + + diff --git a/ru/stl.html b/ru/stl.html new file mode 100644 index 0000000..eaf7bb4 --- /dev/null +++ b/ru/stl.html @@ -0,0 +1,410 @@ + + + + + + + C++ и Standard Template Library (STL) - Алгоритмика + + + + + + + + + + + + + + + +

          C++ и Standard Template Library (STL)

          +

          Первый плюс плюсов - STL, она содержит в себе множество алгоритмов и структур, чтобы ее использовать вам нужно писать

          +
          // либо один раз такую строчку
          +using namespace std;
          +
          +// либо каждый раз писать перед каждой функцией и структурой из STL std::
          +std::vector
          +
            File "<ipython-input-1-3fdc4c3a8d74>", line 1
          +    (/, либо, один, раз, такую, строчку)
          +     ^
          +SyntaxError: invalid syntax
          +

          https://en.cppreference.com/w — это сайт с документацией по языку C++. Там вы можете найти много полезной информации как о самом языке, так и о его стандартных библиотеках. Здесь же вы сможете найдите краткое изложение самого полезного из STL.

          +

          Поговорим об обычных сишных массивах

          +
          T a[x]; // инициализация массива a типа T размера x
          +

          Если вы пишите так, прекращайте, у этого метода много проблем, давайте напишем правильную реализацию и обговорим плюсы и минусы.

          +
          array<T, x> a;
          +a.begin(), a.end() // указатели (итераторы) на начало и конец массива соответственно (конец массива = после последнего элемента)
          +a.front(), a.back() // возвращает ссылку на первый и последний элемент соответственно
          +cout << a.at(5); // метод at позволяет узнать не вышли ли мы за границы массива
          +

          Плюсы :

          +
            +
          1. Указатели (итераторы) на начало и конец - прекрасная вещь, которую мы обсудим позже

          2. +
          3. Сишные массивы уже сейчас противоречат шаблону, в будущем их хотят вообще убрать, так что лучше от них отказаться сейчас

          4. +
          5. Быстрый переход от массива к вектору

          6. +
          +

          Минусы :

          +
            +
          1. Тяжело отвыкнуть от старого массива.
          2. +
          +

          vector

          +

          vector — это динамический массив. Это означает, что его размер может меняться во время исполнения программы, вы можете добавлять элементы в конец и так далее. Чтобы объявить пустой vector, способный содержать в себе целые числа типа T, необходимо воспользоваться следующей конструкцией:

          +
          vector<T> vector_name;
          +

          Здесь T — это тип элементов, которые будут содержаться в vector, а vector_name — имя самого vector. Как и другие контейнеры C++, vector не может содержать элементы разных типов!

          +

          Чтобы добавить элемент в конец вектора, необходимо воспользоваться функцией push_back. Эта функция работает в среднем за \(O(1)\) .

          +

          Существуют два способа обращения к ‘vector’. Оба мы уже обсудили в массивах.

          +
          vector<int> a;
          +cout << a[5];
          +cout << a.at(5);
          +

          Кроме этого вам также могут потребоваться следующие методы :

          +
          vector<int> a;
          +a.push_back(x); // вставляет x в конец a
          +a.size(); // возращает размер вектора а
          +a.resize(x) // сделать размер вектора = x, либо удаляются последние элементы, либо добавляются нули
          +a.resize(x, y) // сделать размер вектора = x, добавляются y
          +// Также можно изначально задать размер и элементы массива
          +vector<int> a(10); // Размер вектора - 10 элементов, каждый из которых равен 0
          +vector<int> b(5, 123); // Размер - 5 элементов, каждый из которых равен 123
          +vector<int> c = {1, 2, 3} // явная инициализация
          +

          Как работает vector?

          +

          Как уже было сказано, добавление в конец вектора работает в среднем за \(O(1)\). Это означает, что, если вы сделаете \(n\) операций push_back, они будут в сумме работать за \(O(n)\). (Но при этом некоторые из них могли работать и за линейное время!)

          +

          У vector есть 2 важные величины: size и capacity — размер и вместимость. Размер — это то, сколько элементов сейчас находится в векторе. Вместимость — то, под сколько элементов памяти выделено. Когда size < capacity, push_back просто добавляет новый элемент в первую свободную ячейку уже выделенной памяти, поэтому работает за \(O(1)\). Когда size = capacity, так сделать не удастся. Поэтому, происходит следующее: 1. capacity увеличивается примерно в 2 раза. 2. Выделяется область памяти, вмещающая capacity элементов. 3. Элементы из старой области памяти копируются в новую. 4. Старая область памяти освобождается.

          +

          Поймём, почему амортизированное время работы push_back действительно равно \(O(1)\). Пусть сейчас capacity = \(n\). Тогда мы выделяли \(n + \frac{n}{2} + \frac{n}{4} + \dots < 2n\) памяти. На копирование также ушло не более \(2n\) операций. Следовательно, так как операций push_back было хотя бы \(\frac{n}{2}\), каждая операция в среднем работала за \(O(1)\).

          +

          Получить capacity у vector можно с помощью одноимённой функции. Рассмотрим пример того, как изменяется capacity.

          +
          vector<int> a;
          +for (int i = 0; i < 10; i++) {
          +    a.push_back(i);
          +    cout << "Size: " << a.size() << ", capacity " << a.capacity() << "\n";
          +}
          +
          +/* Будет выведено:
          +   Size: 1, capacity 1
          +   Size: 2, capacity 2
          +   Size: 3, capacity 4
          +   Size: 4, capacity 4
          +   Size: 5, capacity 8
          +   Size: 6, capacity 8
          +   Size: 7, capacity 8
          +   Size: 8, capacity 8
          +   Size: 9, capacity 16
          +   Size: 10, capacity 16
          +*/
          +

          pair

          +

          pair — это тип, содержащий пару значений, притом значения могут быть разных типов. Объявление пары выглядит так:

          +
          pair<T1, T2> p;
          +

          Здесь T1 и T2 — это имена первого и второго типов, соответственно.

          +

          Первый элемент пары — это p.first; второй — p.second.

          +

          make_pair(a, b) — функция, которая создаёт пару \((a, b)\).

          +

          Рассмотрим пример работы с pair.

          +
          pair<int, double> p = make_pair(1, 2.0);
          +pair<int, double> q = {1, 2.5}; // другой способ инициализировать пару
          +cout << p.first << " " << p.second << "\n";
          +

          queue

          +

          В c++ уже реализована такая структура, как очередь, она названа queue.

          +
          queue<T> q; //очередь типа T
          +

          Очередь - структура, реализующая принцип FIFO (первый пришел - первый вышел), то есть для очереди существуют две основные функции : Вставить в конец и достать с начала.

          +
          q.front(); // ссылка на первый элемент
          +q.back(); // ссылка на последний элемент
          +q.push(x); // добавить в конец
          +q.pop(); //удалить с начала
          +

          deque

          +

          deque - структура, позволяющая работать и с началом и концом одновременно, то есть вставка и удаление с двух сторон

          +
          deque<T> name; // дек типа T с названием name
          +name.front(), name.back(); // первый и последний элемент соответственно
          +name.pop_front(), name.pop_back(); // удаление первого и последнего элемента
          +name.push_front(x), name.push_back(x); // вставка x в начало/конец
          +

          Очередь и дек будут более подробно рассмотрены на одном из следующих занятий.

          +

          Итераторы

          +

          Итератор — это объект, указывающий на элемент контейнера. Чтобы получить элемент, на который указывает итератор it, необходимо воспользоваться оператором разыменования: *it. Также если вам нужно перейти к следующему элементу надо использовать инкремент: ++it.

          +

          Есть несколько категорий итераторов: - InputIterator. Он поддерживает только операции разыменования и инкремента, притом после того, как был произведён инкремент, все копии его предыдущего значения могут стать невалидными. - ForwardIterator. Поддерживает то же, что и InputIterator, но итератор, указывающий на какой-то конкретный элемент, можно инкрементировать сколько угодно раз. - BidirectionalIterator. Поддерживает то же, что и ForwardIterator, но также есть возможность производить декремент (it--) — переходить к предыдущему элементу коллекции. - RandomAccessIterator. Поддерживает то же, что и BidirectionalIterator, но также есть возможность переходить к элементу коллекции, который находится от данного на каком-то расстоянии \(k\). Так, например, для итератора it возможны следующии операции: it + k, it - k, it += k, it -= k. Также можно находить расстояние между двумя позициями, на которые указывают итератора. Так, например, выражение a - b будет означать расстояние между двумя элементами коллекции, на которые указывают итераторы a и b.

          +

          Рассмотрим использование итераторов на примере vector. Для вектора a итератор на его первый элемент можно получить так: a.begin(). Также есть функция, которая возвращает итератор на фиктивный элемент, следующий за последним элементом вектора: a.end(). Таким образом, весь a задаётся полуинтервалом [a.begin(); a.end()) (левый конец включается, правый — нет).

          +

          Итераторы у vector относятся к категории RandomAccessIterator, то есть например мы можем узнать размер vector \(a\), просто взяв a.begin() - a.end(). При этом у vector типа vector<T> итератор будет иметь тип vector<T>::iterator.

          +

          Рассмотрим пример работы с итераторами у vector.

          +
          vector<int> a = {1, -2, 3, 100};
          +
          +vector<int>::iterator first_element = a.begin();
          +cout << *first_element << "\n"; // выведет 1
          +first_element++;
          +cout << *first_element << "\n"; // выведет -2
          +first_element--;
          +cout << *first_element << "\n"; // выведет 1
          +auto third_element = first_element + 2; // пользуемся типом auto, чтобы не писать длинное имя типа
          +cout << *third_element << "\n"; // выведет 3
          +cout << (third_element - first_element) << "\n"; // выведет 2
          +
          +// вывод всех элементов вектора с использованием итераторов
          +for (auto it = a.begin(); it != a.end(); it++) {
          +    cout << *it << " ";
          +}
          +cout << "\n"; // но для таких целей лучше использовать range-based for loop:
          +  
          +for (auto& it : a) {
          +    cout << a << " ";
          +}
          +

          list

          +

          list - структура, которая поддерживает быструю вставку и удаление элементов из любой позиции в контейнере. Быстрый произвольный доступ, к сожалению, не поддерживается (то есть мы не можем быстро взять \(i\)-й элемент). Он реализован в виде двусвязного списка

          +
          list<T> name; // создание листа типа Т с именем name
          +name.insert(it, x); // вставка после итератора it переменной x
          +name.erase(it); // удаляет элемент, на который указывает итератор
          +name.front(), name.back(); // начало и конец name
          +

          set

          +

          set — это коллекция, которая содержит множество уникальных упорядоченных элементов.

          +

          Чтобы добавить элемент в set, есть функция insert. В случае, если элемент уже был в множестве, ничего не происходит.
          +Чтобы удалить элемент из set, есть функция erase(в нее можно передать либо итератор на элемент, либо просто элемент). В случае, если элемента не было в множестве, ничего не происходит.
          +Чтобы посмотреть, если ли элемент в set, есть функция count. Она вернёт \(0\), если элемента нет в множестве, и \(1\), если он есть. Также есть метод find,  который возвращает итератор на элемент или end, если элемента нет.

          +

          Все операции с элементами set (добавление, удаление, поиск) работают за \(O(\log n\)), где \(n\) — количество элементов в нём, так как он реализован с помощью сбалансированного двоичного дерева поиска.

          +

          Итераторы set относятся к категории BidirectionalIterator и имеют тип set<T>::iterator. Начало set можно получить с помощью функции begin, конец — с помощью функции end. Как и в случае с вектором, end указывает на конец полуинтервала. Инкремент и декремент итераторов set также работают за логарифмическое время.

          +

          Стоит отметить, что, так как элементы в set упорядочены, с помощью begin и end можно искать наименьший/наибольший элемент в set. Чтобы найти наименьший элемент, больший или равный заданному, есть функция lower_bound.
          +Чтобы найти наименьший элемент, строго больший заданному, есть функция upper_bound.
          +Каждая из этих функций возвращает итератор на искомый элемент или end(), если такого элемента не существует.

          +

          set может содержать только элементы тех типов, для которых определён оператор <, поскольку ему важен порядок элементов.

          +

          Рассмотрим пример простейших операций с set.

          +
          set<int> s;
          +
          +s.insert(3); // s = {3}
          +s.insert(2); // s = {2, 3}
          +cout << s.size() << "\n"; // выведет 2
          +
          +s.insert(3); // 3 не будет добавлено ещё раз, так как уже присутствует в множестве
          +cout << s.size() << "\n"; // выведет 2
          +
          +s.insert(5); // s = {2, 3, 5}
          +cout << s.count(3) << "\n"; // выведет 1
          +cout << s.count(4) << "\n"; // выведет 0
          +
          +s.erase(3); // s = {2, 5}
          +s.insert(6); // s = {2, 5, 6}
          +
          +set<int>::iterator it1 = s.find(5);
          +it1++;
          +cout << *it1 << "\n"; // выведет 6
          +
          +auto it2 = s.lower_bound(1);
          +cout << *it2 << "\n"; // выведет 2, так как это первый элемент >= 1
          +
          +auto it3 = s.upper_bound(2);
          +cout << *it3 << "\n"; // выведет 5, так как это первый элемент > 2.
          +
          +auto it4 = s.upper_bound(10);
          +if (it4 == s.end()) {
          +    cout << "No element > 10\n"; // аккуратно, если разыменуете it4, получите undefined behaviour!
          +}
          +
          +// вывод всех элементов сета с использованием итераторов; элементы следуют в порядке возрастания
          +for (auto it = s.begin(); it != s.end(); it++) {
          +    cout << *it << " ";
          +}
          +cout << "\n"; // но для таких целей лучше использовать range-based for loop!
          +

          multiset

          +

          multiset — то же, что и set, но может содержать повторяющиеся элементы.

          +

          count работает за \(O(\log n + c)\), где \(c\) — количество искомых элементов. Поэтому, чтобы проверить наличие элемента \(el\) в multiset s, надо воспользоваться: s.find(el) != s.end().

          +

          erase удаляет все элементы с таким значением. Чтобы удалить один надо делать так: `s.erase(s.find(el)).

          +

          Примеры применения сета для решения задач

          +

          Очень часто использование сета позволяет решить задачу, которая решается и абсолютно другим способом. Иногда более сложным, а иногда и более простым. Чаще всего сет используется, если нужно сделать что-то, связанное с количеством разных элементов или с минимумом или максимум какого-то множества.

          +

          1) Задача Девшука или Юноша

          +

          Условие: http://codeforces.com/contest/236/problem/A

          +

          Условие вкратце: Найти чётность числа различных символов в строке.

          +

          Решение: вставим все символы в сет и проверим четность размера сета.

          +

          Заметим, что эту задачу можно легко решить и подсчетом (как в сортировке подсчетом), так как символов бывает очень мало.

          +

          2) A и B и ошибки компиляции

          +

          Условие: http://codeforces.com/contest/519/problem/B

          +

          Условие вкратце: Из массива убрали ровно одно число и перемешали элементы. Затем сделали так еще раз. Найдите, какие два элемента исчезли.

          +

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

          +

          Заметим, что эту задачу можно было бы легко решить и просто отсортировав все числа, что асимптотически тоже \(O(N log N)\).

          +

          3) Минимум на отрезке

          +

          Условие: https://informatics.msk.ru/mod/statements/view3.php?chapterid=756

          +

          Условие вкратце: Найти минимум на каждом отрезке длины \(K\) в массиве.

          +

          Решение: Давайте положим в мультисет первые \(K\) элементов. Далее мы будем двигать это “окно”: добавлять новый элемент справа и убирать самый левый элемент. Каждый раз будем выводить минимум в сете, который лежит в s.begin().

          +

          Также эта задача решается такими структурами данных как

          +
            +
          1. Очередь с минимумом (за \(O(N)\)!)

          2. +
          3. Дерево отрезков

          4. +
          5. Sparse Table

          6. +
          7. Куча

          8. +
          +

          Но решить задачу сетом гораздо проще.

          +

          map

          +

          map — это ассоциативный контейнер: он содержит пары ключ-значение, при этом все ключи уникальны. Внутри контейнера все ключи упорядочены по возрастанию. Так же, как и в set, операции работают за логарифмическое время.

          +

          Объявление map выглядит так: map<T1, T2> map_name, где T1 — тип ключа, T2 — тип значения.

          +

          Доступ к элементам map осуществляется с помощью оператора []. map, аналогично set, поддерживает поиск по ключу с помощью find, lower_bound, upper_bound. При разыменовании итератора получается пара, первый элемент которой — ключ, второй — значение.

          +

          При обращении к несуществующему элементу map с помощью [], значение инициализируется значением по умолчанию для данного типа.

          +

          Рассмотрим работу map на примере:

          +
          map<int, int> a;
          +a[13] = 5;
          +a[2] = 7;
          +cout << a[2] << "\n"; // выведет 7
          +a[2]++;
          +cout << a[2] << "\n"; // выведет 8
          +a[100] = 42;
          +
          +/* Этот цикл выведет 3 строки:
          +   2 8
          +   13 5
          +   100 42
          +
          +   Обратите внимание, что ключи упорядочены.
          +*/
          +for (auto el : a) {
          +    cout << el.first << " " << el.second << "\n";
          +}
          +
          +
          +map<string, int> b;
          +b["Bob"]--;
          +b["Alice"] += 2;
          +b["Dan"] = 123;
          +
          +/* Этот цикл выведет 2 строки:
          +   Alice 2
          +   Bob -1
          +   Dan 123
          +*/
          +for (auto el : b) {
          +    cout << el.first << " " << el.second << "\n";
          +}
          +
          +map<string, vector<int>> c;
          +c["wow"].push_back(2);
          +c["abc"] = {2, -1, 17};
          +cout << c["abc"].size() << "\n"; // выведет 3
          +

          Unordered структуры данных

          +

          Единственная проблема set и map - то что они работают за \(\log(n)\), что в некоторых задачах долго. Тогда возникает идея построить их не на двоичном дереве, а например на хештаблице (о ней вы узнаете на втором курсе), тогда unordered_set поддерживает вставку и удаление за \(O(1)\), единственная проблема - он содержит элементы в неотсортированном порядке, то есть мы уже не сможем искать минимум, максимум.

          +

          На питоне встроенные set и dict - это именно аналоги unordered_set и unordered_map. Упорядоченного сета и мэпа на питоне нет.

          +
          unordered_set<int> a;
          +a.insert(x);
          +a.erase(x);
          +

          Полезные функции из algorithm

          +

          swap

          +

          swap(a, b) — обменивает значения переменных a и b местами.

          +
          int a = 5;
          +int b = 3;
          +cout << a << " " << b << "\n"; // выведет 5 3
          +swap(a, b);
          +cout << a << " " << b << "\n"; // выведет 3 5
          +

          min_element и max_element

          +

          min_element(first, last) — возвращает итератор на минимум на полуинтервале [first; last).
          +max_element(first, last) — возвращает итератор на максимум на полуинтервале [first; last).

          +

          Если минимумов/максимумов несколько, то возвращается первое вхождение.

          +
          vector<int> numbers = {5, 3, 1, 2, 1};
          +auto it = min_element(numbers.begin(), numbers.end());
          +cout << *it << " " << (it - numbers.begin()) << "\n"; // выведет 1 2
          +

          reverse

          +

          reverse(first, last) — переворачивает полуинтервал [first; last) (элементы идут в обратном порядке).

          +
          vector<int> a = {5, 2, 3, 10, 17};
          +reverse(a.begin(), a.begin() + 3);
          +for (int x : a) {
          +    cout << x << " ";
          +}
          +cout << "\n";
          +

          В этом примере будет выведено 3 2 5 10 17.

          +

          sort, unique и компараторы

          +

          sort(first, last) — сортирует полуинтервал [first; last).

          +
          vector<int> a = {5, 2, 10, 11, 2, 3};
          +sort(a.begin(), a.end()); // сортируем весь вектор
          +for (int x : a) {
          +    cout << x << " ";
          +}
          +cout << "\n";
          +

          В этом примере будет выведено 2 2 3 5 10 11.

          +

          Функция sort может принимать третий параметр — компаратор. Компаратор — это функция, которая принимает два объекта и возвращает true, если первый строго меньше второго, и false иначе.

          +

          Допустим, нам хотелось бы отсортировать числа по возрастанию их последней цифры, а при совпадении — по самому значению. Тогда мы могли бы написать следующий код:

          +
          bool cmp(int a, int b) {
          +    return make_pair(a % 10, a) < make_pair(b % 10, b);
          +}
          +
          +// это внутри main
          +vector<int> a = {30, 32, 12, 7, 15};
          +sort(a.begin(), a.end(), cmp);
          +for (int x : a) {
          +    cout << x << " ";
          +}
          +cout << "\n";
          +

          В данном примере, как мы и хотели, будет выведено 30 12 32 15 7.

          +

          unique(first, last) — принимает полуинтервал и удаляет все последовательные повторения элементов в нём. Функция возвращает итератор на конец полуинтервала, соответствующему уникализированным элементам. Значения элементов, которые следуют после этого полуинтервала, становятся неопределёнными. Поэтому рекомендуется использовать функцию unique, например, вместе с функцией resize.

          +
          vector<int> a = {5, 5, 5, 1, 5, 4, 4, 7, 1};
          +a.resize(unique(a.begin(), a.end()) - a.begin());
          +for (int x : a) {
          +    cout << x << " ";
          +}
          +cout << "\n";
          +

          В данном примере будет выведено 5 1 5 4 7 1.

          +

          Часто требуется сначала отсортировать элементы, а потом убрать все повторения. Это делается следующей комбинацией:

          +
          sort(a.begin(), a.end());
          +a.resize(unique(a.begin(), a.end()) - a.begin());
          +

          nth_element

          +

          Функция ставит на переданную позицию элемент, который был бы на этом месте после сортировки массива(работает за линию).

          +
          nth_element(begin, need, end); // need - позиция отсортированного массива, 
          +//begin, end - итераторы на начало и конец места, которое надо сортировать.
          +

          next_permutation, prev_permutation

          +

          Генерирует следующую и предыдущую перестановку массива на отрезке с l по r;

          +
          next_permutation(l, r);//l, r - итераторы
          +prev_permutation(a.begin(), a.end());
          +

          merge

          +
          merge(начало первой последовательности, конец первой последовательности, начало 
          +второй последовательности, конец второй последовательности, куда вставлять);
          +

          Слияние двух массивов, которое используется в сортировке слиянием.

          + +

          Все эти функции принимают полуинтервал [first; last) и значение value. Полуинтервал должен быть упорядочен по отношению element < value (сначала те элементы, которые удовлетворяют этому, потом остальные).

          +

          lower_bound — возвращает первый элемент, больший или равный value.
          +upper_bound — возвращает первый элемент, строго больший value.
          +binary_search — возвращает, присутствует ли value на этом полуинтервале.

          +
          vector<int> a = {1, 5, 5, 6, 7, 10};
          +
          +auto it1 = lower_bound(a.begin(), a.end(), 5);
          +cout << (it1 - a.begin()) << "\n"; // выведет 1
          +
          +auto it2 = upper_bound(a.begin(), a.end(), 5);
          +cout << *it2 << "\n"; // выведет 6
          +
          +if (binary_search(a.begin(), a.end(), 7)) {
          +    cout << "There is an element = 7\n"; // это будет выведено
          +}
          +

          Внимание!

          +

          Не используйте lower_bound, upper_bound, binary_search вместе с set/map! Они будут работать за линейное время. Используйте их собственные функции: set::lower_bound (вызывается через .) и так далее.

          +

          Ускорение ввода и вывода

          +

          Стандартные cin и cout работают очень медленно. Чтобы исправить это, в начале вашей функции main пишите следующее:

          +
          ios::sync_with_stdio(0);
          +cin.tie(0);
          +cout.tie(0);
          +

          Это позволяет ускорить ввод и вывод в разы!

          +

          Также крайне не рекомендуется использовать endl (кроме интерактивных задач). Используйте "\n". Они отличаются тем, что endl делает flush вывода, то есть сразу же выводит то, что вы хотите. Если вы будете использовать "\n", вывод будет накапливаться, а потом единожды выводиться, что гораздо быстрее.

          +

          Задание

          +

          Это просто олимпиадные задачи, но почти в любой задаче можно использовать STL, чтобы упростить себе жизнь.

          +
            +
          • Решите как можно больше задач в контесте на информатиксе: https://informatics.msk.ru/mod/statements/view3.php?id=34778&chapterid=756#1
          • +
          • Решите как можно больше задач в контесте на codeforces: http://codeforces.com/group/g92L0id9Yb/contest/229989
          • +
          + + From 2e53ebed62a962d32285e987d7e6eecea99b31dd Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 15 Sep 2019 16:23:09 +0000 Subject: [PATCH 196/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- pandoc.css | 3 +- ru/dfs.html | 128 +++++++++++++++++++++++++++++++++--------------- ru/sorting.html | 35 +++++++------ 3 files changed, 108 insertions(+), 58 deletions(-) diff --git a/pandoc.css b/pandoc.css index e0d4827..7f7c1c0 100644 --- a/pandoc.css +++ b/pandoc.css @@ -147,7 +147,7 @@ body { font-size: 16px; line-height: 1em; display: inline-block; - width: 250px; + width: 252px; } .contents a:first-letter { @@ -224,6 +224,7 @@ h2, h3 { .contents h2, .contents h3 { padding-bottom: 0; border-bottom: none; + margin-top: 5px; } h4, h5, h6 { diff --git a/ru/dfs.html b/ru/dfs.html index 6868682..22ed2d9 100644 --- a/ru/dfs.html +++ b/ru/dfs.html @@ -11,6 +11,69 @@ span.underline{text-decoration: underline;} div.column{display: inline-block; vertical-align: top; width: 50%;} + + + + + + + + + + + + +

          Бинарный поиск

          +

          Пусть дан отсортированный массив \(a\), и требуется определить, есть ли в нём элемент \(x\).

          +

          Чтобы это сделать, не обязательно даже тратить \(O(n)\) операций на просмотр всех элементов массива. Можно воспользоваться следующим методом.

          +

          Пример. Пусть автор загадал число \(x\) от \(1\) до \(100\), которое вам нужно отгадать какой-нибудь «данеткой». Вам разрешено спрашивать, больше

          +

          Примечание. На самом деле, быстрее \(O(\log n)\), а точнее \(\lfloor \log n \rfloor\).

          +

          Однако иногда найти число X в массиве можно и быстрее! Для этого надо добавить условие на то, что массив отсортирован. Но давайте начнем не с этого.

          +

          Задание

          +

          Я загадал число X от 1 до 100. Вы можете спрашивать, больше ли мое число чем число T, я отвечаю “да” или “нет”. За сколько вопросов в худшем случае вы сможете найти число X? Как нужно действовать?

          +

          Решение и состоит в идее бинарного (двоичного) поиска - нужно первым вопросом спросить “число X больше, чем 50?”. После этого, если ответ “нет”, надо спросить “число X больше, чем 25”? И так далее, нужно уменьшать отрезок возможных значений в два раза каждый раз.

          +

          Почему нужно делить обязательно пополам? Почему бы не спросить “число X больше, чем 80?” первым же вопросом? Но если вдруг ответ “нет”, то мы останемся с 80 вариантами вместо 100. То есть деление отрезка ровно пополам гарантирует, что в худшем случае мы останемся не более чем с половиной вариантов.

          +

          Чтобы понять, как быстро это работает, введём новую математическую функцию. Логарифмом по основанию \(a\) от \(b\) будем называть число \(c\), такое что \(a ^ c = b\). Обозначается как \(\log_a b = c\). Чаще всего мы будем работать с двоичным логарифмом, то есть в какую степень \(c\) нужно возвести двойку, чтобы получить \(b\). Поэтому договоримся, что запись \(\log n\) означает двочный логарифм \(n\).

          +

          Теперь вернёмся к нашей задаче. Можно понять, что такой алгоритм работает как раз за \(O(\log n)\) вопросов (если число 100 на заменить абстрактную переменную \(n\)). Несложно убедиться, что именно логарифм раз нужно поделить число на два, чтобы получилось 1.

          +

          Общий принцип

          +

          А теперь представьте такую задачу: у вас есть массив, состоящий из некоторого количества подряд идущих нулей, за которыми следует какое-то количество подряд идущих единиц.

          +
          a = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]
          +n = len(a)
          +n
          +
          14
          +

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

          +

          Давайте обратимся к идее бинарного поиска. Посмотрим на элемент посередине массива. Если это нуль, то первую единицу стоит искать в правой половину массива, а если единица - то в левой.

          +

          Есть много способов писать бинарный поиск, и в его написании люди очень часто путаются. Очень удобно в данном случае воспользоваться инвариантом (это слово значит “постоянное свойство”):

          +

          Пусть переменная left всегда указывает на \(0\) в массиве, а переменная right всегда указывает на \(1\) в массиве.

          +

          Дальше мы будем переменные left и right постепенно сдвигать друг к другу, и в какой-то момент они станут соседними числами. Это и будет означать, что мы нашли место, где заканчиваются нули и начинаются единицы.

          +

          Чему равны left и right изначально, когда мы ничего про массив не знаем? Первая приходящая в голову идея - поставить их на \(0\) и \(n-1\) соответственно. Увы, в общем случае это может быть неверно, потому что a[0] может быть единицей, а a[n-1] может быть нулём. Правильнее сделать вот так:

          +
          left = -1
          +right = n
          +

          То есть изначально left и right указывают на несуществующие индексы. Но это нормально - например в массиве [1, 1, 1, 1] в конце алгоритма как раз должно быть left == -1, right == 0.

          +

          Осталось нам написать цикл while:

          +
          while right - left > 1:
          +    middle = (left + right) // 2 # именно такая формула для среднего индекса между left и right
          +    if a[middle] == 1:
          +        right = middle # right всегда должна указывать на 1
          +    else:
          +        left = middle # left всегда должна указывать на 0
          +print left, right
          +print a[left], a[right]
          +
          8 9
          +0 1
          +

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

          +

          Например, если мы хотим найти, есть ли число \(X\) в отсортированном массиве, то мы просто представим, что \(0\) - это числа, меньшие \(X\), а \(1\) - это числа, большие или равные \(X\). Тогда достаточно найти первую “единицу” и проверить, равно ли это число \(X\).

          +
          a = [1, 3, 4, 10, 10, 10, 11, 80, 80, 81] # отсортированный массив
          +def bin_search(a, x):
          +    n = len(a)
          +    left = -1
          +    right = n
          +    while right - left > 1:
          +        middle = (left + right) // 2
          +        if a[middle] >= x: # практически единственная строка, которая меняется от задачи к задаче
          +            right = middle
          +        else:
          +            left = middle
          +    if right != n and a[right] == x: # ответ лежит в right
          +        return True
          +    else:
          +        return False
          +
          +print (bin_search(a, 1))
          +print (bin_search(a, 10))
          +print (bin_search(a, 20))
          +print (bin_search(a, 79))
          +print (bin_search(a, 80))
          +print (bin_search(a, 81)
          +
          True
          +True
          +False
          +False
          +True
          +True
          +

          Задание

          +

          Придумайте, как с помощью бинарного поиска решить такие задачи:

          +
            +
          • Найти первое число, равное X в отсортированном массиве, или вывести, что таких чисел нет
          • +
          • Найти последнее число, равное X в отсортированном массиве, или вывести, что таких чисел нет
          • +
          • Посчитать, сколько раз встречается число X в отсортированном массиве (в решении помогают два предыдущих пункта)
          • +
          • Дан массив чисел, первая часть состоит из нечетных чисел, а вторая - из четных. Найти индекс, начиная с которого все числа четные.
          • +
          +

          Все эти задачи решаются бинарным поиском за \(O(\log{n})\). Правда нужно понимать, что в чистом виде такую задачу решать двоичным поиском бессмысленно - ведь чтобы создать массив размера \(n\), уже необходимо потратить \(O(n)\) операций.

          +

          Поэтому зачастую такие задачи сформулированы таким образом:

          +

          Дан отсортированный массив размера \(n\). Нужно ответить на \(m\) запросов вида “встречается ли число \(x_i\) в массиве n”?

          +

          Задание

          +

          Найдите время работы, за которое решается эта задача?

          +

          .

          +

          .

          +

          .

          +

          .

          +

          .

          +

          .

          +

          .

          +

          .

          +

          .

          +

          .

          +

          Решение: Такая задача решается за \(O(n + m\log{n})\) - нужно создать массив за \(O(n)\) и \(m\) раз запустить бинарный поиск.

          +

          Задание

          +

          Решите 3 первые задачи в этом контесте:

          +

          https://informatics.msk.ru/moodle/mod/statements/view.php?id=33216

          +

          Бинарный поиск с вещественными числами

          +

          У нас все еще есть функция f(x), которая сначала равна 0, а потом равна 1, и мы хотим найти это место, где она меняет значение. Но теперь аргумент функции - вещественное число. Например:

          +
            +
          • \(f(x) = 1\), если \(x^2 > 2\)
          • +
          • \(f(x) = 0\), если \(x^2 \leq 2\)
          • +
          +

          Понятно, что при \(x = \sqrt 2\) \(f(x) = 0\), а при любом даже немного большем значении \(f(x) = 1\). Если мы научимся решать такую задачу, то мы научимся находить корень из двух!

          +

          Увы, возникает проблема: действительные числа хранятся в компьютере неточно

          +
          # известный пример
          +0.1 + 0.1 + 0.1
          +
          0.30000000000000004
          +

          Тем более не сможем найти точное значение \(\sqrt 2\), потому что это бесконечная непериодическая дробь. Так что давайте снова воспользуемся бинарным поиском, причем всегда \(f(left) = 0\), \(f(right) = 1\), и мы остановимся тогда, когда left и right будет очень-очень близко.

          +

          И тут снова возникает проблема. Помимо того, что бесконечную дробь в принципе невозможно точно хранить в компьютере, ещё и арифметические операции понижают эту точность. Поэтому, чтобы явно не использовать разность между правым и левым указателем, можно задать фиксированное число шагов, которое будет выполняться.

          +

          Так как мы знаем, что двоичный поиск работает за двоичный логарифм, можно сказать, что на угадывание десятичного разряда числа потребуется примерно три шага бинпоиска (т. к. $10 3 $). Значит, например, если нам нужно посчитать значение функции до шести знаков после запятой, то нам нужно ещё примерно 18 шагов уже после того, как расстояние между left и right достигло одного.

          +

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

          +
          left = 0.0 # 0^2 < 2, а значит f(0) = 0
          +right = 10.0 # 10^2 > 2, а значит f(10) = 1
          +for i in range(100):
          +    middle = (left + right) / 2 # теперь деление не нацело, а вещественное
          +    if middle ** 2 > 2:
          +        right = middle # right всегда должна указывать на 1
          +    else:
          +        left = middle # left всегда должна указывать на 0
          +print left, right
          +print left ** 2, right ** 2
          +
          1.41421356237 1.41421356237
          +2.0 2.0
          +

          Вот мы и нашли корень из 2 с достаточно высокой точностью.

          +

          На самом деле, так можно искать ноль любой непрерывной функции (мы сейчас искали ноль функции \(x^2 - 2\)), у которой вы знаете значение меньше нуля и значение больше нуля.

          +

          Задание

          +

          Придумайте, как с помощью вещественного бинпоиска найти

          +
            +
          • \(\sqrt[\leftroot{-2}\uproot{2}17]{1000}\)
          • +
          • какой-нибудь корень уравнения \(x^4 + 3x = 5\)
          • +
          +

          Поиск максимума выпуклой функции: тернарный поиск, бинарный поиск

          +

          Так мы только что научились находить корень непрерывной функции, у которой мы знаем значение меньше и больше 0. Но можно ли найти с помощью бинарного поиска локальный максимум функции? Можно!

          +

          Как известно, локальный максимум функции \(f\) - это просто такое \(x_0\), что для всех близких к нему \(x\) значения \(f(x) < f(x_0)\). Для непрерывных функций выполняется более крутая вещь: слева от максимума функция возрастает, а справа от максимума функция убывает. Так это как раз отличное условие для нашего вещественного бинарного поиска!

          +

          Если вы знаете \(x_1\) такое, что в его окрестности f(x) возрастает, и \(x_2\) такое, что в его окрестности f(x) убывает, то можно запустить между ними бинпоиск и найти точку \(x_0\) такую, что слева от нее возрастает значение функции, а справа - убывает. Это и есть локальный максимум.

          +

          А если функция выпуклая, то она вообще выглядит красиво: сначала возрастает, потом максимум, потом убывает.

          +

          Проблема только в одном: как по точке понять, в ее окрестности значение функции убывает или возрастает? Достаточно тыкнуть две точки очень-очень рядом с ней и сравнить их значения!

          +

          Задание

          +

          Придумайте, как с помощью вещественного бинпоиска найти

          +
            +
          • максимум функции \(x - e^x\) (она выпуклая, и максимум ровно один)
          • +
          • какой-нибудь локальный максимум функции \(31x+x^3-x^4\)
          • +
          +

          Тернарный поиск

          +

          Другой способ искать максимум - это тернарный поиск. Пусть известно, что максимум находится между left и right. Поделим отрезок на три равные части:

          +
            +
          • middle_left = (2 * left + right) / 3
          • +
          • middle_right = (left + 2 * right) / 3
          • +
          +

          Тогда если f(middle_left) < f(middle_right), то можно спокойно заменить left на middle_left (максимум точно не левее middle_left), а если f(middle_left) > f(middle_right), то можно спокойно заменить right на middle_right. Он будет работать не за двоичный логарифм, а за логарифм по основанию полтора, что больше (но асимптотически то же самое, так как отличается в константу раз).

          +

          Оба способа работают быстро, и обобщаются на дискретный случай (то, что было в начале - когда дан массив, значения в котором сначала возрастают, а потом убывают). Но проблема есть в том, что если функция нестрого возрастает и нестрого убывает, а именно если там есть отрезки постоянства, то алгоритм не работает. В случае, когда значения функции равны, никак нельзя понять, с какой стороны искать максимум - он может быть с любой стороны.

          +

          Задание

          +

          Решите 4 и 5 задачи в этом контесте:

          +

          https://informatics.msk.ru/moodle/mod/statements/view.php?id=33216

          +

          Бинарный поиск по неотсортированному массиву

          +

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

          +

          Поэтому бинарный поиск работает и не для возрастающих массивов / функций, если наша задача состоит именно в поиске двух соседних индексов, в которых условие выполняется и не выполняется.

          +

          Например, если мы знаем, что \(f(x_0) < 0\) и \(f(x_1) > 0\), и функция непрерывная, то бинарным поиском можно найти ноль этой функции между \(x_0\) и \(x_1\), даже если функция не монотонная!

          +

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

          +

          Полезно иметь это в виду, это применяется в нескольких задачах контестов.

          +

          Бинарный поиск по ответу

          +

          Рассмотрим такую задачу:

          +

          Пример: “Корова в стойла”

          +

          Условие: На прямой расположены N стойл (даны их координаты на прямой), в которые необходимо расставить K коров так, чтобы минимальное расcтояние между коровами было как можно больше. Гарантируется, что \(1 < K < N\).

          +

          Решение:

          +

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

          +

          Ответ - да, можно ставить их довольно просто: самую первую ставим в самое левое стойло, это всегда выгодно. Следующие несколько стойл надо оставить пустыми, если они на расстоянии меньше X. В самое левое стойло из оставшихся надо поставить вторую корову и так далее. Даже ясно как это писать: надо идти слева направо по отсортированному массиву стойл, хранить координату последней коровы, и либо пропускать стойло, либо ставить в него новую корову.

          +

          То есть если мы знаем расстояние X, то мы можем за O(n) проверить, можно ли расставить K коров на таком расстоянии. Ну так давайте запустим бинпоиск по X, ведь при слишком маленьком X коров точно можно расставить, а при слишком большом - нельзя, и как раз эту границу и просят найти в задаче (“как можно больше”).

          +

          Осталось точно определить границы, то есть изначальные значения left и right. Нам точно хватит расстояния 0, так как гарантируется, что коров меньше, чем стойл. И точно не хватит расстояния max_coord - min_coord + 1, так как по условию есть хотя бы 2 коровы.

          +
          coords = [2, 5, 7, 11, 15, 20] # координаты стойл
          +k = 3 # число коров
          +
          +def is_correct(x): # проверяем, можно ли поставить K коров в стойла, если между коровами расстояние хотя бы x
          +    cows = 1
          +    last_cow = coords[0]
          +    for c in coords:
          +        if c - last_cow >= x:
          +            cows += 1
          +            last_cow = c
          +    return cows >= k
          +
          +left = 0 # расставить коров на расстоянии хотя бы 0 можно всегда
          +right = max(coords) - min(coords) + 1 # при таком расстоянии даже 2 коровы поставить нельзя
          +while right - left != 1:
          +    middle = (left + right) // 2
          +    if is_correct(middle): # проверяем, можно ли поставить K коров в стойла, если между коровами расстояние хотя бы middle
          +        left = middle # left всегда должна указывать на ситуацию, когда можно поставить коров
          +    else:
          +        right = middle # right всегда должна указывать на ситуацию, когда нельзя поставить коров
          +print left # left - максимальное расстояние, на котором можно расставить коров в стойла
          +
          9
          +

          Общий принцип

          +

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

          +

          По сути мы просто взяли задачу “найдите максимальное X, такое что какое-то свойство от X выполняется” и решили её бинпоиском. Самое сложное - увидеть такую формулировку в задаче. Поэтому рассмотрим еще один пример.

          +

          Пример: “Очень Легкая Задача”

          +

          Условие: есть два принтера, один печатает лист раз в \(x\) минут, другой раз в \(y\) минут. За сколько минут они напечатают \(N\) листов? \(N > 0\)

          +

          Решение: Здесь, в отличие от предыдущей задачи, кажется, существует прямое решение с формулой. Но вместо того, чтобы о нем думать, можно просто свести задачу к обратной. Давайте подумаем, как по числу минут \(T\) (ответу) понять, сколько листов напечатается за это время? Очень легко: \[\lfloor\frac{T}{x}\rfloor + \lfloor\frac{T}{y}\rfloor\]

          +

          Ясно, что за \(0\) минут \(N\) листов распечатать нельзя, а за \(xN\) минут один только первый принтер успеет напечатать \(N\) листов. Поэтому \(0\) и \(xN\) - это подходящие первые границы для бинарного поиска.

          +

          Примечание: заметьте, что задача в контесте немного отличается! Прочитайте внимательно условие.

          +

          Задание

          +

          Решите как можно больше задач в практическом контесте:

          +

          https://informatics.msk.ru/mod/statements/view.php?id=34097

          +

          Там могут встречаться задачи как на бинпоиск по ответу, так и на тернарный поиск по ответу.

          + + diff --git a/ru/dfs.html b/ru/dfs.html index dad9275..77a9596 100644 --- a/ru/dfs.html +++ b/ru/dfs.html @@ -206,14 +206,14 @@

          Точки сочленения

          if (u != p) { if (used[u]) d[v] = min(d[v], h[u]); -     else { + else { dfs(u, v); d[v] = min(d[v], d[u]); -     if (dp[v] >= tin[u] && p != -1) { -     // u -- точка сочленения -     } + if (dp[v] >= tin[u] && p != -1) { + // u -- точка сочленения + } children++; -     } + } } } if (p == -1 && children > 1) { @@ -319,37 +319,49 @@

          2-SAT

          Эйлеров цикл

          Определение. Эйлеров путь — это путь в графе, проходящий через все его рёбра.

          Определение. Эйлеров цикл — это эйлеров путь, являющийся циклом.

          -

          Для простоты будем считать, что граф неориентированный.

          -

          Также есть понятие гамильтонова пути и цикла.

          -

          Чтобы проверить, существует ли эйлеров путь, нужно воспользоваться следующей теоремой.

          -
          -

          Пусть дан неориентированный связный цикл. Эйлеров цикл существует тогда и только тогда, когда степени всех вершин чётны. Эйлеров путь существует тогда и только тогда, когда количество вершин с нечётными степенями равно двум (или нулю, в случае существования эйлерова цикла).

          -
          +

          Для простоты в обоих случаях будем считать, что граф неориентированный.

          +

          Также существует понятие гамильтонова пути и цикла — они посещают все вершины по разу, а не рёбра. Нахождение гамильтонова цикла (задача коммивояжера, англ. travelling salesman problem) — одна из самых известных NP-полных задач, в то время как нахождение эйлерового цика решается за линейное время — и мы сейчас покажем, как.

          +

          Теорема. Эйлеров цикл существует тогда и только тогда, когда степени всех вершин чётны.

          +

          Доказательство. Необходимость показывается так: можно просто взять эйлеров цикл и ориентировать все его ребра в порядке обхода. Тогда из каждой вершины будет выходить столько же рёбер, сколько входить, а значит степень у всех вершин исходного неориентированного графа была четной.

          +

          Достаточность докажем конструктивно — предъявим алгоритм нахождения.

          + +

          Если условие на четность степеней вершин выполняется, то этот алгоритм действительно выводит эйлеров цикл, то есть последовательность из \((m+1)\) вершин, между соседними есть связи.

          +

          Следствие. Эйлеров путь существует тогда и только тогда, когда количество вершин с нечётными степенями не превосходит 2.

          Кстати, аналогичная теорема есть и для ориентированного графа (можете сами попытаться сформулировать).

          Доказать это можно например через лемму о рукопожатиях.

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

          - +

          Упражнение. Сформулируйте и докажите аналогичные утверждения для случая ориентированного графа.

          diff --git a/ru/probability.html b/ru/probability.html index 9ec3e70..4547a69 100644 --- a/ru/probability.html +++ b/ru/probability.html @@ -4,7 +4,7 @@ - Ликбез по теорверу - Алгоритмика + Теорвер для алгоритмистов - Алгоритмика + + + + + + + + + + + + + + + +

          Eytzinger Binary Search

          +

          This tutorial is loosely based on a [46-page paper](https://arxiv.org/pdf/1509.05053.pdf](https://arxiv.org/pdf/1509.05053.pdf) by Paul-Virak Khuong and Pat Morin “Array layouts for comparison-based searching” and describes one particular way of performing efficient binary search by rearranging elements of a sorted array in a cache-friendly way.

          +

          We briefly review relevant concepts in processor architecture; if you want to get deeper, we recommend reading the original 2015 paper, as well as these articles:

          + +

          Our minimalistic implementation is only ~15 lines of code while offering 4-5x speedup over std::lower_bound.

          +

          If you are writing a contest right now, stuck on a problem where binary search is a bottleneck, and suddenly remembered about this article, jump straight to “complete implementation”, it’s compilable and copy-pastable.

          +

          Why is binary search slow?

          +

          Here is a standard way of searching for the first element not less than \(x\) in a sorted array:

          + +

          The running time of this (or any) algorithm is not just the “cost” of all its arithmetic operations, but rather this cost plus the time spent waiting for data to be fetched from memory. Thus, depending on the algorithm and problem limitations, it can be CPU-bound or memory-bound, meaning that the running time is dominated by one of its components.

          +

          If array is large enough—usually around the point where it stops fitting in cache and fetches become significantly slower—the running time of binary search becomes dominated by memory fetches.

          +

          To give an idea, the following code is only ~5% slower for \(n \approx 10^6\):

          + +

          To be more precise about it, we briefly explain how memory fetching works.

          +

          How caching works

          +

          Here is a famous quote about caching:

          +

          +

          The reality is much more complicated, e. g. main memory has pagination and HDD is actually a rotating physical thing with weird access patterns, but we stick to this analogy and introduce some important entities to it:

          +
            +
          • Cache hierarchy is a memory architecture which uses a hierarchy of memory stores based on varying access speeds to cache data. Adjacent cache layers usually differ in size by a factor of 8 to 10 and in latency by a factor of 3 to 5. Most modern CPUs have 3 layers of cache (called L1, L2 and L3 from fastest / smallest to slowest / largest) with largest being a few megabytes large.

          • +
          • Cache line is the unit of data transfer between CPU and main memory. The cache line of your PC is most likely 64 bytes, meaning that the main memory is divided into blocks of 64 bytes, and whenever you request a byte, you are also fetching its cache line neighbours regardless whether your want it or not. Fetching a cache line is like grabbing a 6-pack.

          • +
          • Eviction policy is the method for deciding which data to retain in the cache. In CPUs, it is controlled by hardware, not software. For simplicity, programmer can assume that least recently used (LRU) policy is used, which just evicts the item that hasn’t been used for the longest amount of time. This is like prefering beer with later expiration dates.

          • +
          • Bandwidth is the rate at which data can be read or stored. For the purpose of designing algorithms, a more important characteristic is the bandwidth-latency product which basically tells how many cache lines you can request while waiting for the first one without queueing up. It is around 5 or more on most systems. This is like having friends whom you can send for beers asynchronously.

          • +
          • Temporal locality is an access pattern where if at one point a particular item is requested, it is likely that this same location will be requested again in the near future. This is like fetching the same type of beer over and over again.

          • +
          • Spacial locality is an access pattern where if a memory location is requested, it is likely that a nearby memory locations will be requested again in the near future. This is like storing the kinds of beer that you like on the same shelf.

          • +
          +

          The main problem with binary search over a sorted array is that its memory accesses pattern is neither temporaly nor spacially local. For example, element \(\lfloor \frac n 2 \rfloor\) is accessed very often (each search) and element \(\lfloor \frac n 2 \rfloor + 1\) is not, while they are probably occupying the same cache line.

          +

          We can overcome this by enumerating and permuting array elements in a more cache-friendly way, and chances are you already know this numeration.

          +

          The Eytzinger layout

          +

          Michaël Eytzinger is a 16th century Austrian nobleman known for his work on genealogy, particularily for a system for numbering ancestors called ahnentafel (German for “ancestor table”).

          +

          Ancestry mattered a lot back then, but writing down that data was expensive. Ahnentafel allows displaying a person’s genealogy compactly, without wasting extra space by drawing diagrams.

          +

          It lists a person’s direct ancestors in a fixed sequence of ascent. First the person theirself is listed as number 1, and then, recursively, for each person numbered \(k\) their father is listed as \(2k\) and their mother as \((2k+1)\).

          +

          Here is the example for Paul I, the great-grandson of Peter I, the Great:

          +
            +
          1. Paul I

          2. +
          3. Peter III (Paul’s father)

          4. +
          5. Catherine II (Paul’s mother)

          6. +
          7. Charles Frederick (Peter’s father, Paul’s paternal grandfather)

          8. +
          9. Anna Petrovna (Peter’s mother, Paul’s paternal grandmother)

          10. +
          11. Christian August (Catherine’s father, Paul’s maternal grandfather)

          12. +
          13. Johanna Elisabeth (Catherine’s mother, Paul’s maternal grandmother)

          14. +
          +

          Apart from being compact, it has some nice properties, like that all-even numbered persons are male and all odd-numbered (possibly apart from 1) are female.

          +

          One can also find the number of a particular ancestor only knowing the genders of their descendants. For example, Peter the Great’s bloodline is Paul I → Peter III → Anna Petrovna → Peter the Great, so his number should be \(((1 \times 2) \times 2 + 1) \times 2 = 10\).

          +

          In computer science, this enumeration has been widely used for implicit (i. e. pointer-free) implementation of heaps, segment trees, and other binary tree structures, where instead of names it stores underlying array items.

          +

          This is how this layout will look when applied to binary search:

          +

          +

          You can immediately see how it’s temporal locality is better (in fact, theoretically optimal) as the elements closer to the root are closer to the beginning of the array, and thus are more likely to be fetched from cache.

          +

          Construction

          +

          Here is a function that constructs Eytzinger array by traversing the original search tree.

          +

          It takes two indexes \(i\) and \(k\)—one in the original array and one in constructed—and recursively goes to two branches until a leaf node is reached, which could simply be checked by asserting \(k \leq n\) as Eytzinger array should have same number of items.

          + +

          Despite being recursive, this is actually a really fast implementation as all read accesses are sequential.

          +

          Note that the first element is left unfilled and the whole array is essencially 1-shifted. This will actually turn out to be a huge performance booster.

          +

          Binary search implementation

          +

          We can now descend this array using only indices: we just start with \(k=1\) and execute \(k := 2k\) if we need to go left and \(k := 2k + 1\) if we need to go right. We don’t even need to store and recalculate binary search boundaries, which is another selling point.

          +

          The only problem arises when we need to restore the index of the resulting element, as \(k\) may end up not pointing to a leaf node. Here is an example of how that can happen:

          +
              array:  1 2 3 4 5 6 7 8
          +eytzinger:  4 2 5 1 6 3 7 8
          +1st range:  ---------------  k := 1
          +2nd range:  -------          k := 2*k      (=2)
          +3rd range:      ---          k := 2*k + 1  (=5)
          +4th range:        -          k := 2*k + 1  (=11)
          +

          Here we query array of \([1, …, 8]\) for the lower bound of \(x=4\). We compare it againts \(4\), \(2\) and \(5\), and go left-right-right and end up with \(k = 11\), which isn’t even a valid array index.

          +

          Note that, unless the answer is the last element of the array, we compare \(x\) against it at some point, and after we learn that it is not less than \(x\), we start comparing \(x\) against elements to the left, and all these comparisons will evaluate true (i. e. leading to the right). Hence, the solution to restoring the resulting element is to cancel some number of right turns.

          +

          This can be done in an elegant way by observing that the right turns are recorded in the binary notation of \(k\) as 1-bits, and so we just need to find the number of trailing ones in the binary notation and right-shift \(k\) by exactly that amount.

          +

          To do this we can invert the number (~x) and call “find first set” instruction available on most systems. In GCC, the correspolding builtin is __builtin_ffs.

          + +

          Note that \(k\) will be zero if binary search returned no result (i. e. all elements are less than \(x\) and all turns were right-turns that got cancelled). In that case, you can put a special flag in the first element of b.

          +

          This is already 2-3 times faster than std::lower_bound, but we are not going to stop there and apply a series of small incremental improvements.

          +

          Branch-free

          +

          Compiled program instructions are stored and loaded from main memory too, just as normal data. They are fetched during execution by similar mechanisms, and they have a separate instruction cache. In fact, in large applications you can sometimes remove blocks of literally unused code, and the program may run faster because of better instruction cache hit rate, but this is a topic for another article.

          +

          To avoid performance hits caused by memory latency here, CPU loads 20-ish instructions ahead of time, but to do this it needs to know ahead of time which instructions to fetch. If a program has conditional execution (if-s, while-s, for-s) there is no option other than to take a guess.

          +

          Branch misprediction (guessing “wrong” branch of “if”) costs around 10-20 cycles. To partially negate this penalty, hardware branch predictors were developed. These are complex ad-hoc systems that use statistical methods—some even use simple neural networks—to make a more accurate guess.

          +

          In case of binary search, if all of our data is random, branch prediction doesn’t help at all, just because it can’t: all comparisons are 50-50. This is why we need to get rid of if-s and rewrite our main loop the following way:

          + +

          Prefetching

          +

          Compiler doesn’t like when CPU is sitting idle while waiting for memory fetches. Sometimes it can take a guess which cache line is going to be needed soon and fetch it ahead of time (recall that bandwidth-latency product is usually much larger than 1).

          +

          This works well for simple access patterns, like iterating over array in increasing or decreasing order, but for something complex like what we have here it’s not going to perform well.

          +

          As we know a bit more about our problem than the compiler does, we can tell it to explicitly prefetch a cache line we need. This is done by __builtin_prefetch in GCC:

          + +

          Here, block_size equals 16, which is precisely how many ints are needed to cover a cache line. When we reference cache line at b + k * block_size, we are referencing \(k\)’s grand-grandson (block_size = \(2 \times 2 \times 2 \times 2\), or 4 left turns) and possibly some of his neighbours in his layer (recall that indexes at the same level are just consecutive numbers).

          +

          The whole point of doing this is that there is a good chance that we will prefetch an element that we will use later on \((i+4)\)-th iteration. What chance, exactly? Well, it turns out that it is constant for each iteration.

          +

          Memory allignment

          +

          Note that for each layer in the tree, except for the first 4 and possibly the last one, the number of nodes in that layer is divisible by 16, the block size. This means that the fraction of covered nodes on each iteration depends only on the position of the first offset of the array in respect to its cache line. But what is more important is that it can be made that all of \(k\)’s grand-grandchildren are covered by the same cache line.

          +

          The way to achieve this is to place the first element of the array to the 1st position (0-indexed) of a cache line, or placing the array itself on the beginning of a cache line, since its first (i. e. b[0]) element is blank by design. This way the next \(1 + 2 + 4 + 8 = 15\) elements of first 4 layers will occupy the rest of the cache line, and the rest of the array is alligned in nice 16-element blocks of nodes who share a grandpa.

          +

          We just need to ask memory manager to allocate our array on the beginning of a cache line (by default it allocates your arrays wherever it wants), and that’s it. To do this, we can use alignas specifier:

          + +

          This is it. Now our algorithm is constantly prefetching 4 layers / cache lines ahead of time, which is covered by the bandwith of our RAM. This way the effective latency is reduced by a factor of 4. We are basically trading off bandwidth for latency.

          +

          Complete implementation

          + +

          Few more things to note:

          +
            +
          • It works best when \(n\) is a power of 2 or close to it, because otherwise the branch predictor will have a hard time figuring out whether or not to unroll the \((\log n)\)-th cycle.

          • +
          • Its performance varies by cache size and array length, but stays >3x even on smaller arrays (<1MB).

          • +
          • Preprocessing isn’t costly. It is around 1% of the cost of firing the same number of queries as the array size.

          • +
          • Modern hardware won’t penalize you for prefetching cache lines that aren’t yours, though this maybe be an issue for older CPUs, which can be solved by a simple if statement.

          • +
          • For some reason, basic binary search implementation (the very first code block in this article) is already ~20% faster than std::sort.

          • +
          +

          What about B-trees?

          +

          B-trees are basically \((k+1)\)-ary trees, meaning that they store \(k\) elements in each node and choose between \((k+1)\) possible branches instead of 2.

          +

          They are widely used for indexing in databases, especially those that operate on-disk, because if \(k\) is big, this allows large sequencial memory accesses while reducing the height of the tree.

          +

          To do static binary searches, one can implement a B-tree in an implicit way, i. e. without actually storing any pointers and spending only \(O(1)\) additional memory, and \(k\) could be made equal to the cache line size so that each node request fetches exactly one cache line.

          +

          +

          Turns out, they have the same rate of growth but sligtly larger compute-tied constant. While the latter is explainable (our while loop only has like 5 instructions; can’t outpace that), the former is surprising.

          +

          Let’s assume that arithmetic costs nothing and do simple cache block analysis:

          +
            +
          • The Eytzinger binary search is supposed to be \(4\) times faster if compute didn’t matter, as it requests them ~4 times faster on average.

          • +
          • The B-tree makes \(\frac{\log_{17} n}{\log_2 n} = \frac{\log n}{\log 17} \frac{\log 2}{\log n} = \frac{\log 2}{\log 17} \approx 0.245\) memory access per each request of binary search, i. e. it requests ~4 times less cache lines to fetch

          • +
          +

          This explains why they have roughly the same slope.

          +

          Note that this method, while being great for single-threaded world, is unlikely to make its way into database and heavy multi-threaded applications, because it sacrifices bandwidth to achieve low latency.

          + + diff --git a/en/img/btree.png b/en/img/btree.png new file mode 100644 index 0000000000000000000000000000000000000000..ab28903e6db88c3eb55b6548f4085a882e557929 GIT binary patch literal 97622 zcmaI71CXRa(=OU$bH}!A@7VT^jUC&zZCg9G%^iEkvt!%3z2A5KI4ACndph2Zj;^TA z%BsrFs>~;!2t|1b1Xvtc004j>B`K;50Dukw03eyrkiZsbZnX^H2e^~4lqxjv>)SjN$X^|(di-(nz(Mz&;z zA|u18K&8<&HC@f7-vVOk@?R5v_KE@qC7~zipXSnu;pKyOOr9w?K_5PIWtqy`Ak07s3;!AOEezhdGTIm@6qZ++W%7-J8cnF*;cAw@KnfZGR(zxLo8sKi5!c%uma~=WH+F6K(NeD~ zm20tx;VZbFVYs-tau;JbU(IIhuh8OS-tyMM9zQIQsIM^M&Gp@=9EVrLBe3F;4^rGZ z=R?W+b0);jzYq9GgUS5B)tmGp$Iyo46di;lxbFP2C3C3PhAc81guDiRLnJ>5k?M=6 z?Jyo7ca#0Nw1T%W01FAIX&}+denT@ZC%hkRBM_-O zET~!ANAca~jsEl|g7os(F`lV^ei6oN5Rv8%02|d4c6Z175|EPkIjeBIQcRnwqrg~_ zFGFZ<9W_y^V9$5WM=)Zgq_w2)>%i3H>d&&z^vQ!8$W`65XPDkFN>FK>{uIfrg@_ZE zFugQ8ETmOxg<;HSJeq6Kg!Ko1x?SC7%Mp1b&RM*f0g-Awd<&j%Kdzz*rkfE#qD)KK z`IJe}{Lg)3Nb;sgo7~Tiv5SsyK zg{J1kIIwmgqO?*@w~`f0`#aRIcpFCafY*V#dJQ*fnPGIc0pS=SBAnX|qt%1ATve>3 z5l82l7e~+QGx%%LA)V>Y>s+87WeZ*Z@BN6rLlWb78<*-i4?eB0PQ(?|;#`i!U2DDP zG^3*Xlpvu5H2^ysYN}7L>f7`571bhu3yxNE0^<6I`AadnQf&)8Q4itKl}jQ3)o1iN zAgK=4Y=RkRG_pcNCZggLg2B)(`}};~32%<9aYIKAlD|2l;?ZI-U+k6^`9}A~OaLT_ zq_(^B!&&Z$I@b{TQ+K0dRGHD?+G+MThu_e2k|o}YM#Is!oqYk465gwFIxw(ezhYSv zJR-s)JqzxX*$Ka(J@}9{7C^9? z1##Wj3BV=HT2^HEd^5q#HPQ+hVM_TW#vZEL@Dp^dFBrvIpyGnB*W0|MG`?dd;9*CA zqaZIOv^`ym1^k{H3f<|g>UXcHvV)Cq_ueC!)<+QfH62SVjPHZAV#g=!Eo2(GwhUT4 z$}c>7@P%CHR5rKsvL|VB9*WZDazbJ&AjR-WPe5J3<#s9Hh)9Yfy3G|5W%v@u@dF&1 z4@uxoVQdYV*}Nm(YwsC6c)KTBCWHW2ZPn2Sy~e!%|I zJ15Rv0CA{c1583d)A8M->gZNF-K$-GQ50{IG%<0;Z#152#FB2OSLJU|-&pK3q|Mny zMYUkp*b+S1l@sc9#7L1!P*4&cAJsFD{{&a2Q&%kS<2N}3^Hx|_kE=2ex9e(a*|QKk z#b|YOyO4NrR0x|*Xt8;WisS6okZ4<}B+BfYthj=hVxp6rTtWC$c&Irg;yxBuQ`d6) zjR;d6-oxUY&gFf!I%7`}&Zn!&PnCPwDZDmCQBDb9z5Oh)V3mO@0qGfY~P6YGzAwGeg=8%(6@4|S5B;c(pqD!X6OE+nUtX;?3mTk zbZkwg`{DptsmdGpMmT9n9(pyv|s7o4OJjfblmeaVT1JhqW6jbf@otm?9ftB5#? zI+nvDJ+Xb15$%T}l`z`JN3Zz)q9Y=H+_;)xc&T40C&efQ>#?M>;ksaQ!*CM(e#}YM z`a5kN`Ngpa`u-)`S18AdD!p!oO4>f;CKXZRi*ikMv0k<3Lg0np>@+!8xzt(4y@IUa znOxC&?{oZ-zh%>+AGO@WFR{v4U$ks|;kgT9QONl&J(~)V6^6xM4bI>^$DXS#n~Z;pFHAM@X%9+(&kq>t?Xx-wOySQ(X45p6{RtzH zq*`vl*g0U=;=TQLD&R=)^1@hpqx(~l{Hed`R9I((rMcDeOjO~g@^Ex=GsQw|4C$Dz zsmyYzW8&CF`9)w-vf|j^h^G7dJiD=?Oy+Ui$s!{91Z1B`b$B~rR~BkD>@UU=bhedE zUAC$+T5K6I6CF1I{qqNdrBwbArACqhoK@m_bN+7CG;)GqLcm?Q#ya9+vkYscH4WwO z!#k=f4;I{rf%mm|;Tz`ah-r(v*>0zDyKhh|%=QHp8E+9B7T@H~m%>#?9F=4Ek|}Ej z&T^(un$D)#M zN-8`eoXU_9p&=lhck;h5o+PdSTx1{stM1BiYSY$ zs-`gAY|%lr6oZ|RjEJM(h{|>Alq7?lybQs9W-f-98$)iv9p?abMfehI!r6&R)p8RZ zo0IQ_-0g=qMb3nE63My`;fUJ~0zx z(MHnxVshdhVAWa}4(Bq~xScrPPWDV&_FPL^tmChSU)Y3x@AhdjJA%+!=>+v*vq$3Z zZ4P^X_bXekr?gOJ7x~#um+V{`BjvYHF8vZmT>;#Ffrm|0F;EnkUEK1tH^sF(x+1~% z{sh$@X7YI#vJ=hu?=TLbX}qtA8GMw))0#)Nw?NC23y&&yLnX2NC{u555tHHc`CgX6 zLz`UfHzzm0;$SNV=!YKnP0R`kxsXtl&e>shK+{rF1yvkyxwiMV9wueu z0_R5Zp*1wOO1RQ1`#Y0&dnJGuTMdn=u`v0gdal&eMzS!E_)h_kH9D0)Ck1AT`to!y(oC3IYDE`-H~Pvy@?T7nL>=8%^i*Jp|UMTTL64TnX*$eIEbJ zU{?yQ6e%mr6M;vh*A9|7>C@f}p&8lD8?Hj;cBaQ@qTNri%f|1Z|L&iM^9f-$gZ=C) zjReoT1&NDDCuE-0%Ds=ub^`JJlQB&S#D2%oE;oSwCx>N(nQbf+wNdlmsIqYBj-XzS zJYf?azz=*`T~5p?bSgbB`=q7z2$Aa9CrO6qzV$!^CX6N;5vqDQKQ z(eLmVaO%9gwGmbe2aV8dKBZ(i2gbSCTp#%j+99#fYWtwRT{VxS3$*M2>|1niE*Sjo zC-9Qix@@4@XqVhE2z2)^NF0rmGzf>1v--$pKx&UY!=6DEiW2RQYNxnTly1sdUGNW2 zE8Rn>jjfYmLOsMr0=8ziKCMLB zEPY#BL=~P8-d0o)PFetH?)SKmVk57`2D(Zorc8U85%#R#OoXd-PvTHoRj77_80 zp2P!pc6X(aRv=;5tF3>)Jiw6}D&!}uFTny*3@jE!01|fuYaEDnipW+_W&=v*5^LX# zpZ@5BJIa*JwzC&_@Kt8C81uKAGb9@WbybIHg1lFo;nn4s>jOr)`uE;exlj+ zOU89X`gFRIUcWl=wUhf#-ab0P0I=2uiS>1h-#anGjA)F0vJZMEbZeP=bfB3-$gsl1 z513Q9{>Qm%oNJE%_m@cMbU0;y$DG_tXogv)zXDhxN_IMftd!V3 zci9?B6~QBfB8gO?v(_1}BfMW_K}zz!@)EQX#}*w4NDIxsHRS}s~(c<>FZ87nRw3&cIE|9l9IjQnor zyu)~oL7$C*Npo}k)wKJaHC$3&3L*g#sC=$4D^plV1bB7;2pajdHp|C|*n8T!f6o^# zqy{$Ny3J>o1=R{k*lA(lC`J7fZ?KbL;2Iy-{`9v;_(odvCETe=YYOrwlo?`fw63rM z-R+Z@;Kc%#>kd}r65p`WFVD2XwYqKO#Wg5iPP^fj>UHL_Z3a9oZ&b>jU!#dA;#aHm z-{9{uip@9z(uHNEf|o6sTJ-sYOgCvUt_%c& z=Y3ar%`c;AmPsxN5k&2op-dE2Yh!RmNOB={r@EYT56BA(6NzfKQKx9Te(+I$Rrl@u zNWkhgv`vVR3Kh-%YC-DyvLB4|s}8zi^fMG|-riYy)7FvnZp3P!!f6#YF0$C@_@tw3 z#@uswlL!B$?IIY&+4V{%t~d08ETVe0A7saoH#FsEUQ}?%0vaa)7`8xq(05|?JOFF@ z;Q{yz+BAI7;--hXJ6$r6FnspfhUi-2ec*E9(x?x0|DpnQ0oPt_>FI!@RqS>}z=~2p zab_pEnyE-r3YShS^8J12K3J0yIO%%L2w7CJ* zWwgjUB&owk2s-#PXXkFpEW7K4(TsC($8pC&v zjO0WmUNwNgsOMr)80bm!o}%ey?)-^J%x!`f7uf*b({1e&u2?Ly=xwKAb52O4BzKiH z&wo){u6p2Igm!;C4z&?42^}pPFJ%;GdwRG6plZsQES4n~2EuB~FVEk#Y)0qZwnN@_l>kgo%9MW)k3?~Do6k5PI zOk!(OF@_D52sY#+S$3e^iPLbloE*n&6E)crc#`0b)HcFjy=p4E7wC&$(cVYD{1>><FF#DKT$staS&MmtPN|9?M8dnLi zSsmfeL)w)67}ScdRuzt{)V2IOVT?wN41?0UfNYK}WrDc)8Fg?!qk;folP5N)wVJ3} zm|v+&IX_{XKo&2E(u+iFo)K(h_&qwq1VR457e`Lpc$vLotrJ>O{0jFxfly zunjjUxRK~-rnk8I%T%B-*D(k*bdem6T0;8lRCQ%4q_V?nVMZt<`ZC@ziX+yZay^pX zV$luPacq=RQyTH_`#&CH)iRm)xEXykuf$ z?mmWU<8P|lP2bP~SXeQyuw{kUL|;FhKIP_u`}O+>G<%7+Q*+Uw?YE$wEY{qPPOJn` z5=yl!6gH=R*H=b%J8t4bE}zW;oTW3~IN$g2w9r;N`7MxEs^-2Dj5y@>tk7lGBE80U zYJ(iYspFN9S9)%#Kcvm*O9WEpmWEbViNB{RlrPK{%|G+*&os>wan(xVMr(@hZg$a* zIU@ur%mh)VBXPWDipk#s?-sis=L9m>)WslCxid6eiy&RBRQ5irji*(R#9GQHNb)OXy%80qb|y7|UTTgZrD^9MJe5Hj^H<0{&7D&;7(}Sp8DltgKU$>9<8-1Emc@9txH)` zY>dsz_aQKpVS7~ z{!q1d+gZzrhQg^Hi7G!Wavzl%+s{O18F9*TW7&mGhn%v+m==f4iqh2i1eiJi2Il-t z9PJO8VxJ&qV|L=-Pv`Ama_F471VgELQAubnX!GIkK9M~v<`;-fa)@@h#`dMDZuO02 zqlB-kDQ7i#2ABT8LS~YYL5nkSPS8*Z?l{>p#!GTZmf!(U0*L~lv60fGN_N0OydQ0; z0)mrBipl{(lbYho?}bEyle+2@A@}TuVb|bc7~Ubk5ohRs3T@4<@I-E6=(#qI|^AOG6*sV zY6d*)CTmwo*F!2wAvlYr5N=K&rH}>+UX@^pC&I4L!=u7!6wH&u=erLXc9mkI9=|Hf z@7^4-rm*<%YhFW1jU{n*8SH#{(Zosk(#U8J4>_KBCs#Uq7VhNQb4}1hl1rjvJ(9_v ziz4(XmS%oq+hrzW<*7OgcS|Yhc(M7kttjK#1!l%>s^jR^bL(2j1Tco24j+1SXep-! z303TlD;+hTISWUyXLfw^9@uE@cE#vP$k^n4rG*|hb`G;cB)28Z=hga~VIf2I8!$3q zm9Fe2Yzn9*W~G1D6`y+&Ld$ow-3#l38>?7P^*&uO!xh-Z=1RX_3GI^+L? zvXW6A_o59aVoWx=JfX&Mrf6%~4hMy=hsqInS`CZ?S%7HN!Hic1a@pwC=tZ50PgpS0@Q!l*kSeYMJf#Q3-RY> z3#8R2^Wb8RlhVdFmG_IQxq=s+F&_~J6L03QNFywYr0{2Ls5;X#knCPeh~`DmSQo8+ z<5@xH3Nz*VbD$FNcOdh>HCK(!xvl>=!fh5lA%zBACBpfq!XOhNMujSDgs8I7Z!ZPZzhv33 zCuo(iuNbb0NdU$DGviv_&d^wVUJ_#Bg8M-r6!&lRiRji=P7d1*=!{<`%730nh~+!q zSrIi62InV~O=2*`M3D>La^skdM~H}tL8da0Lgq0e2jh^WbTW`r^Yi!6YDvl}@2a zSzj-noSdw)-xZe4RQ)7KJKyT&1+Ieq!36rhiGF^**|~dqlJN8EZWkPf2p9C_`F%#% zZg!jiWvsM07_Y9b?(Fmjy&{%OHd`)0mMP?ktE#F-kPlsbVjE4PUX(r$aP!~E$hSqw zna_|GDkA8mG)aDjh7K+VD>Vaw~oS&>i>8?j>Xn#C~13e0b{<9r_iX8=l?S*#0Fl*}GRk^dy<~#Z!-fbdca z-OUcsG;JJsczEe|iR;Sg?}KGqnEz&BU}6GJN=oVzR)C4#;TlpP4e_Hy`V~=(ao>@9 z-?6=Ca5n08<4WdL6Y2O2zP<@|EiOIMb+F6`9Uuq_la|s)b91ux7Jhh_cf#y%m6l+C z&V;vzX(s=7pUJFM_P_ssCOaX!lVVk^u}V`4Ch8sIj0Eb*$o6+wbeb+D9m2?$yQ8#q z{V6a=c&Zzj(W@xnz6?(OYV#To6RS`)y4+|7?vS0gx3^-6G|&}X<&C( z5YTgwxm8|L2S+c_T4J%bP*r50uABUOo1O z^AZ=5i1thrCnk=3POiY_Ed+58$VNuu_q|+4rl%3T9?z*;XH=Oc&!|>l9PI6x0Qo-} z%?=(}SO=Lpx@8ysI{d;xJPfUKM-tDZvUD@P>Qgm@g<80MLHl|W437;(#z*+^aJu8j zujuuBZ3wh0VgUhy$Hzy{=y<~|qyrw}=?06rLdO?SxKKmbGIUWgD|amFVy5_%-UKUD zc9?hvN37q!k9;Dpz@+6u(o-R)7ts=R>`a?mT1HxI*6*>f2fOtDQ`=A!q%+Y!Q2w5S zJ#A#xCy&;3Miv?xpSn=QP?bOo#dNrQ@Tv!^nEF^j$yEIWZpD7|uz6!9Sp1H;(! z6X_`0#NAS({TNp~+vme%cH-69Rk1m1|bTP8hp9#}ZC3u^iC#J75FduF?n1)fs}hKW{?4Wzu42*VEHv z3H0Ce-MM4c(l7fe3QjpiE^(5%n`8|wo$rs!v|fK@PE0{>QL zB({9JnsJS%S7NW6My-5%?i;-fk(t04p^C;gPd{$F93J?-$t9U8`4oD6JRPa#XPc9bxA^ECe?kDfjmKS;^NHi_gzabqb4r1?6|=ma&IYc zU(z!(1NWq_TES=TBxPiRA|j+<7#@8145X!rtWlVcrvGx-h27A6l%)=D;#n?jX*RXE zRg5Szq==>&0c}Eni;mbAL88S*N5`ja8eK5PKX4T_Os6Opc@QWZcx;}zE&ixMMfYxp;%JM?ihI^3Edk8r9^W6 zb1Ae)>XQ%(htCfWsz~~gNjkPKhgU)9u~}(-PV|W!MvhMSGB`kEDWFPD=!q3t(%T!i zf2OI<(LtnHBWO7wKD#({EJIkSPtA^*8+ERW{+za$_*RI_M6Ggw=c!PPy^wH2qNxy7 zr%H2YsX0an{FD zVqvIn>o%rxBd9R+ignlv;CIU z<+(}J4%y`GY?ku$S}tY7<|@=I_9#AEJ&1pBwH3b*BrF}w(W%K;95Wm=pa})m*h|WA_Si}~ z?h!PZta#(BRxr0~;-XNp7o6s9&t*i5=a2PbDsCUDjqBJ-cDIz8s9*eFizn=+FxKQl zn(rczVRd-IH|V(%$8t`Ru(eOLU=xsC5|^XxBpz9uDtQgC@#7dQXcvwj83p#a4RGim z^rcn!tr9AW2qEtNu&bNoVBm1V3|nv_@bg@=BitSf!Sfdc;<}>cs zxD?IQXK#0bQUvAD?P=b^D{cMIH65&6DvklI@Q%Wvs6$X zd~9N&(-3~K!1Y+VlI%%qM9o$0;41ru@Wt z887Tp=ub5=S0;^&6YAgSN?j4%Xf77lheEY7QmUe#D~V;@$?)s7#pjju^1SHF)5Rrc zcR2AndRaYP<>R@e564`UD>m#QW|VCnF#iL6SNT6uy!Z+v$(u410zHHJMo2TRLR^c7 z(noAh@bVl-q-DTw@Ka>Z4_s6mti{hgfLu$2eGd|r8XcUP(KOtH-emeEZ{5=63E+hFa~ z(t^P+vODW@g03Jqrka~eQ#)0LoPj8) z4yCd(CFTSXsZ=%`(;4p?wHft?RleYJ2xja=S?+o&0kD6e%(D1l%_rc%0om8zaKHus zlSWK<%xC{f*)cXKqW}-~#|xjTx}vIo!KCE2AyY^^Rm#rf6mi1o7dA{NwVXz+o|p|U zuWGd_OhG{bd5^a%k^?h0H@D&aPCpmHBNl+^Zb89ty-XpZ*8DMbip%#e*(FH2UKz*a z)Rc@je!El-PEmn$WRw!hp9M02fdvv&*1MEHD7qCFDgP%0hF5TC{ zB!7g^yjD0V74W^bXVoA-I6wzhzF;6ZZ!}-OXa#RU?lgD=K`mY{T6o9_q;3}9MSSV7 z;zzm~6Ug(1jbfMU{N}WO(*(f1Ro}nft95s^_2Sxc4dmQ&89~2gE(dS?C&Ex$ZWgnv zp#)cLbrd51tKsvI!?6m~aQVXD(dw#A<*Ab$z6Tyztf7u zAxWk$a?om})?>|X_~vGi)048meUYu;aomUQaJz^koSamen}zlP0|Tz_zv~k%rwz@6 zOLX3M!aW(&@nYi+cou3R_C`8S6k)EID8@25y?&V$=N@8WQZh z!mHbF_QVHMX(~8ePUfoh7!F?+M1gRg#*F5OM>NaGSA=2)0T9kIjBP0PoX^-~>{*2w z2kTdf2nQuv^!>|gqC<(nAU&w4fd2tcg(@@;` zK&dL_w|Q(450|R?V+XFEdf?ngJJ9~+y)f_|3H^!5(zCGQ2$S^w{-Ey4NFX0N2q_n; z49$;$7twsAuYuueJeSBW-wB{5-E4pjT)FjE54!iRy+vL6SsXzf?u`RSz^A9P5v-e* zC{#f$IWF)(9k>Du$a}hFtyq#I1u2aGF`JJd{=yc>VnRP6yBfPQ2`?_&2CTBa41fas z?Z3O9Xi!#O06-&y>mF%4ky$R&RY=stEDefQNS6xNz0ao8e|QMD7H2h5dxqms250^! zE9cz7KRjeUPWM+t5|&a7LDI<)Ye-WLvfQX&wlD#EwB$gBO3>$m#Y+Lx--xnn!S9*( zQtT}8_9jcKS!N7>(yZK>NTl0^KtxUxLy}2LTwE&84u#b=yHAet_kV%@|DY-m!--1$ z_ZDGD?dw?alN%{KNO)obdiY~g&FnAWIz?$2rUPIf7LIP1%FqGOyGAk=<~Yq|XNlLSZ(8~UU-s`eRPj1;B2-_9cbmPa^znSpoU4U<&~{8T zYA~a(l#hyhGo8CA^=XPLZBa`I5S2en*(?{@Tw84WqM0~WKC{pwJO7 zamqDhAXF^-eh~=>Af=0+{9d8zLpNZwVM0G))oPaj8CMOBisP1d0hVNe!>9pg+ zY*kX=bZP|r=t+qf--YgQ#7*cz|H(Z&Qht-8sun1kCWq1gTDdAQM9yiTmtVUJa~QU|nRRpW^G^ztszHeBHxD-ff98?+Q%k z^~RAIqyJp43X?X2=z=e<JYPOajj>7%#sku4C%;+k@ft0K2v8ZM|A!Mr&2CvHPx>(Pz443l{@%Yq zE=yUL@*jz6Tt*4I`ZYG_ZghdUL+hnW?H}D~4r$&Zp2Qa@ak!=7h9#mt-0&#Oa^@B@eQ~@ykz*D~9!51kd zq^t$Ps7ehrpf4~2_W<`Vst}Hj3*%rzE1pBsO*h;!1l}$!v>GvN^?N`|L<1QZ3mpc> zVMatklgD3w{G)6Y1rebxp74R+1mWd0NLqT+%!k|;!m)UY@F6Leff616RG}QZhCX$VAG+11*>TDXUJ4DwdL^r? zAMhauyP7eS<|I+UHx_;x4m-3FL$9(K3TIU3{SUb;dh1#tkHqD~?by~L{E~TwI7w1* zbP7SOQkA+9>tChRe^96Bts#Xh5ap_yoOW`8)vD{iLsHqjzTL9M|JmJteTp!td0hpa zUY#GmM)JIb{&c8ijQ0OqS}c4~lIuX|`fP!^p(s9ZrP+VEPf0XZf?u>{KFJ=vFbN5g znr&2XewJch=~G!%X_~;~tBLa7X99vIj}w`vD^{59jmLL6SOj7F4c^D$@}9V&UhlXc zMV>`_*9h)(r&MX~P+_2{1|!f&&Y8Z*mZmuSSxVdAqy2}!2IDU;u7{`FTJ>_D-|2ku z77${7E{jI(X4%(HW+{W$N(E_EGh8#u4T@;orKJA7KQTdw>r|4GRPDVQtRW|6qrwuq z95{XB!mGpj!kx#z=vj^Uee;Bpar;Y_#9PI5x%F;IZqCb;`RtuOf>BS<>j;tR!iTGY zSyu0cwRiH))9kK9ra$BKDCNdeCVY55jO+D|?f&EgVH%ygGA{hcT*JuZNW6Sk2P~#T zVQAzFb%JElF*vB887!GN>}KT;^3cKOXWV9BKLT!HKp5d=uMBtnB0S>K6pHIPKz`c? zB&OyY8Hr)CCw_q45jD+9Qd<&W8lL-6wm`r_<*(o?sysFrCN`}WB|FJ5Ui zALn~(n4^=oZAU4-!1ppE_J@&@-5f=kH5h!zWXbT_utuSE#?*XMQp7ZE2Cyy-qm{qn z+)(+&LD!bONQQhn>gPp?;j6!S>nJ-Q#9iG+tX!sy9xY3BU{l(6&*)xqO#U}-ul=;b z|5c?@vA2j)i$`J97r-&{$mD142iGOGjbZHIF-joXiWPA=J$%mna#~6SvEnhlCoA{h zDjs9FrTVu>0y>KwS3Tp)iG#!R*7I=S_&{;=M|0mSbw$E;iqQ3zQ2^JIyj6w+0NPle5z0!<9j6MTh*=va4ECDOdniImg<<-=IO z0$SXz9N0>?ozdu_AuNc}v{L#xa|;!&?rDYldssqdQ`;? zqw3oq9(u-{<$PS^Wfh(rT~`*3ed?RpL0N^cTig78#1&v+LB;239-U+~_dk+3TC>&C z!s)@{h2-b!BxSg~xWe~=$n(EmPjfgq{6^M0?cy##5JxbIVSgfke*-LaAg4mB@7Tht z1g4@B#sQ*a5*q_0QJgoTM-yV@MOVA8F%sd7=TC@v{&lz-_Gdq6wvLq&?=^!rV1=v$ z&XE64f*Zsw8f^n2yZa{>>NsF%A9|z18HF>C4AXZ#hysK7OIPgL2|tC3?C(uX6qlcx z#w3p-OX4C4S3Bsfh5(%rFG%>Q#YU$uSe!G5IP*GfR&k$qpa$)nWf#)vICwERB{4HI z^B$uV0s#P+3r}n)EfFLIMJ_^qDD>BogBeUwDRVg7fY9mkhS2!xj6D+)g6?o2RBE?t zq^nctHCrzkB|Sex2M-w$eu{F6Dko*ePDp@31`_W)kdX9~r;&UFK2Zop57$t2AJW47 zhekQ=Z{F2{yTnHqL^p>nC{O;}>35^Y7K1v?v>peAJYGbbo>jUr)4+<^dMdq{FrOZ# z@IdL^xkDF^hV34v5Cxy|^#Pf@d)K>z7IVcgJ*qVIbGM|?QE|g|YB`3hB~d*hUWEFS z#D7nAjl)k_VDmH>c!+^mT&Qrt80*yhJSr}SwR?3lfq*xbUeIfS%TouJr{VWU_O*yw zUmRH6lwm9SGA5LQnR@q03NF=VaR@l$#c~ydH`fb^Ds^(^_i!Nf7>?<2omEn;62<$H zMsN8A)(=n5e>p_Q=35bHy~TjAHMb|k%-UCAdiPd_`J(7h?hD95PcPtiYK+qEv>h}T zgIJJwOFmlu=a4>A?=7)xPBSc&{D%K-ojw!4CHLVwdTg2lvt;AXAnx9e2oD>_El>(R zUifY9tec@y$aAAuqdO&T$m&0#A)7R2Uyk2$@dt0)s~m_oQB#e;YjV2$y_in~gN)RJ z9$^p}u17k8(omdv!5wteoby^yaLZbPAqwrE*DEwT?tq#!^&@qj86Cw#aN+1;C&g_C z>(l<&bs5|4Z>eakJMDwnV8RR4FQqhm$?iq|WI<%MRJUL~;(X_>MR4qWqT@eje4d8u zYfb|Yx*=gQjuwnRnP0ECr=1TmUpy)t>ssM6lt4>HM}lxa4BdY8{*B@h+n}!y2tLs@ zGhC4a$=nIC*#1jN^-ExQeseIPsjKcn6)?bx_m~qjrC_Z%$N(w9Nf_cii({?5ih$n! zw^ie2Xm6!9wQskx#DdW)H0k@)$T|7C1ARM7hujZ;F@3Khy?HTf-FtS7PjCEXpCf}m z$k?v7t5GV95JPzHIADYH1?5LUd1h{bx9`gEcs_70+AIF;I-t-B_CJrlLo=TQ`2Y4| zUfdb0@_BVxmyaIKAS~T|gG;se(*}C^j^V#I4{2c{BPrw~`)*1sP`?jbO)82%(yEeX zbaW#VZgB_Zy0RHT3>Qn%achFtdlIJJj-)LN{W$$Wcv#IFJDON|%;yejHcc2%S}J&6 zn^*KSCB^l39SwFD1<|_|%h$?nuT;uQ_C`O*&>KKOzY$oZk=qq1Zt*|000ke6%&TWM z!S^y%*TYXT!mi}8KG3_F2a-id2O zu{iBS%G1jYxKfZHX6C@asls?3@B>SItOY62T zJwnriW#)Q*5DeC)QJguVoV+wq-xr{!SKkZ|ht&y&c{$DQZMY4zWW=db`@j|QXj{qYP#w;iRT1)sD>8XLq;c5IlQAhoE0)n~Y>m~#NEsi}9p zSkowlf`CZzwl9_#&*C2|pK#~NRe$#g9`j*q*d$uJ(WGk1j*CMA$*0$DnceF4x_igx zaSjg;$6(Zxy}Z0cBP7({_cWi%6|?bNEc=M-DFVJpMZSS)wb*$-evt#|6(hSQMGa%0 zy+T%)w|956z6WUPcK@5pwe=S%ws?ZY>DAg?`BE9}a3vHiGR{)Yxn^7{fUTn;8Q#mL zB>7h&igtNx4!*}lXDDlXTp%la6h>W83c7wmWvxv2EKG+qPM;&0GDR?|k>1KX;6( zUwhQrwbq(@ulZokV|+bv2s7L1CHMo9>JQG!E@UYlr0c1v*|->M2`cA;T;F~ZYZ^S< zV{s;9G>a3~_{Gn*G_vUH;?p#P#oVjNMJGn0$>l_VYGzSrBz#e0)&mKvl^k`S!wc=F z8P0ol2X{<;=?4MFM;oxJK6PT80n>y5ZpBA@MKE2sNwLltW7c1|v_blwG(k8?kgBvq z992&-tcU%z)0TPg%n>-b-ujp^bs3{E9=XDxJq}#y%OB!&OnL-5@&VVUIDxm%8Jkg; z7_nuTPgknevRdGGJwW+n?lyC15woTvWj^%aTl$u!oJB~KUS6R4_jH^PZo$e|l#kME|oZmk^X zJEyz50?fjo?|cGau6+6+>Do3d|MkSP3)QJ+M-nykz8+=syl6W<6$=xM8Am725d7|0 zCeHC$cywTi>YSxVZ?L;7;#H>eJvSWIF*q!lZ9ie2rS?3kJ*b#a{Z89Kb00lcaMVeE zXfmr!&+EyIG*^A~fYYUnqpKjKM7gxNMfg@RDT{|0jK4f4lF5vvO}Fx@~J(6iLAbyGa=>RrPS_Dos+uuAUbD9G}MYJC+s1 zS$YJ-ySsZLva?cLer_wq(zOVlj`ZyUKSD51v{mzAXt_w)F>(~HbH&q0&l4j<6PVSxqM|OOt zrX9_vm{yvQ=VKp}9t&I!D!gu6tG~+7O10P>5>aIhxW3dV6AiQb`V_yrlAi7cV5zac zUe_W2T2sBBscikzXkqPNmu*AYDWKeN1nq7G*QW_(_@!hmTL4kUWTHq2*YTHg{pdA> zQcCu6G#cUoF12efQLkOZT5pk#i${~|)ml1z&AcV%rwa_=?b~bz7oi_aep8(%cAc35yC{s)D7fw89$@q|PdI2=rBO!dl z@tVbNrlct!0r&=S!KN_r$GTAC6Fr&*iu^c4A4-yGuX(f@YekMx zvzMzz^TKg8-u@fhveWHh4=Z6BvoCe4^Zx2^IS=jC>;!AOzJUO%y6D@U{5tplUT$2Z#t!mzK$QLn zD_W*+t61(1ij(ze@LX2l?oznIyC#F-8)I~W{1vL69>5v$OSVs#&Apcbn9z|S;PLwV zVpG;&U3K%#V8=Owo*I@_OzU)=PMX)_z*Ju#0x!Cp-rKu_qVB3A6Sb|VD@@bD4EsTlOP|Ufn+8MIsa)@63WZ;>jUi`e-FUhWuw4%inZ+f?bfq{Vu6Tvb)^Uz}? zCP2v~-{vHpTGN$c7y#bVqVCMmUO@8lxS#jPbT@%~SX7i4#(I_q#OB^Hm`-PwEzBkw zPtWnN4ZWG+*cNxKDG6i8;~ou*76Z;bMG+N%!}VK7T53;cD#4O&cZbYI1&jjLOb83` zv+QUE-H}fZ(4tag@!LBKnAzR&{Ah+q++{Lw{y#T zfPmi?uYj|jThKh?y#ng7^8>M!_y7y%b~Kn3pe|_LTH>_Y^fn>u2`Mq>dHW>Dj9!n_ zVURZ(;fOU;j&(STTaEl+1r1Xy(brx_|Lb0y{{;f=Ca@gQJG}F}n89H{kqxjEEyk=` z`_@Ygm~Gleki6(wu!CxlD#zkKGel4$7@eAHS14t9oQ8o=VY2o8`!0Uo}eSIxx z#n*kXOCV(hh~9pW7#`W3v`D7bDZEhIW zCn{#l`qB5?xQ1d$fvtKsgT_uViO-f|;{FH2%l01Wve$2d5tg6jp##X(utnayU0&A| zK0Zf09n-0{FTca%lr5r(#l)k%5dYX*T-;SCrsu$SZ7C8Lv@{5w*_GvRjcoDC?9FCp zGu>uu0$v@4&NRZTd>p~D-2mgo6A(Z-T#}b7eyeEPZb$F*tp-Mw#}Ci#;7h|1zyU+` zS#x>Tb1d|`-tX4cC|46vrml}{IPFHizNHpksUdsmsVcc-;Uu7*C+;vPQBsR2Y=jDnlCt2dT03m`nTKh24pc|8UYHie7|(o# zeg6sz^4_le4p*?TVhP)u*d%<{Av|hiWP>ubY$0;ymWHg6K9o=d<+Ga(8uPhW^f12^ zbdhOSwDillX{ToVuXibaqO7OTw!#UDbDptmf)Nbh38Tx#E00%H6zzRDf@{8F&KJYn zy%tKR6^>|$fKC4$nGtpVC>5V6#?u8C4dZ$i;}=c024F4JyGGj~`3E~=QnhTeiPe}( z0lznfW3ev?%W(hJq;_)>6;Fk`KQM6`(l$9)hWL)R+37t?)ht z@AWTNtbg7ET+fO4TdH-?ib6T?;*^sk0`v2&pUoDeU)b-vJ|a4D@dHf8f7_wZH#8XG z623oM0QIC#y&Cb#zy?BUrokGBPc=MCLT9+88=Nizs3$m3QaNCs zms675{t96@2{*J92i}jdA);lk&XMIT2C`C+TayKwr@@5$FHZPphGbdmA}_bEiM zXdD*GM_dIq3$`?Y1%4g@CJcslD1Zi*I6T6;PYZHinbVB5Qk6Ix49Ypa{#sk#iw5A8 ze##}~G5h18<3wp8V&VIC?f5Ec)P_q6nk}!`)aRJ_rSZ zO>?~d)X(=>AD>L_*kUc^&b*CuWlZ>V+OM>@c?ZH85Xa``?pc{I83^QG z$Yo78VIsaRK&Q>Qy8~teRe08R%om@W^BhmnSzvAl@jcU0=D+X@rlEwuOnO}+L$GN< z3NFY){0VaT`XZm4DNVqa>I|&c!?j2beL3N7S_2ajdO@%d3VO%FhqDSdRy&1xy{?8k`T@Qe_fTWer=Ri1j>|sW-#uB>12U(q zq_A}8I}`%>_Am`YhKj=RH>q~;yHZ&0VnoxMw%e6cbM5e#@XMw(iiK+`DqKtK` ziZGGhz*HF+M+cfEeABT^>+VL9yB^q@( zzR>|52B)_S&*V#(SujbIYL-rpl#izO-T;)Bg#LU}t1AITK^SKcZ4u^h{I*CqMoQ7cd8 zj{>H5S;WiZ9jfieT@)sk3F-wa0WvZ0oj4GimTerHx^3wZm1{tZ<$_grd`p z!hd$irdYV38uG3F%u^wpzvT}F*N4nyRz^F|ewTl9kwp_M2;;ql3Jvb`$W{L$d>#xh z>^4!9ce%bn)cjd`o1xBo?V~UugJjb614)zgS0Epc2G7za&0P{z-o$pP!FeiCaGG5f zsve>>)t3p#9i>Ns8PjF?nRcPC3e6MpjGw+MyrY?>{j0}Q5weZrg2hh}hu-9&k81d% zc&R!4JWglmdNSA)nvlmqlgdg?xVN#(e-nRS_x)kepT-hic$f-D^yN-&6d01IbvX0R z3)dizmC_>@8k(>e^Z=f(L>FM`A;lZA?cZjE6s_#N0I*WU*oKHa*6 zq@$Zc{Xtc9REEJGE~o1yJ#W7RJy0Cx=CB@OFhOY=c8n?*+Q54|@F%!auv%UC*LiTB zfSb%r!=-jD_v7z`UAF^RGTk@MN8@Yh)8G2jgs9nEc^ihj11KF?KX}`nk?~d=kP&(A z!_|E#eC6CUgA*~4fCsaQ1F|s`*o9Z_xcJ(u$oxzc?r71HSnnZ>h}W$(f}~_ zc3hM3{aH3INrN2q^Dyr+@Nr;NXaGJjj#$BKj#_>mr;BvU3{`?3;fG2YB1%`$fzBf3 zWJM;O?h!5>4VQ?r)ux)XHh|dEC`#~#-aaCQy{g!3yU-{x6*{8-p~9b>&z<8J1(&Nr z@ffUu4I_jcFRM-i>v3SZDa4LaN0`}KRcndHj9Oq54kh~e2p!&%24n=yTH)|xML{(( zaWAbP<7*hz&Ut?UJSVXls<&*oB2Y#<*FDQiWl5-gnHy-2q$@!09LF)=(8__M&3dXA#SrVh{VUbV6XZQG<{J%^4Nbr6JR<|qz?iqd zDRNL*+OPr{5izkwtpUX8QuWZ#fS!0O*8Dy*E=Qt7;!EQd^B-DHw18%{3ndpBm6`s7 z3py^57~L6pmGhf&kxYaDUBnZOMd{Sx=GFJRb02!vcNwF8{M{Icc=vg?jvGvJ*qY4{Cqtwms1kSLS{Itg;IN*Sk@8=wyY;vs>S3U6~w^reuFl^PTY;W zO6Kzo6}fftEgLxq6gXvlZAXkh(+Ls1FWWAJl84>T1o!TF<{ZisSHo|waFdiZT|7~p zJlpK~FeCp!nlQ&c@fKl@1tr<*#g=a$9j`;we`~D2ONcs)^{G-#UtC{Sh2+jcUy~lr~ z`s(>PtAgjAQ-_9hKlZRcD|O_cFXuOdCWRX&jA0C?Y@`;&LdPw7ftzf`q5?6EZ!t9s znhJFV4O*73`|Xa=jRciC;QLKUf@vp0rhuMNJmL8r+-gXdzwx2KVPl(c!$xLzja~2C z?ZZRMCX>4I%4E9*v@!Gfm)Dr0A3qe5yT9Nm!0?k-^K>~*c2VCHP4m#Z$zr(jDs#4* zz@T(3v~=0JUQ`~(P!cBz`O^Hpq0V;f1WL+!Z$iN4kdfpJ3>kih9$BijIUG-=(P>Li zDOXi!bvPi3P9oXBghIg0Iqi5vwr#v=&E}twHUW@#1uuox9bYkd+?kn7WywfO!wCoo z7}rg#nI=FVw;gu~nN1(vG_<>ZK%PVR%GUu+^crqQNlq#+?$)?My)B{Q*0gIyG4F|x zpS@Qwnef0o?t+qPdYe+;F{XZ`HNa9naN~GnW3#jsi(WKP*4&W?Y?fo+r(>{cXZiP7 zQ9cwdJ7ZYcM0_ASht}6yN zId)-hA1gL#aK5dfqYQtbdhT&hyi{H(dGuhoT{N6=c|8Ka0|?ty6R-xfHePF;r}-8U zD#j0X^;nciUjduZ_b12%@ z*#<=t&xwwCN;etKMmg%6yfTezjx2`{RHP`<@q6ZNakY=KK^qk@89;rAkp?X6!Z#7^ z5P9^`!fLRk{YtO*8@e1rVE1wiGd{IA(EN)s+Qqo3)~xl> z2x+B(GCu^-(b;Su4$a^gTYaVd7EHDMJ$!0`OAc?Z!c$AMIia$>(6V^oayw#4Z+kC+ z@olr)Ihj(f-j6F0<}R@5!Waln?edCNn5{H4xx4AY;wpJveXMnQqBg2TKWC>RmbjMg zd?|~dey*+2FKd5R!EG(pJnl_hSbY@JxrBGRu0Edpq4ZzlwRIK;V?R^t7NinHCh$q-Q<^G#}Dr0Ps-7BUYTc9P-LHqf) zs&rghM&CXy(K=eX?cwtBq(+h?`-jxhFT#>j?Is(~1X8^!gTa*Q+GFoh7d1;ry4QdZ z%Z)e0VsY0WO7NQeeBm`AgUo;=DX{?B3}oS@R;`tXBjiqP)&NGmwdq&Yy{g5p8V^H# z30u3>NFDm<1RsBe$4nt}A~d@DT?u|aQExzZK_Ko`N<*KFu2-(PIgKP&q%j6pTYGqw zZ-|`wjXmw{Xb!Lt#*r*J;BrShZ+hw8+?aS4W!86g0NRg8HQnMNhB0wD^g{n2Q?)m=(7`hQ6z?1 z%_8ibF%=mbp4@Q7qJLH$`H@So$0Q|*&T82LWvTuAlrh;CgOGH1SBwPXNJ_t(1PJCN zV@1?@vH4dXsUp>HYTx+BjUDmuOhbXvvEgjT5obldjA(Sn=ff3X)Ch&H?m&h8X{_mA z(7lnyA$bMRMoo<|e|GcM><5t~EAsH}upQNqY_#HrAn5DVFu1O+myupxIm{;zPcuSB@QAR=P|8=d+f?v)jlYq^7Jo5pY4b|{`l`isV zb%YG%zb-45a^fP8@qO|{@TZ4>_Y_6K5uGlQJ`G@KDJ* z)?e;Y1a+JS;@KlTQrt(G*&Xm z9g1w?T=A;f{Rnz>CR@Hwc)0zTZpkwTqZ^Tq@2IY>Y9THYpugKjg=MLC_>okJ*|-k# zu$6lCTj~Pu0X~e~F1wVwb3Y0R?|bF8pPC)B2OQ4Cuz=ibt}cVh3IA{?=bCt2qTw-Q zO*wY5;<*vWsA&)f9dGpfII3C*A(~J@Vea4{QO1qX%IpMThODO5dZ3Wf^*m(@HH@r? z+#`mble5{92wUpks_?>hYOmTO1%IyAfL2-*blP40Y+1LhZf}o9%yFnFA&J*p!b26D z-gQ!X1tIW^p_HY3)~V>@5Uux)akF$@o&@x%hw3h%!5E3zi23Fjot{>Ex`5Hyba3+14E!%Bw!B5|Qj;&7G<&lg|$l z2Yh7Hi>tinaN1(_hGOb1m#T1TjJa!z z^J~AP-SfQWHPt>=B<5-2aKum6_8Z-*!kAz6Cco2|B?HSLPKwjc zi9%e@cZ>|oSAU*vhjWFAdRcu*9c)|Q80T+huw-P^rlL|vOFTLvVPUbF3E}i9-R@+y z!a%IIoRSEKld8ABzM^}`klu%E&PoCWeSwzXERJp5!##WJ+yb>)(H?!*bdjZ6Z(pAd zrpC%&88S&qUf#@eJ+Db`Q15kY)c$4^y`4{s1*+S3VLU#;v09C6?l;Fx9!;KGxWQ9z zv_qcm+1mJ&Mhg_rb;z6NCuKe_YgSW5LI-a?pd-I1{MOUDZVdbAD13B;mE=iqL=-+U?R(96In<3oDiY%@M#OGC4nCbsEE z#*mbh)m?gfLf8cSFEWMtEaQ{MbP0vMr-LZIjBDl@v!h~ZfF<>1)cYS}oP3V02g6Rw zmPl}=MaSAfSgUpJ%tbPJ`U>Kk#(@m@3ZcT5dm3Haf~^ct-w|;;7h|o1-ZpVvEw7|* zuEbESu9W*hpK*;B^q2%LyMM2iHt&Lh0z327kQ5A`EN;2$d&1uVzEUE|;&*b#F`xh9x{amjo0{3l(nF)eR};OoXpO z48dH}{}jLvHe%WTw-+GtoCET-T}xwn00UDldBnQK_G*k}r7Iz;^KXMb4c?HtyX)zf zI`re-eYQu&rvtJxz18?#;^cp!y*9R3sdaqa9Z;|`n{o=2!>NbJEG9?m`hYRqXsbe~ zmL3_>ES#^@V%JXKDk}ZExKbiHN7JA~AvpZ6s{L2d?fVXA%Jbt093g*>PEu=)#1e?+7);bYTsQ9B3 zLMjIM6_mvg0Qt9W&S%byt$nX;QhcIm|F-{27_U;73H^x>M)zzJ9#zn@gy-`6=nrJFg4y$be8fj9Li(RArdk%Y2$@Eg*-n4 zv(1;v&}`Gk#~*J^)dhLBxR^>?oS=(>;dStIQ$dGab?ehVo9>NR z?>PdnC$*Aa6%(pWI7{GMI-Nx=;jR!o_PfJ#n2V^|AvNQeE>+r-u9mJyo-PDsSCh@s zJ8&#mEPof7bq`MqJ>8Da$qK4J{pQZa9Q7Wi1Dj}l>*ulrX74t6I;SHCtc0smN^W+w zMei(vpDkwdAAuJN^(i$}89-h4^vxLhqI6RUEHInIa^#e1Cfom){?ioH<+?f|o+f)a zgkjK>!gzuvt)gpm5vt45UItpFAOr>qm;>h=hya6M)$zoQ>$9j3aFCtH3OEQ;>~7>w+;7rVnYa2kZ2FhB!C+b2Fgv$-V1&X<$m%VIMsQ=g1qpBPE` zE}kK&vA3n4z+bA?9NJ}#_pDha@Kn^$uH8@2L(+4-vy)NBU9uPo6OdQwOc|5CA8Ji_b3 z+LB_~TTKXCrof-1d2|Gm8DT44z)pljv|R_GzGL~GW0K1w)BlgLbvak4*B_e}kKhvn zpkyRB)tb4Zs>aG{YGz><2@Qk=&$%C{ci|(#73yt#Q|)|#PkiA^tFBKi+yHx-56YYi zVRqkmKGPp#p%d{Qa9xWqCnB1yU4iJ0l@+xKVu=#klKy?7r-F60DYYKIp{oAB8P65N z8gp4(S-WLsSU6(S+>Jkc^Ty`iiP?J_Z*!s3-#&66(=F(a#;NXJB`8aH#HCTor#^4)Q37vrQ2Tm${j4{@Vsne z3HD}auye1wQBoq;JF9HGr6wW`w!mii@x}ctlw|8gn-U#TfS4I z46T$2TKs&el&I`r(I1M{I7-dr`B^$ck;z*I1JLBh>HNDZYJA&6eePV)O&$TZCXdAX z+sa%EWs+|B!Sm3*ff|ckMnT583piU0?Rwv#ie2(l7woWED}uVNEht6N`UH+Y= zqXt{G@%yfu6Pc=CzFw!_QgXd#ZYBRB7cSMK*-{OL#~FNkzLgLc?_O%Y0Kcjzo>1m&P}S-^82?f11@Eh}C68 z7QQ93n{J0Y4&ciGotR8!jLa5F zX*61(B~q)o=m*Z83(AQ9e8>$^$TzGrp@0G3@{V}U^3=vrClrmrpEz5o4+{$;q@XB~ zZlZjJOhobU-U;CSkC!TG7Sqx4meo>rS zOJ9a8XHfD;M49da=oC@@M#~6(zbgXQXqi8T5C1E!D(S=tncLy+V3a|AClR{Y$oNS# z7R>;3h!d&9c~E(3*-<}HW^Y6^{*D3tG53>lNAU~03|39`tl<1iAmMeSY1 z^lFK?0=zm&bvh=}*&Wp?H5;TP3X|KpGwF2p5$WaLOeTM+O%66C{X>_akrX+NWF+(A zimo%%Aix%ljdI|<2OAk-Xk!Dk=mhycZ3xK@+1r&k<5LjV^6pAvudAl+~=F8 zVGcT2kk5f2*jp;DYw{H-u|s}T!}b(+r_*^?r_n<>MR&OWelVQXyEO5p%m$=$LYw&Tg12Q)CQnxLIzx_~ZFwilptB!&frDuiqTb zR+Vq9RXong@yLk_?x01mM9qlwRJB+MRepa=xAd+8mVe0oO#Q+s=z4Ejt>UOR)CQi8 zGos;%mL|2!4_55cV4_N$N$SPT>99+fk2N)ryqNL5kJF3o8=uz+sgDGJa!2ytN|S6n z%tv5Ir1BBFu3{=+OqoKqFyv{4NKy5N`$}cG+hg+LKqOLut-Y+!h+9mdJbHZDp37y& z)9C44fM#ct|F7NR7M@+ZB$}OdDE8j;hRU?E`MQptd6I$qZi z_P*tlEJ~(Bk0a)zHw=XmJYCsGkp$$qZpPbr3?g9A7iZ^+)SSHU_+Ddg^bVBHWQoLo zL^Rkr&f^v!OeXymRahWtb0q?KwCS^^6`8PBc9Utt1&-6KRTVZ`a`aynnVw64poxjsrtJs zFBg}CNjsAKLT{~e`E$Fj818^gK8XjQd~pys-(K;;{I~sFq;X2ZcqsXj!SjQta}zzp zv_HV9vlv;k7y1Lk8Q1%rF470+9o&4?ig!U&$P|~hvn}l(Ma5_H)RH_svxDHovGXHK zRIbcT#o3uc5ovx|s-zZ!aj`6D(!k!CW0uxp;$-3Oco9CNh_P=dMiz~Fm-$&mEkq@+ z(Q@f##WY)o(f9FU!??40El02-Ha5$TV!BqOT5UQxkGMxkjyI^d*tF9f-F>D-M7?D3 zNuaF>X76CZ-Bd0AIzY!9?SZL0Lc6(P{M&-HP&E{h+r{gXLDFsBz&4^8ZrWfr;za)H z-&Pv;Qb?F>9<~_#%4}n4e)f~gOaU#&2P#dTwXTn>2-=WoPU%jEHk4yDJbbdR!49kD zBOBY%8n$%q0wcywWW?biA!q93XvgWco%-TiJ~Y?&eFbwfq>UP3>(F12)W}B3nYSuz|bc`0@^Nj zLi@UFKxVFCsj6V@do!oq}!<5~)4 zW)z^>IJNPu8hxSE%TuY)A*N3xwa0*PEYoIN{QmMc7}2UC-F-z(ztjhHSurhe_|s=^Ve7Rq4a=QMb5={1 z83}ANwmd`vprVN%1QQBJt0(s-e#DTjDCLY51}DH0ksJl~f`*=4qWGc4a0_Bq3VVbk zB$}FfSCgj}Iu-MDQ-=9-tT+-KrLQm#8`DxXFF$^Ix<-WH@Chw7PruzQMaNTCQ5jh> zS@hpiN|nJ2@?XjU|K^!XOG@rf?32iXpL03cIh!@(CiA`6aj3{=?tgYh4;J$C8z1sF zx;-95{}D{2<81+E+CE=GCz|YdG#v3ilV9s+x$hrtpmAr)URW>waY~O$VBrq%;2jQ$ z7fX2d5e2X$E_`kTkK`14G*Vf;KW^V6MpkDT9i-FWe&jWtxe!eHKFa17j*Mgt7`ora z6Fgb7-+df&+!8SE@zf;L#+%ey%;B#?`b<8}Bf4APV<{r<4qw@hS#EvwpIUc-N2NGl zGCl+tTSaoot;KZZ@6>@yYxu@u(_0x`-YuqxA8fhPU$0V`E$+~blatMPs^HQNPM91H zqd)16ou_#CImcGlK3V;ArW&aAx%P;BKdiZ{6#K49M_<3wH!{AN7M7ho`26|av)E^E zG`nBsT3nd9Gv!WS+rJpoDn&lK4)#}KzQQ#1<=sSap2NpIAJOzpng#oX66JDr)YFjv z_sSr}Wb=E>==y}-FP%mpn)32xUls3E(OE|~urj)k4$QjNd znrSTm-o=WHxI=P3_{|u&LIRpQygBymwjYl2y%sB}A%&j3Z&gs2_CrA)A=u>GzBp?m zVS@cVN@j^8#y`3fUVhSf=pszaC#h#Jj7LHt4Bn$;kTLinkKMr;pZ2V*{3(5V;Ox~d zS0fb~^Ce~m=kvPSl64Bc-N|s4K&i~Xb(Yac&J4l#zBa)5I@7BfO!MnAha@^3$u6Jw zD%-vn`L?ciPOOkJ#@}+2(qG}By!w%2q?9i^I9(NlIy%Qw3=ixo72Xlm=K(?Um|Rg&)fAn%;7=wt2st;n zWLWSyqGG(nYId?Wc-}_IrELuQ&yJ%EJW|y7l{i(ZsipD{YikDOizY(15r@Do*uW)l zFxTwi7G0a`aw6y%vgqR1?6$U(&Fg$AdA+4#|LfrjPW$^yyX(U&Ov5&fyS4RpUaMMO z|BeDf%;nTUCPy?42ksNQ|GmSHyHP+72C?=ao0z6W3@e3=ThS zEk=GT9p}aGt7j?J8T_B@WC%HSVzEimB=exB?c;A>yw|Us#!e>iRa?KjKAzL&SK?e> z0qPthXXa3vzaZwIZT+OuybI)E3)Zg;U<98(pUMd#t2db=jONVocqpRr6L>oM<|8MH z%-I47BWL^$GIPcd`TB+UjGpG+V7V)^j=B%*LKzWSICoz`!a+R^-tYpbQwwI{ zg_@0UKEQjM%xt-##-lX$zp0tA_#aaLh?&5xf5c2tu%Js-39cSP&~w5#u&xvL!}nWOe!>@yktVpo;=xUso?^Q+a`97|BlnZjp*T;{M);~ zdn1xyp^$g_gpR*+G9Ht0@JpC7m{6`EZr zAUmR!7%Ar~(0?UEX(b)=y*MKpEyldMS&U+Pb&OkW;!OBw>r&5oCnO=juq?=5Rc%Ad zLZw1r^hb@Ve7$8Acg24V=#mwZPb_y?45 zbvF~04huYq4q!N1ADdyk#9_xB1g5zw{wpP7j4C?=t;9zQakCT~(rh84$KP;N zyw@49Mz?A#fr?YR#y5FoYOHD@L}&t1l7by>NJ8hUN9+4hQtrudae{BRQ-qLgarS#mcAAb`O9-e3U`oO`=lmfyw%<));!a8rj2@&!& zxMcCeX_cepT4FqI7eMZ-*>ny#JT^-U;o9Qq{a+hWTGxJvL?uteIJ>WW114oU*{N2h!S2RU8nR$e2zn;#h#?xJ_ zC*{c+FYLv67D?`DVf`_433AT(9Ql0vo@n z2vBK1TcWDjV3lYVA`9DYuWcDj5ATnssbO7@@Uh@>;JRBl|MLdQ7N?nd`%NVI$+OOT zMR3{ec4ukisGcuHDSWjcZld|Us8^bt+*K2?G=*~@a*=n`C{?Y^G0!s zjbqQiZy~((-M6-|=a6@~blj(=ASH$^)J*03 z{TrgeuM~kJI2DmsQ-0L0%rR8cMOlm~*D}&^E|DkpOKLJemm;pi;fnq$-}S(D5fMd8 zA?8Cieg9HKQP|8zdpM&W9_q2+_gv8k`1--{i+kQ6*=wXI&nOH>wjpR%ki(U=slDW~ z6eV2FaK`ctx0X|OVs$_3+2EIuUalGqQI3ny%uhg(*{W;Rvh{>aAxE=EUQ1V6y8O5g z5lKV%_wU~7(mquk45zetlIcALC#%ds`_WX$AF1S`4KT{5ukDMB;B|f&4v8!vNWiR__rkHnN9G>B;Ei^OJ?1S29ND;jSqMai3|_p+r1b?Y|#czY%xAwhjEY#SaH z2lcn^0W}Z4rli+<842B)?yO&a?=8VOj9JN&j5~>r@sv%6L|dSANQXaig7NTH)zVX0 zs7r%7a-#6y27`EbD{4JhS`N?RcPIfY{eGHy9U{9ZI8 z_8^_=YyotwFJYx6GPk?d@UMpX3Ol9+d+2d`rKk0mdWVF%sFDB#XuakPTMHh^hwD=& zUDD#Ei^G4cO#DP8MpuH)yq%@1`80z8M{LrtlCv^9_(5981zA=ptDgs#;^za)8#QNI?IpYt3bNxu*F zJU8jni4;*}5we(LDBcwdO2&&G2TM&Uw;DC>0rh+5v_I&Qfl5>QjOtge{e zBW4m3N7QObc&DRSw7i)u;9nK!wZU>?Lddg(AorLLWR@UZ(&MZ{|4n*(nk8-yk0XOU z2Y77#lax8g$+WG!?{7FE(-~->?<%zJ`p40PEKz|9Q#;G?uZ^@ch`fQ1IMwY8Wouf` zp8JHLP?JQX5ye)f>>{QWep+&L{;mjpC*XG!@C8KGIH8!QiOwca8dYc|b5AYjfcw;& z<|^$wPFj*u_y33DkZAS;Rg%r*t59dmFnS#V#f&?^z3q&t=svy-Y-}v-Y%K0q2L6Yb znx(bb8;oLsLoD_-Kt|M{;X%b2+u|x|F=BjSI#E~68X^Dh5nG;8Fw4rRrk+i_oh)=j zAy5AHU=kB)2#*blQ$_3Ye7H01cGJC;umvkj|GH) zGW;toQB|>}zBI|^WIY#qW!GqA>VEU$&z+@gwR#sr)R3t`uAD})?AQgZdBuF^{qRK%0f=5CZVI=}7ZvWW6h)T7^KgHi49Vf!$nkaa+aR$^w zo(uOFa)ih3hxLp>mi^=JXi$YzI?hzI8@lU?P0Ki{M$E#lYt7U7$oR*Ytl3@>i^W#@ zL*m=jHOr2)6dB_KoY=$rWTf9*D(-58gl)&L;SN4KGl*?2z!@!3yRv6CU`m*}K;Ja6 zrSXXPx3{_VExAumIdfxjkL#r%>j8`YmlCRKkJmpvzosuw8G=LFxf-}7bJ2FkQM#B+ zR}=&xX$;23e}?U_5|2 z$4w2SzsK|MO7qR9KN%iIFg7-xKXNw+1z<#-d}jFB`1vq zPuR*v{4dg~4&>K}d)s_%oqIW1D81^v1sPT*RfhYtA>?eU4XFCC!=QYCA=zw>G@PGH z>EnESbxbt>`K2;soUZTJ*t9h6^ViMxy2>Yb#j-{CMOH)eEqjhz*8=^mX9UD%w**4^ z=mJlm{TsF(AHVrO!b&2}Dc{IyUHqkN0mNA8v>UWu@?nTNnNo`cEsxZG7Tz9-WivIT zMn`4S{`T#+wBl-s$#g8gS$Z_J^-7q<8y$?@!no-PBy>7u2tNqptdF^R?L|ysD8RuJ zr6sRjwoWi0IOR7zDbT-b@%;fShhhIU+3OGM?H2H$C+52QNpHBN%6pTfiMS&Dla3z# zNV?q=)a<=uZO-8NH6Dp)mc~CcDL~yxoi>7^Iyy}vcp{kgvI~w|7GuMeTRAGQLHbM< zV}F45Cna$hi&)x!cDb-PpN!;ZhAb$zOeBoWlHKiBb1Zg7quORZfCtSC2%4FC4|@&_ zUlv#BXAo(*qB*!mpO1y03^cCaPk2g_WlpWcz*#3rrSzm#cmG}*BF z5=eC`oUuvf4YgZXd&ye=UdV=OHNps@H@PBeiHka?_R&8LGqQov_&dTs`93v#&N*phr9n=frX1e(GN1k>r#M%6ejq~@o!0om7R+Btl+_B?u z+wp^P^Zew?STXeTluHZc``JAZyRjmvLRhs(n&JD}c(CaiQafADo}XPJ{Bqp&yqgHJ zKPBubNiK~FHVz`rHmaz9u$?AZu=vzaSN4t?U=lJ5IIm5g`i1Uthj}RHUlYeT>ZP9D zlGQ5bUcVd1%S~=DV{vOu?8p!89vrz;R)`Z_zM?!7yxtiBQDVe!o zlbe_RhSZq|>Q#*?C^VBgy2Y{Lng_DuILZls`@C2m*<{8`9Yo6xQK+d9Zgja|I|i z5~})VZ|_}VT=fiXq1-Rg;9rlBylmsgrFTpe6ZhFTMK;3~&yDQ#g>brFtix|il`0i; zc-&E>xoGhe18jnPwI#$-eH=@b_3Ya4x^4=n18P`zL3MvVrle)ctq?ZfoorgPU`R&g zBlzSRdv$mxKHeo-<*t}rr46_L9$SUCGNKs-==O{m^q0_Q2S|9N@zi4ObM!YeWv)c- z9*_A0&PqV(=! zA>`9?HleBVa<6moyk=#TNi36Oh)jEiPgSO4xX^#%+j(t9bki5?#oO1=)u!b7#a`;8 zG}g2u5_lGznUEMq%KGjvsn8kg9htr{1BUcYuxfJkI{Q~uycyCoE%ks5lFZe*_$! zRHCgc!PgOD{du;pt{{LR`CFzjmr_)Ekor?|_jBihaPG zMB9%7OKhOuP`3BtLT(~#5JJ02=w|m_GymbxMs48}(sWJM`gq(YpwAz#v?XU{N9KC9 zyc$Wfm5qP$H7fDe2e_oSROdFyj=SR0H2&eubkF>7u{k7nyL7t;*s^DKyYb1ohN@2W zh)aym&JkhN^IsLd>5>K`_eAHHi#b%eey|-an9xY10s0m4Y*j+&3oIPpWvkF&JFq*a zyKMWhMU`l@f3>xio!l%D9dC0zp34PSp{Jc9XA(0~SNJKsKrl-q23^@!u=qF~?gR83@(yqY$*8OgJCRDfqmQ zOc$AmDeIp$L|LyLcWBr{8a}DBAWEURz&L{I+jGdA^~u%>V{(QKtqGp(o!g(Rz9FW= z5!LyM|HIZ>Mz!5_Yro~D1zNl~6e#ZQUfkW?-5r7!cP(zko!~CTJ-7sScX!_Oe)fLP zKKq;x`H+zTYm8+5*P8R1zj@8I^mp^~&iwq6r4LB&ZjB~05+tN3TK`B=DnaLc9~TlY zl!&KS6+}U$DbWbf`0%OVqX^1JLBXHMg53fCOryuGY2y|om?X-&?fLqVe*A|D9)>;c zIWw_04GiWvKN+JxUuoH`6sSVTFy5X_VB!Un!gW#OKRBGGAhdMB7$OTuT?6Led>`t&pA#2Kak1S%(VGoX zGwGt8yZlBXBk683>Vg(<@%&}nK-e-}@{JCf(VKqux4GgI{Ow&-ICN$t=l7ne+!(K= zd~O(BAx$I&R6i2uYG|J|D8T29Q4 z^7q{|%n=It@o@(dLPmKd>(xZ2qsgu=9)*FF{&aitOx7OOid|F2c?)&GNaTHb)b*-I z#h0sP;8tsq8b{h z5;8Khwe5|zOX0EWsZ?%NT%>lxgjAjt7kTCBY%2g%qH$rQ00xt*|WmYMg>Vv-wLv1se)W>K<7my!s<08!OJR!gAI zdAZJ>Q~S%AY*qytY|TX_Y#z~JvFzDczZhT47`>f& z7@`R+(%WGW)?Y&E=44D4&nwVW56yGVMuI7fOc%?PxnGDxw%+^V%3wAhXPx4mZZ||` zPa1nf9WmGd6DWQnMesz^{hKZFVOIe2sizs1EvbXX8EKL8r`WIyUG+}=L!-eU@qU)| z-Csr77&BUFK}uB(snvG{5*R`ym)d!B63z2%-v`23}@dHPW{IkJFIPM+@LKzX%}op3EtJ*8n`kPS}o)> z+D^VW7uUL9A5kAwkvB~dFpSyz!!hvdctW-5|n?nWD;g`|8$>l=z&!iCE91HvD=)vco)ndwAlA4G=ZLjiOC=Q z)hOc4o00X(ztx@5>@b^norWGSQLbuxF`~KuRVDHJK4;@=BbIg3yvgegI|Hqw`JwIiG#+^+@mdJb%YI#5g!H@1yAg!5?DFGp0R4BEH9~|40fN)0m@#E>x=h~70w95*> zwrS&R&Um}n8P{$`%lm4MrHk%ZNUJy(xH6v#&{ALUI{aF?%epy@_V4}`a1W<<9h%G z3wy1Lfvb$2o=02yWbWX}UU2L^8&+b=H{WdTAb0tmURY&SA_}LZ#A^!F_{9iSb!dtb zGZc{H@`Eif$&QYGhhSN_id{~3nT}+>xDg`&bDyl`HXS@++Dm z8XD8bkH4L%$*}hLL`=-S3V2YawcYPju>`aA?MFV==I`LEi7Z%tijAAU@oHgv)$3$| zN>TxKK2a;iPu=*{;L+35kEK51@OklTRyb;Jkv1pIq*=WwbE2C>7(Xg*4 zvBR~tG+q@rq_U^7_n+iav!V_#(4dU>8S}(RGOKHMpAkh`Sc}CBE?pU?)a_(?Kk)Gw z5d*7eH=4Uef+xQ-8{bs}`lZ-UI))%~YI?R?C+MEhK+6mAm*1aBKw#!mEVatm{GR3` z9RY?ZMB6MEPyO4=yC34X>LC+zpozlcK>J!gcN21wTm99sr<&a0*iOYsN3F&h*Vv?} z0!ZxE`GKMiO{^n;%7E5c?B;iTBH#Wo`GHTf-wuMFCSK1quv7Z{EYf4Ta_EmEz5Zo7 z9?1vz<;Jt_^!UEnwN=dxlYwD~1pkTA4Fm@;fz9@^dG!+eO$``Hjc(R;wkZ@1p@R-o z;uc?Q)`rVKjL$~eQsb7Ah;g~Qu~Z_L>ASvPcV57;p8tKaB}z%}w6@M8`!)@h1TL@~ zU#E>ddGFb-PK23@Gj3@Pl6Ln?O3RYy!c~NaFSt&+^vsMRW^Dx}Nf$hyZc90;I8!|)yOwZnV7;vR8z`V|T=R48~}bsPqX z&+f$vT7$iB%#5Mv*kU*%#f)^mQKM%!V+xkCW}Y)3yd#tC6c!f7St(zf-w@FB;O$=v z=bcH6C!Aiz-zFkz?_p&Jz4|6Zc z7mkbgeSe#@J+l_9R;4q{A}Hfu)plZyE-x=0s!p8*stJ;!>d-?z zEG^jG^Px6>g31gOfz$mXm>>vl{UQ~O?;gxv<;cn(Q8!e&5ulR88GJ!%n?zQ!#ayimJ zO0{xe_@%O=PpiHu^M3wvBg)elj`#>`H&_G7^H*CTycJ8cyM3}3psuf&);Qs-hAH#L z^Ek7o^Y#yBB6>Ci6@-h>gK_U`Wdz)`Ozn3V5;KAyB#{*r*34hV#ge}?^3KeCp-n~z zD$Csiv%=$PYOnxD%Q|vQcKi+c8Xw0VSay%@6sH~V$itRP)YYb7l*MXY9syu= zyeXPN-LBlSwH=#3KbyhC?54~LcG}EG!R={KVa19m6vFK3Gs5_2-*xZuwx%z!7ev8- zz4o*mV6T-R*>xgHOEe=}r5=jl?@e3T#7TI#-cEZ_+jAU1DplTbw8n(T(ZZd=+Nv?! zo{nMAtYN{3&vqM+mbiOXwb^+MqtjsiSXN}VA1FtM&0deXhvtlP`JxoFL)x!3?K^MB zpPps%1+Ou4;&3T%dYa_}hH3+aGP;Q=*q@deh!>In=i1t?-~S}B`yw1Ke9SY)&5}Y| z{j4W|Z@>Jv!*q?tCt{kcsFfhrXp&LA&3pkGW`nCp-nf~1x&4!g z!SBsbLZcFmsj1u~yZ(7TU=Y=rBKg^;#%>lS(^(N$JJRaZsri57X1kII$LnQ1pu&0vt13R&Jif>j}kHF?`zAN=T`Z zOz2UMWHDUMfKNXSVFjjm6@DMlslNs%r_6(@1$Br|vx4{Q?x2xM3Ps;RVW?;;icrav z(RGw#p0q7Tt}?IaE5UaE37w-*^ct;M)G@JD+hNd_rv44KfVNHeD1keXtzQtf!h-Yx zM%@Ks70pqQ3YwwMewa^bi#Qd1g_kU^(y%6_+*+QKf&T@f4GlT}x)$f*IPvdCgUhK9 z=TF{0iu)IYdqub?0xv2qQx8~dN)BEf@D=kP6;LnlcUP|9`Obb-Ze{It_E_AYt6B+s zZ|!&u)VR}$V{BDHv(3(+Xw(ONeNOJAhhe)#17u06K(0yeRxYg)A&mHSL=-`oEJhU@ zy6o2-3P(WG!n+ob9))E>kjx#E=mn?FTEq12!Lc6?}He&P%n7n-& z{Rmx1FaEM2Q(Yxc!G)f<{uxm7OOzkBp2!TqAkc@Zn%GfNMDZm$XoAxwQfA} zIu2(2@-(j8+ZBL5!aZ6{#C^2r6!xen(^Bo7T!M=rTNLF%7nHdy7okf_+NaK|pBh;l z4mAQs{w2qSSP@|Lj|-I~LS@0>oz8dRE7d2Q?2^>OJCtZ-$^$X@TQ<98J<7#suR1#Y zGxSw?nQd*8tm9L;tEzT`lxW;UC*F&xp5emF?ulP#L&%eEeiz35+6pKZAI`)#U#vD1 z5fO=ti}O!OQSV%a7)RAXT8-Fkmx9bKE$b3dd*h+^YXGo{Btx(i)?Xyc;Irw~LHQ9@ zXf5&;uH2tG_}eOhWme$WB3Ewo4gwoBW|p}XTuxCY1J(>Xi(@Nk?%po1*su0wLLFjk z7ac|06-8whEh)c|vhu`%I&}#y$V=+1coqlRNJHLCv!@ z{amVy54?%gnSS2bSG&0_E6Us-ioYf;rTQzPJX+>??xCku&10jVEA+8BGF*tv9*X-y9iMguGTO8mp8DK zcRG*L@EkBG=(RB&WVLo1!w*k49%bdo^B3lHEV+)be>YDwTTX{>DTS`VJX^<%MU-2Mv9y0Gx@?um19z&=(_=Ns{G z*R@%joeVfp>K4myKL>;-m_M=NK>`Xt|vHp1W`+|MUaSR&P%Cb4SMHnP6f%Uiy3e) zHEX9+y0MDox&lC^UMO{U1@}YW^8$~r-?wxnqpqW={JD`otu*t$C#r5dy|`zrb5(D1 z);0B1Izek;xG|q_=`Z{;XH2uJBFV7+WVHZvkVS>bJZWacXc}R)P)!TyRr&)&@(r<4 zxpAR)9cKJ<*=O9Gu~K@VcX2Q1L`z4H#Kw*(?7+yqe>|8UFZG3vEV%g%dg?10)WODy zP}+iC;aJHQu9oT(@MOq@4Mdv`${Fw4Fc>pyJ(4qP`ZNnW&31yof|b*}$^_x1xPl~s z8{1Yr!y4T7PaWfH%{cJ*>vB$vS&{Hk;MxO(LQmdb39=>Bmn7M}ZZ95Z+$qJaqbAKk zHQRWkLqlcaU6--S9b-@{=GaPs{c#$1>4)+qawOgP+KDM`Nsz`Q`|3H_j(G(6TH^ zX!s&!n8TN@n2gQ8{RZfA$rb*m#MEG9Y;1}DwMUuIPb4pSwNy~Qr8YCTDya4tCo*@9 z5ytLJFmj`V)Y7a|^PZhTo*ZJ_VKUF>V;y}v=Y0J6-2gEh#7#V5!TYi_h+waBp=)>5 z2~2I>fs1g4F_$ZvvY0i=_L-E9!K3(FaZn1cH66ulbwBu{%pEKf_>&#WI2B7F+$|Df zO4(?fKyesTS2q<;W5`ftD4u~>v39|+UujyR)I^rGv@m$DmQH8-ChP$WCzW~2vZPy; zcyi>tzX!xev=JYqPWYo}y|_pDKJjS1v`2JfpfkS#f}g!&i;udl4=FR6{!=Rn$mDm6 zDr3h3KAOna*UVXw`7EMD5!HrUO1h#QRuG&&b2C45#;B{4otqq|0a(lTU6gqmY|{H* z3g&6O7oy_-2s4+a8Ul@Aur2Y8ArbnzbNw&eBCPo4~%$F)9^Gi(bj(aEht>MJIp zG8tk{0L(KpXfxwOJTqc`KBUC8pti%JqNHDE-|5B!B&3Be?-KBjrQ+1YW0`uaOx=qx z%26^!Cp}j<#Y4$3dd}?SkHYmepGTqSwPshCtXSmjH#Be~Mj5ou{m}y!ZwaJDG8kNe}Oat4sTIK!1L7 zri=tVC>EB1iA> zpR6XBt@rq|aaR)o>rZG3e!S(Ke%}Omt~NV;o{d_oE?Xfpx8X5qo56p`B&khxWMRr4 zTaBn~bWXf9RAYbKv`)K|IB+PocTBakTt?KOTpK$;{55DkT>?enc%cqY+mlEo~zQ#jEvNWvqIo&#gN+lVPR_jxiQ?}3w(3Qzav ze!Wj&VQ_Le{JIs-wc9CrxjISILENF6ey{U#{J*Z-!Rcsx+1aMmS+YdXLm+e2=^0y( z;(MTsTnJd#{D;+8xUFJ?6dOq#gUJON+ewUSqsW+~SkgKZEdc)vGi z{&Guv{;!4ggk~hXEqP5$O@WigmroDI;HqbOBlX)VOuvwAp1moSK0JB(i4}o&HNv`e zwVP+)y6KPoDqfVjV12n#@UHlK_g!D(jn(;Q3!Nupa(nM)B6BZ3D2!25zZ;YqEl^oS zVy0Ayc&MAIo!frluaNQdpZ!KHPbu+&MQS%Z(wOjr>|z&hkvzKwMW#T8QB*$n>kj5@#njUSVtKobs57_VGJ1jE6 z&PF%%$rFH)sLA0vANgC)tfa3u&k@-kz4p*DLuLNbFXV%>@(*bzFU*;T`nJR1r5~sJ z0*0BEsKNsvqyDl&@0TO21O2yvl%pXYhBk{Do3wk({3V#}%{{5K-{ROzj^qM4bj0N| z30zlQhfycI{$L7eA()blBXx}X>#kbEXUVU1XWn9jUCjazUGF9H>omx;+2#SB(q^l z7oQ0$+kWgFzB8Sh+pkfhb~Tv&b0pA%HXeZ8HNb@G!G1hgLL{hSuZ;@6e_NyYK)1g> zEg>xYYbn1=8ZQ2N|KNvIac?N{OpPLm4s%`V{;9BOAQUi`Uxb3^eBJd#BJB3#ErHk& zY!_PTZSgc4P7UTgmfy>AxK)y!Z7XM;zU4&Tg8W82i zo|Xu6u|n>uM^OXxsJgl4h4USRz#zDF3ZbyqQexu(Z$jO-*XpYhmx=U!}HZB z`k+#qtJ)G{{QNz^ialBbFf+FsvVsb)ly%+;Mdw;m$oP2%7|-Ov5j$v(2c zliZJS;uY)dQnRJTU|u%pHZ@UBoN&63K-c6ank_vGJRFeyfGB<97}K9#qC|oMeBl@& z+1~mbli9R|rxp02sl5rgv2RAiO0{Gw5zT!GV%)?fAJQE*W%0*=@wd9mfeYv%IB3Jq zR`jPC(cJ@+k*sP7>VfKy_PW0|1hi5=M z-tkGk0e&DeL;5qboGHq2^9Xn7(2x=rc9M)IvN^TK9)3OxHC5KZj*FH0MYEw=Ike*N zfh)IF@&wOzBH7Hq=E11SjZ-&ptm7h#ERp57$CG8tp5B;b^8m?>Y-K{+ugX{@i~v$j zNzBd|%Q-Zo6>qHBzw&9ndle&Rb8o7PqQTOEffYEHs`TWMR89;JM^o{=LW5kx$IhVZIAc0SoDbeMS~2xFfDI>4!n&BJCwmhiVVDm~T7dzHdC6dFW8W zoM(i{7D^!dA^ZFO$2X4-L|XyhNUHbaL3Lf{^b1m3Go7}ng3foWYaTDC?L*!gE;`sQ znZQ8l1GnCbSzIP*J+qhi`I9HtZ$AOp;OEd4oZ7CH3x$y~AkjPWKGTeDqxs0Bl?J7t>{B ztT(^u?QQS%1;wygDjW`=7nz1JK*Fw0I}ZM3LU1ehfVbIJ`NrYc-_O5#AlE}h>i6R@ z*yp~Tc!I-e{U3eTURxAF0feXIz~9;qpy(uXX#ymcI?khSHPp?Ar&J`Nx%BeUM?d0d z{5i(O?(&E0FEcqY!=)$zDsrG45fLLAwT2d_Djm2ooFL~FcHd>|Lha+@iNt|Ic&37H zT7!9X^4EaltJE{R!S-+|yBV0eE%2+GZA{Jhyt5na+%>Q%|VjV9VbHd60?E}AnIva z_0o|8l^t)<#(+7O#-UNYv>A>_!owFq6BZeYu`>!5B+nl{QGtU5F{=f>%~VDK$OAOn z08CG%(=&&Cz2Kp^(10tSN<&iyTs2Tv)p=>P)MFLH@t%r-r+Y@dzFG}`)j!*1`U0!R zNraYY7(#(VQQ+Pq3>t9tJ@Gzr30y2K^C|syMEh{~)^>C#(zJ~_jBs7G?J+?Gj-<@# zYSt$?v&<*}y7C9({ixsK`7g{0&<#&fJHw(Ammm_svC3!0EHB>PgZNcS8&=7bFG&ti z_0q@K0w*jnYB(e5rSe;TWG18Rjb}9p@vKQ}t5_Q*ZapRHjGv-RL16?tco8bZ1*S`! zc=ow@#VmSNg~0y%(jDc0^S$i%^OPMAEpJ{c8`_W!0~e4}+3E7>RVZP6jiK;aZH=!_ zyTIcRBrNDBdk@m!8(Goo(9k(GX7`;;59;qdxPFAXis_>oYITW`LIW^9$%#-WIpCH$ zI%?F5Y;CCw*?)t-{kGmUor9%T!uz=9(nDf(J@~u9h97`tx`;1_OI6B_{w~+vM4UO` zw)S*bj=YuOGmg{hjjB)g7*U7@s5W|ietBCipQWilpi96o&Nki*){0twfU@lXl3!?P zeJN%C^e%T^L)?Dp*9M<-MoAGEkkf`{rhEwL(pUgvCF-r!B$;q?aNPKVD6KE;IUpTq z1>SBAGIkoUuLqzjusE-2rQ=UqdhG^-tSA#(Tei$6npLc6wvJPeskMu*6b3H~V zSl#0TV9RDf6Q(C^JnzEbFWKF5mssWxj<@$bnHy_z^&3^=}jN`wEAu9(whBw!0i zq@r=S=<*d zR!kvK%|uV|!Dy1Rfr^sjh(MO=i^oj^m&~a?9-0B*H(?@$yy0};XPH!oU3=nlU*>TB z+}gVhyIgV#XHAO&GJSG;J+tkD+Vik7bnjIjR;3wQxM|5IbiSiDi7_#i}f7db%ux zAF6Yi_3JX_P(CHy1VuHHqaKfHq&tr`swZx+;CFJ&Kn*4=!EeWC7 zm0(nQT;_$p3U%(>8dQfzeepnov9ZI^&O{#2r4&I;l5~m+yt>-*DQWc6c-T6oU9yEz ziK)=&hj{kI(0T=4IyWVJOwH)%uho1l@qQrTRsl*#bhHMA)SHW0Q8wsY@qED2aw{KY zFwWkKmfuHXI>iX_DK&oSk*sGf&s^t>zD!heT1oD|o5AWvAb3S= z5~D6VZ9R46MgVvb;FlMO5E!jbneYl7E7JUVwX!x~OLGnd#o;Jq9Ie4rlO`Hn{CN#| zllt^4w`|dgK@*=l@Gaz)JL{))@2t{9js3k~o-O~Z%{Q^%;)a92<6UfcmSYPya1{~( z5Eo9z-MW@DmiAgg2$6pEjJtZ_X<3lHJa`s3wdid%%}E<4Bn)wvYZaX-mNy?xAgj=A zOTOA2<>Ah3OBfn6@hS$nEVrj+`M#O_gUB%8z)KzBR9hM(9F5Bsp;>2!iiQ@LqwlLs zu--0&i*Ko;b-;Pb`(MrEgO znqg-*bNyfRu6{dZ0n}4;d58R>68;&PJihL-fY(B%v`ey_EMrEDK>DU|fnX{29QUF( z^3I}i1K=TSplnvP`%yk}CrZqf%_dDYl@J_Shq|w{SaX&~hDp(ZMj&_Z5!|gSBP}ah zQ1JT~9SzSs1_{x}&kRw8SKY##hrjh~ST%oi0B%i~y<@3KcNH%zr(*`)3Tp+ZgV7Yv zJf0tR-Ufj)k27{Fj4(*>?Iiz^>eN)XmC7(E0+p!IwjORgR%Fj0HMVXME!?9*=>uLq zQz4zGgmtf+8^ZYy)kQX36-hMPXiQiWQciF^UaGmS{t@=dAu?89hVjx`YI#XQW|M>= zO?@g>&0IKex6TYniB2C>t}9~l5`{k}nLwmP%Ixg#iJL}=wHxqC`(xv}^vCD&E$3=R zal;pUaqD|-@|eso$3YMS0EppUcq&b(It`hL(7zC} zQmO0JjwGPoI`sPSy1+6(fX-6p{)XJ;iFJ%lo12^W5GnpM#Agtab`;Jx(Wv*dT+=cf zM^b-8){Qch&k(nKP;vhoG7GxhCQp8qcX()LTVJ4M3SD&P#FaK=S^lel1}jyv$B5{Y zbMcK%<#P6g_Zf>6i*Z4GHc4t3ua4~NMp{-P39JCp--A&&F-X#_ zy(dy(qAykh9dG#Y#j#H8mq!I%R9#W~-5UVj*;*pO+^mNK;Lb8;3B=4Ms|Y#@=94bf zsk*rW1P=fkmWwFgfJtP2%}3q{C~a?DXf}s2M+hYyNLUSsOx0T|^YZe3!%ue!1t*~# zXJ%Gc5l9#`;C@sq>-__i&owe#a9?-KJ#@Jf6Lq!|wnvi79;q5z#;^Um(&mxL?{pxZ z=~yAe!KsYk=Pzd}NM}^Rvhew7a1FrKMNUrCo_6*7vDw)X87E7`sa`Uh}kbE=fYl)ww2wi-F3uus0!a zlcC-^dz$hW4NX{E$&z}-zrSJvG+ah>-@@9E?tDx02G^nO;3rb9O#HO+pBrKUEp_Z@ z>?QOA%EzB`+JfN4RjALR_bqzVjOrEs>GPboY4<;GW5%KPWVM->VV<)F@Kb+*NyM4a zW&4~IY>byu>$II?^S&J1S#CBn`YbmX*Hh{f@3E#1ZLb;r9m1@uOJ!mGJYB3=*=1&k z7S``-#CgHVW;>^jgM`n0dEOWc8_!6t#fHoEW zp`NhWXR;W~(PU41rZQVb0=ueTp8-LrXy#Yfnhn()j+V0(FS`AlC z`;7!ak}Pm&OayYiSQK94c3?CPQPRt2CAZmBXhJu2PjE64my%Wkxz$U5>>u)kdEa7F z%y@sU+zSjB2d{qmBX++bO1nChaSy{EJr*kcy^t4KKp+?s`2ak3B23Fij&`X?^W``o zzU3Bi2^l;p4DH3(e2C+#{k59{+RsqFN6i;k8Er7rj0NhJa5LOYFiL+r?!$%6e5|5y zK*?HTULZg8pbow@`ANX=&Mt z`p2$Hl+;=6?TIOQZT;;}^AcF{&k&I;j>h&g=FztsI-MYo(AcNp%cnCWuF?YhZB@0iYZyZ| z;G1~KLw)XRpq+OIOST8kv#i!O#33#!+W*-~fnMIVKDxkJoQoiTQ@@-&Y2}Mui?Ra_ zcy_1m8}C=qWO+E(vgf#Wzg67i#qDpAY>I_q-}T&H-m@JqA#DTNmxz^u{@QCML&+ZL z5|v+6y`8q?czVzH_JkNqXOyQqM zHRr^>=?s>j@M(>Sk@In+3CxUZ#Ml^5>^^&(K1a(~o!Ob=s3$xkAo;E(XB}9(joW;~8!JRSzkK0|1lRLK%AF#Z9aLowavYh=1$fg_h z+X~V)Cw8mcN}~q+AhR;&M8nJ~)+rHi%)qP}UW^4xujTp#x0RXaLk?A4bBGbPjQ8fJ z&g)Au#hC*v!oif+=c!TcwHrzB$wQ9D?#Z^_eRgb8gdRx)e|ExVDT$9@yfjcO&cwYf)c<6p2jRPIrlk zMdyUEehO#sXOZ9m%VVx2Q3xO>y44(* z5U?|N78u1*FvH3j*N`vkFx4^qtINO%WQT1Djd}X|v|ut>uNjTJS0#%~n+-JJjf3gy zQt!L9+gbcb*6*avjy6&>vr^9$zt-)T4iH+(J-{-s?Cr)r-etr;^y`U;c6Y62(0U&$ z@;0=eOLFE;Q5liYbY;qm^c5piW=g22dA52p2STadaOFnjuO8I7q_M>!uK_|I@wy82 zuT)^V+S9%NMtlhovfy6vl3Wgj1(Uo&a1?M#VjK$k8-B7%z@n;fW?E^!;d;s;xLGP2 zVQIe`N~MF(F-fCJOr4(B7jm-JI?3n<}vdJUwg7$#3~@17&l}as<3eyG&)j zN&!4QbOc)e2y_Sp9{hBEESrlSOs4V0uy1b!mmOSLXMT(H2^HEdm%M%mVV3aR;?6waF|4tX= z9JJkT>QHNK=zKGi+nx@_Rzj*Yo=PtE>~QszxsNtp6CrD|3?Xh9@7 z%~w`I!o5qU(^P5+3a*IR7=ap4V>qM(i_7nasE0|sr8|!LjxB8EgU5DmAnS5c z_av>&ax1L0*jHsQck`c#I28E={pf!dxkwJCD7z45e^m05csflvw_i)3I3M%N>=R5d z0!%p0Xm*-)sa1MDP108yzf@0fYMD}67x8;GkgwK8S9(>LkjfUi-+$sv%VLCBF+v%q zvE;!%h7c#g;b>Ajma4Vu$TV{&b?O~Vl`5tpnt_v>KhDtWi?w;<_;3lhkb`T^0Q2}f z64-n&b#mg(#aQO7(NenECV(cp7$jM0!;|z-fb)51dI;C?Tz`H z_Gl=nn52h6(h&Nx0dbn_TtKdRq!=VP#H5RF=ES(d8x-Ojya(Rw2V+pncXDK1ge|== zSwPbIH4&V<>Hk3svNp1$OsNz0aXhE9xI7YfYF%MSQ`{KzKixmRHmR4YYv5>+(hTqp z_P{q_H(f$60rX`z8tT!NY`?r^tr?4Y7<0vL>yj$ZNz|KcS}|{EeX-X`sE}-i|4*fr z51)<*s(%m`pP8srZNA314-pq34XjEp-_pDVN!Od*5=lNua@6J{)&#^3n#!5O%urU- z^7Hd^`K@`hWbA5GxML`8&@4T+MESUzXS-Uy%jCZwPa)_t#>1|{dl=Jyn%Fpch-7(b zeqxxu0{7I~5%&u4$wkU9Fn9Q2^Kf=+j2cr zn{d3;M|TS7hLM!+nL-zGT%xsH6$f~SzvU^&WVI$GQ~{$*uc zmR)ITkP$|Kd@}g_@zL>I(aiPICpxVOy2&pwe|nM+^B+!PqcTn1t!9z>8n%39Z$}@? zOiC9gt99$H#{zShO3$k94Gi4oU=3@|Mq|A|B0SMtgx!{E9ylDK$p8r$2wmpHfyS zef2-MyF)etI}7$4D9P4)+GQ{Lwv;e`?QisC2)A=x3C{C~fZ@|e)jyMhG-mW|rr8Dq z^U&yXv-;tk-_XJ4tggoMn6X^V4h-HYCiYx6rBkbJEO5JBclYu zAby3sP&x?hsmq>tsm=5h>LTTq8HizKG4ueUsXl?CtK39+S{nLH=K4g0MUYT}E}h*r z9S$BIO;J%X25C3u=_XMgvI1H*cUJ2DE*(g{gpwW+mzGu4^{@Ve%jHz?@9fZ;q*64Y zm4|P-|9?Vq?N!mVi7vNz;b-v&Uxnd}ROA1l3H>jKuDEy~Kjs2yMny$-XU0M(RBkGN zLiuw+?hC%5x0)HmX*v7Sx$~#(c?eCXY+VnE#T>J-a_y z8hRk?8ONA{;t!m%2)g@(xm=b5dkx?W&ZPd#H(_bM!Om6K@Ww}lI!ElE1i@@KQxe>b zS*L?a4u~a^9RSsfW~apkpAhxC{8DNyeti;%{oevifY?!s&j~+v^ms9Obnib@tmu$S zPn*@}$t9UqvuCNnnhKJa<@dIbkjrO}MRBYf1vepwr;EilM~)#__p$id#LsB08~;!G zYyzk;GS<}*2uid<%$OwoLx}d}O?5?r;@2I1pyP-SHA!{$(%7T6cY0B_w)_LEwS>z& zs;AiY<^sQ&M?NuLSprT+G|-HGi=lzbnEZB}<#V7)7+%7f5Qmuk14FFj?Y}zGOFF?3 zUy06sdaj)g@6dY%mO_YL&bz4gB;bc*xlEv1lBI&nhJS#$1j|zB&b`uGPHx`9^1>b5 zr=}4^3srFEUlE1$IaseGp(leBe-KZ4l!widOixMkynN>C81L`7o; zqYAxB6$)RRXoiM+Weg@t z>hNXUk*Rg7ASHHFiaBdIXlqX_+s&vj>h#6)YY5_Cf#ul66_M181O8-eHNEa%Wdy4w zoQRlNlG`ul;Mdp+`>**x=SS{seEuVM7TYk}Z%eI#4u+}oHyA&24)4Opdc=qBJ z0o%z>4w?ISwagPwL!w~~zuR&OoP0gaSp;+@Tdmat99GJgO<2Bhd5!!12VJ67&njn$zvuH`d&5MO@)G8mNIFS zoVT|I1o(@Q`{e{1%bmgZ^e1!WSax=Hp7UwDANC;YLqw4_UT4fh>xErYVFQqu-qHUrKXb?#@#a`1Fl#mA-3DbQ!`%_bdA+n96VTNbFjYCYQ6{}u{%I+#|HXqna# zCj9Y90NT_fk4AFZ_E(`5X|Z{{C5_F|nU{LRE%TwK6McVzjETlnLLzFJ*3H1ZKg88> zrq&mF-sc)Ir?`lbnkgH?8s|Ks9!L*+j240+z2M*`d%`2z=C#ujh@7_S9+677eqr&9 zF?-RxG)qJS5E$sLJoHpRtY4<`MgPT?5N!U%mbRUX?17+Xh9>At zk7{diYWnAt*Gt^!7xW?CKR>rYvqu;@vmnSfWv2;%0LvfYG5E5WaHJrHSXfF78+J~` zc~kxW_D>}wDi|9xeS}SkFkILh&yK!bWb&mvmL{KMWlzVKcPT3RrgxfMWQi+(f7T1^ zdW!(>$NmZ&eWv~a!_$Re{sj8T^_i)epEukyA8@r4CW-wtNBZW+vZ&(qRl0SbLnH&jiCj9c zAC>OxMnNcq7*^B`OnvA zn-|TyVruF2Nt4YXAoaDF*@C5*@TXqL$^titPpslJJ;ot|A-4S)zDx9VbPY{%*4vhj zq})rHyYmf#;YKI|H#EOc>@?>^&5R?IQgH=W#4Uy8+$@X(|2!Ix@Zz9Ox-f# z;@w!Sm(HHY?l-h?hpfa?0Am=7wDam(T)kHF`#S%BL2eW85B;x}i`7uE@_b^42Qze# z8m306^PbV`cI=Aj{vglx((zvFwKQ4Ae|V=|4#kZgcyVs17@ZEf3zJD$Q`>ywd?utNHvzh?T-t?rwWiE-^x!p zz}9fUD88Lo@ObI(`b=;`&r;m^Bk>noJGhd?-p<)X%7*;+8wu|eEu6S22=Fh*Ho;V2 zD;nDWeU|qp33nmj1YO=T@hsGe6bd%^_*Lwo*Ztsez z&yNp!%KQ!nB1(?Z5wg2~>?tH8Bt_j<3v4Ydu}V){GrupK26at_`{hNwxb(;*!e(H2 zefzr^oY4Otl+E095QquGpg&f}Ci^F!6^K55H<0+a9a~o-EBq}T;b;=0W6CxUIG=RC z=jOWjR>(72WT^wHRvc(?iK&lEPun^`DIgA$p;_7;rS#qX_Au(l>0w9u7vEGs zZ#N$op;8{G`6>A>2^kItA#?vEq|05y#BOY#ApgkN!^eK`LFNxnjmn6`8(p9^+EKnY zM)LhQ5LI9E-Ek z)`)}K-dV%l)bdPpc)Qp5V8dlPY7B$uE?LdDf#E!-2yIO--8su|MuSzWCy(X~UiYWQ z>u<(=COU)>!HfA`U_b7fIFG`Fo>iLyIe%EaLYIPYT>gfV1HIuXLy6$Y6q?XdJN#c> zHV9jF<`dJ+R^Nr)&-ZGRU%j;Vf|)qk>~oT|(7x$T^vTD?lXw6hV&$NgLiUI(U?}rfJ+}3^n6$Rl*Y3>h<(|Z*Sp_!Z8-k4b(m)As^^S*=M zIj>oTfczK0SG%|Fw4fu}2U`Y`7j*1=BxbM0!I$JxqU`}mP~orbz;k5gwyIs}v+ap{ zearU@YeY-d7P+1qz2SM4Mi5Mn1>GHAN z>Yq!FW22>DLKtIwAl7-9m70EL6tBD({y)P4uTvYz5Geexwvg;MtIM#g8+|357dVbU zVuC0^>Q?7#9XDqyX+Npi6rH$ajq<@-9C1(QvFxrEJ%06eb1>8?r6AZ|bEyBr(>E|y zwzSP=CUz#t#I|kQw(W`SWWq_ZW81c!Ol;e>?d0xr&imb8uzGc`?)6kX)l~+Twgb)l zz5@+AC=ROGg(3$URbUH;`>6(9sodxYUqO6qNd?dD;k>6z-)jy+@nLwHlAq<@-HoEk z^bnTU*7#p*`xcdy_fIJvc7F6XSW2%pRcE==_`JCqtu}~$CD_s32#3aAC+i<(J6^35 z7RhQexn6jZ-h863EOnV_Wgn_gsz=}Oiz0bkawP9%v*-QV{%qoF(6<^asce7w4E4cj zZ^^#-spH_H>dy$63Y8^o7;^Pp_48Lo?>giyXQt7wjc!QZ7|On9hh{@C_{xOB6AQi> z>t_ZYcd{u)D;g>eXbJV*SC0f&qvIIhwFS#oAkt22VG$J6JB2=2-yY%c3|v$?}sT!rg+2OHJrau zt9@RI#v@>t>o7z9{4j9q8w}|WVGk=S^N%y}&hE*NV%TnR`RMg0%iCYsL9xrBiUkl? zren32GL{N(F0Bp~>2y=sU}Af_fQzjzvC&#v=19p&kNklVadQTPPjDPh?}<^o=uv4n zw-p92-mnL^TMkq^DX(8)+#U%LE4Us+sKC#iNCN+d{Jg^F5IiBBIVz zw$G_?QXr%ur~9-rzSFDkk@8acSc322r;MzvcAR50GDr0z>~p~o->U7(WRw%WLgivS zYvPHxYaRV$q66R87w?TZMCstdR(eK>)A{NREhOC_lIw!MNoKmf{M)Y2`^lBdw4)by zg=tLHSN$P-U_AL`Y-VEru5ISBMNyATp+J?Z@%SpCbLndXoQh;BUY}*NDF!~RG2st!q_Lu6MO6zaw_!H3h@%Vox>&5p3@dS*V=G6E+@Fmdsuhtmn6R-8hm zkv~6(HR{b|e-C~rK|8pf^9mU;-;wVR6UTbnQ}QS1kB~WlP;7eioJ-kXl=~# zr2q;B24W3mNa!X(!viET^G&VxQTPt^ZZBp|q`_?gub3 zV&vs3zP|E3Q0=D!!y=Kz07OrxtRQMDEBQ9S`w6V&ThAihC7QL~;33w}hI@tyFr>`r z@`q!Y*qF0UU~*;U$2_)iNmv7&f53ckSi`#?uQ3;EzL|oK?rHAfsx&oG1O$9GT>(4e z`UB_%$;cXPQVeFyzIiQ+>b{P^n~ zL=ONdK$t0J&3*5{Vt;W5)QBjZ9Uw(GBvvd`Nt2eZylA#~oN!o#AncBisur=n_!wDT zEt~qp2o&!g(w9@I{Iwl1iiPVooF(dOc^zV2*>8IA9Wz(jEcI)u)q%156 z(_ZQvH3g9Ht3Lhr2u7=i?7@Vbsi-3oo{lhnQ)Bn*>!18Y;cwp!0Go1OX2vVnH}2*0 z_0lO#w8?Z%UenXg7-El)B{{~iPPgvVbcUwVjA!0Qfy#ZCI}1P-qNS1L&@Dgs|M_!z2~rbcXj76;9Es9J|nTs z<4YAOV|>JYw|g7w*E_JsliS&prq<9V5^1cBoKoiBZ%XXe!he>olrV-zWh$^(0mdht zv)Sn%D66Yj{%De}Q!1WM+2ssSettsG5T(`LD(G7|*ZN%Efh#k|J->@lLDGKy%h$7 zChAM88IfG=IV044$83D)V#`ArML+rG+R8QgEYQT`8xxq?`|}7tK!`POnmJvBhMuLl z1mrGK*ED}_#D~FZZP(Y5_|h@WE-a+hSklVFiz0#T(2`3cXGdWL@3C`Rl#NOsn`+TM zrnBeU10A=Vdm}`|#p~_1_~1$%RBlWB6=T)4R76TC0E?Oy>wz=yzuY1%X-H zWM|b(rL#w8a=SS`UmIFmTkAOmRj1DQ_fYT_S_p;6R0-UOB>f^`-DfGj!gRQ$jLa^k zrPrslC1B-Ak!FvteXJJ1WFH$^_#0v(6nGWCr-p{lO=?0CU)C^q8`cwPe%@ywko}dE zNT^Jer9mH0`Es@(Y^+6*JgG;lWG8=OrFvcGyICkoEZHpQaH>XiXj@pS(1UWuub=+) zg!Soxli%ZiW9cWxggM%+JIE4;<{4JML^QLOT%`4rJ_{RI#r1YKIG-pr*=~$x@p@6H zRiu>mc?dZ>v+!y)9_|!I6ktWNFwQYd45Bs5Vo$UEGCREvLTA8$bN6(QNsNsY%kLZ6 zOf{|N6E-da2dsB>rGGM;#U*Agxtz@>AeLz5ft4%_OC=5-Uq$cE00O^?@Z9_V{HV}s z3@<66$jHou-OtdoGKwm>DrP>7IP+^Ar;EjP%T(?rpD}6|{|!deVI(i>nf(Sv)47!! z`_#>u@?E<5cHY5jRd~@oq59fg(>(G(na%5v4>i2F+cLWmFGky4p|lG!Q4fcMU}&(K zZQ>*Aae7W)xK-$esZrb?2R2%2{>i;n0!$ETQw^5?Gi84)nd5j{B!onfnkzhHqit53 zq|YwMLHKpoVz2i!F&WL1m3wWl1CyuF#myVzPP%QLjrPJY(D?OH`AR)F~S=m2+=r1U8 zq~^qQS+WiOgcwOKC#+h}icVt5lck-}lSK;3BbK%!;Gslu!?uZ)`9nNMT&toT9@K&q zucX8vB2GN_(!~hvFiL($>36~u3jxZSM=Tjja=K6+T6i#5jSf@f@^q=|=u}rD89~gn z`n_{svaoL9*?vXkRINICP-aY>-`Y@S1B;@dBT6P)KC?GXsZas^N#HT$e%QCPJ8Qi_ zruR~pv}gF>8WFXQop^+*Y`>{#*tw0pXiQ|VO1GeD z^o;hsJ|1=@Dr(tIBQ(Ccx>OkD6F;y=K;Y#zC^#gaac&!U{Yi9A&A2GU>gT4RL9?OE zrwI!t=81`k>1@%ifyn@GjCxtn_{xjjNAMOH{SBqv_U98UCjT$mZu0kTy^2BLeSqwx zY*paVGEt~9Tsuvn_)-7;y*Z?H^n2tjrVm=M5b*jbadU$=@hj4iPMb(?3-rs_01Zva zgUO(g5)&;1ZwJ5zwx3w>OzlS~Er^EH%)W>YAs1cRJb;BN z>3-1buK=o`;4sWFS@Y5bY1b?D^#yn04tg}Utb*2;DI>GJq6Fi~Z;Ki8K~t zi|QvuN>w;tKaM+c`h20(QLu!n@psmGOO0|$tHw0DN83XAgWKhT!rz^|s_D%+ZSC!) z^0|M3&xZDrm#$NF!DL)SuYYf|2s>{NZB=KfCV>GyHsJbuv0P@J>Bpsuh`ID!tWaQdX1BVMsKVOfMcsvS~9H8G9xBUBK9E@*~vkt~nwy zx~F97Z{VB;c`=gb1rMbqB}f95;`idukF6A2vak8E%tTF6tR=ru&ozMnD|8DU;H(6P z%s{Pp5<~2DXH4J0CmRn77P>RHp`&Vgf-1SPy{}1{>*rRyJ(X6f%*d338yJgW(K+!8 zeOhR`KfrksXIFm(Ty?$Rw=d!gl`1fKE*iLg-Se-qoJhRoOx~wYM>^ocix(sowNUV45!(ZxX`E+%FW=6%ZAvO=pQO8nphTf6%WQMp=GuLa9&y|i7pHg`OZ-S+boITVa_ zWMCyR2rX8_&ftc@DywB+vMfn_JLK)opq-@TRBl29b!FM2;NeyDdN-==;v$dDtfsPr zyuA7FwBR~)tE}WzfZd#W0m^Qh%bDZjxh6o27!wn-2tgcZQHlf;)ZWfL5P?3lxw%O) zB#*=Yt^F9|HC@}DfEuMx9hwh=b(EwQ6`b8}vp_G?)7fSYyHQ6l)NQTa?D8n#{(hiu zB0?9kJNH+*-tYm8FXsbFVYs%EUN^c&2Pvdqn}~{#9s)^ygV0JoIaBM2EfTlWAskRY z&|tffCgVdTxuTfS=^dWCL9S3%26_1{$-DVv?K{71to)y|#mZl&4FB%!%Gjm@Sf-~7 zNQ1IQcr%x?-5Q_t5^P^^$NGo<&1loldwEOz`QYChA)qAI-)fPoH$ww^ zEEk&Cm^o^coJ{U`jwA4zsk`3I#k?zvixdl1s4&4JafXs{DNiHf<6q8vOqgIzRlQ!y zX}N654lkd$b@la_ojI@|L$3NHWIVM3(fS1_M!>@cIB9Z|jRtXMekt8FZuWm|2bf~v z&|`qzy!z|8U<`QVPOgVM%ZVn5VFd~n6O*ESyX3^o;F*}P9)QlO#baBW^CLMVn3`RdeE5F1jAKvHg64*U1#;ytX2EfKdw~?%8|mj2I+Gus zDBR@KyZQ03JP^v=5dVDbYHC$LCu|(+2CIhCUVan}!3fD{u}X$MF=OLXg&=c=qC!I% zNzt8Se;LzNISL?lM-D;o;du#>3E2ogpuoYE#ac=yeuR%^=Q$`o5JwV zmiG3$ZC{JugM`Sy$qh2+@L-Sf)t;=&Q3lB@M`d^6sMa03N<%sZ&L4bI8W-vg<}8~v ziD{2e1_Cb2ONE`Ja#3D4c3ihLrx8b*XCBQ>Hc+FIueESGU!j678K=SEN0^T09a$d; z3;BebXB=S*#E9VjaXo6@PIUE5?UT{Be7+_FNnR9EI>@yCiy?0jtCe(cQ(_VbC<#*7vOFpzWWJdhrZ z*|4vVeJ)dar@vS{98@dA7lLlcB2HwDSui94Ql^&;FD%~0l*SM;5C$`C9^QWTkbz!+ z-p$1<=^pf3^5quKJ0`hBm{BP&#`xsFonr)C@E;(-g`{R?X2atXvK~_MiAhO={yjdN zzPIWuTtE5G+0DmytCqPAeFo+9n2=nI7|&U3Ik>o##(R$%2gv?gi))uU6D{evsJ7JE zH&sy~s$1=bS^YhWjYSG)1>Gx)9mBeG%PH06Bf_t>fGFsf+v__687{hU0FZU<_h z;BM!n&y5>xXb)8CiRN)b^e-?G&yNOR=4z{TVtN;-oW7MxVKLKrKq*SS3 zPK9Ts?IDtp;kJti&igsR#Sbp%LamW~x$>3n3sj$>#-XVqw5+Ue!zM+orSkvIaR9+P z^_|%S(aS6;0w-5I6=vyAT+u)|+#5$HRu_xl#hbnEnMP@Kv8T*Gig6fxhENg3)kPKh zAQNZJhR5@KDXEqManH6}>VS*Q1duy=1+6sZE^%76t2f;IUO+9%;I3kIt>n}JP)J5j zNg0-tlXGO7oPs-Y@GOj4{!KHLQv#U+)iG{&?s&Bp5`LpCKOw!TMEL(kW*kQ{v%Tr# zn*^XRW!jH}Lxs_jPcu24hKU4vp`gNmS2Z>5^SDb`YcTy%_Z3@WyPD|`#0DYC!*{%& zHynKhS6H3PB6GoVJdOS`WY{+h8_~E$JXZH^U>*3FaT1i1sCgcVEE+8@ULVxtP)9@|JQb z@+tKHM@tEdrgSoEdU^UV9~niW7yto(YODwKOI_A*F8n7T?fx5ACMP~Tj&xyKrGVLQ zHFHT5XZKInA2U14TEo{%zR|5zsfL+jH+PY{IdwTeJKbX`71k9_iMak<(g;Z@DUT!i(I~%dG)0CB*Urta?oBD#TW^ABl4TL3c z+%IqM*Y&aP6y?Un4x&%Vi*`kiVr2cCH_a`Xr&24(|G)&KpyuVkXJ_9IWXV1yxdcZ_ z&$)KH);4HFX+$jdw8y%NYwH#B$+(Xp{1i7I^x(k0U);8XS)B!R+EF@Wd+6fkbvU>+ z+U+4mg;JeO7%;>m79wqK(Gz3I%w}^(GMJ3mojrf|S?@H@6$0vfzjds&k$;don~#SI z#{fL5);s!>IIj#fOKF!A^L%qMUW@pC5Rj3jxp}z~(d_bBMFxCbYz;maZrD557kbmu zgIMFxDpKrIB*IY52g}wu{+|}$2Al-p!yZt$*L9eG5m;L@%T#!##iUe!%u>MUyU0p; zq6yzxalMe7PD98RbZNXv-Yz*m3e{~_umcH;sq--4=6F_3o8vHXQgRr4JqHCV!`_+$ ztk&AwYc0J!ugRj4%tV<8^*n4Tf8)W63hDeY&E^ms4G6ZtVsW>Dc9>lFMbgDj=C?A@ z`@Q%=KbKDbvk|Ae1{wiD%#>j}Gd|M!1EO@8!qi!L zS)mN4-yXWf@z*!iG|&wMmR|;CZnzO|sSgbeK9hHFgxg8B(s^cpWZQ9 zOMDX}BO@xsAOfDATzz72q9#jjA3O&Gxqb;mJbT+E zIZxTl5UEljviREFZ=I`@IB9<`EDR@@QANJ>ia@Q@%g7@9<#e4I z8 z;IKEyV*75EsFg}W7!?i>wzT|c`Q5ONCGN!<=HM{pE=M(_hM&Km6YUWD0rPhZ0qN;< z0k-k=s`J9y#%86(VXqrn@1IgLJfTwGcjWr>Bekr4n{AqdEc_yo3Z=ynoatpvzau3x zDWtNJwu+5y{pFcB0bzusWi3Acp-3`w)?)nD?5JEl{~KX$Gv#4MxU?oeRdZE;`&BgU zw8rf0EC!P^63DNmioV|6D^giRJRYD&&i2K}6Eap{&);!PW(lc?{G(^H{=*OuFgWBL9rv8L zU$_Yilh=qy02f5uuVEq@7C@;m+V9>!N~A{y5k3I@McgH11Pf=Y(}s$_fGq5oF;o`I z*_5=dNPm%wWD(k&vCXpC_WJg{syiC%4kET&Uf=!T z8=c%0aYb``@T{=5E?KQJ{f%GFz!(x_z~h7!T|zO=Y6a|k_7|r+aE-%4W_NI{xK406 zvEQ=^cHT+x0=>RunlE+rHD$=B@IprY5$K?PSGT~2{G4b7cJR^PDz)&}1D8w*uf^#`I*0lWtSzy^;<5bsM+`$bJWy^;8 zG;yYiDe9xt>)>E#@^aB}rCxoB5hiZgT&fwMte4zB~ZRc9+nMtraRaP{oq zep9|}=W)3PHK{Uoy1xFXau7`Tw1vD3FPl1e%;oOAo{oEZO}0<$eJSvpSYQ&XE=r_Q zXnJc1)3+{XI%ar(n8fcT`b~4x(QmTGaW|^LOCV@dd{SRO_G&M&1)iV0D1mypOq>G& zv||43q1ic~TjX8u6`hU7H2kQ~;m;BrrDpHHG(P5Sk+#y7j8ZiJuxj8$Z_)gJuGa7! z9dDOb#y^fLr9%n}NC3@t4wut}Rx;@b{z`;L?J*yKij~NkbJvy@633HQ53c6(s2+pu z(a4DV<2iWtucf(DIyij!|CEqRcnm1W94Fqhf-eH)@oGzc6 zh9)zmk5?q@`0lR3^=K&B@PVa#45z{Ip3z(;5X{sQKnA1#{WyywyyyG7&x)VH7WGy6 zF9wW1lPzPx&z%^I`_)g4Rg#ozT-Eo>b^-r^PAZ_J(%#jTF2*O#*;;i^C+$?88?g`^ zO+HMfw7%fDX&4>&KM7Lbo6O{PhK3QseT=z+^NcE;c%EK`IC3!F3OC`PqXD?mYCGD(IPdkt0I|{J@BB9OcLKUH94JDAsc*r99r5|yt^2kato6zH;m7RW}wO_voqb#bgGD$2LNNb zMc=MS(bHZ>ZEEGITrNjjrdIhn8ov`5`l#F`q*=yFFL`lzfVp5qnJl-N+hD^BY%LoR z5dx`Y_YmeM6pPkfcHcHUn^OguyrHZ~8ov6!^M+7Uv0WF`Mrq)&UhMn)7saY!$o)KR zzv*P-6sQs#e1yqsq$DH=NXa1ju8JAFJ8@4=Ri#$f_RM|_oV*bhn3yxPxOg}Z#Qo`m z)E)w{QiT`ERWK-G%l8a+`y&Ta>@BQ+zM(heVksF1CEY$6_SSCyP*?nsBfte$C0~pD z$gv$7?Gm|!EUrjqD~|HdIOC$w0CuPHFH86GwKbS}9;kR~q%yc6*EnX4zYIyU|Mae$ z=wAuPk&NQ<^!o&6ImYggNF#5oHu)GOEwKxl$-q5MPCj#TJNXVekK--8W_9@CqKXMM zJ&}?Uhw9x;&D9!H-@~wfmm0dA*t7vAVMD~p?a7|>g88gi$(mH2T zt~T1m&zfi%sT?u7>!f`%Yf{x@&-fZSuMaRAV5G&Ov4A~1kDkq-0D^yp3MNRAM6|#<@R}Czze>L_K@%&57{4*jTA4>l88u@NI7E25(%aoCl z@<*z$x3~8@B{K>oUD7-xqDGekF*D!R_rL217pWKt?B>69=#IRPefFh5O{JF&rwfSU zP2(%*CpYg1;f}cguU9M(+mD^;860ka*NM~J)1zxGvSt4c%p>3KZfrMv@zuv_$iFw8 z$-@&Y3ad?cdTOc)!y;B@FuSV%o-0HWaU@D;^dtIeEp0}d54tE~_ZqG;aITY+DpZ?>qKqy_bP)5E5t*%=Ju5R9recdxHUd^+Ktx`k+$l$0 zXKHKVtPUON9@G6*GYr{qGL8H+`_=XMnmx>Df}G)=P}5MJPmwAZ7lfW%MIU?q618%ATAo(rz+jhARi-+a+5O!cn8PuPfsw8(+h%<-Zi^FxMwG z&@0x6(*l@q$ss6sxQRcr^*nZbeXK}^i<>k%8zXdidv>|OlEDY+59@Z7cQ%*2Tqs>L zb5gRuBbYy1`111V0%GQ773!l3X%eoWHooyYvXJx2=N;BFFc!!(#?p?Jqg<|Z;e(Pr z5U{-CrE;cH%rlrgZ+{QtCLsZ9)QW6xe?y6;)vwKv(Ioo0wj6If4knPVJlAH2i(tzg zsAf5kQfn;AqyzOQNCQ|XV+x_nYWnej@ta2wr6T!xTsVE_#rmg5ImYQ0)zFxodp5m; zPud?h^ZYbp=;jtLGqIj8jb7&I#a!n~`zQQ+PEtNGymT@V2M~nrn~1Sm1x3mwKY>Mt zz`?n5tmP?=+0(@$v>r)Jxe6H{L}4G8 zE8$iyUIsCm_VlkTvZrj~g-ZEKrKP&8F_1fiEs2i(D`N=3N>O&f$l$JeW9l@fs{l*y z+Y1H9I!DYkV=iL8OA&)Lsq@uaYQx+eniX!W5emkN5uvE7)_yNgPle4cMy*2C-vflG za5oFUjJO?47AkOCHZAAVJWgOR)n!2B;Nn037Q*HRk(0y6=(rVfvCs%~@uk9T@lfWt z_?2cuUU+QQJkNtsoGeaI;MH}EsZ7$HLs{U9%Ffl9&Xg^@d)|=&JKY5FgicQRLajksK5&mM_pS5sT@NwH$q-f-f6Utu{> zk@gQR0=YEH&g87*Q98v88NAjvRJw3L@^G0`bLTStX<$Fo>_8$)UbrgeH{5JIGv>Sv z%|a&kACs0CsDK!_OT|J!g;!6^8Zgiij*zlj!c?snX_w>ITBY*Nz`~4BLE3jT+Qn{|u z-L8cK7ypkMRS+pCDqyg=5I_Zcl#P+SaZU}>qD1ALSJk>v_t z8Nd_-I@xqZD&jypU;$`uUTgSR;m6BGGth5;G5llH?F{1Y?z94$-v?{zFSUFX3J{5r z@q&(GNOim*qL z#SX?$Uj0xgV`8OV%9Sq`1f;S0PxfiWB*u#x81Rb8mc1 zhUfMTr%@zCKW)`Z>r1Uc5c!QxIJmmHF0X`L{;-4j?~LdR<96uynh-T>q7yC$QU3=c zvwk)GVAcJ_oc|d5k7Oid9v)j#w-sgxS3m#u&g-&Cd#P%GpEz*p{Cp&lWK>kteyAL5 zK>t=l8jUuuqnI3ieIxcVSU?M_EJXt(Es1t47Mopx8ud6*EMakR*xUQMTq_Jz+;?zh z5{B6Wb@qZ*;w0AFXB*Bz(a;%&CYU7NiGL&$7J7oAjHl(+0NDo1O`w=nyqJ}|MvT}t zcs<8=BLtOxum8l!&;D#uR)y+kp>*=69sM>e7$oFeu9J2}cCzpTqYounL9YKonlzTOIpC|q|9&+QT?78i_Or(qjw1y7I88o!!? zrMc%r8=?DF5D_f6V-~E_h3c%y3A16@$S|O89;pf}NyHRZY#ZHcUv~V^90|V>0HPMJ8Y5J(a z8+T0U%B6Q|LLecf4D>=bDpjrsl?d5l61`If@?1NlDvvb>Ydwv?igm;rtzl)J5Q^Sl zvy}KSldH?VeLtF?aC;IL4+{=8+8s)uJBv44TaN%J+=fFq-2DwIqum&;MJnxhd#O8k zvE1X|<}#C7Dp+VX(c;iQdeDDlGTZK=T2OMeeW<%wxh7vF*fT$|Bsn(xcc}fl+r1B? zbn}j3uPVe7j_lB(?zcbTVcSdmQPYhs^q!4ZLlvg+7PIwe5P`oLjR&u7rzXMg0y+zD zgv~Z1oqlr>@kJ7IGlv3uvvEesm`iUc74rzytav*MDA2Y4Fp?buOo%uHHW%i@CQP=d zPQsdXByw+lBDS`l{Y}Q=k$Fk-lB=z)fNZXKDVc$q^F2ok6gVx_B9n8OZzV-uP#tCTOPwXIyqt73))3xa!SUx(JGXx6ciO7 z;n>GG_N&cBe+Oo9L(_*A z@#@A#m*1TTaU?e<0WaejtSE_6BORjkCv)wHT7^HQJoH*efVg%j(1WI~rm8xj$E0NZEsz(R zYmuWKeNM{P`pXTxUO=peM=B{Lxqod3`D``E{;WnXH&E22^2;{~-}m`jqn7Mc7VlRQ z9D2iv6WQI$s!PSECC*8yrp!4)0>w`b z%g&PhY2-$sw^1V$7|8#XOh%pvJ+YL}T)@P;LPlW2d?R(3zh+^1*ZA%I5^ z0VR{29Z72jRo5mD;7_kb!gG#3x@W_QP~B#H%YFI1#ZuI6RTiq-LxEvskxrLy7E{;3 z@oFw4gen_z5jrsm|?X$)b#kKFC&<@Fe(6I$6Nurz9 zTyvVze^OPiiF0Aicschsm?w*gVPN6E$3Nif;(rnW5#1&@5a8zJ-0<3Vdp19{yzLq; z9ZeoO>{|f=VDbUl26QZezNF1QE+smcUgqX@itT!760auRz1pZbeW3URkr{=V zJxRW?63i#DEUAb=UpW|KpS@>Jgt1`${UWdpBqk+pn7$Zcu1nJ#!Wnn4L=*IcrGXPO z6%#1k3I9#}mUeoueK?WcV1XOdm>=?#>^iu}Xcxi}tkjdh>Z3@;y_-WBrBA6(g+?tL z!WUczuauJfN@}b{OHNG;nH|TMKusC9lPC5cLfbaWFBFHD|Az{=`1pKF=;|#mq|--m zd2xIhb1IS*2ExQQoD7LLfJ$_DfyY&ryH%r(etjA|x7%!`x-e0vElXG4n@;jCcuZkqdVM6qM1sJ3;^*o#*0W#q9tF zL^=dfdpBpfsNZ5&p`Gdy~RG*hT5Kj_Dju^fc4i(=gRvCZ}M zc7ttggRjpCG!c2H=FCnm-AW`0n{F7(fO+}WEj3`dnNJ4N?d$igwFhu2hiku>=zYnj z_dtR#v_t+Mf8h>WP^BgN@N20hC0%%;1Zwp8)xq{w#AK=J&~C0uNmNHS+<}EBBRrGq zHx2GyA^mc*6LvFRk;8ctaV>OhKVHVr>&x17iX{|I_*eMdB45bv#2 zVhX_l(FVW2vnyZR%pX0oDSz9{V*)CIZo>5Nai9(hOi%+e|13#~3 z=A0>{H0Mew=W*QkF^_tSG1SpS#J@V0!uNqBn_g~6k5y`L`qL29YaN$ZYOaSYXIg=s z>AvFSH74)N(7+C|UYf+8e`w1cPZOAQx%It`vVl^q-pTo$4p%3+@xNPLEV@!I%q|qY z%qE}CDS)q!=!(7W-!sF8=H#z>9Eg9b?$8;qnGzaQsTrM|#1*UYD%-lps)wR}o0x3= z)w;Kt3!1LrDmV+QZS>@EE8*|^KP|xNFB{XqNjcFMh|S@jx9Vtq5)x2NO-&ZGsys4? zWHc8rZ}MOMcHr!uk%-@O^ebD}BU_?c&FQ-F+Xm~PxsSQp7{eGT0Tyk4Fwby{y!_OaD%{mq_?Sexi6`!^ecNl!$8j4`o$%Q0{?T;_J47Z-Abi=2 zU#K4$nINQNOuAc|tT8-WRZoMn+w4f~5r#0dF&}w8*q~6Z1OsVIOEf;~1zm@YBH-gn zJs;N@ReORUU}OtcO60X%tEL0#zUY*(O%lOegK*bW6{1t_sB;f)PXBK~2&bpQwpS(; zx_b+97`Qk05MvEc9Yb0;#zD`F^okK2RX4luoq#De{B+dXXqqRpNWK7C>Z6!6!-B2e zxq^b?xtt8K1kJC%(MGvOkOqiY}BD*zLK1RshahLdbK#;A^<0T-vkq5#THL1 zaR4@&5^Cyyq^H8L>G8bild)1h>f8dFnpl1;vYZ@TwQ9UWrS{iFZ4f**!x#VKB{M-8 z^F`k?Yp@}Pr+7FjWgaSJ072gJfF1?gIk?xASpLkiHVc-Qv~_L&rk8N-Q#g!7>}Ckp zoiB#);=(!E%6XgJ<2&CDC-V`HlUWcy;#BJu!~-Q0YoCHz`0FFnu&e-?vI09nMlEP@ z4h>!2Qi=uL7Ki-C>O-w7BQaYCdmyRLSLL2s($O8m8_j@N7{_AG;N}DjH#+QnB&v!7 zL!XSTyeh9e9S&ZY$a~nA*P?yHnDHZBXSfE5;r_Lv5C+;i<)>Ts94y%P-vZZP050l? z^0&ts=V<~MDXA3uZGvA_gE#Jr>LsE?{did&NMY{2A6zDUIbE`3Xd$Z|)a8OwpR}vs z;Gj9g>f0xWvbGLrS%z{J)FxlEEov1Sz#D@Lmh&Yu;%{+raSROe#D%+r^p1Na3dwu; zYMG+M&Gn|AicCsQ4D}58+8dG-1rT5NI_d1zAIXDMNZ#Q{f53smB6y~6v^tP_*VVmY zWD)qdCEY(H^8H8)h(pe^S~44YIO`Z6-qv+sRMTyXBTt;|9m#5A9ha1u+B0!MAB0Ph z^9_b?wBj)w%X;8Ybs^(rj)c82Pp#dK@S}t?H!+!R^#Y5|<|`=4Nnulz#pE<4a7_JP zsxdgzX^t`DM`WfZ2GE`fw!&)yp}60NOT`rw4f)=jCJ6Ra`Ig>WBXE611ZD&rBN7x} zzT4GiZgFvFR1|_$A+n1YL@K>rjn5Qb2++YL%FN7rrHM8Z6cpsTe#u#W-Qj}^#3eI> z;esE;M9X`n8pUN@=Gu{Ipl>05dwOSIT?1sg(#Ov6*6~l~EDA3#5Dw`_BquN7$NivkyXo1Np11ovnpbj6AJ#0l}B9%^%UD9Fi}?}_0P8F-;a z3|JO{SS7-^=;8g83`5=MiVO}0_t6efZg2qosok8z^w&%;^EFP`u9)eD6+dV%0s2yu#9*A3in9|h8I}t$;iD zdhhWQBy7rUNndz^kg(&R+vBdsh}vRDG5z^V%Z`lGTXbA&FbT6 zOqm#uG%E#BDsp+t@!VJRpcq05VXO6n=ConQ+ z?yN$UYojd#;X%yrgrR5sO&Rk9946!ePrkh<69nVtKwxqxlc>lX&XtuWM#9f;0Vz>x zK)v4lc453);dl(QmH@dMV&Pi}=C;UegtHO$1UY{@R5pQI1)yB1ii)tHlwW%2!f2_v zSWhb}IAnf$=>)Zar{+ZS7hb51(FsW_w`(1Kn!|vb~_b8t18g78K@6$(S59 z**I&~f%(5+voZMfK}dCL2TPC`-3>&*Eq8$9rUHm>Zk7j!)I&CbuacC(5mwibV19B! z^9LY6ZDS)72{=BynCO8;rvU0ka7xt`<*1xX1&mz~S%)f9KRbEBPmr%o`@`+cS{}r$a zP0|P{EF?`zN=hSN?d=T)O4@ycQxpIEu0Q+QVT1zvvdtz(O5QYby;rI0;v3q1_1wq7 z6cD(lu&%DoJt$tyH@`CJofi+1)9>@2#kS61N&0q?ps}eh{9J8n@$bg|^sma~o@+Ok z#|jA&(8Nqg%%h_XtYadHijujS2^=wm&uKvQZ(CH2kt_^q;c$ALF@6mVAwmuVIsSn& zeydz8IXNPzsWp0QJ|l+_s9AIiQOh-cpf!23vbNIa~f#%bXvYJkMhM- zpUznT8Q-PX-(sPkb+lh|@6XrwCZ!+va!O&$r$m>9x|oy8B#Jr)t-(z1>;oFyskoD3ThZRrJUODC(t$1f20A z$6(y!U9j=Oyesj>8Jn2<|B7PZ>DpSkx9^#*Vrq9`OPu@iNkwDw?;>Pt6F$qo(FU|0 z??W#h2G`L^H24me8k%R!CAdHy&DOsSZRAPimRr=@U}f`}_#jO3W4f;5((tekdTn$Gix*Vc`LT9SnPp))3M*x_JZDJ8R4@!kIjz*`O-g(k1h)m z;@}YUF$fb^kY>!jn_B6DoIcN+Ztk1v__)JEy5Xc&c1K2x*k9F`s!%74HAxQl3?!XL zC09t_*dI^!A+2dJS#q36uCy*W7a3P2q~z-pKGQtI2DD~{rR@$^a(_?DZKzDeseY7- zUH1mrIP&2 zblP7*;9C>7;U-kkL77hx*VW~h7J!A4$ z;3y}g$j&_4qT0OVm_J?RQ=~ByQ{eaybcr~e9)qD8R@O{BHb}xAucpkqQQhM$rO-*c z8rxU|CKTZ_$&#EDZXCGj*E~PF13TBT$`jPE+S5p2D6+$gGYe*Yw>cIVS_a5AQ9V69 zh@s$qT)AY3CB-I$3ml?PN3upDv>u95R*A1AxlQ(z7N?bSPv;zhu`xXFQ^mQ&co#f2?4|!+AY7O$4&u?vTW{R6WB(8vKC4Q2*Q?F)Fw| zvK2R3VK{d%26B$enaBLB+Q108y`9|w@;x+|&~^@;mKHH$V12bCE!NO*{^{o64eYJ1 zxw#s*P^J7u@$JbF)8G*h6Zhd;3xTG~YY~7>)w{5OMnOSwJH|Xr zEob)e3P+pK9J*YydUvV_9h5<9aG2`*U+E^+0-=9T5Zo;Kl#35UDhnQ;XYvBTl56~U zyjb&lZQ~aqR8y>^75i}O(Ul_I)wd&m7N3&am>Rz*_HHYKQS}v>X&oMxxWDJ8y(lOo zX(T}d65#qrOB?KpYt&12XYJkF7EC|NR7}3_La4ho?Mvm}@kRp$a0Bif?SEfcDRruE z^JI(tHkNur$d%3?1c{yp{$LFcyQCaH0>a5+73jFGz z*Ty!q7M1}4M8qOsp3PQ2KjIO6Io%PL!X-#IH#0t%u@HjJjEv2PqA(b)udfB~`M2dV z83V{*FlLqHg+})vIoFndRr-Qiy)KDWw9_QT;u&tW&Hcv4sn4^A6(VPz*RUKe)9bWpVV+2C(mg^Sj6+y^iMY{)F4i_mMBYv& z-W1i0TlH(5Lxu1;W_^AA7yzTi*HWa&prD}qPX9ZFR09(e!Iv}d$Zr#IzoC;y9iz*C zf9{o}@|!&zPjHT}$Q+gTR0TT%yHQN+pGQ4R|5YMeceP9=rt12%wv15uWmCGe=IdGxf~CrK2vt940mC2|gL<5d%kLHJMjy!m(Y6Z`l~FO{fgva>?L7~jbY>@1cbC<*k8 z9@$yUWBTXp4f(OkJ!PdH?P114}=ZJtG^Vf!-DV*F84^0b6v^5klSL?)TXw zC(IR3D$hYjZ(&_1tIOU+aPca_7Rq4#-(qs_hq z$Yd7J)T(mHv1$uF4!!Q~VkSpQKxE z_mo0=gG5!YW3)%F3bHRXlO+7r{=Yt6U64^w*lKzVMWUm9sjfT@M$z=TJVV+Gl8ZlG zF%S3yvxfRH4j6g+(vY&f70TvSVswV;aO^GcxBseZ?_#cRW*&#yiE=pag(=G~`@N7| z2$vgLR&JY0;e&(>n5vY{|Hh;i+=|u4Vs?IzfwE_hZ?PPSer8VnFTop`NXc?A_^#ig zaNz6qPE#gdZgdnXyzliGpr&CMoRAG(chk2+aBE2RTbx8I>eyGhd5K)Wt);Y=ooDHvsKgtGZ{Ho>!eqWz}fqQ6WC9Pqo2GBlzimt4z z9G{)3SI4MUs3@oeQ*&UHRng&eKL=j+3Y9iE5emdfc04hDE!QE$-Z+Iofb8Y}qokzz zw*CI>@Ljc{c!dZ#f1ySoHZ_&>Ptna@K}h4(GE_oBg4y7c+vakST1wK}Bb6L}z_Rvf zLUN9{O$lQ06MX0_ui915#Nu8Y7jq3BM^5qFLj?)%DsE6tY_#gzRsG$)vK;OC-;1Lw z3qA*dIQAif(vpd@d0yT(8-Q;>k|I&6I~QwQAyz<7Ma0GwlEIf&0MTy*l>>i>tIH2b zi|3(EOkC+AE+suW%rIUe5+xDZZ-K22`%%E4Sk-}|q>Zv!LXRoECl-Un?#jrV9k)dM zb!rWGCuTbY|LF?@sm!Vmt8DbXZyFE3oX_U(dV8<7rbK+_z+^W0L8Del$j0`N*1q&r zFg5>MB-Z>qF6-(#yn`Xivb~%qc5rsohpxv9;bKP*DqR zIz-@VP&_RlmP$-wbZ{{q7*2q-zzpRUAb^H>IozibyIB4vScAoKJiw6eChQK=tkV@) zm`rAN%+ZZV%hN=vj{&&B7^lCAD;px`8JUH(^;fZj znu-eIv3``ed`^K;VmV7#}{8UH!8?yNmJjD)CZP68{kq z^K0@KG?c?=1N|(#M|Swi&4#SgNj(|tTe1i$`)8va&RD+1LaQ8lmQ*?Neun6suAv0| zc=5QIgzs;myJw1-p5F{BSKCoTVgr-!Tl%+pM$2W#{M0ek%4k|&E(xiA%G0#KWp0FU zry^lmYJVK=0lyv$zxA0}TM^H zBV^Za>svOPppG-oD-^M^qaUqj2Kh5dACAKzWMT#V`daGs0AX-PG5GBC<5LAwug;qb z7*+*DDuq7Uge9f_aPcLUUOXq|VoI2ug+}Fe?AGxOCuj2{nqw}>>q{hAs&RNYftzL7 z+Bx4J;BoO8O(Ab>HFMfIv=w{El@Q)_y6n`69O%0eqlFd57RtXn=No>t-k)x-C=KEQzua%f>0_FAdmNa@XHL(?A zTr$(G`JLn~dT4Pp_RMV$q}0gp-vJH=$neP6&Sz4GN?qf(zkTD8&rfkQwAA}EcaRzx zmS*N%8$QeJZhR-{mg}8<*{y47mH-Qie?rFrs@8Z_T{*nRA$!hT9t9EcdgMvjw7Qz6#`W(G27Ctr3fQ*htodjz z8BEkD8R^gtCt2Q#rQLx{^JUGoq?`n?md6Y%OCLTk9o!9xF^R&^C<-x%L0(z+=o)cb zDJg09>@5F;fJ1!LG8HL9MWKUbm=tge$)3 zaAy}B;8rF^-I$Mzj*xN_XHmsqJ5$_0z%93WfCW6f`z?9Z%rWCy zHd#qr*nGp}H=+!ayEOhNk|d=tVPtY7yK9L9=pGo9jh`I?L!iA!r2m64pV81?9ko0^ z-Bi*o=4**)(r9}JR+2p%Tnx5`SH5Rj0mAkpXC({)tk&;TMby>Rz$|unJdD9qC4eg|m$~Wz3>rAo#VtueZyRQCLzjb=MCkgU24_uhk23kF}Q0 zgJ6$*j1t`yLozf)Pz)Rd)yW92w`*K4Q^qKoZ_c;MweG|^F!H(Rvx!j9u*EJzzf{P`V6SH40+d7Vwoi2Rx;|ymvqwA69ladYtqfv)-OjU{+ zn#r;1?`auSXtAfo+3@ttT(R^&BmkKI@d-urKLpcH!P}b`A0L1Cz!78~4J}VaDK#ys zIfkx;!jp+A; z{gks#G|ch6PDAG8jco+P^uRQ{pSVB7LllC2zd|;O)7yw+por`k2UFQ+Aq}&F22)*0 zM$nO1H?&Ed6nTcIlWT&aA4CDneVISGwUskECI$)`x}4V+V@l7R^?vVorZ5;Q1Q7*A z`2|0)7)BQuO$fZ?j2$?7Z>2@NxlvJ19Y~$Z@!~8qdp(P!#;pAuJ(B?vf2on#lRaOH z;I%s8|Bn!qvIVQG3e~;k96o?YK0AJhPBSt!y#Ltc#fr)MJAI>Lj)wHRv@KS0>g~hM zKkvFL3J~nadQ_a+vA%cu>IDa{k3O( zMUJ>^n^Q`kh$Wgmr-w&T%UcA|(MVWW2^& zGpgf6v<;xTJ^pG&eUTw{eSbjGcvo-v_>XzNEgdr@Ss^*4*M`LSGICo^lj*T4qd>O= zzFEkHvaxpiD6(+3=PcgJ%1X-J{h4A*n}LC$v$NCpOngV6IFB}pri_QOno@8{Ff5|N zh;nMKK!ije)OzV<{U6GYPklttI$+B39yvR!WKxenlU+eWO*t_sW@jh&-tF_eto~D1 z0R%h*m(wXU#OvvnnJ6Hf-PYpZVxW=T7Ua0y8$9fCv#31p82nXHA+!pBqxrn{;7#T`$zu1K8dh%Bja7YHABOl=$sig;ZulNmI49G zE2VHOG$EtdZ;$P2o$g73Q{h74qQqI{+*zbo^SP}oEjundLpuJnQy68OrjQ45+rWYMy_X<%rm?3jf{;$V+rDJw#MVZ zdOAXH{3_NHS*wdL3RH?J*B9GurLD9ZMW{bjQhb5lnM#-t{l|AdkEvXNp_v(^i2c1u z?TVhaSbE)#f{F?lGBTiB)6x`mm7-p(;+ZqrrX6g>Q4p7g3IQMA>WGu{dIZ_ts>TDV zDs@-2D8Fm}sHq&L23?-w%QbODOG{J2&BY!92$4{we5G%{uErQ>hNj0w=8)Zia{+0D zf%;ZK9gYe;4~=*F_QGVgE!eG92m0Z9kDn8b>=G8l&~jHV2pe3m8ZR>E@4#f{7$svDs0L4F4oH0c@w1N;rnJSj9;{&OXD(CMS#4zwGAI zFsA>&CWeJ6X$^qTguwV7R)x?zf%abAFMZe%0o`*6t!C$M-;cM#>BZ1K;rWn|WHZy7 z{T&gjpFisWcN;B;?|CFC!aua%9-^f*S&hMyPbCXfZ$?2cJl_MqOZ1Rn2E@# zR@~C^Wh82eaA?pWGcm!6SSTcIL*!LFTD$brW=5=89Zzg&F!Q zm&hgLmQk9)yrw4F2c!s9zJR2R2%?|d7F8}vl$%{j=Nn0E|Ni(x}>92|bQ z!oyfJ-0qv_d`*d&f&PX|&kUx%}&- zis%?TPiN5tI3{;+uw1wISa1aV`(DaUm88g8!)VdNBg87&nbKrr!5W%7uDjpspL%rO zRRLK=d_O-wAf-`KPm$H!dalcu$!@~kb=3Uu$7n|s4A?q?U|@hR1E{iC*x z%z;AU#D+G9#fC4YS?DV&S{h={R7e}xnR0$AlhcXsJTaEGJXvCPW>j`lV8=-DvLo?Zw+9>r1tS@6BX%=0&HEGPV6Qm)ySWiQ(Po^`YT>|U896aE zZiS@VTV}^Qr}#0KX_iI9%Sw;vT>i+%k4z&wyWYz#0i_zli>I1AXp|C}Jf9BN#wyLE zzkF}*JRa1iyGDBZCUZZdk|W%s#Ti%APVK8+QJ`as><*vFbo!|(4gc`awZ))+gD zhv2<^d;pVrp(?dP+9|S=4H}3ig2C*}CrT>%!m;qf;GyqU_&wGFcHOSgI+(IQJ;1Bk zO6v7?fO9_dOD`e>?e)v}kI2yLw;qGT0l=<8wCIKO3}Nry)Q(ObpWFPGRa#E2CrW`8 zTtw7V--;4VA>nJUmmNS(S?z==1O(8v^J^6UvUq0P2Q%inxh4V`fjLyabZVGaQRVdf;s=4((+juqB%GZSsjL_a zEt0le%LnU#Cnz?n(UaJ6^GpI6W4GG}AVKdg3#LQAIZawJvJm(So?jQQY~>oNh8N`z z-=7!qI~xNZGMDO{2Z*&ePXE?{oh?87v+LIG{N{h*)m-m<0K9$qH-Inf#qi%#WqJdM z$^q?GW5AxcJkm-%4acQ^p=pIhcWw(}g2rHDk+?(6q2yI*z<-^lE=KGueF2Z7E!O~= z`SENuUG~~wd{&0U)Jy_t>UU)|n~7|59LyCr;^P<`Uc>+$z56|&cDgxwAFIk5h)8lMXm|NecE+{~Rq)JRrv&F{G+jvunT zfOznyXg%Ok)0~-6_cfv%c(r~&;p9|Frqt)+jG3Dx3ueoOAsq?D#9DAVoG|>aFaG8A z*DZ>)0e@O!Bdh1biR2vl1SXSrzrClNogRGG>%gzqeXMyaOis4I=|}w1QiGLN-9M&g zb*JZ?%u0?Cwwwq!=E?vS1Ilh^q(%8rK}$*OHp&e%9do*AMS%zw=KB}uf8~ClfUcby zx>#}XAA#1mvTFR~R6$874^2&-Hix8Z#lmG~D^j}kbw=+mk#(O@m7J0ik|ZfhMjd60 zrz3+*V9#$Y3-{3Zd^mWcLvm!IigzhOkz@uRb1ZM20g$~vml zN;O_Wp|Fe@5eKQa@2W@qWG`rW0WKjH9yug1Z6b5;_*g<%7&JCEmY}n{)b|6h=_4d2 z-VJIVDv`?~W~PS55a-CD;58Mj5xS2)m9})?@)2|X*OfsLMP5mMxk$$C7G*YdYJ5y} zuhH(!1n4OaO^jp%F}wO;JsMNd@~hUxXduJ>H^&K%Rja$je!`{Z-T|D(Pv)jjlRg zTV^MB<*5sGrVs{$K|j60oblhR4RsnIz=S3xp_Cs@Q7w0d6l{16%Xo?;$HMonI(9ai zoKsSZetqP=2UTe75zZL}$UvZ*MgL}p`&U{D7%#jFdU$ZnZnX`zmX?O!pbX9wcyPvK zh9u~xN(yO`55?jc?{2Tm+v#Md;Gj3WO+V4<@?k*IaKM@zN(?YJ+i%4JvK8#`sefqn z6RTwmX7g4$eUf@trBNh3QhICAqnOH>LE^XOW_ID9LTV;85)wfo*N+}^FIXJzG&(gu+w`5W2 z-JQbG0h9HdnOe^DI_@DBs?1b-ulGVXABb;K$@T39j}9H(Nw)>lwe&F`x8wIo6bhu% z)6tPIgtoVX`_VgDE3C;%k+9#jUK}7WBqT!gka*w+S{e4nBpt4%lf|->){uk_4&QWj z0U@mrKFc%cOy^bp`SHNJf)I)+y z>GXAJsDC)Q#Zt1BTTQ&ET(G#P><1ybbi4#<>E@QVx}su0vrLC$ZmLR!W3B{g`MVa7 z;FVMfg;AoN(L70uItMog1BgwD(pRvKsael{^R0KTbb8T=c*Pz*{>vmS?(E%ELwP*| zSOzPD;Qg*RlN&=V0L(Kmwgy9j!dD!dIhj}Y!Rc{AIi<%}EW5VYc=aM3!HhtHz{|_a z-<}G5HxCvwM#EQU(3ohAV!9!%0*|e}1L#mw^W8GCJtIjb!$b6CnVjHGU!O)^E5}Ry zAD$5r5zEWV-i_Vd^z^Zdw%RjwB9MS+>Z=gnj=W--s!pW9PnjitOzo3eBl2Y-|yH zXKEw@HHfxt=;Y`Sl=la%uNKs?&ta1w*|a) zf$`>0upof-#f?o(4Rv|C;13NI(f~}EneoH&WGVd49-@oU7MkmZrUhC(*-2eCht;+%Zj~7Fc6PZkStUpfPhNNG}mVLV%_QY!#$jHEplp-`} zKl4@twW`dHr}KGRzYwKqU;wSaFcs3mKY9KkFc{YxuMdqj^Em`=_y%u-y54+t@TLbw z?eJ%V@t1{`NQrBH@$*W@s(LJ>)C7p)9dq~C?v{`caDrK>?P$*U=$4a zWCf-eIi2pq9plclGF6DmQfFeezZuOr@wA=EKQC3IVWUeeDa532LsQ|fnH?^F5No@T z0Pn*(KU|j;$B5FT@FO8bXR`&z(iOBhTwyn>+WdgO5u;7IJ4jB4#b)zg)eGSDc=^p< zwKmhLONmU!gws-8xaHs7c#bEAMnm&>xh8G#K`2W6+gwx>X=%?3&$5$~gJWXgh!etZgTE)2$j!MO!d`azjG4mFz*DBo zOG_U_hoGqzDS?_YgN~0cTbso$k4Nw94H{j$J;X8SwED-zA$WRv_FRa-RleGh)qz3o z5a>jORFoK7P>E|MOg&XR)mqNT-{-7HW*+VSx^vuJ!((x9br#njOJ1Khf_NYh+cPs1 zeL$dS*oR9WkQ~qL!h%F_5~GVOCX|=NxUyum0IUsK@OtTZS2&Lte}WHmcIL{UL9*bM zTem;@aQF~hDLHDmSrXf$ur~ZnZ62I2y^s(jaYK(=olNUmOnN1ciVtnNX|apQNrh@x zGTz?G0`3#2pxc1)rDs=>qj=DXGiHV=u)MOUZ2K!oC`!yQy z{-U&E0OP9?Sm*CFXHXhpILg{=hLe`dsmz+l26Ae6e;%DkaaDRmLqqeSof0xJ0YU%d zWcWQC2JPj=h7 zJtXwapx?qaM%uGzW{s9lUL60Y`4ohZE_N&v&pof%H zqyw|MQNBx8pzlfnfIMIBU-73+Y*h(8Hprpo7KlfaIfc3^)`QVl%5A}gQR1ZE9(*FA zqW-O}z6M=e85EE06o&D?V9hQrnhm@?wLOwEG@ID`%_K{C!^6WH86N(-w)PfuRHjOu zI+Za{%g~)y7X%hXN5>>31M!!YA&lG+@BEJv;E|Gwv2R#Z5mlh8ec$e8oRWg}0eN5K z93TM2x>~NOfF|W=O$oqWbm)}YDt+R@0-#M!qz139pr+&SyK ziPmE_``a>0z%}MzNF2p&_nYiHiH4`ecXqkMPC1D`iWGj>F1IDV+63I~vc!ewXysY7 zyK=k+FZ~2iwtN3lZE=>J+<6Mo#f=Rrr4^jy;qBXC1=Y0h%@)O4-Pef3gnxB`93p%K z=mj(=pr;_Rbix@Wpr*9rUKwCiAx4PK zmnwpJ`IL+<9UdJ$jXWLOA28?(ZN7tR;0lCR9u6Hm0QuRT1i&x1dlYrk)4UKmK=K;xm+pk&~dSBy5Bh_ z3j)3nDKb;BkC&)wxqkrBffd_lj$Y|cqj_Sa(_zX#R~^MC$;pg|mwa{`?Gt&+LYPZX z!tb9h=xAt2XjlVh>h`s@jRsfUe7`d?K;ttqgnwldIy&7=&&(WdKic28HU-Rr!sGJD zwj@YTCrSa#3DPXhk!1_Nc?nE@I{{Uh7>`$S6ciGXSbgfhy@NT(O-usW-Q$yJkW3s+ zd8&nvjpqygjnSx;<5h}fD!|m*xR{P;UM8&&97{#TB9+{|?XA{#i27A@z>(<_*8{w* zRRegUd{XFR&ePki5KzpXetr*j*V?|yg4Obbjkz)G4ShJKHbJG1(yYJ6X%rz5G!tY? z2!}*?1#(ZE1Zm&WQj24=JjA-!{kysVO*Xen0f2_&t{ELppe?{Z7X%mn$)5}=B$EM99jMGP@%5vN0B4BY7^fA;Gi z7M<<%-Nl0PioLxYVJEBQEdC?+ZiAeqUjFS}2z3}Us&!$35Uf-d%PwXsJ2i?(Pa)z- z%k5hhzT<+iyX@^tSo@E;6J!mg@>CTW5mKD2_ntm95BF5Cg<(sb#=mclVQ5epW$(k$ zjo+GX*R7wJrYn&vv;oy{5>-8qm>?me_ZUWYGjD|kZmP-oGo;o;hLOe7e%n?U!cbPk z@`M-HHp1*BR(nt2{SgE>GKk1-rx$$sZIL2nuYu4Mk(ID5RwXC~0bn zoongf3T5-$z5e}XBO{v!WO^D22`Q7S7no>(wr2nKSd^YgqY4N2?N<(G#L^W{6j#%g ze{L6_I)yywHbCEx(A4yE648=7;Osuz%>MU8aJ`VQCsCx-eXF`WjVdfhpL2EYKNA)~^U!wV;hAZF~{^=!u{jd*Ss$yj{^f0(SpVKVM ziA#BdI2ur z@mdYX#~a+AyDb-ox0S|r7MdP_x83PIIuc!59|EvsX=^8B0yDDIg$qYH=iS1#MImh@)vjuJ+G<4 zU5%!L5v%KD>UWMO6XAKO^5x81#!s}Sz>QkAs?gYZkP@+`l`G__=C&hXm)rRSD^Ise^}9uNbYOpml)^-{x?f=P$l;nF?2!CAg17DJx^AAUgj4Bfo?{ zZC`?sUu}dX1TZPet|b|&#W36IdQN5u>VCa8+AODvl=u~lA$a}faKI`uEz0WlNNM1y zCYeiAY;pC$7K2Potq_$iAIP7BOn~u14aZIi`h-^DV zQfgKI@^rAZ+b(Ad3x0YktU09mBf*xemKuAinVL=yj=IPArXtvij|tC7dYMXH?T%%i z@#6TxIwJ=5ay)p2InX=80ieOF7=qFAy)Qsc87aa!D%(4gRfxxJ;>|40!9VBePPuBP z6Vv=-GTpzK{JoVZ@fZnnIbP9&IhFo1wAps`_MwG_<_tE1g8mh>kIUb zDwD&$SUe6N7I!@^1O~k@{!HzXhMwMRv(ux$zkmChqiFpzAu3z5SIm)5{MS*I7HA>MoV?QbbXK8jg5 zv~wNb$e^QY7Dq`|4ES|Jg?&;p-z@@TC!auVcgNQu_t7zGcgHul1zwxlMz(K+t!)${ znbzf@yd9aM7h;zZO{_(WHWiwA=70W9s~}vp(;uIX)s<2J^&2crP+)O2=V^1JrWuUn9dE$0&fWNhr$p&UwJ zmSOx~Cz;pQOU=WBgMyM2_xK+TnvF*Nc5k(*6b3pk55z@?|4ly2;BJTHJSs}$Y{Z!O z{k_aC|3r)3;0`@j!2Lh?g`S`PKm0=RTZY(oy1_VcO)aUXJVLX}t*N%5I4(9bR*;gx zXmaOEUhe1D`lCZx_WMg!u2^-aLj}`uOuKGLplEh*Iz@0MTM#Qlp7XeM7JJAPZFt?W+3E@Toieu4Xn`@>M@+xbIhua41mx}?j}_uUrK_lE zs6a>%p+^jlj{F^6&&cYup{5?CX#qn`OD0@e%>UE)pR|+)t(bIqhzcNR6q{;}@| zNLbg7Pefd72?e{v_bzOHpd3uh2cJni&tBk(0H~T@58}fod+prlv^$*s(P+CGk@-|p zNK4<>l?r3FNDf;%6U0~2Qg6D&?fifit%ue7^WrWqblLFE5(h_YC;-mgl_Fv0qMIl$ z2oBnZYb`B%|2lQd8F$$qr2_%zOA#Bo@!}%7!MT;dVlTOdwl@ngdF){IC$b(8q`iRA zdA(F37C@R!o6n7V+Y6736%@Df zkDd3=iuM+Z#ufVd_HhI4Ox{5na-P@{9iA@dDMgM`EqAy-c^152HMI(Z!c@n&J$;$f zc#0)6KRqaFhBiBKC0#JI@Gnm#?U^M`gWV2;|3L?fvvdn`0X3`ZoRWb?TwFcVDyJDU z^UTqZUMpp<^ud z>CG4_VvB9Yt0t7@^2+0ICOh4S>uQn<@b1le%0iHe6)9@=rbj#d&hnPT)(C?iG-7gi z7a>x*yYQ_^CTLVW$VLdnGdqWSN349FHY~S;0xQ%+tI2gcNDEA{3~jypJ{Z4}56!4h#`?T+w0+f`l_|?LerMiWG1nAZHSvmv3l4HG8tym(5 z7rbI4sXT0X;|0hRoZdf&4`p)fSXxI-p9q#1PZtc>^!y4C9~o&Z)z8JUT|@kR_oMRx z0|n$*sf{^_-S*H!3VQ31*RvZ7W2kW7guL0J7el)lG3`-&V|_WDh3fjGq>|qOw08gW zGd8V|EbV4{A`=7^3SO+fL4=_p|7gA}l{ExgcG=f}rOOrHe1AyafHec_eox_0_C05o z$dt=thcl?>H7lDY?wXeA8(^Fi1p&gldqo%e1<=DJBUD4IfsYmp2L5ejG@v7k?eo&sXZ?{wsM@WUmCxDX$nd4a7z( zpEFmX7!6b1eqV4>fvyo1df-Zi=9;itD84$V1}>;Su0=m0x%9x=A`SVtXU$bG zMnhD0jqbTTU5^D7{6W%V)Az(!+;#LpBfmQw?bwR>atFqbrmio0Y|-XT`R6>pf&XGS zgjc5Um2{s#9yUQ1_;^N{&|*8tG@TOyj867YmEI?B1PSD)AW*0@M{;Z2{Kk7@j)2uY z=(B1$G2;?)V&>@RdD^H=Ua4Z67(yF7T5cDrQ~2yK)<1?rQYx01Vl}7w$Tc0Zi7)&x zLw5V`3EJLu?cPutnIlNLM)Z1vz)Z1{=QT*Vpd^i7&uOwEcHAzS);;t zJNE9@msjHc<(|fqZlBj~XFml|wC>MQK{YjWv?$?!p>PgMs4rs=G^? z{h38Q*S&w#a0KF*ZRpR1$E{#EirT$wbTY}->gzLI+N;+aLGuKnK$CI|2Z4`1zoWkl zMg*1W-GGAA1P9$j2cMXwe>tGoY(l3rTfgG>i0b7RDO{zb+#_a;?OZk*&u=_3+(_F` zjb~KWt?}_G=3jBKSivPisb*8wkh5J?#3v||Kwb)xM}v165_$ak8}#gNMOv| zyJ-;slklGZn|h~ykI7t3lIDDGp@`cfD6X)~=q0Qksk$~{AJ%Nh{_mVinG#bEN|M}M zxC@>Wm2Psq%?Xq7*qB_tdA|ml=$WySw2YaBMBIBao+y{eclCMwr};%kW3lH3wN9sd z0^n-CcXbsvVzb>wpPC*uL{ipp!qy4Zv6QpBwgQ9sC4!)0MMi`cxvM;m*QOJinT8^? z(`|81JJnwymU=i>y!FfGi-g(s<7B;#h&nygrXkfTM%uyZimi|Ot>J6QY2HQ=qG;5S z#^;kYrTN2JxmXMl`~!wD?^B&Logh8~`CoOeT>Wc>?!3Kzz)T^)GWH_$8Eg?OQ$w67 zB|SU580yMn8FJIh*GiflMoXRdk8}wfk(m1ypHfDVmrTYCQ|sINjhIKh_}}&`DGji-um2Xm~j zdhnvG(0fYBrbt`)$5FwoPeUU4e_!fEf4NKlR{jw(O;|x*2<)1C_xyf%`VmvXp#QGk zWQ+k{8EJO*-rygtC_;{O_^4k^4h8{-eXk#PQ=R1VfE_xPHZ3Yf?Bp&3GEbj5~f?=a6zvtWOTwUdkNy*TL zV29Sa2npQq#;5Hu>K~55&799-WPTriUJ&E+M;4XZGkMZ#Pd-KH>&064tqI1guM31(fo60~RMXTC*T+8! zn(rUUY;>DvTe|K|-`4Uy#dx`@Q8p~*GK5%ocoQ?zU6T^xaaSzpHdxzRDlOFq!aowk z`0P3TgXY_RX276}A7TwYi^rVpQTyC78Cc5vNMl3Bf@T9Wm8F#Hef4E1E=iJ1vUuA< zlKqV^4;=e~l&BhC*uy6ENg7oc;k#jD>$F-QUP$@~0QwsVR2I zYIbWgr)5ScI%BUQw(0Dy?VLsMzxnUn@!XshU1%`FwEehH?~`-7$X;d1ES=2-G=Z18$mhFu(oDlp)dFwy7{59))5- z6kOFyR;qKUG030@&-lTk4x>^{n&eR5pNudlL>rmPdvB{Ur`^yEE91+WK{wN=WMjXj zng(}lv^`J}2ry!8+5{)iP2-P}|L}A^fZ{z2_6vACe;nS!;v< zBL=#|sg6^}LrM|@a-K#`kyd&Nt133poc07n(v$BIw4I7A3)K_Xw@q{;NS=|#l(daf zvqh^C*D!jO< zOL&#WOWc1^@ks;RKg{Xp3^0BA-QMXRZ;v-8CAooekXm%Y72s9qnR$eR-uFl-9d3|a z`1jn^>+P~(HQhUta}@8-d-Bc|3;qn;&i@G26YNkM30MN!RdH?=N1$oHOriE-bqe&; zS7-bp#LC{xTD6vFC$c(Iw`Y#q-AEw+6d3*1U;k@jXe|zH&9A_y$#vc1SJctS z2yJ>dIv@@GqLhNkWQ&-+3Qu-80#?1zHnDj1l&?}=s@+5+QUaT&n$!z49}X{vs;59tB6J*!PifI@GgDm1XOS?4L zEyeO?F2}$Vu#~9Zff8%TQ5{>HwRe;zCjM87ZCQpqZD|1}NclHn9gQ4YpYuLZEGDzr*=DiA(AtjrmAs~s898xCZ>tF1QZNfKj#g4 z+g;{ipo$!dIq)H6e-y&u7to&Vx-q#ud)nA8z@E&(9=Q)Yql${A?!LA`&f6)+vb+_R zsZ?hp z+vQ)o8U+bYeHyoLc3%*4cO`&c!hh$`2*-v>>it_XwX0}h(`GkZtbH)b>2ea;X&^-0 z)rHmMfkKXhqcv*MXte;Hn^jD<5xo7`AAt!Aks~0Pr#W}nHd&kU#b=}4BCJY|`!k`> zgYLgpgcWE-pbe;c_m7Xm0exkdH~DPtfuWDLxe9Rv{?m1=6vX)KnBm1Bew?2-yNy-a zX}KkGP0rHHCfc79?6uxYa^#<#vEmUC{g>+_(_(Np&dvr0BP6i8PcY0-#h=$;Jh3)p zR(+hzsNl~=V{-5B()Gs`G3!3=QTfUc3O%L*&>k<22ZLm|cv)EQGe1Cpf0=Dg1z%lW z_Vo|%%ppHCjd%lo2L2k?v`}p}0e{5bfbt))5vETn9JkE0HZJTh*=KKlYN&8JtVKL^hd0{bVHjFJk6>nJ(4hP?Yxace8BebF3H|v- zq?CU~Q^+HU@4&JOyQ6ZuNsX)MR_SIo4?P}#DHnBq)t3o8)SfwN#RK*_N=ka%qN7L& z27+cDuGur(eQ8f94Ys>=Uj>}=+oMW7m_pbmUpRL-`JN;7;OGgon=Np>))--R^jM%u zTRy{|V`FiOsV|W5Nzy^7%&m2G-}=UU<5=4T13H_59@v_17{BStVdM(ge1l{SI$b#% zyGDq&D*e@tqjz@f7dH=R!_f)*OkXxpDkrmlhMUl!gL?0Js0!zCC^z=ku%1LfBx5|I z{zgu#A%j_?rce*!JpM~1-6EdYsKY0HA)SdFmdV>2#6T6EpfA94q=rVF89O<(iI`5G zakM#FBc17c`+%x@K_{h@8H>J*Rj9(=TFXqJ*My#|Wf<_~Gkj*w5ZzaLHg?()<$Cdo zk}!!7I!Q@_70ac9u|9dKtrmSnQ9{Dw&*qWRrCI7@O079YO~B*i1}XEeOjSBX40>6I zC4EcSB=I9o=3}Y!5&ikcPgh7gZ-e&!i%*D{R2|nD(9VzL?@#mme5s90ufFD0iE`l+ z%tGJ=#ppq$;1cwn?%4gONhRFQLRs0vMuPI4_I)`>@#bigE^-OUGC@(%UD6?SV?PPh z6KFAo_BMg34EN`-EOb0hS7eQ)T5TG3OWhj|U29zo zLVjs!c65bIg%n$9aG8C;%~#?9YQeI|0QqX(1y~AufvnN;`0k#`1)y@T83H}}D%(-TfVbo3}tc~oJl#K?B9Df4fHvowwRfWtKK zTPK|duQ5H4T$n+xJ+KyIqrBJ^sf~_CojU=24o<1l7|?XAr6|^F%{_Xi(j1Gs&ZKBK zK2C;pabfWS{2F`shYkoWgyi_B!XZYqOLUE=GPc1IR_ zS~w%CAYG0YQu#LV@1@>oNMSk2KGp9@lfg8q;~}FrqE61BGUz~nkze|Qy{uqDW+%qJ zXhd$2^(8%COTdQO=kD0#aJg$_ht0Z)roVJBg2kK=()5h?RVQ@sP#d}wuCO@S?!74! zJ82Wyszz@eR>oxCzqj%ep~EU!iIg%&er8?ts>0FA>Xq*{y30y7J&GI8<_Mh52p++r zTcvMt{9EQSPIQe4ymgn$V}nubjof@EYdhD#CYzxY)i9jy+*6IC2bw(ir!s|@25+^}HNEoh|6>3wZ-J0*w-|Wm)XFM34`W*3_ z?3HBDk^=((kcWa5&BoFOp6d4VDK#Gjq z8AJ;OU9ZT6ABIQuFWgIf^`;ZPRBx_9R$sb|uj=evZD&GG?R=6GGZprFtNuxYLd_i- zZfAiny2Zf2lMpfHUp&(-2}l>FDM1eQJ-)bO9Fpu`#w0=;-B5v#qydPO|IubCO}b{t zdfa(QFe55-MgK^!r6qElgMfmD<(in$Zc7fAj94@?QqIW~3aNSb!}>cXG{tdee+Iso zg7Q*{UFg}DguI!yCj~thpL>_Ea1u~#3f^oWSqECQ;n@%C?i{Cnnt%I2-h89v3C~yi zFwglbKtHshSdMAEMMRU5(T7Q>&osF=Xw1jz6gOB`dD>S>OTs<7f8u7*5h|g0906Qy zUO^2k{D#vR@wL=BB(>(=?WN0v%RI-a~QLbn3=D|oJBPcG(#QHf(cXDKjA@iJyy+UjTOcJ{OkxdW({x^b>!Bv`BB0 z-PQ$YMS(AGYj>_fvu|uM{H_z_DaStWV#R=`7Rogh49QEtZ^4jC|R`Q1k$+5&7t*u#_{;-s`6R-%kT>-dA&?Og*i2-U}~$%lR@P-bN#=?k9;2&A)Ru1Z<-VeGoFV0uFcmg zr_g^elg*ZMDmjncc$xH8oDYrY`}@Gq;utJuoZyhol32~K*;F34najZ!h68*vQO0uU9Qjk29o!#xe{APg;r;R;PuQoVYGA8y5guWz?HZ2b}B&Zxqz*! z+fNTXO#+Sk*Q8d zu413|{|0y#MVf62uv?f|iJ*|C`HrDggtX@&0WyIobsQ7zr)b9Z0Mamw71}VK(;Nd0 zc29~j@~Nvuv4>`b8<3s zazus|C7ed*J|SABph;SlM5__6$+f?wGyx(Um*)sAq>U|BRpi|1P=G6aAUS~bVuUA|owxUe2TeI6JB;jsRB#n_qQiTIO&~Ndppg3vYa-M()}Ka3A;@wX>e? z3$13c%vb}oAMPD(rtB#sB@gLSulFIzBErHj4PObQA6|Vz(ZpcnvpEOp%MN6U(T-Q! zxws}uMZ%EE^_B0I|G7iu^OYuGbCE(oAXf`yUgvS9FZguKzT9X7PnB#ob|4o%V|k!# zJ}!ZGJ9rHboJK4 zjKgvs4Er2>K`$)BOhQH@@K922(u~O)neXKj$_nDc=$*d^--YG;RBA(O6#DZmUV^M# zjah#=P(%UC6|}Y8$V~_5YaF!0uXBDpIBt%B*ow45@T7<@6fz18cw9U= ziq|8huYae#bn1$?l6nj=#gI==g~c>uSgz5e_zCKpN@E<6Glf zmGYC*>xg`5)d_r{OjWTdWdKnQ!)NMMegM=T(K5CAc+|wz;0@f%Q}`hPuzZ=Z^*Y2V zr34jNFl{khEV`O<2(~!aCoi<9G`yU)-5L^QjuIM4PD6Z;IDVaTW;@ z%Kk0V9OCA!8=OdvvZCe4EUrTIt@cBx}d~#OsOO%Q>On@YyI4;a;rIuKravt1xCWeXeImgJc?xe<;EM(PmF3r-9B^0oM@7^~oRHiOxUs zOe*vBKp6}K80k%q8g~eA?3mXXBdiZdh?>Xdc#F|AQR}LZwEceDoFYY&Dd-!<`_OM9 zj$DG6jmvqFL4?v{^BCJlK~QgTi-YOX_6TAeCMl@pzbF+!L>=-CcrU5*c}j)79B`U^ zgq$4~iq7LL9i;xV*f)xt1JDT@`pu-MKy*dM)|-kjxSb!HIAP8 ztj^+()wSS9&-aE7W4R2Nb`cx{LJerMcan|_dU%~83g*sKN4uvc2Qx~Zk1dyD9`N^O zz3=Ge5#bmT7mccRza)>bWRLp_v|yrmd=Q~hV(<81;MbPsc6I9nUG$gkao0ctCN18) zNogrjMKJ&?a3;s$cf^QxtMVx!GOsuJ;4rZl3vd4f3I$GpVra;hOmp@b=_x0cOj_cb zglvgk5PG&H!KS1+8Fh5a5^KOb+-()##Nd{PCGoMS=AQ?=Yl&Y1e&fy>T!&tsbsS%E ziC3DPdMijJ$o5~ZS=U=zX5~!AqQ{XR<=zr4&qVOT3@7G>Q(Wq|MjQ0X*&3dG1;Y{7 z!V)N=WazM8kv=+07q*B<7mgxp7QSMuSs#tbdVp;4$%f;%YOO%~U10*<0juO$^EWQm1y|=yQ`U+i?w3^{TP!dm$k17sKjx z%59sg+mg5$r~4ABa1!nW4P!47Gl?4!j)YLa-iQ!5dB2%QKVt7TRI?-SBqe;w#Fi2n zy4tol7T}6Mvv?MB{6-TKkTv$3er5^Wd2xAeGMkV|K_z6decj8w#uy`lKCTgCUNF*R zHd?Ed)9oETCGpw(CdZ&oX=S>74h9Z*CLW`IBQ+&Aye2I=EeV(BgaPw4!Pa+_dcA?H zHU+hakEzspOPN&o_{8(DiC(tmVuF=Gt^wtdC^0z(UA>ORJ2&Wpgn@j;kequ2bJzjJ zs7$Fq1x$N*FfuExL;Lq&1XT9Zl|&O4MiQ4#M*M^0*)k@-pKg>-hSUykFE>ex{7@a= zF~mE|U=<0iP<4jufCtnzZW>iVs13?=*vexWnGAYFl)(9Er@7>X2;UdnvTGC*0t$b3 zW3hE;t!QivH&}>?XSfxpj?g6NH!iN|u)6JhOc6B)@rJ=l8UP>LPI`K8QKqZ#tAM*C zrF{!ijXXHhUKB~#S7#9OuP$;@D|q}VaWW%tt)&a|mctuzrus-pXC;#P%!}i~D|3cV zB*-kE%l<7B=EcM%Ga1*%$>}wQXqoK3)Tumo2N?7gwP^Zwd{Uv>OAKhI0vrhZI$H0d-UW*eA=kSu z(`|0WJM{MP#|1oVqg>>|b0MNKN=g~x!H{sAj5m*2IA5)~37+sIU6#-({gnHlvnu3*-#X!+Ffu5Wj-EPezlNvk&z$s836{gzwsr(~Wu}r2v2^zQ# zYO@=2xw-ax7X7s8wMph*7Jo7EO5ZR~fOoVl*Cg`&_}cH7`0zYwO!{j!587SmI+EvK z%cQ#H$$?#WS9T8L}qLq+ek7y zDOz(QrYS1F%FESNB&te-$p|G|k$;1bgE42Ny$PmH-xG6>?a0-#Do>g^v3*SvGa>ve z%f@bLTx683F6+T3o1)4n-7sx=PsW~$L!i@S1%l$~tzzhtDV8D27d@86ZOh@Gml?Eo ziIQ#YtV%D;mOA+ue%rME`h96rBxQ?)^PbGo>5M4^NMxR>Kde@jMsmc}Gqk8||8wW1 z{hH4+K@GxNZPi#w!UD4x9+!a2ASD#{`C~kZWD>X>)%AD@eI=*dZYl+f$TXaaB|H1PM{ zu%d2Dy3YjZ2}`_UjGT3c=XS>DT$?+5zt~inZB|RWCj?pYGv8LeGOKhv6(xEgDVbDk z%vQaIu&yavI0bd$v;#o?1lsRYfpT~mi(Bw=aO{f8Y=}8-XmN59uw)h~Ot}k9keulw z+A`IR)U8!Mu<5*V1y62OSx{VfOt>xY%JP>n&JCh$5l3S>yJLW5VV#+;3|p16Og)$~ z3eNr3RnA+%DK$%PnPDi6IYPNa0>ScEozZ>Blj zBx&81*Sy(2BsU(FO+!P__c{_x=)cjT3p~FqeCc@LhG^xioR%rFdY|e-r+Vfd9=yJM zxKQqBv|xLUnpuY-R|VPk$sqQtfQPW)ol&wm%l)D`rJLo()gv5Y;=HG=UC~@%y+(D1 zqgylc5!t4;o)Gj4gPQCw;e!n z@szaZE0Hhgr`hI8yFs>tfQqs;hx$a`A4wr+$VrO$CF3&!2;GR&a1s&<3GInRqe{pW z#*kuT$JDSjUF@a7a{h?an#1$O`#WbX3mfPg1}S@H;s+@a(#(y8E$+4eQ1E<%s_i`I zJUw>bv-w1#S$5))JOVrE2L$$viL}GdB7u-tv}w0HltSWF;i3i7WcvF?Cwlu)5Pp-> z7c-Hue;spD0qEJ%3dNHlHqtGcOwFv*Y^h`%l>8z!J4%R!Gaa&}03l{V2KM)3Y`c75 zK`bU^brwnomsrY>>1SOmf|4u-J)DnvNVPwW2i9j2^G7v4B%>R#K;bMk6&g`fqZFDyP2J3?EJ9@dQl`5Y zRFvF?`1snM^_YorHr=9B+Rh_nmQlB~Dg|tKKp{+WA}qC{@PgL!BT33eoS@sDcS8(% z!OBd-4AMDIr+h>%g}FKkIi~l8VKk;}a%}iz&3426-89fHLM-Uj2c_G4SKHWFqEsZ& z+IIQ;;BtEmNq%axTWKm+O+&c6Ia=iLd*m>5LWA=CzD&{7{*PlD@01`DaVLEIgoB2Q zo1R^Lw+>x;7;8vM8bd!JW)|?)gcOZP(j_9w6^YI{d5dJwB|kJFIUz3(*xt@(WMow1 zt0*EOq5#kC=;k)zR<9oc#dka19&53&vtL|Y6?xwrwqCXQJ#j*ldxKt{RLDsK^#y#A zlCr|HD?U|d)C{c*@0Ga3L`7}Bs9vz$Gynr3p<-PlILyz(D(Fcz5^Pr+~A|oiQRl^QcKa zUfx-#TH&gR%kDVTazJ4CXF_G@mfqs4ZKdWz*)*Yiu{pluJWZX_sKHCP?~u!h%e*r- z3f*uA&futl;pDiOsVS+8i>UJUQ$&|~C5(CmR2aY`f}581phS)8KU|y|Q49K%B>fay zg&pm3>eYme)2kC*C4;zpH}8100CkOc-6d}E#6ubsB5|<V^kWjKi{U@AzAZ3r>+5?l%H*-wEHGeTVEn7a2>OU; zC#b#68(TYCeTf$voTdJ)xvG3oOHUHk@sH8*^t_#QS5;6H;(vRwP~a8da)@jp-^4ls zv3QcdZ}Inu)So}`NPzIaKKA$%`e~<6XL+ctF_1NlODpSpn}WronWLM3&u}g{rH62# zzv2g_s)T|^{{8f0?qIsJEO$9p;)_HPE3`e>*Y+N|r@Y@dPj9@E*b*BZ$b^a)O6@3R zC)CT90ag?%1E?z)>mv^^P~rdw{d12Z7JJaeLdebFs`nrl=e0`g@1w<1FJ zIE8*8X)kJE#h^pC*H^CuHX{nCA`lh|aOtH>c31431~!=a>wdu9Gp^O|UuyPTHS39E zJDxs1Z@4a=T=(}|@k$lHy%>jt6qJ=Qd~S~mpg!%zI4Y!|s)_|ARP|xrd-ZX>m!EvG zicT}}$X6+-r58t9>U{j{6{%ifHj(ue3PQYyi)%fZH19a>`1x1wATI8Kv&HoQL%Zs> zbN(qpNeNG$%KQ2_Y;`9f;9@U($dcpoX3+p7SoZPdX#gr?=Jj}O)wS>x8;h*0to&;B zmMY|C=VuZbkRx{?3vaQpbZU|K&?ktyF=KZ{$i3cou{9i4R8$00ne7I$Uzljq;yN<_ zQ>i1kyxe3*j||sOc%^VfD#9=ffuL{ob2J2&98@D#EkuHvSD;smh>3lGlG&WuG3x&} z)%h=m7A;XE%oN}P5QDx4bqp+BWXJ{lDpzp!dltd%R})IIDuV^Hy>)>%R1SiPm5jju z{gy(bZ@yy048M|<%C&6hh5;Kr0;JC<6|53*Px9qRaM+!AuYfMaJJIaiH{bW* zm^x?0C*w%#<4+>3WSFN892y4}!S;cTcBc7X&%^?1Qk}yWA=I9vp9&TFkv#YAkbHMd z3l)X>I*sv8nJGf>qq;lt`!$rEKG#U*eqZc3kh)f9O(ycol03yme7f2Oow?W!^K3dV z;KG`{;fn_(`%1) z$i?f;sJa-63#vgWsAlUCI|ke3_?vE{4lDF={@{)47r0bd;8NgJoIKe%zhOX92Nv&s z0?Omu#h+~7i((htkkei{Ecw22CC-P$o^nK4cGF?SXG@O($6D{Yu<}?WuJq1=o*f0# zj*bLNWqzX*O^(|Xyc#J_)op|Hk#(H1-wTJW5n3#H+F<)VGE>vG;A|YkSk76aXk6N9 z7yuSM7>v!krL3=YC;s5JwGgy+)$$2+t@h1y#vEuc=Afzzk_!EK@4DzwYiTY-ZgXvV z6O%mr>`K2br#BvrJ@#M(VN0`X`=;!V6trIrGMSNk6kqtUci7&+;-i9W^K@qtSHR#V zQ^MNq=aaTJp{VhC=K6GIOa5jU+dY1IBUIV>k$e&p8RIJqL5Ff}4MeC!nm#ZN^ z{$d{KYXSFYVk{*k&H6kLbH~|V!aR7d2KJ(u7YfrSBE%qWjBYdbD&kKN3y$^c0!lPpKRCRTppb zaZl|oc-vm;QK<|J$#0``ZpQ*P+r8+QKqgp%?P;~IuOu_y=BmEhD$SGkpnD zJaXJM@NcF+VSJxQ)Zmg#z%8}OH|K^i-0VFpq4;_r{_{D7mcpgXdkaAq#%0rrX|(;^ zOCw(PbH9nl7Vxlyn{TxQPdK<)CU&^bAq16|79?smNc zPnv08j%!--;%p-Q)tugi5LuXCJIPFggAM=XT`n`O5!NctHM?X{&E?8ARmxAURCX;d z#@|9A<1etg)s~8@#ROQ0L_J#z`7=v=-TL=;^36L~cIy$lX$G((ey}u$+tDj$-EiZq z&-H>arMc1Mk_l7K8@SSt;ZsTZgO$g(9fYz|lhXpyN6x1sIP)`xQcZV1fgHghdJ(MU1h$2dsp-l#tWR*3J5= zex{}hXwPDn%{SA$!nuC9MVOK!PGQ(dE+c=_cs}tpfM-YSmNyAG<1qi~=4E5t1<<$K z3kYl5-DAiX3Q{t$sB>TBKU{kNio?4)=xuU=*C7w7u^-%7cba+ z#SYOEaK%!sGsExX#p&+fEbgV<+q!Nn@`oRj5*-S+z_$0k10M`@rlWUF&o}2Z1K`W+OTc)!c&z6d@-6 z9bbc>*GE*jHI4A7qKaL91>cB!kK<>Dgx1ue%5MD5jg!a+_j@0{H@>6=dDFZ$zM|~3 zg54gHC4Q#BkZj;9EL%72zZPt;e*@G!d|AplC!FKFXR}jR(S=dI1#LH=-n=A5`Q^ke z6jKW@g;RsirhjcN)_Vyo24swPv0>ut-3-z@c-JN&`tGxod9014fiTIwY=&m6=mCod2*(~V4n4rhKhT;)d;!uW5YTX_HqYB7`H4dYrxsaM!Lr*Y{x_g6nsHNdNa5ra0y7Ye@m6`_(bvYoLJ6f*vRew z5X{~3jHv_`r}g!*e>3ULfDTB7gR?>TQD5&~rw@uxio~|s+{Bdo)eSpb&SoTHU2eN9 z_L8E~02bDKdWk81+pnlY=BzizIh9e!ep@UydUyRz<|@#GM}A-J>T9y2JNuCa5jmB_ zP;`mTw5zQ|O9iqr3d^rS$?^6o!@*Lkr52nr?KvPao_ZRF7e{tLdUvx63w4g!)gv}h zbd+$DF|7^P1+CI!t;LNj4HU2YdH5G*x2Xw6{r?j8{ZPzP;+_H`SVJc?MXedo7O7m z(slZ+90?(NFlWDWe%U>v0wNC;ACp6UFuUe5b$tuh7vI6dENZ}I9qUPB<6sy_rH7*>kCWS?E|!^{I{O$ zOJO=h&dl6pCa0bQf0NxHstlk3aC-||0Sqy+FFtIoI`C9L-QccE6V5?56$E(Xr!aMHdddu!}Hh0OyU+V8PG_s?WZc5%e_2JAPc+Lmh)GgN?ZQ;>7dz(iNY^T{ubRQauj-H=ptk0-|msNtoy!4vfJ;w`eln;or-c z`N?5V5K^xV%O|IG#O{Q3{@9FVhx=7GOntS0hk*wxDsGJDsoaS)RqmfB0za{?a*(dK$76AH2Ys!sj%wTgobkH*o&(W4xhb8tV)s z@-T5rL zrcT!Qr-}LM7bem^9QS+dY#pdIr#DS3UD^Nk14cp=9~2;*15SUx!QB5H>%3|$tW2sm zih&)5loRDM}6N916;agcjAUZ*u?6|8gRwf?iAhn;Q)iJ=5Bbcuyg}X9IV=Kc3dm?v+SkQEl@t z*ocAGo*5W9LPQi96&ZUC&y@G_elb~T;Mpg^=Kwi;@=58oJY+i>*X!Axn!L^Cd6a^j zZPq@edo_@{11I`L=b-OO1{72Oyb5~D) zK^Ev;9iJlTVm91-?8L<2HlRkD(%B$wNFf5dUy+gCaf7Ah7}E+4zvr#cL+^p~?;fHr zZU_A4#Ju5IA%ZYHRp%7sPxR+YjSh}G9_hG}Vy0;ST0}yIjMEF-{WojM&bz?4{OZY# og+7n_=OIBN8do1bF|plyLQOz6R!NDM3JkPJiz|p#ix>v~KV#6RLjV8( literal 0 HcmV?d00001 diff --git a/en/img/cache_and_beer.png b/en/img/cache_and_beer.png new file mode 100644 index 0000000000000000000000000000000000000000..27f3e63d5ca4544047ba16a3b1ba0034c8f1a94d GIT binary patch literal 40846 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 N#TTk_B{C)-{sl{iF5dtE literal 0 HcmV?d00001 diff --git a/en/img/eytzinger.png b/en/img/eytzinger.png new file mode 100644 index 0000000000000000000000000000000000000000..97237c734cf59c1deda7cc16192b6f432c3afaf0 GIT binary patch literal 28730 zcmb4rWl)w~*ER}>ba#VvcXv0^-AE(dAxJj}0@B?jsdR&MNK1Ej$Gf>_p6~zXj3Zpv z89UcnM{L8D6{Qg2@ZcaIAP{Aw#Z@36-c*6VsPEr_Pln5=p};>-&Z07E@4+9R_hw<> zcU%_|#pIqFHoy;LD>_6L?GdP<%nVZ`?TYh#qfovCqfFOa85f@SONI(2%p@pG| z|8CXdO#zK8Qv?Y~7#T_6dsh<2&>j*Sjz*RV@-4WQ}K5}QRGZXN;84{fVxHxLq z`2Xu>j1PogjQ44Cl*6!@3|6;>lBsEF19Nl9?=#PAg&UVzEeA??P*ruytiAoxH;NwD zH`Ny?A;)bMYrN0;`eMA6mKKNeL+E0+)IO``QvIT}+mc$3)73XcN38Pt-HiCG@*Ldm zM*%64gyRE~Nf!;ynJd8(BFrtV$4<+f-P4$0qUA9q#Kk+>RIx2%g_QkZ5DmoY+}XQH z6iA&sWtk_S-VzD+uP{Y^TuzR;rH~e2g%x>@HL|xK_4ErVfuA!M3n|I{R-`C0(V`nH z03S_YcNXc$>9!o!v2D|-wNKTdX;?2oymh{~L&N?h!P7mR*Y!vDG=gg5>ylF6+&Y4R z{x`G5)T1hDA?J#bzeRSiGF86RW81@}7A%8q5PXu8Z5r?(7|Sg^#O#U=7MiLAX{U(t zbj7NO**bZd@cRw7zTLl z*TsK=mOFDiS(8I}7$s;|{2XLYdNQHRKzyJiPOhhX=1pB}O~S{P@vQ|x6}yIwDWrs! z?2G1EL3>xe7JYL6dQnxVHvP(Y0pm=+7OO-c1T?XVuf%fO(LAga#rw8l7 z?l~ z99%mbG_7{tg@V4?R_3%e+~9;?4^+l+K`MeIs_p7!#xz<1?J3l#&{>9F-S@pz2;T6oQ~C*4EbMfX_;BPN|>0&yD2-he+328D|*P z(l15ovG?^rld)@XP$o(SxA54=0xU_f(b3VcnD?R1Y$O|-^exCO??sI5e;vGsb3PS3 zC1X__72Vl(3Jdocg#+Ds!*^kVgLHRyXZ%!H(d4<=&#gn#uU{fcN=lQCxRR8wQ_qo@nTZ5G@bL0lUvBl5R#1>=wEwrdzYnLW zsaa=rvGR3zZEY=g3q+IbygYK9O7ENF@#%8QQp^xzh}Sst=Vxv0+SV2Yt*E1;=PM4lA#+}xa6X<4%}wYaqO`{RGjFt0E1_U@jW!x8Yh{8Cm%r>Cbko-fn;^_ErM zw+F;lH4-K!eVdExOF@d@>(FVx=s>-d67$hm&Oie!oYY z3f)F5%-%}E==;I9lOIP?ABDL%Qy#qYr^<+7E<#i7j2&xbRf@bL2H8h8J353Q

          &1lU?y;lKpM%(?szw1ybq(%lHw&rjX}oDtgHs* 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 Date: Tue, 31 Mar 2020 10:26:10 +0000 Subject: [PATCH 213/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/eytzinger.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/en/eytzinger.html b/en/eytzinger.html index 4ba130d..e171aae 100644 --- a/en/eytzinger.html +++ b/en/eytzinger.html @@ -114,7 +114,7 @@

          Eytzinger Binary Search

          -

          This tutorial is loosely based on a [46-page paper](https://arxiv.org/pdf/1509.05053.pdf](https://arxiv.org/pdf/1509.05053.pdf) by Paul-Virak Khuong and Pat Morin “Array layouts for comparison-based searching” and describes one particular way of performing efficient binary search by rearranging elements of a sorted array in a cache-friendly way.

          +

          This tutorial is loosely based on a 46-page paper by Paul-Virak Khuong and Pat Morin “Array layouts for comparison-based searching” and describes one particular way of performing efficient binary search by rearranging elements of a sorted array in a cache-friendly way.

          We briefly review relevant concepts in processor architecture; if you want to get deeper, we recommend reading the original 2015 paper, as well as these articles:

          • Modern Microprocessors: a 90-minute guide by Jason Patterson

          • @@ -207,12 +207,12 @@

            Construction

            Binary search implementation

            We can now descend this array using only indices: we just start with \(k=1\) and execute \(k := 2k\) if we need to go left and \(k := 2k + 1\) if we need to go right. We don’t even need to store and recalculate binary search boundaries, which is another selling point.

            The only problem arises when we need to restore the index of the resulting element, as \(k\) may end up not pointing to a leaf node. Here is an example of how that can happen:

            -
                array:  1 2 3 4 5 6 7 8
            -eytzinger:  4 2 5 1 6 3 7 8
            -1st range:  ---------------  k := 1
            -2nd range:  -------          k := 2*k      (=2)
            -3rd range:      ---          k := 2*k + 1  (=5)
            -4th range:        -          k := 2*k + 1  (=11)
            +

            Here we query array of \([1, …, 8]\) for the lower bound of \(x=4\). We compare it againts \(4\), \(2\) and \(5\), and go left-right-right and end up with \(k = 11\), which isn’t even a valid array index.

            Note that, unless the answer is the last element of the array, we compare \(x\) against it at some point, and after we learn that it is not less than \(x\), we start comparing \(x\) against elements to the left, and all these comparisons will evaluate true (i. e. leading to the right). Hence, the solution to restoring the resulting element is to cancel some number of right turns.

            This can be done in an elegant way by observing that the right turns are recorded in the binary notation of \(k\) as 1-bits, and so we just need to find the number of trailing ones in the binary notation and right-shift \(k\) by exactly that amount.

            @@ -252,7 +252,7 @@

            Memory allignment

            The way to achieve this is to place the first element of the array to the 1st position (0-indexed) of a cache line, or placing the array itself on the beginning of a cache line, since its first (i. e. b[0]) element is blank by design. This way the next \(1 + 2 + 4 + 8 = 15\) elements of first 4 layers will occupy the rest of the cache line, and the rest of the array is alligned in nice 16-element blocks of nodes who share a grandpa.

            We just need to ask memory manager to allocate our array on the beginning of a cache line (by default it allocates your arrays wherever it wants), and that’s it. To do this, we can use alignas specifier:

            -

            This is it. Now our algorithm is constantly prefetching 4 layers / cache lines ahead of time, which is covered by the bandwith of our RAM. This way the effective latency is reduced by a factor of 4. We are basically trading off bandwidth for latency.

            +

            This is it. Now our algorithm is constantly prefetching 4 layers / cache lines ahead of time, which is covered by the bandwith of our RAM. This way the effective latency is reduced by a factor of 4, and we’re basically trading off bandwidth for latency.

            Complete implementation

            #pragma GCC optimize("O3")
             #include <bits/stdc++.h>
            
            From 45efbc14f7346b2688494721a48ca3cfcc9a2ad9 Mon Sep 17 00:00:00 2001
            From: "Deployment Bot (from Travis CI)" 
            Date: Tue, 31 Mar 2020 11:17:33 +0000
            Subject: [PATCH 214/230] Deploy algorithmica.org to
             github.com/algorithmica-org/articles.git:gh-pages
            
            ---
             en/eytzinger.html | 18 +++++++++---------
             1 file changed, 9 insertions(+), 9 deletions(-)
            
            diff --git a/en/eytzinger.html b/en/eytzinger.html
            index e171aae..182e9a3 100644
            --- a/en/eytzinger.html
            +++ b/en/eytzinger.html
            @@ -161,13 +161,13 @@ 

            How caching works

            • Cache hierarchy is a memory architecture which uses a hierarchy of memory stores based on varying access speeds to cache data. Adjacent cache layers usually differ in size by a factor of 8 to 10 and in latency by a factor of 3 to 5. Most modern CPUs have 3 layers of cache (called L1, L2 and L3 from fastest / smallest to slowest / largest) with largest being a few megabytes large.

            • Cache line is the unit of data transfer between CPU and main memory. The cache line of your PC is most likely 64 bytes, meaning that the main memory is divided into blocks of 64 bytes, and whenever you request a byte, you are also fetching its cache line neighbours regardless whether your want it or not. Fetching a cache line is like grabbing a 6-pack.

            • -
            • Eviction policy is the method for deciding which data to retain in the cache. In CPUs, it is controlled by hardware, not software. For simplicity, programmer can assume that least recently used (LRU) policy is used, which just evicts the item that hasn’t been used for the longest amount of time. This is like prefering beer with later expiration dates.

            • +
            • Eviction policy is the method for deciding which data to retain in the cache. In CPUs, it is controlled by hardware, not software. For simplicity, programmer can assume that least recently used (LRU) policy is used, which just evicts the item that hasn’t been used for the longest amount of time. This is like preferring beer with later expiration dates.

            • Bandwidth is the rate at which data can be read or stored. For the purpose of designing algorithms, a more important characteristic is the bandwidth-latency product which basically tells how many cache lines you can request while waiting for the first one without queueing up. It is around 5 or more on most systems. This is like having friends whom you can send for beers asynchronously.

            • Temporal locality is an access pattern where if at one point a particular item is requested, it is likely that this same location will be requested again in the near future. This is like fetching the same type of beer over and over again.

            • Spacial locality is an access pattern where if a memory location is requested, it is likely that a nearby memory locations will be requested again in the near future. This is like storing the kinds of beer that you like on the same shelf.

            -

            The main problem with binary search over a sorted array is that its memory accesses pattern is neither temporaly nor spacially local. For example, element \(\lfloor \frac n 2 \rfloor\) is accessed very often (each search) and element \(\lfloor \frac n 2 \rfloor + 1\) is not, while they are probably occupying the same cache line.

            -

            We can overcome this by enumerating and permuting array elements in a more cache-friendly way, and chances are you already know this numeration.

            +

            The main problem with binary search over a sorted array is that its memory accesses pattern is neither temporary nor spacially local. For example, element \(\lfloor \frac n 2 \rfloor\) is accessed very often (each search) and element \(\lfloor \frac n 2 \rfloor + 1\) is not, while they are probably occupying the same cache line.

            +

            We can overcome this by enumerating and permuting array elements in a more cache-friendly way. The numeration we will use is actually half a millenium old, and chances are you already know it.

            The Eytzinger layout

            Michaël Eytzinger is a 16th century Austrian nobleman known for his work on genealogy, particularily for a system for numbering ancestors called ahnentafel (German for “ancestor table”).

            Ancestry mattered a lot back then, but writing down that data was expensive. Ahnentafel allows displaying a person’s genealogy compactly, without wasting extra space by drawing diagrams.

            @@ -213,10 +213,10 @@

            Binary search implementation

            2nd range: ------- k := 2*k (=2) 3rd range: --- k := 2*k + 1 (=5) 4th range: - k := 2*k + 1 (=11)
            -

            Here we query array of \([1, …, 8]\) for the lower bound of \(x=4\). We compare it againts \(4\), \(2\) and \(5\), and go left-right-right and end up with \(k = 11\), which isn’t even a valid array index.

            +

            Here we query array of \([1, …, 8]\) for the lower bound of \(x=4\). We compare it against \(4\), \(2\) and \(5\), and go left-right-right and end up with \(k = 11\), which isn’t even a valid array index.

            Note that, unless the answer is the last element of the array, we compare \(x\) against it at some point, and after we learn that it is not less than \(x\), we start comparing \(x\) against elements to the left, and all these comparisons will evaluate true (i. e. leading to the right). Hence, the solution to restoring the resulting element is to cancel some number of right turns.

            This can be done in an elegant way by observing that the right turns are recorded in the binary notation of \(k\) as 1-bits, and so we just need to find the number of trailing ones in the binary notation and right-shift \(k\) by exactly that amount.

            -

            To do this we can invert the number (~x) and call “find first set” instruction available on most systems. In GCC, the correspolding builtin is __builtin_ffs.

            +

            To do this we can invert the number (~x) and call “find first set” instruction available on most systems. In GCC, the corresponding builtin is __builtin_ffs.

            -

            Note that \(k\) will be zero if binary search returned no result (i. e. all elements are less than \(x\) and all turns were right-turns that got cancelled). In that case, you can put a special flag in the first element of b.

            +

            Note that \(k\) will be zero if binary search returned no result (i. e. all elements are less than \(x\) and all turns were right-turns that got canceled). In that case, you can put a special flag in the first element of b.

            This is already 2-3 times faster than std::lower_bound, but we are not going to stop there and apply a series of small incremental improvements.

            Branch-free

            Compiled program instructions are stored and loaded from main memory too, just as normal data. They are fetched during execution by similar mechanisms, and they have a separate instruction cache. In fact, in large applications you can sometimes remove blocks of literally unused code, and the program may run faster because of better instruction cache hit rate, but this is a topic for another article.

            @@ -249,10 +249,10 @@

            Prefetching

            The whole point of doing this is that there is a good chance that we will prefetch an element that we will use later on \((i+4)\)-th iteration. What chance, exactly? Well, it turns out that it is constant for each iteration.

            Memory allignment

            Note that for each layer in the tree, except for the first 4 and possibly the last one, the number of nodes in that layer is divisible by 16, the block size. This means that the fraction of covered nodes on each iteration depends only on the position of the first offset of the array in respect to its cache line. But what is more important is that it can be made that all of \(k\)’s grand-grandchildren are covered by the same cache line.

            -

            The way to achieve this is to place the first element of the array to the 1st position (0-indexed) of a cache line, or placing the array itself on the beginning of a cache line, since its first (i. e. b[0]) element is blank by design. This way the next \(1 + 2 + 4 + 8 = 15\) elements of first 4 layers will occupy the rest of the cache line, and the rest of the array is alligned in nice 16-element blocks of nodes who share a grandpa.

            +

            The way to achieve this is to place the first element of the array to the 1st position (0-indexed) of a cache line, or placing the array itself on the beginning of a cache line, since its first (i. e. b[0]) element is blank by design. This way the next \(1 + 2 + 4 + 8 = 15\) elements of first 4 layers will occupy the rest of the cache line, and the rest of the array is alligned in nice 16-element blocks of nodes that share a grandpa.

            We just need to ask memory manager to allocate our array on the beginning of a cache line (by default it allocates your arrays wherever it wants), and that’s it. To do this, we can use alignas specifier:

            -

            This is it. Now our algorithm is constantly prefetching 4 layers / cache lines ahead of time, which is covered by the bandwith of our RAM. This way the effective latency is reduced by a factor of 4, and we’re basically trading off bandwidth for latency.

            +

            This is it. Now our algorithm is constantly prefetching 4 layers / cache lines ahead of time, which is covered by the bandwidth of our RAM. This way the effective latency is reduced by a factor of 4, and we’re basically trading off bandwidth for latency.

            Complete implementation

            #pragma GCC optimize("O3")
             #include <bits/stdc++.h>
            @@ -291,7 +291,7 @@ 

            Complete implementation

          What about B-trees?

          B-trees are basically \((k+1)\)-ary trees, meaning that they store \(k\) elements in each node and choose between \((k+1)\) possible branches instead of 2.

          -

          They are widely used for indexing in databases, especially those that operate on-disk, because if \(k\) is big, this allows large sequencial memory accesses while reducing the height of the tree.

          +

          They are widely used for indexing in databases, especially those that operate on-disk, because if \(k\) is big, this allows large sequential memory accesses while reducing the height of the tree.

          To do static binary searches, one can implement a B-tree in an implicit way, i. e. without actually storing any pointers and spending only \(O(1)\) additional memory, and \(k\) could be made equal to the cache line size so that each node request fetches exactly one cache line.

          Turns out, they have the same rate of growth but sligtly larger compute-tied constant. While the latter is explainable (our while loop only has like 5 instructions; can’t outpace that), the former is surprising.

          From 5cee0755542ea66a00a0ffb2a6196563fc46a1bc Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 2 Apr 2020 10:50:29 +0000 Subject: [PATCH 215/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/b-tree.html | 270 ++++++++++++++++++++++++++++++++++++++++++++++ en/eytzinger.html | 11 +- 2 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 en/b-tree.html diff --git a/en/b-tree.html b/en/b-tree.html new file mode 100644 index 0000000..10731a2 --- /dev/null +++ b/en/b-tree.html @@ -0,0 +1,270 @@ + + + + + + + Implicit Static B-trees - Algorithmica + + + + + + + + + + + + + + + + +

          Implicit Static B-trees

          +

          This is a follow up on a previous article about using Eytzinger memory layout to speed up binary search. Here we use implicit (pointerless) B-trees accelerated with SIMD operations to perform search efficiently while using less memory bandwidth.

          +

          It performs slightly worse on array sizes that fit lower layers of cache, but in low-bandwidth environments it can be up to 3x faster (or 7x faster than std::lower_bound).

          +

          B-tree layout

          +

          B-trees generalize the concept of binary search trees by allowing nodes to have more than two children.

          +

          Instead of single key, a B-tree node contains up to \(B\) sorted keys may have up to \((B+1)\) children, thus reducing the tree heigh in \(\frac{\log_2 n}{\log_B n} = \frac{\log B}{\log 2} = \log_2 B\) times.

          +

          They were primarily developed for the purpose of managing on-disk databases, as their random access times are almost the same as reading 1MB of data sequentially, which makes the trade-off between number of comparisons and tree height beneficial. In our implementation, we will make each the size of each block equal to the cache line size, which in case of int is 16 elements.

          +

          Normally, a B-tree node also stores \((B+1)\) pointers to its children, but we will only store keys and rely on pointer arithmetic, similar to the one used in Eytzinger array:

          +
            +
          • The root node is numbered \(0\).

          • +
          • Node \(k\) has \((B+1)\) child nodes numbered \(\{k \cdot (B+1) + i\}\) for \(i \in [1, B]\).

          • +
          +

          Keys are stored in a 2d array in non-decreasing order. If the length of the initial array is not a multiple of \(B\), the last block is padded with the largest value if its data type.

          + +

          In the code, we use zero-indexation for child nodes.

          +

          Construction

          +

          We can construct B-tree similarly by traversing the search tree.

          + +

          It is correct, because each value of initial array will be copied to a unique position in the resulting array, and the tree height is \(\Theta(\log_{B+1} n)\), because \(k\) is multiplied by \((B + 1)\) each time a child node is created.

          +

          Note that this approach causes a slight imbalance: “lefter” children may have larger respective ranges.

          + +

          Here is a short but rather inefficient implementation that we will improve later.

          + +

          The issue here is that it runs a linear search on the whole array, and also that it has lots of conditionals that costs much more than just comparing integers.

          +

          Here are some ideas to counter this:

          +
            +
          • We could unroll the loop so that it performs \(B\) comparisons unconditionally and computes index of the right child node.

          • +
          • We could run a tiny binary search to get the right index, but there is considerable overhead to this.

          • +
          • We could code all the binary search comparisons by hand, or force compiler to do it so that there is no overhead.

          • +
          +

          But we’ll pick another path. We will honestly do all the comparisons, but in a very efficient way.

          +

          SIMD

          +

          Back in the 90s, computer engineers discovered that you can get more bang for a buck by adding circuits that do more useful work per cycle than just trying to increase CPU clock rate which can’t continue forever.

          +

          This worked particularly well for parallelizable workloads like video game graphics where just you need to perform the same operation over some array of data. This this is how the concept of SIMD became a thing, which stands for single instruction, multiple data.

          +

          Modern hardware can do lots of stuff under this paradigm, leveraging data-level parallelism. For example, the simplest thing you can do on modern Intel CPUs is to:

          +
            +
          1. load 256-bit block of ints (which is \(\frac{256}{32} = 8\) ints),

          2. +
          3. load another 256-bit block of ints,

          4. +
          5. add them together,

          6. +
          7. write the result somewhere else

          8. +
          +

          …and this whole transaction costs the same as loading and adding just two ints—which means we can do 8 times more work. Magic!

          +

          So, as we promised before, we will perform all \(16\) comparisons to compute the index of the right child node, but we leverage SIMD instructions to do it efficiently. Just to clarify—we want to do something like this:

          + +

          …but ~8 times faster.

          +

          The algorithm:

          +
            +
          1. Somewhere before the main loop, convert \(x\) to a vector of \(8\) copies of \(x\).

          2. +
          3. Load the keys stored in node into another 256-bit vector.

          4. +
          5. Compare these two vectors. This returns a 256-bit mask in which pairs that compared “greater than” are marked with ones.

          6. +
          7. Create a 8-bit mask out of that and return it. Then you can feed it to __builtin_ffs.

          8. +
          +

          This is how it looks using C++ intrinsics, which are basically built-in wrappers for raw assembly instructions:

          + +

          After that, we call this function two times (because our node size / cache line happens to be 512 bits, which is twice as big) and blend these masks together with bitwise operations.

          +

          Complete implementaiton

          + +

          That’s it. This implementation should outperform even the state-of-the-art indexes used in high-performance databases, though it’s mostly due to the fact that data structures used in real databases have to support fast updates while we don’t.

          +

          Note that this implementation is very specific to the architecture. Older CPUs and CPUs on mobile devices don’t have 256-bit wide registers and will crash (but they likely have 128-bit SIMD so the loop can still be split in 4 parts instead of 2), non-Intel CPUs have their own instruction sets for SIMD, and some computers even have different cache line size.

          +

          Also, it may take some work to get it working with other data types or different comparators (you’ll have to implement custom comparison using SIMD instructions), but in most cases this should be totally doable.

          + + diff --git a/en/eytzinger.html b/en/eytzinger.html index 182e9a3..f5beff6 100644 --- a/en/eytzinger.html +++ b/en/eytzinger.html @@ -166,7 +166,7 @@

          How caching works

        26. Temporal locality is an access pattern where if at one point a particular item is requested, it is likely that this same location will be requested again in the near future. This is like fetching the same type of beer over and over again.

        27. Spacial locality is an access pattern where if a memory location is requested, it is likely that a nearby memory locations will be requested again in the near future. This is like storing the kinds of beer that you like on the same shelf.

        28. -

          The main problem with binary search over a sorted array is that its memory accesses pattern is neither temporary nor spacially local. For example, element \(\lfloor \frac n 2 \rfloor\) is accessed very often (each search) and element \(\lfloor \frac n 2 \rfloor + 1\) is not, while they are probably occupying the same cache line.

          +

          The main problem with binary search over a sorted array is that its memory accesses pattern is neither temporary nor spacially local. For example, element \(\lfloor \frac n 2 \rfloor\) is accessed very often (every search) and element \(\lfloor \frac n 2 \rfloor + 1\) is not, while they are probably occupying the same cache line. In general, only the first 3-5 reads are temporary local and only the last 3-4 reads are spacially local, and the rest are just random memory accesses.

          We can overcome this by enumerating and permuting array elements in a more cache-friendly way. The numeration we will use is actually half a millenium old, and chances are you already know it.

          The Eytzinger layout

          Michaël Eytzinger is a 16th century Austrian nobleman known for his work on genealogy, particularily for a system for numbering ancestors called ahnentafel (German for “ancestor table”).

          @@ -187,7 +187,7 @@

          The Eytzinger layout

          In computer science, this enumeration has been widely used for implicit (i. e. pointer-free) implementation of heaps, segment trees, and other binary tree structures, where instead of names it stores underlying array items.

          This is how this layout will look when applied to binary search:

          -

          You can immediately see how it’s temporal locality is better (in fact, theoretically optimal) as the elements closer to the root are closer to the beginning of the array, and thus are more likely to be fetched from cache.

          +

          You can immediately see how its temporal locality is better (in fact, theoretically optimal) as the elements closer to the root are closer to the beginning of the array, and thus are more likely to be fetched from cache.

          Construction

          Here is a function that constructs Eytzinger array by traversing the original search tree.

          It takes two indexes \(i\) and \(k\)—one in the original array and one in constructed—and recursively goes to two branches until a leaf node is reached, which could simply be checked by asserting \(k \leq n\) as Eytzinger array should have same number of items.

          @@ -217,7 +217,7 @@

          Binary search implementation

          Note that, unless the answer is the last element of the array, we compare \(x\) against it at some point, and after we learn that it is not less than \(x\), we start comparing \(x\) against elements to the left, and all these comparisons will evaluate true (i. e. leading to the right). Hence, the solution to restoring the resulting element is to cancel some number of right turns.

          This can be done in an elegant way by observing that the right turns are recorded in the binary notation of \(k\) as 1-bits, and so we just need to find the number of trailing ones in the binary notation and right-shift \(k\) by exactly that amount.

          To do this we can invert the number (~x) and call “find first set” instruction available on most systems. In GCC, the corresponding builtin is __builtin_ffs.

          -
          int search(int x) 
          +
          int search(int x) {
               int k = 1;
               while (k <= n) {
                   if (b[k] >= x)
          @@ -237,8 +237,9 @@ 

          Branch-free

          In case of binary search, if all of our data is random, branch prediction doesn’t help at all, just because it can’t: all comparisons are 50-50. This is why we need to get rid of if-s and rewrite our main loop the following way:

          +

          It also to saves us from executing a few arithmetic instructions directly.

          Prefetching

          -

          Compiler doesn’t like when CPU is sitting idle while waiting for memory fetches. Sometimes it can take a guess which cache line is going to be needed soon and fetch it ahead of time (recall that bandwidth-latency product is usually much larger than 1).

          +

          Compiler doesn’t like when CPU is sitting idle while waiting for memory fetches. Sometimes it can take a guess about which cache line is going to be needed soon and fetch it ahead of time (recall that bandwidth-latency product is usually much larger than 1).

          This works well for simple access patterns, like iterating over array in increasing or decreasing order, but for something complex like what we have here it’s not going to perform well.

          As we know a bit more about our problem than the compiler does, we can tell it to explicitly prefetch a cache line we need. This is done by __builtin_prefetch in GCC:

          while (k <= n) {
          @@ -292,7 +293,7 @@ 

          Complete implementation

          What about B-trees?

          B-trees are basically \((k+1)\)-ary trees, meaning that they store \(k\) elements in each node and choose between \((k+1)\) possible branches instead of 2.

          They are widely used for indexing in databases, especially those that operate on-disk, because if \(k\) is big, this allows large sequential memory accesses while reducing the height of the tree.

          -

          To do static binary searches, one can implement a B-tree in an implicit way, i. e. without actually storing any pointers and spending only \(O(1)\) additional memory, and \(k\) could be made equal to the cache line size so that each node request fetches exactly one cache line.

          +

          To perform static binary searches, one can implement a B-tree in an implicit way, i. e. without actually storing any pointers and spending only \(O(1)\) additional memory, and \(k\) could be made equal to the cache line size so that each node request fetches exactly one cache line.

          Turns out, they have the same rate of growth but sligtly larger compute-tied constant. While the latter is explainable (our while loop only has like 5 instructions; can’t outpace that), the former is surprising.

          Let’s assume that arithmetic costs nothing and do simple cache block analysis:

          From c2369ed797ef640a6d93492c7b5f8e41aef1346e Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 2 Apr 2020 11:33:52 +0000 Subject: [PATCH 216/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/b-tree.html | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/en/b-tree.html b/en/b-tree.html index 10731a2..f2f35ff 100644 --- a/en/b-tree.html +++ b/en/b-tree.html @@ -185,7 +185,7 @@

          SIMD

          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 mss5*tqrHUBrvGVHB7*Ih$Ku|=9A`&F$EVM_0 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 j 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 literal 0 HcmV?d00001 diff --git a/img/safe-edge.png b/img/safe-edge.png new file mode 100644 index 0000000000000000000000000000000000000000..465e0a1ad478df3b4ae4143e4e7f2de8b718c02c GIT binary patch literal 8617 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%0A^Dh+em2}Cjyn`A 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

        …and this whole transaction costs the same as loading and adding just two ints—which means we can do 8 times more work. Magic!

        So, as we promised before, we will perform all \(16\) comparisons to compute the index of the right child node, but we leverage SIMD instructions to do it efficiently. Just to clarify—we want to do something like this:

        -
        int mask = 0;
        +
         

        That’s it. This implementation should outperform even the state-of-the-art indexes used in high-performance databases, though it’s mostly due to the fact that data structures used in real databases have to support fast updates while we don’t.

        Note that this implementation is very specific to the architecture. Older CPUs and CPUs on mobile devices don’t have 256-bit wide registers and will crash (but they likely have 128-bit SIMD so the loop can still be split in 4 parts instead of 2), non-Intel CPUs have their own instruction sets for SIMD, and some computers even have different cache line size.

        -

        Also, it may take some work to get it working with other data types or different comparators (you’ll have to implement custom comparison using SIMD instructions), but in most cases this should be totally doable.

        +

        Legacy version

        +

        If you consider using SIMD a form of cheating (not sure if this qualifies as “comparison-based binary search” anymore), or running it on older hardware, or need to use it with a custom data type and don’t have too much time, try replacing search with this simpler version:

        + +

        It is ~30% slower (which is still good, because std::sort is 600% slower), but it saves you from rewriting the comparator with SIMD by hand, which in most cases is totally doable, but requires some effort.

        From d621ca911f920036d5154702197c61332d23f72f Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 2 Apr 2020 11:40:20 +0000 Subject: [PATCH 217/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/b-tree.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/en/b-tree.html b/en/b-tree.html index f2f35ff..7881392 100644 --- a/en/b-tree.html +++ b/en/b-tree.html @@ -118,7 +118,7 @@

        Implicit Static B-trees

        It performs slightly worse on array sizes that fit lower layers of cache, but in low-bandwidth environments it can be up to 3x faster (or 7x faster than std::lower_bound).

        B-tree layout

        B-trees generalize the concept of binary search trees by allowing nodes to have more than two children.

        -

        Instead of single key, a B-tree node contains up to \(B\) sorted keys may have up to \((B+1)\) children, thus reducing the tree heigh in \(\frac{\log_2 n}{\log_B n} = \frac{\log B}{\log 2} = \log_2 B\) times.

        +

        Instead of single key, a B-tree node contains up to \(B\) sorted keys may have up to \((B+1)\) children, thus reducing the tree height in \(\frac{\log_2 n}{\log_B n} = \frac{\log B}{\log 2} = \log_2 B\) times.

        They were primarily developed for the purpose of managing on-disk databases, as their random access times are almost the same as reading 1MB of data sequentially, which makes the trade-off between number of comparisons and tree height beneficial. In our implementation, we will make each the size of each block equal to the cache line size, which in case of int is 16 elements.

        Normally, a B-tree node also stores \((B+1)\) pointers to its children, but we will only store keys and rely on pointer arithmetic, similar to the one used in Eytzinger array:

        After that, we call this function two times (because our node size / cache line happens to be 512 bits, which is twice as big) and blend these masks together with bitwise operations.

        -

        Complete implementaiton

        +

        Complete implementation

        -

        It is ~30% slower (which is still good, because std::sort is 600% slower), but it saves you from rewriting the comparator with SIMD by hand, which in most cases is totally doable, but requires some effort.

        +

        It is ~30% slower (which is still good, because std::lower_bound is still ~600% slower), but it saves you from rewriting the comparator with SIMD by hand, which in most cases is totally doable, but requires some effort.

        From ecc9392bb9cedf2b6c9339a94346fb5859f79156 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 2 Apr 2020 13:45:11 +0000 Subject: [PATCH 218/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/b-tree.html | 84 +++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/en/b-tree.html b/en/b-tree.html index 7881392..bed39b6 100644 --- a/en/b-tree.html +++ b/en/b-tree.html @@ -148,10 +148,10 @@

        Construction

        It is correct, because each value of initial array will be copied to a unique position in the resulting array, and the tree height is \(\Theta(\log_{B+1} n)\), because \(k\) is multiplied by \((B + 1)\) each time a child node is created.

        Note that this approach causes a slight imbalance: “lefter” children may have larger respective ranges.

        -

        Here is a short but rather inefficient implementation that we will improve later.

        +

        Here is a short but rather inefficient implementation that we will improve later:

        …but ~8 times faster.

        -

        The algorithm:

        +

        Actually, compiler quite often produces very optimized code that leverages these instructions for certain types of loops. This is called auto-vectorization, and this is the reason why a loop that sums up an array of shorts is faster (theoretically by a factor of two) than the same loop for ints: you can fit more elements on the same 256-bit block. Sadly, this is not our case, as we have loop-carried dependencies.

        +

        The algorithm we will implement:

        1. Somewhere before the main loop, convert \(x\) to a vector of \(8\) copies of \(x\).

        2. Load the keys stored in node into another 256-bit vector.

        3. @@ -222,47 +223,46 @@

          Complete implementation

          typedef __m256i reg; -const int n = (1<<20), m = (1<<22), B = 16; +const int n = (1<<20), B = 16; const int nblocks = (n + B - 1) / B; const int INF = numeric_limits<int>::max(); -int a[n], q[m], results[m]; -alignas(64) int b[n+1], btree[nblocks][B]; - -int go(int k, int i) { return k * (B + 1) + i + 1; } - -void build(int k = 0) { - static int t = 0; - if (k < nblocks) { - for (int i = 0; i < B; i++) { - build(go(k, i)); - btree[k][i] = (t < n ? a[t++] : INF); - } - build(go(k, B)); - } -} - -int cmp(reg x_vec, int* y_ptr) { - reg y_vec = _mm256_load_si256((reg*) y_ptr); - reg mask = _mm256_cmpgt_epi32(x_vec, y_vec); - return _mm256_movemask_ps((__m256) mask); -} - -int search(int x) { - int k = 0, res = INF; - reg x_vec = _mm256_set1_epi32(x); - while (k < nblocks) { - int mask = ~( - cmp(x_vec, &btree[k][0]) + - (cmp(x_vec, &btree[k][8]) << 8) - ); - int i = __builtin_ffs(mask) - 1; - if (i < B) - res = btree[k][i]; - k = go(k, i); - } - return res; -}
          +alignas(64) int btree[nblocks][B]; + +int go(int k, int i) { return k * (B + 1) + i + 1; } + +void build(int k = 0) { + static int t = 0; + if (k < nblocks) { + for (int i = 0; i < B; i++) { + build(go(k, i)); + btree[k][i] = (t < n ? a[t++] : INF); + } + build(go(k, B)); + } +} + +int cmp(reg x_vec, int* y_ptr) { + reg y_vec = _mm256_load_si256((reg*) y_ptr); + reg mask = _mm256_cmpgt_epi32(x_vec, y_vec); + return _mm256_movemask_ps((__m256) mask); +} + +int search(int x) { + int k = 0, res = INF; + reg x_vec = _mm256_set1_epi32(x); + while (k < nblocks) { + int mask = ~( + cmp(x_vec, &btree[k][0]) + + (cmp(x_vec, &btree[k][8]) << 8) + ); + int i = __builtin_ffs(mask) - 1; + if (i < B) + res = btree[k][i]; + k = go(k, i); + } + return res; +}

          That’s it. This implementation should outperform even the state-of-the-art indexes used in high-performance databases, though it’s mostly due to the fact that data structures used in real databases have to support fast updates while we don’t.

          Note that this implementation is very specific to the architecture. Older CPUs and CPUs on mobile devices don’t have 256-bit wide registers and will crash (but they likely have 128-bit SIMD so the loop can still be split in 4 parts instead of 2), non-Intel CPUs have their own instruction sets for SIMD, and some computers even have different cache line size.

          Legacy version

          @@ -280,6 +280,6 @@

          Legacy version

          } return res; } -

          It is ~30% slower (which is still good, because std::lower_bound is still ~600% slower), but it saves you from rewriting the comparator with SIMD by hand, which in most cases is totally doable, but requires some effort.

          +

          It is ~30% slower (which is still quite good, because std::lower_bound is ~600% slower), but it saves you from rewriting the comparator with SIMD by hand, which in most cases is totally doable, but requires some effort.

          From 70b0ca1064aa0e4c22e3d7ae204030b15198cfe9 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Thu, 2 Apr 2020 13:50:42 +0000 Subject: [PATCH 219/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- en/eytzinger.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/en/eytzinger.html b/en/eytzinger.html index f5beff6..3e6d6f0 100644 --- a/en/eytzinger.html +++ b/en/eytzinger.html @@ -121,7 +121,7 @@

          Eytzinger Binary Search

        4. Cache-Oblivious Algorithms and Data Structures by Erik Demaine

        5. What Every Programmer Should Know About Memory by Ulrich Drepper

        6. -

          Our minimalistic implementation is only ~15 lines of code while offering 4-5x speedup over std::lower_bound.

          +

          Our minimalistic implementation is only ~15 lines of code while offering 4-5x speedup over std::lower_bound. The exact speedup depends a lot on available memory bandwidth (see notes below).

          If you are writing a contest right now, stuck on a problem where binary search is a bottleneck, and suddenly remembered about this article, jump straight to “complete implementation”, it’s compilable and copy-pastable.

          Why is binary search slow?

          Here is a standard way of searching for the first element not less than \(x\) in a sorted array:

          @@ -303,5 +303,6 @@

          What about B-trees?

          This explains why they have roughly the same slope.

          Note that this method, while being great for single-threaded world, is unlikely to make its way into database and heavy multi-threaded applications, because it sacrifices bandwidth to achieve low latency.

          +

          Part 2 explores efficient implementation of implicit static B-trees in bandwidth-constrained environment.

          From 856ea8e502412b0e4e1d9fc598e899bd374d395c Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Fri, 3 Apr 2020 18:18:08 +0000 Subject: [PATCH 220/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/combinatorics.html | 262 ++++++++++++++++++++++++++++++++++++++++ ru/index.html | 3 +- ru/karatsuba.html | 4 +- ru/no-deallocation.html | 137 +++++++++++++++++++++ 4 files changed, 403 insertions(+), 3 deletions(-) create mode 100644 ru/combinatorics.html create mode 100644 ru/no-deallocation.html diff --git a/ru/combinatorics.html b/ru/combinatorics.html new file mode 100644 index 0000000..4a85234 --- /dev/null +++ b/ru/combinatorics.html @@ -0,0 +1,262 @@ + + + + + + + Ликбез по комбинаторике - Алгоритмика + + + + + + + + + + + + + + + + +

          Ликбез по комбинаторике

          +

          Статья не затрагивает теоирию чисел и линейную алгебру.

          +

          Воможно, поможет почитать про нахождение обратного.

          +

          [Конкретная математика](http://sereja.me/f/discrete_math_knuth.pdf Грэхэма, Кнута и Паташика.

          +

          Рекуррентные задачи

          +

          Ханойские башни

          +

          123

          +

          +

          Задача Иосифа

          +

          Иосиф Флавий (лат. Flavius Josephus) — знаменитый историк первого векf. По легенде, он бы не дожил до известности если бы не его математические способности.

          +

          Во время еврейсDuring the Jewish-Roman war, he was among a band of 41 Jewish rebels trapped in a cave by the Romans. Preferring suicide to capture, the rebels decided to form a circle and, proceeding around it, to kill every third remaining person until no one was left. But Josephus, along with an unindicted co-conspirator, wanted none of this suicide nonsense; so he quickly calculated where he and his friend should stand in the vicious circle.

          +

          Принцип Дирихле. Если кролики рассажены в клетки, причём число кроликов больше числа клеток, то хотя бы в одной из клеток находится более одного кролика.

          +

          В англоязычном мире называется Pigeonhole principle и говорит о голубях, но суть не меняется.

          +

          +

          Принцип кажется очевидным, однако его применение очевидно не всегда. Рассмотрим несколько примеров.

          +

          Задача. Дан квадрат \(2 \times 2\) и \(5\) точек внутри него. Доказать, что среди них найдется две такие, что расстояние между ними \(\leq \sqrt 2\).

          +

          Решение. Рассмотрим \(4\) квадратика \(1 \times 1\). По принципу Дирихле найдётся квадратик, внутри или на границе которого 2 точки. Эти точки — искомые.

          +

          Примеры посложнее:

          +
            +
          • Вы пытаетесь замостить шахматную доску доминошками. Если убрать два противоположных угловых поля, это станет невозможно.

          • +
          • В группе из шести человек, всегда найдутся трое, которые являются либо взаимными друзьями, либо незнакомцами.

          • +
          • Пример из реального мира: любой алгоритм сжатия без потери данных не может гарантировать сжатие для всех возможных входных файлов.

          • +
          +

          Задача про покемонов.

          +

          Бином Ньютона

          +

          Поли=много, Би=два. nomen = название.

          +

          \[ +(a + b)^n +\]

          +

          Коэффициенты при разложении имеют огромную роль в комбинаторике.

          +

          В России их обозначают как , во всем остальном мире — как .

          +

          Сильно раздражает, что нет даже единого мнения, где \(n\), а где \(k\).

          +

          Биномиальные коэффициенты

          +

          Алгоритм Евклида

          +

          Решето

          +

          Треугольник Паскаля

          +

          Можно заметить

          +

          +

          Сочетания

          +

          Упорядоченный набор из \(k\) элементов данного множества, среди которых могут быть повторяющиеся, называется размещением из \(n\) элементов по \(k\) с повторением.

          +

          Соответственно, если элементы могут повторяться, то набор называется размещением без повторений, или просто размещением.

          +

          Сочетанием из \(n\) элементов по \(k\) называется набор \(k\) элементов этого множества, при том что наборы, отличающиеся только порядком следования элементов, считаются одинаковыми (этим сочетание отличается от размещения).

          +

          Аналогично, сочетания бывают с повторениями и без.

          +

          Читается как «цэ из \(n\) по \(k\)».

          +

          Как это считать

          + +

          Разложения

          +

          Шары и перегородки.

          +

          Автор не запоминает эту формулу и не пытается проделать алгебру в уме, а просто определяет вспомогательную функцию:

          + +

          Известные равенства

          +

          \[ +\sum_k \binom{r}{m+k}\binom{s}{n-k} = \binom{r+s}{m+n} +\]

          +

          \[ +\sum_k \binom{l}{m+k} \binom{s}{n+k} = \binom{l+s}{l-m+n} +\]

          +

          \[ +\sum_k \binom{l}{m+k}\binom{s+k}{n} (-1)^k = (-1)^{l+m} \binom{s-m}{n-l} +\]

          +

          \[ +\sum_{k \leq l} \binom{l-k}{m}\binom{s}{k-n} (-1)^k = (-1)^{l+m} \binom{s-m-l}{l-m-n} +\]

          +

          \[ +\sum_{0 \leq k \leq l} \binom{l-k}{m}\binom{q+k}{n} = \binom{l+q+1}{m+n+1} +\]

          +

          Hockey stick identity

          +

          «Равенство хоккейной клюшки».

          +

          +

          Правильные скобочные последовательности

          +

          Диаграмма Юнга

          +

          Числа Каталана

          +

          Числа Каталана — числовая последовательность, встречающаяся в удивительном числе комбинаторных задач. Например, \(n\)-ное число Каталана равно числу:

          +
            +
          • …корректных скобочных последовательностей, состоящих из \(n\) открывающих и \(n\) закрывающих скобок.

          • +
          • …корневых бинарных деревьев с n+1 листьями (если вершины не пронумерованы).

          • +
          • …разбиений выпуклого многоугольника непересекающимися диагоналями на треугольники \((n+2)\)-угольника (это называется триангуляцией).

          • +
          • …способов соединить \(2n\) точек на окружности \(n\) непересекающимися хордами.

          • +
          • …разбиений отрезка из \(n\) элементов на непрерывные блоки.

          • +
          • …путей из точки \((0,0)\) в точку \((n,n)\) в квадратной решётке размером \(n \times n\), не поднимающихся над главной диагональю.

          • +
          +

          Первые несколько чисел Каталана, начиная с нулевого:

          +

          \[ +1, 1, 2, 5, 14, 42, 132, 429, 1430, … +\]

          +

          Числа Каталана можно считать квадратичной динамикой. Пусть \(k\)-тое число Каталана, \(C_k\), равно количеству правильных скобочных последовательностей длины \(2k\), то есть из \(k\) пар скобок. Последовательность начинается с открывающей скобки, и этой скобке соответствует какая-то закрывающая. Можно перебрать её место (все нечетные при нумерации с нуля позиции), и для каждого варианта последовательность разделится на две правильные скобочные последовательности — до закрывающей скобки (из \(k\) пар скобок) и после (из \((n-1-k)\) пар).

          +

          Получается следующая рекуррентная формула:

          +

          \[ +C_n = \sum_{k = 0}^{n-1} C_k C_{n-1-k} +\]

          +

          Аналитическая формула

          +

          Но не нужно, потому что есть аналитическая формула, которую мы сейчас докажем:

          +

          \[ +C_n = \dfrac{1}{n+1} \dbinom {2n} {n} +\]

          +

          Эту формулу проще всего вывести из задачи о монотонных путях —. Общее количество монотонных путей в решётке размером n n равно C_{2n}^{n}. Теперь посчитаем количество монотонных путей, пересекающих диагональ. Рассмотрим какой-либо из таких путей, и найдём первое ребро, которое стоит выше диагонали. Отразим относительно диагонали весь путь, идущий после этого ребра. В результате получим монотонный путь в решётке (n-1) (n+1). Но, с другой стороны, любой монотонный путь в решётке (n-1) (n+1) обязательно пересекает диагональ, следовательно, он получен как раз таким способом из какого-либо (причём единственного) монотонного пути, пересекающего диагональ, в решётке n n. Монотонных путей в решётке (n-1) (n+1) имеется C_{2n}^{n-1}. В результате получаем формулу:

          +

          Все \(C_4 = 14\) путей на квадрате \(4 \times 4\):

          +

          +

          \[ +C_n = C_{2n}^{n} - C_{2n}^{n-1} = \frac{1}{n+1} C[...] +\]

          +

          Перестановки

          +

          Перестановки как граф

          +

          Граф, у которого

          +

          Наибольшая общая подпоследовательность двух перестановок.

          +

          Ничего не мешает нам переименовать объекты так, что в первой они будут идти по порядку.

          +

          Генерация комбинаторных объектов

          +

          Обратная задача

          +

          Задача «Попугаи». Вам нужно передать 64 байт некоторой информации. Для этого у вас есть 320 специально обученных попугаев, каждый из которых может запомнить число от 0 до 255. Все попугаи рано или поздно долетают до точки назначения и сообщают свой байт, но, возможно, не в том порядке, в котором были выпущены. Попугаи внешне неразличимы.

          +

          Принцип включений-исключений

          +

          Это бывает полезно при подсчете площадей многомерных фигур через префиксные суммы.

          +

          Куда дальше, или полезные незатронутые темы

          +

          Если вам зачем-то понадобилось.

          +

          Лемма бернсайда.

          +

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

          +

          Линейные рекурренты.

          +

          Теория чисел.

          +

          Частично упорядоченные множества и теорема Дилуорса.

          +

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

          +

          Разные отдельные факты на границе комбинаторики и теории графов.

          +

          Комбинаторика, которая находится на границе с теорией вероятностей.

          + + diff --git a/ru/index.html b/ru/index.html index 5949a9a..fdef421 100644 --- a/ru/index.html +++ b/ru/index.html @@ -95,7 +95,7 @@

          Деревья

          Высшая математика

          diff --git a/ru/karatsuba.html b/ru/karatsuba.html index e61a9de..293c30d 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -4,7 +4,7 @@ - Разделяй-и-властвуй - Алгоритмика + Многочлены и алгоритм Карацубы - Алгоритмика + + + + + + + + + + + + + + +

          Неудаляющий аллокатор

          +

          Программы на C могут выделять память под объекты двумя способами: на стэке (stack allocation) и на куче (heap allocation).

          +

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

          +

          Во втором подходе программа вежливо просит операционную систему выделить сколько-то байт памяти как ей удобно и вернуть на них указатель, а также гарантировать, что другие процессы не будут иметь к ней никакого доступа (хотя компьютер — вещь сложная, и это не всегда получается). В этом случае программист сам должен думать об удалении объектов, когда они становятся не нужны. Для создания объектов таким образом нужно вызвать оператор new, а при удалении вызывать оператор delete.

          +

          Низкоуровневая работа с памятью — это, пожалуй, основная причина, почему люди любят и ненавидят C и C++. Если не удалять объекты, выделенные на куче, то возникает утечка памяти, но в условиях олимпиады это нам не важно — за несколько секунд времени редко удается выделить больше памяти, чем доступно.

          +

          Пожертвовав «правильным» освобождением памяти, можно значительно ускорить — иногда в полтора-два раза — скорость работы работы структур данных, которые используют delete (в частности, большинство контейнеров STL).

          +

          Для этого можно глобально переопределить new и delete:

          + +

          Оператор new принимает количество байт памяти, которые требуется выделить, и возвращает указатель на выделенную память — у нас он возвращает и увеличивает конец «стэка».

          +

          Оператор delete принимает указатель на начало памяти и освобождает её. В нашем случае, он просто не делает ничего.

          + + From 9cb5bae00dfe184bbce16e3e5e6014bdd6841491 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 4 Apr 2020 21:23:54 +0000 Subject: [PATCH 221/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sparse-table.html | 47 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/ru/sparse-table.html b/ru/sparse-table.html index f586789..83ae247 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -115,44 +115,43 @@

          Разреженная таблица

          Разреженная таблица (англ. sparse table) — структура данных, позволяющая отвечать на запросы минимума на отрезке за \(O(1)\) с препроцессингом за \(O(n \log n)\) времени и памяти.

          -

          Определение. Разреженная таблица — это следующий двумерный массив размера \(n \times\log n\):

          +

          Определение. Разреженная таблица — это следующий двумерный массив размера \(\log n \times n\):

          \[ -t[i][k] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} +t[k][i] = \min \{ a_i, a_{i+1}, \ldots, a_{i+2^k-1} \} \]

          По-русски: считаем минимумы на каждом отрезке длины \(2^k\).

          Такой массив можно посчитать за его размер, итерируясь либо по \(i\), либо по \(k\):

          \[ -t[i][k] = \min(t[i][k-1], t[i+2^{k-1}][k-1]) +t[k][i] = \min(t[k-1][i], t[k-1][i+2^{k-1}]) \]

          Имея таком массив, мы можем для любого отрезка быстро посчитать минимум на нём. Заметим, что у любого отрезка имеется два отрезка длины степени двойки, которые пересекаются, и, главное, покрывают его и только его целиком. Значит, мы можем просто взять минимум из значений, которые соответствуют этим отрезкам.

          -

          Последняя деталь: для того, чтобы константа на запрос стала настоящей, вместо функции log нужно предпосчитать массив округленных вниз логарифмов.

          -
          int a[maxn], lg[maxn], mn[maxn][logn];
          +

          Последняя деталь: для того, чтобы константа на запрос стала настоящей, нужно научиться считать сам логарифм за константу. Для этого можно воспользоваться функцией __builtin_clz (“count leading zeros”), которая возвращает количество нулей до первой единицы в бинарной записи, из чего можно получить нужный округленный логарифм.

          + +int lg(int x) { + return 31 - __builtin_clz(x); +} + +// Это считается где-то в первых строчках main: + +memcpy(mn[0], a, sizeof a); + +for (int l = 0; l < logn - 1; l++) + for (int i = 0; i + (2 << l) <= n; i++) + mn[l+1][i] = min(mn[l][i], mn[l][i + (1 << l)]);

          Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

          -

          Упражнение. Какой из 4 вариантов итерирования и layout-а самый эффективный? (Подсказка: не тот, который приведен выше.)

          +

          Упражнение. Подумайте, в чём недостатки других 4 вариантов итерирования и layout-а.

          Применения

          Разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но можно достраивать на ходу — см. задачу «Антиматерия» с РОИ-2017).

          Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.

          2d Static RMQ

          -

          Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[i][k] у нас будет массив t[i][j][k], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

          +

          Эту структуру тоже можно обобщить на большие размерности. Пусть мы хотим посчитать RMQ на подквадратах. Тогда вместо массива t[k][i] у нас будет массив t[k][i][j], в котором вместо минимума на отрезах будет храниться минимум на квадратах тех же степеней двоек. Получение минимума на произвольном квадрате тогда уже распадется на четыре минимума на квадратах длины \(2^k\).

          В общем же случае от нас просят минимум на прямоугольниках \(d\)-мерного массива. Тогда делаем предподсчет, аналогичный предыдущему случаю, только теперь тут будет \(O(n \log^d n)\) памяти и времени на предподсчет — нужно хранить минимумы на всех гипер-прямоугольниках со сторонами степени двойки.

          Ограничения на операцию

          Разреженную таблицу можно применять не только для минимума или максимума. От операции требуется только ассоциативность (\(a ∘ (b ∘ c) = (a ∘ b) ∘ c\)), коммутативность (\(a ∘ b = b ∘ a\)) и идемпотентность (\(a ∘ a = a\)). Например, её можно применять для нахождения \(\gcd\).

          @@ -191,19 +190,19 @@

          Реализация

          void build(int l, int r, int level = logn - 1) { int m = (l + r) / 2; - + int cur = neutral; for (int i = m + 1; i < r; i++) { cur = f(cur, a[i]); t[level][i] = cur; } - + cur = neutral; for (int i = m; i >= l; i--) { cur = f(cur, a[i]); t[level][i] = cur; } - + if (r - l > 1) {      build(l, mid, level+1);      build(mid, r, level+1); @@ -218,6 +217,6 @@

          Реализация

          res = f(res, t[level][r]); return res; } -

          TODO: скорее всего, тут есть баги.

          +

          TODO: очень вероятно, тут есть баги.

          From 95316c48559577f8bf3c6fdf567ac55d805a4dc5 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sat, 4 Apr 2020 22:30:33 +0000 Subject: [PATCH 222/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/sparse-table.html | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ru/sparse-table.html b/ru/sparse-table.html index 83ae247..f6a55b2 100644 --- a/ru/sparse-table.html +++ b/ru/sparse-table.html @@ -129,13 +129,13 @@

          Разреженная табли

          Последняя деталь: для того, чтобы константа на запрос стала настоящей, нужно научиться считать сам логарифм за константу. Для этого можно воспользоваться функцией __builtin_clz (“count leading zeros”), которая возвращает количество нулей до первой единицы в бинарной записи, из чего можно получить нужный округленный логарифм.

          Для больших таблиц порядок итерирования и расположение данных в памяти сильно влияет на скорость построения — это связано с работой кэшей.

          Упражнение. Подумайте, в чём недостатки других 4 вариантов итерирования и layout-а.

          +

          Интересно, что последний цикл не векторизуется компилятором автоматически (видимо, потому что std::min это что-то сложное внутри), и поэтому замена тела цикла на конструкцию вида (x < y ? x : y) ускоряет построение ещё в ~2 раза.

          Применения

          Разреженная таблица является статической структурой данных, то есть её нельзя дёшево обновлять (но можно достраивать на ходу — см. задачу «Антиматерия» с РОИ-2017).

          Разреженную таблицу часто применяют для решения задачи о наименьшем общем предке, так как её можно свести к RMQ.

          From 16727fbacf1b57acfb77b1f97579e355eeaedf61 Mon Sep 17 00:00:00 2001 From: "Deployment Bot (from Travis CI)" Date: Sun, 5 Apr 2020 05:55:05 +0000 Subject: [PATCH 223/230] Deploy algorithmica.org to github.com/algorithmica-org/articles.git:gh-pages --- ru/fft.html | 2 +- ru/karatsuba.html | 225 ++++++++++++++++++++++++++++++++++++++++--- ru/sparse-table.html | 30 +++--- 3 files changed, 226 insertions(+), 31 deletions(-) diff --git a/ru/fft.html b/ru/fft.html index 71cb182..9076359 100644 --- a/ru/fft.html +++ b/ru/fft.html @@ -130,7 +130,7 @@

          Умножение много \]

          Основание x при этом может быть выбрано произвольно.

          Чтобы перемножить два числа, мы можем перемножить соответствующие им многочлены, а затем произвести каррирование: пройтись от нижних разрядов получившегося многочлена и «сдвинуть» переполнившиеся разряды:

          -
          const int base = 10;
          +
          const int base = 10;
           
           vector<int> normalize(vector<int> a) {
               int carry = 0;
          diff --git a/ru/karatsuba.html b/ru/karatsuba.html
          index 293c30d..00ba8f6 100644
          --- a/ru/karatsuba.html
          +++ b/ru/karatsuba.html
          @@ -11,6 +11,69 @@
                 span.underline{text-decoration: underline;}
                 div.column{display: inline-block; vertical-align: top; width: 50%;}
             
          +  
             
             
             
             
          -  
          +
             
             
             
             
          -  
          +
             
          +  
             
             
          -  
          +
             
             
             
             
          -  
          +
             
          +  
             
             
          -  
          +
             
             
             
             
          -  
          +
             
             
             
          -  
          +
             
             
             
             
          -  
          +
             
           
           
          +
          I'm writing a book. It's free and available online. You can find an updated version of this and many more of my old articles there.
          — Sergey Slotin

          Algorithmica

          -One day /ru/ will be translated and put here. +The website has moved.
          diff --git a/index.html b/index.html index f0ac554..6b15388 100644 --- a/index.html +++ b/index.html @@ -23,10 +23,10 @@ case 'ru-RU': case 'uk-UA': case 'be-BY': - window.location.href = 'ru/'; + window.location.href = 'https://ru.algorithmica.org/'; break; default: - window.location.href = 'ru/'; + window.location.href = 'https://ru.algorithmica.org/'; //window.location.href = 'en/'; break; } diff --git a/pandoc.css b/pandoc.css index 7f7c1c0..87d9946 100644 --- a/pandoc.css +++ b/pandoc.css @@ -27,6 +27,19 @@ src: url(fonts/opensans.woff2); } +.migration { + background: rgb(219, 245, 227); + border: 1px solid green; + margin: 20px 0px; + padding: 6px 12px; + font-family: "Garamond"; +} + +.migration a { + display: block; + font-size: 1.25em; +} + html { font-size: 100%; overflow-y: scroll; diff --git a/ru/aho-corasick.html b/ru/aho-corasick.html index 6e2fb9b..70ef359 100644 --- a/ru/aho-corasick.html +++ b/ru/aho-corasick.html @@ -12,26 +12,28 @@ div.column{display: inline-block; vertical-align: top; width: 50%;} - + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + +
          Сайт переезжает. Большинство статей уже перенесено на новую версию.
          Скоро добавим автоматические переходы, но пока обновленную версию этой статьи можно найти там.

          Алгоритмика

          -
          -

          Структуры данных

          +
          +

          Структуры данных

          -
          -
          -

          Графы

          + +
          +

          Графы

          -
          -
          -

          Строки

          + +
          +

          Строки

          -
          +
          -
          -

          Общие техники

          +
          +

          Общие техники

          -
          -
          -

          Деревья

          + +
          +

          Деревья

          -
          -
          -

          Высшая математика

          + +
          +

          Высшая математика

          -
          +
          -
          -

          Теория чисел

          +
          +

          Теория чисел

          -
          -
          -

          Теория игр

          + +
          +

          Теория игр

          -
          -
          -

          Динамика

          + +
          +

          Динамика

          -
          +
          -
          -

          Комбинаторика

          +
          +

          Комбинаторика

          -
          -
          -

          Геометрия

          + +
          +

          Геометрия

          -
          -
          -

          Разное

          + +
          +

          Разное

          -
          +
          -
          +

          Спасибо: (в алфавитном порядке)

          diff --git a/ru/karatsuba.html b/ru/karatsuba.html index a1880d0..41411f0 100644 --- a/ru/karatsuba.html +++ b/ru/karatsuba.html @@ -12,26 +12,28 @@ div.column{display: inline-block; vertical-align: top; width: 50%;} - + - + - + + - + - + + - + - + + - + - + - + - + + - + - + + - + - + + - + - + + - + - + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + - + - + + - + - + - + - + +
          Сайт переезжает. Большинство статей уже перенесено на новую версию.
          Скоро добавим автоматические переходы, но пока обновленную версию этой статьи можно найти там.

          Tinkoff Generation

          Тут (возможно, временно) лежат некоторые конспекты первого курса Tinkoff Generation.

            diff --git a/tg/knapsack-gis-gcs.html b/tg/knapsack-gis-gcs.html index fda0e1e..3ec923f 100644 --- a/tg/knapsack-gis-gcs.html +++ b/tg/knapsack-gis-gcs.html @@ -12,18 +12,18 @@ div.column{display: inline-block; vertical-align: top; width: 50%;} - + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + + - + - + -
            I'm writing a book. It's free and available online. You can find an updated version of this and many more of my old articles there.
            — Sergey Slotin

            Algorithmica

            The website has moved. diff --git a/ru/index.html b/ru/index.html index 521625a..75ffb16 100644 --- a/ru/index.html +++ b/ru/index.html @@ -43,7 +43,6 @@ -
            Сайт переезжает. Большинство статей уже перенесено на новую версию.
            Скоро добавим автоматические переходы, но пока обновленную версию этой статьи можно найти там.

            Алгоритмика

            Структуры данных

            diff --git a/tg/index.html b/tg/index.html index d513286..acdca12 100644 --- a/tg/index.html +++ b/tg/index.html @@ -42,7 +42,6 @@ -
            Сайт переезжает. Большинство статей уже перенесено на новую версию.
            Скоро добавим автоматические переходы, но пока обновленную версию этой статьи можно найти там.

            Tinkoff Generation

            Тут (возможно, временно) лежат некоторые конспекты первого курса Tinkoff Generation.

              From bb9c941660afb0a9f996fc3f62c94f29b02851b7 Mon Sep 17 00:00:00 2001 From: Sergey Slotin Date: Tue, 15 Feb 2022 05:04:28 +0300 Subject: [PATCH 230/230] making default redirect to the english version except for east slavic languages --- index.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.html b/index.html index 6b15388..c092799 100644 --- a/index.html +++ b/index.html @@ -26,8 +26,7 @@ window.location.href = 'https://ru.algorithmica.org/'; break; default: - window.location.href = 'https://ru.algorithmica.org/'; - //window.location.href = 'en/'; + window.location.href = 'https://en.algorithmica.org/'; break; }