《剑指Offer》62:圆圈中最后剩下的数字(约瑟夫环)

2023-11-17

题目

0,1,2…,n-1这n个数字排成一个圆圈,从数字0开始,每次从这圆圈你删除第m个数字。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次2、0、4、1,因此最后剩下的数字是3。

本题就是有名的约瑟夫(Josephuse)环问题。

分析

两种解题方法:

  1. 用环形链表模拟圆圈的经典解法
  2. 分析每次被删除的数字的规律并直接计算出圆圈中最后剩下的数字

放码

用环形链表模拟圆圈的经典解法

public int lastRemaining(int n, int m) {
	if(n < 1 || m < 1) {
		throw new IllegalArgumentException();
	}
	
	LinkedList<Integer> list = new LinkedList<>();
	
	for(int i = 0; i < n; i++) {
		list.add(i);
	}
	
	int count = 0, index = 0;
	while(list.size() > 1) {
		count++;
		if(count == m) {
			list.remove(index);
			count = 0;
		}else {
			index++;
		}
		if(index == list.size()) {
			index = 0;
		}
	}
	
	return list.get(0);
}

分析每次被删除的数字的规律并直接计算出圆圈中最后剩下的数字

首先我们定义一个关于 n 和 m 的方程f(n,m),表示每次在 n 个数字 0,1, … ,n-1中每次删除第 m 个数字最后剩下的数字。

在这 n个数字中,第一个被删除的数字是(m-1)%n。为了简单起见,我们把(m- 1)%n 记为 k,那么删除k之后剩下的 n-1 个数字为 0,1,… ,k-1,k+1,… ,n-1,并且下一次删除从数字 k+1 开始计数。相当于在剩下的序列中, k+1 排在最前面,从而形成 k+1,… ,n- 1,0,1,… ,k-1 。

该序列最后剩下的数字也应该是关于 n 和 m 的函数。由于这个序列的规律和前面最初的序列不一样(最初的序列是从 0 开始的连续序列),因此该函数不同于前面的函数,记为 f’(n-1,m)。

最初序列最后剩下的数字 f(n, m)一定是删除一个数字之后的序列最后剩下的数字,即 f(n, m)=f'(n-1, m)

接下来我们把剩下的这 n-1 个数字的序列 k-1, …,n-1,0,1,… ,k-1 做一个映射,映射的结果是形成一个从 0 到 n-2 的序列:

last index -> index
k+1 0
k+2 1
n-1 n-k-2
0 n-k-1
1 n-k
k-1 n-2

我们把映射定义为p,则p(x)=(x-k-1)%n if p(x)<0, then p(x)+=n

它表示如果映射前的数字是x,那么映射后的数字是(x-k-1)%n。该映射的逆映射是p⁻¹(x)=(x+k+1)%n

由于映射之后的序列和最初的序列具有同样的形式,即都是从0开始的连续序列,因此仍然可以用函数f来表示,记为f(n-1, m)。根据我们的映射规则,映射之前的序列中最后剩下的数字f'(n-1, m)=p⁻¹[f(n-1, m)]=[f(n - 1, m) + k + 1] % n,把k = (m - 1) % n代入得到f(n, m)=f'(n-1, m)=[f(n-1, m) + m] % n

经过上面复杂的分析,我们终于找到了一个递归公式。要得到n个数字的序列中最后剩下的数字,只需要得到n-1个数字的序列中最后剩下的数字,并以此类推。当n=1时,也就是序列中开始只有一个数字0,那么很显然最后剩下的数字就是0。我们把这种关系表示为:

public int lastRemaining2(int n, int m) {
	if(n < 1 || m < 1) {
		throw new IllegalArgumentException();
	}
	return n == 1 ? 0 : (lastRemaining2(n - 1, m) + m) % n;
}

n=5,m=3的推导过程

针对这个题目,先说说难点:

数字组成是环形的结构,当数到最后个数字时,还不是需要删除的第 m 个数,需要回至数组的首位继续;
每次重新数的位置,都是上次删除数字的下一位。
针对第一个难点,可以考虑取模;

针对第二个难点,可以考虑将删除数字下一位,作为下次重新数的起点,剩余数字依次排列。(注意数字组成是环状的)

考虑先模拟,然后再进行逆推:

(为体现闭环,这里将数组进行复制。注意: 未得到最后 1 位数时,除第 1 轮开始 ,每一轮都是以上一轮删除数字下一位作为起点,重新数需要删除的第 m 个数)

这就是模拟之后得到的结果。

现在我们来进行逆推:

最终确定的 1 个数字,这个数字对应的索引一定是 0,逆推这个最终数字在每一轮中所处的索引位置,那么假设(n 表示数组元素个数,m 表示要删除的第 m 个数,取示例 1,n = 5, m = 3):

  • n = 1 时,索引:0;
  • n = 2 时,索引:(0 + m) % 2 = 3 % 2 = 1;
  • n = 3 时,索引:((0 + m) % 2 + 3) % 3 = (1 + 3) % 3 = 1;
  • n = 4 时,索引:(((0 + m) % 2 + 3) % 3 + m) % 4 = (1 + 3) % 4 = 0;
  • n = 5 时,索引:((((0 + m) % 2 + 3) % 3 + m) % 4 + m) % 5 = (0 + 3) % 5 = 3 。

大致讲下前面的逆推过程,找出剩余元素在前面每一轮所处的位置:

  • 当剩下 1 个数字的时候,这个数字(3)的索引为 0;
  • 往前逆推,当剩下 2 个数字的时候,在上一轮元素索引的基础上,要补上 m 个位置,然后对数组元素个数取模,得到这一轮该元素所在的位置,代入 n,m,可得数字(3)索引为 1;
  • 当剩下 3 个数字时,同样补上 m 个位置,然后对数组元素个数取模(这个时候数组元素个数为 3),代入 m,n,得数字(3)索引为 1;

对上面的逆推过程进行总结:从最后 1 轮往前逆推时,前面一轮的元素所处的位置为,(当前索引 + m) % 前面一轮元素个数

那么根据这个公式,用代码进行实现。

class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        ans = 0
        # 最后 1 位为最终保留数字
        # 往前逆推,从元素个数为 2 开始
        for i in range(2, n + 1):
            # 逆推公式
            ans = (ans + m) % i

        return ans

测试

import org.junit.Assert;
import org.junit.Test;

public class LastRemainingTest {

	@Test
	public void test() {
		LastRemaining lr = new LastRemaining();
		
		Assert.assertEquals(3, lr.lastRemaining(5, 3));
		Assert.assertEquals(2, lr.lastRemaining(10, 17));
		Assert.assertEquals(3, lr.lastRemaining2(5, 3));
		Assert.assertEquals(2, lr.lastRemaining2(10, 17));
	}
	
}

参考

  1. LeetCode 面试题62. 圆圈中最后剩下的数字

  2. LaTex数学公式生成

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《剑指Offer》62:圆圈中最后剩下的数字(约瑟夫环) 的相关文章

  • JavaScript实现数据结构 -- 链表

    文章目录 链表 链表的特点 链表和数组的区别 JS模拟链表 遍历链表 插入节点 删除节点 链表应用 删除链表中的节点 leetcode 237 思路 代码 反转链表 leetcode 206 思路 代码 链表 链表和数组一样是有多个元素组成
  • 【数据结构】堆、栈的区别

    heap 是堆 stack 是栈 在编程语言中 内存分配方式主要包括 栈 堆 静态存储分配 栈的内存是由操作系统自动分配 释放的 存放函数的参数值 局部变量等 堆的内存是由程序员手动申请和释放的 对应C语言中的malloc函数和C 中的ne
  • 先验概率及后验概率等解释

    20201010 0 引言 在学习统计学的时候 在概率估计的部分 经常会遇到最大似然估计 最大后验估计等名词 这些似然和后验 都跟贝叶斯准则中的一些名词定义有关 这里参考书籍 Think Bayes 这部书 来记录这些名词 1 由糖果例子来
  • c++ 链表的创建与链表常见操作

    c 链表的创建与链表常见操作 一 链表定义 struct 下面的结构体定义了C 语言中的一种常见的链表节点 包括数据 指针和两种种不同类型的构造函数 struct ListNode int val 存储数据 ListNode next ne
  • 剑指Offer 22. 链表中倒数第k个节点(Easy)/ 19. 删除链表的倒数第 N 个结点(Medium)/ ListNode调用!!!

    LeetCode 19 删除链表的倒数第 N 个结点 Medium 题目链接 题解 链表中倒数第 k 个节点 双指针 清晰图解 思路 代码 Definition for singly linked list class ListNode d
  • 单链表——多项式相加

    时间限制 1000ms 内存限制 256M 实验目的 编写代码 使用两个单链表表示下面的多项式 完成两个多项式相加 并输出相加后的多项式结果 实验要求 1 单链表的类型定义如下 typedef int datatype 结点数据类型 假设为
  • 对于单向链表的排序与去重

    include bits stdc h using namespace std struct node int num node next class Chain public Chain head tail new node void G
  • 模拟实现 队列 - JAVA(使用链表,数组)

    以链表实现 以数组实现 以链表实现 class Node public int val public Node next public Node int val this val val public class MyQueue publi
  • C语言单向循环链表的建立

    1 头文件 include
  • 148.排序链表(java)

    148 排序链表 题目描述 在 O n log n 时间复杂度和常数级空间复杂度下 对链表进行排序 示例 1 输入 4 gt 2 gt 1 gt 3 输出 1 gt 2 gt 3 gt 4 示例 2 输入 1 gt 5 gt 3 gt 4
  • 带头双向循环链表:一种高效的数据结构

    博客主页 江池俊的博客 收录专栏 数据结构探索 专栏推荐 cpolar C语言进阶之路 代码仓库 江池俊的代码仓库 编译环境 Visual Studio 2022 欢迎大家点赞 评论 收藏 文章目录 一 带头循环双向链表的概念及结构 二 使
  • 算法题-简单系列-04-链表中倒数最后k个结点

    文章目录 1 题目 1 1 快慢指针 1 题目 输入一个长度为 n 的链表 设链表中的元素的值为 ai 返回该链表中倒数第k个节点 如果该链表长度小于k 请返回一个长度为 0 的链表 1 1 快慢指针 代码中的类名 方法名 参数名已经指定
  • 算法题-简单系列-07-判断一个链表是否为回文结构

    文章目录 1 题目 1 1 使用list集合判断 1 题目 给定一个链表 请判断该链表是否为回文结构 回文是指该字符串正序逆序完全一致 1 1 使用list集合判断 因为需要判断是否为回文结构 所以要比较头尾的数据 而链表无法随机查询数据
  • 华为OD机试 Python 【最大载货量】

    描述 在火车站旁的货运站 小明负责调度2K辆中转车 其中K辆用于干货 K辆用于湿货 每批到站的货物来自不同的供货商 需要按照顺序装入中转车 注意 一个供货商的货物只能装在一辆车上 不能分开 但是 一辆车可以放多个供货商的货物 问题是 要让所
  • 华为OD机试 Java 【最大载货量】

    描述 在火车站旁的货运站 小明负责调度2K辆中转车 其中K辆用于干货 K辆用于湿货 每批到站的货物来自不同的供货商 需要按照顺序装入中转车 注意 一个供货商的货物只能装在一辆车上 不能分开 但是 一辆车可以放多个供货商的货物 问题是 要让所
  • LeetCode21. Merge Two Sorted Lists

    文章目录 一 题目 二 题解 一 题目 You are given the heads of two sorted linked lists list1 and list2 Merge the two lists into one sort
  • 【LeetCode:114. 二叉树展开为链表 | 二叉树 + 递归】

    算法题 算法刷题专栏 面试必备算法 面试高频算法 越难的东西 越要努力坚持 因为它具有很高的价值 算法就是这样 作者简介 硕风和炜 CSDN Java领域新星创作者 保研 国家奖学金 高中学习JAVA 大学完善JAVA开发技术栈 面试刷题
  • 「优选算法刷题」:移动零

    嗨 这个假期罗根开始接触了算法 在为今年的蓝桥杯做准备 所以 开个新专栏 记录记录自己做算法题时的心得 一 题目 给定一个数组 nums 编写一个函数将所有 0 移动到数组的末尾 同时保持非零元素的相对顺序 请注意 必须在不复制数组的情况下
  • 高中数学:因式分解(初接高)

    一 乘法公式 二 十字相乘法 例题 三 增添项法 主要解决整式中含高次项的因式分解题 补充 由于数学笔记 用键盘敲实在是麻烦 这里就把我的笔记截图上来了 大家将就看 有看不清楚的地方 可评论 定回复
  • 高中数学:不等式(初接高)

    1 二次不等式 2 分式不等式 最后的例题 是为了说明第三种情况 就是 不等号右边不为0时 要先进行移项操作 将右边化为0 这样 就转化成1 2两种情况了 3 其它复杂不等式 3 1 高次不等式 3 2 绝对值不等式 3 3 根式不等式 补

随机推荐

  • 忘掉 MindNode, 这才是Mac颜值最高的思维导图工具

    思维导图是一款帮助我们将大脑中浮现的各种思维串联起来的工具 非常适合用于逻辑性写作或者团队的头脑风暴 应用的范围比较广 关于写如何用思维导图工具的文章多如牛毛 不管是对职场人士 学生 还是全职妈妈的作用 各种文章都有涉猎 既然思维导图工具如
  • 安装CentOS7.6并创建用户及优化系统

    文章目录 安装系统 创建用户及基础优化系统 安装系统 制作CentOS7 6的镜像光盘或U盘 略过 将光盘或U盘放入到服务器中 修改BIOS启动选项 将其修改为光盘或U盘启动 启动服务器 如果是物理机的话 启动服务器后 进入远控卡 设置远控
  • JAVA socket编程实例

    转载文章 原作者无从考证 感谢作者的无私奉献 事实上网络编程简单的理解就是两台计算机相互通讯数据而已 对于程序员而言 去掌握一种编程接口并使用一种编程模型相对就会显得简单的多了 Java SDK提供一些相对简单的Api来完成这些工作 Soc
  • 【实战练习】汽油辛烷值优化建模(一)(题目+数据集)

    先放上题目和数据集 链接 https pan baidu com s 15 iDC9Wdx49rUe Qt2b Uw 提取码 6666 一 题目 1 背景 汽油是小型车辆的主要燃料 汽油燃烧产生的尾气排放对大气环境有重要影响 为此 世界各国
  • 《Qt快速入门》-- 信号与槽机制

    每一个图形开发语言 工具都有自己的一套的ui交互机制 Qt也不例外 Qt有自己独特的信号与槽机制用于ui与功能算法的交互 Qt的信号与槽机制包含以下三点 1 确定是哪个控件发出了信号 Who 2 确定发出了什么信号 What 3 确定这个信
  • java必懂之"=="与equals的区别

    屁话不多说 直接上代码 equals和关系运算符 的区别 author 刘威辰的秘密花园 1 用在基本数据类型boolean a b 2 判断引用是否指向同一个地址且内容是否相同 equals 1 用于判断两个变量是否对同一个对象的引用 即
  • Django报错403 Forbidden. CSRF token missing or incorrect的解决办法

    Django报错403 Forbidden CSRF token missing or incorrect的解决办法 首先要确认自己在views py中使用的是render 之后确认自己在xx html中的
  • KCF高速跟踪详解

    思想 一般化的跟踪问题可以分解成如下几步 1 在 It 帧中 在当前位置 pt 附近采样 训练一个回归器 这个回归器能计算一个小窗口采样的响应 2 在 It 1 帧中 在前一帧位置 pt 附近采样 用前述回归器判断每个采样的响应 3 响应最
  • IntelliJ IDEA 如何创建一个包,并在包中创建一个Java程序

    1 选中scr右键后 将鼠标放到New上 点击Package 2 采用域名倒置的方式对包名进行命名 3 选中包后 鼠标右键选中New 点击Java Class 完成一个Java程序的创建
  • Python算法:深度优先搜索—DFS(模板及其样例)

    深度优先搜索搜索 介绍 沿着一条路径一直搜索下去 在无法搜索时 回退到刚刚访问过的节点 并且每个节点只能访问一次 本质上是持续搜索 遍历了所有可能的情况 必然能得到解 流程是一个树的形式 每次一条路走到黑 目的主要是达到被搜索结构的叶结点直
  • Nginx学习与实战 · 解决net::ERR_CONTENT_LENGTH_MISMATCH 206问题

    Vue项目引入了d3 js 在打包部署到nginx静态服务后 页面不能正常展示 F12打开控制台 发现报了几个net ERR CONTENT LENGTH MISMATCH 206 Partial Content 错误 第一次遇到Statu
  • Java 实现微信支付详细教程

    摘要 最近的一个项目中涉及到了支付业务 其中用到了微信支付和支付宝支付 在做的过程中也遇到些问题 所以现在总结梳理一下 分享给有需要的人 也为自己以后回顾留个思路 一 微信支付接入准备工作 首先 微信支付 只支持企业用户 个人用户是不能接入
  • 【Pytorch论文相关代码】使用SOLD2预训练好的模型检测与匹配线段(自己的数据集)

    文章目录 前言 使用流程 检测与匹配结果 前言 论文链接 SOLD2 Self supervised Occlusion aware Line Description and Detection 论文源码 https github com
  • Spyder 运行时kernels启动报错

    1 报错如下 An error ocurred while starting the kernel Your Python environment or installation doesn t have the spyder kernel
  • 表格增删查改使用ajax请求后端数据并没有更新的问题

    1 如果使用的是vue 那么先检查你的表格有没有使用v model实现数据双向绑定 2 使用ajax请求 因为ajax默认使用的是异步请求 也就是客户端请求给服务端时 客户端不需要等待也可以做其他事情 这是我实现添加的代码 其中标框的asy
  • MySQL在线大表DDL操作

    MySQL在线大表DDL操作的方法 1 主从架构轮询修改 a 主库会话级别的记录binglog的参数关闭 b 500 502错误异常捕捉 c 检查备库的second behind master是否有延迟 d varchar有页分裂的情况 尽
  • 【linux 异常断电】进入了emergency mode解决办法

    系统异常断电关机 导致启动时进入了emergency mode 解决办法 1 查看日志或报错信息 查看日志 journalctl 按他的操作输入journalctl之后输入shift g到日志最后查看报错发现是xfs sda3有问题 发现
  • ES6关于函数详解

    设置默认值的方式 ES6 之前 不能直接为函数的参数指定默认值 只能采用变通的方法 ES6 允许为函数的参数设置默认值 即直接写在参数定义的后面 function log x y World console log x y log Hell
  • 基于SpringBoot的共享单车管理系统

    末尾获取源码 开发语言 Java Java开发工具 JDK1 8 后端框架 SpringBoot 前端 采用HTML和Vue技术开发 数据库 MySQL5 7和Navicat管理工具结合 服务器 Tomcat8 5 开发软件 IDEA Ec
  • 《剑指Offer》62:圆圈中最后剩下的数字(约瑟夫环)

    题目 0 1 2 n 1这n个数字排成一个圆圈 从数字0开始 每次从这圆圈你删除第m个数字 求出这个圆圈里剩下的最后一个数字 例如 0 1 2 3 4这5个数字组成一个圆圈 从数字0开始每次删除第3个数字 则删除的前4个数字依次2 0 4