阅读目录
题目描述
有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,随机指定一个数 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