PAT-ADVANCED1034——Head of a Gang

博客介绍了如何使用图的深度优先遍历和并查集解决PAT-1034问题,寻找犯罪团伙及其头目。文章提供了两种解题思路,强调了关键点,如判断团伙条件、总边权计算、姓名与编号的对应,以及并查集中的点权维护和路径压缩。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我的PAT-ADVANCED代码仓:https://github.com/617076674/PAT-ADVANCED

原题链接:https://pintia.cn/problem-sets/994805342720868352/problems/994805456881434624

题目描述:

题目翻译:

1034 犯罪团伙的头目

警察发现一个犯罪团伙的头目的方法之一是:检查人们的电话记录。如果A和B之间有一个电话记录,我们称A和B是相联系的。此联系的权重被定义成在这两人之间所有电话记录的总时长。一个"Gang"的定义如下:成员人数超过两人,且总的联系权重大于一个给定的权重下限K对于每一个犯罪团伙,有着最大总权重的那个人就是团伙的头目。现在提供一个电话记录的列表,你需要寻找犯罪团伙及其头目。

输入格式:

每个输入文件包含一个测试用例。在每个测试用例中,第一行包含两个正数N和K(均小于或等于1000),分别代表电话记录数和判定"Gang"的权重下限K。接下来的N行,有着以下格式:

Name1 Name2 Time

其中,Name1和Name2分别是打电话两人的姓名,Time是这个电话的时长。名字是由"A"~"Z"组成字符串,长度为3。时长是一个不超过1000分钟的正整数。

输出格式:

对每一个测试用例,在第一行中打印出犯罪团伙的数目。接着,对于每一个犯罪团伙,在同一行中打印出其头目和犯罪团伙的人数。题目保证犯罪团伙的头目是唯一的。输出必须按照犯罪团伙头目的字母顺序排列。

输入样例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

知识点:图的深度优先遍历、并查集

思路一:图的深度优先遍历

注意点:

(1)判断一个团伙是否是"Gang",需要满足两个条件:

a:成员数超过两人(等于两人不算)。

b:总的联系权重大于一个给定的下限K(等于K不算)。

对于条件b,总的联系权重指的是总边权。而本题中边权的定义:在这两人之间所有电话记录的总时长,既包括A打给B的,也包括B打给A的

(2)寻找一个"Gang"中的头目,用的是点权。具体看头目的定义:于每一个犯罪团伙,有着最大总权重的那个人就是团伙的头目

总边权,经过观察,我们可以发现其刚好是总点权的一半

(3)由于通话记录的条数最多有1000条,这意味着不同的人可能有2000人,因此数组大小必须在2000以上。

步骤如下述所示:

(1)首先要解决的问题是姓名与编号的对应关系。我使用的是map<string, int>集合和map<int, string>集合来直接建立字符串与整型的映射关系。

(2)进行图的遍历。使用深度优先遍历的方法遍历每一个连通块,目的是获取每个连通块的头目(即连通块内点权最大的节点)、成员个数、总边权。

(3)通过步骤(2)可以获得连通块的总边权。如果连通块的总边权大于给定的下限K,且连通块的成员人数大于2,则说明该连通块是一个"Gang",将该"Gang"的信息存储下来。

由于本思路是用邻接表实现的,因此时间复杂度是O(N)。空间复杂度是O(V + N),其中V图中的节点数。

C++代码:

#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<algorithm>

using namespace std;

struct node{
	int v;
	int time;
	node(int _v, int _time){
		v = _v;
		time = _time;
	}
};

struct gang{
	string head;
	int size;
	gang(string _head, int _size){
		head = _head;
		size = _size;
	}
};

int N, K;
map<string, int> stringToInt;
map<int, string> intToString;
int total = 0;
vector<node> graph[2000];
bool visited[2000];
int head, headTime, gangSize, pointWeight;

int changeToInt(string str);
void dfs(int nowVisit);
bool cmp(gang g1, gang g2);

int main(){
	fill(visited, visited + 2000, false);
	scanf("%d %d", &N, &K);
	string name1, name2;
	int person1, person2, time;
	for(int i = 0; i < N; i++){
		cin >> name1 >> name2 >> time;
		person1 = changeToInt(name1);
		person2 = changeToInt(name2);
		graph[person1].push_back(node(person2, time));
		graph[person2].push_back(node(person1, time));
	}
	vector<gang> gangs;
	for(int i = 0; i <= total; i++){
		if(visited[i]){
			continue;
		}
		head = i;
		headTime = 0;
		gangSize = 0;
		pointWeight = 0;
		dfs(i);
		if(gangSize > 2 && pointWeight > K * 2){
			gangs.push_back(gang(intToString[head], gangSize));
		}
	}
	sort(gangs.begin(), gangs.end(), cmp);
	cout << gangs.size() << endl;
	for(int i = 0; i < gangs.size(); i++){
		cout << gangs[i].head << " " << gangs[i].size << endl;
	}
	return 0;
} 

int changeToInt(string str){
	if(stringToInt.find(str) != stringToInt.end()){
		return stringToInt[str];
	}
	stringToInt[str] = total;
	intToString[total] = str;
	return total++;
}

void dfs(int nowVisit){
	visited[nowVisit] = true;
	gangSize++;
	int callTime = 0;
	for(int i = 0; i < graph[nowVisit].size(); i++){
		callTime += graph[nowVisit][i].time;
	}
	pointWeight += callTime;
	if(callTime > headTime){
		head = nowVisit;
		headTime = callTime;
	}
	for(int i = 0; i < graph[nowVisit].size(); i++){
		int v = graph[nowVisit][i].v;
		if(!visited[v]){
			dfs(v);
		}
	}
}

bool cmp(gang g1, gang g2){
	return g1.head < g2.head;
}

C++解题报告:

思路二:并查集

本题只要求连通分量有几个,以及每个连通分量中的节点个数和每个连通分量中拥有最大点权的节点,并没有任何关于路径的信息,因此,本题也可以用并查集来实现。

和普通的并查集不同,在合并操作的时候,我们需要以点权较大的那个根节点为新的根结点,这样省去了我们寻找点权最大值的操作。

因此在合并的过程中我们需要维护一个数组gangEdgeWeight[2001],gangEdgeWeight[i]表示以i为根节点的一个连通分量的总点权,其初始值gangEdgeWeight[i]就等于weight[i],其中weight[i]表示编号为i的人的点权值。

同理,为了对连通分量中的总节点数进行计数,在合并的过程中我们还需要维护一个数组gangSize[2001],gangSize[i]表示以i为根节点的一个连通分量的总节点个数,其初始值gangSize[i] = 1,相当于每个人各自成一个连通分量。

注意点:

(1)为避免超时,在并查集中添加路径压缩操作。添加路径压缩后的并查集的查找函数如下:

int findFather(int x) {
	int a = x;
	while(x != father[x]) {
		x = father[x];	 
	}
	while(a != father[a]){	//路径压缩 
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}

路径压缩后的并查集查找函数均摊效率认为是一个几乎为O(1)的操作。

(2)用map<string, int>建立头目姓名与成员人数的关系可以自动按头目的字母顺序输出。

(3)并查集的基本操作:查找父亲节点需要调用findFather()函数,而不能直接取其father[]数组中的值

时间复杂度和空间复杂度均是O(V ^ 2),其中V图中的节点数,即通话记录中的总人数,最大是2000。

C++代码:

#include<iostream>
#include<map>
#include<vector>
#include<string>

using namespace std;

int n;	//电话数
int k;	//判断"Gang"的下限
int father[2001];	//并查集数组
int weight[2001] = {0};	//weight[i]表示编号为i的人的点权
int graph[2001][2001] = {0};	//无向图
int visited[2001] = {false};	//标记数组
map<string, int> result;	//存放结果,头头名字->该头头对应团队的成员数 
map<string, int> stringToInt;	//姓名->编号
map<int, string> intToString;	//编号->姓名
int totalPeople = 0;	//总人数
int gangEdgeWeight[2001];	//gangEdgeWeight[i]存放以i为头头的一个"Gang"的总点权
int gangSize[2001];	//gangSize[i]存放以i为头头的一个"Gang"的总人数

int change(string str);
int findFather(int x);
void unionTwo(int x, int y);

int main() {
	cin >> n >> k;
	string name1, name2;
	int time;
	for(int i = 0; i < n; i++) {
		cin >> name1 >> name2 >> time;
		int id1 = change(name1);
		int id2 = change(name2);
		graph[id1][id2] += time;
		graph[id2][id1] += time;
		weight[id1] += time;
		weight[id2] += time;
	}
	for(int i = 0; i < totalPeople; i++) {
		father[i] = i;	//并查集初始化
		gangSize[i] = 1;	//每个人各自一伙
		gangEdgeWeight[i] = weight[i]; 
	}
	for(int i = 0; i < totalPeople; i++) {
		for(int j = 0; j < totalPeople; j++) {
			if(graph[i][j] != 0) {
				unionTwo(i, j);	//并查集的合并操作
			}
		}
	}
	for(int i = 0; i < totalPeople; i++) {
		if(gangSize[findFather(i)] > 2 && gangEdgeWeight[findFather(i)] > 2 * k) {
			result[intToString[findFather(i)]] = gangSize[findFather(i)]; 
		}
	}
	map<string, int>::iterator it;
	cout << result.size() << endl;
	for(it = result.begin(); it != result.end(); it++) {
		cout << it->first << " " << it->second << endl;
	}
	return 0;
}

int findFather(int x) {
	int a = x;
	while(x != father[x]) {
		x = father[x];	 
	}
	while(a != father[a]){	//路径压缩 
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}

void unionTwo(int x, int y) {
	int xFather = findFather(x);	//这里得调用findFather()函数,而不是直接在father[]数组里取 
	int yFather = findFather(y);
	if(xFather == yFather){
		return;
	}
	if(weight[xFather] > weight[yFather]) {
		father[yFather] = xFather;
		gangSize[xFather] += gangSize[yFather];
		gangEdgeWeight[xFather] += gangEdgeWeight[yFather];
	} else {
		father[xFather] = yFather;
		gangSize[yFather] += gangSize[xFather];
		gangEdgeWeight[yFather] += gangEdgeWeight[xFather];
	}
}

int change(string str) {
	if(stringToInt.find(str) != stringToInt.end()) {
		return stringToInt[str];
	}
	stringToInt[str] = totalPeople;
	intToString[totalPeople] = str;
	return totalPeople++;
}

C++解题报告:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值