约瑟夫环问题

约瑟夫问题

一道例题

在这里插入图片描述

题目解析:

如果第一次接触这道题,难免有些陌生。但如果只是在脑子里想象的话,那大概率会是一团浆糊,所以建议在纸上画一画,模拟一下,其实这道题也是能锻炼一定的代码能力的。(这里分享一个做题的小技巧:不要死磕一个题太久,该看答案咱还是得看,有时候退一步是为了更好的进步)。
好了,回归正题。题目的大概意思就是:有n个人,围成一个圈,从第一个人开始报数,报到数字m的人出列,同时从下一个人开始继续报数,就这样循环往复,直到所有人都出列。如果听到这,有些人肯定会感觉很熟悉,毕竟可能玩过相似的游戏;但是又有些玄幻,因为,如果仔细一想这个报数,出列的过程就乱了,想象不出来整个过程,此时就不要吝啬你的纸和笔了,直接分析,更有利于理解。

算法原理(含代码解析):

使用模拟

既然题目都说了要挨个报数,出列。那我们就认为创建一个链表,来模拟这个过程。

1,创建链表节点类

//使用单链表模拟:
class Node{
	int val;
	Node next;
	public Node(int val) {
		this.val = val;
	}
}

2,一些基本变量输入处理

public class Demo111314 {
	public static void main(String[] args) {
		//基本变量的输入
		Scanner sc = new Scanner(System.in);
		int n =sc.nextInt();
		int m = sc.nextInt();
}

创建一个环形链表:

		Node head = new Node(0);//这里head的创建就是为了,方便创建环形链表
		Node cur = head;
		for(int i =0;i<n;i++) {
			Node node = new Node(i+1);
			cur.next = node;
			cur = node;
		}
		//将链表头尾相连:(注意细节,head不参与环形链表)
		cur.next = head.next;

创建好后的环形链表参考如下:

在这里插入图片描述

进行模拟:

还是假设,n = 10,m = 3;
从起始位置开始报数,即从1位置开始(这里要注意,其实这个1也要算在报数里的,不能想当然的:直接1+3 = 4,4出列),编号为3的人出队;然后再从4开始报数,编号为6的人出队,依此类推……
简图如下:(歩奏很多,我只画出了前几步)
在这里插入图片描述
由上述简图,下一步就是将这个过程模拟出来;

  • 首先就是定义一个指向节点的指针,方便操作。但是,仔细思考一下:会发现,一个指针根本不够用,因为:如果我们只是3个3个的移动指针找到那个删除的元素还好。但是我们还要进行删除节点操作啊,而要想删掉指针指向的这个节点,就必须找到上一个节点啊,因为 p.next = q.next (p代表上一个节点,q代表该节点)这个操作才能正确删掉某个节点啊。因此,经过分析,我们需要两个指针,一个指向本节点,一个指向上一个节点,这样才能完成删除节点操作。
  • 但是,单单思路想到这还远远不够,因为如果要定义两个指针,一个q指向该节点,一个p 指向 上一个节点,在初始化的时候我们会遇到一个问题,q直接指向第一个节点就行了,但是p怎么初始化?难不成先找到最后一个节点,然后p指向它,其实啊,都不用,还记得我们是如何创建环形队列的吗?创建的最后那个cur 一定指向的是最后一个节点,所以我们直接用cur就行了,而不用再找最后一个节点在哪里了。
  • 其实还有一个思路,就是直接让 p 指向 head,虽然head没有意义,但是这个初始化的p的方式并不会干扰到结果,为啥呢?举个例子:我们知道p 只是在删除节点的时候用的,想象一下,此时链表只有一个节点,此时的m>=1,不可能是0,所以元素一定会出队,但是此时只有一个元素啊,那你还删他干啥,直接输出不是更简便吗? 如果链表里的元素个数大于1,你可以模拟着试试,这个对结果不会有影响。(但是有一点需要注意,下边有涉及到)
//下一步:模拟:
		Node p = head;//这里的head 改成cur也对。
		Node q = p.next;
		while(p != q) {
			for(int i =1;i<m;i++) {
				p = p.next;
				q = q.next;
			}
			System.out.print(q.val+" ");
			//找到以后,再将这个节点删掉:q指向下一个节点
			p.next = q.next;
			q = p.next;
		}
		System.out.print(q.val);

根据上面的代码,先说明一个问题:这里的while循环里的条件可能是很多人想不到的,一开始我写的条件是 q != null 但是运行以后发现死循环了。于是我又重新分析了一遍歩奏。发现,无论什么时候,最终链表里删的就只剩下一个元素了,而这个元素总是自己指向自己,就是因为 p.next = q.next 这句代码带来的作用,所以,只要是当 p 和 q 指针指向了同一个节点,那就说明,循环结束了,p和q同时指向的节点是最后一个应该删除的节点。
所以循环条件应该是 p != q。

  • 特殊情况 :当m =1,n =1时:
    • 假如我们用的是cur,由于初始化就将整个链表首尾相连了,此时就不用做任何操作,图解如下:
      在这里插入图片描述

    • 假如我们用的是head,会发生死循环,简图如下:
      在这里插入图片描述
      因此我们要在开头直接加上一个判断,来解决使用head的时候n = 1的情况

//解决使用head的时候 n = 1的情况
if(n == 1) {
			System.out.print(1);
			return ;
		}

使用队列

如果是一个一个的删除,那不难想到队列,如果我们将所有元素都放进一个队列里,然后挨个出队列判断,如果此时的元素是我们要删除的元素,那就将他出队列,如果不是,那就再将这个元素入队尾。利用队列的先入者先出,后入者后出的特性,不难解答问题。
在这里插入图片描述

public class Demo111415 {
	 public static void main(String[] args) {
		Queue<Integer> queue = new LinkedList<>();
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int m = sc.nextInt();
		//先将所有的值都加入到队列里:
		for(int i =0;i<n;i++) {
			queue.offer(i+1);
		}
		while(!queue.isEmpty()) {
			//找到要删除的元素
			for(int i =1;i<m;i++) {
				queue.add(queue.poll());
			}
			//将该元素删除:
			System.out.print(queue.poll()+" ");
		}
	}

}

看到这不知道你们会不会有疑问:反正有那么一瞬间,我是有点疑问的:他怎么直接就poll 了不用判断队列是否为空吗?
但是如果你仔细的再研究研究,就会发现,其实并不用,下面有简图,可供参考理解
在这里插入图片描述

使用数学公式(进阶)

这个方法只适用于一定的场景,请看一道题:
在这里插入图片描述
此问题题意:就是只求最后一个出队的元素,因此我们有两种思路:

第一种:就是将之前写的代码稍作修改直接就的出答案了,但是时间复杂度太高,过不了题目

代码仅供参考

import java.util.*;
public class Demo111424 {
	 public static void main(String[] args) {
		Queue<Integer> queue = new LinkedList<>();
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int m = sc.nextInt();
		//先将所有的值都加入到队列里:
		for(int i =0;i<n;i++) {
			queue.offer(i+1);
		}
		while(queue.size() != 1) {
			for(int i =1;i<m;i++) {
				queue.add(queue.poll());
			}
			queue.poll();
		}
		System.out.println(queue.poll());
	}

}

第二种就是我们要讲的数学方法:

在这里插入图片描述
上述简图描述了歩奏的一部分,但足以让我们发现规律:
知道为啥突然要标记下标吗?因为要用到下标了。(我们还是以m = 3为例,使用例子讲解更容易理解)
仔细观察:我们会发现每次删除一个元素,所有的元素都要减少3个单位
比如:上面的1 在进行第一次删除的时候下标从0->5 正好对应一个下表变换公式(i-m+len)%len;
再用这公式看元素2 也是一样。所以这就是规律啊。
但是知道了这个规律和我们的结果又有什么关系呢?如下:
逆向思维:如果我每删除一个元素,某个元素的下标都会减少3的话,那我总会删到只剩下一个元素:此时的元素不就是我们要求的那个元素吗?但是问题又来了,我怎么确定这个“某个元素”到底是哪个元素呢?问题就在这,这也是我们要求的啊。
所以这样想不对,我们在换个思路,如果我每删除一个元素,所有元素下标的都会减少3,那么如果此时我加上一个元素,那么元素的下标是不是都会增加3呢? 如果我逆着想,此时就只有一个元素,即为我所求的答案,但这个答案是未知的的,因为我也不知道这个元素是谁,此时我就需要求这个元素的下标,因为要想用到之前我们发现的规律,就必须向下表靠近,和下标扯关系。此时只有一个元素,他的下标是0,但是我们经过增加元素,所求的下标早已经变了,此时如果能找到改变后的元素的下标,那不就找到这个元素了吗,也就求出了答案。

  • 算法原理
    此时只有一个元素,他的下标是0,记作: F(1) = 0
    增加了一个元素后 下标 :(0+3)%2 = 1 ; F(2) = 1
    增加了两个元素后 下标: (1+3)%3 = 1 ;F(3) = 1
    增加了三个元素后 下标: (1+3)%4 = 0 ;F(4) = 0
    ……
    通过以上例子我们能总结出一个规律:
    (F(2) = 1 表示:2个元素 时候所求答案的下标为1)
    F(n) = (F(n-1)+m)%n
    我知道公式可能有点抽象,但是如果你真正自己动手写,照着理解一遍,其实很好理解的。
    这样我们就得到了一个递推公式,能推出来答案的下标到底是啥:
public class Demo111458 {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int m = sc.nextInt();
		int ret =0;
		//使用该递推公式就能算出来最终结果:
		for(int i =2;i<=n;i++) {
			ret = (ret+m)%i;
		}
		System.out.println(ret+1);
	}
}

代码实现:

模拟代码:

import java.util.*;
//使用单链表模拟:
class Node{
	int val;
	Node next;
	public Node(int val) {
		this.val = val;
	}
}
public class Demo111314 {
	public static void main(String[] args) {
		//先创建一个约瑟夫环:
		Scanner sc = new Scanner(System.in);
		int n =sc.nextInt();
		int m = sc.nextInt();
		if(n == 1) {
			System.out.print(1);
			return ;
		}
		Node head = new Node(0);
		Node cur = head;
		for(int i =0;i<n;i++) {
			Node node = new Node(i+1);
			cur.next = node;
			cur = node;
		}
		//将链表头尾相连:
		cur.next = head.next;
		//下一步:模拟:
		Node p = head;
		//Node p = cur;//都能用
		Node q = p.next;
		while(p != q) {
			for(int i =1;i<m;i++) {
				p = p.next;
				q = q.next;
			}
			System.out.print(q.val+" ");
			//找到以后,再将这个节点删掉:q指向下一个节点
			p.next = q.next;
			q = p.next;
		}
		System.out.print(q.val);
	}
	
}

队列代码:

import java.util.*;
public class Demo111415 {
	 public static void main(String[] args) {
		Queue<Integer> queue = new LinkedList<>();
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int m = sc.nextInt();
		//先将所有的值都加入到队列里:
		for(int i =0;i<n;i++) {
			queue.offer(i+1);
		}
		while(!queue.isEmpty()) {
			for(int i =1;i<m;i++) {
				queue.add(queue.poll());
			}
			System.out.print(queue.poll()+" ");
		}
	}

}

数学公式代码

import java.util.Scanner;
public class Demo111458 {
	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int m = sc.nextInt();
		int ret =0;
		//使用该递推公式就能算出来最终结果:
		for(int i =2;i<=n;i++) {
			ret = (ret+m)%i;
		}
		System.out.println(ret+1);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值