【AtCoder Beginner Contest 152 C問題】TLEについてと、printデバッグ位置のコツ


printデバッグを入れる位置で混乱していませんか?

お久しぶりです。宇宙意志です。

今回はいつものように書き捨てのSeleniumではなく、ABC152-C問題を見ていきたいと思います。別にリングフィット購入の自動転売で大儲けしようとしたのにLambdaが動かなくてキレていた訳ではないです。

今回の趣旨ですが考え方とデバッグの位置を說明です、まだTLE(時間切れ)で解けていません。

問題の概要

1.N個の順列がバラバラに並んでいます
2.前からiを走査させます
3.走査中のiよりも手前にある順列全て(ここではjと定義されてます)と比較して、j!>=i! j>=iが常に成立するならi 1つに付き1カウント
4.条件が成立するiの合計カウントを求める

という問題です。

解法の考え方

1.まずこの問題を聞いた時に、2重for文を使って配列のiの位置を記憶させて、
 jを配列として左端からiの位置まで走査すれば良いな
、というのはすぐに思いつくと思います。

2.順列の並べ替え
 ここもすぐに気付かなければなりません。この問題はタイムアウトを狙っています。ですから、
 順列は常に n >= m の時常に n! >= m! が成立するので、n>mの確認をすれば順列を計算する必要はないと分かります。
 これで問題は単なる大小比較になりました。ここで順列の計算を始めると大量にTLE(時間切れ)が出てしまいます。

 @mui_nyan さまからコメントでご指摘が合ったためyoutubeの解説動画で確認しました。順列という言葉に
 特に意味はなく、普通の順番のランダムの数値という事のようです。申し訳ございませんでした。
 (2020/03/24 15:56 修正)

 また、j用の配列を左端からiまでを走査する時、途中で条件不成立ならbreakさせよう。(TLEを避けるため)
 というのも思いつきます。

3.いつカウントを加えればいいの?
 これについて考えなければなりません。
 jの配列を全て比較し終わった時に条件不成立ならcount++したい。という事は、
 j用のfor分をぐるぐる回してバターになっている時はcountはいじれない。
 最後に加えたい。という事は、フラグ(良い名前をつけましょう)を使って保持して、
 for文が終わった時点でcount++するかしないか判定すれば良い。

 
 ここまで分かれば解けたも同然です。
 解けたも同然というのは内心的効果意思であって、解けたという事実を示したものでありません。(民法)

以下、コメントを入れたソースを見ていきましょう。

import java.io.*;
import java.util.*;

public class Main {

    void submit() {

        int N = nextInt();
        int[] P = new int[N];

        for(int i=0; i<N; i++){
            P[i] = nextInt();
        }

上は入力値を入れてるだけですね。順列にする必要がないのは上記で説明した通りです。

        int total = 1;

        if(N==1){
            out.println(total);
            return;
        }

最初に与えられた総数が1の時は特別で、i,jと2重配列を作りようがないのです。
こういう所で無理してネストを増やしてがんばってはいけません。
ソースを見やすくします。最初に貰った数値が1つだけの時はreturnして終了させて、ネストを減らします。

※以下のソースコードは解法の追記で書き直しています

        for(int i=1; i<N; i++){
            /*** big_flgとかいう分かりにくい名前はやめて、リーダブルコードを読みましょう!!! ***/
            boolean big_flg= true;

            /*** jを走査しています。i以下を比較する配列なので、初期位置は int j=0になります ***/
            for(int j=0; j<=i; j++){

                /*** 今回見て欲しいデバッグ部分1です。比較対象になる整数を全て出力しています。 ***/
                out.println("P[i]: "+ P[i]);
                out.println("P[j]: "+ P[j]);

                if(P[i]<=P[j]){
                    big_flg = true;
                }else{
                    big_flg = false;
                    /*** 計算量を減らすために、駄目だと分かり次第breakして2重ループを抜けて次のiに進みます。
               ちなみにcontinueも処理とネスト減らしの為によく使います ***/
                    break;
                }
            }

大体コメントに書いたのですが、for文の2重配列で比較しているだけです。で、問題は、「エラーが出た時どうすれば良いのかわからないよドラえもん!」という方です。

今回記載しているように、常にデバッグ値は急いでいても、「"名前:数値 」という形で出してあげましょう。

out.println("P[i]: "+ P[i]);
out.println("P[j]: "+ P[j]);

このように書く事で、いわゆるprintデバッグの精度を上げる事が出来ます。

            /*** 今回見て欲しいデバッグ部分2です。結局出てきた時のフラグはどっちなの? 成立? 不成立?を見ています ***/
            out.println("big_flg: "+ big_flg);                    

            if(big_flg){
                total++;
            }

        }
        out.println(total);
    }

で、のび太さんはまたデバッグする時にどこでデバッグするの? どの位置でフラグの値を知りたいの?という事が重要です。
これに関しては一言でいうと、if文の前に入れましょう。大体if文に入る前に間違ってます。

printデバッグはどこに入れれば良いのか

二重for文で実装時に間違えるケースはそんなに多くなくて、

1.forの境界値で間違えてる
2.if文の条件が間違えてる
3.if文に入るフラグが間違えてる

の3つです。1.2は余程複雑な条件でなければすぐに気付きます。
そもそも複雑な条件は作ってはいけません。
だから3です。C問題が解けない方は、3のデバッグの仕方を鍛えましょう、と言っています。

以下はトップコーダーのパクリなのでおまじない。気にしません。わたしのソースにSystem.out.println()ではなく、
out.println()で出力しているのはこのおまじないによります。

    void preCalc() {

    }

    void stress() {

    }

    void test() {

    }

    Main() throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        out = new PrintWriter(System.out);
        preCalc();
        submit();
        //stress();
        //test();
        out.close();
    }

    static final Random rng = new Random();

    static int rand(int l, int r) {
        return l + rng.nextInt(r - l + 1);
    }

    public static void main(String[] args) throws IOException {
        new Main();
    }

    BufferedReader br;
    PrintWriter out;
    StringTokenizer st;

    String nextToken() {
        while (st == null || !st.hasMoreTokens()) {
            try {
                st = new StringTokenizer(br.readLine());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return st.nextToken();
    }

    String nextString() {
        try {
            return br.readLine();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    int nextInt() {
        return Integer.parseInt(nextToken());
    }

    long nextLong() {
        return Long.parseLong(nextToken());
    }

    double nextDouble() {
        return Double.parseDouble(nextToken());
    }
}

まとめたソースは以下。

ちなみにこれでもTLEになりますた😭 ボスケテ

abc152_c.java
import java.io.*;
import java.util.*;

public class Main {

    void submit() {

        int N = nextInt();
        int[] P = new int[N];

        for(int i=0; i<N; i++){
            P[i] = nextInt();
        }

        int total = 1;

        if(N==1){
            out.println(total);
            return;
        }

        for(int i=1; i<N; i++){
            boolean big_flg= true;
            for(int j=0; j<=i; j++){
                //out.println("P[i]: "+ P[i]);
                //out.println("P[j]: "+ P[j]);
                if(P[i]<=P[j]){
                    big_flg = true;
                }else{
                    big_flg = false;
                    break;
                }
            }
            //out.println("big_flg: "+ big_flg);                    
            if(big_flg){
                total++;
            }
        }
        out.println(total);
    }

    void preCalc() {

    }

    void stress() {

    }

    void test() {

    }

    Main() throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        out = new PrintWriter(System.out);
        preCalc();
        submit();
        //stress();
        //test();
        out.close();
    }

    static final Random rng = new Random();

    static int rand(int l, int r) {
        return l + rng.nextInt(r - l + 1);
    }

    public static void main(String[] args) throws IOException {
        new Main();
    }

    BufferedReader br;
    PrintWriter out;
    StringTokenizer st;

    String nextToken() {
        while (st == null || !st.hasMoreTokens()) {
            try {
                st = new StringTokenizer(br.readLine());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return st.nextToken();
    }

    String nextString() {
        try {
            return br.readLine();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    int nextInt() {
        return Integer.parseInt(nextToken());
    }

    long nextLong() {
        return Long.parseLong(nextToken());
    }

    double nextDouble() {
        return Double.parseDouble(nextToken());
    }
}

という訳で、今回は以上です。

最期に

java8で回答された優秀な方の解法がたんまりありますので、table右側の詳細で見てみて下さい。
https://atcoder.jp/contests/abc152/submissions?f.Task=abc152_c&f.Language=3016&f.Status=AC&f.User=

今回は以上になります。
・デバッグの位置に気を遣おうね
・名前は表示しようね

という2点でした。

それでは、また会いましょう。

解法の追記(2020/03/24 15:57)

数時間ぶりにお久しぶりです。宇宙意志です。
@swordone さまより (i番目までの最小値)>(i+1番目の値)の個数を数えれば済む というアドバイスを頂きましたので、
解説動画も見て確認しました。上記の手法だとTLEしないという事です。という事で実装しました。

min を保持して、そこから走査すれば計算量が減らせるという事です。以下のようになります。

        int min=P[0];
        for(int i=1; i<N; i++){
            if(min >= P[i]){
                min = P[i];
                count++;
            }
        }
        out.println(count);
    }

大分スッキリしましたね! これがパッと思いつけば良いんですが、本番では工夫せずに素直に実装してTLEを出してしまうのが筆者の実力という事でしょうか……ぴえん😭

という事で、コメント頂いたお二人の方、ありがとうございました!

追々記(2020/03/24 20:30)

java用にソースのシンタックスハイライト(今知った)を修正して頂きました! @altさま、ありがとうございます!😭