質問とテスト
package sword045;
/* 题目:0,1,...,n-1 这n个数字排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
测试样例:
输入: 0,1 , 2, 3, 4
输出: 3
*/
import java.util.List;
public class main {
public static void main(String[] args) {
int [] testTable = {5,10,15};
int [] testTable2 = {1,2,3};
for(int i=0;i<testTable.length;i++){
test(testTable[i],testTable2[i]);
}
}
private static void test(int ito,int ito2) {
Solution solution = new Solution();
int rtn;
long begin = System.currentTimeMillis();
System.out.print(ito+" ");
System.out.print(ito2);
System.out.println();
//开始时打印数组
rtn= solution.lastRemaining(ito,ito2);//执行程序
long end = System.currentTimeMillis();
System.out.println("rtn=" );
System.out.print(rtn);
System.out.println();
System.out.println("耗时:" + (end - begin) + "ms");
System.out.println("-------------------");
}
}
解決策1(成功)
タイトルにはデジタルサークルがあるため、データ構造を使用してこのサークルをシミュレートするのが自然なアイデアです。一般的に使用されるデータ構造の中で、循環リンクリストを簡単に考えることができます。合計n個のノードを持つ循環リンクリストを作成し、そのたびにこのリンクリストからm番目のノードを削除できます。
package sword045;
public class Solution {
public int lastRemaining(int n, int m) {
if(n == 0 || n==1) {
return 0;
}
ListNode first = new ListNode(0);
ListNode now = first;
for(int i=1;i<n;i++) {
now.next = new ListNode(i);
now = now.next;
}
now.next = first;
now = first;
for(int i=0;i<n-1;i++) {
for(int j = 1;j<m;j++) {
now = now.next;
}
removeNode(now);
}
return now.val;
}
private void removeNode(ListNode node) {
ListNode next = node.next;
node.val = next.val;
node.next = next.next;
next.next = null;
}
}
解決策2(他の人の)
関数f(n、m)を定義します。これは、m番目の数の最後の残りの数をn個の数0,1、...、n-1から毎回削除することを意味します。
n個の数のうち、最初に削除される数は(m%n)-1であり、この数をKと表記します。最初の要素Kを削除した後、残りのn-1個の数は0、1、2、...です。 。、k-1、k + 1、...、n-1、そして次の削除はK +1からカウントを開始します。次に、次のカウントは、実際には、K + 1、...、n-1、0、1、...、K-1のような順序でトラバースすることと同じです。このシーケンスは実際には前のシーケンスと同じです。違いは、順序を変更したことですが、要素を削除するときの走査順序は同じです。したがって、いくつかの削除後の残りの数は、前のシーケンスと同じである必要があります。最後のシーケンスのm番目の桁を削除し、残りの桁をf '(n-1、m)として記録します。なぜf'(n-1、m)として記録されるのかについては、後で理解します。したがって、少なくともf(n、m)= f '(n-1、m)であることを確認できます。
このシーケンスをもう一度見てみましょう:k + 1、...、n-1,0,1、...、k-1。このシーケンスをマッピングすると、マッピングの結果は0からn-2までのシーケンスになります
。k+ 1-> 0
k + 2-> 1
......
n-1-> nk-2
0-> nk-1
1-> nk
......
k-1-> n-2
f '(n-1、m)f(n-1、m)
マッピングをpとして定義し、次にp(x) =(xk-1)%n。これは、マッピング前の数値がxの場合、マッピング後の数値は(xk-1)%nであることを意味します。このマッピングの逆は、p-1(x)=(x + k + 1)%nです。この方法をマスターしたいので、それを完全に理解する必要があります。以下で私と一緒にそれを証明させてください。
証明:
y = p(x)、つまりy =(xk-1)%nとし
、y =(xk-1)+ t1n、t1は整数、0 <= y <n
<--- > x = y-t1n + k + 1
<----> x =(y + k + 1)+ t2n、つまりy =(x + k + 1)+ tnなので、p-1(x) =(x + k +1)%n
証明が完了しました。
さて、マッピング後のn-1の数は、フォームの元のnの数と同じであることがわかりますか?1つ少ない数n-1を見てください。次に、n-1個の数0、1、...、n-2について、それらを円に配置し、番号0から毎回m番目の数を削除します。残りの数はf(n -1、m)?!さて、以前にシーケンスをf '(n-1、m)として定義した理由を発見しましたか?これは、2つの削除間の接続を確立するためです!つまり、元のn個の要素は、最初の要素kを削除した後、おそらく最初のシーケンスが中断され、ルールはありませんが、マッピング関係を介してシーケンスを最初のシーケンスの形式に再配置します。このように、このようなマッピング関係を見つけ、2つの操作間の関数関係(反復法則)を見つける限り、問題は再帰的な問題に変換されます。再帰的問題の終了は非常に簡単に判別できます。n= 1の場合、シーケンスには1つの要素しかありません。0、f(1、m)はこの0です。
マッピング関係ができたので、2つの反復操作間の関係、つまりf(n-1、m)からf(n、m)を見つける方法を見つけましょう。
解決策:
f(n、m)= f '(n-1、m)、およびf'(n-1、m)=(f(n-1、m)+ k + 1)%nであるため、f (n、m)=(f(n-1、m)+ k + 1)%n。また、k =(m%n)-1であるため、f(n、m)=(f(n-1、m)+ k + 1)%nを取得すると、次のようになります。f(n、m)=(f (n-1、m)+ m)%n。
したがって、n = 1の場合、f(n、m)= 0
n> 1の場合、f(n、m)= [f(n-1、m)+ m]%n
この漸化式で、コードを書くことは可能ですか?上から下に再帰を使用することも、下から上に繰り返すこともできます。再帰には明らかにここでサブ問題を解決する問題はありませんが、スタック操作がたくさんあるので、反復法を直接使用することをお勧めします。反復法のソースコードについては、上記のとおりです。int last = 0;はn = 1のときのf(1、m)の値です。次のforループは、f(n、m)の値を下から上に解くことです。
このアイデアは非常に複雑ですが、コードは特に簡潔であり、主な時間は数式の分析と導出に費やされます。この方法の時間計算量はO(n)であり、空間計算量はO(1)です。時間計算量と空間計算量の両方が最初の方法よりも優れています。
public static int lastRemaining_2(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;
}
return last;
}