一、大纲
本周作业与实验题目如下:
- 氪金带东(DFS求取树的直径)
- 戴好口罩 (并查集思想)
- 黄河水灌溉(Kruskal最小生成树)
- 数据中心(Kruskal最小生成树)
二、逐个击破
1.氪金带东
题目描述
实验室里原先有一台电脑(编号为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).
题目分析
在解决问题之前,先介绍一种新的概念——二叉树的直径,其定义为树中任意两点之间距离的最大值。
- 求取直径
-首先明确树的直径一定是某两个叶子节点的距离,如果不是叶子节点则在其向叶子节点扩展的路径上一定有更长的距离
-任选一个结点 NiN_iNi 利用一次广度优先搜索(BFS)或者深度优先搜索(DFS)找到距离 NiN_iNi 最远的一个叶子节点 L1L_1L1
-然后再从 L1L_1L1 进行一次搜索遍历找到距离 L1L_1L1最远的叶子节点 L2L_2L2,则 L1L_1L1到 L2L_2L2的距离就是最长距离,也就是直径
现在来看这道题,题目所要求取的是任意一个结点 LiL_iLi 在树中最远的距离 DisiDis_iDisi ,不难发现,距离 LiL_iLi最远的结点一定是树的直径两端点其中的一个,由于无法确定每个点到 L1L_1L1 还是 L2L_2L2 更远,所以我们从两个端点 L1L_1L1 和 L2L_2L2 分别再进行一次遍历便可以找到从这两点到任意点的距离,也就得到了LiL_iLi在树中的最远可达距离,输出时选择较长的进行输出即可。
本题的数据结构选择的是邻接链表,也可以选择链式前向星(这种描述方式在日后题目介绍),两种方法都相较于邻接数组要好,因为节省空间占用,下面是数据结构使用和基本加边操作,代码如下:
struct edge
{
int u,v,w;
};
vector<edge> edges[MAXN];
//注意二维数组这种定义方式
void add_edge(int u,int v,int w)
{
edges[u].push_back({u,v,w});
}
void init()
{
for(int i=0;i<MAXN;i++)
{
vis[i]=false;
length[i]=0;
}
biggest=0,M=0;
}
然后下面是题目的全部代码:
#include<iostream>
#include<vector>
using namespace std;
int N;//回来再考虑会不会int爆零的问题
const int MAXN = 1e4 + 50;
bool vis[MAXN];
int length[MAXN],length1[MAXN],length2[MAXN];
int biggest=0,M=0;
struct edge
{
int u,v,w;
};
vector<edge> edges[MAXN];
//注意二维数组这种定义方式
void add_edge(int u,int v,int w)
{
edges[u].push_back({u,v,w});
}
void init()
{
for(int i=0;i<MAXN;i++)
{
vis[i]=false;
length[i]=0;
}
biggest=0,M=0;
}
int findMax()
{
int m = 0,index=-1;
for(int i=1;i<=N;i++)
{
if(length[i]>m)
{
m = length[i];
index = i;
}
}
return index;
}
void dfs(int u)
{//注意这个地方找直径的方法
vis[u]=true;
int n = edges[u].size();
for(int i=0;i<n;i++)
{
if(!vis[edges[u][i].v])
{
length[edges[u][i].v]=length[edges[u][i].u]+edges[u][i].w;
vis[edges[u][i].v] = true;
dfs(edges[u][i].v);
}
}
}
void dfs2(int u)
{
vis[u]=true;
int n = edges[u].size();
for(int i=0;i<n;i++)
{
if(!vis[edges[u][i].v])
{
length1[edges[u][i].v]=length1[edges[u][i].u]+edges[u][i].w;
vis[edges[u][i].v] = true;
dfs2(edges[u][i].v);
}
}
}
void dfs3(int u)
{
vis[u]=true;
int n = edges[u].size();
for(int i=0;i<n;i++)
{
if(!vis[edges[u][i].v])
{
length2[edges[u][i].v]=length2[edges[u][i].u]+edges[u][i].w;
vis[edges[u][i].v] = true;
dfs3(edges[u][i].v);
}
}
}
int main()
{
while(~scanf("%d",&N))
{
for(int i=2;i<=N;i++)
{
int v,w;
scanf("%d%d",&v,&w);
getchar();
add_edge(i,v,w);
add_edge(v,i,w);
}
dfs(edges[1][0].v);//树直径的一端
int d1 = findMax();
init();
dfs(d1);//树直径的另一端
int d2 = findMax();
init();
dfs2(d1);
init();
dfs3(d2);
for(int i=1;i<=N;i++)
printf("%d\n",max(length1[i],length2[i]));
for(int i=0;i<MAXN;i++)
{
length1[i] = 0;
length2[i] = 0;
}
init();
for(int i=1;i<=N;i++)
{//由于有多组数据,所以要clear
edges[i].clear();
}
}
return 0;
}
题目总结
这道题我的代码写法有一个不太好的地方,由于使用的是DFS,而DFS是通过递归的写法实现的,那么如果想要在递归过程中保存一些信息,那么需要利用全局变量,因为递归函数中局部变量无法保持信息的更新,但是这个题目需要多组信息的保存和更新,更好的方法是利用函数参数的改变来实现全局变量更新,而不需要写这么多遍函数。
2.戴好口罩
题目描述
新型冠状病毒肺炎(Corona Virus Disease 2019,COVID-19),简称“新冠肺炎”,是指2019新型冠状病毒感染导致的肺炎。
如果一个感染者走入一个群体,那么这个群体需要被隔离!小A同学被确诊为新冠感染,并且没有戴口罩!
需要尽快找到所有和小A同学直接或者间接接触过的同学,将他们隔离,防止更大范围的扩散。众所周知,学生的交际可能是分小团体的,一位学生可能同时参与多个小团体内。
- Input
多组数据,对于每组测试数据:
第一行为两个整数n和m(n = m = 0表示输入结束,不需要处理),n是学生的数量,m是学生群体的数量。0<n<=3×1040 < n <= 3\times10^40<n<=3×104 ,0<n<=5×1020 < n <= 5\times10^20<n<=5×102
学生编号为0~n-1,小A编号为0
随后,m行,每行有一个整数num即小团体人员数量。随后有num个整数代表这个小团体的学生。
- Output
输出要隔离的人数,每组数据的答案输出占一行
题目分析
在分析题目之前先了解一种新的数据结构——并查集,这种数据结构可以使用树形结构来实现,而与传统理解的树形结构不同,它具有以下区别:
- 并不在意严格意义上的父子结点关系以及树的形状
- 只关心结点属于哪一个类别
那么既然我们只关心一个元素属于哪个类别,那么我们可以用一个代表元素来代表这个类,一般是这颗树的根结点的元素。对于 nnn 个元素的初始化,它们独自是一个分组,即初始化代表元素就是他们自身,代码如下:
void init(int n)
{
for(int i=0;i<n;i++)
{
par[i]=i;
rnk[i]=1;
}
}
并查集的查询操作,如果我们想要查找一个元素,即为查找一个元素他的根节点的元素(代表元素),如下图所示,find[7]=find[4]=6find[7]=find[4]=6find[7]=find[4]=6
然而如果这样新的元素一直被连接在叶节点上,如果元素非常多那么时间复杂度会达到O(n)O(n)O(n),但是由于我们并不关心这种父子关系,所以这里我们可以压缩路径,没有必要一直增加树的高度,压缩路径如下图所示
具体实现路径压缩和查找路径的代码如下:
int find(int x)
{
if(par[x]==x)return x;
else return par[x] = find(par[x]);
}
而对于属于一个类别的元素我们需要进行合并操作,然而这里涉及一个谁作为“父树”,谁是“子树”的问题,也就是说应该把谁挂到谁上面,如果是“大树”挂“小树”上,那么树会更高,变成高树,所以应该“小树”挂“大树”上,具体代码如下:
bool unite(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return false;
if(rnk[x]>rnk[y]) par[y]=x,rnk[x]+=rnk[y];
else par[x]=y,rnk[y]+=rnk[x];
return true;
}
综上所述,该题全部解决代码如下:
#include<iostream>
using namespace std;
const int N = 3e4 + 50;
int par[N],rnk[N];
void init(int n)
{
for(int i=0;i<n;i++)
{
par[i]=i;
rnk[i]=1;
}
}
int find(int x)
{
if(par[x]==x)return x;
else return find(par[x]);
}
bool unite(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return false;
if(rnk[x]>rnk[y]) par[y]=x,rnk[x]+=rnk[y];
else par[x]=y,rnk[y]+=rnk[x];
return true;
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m))
{
if(n==0 && m==0) break;
init(n);
while(m--)
{
int num;
int last=-1;
scanf("%d",&num);
for(int i=0;i<num;i++)
{
int p;
scanf("%d",&p);
if(last!=-1) unite(p,last);
last=p;
}
getchar();
}
int index = find(0);
printf("%d\n",rnk[index]);
}
return 0;
}
3.黄河水灌溉
题目描述
农田有 n 块,编号从 1~n。种田要灌溉
众所周知东东是一个魔法师,他可以消耗一定的 MP 在一块田上施展魔法,使得黄河之水天上来。他也可以消耗一定的 MP 在两块田的渠上建立传送门,使得这块田引用那块有水的田的水。
黄河之水天上来的消耗是 Wi,iW_i,iWi,i是农田编号
建立传送门的消耗是PijP_{ij}Pij,i,ji,ji,j是农田编号
求东东为所有的田灌溉的最小MP消耗
- Input
第1行:一个数n
第2行到第n+1行:数wi
第n+2行到第2n+1行:矩阵即pij矩阵
- Output
东东最小消耗的MP值
题目分析
这道题目是典型的求取最小生成树的题目,这里使用的是Kruskal算法,是贪心算法的应用,其贪心准则为:将图中最小的非树边标记为树边,非法则跳过。其具体实现步骤如下:
1. 新建图GGG,GGG中拥有相同的节点,但没有边
2. 将原图中所有的边按照权值从小到大排序
3. 从权值最小的边开始,如果这条边连接的两个结点于图GGG中不在同一个连通分量中,则添加这条边到图GGG
4. 重复3,直至图GGG中所有的节点都在同一个连通分量中
而在步骤3中判断是否属于一个连通分量使用的便是并查集的思想,即在加入树边之前需要判断这条边的两个点是否在同一个连通分量中,如果不是则加入如果是则跳过。
Tips:这道题目需要特别注意的地方是,在给天地灌溉时还可以凭空浇灌,所以这就影响了Kruskal的使用,但是我们如果加入一个源点作为“凭空浇灌”的源头,与各个田地相连接的边作为待加入的边进行判断,这样就可以保证Kruskal的正确性了。
Kruskal获得最小生成树的代码如下:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXN = 1e5 + 50;
int w[MAXN],n,cnsum;
int par[MAXN],rnk[MAXN];
//并查集部分
int find(int x)
{
if(par[x]==x)return x;
else return find(par[x]);
}
bool unite(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return false;
if(rnk[x]>rnk[y]) par[y]=x,rnk[x]+=rnk[y];
else par[x]=y,rnk[y]+=rnk[x];
return true;
}
struct edge
{
int u,v,w;
bool operator <(const edge& theE)const
{
if(w!=theE.w)
return w<theE.w;
}
};
vector<edge> edges;
void add_edge(int u,int v,int w)
{
edges.push_back({u,v,w});
}
void init()
{
for(int i=0;i<MAXN;i++)
{
par[i]=i;
rnk[i]=1;
}
}
void kruskal()
{
int cnt=0;
for(vector<edge>::iterator it = edges.begin();it!=edges.end();it++)
{
int p1 = find(it->u);
int p2 = find(it->v);
if(p1 != p2)
{
cnt++;
cnsum+=it->w;
unite(it->u,it->v);
}
}
printf("%d",cnsum);
}
int main()
{
scanf("%d",&n);
getchar();
for(int i=1;i<=n;i++)
{
int tmp;
scanf("%d",&tmp);
add_edge(0,i,tmp);
getchar();
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
int tmp;
scanf("%d",&tmp);
add_edge(i,j,tmp);
}
getchar();
}
init();
sort(edges.begin(),edges.end());
kruskal();
return 0;
}
4.数据中心
题目分析
这道题目的整体描述非常复杂,但是总结概括题干所要求的问题其实就是求取最小生成树中最大的边权,如果搞清楚这个目标这个题目就和第三题完全一致,只需要在Kruskal算法进行过程中不断判断和更新最大边权然后最后输出即可,关于算法和数据结构在前面均已经描述过,这里就不再赘述,下面是题目的全部代码。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 50005;
const int M = 100005;
int m,n,root;
int par[N],rnk[N];
//并查集部分
int find(int x)
{
if(par[x]==x)return x;
else return find(par[x]);
}
bool unite(int x,int y)
{
x=find(x),y=find(y);
if(x==y)return false;
if(rnk[x]>rnk[y]) par[y]=x,rnk[x]+=rnk[y];
else par[x]=y,rnk[y]+=rnk[x];
return true;
}
struct edge
{
int u,v,w;
bool operator <(const edge& theE)const
{
if(w!=theE.w)
return w<theE.w;
}
};
vector<edge> edges;
void add_edge(int u,int v,int w)
{
edges.push_back({u,v,w});
}
void init()
{
for(int i=0;i<N;i++)
{
par[i]=i;
rnk[i]=1;
}
}
void kruskal()
{//二分答案求得最优解
int cnt=0,ans=0;
for(vector<edge>::iterator it = edges.begin();it!=edges.end();it++)
{
int p1 = find(it->u);
int p2 = find(it->v);
if(p1 != p2)
{
cnt++;
unite(it->u,it->v);
ans = max(ans,it->w);
if(cnt == n-1) break;
}
}
printf("%d",ans);
}
int main()
{
scanf("%d%d%d",&n,&m,&root);
getchar();
for(int i=1;i<=m;i++)
{
int tu,tv,tw;
scanf("%d%d%d",&tu,&tv,&tw);
add_edge(tu,tv,tw);
getchar();
}
sort(edges.begin(),edges.end());
init();
kruskal();
return 0;
}