语雀满了,只能暂时先用优快云来记录一下了。
这道题之前看王道书的时候做过一次,当时是照着查并集的思想做的。昨天刷PAT重做的时候,第一时间想到的还是查并集,然后尝试着自己独立去写,发现仅仅用查并集其实还是有很多的坑的。
Sample Input 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
Sample Output 1:
2
AAA 3
GGG 3
Sample Input 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
Sample Output 2:
0
题目大意
给定N条通话记录和一个限制K。每条通话记录包含了两个名字和一个通话时间,相互联系的人同属于一个帮派。题目要求,只有人数超过2人并且总通话时长超过K的集体才能算作帮派Gang,并且帮派内部通话时长最长的人是帮派首领Boss。
解法一(繁琐)
我看到需要区分集合,所以第一时间就想到了查并集。于是就有了以下思路:
- 创建一个map,用来完成人名和id编号的映射。使用number来记录人数,如果没有当前人名才++。
- 创建一个person结构,存储每个人的名字和总通话时间,下标是映射的id,使用people数组进行保存。
- 编写Find查找和Union合并的函数,来对每个人的集合进行划分。(Find函数一定要带路径压缩)
- 创建一个set容器(可以忽略重复项),用来保存找到的leader,顺带计算每个帮派的总通话时间,保存在total里,以leader的id为下标(不一定是真的leader)。然后,再把所有相关的人保存到一个邻接表gang里面(方便统计每个帮派的人数)。
- 然后就是根据gang对应的帮派人数和总通话时间,来判断是否为帮派。如果是,把leader的id转储到向量里面(本来想用set.erase,但是不知道为什么会出bug...)
- 最后就是根据每个人的通话时间,找到已知帮派里面的真正的leader,保存到优先队列里面,有序输出。
虽然这样也能实现,但是过程很繁琐,而且很容易出现bug。
- 查并集的生成的树的根节点不是根据total来算的,所以根节点不一定是真正的leader。
- 虽然Find函数能够实现路径压缩。但是当根结点又与另一个集合合并时候,孩子的father仍然指向旧根节点,而不是合并后的新根结点。所以在后面遍历people数组时候,必须要对每一个结点都要再Find一次,再一次路径压缩。
- 结果必须是按照string有序输出。
- 通话记录最多有1000条,所以数组必须要≥2000。
#include <bits/stdc++.h>
using namespace std;
const int maxn=2020;
struct person
{
string name;
int t=0;
} people[maxn];
map<string,int> id;
map<int,int>total;
vector<person> gang[maxn];
int father[maxn];
int height[maxn];
void Init()
{
for(int i=1; i<=maxn; ++i)
{
father[i]=i;
height[i]=0;
}
return ;
}
int Find(int x)
{
if(x!=father[x])
{
father[x]=Find(father[x]);
}
return father[x];
}
void Union(int a,int b)
{
int xa=Find(a);
int xb=Find(b);
if(xa!=xb)
{
if(height[xa]<height[xb])
{
father[xa]=xb;
}
else if(height[xa]>height[xb])
{
father[xb]=xa;
}
else
{
father[xb]=xa;
height[xa]++;
}
}
}
int cmp(person a,person b)
{
return a.t>b.t;
}
int main()
{
int n,k,number=1;
while(cin >> n >>k)
{
number=1;
Init();
for(int i=0; i<n; ++i)
{
string a,b;
int t;
cin >> a>> b>> t;
if(id[a]==0)
{
id[a]=number++;
people[id[a]].name=a;
}
if(id[b]==0)
{
id[b]=number++;
people[id[b]].name=b;
}
people[id[a]].t+=t;
people[id[b]].t+=t;
Union(id[a],id[b]);
}
int cnt=0;
set<int> leaders;
for(int i=1; i<number; ++i)
{
Find(i);
gang[father[i]].push_back(people[i]);
total[father[i]]+=people[i].t;
leaders.insert(father[i]);
}
set<int>::iterator it=leaders.begin();
vector<int> ans;
for(it=leaders.begin(); it!=leaders.end(); ++it)
{
if(total[(*it)]>k*2 && gang[(*it)].size()>2)
{
ans.push_back((*it));
cnt++;
}
}
cout <<cnt<<"\n";
map<string,int> output;
for(int i=0; i<cnt; ++i)
{
int key=ans[i],tmax=0;
string boss;
for(int i=0; i<gang[key].size(); ++i)
{
if(gang[key][i].t>tmax)
{
tmax=gang[key][i].t;
boss=gang[key][i].name;
}
}
output[boss]=gang[key].size();
}
map<string,int>::iterator ot=output.begin();
for(;ot!=output.end();++ot){
cout << ot->first<<" "<<ot->second<<"\n";
}
}
return 0;
}
解法二
王道书里提供的解法,是查并集的变体。
使用了map<string,string> 来保存father,这样就省去了id之间映射。
同样需要考虑到并查集的根节点不一定是真正首领的问题
#include <bits/stdc++.h>
using namespace std;
map<string, string>father;
map<string, int>height;
map<string, int>weight;
map<string, int>gangnum;
priority_queue<string, vector<string>, greater<string> >gang;
vector<string>head; //保存并查集的根节点
vector<string>head2; //找到真正的头领
string Find(string name) {
//如果能找到,就一直找到根节点
if (father.find(name) != father.end()) {
if (name != father[name]) {
father[name] = Find(father[name]);
}
} else {
//如果找不到,就初始化自己
father[name] = name;
height[name] = 0;
}
return father[name];
}
void Union(string a, string b, int t) {
//将查并的标准改成weight,weight更大的成为父节点
weight[a] += t;
weight[b] += t;
a = Find(a);
b = Find(b);
if (a != b) {
if (weight[a] < weight[b]) {
father[a] = b;
} else if (weight[a] > weight[b]) {
father[b] = a;
} else {
father[b] = a;
height[a]++;
}
}
}
void FindHead() {
map<string, string>::iterator it;
for (it = father.begin(); it != father.end(); ++it) {
if (it->first == it->second) {
head.push_back(it->first);
}
}
}
void FindReal() {
for (int i = 0; i < head.size(); ++i) {
map<string, string>::iterator it;
int maxn = 0;
string name;
for (it = father.begin(); it != father.end(); ++it) {
//因为查并集的根节点不一定是真的头领
//所以需要找到权重最大的人,他才是头领
if (head[i] == Find(it->first)) {
//cout << it->first << '\n';
if (weight[it->first] > maxn) {
maxn = weight[it->first];
name = it->first;
}
}
}
head2.push_back(name);
}
//printf("M:%d\n",weight["M"]);
//printf("Q:%d\n",weight["Q"]);
//printf("S:%d\n",weight["S"]);
}
void FindGang(int k) {
FindHead();
FindReal();
//根据并查集的根节点,计算每个帮派的总时长
for (int i = 0; i < head.size(); ++i) {
//cout << head2[i]<<'\n';
//cout << k << '\n';
string leader = head[i];
int num = 0, sum_weight = 0;
map<string, string>::iterator it;
for (it = father.begin(); it != father.end(); ++it) {
if (it->second == leader) {
sum_weight += weight[it->first];
num++;
}
}
if (num > 2 && sum_weight > 2 * k) {
gang.push(head2[i]);
gangnum[head2[i]] = num;
}
}
}
void initial() {
father.clear();
height.clear();
weight.clear();
gangnum.clear();
head.clear();
}
int main() {
int n, k;
while (scanf("%d%d", &n, &k) != EOF) {
int time = 0;
while (n--) {
string str1;
string str2;
int t;
cin >> str1 >> str2 >> t;
Union(str1, str2, t);
}
FindGang(k);
cout << gang.size() << '\n';
while (!gang.empty()) {
cout << gang.top() << ' ' << gangnum[gang.top()] << '\n';
gang.pop();
}
initial();
}
return 0;
}
解法三
晴神书里的解法,用DFS来做。
- 通过两个map,来完成name和id的互相映射,方便哈希查找。
- 再用一个map,来保存头领姓名和帮派人数的,保存答案。
- w[]数组来保存每个人的通话时间,g[][]来保存边的权重,并且方便dfs找到帮派
- 找到新的w更大的结点,head指向该结点,成为新的头领。
- totalweight来记录每个帮派的总通话时间。一定要记住,先遍历边,累加了通话时间,之后一定要把这条边删除。这样一方面避免出现环的情况,造成边权重漏加或者重复加的情况。
#include <bits/stdc++.h>
using namespace std;
const int maxn=2010;
const int inf=1e8;
map<string,int> nameToid;//姓名转编号
map<int,string> idToname;//编号转姓名
map<string,int> gang; //头领->人数
int n,k,number=0; //通话记录数量,最低时间,总人数
int w[maxn]={0},G[maxn][maxn]={0};
bool vis[maxn]={false};
//遍历节点
void DFS(int now,int &head,int &gangnum,int &totalweight){
gangnum++; //成员数量增加
vis[now]=true; //已经访问过当前结点
if(w[now]>w[head]){
head=now; //找到权重最大的点,成为新的首领
}
for(int i=0;i<number;++i){
if(G[now][i]>0){
totalweight+=G[now][i]; //累加这条边
G[now][i]=G[i][now]=0; //将这两个点之间的边删除
if(!vis[i]){ //如果下一个点还没被访问过,就访问
DFS(i,head,gangnum,totalweight);
}
}
}
}
//遍历整个图
void DFSTrave(){
for(int i=0;i<number;++i){
if(!vis[i]){
int head=i,gangnum=0,totalweight=0; //不要忘了初始化
DFS(i,head,gangnum,totalweight);
if(gangnum>2 && totalweight>k){
//gang是保存name对应人数的
gang[idToname[head]]=gangnum;
}
}
}
}
//完成名字和id的编号,方便查找
int change(string s){
//找到的话,就不需要再添加姓名了
if(nameToid.find(s)!=nameToid.end()){
return nameToid[s];
}else{
nameToid[s]=number;
idToname[number]=s;
return number++;
}
}
int main(){
int weight;
cin >> n>>k;
string s1,s2;
for(int i=0;i<n;++i){
cin >> s1>>s2 >>weight;
int id1=change(s1);
int id2=change(s2);
w[id1]+=weight; //保存到对应的权重里
w[id2]+=weight;
G[id1][id2]+=weight; //保存到邻接矩阵里面
G[id2][id1]+=weight;
}
DFSTrave();
cout << gang.size()<<"\n";
//因为map是按照关键字从小到大输出的,所以不用排序
map<string,int>::iterator it;
for(it=gang.begin();it!=gang.end();++it){
cout << it->first << " "<< it->second<<"\n";
}
return 0;
}