这篇博客讨论图的基本算法第一部分,包括两点内容:1、22.1节课后习题算法;2、广度优先搜索。对于深度优先搜索由于有递归形式以及非递归形式,还有对边类型和课后习题等等,内容较多,将重新开辟一章。
本章算法中主要采取的图的表示
图的表示比较简单,在此就不再讨论。我们约定一下,在本章中各种算法的图的相关表示,便于后续算法的讨论。约定如下:
1、边节点结构;
struct edgeNode
{//边节点
size_t adjvertex;//该边的关联的顶点
int weight;//边权重
edgeNode *nextEdge;//下一条边
edgeNode(size_t adj, int w) :adjvertex(adj), weight(w), nextEdge(nullptr){}
};
这是本篇博客采用的边节点结构,在后续关于图的各种算法中,边结构都会有相应的改变。
2、所有节点均用数字标号,按顺时针方向标,且标号从1开始,标号0在后续某些算法中用作特殊用途,编号数据类型为size_t;
3、所有算法将采用图的邻接表表示形式,少量习题算法要求邻接矩阵表示的将会改用邻接表实现;
4、图基本数据成员如下:
class AGraph
{//图,可以表示有向图和无向图
private:
vector E;
size_t nodenum;//顶点数目
};
本篇博客没有节点类型,因为暂时用不上。vector从索引1开始存入数据,且索引i处对饮i号节点的邻接链表。
那么,约定之后,就让我们直奔主题吧。直接开始22.1节课后习题的讨论。
习题22.1-1
计算有向图的每个顶点的出度,只需将每个顶点的邻接链表遍历以便即可,时间为O(V+E),如果是连通图,则|E| >= |V| - 1,即V=O(E),则时间为O(E),程序如下:
void AGraph::outDegree()
{
cout << "Out-degree-----------" << endl;
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
size_t count = 0;
while (curr != nullptr)
{
++count;
curr = curr->nextEdge;
}
cout << "vertex " << i << " : " << count << endl;
}
}
计算每个顶点的入度有两种方式。第一种方法直接求解,比较暴力,每次对整个邻接表扫描一遍,确定一个顶点的入度,扫描一次需要O(V+E)时间,连通图(V=O(E))则要O(E),扫描V次,则时间为O(VE);第二种是建立一个入度数组记录每个顶点的入度,扫描一次邻接表,则可以取得每个顶点的入度了,时间为O(V+E)。在此,我们采用第二种,代码如下:
void AGraph::inDegree()
{
vector degree(nodenum + 1);
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
while (curr != nullptr)
{
++degree[curr->adjvertex];
curr = curr->nextEdge;
}
}
cout << "In-degree-------------" << endl;
for (size_t i = 1; i != degree.size(); ++i)
cout << "vertex " << i << " : " << degree[i] << endl;
}
习题22.1-3
计算有向图的转置,扫描一次邻接表,对于边(u,v),向另一张图中添加(v,u)边即可,时间为O(V+E);对于用邻接矩阵表示的,很显然,时间为O(V^2),我们给出前者的代码:
void AGraph::reverse(AGraph *regraph)
{//用regraph存储图转置
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
while (curr != nullptr)
{
regraph->add1Edge(curr->adjvertex, i);
curr = curr->nextEdge;
}
}
}
习题22.4-4
简化多重图,获得其等价图,我们可以扫描一次整个邻接表,对于边(u,v),在添加前先查找是否已添加,再作出决定,平均时间为O((V+E)E/V),若为连通图,则V=O(E),那么时间为O(E),search和add2Edge代码如下:
edgeNode* AGraph::search(size_t start, size_t end)
{
edgeNode *curr = E[start];
while (curr != nullptr && curr->adjvertex != end)
curr = curr->nextEdge;
return curr;
}
void AGraph::add1Edge(size_t start, size_t end, int weight = 1)
{
edgeNode *curr = search(start, end);
if (curr == nullptr)
{
edgeNode *p = new edgeNode(end, weight);
p->nextEdge = E[start];
E[start] = p;
}
}
inline void AGraph::add2Edges(size_t start, size_t end, int weight = 1)
{
add1Edge(start, end, weight);
add1Edge(end, start, weight);
}
习题22.1-5
计算有向图的平方图。扫描邻接表,对于边(u,v),进一步扫描顶点v的邻接链表,若存在边(v,w),则添加边(u,w),对于连通图,时间为O(E),程序代码如下:
void AGraph::square(AGraph *sqgraph)
{//sqgraph存储平方图
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr1 = E[i];
while (curr1 != nullptr)
{
edgeNode *curr2 = E[curr1->adjvertex];
while (curr2 != nullptr)
{
sqgraph->add1Edge(i, curr2->adjvertex);
curr2 = curr2->nextEdge;
}
curr1 = curr1->nextEdge;
}
}
}
习题22.1-6
当采用邻接矩阵存储有向图时,在O(V)时间内判断是否存在通用汇点(universal sink),即入度为V-1,出度为0的顶点。
我们可以采用两个索引i和j,分别指向行和列,对于邻接矩阵G,有如下两种情况:
1、G[i][j] = 0,则j不是通用的汇,因为节点i没有指向它,因而i以及++j可能是通用的汇;
2、G[i][j] = 1,则i不是通用的汇,因为i有出度,因而++i和j可能是通用汇点。
采用循环不变式:每次迭代之前,i之前和j之前但不包括i的所有节点不可能是通用汇点,言下之意为,i指向的顶点可能为通用汇点。
初始:i = 1,j = 2,则i之前为空,j之前且不包括i亦为空,循环不变式成立。
保持:在迭代过程中,若遇到G[i][j] = 1,根据情况2可知,i不是通用汇点,那么i应当跳到下一个可能是通用汇点的位置,由循环不变式可知,为i之后和j中的最大者,即i = max{i+1,j},j++,循环不变时得以保持;若遇到G[i][j] = 0,根据情况1可知,j不是通用汇点,此时i依然可能会是通用汇点,为了保持循环不变式成立,j = max{i+1,j+1},i不变。
终止:如果i<=N,j=N+1,根据循环不变式可知,i可能是通用汇点,需要全面检查,若i>N,则必然不存在通用汇点。根据该循环不变式,我们可以得到程序代码如下:
size_t MGraph::universalSink()
{
for (size_t k = 1; k != graph.size(); ++k)
if (graph[k][k] != 0) return 0;
size_t i = 1, j = 2;
while (j < graph.size())
{
if (graph[i][j] == 1)
{
i = max(i + 1, j);
++j;//根据循环不变式,无论i取何值,j都必须更新
}
else j = max(i + 1, j + 1);//i不用变,因为依然有可能是通用汇点
}
if (i < graph.size())
{
for (size_t x = 1; x != graph.size(); ++x)
{
if (graph[i][x] != 0) return 0;
if (i != x && graph[x][i] != 1) return 0;
}
return i;
}
else return 0;
}
习题22.1-7
计算出BBT即可得知:对角线为各定点的度,其他各项的绝对值表示两点间边的条数。
习题22.1-8
索引与定点标号对应,O(1)取的该定点邻接链表,等可能的查询散列表,时间为O(1),故确定某条边是否在图中所需时间为O(1)。缺点是对于每个槽都需要事先分配O(V)空间,有较大浪费,不如采取邻接矩阵,但是和采用邻接表相比,邻接矩阵在很多算法中时间花费较大。
广度优先搜索(Breadth-First Search)
广度优先搜索比较简单,不进行详细讨论,稍后给出上述习题包括BFS整个源代码,然后在解决22.2节相关习题。
上述所有习题及BFS代码如下:
#include
#include
#include
#include
#define NOPARENT 0
#define MAX 0x7fffffff
using namespace std;
enum color{ WHITE, GRAY, BLACK };
struct edgeNode
{//边节点
size_t adjvertex;//该边的关联的顶点
int weight;//边权重
edgeNode *nextEdge;//下一条边
edgeNode(size_t adj, int w) :adjvertex(adj), weight(w), nextEdge(nullptr){}
};
class AGraph
{//图,可以表示有向图和无向图
private:
vector E;
size_t nodenum;
void printPath(size_t, size_t, vector&);
public:
AGraph(size_t n) :nodenum(n)
{
if (n != 0) E.resize(n + 1);
}
void initDGraph();//初始化有向图
void initUGraph();//初始化无向图
edgeNode* search(size_t, size_t);//查找边
void add1Edge(size_t, size_t, int);//有向图中添加边
void add2Edges(size_t, size_t, int);//无向图中添加边,一次添加两条
void delete1Edge(size_t, size_t);//有向图中删除边
void delete2Edges(size_t, size_t);//无向图中删除边
void outDegree();//各节点出度&&无向图的度
void inDegree();//各节点入度&&无向图的度
void degree();//有向图各节点的度
void BFS(size_t);
void reverse(AGraph*);//图转置
void square(AGraph*);//平方图
void print();
~AGraph();
};
void AGraph::printPath(size_t s, size_t f, vector &p)
{
if (s == f) cout << s;
else if (p[f] == NOPARENT)
cout << s << " has no path to " << f;
else
{
printPath(s, p[f], p);
cout << " --> " << f;
}
}
void AGraph::BFS(size_t s)
{
queue Q;
vector dis(nodenum + 1), p(nodenum + 1), color(nodenum + 1);
for (size_t i = 1; i <= nodenum; ++i)
{
dis[i] = MAX;
p[i] = NOPARENT;
color[i] = WHITE;
}
color[s] = GRAY;
dis[s] = 0;
p[s] = NOPARENT;
Q.push(s);
while (!Q.empty())
{
size_t u = Q.front();
Q.pop();
edgeNode *curr = E[u];
while (curr != nullptr)
{
if (color[curr->adjvertex] == WHITE)
{
color[curr->adjvertex] = GRAY;
dis[curr->adjvertex] = dis[u] + 1;
p[curr->adjvertex] = u;
Q.push(curr->adjvertex);
}
curr = curr->nextEdge;
}
color[u] = BLACK;
}
for (size_t i = 1; i <= nodenum; ++i)
{
if (s != i)
{
printPath(s, i, p);
cout << "\tdistance: " << dis[i] << endl;
}
}
}
void AGraph::initDGraph()
{
size_t start, end;
ifstream infile("F:\\ugraph.txt");
while (infile >> start >> end)
add1Edge(start, end,1);
}
void AGraph::initUGraph()
{
size_t start, end;
ifstream infile("F:\\ugraph.txt");
while (infile >> start >> end)
add2Edges(start, end,1);
}
edgeNode* AGraph::search(size_t start, size_t end)
{
edgeNode *curr = E[start];
while (curr != nullptr && curr->adjvertex != end)
curr = curr->nextEdge;
return curr;
}
void AGraph::add1Edge(size_t start, size_t end, int weight = 1)
{
edgeNode *curr = search(start, end);
if (curr == nullptr)
{
edgeNode *p = new edgeNode(end, weight);
p->nextEdge = E[start];
E[start] = p;
}
}
inline void AGraph::add2Edges(size_t start, size_t end, int weight = 1)
{
add1Edge(start, end, weight);
add1Edge(end, start, weight);
}
void AGraph::delete1Edge(size_t start, size_t end)
{
edgeNode *curr = search(start, end);
if (curr != nullptr)
{
if (curr->adjvertex == end)
{
E[start] = curr->nextEdge;
delete curr;
}
else
{
edgeNode *pre = E[start];
while (pre->nextEdge->adjvertex != end)
pre = pre->nextEdge;
pre->nextEdge = curr->nextEdge;
delete curr;
}
}
}
inline void AGraph::delete2Edges(size_t start, size_t end)
{
delete1Edge(start, end);
delete1Edge(end, start);
}
void AGraph::outDegree()
{
cout << "Out-degree-----------" << endl;
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
size_t count = 0;
while (curr != nullptr)
{
++count;
curr = curr->nextEdge;
}
cout << "vertex " << i << " : " << count << endl;
}
}
void AGraph::inDegree()
{
vector degree(nodenum + 1);
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
while (curr != nullptr)
{
++degree[curr->adjvertex];
curr = curr->nextEdge;
}
}
cout << "In-degree-------------" << endl;
for (size_t i = 1; i != degree.size(); ++i)
cout << "vertex " << i << " : " << degree[i] << endl;
}
void AGraph::degree()
{
vector d(E.size());
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
while (curr != nullptr)
{
++d[i];
++d[curr->adjvertex];
curr = curr->nextEdge;
}
}
cout << "Degree---------------" << endl;
for (size_t i = 1; i != d.size(); ++i)
cout << "vertex " << i << " : " << d[i] << endl;
}
void AGraph::reverse(AGraph *regraph)
{
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
while (curr != nullptr)
{
regraph->add1Edge(curr->adjvertex, i);
curr = curr->nextEdge;
}
}
}
void AGraph::square(AGraph *sqgraph)
{
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr1 = E[i];
while (curr1 != nullptr)
{
edgeNode *curr2 = E[curr1->adjvertex];
while (curr2 != nullptr)
{
sqgraph->add1Edge(i, curr2->adjvertex);
curr2 = curr2->nextEdge;
}
curr1 = curr1->nextEdge;
}
}
}
inline void AGraph::print()
{
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i];
cout << i;
if (curr == nullptr) cout << " --> null";
else
while (curr != nullptr)
{
cout << " --<" << curr->weight << ">--> " << curr->adjvertex;
curr = curr->nextEdge;
}
cout << endl;
}
}
AGraph::~AGraph()
{
for (size_t i = 1; i != E.size(); ++i)
{
edgeNode *curr = E[i],*pre;
while (curr != nullptr)
{
pre = curr;
curr = curr->nextEdge;
delete pre;
}
}
}
习题22.3-3
时间为O(V^2)
习题22.2-6 职业摔跤手
其实这是一个图着色(用两种颜色)问题。对整个比赛图进行BFS,对顶点u的每条边,即(u,v),v已着色,且颜色和u相同,则无解;若v已着色,但不相同,则继续遍历;若v未着色,则将v着成和u不同的颜色。最后根据颜色不同将摔跤手分配比赛。代码如下:
enum color{NOCOLOR,WHITE, BLACK };
void AGraph::assignWrestler(size_t s)
{
queue Q;
vector color(nodenum + 1);
for (size_t i = 1; i <= nodenum; ++i)
color[i] = NOCOLOR;
color[s] = WHITE;
Q.push(s);
while (!Q.empty())
{
size_t u = Q.front();
Q.pop();
edgeNode *curr = E[u];
while (curr != nullptr)
{
if (color[curr->adjvertex] == color[u])
{
cout << "No solution!" << endl;
return;
}
else if (color[curr->adjvertex] == NOCOLOR)
{
if (color[u] == WHITE) color[curr->adjvertex] = BLACK;
else color[curr->adjvertex] = WHITE;
Q.push(curr->adjvertex);
}
curr = curr->nextEdge;
}
}
for (size_t i = 1; i <= nodenum; ++i)
if (color[i] == WHITE) cout << i << " good" << endl;
else cout << i << " bad" << endl;
}
习题22.2-7 树的直径
任选一顶点,对该树进行BFS,选出离该顶点最远的顶点,然后以该顶点为源点,在进行一次BFS,取得最远距离,即为该树直径。两次BFS,所以时间为O(V+E),代码就不贴了。