方法一:朴素模拟法 O(m*n)
public class Solution { public int LastRemaining_Solution(int n, int m) { if(n<=0 || m<=0)return -1; ListNode head = new ListNode(0); ListNode p = head; for(int i=1; i<=n-1; ++i){ ListNode node = new ListNode(i);//串成链 p.next = node; p = p.next; } p.next = head;//p回到开头位置的前一个,形成闭环 //还有一个作用是,让p指向head前一个开始,可以使每轮循环一样套路 for(int i=1; i<=n-1; ++i){ for(int j=1; j<=m-1; ++j){ p=p.next; } p.next = p.next.next;//java会自动回收,所以不管那个被删除的节点 } return p.val;//剩下的最后一个 } }//这种思路是:模拟完整的游戏运行机制,不跳步 //时间O(m*n) 空间O(n)
方法二:数学归纳法 O(n) [分析是难点]
public class Solution { public int LastRemaining_Solution(int n, int m) {//时光倒流递推法:为什么要逆向?因为逆向是由少到多不会有空位;而正向会有空位,必须模拟、不能跳步 if(n<=0)return -1; int res = 0; //f(1,m)=0 for(int i=2; i<=n; i++){//i就是小朋友数量,i=2是游戏最后一轮,但是我解法的第一轮 //循环从i=2到i=n,小朋友越来越多,此解法是倒推(时光倒流) res = (res + m) % i; //相邻项关系:左边res是f(n,m) 右边res是f(n-1,m) } return res; } }//时间O(n) 空间O(1)
数学归纳法分析:
数学归纳法:
f(n,m)表示:【相对参考系:从0位置开始,最终到达f(n,m)位置】//例如:从0开始,最终到达f(5,3)=3的位置
f(1,m)=0;//首项
f(n,m)=[(m%n) + f(n-1,m)]%n;//公式化简为:f(n,m)=[m + f(n-1,m)]%n //相邻项关系【重点,推导如下】:
例如:
f(5,3)=[f(4,3)+ 3%5 ] %5=f(4,3)+3 什么意思?
f(5,3)从0开始,0-1-2,删除2节点,然后来到3==》这时的情况就类似f(4,3)。但还有一点不一样,就是标准的f(4,3)从0开始,而这里从3开始
f(4,3)根据定义,必须从0开始(所有的f(i,m)的定义需要一致),而不是从3。所以必须进行【对齐操作】:
于是f(5,3)的参靠系里:先走3步,然后以3为起点,走f(4,3)步 ==》f(5,3)=3+ f(4,3) [这里是简化,再考虑%n的细节优化下就ok了]
有了递推公式,用递归法or迭代法,求解都几乎同理
迭代法:f(1,m)=0 f(2,m)=[m + f(1,m)]%n ... 一直算到f(n,m)