поиск самого длинного пути в списке смежности

У меня есть список смежности, который я создал для данного графа с узлами и взвешенными ребрами. Я пытаюсь выяснить, как лучше всего найти самый длинный путь в графе. У меня есть метод топологической сортировки, который, как я слышал, может быть полезен, но я не знаю, как его реализовать, чтобы найти самый длинный путь. Итак, есть ли способ сделать это с помощью сортировки по топологии или есть более эффективный метод?

Вот пример моего выхода для списка adj (значение в скобках — это стоимость доступа к узлу после стрелки (cost)to get to -> node:

Node 0 (4)->1(9)->2
Node 1 (10)->3
Node 2 (8)->3
Node 3
Node 4 (3)->8(3)->7
Node 5 (2)->8(4)->7(2)->0
Node 6 (2)->7(1)->0
Node 7 (5)->9(6)->1(4)->2
Node 8 (6)->9(5)->1
Node 9 (7)->3
Node 10 (12)->4(11)->5(1)->6

person bardockyo    schedule 30.04.2013    source источник


Ответы (2)


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

Во-первых, как он указал, эта проблема легко разрешима только при отсутствии циклов. Если есть циклы, вы сталкиваетесь с ситуацией, когда у вас есть бесконечно длинные пути. В этом случае вы можете определить самый длинный путь как любой путь без повторяющихся узлов. К сожалению, можно показать, что эта задача является NP-трудной. Поэтому вместо этого мы сосредоточимся на проблеме, которую вам действительно нужно решить (поскольку вы упомянули топологическую сортировку) — самом длинном пути в направленном ациклическом графе (DAG). Мы также предположим, что у нас есть два узла s и t, которые являются нашими начальным и конечным узлами. В противном случае проблема немного уродливее, если вы не можете сделать определенные предположения о вашем графике. Если вы понимаете текст ниже, и такие предположения в ваших графах верны, то, возможно, вы можете убрать ограничения s и t (иначе вам придется запускать его на каждой паре вершин вашего графа! Медленно...)

Первый шаг алгоритма — топологически упорядочить вершины. Интуитивно это имеет смысл. Скажем, вы упорядочиваете их слева направо (т. е. у самого левого узла не будет входящих ребер). Самый длинный путь от s до t обычно начинается слева и заканчивается справа. Также невозможно, чтобы путь когда-либо шел в левом направлении. Это дает вам последовательный порядок для создания самого длинного пути — начните слева и двигайтесь вправо.

Следующий шаг — последовательно пройти слева направо и определить самый длинный путь для каждого узла. Для любого узла, не имеющего входящих ребер, самый длинный путь к этому узлу равен 0 (это верно по определению). Для любого узла с входящими ребрами рекурсивно определите самый длинный путь к этому узлу как максимальный среди всех входящих ребер + самый длинный путь к «входящему» соседу (обратите внимание, что это число может быть отрицательным , если, например, все входящие ребра отрицательны!). Интуитивно это имеет смысл, но доказательство также тривиально:

Предположим, наш алгоритм утверждает, что самый длинный путь к некоторому узлу v равен d, но на самом деле самый длинный путь — это какой-то d' > d. Выберите «наименьший» такой узел v (мы используем порядок, определенный топологической сортировкой. Другими словами, мы выбираем «крайний левый» узел, в котором наш алгоритм потерпел неудачу. Это важно, чтобы мы могли предположить, что наш алгоритм правильно определил самый длинный путь для любых узлов «слева» от v). Определите длину гипотетического самого длинного пути равной d' = d_1 + e, где d_1 — это длина гипотетического пути до узла v_prev с ребрами от e до v (обратите внимание на неаккуратное название. Ребро e также имеет вес e). Мы можем определить его как таковое, потому что любой путь к v должен проходить через одного из своих соседей, у которых есть ребро, ведущее к v (поскольку вы не можете добраться до v, не пройдя туда через какое-то ребро, ведущее к нему). Тогда d_1 должен быть самым длинным путем к v_prev (иначе противоречие. Существует более длинный путь, который противоречит нашему выбору v в качестве «наименьшего» такого узла!) И наш алгоритм выбрал бы путь, содержащий d_1 + e, по желанию.

Чтобы сгенерировать фактический путь, вы можете выяснить, какое ребро было использовано. Допустим, вы реконструировали путь до некоторой вершины v, которая имеет наибольшую длину пути d. Затем просмотрите все входящие вершины и найдите вершину с наибольшей длиной пути d' = d - e, где e — вес ребра, входящего в v. Вы также можете просто отслеживать родительские узлы по мере прохождения алгоритма. То есть, когда вы найдете самый длинный путь к v, установите его родителем любой соседний узел, который был выбран. Вы можете использовать простое противоречие, чтобы показать, почему любой метод генерирует самый длинный путь.

Наконец, немного псевдокода (извините, он в основном на C#. Это намного сложнее кодировать на C без пользовательских классов, а я давно не кодировал C).

public List<Nodes> FindLongestPath(Graph graph, Node start, Node end)
{
    var longestPathLengths = Dictionary<Node, int>;

    var orderedNodes = graph.Nodes.TopologicallySort();
    // Remove any nodes that are topologically less than start. 
    // They cannot be in a path from start to end by definition
    while (orderedNodes.Pop() != start);
    // Push it back onto the top of the stack
    orderedNodes.Push(start);

    // Do algorithm until we process the end node
    while (1)
    {
        var node = orderedNodes.Pop();
        if (node.IncomingEdges.Count() == 0)
        {
            longestPathLengths.Add(node, 0);
        }
        else
        {
            var longestPathLength = Int.Min;
            foreach (var incomingEdge in node.IncomingEdges)
            {
                var currPathLength = longestPaths[incomingEdge.Parent] +               
                                     incomingEdge.Weight);
                if (currPathlength > longestPathLength)
                {
                    longestPath = currPathLength;
                }
            }

            longestPathLengths.Add(node, longestPath);
        }

        if (node == end)
        {
            break;
        }
    }

    // Reconstruct path. Go backwards until we hit start
    var node = end;
    var longestPath = new List<Node>();
    while (node != start)
    {
        foreach (var incomingEdge in node.IncomingEdges)
        {
            if (longestPathLengths[incomingEdge.Parent] == 
                    longestPathLengths[node] - incomingEdge.Weight)
            {
                longestPath.Prepend(incomingEdge.Parent);
                node = incomingEdge.Parent;
                break;
            }
        }
    }

    return longestPath;
}

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

person rliu    schedule 30.04.2013

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

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

person Bryan Olivier    schedule 30.04.2013