【dawn·数据结构】最优结点问题

简要说明:
(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];
}
上一篇:USACO翻译:USACO 2012 FEB Silver三题


下一篇:非递归法创建二叉树