A - 氪金带东
题目:
实验室里原先有一台电脑(编号为1),最近氪金带师咕咕东又为实验室购置了N-1台电脑,编号为2到N。每台电脑都用网线连接到一台先前安装的电脑上。但是咕咕东担心网速太慢,他希望知道第i台电脑到其他电脑的最大网线长度,但是可怜的咕咕东在不久前刚刚遭受了宇宙射线的降智打击,请你帮帮他。
提示: 样例输入对应这个图,从这个图中你可以看出,距离1号电脑最远的电脑是4号电脑,他们之间的距离是3。 4号电脑与5号电脑都是距离2号电脑最远的点,故其答案是2。5号电脑距离3号电脑最远,故对于3号电脑来说它的答案是3。同样的我们可以计算出4号电脑和5号电脑的答案是4.
Input:
输入文件包含多组测试数据。对于每组测试数据,第一行一个整数N (N<=10000),接下来有N-1行,每一行两个数,对于第i行的两个数,它们表示与i号电脑连接的电脑编号以及它们之间网线的长度。网线的总长度不会超过10^9,每个数之间用一个空格隔开。
Output:
对于每组测试数据输出N行,第i行表示i号电脑的答案 (1<=i<=N).
Sample Input:
5
1 1
2 1
3 1
1 1
Sample Output:
3
2
3
4
4
题目思路:
本题是让我们求树中的每个点,到达其他的所有点中的最远距离,思考一下这个距离是如何得到的,对于一棵树来说,一定存在一条最长的路径,我们称其为这棵树的直径,不妨令形成这条直径的两个端点为这棵树的最左端和最右端。再想一下,对于树中的任意一点,它到所有的其他点的最远距离,一定是它到这课树的最左端或者最右端形成的。因此,对于这个题目我们即有了大致的思路:第一步:求出所给树的直径,即得到这棵树的最左端和最右端。第二步,分别从最左端和最右端开始,对整个树中的所有点进行遍历(BFS,DFS均可,我使用的是DFS),求出每个点到达左端点和右端点的距离。第三步:对于每一个点来说,比较他距离左端点和右端点的距离,输出大者即可。
关于具体的实现,数据的储存:建立一个边的结构体,包括两个点,边长度,下一条边的储存位置。然后使用一个边类型的数组edgex来储存数据。然后一个标记数组lable用于记录边之间的关系,用于DFS遍历时的链接。一个布尔型数组visit用于在DFS过程中记录点的到达情况。两个数组分别记录每个点到左端点和右端点的距离。然后我使用了三遍DFS,直接写了三个DFS函数,第一个DFS函数从第一条边开始遍历,用于找寻树的右端点。第二个DFS函数从右端点开始遍历,寻找树的左端点,于此同时,记录下每个点到达右端点的距离。第三个DFS函数,从树的左端点开始遍历,记录下每个点到左端点的距离,并于此同时完成对比得出最大距离。
代码实现:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
struct edge
{
int from;
int to;
int value;
int next;
};
int N;
int lable[10010];
edge edgex[20010];
bool vis[10010];
int length1[10010];
int numedge=0;
int l1,l2;
int maxlength=0;
int length2[10010];
void addedge(int f, int t, int v)
{
edgex[numedge].from=f;
edgex[numedge].to=t;
edgex[numedge].value=v;
edgex[numedge].next=lable[f];
lable[f]=numedge;
numedge++;
}
void dfs1(int x)
{
vis[x]=1;
for(int i=lable[x]; i!=-1; i=edgex[i].next)
{
int node1 = edgex[i].to;
if(!vis[node1])
{
vis[node1]=1;
length1[node1] = length1[x] + edgex[i].value;
if(length1[node1] > maxlength)
{
maxlength=length1[node1];
l1=node1;
}
dfs1(node1);
}
}
}
void dfs2(int x)
{
vis[x]=1;
for(int i=lable[x]; i!=-1; i=edgex[i].next)
{
int node1=edgex[i].to;
if(!vis[node1])
{
vis[node1]=1;
length1[node1]=length1[x]+edgex[i].value;
if(length1[node1] > maxlength)
{
maxlength=length1[node1];
l2=node1;
}
dfs2(node1);
}
}
}
void dfs3(int x)
{
vis[x]=1;
for(int i=lable[x]; i!=-1; i=edgex[i].next)
{
int node1 = edgex[i].to;
if(!vis[node1])
{
vis[node1]=1;
length2[node1]=length2[x]+edgex[i].value;
if(length2[node1] > length1[node1])
{
length1[node1]=length2[node1];
}
dfs3(node1);
}
}
}
int main()
{
while(scanf("%d",&N) != EOF)
{
for(int i=1; i<=N; i++)
{
vis[i]=0;
length1[i]=0;
length2[i]=0;
lable[i]=-1;
}
for(int i=0; i<N;i++)
{
edgex[i].next=-1;
}
int a,b;
numedge = 0;
for(int i=2;i<=N;i++)
{
scanf("%d%d",&a,&b);
addedge(i,a,b);
addedge(a,i,b);
}
maxlength=0;
dfs1(1);
maxlength=0;
for(int i=1; i<=N;i++)
{
length1[i]=0;
vis[i]=0;
}
dfs2(l1);
for(int i=1; i<=N;i++)
{
vis[i]=0;
}
dfs3(l2);
for(int i=1; i<=N;i++)
{
printf("%d\n",length1[i]);
}
}
return 0;
}
心得体会:
这道题是从求树的直径延伸而来,归根结底考察的核心知识点是树的遍历,这就牵扯到最基本最核心的算法DFS,BFS的掌握了。具体实现思路,助教在上课时已经讲的很充分,所以大致思路上问题不大,但是在具体的实现上,有很多细节需要仔细思考,关于数据的读入,边的链接,还有三遍DFS的间隙中,对于一些数组的重新初始化,都需要我们谨慎处理。
——————————————————————————————————————————————
B - 戴好口罩!
题目:
新型冠状病毒肺炎(Corona Virus Disease 2019,COVID-19),简称“新冠肺炎”,是指2019新型冠状病毒感染导致的肺炎。
如果一个感染者走入一个群体,那么这个群体需要被隔离!
小A同学被确诊为新冠感染,并且没有戴口罩!!!!!!
危!!!
时间紧迫!!!!
需要尽快找到所有和小A同学直接或者间接接触过的同学,将他们隔离,防止更大范围的扩散。
众所周知,学生的交际可能是分小团体的,一位学生可能同时参与多个小团体内。
请你编写程序解决!戴口罩!!
Input:
多组数据,对于每组测试数据:
第一行为两个整数n和m(n = m = 0表示输入结束,不需要处理),n是学生的数量,m是学生群体的数量。0 < n <= 3e4 , 0 <= m <= 5e2
学生编号为0~n-1
小A编号为0
随后,m行,每行有一个整数num即小团体人员数量。随后有num个整数代表这个小团体的学生。
Output:
输出要隔离的人数,每组数据的答案输出占一行
Sample Input:
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
Sample Output:
4
1
1
题目思路:
仔细阅读这个题目后,这个题目让我们找的是所有和“0”相关的元素数目,相关的含义是:和0在一个集合中,和0所在集合的成员在一个集合中,和上述成员在一个集合中,然后不断的辐射下去,所有被辐射到的元素都是和0相关的。然后这个题就变成了,对于每一个元素,判断他的“祖先”是不是0,然后如何判断呢,我们可以由下边的元素往上查找,对于某个元素,他在一个集合中,我们可以使用一个元素来代表这个集合,称其为代表元,然后再对这个代表元来说,他如果在其他的集合中,其他的集合也有代表元,然后不断往上寻找,直到找找到了一个集合中有0,或者找不下去了,任务结束。因此我们可以看出,这个过程中涉及到的数据结构显然是一棵树。但是我们有不关心树的具体结构是什么,所以这就用到了树中的一个重要知识点并查集。
具体的实现的话,我们通过形成并查集的方式,将属于一个群体的学生都合并在一个并查集中,最后我们只要查询0所在的并查集中有多少个元素就行了。
我们使用两个数组,第一个数组dby记录元素的代表元是谁,第二个数组num记录这个元素所在群体的成员数量。首先我们先初始化num数组和dby数组,每个人的代表元都是自己,然后每个团体的成员数都是1。然后函数find用于递归寻找某元素的代表元,函数hebing用于将两个本属于同一个团体的成员合并到一个并查集中(先利用find函数查找两个成员所在并查集的代表元是否相同,如果不同的话,将小的并查集挂到大的并查集上去,并更新两个成员的代表元和成员数目),然后在每轮的输入团体中,将团体中的成员都合并到一块去。最后输出0所在并查集的成员数量即可。
代码实现:
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int n,m;
int dby[30010];
int num[30010];
int num1;
int x1,x2;
int i,j,lable;
int find(int x)
{
if(dby[x] == x)
{
return x;
}
return dby[x] = find(dby[x]);
};
void hebing(int x, int y)
{
x = find(x);
y = find(y);
if(x == y)
{
return;
}
if(num[y] > num[x])
{
lable = y;
y = x;
x = lable;
}
dby[y] = x;
num[x] = (num[y] += num[x]);
}
int main()
{
while(1)
{
scanf("%d %d",&n,&m);
if(n==0 && m==0)
{
break;
}
for(i=0; i<n; i++)
{
dby[i] = i;
num[i] = 1;
}
for(i=0; i<m; i++)
{
scanf("%d",&num1);
scanf("%d",&x1);
for(j=1; j<num1; j++)
{
scanf("%d",&x2);
hebing(x2,x1);
x1 = x2;
}
}
printf("%d\n",num[find(0)]);
}
return 0;
}
心得体会:
并查集作为树形结构中一个重要的知识点,需要我们对其拥有超级熟练的掌握度。这个题作为并查集的形象应用,进一步地加深了我们对于并查集的理解和掌握。并查集的具体知识,助教在课上已经讲解的非常透彻,本题的思路也有所提醒。所以大致思路不算是大问题,然后助教所教的一些小技巧,比如路径亚索,也十分地实用有效!使问题的解答更加简洁。
——————————————————————————————————————————————
C - 掌握魔法の东东 I
题目:
东东在老家农村无聊,想种田。农田有 n 块,编号从 1~n。种田要灌氵
众所周知东东是一个魔法师,他可以消耗一定的 MP 在一块田上施展魔法,使得黄河之水天上来。他也可以消耗一定的 MP 在两块田的渠上建立传送门,使得这块田引用那块有水的田的水。 (1<=n<=3e2)
黄河之水天上来的消耗是 Wi,i 是农田编号 (1<=Wi<=1e5)
建立传送门的消耗是 Pij,i、j 是农田编号 (1<= Pij <=1e5, Pij = Pji, Pii =0)
东东为所有的田灌氵的最小消耗
Input:
第1行:一个数n
第2行到第n+1行:数wi
第n+2行到第2n+1行:矩阵即pij矩阵
Output:
东东最小消耗的MP值
Example:
Input
4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0
Output
9
题目思路:
看到这个题,我们首先抽象题目模型,所有的农田,相当于一个图,其中每一块农田就相当于图中的点,传送门就相当于图的边,然后题目要求我们选择一部分的农田,并且连接一部分的农田,使得所有的农田都被灌溉、假设无视掉“黄河之水天上来”这种操作的话,就是农田要想被灌溉的话,必须和其他的农田有连接,那么这个题目就可以抽象成为,在一个图中,选择一部分的边,能够使得所有的点都被联通,而且这一部分边的权值之和在所有的选择之中是最小的,那么,显然,这个问题是一个最小生成树问题,我们可以使用最小生成树的相关算法再结合一系列的微操来处理这个问题。但是,事实上,我们并不能忽略"黄河之水天上来"这个操作,那该怎么办呢?
经过助教在课上的指导后,可以采用一个非常巧妙的方法,我们可以臆想出一个虚拟的农田,这个农田,和所有的实际农田之间,都有传送门,即是联通的,有边存在,边的权值即为“黄河之水天上来”所需要的权值。这样,我们就可以将整个问题,变形成为一个最小生成树问题。
在实际的代码实现上,关于数据的储存,还是利用结构体储存每一条边(注意重载操作符<),使用结构体数组储存所有的边。在最小生成树的实现中,我选择了kruskal算法,事先对所有的边关于权值进行升序排序,然后每次选择一条权值最小的边,每次选择时注意查看选择的边是否会生成环路(我们需要不生成)和更新总权值,直到选择足够的边。在判断环路形成时,我们仍然需要用到并查集的相关知识,用来检测即两个农田之间的边被选择后会不会生成环路(即两个农田是否已经属于一个并查集,这个可以用并查集实现中的合并函数的返回值来表示)。最后输出累计的权值即可。
代码实现:
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int n;
int dby[90010];
int find(int x)
{
if(dby[x]==x)
{
return x;
}
return dby[x]=find(dby[x]);
}
int hebing(int x, int y)
{
x = find(x);
y = find(y);
if(x == y)
{
return -1;
}
dby[x] = y;
return 0;
}
struct edge
{
int a;
int b;
int value;
bool operator < (const edge &edge1) const
{
return value < edge1.value;
}
};
edge edgex[90010];
int numedge;
int ans;
int x;
int i,j,lable;
void kruskal()
{
lable = 0;
for(i=1; i<=numedge; i++)
{
if(hebing(edgex[i].a, edgex[i].b) == 0)
{
lable+=1;
ans+=edgex[i].value;
}
if(lable == n)
{
return;
}
}
}
int main()
{
scanf("%d",&n);
numedge = 1;
for(i=1; i<=n; i++)
{
scanf("%d",&x);
edgex[numedge].a = 0;
edgex[numedge].b = i;
edgex[numedge].value = x;
numedge += 1;
}
for(i=1; i<=n; i++)
{
for(j=1; j<=n; j++)
{
scanf("%d",&x);
if(x == 0)
{
continue;
}
edgex[numedge].a = i;
edgex[numedge].b = j;
edgex[numedge].value = x;
numedge += 1;
}
}
numedge--;
ans = 0;
for(i=0; i<=n; i++)
{
dby[i] = i;
}
sort(edgex+1,edgex+numedge+1);
kruskal();
printf("%d",ans);
}
心得体会:
关于图中最小生成树的问题,上学期的数据结构课程已经深入的学习过,再经过课上助教的复习后,更加地印象深刻,关于这个题目,最巧妙的地方就在于,如何对于"黄河之水天上来",这个操作进行转化,如果这个能够按照助教的引导深刻理解后。那么这个题目就变成了单纯的最小生成树问题了。关于最小生成树问题的解答,助教提供的思路在环路判断时,使用了并查集,清晰明了还简洁,非常具有指导意义。(上学期我隐约记得关于这个kruskal算法的实现,我使用了最小堆来进行的处理,极其繁琐,难受嗷)。
——————————————————————————————————————————————
D - 数据中心
题目思路:
其实这个题目,和C题目基本上来说是差不多,也是一个最小生成树问题,由于和上边C题差不多,具体思路就不重复说了,核心也是并查集和Kruskal算法,具体实现方法和上题差不多,大致变通一下就行了。
代码实现:
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;
/* run this program using the console pauser or add your own getch, system("pause") or input loop */
int n,m,root;
struct edge
{
int a;
int b;
int value;
bool operator < (const edge &edge1)const
{
return value<edge1.value;
}
};
edge edgex[100010];
int dby[50010];
int lable,ans;
int find(int x)
{
if(x == dby[x])
{
return x;
}
return dby[x]=find(dby[x]);
}
int hebing(int x, int y)
{
x = find(x);
y = find(y);
if(x == y)
{
return -1;
}
dby[x] = y;
return 0;
}
int zxscs()
{
lable=0, ans=0;
for(int i=1; i<=m; i++)
{
if(hebing(edgex[i].a, edgex[i].b) == 0)
{
ans=max(ans,edgex[i].value);
lable++;
if(lable == n-1)
{
return ans;
}
}
}
return -1;
}
int main()
{
scanf("%d",&n);
scanf("%d",&m);
scanf("%d",&root);
for(int i=1; i<=n; i++)
{
dby[i] = i;
}
for(int i=1; i<=m; i++)
{
scanf("%d%d%d", &edgex[i].a, &edgex[i].b, &edgex[i].value);
}
sort(edgex+1, edgex+m+1);
printf("%d",zxscs());
return 0;
}
心得体会:
经过了C题和D题的训练后,深刻感觉到最小生成树这个知识点在图和树的相关知识体系中的重要性,以及在csp考试中的重要性,实在需要我们好好理解以及掌握。