算法笔记-图概念及基本操作

基本概念
图由顶点(Vertex)和边(Edge)组成,每条边的两端都必须是图的两个顶点(可以是相同的顶点)。而记号G(V,E)表示图G的顶点集为V、边集为E。
图可分为有向图无向图。有向图的所有边都有方向,即确定了顶点到顶点的一个指向;而无向图的所有边都是双向的,即无向边所连接的两个顶点可以互相到达。
顶点的是指和该顶点相连的边的条数。特别是对于有向图来说,顶点的出边条数称为该顶点的出度,顶点的入边条数称为该顶点的入度
顶点和边都可以有一定属性,而量化的属性称为权值,顶点的权值和边的权值分别称为点权边权。权值可以根据问题的实际背景设定,例如点权可以是城市中资源的数目,边权可以使两个城市之间来往所需要的时间或花费。

存储方式
图的存储方式有两种:邻接矩阵和邻接表。
邻接矩阵
设图G(V,E)的顶点标号为0,1,…,N - 1,那么可以令二维数组G[N][N]的两维分别表示图的顶点标号,即如果G[i][j]为1,则说明顶点i和顶点j之间有边;如果G[i][j]为0,则说明顶点i和顶点j之间不存在边,而这个二维数组G[][]则被称为邻接矩阵。另外,如果存在边权,则可以令G[i][j]存放边权,对不存在的边可以设边权为0、1或是一个很大的数。
邻接矩阵比较好些,但是由于需要开一个二维数组,如果顶点数目太大,便可能超过题目限制的内存。因此邻接矩阵只适用于定点数目不太大的(一般不超过1000)题目
邻接表
设图G(V,E)的顶点编号为0,1,…,N - 1,每个顶点都可能有若干条出边,如果把同一个顶点的所有出边放在一个列表中,那么N个顶点就会有N个列表(没有出边,则对应空表),这N个列表被称为图G的邻接表,即为Adj[N],其中Adj[i]存放顶点i的所有出边组成的列表,这样Adj[0],Adj[1],…,Adj[N - 1]就分别都是一个列表,列表可以用链表实现,每个结点存放一条边的信息(边的终点编号、边权)
在此介绍一种更为简单的工具来实现邻接表:vector
由于vector有变长数组之称,因此可以开一个vector数组Adj[N],其中N为顶点个数。这样每个Adj[i]就都是一个变长数组vector,使得存储空间只与图的边数有关。
如只存放每条边的终点编号,而不存放边权,则vector中元素类型可以直接定义为int型

vector<int> Adj[N];

如添加一条从1号顶点到达3号顶点的有向边,只需要在Adj[1]中添加终点编号3即可

Adj[1].push_back(3);

如需要同时存放边的终点编号和边权,那么可以建立结构体Node,用来存放每条边的终点编号和边权

struct Node {
    int v;
    int w;
}
vector<Node> Adj[N];

如添加从1号到达3号顶点的有向边,边权为4.

Node temp;
temp.v = 3;
temp.v = 4;
Adj[1].push_back(temp);

更快的做法是定义结构体Node的构造函数

struct Node {
    int v, w;
    Node(int _v, _w) : v(_v), w(_w) {}
}

这样就能不定义临时变量来实现加边操作

Adj[1].push_back(Node(3, 4));

如顶点数较大(在1000以上)用邻接表来存储图比较合适

图的遍历
图的遍历方法一般有两种:深度优先搜索(DFS)和广度优先搜索(BFS)
###采用深度优先搜索(DFS)遍历图
沿着一条路径直到无法继续前进,才退回到路径上离当前顶点最近的还存在未访问分支顶点的岔道口,并前往访问那些未访问分支顶点,直到遍历完整个图。
两个概念:
连通分量:在无向图中,如果两个顶点之间可以相互到达(可以是通过一定路径间接到达),那么就称这两个顶点连通。如果G(V,E)的任意两个顶点都连通,则称图G为连通图;否则,称图G为非连通图,且称其中的极大连通子图为连通分量。
强连通分量:在有向图中,如果两个顶点可以各自通过一条有向路径到达另一个顶点,就称这两个顶点强连通。如果图G(V,E)的任意两个顶点都强连通,则称图G为强连通图;否则,称图G为非强连通图,且称其中的极大强连通子图为强连通分量。
把连通分量和强连通分量均称为连通块
如果要遍历整个图,就需要对所有连通块分别进行遍历。
基本思路:将经历过的顶点设置为已访问,在下次递归碰到这个顶点时就不再去处理,直到整个图的顶点都被标记为已访问。
如已知给定的图时一个连通图,则只需要一次DFS就能完成遍历
伪代码:

DFS(u) {	//访问顶点u 
	vis[u] = true;	//设置u已被访问 
	for(从u出发能到达的所有顶点v)	//枚举从u出发可以到达的所有顶点v
		if vis[v] == false		//如果v未被访问 
			DFS(v); 	//递归访问v 
}

DFSTrave(G) {	//遍历图G 
	for(G的所有顶点u)	//对G的所有顶点u
		if vis[u] == false  //如果u未被访问
			DFS(u);		//访问u所在的连通块 
} 

实现邻接矩阵和邻接表前需要先定义MAXV为最大顶点数、INF为一个很大的数字

const int MAXV = 1000;    //最大顶点数
const int INF = 1000000000;  //设INF为一个很大的数

邻接矩阵版本

int n, G[MAXV][MAXV];		//n为顶点数,MAXV为最大顶点数
bool vis[MAXV] = {false};	//如果顶点i已被访问,则vis[i] == true。初值为false

void DFS(int u, int depth) {	//u为当前访问的顶点标号,depth为深度 
	vis[u] = true;		//设置u已被访问
	//如果需要对u进行一些操作,可以在这里进行
	//下面对所有从u出发能到达的分支顶点就像枚举
	for(int v = 0; v < n; v++) {
		if(vis[v] == false && G[u][v] != INF) {		//如果v未被访问,且u可到达v 
			DFS(v, depth + 1);		//访问v, 深度加一 
		}
	} 
} 

void DFSTrave() (
	for(int u = 0; u < n; u++) {		//对每个顶点u 
		if(vis[u] == false) {	//如果u未被访问	 
			DFS(u, 1);		//访问u和u所在的连通块,1表示初始为第一层 
		}
	}
) 

邻接表版本

vector<int> Adj[MAXN];	//图G的邻接表
int n;		//n为顶点数,MAXV为最大顶点数
bool vis[MAXV] = {false};	//如果顶点i已被访问,则vis[i] == true。初值为false
 
void DFS(int u, int depth) {	//u为当前访问的顶点标号,depth为深度 
	vis[u] = true;	//设置u已被访问 
	//如果需要对u进行一些操作,可以在这里进行
	for(int i = 0; i < Adj[u].size(); i++) {
		int v = Adj[u][i];
		if(vis[v] == false){
			DFS(v, depth + 1);	//访问v, 深度加一 
		}
	} 
} 

void DFSTrave() (
	for(int u = 0; u < n; u++) {		//对每个顶点u 
		if(vis[u] == false) {	//如果u未被访问	 
			DFS(u, 1);		//访问u和u所在的连通块,1表示初始为第一层 
		}
	}
) 

实例:
给出若干人之间的通话长度(视为无向边),这些通话将他们分为若干组。每个组的总边权设为该组内的所有通话的长度之和,而每个人的点权设为该人参与的通话长度之和。现在给定一个阀值K,且只要一个组的总边权超过K,并满足成员人数超过2,则将该组视为”犯罪团伙(Gang)“,而该组内点权最大的人视为头目。要求输出”犯罪团伙“的个数,并按头目姓名字典序从小到大的顺序输出每个”犯罪团伙“的头目姓名和成员人数。
思路:

  1. 解决姓名和编号的对应关系。可使用map<string, int>直接建立字符串与整型的映射关系,也可使用字符串hash的方法将字符串转换为整型。编号与姓名的对应关系则可以直接使用string数字进行定义,或者使用map<int, string>也是可以的
  2. 需要获得每个人的点权,即与之相关的通话记录的时长之和,可以在读入时进行处理。该步在求与某个点相连的边的边权之和
  3. 图的遍历。使用DFS遍历每个连通块,目的是获取每个连通块的头目(即连通块内点权最大的结点)、成员个数、总边权
  4. 通过步骤3可以获得连通块的总边权totalValue。如果totalValue大于给定的阀值K,且成员人数大于2,则说明该连通块是一个团伙,将该团伙的信息存储下来
    可以定义map<string, int>,来建立团伙头目的姓名与成员人数的映射关系。由于map中元素自动按键从小到大排序。

注意点:

  1. 由于通话记录的条数最多有1000条,意味着不同的人可能有2000人,因此数组大小必须在2000以上
  2. 可以使用使用结构体来存放头目姓名与成员人数,但会增加一定代码量
struct Gang {
	string head;	//团伙头目
	int numMember;	//成员数量
}arrayGang[maxn];
int numGang = 0;	//团伙个数
bool cmp(Gang a, Gang b) {
	return a.head < b.head;		//按头目姓名的字典序从小到大排序 
}
  1. 由于每个结点在访问后不应再次被访问,但是图中可能有环,即遍历过程中发生一条边连接已访问结点 的情况。此时为了边权不被漏加,需要先累加边权,再去考虑结点递归访问的问题
  2. 本题也可用并查集解决
//输入1
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
//输出1
2
AAA 3
GGG 3
//输入2
8 70
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
0
#include<iostream>
#include<string>
#include<map>
using namespace std;
const int maxn = 2010;	//总人数
const int INF = 1000000000;	//无穷大

map<int, string> intToString;	//编号->姓名
map<string, int> stringToInt;	//姓名->编号
map<string, int> Gang;			//head->人数
int G[maxn][maxn] = {0}, weight[maxn] = {0};	//邻接矩阵G,点权weight
int n, k, numPerson = 0;	//边数n、下限k、总人数numPerson
bool vis[maxn] = {false};	//标记是否被访问

//DFS函数访问单个连通块,nowVisit为当前访问的编号
//head为头目,numMember为成员编号,totalValue为连通块的总边权/
void DFS(int nowVisit, int& head, int& numMember, int& totalValue) {
	numMember++;	//成员人数+1
	vis[nowVisit] = true;	//标记已被访问
	if(weight[nowVisit] > weight[head]) {
		head = nowVisit;	//更新头目 
	} 
	for(int i = 0; i < numPerson; i++) {
		if(G[nowVisit][i] > 0) {	//如果nowVisit能到达i 
			totalValue += G[nowVisit][i];	//连通块的总边权增加该边权
			G[nowVisit][i] = G[i][nowVisit] = 0;	//删除这条边,防止回头
			if(vis[i] == false) {
				DFS(i, head, numMember, totalValue);
			} 
		}
	}
} 

//DFSTrave函数遍历真个图,获得每个连通块的信息
void DFSTrave() {
	for(int i = 0; i < numPerson; i++) {
		if(vis[i] == false) {
			int head = 1, numMember = 0, totalValue = 0;
			DFS(i, head, numMember, totalValue);
			if(numMember > 2 && totalValue > k) {
				Gang[intToString[head]] = numMember;
			}
		}
	}
} 

//change函数返回姓名str对应的编号
int change(string str) {
	if(stringToInt.find(str) != stringToInt.end()) {  //如果str已经出现过, 
		return stringToInt[str];			//返回编号 
	} else {
		stringToInt[str] = numPerson;	//str的编号为numPerson
		intToString[numPerson] = str;	//numPerson对应str
		return numPerson++; 
	}
} 

int main() {
	int w;
	string str1, str2;
	cin >> n >> k;
	for(int i = 0; i < n ; i++) {
		cin >> str1 >> str2 >> w;
		int id1 = change(str1);
		int id2 = change(str2);
		weight[id1] += w;
		weight[id2] += w;
		G[id1][id2] += w;
		G[id2][id1] += w;
	} 
	DFSTrave();
	cout << Gang.size() << endl;
	map<string, int>::iterator it;
	for(it = Gang.begin(); it != Gang.end(); it++) {
		cout << it->first << " " << it->second << endl;
	}
	return 0;
}

采用广度优先搜索(BFS)遍历图
通过反复取出队首顶点,将该顶点可到达的**未曾加入过队列(而不是未访问)**的顶点全部入队,直到队列为空时遍历结束。
伪代码:

BFS(u) {	//遍历u所在的连通块 
	queue q; //定义队列q
	将u入队;
	inq[u] = true;	//设置u已入过队
	while(q非空) {	//只要队列非空 
		取出q的队首元素u进行访问
		for(从u出发可达的所有顶点v) {	//枚举从u能直接到达的顶点v 
			if(inq[v] == false) {	//如果v未曾加入过队列 
				将v入队;
				inq[v] = true; 	//设置v已加入过队列 
			}
		} 
	} 
} 

BFSTrave(G) {	//遍历图G 
	for(G的所有顶点u)		//枚举G的所有顶点 
		if(inq[u] == false) {	//如果u未曾入过队列 
			BFS(u);		//遍历u所在的连通块 
		} 
} 

邻接矩阵版本

bool inq[MAXV] = {false};	//记录顶点是否入过队列 
 
void BFS(int u) {
	queue<int> q;
	q.push(u);
	inq[u] = true;
	while(!q.empty()) {
		int u = q.front();
		q.pop();
		for(int v = 0; v < n; v++) {
			if(inq[v] == false && G[u][v] != INF){
				q.push(v);
				inq[v] = true;
			}
		}
	}
}

void BFSTrave() {
	for(int u = 0; u < n; u++) {
		if(inq[u] == false) {
			BFS(u);
		}
	}
}

邻接表版本

vector<int> Adj[MAXN];
int n;
bool inq[MAXV] = {false};

void BFS(int u) {
	queue<int> q;
	q.push(u);
	inq[u] = true;
	while(!q.empty()) {
		int u = q.front();
		q.pop();
		for(int i = 0; i < Adj[u].size();i++) {
			int v = Adj[u][i];
			if(inq[v] == false) {
				q.push(v);
				inq[v] = true;
			}
		}
	}
} 

void BFSTrave() {
	for(int u = 0; u < n; u++) {
		if(inq[u] == false) {
			BFS(u);
		}
	}
}

如果需要存放顶点层号,需要定义结构体Node,存放顶点的编号和层号

struct Node {
	int v;
	int layer;
};

vector<Node> Adj[N];

void BFS(int s) {
	queue<Node> q;
	Node start;
	start.v = s;
	start.layer = 0;
	q.push(start);
	inq[start.v] = true;
	inq(!q.empty()) {
		Node topNode = q.front();
		q.pop();
		int u = topNode.v;
		for(int i = 0; i < Adj[u].size(); i++) {
			Node next = Adj[u][i];
			next.layer = topNode.layer + 1;
			if(inq[next.v] == false) {
				q.push(next);
				inq[next.v] = true;`
			}
		}
	} 
}

实例:
在微博中,每个用户都可能被若干个其他用户关注。而当该用户发布一条信息时,他的关注者就可以看到这条信息并选择是否转发它,且转发的信息也可以被转发者的关注者再次转发,但同一用户最多只能转发该信息一次(信息的最初发布者不会转发该信息)。现在给出N个用户的关注情况(即他们各自关注了哪些用户)以及一个转发层数上限L,并给出最初发布消息的用户编号,求在转发层上限内消息最多会被多少用户转发

//输入
7 3
3 2 3 4
0
2 5 6
2 3 1
2 3 4
1 4
1 5
2 2 6
//输出
4
5
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int MAXV = 1010;

struct Node {
	int id;
	int layer;
};

vector<Node> Adj[MAXV];	//邻接表
bool inq[MAXV] = {false};

int BFS(int s, int L) {
	int numForward = 0;
	queue<Node> q;
	Node start;
	start.id = s;
	start.layer = 0;
	q.push(start);
	inq[start.id] = true;
	while(!q.empty()) {
		Node topNode = q.front();
		q.pop();
		int u = topNode.id;
		for(int i = 0; i < Adj[u].size(); i++) {
			Node next = Adj[u][i];
			next.layer = topNode.layer + 1;
			if(inq[next.id] == false && next.layer <= L) {
				q.push(next);
				inq[next.id] = true;
				numForward++;
			}
		}
	}
	return numForward;
} 

int main() {
	Node user;
	int n, L, numFollow, idFollow;
	scanf("%d%d", &n, &L);
	for(int i = 1; i <= n; i++) {
		user.id = i;
		scanf("%d", &numFollow);
		for(int j = 0; j < numFollow; j++) {
			scanf("%d", &idFollow);
			Adj[idFollow].push_back(user);
		}
	}
	int numQuery, s;
	scanf("%d", &numQuery);	//查询个数
	for(int i = 0; i < numQuery; i++) {
		memset(inq, false, sizeof(inq));	//inq数组初始化 
		scanf("%d", &s);				//起始结点编号 
		int numForward = BFS(s, L);			//BFS,返回转发数 
		printf("%d\n", numForward);			//输出转发数 
	} 
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值