剑指offer:Python 孩子们的游戏(圆圈中最后剩下的数) Python实现约瑟夫问题

题目描述

有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到终极大奖。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从 0 到 n-1 ) 如果没有小朋友,请返回 -1

思路和Python实现

这应该是著名的“约瑟夫环问题”

【思路一】数学推导

数学推导过程:(看不懂,不想看,直接跳到下面的图解推导)

  • 首先定义最初的n个数字(0,1,…,n-1)中最后剩下的数字是关于n和m的方程为f(n,m)。
    在这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,…k-1。

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

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

    k+1 -> 0
    k+2 -> 1

    n-1 -> n-k-2
    0 -> n-k-1

    k-1 -> n-2

  • 把映射定义为p,则p(x)= (x-k-1)%n,即如果映射前的数字是x,则映射后的数字是(x-k-1)%n。对应的逆映射是p-1(x)=(x+k+1)%n。

  • 由于映射之后的序列和最初的序列有同样的形式,都是从0开始的连续序列,因此仍然可以用函数f来表示,记为f(n-1,m)。根据我们的映射规则,映射之前的序列最后剩下的数字f’(n-1,m)= p-1 [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。我们把这种关系表示为:
    在这里插入图片描述
    尽管得到这个公式的分析过程非常复杂,但它用递归或者循环都很容易实现。最重要的是,这是一种时间复杂度为O(n),空间复杂度为O(1)的方法。

图解过程

  • 如下图:假设有数值为 0 - n-1 的 n-1个数,它们的序号也是 0 - n-1 ,指定数 为 m ,从下标 index=0 开始,数到m个数,正好是 index=m -1 对应的数,取走;此时 新的序列 缺少了个item=m-1,如果将 前者完整序列看做一个函数 f(n) 因为 取走数值后 对应的序号发生了变化,不能简单看做f(n-1)或者说是通过f(x)就能得到,所以记做一个新函数 f ’ (n-1)

在这里插入图片描述

  • 为了能 通过 f(n)变换就能得到下一次要取走的数,得到 f(n-1) ,所以进行移动,让 index=m-1, item=m 向前移动到 index=0 的位置,此时 它是从 index=0 出发再去 按照 m 取值的,所以可以记做 f(n-1)

在这里插入图片描述

  • 整个过程,在给定m的数值后,无论经过多次取走下一个index=m-1 的过程,无论取走多少个index=m-1 对应的值,即经历了多少次f(x)->f ’ (x-1)->f ’ (x-2)… 要找的最终的 i 是相同的;所以第一次f(n) 要找的 i 和 f ’ (n-1) i 是同一个值,即相等。

  • 综合来推导:

    f ( n - 1 ) = iii (假设它找到的一个值)
    ↓↓↓
    f ’ ( n - 1 ) = ( iii + m ) % n
    ↓↓↓
    f ( n ) = f ’ ( n -1 ) = ( iii + m ) % n
    ↓↓↓
    f ( n ) = ( f ( n - 1 ) + m ) % n

  • 解释下上面的推导过程:
    为什么:f ’ ( n - 1 ) = ( iii + m ) % n 因为 f ( n - 1 ) 是向前移动的 m个间隔(index:0~m-1),所以它上一个是 ( iii + m ) % n
    为什么:f ( n ) = f ’ ( n -1 ) 看上面的详解,总之就是把所有的变换步骤当成一个整体的过程,给定一个m值,无论变换到哪一步,都是为了找最终的 i

在这里插入图片描述
在这里插入图片描述

class Solution(object):
    def last_remaining_solution(self, n, m):
        '''
        :param n: 总个数
        :param m: 指定报几个数,相隔距离
        :return: 返回最终留下的值
        '''
        if n < 1 or m < 1:
            return -1
        if n == 1:
            return 0
        last = 0 # 如果n=1时,直接找到最终的last=0 说明 f(1)=0 
        for index in range(2, n + 1):
            current_last = (last + m) % index
            last = current_last
        return last

'''
测试一下:让 n = 5 , m 分别取 2 3 4 
obj = Solution()
print(obj.last_remaining_solution(5, 3))

m =2,last = 2
# 0 1 2 3 4
# 0 2 3 4
# 0 2 4
# 2

m = 3,last = 3
# 0 1 2 3 4
# 0 1 3 4
# 1 3 4
# 1 3
# 3

m = 4,last = 0
# 0 1 2 3 4
# 0 1 2 4
# 0 1 4
# 0 1
# 0
'''

约瑟夫环问题:可以用 模拟循环列表,模拟循环链表 实现

模拟环形列表

class Solution:
    def LastRemaining_Solution(self, n, m):
        if n < 1 or m < 1:
            return
        childNum = list(range(n))
        cur = 0  # 指向list的指针
        while len(childNum) > 1:
            for i in range(1, m):
                cur += 1
                # 当指针移到list的末尾,则将指针移到list的头
                if cur == len(childNum):
                    cur = 0
            # 删除一个数,此时由于删除之后list的下标随之变化
            # cur指向的便是原数组中的下一个数字,此时cur不需要移动
            childNum.remove(childNum[cur])
            if cur == len(childNum):  # list的长度和cur的值相等则cur指向0
                cur = 0
        return childNum


obj = Solution()
print(obj.LastRemaining_Solution(5, 3)) # [3]

模拟环形链表

class Node(object):
    def __init__(self, no):
        self.no = no
        self.next = None


class CircleSingerLink(object):
    def __init__(self):
        self.__head = None

    def is_empty(self):
        return self.__head is None

    def append(self, no):  # 尾插法
        if no < 0:
            print("输入数据不合法")
            return
        for index in range(no):
            node = Node(index)
            if index == 0:
                self.__head = node
                node.next = self.__head
            else:  # 先移到链表尾部
                cur = self.__head
                while cur.next != self.__head:
                    cur = cur.next
                cur.next = node  # 将尾节点指向新增结点node
                node.next = self.__head  # 将新增结点node指向头节点__head

    def travel(self):  # 遍历查看
        if self.is_empty():
            return
        cur = self.__head
        while cur.next != self.__head:
            print('小孩的编号:%d \n' % cur.no)  # 放在前面,因为判断条件是至少有一个结点
            cur = cur.next
        print('小孩的编号:%d \n' % cur.no)

    def count_boy(self, m, n):
        if m < 1 or n < 1:
            return -1
        helper = self.__head  # 放在尾结点的指针
        cur = self.__head  # 放在头结点的指针
        while helper.next is not cur:  # 先让 helper指针移动到尾结点
            helper = helper.next
        while True:
            if helper is cur:  # 表示只有一个结点
                break
            for i in range(m - 1):  
            # 报数开始位置算一个,而且只要删除m-1位置的数,所以真正移动的数量是m-1
                cur = cur.next
                helper = helper.next
            print("小孩:%d出圈\n" % cur.no)  # 要移除的就是cur指向的位置
            cur = cur.next  # 移除一个结点后,让cur往后移一个位置
            helper.next = cur  # 让后一个结点,连上cur
        print("最后留圈中的小孩是:%d\n" % cur.no)


if __name__ == "__main__":
    s = CircleSingerLink()
    s.append(5)
    s.travel()
    s.count_boy(3, 5)  # 3 
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值