UVa679小球下落(二叉树的编号)
树是n个元素的有限集合,不再是序列,其中\(n>=0\)。树可以看成无共享纯广义表。
二叉树的3个要素,根结点,左子树和右子树。二叉树不是树,树的两个要素是根结点和子树森林。
无论线性结构还是树形结构,第一个元素都没有前驱。线性结构的最后一个元素都没有后继,树形结构有多个叶子结点,都没有后继。对于中间的一般元素,树形结构有一个前驱,多个后继,线性结构有一个前驱(无共享),一个后继。
完全二叉树是从上到下从左到右依次排过来,整棵树有唯一的深度取值。丰满二叉树是每一层都排满的完全二叉树,有\(2^i-1\)个结点,每层的结点数目是\(2^{i-1}\)个,其中i是深度。
完全二叉树的从上到下从左到右编号上有规律和结论:编号为1,没双亲,不为1就有双亲;\(2i>n\),没有左孩子,否则就是左孩子;\(2i+1>n\),没有右孩子,否则就是右孩子。
采用顺序存储,完全二叉树不浪费空间,各种运算简单,由上述性质,求双亲求孩子均为常量算法。
模拟
对于这道题目来说,可以模拟小球下落的过程。题目告诉了最大深度为D,所有叶子深度都相同,就告诉了这是一棵丰满二叉树,初始所有结点的开关全部关闭。如果把0当作关闭,1当作打开,那么当小球到达一个结点时,就是先走再改变状态。判断,如果是关闭,往左走,如果是打开就往右走,直到走到叶子结点。
题目给定深度D和小球个数I,由深度D就可以求出最大的小球编号n = \(2^D-1\),接下来就利用上述性质进行模拟。
深度不超过20,编号最大是2的20次方-1,接近1000000的结点个数。
表示2的次方可以不用cmath里的pow(),而是用二进制计算,用1来移位。逻辑运算布尔非改变开关状态。
从这个数据范围以及多组测试数据来看,肯定会超时2 ≤ D ≤ 20, and 1 ≤ I ≤ 524288.
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int MAXN = 1000010;
int s[MAXN];
int D, I;
int main() {
int T;
scanf("%d", &T);
while (T--) {
if (scanf("%d%d", &D, &I) != 2) {
break;
}
memset(s, 0, sizeof(s));
int n = (1<<D)-1;
int last = 1;
for (int i = 0; i < I; i++) {
int cur = 1;
// printf("小球%d\n", i);
while (cur <= n) {
// printf("%d\n", cur);
last = cur;
if (s[cur]) {
cur = (cur<<1)+1;
} else {
cur = cur<<1;
}
s[last] = !s[last]; // 逻辑运算,cur改变了,应该改的是原来的开关,看的是改之前的值
}
}
printf("%d\n", last);
}
return 0;
}
只看一个小球
对于一个结点,必然是上一个往左下一个往右。对于根结点,奇数小球往左走,偶数小球往右走,所以只需看小球编号就知道怎么走。以下所有结点都是如此。
例子,4个小球里面就有2个往左2个往右。也就对应了2个来到左孩子,2个来到右孩子,改变孩子结点的状态。
第1个小球是第1个往左的,第2个小球是第一个往右的,第3个小球是第2个往左的,第4个小球是第2个往右的。
所以把走小球变成走深度就可以了,只看最后一个小球。
这样不仅节省大大时间,而且节省空间。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
int D, I;
int main() {
int T;
scanf("%d", &T);
while (T--) {
if (scanf("%d%d", &D, &I) != 2) {
break;
}
int n = (1<<D)-1;
int k = 1;
for (int i = 2; i <= D; i++) {
if (I % 2 == 1) {
k = (k << 1);
I = (I + 1) >> 1;
} else {
k = (k << 1) + 1;
I = I >> 1;
}
}
printf("%d\n", k);
}
return 0;
}