M-SOLUTIONS プロコンオープン 2020

コンテスト前のレート:1805
レート通りのパフォーマンスが得られる順位:493位

A - Kyu in AtCoder

問題
こういう簡単な問題でも、解説を見たら下手さに気付かされる。

思考過程
  1. if文を書くか計算するか。34個くらいまでならif文にしてもいいが、8個はだるいと思ったので計算することにする。
  2. 2000からXを引いて200で割れば、一発で求められそう。
  3. 17991800で商が変わるようにするためには、1999から引くと良さそう。
  4. それだと18001999の場合に0となるので割った後に+1してもよいが、200足して2199から引くようにしても結果は同じ。
  5. 境界値を念入りにテストして提出する。
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int x = sc.nextInt();
        sc.close();

        int ans = (2199 - x) / 200;
        System.out.println(ans);
    }
}

ここまで1分52秒+0ペナ。719位。


B - Magic 2

問題
A問題に少し時間をかけ過ぎた焦りがあったのかも。
誤読というか問題文がまともに頭に入ってきていなかった。

思考過程
  1. K個を3つに分ける組み合わせを全探索する?
  2. 制約が小さいし判定するだけなので、重複とか気にせずK^3全探索しても問題ない。
  3. AX回、BY回、CZ(0 \leq X, Y, Z \leq K)操作するとし、X+Y+Z \leq Kの場合に計算を行い、条件を満たすか判定する。
ペナルティ
  1. 計算をする部分を、A \times 2^Xではなく、A^XとしたりA^{X+1}としたりしていた。 →WA2回
  2. ループ範囲が0 \leq X, Y, Z \lt Kになっていた。 →WA1回
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();
        int c = sc.nextInt();
        int k = sc.nextInt();
        sc.close();

        for (int x = 0; x <= k; x++) {
            for (int y = 0; y <= k; y++) {
                for (int z = 0; z <= k; z++) {
                    if (x + y + z <= k) {
                        int ax = a * (1 << x);
                        int by = b * (1 << y);
                        int cz = c * (1 << z);
                        if (ax < by && by < cz) {
                            System.out.println("Yes");
                            return;
                        }
                    }
                }
            }
        }
        System.out.println("No");
    }
}

ここまで10分25秒+3ペナ。2866位。
ペナルティの影響で、C問題を通す直前には3721位まで下がったらしい。
もはや、4完でも比較的浅い傷で済むとかは期待できなそう。


C - Marks

問題
本当にこれだけでいいの?と少し不安になったが問題なかった。

思考過程
  1. i-1学期の評点は、A_{i-K}A_{i-1}の積。
    i学期の評点は、A_{i-K+1}A_iの積。
  2. よって、A_{i-K}A_iを比較すれば大小関係がわかる。
  3. A_i0もマイナスもないので、上記の比較結果がそのまま大小関係にならないようなケースはない。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] sa = br.readLine().split(" ");
        int n = Integer.parseInt(sa[0]);
        int k = Integer.parseInt(sa[1]);
        sa = br.readLine().split(" ");
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = Integer.parseInt(sa[i]);
        }
        br.close();

        PrintWriter pw = new PrintWriter(System.out);
        for (int i = k; i < n; i++) {
            if (a[i - k] < a[i]) {
                pw.println("Yes");
            } else {
                pw.println("No");
            }
        }
        pw.flush();
    }
}

ここまで13分55秒+3ペナ。1572位。


D - Road to Millionaire

問題
順位表を見たら水色でも4分程度で通している人がいたので、結構単純なのだろうと思って取り掛かった。

思考過程
  1. 例1ではわざわざ2日に分けて取引を行ったりしているが、取引を中途半端に行うよりも、必ず買えるだけ買うか、売れるだけ売るかした方が、利益or損失が増える。
  2. DPでできそうな雰囲気も感じたが、ぱっと書けそうにない。
  3. 考え方を変えて、どんな時に買ってよいかを考えると、A_i \lt A_j (i \lt j)となる場合、i日目に買ってj日目に売れば利益を出せる。
  4. ただし、A_i \gt A_k \lt A_j (i \lt k \lt j)となる場合は、i日目はスルーしてk日目に買った方が得する。
     
  5. 突き詰めていくと、買いについては、連続する2日を見たとき、1日目\gt 2日目なら、2日目に買えばよく(下がり続ける場合は単調減少が止まるところで買うことになる)、1日目\lt 2日目なら、1日目に買えばよい(2日目に売れば確実にプラスにはなる)。
  6. 売りについても逆に、下がるなら初日売りで、上がり続ける限り待つのが最適。
  7. 1日目= 2日目の場合は、判断不能だし、2日目に行動すれば同じなので何もしない。
  8. 最終日は必ず売る。
  9. 100200が交互にやってきた場合、2日で倍になるため、1000 \times 2^{40}くらいが最大値。これはlong型に収まる。
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] sa = br.readLine().split(" ");
        int n = Integer.parseInt(sa[0]);
        sa = br.readLine().split(" ");
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = Integer.parseInt(sa[i]);
        }
        br.close();

        long now = 1000; // 所持金
        long cnt = 0; // 所持株数
        for (int i = 0; i < n - 1; i++) {
            if (a[i] < a[i + 1]) {
                // 買い
                cnt += now / a[i];
                now %= a[i];
            }
            if (a[i] > a[i + 1]) {
                // 売り
                now += a[i] * cnt;
                cnt = 0;
            }
        }
        now += a[n - 1] * cnt; // 最終日:売り
        System.out.println(now);
    }
}

ここまで22分25秒+3ペナ。565位。
ペナ分の時間が過ぎた頃には1300位くらい。その時点でEとFどちらもまだ20人も解いておらず、5完はかなり難しそうだが、片方解ければ救われそうだと思っていた。


E - M's Solution

問題
解けず。集落を通るところだけが候補とすらも気付かず。
10分ほどかけて例2までを吟味していたところで、とりあえずF問題も確認しておこうと見てみたら、F問題の方ができそうな気がしたので以後放置。

思考過程
  1. _{40000}C_i全探索はよく考えなくても余裕でTLE。
  2. K=1の場合はなんとなくある程度絞れる気がしないでもないが、K \geq 2になったら何が最適なのか全然わからない。マラソンか?と思うくらい全く取っ掛かりもつかめない。(まさか本当に解説にマラソンがあるとは・・。)

 

F - Air Safety

問題
ABC168-Fで似たような実装を量産して破滅した経験を生かせているのかいないのか・・・。
回転させることで実装を1回で済ませるとかは、思いつかないわけではないけど、だいたいはコピペしてちょっと直した方が早いと思ってしまいがち。

思考過程
  1. 例1のように、向かい合っている場合は衝突する。
  2. 例3では、3番目と4番目(111 198 D、121 188 L)が衝突する。このように、進行方向が90度違う場合にも衝突する可能性がある。
  3. 2つの飛行機の組み合わせ_{200000}C_2通りを全てチェックしたのではTLEなので、なんとかしてチェックする組み合わせを減らすなりまとめてチェックするなりしたい。
     
  4. 向かい合っているパターンを考える。
    1. UとDの場合、x座標が同じ場合のみ衝突するので、まずx座標でグルーピングする。
    2. あるUの飛行機O_1については、x座標が同一のDの飛行機の中から、y座標がUより大きい中で最小のものO_2を特定し、O_1O_2とのy座標の差\times 10 / 2を求める。
    3. 上記を全てのUの飛行機について求める。
    4. 計算量は、HashMap<x座標、TreeSet<y座標>>のような形でデータを持てば、グルーピングも計算もO(NlogN)
    5. RとLの場合は、xとyを入れ替えて同様。
       
  5. 進行方向が90度違うパターンを考える。
    1. 例3のDやLの飛行機と衝突するものを増やすことを考えてみると、同じ斜め45度(y=-x+b)の直線にいる飛行機が衝突することがわかる。
    2. 同一直線上にいることの判定は、x座標とy座標の和(=b)が等しいかどうかでできそう。
    3. 向かい合いのパターンと流れは同様だが、異なるのはまずbでグルーピングするところ。
    4. それから、あるDの飛行機O_1については、bが同一のLの飛行機の中から、x座標がDより大きい中で最小のものO_2を特定し、O_1O_2とのx座標の差\times 10を求める。
       
  6. UとRの場合については、同一直線上の判定はDとLの場合と同じ。各Uについて、x座標がUより小さい中で最大のRを特定するところが異なる。
  7. DとRの場合、UとLの場合は、逆向きの斜め45度(y=x+b)の直線上にいる飛行機が衝突するので、b=y-xでグルーピングする。
  8. 各Dについてx座標がDより小さい中で最大のRを、各Uについてx座標がUより大きい中で最小のLを特定する。

コンテスト中は、ここまでのグルーピングの仕方(x+yy-xか)の考察が雑すぎて1WA。また、HashMapで十分なところをTreeMapにして、計算量にlogを余計につけていた。TLEにはならなかったが。

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] sa = br.readLine().split(" ");
        int n = Integer.parseInt(sa[0]);
        Obj[] arr = new Obj[n];
        for (int i = 0; i < n; i++) {
            sa = br.readLine().split(" ");
            Obj o = new Obj();
            o.x = Integer.parseInt(sa[0]);
            o.y = Integer.parseInt(sa[1]);
            o.u = sa[2];
            arr[i] = o;
        }
        br.close();

        int ans = Integer.MAX_VALUE;

        // 上下
        Map<Integer, TreeSet<Obj>> map1 = new HashMap<>();
        Map<Integer, TreeSet<Obj>> map2 = new HashMap<>();
        for (Obj o : arr) {
            if ("U".equals(o.u)) {
                TreeSet<Obj> set = map1.get(o.x); // グルーピング:x
                if (set == null) {
                     set = new TreeSet<Obj>((o1, o2) -> o1.y - o2.y);
                     map1.put(o.x, set);
                }
                set.add(o);
            }
            if ("D".equals(o.u)) {
                TreeSet<Obj> set = map2.get(o.x); // グルーピング:x
                if (set == null) {
                     set = new TreeSet<Obj>((o1, o2) -> o1.y - o2.y);
                     map2.put(o.x, set);
                }
                set.add(o);
            }
        }
        for (Integer x : map1.keySet()) {
            TreeSet<Obj> set1 = map1.get(x);
            TreeSet<Obj> set2 = map2.get(x);
            if (set2 != null) {
                for (Obj o1 : set1) {
                    Obj o2 = set2.higher(o1); // Uより大きい最小のD
                    if (o2 != null) {
                        int val = (o2.y - o1.y) * 5;
                        ans = Math.min(ans, val);
                    }
                }
            }
        }

        // 左右(上下の場合のxとyを入れ替え)
        map1 = new HashMap<>();
        map2 = new HashMap<>();
        for (Obj o : arr) {
            if ("R".equals(o.u)) {
                TreeSet<Obj> set = map1.get(o.y); // グルーピング:y
                if (set == null) {
                     set = new TreeSet<Obj>((o1, o2) -> o1.x - o2.x);
                     map1.put(o.y, set);
                }
                set.add(o);
            }
            if ("L".equals(o.u)) {
                TreeSet<Obj> set = map2.get(o.y); // グルーピング:y
                if (set == null) {
                     set = new TreeSet<Obj>((o1, o2) -> o1.x - o2.x);
                     map2.put(o.y, set);
                }
                set.add(o);
            }
        }
        for (Integer y : map1.keySet()) {
            TreeSet<Obj> set1 = map1.get(y);
            TreeSet<Obj> set2 = map2.get(y);
            if (set2 != null) {
                for (Obj o1 : set1) {
                    Obj o2 = set2.higher(o1); // Rより大きい最小のL
                    if (o2 != null) {
                        int val = (o2.x - o1.x) * 5;
                        ans = Math.min(ans, val);
                    }
                }
            }
        }

        // 下左
        Map<Integer, TreeSet<Obj>> mapD = make(arr, "D", true); // グルーピング:y+x
        Map<Integer, TreeSet<Obj>> mapL = make(arr, "L", true); // グルーピング:y+x
        for (Integer v : mapD.keySet()) {
            TreeSet<Obj> set = mapD.get(v);
            TreeSet<Obj> set2 = mapL.get(v);
            if (set2 != null) {
                for (Obj o : set) {
                    Obj o2 = set2.higher(o); // Dより大きい最小のL
                    if (o2 != null) {
                        int val = (o2.x - o.x) * 10;
                        ans = Math.min(ans, val);
                    }
                }
            }
        }

        // 上右
        Map<Integer, TreeSet<Obj>> mapU = make(arr, "U", true); // グルーピング:y+x
        Map<Integer, TreeSet<Obj>> mapR = make(arr, "R", true); // グルーピング:y+x
        for (Integer v : mapU.keySet()) {
            TreeSet<Obj> set = mapU.get(v);
            TreeSet<Obj> set2 = mapR.get(v);
            if (set2 != null) {
                for (Obj o : set) {
                    Obj o2 = set2.lower(o); // Uより小さい最大のR
                    if (o2 != null) {
                        int val = (o.x - o2.x) * 10;
                        ans = Math.min(ans, val);
                    }
                }
            }
        }

        // 下右
        mapD = make(arr, "D", false); // グルーピング:y-x
        mapR = make(arr, "R", false); // グルーピング:y-x
        for (Integer v : mapD.keySet()) {
            TreeSet<Obj> set = mapD.get(v);
            TreeSet<Obj> set2 = mapR.get(v);
            if (set2 != null) {
                for (Obj o : set) {
                    Obj o2 = set2.lower(o); // Dより小さい最大のR
                    if (o2 != null) {
                        int val = (o.x - o2.x) * 10;
                        ans = Math.min(ans, val);
                    }
                }
            }
        }

        // 上左
        mapU = make(arr, "U", false); // グルーピング:y-x
        mapL = make(arr, "L", false); // グルーピング:y-x
        for (Integer v : mapU.keySet()) {
            TreeSet<Obj> set = mapU.get(v);
            TreeSet<Obj> set2 = mapL.get(v);
            if (set2 != null) {
                for (Obj o : set) {
                    Obj o2 = set2.higher(o); // Uより大きい最小のL
                    if (o2 != null) {
                        int val = (o2.x - o.x) * 10;
                        ans = Math.min(ans, val);
                    }
                }
            }
        }

        if (ans == Integer.MAX_VALUE) {
            System.out.println("SAFE");
        } else {
            System.out.println(ans);
        }
    }

    static Map<Integer, TreeSet<Obj>> make(Obj[] arr, String u, boolean plus) {
        Map<Integer, TreeSet<Obj>> map = new HashMap<>();
        for (Obj o : arr) {
            if (u.equals(o.u)) {
                if (plus) {
                    o.val = o.y + o.x;
                } else {
                    o.val = o.y - o.x;
                }
                TreeSet<Obj> set = map.get(o.val);
                if (set == null) {
                     set = new TreeSet<Obj>((o1, o2) -> o1.x - o2.x);
                     map.put(o.val, set);
                }
                set.add(o);
            }
        }
        return map;
    }

    static class Obj {
        int x, y, val;
        String u;
    }
}

ABCDFと解いた時点で94分52秒+4ペナ。297位。



終結果:ABCDFの5完、114分52秒、330位、パフォーマンス1952
レート変動:1805→1820


highestを2だけ更新。
F問題は重実装やり切れて良かったが、B問題のようにお粗末過ぎるのはないようにしたい。