HDU 3042 Josephus Again 题解

题目大意:

给你一个初始的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 nn2n22...n2r

首先我们考虑简单的情况(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 113714415816217918519102032111226231224
第三轮添加之后结果如上,虽然初步感觉每行之间好像有什么关系,但是我们先搁置,下边先分析第一行是怎么生成的。

元素:1
4
7
8
13
14
15
16

在1和2之间加的元素:13,7,14,4,15,16,明显的中序遍历。更一般的我们可以用符号表示(如下图),其中n = 3代表初始元素个数,h = 3代表树的高度(也是r,添加的轮数)

元素k = 1
n+k
2*n+k
2*n+k+1
4*n+k
4*n+k+1
4*n+k+2
4*n+k+3

对于一颗满的二叉树,我们可以计算其节点的总个数,从而判断最终到达哪个节点。

  1. 对于当前询问qi = 2,我们首先在先减去不在树内的节点1,then qi = 1,于是我们需要得到树内的第一个节点(中序遍历)。
  2. 当前节点:根节点n+1。计算左子树的节点数目为: 2 ( h − 1 ) − 1 = 3 , 2^{(h-1)} - 1 = 3, 2(h1)1=3, 我们要找的是第一个,1 < 3 + 1,于是询问走向左子树2n+1。(ps:如果这里要找的是4,即等于左子树的数目加1,那就可以直接返回当前节点了)
  3. 当前节点:2*n+1。计算左子树个数为: 2 ( h − 2 ) − 1 = 1 , 2^{(h-2)} -1 = 1, 2(h2)1=1 qi = 1 < 1 + 1,所以还得走向左子树。
  4. 当前节点:4*n+1。计算左子树个数为: 2 ( h − 3 ) − 1 = 0 , 2^{(h-3)} -1 = 0, 2(h3)1=0 qi = 1 == 0 + 1,当前返回节点。
  5. 计算当前节点的大小。按照上述方法的确可以找到,但是需要很大的空间去储存每个节点的内容。但是就如图2的符号化表示,我们无需实际存树,通过当前节点的高度hh和从左往右的序数ll就可以计算出来。如下式子: n ∗ 2 h − 1 + k + l l n * 2^{h-1} + k + ll n2h1+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 113714415816217918519102032111226231224
    找规律:
  1. 原始序列是+1+1的,没啥考虑的。
  2. 我们发现有的元素+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 轮的。

注意两个点

  1. temp取模完等于0的情况需要特判。因为这时候询问 qi 肯定大于 2*temp,按上述逻辑会使用 r-1 轮的计算。

  2. 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;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值