Boost Graph Library 快速入門

<!--[if !vml]--><!--[endif]-->

Boost Graph Library 快速入門

   圖領域的數據結構和算法在某些方面比容器更爲複雜,圖算法在圖中移動有着衆多的路線,而STL使用的抽象迭代器接口不能有效的支持這些。作爲替換,我們爲圖提供了一個的抽象的結構,其與容器迭代器的目的類似(儘管迭代器扮演着更大的角色)。圖1 描述了STL 和BGL 之間的對比
                        圖1: The analogy between the STL and the BGL.


  圖由一系列頂點vertices,以及連接頂點的邊edges組成. 如圖2描述了一個擁有5個頂點和11條邊的有向圖directed graph. 離開一個頂的邊稱爲該點的out-edges。邊 {(0,1),(0,2),(0,3),(0,4)} 都是節點0的out-edges ,進入一個頂點的邊稱爲該點的in-edges , 邊{(0,4),(2,4),(3,4)} 是節點0的in-edges

                             圖2 一個有向圖例子


在後面的章節中,我們使用BGL構造上圖並展示各種操作。全部的代碼可以在examples/quick_tour.cpp  找到,下面每個章節都是這個例子文件的一個片斷

構造一個圖

在這個例子中,我們將使用BGL 鄰接表adjacency_list 類來示範BGL接口中的主要概念.adjacency_list類提供了典型鄰接表數據結構的一個泛型版本. adjacency_list 是一個擁有6個模板參數的模板類。但我們只使用了前3個參數,剩餘的3個使用默認參數。頭兩個模板參數(vecS, vecS)分別用來描述離開頂點的out-edges邊和圖中頂點的集合所使用的數據結構,(閱讀 Choosing the Edgelist and VertexList 章節可以獲得更多關於平衡不同數據結構的信息) 第三個參數, 使用bidirectionalS表示選擇一個可訪問出、入邊的有向圖,directedS 爲選擇一個僅提供出邊的有向圖。undirectedS 表示選擇一個無向圖

  一旦我們選定了圖的類型,我們可以創建一個圖2所示的圖。聲明一個圖對象,使用 MutableGraph 接口中的add_edge() 函數來填充邊,在這個例子中我們簡單的使用 pairs 數組edge_array來建立邊在這個例子中我們簡單的使用 pairs 數組edge_array來建立邊

#include <iostream> // for std::cout
#include <utility> // for std::pair
#include <algorithm> // for std::for_each
#include <boost/graph/graph_traits.hpp>
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/dijkstra_shortest_paths.hpp>
using namespace boost;

int main(int,char*[])
{
    typedef adjacency_list<vecS, vecS, bidirectionalS> Graph;
    // Make convenient labels for the vertices
    enum { A, B, C, D, E, N }; //代表 0 ,1,2,3,4 頂點
    const int num_vertices = N;
    const char* name = "ABCDE";
    //圖中的邊
    typedef std::pair<int, int> Edge;
    Edge edge_array[] = { Edge(A,B), Edge(A,D), Edge(C,A), Edge(D,C),
                            Edge(C,E), Edge(B,D), Edge(D,E) };
    const int num_edges = sizeof(edge_array)/sizeof(edge_array[0]);
    // 創建一個擁有5個頂點的圖對象
    Graph g(num_vertices);
    // 給圖對象添加邊
    for (int i = 0; i < num_edges; ++i)
      add_edge(edge_array[i].first, edge_array[i].second, g);
   return 0;
}
  我們可以使用圖的edge iterator constructor 構造函數來代替爲每個邊調用add_edge()函數,這種方法
更具代表性比add_edge()更有效率, edge_array 指針可以被視爲迭代器,所以我們可以傳遞數組開始和結束的指針給圖構造函數
Graph g(edge_array, edge_array + sizeof(edge_array) / sizeof(Edge), num_vertices);
同樣可以使用 MutableGraph 接口的add_vertex()remove_vertex() 來爲圖添加和刪除頂點,
而不是一開始就創建一個擁有一定數目頂點的圖

訪問頂點集合

  現在我們創建了一個圖,我們可以使用圖接口訪問圖數據,首先我們可以通過VertexListGraph 接口的vertices() 函數來訪問圖中所有的頂點。這個函數返回一個頂點迭代器的std::pair 類型(第一個迭代器指向頂點的開始,第二個迭代器指向頂點的結束)。提領一個頂點迭代器放回一個頂點對象。頂點迭代器的類型可由graph_traits 類取得,值得注意的是不同的圖類型可能有不同的頂點迭代器類型,這也是爲什麼我們需要graph_traits 類的原因。給定一個圖類型,graph_traits類能提供該圖的vertex_iterator類型,下面的例子打印了圖中每個頂點的索引。所有的頂點和邊屬性,以及索引,可以通過property map 對象訪問。property_map 類可用來獲得指定屬性(通過指定BGL預定義的vertex_index_t來取得索引)的property map 類型,通過調用函數get(vertex_index, g) 來獲得圖當前的property map對象
int main(int,char*[])
{
    //獲得頂點索引的 property map
    typedef property_map<Graph, vertex_index_t>::type IndexMap;
    IndexMap index = get(vertex_index, g);

    std::cout << "vertices(g) = ";
    typedef graph_traits<Graph>::vertex_iterator vertex_iter;
    std::pair<vertex_iter, vertex_iter> vp;
    for (vp = vertices(g); vp.first != vp.second; ++vp.first)
            std::cout << index[*vp.first] << " ";
    std::cout << std::endl;
    return 0;
}
輸出結果:
vertices(g) = 0 1 2 3 4

訪問邊集合

   一個圖的邊集合可以使用EdgeListGraph接口中的 edges()函數訪問。與vertices() 函數類似,這個函數也返回一對迭代器,但在這裏的迭代器是邊迭代器edge iterators。提領邊迭代器可以獲得一個邊對象,調用source()和target()函數可以取得邊連接的兩個頂點。這次我們使用tie()輔助函數,而不是爲迭代器聲明一個pair類型,這個便利的函數可以用來分開std::pair 到兩個分離的變量,這裏是ei 和 ei_end,這樣比創建一個std::pair 類型方便。這也是我們爲BGL選擇的方法

int main(int,char*[])
{
    std::cout << "edges(g) = ";
    graph_traits<Graph>::edge_iterator ei, ei_end;
    for (tie(ei, ei_end) = edges(g); ei != ei_end; ++ei)
    std::cout << "(" << index[source(*ei, g)]<< "," << index[target(*ei, g)] << ") ";
    std::cout << std::endl;
    return 0;
}
輸出結果:
edges(g) = (0,1) (0,2) (0,3) (0,4) (2,0) (2,4) (3,0)(3,1) (3,4) (4,0) (4,1)

鄰接結構

   在下面的例子中我們通過觀察一個特殊的頂點來展示圖的鄰接結構,我們將看到頂點的 in-edges, out-edges, 以及他的鄰接點adjacent vertices. 我們將這些封裝到一個"exercise vertex" 函數對象,並針對圖的每個頂點調用它。爲了示範BGL同STL協作的能力, 我們使用STL的for_each() 函數迭代每個頂點並調用此函數.
int main(int,char*[])
{
    std::for_each(vertices(g).first, vertices(g).second,
    exercise_vertex<Graph>(g));
    return 0;
}
 當我們訪問每個頂點的信息時需要使用到圖對象,所以我們把exercise_vertex寫成一個函數對象而不是函數,在std::for_each()執行期間,使用函數對象可以給我們提供了一個位置來保持對圖對象的引用。爲了能夠處理不同的圖對象,我們將此函數對象模板化。這裏是exercise_vertex 函數對象的開始
template <class Graph> struct exercise_vertex
{
    exercise_vertex(Graph& g_) : g(g_) {}
    Graph& g;
};

頂點描述符

 在撰寫函數對象operator()方法時,我們首先要知道的是圖中頂點對象的類型。頂點類型用來聲明operator()中的參數。確切的說,我們實際上並不處理頂點對象,而是使用頂點描述符vertex descriptors. 許多圖結構(如鄰接表adjacency lists)並不需要存儲頂點對象,而另一些存儲(例如 pointer-linked graphs),這些不同將被頂點描述符對象的黑箱操作所隱藏。頂點描述符由圖類型提供,在後面章節將介紹通過對操作符調用函數out_edges(), in_edges(), adjacent_vertices(),和property map來訪問圖信息。頂點描述符類型可以通過graph_traits類獲得,下面語句中的typename 關鍵字是必須的,因爲在範圍操作符::左邊(graph_traits<Graph>類型)由模板參數(Graph類型)確定。下面是我們定義的函數對象
template <class Graph> struct exercise_vertex
{
    typedef typename graph_traits<Graph>::vertex_descriptor Vertex;
    void operator()(const Vertex& v) const
    {
    }
};

Out-Edges, In-Edges, 和Edge 描述符

   可以通過IncidenceGraph接口中的out_edges()函數來訪問一個頂點的out-edges, 這個函數需要兩個參數:第一個參數是頂點,第二個是圖對象。函數返回一對迭代器,來提供對一個頂點所有out-edges的訪問(與vertices()函數返回pair對象類似)。這些迭代器稱爲out-edge iterators, 提領這些迭代器將返回一個邊描述符對象,邊描述符跟頂點描述符扮演類似性質的角色,也是圖類型提供的黑盒,後面的代碼片斷按source-target順序打印了頂點v對應的每個out-edge邊上的兩個點
template <class Graph> struct exercise_vertex
{
    void operator()(const Vertex& v) const
    {
         //......
        typedef graph_traits<Graph> GraphTraits;
        typename property_map<Graph, vertex_index_t>::type
        index = get(vertex_index, g);

        std::cout << "out-edges: ";
        typename GraphTraits::out_edge_iterator out_i, out_end;
        typename GraphTraits::edge_descriptor e;
        for (tie(out_i, out_end) = out_edges(v, g);out_i != out_end; ++out_i)
        {
            e = *out_i;
            Vertex src = source(e, g), targ = target(e, g);
            std::cout << "(" << index[src] << "," << index[targ] << ") ";
        }
        std::cout << std::endl;
    }
};
對於頂點0 輸出結果是:
out-edges: (0,1) (0,2) (0,3) (0,4)

in_edges()  函數位於BidirectionalGraph接口中,此函數可以通過in-edge迭代器訪問一個頂點所有的in-edges。 只有當鄰接表的Directed(第三個)模板參數設爲bidirectionalS 才能使用此函數. 而指定bidirectionalS代替directedS時將會花費更多的空間
template <class Graph> struct exercise_vertex
{
    void operator()(const Vertex& v) const
    {
        //....... 省略與上面重複代碼
        std::cout << "in-edges: ";
        typedef typename graph_traits<Graph> GraphTraits;
        typename GraphTraits::in_edge_iterator in_i, in_end;
        for (tie(in_i, in_end) = in_edges(v,g); in_i != in_end; ++in_i)
        {
            e = *in_i;
            Vertex src = source(e, g), targ = target(e, g);
            std::cout << "(" << index[src] << "," << index[targ] << ") ";
        }
        std::cout << std::endl;
    }
};
對於頂點 0 輸出是:
in-edges: (2,0) (3,0) (4,0)

鄰接點

 當給出一個頂點的所有的out-edges邊時,這些邊上的目標點對於源點鄰接。有時一個算法不需要關注一個圖的邊,而是僅關心頂點。因此圖形接口AdjacencyGraph 提供了adjacent_vertices()函數來直接訪問鄰接點。此函數返回一對adjacency iterators ,提領一個鄰接點迭代器將會得到領接頂點的頂點描述符。
template <class Graph> struct exercise_vertex
{
    void operator()(Vertex v) const
    {
        //.......
        std::cout << "adjacent vertices: ";
        typename graph_traits<Graph>::adjacency_iterator ai;
        typename graph_traits<Graph>::adjacency_iterator ai_end;
        for (tie(ai, ai_end) = adjacent_vertices(v, g);ai != ai_end; ++ai)
               std::cout << index[*ai] << " ";
        std::cout << std::endl;
    }
};

給你的圖添加一些顏色

  BGL實現儘可能靈活地適應圖的附加屬性,舉個例子,屬性如邊的權重存在於在圖對象的整個生命週期都,因此讓圖對象管理這個屬性的存儲將會帶來很多便利;另外,屬性如頂點顏色只在某個算法的運行期內需要,將此屬性和圖對象分開存儲將會更好。第一種屬性稱爲內在存儲屬性,而第二種稱爲外在存儲屬性。BGL 在圖算法中爲兩種屬性提供了一致的訪問接口property map,此接口在章節Property Map Concepts中有詳細描述。另外,PropertyGraph 配接器也爲獲得一個內在存儲屬性的property map 對象定義了接口

  BGL 鄰接表類允許用戶通過設置圖對象模版參數來指定內在存儲屬性,如何實現這些在Internal Properties 章節有詳細論述。外在存儲屬性有多種創建方法,儘管他們基本上作爲分離參數傳遞給圖算法。一個簡單存儲外在屬性的辦法是通過頂點或邊的索引來創建一個索引數組。如鄰接表中的VertexList模版參數指定爲vecS,每個頂點的索引將會自動建立。通過指定vertex_index_t作爲模版參數的property map對象來訪問這些索引。每個邊雖不能自動建立索引。但是可以通過使用屬性機制把索引和邊聯繫起來,來索引其他的外在存儲屬性。

  在下面的例子中,我們創建一個圖並執行dijkstra_shortest_paths()算法,完整的源代碼在例子examples/dijkstra-example.cpp中。Dijkstra 算法用來計算從起始頂點到其他頂點的最短路徑。Dijkstra 算法要求設置每個邊的權重和每個頂點的距離,這裏我們把權重做爲一個內在屬性,距離作爲外在屬性。對於權重屬性,我們創建屬性類並指定int 作爲權重類型,edge_weight_t 作爲屬性標記(一個BGL預定義的屬性標記)。此權重屬性類將作爲鄰接表adjacency_list 的一個模版參數

  選擇listS或者vecS類型取決於我要在鄰接表中使用的數據結構(可以看 Choosing the Edgelist and VertexList章節)。directedS 類型指定圖爲有向圖(相對的是無向圖)。後面的代碼展示了一個圖類型的聲明和初始化,以及帶權重屬性的邊如何傳遞給(使用迭代器作爲參數的)圖構造函數(要求隨機迭代器)
typedef adjacency_list<listS, vecS, directedS,
no_property, property<edge_weight_t, int> > Graph;
typedef graph_traits<Graph>::vertex_descriptor Vertex;
typedef std::pair<int,int> E;

const int num_nodes = 5;
E edges[] = { E(0,2),
E(1,1), E(1,3), E(1,4),
E(2,1), E(2,3),
E(3,4),
E(4,0), E(4,1) };
int weights[] = { 1, 2, 1, 2, 7, 3, 1, 1, 1};
Graph G(edges + sizeof(edges) / sizeof(E), weights, num_nodes);

 對於外部距離屬性,我們使用std::vector 來存儲, BGL 算法視隨機迭代器爲property maps。所以我們能夠傳遞距離數組vector迭代器到Dijkstra's 算法。緊接上面的例子,下面的代碼創建了一個distance vector, 然後調用Dijkstra's 算法(內部使用了權重屬性),輸出結果:
// vector for storing distance property
std::vector<int> d(num_vertices(G));
// get the first vertex
Vertex s = *(vertices(G).first);
// invoke variant 2 of Dijkstra's algorithm
dijkstra_shortest_paths(G, s, distance_map(&d[0]));

std::cout << "distances from start vertex:" << std::endl;
graph_traits<Graph>::vertex_iterator vi;
for(vi = vertices(G).first; vi != vertices(G).second; ++vi)
std::cout << "distance(" << index(*vi) << ") = "
<< d[*vi] << std::endl;
std::cout << std::endl;

結果是:
distances from start vertex:
distance(0) = 0
distance(1) = 6
distance(2) = 1
distance(3) = 4
distance(4) = 5

使用Visitors擴充圖算法

  通常一個庫中的算法能夠滿足你大部分的需求,但事無絕對,例如在前面的章節中,我們使用Dijkstra's 算法來計算到每個頂點的最短路徑,但可能我們想記錄路徑最短的樹,可以通過在記錄最短路徑樹中記錄每個節點的前驅來實現。
  當然我們最好能夠避免重寫Dijkstra's 算法,並且只增加記錄前輩節點的額外需求[1],在STL中,可以使用仿函數作爲算法的可選參數來提供這種伸縮性。在BGL中,visitors 扮演着類似的角色。Visitor 類似stl仿函數。仿函數只有一個執行函數,但Visitor 擁有更多的方法,每個方法將在明確定義的算法點被調用。Visitor 函數在Visitor Concepts章節有詳細說明。BGL 爲通常的任務提供了幾種visitor,包括記錄前驅節點的visitor.作爲擴充BGL的一種方法鼓勵用戶寫自己的visitor.這裏我們將迅速瀏覽實現和使用前驅記錄 , 由於我們使用dijkstra_shortest_paths()算法,所以我們創建的visitor也必須是一個Dijkstra Visitor.

  record_predecessors visitor 的泛函性分成兩部分。我們使用一個property map來存儲和訪問前驅屬性。前驅 visitor 只負責記錄前驅節點。爲了實現這些,我們創建一個使用模版參數的record_predecessors類。由於這個visitor將在一個visitor方法中被填充,我們從一個提供空方法的dijkstra_visitor類繼承。predecessor_recorder 類的構造函數將接受一個property map 對象,並把他保存在數據成員中。
template <class PredecessorMap>
class record_predecessors : public dijkstra_visitor<>
{
public:
record_predecessors(PredecessorMap p)
: m_predecessor(p) { }

template <class Edge, class Graph>
void edge_relaxed(Edge e, Graph& g) {
// set the parent of the target(e) to source(e)
put(m_predecessor, target(e, g), source(e, g));
}
protected:
PredecessorMap m_predecessor;
};
  記錄前驅節點的工作十分簡單,當Dijkstra's algorithm算法釋放一個邊的時候(添加他到最短路徑樹中) 我們記錄源頂點作爲目標頂點的前驅。稍後,如果邊再次釋放前驅屬性將被新的前驅重寫,這裏我們使用put() 函數在property map中記錄前驅。Visitor的edge_filter將告訴算法什麼時候調用explore()方法。我們希望邊在最短路徑樹中被通知,所以我們指定tree_edge_tag標記

  最後,我們創建一個輔助函數來更方便的創建predecessor visitors,所有的BGL visitor 都有一個類似的輔助函數。
template <class PredecessorMap> record_predecessors<PredecessorMap>
make_predecessor_recorder(PredecessorMap p)
{
    return record_predecessors<PredecessorMap>(p);
}
現在我們準備在Dijkstra's 算法中使用record_predecessors。BGL 的Dijkstra's 算法配備了一個vistitors句柄,所以我們只需要傳入我們新的visitor即可。 在這個例子中我們只需要使用1個visitor,儘管BGL在算法中配置了多visitors句柄參數??(參見Visitor Concepts章).
using std::vector;
using std::cout;
using std::endl;
vector<Vertex> p(num_vertices(G)); //the predecessor 數組
dijkstra_shortest_paths(G, s, distance_map(&d[0]).
visitor(make_predecessor_recorder(&p[0])));

cout << "parents in the tree of shortest paths:" << endl;
for(vi = vertices(G).first; vi != vertices(G).second; ++vi) {
cout << "parent(" << *vi;
if (p[*vi] == Vertex())
cout << ") = no parent" << endl;
else
cout << ") = " << p[*vi] << endl;
}
輸出結果:
parents in the tree of shortest paths:
parent(0) = no parent
parent(1) = 4
parent(2) = 0
parent(3) = 2
parent(4) = 3

注意:
新版本Dijkstra's algorithm包括了一個用來記錄前驅的指定參數,所以前驅visitor 並不需要。但上面仍不失爲一個好的例子。

發佈了11 篇原創文章 · 獲贊 5 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章