AtCoder Beginner Contest 006 C - スフィンクスのなぞなぞ
解説を見て、なるほど!と感心したので記しておきます。
C: スフィンクスのなぞなぞ - AtCoder Beginner Contest 006 | AtCoder
自分で解いたとき
まず、この問題を見たとき、
「AtCoder Beginners Selection」の「ABC085C - Otoshidama」を思い出しました。
こちらは、価格の違うお札が3種類あり、そのお札をトータルでN枚使って、ぴったりY円にできるかという問題です。
僕が最初に解いたときは、まんまと3重for文で「TLE」を食らってしまいましたが、
実は、最後のお札の枚数は、前の2種類のお札の枚数が決まると、自動的に1つに決まるので調べる必要がなかったのです。
そして、今回の問題も、
脚の本数が異なる3種類の人間がいて、その合計人数N人で、ぴったりM本にできるか、
という問題だったので、「ABC085C - Otoshidama」と同じように解けると考えました。
提出したソースコード
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int m = sc.nextInt(); // 大人の足のループ変数 for (int i = 0; i <= n; i++) { // 老人の足のループ変数 for (int j = 0; j <= n - i; j++) { int k = n - i - j; // 赤ちゃんの足 if (2 * i + 3 * j + 4 * k == m) { System.out.println(i + " " + j + " " + k); System.exit(0); } } } System.out.println("-1 -1 -1"); } }
結局これで「TLE」をもらってしまうのですが、
上記の「ABC085C - Otoshidama」と違うのは、制約のNの量です!
「ABC085C - Otoshidama」で入力で受け取る枚数Nは、最大 2000 でしたが、
今回の「C - スフィンクスのなぞなぞ」では、最大で 10^5 受け取ります。
同じように二重for文で実行した場合、そのループ回数は最大で、
・「ABC085C - Otoshidama」:2,000^2 = 4,000,000回(4 * 10^6)
・「C - スフィンクスのなぞなぞ」:100,000^2 = 10,000,000,000回(10^10)
となります。
ここで、
1 秒間で処理できる for 文ループの回数は、10^8 = 100,000,000 回程度
ということから、今回の問題は「ABC085C - Otoshidama」とは
同じ方法では解けないということが分かりますね。
qiita.com
(1秒間に処理できるループの回数のお話はこちら。)
ではどう解くのか?
前述の通り、今回与えられる入力値は「10^5」と大きな値であるため、
オーダーがN^2になることは許されません。
「解説」によると、この問題の解法には、
・つるかめ算を使う
・老人の数を固定する
の2パターンあるみたいです。
どちらもオーダーがNで済むように工夫されています。
そして、僕が個人的に感動したのは、後者の「老人の数の固定」です。
つるかめ算
恥ずかしながら、つるかめ算は名前しか知らなかったので、
さっきググって覚えました。
www.manabinoba.com
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int m = sc.nextInt(); // 老人(3本足)の足 for (int i = 0; i <= n && m >= i * 3; i++) { int j = 0; // 2本足の数 int k = 0; // 4本足の数 // つるかめ算 // 残りすべてが2本足の場合の、足の総数を数える int needLegs = m - i * 3; int numJ = 2 * (n - i); if (numJ >= needLegs) {// 本数が上回った場合 int over = numJ - needLegs; if (over % 2 == 0) { k = over / 2; j = (numJ / 2) - k; } else { // 奇数なら2本足と4本足で賄えないので飛ばす continue; } } else if (numJ < needLegs) {// 本数が足りない場合 int more = needLegs - numJ; if (more % 2 == 0) { k = more / 2; j = (numJ / 2) - k; } else { continue; } } if (3 * i + 2 * j + 4 * k == m && j >= 0 && k >= 0) { // 出力する順は、大人(j)→老人(i)→赤ちゃん(k) System.out.println(j + " " + i + " " + k); System.exit(0); } } System.out.println("-1 -1 -1"); } }
つるかめ算を使うことで、
老人の人数を決めたときに、大人と赤ちゃんの人数も決められるので、
必要な計算量は、老人が0人からN人までの計N+1回だけで済みました。
老人の数の固定
2本足の大人、3本足の老人、4本足の赤ちゃんがいたとき、
老人2人は計6本、大人と赤ちゃん一人ずつの2人組も計6本となります。
人数がどちらも2人で変わらず、足の本数も変わらないので、
2人の老人は、すべて大人と赤ちゃんのペアによって置き換えることができます。
例)老人50人は、大人25人と赤ちゃん25人で置き換えられる。
例)老人11人のうち10人は、大人5人と赤ちゃん5人で置き換えられる。
よって、調べる老人の人数というのは、
0人の場合と、1人の場合の2パターンしかないのです!
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int m = sc.nextInt(); // 老人2人=6本足、大人1人赤ちゃん1人の計2人=6本足 // 人数と足の本数が変わらないので、老人は0と1人のケースだけ調べれば良い for (int a = 0; a <= 1; a++) { for (int b = 0; b <= n - a; b++) { int c = n - a - b; if (3 * a + 2 * b + 4 * c == m) { System.out.println(b + " " + a + " " + c); System.exit(0); } } } System.out.println("-1 -1 -1"); } }
外側のループで、老人を0から1人までの2通り調べ、
内側のループで、大人の数を0からNまで調べます。
赤ちゃんの人数は「ABC085C - Otoshidama」と同じく、残った人数から自動的に決まります。
すると、オーダーは2*(N+1)となるので、10^8の範囲に収まります。
こういった、ひらめきや工夫で計算量を減らすタイプの解説を見ると、
「おわぁ〜〜!なるほどぉ〜〜!!!!!」ってすごく感心させられます。
この考え方を知っていれば、
もし今後、4本足、5本足、7本足の得体の知れない生き物が現れたときなんかに、
5本足が3匹は、4本足が2匹と7本足が1匹の計3匹で、同じように補えるので、
5本足は0匹、1匹、2匹の3パターンだけ調べれば良いことが分かりますね。
忘れないうちに、そんな問題お待ちしてます。