算法导论 第22章 图的基本算法(一)

    这篇博客讨论图的基本算法第一部分,包括两点内容: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),代码就不贴了。














你可能感兴趣的:(算法和数据结构)