Мне нужно ответить на вопрос: учитывая узел в графе зависимостей, сгруппируйте его иждивенцев по их собственным переходным иждивенцам, на которые будет влиять конкретный начальный узел.
Другими словами, учитывая узел в графе зависимостей, найдите набор множеств прямых зависимостей, которые транзитивно имеют общих зависимостей, производных от этого конкретного начального узла.
например. учитывая псевдокод:
let a = 1
let b = 2
let c = a + b
let d = a + b
let e = a
let f = a + e
let g = c + d
Вы можете вычислить этот график:
Если мы использовали a
в качестве начального узла, мы можем видеть, что из зависимостей a
и c
, и d
имеют зависимость g
. И f
имеет зависимость e
и a
.
Обратите внимание, что a
никак не влияет на b
, поэтому его не следует принимать во внимание при принятии решения о том, как группировать иждивенцев a
.
Используя a
в качестве начального узла, мы бы хотели получить следующие сгруппированные наборы зависимостей:
groups = {{c, d}, {e, f}}
c
и d
имеют прямые или переходные нисходящие отношения, а также e
и f
вместе. Но, например, e
и f
вообще не имеют никакого зависимого (нисходящего) отношения с c
или d
ни прямо, ни косвенно (транзитивно). И b
не является производным от a
прямо или косвенно, поэтому оно не должно влиять на решение о нашей группировке.
Также имейте в виду, что этот график мал для простоты. Вполне возможно, что переходные иждивенцы происходят намного дальше вниз по подграфу, чем этот пример.
Я провел кучу бумажных исследований, и действительно, существует множество решений, однако они не имеют тех характеристик производительности, которые я ищу. График постепенно создается с течением времени, и на каждом этапе мне нужно иметь возможность ответить на этот вопрос, поэтому обход графа каждый раз является нарушителем.
Я думаю, что у меня есть главное преимущество, на которое нет ссылок в различных подходах, которые я мог найти: у меня есть полный контроль над созданием графа, и зависимости добавляются в обратном топологическом порядке, поэтому граф отсортирован правильно. Имея это в виду, я рассмотрел очевидное решение постепенного вычисления ответа (динамическое программирование).
Я подумал, что битовая маска будет быстрым способом хранения и поиска зависимостей данного узла. Когда зависимый элемент добавляется к узлу, я обновляю маску этого узла, чтобы включить биты этого зависимого элемента (который сам включает в себя его зависимые элементы и т. Д.)
let maskCounter = 0;
class Node {
constructor(name) {
this.name = name;
this.dependents = [];
this.mask = 1 << maskCounter;
maskCounter++;
}
addDependent(dependent) {
// Now our mask contains the bits representing all of
// its direct and transitive dependents
this.mask = this.mask | dependent.mask;
// Need to see if this dependent has a transitive
// dependent of its own that exists in one of the groups
for (const group of this.dependents) {
const result = group.mask & dependent.mask;
if (result) {
group.mask |= dependent.mask;
group.values.push(dependent);
return;
}
}
// If reached, this dependent has no transitive dependents
// of its own with any of this node's other dependents.
// That's confusing, huh?
this.dependents.push({
mask: dependent.mask,
values: [dependent]
});
}
}
Однако необходимо добавить иждивенцев в обратном порядке вверх по графику, чтобы график был отсортирован правильно, а верхняя часть графика содержала маски всех его зависимых элементов.
const a = new Node('a');
const b = new Node('b');
const c = new Node('c');
const d = new Node('d');
const e = new Node('e');
const f = new Node('f');
const g = new Node('g');
b.addDependent(c);
b.addDependent(d);
c.addDependent(g);
d.addDependent(g);
e.addDependent(f);
a.addDependent(c);
a.addDependent(d);
a.addDependent(e);
a.addDependent(f);
Битовые маски будут постепенно выглядеть так:
b = b 00000010 | c 00000100
b = b 00000110 | d 00001000
c = c 00000100 | g 01000000
d = d 00001000 | g 01000000
e = e 00010000 | f 00100000
a = a 00000001 | c 01000100
a = a 01000101 | d 01001000
a = a 01001101 | e 00110000
a = a 01111101 | f 00100000
===========================
a = 01111101
В конце a
имеет маску 01111101
, каждый бит представляет каждого из своих нижестоящих переходных зависимостей. Обратите внимание, что второй-последний бит не перевернут, это бит для b
, который вообще не зависит от a
.
Если мы посмотрим на итоговое значение a.dependents
, то увидим:
[
{ values: [c, d], mask: 0b00110000 },
{ values: [e, f], mask: 0b01001100 }
]
, который дает ответ, который мы ищем, в конечном итоге набор наборов. a.dependents.map(group => group.values)
- это массив, он же список, но он используется как набор для простоты.
Вот JSBin: https://jsbin.com/jexofip/edit?js,console
Это работает, и для процессора это приемлемо, потому что мне очень часто нужно знать сгруппированные иждивенцы, но они меняются гораздо реже.
В приведенном выше примере для простоты демонстрации используется JavaScript, который использует 32-разрядные целые числа со знаком для побитовых операций, поэтому мы можем создать только 31 уникальный узел. Мы можем использовать произвольное целое число точности (например, BigInt ) для создания "неограниченного" количества узлов, но проблема заключается в использовании памяти.
Поскольку каждому узлу нужен свой уникальный перевернутый бит, я думаю использование памяти:
N * (N + 1) / 2 = bits (where N = number of nodes)
e.g. 10,000 nodes is about 6.25 megabytes!
Это исключает любые накладные расходы платформы для использования произвольных целочисленных значений точности (или аналогичной пользовательской структуры данных).
В моем случае было бы обычно иметь 10k +, а на самом деле в некоторых случаях возможно 100k + (625 МБ !!!) и также возможно создание новых узлов на неопределенный срок, используя бесконечный объем памяти, поэтому это решение не практично, поскольку нет простого способа «собрать мусор», который больше не использует биты маски из узловэто исключает график - это, конечно, возможно, но это традиционная проблема GC, которую я бы хотел избежать, если это возможно.
Примечание: в зависимости от размера и глубины графика, это может быть не совсемвыступать хорошо тоже.Несмотря на то что сами побитовые операции выполняются относительно быстро, выполнение этого для BigInt в 100 000 битов для каждого узла в верхней части графа не выполняется.Так что полностью переосмыслить мой подход также приветствуется.
В конечном счете, торговля памятью для процессора - это обычное взаимное преимущество, но мне интересно, возможно, мне не хватает другого подхода, который обеспечивает лучший баланс или требуетзначительно меньше памяти?
Могут быть и другие уникальные соображения, о которых я даже не думал, что они могут быть использованы.
Обучи меня!