简要说明:
(1) 题目来源:网络(华为2021-09-08秋招题)
(2) 代码仅供参考,尚可优化。如有改进空间,欢迎评论分享。
目录
题目简介
- 二叉树的最优结点,指移除以该结点为根结点的完整子树时,这棵子树与剩余的树各自的所有结点之和的差值的绝对值最大。如果一棵二叉树同时有多个满足这一条件的结点,认为其最优结点是序号最小的那一个。
- 给定一棵二叉树,每个结点分别有其编号和值,其值可能是一个负数。找出其最优结点。
输入样例:
4
4 9 -7 -8
0 1
0 3
1 2
输入样例说明:
第一行是这棵树的总结点数N,其中1 ≤ N ≤ 10000。
第二行是所有结点的权值,对应结点编号依次升高,允许权值为负数,每两个权值之间以空格隔开。
之后所有行表示各结点(输入值为其编号)之间的父子关系,前者为父,后者为子。同一结点两次出现在父结点位置上时,总是假设左孩子结点出现在右孩子结点之前。以本输入样例为例,这就意味着0的左孩子为1,右孩子为3。
输出样例:
3
输出样例说明:
不难得证,这棵二叉树的最优结点为编号为3的结点。读者可自行验证。
思路分析
注:仅代表个人思路。
- 本题难度应该不是特别大,可以使用最为“暴力”的方法——依次尝试各个结点,通过逐一比较选择出编号最小的最优结点。
- 唯一的问题在于树结构。这道题虽然在二叉树的背景下,但我们并没有必要去完整地建一棵树(关于如何完整建树,请参见后面的部分)。这就需要我们给出一个“模拟”这棵树的数据结构。
- 本题给出的方法,是根据结点个数建立一个数组,每个数组表项存放两个数字,分别是其左孩子和右孩子。这个方案的本质上就是一个树结构,但占用空间会更小一些。最后考虑用层次遍历的方式,线性计算每个结点对应子树的结点和。
代码
#include <iostream>
#include <queue>
using namespace std;
struct relationship {
int left; //表示这一对中父结点的索引
int right; //表示这一对中子结点的索引
relationship():left(-1),right(-1) {}
};
int main() {
//进行输入
int nodes_number, *node, sum;
int record = 0, result = -1;
relationship *pairs;
bool *isRoot;
cin >> nodes_number;
node = new int[nodes_number];
sum = 0;
pairs = new relationship[nodes_number]; //由于二叉树的性质, 边数一定等于结点数减去1.
isRoot = new bool[nodes_number]; //需要筛选掉根结点. 如果一个结点没有出现过成为子结点的情况, 就一定是根结点.
for (int i = 0; i < nodes_number; ++i)
isRoot[i] = 1;
for (int i = 0; i < nodes_number; ++i) {
cin >> node[i];
sum += node[i];
}
//建对
for (int i = 0; i < nodes_number - 1; ++i) {
int inputNode, childNode;
cin >> inputNode >> childNode;
if (pairs[inputNode].left == -1)
//代表是第一次作为父结点, 则是左孩子.
pairs[inputNode].left = childNode;
else
//代表是右孩子.
pairs[inputNode].right = childNode;
isRoot[childNode] = 0;
}
//解决问题
//使用队列和循环解决这个问题
queue<int> q;
for (int i = 0; i < nodes_number; ++i) {
if (isRoot[i])
continue;
int subSum = 0;
//假设拿出第i个结点, 并且这个结点已经证明不是根结点.
//从理论来说, 每次新的循环内q都是空的.
q.push(i);
while (!q.empty()) {
int front = q.front();
subSum += node[front];
if (pairs[front].left != -1) {
q.push(pairs[front].left);
if (pairs[front].right != -1)
q.push(pairs[front].right);
}
q.pop();
}
//最后比较绝对值.
int subSum2 = sum - subSum;
int Ri = subSum - subSum2 > 0 ? subSum - subSum2 : subSum2 - subSum;
if (Ri > record) {
record = Ri;
result = i;
}
}
cout << result;
}
注:原题中结点个数可能会逼近10000,这就意味着结点和使用简单的int可能会有越界问题。在本题中仅考虑简单情况,读者需要在实际情况下替换为long long型,避免溢出问题。
讨论:建树
之后的一个问题就是,如果要以本题的输入来建一棵二叉树,怎么建。最开始我想得比较复杂,甚至搬出了不怎么熟悉的并查集(Union-Find Sets)出来,但后来考虑一下,多个数组就可以解决这个问题。如果您已有思路,可以直接前往代码部分,这里简单地对代码进行一下解释。
建树的大概过程和之前代码类似,不过会有几个关键问题。
- 由于后半部分结点关系的输入的顺序不能得到保证,这就说明在不断建树的过程中,可能会出现多个根结点,甚至会出现多棵子树的情况。问题在于如何确定最后的根结点。
- 如何插入新结点。考虑到树结点本身可能不存放编号,那么对于特定结点的孩子的插入,就可能会要遍历一遍树,这样在最差情况下最终的时间复杂度会达到O(N2),这个时间复杂度难以接受。
考虑到这些问题,最后的解决方案是建立一个看似没有必要的指针数组,储存对应编号对应结点的地址。同时,建立一个整型数组,存放每个结点的父结点。
无论是合并子树还是新插入结点,都会给这个新插入的结点设置父结点。那么最终父结点没有得到设置的结点,就一定是根结点。在这里,并不考虑错误输入的情况。
最终代码如下:(只考虑关键代码)
struct TreeNode {
int index;
int key;
TreeNode *lchild, *rchild;
TreeNode(int i, int k = 0, TreeNode *l = NULL, TreeNode *r = NULL) :index(i), key(k), lchild(l), rchild(r) {}
};
class Tree {
public:
Tree() :root(NULL) {}
TreeNode *create(istream& is);
void prePrint() { prePrint(root); } //先序遍历
void midPrint() { midPrint(root); } //中序遍历
TreeNode *root;
private:
void prePrint(TreeNode *r);
void midPrint(TreeNode *r);
};
//TODO
//如何仿照本题要求的格式进行树的构造
TreeNode *Tree::create(istream& is) {
int nodeNumber, rootIndex;
TreeNode **nodes;
int *fathers; //记录各个结点父结点的索引.
is >> nodeNumber;
nodes = new TreeNode*[nodeNumber];
fathers = new int[nodeNumber];
for (int i = 0; i < nodeNumber; ++i)
fathers[i] = -1;
for (int i = 0; i < nodeNumber; ++i) {
int x;
cin >> x;
nodes[i] = new TreeNode(i, x);
}
for (int i = 1; i < nodeNumber; ++i) {
//开始建树
int fi, si;
is >> fi >> si;
fathers[si] = fi;
if (nodes[fi]->lchild != NULL)
//说明应该是右孩子
nodes[fi]->rchild = nodes[si];
else
//说明应该是左孩子
nodes[fi]->lchild = nodes[si];
}
//最后遍历fathers数组, 哪一个值没有进行修改就说明这是根结点. 这里不考虑错误输入的情况.
for (rootIndex = 0; rootIndex < nodeNumber&&fathers[rootIndex] >= 0; ++rootIndex);
return root = nodes[rootIndex];
}