1
0
Fork 0
arangodb/arangod/Graph/KShortestPathsFinder.cpp

423 lines
14 KiB
C++

////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2014-2016 ArangoDB GmbH, Cologne, Germany
/// Copyright 2004-2014 triAGENS GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
///
/// @author Markus Pfeiffer
////////////////////////////////////////////////////////////////////////////////
#include "KShortestPathsFinder.h"
#include "Aql/AqlValue.h"
#include "Cluster/ServerState.h"
#include "Graph/EdgeCursor.h"
#include "Graph/EdgeDocumentToken.h"
#include "Graph/ShortestPathOptions.h"
#include "Graph/ShortestPathResult.h"
#include "Graph/TraverserCache.h"
#include "Transaction/Helpers.h"
#include "Utils/OperationCursor.h"
#include "VocBase/LogicalCollection.h"
#include <velocypack/Iterator.h>
#include <velocypack/Slice.h>
#include <velocypack/StringRef.h>
#include <velocypack/velocypack-aliases.h>
using namespace arangodb;
using namespace arangodb::graph;
//
KShortestPathsFinder::KShortestPathsFinder(ShortestPathOptions& options)
: ShortestPathFinder(options), _pathAvailable(false) {}
KShortestPathsFinder::~KShortestPathsFinder() = default;
// Sets up k-shortest-paths traversal from start to end
bool KShortestPathsFinder::startKShortestPathsTraversal(
arangodb::velocypack::Slice const& start, arangodb::velocypack::Slice const& end) {
TRI_ASSERT(start.isString());
TRI_ASSERT(end.isString());
_start = arangodb::velocypack::StringRef(start);
_end = arangodb::velocypack::StringRef(end);
_pathAvailable = true;
_shortestPaths.clear();
_candidatePaths.clear();
TRI_IF_FAILURE("TraversalOOMInitialize") {
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
}
return true;
}
bool KShortestPathsFinder::computeShortestPath(VertexRef const& start, VertexRef const& end,
std::unordered_set<VertexRef> const& forbiddenVertices,
std::unordered_set<Edge> const& forbiddenEdges,
Path& result) {
Ball left(start, FORWARD);
Ball right(end, BACKWARD);
VertexRef join;
result.clear();
auto currentBest = std::optional<double>{};
// We will not improve anymore if we have found a best path and the smallest
// combined distance between left and right is bigger than that path
while (!left._frontier.empty() && !right._frontier.empty() &&
!(currentBest.has_value() &&
(left._closest + right._closest > currentBest.value()))) {
_options.isQueryKilledCallback();
// Choose the smaller frontier to expand.
if (left._frontier.size() < right._frontier.size()) {
advanceFrontier(left, right, forbiddenVertices, forbiddenEdges, join, currentBest);
} else {
advanceFrontier(right, left, forbiddenVertices, forbiddenEdges, join, currentBest);
}
}
if (currentBest.has_value()) {
reconstructPath(left, right, join, result);
return true;
} else {
// No path found
return false;
}
}
void KShortestPathsFinder::computeNeighbourhoodOfVertexCache(VertexRef vertex,
Direction direction,
std::vector<Step>*& res) {
auto lookup = _vertexCache.try_emplace(vertex, FoundVertex(vertex)).first;
auto& cache = lookup->second; // want to update the cached vertex in place
switch (direction) {
case BACKWARD:
if (!cache._hasCachedInNeighbours) {
computeNeighbourhoodOfVertex(vertex, direction, cache._inNeighbours);
cache._hasCachedInNeighbours = true;
}
res = &cache._inNeighbours;
break;
case FORWARD:
if (!cache._hasCachedOutNeighbours) {
computeNeighbourhoodOfVertex(vertex, direction, cache._outNeighbours);
cache._hasCachedOutNeighbours = true;
}
res = &cache._outNeighbours;
break;
default:
TRI_ASSERT(false);
}
}
void KShortestPathsFinder::computeNeighbourhoodOfVertex(VertexRef vertex, Direction direction,
std::vector<Step>& steps) {
std::unique_ptr<EdgeCursor> edgeCursor;
switch (direction) {
case BACKWARD:
edgeCursor.reset(_options.nextReverseCursor(vertex));
break;
case FORWARD:
edgeCursor.reset(_options.nextCursor(vertex));
break;
default:
TRI_ASSERT(false);
}
// TODO: This is a bit of a hack
if (_options.useWeight()) {
auto callback = [&](EdgeDocumentToken&& eid, VPackSlice edge, size_t cursorIdx) -> void {
if (edge.isString()) {
VPackSlice doc = _options.cache()->lookupToken(eid);
double weight = _options.weightEdge(doc);
if (edge.compareString(vertex.data(), vertex.length()) != 0) {
VertexRef id = _options.cache()->persistString(VertexRef(edge));
steps.emplace_back(std::move(eid), id, weight);
}
} else {
VertexRef other(transaction::helpers::extractFromFromDocument(edge));
if (other == vertex) {
other = VertexRef(transaction::helpers::extractToFromDocument(edge));
}
if (other != vertex) {
VertexRef id = _options.cache()->persistString(other);
steps.emplace_back(std::move(eid), id, _options.weightEdge(edge));
}
}
};
edgeCursor->readAll(callback);
} else {
auto callback = [&](EdgeDocumentToken&& eid, VPackSlice edge, size_t cursorIdx) -> void {
if (edge.isString()) {
if (edge.compareString(vertex.data(), vertex.length()) != 0) {
VertexRef id = _options.cache()->persistString(VertexRef(edge));
steps.emplace_back(std::move(eid), id, 1);
}
} else {
VertexRef other(transaction::helpers::extractFromFromDocument(edge));
if (other == vertex) {
other = VertexRef(transaction::helpers::extractToFromDocument(edge));
}
if (other != vertex) {
VertexRef id = _options.cache()->persistString(other);
steps.emplace_back(std::move(eid), id, 1);
}
}
};
edgeCursor->readAll(callback);
}
}
void KShortestPathsFinder::advanceFrontier(Ball& source, Ball const& target,
std::unordered_set<VertexRef> const& forbiddenVertices,
std::unordered_set<Edge> const& forbiddenEdges,
VertexRef& join,
std::optional<double>& currentBest) {
VertexRef vr;
DijkstraInfo *v, *w;
std::vector<Step>* neighbours;
bool success = source._frontier.popMinimal(vr, v);
TRI_ASSERT(v != nullptr);
TRI_ASSERT(vr == v->_vertex);
if (!success) {
return;
}
computeNeighbourhoodOfVertexCache(vr, source._direction, neighbours);
TRI_ASSERT(neighbours != nullptr);
for (auto& s : *neighbours) {
if (forbiddenEdges.find(s._edge) == forbiddenEdges.end() &&
forbiddenVertices.find(s._vertex) == forbiddenVertices.end()) {
double weight = v->_weight + s._weight;
auto lookup = source._frontier.find(s._vertex);
if (lookup != nullptr) {
if (lookup->_weight > weight) {
source._frontier.lowerWeight(s._vertex, weight);
lookup->_pred = vr;
lookup->_edge = s._edge;
lookup->_weight = weight;
}
} else {
source._frontier.insert(s._vertex,
std::make_unique<DijkstraInfo>(s._vertex, std::move(s._edge),
vr, weight));
}
}
}
v->_done = true;
source._closest = v->_weight;
w = target._frontier.find(v->_vertex);
if (w != nullptr && w->_done) {
// The total weight of the found path
double totalWeight = v->_weight + w->_weight;
if (!currentBest.has_value() || totalWeight < currentBest.value()) {
join = v->_vertex;
currentBest = totalWeight;
}
}
}
void KShortestPathsFinder::reconstructPath(Ball const& left, Ball const& right,
VertexRef const& join, Path& result) {
result.clear();
TRI_ASSERT(!join.empty());
result._vertices.emplace_back(join);
DijkstraInfo* it;
it = left._frontier.find(join);
TRI_ASSERT(it != nullptr);
double startToJoin = it->_weight;
result._weight = startToJoin;
while (it != nullptr && it->_weight > 0) {
result._vertices.push_front(it->_pred);
result._edges.push_front(it->_edge);
result._weights.push_front(it->_weight);
it = left._frontier.find(it->_pred);
}
// Initial vertex has weight 0
result._weights.push_front(0);
it = right._frontier.find(join);
TRI_ASSERT(it != nullptr);
double joinToEnd = it->_weight;
result._weight += joinToEnd;
while (it != nullptr && it->_weight > 0) {
result._vertices.emplace_back(it->_pred);
result._edges.emplace_back(it->_edge);
it = right._frontier.find(it->_pred);
TRI_ASSERT(it != nullptr); // should run into 0 weight before
result._weights.emplace_back(startToJoin + (joinToEnd - it->_weight));
}
TRI_IF_FAILURE("TraversalOOMPath") {
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
}
}
bool KShortestPathsFinder::computeNextShortestPath(Path& result) {
std::unordered_set<VertexRef> forbiddenVertices;
std::unordered_set<Edge> forbiddenEdges;
Path tmpPath, candidate;
TRI_ASSERT(!_shortestPaths.empty());
auto& lastShortestPath = _shortestPaths.back();
bool available = false;
for (size_t i = lastShortestPath._branchpoint; i + 1 < lastShortestPath.length(); ++i) {
auto& spur = lastShortestPath._vertices.at(i);
forbiddenVertices.clear();
forbiddenEdges.clear();
// Must not use vertices on the prefix
for (size_t j = 0; j < i; ++j) {
forbiddenVertices.emplace(lastShortestPath._vertices[j]);
}
// TODO: This can be done more efficiently by storing shortest
// paths in a prefix/postfix tree
// previous paths with same prefix must not be
for (auto const& p : _shortestPaths) {
bool eq = true;
for (size_t e = 0; e < i; ++e) {
if (!p._edges.at(e).equals(lastShortestPath._edges.at(e))) {
eq = false;
break;
}
}
if (eq && (i < p._edges.size())) {
forbiddenEdges.emplace(p._edges.at(i));
}
}
if (computeShortestPath(spur, _end, forbiddenVertices, forbiddenEdges, tmpPath)) {
candidate.clear();
candidate.append(lastShortestPath, 0, i);
candidate.append(tmpPath, 0, tmpPath.length() - 1);
candidate._branchpoint = i;
auto it = find_if(_candidatePaths.begin(), _candidatePaths.end(),
[candidate](Path const& v) {
return v._weight >= candidate._weight;
});
if (it == _candidatePaths.end() || !(*it == candidate)) {
_candidatePaths.emplace(it, candidate);
}
}
}
if (!_candidatePaths.empty()) {
auto const& p = _candidatePaths.front();
result.clear();
result.append(p, 0, p.length() - 1);
result._branchpoint = p._branchpoint;
_candidatePaths.pop_front();
available = true;
}
return available;
}
bool KShortestPathsFinder::getNextPath(Path& result) {
bool available = false;
result.clear();
// TODO: this looks a bit ugly
if (_shortestPaths.empty()) {
if (_start == _end) {
TRI_ASSERT(!_start.empty());
result._vertices.emplace_back(_start);
result._weight = 0;
available = true;
} else {
available = computeShortestPath(_start, _end, {}, {}, result);
result._branchpoint = 0;
}
} else {
if (_start == _end) {
available = false;
} else {
available = computeNextShortestPath(result);
}
}
if (available) {
_shortestPaths.emplace_back(result);
_options.fetchVerticesCoordinator(result._vertices);
TRI_IF_FAILURE("TraversalOOMPath") {
THROW_ARANGO_EXCEPTION(TRI_ERROR_DEBUG);
}
}
_pathAvailable = available;
return available;
}
bool KShortestPathsFinder::getNextPathShortestPathResult(ShortestPathResult& result) {
Path path;
result.clear();
if (getNextPath(path)) {
result._vertices = path._vertices;
result._edges = path._edges;
return true;
} else {
return false;
}
}
bool KShortestPathsFinder::getNextPathAql(arangodb::velocypack::Builder& result) {
Path path;
if (getNextPath(path)) {
result.clear();
result.openObject();
result.add(VPackValue("edges"));
result.openArray();
for (auto const& it : path._edges) {
_options.cache()->insertEdgeIntoResult(it, result);
}
result.close(); // Array
result.add(VPackValue("vertices"));
result.openArray();
for (auto const& it : path._vertices) {
_options.cache()->insertVertexIntoResult(it, result);
}
result.close(); // Array
if (_options.useWeight()) {
result.add("weight", VPackValue(path._weight));
} else {
// If not using weight, weight is defined as 1 per edge
result.add("weight", VPackValue(path._edges.size()));
}
result.close(); // Object
TRI_ASSERT(result.isClosed());
return true;
} else {
return false;
}
}