一、简介
众所周知,程序的组成可以分为数据结构和算法两部分组成,约瑟夫环普遍上的解法使用的数据结构为数组,链表,队列,其算法为暴力模拟法,时间复杂度O(N^2),而我今天给大家分享两个O(NlogN)的方法,帮助大家解决大规模数据下的约瑟夫环问题。那我们就正式开始吧!
我们的优化思路是在数据结构方面进行优化,采用高级数据结构线段树和树状数组来实现对约瑟夫环问题的优化。
二、线段树
线段树是一种基于二叉树的数据结构,它用于处理一维区间查询问题。线段树的主要应用是在数组上进行区间操作,例如求区间最大值、区间最小值、区间和等等。线段树的构建和查询时间复杂度均为O(logn),因此它是一种高效的数据结构。
线段树解决的问题是是对于给定的目标区间,求其区间的最大值或最小值或区间和的问题。
例如下图其思路是求目标[0,7] 区间的最小值,我们的思想是不断的二分,求区间[0,3]的最小值和区间[4,7]的最小值,在不断的二分的过程中寻找更小区间的最小值。二分的终点是一个区间的左端点等于一个区间的右端点,即,该区间是一个点,其最小值就是他本身,,然后依据最小值返回给上层。如下图。
简单来说就是空间换时间,把一部分区间的最小值预先算出来然后存起来。
我们先给出线段树的定义
const int N=8;//[0,7]区间大小为8
int tree[N*4]//线段树倒数第一层有N个数,倒数第二层有N/2个数,以此类推
//可以证明线段树所需空间大于N小于4*N,我们这里开4*N
线段树的优点在于它可以在O(logn)的时间内完成区间查询操作,他的缺点是它的空间复杂度也较大。
本题中我们使用的是线段树的单点查询和单点修改的功能,我们来看代码吧。
#include<iostream>
#define ll long long
using namespace std;
const int N = 100;
int n, m;
int t[N << 2];//线段树
int ls(int p)//返回左孩子的下标
{
return p << 1;//p<<1等价于p*2
}
int rs(int p)//返回右孩子的下标
{
return p << 1 | 1;//p<<1|1等价于p*2+1
}
//递归建树
void build(int p, int l, int r)
{
if (l == r)
{
t[p] = 1;//初始化为1,表示这里是有人的
return;
}
int mid = (l + r) >> 1;
build(ls(p), l, mid);
build(rs(p), mid + 1, r);
t[p] = t[ls(p)] + t[rs(p)];//这里是求区间和
//tp=min(t[ls(p)] , t[rs(p)]);//这是求区间最小值
}
//把 x 踢出去
void change(int p, int x,int l,int r)
{
if (l == r)
{
t[p] = 0;
return;
}
int mid = (l + r) >> 1;
if (x <= mid) change(ls(p), x, l, mid);
else change(rs(p), x, mid + 1, r);
t[p] = t[ls(p)] + t[rs(p)];
}
//查询 x 的位置
int query(int p, int x,int l,int r)
{
if (l == r)
return l;
int mid = (l + r) >> 1;
//如果左边的剩余位置小于这个编号,那就在右边区域查找左边区域放不下的
if (x > t[ls(p)]) return query(rs(p), x - t[ls(p)], mid + 1, r);
else return query(ls(p), x, l, mid);
}
int main()
{
scanf_s("%d%d", &n, &m);
if (n == 0) return 0;
build(1, 1, n);
int pos = 1;
for(int i=0;i<n;i++)
{
pos = (pos + m - 2) % t[1] + 1;//t[1].dat即剩余总人数
//先给 pos-1, 避免出现mod 完变成0的情况,mod完之后在 +1
//处理位置
// if(pos==0) pos=t[1].dat;
int qwq = query(1, pos,1,n);
//查寻当前这个人的位置
cout << qwq << " ";
//输出
change(1, qwq,1,n);
//踢出队伍
}
return 0;
}
//By HJC
这样子我们就实现了约瑟夫环的O(NlogN)的解法,但是,提到线段树这种数据结构,我们就不得不提到另一种和他很类似的数据结构,树状数组。
三、树状数组
线段树是空间换时间,预处理几段区间的最值以便于解决问题,但是有没有不增大空间,还可以预处理一部分区间的最值出来的方法,答案肯定是有的,那就是树状数组。
树状数组的思想是,利用数的二进制特征来进行检索的一种树状结构,将线段树4*N的空间转换为只需要N的空间。你可能不太理解,我们来举个例子把。
在线段树里,若N=2,我们会建树如下图:
而树状数组的思想是,我知道了[1,1],我知道了[1,2],
那么区间[2,2]我完全可以通过区间[1,2]的值减去区间[1,1]的值来获得,所以我们就不必记录区间[2,2]的值,我们可以将区间[1,2]的值写道区间[2,2]的位置,类似这样,
我们先采用一个函数叫做lowbit,他返回一个数二进制的最后一个1的位置。
inline int lowbit(int x)
{
return x & -x;//返回x的二进制的最后一个1
}
树状数组就是通过lowbit()函数计算出的树状数组,它能够以log2N的复杂度存储一个数列的数据。
tree[x]中放的数据时区间[x-lowbit(x)+1,x]中每个数的和!!!
那么代码怎么实现呢?
#include<iostream>
#define ll long long
using namespace std;
const int maxn = 3e4 + 10;
int n, m, maxx;
int tree[maxn];
int lowbit(int x)
{
return x & -x;
}
void add(int pos, int x)
{
for (int i = pos; i <= maxx; i += lowbit(i))
tree[i] += x;
}
int find_kth(int k)
{
int ans = 0, now = 0;
for (int i = 15; i >= 0; i--)
{
ans += (1 << i);
if (ans > maxx || tree[ans] + now >= k)ans -= (1 << i);
else now += tree[ans];
}
return ans + 1;
}
int main()
{
scanf("%d %d", &n, &m);
maxx = n; //先记录一下n的值。
for (int i = 1; i <= n; i++)tree[i] = lowbit(i);
//这里完全等价于add(i,1),因为一开始都是1,
//所以bit[i]=i-(i-lowbit(i)+1)+1=lowbit(i)
int now = 1;//从1开始
while (n)
{
now = (now - 1 + m - 1) % n + 1;
//这里是小细节,本来的式子应该是(now+m-1)%n的,
//但是考虑如果只剩下2个元素,而我们当前要找的就是第二个元素呢?
//直接模就是0了,所以用一个+1 -1 的小操作更改取模运算的值域,
//这样就可以取到n的值了,而对别的无影响
int ans = find_kth(now);//找kth
add(ans, -1);//把这个人删除
printf("%d ", ans);
n--;
}
return 0;
}
这样子我们也可以实现对约瑟夫环的问题的求解。
除此之外,笔者在给出几个递推的约瑟夫环的问题的求解,拒绝模拟,从我做起,从小事做起。
#include<iostream>
#define ll long long
using namespace std;
int ysfh(int n, int m, int i)
{
if (i == 1)
{
return (n + m-1) % n;
}
else {
return (ysfh(n - 1, m, i - 1) + m) % n;
}
}
int main()
{
int n, m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
{
cout << ysfh(n, m, i) + 1 << " ";
}
return 0;
}
完结撒花