ホームに戻る
TopCoder の傾向と対策2(Java編)
0、はじめに
TopCoder の div2 の midium 以上を解くための練習問題集です。
TopCoder では問題を理解してコードにする技術が必要です。
しかし、これだけではシステムテストを通すことはできません。
例えば、1000000000個の要素を扱うときでも、
TopCoder では実行時間を2秒以内に抑えないといけません。
1000000000個のループは普通は2秒で回りません。
探索をいかに早く回すかという技術も必要になります。
ここでは探索をいかに短い時間で行うかを考えます。
まず、実行時間は2秒以内です。
ループは10000000回程度であれば回ります。
100000000回は非常にキツイと思います。
回数はループ内で行う処理の程度にもよるので、
最大のケースを入れてみて実行時間を計る必要があります。
使用できるメモリは int 型で 10000000 個程度。
20000000 個では確実にオーバーします。
メモリの制限をオーバーすると実行時に以下のエラーがでます。
java.lang.OutOfMemoryError: Java heap space
このエラーはコンパイル時には出ないので注意しましょう。
アリーナのExampleのテストを1つはしておくと良いです。
以降、例えば10の2乗は 10^2 と表記します。
1、例題1
問題
N個(最小1〜最大50)の数字がある。
1つの数字は最小1〜最大1000の範囲内にある。
また、それぞれの数字は重複することがある。
このときNからいくつか数字を取り出し、
足し算をして数字をちょうどK(最小1〜最大50000)にしたい。
ただし、1つの数字は1回のみしか使えない。
可能であるかどうかを判定せよ。
例えば2、2、5の3つの数字があり、
K=7であれば2+5=7なので可能である。
K=8であれば2+5=7でも2+2+5=9でも8にならない。
よって、K=8のときは可能でない。
考え方
まず全探索するとどうなるか考える。
1つの数字について数字を使う場合と使わない場合があるので、
数字1つにつき2通りの場合がある。
また、数字は最大50個あるので、
全探索すると 2^50 が必要になる。
これは 1125899906842624 通りに相当する。
この方法では2秒で終了させるのは無理である。
少し考えると、
全探索をして足し算しKを超えたら以降の処理をやめる、
というような処理で計算を減らせそうである。
しかし、この方法は有効で無い。
例えば、1000が50個あり、Kが50000であるとき、
足し算の結果が50000を超えることは無い。
よって、2^50 の探索が回ってしまう。
ここでメモ化を利用します。
メモ化を行うとある値からデータを高速に取り出すことができます。
例えば、100のとき3、212のとき5などの対応を記録するとします。
2個や3個のデータであれば早くアクセスできますが、
データ量が10000などと多くなってくると、
この中からデータを探すのに最大10000のループを回す必要があります。
このときにメモ化を使って1回の探索?でデータを取り出すことができます。
例えばデータのキーの値が0から100000までなどと制限されている場合、
int data[100001];
といった配列を用意します。
例えば100のとき3という対応を記録する際には、
data[100] = 3;
と記録します。
キーが100のデータを取り出す場合は data[100] を参照すれば良い。
メモ化の弱点はメモリを大量に使用するところです。
使用できるキーの値の最大値も制限されます。
では、メモ化を使ってみましょう。
まず、数字の最大の個数×Kのとり得る値の最大の配列を用意します。
Kの最大のケースは数字が50個でどの数字も1000の場合です。
よって、Kの最大値は50×1000で50000になります。
dp[51][50001] という配列を用意したとします。
では、1、3、4、7という4つの数字があった場合に、
メモをしていく過程を書いていきます。
dp[12][34] == 1 の場合は、
12個目までの要素で34を表現できることを意味します。
1つ目の数字は 1 なので、
dp[1][1] = 1; // 1つ目の数字で1は表現可能
2つ目の数字は 3 、
dp[2][3] = 1; // 3 の数字を使えば可能
dp[2][1] = 1; // 3 は使用しないが dp[1][1] == 1 なので可能
dp[2][4] = 1; // 3 を使用して dp[1][1] == 1 なので可能
3つ目の数字は 4 、
dp[3][4] = 1; // 4 の数字を使えば可能
dp[3][1] = 1; // 4 は使用しないが dp[2][1] == 1 なので可能
dp[3][3] = 1; // 4 は使用しないが dp[2][3] == 1 なので可能
dp[3][4] = 1; // 4 は使用しないが dp[2][4] == 1 なので可能
dp[3][5] = 1; // 4 を使用して dp[2][3] == 1 なので可能
dp[3][7] = 1; // 4 を使用して dp[2][1] == 1 なので可能
dp[3][8] = 1; // 4 を使用して dp[2][4] == 1 なので可能
この時点で1、3、4、5、7、8が可能であるのがわかります。
4つ目の数字は 7 、以降も同様の操作を行う。
この方法を使えば最大50×50000=2500000の探索で済む。
時間を測定してみたが最悪の場合でも0.1秒ほどで結果がでる。
サンプルコード1−1
int[] d = new int[]{1, 3, 4, 7};
int k = 12;
int[][] dp = new int[51][50001];
for(int i = 0; i < d.length; i++){
dp[i + 1][d[i]] = 1; // 現在の数字で表現できる
for(int t = 0; t <= 50000; t++){
if(dp[i][t] == 1){ // これより前の数字で表現できる
dp[i + 1][t] = 1;
}
else if(t - d[i] >= 0){ // 負の値のチェック
if(dp[i][t - d[i]] == 1){ // これまでの数字に現在の数字を足して表現できる
dp[i + 1][t] = 1;
}
}
}
}
if(dp[d.length][k] == 1){
System.out.println("YES");
}
else{
System.out.println("NO");
}
上の例では dp[51][50001] という配列を用意したが、
じつはこの問題は dp[50001] でも解ける。
サンプルコード1−2
int[] d = new int[]{1, 3, 4, 7};
int k = 12;
int[] dp = new int[50001];
for(int i = 0; i < d.length; i++){
for(int t = 50000; t > 0; t--){
if(t - d[i] >= 0){
if(dp[t - d[i]] == 1){
dp[t] = 1;
}
}
}
dp[d[i]] = 1;
}
if(dp[k] == 1){
System.out.println("YES");
}
else{
System.out.println("NO");
}
メモを1次配列で行うようにしてみた。
探索の量はサンプルコード1−1と比べて減っていない。
サンプルコード1−2はサンプルコード1−1の処理と異なる部分があり、
探索の順番や評価の順番が入れ替わっている。
サンプルコード1−2はこの順番で無いと正確な答えがでない。
省メモリを行うほど処理を行う際の制限が増える。
使えるのであればメモリは贅沢に使うべきであるが、
問題によっては省メモリをせざるを得ない場合もでてくる。
2、例題2
問題
N個(最小1〜最大50)の数字がある。
1つの数字は最小1〜最大1000の範囲内にある。
また、それぞれの数字は重複することがある。
このときNからいくつか数字を取り出し、
足し算をして数字をちょうどK(最小1〜最大50000)にしたい。
ただし、1つの数字は1回のみしか使えない。
この場合に使用する数字の個数の最小値を求めよ。
Kにするのが不可能であれば0を答えとする。
例えば1、1、2、5の4つの数字があり、
K=7であれば1+1+5=7なので数字の個数は3個である。
しかし、2+5=7で数字を2個しか使わない場合もありうる。
よって、答えは3ではなく2である。
考え方
配列は例題1と同じように用意する。
例題2が例題1と違うのは配列に入れる数値の意味である。
配列に入れる数値の意味を以下のようにする。
dp[12][34] == 5 の場合は、
12個目までの要素で34を表現するには最少で5個の数字が必要。
値の更新は次のように行う。
以前までの数字でしか表現できないのであればその個数を最小の個数として使用する。
ただ、以前の結果に現在の値を足した場合の個数がより少ない数字の個数になるのであれば、
以前の結果の個数に現在の数字の個数 1 を足した個数を最小の個数とする。
現在の数値のみで表現できるのであれば、最小の個数を 1 にする。
サンプルコード2−1
int[] d = new int[]{1, 3, 4, 7};
int k = 8;
int[][] dp = new int[51][50001];
for(int i = 0; i < d.length; i++){
for(int t = 0; t <= 50000; t++){
dp[i + 1][t] = dp[i][t];
if(t - d[i] >= 0){ // 負の値のチェック
if(dp[i][t - d[i]] > 0){
if(dp[i][t] != 0){
// これより前の数字を使う場合と現在の数字を足す場合で比較
dp[i + 1][t] = Math.min(dp[i][t], dp[i][t - d[i]] + 1);
}
else{ // これまでの数字に現在の数字を足して表現できる
dp[i + 1][t] = dp[i][t - d[i]] + 1;
}
}
}
}
dp[i + 1][d[i]] = 1; // 現在の数字のみで表現できるなら 1 である
}
System.out.println(dp[d.length][k]);
3、例題3
問題
N個(最小1〜最大50)の数字がある。
1つの数字は最小1〜最大1000の範囲内にある。
また、それぞれの数字は重複することがない。
このときNからいくつかの数字を取り出し、
足し算をして数字をちょうどK(最小1〜最大50000)にしたい。
数字はどの数字を何度使っても良いものとする。
可能であるかどうかを判定せよ。
例えば2、7、9の3つの数字があり、
K=10であれば2+2+2+2+2=10なので可能である。
考え方
全探索するとN=50のときに50^50の探索になり、
8881784197001252323389053344726562500000000000000000000000000000000000000000000000000 回
の探索を行う必要がありますが当然無理です。
メモ化を使うと50×50000=2500000で探索できます。
サンプルコード3−1
int[] d = new int[]{10, 6, 300};
int k = 5555;
int[][] dp = new int[51][50001];
for(int i = 0; i < d.length; i++){
dp[i][d[i]] = 1;
for(int t = 0; t <= 50000; t++){
dp[i + 1][t] = dp[i][t]; // 過去の数字をコピー
if(t - d[i] >= 0){ // 負の値のチェック
if(dp[i + 1][t - d[i]] == 1){ // 現在の数字を積み重ねて足していく
dp[i + 1][t] = 1;
}
}
}
}
if(dp[d.length][k] == 1){
System.out.println("YES");
}
else{
System.out.println("NO");
}
4、例題4
問題
N個(最小1〜最大50)の数字がある。
1つの数字は最小1〜最大1000の範囲内にある。
また、それぞれの数字は重複することがある。
このときNからちょうどM個(最小1〜最大N)の数字を取り出し、
足し算をして数字をちょうどK(0ではない)にしたい。
ただし、1つの数字は1回のみしか使えない。
可能であるかどうかを判定せよ。
例えば2、2、4、5、8の5つの数字があり、
M=2、K=8であるとき、
和が8なのは2+2+4=8であるが数字の個数は3個である。
また、数字1つで8があるがこれも数字を1個しか使わない。
よって、数字2個で8を表現することはできない。
考え方
M=2であるという縛りがあればNが最大の50であったとしても、
50×49=2450の探索で解ける。
ただし、最悪の場合はN=50、M=50のときで、
探索は50!になる。
50!回の探索というと
30414093201713378043612608166064768844377641568960512000000000000 回
の探索を行うことなのでとてもではないが全探索を行えない。
探索を回せるのはせいぜいM=4のときまででM=5以降は無理である。
探索する発想として、
目的の数字になるには1つの数字、3つの数字で可能という状態を記録し、
1つ数字が増えるたびに更新していくというものがある。
1、3、4、7という4つの数字があった場合に、
メモをしていく過程を書いていきます。
1つ目の数字は 1 なので、
1つの数字で1を作ることは可能
2つ目の数字は 3 なので、
1つの数字で 3 を作ることは可能
2つの数字で 4 を作ることは可能
3つ目の数字は 4 なので、
1つの数字で 4 を作ることは可能
1つの数字で 3 を作ることは可能
2つの数字で 4 を作ることは可能
2つの数字で 7 を作ることは可能
3つの数字で 8 を作ることは可能
というふうに更新をしていきます。
さて、ここで問題が発生します。
3つ目の数字の段階で、
1つの数字でも2つの数字でも 4 を作ることができます。
「1つの数字」と「2つの数字」は両方とも必要な情報です。
この先も処理を行っていくのであれば複数の情報を保持する必要があります。
この情報をビットの状態を使って保持する工夫をします。
例えば、1つの文字でも2つの文字でも 4 を作ることができるなら、
dp[3][4] に 00000000000000000000000000000011(2進数) と記録します。
下位1ビットめと下位2ビットめが1になっています。
1であれば可能である、0であれば可能でないということです。
こうすることで32個までの情報を保持することができます。
実際には50個まで記録する必要があるので int ではなく long を使用します。
この方法だと2500000の探索で収まります。
サンプルコード4−1
int[] d = new int[]{1, 3, 4, 7};
int m = 2;
int k = 14;
long[][] dp = new long[51][50001];
for(int i = 0; i < d.length; i++){
for(int t = 0; t <= 50000; t++){
dp[i + 1][t] |= dp[i][t];
if(t - d[i] >= 0){ // 負の値のチェック
if(dp[i][t - d[i]] > 0L){ // これまでの数字に現在の数字を足して表現できる
dp[i + 1][t] |= (dp[i][t - d[i]] << 1);
}
}
}
dp[i + 1][d[i]] |= 1L; // 現在の値のみを使用する 1回 を有効にする
}
if(((dp[d.length][k] >> (m - 1)) & 1L) == 1L){
System.out.println("YES");
}
else{
System.out.println("NO");
}
この方法ではN=100には対応できない。
対応するには100のサイズの配列を用意する方法がある。
他には long を2つ使って対応する方法もある。
どちらの場合も処理を追加変更する必要がありメモリや速度への配慮が必要になる。
5、例題5
問題
N個(最小1〜最大50)の数字がある。
1つの数字は最小1〜最大1000の範囲内にある。
また、それぞれの数字は重複することはない。
このときNから2つ以上の数字を選び昇順に並べたい。
昇順に並べる場合には条件があり、
2番目以降の数字はひとつ前の数字で割り切れなければならない。
並べ方は何通りあるか?
例えば1、2、3、9の4つの数字があったとき、
(1、2)(1、3)(1、9)(3、9)(1、3、9)
の5通りの並べ方がある。
考え方
単純に考えてみる。
まず昇順にソートする。
次に、数字を使うか使わないかでリストを作り、
作ったリストが割り切れる条件を満たすか調べる。
条件を満たすケースをカウントする。
この方法は 2^50 の探索が必要になる。
よって、この方法は採用できない。
次のように考えます。
例えば、数字が1、2、4、8の4つあったとする。
ここから表に書いて考えます
1つ目の選び方は以下のようです。
1| 1 (数字の1を1つだけ選んで1通り)
2| 1 (数字の2を1つだけ選んで1通り)
4| 1 (数字の4を1つだけ選んで1通り)
8| 1 (数字の8を1つだけ選んで1通り)
2つ目の選び方は以下のよう
1| 1 0(1の次に1は選べないので0通り)
2| 1 1(1→2の1通り)
4| 1 2(1→4、2→4の2通り)
8| 1 3(1→8、2→8、4→8の3通り)
3つ目の選び方は以下のよう
1| 1 0 0(1の次に1は選べないので0通り)
2| 1 1 0(2の次に2は選べないので0通り)
4| 1 2 1((1→2の1通り)→4の1通り)
8| 1 3 3((1→2の1通り)→8、(1→4、2→4の2通り)→8の3通り)
4つ目の選び方は以下のよう
1| 1 0 0 0(1の次に1は選べないので0通り)
2| 1 1 0 0(2の次に2は選べないので0通り)
4| 1 2 1 0(3の次に3は選べないので0通り)
8| 1 3 3 1(((1→2の1通り)→4の1通り)→8の1通り)
数字の並べ方は表の数字を全部足せば全通りを調べたことになります。
よって 1+1+1+1+2+1+1+3+3+1=15 通りになります。
問題では1つのみは選択できないので4つ引いて15−4=11通りになります。
(この表を使えば3つの数字からなる列という条件があっても1+3=4通りがわかる)
この方法を使用するとN=50であっても
探索は50×50×50=125000で終了します。
メモリも int 型で50×50=2500しか使用しません。
サンプルコード5−1
int[] d = new int[]{1, 2, 3, 9};
int[][] dp = new int[50][50];
for(int i = 0; i < d.length; i++){
dp[0][i] = 1;
}
for(int i = 0; i < d.length - 1; i++){
for(int t1 = 0; t1 < d.length; t1++){
for(int t2 = 0; t2 < d.length; t2++){
if(d[t1] < d[t2] && d[t2] % d[t1] == 0){
dp[i + 1][t2] += dp[i][t1];
}
}
}
}
int total = 0;
for(int i = 0; i < d.length; i++){
for(int t = 0; t < d.length; t++){
total += dp[i][t];
}
}
System.out.println(total - d.length);
6、例題6
問題
N個(最小1〜最大1000)の数字がある。
1つの数字は最小1〜最大100000000の範囲内にある。
また、それぞれの数字は重複することがない。
このときNから3つの数字を使って、
足し算をして数字をちょうどK(最小3〜最大300000000)にしたい。
使う数字は同じものを何回も使用できる。。
可能であるかどうかを判定せよ。
例えば、20000001、20000013の2つの数字があったとする。
20000001と20000013を2度使って3つの数字とする。
K=60000026であれば、
20000001+20000013+20000013=60000027
としても60000026にはならないので可能ではない。
考え方
これまでの例題と違って数字が大きい。
ただし、選ぶ数字の個数は3個なのでここは制限が緩くなっている。
全探索する場合、
1000×1000×1000=1000000000でありできない。
メモ化を行う場合、
dp[300000000] という量のメモリを必要とするがこれもできない。
よって、他の方法を試す必要がある。
ここでは探索の回数の負荷の一部をメモリに負担させる方法を考えます。
2分探索という有名な方法を使用します。
まず3つの数字の検証方法を変えます。
問題の場合であれば、
if(n1 + n2 + n3 == k)
の検証を行うのが素直なやりかたですが、
if(n1 + n2 == k - n3)
で検証を行うことにします。
まず、k - n3 の結果を配列に入れます。
配列のありえる最大サイズは1000です。
次に n1 と n2 で探索を行います。
そして n1 + n2 の結果が配列内に含まれるかどうかを調べます。
このとき2分探索を使用します。
2分探索を使用する前に配列をソートします。
java のソートはバブルソートなので nlogn ほどで終了します。
配列のサイズは1000なのでこの場合の回数は300ほどです。
ソートが終わったら2分探索を行います。
配列のサイズが1000であれば探索回数は
計算すると2^x=1000なのでx=10ほどで終了します。
よって、探索回数は最大1000×1000×10=10000000です。
これくらいなら2秒以内に回せます。
サンプルコード6−1
int[] d = new int[]{1, 2, 3, 9};
int[] array = new int[1000];
int k = 20;
for(int i = 0; i < d.length; i++){
array[i] = k - d[i];
}
Arrays.sort(array);
int ans = 0;
for(int i = 0; i < d.length; i++){
for(int t = 0; t < d.length; t++){
if(Arrays.binarySearch(array, d[i] + d[t]) > 0){
ans = 1;
}
}
}
if(ans == 1){
System.out.println("YES");
}
else{
System.out.println("NO");
}
じつはこの方法は4つの数字の足し算にも対応します。
サンプルコード6−2
int[] d = new int[]{1, 2, 3, 9};
int[] array = new int[1000000];
int k = 22;
for(int i = 0; i < d.length; i++){
for(int t = 0; t < d.length; t++){
array[1000*i+t] = k - d[i] - d[t];
}
}
Arrays.sort(array);
int ans = 0;
for(int i = 0; i < d.length; i++){
for(int t = 0; t < d.length; t++){
if(Arrays.binarySearch(array, d[i] + d[t]) > 0){
ans = 1;
}
}
}
if(ans == 1){
System.out.println("YES");
}
else{
System.out.println("NO");
}
7、例題7
問題
N個(最小3〜最大50)の数字がある。
1つの数字は最小1〜最大100000000の範囲内にある。
また、それぞれの数字は重複することはない。
ここで全ての数字を使って1つの輪を作る。
このとき隣り合う数字の差をとり絶対値をとる。
これらの絶対値の最大値が最小になる場合の値を求めよ。
例えば、3、6、9、10の4つの数字があったとする。
以下のように輪にしてみる。
3 10
6 9
このとき差の絶対値は 3、3、1、7 で最大値は7である。
では、次のように輪にしてみる。
3 9
6 10
このとき差の絶対値は 3、4、1、6 で最大値は6である。
この問題の場合では最小となる最大値は6である。
考え方
全探索はN=50のとき49!であり無理である。
ここでも例題6と同じく2分探索を使用する。
まず、あらゆる数字に対して答えが 100000000 以下になるのはわかる。
しかし、答えが 50000000 以下かどうかはわからない。
例えば、1、1000、60000000のときは 50000000 を上回る。
3、42、201 であれば 50000000 を下回るであろう。
よって、50000000 を超えるようなら 75000000 を試す。
逆に、50000000 以下なら 25000000 を試す。
これを繰り返していくと最終的には1つの値に行き着く。
最初の数値が 100000000 であれば探索は27回程度で終わる。
サンプルコードでは条件を満たすかのチェックに50×50の探索を用いているので、
すべての探索には50×50×27=67500の探索で済む。
条件を満たすかについては、まず最小値の位置を固定する。
次に、条件を満たすように次の数字を選んでいくが、できるだけ大きい数字を選ぶ。
条件を満たす数字が選べなかったら輪にする数字が用意できないことになる。
もし輪にするための数字が用意できたらその最後の数字は可能な限り小さな数字である。
この数字が最初の数字につながれば輪ができるといえる。
サンプルコード7−1
int[] d = new int[]{3, 6, 9, 10};
int min = 1;
int max = 100000000;
Arrays.sort(d);
while(min < max){
int mid = (max + min) / 2;
int index = 0;
int r_num = d[index];
Set<Integer> set = new HashSet<Integer>();
set.add(0);
for(int i = 0; i < d.length - 1; i++){
index = 0;
for(int t = 0; t < d.length; t++){
if(set.contains(t) == false){
if(Math.abs(r_num - d[t]) <= mid){
if(d[index] < d[t]){
index = t;
}
}
}
}
set.add(index); // いちど使った数字は使えないようにする
r_num = d[index]; // 現在の数字を更新
}
// すべての要素を使い かつ 最初と最後がつながるか をチェック
if(set.size() == d.length && d[index] - d[0] <= mid){
max = mid;
}
else{
min = mid + 1;
}
}
System.out.println(min);
8、例題8
問題
1からN(最小1〜最大2000000000)までの数字がある。
この全ての数字について以下のことを調べる。
条件1:各桁に0〜9を1つずつ使う。ただし、最上位桁に0は使えない。
条件2:数字は 7n+1(nは自然数)の条件を満たす。
このとき条件1と条件2の両方を満たす数字はいくつあるか数えよ。
例えば、N=2000000000のとき、
1637248509 は0〜9の数字をすべて使っている。
また、1637248509 は 7×233892644+1 である。
よって、1637248509 を条件を満たすものとして数える。
考え方
全探索であれば、1から2000000000で探索を行い、
すべての桁の数字を調べて7n+1の条件を確認する方法がある。
少し考えると条件1を満たす最小の数字は 1023456789 であるから、
1023456789 より小さい数字については考えなくても良いことがわかる。
また、条件1を満たす最大の数値は 1987654320 である。
ただ、1023456789 から探索を開始したとしても、
1987654320−1023456789+1=964197532 回
の探索回数が必要であり探索は回らない。
ここで最上位の桁の数字は1だとわかっているので、
あとは0と2〜9の数字を重複しないように並べて検証することを考えます。
数字が9個あるので探索回数は9!です。
9!というと 362880 回の探索のことです。
これなら探索が回せそうです。
順列は10!が 3628800 で 11!が 39916800 になります。
10!くらいまでなら探索が回せそうですね。
以下のサンプルコード8−1は再帰関数で順列を生成しています。
サンプルコード8−1
// n >= 1000000000 && n <= 2000000000 であること
private int f(int n, int num, int mask){
if(mask == 0x03FF){
num += 1000000000; // 順列ができたら頭に1をつける
if(num <= n && (num - 1) % 7 == 0){
System.out.println(num);
return 1;
}
return 0;
}
int count = 0;
// 各桁が0と2〜9からなる数字を作成する
for(int i = 0; i < 10; i++){
if(((mask >> i) & 1) == 1)continue;
count += f(n, num * 10 + i, mask | (1 << i));
}
return count;
}
サンプルコード8−2ではサンプルコード8−1の再帰関数を使用する。
サンプルコード8−2
int n = 2000000000;
if(n < 1000000000){ // 1000000000 より小さい数字は問題の条件を満たさない
System.out.println(0);
}
else{
System.out.println(f(n, 0, 0x02)); // 1は順列作成の要素から省く
}
9、例題9
問題
N個(最小1〜最大50000)の数字が1列に並んでいる。
1つの数字は最小1〜最大10000の範囲内にある。
また、それぞれの数字は重複することがある。
このときこの列から隣り合ういくつかの数字を取り出す。
取り出したすべての数字の和がK(最小1〜最大500000000)になるとき、
取り出す数字の個数を求めよ。
すべての数字を選んでもKにならない場合は0を答えとする。
例えば、3、10、3、5の5つの数字があったとする。
K=16のとき、
数字を1つ選ぶやりかたは3、10、3、5の4通りだが16にはならない。
数字を2つ選ぶやりかたは(3、10)(10、3)(3、5)の3通りだが、
和が13、13、8でありいずれも16にはならない。
数字を3つ選ぶやりかたは(3、10、3)(10、3、5)の2通り。
この場合は和が16、18で16のとき条件を満たす。
数字を4つ選ぶやり方は(3、10、3、5)の1通り。
和は21となり16にはならない。
このとき(3、10、3)を選ぶやりかたが和で16になる。。
考え方
数字の個数が最大の50000のとき全探索は
50000×(50000+1)/2=1250025000 になる。
ここでは数字が1列で動かせず隣り合った範囲でしか数字を選べないことを利用する。
まず左端と右端を用意し共に0とする。
右端を1つずつ移動し合計に右端で増えたぶんを加算する。
もし合計がKであれば、
右端から左端までの要素数を数えて記録する。
続けてもし合計がK以上であれば、
左端を1つ右に動かし合計から左端で減ったぶんを減算する。
再び右端を1つずつ移動する操作に戻る。
この操作を最右端まで行う。
なんと探索は数字の個数が最大の50000であれば50000回程度で終わる。
サンプルコード9−1
int[] d = new int[]{3, 10, 3, 5};
int k = 16;
int ans = Integer.MAX_VALUE;
int sum = 0;
int left = 0, right = 0;
while(true){
while(right < d.length && sum < k){
sum += d[right];
right++;
}
if(sum < k){
break;
}
if(sum == k){
ans = Math.min(ans, right - left);
}
sum -= d[left];
left++;
}
if(ans == Integer.MAX_VALUE){
System.out.println(0);
}
else{
System.out.println(ans);
}
10、例題10
問題
N個(最小1〜最大50)の数字が書かれたマスが1列に並んでいる。
1つの数字は最小1〜最大49の範囲にある。
また、それぞれの数字は重複することがある。
自分は最初にいちばん左端のマスに立っている。
1回の動作でできることは
右に1マス進む、左に1マス進む、右に現在のマスの数字の数だけ進む、
のいずれかを好きなように選択できる。
ただし、移動する際に左右の端を出ることはできない。
最小の回数でいちばん右端のマスまで移動したい。
最小の回数を求めよ。
例えば、「4、1、3、6、3、5、2、4、1、7」のマスがあったとする。
右端に移動するのであるから数字の数だけ移動すれば良いようであるが、
4→3→ と4つ右へ移動、3つ右へ移動とすると、
次の4は右端を越えてしまうので1しか進めない。
1る右のマスは1なので1つ右に移動して右端に到着する。
よって 4→3→4→1→7 の4回移動になる。
しかし、3から左に1つ戻れば4→3→6→7 と移動して3回である。
考え方
単純に考えてみる。
最初のマスで右に1進むかマスの数字だけ進むことの両方を考える。
1回をカウントし、現在のマスにフラグを立てる。
次のマスでは1進むかマスの数字だけ進むか1つ戻るか考える。
1つ前のマスにフラグが立っていれば戻ることはできない。
すべての試行をおこなったら1回をカウントしマスにフラグを立てる。
この操作を繰り返して右端に到着したときの回数の最小を答えとする。
サンプルコード10−1に探索用に再帰関数を書いてみた。
サンプル10−2からこの関数を使っている。
さて、この場合マスの数字がすべて 1 で30個あったとしよう。
このときの探索数は2^29になる。
これは5億ほどの探索になるので探索は無理だといえる。
そこで探索方法を次のように考える。
スタート地点を0とする。
次に1回で行ける地点に1を記録する。
スタート地点から行けるすべての地点へ分身して移動する。
このとき行ける地点の数字が最小のものから次の操作を行う。
この場合は数字はすべて1のはずだから順番はどれでも良い。
すでに行った地点は除いて次に行けるすべての地点に
現在の地点の数字+1 を記録する。
もし記録する地点の数字がすでにあったなら、
すでにある数字と現在の地点の数字+1で小さいほうを記録する。
次に、すでに行った地点は除いて現在の地点から行ける地点へ分身して移動する。
行ける地点の数字が最小のものから上と同様の操作を行う。
このとき現在の数字は1の場合と2の場合があると思うので、
1の場合を2の場合より優先して先に行う必要がある。
これをひたすら繰り返す。
全操作が終了したときにその地点の数値がその地点へ行く最短回数である。
この方法をダイクストラ法という。
ある地点からある地点へいく最短距離を求めることができる。
この方法はたとえば地点間の距離が違っても対応できる。
ただし、地点間の数値がマイナスであるとうまく動作しない。
マイナスがある場合はベルマンフォード法を使う。
サンプルコード10ー3ではデータ構造にグラフを使って
ダイクストラ法で最短経路を求めています。
この方法ではマスの数字がすべて 1 で30個 の場合、
1000回ほどの探索で答えを出しました。
サンプルコード10−1
int min = Integer.MAX_VALUE;
void f(int[] d, int[] flag, int index, int count){
if(index < 0 || index >= d.length)return;
if(index == d.length - 1){
min = Math.min(min, count);
return;
}
int[] f = new int[flag.length];
for(int i = 0; i < f.length; i++){
f[i] = flag[i];
}
f[index] = 1;
f(d, f, index + 1, count + 1);
f(d, f, index + d[index], count + 1);
if(index != 0 && f[index - 1] != 1){
f(d, f, index - 1, count + 1);
}
}
サンプルコード10−2
int[] d = new int[]{4, 1, 3, 6, 3, 5, 2, 4, 1, 7};
int[] flag = new int[d.length];
f(d, flag, 0, 0);
System.out.println(min);
サンプルコード10−3
int[] d = new int[]{4, 1, 3, 6, 3, 5, 2, 4, 1, 7};
int[] flag = new int[d.length]; // その地点へ行ったかどうかのフラグ
int[] ans = new int[d.length]; // その地点へ行く最小回数が入る
Arrays.fill(ans, Integer.MAX_VALUE);
ArrayList<ArrayList<Integer>> l = new ArrayList<ArrayList<Integer>>();
// グラフを作る
for(int i = 0; i < d.length; i++){
l.add(new ArrayList<Integer>());
if(i > 0){
l.get(i).add(i - 1);
}
if(i < d.length - 1){
l.get(i).add(i + 1);
}
if(i < d.length - d[i]){
l.get(i).add(i + d[i]);
}
}
ans[0] = 0; // スタート地点を0にする
while(true){
int r = -1;
// まだ行っていない場所で回数が最小の場所を検索
for(int i = 0; i < d.length; i++){
if(flag[i] == 0 && ((r == -1) || (ans[i] < ans[r]))){
r = i;
}
}
if(r == -1)break;
flag[r] = 1;
// そのままの場合と回り道した場合の小さいほうを採用する
for(int i = 0; i < l.get(r).size(); i++){
ans[l.get(r).get(i)] = Math.min(ans[l.get(r).get(i)], ans[r] + 1);
}
}
System.out.println(ans[d.length - 1]);
11、例題11
問題
N個(最小1〜最大50)の数字がある。
1つの数字は最小1〜最大1000の範囲内にある。
また、それぞれの数字は重複することがない。
ここから2つの数字をとりだして次の条件をチェックする。
条件:2つの数字の和が素数である。
この条件を満たす2つの数の組み合わせ数を最大にしたい。
ただし、このとき同じ数は2回使えない。
このときの最大の組み合わせ数を求めよ。
例えば、1、2、4、5、7、10、11 の7つの数字がある。
1+10=11、2+11=13、4+7=11
このとき最大の組み合わせは3個できることになる。
考え方
数字が50個であれば50!で組み合わせを探索できる。
ただし、50!の探索は回らない。
まず、次のように考える。
最小の和は1+2 のとき3である。
よって、最小の素数は3であり2にはならない。
よって、和でできる素数は必ず奇数である。
したがって、和の組み合わせは必ず 奇数+偶数 である。
(偶数+偶数、奇数+奇数はいずれも偶数になる。)
ただし、奇数+偶数をすれば必ず素数になるわけではない。
よって、奇数群と偶数群で素数になる可能性のあるペアを登録する。
このとき最大数のペアを作ることを考えればよい。
では、最大数のペアを作るにはどうするか?
例えばE1、E2、O1、O2について次の関係が成り立つとする。
E1はO1、O2とペアを組める。
E2はO1のみとペアを組める。
線でつなぐと以下のようになる
E1ーO1
X
E2 O2
つなげることができるものからつないでいくと
E1とO1をつないだ時点でE2がペアを作れなくなる。
これではE1とO2、E2とO1という最大数のペアにならない。
このときに次のような操作が可能である。
「使用していない左→右への移動」と「使用している左→右の移動の逆行」
によって左から右へ移動する新しいルートが存在する場合、
すでにできているペアを解消して新たなペアに更新することができる。
上の例でやってみる。
E1ーO1
X
E2 O2
まず、E1→O1はペアを作ることができる。
次に、E2→O1は使用していない左→右の移動なので移動できる。
O1→E1は使用しているE1→O1の逆行なので移動できる。
E1→O2は使用していない左→右の移動なので移動できる。
よって、E2→O1→E1→O2の移動が可能であり、
このときペアの成立を反転させる。
すなわち、E2とO2がペアとなり、O1とE1がペア解消、
また、E1とO2がペアになる。
この例は2つと2つのペアなので非常に単純であるが、
この方法はどんな複雑なペアマッチングの場合でも正しく動作する。
サンプルコード11−1は素数判定の関数
サンプルコード11−2はマッチング用の再帰関数
サンプルコード11−3はグラフの作成と再帰関数の呼び出し
サンプルコード11−2は
互いにマッチング相手がいなければマッチングし、
マッチング相手がいれば逆行の探索を繰り返し、
逆行の試行が可能であれば再マッチングを行う。
1つの再帰関数で2つの
サンプルコード11−1
// n が素数であれば 1 を返す そうでなければ 0 を返す
private int isPrime(int n){
int i;
if(n < 2){
return 0;
}
else if(n == 2){
return 1;
}
if(n % 2 == 0){
return 0;
}
for(i = 3; i * i <= n; i += 2){
if(n % i == 0){
return 0;
}
}
return 1;
}
サンプルコード11−2
ArrayList<ArrayList<Integer>> l;
int[] match; // マッチング相手を記録する なければ -1
int[] flag; // すでに調べたかどうかを記録するフラグ
int f(int v){
flag[v] = 1;
for(int i = 0; i < l.get(v).size(); i++){
int d = l.get(v).get(i); // v の相手を d とする
int dm = match[d]; // d のマッチング状態を dm とする
if(dm == -1 || (flag[dm] == 0 && f(dm) == 1)){
match[v] = d;
match[d] = v;
return 1;
}
}
return 0;
}
サンプルコード11−3
int[] d = new int[]{1, 2, 4, 5, 7, 10, 11};
flag = new int[d.length];
match = new int[d.length];
l = new ArrayList<ArrayList<Integer>>();
Arrays.fill(match, -1);
for(int i = 0; i < d.length; i++){
l.add(new ArrayList<Integer>());
for(int t = 0; t < d.length; t++){
if(i == t)continue;
if(isPrime(d[i] + d[t]) == 1){
l.get(i).add(t);
}
}
}
int count = 0;
for(int i = 0; i < d.length; i++){
if(d[i] % 2 == 0 && match[i] == -1){
Arrays.fill(flag, 0);
count += f(i);
}
}
System.out.println(count);