1.背景
之前做的一个视觉检测项目,本来各个视觉检测工具之间是独立的,单独执行图像输入、得出结果,然后综合汇总结果,简单明了。
后来要求工具之间能存在依赖关系,也就是A工具的输出可以作为B工具的输入,这样就很麻烦了,谁知道用户会怎么勾搭各个工具的输入和输出?怎么知道如何处理工具的执行顺序才能保持每个工具的依赖项都先于本工具执行?
当时是用链表简单处理了,处理得很粗糙,逻辑上不清晰。想优化却不知道怎么查找资料。
2.处理
最近,项目经理又来说这个问题了。
于是,静下心来找资料。结果发现,这个工具与工具之间的依赖、拓扑关系,是一个著名的概念:有向无环图(DAG)。
ok,既然找到资料的切入点,那就好办了。
处理DAG的最有效的C++库应该是这个【Boost Graph Library (BGL) 】,但是这个库貌似是依赖boost,有点大。暂时不用它,后面再看看。
目前打算用的是这个库【 OCEChain /DAG】,只有寥寥两个文件。已经基本能够满足我的需求了。先用它了。
3.实现
需要对它的源码进行一下修改
将DirectedAcyclicGraph.h的100行注释掉。
否则会报:
error: C2580: “DAG::Node<const int>::Node(const DAG::Node<const int> &)”: 不允许使用多个版本的有默认特殊成员函数
error C2580: “DAG::Node<const int>::Node(const DAG::Node<const int> &)”: 不允许使用多个版本的有默认特殊成员函数
查看对正在编译的 类 模板 实例化“DAG::Node<const int>”的引用
这个库是实现了有向无环图的表示,但是在添加子对象时,没有检查添加子对象后会不会造成环型;也没有实现拓扑排序布局。
还是差点意思。那我们来自己实现
3.1.闭环检测
闭环检测可以这样
// 检测将child接到parent时,是否会闭环
// 注意这个child parent的逻辑关系和实际输入输出的关系
template <typename T>
bool willItBeCyclic(const T *parent, const T *child)
{
// Start at node 0
DAG::BFSVisitor<T> bfs;
// 递归遍历子对象,看子对象中是否存在自己,存在自己则不能添加,因为会形成环
auto children = bfs.traverseChildren(*child);
auto it = std::find(children.begin(), children.end(), parent);
if(it != children.end())
{
return true;
}
return false;
}
3.2.拓扑排序
拓扑排序可以参考这里,自己实现一下(都是模板函数)。这里给出BFS、DFS两种排序的代码
【有向无环图与拓扑排序】
// 使用BFS(宽度优先)排序
template <typename T>
QVector<T *> topologicalSort_BFS(QVector<T *> nodeList)
{
// 下面涉及的入度,指的是节点的parent的个数
// 邻接节点,指的是节点的children
QVector<T*> sortedVector;
QHash<T*, bool> visited;
for (int i = 0; i < nodeList.length(); ++i) {
visited.insert(nodeList.at(i), false);
}
std::unordered_map<T*, int> inDegree; // 存储每个节点的入度
// 初始化每个顶点的入度
foreach (T* node, nodeList) {
inDegree[node] = node->parents().size();
}
// 将所有入度为0的顶点入队
std::queue<T*> q;
foreach (T* node, nodeList) {
if (inDegree[node] == 0) {
q.push(node);
}
}
// 使用宽优先搜索进行拓扑排序
while (!q.empty()) {
T* node = q.front();
q.pop();
sortedVector.push_back(node);
// 对于当前节点的每一个邻接顶点,减少其入度
for (auto child : node->children()) {
T* item = (T *)child;
inDegree[item]--;
// 如果邻接顶点的入度变为0,则将其入队
if (inDegree[item] == 0) {
q.push(item);
}
}
}
// 如果排序后的顶点数不等于图中的顶点数,说明图中存在环
if (sortedVector.size() != nodeList.length()) {
throw std::runtime_error("Graph has at least one cycle.");
}
return sortedVector;
}
// 使用DFS进行拓扑排序 DFS:深度优先搜索
template <typename T>
void topologicalSortUtil(T *node, QHash<T*, bool>& visited, QVector<T*>& vector) {
// 标记当前节点为已访问
if(visited[node] == true)
{
return;
}
visited[node] = true;
// 递归访问所有的子成员
for(auto child : node->children()) {
if(visited[(T*)child] == false) {
topologicalSortUtil((T*)child, visited, vector);
}
}
// 将当前顶点添加到push_front到vector中,vector的顺序即为拓扑顺序
vector.push_front(node);
}
// 输入节点列表,返回经过拓扑排序的节点列表
template <typename T>
QVector<T*> topologicalSort_DFS(QVector<T*> nodeList)
{
QVector<T*> sortedVector;
QHash<T*, bool> visited;
for (int i = 0; i < nodeList.length(); ++i) {
visited.insert(nodeList.at(i), false);
}
for(int i = 0; i < nodeList.length(); i++)
{
topologicalSortUtil<T>(nodeList.at(i), visited, sortedVector);
}
return sortedVector;
}
这样使用
typedef DAG::Node<const int> INode;
------
{
INode *n0 = new INode(0);
INode *n1 = new INode(1);
INode *n2 = new INode(2);
INode *n3 = new INode(3);
qDebug() << "will it be cycled:" << willItBeCyclic<INode>(n3, n0);
// 这里可以根据实际流向设置
n0->addChild(*n1);
n1->addChild(*n2);
n2->addChild(*n3);
QVector<INode*> nodeList;
nodeList << n0 << n1 << n2 << n3;
// 将DAG进行排序,从而确定数据的流动顺序
auto sortedList = topologicalSort_BFS<INode>(nodeList);
//auto sortedList = topologicalSort_DFS<INode>(nodeList);
qDebug() << "sorted vec:";
for(int i = 0; i < sortedList.length(); i++)
{
qDebug() << i << sortedList.at(i)->value();
}
}
4.总结
这么好用的东西为啥我现在才知道?