约瑟夫问题
约瑟夫问题是个著名的问题:N个人围成一圈,第一个人从1开始报数,报M的将被杀掉,下一个人接着从1开始报。如此反复,最后剩下一个,求最后的胜利者。
例如只有三个人,把他们叫做A、B、C,他们围成一圈,从A开始报数,假设报2的人被杀掉。
- 首先A开始报数,他报1。侥幸逃过一劫。
- 然后轮到B报数,他报2。非常惨,他被杀了
- C接着从1开始报数
- 接着轮到A报数,他报2。也被杀死了。
- 最终胜利者是C
解决方案
普通解法
刚学数据结构的时候,我们可能用链表的方法去模拟这个过程,N个人看作是N个链表节点,节点1指向节点2,节点2指向节点3,……,节点N-1指向节点N,节点N指向节点1,这样就形成了一个环。然后从节点1开始1、2、3……往下报数,每报到M,就把那个节点从环上删除。下一个节点接着从1开始报数。最终链表仅剩一个节点。它就是最终的胜利者。
缺点:要模拟整个游戏过程,时间复杂度高达O(nm),当n,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。
package main
import "fmt"
type circleNode struct {
num int
next *circleNode
}
func NewCircleNode(data int) (*circleNode) {
n := new(circleNode)
n.num = data
n.next = nil
return n
}
type circleLinked struct {
head *circleNode
tail *circleNode
}
func NewCircleLinked()(*circleLinked){
c := new(circleLinked)
c.head = nil
c.tail = nil
return c
}
func (c *circleLinked)Add(data int) {
n := NewCircleNode(data)
if(c.tail == nil){
n.next = n
c.tail = n
c.head = n
}else{
c.tail.next = n
c.tail = n
n.next = c.head
}
}
func (c *circleLinked)showListed() {
if(c.head == nil){
return
}else{
temp := c.head
for temp != c.tail {
fmt.Print(temp.num ,"\t")
temp = temp.next
}
fmt.Print(temp.num ,"\n")
}
}
// head为第1个
//从第K个,循环起第num个,留下最后一个
func (c *circleLinked)jock(k,num int) {
if(c.head == nil){
return
}
// 循环到起点
for i := 1; i < k; i++{
c.head = c.head.next
c.tail = c.tail.next
}
count := 1 // 记录次数, 从1开始喊
for{
if(num == count){ // 判断当前是不是出局的人
fmt.Println("出局", c.head.num)
c.tail.next = c.head.next
c.head = c.head.next
count = 1 // 下一个
}
if(c.head == c.tail){ // 判断是不是最后一人
fmt.Println("最后一个", c.head.num)
break
}
// 如果不是,需要继续喊
c.head = c.head.next
c.tail = c.tail.next
count++
}
}
func main() {
c := NewCircleLinked()
for i := 1; i < 12 ; i++ {
c.Add(i)
}
c.showListed()
c.jock(1, 3)
}
公式法
下面我们不用字母表示每一个人,而用数字。
1、2、3、4、5、6、7、8、9、10、11、
表示11个人,他们先排成一排,假设每报到3的人被杀掉。
- 刚开始时,头一个人编号是1,从他开始报数,第一轮被杀掉的是编号3的人。
- 编号4的人从1开始重新报数,这时候我们可以认为编号4这个人是队伍的头。第二轮被杀掉的是编号6的人。
- 编号7的人开始重新报数,这时候我们可以认为编号7这个人是队伍的头。第三轮被杀掉的是编号9的人。
…… - 第九轮时,编号2的人开始重新报数,这时候我们可以认为编号2这个人是队伍的头。这轮被杀掉的是编号8的人。
- 下一个人还是编号为2的人,他从1开始报数,不幸的是他在这轮被杀掉了。
- 最后的胜利者是编号为7的人。
下图表示这一过程(先忽视绿色的一行)
现在再来看我们递推公式是怎么得到的!
将上面表格的每一行看成数组,这个公式描述的是:幸存者在这一轮的下标位置
很神奇吧!现在你还怀疑这个公式的正确性吗?上面这个例子验证了这个递推公式的确可以计算出胜利者的下标,下面将讲解怎么推导这个公式。
-
问题1:假设我们已经知道11个人时,胜利者的下标位置为6。那下一轮10个人时,胜利者的下标位置为多少?
答:其实吧,第一轮删掉编号为3的人后,之后的人都往前面移动了3位,胜利这也往前移动了3位,所以他的下标位置由6变成3。 -
问题2:假设我们已经知道10个人时,胜利者的下标位置为3。那下一轮11个人时,胜利者的下标位置为多少?
答:这可以看错是上一个问题的逆过程,大家都往后移动3位,所以f(11,3)=f(10,3)+3。不过有可能数组会越界,所以最后模上当前人数的个数,f(11,3)=(f(10,3)+3)%11 -
问题3:现在改为人数改为N,报到M时,把那个人杀掉,那么数组是怎么移动的?
答:每杀掉一个人,下一个人成为头,相当于把数组向前移动M位。若已知N-1个人时,胜利者的下标位置位f(N−1,M),则N个人的时候,就是往后移动M位,(因为有可能数组越界,超过的部分会被接到头上,所以还要模N),既f(N,M)=(f(N−1,M)+M)%N
注:理解这个递推式的核心在于关注胜利者的下标位置是怎么变的。每杀掉一个人,其实就是把这个数组向前移动了M位。然后逆过来,就可以得到这个递推式。