Josephus Problem — 线段树查找

只是记录一下自己的学习过程

Josephus Problem — 线段树查找

题目描述

题目描述
Do you know the famous Josephus Problem? There are n people standing in a circle waiting to be executed. The counting out begins at the first people in the circle and proceeds around the circle in the counterclockwise direction. In each step, a certain number of people are skipped and the next person is executed. The elimination proceeds around the circle (which is becoming smaller and smaller as the executed people are removed), until only the last person remains, who is given freedom.

In traditional Josephus Problem, the number of people skipped in each round is fixed, so it’s easy to find the people executed in the i-th round. However, in this problem, the number of people skipped in each round is generated by a pseudorandom number generator:

x[i+1] = (x[i] * A + B) % M.

Can you still find the people executed in the i-th round?

输入
There are multiple test cases.

The first line of each test cases contains six integers 2 ≤ n ≤ 100000, 0 ≤ m ≤ 100000, 0 ≤ x[1], A, B < M ≤ 100000. The second line contains m integers 1 ≤ q[i] < n.

输出
For each test case, output a line containing m integers, the people executed in the q[i]-th round.

样例输入
2 1 0 1 2 3
1
41 5 1 1 0 2
1 2 3 4 40

样例输出
1
2 4 6 8 35

我的简要分析

在写这个题目之前已经写过另外三个Josephus的问题,出题目的指导老师说三个题目分别用过程模拟、找F(2n)跟F(n)的关系、大问题化小问题三个方法,但是我这三个题目都是把大问题化校问题来写,也就是递归公式求解,不过因为时间超限对算法进行了一些优化,跳过了一些没有必要的过程。我本打算用一样的方法来写这个题目,将公式改一下并且每轮更新一下跳过的人数,但是想了一下又否决了。

以下是我个人想法:如果单纯用公式递归来写,先不考虑是否超时,因为需要求的是每轮出圈的人的编号,每轮之后会有人出圈,单纯的用公式递归可能出圈的人会有重复,甚至后面的结果也都会错误。

用公式递归是可以的,不过需要在每轮之后将出圈的人去掉,这样他就不会影响后面的过程。最后在CSDN看到了其他博主的博客提到了线段树,于是我简单的学习了一下线段树,再用线段树来做这个题目思路就很顺畅了。先贴上我的代码,然后我再进行一点补充说明,因为借鉴的其他博主的思路,所以可能代码会有雷同,仅是我个人的学习。

关于线段树推荐这篇博客 链接.

详细代码


#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
using namespace std;
 
 
//用于存储线段树
long long T[1000007];
 
 
//递归建立线段树
void  tree(long long l, long long r, long long n)
{
    long long m;
 
    T[n] = r - l + 1;
    m = (l + r) / 2;
 
    //当线段长度为0就结束
    if (l == r)
        return;
 
    tree(l, m, n * 2);
    tree(m + 1, r, n * 2 + 1);
}
 
 
 
//递归更新线段树中的元素
long long newT(long long l, long long r, long long n, long long change)
{
    long long m;
    T[n]--;
 
    当线段长度为0就结束
    if (l == r)
        return l;
 
    //若需要更新的值小于等于左子树的值则进入左子树,反之进入右子树
    m = (l + r) / 2;
    if (change <= T[2 * n])
        return newT(l, m, n * 2, change);
 
    else
        return newT(m + 1, r, n * 2 + 1, change - T[2 * n]);
 
}
 
 
 
int main()
{
    long long n, m, x, a, b, M, s;
    long long q[100007] = { 0 };         //储存需要求解的轮数
    long long num[100007] = { 0 };         //储存退圈的人的编号
    while (cin >> n)
    {
        cin >> m >> x >> a >> b >> M;
 
        s = 1;
        
        //建立线段树
        tree(1, n, 1);
 
        //不断更新线段树
        for (long long i = 1; i <= n; i++)
        {
            s = (s + x) % T[1];
 
            //若s等于0,s返回(s+x),即T[1];
            if (s == 0)
            {
                s = T[1];
            }
 
            num[i] = newT(1, n, 1, s);
            x = ((x * a)%M + b) % M;
        }
 
 
        //输入轮数
        for (long long i = 1; i <= m; i++)
        {
            cin >> q[i];
        }
 
 
        //输出相应轮数退圈的人的编号
        for (long long i = 1; i <= m; i++)
        {
            cout << num[q[i]] << " ";
        }
 
        cout << endl;
 
    }
}

补充说明

我一般看到的线段树是将左端点跟右端点分别储存,但是这里是把每一小段的长度进行了储存,而端点的值则储存在每次所传输的参数中。
由于每一轮都退圈一个人,所以需要把退圈的人所在的那一段的长度都减去1,然后在寻找的时候,基于每一轮都会有一个人退圈,本来所需要出圈的人是要在上一轮基础上加上跳过的人再加1,这里只需要加上跳过的人。

跟同学一起谈论的时候,他问我更新线段树的时候递归到右边的change可不可以不减去T[n2],由于掌握的不熟练,于是后面我们一起探讨了一下,其实这个减去T[n2],是基于左边动态减少的人。

因为这个问题实质是找现在剩余圈子里面第change个人的编号,所以左边每减去一个人,在右边查找的时候需要在原基础上往前加一个位置,如果想要递归到右边的change值不改变,可以在建树的时候储存右端点的值,并且每次出圈一个人的时候,除了这个人所在线段的值需要减1外,这个人所在线段对应右边的线段的值也需要减1,这样的改变复杂,意义不大。

我的理解比较粗浅,甚至是对自己想法的强行解释,若是发现我的错误,希望能在评论区指出,谢谢。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值