Что было бы хорошим алгоритмом для круговой контрольной проверки в этом случае?

Учитывая, что у вас есть следующий класс (плохой С#, но вы получаете дрейф):

public abstract class AmICircular
{
  // assume Children is never null
  private List<AmICircular> Children {get;set;}

  // assume target is never null
  public void Add(AmICircular target)
  {
    target.PerformCircularReferenceCheck(this);
    Children.Add(target);
  }

  // throws when a circular reference is detected
  protected abstract void PerformCircularReferenceCheck(AmICircular target);
}

Как бы вы реализовали PerformCircularReferenceCheck? И нет, это не домашнее задание.

Наивная реализация, imo, должна была выполнить контрольную проверку на this и всех дочерних, а затем вызвать PerformCircularReferenceCheck на target, передав this. Но мне интересно, есть ли более эффективные, эффективные способы сделать это, например, добавить метод свернуть все дерево детей ссылок для this и target, а затем изучить результаты (меньшее давление на стек?), или, возможно, полностью избежать проверки, используя другую (возможно, самоконтрольную!) коллекцию, отличную от List <T> ?

Как вы это сделаете?

edit: Как указал Стефан, нужно только определить, достижимо ли это целевым

Ответы

Ответ 1

В случае, когда вы никогда не добавляете объекты самореференции (для определения позже), ваша структура данных описывает направленный ациклический граф (http://en.wikipedia.org/wiki/Directed_acyclic_graph), где каждый экземпляр класса IAmCircular описывает node с набором прямых узлов-потомков = Дети.

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

Теория сложности мудрая, эта проблема является ST-связностью (http://en.wikipedia.org/wiki/St-connectivity) и является полной для класса NL (http://en.wikipedia.org/wiki/NL_(complexity)), даже если вы ограничиваете ввод ациклическими графами (это ваш случай).

В частности, теорема Саввича (http://en.wikipedia.org/wiki/Savitch%27s_theorem) дает конструктивный способ создания O (log ^ 2 n) пространственного алгоритма ( работающий во времени O (n ^ 2)) для него, где n - количество узлов.

Кроме того, будучи NL-полным, маловероятно, что существует алгоритм, который выполняется в пространстве O (log n) (т.е. использует только постоянное число указателей на узлы), поскольку это будет означать маловероятное NL = L. EDIT: в частности, небольшие вариации заяц-заяц и черепахи, предложенные кем-то, не будут работать (потому что они будут использовать слишком мало места).

Я бы рекомендовал реализовать тривиальный алгоритм O (n) времени O (n), который генерирует для "target" его набор преемников (рекурсивно) и проверяет, появляется ли в этом наборе "this".

Будьте осторожны, важна явная конструкция набора. В противном случае, если вы просто рекурсивно проверяете, доступен ли "this" любым преемником "цели", вы рискуете запустить его в экспоненциальном времени.

Я рекомендовал O (n) time/O (n) пространственный алгоритм, потому что асимптотически лучше, чем вы можете делать по времени, и вы уже используете O (n) пространство для своей структуры данных.

Ответ 2

Итеративным решением является определение множества R (достижимое) и CR (дочерние элементы Reachable).

Вы начинаете с R = {this} и CR = {this.children}.

На каждом шаге вы проверяете, содержит ли CR this (или target, в зависимости от вашей точной цели). Если нет, вы добавляете CR в R и устанавливаете CR для детей CR и удаляете элементы R из CR.

Если CR становится пустым, R - это полный набор элементов, доступных из this.