Русский
Русский
English
Статистика
Реклама

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

SQL HowTo обрабатываем дерево упорядочиваем иерархию с рекурсией и без

19.10.2020 20:09:27 | Автор: admin
Видимо, это осень так влияет, что за последний месяц на PostgreSQL уже и в Морской бой играли, и Жизнь Конвея эмулировали Что уж оставаться в стороне! Давайте и мы потренируем мозг в реализации нетривиальных алгоритмов на SQL.

Тем более, сегодняшняя тема родилась из обсуждения моей же статьи PostgreSQL Antipatterns: Бесконечность не предел!, или Немного о рекурсии, где я показал, как можно заменить рекурсивное чтение иерархичных данных из таблицы на линейное.

Прочитать-то мы прочитали, но ведь чтобы для вывода упорядочить элементы дерева в соответствии с иерархией, уж точно придется воспользоваться рекурсией! Или нет? Давайте разберемся, а заодно решим на SQL пару комбинаторных задач.


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



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


Давайте все-таки сначала формально определим те правила, которым должен отвечать искомый порядок записей:

  • элемент-предок стоит раньше любого из элементов его поддерева
  • дочерние элементы внутри одного родительского упорядочиваются по дополнительному прикладному ключу

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

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

Таков путь!


Собственно, а что мы такое можем сконструировать, чтобы сортировка этого чего-то давала нам желаемый результат?

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



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

Рекурсивная сортировка


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

WITH RECURSIVE src(id, pid, data) AS (  VALUES    (1, NULL, 'A')  , (2, 1, 'AA')  , (3, 2, 'AAA')  , (4, 1, 'AB')  , (5, 2, 'AAB')  , (6, 3, 'AAAA')  , (7, 5, 'AABA')  , (8, 4, 'ABA')  , (9, 7, 'AABAA')  , (10, 2, 'AAC')), T AS (  SELECT    id  , ARRAY[data] path -- инициализируем массив пути корневым элементом  FROM    src  WHERE    pid IS NULLUNION ALL  SELECT    s.id  , T.path || s.data -- наращиваем путь  FROM    T  JOIN    src s      ON s.pid = T.id)SELECT  *FROM  srcNATURAL JOIN  TORDER BY  path; -- сортируем согласно пути

 id | pid | data  |         path----+-----+-------+-----------------------  1 |     | A     | {A}  2 |   1 | AA    | {A,AA}  3 |   2 | AAA   | {A,AA,AAA}  6 |   3 | AAAA  | {A,AA,AAA,AAAA}  5 |   2 | AAB   | {A,AA,AAB}  7 |   5 | AABA  | {A,AA,AAB,AABA}  9 |   7 | AABAA | {A,AA,AAB,AABA,AABAA} 10 |   2 | AAC   | {A,AA,AAC}  4 |   1 | AB    | {A,AB}  8 |   4 | ABA   | {A,AB,ABA}

Подключаем комбинаторику


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

Комбинации


Пусть у нас есть исходный массив {A,B,C}, все элементы которого уникальны. Получим все комбинации массивов той же длины, состоящие из его элементов:

{A,A,A}{A,A,B}{A,A,C}{A,B,A}{A,B,B}...{C,C,B}{C,C,C}

Достаточно очевидно, что при длине массива N таких вариантов будет ровно N^N, но как получить их на SQL?

Обратим внимание, что каждой комбинации элементов соответствует комбинация позиций этих элементов в исходном массиве, если пронумеровать их с нуля. А каждой такой комбинации число в N-ричной системе счисления:

3^2 |  0  0  0  0  0  0  0  0  0  1  1  1  1  1  1  1  1  1  2  2  2  2  2  2  2  2  23^1 |  0  0  0  1  1  1  2  2  2  0  0  0  1  1  1  2  2  2  0  0  0  1  1  1  2  2  23^0 |  0  1  2  0  1  2  0  1  2  0  1  2  0  1  2  0  1  2  0  1  2  0  1  2  0  1  2=== |  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

Решение становится достаточно очевидным:

  • генерируем каждое число в диапазоне 0 .. N^N - 1
  • раскладываем в N-ричную систему счисления
  • берем элемент на соответствующей позиции разложения

SELECT  dstFROM  -- исходный набор элементов  (VALUES('{A,B,C}'::varchar[])) data(src)  -- кэшируем размер набора, LATERAL array_length(src, 1) n  -- кэшируем границу интервала, LATERAL (SELECT (n ^ n)::bigint l) X  -- генерируем все числа на интервале, LATERAL generate_series(0, l - 1) num  -- формируем разложение числа в N-ричной системе, LATERAL (    SELECT      array_agg((num % (n ^ (pos + 1))::bigint) / (n ^ pos)::bigint ORDER BY pos DESC) num_n    FROM      generate_series(0, n - 1) pos  ) Y  -- собираем элементы согласно "цифрам", LATERAL (    SELECT      array_agg(src[num_n[pos] + 1]) dst    FROM      generate_subscripts(num_n, 1) pos  ) Z;

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


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

JOIN LATERAL(  SELECT    count(DISTINCT unnest) = n cond  FROM    unnest(num_n)) flt  ON cond

Полный вариант запроса
SELECT  dstFROM  -- исходный набор элементов  (VALUES('{A,B,C}'::varchar[])) data(src)  -- кэшируем размер набора, LATERAL array_length(src, 1) n  -- кэшируем границу интервала, LATERAL (SELECT (n ^ n)::bigint l) X  -- генерируем все числа на интервале, LATERAL generate_series(0, l - 1) num  -- формируем разложение числа в N-ричной системе, LATERAL (    SELECT      array_agg((num % (n ^ (pos + 1))::bigint) / (n ^ pos)::bigint ORDER BY pos DESC) num_n    FROM      generate_series(0, n - 1) pos  ) Y  -- фильтруем комбинации с неполным набором "цифр"JOIN LATERAL(    SELECT      count(DISTINCT unnest) = n cond    FROM      unnest(num_n)  ) flt    ON cond  -- собираем элементы согласно "цифрам", LATERAL (    SELECT      array_agg(src[num_n[pos] + 1]) dst    FROM      generate_subscripts(num_n, 1) pos  ) Z;

Это дает нам все возможные перестановки исходного набора:

{A,B,C}{A,C,B}{B,A,C}{B,C,A}{C,A,B}{C,B,A}

Можно использовать и неэкспоненциальный алгоритм на основе тасований, работающий за O(N*N!), но его реализация явно выходит за рамки данной статьи.

Подмножества


Сделаем шаг чуть в сторону и научимся генерировать все подмножества исходного набора с сохранением порядка. То есть для нашего набора {A,B,C} должно получиться вот это:

{}{A}{B}{A,B}{C}{A,C}{B,C}{A,B,C}

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

2^2 |  0  0  0  0  1  1  1  12^1 |  0  0  1  1  0  0  1  12^0 |  0  1  0  1  0  1  0  1=== |  0  1  2  3  4  5  6  7

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

  -- кэшируем разложение числа в двоичной системе, LATERAL (SELECT num::bit(64) num_n) Y  -- собираем элементы согласно "цифрам", LATERAL (    SELECT      coalesce(array_agg(src[i]) FILTER(WHERE get_bit(num_n, 64 - i) = 1), '{}') dst    FROM      generate_series(1, n) i  ) Z

Полный вариант запроса
SELECT  dstFROM  -- исходный набор элементов  (VALUES('{A,B,C}'::varchar[])) data(src)  -- кэшируем размер набора, LATERAL array_length(src, 1) n  -- кэшируем границу интервала, LATERAL (SELECT (2 ^ n)::bigint l) X  -- генерируем все числа на интервале, LATERAL generate_series(0, l - 1) num  -- кэшируем разложение числа в двоичной системе, LATERAL (SELECT num::bit(64) num_n) Y  -- собираем элементы согласно "цифрам", LATERAL (    SELECT      coalesce(array_agg(src[i]) FILTER(WHERE get_bit(num_n, 64 - i) = 1), '{}') dst    FROM      generate_series(1, n) i  ) Z;

Иерархия без рекурсии!


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

Пути-дороги


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

Но сортировать по такому пути, конечно, будет некорректно поэтому для дальнейшей сортировки превратим id записей в соответствующее значение data, которое использовали в первом варианте.
Дано: газовая плита, чайник. Задача: вскипятить воду.
Физик: Зажечь плиту, наполнить чайник водой и поставить на плиту, ждать.
Математик: Аналогично.

Дано: зажженная газовая плита, наполненный водой чайник. Задача: вскипятить воду.
Физик: Поставить чайник на плиту, ждать.
Математик: Выливаем воду из чайника на плиту. Огонь погас, чайник пуст, задача сведена к предыдущей.
Народный анекдот
Но как найти путь до каждого из элементов без рекурсии? Вот здесь нам и пригодятся алгоритмы выше.

Корректный путь от корня до конкретного элемента обладает следующими свойствами:

  • Правило #1: начинается и заканчивается нужными нам элементами
    path[1] = root AND path[array_length(path, 1)] = id
  • Правило #2: предок каждого элемента, кроме корневого, так же присутствует в наборе
    pid = ANY(path) OR pid = root
  • Правило #3: из всех таких наборов искомый самой маленькой длины
    Иначе для id=3 наборы {1,2,3} и {1,2,3,4} окажутся равноподходящими, поскольку предок id=4 (pid=1) тоже присутствует.
  • Правило #4: предок каждого элемента стоит ровно в предыдущей позиции
    pid = path[pos - 1]

Итак, намечаем план действий:

  • генерируем все подмножества элементов, исключая root и id, формируя тело пути по правилу #1
  • проверяем выполнение правила #2
  • выбираем, согласно правилу #3, самый короткий набор
  • генерируем все перестановки его элементов
  • проверяем выполнение правила #4
  • что осталось искомый путь

Полный вариант запроса, смотреть с осторожностью
Я вас предупредил [источник картинки]

WITH src(id, pid, data) AS (  VALUES    (1, NULL, 'A')  , (2, 1, 'AA')  , (3, 2, 'AAA')  , (4, 1, 'AB')  , (5, 2, 'AAB')  , (6, 3, 'AAAA')  , (7, 5, 'AABA')  , (8, 4, 'ABA')  , (9, 7, 'AABAA')  , (10, 2, 'AAC'))-- кэшируем ID корневого элемента, root AS (  SELECT    id  FROM    src  WHERE    pid IS NULL  LIMIT 1)-- формируем уже известные пути и предварительные наборы, preset AS (  SELECT    *  , CASE      -- для корневого элемента путь состоит только из него самого      WHEN pid IS NULL THEN ARRAY[id]      -- для ссылающегося на корневой - из пары      WHEN pid = (TABLE root) THEN ARRAY[pid, id]    END prepath  , CASE      WHEN pid IS NULL THEN NULL      WHEN pid = (TABLE root) THEN NULL      -- все ID, кроме корневого и собственного - EXCLUDE CURRENT ROW      ELSE array_agg(id) FILTER(WHERE pid IS NOT NULL) OVER(ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING EXCLUDE CURRENT ROW)    END preset  FROM    src)-- формируем "переборные" пути, prepath AS (  SELECT    id  , prepath  FROM    -- отбираем только элементы, чей путь еще не определили    (      SELECT        id      , pid      , preset src -- комбинируемый набор      FROM        preset      WHERE        prepath IS NULL    ) data    -- подмножества  , LATERAL (      SELECT        dst pathset      FROM        -- кэшируем размер набора        LATERAL array_length(src, 1) n        -- кэшируем границу интервала      , LATERAL (SELECT (2 ^ n)::bigint l) X        -- генерируем все числа на интервале      , LATERAL generate_series(1, l - 1) num -- тут можно с 1, поскольку пустой набор нас заведомо не интересует        -- кэшируем разложение числа в двоичной системе      , LATERAL (SELECT num::bit(64) num_n) Y        -- собираем элементы согласно "цифрам"      , LATERAL (          SELECT            coalesce(array_agg(src[i]) FILTER(WHERE get_bit(num_n, 64 - i) = 1), '{}') || data.id dst          FROM            generate_series(1, n) i        ) Z        -- проверяем наличие предка в наборе      , LATERAL (          SELECT            NULL          FROM            (              SELECT                (SELECT pid FROM src WHERE id = dst[i] LIMIT 1) _pid              FROM                generate_subscripts(dst, 1) i            ) T          HAVING            bool_and(_pid = (TABLE root) OR _pid = ANY(dst))        ) rule2      -- отбираем первый подходящий минимальной длины      ORDER BY        array_length(dst, 1) -- rule3      LIMIT 1    ) X    -- перестановки  , LATERAL (      SELECT        dst prepath      FROM        -- исходный набор элементов        (SELECT pathset) data(src)        -- кэшируем размер набора      , LATERAL array_length(src, 1) n        -- кэшируем границу интервала      , LATERAL (SELECT (n ^ n)::bigint l) X        -- генерируем все числа на интервале      , LATERAL generate_series(0, l - 1) num        -- формируем разложение числа в N-ричной системе      , LATERAL (          SELECT            array_agg((num % (n ^ (pos + 1))::bigint) / (n ^ pos)::bigint ORDER BY pos DESC) num_n          FROM            generate_series(0, n - 1) pos        ) Y        -- фильтруем комбинации с неполным набором "цифр"      JOIN LATERAL(          SELECT            count(DISTINCT unnest) = n cond          FROM            unnest(num_n)        ) flt          ON cond        -- собираем элементы согласно "цифрам"      , LATERAL (          SELECT            array_agg(src[num_n[pos] + 1]) dst          FROM            generate_subscripts(num_n, 1) pos        ) Z        -- проверяем наличие предка в предыдущей позиции      , LATERAL (          SELECT            NULL          FROM            (              SELECT                (SELECT pid FROM src WHERE id = dst[i] LIMIT 1) _pid              , i              FROM                generate_subscripts(dst, 1) i            ) T          HAVING            bool_and((i = 1 AND _pid = (TABLE root)) OR _pid = dst[i - 1])        ) rule4    ) Y)SELECT  src.*  -- восстанавливаем "путь" из прикладных ключей, (    SELECT      array_agg(data ORDER BY i)    FROM      coalesce(X.prepath, ARRAY[(TABLE root)] || Y.prepath) p -- помним о необходимости добавить "корень"    , LATERAL generate_subscripts(p, 1) i    , LATERAL (        SELECT          data        FROM          src        WHERE          id = p[i]        LIMIT 1      ) T  ) pathFROM  srcLEFT JOIN  preset X    USING(id)LEFT JOIN  prepath Y    USING(id)ORDER BY  path;

А попроще можно?..


Можно и попроще, если заранее известно, что порядок детей внутри одного родителя определяется некоторым сквозным ключом. Например, это может быть некоторый монотонно возрастающий timestamp сообщений в ветке форума или, как в нашем случае, первичный ключ типа serial (id).

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

WITH src(id, pid, data) AS (  VALUES    (1, NULL, 'A')  , (2, 1, 'AA')  , (3, 2, 'AAA')  , (4, 1, 'AB')  , (5, 2, 'AAB')  , (6, 3, 'AAAA')  , (7, 5, 'AABA')  , (8, 4, 'ABA')  , (9, 7, 'AABAA')  , (10, 2, 'AAC'))-- кэшируем ID корневого элемента, root AS (  SELECT    id  FROM    src  WHERE    pid IS NULL  LIMIT 1)-- собираем все предстоящие id в массив для текущего, prepath AS (  SELECT    id  , pid  , array_agg(id) OVER(ORDER BY id /*!!! сортировка по тому самому ключу*/ ROWS UNBOUNDED PRECEDING EXCLUDE CURRENT ROW) src  FROM    src  WHERE    pid IS NOT NULL)-- находим пути, pre AS (  SELECT    id  , path  FROM    prepath    -- подмножества  , LATERAL (      SELECT        dst path      FROM        -- кэшируем размер набора        LATERAL array_length(src, 1) n        -- кэшируем границу интервала      , LATERAL (SELECT (2 ^ n)::bigint l) X        -- генерируем все числа на интервале      , LATERAL generate_series(0, l - 1) num        -- кэшируем разложение числа в двоичной системе      , LATERAL (SELECT num::bit(64) num_n) Y        -- собираем элементы согласно "цифрам"      , LATERAL (          SELECT            coalesce(array_agg(src[i]) FILTER(WHERE get_bit(num_n, 64 - i) = 1), '{}') || id dst          FROM            generate_series(1, n) i        ) Z        -- проверяем наличие предка в предыдущей позиции      , LATERAL (          SELECT            NULL          FROM            (              SELECT                (SELECT pid FROM src WHERE id = dst[i] LIMIT 1) _pid              , i              FROM                generate_subscripts(dst, 1) i            ) T          HAVING            bool_and((i = 1 AND _pid = (TABLE root)) OR (i > 1 AND _pid = dst[i - 1]))        ) rule4    ) X)SELECT  src.*  -- восстанавливаем "путь" из прикладных ключей, (    SELECT      array_agg(data ORDER BY i)    FROM      (        SELECT          CASE            -- для корневого элемента путь состоит только из него самого            WHEN pid IS NULL THEN ARRAY[id]            -- для ссылающегося на корневой - из пары            WHEN pid = (TABLE root) THEN ARRAY[pid, id]            ELSE ARRAY[(TABLE root)] || pre.path          END p -- помним о необходимости добавить "корень"      ) p    , LATERAL generate_subscripts(p, 1) i    , LATERAL (        SELECT          data        FROM          src        WHERE          id = p[i]        LIMIT 1      ) T  ) pathFROM  srcLEFT JOIN  pre    USING(id)ORDER BY  path;

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

Четырёхмерные таблицы в комбинаторике два странных способа посчитать сочетания

30.11.2020 12:14:44 | Автор: admin


В комбинаторике сочетанием из $inline$n$inline$ по $inline$k$inline$ называют набор $inline$k$inline$ элементов, выбранных из $inline$n$inline$ элементов. В отличие от размещений, число сочетаний не учитывает последовательность размещения элементов, например: Сколько групп из 4 человек, можно получить, если всего в классе 20 человек?. Хотя удобные способы подсчёта давно известны, на ещё два стоит взглянуть.

Обозначается сочетание из $inline$n$inline$ по $inline$k$inline$ так: $inline$C_n^k$inline$. В литературе они чаще обозначаются $inline$ \binom nk$inline$ (но мне больше нравится первый вариант, чтобы не путать с матрицами).

В комбинаторике известны несколько способов подсчёта:

$$display$$C_n^k = \frac{n!}{k!(nk)!}$$display$$

$$display$$C_n^k=\frac{n(n1)(n2)(nk+1)}{k!}=\frac{\prod\limits_{i = nk+1}^n i}{k!}$$display$$

Где $inline$n!$inline$ эн факториал, произведение всех целых чисел от 1 до n (например: $inline$4!=1\cdot2\cdot3\cdot4=24$inline$), а $inline$0!$inline$ считается равным единице. Для вышесказанной задачи получается:

$$display$$C_{20}^4 = \frac{20!}{4!(204)!} = \frac{17\cdot18\cdot19\cdot20}{1\cdot2\cdot3\cdot4} = 4845$$display$$

Или так:

$$display$$C_{20}^4=\frac{20\cdot19\cdot18\cdot17}{4!}=\frac{116280}{24}=4845$$display$$


Вторая формула сочетаний выводится очень просто. Есть понятие числа размещений из $inline$n$inline$ по $inline$k$inline$, когда последовательность элементов имеет значение (то есть набор первый со вторым с пятым это не тоже самое, что первый с пятым со вторым), обозначается $inline$A_n^k$inline$.

Например все размещения из 3 по 2 выглядят так:

12 21
13 31
23 32


Первый элемент можно выбрать $inline$n$inline$ способами, второй $inline$(n1)$inline$ способами, последний $inline$(n(k1))$inline$ способами. Поэтому число размещений из $inline$n$inline$ по $inline$k$inline$ равно $inline$n(n1)(n2)(nk+1)$inline$, всего $inline$k$inline$ множителей.

Для подсчёта сочетаний получившиеся число нужно разделить на $inline$k!$inline$, поскольку есть $inline$k!$inline$ способов разместить $inline$k$inline$ элементов. В данном случае $inline$2!=2$inline$ способа (первый со вторым и второй с первым это одно и тоже сочетание).

Вспомним, как идет счёт в шашках по круговой системе (когда все играют со всеми). Для этого пишут обычную таблицу:

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

Число вычеркнутых клеток равно $inline$n$inline$, а число всех клеток это $inline$n^2$inline$. Стало быть, число сочетаний по 2 можно посчитать так:

$$display$$C_n^2=\frac{n^2n}{2}$$display$$

Когда я это заметил, не сразу увидел что это равно $inline$\frac{n(n1)}{2!}$inline$. Решил сделать также для $inline$k=3$inline$, с трёхмерной таблицей:

На каждом её слое, кроме диагонали исключаются еще клетки. Например, клетка 161 исключается, потому что единица повторилась два раза.

Всего на каждом слое исключается $inline$n+2(n1)$inline$ клеток.

$$display$$n+2(n1)=3n2$$display$$






Итак, на каждом слое исключается $inline$3n2$inline$ клетки. Всего $inline$n$inline$ слоёв, поэтому общее число исключившихся ячеек:

$$display$$n(3n2)=3n^22n$$display$$

А вся таблица $inline$n^3$inline$. Значит, число сочетаний по 3 можно посчитать так:

$$display$$C_n^3=\frac{n^3(3n^22n)}{3!}=\frac{n^33n^2+2n}{3!}$$display$$

Как уже сказано выше, это и есть общая формула, в которой просто раскрыли скобки:

$$display$$C_n^3=\frac{n^33n^2+2n}{3!}=\frac{n(n1)(n2)}{3!}$$display$$

И тут возник вопрос, можно ли посчитать сочетания, так же представив их через таблицы, не используя факториалы вообще? К слову, в комбинаторике уже известна рекуррентная формула для числа сочетаний: $inline$C_n^k=C_{n1}^{k1}+C_{n1}^k$inline$ (число сочетаний по 0 равно 1, из любого количества элементов можно получить только одну группу в которой 0 элементов). Итак, я попробовал посмотреть, какие еще клетки исключаются в трёхмерной таблице, когда мы делим число оставшихся клеток на $inline$k!$inline$.

Клетки ниже диагонали дублируют клетки, которые выше диагонали вычеркнем их (ячейка 431 то же самое, что и 341). Для того, чтобы получить такое же количество клеток, просто поделим число размещений на 2. Цифрами в самой таблице я отметил, в какой строке встретилось данное сочетание. Например, сочетание 251 впервые встретилось здесь во второй строке, поэтому отмечено цифрой 2. Посмотрим на следующий слой:

Закрашенные клетки с цифрой означают, что данное сочетание уже встречалось. Например, комбинация 152 уже встречалась во второй строке (в виде 251), поэтому отмечена цифрой 2 в закрашенной клетке.

Итак, на втором слое исключается ещё $inline$(n2)$inline$ клетки (кроме тех, которые вычеркнуты из-за повторяющихся чисел в них, и тех, которые вычеркнуты ниже диагонали, из-за того что дублируют верхние). Перебрав так всю таблицу, я получил следующее. На третьем слое исключается ещё $inline$(n2)+(n3)$inline$ клеток:

На четвертом слое вычеркивается ещё $inline$(n2)+(n3)+(n4)$inline$ клеток:

На пятом $inline$(n2)+(n3)+(n4)+(n5)$inline$:

На шестом, по видимому, $inline$(n2)+(n3)+(n4)+(n5)+(nn)$inline$:

Получается, что для подсчёта сочетаний по 3, из числа $inline$\frac{n(n1)(n2)}{2}$inline$ нужно вычесть такую сумму:

$$display$$(n2)+((n2)+(n3))+((n2)+(n3)+(n4))+\\((n2)+(n3)+(n4)+(n5))+((n2)+(n3)+(n4)+(n5)+(nn))$$display$$

Это можно переписать в таком виде:

$$display$$\sum_{i=2}^n{\sum_{j=2}^i(nj)}$$display$$

Ещё можно переписать эту сумму так:

$$display$$5(n2)+4(n3)+3(n4)+2(n5)+(nn)=\\(n1)(n2)+(n2)(n3)+(n3)(n4)+(n4)(n5)+(n5)(nn)$$display$$

То есть:

$$display$$\sum_{i=2}^{n1}{(ni)(ni+1)}$$display$$

Значит, число сочетаний по 3 можно вычислить так:

$$display$$C_n^3=\frac{n(n1)(n2)}{2}\sum_{i=2}^n\sum_{j=2}^i(nj)=\frac{n(n1)(n2)}{2}\sum_{i=2}^{n1}{(ni)(ni+1)}$$display$$

Например число сочетаний из 8 по 3:

$$display$$C_8^3=\frac{8\cdot7\cdot6}{2}\sum_{i=2}^7{(n-i)(n-i+1)}=\\ 168(6\cdot7+5\cdot6+4\cdot5+3\cdot4+2\cdot3+1\cdot2)=\\ 168112=56$$display$$

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

Весь первый двухмерный слой этого слоя исключается (на нем единица везде повторяется). В итоге на первом трёхмерном слое вычитается такая сумма:

$$display$$\begin{pmatrix}-\\0+\\(n3)+\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+(nn)\end{pmatrix}$$display$$

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

Значит, на втором слое исключается такая сумма:

$$display$$\begin{pmatrix}(n3)+(n4)+(n5)+\\-\\(n3)+\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}=\begin{pmatrix}-\\(n3)+\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}$$display$$

Посмотрим на третий:

На третьем исключается сумма:

$$display$$\begin{pmatrix}(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\-\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}=\begin{pmatrix}-\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}$$display$$

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

$$display$$\begin{pmatrix}-\\0+\\(n3)+\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\(n3)+\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}+\\\begin{pmatrix}-\\(n3)+(n4)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}+\\\begin{pmatrix}-\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)+\\(n3)+(n4)+(n5)\end{pmatrix}$$display$$

её можно переписать в таком виде:

4(n3)+3(n4)+2(n5)+1(nn)+
5(n3)+4(n4)+3(n5)+2(nn)+
5(n3)+5(n4)+4(n5)+3(nn)+
5(n3)+5(n4)+5(n5)+4(nn)+
5(n3)+5(n4)+5(n5)+5(nn)+
5(n3)+5(n4)+5(n5)+5(nn)


Получаем:

$$display$$29(n3)+27(n4)+24(n5)+20(nn)$$display$$

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

$$display$$C_n^4=\frac{n(n1)(n2)(n3)}{2}\sum_{i=3}^{n1}{(ni)\left(30\sum_{j=1}^{i2}j\right)}$$display$$

Однако, получившаяся формула будет работать только для $inline$n=6$inline$, ведь для других $inline$n$inline$ коэффициенты будут другие. И, поскольку я не увидел зависимости от $inline$k$inline$ и здесь, решил посмотреть тоже самое для $inline$k=5$inline$, через пятимерную таблицу, весь её первый трёхмерный слой исключается, поскольку единица здесь повторяется во всех ячейках:

Для наглядности запишем это так:

$$display$$\begin{pmatrix}-\\-\\-\\-\\-\\-\end{pmatrix}$$display$$


Смотрим дальше:

Тут, как видно, вычитается такая сумма:

$$display$$\begin{pmatrix}-\\-\\0\\(n4)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}$$display$$

Третий:

Тут вычитается:

$$display$$\begin{pmatrix}-\\(n4)+(n5)+\\-\\(n4)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}=\begin{pmatrix}-\\-\\(n4)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}$$display$$



$$display$$\begin{pmatrix}-\\(n4)+(n5)+\\(n4)+(n5)+\\-\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}=\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}$$display$$



$$display$$\begin{pmatrix}-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\-\\(n4)+(n5)\end{pmatrix}=\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}$$display$$


Затем, шестой фрагмент:

$$display$$\begin{pmatrix}-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\\-\end{pmatrix}=\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}$$display$$


На этом первый четырёхмерный слой заканчивается. Не сложно понять, что трёхмерный слой 21 будет дублировать слой 12, а слой 22 исключится весь, также как 11.

Перейдем к слою 23:

Здесь встретилось последнее сочетание из 6 по 5 56423 (клетки, которые встретились впервые отмечены белым, если кто забыл)). Напишем, какую в итоге надо вычесть сумму из $inline$\frac{n(n1)(n2)(n3)(n4)}{2}$inline$, чтобы получить число сочетаний из $inline$n$inline$ по 5. На первом четырёхмерном слое исключилось:

$$display$$\begin{pmatrix}-\\-\\-\\-\\-\\-\end{pmatrix}+\begin{pmatrix}-\\-\\0+\\(n4)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\-\\(n4)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\\\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}$$display$$

На втором:

$$display$$\begin{pmatrix}-\\-\\-\\-\\-\\-\end{pmatrix}+\begin{pmatrix}-\\-\\(n4)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\\\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}+\begin{pmatrix}-\\-\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)+\\(n4)+(n5)\end{pmatrix}$$display$$

Получается что каждая сумма в скобке обозначает исключившиеся ячейки на трёхмерных слоях. Всего $inline$n$inline$ четырёхмерных слоёв. Запишем это в виде суммы:

$$display$$\begin{aligned} &C_n^5=\frac{n(n1)(n2)(n3)(n4)}{2}\sum_{i_3=1}^n\sum_{i_2=1}^{nk+4}\sum_{i_1=a}^n\sum_{i_0=k1}^b(ni_0)\\ &a=\left\{ \begin{aligned} &k1, \ если \ i_2=i_3=1 \\ &k2, \ иначе \\ \end{aligned}\right.\\ &b=\left\{ \begin{aligned} &i_12+i_2+i_3, \ если \ i_12+i_2+i_3\leqslant n1 \\ &n1, \ иначе \\ \end{aligned}\right. \end{aligned}$$display$$

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

Индекс второй суммы берем до $inline$(nk+4)$inline$, поскольку она образует исключения на четырёхмерных слоях, суммируя вычеркнутые ячейки на трёхмерных, как мы могли видеть выше, у пятимерной таблицы на каждом четырёхмерном слое исключается один трёхмерный, то есть для $inline$n=6$inline$, на каждом четырёхмерном остается пять трёхмерных слоёв, $inline$65+4=5$inline$.

Для $inline$k=6$inline$, шестимерную таблицу можно не смотреть, достаточно выписать индексы её последних трёх измерений:

111 121 131 141 151 161
112 122 132 142 152 162
113 123 133 143 153 163
114 124 134 144 154 164
115 125 135 145 155 165
116 126 136 146 156 166

211
212


Опять же, вычеркнули мы те слои, где хоть одна цифра повторилась. Уже по первому пятимерному слою видно, что на пятимерных слоях для шестимерной таблицы останется на один четырёхмерный слой меньше. Количество оставшихся четырёхмерных слоёв равно $inline$(nk+5)$inline$. А трёхмерных слоёв на каждом четырёхмерном остается все также $inline$(nk+4)$inline$ (по 4 в данном случае: $inline$66+4$inline$). Запись в виде сумм получилась такой:

$$display$$\begin{aligned} &C_n^6=\frac{n(n1)(n2)(n3)(n4)(n5)}{2}\sum_{i_4=1}^n\sum_{i_3=1}^{nk+5}\sum_{i_2=1}^{nk+4}\sum_{i_1=a}^n\sum_{i_0=k1}^b(ni_0)\\ &a=\left\{ \begin{aligned} &k1, \ если \ i_2=i_3=i_4=1 \\ &k2, \ иначе \\ \end{aligned}\right.\\ &b=\left\{ \begin{aligned} &i_13+i_2+i_3+i_4, \ если \ i_13+i_2+i_3+i_4\leqslant n1 \\ &n1, \ иначе \\ \end{aligned}\right. \end{aligned}$$display$$


Запись для $inline$k=3$inline$ можно переписать так:

$$display$$\begin{aligned} &C_n^3=\frac{n(n1)(n2)}{2}\sum_{i_1=a}^n\sum_{i_0=k1}^b(ni_0)\\ &a=k1\\ &b=\left\{ \begin{aligned} &i_1, \ если \ i_1\leqslant n1 \\ &n1, \ иначе \\ \end{aligned}\right. \end{aligned}$$display$$


Для $inline$k=4$inline$ получается:

$$display$$\begin{aligned} &C_n^4=\frac{n(n1)(n2)(n3)}{2}\sum_{i_2=1}^n\sum_{i_1=a}^n\sum_{i_0=k1}^b(ni_0)\\ &a=\left\{ \begin{aligned} &k1, \ если \ i_2=1 \\ &k2, \ иначе \\ \end{aligned}\right.\\ &b=\left\{ \begin{aligned} &i_11+i_2, \ если \ i_11+i_2\leqslant n1 \\ &n1, \ иначе \\ \end{aligned}\right. \end{aligned}$$display$$


Для $inline$k=5$inline$:

$$display$$\begin{aligned} &C_n^5=\frac{n(n1)(n2)(n3)(n4)}{2}\sum_{i_3=1}^n\sum_{i_2=1}^{nk+4}\sum_{i_1=a}^n\sum_{i_0=k1}^b(ni_0)\\ &a=\left\{ \begin{aligned} &k1, \ если \ i_2=i_3=1 \\ &k2, \ иначе \\ \end{aligned}\right.\\ &b=\left\{ \begin{aligned} &i_12+i_2+i_3, \ если \ i_12+i_2+i_3\leqslant n1 \\ &n1, \ иначе \\ \end{aligned}\right. \end{aligned}$$display$$


В общем виде запись для $inline$k\geqslant2$inline$ получается такой (сумма, которую мы вычитаем вычисляется рекуррентно):

$$display$$\begin{aligned} &C_n^k=\frac{\prod\limits_{i=nk+1}^ni}{2}c_k\\ &k_0=k\\ &c_k=\left\{\begin{aligned} &0, \ k=2 \\ &\sum_{i_3=a}^{n}\sum_{i_2=k1}^{b}(ni_2), \ k=3 \\ &\sum_{i_k=1}^{nk_0+k}c_{k1}, \ k\geqslant4 \end{aligned}\right. \\ &a=\left\{ \begin{aligned} &k_01, \ если \ i_4=i_5==i_k=1 \ или \ k_0=3 \\ &k_02, \ иначе \end{aligned}\right.\\ &b=\left\{ \begin{aligned} &i_3(k_03)+i_4+i_5++i_k, \ если\ i_3(k3)+i_4+i_5++i_k\leqslant n1 \ и \ k_0>3 \\ &i_3, \ если\ i_3\leqslant n1 \ и\ k_0=3 \\ &n1, \ иначе \end{aligned}\right. \end{aligned}$$display$$


Второй способ


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

$$display$$C_n^3=\frac{n(n1)(n2)}{2}\sum_{i=2}^n\sum_{j=2}^i(nj)=\frac{n(n1)(n2)}{2}\sum_{i=2}^{n1}{(ni)(ni+1)}$$display$$

А число сочетаний из 6 по 4 можно записать так:

$$display$$C_n^4=\frac{n(n1)(n2)(n3)}{2}\sum_{i=3}^{n1}{(ni)\left(30\sum_{j=1}^{i2}j\right)}$$display$$

Оказывается, что 30 во вторых скобках, это $inline$n(n1)$inline$ (логического обоснования этому я так и не смог найти).

$$display$$C_n^4=\frac{n(n1)(n2)(n3)}{2}\sum_{i=3}^{n1}{(ni)\left(n(n1)\sum_{j=1}^{i2}j\right)}$$display$$

Вторые скобки здесь представляют собой коэффициент, который умножается на $inline$(ni)$inline$. Для $inline$k=3$inline$ этот коэффициент просто уменьшается на 1 с каждым слагаемым первой суммы. Для $inline$4$inline$ число, на которое уменьшается этот коэффициент возрастает на 1. Затем я просто посчитал количество слагаемых $inline$(n4)$inline$, $inline$(n5)$inline$ и $inline$(nn)$inline$ в сумме которая вычитается для $inline$5$inline$:

$$display$$119(n4)+116(n5)+110(nn)$$display$$

Число, на которое увеличивается число, на которое уменьшается коэффициент, возрастает на 1. Этот коэффициент можно записать через две суммы:

$$display$$C_n^5=\frac{n(n1)(n2)(n3)(n4)}{2}\sum_{i=4}^{n1}{(ni)\left(n(n1)(n2)\sum_{j_1=1}^{i3}\sum_{j_0=1}^{j_1}j_0\right)}$$display$$

Зависимость ясна для $inline$k=6$inline$ эта формула будет выглядеть так:

$$display$$C_n^6=\frac{n(n1)(n2)(n3)(n4)(n5)}{2}\\\sum_{i=5}^{n1}{(ni)\left(n(n1)(n2)(n3)\sum_{j_2=1}^{i4}\sum_{j_1=1}^{j_2}\sum_{j_0=1}^{j_1}j_0\right)}$$display$$

А в общем виде (опять же, для $inline$k\geqslant2$inline$) получилось так:

$$display$$\begin{aligned} &C_n^k=\frac{\prod\limits_{i=nk+1}^ni}{2}c_k\\ &k_0=k\\ &c_k=\left\{ \begin{aligned} &0, \ k=2 \\ &\sum_{i=k1}^{n1}{(ni)\left(\prod_{j_k=nk+3}^nj_kd_k\right)}, \ если \ k\geqslant3 \\ \end{aligned}\right.\\ &d_k=\left\{ \begin{aligned} &i1, \ k=3 \\ &\sum_{j_4=1}^lj_4, \ k=4 \\ &\sum_{j_k=1}^ld_{k1}, \ если \ k>4 \\ &l=j_{k+1}, \ если \ k\neq k_0 \\ &l=ik_0+2, \ если \ k = k_0 \\ \end{aligned}\right.\\ \end{aligned}$$display$$

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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru