《算法笔记》读书记录DAY_42

CHAPTER_10  提高篇(4)——图算法专题

10.3.1图的遍历—DFS

图的遍历是指对图的所有顶点按一定顺序进行访问。用DFS遍历图,总是以“深度”作为第一关键词,每次都是沿着路径到不能再前进时才退回到最近的岔道口。也就是沿着一条路径直到无法继续前进,才退回到路径上离当前顶点最近的还存在为访问分支顶点的岔道口,并前往访问那些未访问的分支顶点。

例如上面这个例子,沿着V1、V2、V4、V8、V5走至最深,这时候已经无法继续前进了,这个时候回退到最近的V1分岔口,因为V1还有未访问的分支V3,然后同样沿着V3、V6、V7的路径访问走至最无路可走,至此所有顶点访问完毕。

在讲解图的DFS的具体实现前,还需要介绍两个概念:

连通分量:在无向图中,如果两个顶点之间有路径,则称这两个顶点连通。如果G(V,E)的任意两个顶点连通,则图G为连通图;否则,称图G为非连通图,且称其中的极大连通子图为连通分量

强连通分量在有向图中,如果两个顶点可以相互到达,则称这两个顶点强连通。如果G(V,E)的任意两个顶点强连通,则图G为强连通图;否则,称图G为非强连通图,且称其中的极大连通子图为强连通分量

例如下图:图(a)是无向图,V1、V4V5V6V7、V8V9形成了三个连通分量;图(b)是有向图,V1V2V3、V4、V5V6V7V8形成了三个强连通分量。

为了叙述方便,我们暂且将连通分量和强连通分量称为连通块。显然,要遍历整个图,就要对每个连通块进行遍历。如果给定的图是一个连通图或者强连通图,那么只需依次DFS就能遍历完成。遍历的基本思路就是将经过的顶点设置为已访问,下次递归碰到这个顶点时就不会再去处理,直到所有顶点被标记为已访问。

代码实现(邻接表):

const int maxv=1000;            //最大顶点数
vector<int> Adj[maxv];          //图的邻接表实现
bool isArrive[maxv]={0};        //标志顶点i是否被访问
int n;                          //实际顶点数

void DFS(int u,int depth) {     //u为当前访问的顶点标号,depth为深度 
	isArrive[u]=true;           //访问顶点u
	//如果要对u进行一些操作,可以在此处进行
	for(int i=0;i<Adj[i].size();i++) {   //遍历所有u可以到达的顶点 
		int v=Adj[u][i];
		if(isArrive[v]==0) {
			DFS(v,depth+1);
		}
	} 
} 

void DFSTrave() {               //一次DFS只能访问完一个连通块 
	for(int i=0;i<n;i++) {      //遍历所有顶点,访问所有连通块 
		if(isArrive[i]==0) {
			DFS(i,1);
		}
	}
}

下面通过一道例题,来练习图的DFS。

题目:

警方有一批通话记录,其中记录了若干人之间的通话时间。如果A与B有通话时间,则称A与B有关系。如果A与B有关系的同时B与C也有关系,警方就将A、B、C归为一组。通过这种方式警方将它们分为若干组。

现在给定一个值K,只要一个组的总通话时间超过K并且组内人数大于2,就将该组定为“犯罪团伙”,其中通话时间最多的那个人就是头目。现给定通话记录,要求输出“犯罪团伙“的个数,并按头目姓名字典序从小到大的顺序输出每个”犯罪团伙“的头目姓名和成员数。

输入格式:

每个输入包含一个测试用例。对于每个用例,第一行输入两个正整数N和K(N,K<=1000),N表示通话记录的数目,K为阈值。

接下来输入N行,每行包括每段通话的信息:姓名1 姓名2 通话时间,其中姓名由三个字母组成,通话时间是一个小于1000的正整数。

输出格式:

对于每个输入。第一行输出犯罪团伙的数目,接下来按照头目姓名字典序从小到大,每个犯罪团伙单独输出一行,每个犯罪团伙输出其头目姓名和成员人数。

需要注意的是,当没有犯罪团伙时直接输出0即可。

输入样例:

8 59

AAA BBB 10

BBB AAA 20

AAA CCC 40

DDD EEE 5

EEE DDD 70

FFF GGG 30

GGG HHH 20

HHH FFF 10

输出样例:

2

AAA 3

GGG 3

思路:

我们用无向图模型来解决问题,其中每个顶点的点权表示该人的总通话时长,同时还需要抽象出每条边的边权来表示通话时长,每个组在图上表示为连通块。在某些情况下我们要对通话时长进行累加,例如读入A与B通话时间T1,又读入了B与A的通话时长T2,实际在无向图模型上A与B之间只存在一条权值为T1+T2的边。

步骤1-1:首先要解决的问题是姓名与编号的对应关系。我们使用map<string ,int>直接建立字符串与整数的映射。确定好每个姓名对应的编号,我们就能在读入数据时建立相应编号的顶点,并且为数据建立图模型。

步骤1-2:根据题目要求,我们需要获得每个人的通话总时长,实际上就是获得图中每个顶点的权值。我们可以在读入数据时就进行处理,假设读入A与B的通话时长T,那么A与B的点权都增加T。

步骤2-1:进行DFS遍历每个连通块,目的是获得每个连通块的顶点个数、最大权值顶点、边权之和。每个连通块的顶点个数即为递归层数depth;DFS可以设置一个数据maxNode记录该连通块中的最大权值顶点。

步骤2-2:在DFS中,我们还要获得该连通块的边权之和,这实际上就是该连通块上所有顶点的点权之和的1/2(想想为什么?)。

步骤3:每次DFS遍历完一个连通块,判断是否为犯罪团伙。如果是,则将该连通块信息存储下来,同时犯罪团伙数加1。

需要注意的是,题目要求输出按头目姓名字典序由小到大,我们可以使用map<string,int>才存储每个犯罪团伙信息,这样map自动按键值string递增排序了。

参考代码:

其实本题也可以用并查集解决,在使用并查集时,要注意合并时总是保持点权更大的节点为集合的根节点。同时为了记录总边权和总人数,需要定义数组来存放。这里留作自行思考。

#include<iostream>
#include<string>
#include<map>
#include<vector>
#include<utility>
#include<algorithm>
using namespace std;

const int maxn=2010;            //最大顶点数 
int n,k,nodeNum=0;              //n为通话记录,k为阈值,nodeNum为当前顶点个数
int depth,sum;                  //depth记录顶点数,sum记录点权之和 
bool isArrive[maxn]={0};        //记录每个顶点是否被访问 
map<int,string> intToString; 
map<string,int> stringToInt;    //姓名和编号间的转换
map<string,int> gang;           //记录犯罪团伙 
pair<int,int> maxNode;          //maxNode记录每个连通块头目,第一个值表示顶点编号,第二个值表示权 

struct graph {                  //图的邻接表 
	int nodeW;                  //点权 
	vector<int> Adj;            //记录每个顶点的边 
}G[maxn];

int change(string str) {                                  //将姓名str转换为整数编号 
	map<string,int>::iterator it=stringToInt.find(str);
	if(it!=stringToInt.end()) {                           //如果str已经出现过 
		return it->second;                                //返回str对应的整数 
	}
	else {
		stringToInt[str]=nodeNum;                         //str对应nodeNum 
		intToString[nodeNum]=str;                         //nodeNum对应str
		return nodeNum++;                                 //返回当前编号,之后顶点数加1 
	}
}

void DFS(int u,int &depth) {                              //一次DFS递归遍历一个连通块,depth为递归深度 
	isArrive[u]=1;                                        //设置u已经访问
	sum+=G[u].nodeW;                                      //将点权累加 
	if(G[u].nodeW>maxNode.second) {                       //更新最大权值顶点编号 
		maxNode.first=u;
		maxNode.second=G[u].nodeW;
	}
	for(int i=0;i<G[u].Adj.size();i++) {                  //遍历u的所有边 
		int v=G[u].Adj[i];
		if(isArrive[v]==0) {                              //如果邻接点v没有访问 
			DFS(v,++depth);                               //递归进入下一层 
		}
	}                    
}

int main() {
	string str1,str2;
	int w;
	cin>>n>>k;
	for(int i=0;i<n;i++) {                                 //为输入数据创建图模型 
		cin>>str1>>str2>>w;
		int id1=change(str1);                              //获得str1对应的编号 
		int id2=change(str2);                              //获得str2对应的编号 
		G[id1].nodeW+=w;                                   //id1号顶点点权增加w 
		G[id2].nodeW+=w;                                   //id2号顶点点权增加w
		if(find(G[id1].Adj.begin(),G[id1].Adj.end(),id2)==G[id1].Adj.end()) {      //如果id1的邻接表中没有到id2的边 
			G[id1].Adj.push_back(id2);                                             //将到id2的边加入id1的邻接表 
		}
		if(find(G[id2].Adj.begin(),G[id2].Adj.end(),id1)==G[id2].Adj.end()) {      //如果id2的邻接表中没有到id1的边 
			G[id2].Adj.push_back(id1);                                             //将到id1的边加入id2的邻接表 
		}
	}
	for(int i=0;i<nodeNum;i++) {                            //遍历每个顶点 
		if(isArrive[i]==0) {                                //如果顶点i未被访问 
			depth=1;
			maxNode=make_pair(0,0);
			sum=0;                                          //每次遍历新连通块前将depth,maxNode,sum初始化 
			DFS(i,depth);                                   //i所在连通块遍历完毕 
			sum=sum/2;                                      //该连通块总边权等于总点权的一半
			if(sum>k&&depth>2) {                            //如果是犯罪团伙 
				gang[intToString[maxNode.first]]=depth;     //将犯罪团伙头目名字和人数记录 
			}
		}
	}
	cout<<gang.size()<<endl;                                //按格式输出信息
	if(gang.size()!=0) {
		for(map<string,int>::iterator it=gang.begin();it!=gang.end();it++) {
		cout<<it->first<<' '<<it->second<<endl;
		}
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/jgsecurity/article/details/121041747