简体   繁体   中英

Find the most optimal way in a graph, starting and ending at the same vertex

I'm trying to solve the following problem, but need help.

Imagine we have a map of a town with different places we can visit, however we have a limited time to spend in this town and have to visit as many places as we can before reaching the limited time. We can represent this map as an undirected weighted graph, where the vertices are the places we can visit and the weights of the edges are how many minutes it takes to go from one place to another. The goal is to start from the Railway station, go to as many places as you can before reaching the maximum time limit, and in the end be back again at the Railway station.

For example, if we look at the picture, our result will be Railstation RomanStadium DzhumayaSquare ArtGallery AntiqueTheatre ArtGallery Railstation . The time constraint in this particular example is 68 min, so we start from the Railway station (always start from there), and then visit as many places as we can before reaching the 68 min. constrain (so this will mean in the example: 20 +2 + 5 + 7 + 7 + 26 <= 68), in the end we should be back at the Railway. It's important that we can repeat edges (like in the example) and vertices.

在此处输入图像描述

Unfortunately, I got nowhere, because I always enter some infinite loops. What really bugs me is that we can repeat edges and vertices and I can't think of a way to mark my visited vertices so that I don't enter an infinite loop. I can't think of a way to solve this task so every suggestion will help.

The way I represent my graph is as a matrix (I will share the implementation if someone needs it)

Here is my Graph.h file:

#ifndef GRAPH_IMPL_H
#define GRAPH_IMPL_H

#include<vector>
#include<cstddef>

class MatrixGraph {
    public:
    using vertex_t = unsigned int;
    using weight_t = unsigned int;

    private:
    std::vector<std::vector<weight_t>> data;

    public:
    MatrixGraph(size_t);

    void addVertex();
    void removeVertex(vertex_t);

    void addDirectedEdge(vertex_t from, vertex_t to, weight_t w);
    void addEdge(vertex_t from, vertex_t to, weight_t w);
    void removeEdge(vertex_t from, vertex_t to);

    bool validVertex(vertex_t) const;
    weight_t getWeight(vertex_t, vertex_t) const;

    size_t vertexCount() const;
    bool adjacent(vertex_t from, vertex_t to) const;

    std::vector<vertex_t> getSuccessors(vertex_t) const;
    std::vector<vertex_t> getPredecessors(vertex_t) const;
};

#endif

And here is the cpp:

using vertex_t = MatrixGraph::vertex_t;
using weight_t = MatrixGraph::weight_t;

MatrixGraph::MatrixGraph(size_t v) {
    data = std::vector<std::vector<weight_t>>(v, std::vector<weight_t>(v, 0));
}

bool MatrixGraph::validVertex(vertex_t v) const {
    return v < data.size();
}

bool MatrixGraph::adjacent(vertex_t from, vertex_t to) const {
    if(!validVertex(from) || !validVertex(to))
        throw "Invalid vertex";
    
    return data[from][to] > 0;
}

void MatrixGraph::addDirectedEdge(vertex_t from, vertex_t to, weight_t w) {
    if(!validVertex(from) || !validVertex(to))
        throw "Invalid vertex";
    
    data[from][to] = w;
}

void MatrixGraph::addEdge(vertex_t from, vertex_t to, weight_t w) {
    addDirectedEdge(from, to, w);
    addDirectedEdge(to, from, w);
}

void MatrixGraph::addVertex() {
    for(auto& v : data)
        v.push_back(0);
    
    data.push_back(std::vector<weight_t>());
}

void MatrixGraph::removeEdge(vertex_t from, vertex_t to) {
    if(!validVertex(from) || !validVertex(to))
        throw "Invalid vertex";
    
    data[from][to] = 0;
}

void MatrixGraph::removeVertex(vertex_t v) {
    if(!validVertex(v))
        throw "Invalid vertex";
    
    data.erase(data.begin() + v);

    for(auto& vertex : data)
        vertex.erase(vertex.begin() + v);
}

weight_t MatrixGraph::getWeight(vertex_t from, vertex_t to) const {
    if(!validVertex(from) || !validVertex(to))
        throw "Invalid vertex";
    
    return data[from][to];
}

std::vector<vertex_t> MatrixGraph::getSuccessors(vertex_t from) const {
    if(!validVertex(from))
        throw "Invalid vertex";

    std::vector<vertex_t> result;

    for (size_t i = 0; i < data.size(); i++)
        if(data[from][i] > 0) 
            result.push_back(i);

    return result;
}

std::vector<vertex_t> MatrixGraph::getPredecessors(vertex_t from) const {
    if(!validVertex(from))
        throw "Invalid vertex";

    std::vector<vertex_t> result;

    for (size_t i = 0; i < data.size(); i++)
        if(data[i][from] > 0) 
            result.push_back(i);

    return result;
}

size_t MatrixGraph::vertexCount() const {
    return data.size();
}

Here is a solution that starts by precalculating the minimal path between any 2 nodes in a adjacency-like matrix. It provides 2 things to the algorithm:

  1. Your question requires checking how to go back to the starting point. The matrix provides the cost for every node.
  2. As the algorithm know how to join any 2 nodes, there is a conceptual difference between nodes it visits and nodes it only traverses. To be precise, the algorithm only decides the nodes to visit, and these nodes must not appear more than once.
    It is only after the list is built that it is able to insert the nodes to traverse between the consecutive visited nodes. A node can be traversed any number of times (including when it visits it).

Note: in your question, I did not see any indication we must maximize the number of visited nodes while minimizing the costs . You will see what I mean if you call (in main ) g.walk(0, 86) or g.walk(0, 85) for instance.
I have hardcoded your above graph and left some helper methods and lines for you to uncomment; might provide some insight.

Graph.h

#pragma once

#include <string>
#include <vector>

/// @brief Class representing undirected weighted graph, limited to no more than 1 edge between every pair of nodes.
/// @details The class is specialized to find paths starting on a node and looping back to it, under a threshold.
/// As such, it does not keep an adjacency matrix, but we will call the matrix that keeps the weights the adjacency weights for the sake of conciseness.
class Graph
{
public:
    explicit Graph(size_t order);
    ~Graph();

    /// @brief Outputs the weights adjacency matrix to std::cout.
    void outputPathWeights(unsigned int width = 4);

    /// @brief Output the minimal paths between any 2 nodes.
    void outputAllMinimalPath();

    /// @brief Outputs a path to std::cout.
    /// @param path Path to be output.
    void outputPath(const std::vector<size_t> path) const;

    /// @brief Permutes the nodes, then finds the longest path that can be walked from`\p start, back to \p start and under \p threshold
    std::pair<size_t, std::vector<size_t>> walk(size_t start, uint32_t threshold);

protected:
    /// @brief Initialize the graph with hardcoded data.
    void initGraph();

    /// @brief Precalculates the matrix with minimum weights required to travel from any node to any other node (i.e. not the shortest path in terms of number of nodes visited).
    /// @details Precalculating the matrix this way makes it possible to:
    /// - Very easily know how costly it is to get back to the starting point of the path.
    /// - Create a difference between nodes the \c walk algorithm decides to visit and nodes it needs to go through. The \c walk algorithm only knows about the former and ignores the latter.
    /// @pre \c edge is initialized with the weights of individual edges or the max possible value if there is no edge between 2 nodes (and the diagonal)
    /// @post \c edge contains the minimal path weight between any 2 nodes (except the diagonal).
    void calculateMinPaths();

    /// @brief Permutes nodes to put all the nodes that can be reached from a given starting node to the left of
    /// @param start Starting node used to evaluate which other nodes can be reached.
    /// @param threshold Maximum cost of the path being searched.
    /// @param[out] remainingSize After the permutation, nodes that cannot be reached by any path starting from \p start and under \p threshold are push to the right of the adjacency matrix.
    void permute(size_t start, uint32_t threshold, size_t& remainingSize);

    /// @brief Swaps 2 nodes in the adjacency matrix.
    void swapNodes(size_t node1, size_t node2);

    /// @brief Walks a path to find the longest path, recursively.
    /// @details The function only cares about nodes it wants to "visit".
    /// @param start Start and end point of the path. Note that the end point is not added by this method.
    /// @param from Node the function is currently evaluating.
    /// @param threshold Maximum cost for acceptable path.
    /// @param[out] walked Path.
    /// @return Length of the path.
    size_t tryWalk(size_t start, size_t from, uint32_t threshold, std::vector<size_t>& walked) const;

private:
    size_t size;
    std::string* name;
    uint32_t** path;
    std::vector<size_t>** intermediates;
};

Graph.cpp

#include "Graph.h"

#include <algorithm>
#include <iostream>
#include <iomanip>
#include <memory>

Graph::Graph(size_t order)
    : size(order)
    , name(nullptr)
    , path(nullptr)
    , intermediates(nullptr)
{
    //Create objects
    name = new std::string[size];
    path = new uint32_t * [size];
    intermediates = new std::vector<size_t>*[size];

    //We force adjacencies to be 0 and weights to be the maximum possible value by default (especially the diagonal)
    for (size_t r = 0; r < size; ++r) {
        intermediates[r] = new std::vector<size_t>[size];
        path[r] = new uint32_t[size];
        for (size_t c = 0; c < size; ++c)
            path[r][c] = std::numeric_limits<uint32_t>::max();
    }
    initGraph();
}

Graph::~Graph()
{
    if (name)
        delete[] name;
    if (path) {
        for (size_t r = 0; r < size; ++r)
            delete[] path[r];
        delete[] path;
    }
    if (intermediates) {
        for (size_t r = 0; r < size; ++r)
            delete[] intermediates[r];
        delete[] intermediates;
    }
}

void Graph::initGraph()
{
    // Initialize the nodes
    name[0] = "Rail station";
    name[1] = "Roman stadium";
    name[2] = "Dzhumaya square";
    name[3] = "Art gallery";
    name[4] = "Historical museum";
    name[5] = "Antique theatre";
    name[6] = "Some bar";
    name[7] = "Some restaurant";
    name[8] = "Some cinema";
    name[9] = "Some hotel";

    ////Initialize the actual adjacency matrix
    //adjy[0][3] = adjy[3][0] = 1;
    //adjy[0][1] = adjy[1][0] = 1;
    //adjy[1][2] = adjy[2][1] = 1;
    //adjy[2][3] = adjy[3][2] = 1;
    //adjy[3][4] = path[4][3] = 1;
    //adjy[2][4] = adjy[4][2] = 1;
    //adjy[3][5] = adjy[5][3] = 1;
    //adjy[4][5] = adjy[5][4] = 1;
    //adjy[6][7] = adjy[7][6] = 1;
    //adjy[8][7] = adjy[7][8] = 1;
    //adjy[6][9] = adjy[9][6] = 1;

    //Initialize the weights
    path[0][3] = path[3][0] = 26;
    path[0][1] = path[1][0] = 20;
    path[1][2] = path[2][1] =  2;
    path[2][3] = path[3][2] =  5;
    path[3][4] = path[4][3] = 14;
    path[2][4] = path[4][2] = 18;
    path[3][5] = path[5][3] =  7;
    path[4][5] = path[5][4] = 12;
    path[6][7] = path[7][6] =  1;
    path[8][7] = path[7][8] =  2;
    path[6][9] = path[9][6] =  3;
    //path[0][6] = path[6][0] =  8; //Uncomment this  line for all nodes to be connected
}

void Graph::calculateMinPaths()
{
    bool lookForMinPath = true;
    while (lookForMinPath) {
        lookForMinPath = false;
        for (size_t r = 0; r < size; ++r) {
            for (size_t c = 1 + r; c < size; ++c) {
                //Look for the best intermediary node
                for (size_t i = 0; i < size; ++i) {
                    //Compare the individual edges (to prevent overflow) and their sum
                    if (path[r][i] < path[r][c] && path[i][c] < path[r][c] && path[r][i] + path[i][c] < path[r][c]) {
                        lookForMinPath = true;
                        path[r][c] = path[c][r] = path[r][i] + path[i][c];

                        intermediates[r][c].clear();
                        std::copy(intermediates[r][i].begin(), intermediates[r][i].end(), std::back_inserter(intermediates[r][c]));
                        intermediates[r][c].push_back(i);
                        std::copy(intermediates[i][c].begin(), intermediates[i][c].end(), std::back_inserter(intermediates[r][c]));

                        intermediates[c][r].clear();
                        std::copy(intermediates[c][i].begin(), intermediates[c][i].end(), std::back_inserter(intermediates[c][r]));
                        intermediates[c][r].push_back(i);
                        std::copy(intermediates[i][r].begin(), intermediates[i][r].end(), std::back_inserter(intermediates[c][r]));
                    }
                }
            }
        }
    }
}

void Graph::outputPathWeights(unsigned int width)
{
    std::cout << std::fixed << std::setfill(' ');
    for (size_t r = 0; r < size; ++r) {
        for (size_t c = 0; c < size; ++c) {
            if (r == c)
                std::cout << std::setw(width) << ' ';
            else if (path[r][c] == std::numeric_limits<uint32_t>::max())
                std::cout << std::setw(width) << 'X';
            else
                std::cout << std::setw(width) << path[r][c];
        }
        std::cout << std::endl;
    }
}

void Graph::outputAllMinimalPath()
{
    for (size_t r = 0; r < size; ++r) {
        for (size_t c = 0; c < size; ++c) {
            if (r != c && path[r][c] < std::numeric_limits<uint32_t>::max()) {
                std::cout << name[r] << '(' << r << ')';
                for (auto intermediary : intermediates[r][c])
                    std::cout << " > " << name[intermediary] << '(' << intermediary << ')';;
                std::cout << " > " << name[c] << '(' << c << ')' << std::endl;
            }
        }
    }
}

void Graph::outputPath(const std::vector<size_t> path) const
{
    if (path.size() > 0) {
        std::cout << name[path[0]];
        auto lastNode = path[0];
        std::for_each(path.begin() + 1, path.end(), [&](size_t n) {
            for (auto intermediary : intermediates[lastNode][n])
                std::cout << " > " << name[intermediary];
            std::cout << " > " << name[n];
            lastNode = n;
            });
    }
    std::cout << std::endl;
}

std::pair<size_t, std::vector<size_t>> Graph::walk(size_t start, uint32_t threshold)
{

    calculateMinPaths();
    //Uncomment to see the cost-minimal paths
    //outputPathWeights(3);
    //outputAllMinimalPath();
    //std::cout << std::endl;

    size_t workingSize = 0, cachedSize = size;
    permute(start, threshold, workingSize);
    size = workingSize;

    //Uncomment to see the effect of permute
    //outputPathWeights(3);
    //outputAllMinimalPath();
    //std::cout << std::endl;

    std::vector<size_t> visitPath;
    visitPath.reserve(1 + size);
    visitPath.push_back(start);

    auto cost = tryWalk(start, start, threshold, visitPath);

    if (visitPath.size() > 1) {
        auto lastNode = *(visitPath.end() - 1);
        cost += path[lastNode][start];
        visitPath.push_back(start);
    }
    size = cachedSize;
    return std::make_pair(cost, visitPath);
}

size_t Graph::tryWalk(size_t start, size_t from, uint32_t threshold, std::vector<size_t>& walked) const
{
    size_t result = 0;
    std::vector<size_t> longestWalk;
    std::copy(walked.begin(), walked.end(), std::back_inserter(longestWalk));
    for (int c = 0; c < size; ++c) {
        std::vector<size_t> continuedWalk;
        std::copy(walked.begin(), walked.end(), std::back_inserter(continuedWalk));
        if (c != from && path[from][c] + path[c][start] <= threshold && std::find(walked.begin(), walked.end(), c) == walked.end()) {
            continuedWalk.push_back(c);
            //Try to find the longest path starting at c, with the remaining threshold.
            auto pathCost = tryWalk(start, c, threshold - path[from][c], continuedWalk);
            //Select the longer path between the currently known longest path and the path we just tried to find.
            if (longestWalk.size() < continuedWalk.size()) {
                result = path[from][c] + pathCost;
                longestWalk.clear();
                std::copy(continuedWalk.begin(), continuedWalk.end(), std::back_inserter(longestWalk));
                
                if (longestWalk.size() == size)
                    break;
            }
        }
    }
    walked.clear();
    std::copy(longestWalk.begin(), longestWalk.end(), std::back_inserter(walked));
    return result;
}

void Graph::permute(size_t start, uint32_t threshold, size_t& remainingSize)
{
    if (start > 0)
        swapNodes(0, start);
    remainingSize = size;
    for (size_t c = 1; c < remainingSize; ++c) {
        if (path[0][c] > (threshold >> 1)) {
            remainingSize -= 1;
            swapNodes(c, remainingSize);
            c -= 1;
        }
    }
}

void Graph::swapNodes(size_t node1, size_t node2)
{
    if (node1 != node2 && node1 < size && node2 < size) {
        //Swap names
        std::swap(name[node1], name[node2]);
        //Swap rows
        std::swap(intermediates[node1], intermediates[node2]);
        std::swap(path[node1], path[node2]);
        //Swap columns
        for (size_t r = 0; r < size; ++r) {
            {
                auto cache = intermediates[r][node1];
                intermediates[r][node1] = intermediates[r][node2];
                intermediates[r][node2] = cache;
            } {
                auto cache = path[r][node1];
                path[r][node1] = path[r][node2];
                path[r][node2] = cache;
            }
        }
        //Swap the intermediates content too
        for (size_t r = 0; r < size; ++r) {
            for (size_t c = 0; c < size; ++c) {
                for (auto& intermediary : intermediates[r][c]) {
                    if (intermediary == node1)
                        intermediary = node2;
                    else if (intermediary == node2)
                        intermediary = node1;
                }
            }
        }
    }
}

main.cpp

#include "Graph.h"
#include <iostream>

int main()
{
    constexpr size_t graphOrder = 10;
 
    Graph g(graphOrder);
    
    //C++17
    //auto [cost, path] = g.walk(0, 85); 
    
    //C++14
    auto costAndPath = g.walk(0, 68);
    auto cost = std::get<0>(costAndPath);
    auto path = std::get<1>(costAndPath);

    //Note: There are fewer visited than traversed nodes.
    std::cout << "Found path (" << path.size() << " visited nodes) with cost : " << cost << std::endl;

    g.outputPath(path);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM