约瑟夫环问题(模板题,递推,树状数组,双端队列)

2024-08-23 22:04

本文主要是介绍约瑟夫环问题(模板题,递推,树状数组,双端队列),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 最后活的人(递推)
    • [LCR 187. 破冰游戏 ](https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/)
    • [P8671 约瑟夫环 - 洛谷 ](https://www.luogu.com.cn/problem/P8671)
  • 出局顺序(递推,树状数组)
    • 递推代码(编号从0开始)
    • L-koala的程序(双端队列)
    • 树状数组
  • 编号从1开始
    • [P8671 约瑟夫环 - 洛谷(最后活的人) ](https://www.luogu.com.cn/problem/P8671)
    • 出局顺序

约瑟夫问题,接下来分两类问题讲解:求 最后活下的人,出局顺序

其中皆有数学递推公式做法。 对于出局顺序还加了 双端队列,树状数组做法,时间复杂度更低。
最后还有编号从1开始,与从0开始的不同之处。

最后活的人(递推)

序列长度为10,target为3时

1 2 3 4 5 6 7 8 9 10

每死一个人,都会影响后面的报数,也就是确定了下一个要死的

第一个死的是3

第二个要死的是6变成的3

第三个死的,是9变成6又变成3

这就给我们提供了一个思路,可以通过记录每死一个人的状态,来归纳总结答案。

即,当活着的人数为 n u m num num时,要死的是谁!

12345678910num
893123456710
56378312349
23345367318

当长度为num-1,死的下标为f,由上表中3,6,9,可推得,该数字在长度为num时的下标F为

F=(f+target)%num

那最后留下的,只有它一个,下标自然就是1喽。不断向上递推不就得出,长度为num时,最后活着的人的下标了么。

请看下面例题:

LCR 187. 破冰游戏

注意!!!,这题编号是从0开始的,也就是最后留下的是0.

所以 是$return $ 0。

下面是从递归和迭代两个方面入手的代码:

代码

递归

class Solution {int f(int num,int target){if(num==1) return 0;int x=f(num-1,target);return (target+x)%num;}
public:int iceBreakingGame(int num, int target) {return f(num,target);}
};

迭代

class Solution {
public:int iceBreakingGame(int num, int target) {int f=0;for(int i=2;i!=num+1;i++)//长度为2到num{f=(target+f)%i;}return f;}
};

看完这个,可以那下面题练练手。这是编号从1开始的,能A出来,说明理解上述的推导。

(如果直接用上面代码输出+1,就没意义了)

P8671 约瑟夫环 - 洛谷

正确代码及解析,放最后了。

出局顺序(递推,树状数组)

最后留下的编号为1,我们从上述表格也可得知,每次出局的编号为(target%num),以10 3为例,出局时的编号为3

以9为例,它是第三个出局。

出局时编号返回3,利用上述递推公式,得出

第二局编号为 (3+3)%10=6

第一局编号为 (6+3)%10=9

这样我们就能得出第三局出局的是9

所以我们在上述代码中加一个循环,还有一个变量,控制每次递推次数,就可以得出每次出局的人了

同样的,我们先以编号从0开始为例,给出代码

递推代码(编号从0开始)

#include<bits/stdc++.h>
using namespace std;
//用递归实现约瑟夫环问题
int f(int N,int M,int i)
{if(i==1)//到达该数字出局时{return (M-1+N)%N;//因为从0开始,那么下标应该这样表示,且保证结果在0,N-1范围}return (f(N-1,M,i-1)+M)%N;
}int main()
{int num,target;cin>>num>>target;   for(int i=1;i<=num;i++)//第i个出局的递推i次cout<<f(num,target,i)<<" ";return 0;
} 

时间复杂度为O( n 2 n^2 n2.

L-koala的程序(双端队列)

数据很大,显然不能用上述方法

太傻啦,竟然没看出来

看有大佬用的双端队列(勉勉强强能过,不是最优解)

代码

#include<bits/stdc++.h>
using namespace std;
int main()
{int n,k,pos=0;cin>>n>>k;deque<int> q;for(int i=1;i<=n;i++)q.push_back(i);while(q.size()>1){pos+=k-1;//因为队列从0开始while(pos+1>q.size()) pos-=q.size();cout<<q[pos]<<' ';q.erase(q.begin()+pos);}
}

树状数组

树状数组是 O( N l o n g N NlongN NlongN)时间复杂度。

用树状数组来存放初始位置每个人的状态。也就是N个人,每个人初始为1。

当一个人死了,标记该位置-1。

通过加k对当期序列长度取模,得到下一个该死的人在新序列中的编号p,结合二分 找到在 树状数组前缀和为p的位置,这就是原序列中该死的人,标记为-1.

先看代码,后面还有解释…

代码

#include <bits/stdc++.h>
using namespace std;
const int N=3e5+7;
#define lowbit(x) ((x)&(-x))
int tree[N],n,m;
void update(int i,int x){while(i<=n) {tree[i]+=x;i+=lowbit(i);}
}
int query(int x)
{int ans=0;while(x>0){ans+=tree[x];x-=lowbit(x);}return ans;
}
int main() {cin>>n>>m;int p=1;//初始化保证后面操作,一会儿会说for(int i=1;i<=n;++i) update(i,1);  //或这tree[i]=lower(i);  int t=n;while(t>1){p=(p+m-1-1)%t+1;//每次都有一个数-1被删,只需找到p-=1就好了int l=1,r=n;while(l<r){int mid=l+r>>1;if(query(mid)>=p) r=mid;else l=mid+1;}cout<<l<<' ';update(l,-1);t--;}return 0;
}

易错疑难点

为什么t=n

update操作也是需要n的,直接用 n − − n-- n,会影响结果正确性
要对不断递减的t取模

为什么p初始化1,循环里p=(p+m-1-1)%t+1这样更新值

  1. (n-1)%m+1 这种-1+1取模,保证结果在【1,m】区间,符合编号

  2. 我们是通过前缀和来找到要删除的数,可是在删除上一个数时,会使前缀和-1。所以我们原本要找sum=p,变成了sum=p-1,该位置是要找的。 可对于第一次进循环,没有数删除时,不需-1,因而初始化为1.

  3. 也有代码初始就将 m − − m-- m ,这样就不需要 p − − p-- p

编号从1开始

n%m

所得结果区间[0,m-1],恰巧符合下标从0开始

(n-1)%m+1

区间**[1,m]**,这样取模解决的这个问题。

所以上述代码中,将取模处和返回值修改一下就得出答案。

P8671 约瑟夫环 - 洛谷(最后活的人)

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int f(int num,int target)
{if(num==1) return 1;//只剩一个数编号为1int x=f(num-1,target);return (target+x-1)%num+1;//上面说的,-1 +1 取模
}
signed main()
{IOSint n,k;cin>>n>>k;cout<<f(n,k);
}

出局顺序

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);int f(int num,int target,int i){if(i==1) return (target-1)%num+1;int x=f(num-1,target,i-1);return (target+x-1)%num+1;}
signed main()
{IOSint num,target;cin>>num>>target;for(int i=1;i<=num;i++)cout<<f(num,target,i)<<" ";
}

这篇关于约瑟夫环问题(模板题,递推,树状数组,双端队列)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1100620

相关文章

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

hdu1180(广搜+优先队列)

此题要求最少到达目标点T的最短时间,所以我选择了广度优先搜索,并且要用到优先队列。 另外此题注意点较多,比如说可以在某个点停留,我wa了好多两次,就是因为忽略了这一点,然后参考了大神的思想,然后经过反复修改才AC的 这是我的代码 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

poj3468(线段树成段更新模板题)

题意:包括两个操作:1、将[a.b]上的数字加上v;2、查询区间[a,b]上的和 下面的介绍是下解题思路: 首先介绍  lazy-tag思想:用一个变量记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。 比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果刚好执行到一个子节点,

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

uva 568 Just the Facts(n!打表递推)

题意是求n!的末尾第一个不为0的数字。 不用大数,特别的处理。 代码: #include <stdio.h>const int maxn = 10000 + 1;int f[maxn];int main(){#ifdef LOCALfreopen("in.txt", "r", stdin);#endif // LOCALf[0] = 1;for (int i = 1; i <=

hdu 1166 敌兵布阵(树状数组 or 线段树)

题意是求一个线段的和,在线段上可以进行加减的修改。 树状数组的模板题。 代码: #include <stdio.h>#include <string.h>const int maxn = 50000 + 1;int c[maxn];int n;int lowbit(int x){return x & -x;}void add(int x, int num){while