Обновите график в Форде-Фулкерсоне

Я реализую алгоритм Форда-Фалкерсона, но у меня возникла проблема с обновлением графика после фазы увеличения. Думаю, моя структура данных не упрощает эту задачу.

Для представления графика я использую это:

private Map<Vertex, ArrayList<Edge>> outgoingEdges;

То есть я связываю в каждой вершине свой список исходящих ребер.

Чтобы управлять обратными ребрами, я связал «противоположное» ребро для каждого ребра в графе.

Любое предложение приветствуется.

public class FF {

    /**
     * Associates each Vertex with his list of outgoing edges
     */
    private Map<Vertex, ArrayList<Edge>> outgoingEdges;

    public FF() {
        outgoingEdges = new HashMap<Vertex, ArrayList<Edge>>();
    }

    /**
     * Returns the nodes of the graph
     */ 
    public Collection<Vertex> getNodes() {
        return outgoingEdges.keySet();
    }

    /**
     * Returns the outgoing edges of a node
     */
    public Collection<Edge> getIncidentEdges(Vertex v) {
        return outgoingEdges.get(v);
    }

    /**
     * Adds a new edge to the graph
     */
    public void insertEdge(Vertex source, Vertex destination, float capacity) throws Exception {
        if (!(outgoingEdges.containsKey(source) && outgoingEdges.containsKey(destination)))
            throw new Exception("Unable to add the edge");

        Edge e = new Edge(source, destination, capacity);
        Edge opposite = new Edge(destination, source, capacity);
        outgoingEdges.get(source).add(e);
        outgoingEdges.get(destination).add(opposite);
        e.setOpposite(opposite);
        opposite.setOpposite(e);
    }

    /**
     * Adds a new node to the graph
     */
    public void insertNode(Vertex v) {
        if (!outgoingEdges.containsKey(v))
            outgoingEdges.put(v, new ArrayList<Edge>());
    }

    /**
     * Ford-Fulkerson algorithm
     * 
     * @return max flow
     */
    public int fordFulkerson(Vertex source, Vertex destination) {
        List<Edge> path = new ArrayList<Edge>();
        int maxFlow = 0;
        while(bfs(source, destination, path)) {
            // finds the bottleneck
            float minCap = bottleneck(path);
            // updates the maxFlow
            maxFlow += minCap;            
            // updates the graph <-- this updates only the local list path, not the graph!
            for(Edge e : path) {
                try {
                    e.addFlow(minCap);
                    e.getOpposite().addFlow(-minCap);
                } catch (Exception e1) {
                    e1.printStackTrace();
                }
            }
            path.clear();
        }
        return maxFlow;
    } 

    /**
     * @param Path of which we have to find the bottleneck
     * @return bottleneck of the path
     */
    private float bottleneck(List<Edge> path) {
        float min = Integer.MAX_VALUE;
        for(Edge e : path) {
            float capacity = e.getCapacity();
            if(capacity <= min) {
                min = capacity;
            }
        }
        return min;
    }

    /**
     * BFS to obtain a path from the source to the destination
     * 
     * @param source 
     * @param destination
     * @param path
     * @return
     */
    private boolean bfs(Vertex source, Vertex destination, List<Edge> path) {
        Queue<Vertex> queue = new LinkedList<Vertex>();
        List<Vertex> visited = new ArrayList<Vertex>(); // list of visited vertexes
        queue.add(source);
        //source.setVisited(true);
        visited.add(source);
        while(!queue.isEmpty()) {
            Vertex d = queue.remove();
            if(!d.equals(destination)) {
                ArrayList<Edge> d_outgoingEdges = outgoingEdges.get(d);
                for(Edge e : d_outgoingEdges) {
                    if(e.getCapacity() - e.getFlow() > 0) { // there is still available flow
                        Vertex u = e.getDestination();
                        if(!visited.contains(u)) {
                            //u.setVisited(true);
                            visited.add(u);
                            queue.add(u);
                            path.add(e);
                        }
                    }
                }
            }
        }
        if(visited.contains(destination)) {
            return true;
        }
        return false;
    }
}

Грань

public class Edge {

    private Vertex source;
    private Vertex destination;
    private float flow;
    private final float capacity;
    private Edge opposite;

    public Edge(Vertex source, Vertex destination, float capacity) {
        this.source = source;
        this.destination = destination;
        this.capacity = capacity;
    }

    public Edge getOpposite() {
        return opposite;
    }

    public void setOpposite(Edge e) {
        opposite = e;
    }

    public void setSource(Vertex v) {
        source = v;
    }

    public void setDestination(Vertex v) {
        destination = v;
    }

    public void addFlow(float f) throws Exception {
        if(flow == capacity) {
            throw new Exception("Unable to add flow");
        }
        flow += f;
    }

    public Vertex getSource() {
        return source;
    }

    public Vertex getDestination() {
        return destination;
    }

    public float getFlow() {
        return flow;
    }

    public float getCapacity() {
        return capacity;
    }

    public boolean equals(Object o) {
        Edge e = (Edge)o;
        return e.getSource().equals(this.getSource()) &&       e.getDestination().equals(this.getDestination());
    }
}

Вершина

public class Vertex {

    private String label;

    public Vertex(String label) {
        this.label = label;
    }

    public boolean isVisited() {
        return visited;
    }

    public String getLabel() {
        return label;
    }

    public boolean equals(Object o) {
        Vertex v = (Vertex)o;
        return v.getLabel().equals(this.getLabel());
    }
}

person user3075478    schedule 30.11.2014    source источник
comment
IIRC, концепция связывания каждого края с противоположным краем является обычным способом здесь (хотя детали реализации могут различаться). Можете более подробно описать проблему? Это связано с комментарием this updates only the local list...? Этого не должно быть, так как кажется, что ссылки ребер используются везде, а новые ребра не создаются. Если есть сомнения, вставьте метод printDebugInfo, который печатает все ребра и их потоки, и вызывайте этот метод после каждого шага обновления (используя небольшой график, чтобы вы могли легко проверить вывод с ручкой+бумагой)   -  person Marco13    schedule 30.11.2014
comment
Так для вас код в порядке? Это хорошая реализация? Прямо сейчас, если я запущу программу, она никогда не закончится. Тем не менее, я сделаю то, что вы говорите. PS: сеи итальяно?   -  person user3075478    schedule 30.11.2014
comment
На первый взгляд, код выглядит нормально, но я не пробовал его и не читал все подробности — я просто хотел знать, в чем проблема. Можно рассмотреть возможность разделения структуры графа и алгоритма, но подобные рекомендации скорее подходят для codereview.stackexchange.com . Но есть одна потенциальная проблема....   -  person Marco13    schedule 30.11.2014
comment
Потенциальная проблема: кажется, что вы никогда не очищаете флаги посещений узлов, вызывая node.setVisited(false) на всех узлах. Если он работает вечно, вывод отладки (как упоминалось выше) уже может быть полезен. Если нет, попробуйте создать stackoverflow.com/help/mcve (с основным методом и небольшим примером графика).   -  person Marco13    schedule 30.11.2014
comment
Хорошо, я изменил BFS. Теперь программа завершается, но возвращает 0... ммм. Выложу обновленный код.   -  person user3075478    schedule 30.11.2014


Ответы (1)


Хотя, строго говоря, этот вопрос можно рассматривать как «не по теме» (поскольку вы в основном ищете помощь по отладке), это один из ваших первых вопросов, поэтому несколько общих советов:


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

public class FFTest
{
    /**
     *     B---D
     *    / \ / \
     *   A   .   F
     *    \ / \ /
     *     C---E
     */
    public static void main(String[] args) throws Exception
    {
        FF ff = new FF();

        Vertex vA = new Vertex("A");
        Vertex vB = new Vertex("B");
        Vertex vC = new Vertex("C");
        Vertex vD = new Vertex("D");
        Vertex vE = new Vertex("E");
        Vertex vF = new Vertex("F");
        ff.insertNode(vA);
        ff.insertNode(vB);
        ff.insertNode(vC);
        ff.insertNode(vD);
        ff.insertNode(vE);
        ff.insertNode(vF);

        ff.insertEdge(vA, vB, 3.0f);
        ff.insertEdge(vA, vC, 2.0f);
        ff.insertEdge(vB, vD, 1.0f);
        ff.insertEdge(vB, vE, 4.0f);
        ff.insertEdge(vC, vD, 2.0f);
        ff.insertEdge(vC, vE, 1.0f);
        ff.insertEdge(vD, vF, 2.0f);
        ff.insertEdge(vE, vF, 1.0f);

        float result = ff.fordFulkerson(vA, vF);
        System.out.println(result);
    }
}

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


Вы должны четко указать, что вы НЕ используете StackOverflow как "волшебную машину для решения проблем". В этом случае: я уже упоминал, что вы должны включить выводы отладки. Если бы вы расширили свой класс FF такими методами....

private static void printPath(List<Edge> path)
{
    System.out.println("Path: ");
    for (int i=0; i<path.size(); i++)
    {
        Edge e = path.get(i);
        System.out.println(
            "Edge "+e+
            " flow "+e.getFlow()+
            " cap "+e.getCapacity());
    }
}

который можно вызвать в основном цикле следующим образом:

    while(bfs(source, destination, path)) {
        ...
        System.out.println("Before updating with "+minCap);
        printPath(path);

        // updates the maxFlow
        ....

        System.out.println("After  updating with "+minCap);
        printPath(path);

        ...
    }

то вы бы уже заметили основную проблему с вашим кодом: ...


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

Быстрый и грязный подход может выглядеть примерно(!) так:

private boolean bfs(Vertex source, Vertex destination, List<Edge> path) {
    Queue<Vertex> queue = new LinkedList<Vertex>();
    List<Vertex> visited = new ArrayList<Vertex>(); // list of visited vertexes
    queue.add(source);
    visited.add(source);
    Map<Vertex, Edge> predecessorEdges = new HashMap<Vertex, Edge>();
    while(!queue.isEmpty()) {
        Vertex d = queue.remove();
        if(!d.equals(destination)) {
            ArrayList<Edge> d_outgoingEdges = outgoingEdges.get(d);
            for(Edge e : d_outgoingEdges) {
                if(e.getCapacity() - e.getFlow() > 0) { // there is still available flow
                    Vertex u = e.getDestination();
                    if(!visited.contains(u)) {
                        visited.add(u);
                        queue.add(u);
                        predecessorEdges.put(u, e);
                    }
                }
            }
        }
        else
        {
            constructPath(destination, predecessorEdges, path);
            return true;
        }
    }
    return false;
}

private void constructPath(Vertex destination,
    Map<Vertex, Edge> predecessorEdges, List<Edge> path)
{
    Vertex v = destination;
    while (true)
    {
        Edge e = predecessorEdges.get(v);
        if (e == null)
        {
            return;
        }
        path.add(0, e);
        v = e.getSource();
    }
}

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


Дополнительные замечания:

Всякий раз, когда вы переопределяете метод equals, вы также должны переопределять метод hashCode. Здесь применяются некоторые правила, вам следует определенно обратиться к документация по Object#equals и Object#hashCode.

Часто полезно дополнительно переопределить метод toString, чтобы можно было легко вывести объекты на консоль.

В вашем случае эти методы можно было бы реализовать так, для Vertex

@Override
public int hashCode()
{
    return getLabel().hashCode();
}

@Override
public boolean equals(Object o) {
    Vertex v = (Vertex)o;
    return v.getLabel().equals(this.getLabel());
}

@Override
public String toString()
{
    return getLabel();
}

И для Edge

@Override
public String toString()
{
    return "("+getSource()+","+getDestination()+")";
}

@Override
public int hashCode()
{
    return source.hashCode() ^ destination.hashCode();
}

@Override
public boolean equals(Object o) {
    Edge e = (Edge)o;
    return e.getSource().equals(this.getSource()) &&       
        e.getDestination().equals(this.getDestination());
}

Пропускная способность ребер равна float значениям, поэтому результирующий поток также должен иметь значение float.

С указанными выше изменениями программа запускается и выводит «правдоподобный» результат. Я НЕ проверял его правильность. Но это ваша задача, и сейчас она должна быть намного проще.


P.S.: Нет, я соно тедеско.

person Marco13    schedule 30.11.2014
comment
Большое спасибо! Мой следующий вопрос будет лучше этого, обещаю. Теперь он возвращает поток, неправильный поток, но это что-то. Я использовал ваш метод печати для отладки, и кажется, что поток на краю может превышать его возможности! - person user3075478; 30.11.2014
comment
@ user3075478 В методе узкого места используется только емкость. Но он должен возвращать минимальный поток мощности всех ребер. (Часть емкости может быть уже занята определенным потоком, и это пока не считается). Но вы, вероятно, разберетесь с этим. - person Marco13; 30.11.2014
comment
Я могу это исправить ;) Мне также нужно сделать этот алгоритм как можно быстрее, потому что он должен работать с ОЧЕНЬ БОЛЬШИМИ графами. Еще раз большое спасибо! - person user3075478; 30.11.2014
comment
Есть некоторые библиотеки, которые уже предлагают реализации FordFulkerson и, возможно, других алгоритмов MaxFlow (которые могут быть даже более эффективными для больших графов!). Но это подразумевает рекомендацию библиотеки, и это определенно не относится к теме StackOverflow.... - person Marco13; 30.11.2014
comment
Мне нужно реализовать это самому, без библиотек;) - person user3075478; 01.12.2014