题目描述
在一片海域里面有 N N N 个海岛连成了树的形状,即互相连通。现在我们想去掉一个海岛 U U U 把整片连通区域分割成更小的连通区域,并且希望满足每个小连通区域的海岛数量不超过 k k k,请根据输入的海岛区域信息来输出所有可能使分割愿望成立的海岛 U U U 的编号(保证给出的海岛信息可以构成一棵树)。
输入格式
第 1 行,两个整数 n n n 和 k k k,代表有 n n n 个海岛,且每个海岛的编号为 1 , 2 , … , n 1,2,…,n 1,2,…,n。
第 2~n 行,每行两个整数 a , b a,b a,b,代表海岛 a a a 和海岛 b b b 之间连通。
输出格式
一行,如果没有符合条件的海岛
U
U
U,则输出 None
,
如果有,从大到小依次输出所有满足条件的海岛 U U U 的编号,用空格分开。
样例输入
10 5
1 7
4 7
6 1
8 4
9 4
5 6
2 1
3 1
10 8
样例输出
7 1
数据范围
对于百分之三十的数据, 1 < k < n ≤ 30 1<k<n≤30 1<k<n≤30
对于百分之六十的数据, 1 < k < n ≤ 1000 1<k<n≤1000 1<k<n≤1000
对于百分之百的数据, 1 < k < n ≤ 1 0 5 1<k<n≤10^5 1<k<n≤105
题目分析
首先这个题目我觉得作为刚刚学习程序设计的同学来说还是有点难度的。比较这个题目已经在考察图、树的一些知识了。我们首先来看看题目涉及到的知识(或者说考差要完成的任务)有下面几条:
- 树的基本概念
- 树的分割
- 子结点的数量统计
- 父节点的保存
- 动态规划的思想
其实呢这个题目是由另一道题目改编过来的,如果有兴趣当然可以看一看文末的一道补充题目,比较一下,唯一的区别就是下面的这个题目的 k = m / 2 k=m/2 k=m/2 。当然两个题目都是交大出的题目hhhh
问题解答
考虑到初次接触图的题目,还是列举一下新手可能问的问题:
- 问题一:如何保存一个树或者说一个图 ?
- 解答:严格的说这一部分是属于离散数学的东东,但是这里要用我们就干脆系统的归纳一下有哪些方法吧!
方法一:邻接矩阵: 举个例子我们要保存下面这个图,1号岛屿连接2、3号岛屿,我们可以构建一个矩阵:
(
0
1
1
1
0
0
1
0
0
)
\begin{pmatrix} 0 & 1 & 1\\ 1 & 0 &0 \\ 1 & 0 & 0 \end{pmatrix}\quad
⎝⎛011100100⎠⎞,显然这个矩阵是一个实对称矩阵,矩阵中,如果结点
i
i
i 与与结点
j
j
j 相通,则
a
i
j
=
1
a_{ij}=1
aij=1,反之,
a
i
j
=
0
a_{ij}=0
aij=0表示不相同!此时有人会问一定要是1吗,可不可以是别的正数呢?当然可以!可以按照加权值来构造这个矩阵,那么这个矩阵就是权矩阵。
方法二:边列表、正向表、逆向表:
首先我们要了解有向图和无向图的概念,任意两个结点相连的线段有方向(或者说是有向边)就是有向图,任意两个结点相连的线段没有方向(或者说是无向边)就是无向图。
然后我们开两个空间尽可能大的数组,a[1000]
和 b[1000]
,如果是有向图,那么a数组表示有向边的起点,b数组表示有向边的终点,无向边那就可以随性一点点。当然还可以补充一个数组c[1000]
来保存边的权值。
- 问题二:这个题目如何保存图?
- 解答:使用
<vector>
,如果你不太熟悉这个,可以去看一看我们之前学过的广度优先搜索里面的队列的知识,传送门:队 <queue> 的知识 ,vector的本义是矢量,实际上这是一个封装了动态大小的数组,说白了就是队列的升级版,你可以利用这个vector构造几十个或者几百个队列,当然还有别的功能那就不细说了。这个题目用vector更好,因为题目的岛屿可能有一万多,如果是二维数组保存会非常占用空间,然而用队列的话哈哈哈哈,节约的空间很多,比如,岛屿 i i i 与岛屿 j j j 相连,我们就在队伍 i i i 里面加入元素 j j j ,然后在队伍 j j j 元素里面加入元素 i i i。与二维数组相比节约的空间很明显。 - 问题三:这个题目算法思想是什么?
- 解答:首先如果你仔细读题的话,题目中有一句话说到保证给出的海岛信息可以连成一棵树,那么不妨就从树的思想开始,(完全不会树可以看数据结构的书去咯)树里面除了根结点,其他的结点都有唯一的一个父结点和若干的子结点(或者没有子结点),我们要逐一的判断每一个岛屿能不能去掉,当然判断之前我们首先得搞出一个树出来,毕竟题目的数据给的并不是一个树,我们必须得找一个根结点,与其纠结根结点是啥不妨就选1号得了hhh。
然后我们要计算每一个结点的所有的儿子结点的个数(当然也包括自身!)这里用一个数组p[]
保存就完事了。请记住这个数组,后门会用到!
判断某一节点 m m m 可以不可以去掉,依据是什么?我们想一下,去掉这个节点,还剩下什么?剩下的是 m m m 节点的所有的儿子结点的相关的子树,还有 m m m 结点的父结点相关的那一坨东西。举个例子看下面,我们去掉结点3,剩下就是的父结点1相关的一坨东西,还有以3结点的子结点为根结点的子树!具体可以对照这个图理解。
所以就很明显了,我们的要判断结点
m
m
m 可不可以去掉,就要检测所有的儿子结点的相关的子树,还有
m
m
m 结点的父结点相关的那一坨东西是否超过题目的条件(即是否会超过
k
k
k)儿子结点的子树的结点数可以根据我们之前说的数组来直接判断,父结点相关的一部分可以用总共的结点数减去删去结点对应在数组p的数值即可。然后我们来看看代码吧!
#include <iostream>
#include <vector>
#include <queue>
#include <string.h>
using namespace std;
int n, k;
int father_node[100005];
int self_and_sonnum[100005];
bool ifvisit[100005];
vector<int> island[100005];
vector<int> son_island[100005];
// 下面这个函数使用的时候必须使用 tree_generator(1);
// 函数的目的:以1为根结点构造一个树
void tree_generator(int begin)
{
ifvisit[begin] = true;
for(auto i:island[begin])
{
if (ifvisit[i] == false)
{
son_island[begin].push_back(i);
father_node[i] = begin;
tree_generator(i);
}
}
}
// 函数目的:计算每个结点的相关的所有的子结点以及自己的个数!
int son_num(int begin)
{
int count = 1;
for(auto i: son_island[begin])
{
count = count + son_num(i);
}
self_and_sonnum[begin] = count;
return count;
}
int main()
{
memset(ifvisit, false, sizeof(ifvisit));
cin >> n >> k;
int temp1, temp2;
for (int i = 2; i <= n;i++)
{
cin >> temp1 >> temp2;
island[temp1].push_back(temp2);
island[temp2].push_back(temp1);
}
tree_generator(1);
son_num(1);
int num = 0;
for (int j = n; j >= 1;j--) // 从大到小检测结点j可不可以删除
{
bool flag = true; // 先假设可以删除
if (n - self_and_sonnum[j] > k)
{
continue; // 父结点的相关部分不满足要求,拒绝
}
for (auto x : son_island[j])
{
if (self_and_sonnum[x] > k)
{
flag = false;
break; // 结点j的儿子结点的子树不满足要求,拒绝
}
}
if(flag) // 满足条件,则输出结果
{
cout << j << " ";
num++;
}
}
if (num == 0)
cout << "None";
system("pause");
return 0;
}
补充知识 auto遍历
下面这个方法可以用for
循环遍历一个动态数组/数组!看看就好。
for(auto 变量名: 数组名/首地址)
补充题目
Description
现在有一棵树T,有N个节点,我们想通过去掉一个节点p来把T分割成更小的树,并且满足每个小树中的节点数不超过n/2。
请根据输入的树来输出所有可能的p的号码。
Input Format
第1行:一个整数N,代表有N个节点,且每个节点的编号为1,2,…,N。
第2~N行:每行两个整数x,y,代表节点x和节点y之间连通。
Output Format
从小到大一次输出满足条件的p的号码,每行1个可行解。
Input Sample
10
1 2
2 3
3 4
4 5
6 7
7 8
8 9
9 10
3 8
Output Sample
3
8