O(NlogN)的约瑟夫环

一、简介

        众所周知,程序的组成可以分为数据结构和算法两部分组成,约瑟夫环普遍上的解法使用的数据结构为数组,链表,队列,其算法为暴力模拟法,时间复杂度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;
}

完结撒花

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星星也倦了/

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值