CimNet 使用说明

如果你之前编写过 C 语言,但是从未接触过 C++ 语言,或者不了解 C++11 标准的新特性,可以跳转到章节“关于 C++ 语言”快速了解 CimNet 使用的语言特性。

文件结构

CimNet 工具包含于 cimnet 文件夹内,由以下文件组成:

文件

内容概述

cimnet/_types.h

基础数据类型

cimnet/_exception.h

网络异常类

cimnet/_base_net.h

通用无向/有向网络类

cimnet/network.h

已实现的常用网络结构

cimnet/random.h

MT随机数生成

一般情况下,你只需要引用 cimnet/network.h 这个头文件,就可以使用默认的基础数据类型、网络异常类和有向/无向通用网络类。已实现的常用网络结构全部继承于通用无向网络,网络的节点编号类型为整型。

如果你想使用优秀的随机数生成器,可以引用 cimnet/random.h ,也可以使用 C++ 标准库中的随机数函数。

使用网络

1. 创建网络

你可以用下面的语句创建一个不含任何点边的空的无向网络:

Network<> net;

这个网络用 Id 类型作为节点编号的标识(见 CimNet 接口的基本数据类型)。每个节点的编号应该互不相同。创建一个空的有向网络:

DirectedNetwork<> net;

你也可以指定自己节点类型:

Network<std::string> net;

这样一来,在网络 net 中你可以使用 std::string 来唯一标识一个节点。

2. 编辑网络结构

你可以向网络中添加节点:

net.add_node(1);

网络中就增加了一个以 1 为编号的节点。如果你指定了节点编号类型为 std::string ,也可以用以下方式添加节点:

net.add_node("A");

你可以使用 has_node(id) 方法判断网络中是否有以 id 为编号的节点,以上面的网络为例,下列语句:

std::cout << net.has_node("A") << std::endl;
std::cout << net.has_node("Not existed") << std::endl;

会打印

true
false

两个节点间的连接关系称为边。向网络中添加一条边:

net.add_edge(1, 2);

如果网络 net 为无向网络,可以使用 has_edge(id1, id2) 方法判断网络中是否存在边, id1id2 的位置可以调换。你也可以使用 is_neighbor(id1, id2) 判断网络中 id1id2 是否为邻居节点,它的作用和 has_edge 是一样的。

如果网络 net 为有向网络,可以使用 has_successor(id1, id2) 判断节点 id1 是否存在后继节点 id2 ,使用 has_predecessor(id1, id2) 判断节点 id1 是否存在前序节点 id2 。有向网络也有 has_edge 方法,它和 has_successor 方法是等效的。有向网络的 is_neighbor(id1, id2) 方法在 id1id2 之间存在连边(无论是 id1 指向 id2id2 指向 id1 )时返回 true 。如果有向网络 net 中存在一条以节点 1 指向节点 2 的边,下列表达式:

net.has_successor(1, 2)
net.has_predecessor(2, 1)
net.has_edge(1, 2)
net.is_neighbor(1, 2)
net.is_neighbor(2, 1)

的值均为 true

3. 存储节点/边数据

在存储数据前需要在模板类处指定存储的数据类型。我们定义如下两种类型:

typedef std::string NodeDescribe;
typedef struct {
    int amount;
    double weight;
} EdgeDetail;

并且以这种方式声明网络,并添加一条边:

Network<Id, NodeDescribe, EdgeDetail> net;
net.add_edge(1, 2);

网络中便有了两个点和一条边。接下来你可以这样在网络的节点中添加数据:

net.node(1) = "First node";
net[2] = "Second node;

上面两个语句都能用来添加数据。第一条语句使用 node(id) 方法返回了节点 id 存储的引用,这使得你可以通过引用修改内部存储。网络类也提供了下标的方式返回节点引用(即第二条语句所示),这使得你可以更方便地存取网络节点的数据。在网络的边上添加数据也可以通过类似的方式:

net.edge(1, 2) = {3, 3.14};
net(1, 2) = {3, 3.14};

同理,第一条语句使用调用函数的形式访问边数据的引用,第二条语句是一种更简便的方式——它重载了这个类的括号操作符。当然,由于 edge(id1, id2) 方法返回的是 EdgeDetail 结构体的引用,你可以使用 . 直接修改内部成员:

net.edge(1, 2).amount = 4;
net(1, 2).amount = 4;

点数据和边数据都能存储其它任意的你定义过的类的对象,但是每个点之间或每条边之间存储的类型要一致。

在添加节点和边的方法里也可以直接添加数据:

net.add_node(1, "First node");
net.add_node(2, "Second node");
net.add_edge(1, 2, {3, 3.14});

4. 网络拷贝和转换

CimNet 提供拷贝构造器完成网络的拷贝操作。你可以将一个网络及其内部数据完全复制给另一个网络,此后前一个网络的修改不影响拷贝后的网络数据。只需要将原始网络作为参数传入新网络的初始化参数列表中即可。

Network<Id, NodeDescribe, EdgeDetail> net;
// Add nodes and edges to net
Network<Id, NodeDescribe, EdgeDetail> net_copy(net);
DirectedNetwork<Id, NodeDescribe, EdgeDetail> di_net_copy(net);

需要注意拷贝网络的模板参数需要保持一致,它的类既可以是 Network 也可以是 DirectedNetwork 。如果原网络是无向网络且新网络是有向网络,所有无向边会转化成两条有向且指向相反的连边。如果将有向网络拷贝为无向网络,所有的有向边失去方向,变为无向边;双向边则会变为一条无向边。

5. 网络数据和遍历

在无向网络中使用 degree(id) 方法可以获知节点 id 的度。在有向网络中, in_degree(id) 返回节点 id 的入度, out_degree(id) 返回节点 id 的出度, degree(id) 返回了入度和出度的和。

number_of_nodes() 方法会返回网络的节点数量, number_of_edges() 方法会返回网络边的数量。 total_degree() 方法会返回网络的总度数,它从数值上等于网络总边数的两倍。

你也可以直接使用 std::cout 输出流打印网络信息:

std::cout << net << std::endl;

它会简要打印网络的节点数、边数和总度数信息。

调用 nodes() 方法可以获取网络中所有的节点编号( std::vector<NodeId> ), edges() 方法可以获取网络中所有的边,它是一个点对的集合容器( std::unordered_set<std::pair<NodeId, NodeId>> )。 neighbors(id) 返回节点 id 的所有邻居( std::vector<NodeId> )。可以参考下面的方式遍历网络和邻居的节点编号:

for (auto &node : net.nodes())
    // Visit node
for (auto &edge : net.edges())
    // Visit edge.first and edge.second
for (auto &neighbor : net.neighbors())
    // Visit neighbor

其中由于边 edgestd::pair<NodeId, NodeId> 类型, C++11 中需要用 edge.firstedge.second 访问一条边中的两个节点编号。对于无向网络而言, edge.first 是一条边中较小的一个节点编号(数值较小或字符串字母序靠前的);有向网络中 edge.first 是前序节点, edge.second 是后继节点。

(如果你支持 C++17 及以上的编译环境,可以尝试将循环替换为 for (auto &[i, j] : net.edges()) ,其中 ij 等效于 edge.firstedge.second 。)

另外,对于无向网络而言, neighbors(id) 返回了节点 id 所有相邻节点编号。对于有向网络而言, neighbors(id) 返回了所有与节点 id 有关联(无论方向)的节点编号。有向网络还提供了 predecessors(id) 方法用来返回节点 id 的所有前序节点编号, successors() 方法用来返回节点 id 的所有后继节点编号,它们的遍历方式与 neighbors(id) 类似。

6. 已实现的网络结构

目前所有已实现的网络结构都是模板类,且节点编号都是 Id 类型的——所以模板只接受两个模板参数, NodeDataEdgeData ,它们默认都是 None 。这些网络的用法和通用网络类型基本一致,只是初始化时需要传入指定的参数。这里给出一个较为完整的程序实例:构建一个包含10个节点的规则网络,这个网络的每个节点都和周围6个邻居连边(即,每个节点向顺时针方向的3个邻居添加连边)。最后我们打印网络信息和一号节点的邻居节点编号。

 1#include "cimnet/network.h"
 2
 3int main(void) {
 4    RegularNetwork<> net(10, 3);
 5    std::cout << net << std::endl;
 6    std::cout << "Neighbors of node 1: ";
 7    for (auto &n : net.neighbors(1))
 8        std::cout << n << " ";
 9    std::cout << std::endl;
10    return 0;
11}

这段代码的第1行引用了网络结构的头文件。第4行用模板类定义了规则网络 net ,第7行遍历了网络中1号节点的邻居,变量 n 的值为每次遍历到的一个邻居节点的编号。