约瑟夫问题结合详解

前端时间之前(大概有两周了吧)我们进行了第一次ACM赛制的模拟比赛(虽然最后总分爆零了但还是学到了很多东西滴)

T1的题目为约瑟夫问题

我当时一看这个问题就感到十分狂喜,这T1果然是最简单的肯定能切了呀!

(然而我还是没有体会到社会的阴暗)

题面要求是n个人数到m枪毙,然后要输出最后的幸存者编号

数据范围:1≤n≤1e9,1≤m≤1e5

当时我一看这个数据范围就直接懵掉,如果是这个数据范围的话那么时间复杂度必须要控制在O(mlogn)以内

事实证明最优算法的时间复杂度确实是O(mlogn)

ROS是通过oiwiki上的词条学习的约瑟夫问题:戳这里

ROS只是讲一讲思想并且把代码贴在这里

关于传统的约瑟夫问题的模拟解法由于过于简单故不在此讨论,请保证在看这篇博客之前你已经能够熟练掌握运用模拟思想解决约瑟夫问题。

1.O(n²)朴素算法

没什么好说的,就是模拟。可以在底层循环中运用膜的相关知识进行相关优化

2.O(nlogn)算法

详细过程可以见我在上面放的oiwiki链接

(此处先简单表述一下之后更改)我们用线段树维护一段区间内的还生存的人的数目。然后因为我们要m人枪毙一个,所以我们便要从此结点往后找和为m的节点,如果仍然小于就回到开头,仍然小于就%一下之后继续寻找(也可以先%貌似更合理一些)

(因为觉得这种方法代码十分得长并且时间复杂度还很高所以就没有写)

3.O(n)算法

大概思路见oiwiki

需要注意的是由于我们在之前已经枪毙了的人是不能计数的所以我们要将这些人跳过之后重新计数

所以就要运用一些数学知识了:

我们将有i个人参与游戏从1开始编号的胜者编号记为f[i]

假设f[i]已知,我们现在要求f[i+1]

如何求解?

我们在此O(n)递推算法下是一次操作只计算一个人,即我们要每次只能处决一个人。

所以我们有在f[i+1]的状态下我们一定有在这一层的计算下编号为m%(i+1)的人一定是要被淘汰的

所以我们知道在f[i]层编号为f[i]得点是最终的幸存者,于是在f[i+1]层中我们很容易得到胜者的编号为(m%(i+1)+f[i])%(i+1)

正确性显然,省略证明

 

 

值得注意的是这种算法虽然时间复杂度优秀但是只能计算最后一个人却不能依次输出被淘汰的人的编号。如果你要输出依次出圈的人的编号,请用1和2算法

 

并且编号可以从0开始或者从1开始,从0开始的话在%的过程中会简便很多但是ROS因为习惯问题一般采用从1编号(二者均可)

并且需要注意的是注意的是,由于单纯的O(n)线性算法的时间复杂度仍然很高所以需要进行相关的优化(之后再讲),这个代码就是进行了相关优化的代码。如果不进行相关优化而只写朴素的O(n)算法的话则仍然会爆掉大数据(只有20分)

(由于我一开始学会的是更加优秀的O(mlogn)算法所以关于O(n)算法的相关优化并没有仔细研究,想要了解的话可以上网自行查阅

4.O(mlogn)算法

一开始我先看懂的是这个算法,整体思路还是很容易理解并且很清晰的。

Johnsef(x,t)表示现在场上剩余x个人,我们需要数到t时处决一个人

那么我们在函数内部可以分情况讨论

当x>t的时候我们得知所有小于x的t的倍数都应该被处决(从1编号)

但是由于数据范围的限制我们不能用一个vis数组保存而通过运算使得计算的时候跳过去。

如何实现?

我们首先看一下Johnsef函数的代码并讨论x>=t的情况
假设我们已经将所有小于x的t的倍数全部筛去
那么我们知道我们应该筛去x/t个数字
那么他的子问题的解就是Johnsef(x-x/t,t)
那么我们先用一个int类型变量res储存一下Johnsef(x-x/t,t)
之后我们得知在这个母问题中(从1编号)所有t的倍数的点全部被筛掉了
观察代码我们可以明白
首先我们需要判断这个数字是不是末尾的x%t数中的一位
实现方法就是先将这个子问题的解减去x%t,如果这个值大于0那么说明幸存者的编号前x/t*t个数中的一个,并且编号就是这个数
否则说明幸存者的编号在在最后的x%t个数字中
那么我们应该加上n就得到了在这个母问题中的幸存者编号
为什么加上n之后就是幸存者编号了?
感性理解:因为我们在母问题中一共有n个人,我们将队尾的人减去了。然而此时运算之后的差值是小于零的,所以有我们需要将结果加上x才是从x/t*t之后数字的正确编号
理性证明:假设子问题的解减去x%t为tmp,那么当tmp小于零的时候我们需要将x%t加回去并且再加上x/t*t;而因为x%t==x-x/t*t,所以有tmp-x%t+x==tmp-(x-x/t*t)+x==tmp+x/t*t,得到的这个值也就是我们要求的母问题的编号了!
那么我们如何跳过已经被处决的人呢?
我们知道每到达一个编号为m的人我们就要进行一次处决
所以我们可以将m个人分成1组
而当我们进行子任务的求解之后假设在母任务的编号是在前x/t*t个数字之中,那么我们有没t-1个数字可以分成一组,我们只需要看其为哪一组就可以了!如果是第二组的话就需要加上1,是第i组的话我们就需要加上i-1(这是用于理解的方法)
代码中只需要加上(res-1)/(t-1)就可以了(因为从1开始编号所以我们需要将res减去1)

然后我们来讨论一下x<t的情况。
因为一开始ROS对这种情况并没有很理解所以自己写的时候就写了一系列的特判。最后发现完全没有必要
我们只需要将我们得到的这个结果每一个值都%x就可以了
而因为对于乘法运算来说,容易得到我们对每一个乘数取模之后再对乘积的结果取模的结果等于我们将这两个没有取模的数字相乘之后再取模,所以我们只需要在最后取模就好了
同时不可能需要跳过已经处决过的人,因为子问题的人数一定小于x-1,而x小于t所以子问题的序号一定小于t不可能走完一周到达下一个被处决的点
因为从1开始编号所以最后得到的res需要减1之后再进行加上跳过的人数

 

代码:

#include<iostream>
#include<cstdio>
using namespace std;
int t;
int n,m;
int Johnsef(int x,int t){
    if(x==1) return 1;
    if(t==1) return x;
    if(t>x){
        int tmp=(Johnsef(x-1,t)+t)%x;
        return tmp==0?x:tmp;
    }
    int res=Johnsef(x-x/t,t);
    res-=x%t;
    if(res<=0) res+=x;
    else res+=(res-1)/(t-1);
    return res;
}
int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d%d",&n,&m);
        printf("%d\n",Johnsef(n,m));
    }
    return 0;
}

 

 

 5.O(mlogn)算法的相关优化

由于递归算法会浪费大量的空间,我们我们可以用类似递推的方法来求得该类问题的解

总体思路与方法4很类似

(此方法ROS从0开始编号)

同样用res保存一下我们在该曾循环中找到的幸存者编号

因为我们可以得到f[i]=(f[i-1]+m)%i

所以类似方法4我们可以一下子跳过i/m个被处决的人

依然是枚举当前的人数i

令i==res+m*x,有x==(i-res)/m

所以有我们可以一下子跳过x个人,但是我们极有可以跳过x个人之后发现人数大于n了(即跳过了我们要求的答案)

所以我们需要进行一步判断防止在内部跳过最多人数

需要注意的是,这种算法是对方法4的递归优化,当n远远大于m的时候十分优秀。但如过n小于m的话,其时间复杂度与方法三的O(n)没有什么区别了(其实其他方法也是)

因为在这道题中ROS从0开始编号所以我们需要对最后结果加上1才能得到题目要求的编号

AC代码:

#include<iostream>
#include<cstdio>
using namespace std;
int n,m;
int t;
int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d%d",&n,&m);
        int res=0;
        int x=0;
        for(int i=2;i<=n;i++){
            if(res+m<i){
                x=(i-res)/m;
                if(i+x<n){
                    i+=x;
                    res+=x*m;
                }
                else{
                    res+=(n-i)*m;
                    i=n;
                }
            }
            res=(res+m)%i;
        }
        printf("%d\n",res+1);
    }
    return 0;
}

 


 

THE END.

posted @ 2020-09-12 17:06  Robertspot  阅读(430)  评论(1编辑  收藏  举报