题目大意:
给你一个初始的n个人,每次间隔的加人,直到加到k个人。
对于样例3,7,q,初始序列为:1,2,3。
第一轮加人:1,4,2,5,3,6。人不够,所以接着加
第二轮:1,7,4,2,5,3,6。到了第七个人,所以停止。
q次询问,第i次问你 qi 的编号是多少。如上例:1:1,2:7,3:4,4:2
首先分析数据生成的规律,每次翻倍,即:
n
→
n
∗
2
→
n
∗
2
2
.
.
.
→
n
∗
2
r
n \rightarrow n * 2 \rightarrow n * 2^2 ... \rightarrow n*2^r
n→n∗2→n∗22...→n∗2r
首先我们考虑简单的情况(k = n*2^r)
对于上边的样例,按照题目要求添加到第七个人就停止了,我们先考虑不停止,加满的情况。对于加满的情况,我们可以直接看题目中给的例子:
找规律,我们可以发现,对于初始的1和2,2和3之间的元素个数是一样的,所以这是一个循环节(在数量上)。因为每次通过同样的方式加元素。我们考虑样例:
···
3 24 2
2
3
output:
13
7
···
1
,
13
,
7
,
14
,
4
,
15
,
8
,
16
,
2
,
17
,
9
,
18
,
5
,
19
,
10
,
20
,
3
,
21
,
11
,
22
,
6
,
23
,
12
,
24
1,13,7,14,4,15,8,16,\\ 2,17,9,18,5,19,10,20,\\ 3,21,11,22,6,23,12,24
1,13,7,14,4,15,8,16,2,17,9,18,5,19,10,20,3,21,11,22,6,23,12,24
第三轮添加之后结果如上,虽然初步感觉每行之间好像有什么关系,但是我们先搁置,下边先分析第一行是怎么生成的。
在1和2之间加的元素:13,7,14,4,15,16,明显的中序遍历。更一般的我们可以用符号表示(如下图),其中n = 3代表初始元素个数,h = 3代表树的高度(也是r,添加的轮数)
对于一颗满的二叉树,我们可以计算其节点的总个数,从而判断最终到达哪个节点。
- 对于当前询问qi = 2,我们首先在先减去不在树内的节点1,then qi = 1,于是我们需要得到树内的第一个节点(中序遍历)。
- 当前节点:根节点n+1。计算左子树的节点数目为: 2 ( h − 1 ) − 1 = 3 , 2^{(h-1)} - 1 = 3, 2(h−1)−1=3, 我们要找的是第一个,1 < 3 + 1,于是询问走向左子树2n+1。(ps:如果这里要找的是4,即等于左子树的数目加1,那就可以直接返回当前节点了)
- 当前节点:2*n+1。计算左子树个数为: 2 ( h − 2 ) − 1 = 1 , 2^{(h-2)} -1 = 1, 2(h−2)−1=1, qi = 1 < 1 + 1,所以还得走向左子树。
- 当前节点:4*n+1。计算左子树个数为: 2 ( h − 3 ) − 1 = 0 , 2^{(h-3)} -1 = 0, 2(h−3)−1=0, qi = 1 == 0 + 1,当前返回节点。
- 计算当前节点的大小。按照上述方法的确可以找到,但是需要很大的空间去储存每个节点的内容。但是就如图2的符号化表示,我们无需实际存树,通过当前节点的高度hh和从左往右的序数ll就可以计算出来。如下式子:
n
∗
2
h
−
1
+
k
+
l
l
n * 2^{h-1} + k + ll
n∗2h−1+k+ll
hh在每次移到左右子树的时候加1,ll走到左子树的时候 l l ∗ = 2 , ll *= 2, ll∗=2,走到右子树的时候 l l ∗ = 2 + 1 。 ll *= 2+1。 ll∗=2+1。
按照此方法,我们可以得到第一个循环节的所有元素。接下来考虑如何生成后续的循环节。
- 生成后续循环节
1 , 13 , 7 , 14 , 4 , 15 , 8 , 16 , 2 , 17 , 9 , 18 , 5 , 19 , 10 , 20 , 3 , 21 , 11 , 22 , 6 , 23 , 12 , 24 1,13,7,14,4,15,8,16,\\ 2,17,9,18,5,19,10,20,\\ 3,21,11,22,6,23,12,24 1,13,7,14,4,15,8,16,2,17,9,18,5,19,10,20,3,21,11,22,6,23,12,24
找规律:
- 原始序列是+1+1的,没啥考虑的。
- 我们发现有的元素+1+2,有的+4,区别就是,+1的是在第一轮添加的,+4实在第三轮添加的。因此我们可以直接加上2的幂乘上循环节的个数来解决。
k多出来的情况
对于之前不多出来的情况,我们有函数 int cal(int n,int h,int qi),返回第qi个人的序号。
- 对于样例(1):3 7 q,序列为:1,7, 4,2,5,3,6
- 对于样例(2):3 8 q,序列为:1,7,4,8,2,5,3,6
- 对于样例(3): 3 9 q,序列为:1,7,4,8,2,9,5,3,6
- 对于样例(4):3 12 q,序列为:1,7,4,8,2,9,5,10,3,11,6,12
我们可以发现,对于多出来的部分,我们完全可以使用完整的那组的部分,因为前边的元素是一样的,同时题目中也提到q的讯问中不会超过k。
所以对于超过部分我们可以适当的转化。观察上述的函数,我们还需要改变询问的位置qi。
假设r论为完全填满,对于超出k的部分,我们要使用r-1。
对于样例(1),对于超过2的部分我们需要改用第 r-1 = 1轮。对比第1轮,多了 k-n*2 = 1个元素。
对于样例(2)对于超过4的部分用 r-1 = 1轮。对比第1轮,多了 k-n*2 = 2个元素。
对于样例(3)对于超过6的部分用 r-1 = 1轮。对比第1轮,多了 k-n*2 = 3个元素。
所以,对于两倍的 temp = k % (n2^r) 以外的部分,直接适用上一轮的 r-1 就可以了,直接减去temp个元素。在 2temp 以内的直接使用 r 轮的。
注意两个点
-
temp取模完等于0的情况需要特判。因为这时候询问 qi 肯定大于 2*temp,按上述逻辑会使用 r-1 轮的计算。
-
cal函数中需要特判 h == 0 的情况,因为第一轮的数是直接得到的,不是通过建树得到的。
AC代码
#include <stdio.h>
#include <string.h>
#include <cmath>
#include <iostream>
using namespace std;
typedef long long int lli;
// n:初始个数 h:添加了几轮(也是高度) q:询问点
int cal(int n, int h, int q) {
if (h == 0) {
return q;
}
int ans = 0;
int xunhuan = pow(2, h);
int k = q / xunhuan;
if (q % xunhuan != 0) {
k++;
}
// search_tree
int b = q % xunhuan;
if (b == 1) {
return k;
} else {
if (b == 0) b = xunhuan;
b--;
int al = xunhuan - 1;
int num_l = 0, num_h = 1;
//在左边的是al个,需要找到第b个
for (int i = 1; i <= h; i++) {
al--;
al /= 2;
if (b < al + 1) {
num_h++;
num_l *= 2;
} else if (b > al + 1) {
b -= al + 1;
num_h++;
num_l = num_l * 2 + 1;
} else {
break;
}
}
return pow(2, num_h - 1) * n + 1 + (k - 1) * pow(2, num_h - 1) + num_l;
}
}
int main() {
int n, k, q;
while (scanf("%d %d %d", &n, &k, &q) != EOF) {
lli base = 1; //用int也A了
int r = 0;
for (int i = 1;; i++) {
if (n * base >= k) {
break;
}
r++;
base *= 2;
}
int qq, ans;
for (int i = 1; i <= q; i++) {
scanf("%d", &qq);
lli temp = k % (n * base / 2);
if (temp == 0 || qq <= temp * 2) {
ans = cal(n, r, qq);
} else {
ans = cal(n, r - 1, qq - temp);
}
printf("%d\n", ans);
}
}
return 0;
}