从今天早些时候的评论扩展。
让我们使用 Dijkstra 算法来寻找节点之间的路径。
首先我们需要定义一个节点,对我们来说,我们至少需要一个位置和邻居容器,但是对于算法来说,有一个标志来表示我们是否访问过节点,以及一个数字来表示从开始到现在计算的最短距离。这可能看起来像:
struct Node {
Node(const sf::Vector2f& position) :
position{ position },
visited{}, tentative_distance{}
{}
sf::Vector2f position;
bool visited;
float tentative_distance;
};
它还有助于绘制一个节点及其与其邻居的连接,所以让我们从sf::Drawable 继承并编写一个函数来渲染节点。我还添加了一个高亮标志以突出显示最终路径。
struct Node : public sf::Drawable {
Node(const sf::Vector2f& position) :
position{ position },
visited{}, tentative_distance{}, highlight{}
{}
sf::Vector2f position;
bool visited;
float tentative_distance;
bool highlight;
std::vector<Node*> neighbours;
private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const {
// draw node circle
constexpr auto radius = 8.f;
auto color =
highlight ? sf::Color::White : sf::Color(127, 127, 127);
sf::CircleShape shape(radius);
shape.setOrigin({ radius, radius });
shape.setPosition(position);
shape.setFillColor(color);
target.draw(shape, states);
// draw lines to neighbours
for (const auto& neighbour : neighbours) {
color =
highlight && neighbour->highlight ?
sf::Color::White :
sf::Color(127, 127, 127);
sf::Vector2f delta = neighbour->position - position;
sf::Vertex line[] = {
{ position, color },
{ position + delta / 2.f, color }
};
target.draw(
line,
2,
sf::Lines
);
}
}
};
最后,我们将需要比较节点的距离,所以我们将编写一个成员函数来执行此操作。
...
float distance(Node& rhs) const {
const auto delta = rhs.position - position;
return sqrt(pow(delta.x, 2) + pow(delta.y, 2));
}
...
太棒了,我们现在可以存储节点并绘制它们。
让我们定义一个包含我们的节点的节点类,并具有 Dijkstra 函数。再一次,我希望它是可绘制的,所以它也将继承自 sf::Drawable。
class Nodes : public sf::Drawable {
public:
void dijkstra() {
// todo: algorithm
}
private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const {
for (const auto& node : nodes) {
target.draw(node, states);
}
}
std::vector<Node> nodes;
};
现在我们将创建一个构造器来创建节点。我们可以通过多种方式创建节点。过去,我编写了工具来编辑节点并保存到 JSON 文件,但为了本示例的目的,我们将使用一个简单的构造函数来读取字符串并在字符串中的每个 # 处创建一个节点。请注意,如果您想连接节点 A 和节点 B,则节点 A 必须在其邻居中有节点 B,反之亦然,您可能需要编写一个函数来确保这种双向连接。
Nodes() {
// create maze (for testing purposes)
const std::string maze{ R"(#####
# #
#####
# #
# ###
# #
####
#
#### )" };
// create nodes
sf::Vector2f position;
constexpr auto increment = 32.f;
for (const auto character : maze) {
switch (character) {
case '\n':
position.x = 0.f;
position.y += increment;
break;
case '#':
nodes.emplace_back(position);
case ' ':
position.x += increment;
break;
}
}
// connect to neighbours
for (size_t i = 0; i < nodes.size(); ++i) {
for (size_t j = i + 1; j < nodes.size(); ++j) {
if (nodes[i].distance(nodes[j]) <= increment + 1.f) {
nodes[i].neighbours.push_back(&nodes[j]);
nodes[j].neighbours.push_back(&nodes[i]);
}
}
}
}
好的,现在让我们查看我们的节点。
太棒了。现在开始编写算法。
我不会解释算法,但我会实现它。
对于未来的任务,我建议使用基于 Dijkstra 算法的改进算法,例如 A*。
- 标记所有未访问的节点。创建一个包含所有未访问节点的集合,称为未访问集。
unordered_set<Node*> unvisited;
for (auto& node : nodes) {
node.visited = false;
node.highlight = false;
unvisited.insert(&node);
}
- 为每个节点分配一个暂定距离值:对于我们的初始节点将其设置为零,对于所有其他节点将其设置为无穷大。将初始节点设置为当前节点。
初始节点和目标节点在这里被硬编码!将索引或其他任何内容传入函数以允许调用两个节点之间的任何路径。
Node& initial = nodes.front();
Node& destination = nodes.back();
initial.tentative_distance = 0.f;
constexpr auto infinity = std::numeric_limits<float>::infinity();
for (size_t i = 1; i < nodes.size(); ++i) {
nodes[i].tentative_distance = infinity;
}
Node* current = &initial;
- 对于当前节点,考虑其所有未访问的邻居并计算它们通过当前节点的暂定距离。将新计算的暂定距离与当前分配的值进行比较,并分配较小的那个。例如,如果当前节点 A 的距离为 6,连接它与邻居 B 的边的长度为 2,那么通过 A 到 B 的距离将是 6 + 2 = 8。如果 B 之前用大于 8 的距离,然后将其更改为 8。否则,将保留当前值。
for (auto* neighbour : current->neighbours) {
if (neighbour->visited) {
continue;
}
// Compare the newly calculated tentative distance to the
// current assigned value and assign the smaller one.
const auto neighbour_distance = current->distance(*neighbour);
neighbour->tentative_distance = std::min(
neighbour->tentative_distance,
current->tentative_distance + neighbour_distance
);
}
- 当我们考虑完当前节点的所有未访问邻居后,将当前节点标记为已访问并将其从未访问集中删除。被访问的节点将永远不会被再次检查。
current->visited = true;
unvisited.erase(current);
- 如果目标节点已被标记为已访问(在规划两个特定节点之间的路线时),或者如果未访问集中节点之间的最小暂定距离为无穷大(在计划完整遍历时;发生在两个特定节点之间没有连接时)初始节点和剩余的未访问节点),然后停止。算法已完成。
Node* smallest_tentative_distance{};
for (auto* node : unvisited) {
if (
!smallest_tentative_distance ||
node->tentative_distance <
smallest_tentative_distance->tentative_distance
) {
smallest_tentative_distance = node;
}
}
if (destination.visited) {
// trace path back and highlight it
while (current != &initial) {
current->highlight = true;
Node* smallest_distance{};
for (auto* node : current->neighbours) {
if (
!smallest_distance ||
node->tentative_distance <
smallest_distance->tentative_distance
) {
smallest_distance = node;
}
}
current = smallest_distance;
}
current->highlight = true;
break;
}
if (smallest_tentative_distance->tentative_distance == infinity) {
break;
}
- 否则,选择标记为最小暂定距离的未访问节点,设置为新的“当前节点”,返回步骤3。
请注意,我们现在需要将步骤 3、4、5 和 6 包装在一个循环中。
while (true) {
...
current = smallest_tentative_distance;
}
这就是实现的算法!现在调用它!
万岁。这是完成的困难部分。我没有过多地测试我的代码,也没有优化它或任何东西,这只是一个例子,我建议你自己尝试。目前我们只是将结果可视化,但您应该能够弄清楚如何使某些东西遵循路径。
过去我将计算出的路径存储在位置容器中(路径中每个节点的位置),然后让对象向容器后面的位置移动,然后一旦对象通过位置(x 或 y 符号已更改)弹出容器的背面并重复,直到容器为空。
完整示例代码:
#include <SFML/Graphics.hpp>
#include <vector>
#include <unordered_set>
struct Node : public sf::Drawable {
Node(const sf::Vector2f& position) :
position{ position },
visited{}, tentative_distance{}, highlight{}
{}
sf::Vector2f position;
bool visited;
float tentative_distance;
bool highlight;
std::vector<Node*> neighbours;
/// returns distance between this node and passed in node
float distance(Node& rhs) const {
const auto delta = rhs.position - position;
return sqrt(pow(delta.x, 2) + pow(delta.y, 2));
}
private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const {
// draw node circle
constexpr auto radius = 8.f;
auto color =
highlight ? sf::Color::White : sf::Color(127, 127, 127);
sf::CircleShape shape(radius);
shape.setOrigin({ radius, radius });
shape.setPosition(position);
shape.setFillColor(color);
target.draw(shape, states);
// draw lines to neighbours
for (const auto& neighbour : neighbours) {
color =
highlight && neighbour->highlight ?
sf::Color::White :
sf::Color(127, 127, 127);
sf::Vector2f delta = neighbour->position - position;
sf::Vertex line[] = {
{ position, color },
{ position + delta / 2.f, color }
};
target.draw(
line,
2,
sf::Lines
);
}
}
};
class Nodes : public sf::Drawable {
public:
Nodes() {
// create maze (for testing purposes)
const std::string maze{ R"(#####
# #
#####
# #
# ###
# #
####
#
#### )" };
// create nodes
sf::Vector2f position;
constexpr auto increment = 32.f;
for (const auto character : maze) {
switch (character) {
case '\n':
position.x = 0.f;
position.y += increment;
break;
case '#':
nodes.emplace_back(position);
case ' ':
position.x += increment;
break;
}
}
// connect to neighbours
for (size_t i = 0; i < nodes.size(); ++i) {
for (size_t j = i + 1; j < nodes.size(); ++j) {
if (nodes[i].distance(nodes[j]) <= increment + 1.f) {
nodes[i].neighbours.push_back(&nodes[j]);
nodes[j].neighbours.push_back(&nodes[i]);
}
}
}
}
void dijkstra() {
using namespace std;
// 1. Mark all nodes unvisited.
// Create a set of all the unvisited nodes called the unvisited set.
unordered_set<Node*> unvisited;
for (auto& node : nodes) {
node.visited = false;
node.highlight = false;
unvisited.insert(&node);
}
// 2. Assign to every node a tentative distance value:
// set it to zero for our initial node
// and to infinity for all other nodes.
Node& initial = nodes.front();
Node& destination = nodes.back();
initial.tentative_distance = 0.f;
constexpr auto infinity = std::numeric_limits<float>::infinity();
for (size_t i = 1; i < nodes.size(); ++i) {
nodes[i].tentative_distance = infinity;
}
// Set the initial node as current.
Node* current = &initial;
while (true) {
// 3. For the current node, consider all of its unvisited neighbours
// and calculate their tentative distances through the current node.
for (auto* neighbour : current->neighbours) {
if (neighbour->visited) {
continue;
}
// Compare the newly calculated tentative distance to the
// current assigned value and assign the smaller one.
const auto neighbour_distance = current->distance(*neighbour);
neighbour->tentative_distance = std::min(
neighbour->tentative_distance,
current->tentative_distance + neighbour_distance
);
}
// 4. When we are done considering all of the unvisited neighbours
// of the current node, mark the current node as visited and remove
// it from the unvisited set.
current->visited = true;
unvisited.erase(current);
// 5. If the destination node has been marked visited
// (when planning a route between two specific nodes) or
// if the smallest tentative distance among the nodes in the
// unvisited set is infinity (when planning a complete traversal;
// occurs when there is no connection between the initial node and
// remaining unvisited nodes), then stop.
// The algorithm has finished.
Node* smallest_tentative_distance{};
for (auto* node : unvisited) {
if (
!smallest_tentative_distance ||
node->tentative_distance <
smallest_tentative_distance->tentative_distance
) {
smallest_tentative_distance = node;
}
}
if (destination.visited) {
// trace path back and highlight it
while (current != &initial) {
current->highlight = true;
Node* smallest_distance{};
for (auto* node : current->neighbours) {
if (
!smallest_distance ||
node->tentative_distance <
smallest_distance->tentative_distance
) {
smallest_distance = node;
}
}
current = smallest_distance;
}
current->highlight = true;
break;
}
if (smallest_tentative_distance->tentative_distance == infinity) {
break;
}
// 6. Otherwise, select the unvisited node that is marked with the
// smallest tentative distance, set it as the new "current node",
// and go back to step 3.
current = smallest_tentative_distance;
}
}
private:
void draw(sf::RenderTarget& target, sf::RenderStates states) const {
for (const auto& node : nodes) {
target.draw(node, states);
}
}
std::vector<Node> nodes;
};
int main() {
using namespace std;
Nodes nodes;
nodes.dijkstra();
sf::RenderWindow window(
sf::VideoMode(240, 360),
"Dijkstra!",
sf::Style::Default,
sf::ContextSettings(0, 0, 8)
);
window.setView({ { 64.f, 128.f }, { 240.f, 360.f } });
while (window.isOpen()) {
sf::Event event;
while (window.pollEvent(event)) {
if (event.type == sf::Event::Closed)
window.close();
}
window.clear();
window.draw(nodes);
window.display();
}
return 0;
}