题目链接:http://soj.sysu.edu.cn/1034
题目大意:给定一个森林,判断该森林是否合法。如果不合法输出INVALID,如果合法输出森林的深度和宽度。
解题思路:对合法的森林构建一棵树,对该树按照广度优先搜索进行遍历,遍历每一层时把下一层的节点数算出来与maxwidth进行比较,把最大值赋给maxwidth从而算出森林宽度。而深度则相对比较简单,只需从根节点遍历到结束,遍历完每一层时深度加一即可。
那么,如果构建一棵树呢?因为森林是由很多的树构成的,这是只需增加一个根节点指向所有原先森林中的树的根节点就可以把森林变成一棵树了。
那怎么判断输入的森林是否合法呢?当然最先想到的就是用深搜,看是否存在环路。这种方法当然可行,不过我不想用深搜,觉得深搜很浪费时间,所以我就用了一种比较繁琐的方法进行判断。首先判断森林中是否不存在根节点,即所有节点构成一个环;其次判断是否存在自己指向自己的节点;然后再判断新构成的树中是否存在入度大于一的点;最后再判断除了新构成的树外是否还存在环。
嗯,真的比较繁琐,没有直接深搜来得方便,也没有深搜那么容易理解。不多说了,注释都在代码中了。
#include <iostream>
#include <queue>
using namespace std;
// 用于存储每一个节点的结构体
struct Node
{
int father; // 当前节点的父节点号码
int count; // 当前节点的子节点的数量
int children[100]; // 存储当前节点的子节点号码
Node() // 初始化函数
{
father = -1;
count = 0;
for (int i = 0; i < 100; i++)
children[i] = -1;
}
};
int main()
{
/* 变量说明
// n, m 是题目输入的变量,表示节点总数和边总数
// a, b 是题目输入的变量,表示从 a 到 b 有一条有向边
// edge表示把森林(多棵树)变成一棵树后的边的数量
// maxwidth表示最大的层节点数,即题目所要求的森林宽度(width of forest)
// nowwidth表示当前这一层的节点的数量
// nextwidth表示下一层节点的数量
// depth表示把森林(多棵树)变成一颗树后树的深度,其减一就是题目要求的森林深度(depth of forest)
// cnt表示当前这个节点在该层节点中的位置,用于判断该层节点是否已访问完毕
// nodecount表示访问的节点数量,用于最后判断是否还有节点没访问(即存在环路)
// iserror用于判断输入的森林是否合法,合法为 false,非法为true
*/
int n, m, a, b, edge, maxwidth, nowwidth, nextwidth, depth, cnt, nodecount;
bool iserror;
while (1)
{
// node[i]表示编号为 i 的节点
// 这里把数组放在循环里面,方便每次对node数组的初始化
Node node[102];
Node now; // 用于表示当前节点
queue<Node> q;
iserror = false;
cin >> n >> m;
if (n == 0)
break;
if (m == 0)
{
cout << "0 " << n << endl;
continue;
}
for (int i = 0; i < m; i++)
{
cin >> a >> b; // 读入变量从 a 指向 b
node[a].children[node[a].count++] = b; // 把 b 添加到 a 节点的子节点中
node[b].father = a; // 把 a 添加到 b 节点的父节点中
}
edge = m; // 还没把森林变树前,边是相等的
/* for 循环说明
// 此循环通过增加根节点将合法的森林变成一棵树
// 如果不合法,这里只对“自己指向自己的节点”这种情况进行判断,其他情况在程序后面判断
// 从 1 到 n 是因为 node[i]对应第 i 个节点,而节点编号刚好是从 1 到 n
// 另外,node[0]留出来用于表示把森林变成树后的根节点
*/
for (int i = 1; i <= n; i++)
{
// 如果这是森林中的一棵树的根节点,那么把它作为node[0]的子节点
if (node[i].father == -1)
{
node[i].father = 0;
node[0].children[node[0].count++] = i;
edge++; // 因为增加了 node[0] 指向当前节点,故边要加一
}
// 这里的 if 条件用于判断“自己指向自己”的情况
if (node[i].father == i)
{
iserror = true;
break;
}
}
/* 排除一些不合法的情况
如果 m == edge,那么表示原森林中不存在根节点,即所有的节点构成一个环
如果 iserror == true,表示存在自己指向自己的节点
如果 n != edge, 表示存在入度大于 1 的节点
关于 n != edge为什么就不合法了这个问题,这里详细说明一下:
如果是一棵树,则节点数 = 边数 + 1;
由于这里增加了一个根节点,所以当 n + 1 == edge + 1(即 n == edge)时就表
示是一颗树(当然,这里有漏洞,即存在这棵树外还存在环,这个环是没有被包含
在这棵新构建的树中的,这里把这种情况留在程序后面进行判断。但是这里能排除
在新构建的树中的一些非法的情况,如入度大于2),否则就不是一颗树,从而说
明原来的森林是不合法的森林
*/
if (n != edge || iserror || m == edge)
{
cout << "INVALID" << endl;
continue;
}
// 初始化,从根节点开始
q.push(node[0]);
maxwidth = -1;
cnt = 0;
nowwidth = 1;
nextwidth = 0;
depth = -1; // 这里初始化为 -1 是因为从根节点开始,而根节点是自己加上去的,所以会多了一层
nodecount = 0;
while (!q.empty())
{
now = q.front();
q.pop();
nodecount++;
for (int i = 0; i < now.count; i++) // 把当前节点的子节点压入队列
q.push(node[now.children[i]]);
cnt++;
nextwidth += now.count; // 把当前节点的子节点数量加到nextwidth中
// 如果cnt == nowwidth,即表示该节点是这层的最后一个节点,需要重新进行初始化
if (cnt == nowwidth)
{
if (nextwidth > maxwidth)
maxwidth = nextwidth;
nowwidth = nextwidth;
nextwidth = 0;
depth++;
cnt = 0;
}
}
/* 这里判断是否存在“新构建的树外还有环”的情况
// 如果nodecount - 1 != n 表示存在环,因为上述队列的操作中只对新构建的树
// 进行操作,其遍历过的节点数记录在nodecount中,由于增加了一个根节点,故
// nodecount 需要减一,如果nodecount - 1 != n,那么表示还有节点没有被访问
// 到,由于已经遍历了树的所有节点,那剩下的未被遍历的节点只能是以环的形式
// 存在,否则也会被纳入这棵树中。至此,把最后一种不合法的情况也排除了。
// depth - 1是因为最后一层的最后一个节点访问结束后会让depth加一,因此这里需要减一
*/
if (nodecount - 1 == n)
cout << depth - 1 << " " << maxwidth << endl;
else
cout << "INVALID" << endl;
}
return 0;
}