有LeetCode算法/华为OD考试扣扣交流群可加 948025485
可上全网独家的 欧弟OJ系统 练习华子OD、大厂真题
绿色聊天软件戳od1336
了解算法冲刺训练
约瑟夫环
这个魔术的核心其实就是经典的约瑟夫环问题!!
-
考虑最简单的情况。
假设牌是2张,编号分别是1 2
会把1放到后面,扔掉2。剩下的就是最开始放在最上边的那张1。 -
稍微复杂一点的情况,牌的张数是2的n次方。
比如牌是8张,编号分别是1 2 3 4 5 6 7 8
第一轮会把2 4 6 8扔掉,剩下1 3 5 7按顺序放在后面,又退化成了4张牌的情况。
第二轮会把3 7扔掉,剩下1 5按顺序放在后面,又退化成了2张牌的情况。
第三轮把5扔掉,剩下1,就是最初在最前面的那张。
可以得到结论:如果牌的张数是2^n,最后剩下的一定是最开始放在牌堆顶的那张。 -
考虑任意的情况,牌的张数是2^n+m。
比如牌的张数是11,等于8+3。
把1放到后面,把2扔掉
把3放到后面,把4扔掉
把5放到后面,把6扔掉
现在剩下的编号序列是7 8 9 10 11 1 3 5
这又是8张牌的情况了!
最后一定剩下的是现在牌堆顶的7!
所以只要提前知道牌的张数,就一定能马上推导出最终是剩下哪一张牌。
只需要按照一些规则把想要留下来的那张牌插入到原来牌堆中正确的位置就可以了!
一切的魔法都是数学!!都是算法!!
魔术流程
-
4张牌对折后撕开,就是8张,叠放在一起就是ABCDABCD
❗️注意ABCD四个数字是完全等价的 -
(无关步骤)根据名字字数,把顶上的牌放到下面,但怎么放都不会改变循环序列的相对位置。
譬如2次,最后变成CDABCDAB
譬如3次,最后换成DABCDABC
但无论怎么操作,第4张和第8张牌都是一样的。 -
(关键步骤)把顶上3张插到中间任意位置。
这一步非常重要!这个3也是最关键的数字!
因为操作完之后必然出现第1张和第8张牌是一样的!以名字两个字为例,可以写成BxxxxxxB
(这里的x是其他和B不同的牌) -
(关键步骤)拿掉顶上的牌放到一边,记为B。剩下的序列是xxxxxxB,一共7张牌。
-
(无关步骤)南方人/北方人/不确定,分别拿顶上的1/2/3张牌插到中间,但是不会改变剩下7张牌是xxxxxxB的结果。
-
(关键步骤)男生拿掉1张,女生拿掉2张。也就是男生剩下6张,女生剩下5张。
分别是xxxxxB和xxxxB。 -
(关键步骤)循环7次,把最顶上的放到最底下,男生和女生分别会是xxxxBx和xxBxx
-
最后执行约瑟夫环过程!操作到最后只剩下1张
当牌数为6时(男生),剩下的就是第5张牌。
当牌数为5时(女生),剩下的就是第3张牌。
Bingo!就是4步拿掉的那张牌!
模拟程序
# 用队列代替列表,方便移动牌的操作
from collections import deque
# 随机数操作用于一些无关紧要的步骤
from random import randint
# 定义一个函数,用于把牌堆顶n张牌移动到末尾
# 步骤2和步骤7都要用到这个函数
def move_card_back(n, arr):
# 循环n次,把队列第一张牌放到队列末尾
for _ in range(n):
move_card = arr.popleft() # 弹出队头元素,即第一张牌
arr.append(move_card) # 把原队头元素插入到序列末尾
return arr
# 定义一个函数,用于把牌堆顶n张牌移动到中间的任意位置
# 步骤3和步骤5都要用到这个过程
def move_card_middle_random(n, arr):
# 插入在arr中的的位置,随机生成一个idx
# 这个位置必须是在n+1到len(arr)-1之间
idx = randint(n+1, len(arr)-1)
# 转成列表后使用切片,执行插入操作
return deque(list(arr)[n:idx] + list(arr)[:n] + list(arr)[idx:])
# 步骤1:初始化8张牌,假设为"ABCDABCD"
arr = deque("ABCDABCD")
print(f"步骤1:拿出4张牌,对折撕成8张,按顺序叠放。\n此时序列为:{''.join(arr)}\n---")
# 步骤2(无关步骤):名字长度随机选取,这里取2到5(其实任意整数都行)
name_len = randint(2, 5)
# 把name_len张牌移动到序列末尾
arr = move_card_back(name_len, arr)
print(f"步骤2:随机选取名字长度为{name_len},把第1张牌放到末尾,操作{name_len}次。\n此时序列为:{''.join(arr)}\n---")
# 步骤3(关键步骤):把牌堆顶三张放到中间任意位置
arr = move_card_middle_random(3, arr)
print(f"步骤3:把牌堆顶3张放到中间的随机位置。\n此时序列为:{''.join(arr)}\n---")
# 步骤4(关键步骤):把最顶上的牌拿走
rest_card = arr.popleft() # 弹出队头元素
print(f"步骤4:把最顶上的牌拿走,放在一边。\n拿走的牌为:{rest_card}\n此时序列为:{''.join(arr)}\n---")
# 步骤5(无关步骤):根据南方人/北方人/不确定,把顶上的1/2/3张牌插入到中间任意位置
# 随机选择1、2、3中的任意一个数字
move_num = randint(1, 3)
arr = move_card_middle_random(move_num, arr)
print(f"步骤5:我{'是南方人' if move_num == 1 else '是北方人' if move_num == 2 else '不确定自己是哪里人'},"\
f"把{move_num}张牌插入到中间的随机位置。\n此时序列为:{''.join(arr)}\n---")
# 步骤6(关键步骤):根据性别男或女,移除牌堆顶的1或2张牌
male_num = randint(1, 2) # 随机选择1或2
for _ in range(male_num): # 循环male_num次,移除牌堆顶的牌
arr.popleft()
print(f"步骤6:我是{'男' if male_num == 1 else '女'}生,移除牌堆顶的{male_num}张牌。"
f"\n此时序列为:{''.join(arr)}\n---")
# 步骤7(关键步骤):把顶部的牌移动到末尾,执行7次
arr = move_card_back(7, arr)
print(f"步骤7:把顶部的牌移动到末尾,执行7次\n此时序列为:{''.join(arr)}\n---")
# 步骤8(关键步骤):执行约瑟夫环过程。把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。
print(f"步骤8:把牌堆顶一张牌放到末尾,再移除一张牌,直到只剩下一张牌。")
while len(arr) > 1:
luck = arr.popleft() # 好运留下来
arr.append(luck)
print(f"好运留下来:{luck}\t\t此时序列为:{''.join(arr)}", )
sadness = arr.popleft() # 烦恼都丢掉
print(f"烦恼都丢掉:{sadness}\t\t此时序列为:{''.join(arr)}",)
print(f"---\n最终结果:剩下的牌为{arr[0]},步骤4中留下来的牌也是{rest_card}")
华为OD算法/大厂面试高频题算法练习冲刺训练
-
华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务300+同学成功上岸!
-
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
-
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
-
60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁
-
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
-
可查看链接 大厂真题汇总 & OD真题汇总(持续更新)
-
绿色聊天软件戳
od1336
了解更多