点击上方“AI公园”,关注公众号,选择加“星标“或“置顶”
导读作者:Vardan Grigoryan
编译:ronghuaiyang
知识图谱是AI领域非常有用的一种工具,知识图谱的基础就是图论,从今天开始,给大家介绍一些图论的基础内容,今天是第4篇,图的表示:结尾。
关于图的坏消息是,图表示没有一个单独的定义。这就是为什么你在库里找不到“std::graph”。我们已经有机会表示一个称为BST的“特殊”图。重点是,树是一个图,但图不是树。最后的插图向我们表明,我们在单个对象中有很多的树,“价格与房屋”和一些顶点是“不同”的类型,价格节点是图中只有价格的节点,指向具有特定的价格的整棵树的ID(home顶点)。它更像是一种混合数据结构,而不是我们在教科书例子中看到的简单的图。
这是图表示的关键点,图表示没有固定的结构(不像BSTs使用指定的基于节点的表示和左/右子指针,尽管你也可以使用单个数组表示BST)。你可以用你希望的最方便的方式来表示一个图(对于特定的问题最方便),最重要的是你可以将它“视为”一个图。我们所说的“看图”是指应用特定于图的算法。
那么n元树呢,它更像一个图。
首先想到的表示一个n元树节点是这样的:
struct NTreeNode
{
T value;
vector<NTreeNode*> children;
};
这个结构只表示树的一个节点。完整的树看起来更像这样:
// almost pseudocode
class NTree
{
public:
void Insert(const T&);
void Remove(const T&);
// lines of omitted code
private:
NTreeNode* root_;
};
这个类是围绕一个名为 root_
的树节点的抽象。这就是我们构建任何大小的树所需要的。这是树的起点。要添加一个新的树节点,我们需要为它分配一个内存,并将该节点添加到树的根中。
图很像n元树,只是略有不同。试着去发现它。
这是一个图表?不是,我的意思是,是的,但是它和之前的图中的n元树是一样的,只是稍微旋转了一下。根据经验,无论何时看到一棵树(即使是苹果树、柠檬树或二叉搜索树),都可以确定它也是一个图。因此,设计一个图节点(图顶点)的结构,我们可以得到相同的结构:
struct GraphNode
{
T value;
vector<GraphNode*> adjacent_nodes;
};
这足以构成一个图表吗?嗯,没有。这是为什么。看看这两张图从之前的插图,找到一个不同之处:
上面插图中的图没有一个点可以“enter”(它是一个森林而不是一棵树),相反,右边插图中的图没有不可到达的顶点。听起来很熟悉。
当每一对顶点之间都有一条路径时,图就是连通的
显然,对于“价格vs房屋”图,在每一对顶点之间并没有一条路径(如果从图中看不出来,就假设房价之间没有联系)。虽然这只是一个例子,说明我们无法使用单个GraphNode结构构造一个图,但在某些情况下,我们必须处理这样的断开连接的图。看看这个类:
class ConnectedGraph
{
public:
// API
private:
GraphNode* root_;
};
正如n元树是围绕单个节点(根节点)构建的一样,连通图也可以围绕根节点构建。树是有根的,也就是说它们有一个起点。一个连通图可以表示为一个有根树(还有几个属性),这已经很明显了,但是请记住,实际的表示可能会因算法而异,甚至对于一个连通图,也可能因问题而异。但是,考虑到图的节点属性,断开连接的图可以表示为:
class DisconnectedGraphOrJustAGraph
{
public:
// API
private:
std::vector<GraphNode*> all_roots_;
};
对于像DFS/BFS这样的图遍历,使用树状表示是很自然的。非常有用。但是,像高效路径跟踪这样的情况需要不同的表示。还记得欧拉图吗?要跟踪一个图的“欧拉值”,我们应该跟踪其中的欧拉路径。这意味着通过只遍历每条边一次来访问所有顶点,当跟踪完成并且我们有未遍历的边时,图就没有欧拉路径,因此不是欧拉图。
还有一种更快的方法,我们可以检查顶点的度数(假设每个顶点都存储它的度数),正如定义所说,如果一个图有奇数度的顶点,并且没有恰好两个,那么它就不是欧拉图。这种检查的复杂度是O(|V|),其中|V|是图顶点的数量。我们可以在插入新边时跟踪奇/偶度,从而将奇/偶度检查增加到O(1)。我们只需要跟踪一个图形。下面是图的表示形式和返回路径的Trace()函数。
// A representation of a graph with both vertex and edge tables
// Vertex table is a hashtable of edges (mapped by label)
// Edge table is a structure with 4 fields
// VELO = Vertex Edge Label Only (e.g. no vertex payloads)
class ConnectedVELOGraph {
public:
struct Edge {
Edge(const std::string& f, const std::string& t)
: from(f)
, to(t)
, used(false)
, next(nullptr)
{}
std::string ToString() {
return (from + " - " + to + " [used:" + (used ? "true" : "false") + "]");
}
std::string from;
std::string to;
bool used;
Edge* next;
};
ConnectedVELOGraph() {}
~ConnectedVELOGraph() {
vertices_.clear();
for (std::size_t ix = 0; ix < edges_.size(); ++ix) {
delete edges_[ix];
}
}
public:
void InsertEdge(const std::string& from, const std::string& to) {
Edge* e = new Edge(from, to);
InsertVertexEdge_(from, e);
InsertVertexEdge_(to, e);
edges_.push_back(e);
}
public:
void Print() {
for (auto elem : edges_) {
std::cout << elem->ToString() << std::endl;
}
}
std::vector<std::string> Trace(const std::string& v) {
std::vector<std::string> path;
Edge* e = vertices_[v];
while (e != nullptr) {
if (e->used) {
e = e->next;
} else {
e->used = true;
path.push_back(e->from + ":-:" + e->to);
e = vertices_[e->to];
}
}
return path;
}
private:
void InsertVertexEdge_(const std::string& label, Edge* e) {
if (vertices_.count(label) == 0) {
vertices_[label] = e;
} else {
vertices_[label]->next = e;
}
}
private:
std::unordered_map<std::string, Edge*> vertices_;
std::vector<Edge*> edges_;
};
小心bugs,bugs无处不在。这段代码包含了很多假设,比如标签,通过一个顶点我们可以理解一个字符串标签。当然,你可以轻松地将其更新为你想要的任何内容。在本例的上下文中并不重要。接下来,命名。正如评论中提到的,VELOGraph只用于顶点边标签图(这是我编的)。重点是,这个图表示包含一个表,用于映射一个顶点标签,其中的边与该顶点关联,以及一个包含一对顶点(由特定的边连接)的边列表和一个仅由Trace()函数使用的标志。看一下Trace()函数的实现。它使用edge的标志来标记已经遍历的edge(应该在任何Trace()调用之后重置标志)。
(未完待续)
— END—英文原文:https://medium.com/free-code-camp/i-dont-understand-graph-theory-1c96572a1401
请长按或扫描二维码关注本公众号
喜欢的话,请给我个好看吧!