题目: 0,1,...,n-1等n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,最后剩下的数字是3.
删除过程:
loop 1: 删除从0开始的第三个位置上的数字2
loop 2: 重新编号,从3开始计数,第三个数字为0
依次类推,最后剩余数字3.
解题思路: 令f(n,m)表示从数列中删除第m个数字最后剩余数字的索引,则有递推式:
根据以上公式,可用递归或动态规划求解,代码如下。
/**
* 剑指offer--环中最后的剩余数字(约瑟夫环)
* 解法1: 模拟环形链表
* 解法2: 找规律(推荐)
* @param n ...... 编号0~n-1
* @param m ...... 每次删除第m个数字
* @return last remaining number
* */
public static int LastRemaining_DP(int n, int m) {
if (n < 1 || m < 1) {
return -1;
}
int last = 0;
for(int i = 2; i <= n; i++) {
last = (last + m) % i;
}//point: mod i
return last;
}
public static int LastRemaining_Recur(int n, int m) {
if (n < 1 || m < 1) {
return -1;
}
if (n == 1) {
return 0;
}
return (LastRemaining_Recur(n-1,m) + m) % n;
}
// 单链表实现
public static int LastRemaining_Solution(int n, int m) {
if (n < 1 || m < 1) {
return -1;
}
ListNode head = new ListNode(0);
ListNode tail = head;
for(int i = 1; i < n; i++) {
tail.next = new ListNode(i);
tail = tail.next;
}
tail.next = head;// 构造环形链表
int length = n, index = (m - 2) % length;
while (length > 1) {
int i = 0;
ListNode p = head;
while (i != index) {
p = p.next;
i++;
}
head = p.next.next;
p.next = head; // 删除结点
length--;
index = (m - 2) % length;
}
return head.val;
}
证明过程: 记方程f(n,m)为从n个数字中删除第m个数字之后最后剩下的数字,从编号为0~n-1的n个数字中删除第m个数字,即索引值为m-1的数字,第一个被删除的数字是(m-1)%n, 记k=(m-1)%n, 删除k之后,序列中剩余的数字为k+1,...,n-1,0,1,2...k-1,该序列的规律与最初序列的规律不同,记为g(n-1,m),表示从新序列中继续删除后剩余的最后一个数字,从而有f(n,m)=g(n-1,m).
对数列进行重新编号之后,对应的索引值为
k+1 -> 0
k+2 -> 1
...
n-1 -> n-k-2
0 -> n-k-1
1 -> n-k
...
k-1 -> n-2
令重新排列之后的索引值为自变量x,新编号为y
x在[k+1, n-1]范围内时,对应的有x-(k+1) = y-0 => y=x-k-1
x在[0,k-1]范围内时,对应的有x-0=y-(n-k-1) => y=x-k-1+n
综上,y=p(x)=(x-k-1+n)%n, 其反函数为y=(x+k+1)%n,根据新序列最后剩余的数字g(n-1,m)与原序列的对应关系,有:
证明完毕