约瑟夫环问题

前言

  约瑟夫环问题是一个很很经典的问题,问题的描述大概如下:0,1,,n1n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

  可以用队列来模拟或者是递推来解决这个问题。

 

玩游戏

n 个小朋友围成一圈,玩数数游戏。

小朋友们按顺时针顺序,依次编号为 1n

初始时,1 号小朋友被指定为领头人。

游戏一共会行进 k 轮。

在第 i 轮中,领头人会从他的顺时针方向的下一个人开始,按顺时针顺序数 ai 个人。

其中,最后一个被领头人数到的人被淘汰出局,这也意味着该轮游戏结束。

出局者的顺时针方向的下一个人被指定为新领头人,引领新一轮游戏。

例如,假设当游戏即将开始第 i 轮时,还剩下 5 个小朋友,编号按顺时针顺序依次为 8,10,13,14,16,并且当前领头人为 13 号小朋友,ai=12,则第 i 轮游戏结束后,最后一个被数到的小朋友为 16 号小朋友,他将被淘汰出局,并且处于其下一位的第 8 号小朋友将被指定为新领头人。

现在,请你求出每一轮次被淘汰的小朋友的编号。

输入格式

第一行包含两个整数 n,k

第二行包含 k 个整数 a1,a2,,ak

输出格式

一行,k 个整数,其中第 i 个整数表示在第 i 轮中被淘汰的小朋友的编号。

数据范围

前三个测试点满足 2n10
所有测试点满足 2n1001kn11ai109

输入样例1:

7 5
10 4 11 4 1

输出样例1:

4 2 5 6 1

输入样例2:

3 2
2 5

输出样例2:

3 2

 

解题思路

  数据范围比较小,所有可以直接开个队列来模拟。一开始先将1,2,,n按顺序放到队列中,每次都从队头开始数,每数到一个数就把这个数放到队尾,因此队列就起到一个循环枚举的作用。当数完k个数后,此时队头的数就是要删除的数,把这个数从队头删除。

  ai的取值范围很大,但由于是环形的枚举,因此ai mod n就可以去掉重复的循环枚举,为最终枚举的次数。因此时间复杂度为O(n2)

  AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <queue>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 queue<int> q;
 7 
 8 int main() {
 9     int n, m;
10     scanf("%d %d", &n, &m);
11     for (int i = 1; i <= n; i++) {
12         q.push(i);
13     }
14     
15     while (m--) {
16         int val;
17         scanf("%d", &val);
18         val %= q.size();
19         
20         for (int i = 0; i < val; i++) {
21             q.push(q.front());
22             q.pop();
23         }
24         
25         printf("%d ", q.front());
26         q.pop();
27     }
28     
29     return 0;
30 }
复制代码

  如果n的数据范围很大,就需要用递推的做法了。

 

圆圈中最后剩下的数字

0,1,,n1n 个数字 (n>0) 排成一个圆圈,从数字 0 开始每次从这个圆圈里删除第 m 个数字。

求出这个圆圈里剩下的最后一个数字。

数据范围

1n,m4000

样例

输入:n=5 , m=3

输出:3

 

解题思路

  这题的数据范围用模拟也可以过,时间复杂度为O(n2)。但下面讲递推的做法,时间复杂度为O(n)

  我们假设在这n个人中删除n1个人后最终留下的人为x,这个人在长度为n的序列中的下标就是x。一开始在长度为n的序列中删除第m个人,如下图:

  接下来进行第二次的删除操作,从被删除的下一个人开始,也就是下标为m开始继续数m个人然后删除。此时序列的长度变成了n1,我们为这个环剩余的数字重新编号,使得开始数的那个数的下标为0,也就是m这个数的下标为0,顺时针往后的每个数下标都加1,直到m2这个数时,下标为n2,如下图(新的下标的颜色为红色):

  可以发现新的下标编号加上m再模n就是将原来的下标编号。原来在长度为n的序列中,x所在的下标重新编号后变成了y,即x=(y+m)%nx位置和y位置上的数是同一个(就是最终留下来的那个数),只是下标的编号不一样。

  所以我们发现,如果求出了在长度为n1的序列中从第0个人开始,每次数m个人将其删掉,最终留下的那个人的下标编号(在长度为n1的序列中的下标),那么就可以反推回这个最终留下的人在长度为n的序列中的下标(加mn)。

  我们用f(n,m)来表示在n个人中,每次从被删掉的下一个人开始数m个人将其删除(一开始从0开始数),最终留下的那个人的编号

  第一次删掉一个人后,序列长度变成了n1,并且重新进行编号,因此在长度为n1的序列中,最后留下的人的编号就是f(n1,m)

  当我们得到新编号后,会发现与旧编号有个映射关系,也就是新的编号加上mn后,就得到旧编号。

  现在留下的人在n1的序列中的新编号是f(n1,m),因此转换为在n的序列中的旧编号就是f(n,m)=(f(n1,m)+m)%n

  因此递推公式为(m可以省略):f(i,m)=(f(i1,m)+m)%i

  所以我们找到了nn1的递推关系,其中边界是只有一个人,此时编号为0,即f(1)=0

  我们可以用递归或循环递推的写法来写。

  当数据规模不大的时候可以用递归,AC代码如下:

1 class Solution {
2 public:
3     int lastRemaining(int n, int m){
4         if (n == 1) return 0;
5         return (lastRemaining(n - 1, m) + m) % n;
6     }
7 };

  循环递推写法的AC代码如下:

复制代码
 1 class Solution {
 2 public:
 3     int lastRemaining(int n, int m){
 4         int ret = 0;    // f(1) = 0
 5         for (int i = 2; i <= n; i++) {  // 从2开始,一直求到n
 6             ret = (ret + m) % i;    // f(i) = (f(i-1) + m) % i
 7         }
 8         return ret;
 9     }
10 };
复制代码

  下面是一道变形题目。

 

招聘

某公司招聘,有 n 个人入围,HR在黑板上依次写下 m 个正整数 A1,A2,,Am,然后这 n 个人围成一个圈,并按照顺时针顺序为他们编号 0,1,2,,n1

录取规则是:

第一轮从 0 号的人开始,取用黑板上的第 1 个数字,也就是 A1

黑板上的数字按次序循环使用,即如果某轮用了第 k 个,如果 k<m,则下一轮需要用第 k+1 个;如果 k=m,则下一轮用第 1 个。

每一轮按照黑板上的次序取用到一个数字 Ax,淘汰掉从当前轮到的人开始按照顺时针顺序数到的第 Ax 个人。

下一轮开始时轮到的人即为被淘汰掉的人的顺时针顺序下一个人,被淘汰的人直接回家,所以不会被后续轮次计数时数到。

经过 n1 轮后,剩下的最后 1 人被录取,所以最后被录取的人的编号与 (n,m,A1,A2,,Am) 相关。

输入格式

输入包含多组测试数据。

第一行包含整数 T,表示共有 T 组测试数据。

接下来 T 行,每行包含若干个整数,依次存放 n,m,A1,A2,,Am,表示一组数据。

输出格式

输出共 T 行,每行对应相应的那组数据确定的录取之人的编号。

数据范围

0<T<10,
0<m,Ax<103,
0<n<107

输入样例:

1
4 2 3 1

输出样例:

1

样例解释

样例里只有 1 组测试数据,说的是有 4 人入围(编号 03)。

黑板上依次写下 2 个数字:31,那么:

第一轮:当前轮到 0 号,数到数字 3,顺时针数第 3 个人是 2 号,所以淘汰 2 号,下一轮从 3 号开始,目前剩余:013

第二轮:当前数到 3 号,取到数字 1,顺时针数第 1 个人是 3 号,所以淘汰 3 号,下一轮从 0 号开始,目前剩余:01

第三轮:当前轮到 0 号,循环取到数字 3,顺时针数第 3 个人是 0 号,所以淘汰 0 号,最后只剩下 1 号,所以录取 1 号,输出 1

 

解题思路

   可以发现在这一题中,每次数的人的个数不再是一个固定的数m了。但递推公式还是成立的,只需要把m改为相应的从i变到i1要数的那个数a[(ni)%m]就可以了。

  a[(ni)%m]表示,如果要从长度为i的序列中删除一个数,那么应该数第ni个数,即a[ni],因为数组a的数是循环取的,因此要模上数组a的长度m

  递推公式就变成了f(i)=(f(i1)+a[(ni)%m])%i

  由于n最大取到107,递归来做肯定会暴栈的,因此必须要用循环。

  AC代码如下:

复制代码
 1 #include <cstdio>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1010;
 6 
 7 int a[N];
 8 
 9 int main() {
10     int tot;
11     scanf("%d", &tot);
12     while (tot--) {
13         int n, m;
14         scanf("%d %d", &n, &m);
15         for (int i = 0; i < m; i++) {
16             scanf("%d", a + i);
17         }
18         
19         int ret = 0;    // f(1) = 0
20         for (int i = 2; i <= n; i++) {
21             ret = (ret + a[(n - i) % m]) % i;   // f(i) = (f(i-1) + a[(n-1) % m]) % i
22         }
23         printf("%d\n", ret);
24     }
25     
26     return 0;
27 }
复制代码

 

 参考资料

  AcWing 4400. 玩游戏(AcWing杯 - 周赛):https://www.acwing.com/video/3826/

  AcWing 82. 圆圈中最后剩下的数字:https://www.acwing.com/video/205/

  AcWing 1455. 招聘:https://www.acwing.com/solution/content/18760/

posted @   onlyblues  阅读(453)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示