第二回 アルゴリズム実技検定

はじめに

プログラミング関連では初めて記事を書きます。ks2mです。
2019年1月にAtCoderを始め、緑色最下層くらいから徐々にレートを上げて現在青色。
黄色以上の人の記事と比べ、解法の正当性やコードの簡潔さなど劣ると思いますが、解いたときの実際の思考過程も踏まえつつ、飛躍し過ぎない内容が書ければいいなと思っています。
ソースコードの言語はJavaです。

A - エレベーター

問題
どうするのが一番楽なのかちょっと悩んだが、時間をかけすぎても仕方ないので適当にやった。
一番楽な方法がぱっと出てくるようにしたい。。

問題概要

B9, B8, ... , B1, 1F, 2F, ... , 9Fの18のフロアを持つ建物がある。
この18フロアの内、異なる2つのフロアを表す文字列STが与えられるので、その距離を出力せよ。

考察
  1. STをそれぞれ数値化して差を取りたいと思う。
  2. しかし、数値部分が1文字目だったり2文字目だったりで、条件分岐しつつ数値化するのはなんかめんどくさそう?
  3. べた書き過ぎて嫌だが、"B9"~"9F"をその順に持った文字列の配列を作り、一致するインデックスを取得した。
import java.util.Scanner;

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

        String[] a = {
                "B9",
                "B8",
                "B7",
                "B6",
                "B5",
                "B4",
                "B3",
                "B2",
                "B1",
                "1F",
                "2F",
                "3F",
                "4F",
                "5F",
                "6F",
                "7F",
                "8F",
                "9F",
                };

        int si = 0;
        int ti = 0;
        for (int i = 0; i < a.length; i++) {
            if (a[i].equals(s)) {
                si = i;
            }
            if (a[i].equals(t)) {
                ti = i;
            }
        }
        System.out.println(Math.abs(si - ti));
    }
}
考察2

後からよく考えたら、数値化するのそんなに大変でもなかった。

  1. "B"→"-"、"F"→空文字に置換すると、数値変換可能になる。
  2. B1(-1)と1F(1)の間が2になるので、マイナスの場合+1するか、プラスの場合-1する。
import java.util.Scanner;

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

        int si = toInt(s);
        int ti = toInt(t);
        System.out.println(Math.abs(si - ti));
    }

    static int toInt(String s) {
        s = s.replaceAll("B", "-").replaceAll("F", "");
        int ret = Integer.parseInt(s);
        if (ret < 0) {
            ret++;
        }
        return ret;
    }
}

B - 多数決

問題
これはさすがにやるだけと言ってもいいかな。

問題概要

'a', 'b', 'c'からなる文字列Sが与えられる。
最も多く含まれる文字は3つの内どれかを出力せよ。

  • 1 \le |S| \le 1000
  • 最多の文字は1種類
考察
  1. それぞれの文字をカウントする。これは'a'を引けば0~2になるので、カウントの格納先は変数3つより長さ3の配列の方が楽そう。
  2. 3つの数値を比較し、最大数に対応する文字を出力する。こちらは文字種がもっと多ければfor文を使うが、3つ程度であれば汚いけどif文並べた方が早いと思った。
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        char[] s = sc.next().toCharArray();
        sc.close();

        int[] cnt = new int[3];
        for (int i = 0; i < s.length; i++) {
            cnt[s[i] - 'a']++;
        }

        char ans = 'a';
        int max = cnt[0];
        if (max < cnt[1]) {
            ans = 'b';
            max = cnt[1];
        }
        if (max < cnt[2]) {
            ans = 'c';
        }
        System.out.println(ans);
    }
}

C - 山崩し

問題
なんか一気に難しくなった気がする。まず問題文が長くて辛い。
しかし問題を把握すれば書いてある通りにやるだけなので、ある意味第一回のC(3番目)と比べてやり方を考える必要はない?

問題概要

N、横2N-1のマス目の中に、1つ以上の 'X' を含む山型の '#' がある。
以下のように、下から順に、初期状態が '#' の箇所について、左下、真下、右下のいずれかが 'X' ならば 'X' に塗り替えていった後の状態を出力せよ。

....#....     ....X....
...###...     ...XXX...
..#####..  →  ..XXXXX..
.####X##.     .XX##X##.
#X#######     #X#######
  • 2 \le N \le 50
考察
  1. 入出力例がそのように見えたので、「各 'X' の左上、真上、右上を 'X' に塗り替える」といつの間にか読み替えていた。
  2. 下の段から順に、マス(i, j)が'X'ならば、マス(i-1, j-1)(i-1, j+1)を'X'にする。
  3. ただし、書き換えるマスが配列内かつ '#' であること。

※後から思えば、余計な読み替えをしなければ配列外参照に気を遣う必要はなかった。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        char[][] s = new char[n][];
        for (int i = 0; i < n; i++) {
            s[i] = sc.next().toCharArray();
        }
        sc.close();

        // N段目~2段目の'X'を探す
        for (int i = n - 1; i > 0; i--) {
            for (int j = 0; j < s[i].length; j++) {
                if (s[i][j] == 'X') {
                    // 元が'X'の箇所の上3箇所(配列外参照対策込み)
                    for (int j2 = Math.max(0, j - 1); j2 < Math.min(s[i].length, j + 2); j2++) {
                        if (s[i - 1][j2] == '#') {
                            s[i - 1][j2] = 'X';
                        }
                    }
                }
            }
        }
        // 結果出力
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < s[i].length; j++) {
                System.out.print(s[i][j]);
            }
            System.out.println();
        }
    }
}

D - パターンマッチ

問題
やっぱり前回のD問題よりかなり大変。

問題概要

英小文字からなる文字列Sが与えられる。
英小文字または '.'からなる長さ1~3の文字列全てのうち、Sの連続する部分文字列であるものの個数を求めよ。('.' は任意の1文字に相当)

  • 1 \le |S| \le 100
考察
  1. 全ての文字列を実際に作って条件を満たすか試す。
  2. 文字列の数が27^3くらい(もう少し多いけど)、それぞれの判定に100 \times 3くらい。計算量は大丈夫そう。
  3. 実装を三重ループにするか再帰にするか。まあたかだか3回なら三重ループでいいかと思った。
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        char[] s = sc.next().toCharArray();
        sc.close();

        int ans = 0;
        char[] t = new char[3];
        for (int i = 0; i < 27; i++) {
            if (i < 26) {
                t[0] = (char) ('a' + i);
            } else {
                t[0] = '.';
            }
            if (isOk(s, t, 1)) {
                ans++;
            }
            for (int i2 = 0; i2 < 27; i2++) {
                if (i2 < 26) {
                    t[1] = (char) ('a' + i2);
                } else {
                    t[1] = '.';
                }
                if (isOk(s, t, 2)) {
                    ans++;
                }
                for (int i3 = 0; i3 < 27; i3++) {
                    if (i3 < 26) {
                        t[2] = (char) ('a' + i3);
                    } else {
                        t[2] = '.';
                    }
                    if (isOk(s, t, 3)) {
                        ans++;
                    }
                }
            }
        }
        System.out.println(ans);
    }

    static boolean isOk(char[] s, char[] t, int k) {
        label:
        for (int i = 0; i <= s.length - k; i++) {
            for (int j = 0; j < k; j++) {
                if (t[j] != '.' && s[i + j] != t[j]) {
                    continue label;
                }
            }
            return true;
        }
        return false;
    }
}


さすがにちょっとひどいので再帰版も。

import java.util.Scanner;

public class Main {
    static int ans = 0;
    static char[] s;

    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        s = sc.next().toCharArray();
        sc.close();

        dfs(new char[3], 0);
        System.out.println(ans);
    }

    static void dfs(char[] t, int idx) {
        if (idx == 3) {
            return;
        }
        for (int i = 0; i < 27; i++) {
            if (i < 26) {
                t[idx] = (char) ('a' + i);
            } else {
                t[idx] = '.';
            }
            if (isOk(s, t, idx + 1)) {
                ans++;
            }
            dfs(t, idx + 1);
        }
    }

    static boolean isOk(char[] s, char[] t, int k) {
        // 三重ループ版と同内容
    }
}

E - 順列

問題
CやDより簡単では。

問題文(原文のまま)

1, 2, ... , Nの順列A_1, A_2, ... , A_Nが与えられます。
各整数1 \le i \le Nに対して、次の条件を満たす1以上の整数jとして考えられる最小の値を求めてください。

  • x=iとする。xA_xで置き換えるという操作をちょうどj回行った後、x=iとなる。
  • 1 \le N \le 100
  • 1 \le A_i \le N
  • A_i \ne A_j (i \ne j)
  • 入力は全て整数
考察
  1. とりあえずそのままやることを考える。
  2. x=iで初期化する。
  3. x \ne iの間、xa[x]を代入し続け、ループ回数をカウントする。
  4. 0回になってしまうので、do-while文に変更して1回以上回るようにした。初期値をx=a[x]とし、カウント1からスタートしてもよかった。
  5. スペース区切りは末尾に余計なスペースが付かないようにするのが面倒。余計なスペースがあっても改行区切りでも通るのかもしれないけど、一応ちゃんとやった。
import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextInt() - 1;
        }
        sc.close();

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            int j = 0;
            int x = i;
            do {
                x = a[x];
                j++;
            } while (x != i);
            sb.append(j).append(' ');
        }
        sb.deleteCharAt(sb.length() - 1);
        System.out.println(sb.toString());
    }
}
考察2

解説PDFの最後の2行について。
例えば例1の場合、「1→1→・・・」「2→3→2→3→・・・」「4→5→6→4→5→6→・・・」のようにグループごとに一定周期でループするので、同じグループ内の値は同一周期(同一回数)となる。

import java.util.Scanner;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextInt() - 1;
        }
        sc.close();

        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            if (ans[i] == 0) {
                int j = 0;
                int x = i;
                do {
                    x = a[x];
                    j++;
                } while (x != i);

                do {
                    x = a[x];
                    ans[x] = j;
                } while (x != i);
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            sb.append(ans[i]).append(' ');
        }
        sb.deleteCharAt(sb.length() - 1);
        System.out.println(sb.toString());
    }
}

N=100程度では実行時間全然変わらず。

F - タスクの消化

問題
誤読というか勘違いして時間かかってしまった。
茶色の半分以上が解けたらしいけど、非AtCoderユーザで初級以上は9/71人しかいないし、やっぱり難しめ?
というか、自分の場合はQueue系のコレクションは競プロ以外でほとんど使ったことなかったし、同じように馴染みのない人が多かったのかも?

問題概要

N個のタスクがある。i個目のタスクはA_i日目以降に消化することができ、消化することでB_iポイントを得られる。
1日目からN日目まで、毎日1つずつタスクを消化するとき、1 \le k \le Nの全ての整数kに対して、k日目までに得られるポイントの合計の最大値を求めよ。

  • 1 \le N \le 2 \times 10^5
  • 1 \le A_i \le N
  • 1 \le B_i \le 100
  • 入力は全て整数
  • k日目までに実行可能なタスクはk個以上存在する
考察
  1. 日付順に消化可能キューに追加していくことになるので、ABをセットにした上で、Aの昇順にソートする。
  2. k日目の処理としては、A_i \le kの要素を全部消化可能キューに追加した上、B_iの降順にk個取り出した合計を出力する? →k日目のタスクが複数選ばれるのは駄目だった。O(N^2)にならないように無駄に苦労して実装したけどWA。
  3. k日目の処理としては、A_i=kの要素を新たに消化可能キューに追加し、キューからB_iが最大のものを1つ取り出し、これまでの合計に足すだけでよかった。
  4. 制約が20万なので入出力を高速化。*1
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.PriorityQueue;

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

        // aの昇順でソート
        Arrays.sort(arr, (o1, o2) -> o1.a - o2.a);

        // 降順に取り出す優先度付きキュー
        PriorityQueue<Integer> que = new PriorityQueue<>((o1, o2) -> o2 - o1);

        int ans = 0;
        int idx = 0;
        PrintWriter pw = new PrintWriter(System.out);
        for (int k = 1; k <= n; k++) {
            // k日目のタスクを追加
            while (idx < n && arr[idx].a <= k) {
                que.add(arr[idx].b);
                idx++;
            }
            // 最大のものを1つ取り出し
            ans += que.poll();
            pw.println(ans);
        }
        pw.flush();
    }

    static class Obj {
        int a, b;
    }
}

G - ストリング・クエリ

問題
連続してキューを使用する問題。F問題と違って前から順に処理すればいいが多少の計算が必要。

問題概要

初め空文字列である文字列Sに対してQ回の操作を行う。
i回目の操作の種類をT_iとし、内容は以下の通り。

  • T_i=1のとき
    • Sの末尾に英小文字C_iX_i文字追加する。
  • T_i=2のとき
    • Sの先頭からD_i文字削除する。
    • この操作で削除された'a' ~ 'z'の各文字数の2乗の和を出力する。
  • 1 \le Q, X_i, D_i \le 10^5
  • 1 \le i \le Q
  • T_i=1または2
  • C_iは英小文字、それ以外は整数
  • T_i=2の操作が1つ以上存在する
考察
  1. CX個連結した文字列を実際に作るのは、10^{10}になるので駄目。
  2. T=1の場合はとりあえずそのままの情報をキューに追加。
  3. T=2の場合、キュー先頭のXDの大小関係を気にしながら頑張って計算したい。
  4. X \le Dならキューの先頭を削除、X \gt Dなら先頭を残したまま処理終了するのを上手いことやる。
  5. ついでに文字種ごとにカウントもしておき、2乗の和を計算する。10^5の2乗はintの範囲を超えるので注意。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.Queue;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int q = Integer.parseInt(br.readLine());
        Queue<Obj> que = new ArrayDeque<>();
        PrintWriter pw = new PrintWriter(System.out);
        for (int i = 0; i < q; i++) {
            String[] sa = br.readLine().split(" ");
            if (sa[0].equals("1")) {
                Obj o = new Obj();
                o.c = sa[1].charAt(0) - 'a';
                o.x = Integer.parseInt(sa[2]);
                que.add(o);
            } else {
                int d = Integer.parseInt(sa[1]);
                long[] cnt = new long[26];
                while (d > 0 && !que.isEmpty()) {
                    Obj o = que.peek();
                    int del = Math.min(d, o.x);
                    cnt[o.c] += del;
                    d -= del;
                    o.x -= del;
                    if (o.x <= 0) {
                        que.poll();
                    }
                }
                long ans = 0;
                for (int j = 0; j < cnt.length; j++) {
                    ans += cnt[j] * cnt[j];
                }
                pw.println(ans);
            }
        }
        pw.flush();
        br.close();
    }

    static class Obj {
        int c, x;
    }
}

H - 1-9 Grid

問題
例1を見るまで問題文が何言っているのかよくわからなかった。
「S123456789123456789G」のような順に通る必要があるのか?とか思ったりしてた。
それはさておき、G問題とI問題が中級レベルとしてはやるだけだったので、この問題が中級の門番だったと思う。

問題概要

Nマス、横Mマスのマス目があり、各マスには 'S'、'G'、'1'~'9' のいずれかが書かれている。
「S-1-2-3-4-5-6-7-8-9-G」("-"は任意の0マス以上)のように、1~9を順に経由してSからGまで移動するときの最小の移動回数を求めよ。
そのような経路が存在しない場合は-1を出力せよ。
上下左右の4方向に移動可能。

考察
  1. S→1のどこか→2のどこか→・・・→9のどこか→G の順に移動することになる。
  2. S→一番近い1→一番近い2→・・・のように貪欲に移動すると、Sには近くても2から遠ざかるような1に行くと駄目そう。つまり、どの1を経由すればいいかすら、ゴールしないとわからない。
  3. 各2のマスへの最短距離を求めるには、全ての1→2の組み合わせを調べて最小を取るとよさそう。とりあえず、1つ前の数字より前は見る必要がない。
  4. dp[i][j]=ij列目のマスまでの最短距離 として、数字の小さい順に埋めていく。
  5. 遷移は、「1つ前の数字が書かれた各マスの最短距離+そのマスからマス(i, j)までの距離」の最小値。
  6. 計算量はざっくり、数字が変わるのが10\times 遷移先マスの探索が50^2 \times 遷移元マスの探索が50^2 で数千万くらいになりそうな気もするけど、if文の中に入る回数はだいぶ少ないし、とりあえず投げてみよう。 →128msで余裕だった。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;

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 m = Integer.parseInt(sa[1]);
    char[][] a = new char[n][m];
    for (int i = 0; i < n; i++) {
      a[i] = br.readLine().toCharArray();
    }
    br.close();

    int[][] dp = new int[n][m];
    for (int i = 0; i < n; i++) {
      Arrays.fill(dp[i], 100000000);
    }

    char[] c = {'S', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'G'};
    for (int k = 0; k < c.length; k++) {
      for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
          // 遷移先マスの数字がc[k]の場合
          if (a[i][j] == c[k]) {
            if (k == 0) {
              dp[i][j] = 0; // Sのマス
            } else {
              for (int i2 = 0; i2 < n; i2++) {
                for (int j2 = 0; j2 < m; j2++) {
                  // 遷移元マスの数字はc[k - 1]
                  if (a[i2][j2] == c[k - 1]) {
                    int alt = dp[i2][j2] + Math.abs(i - i2) + Math.abs(j - j2);
                    dp[i][j] = Math.min(dp[i][j], alt);
                  }
                }
              }
              if (c[k] == 'G') {
                if (dp[i][j] == 100000000) {
                  System.out.println(-1);
                } else {
                  System.out.println(dp[i][j]);
                }
                return;
              }
            }
          }
        }
      }
    }
  }
}

I - トーナメント

問題
ただ愚直にシミュレーションするだけ。本当にそれでいいのか一瞬疑った。
前回と比べて、初級までは難しくなったと思ったけど、中級は随分簡単になったのでは。

問題概要

2^N人が番号順に一列に並んでおり、番号の若い順に2人ずつペアになって戦うトーナメントを行う。
iの強さはA_iであり、値が大きい方が勝つ。2回戦以降も勝者を番号順に並べて同様。
それぞれの人が何回戦まで残るかを求めよ。

  • 1 \le N \le 16
  • 1 \le A_i \le 2^N
  • Aの要素は相異なる
考察
  1. 全員をリストに入れる。
  2. 奇数番目と偶数番目を比べ、弱い方を取り除いてその際何回戦かを記録する。
  3. リスト上でエレメントを取り除くと、前の方を取り除いたときに要素数分の時間がかかってしまうので、回戦ごとに新しいリストに強い方を追加していく。
  4. リストに追加する回数が、回戦ごとに半分になっていく。トータルで2^N-1回なので間に合う。
  5. 優勝者についても忘れずに設定する。(サンプル試して0になって気が付いた)
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

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

        int[] ans = new int[n2];
        List<Integer> list = new ArrayList<>(n2);
        for (int i = 0; i < n2; i++) {
            list.add(i);
        }
        int cnt = 1;
        while (list.size() > 1) {
            List<Integer> work = new ArrayList<>(); // 勝者リスト
            for (int i = 0; i < list.size(); i+=2) {
                if (a[list.get(i)] > a[list.get(i + 1)]) {
                    work.add(list.get(i)); // 勝った方は勝者リストへ
                    ans[list.get(i + 1)] = cnt; // 負けた方は何回戦かを記録
                } else {
                    work.add(list.get(i + 1)); // 勝った方は勝者リストへ
                    ans[list.get(i)] = cnt; // 負けた方は何回戦かを記録
                }
            }
            list = work;
            cnt++;
        }
        ans[list.get(0)] = cnt - 1; // 優勝者
        PrintWriter pw = new PrintWriter(System.out);
        for (int i = 0; i < n2; i++) {
            pw.println(ans[i]);
        }
        pw.flush();
    }
}

J - 文字列解析

問題
これも知識はほとんどいらない実装メインの問題。
H問題を飛ばしてこれをやった方が中級取れそう。

問題概要

英小文字、'('、')'からなる文字列Sが与えられる。
「(abc)」→「abccba」のように、括弧部分を、そのままの文字列と反転した文字列の連結に置き換えたときの最終的な文字列を出力せよ。

  • 1 \le |S| \le 1000
  • Sは1文字以上の英小文字を含む
  • S内の括弧の対応は取れている(括弧がない場合、入れ子になっている場合、中身が空の場合あり)
  • 最終的な文字列の長さ\le 1000
考察
  1. 制約があまり大きくないので、普通にやればよさそう。
  2. 前から見ていき、括弧以外はそのまま連結する。*2
  3. 開き括弧に当たった場合、後々の反転のことを考えたら、括弧内だけの文字列を別に持っておきたい。
  4. 編集中の文字列をスタック*3に退避し、新しい編集用文字列を用意。
  5. 閉じ括弧の場合、編集中文字列+反転文字列の処理を行い、スタックに退避していた文字列に連結する。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Deque;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        char[] s = br.readLine().toCharArray();
        br.close();

        StringBuilder sb = new StringBuilder();
        Deque<StringBuilder> que = new ArrayDeque<>();
        for (int i = 0; i < s.length; i++) {
            if (s[i] == '(') {
                que.push(sb);
                sb = new StringBuilder();
            } else if (s[i] == ')') {
                // 括弧内の文字列
                StringBuilder sb2 = sb;
                // 複製して反転
                StringBuilder sb3 = new StringBuilder(sb2);
                sb3.reverse();

                sb = que.pop();
                sb.append(sb2).append(sb3);
            } else {
                sb.append(s[i]);
            }
        }
        System.out.println(sb.toString());
    }
}

K - 括弧

問題
そろそろ本格的に難易度が上がってきた。
ここまでの難問はDPに偏っているような?

問題概要

'(' と ')' からなる長さNの文字列Sが与えられる。
以下の操作を0回以上好きなだけ行い、Sを括弧の対応が取れた文字列にするときの合計コストの最小値を求めよ。

  • S_iSi文字目)を逆の文字に変更する。コストはC_i
  • S_iを削除する。コストはD_i
  • 1 \le N \le 3000
  • 1 \le C_i, D_i \le 10^9
  • N, C_i, D_iは整数
考察
  1. とりあえず最悪Dの合計で達成できる。
  2. 例1を見ると、1文字目が ')' だと操作必須だが、変更と削除どちらが最適かは全くわからない。貪欲はない。
  3. '(' を+1、')' を-1とすると、途中で負になってはならず、最後に0になっている必要がある。
  4. dp[i][j]=i文字目まで見て、'(' がj文字多い状態での最小コスト で遷移が書けるか?
  5. i文字目が '(' の場合、そのままならdp[i+1][j+1]へ、変更するならdp[i+1][j-1]へ、削除するならdp[i+1][j]への遷移が書ける。 ')' なら逆。
  6. jは、前半開いて後半閉じることを考えたら、N/2程度で十分。3000 \times 1500 \times 3くらいなので間に合う。
  7. 一応、i \lt jとなる部分は不可能なので見る必要ない。
  8. DPの初期値は適当に3000 \times 10^9より多めに。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        char[] s = br.readLine().toCharArray();

        String[] sa = br.readLine().split(" ");
        int[] c = new int[n];
        for (int i = 0; i < n; i++) {
            c[i] = Integer.parseInt(sa[i]);
        }

        sa = br.readLine().split(" ");
        int[] d = new int[n];
        for (int i = 0; i < n; i++) {
            d[i] = Integer.parseInt(sa[i]);
        }
        br.close();

        long[][] dp = new long[n + 1][1502]; // n/2 + 配列外参照回避分くらい
        for (int i = 0; i < n + 1; i++) {
            Arrays.fill(dp[i], 1000000000000000L);
        }
        dp[0][0] = 0;
        for (int i = 0; i < n; i++) {
            int lim = Math.min(i, 1500);
            if (s[i] == '(') {
                for (int j = 0; j <= lim; j++) {
                    // そのまま
                    dp[i + 1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i][j]);
                    // 削除
                    dp[i + 1][j] = Math.min(dp[i + 1][j], dp[i][j] + d[i]);
                    // 変更
                    if (j > 0) {
                        dp[i + 1][j - 1] = Math.min(dp[i + 1][j - 1], dp[i][j] + c[i]);
                    }
                }
            } else {
                for (int j = 0; j <= lim; j++) {
                    // そのまま
                    if (j > 0) {
                        dp[i + 1][j - 1] = Math.min(dp[i + 1][j - 1], dp[i][j]);
                    }
                    // 削除
                    dp[i + 1][j] = Math.min(dp[i + 1][j], dp[i][j] + d[i]);
                    // 変更
                    dp[i + 1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i][j] + c[i]);
                }
            }
        }
        System.out.println(dp[n][0]);
    }
}

L - 辞書順最小

問題
ABC146-F(Sugoroku)っぽさを感じたりしていた。
今回はこの問題で初めてライブラリ貼った。
前回より1時間以上早い2時間45分ほどで上級達成。

問題概要

長さNの整数列A_1, A_2, ... , A_Nから、K個の要素をD以上の間隔で抜き出し、元の順序のまま新しい数列を作る。
この操作で作れる数列の内、辞書順最小であるものを求めよ。
そのような操作が不可能な場合は-1を出力せよ。

  • 2 \le N \le 2 \times 10^5
  • 1 \le D, K \le N
  • 0 \le A_i \le 10^9
  • 入力は全て整数
考察
  1. K文字目に選べる右端のインデックスはN-1K-1文字目の右端はN-1-D、・・・と後ろから順にDずつ減らしていくと、1文字目に選べる範囲がわかる。
  2. この時点で右端が0未満なら-1
  3. あとは、0~右端インデックスリスト[0] の範囲の最小値(複数あれば一番左のもの)、前回選択インデックス+D~右端リスト[1] の範囲の最小値、・・・と貪欲に選択していく。
  4. ただし、最小値の探索にかけられるオーダーがlogくらい。
  5. 区間の最小値と言えば、セグメント木を使う? F問題のようにPriorityQueueで簡単にできないか?
  6. PriorityQueueだと、右の方の追加は簡単だが、左の方の使えなくなったものを削除する方法がぱっとわからなかったので、悩むくらいならセグ木を使うことにした。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;

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]);
        int d = Integer.parseInt(sa[2]);
        sa = br.readLine().split(" ");
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = Integer.parseInt(sa[i]);
        }
        br.close();

        // 右端インデックスリスト
        List<Integer> list = new ArrayList<>();
        int idx = n - 1;
        list.add(idx);
        for (int i = 0; i < k - 1; i++) {
            idx -= d;
            if (idx < 0) {
                break;
            }
            list.add(idx);
        }
        if (list.size() < k) {
            System.out.println(-1);
            return;
        }
        // 後ろから入れているので逆順にする
        Collections.reverse(list);

        Obj na = new Obj();
        na.i = -1;
        na.a = Integer.MAX_VALUE;
        SegTree<Obj> st = new SegTree<>(n, na, (o1, o2) -> {
            // Aが小さい方、同じならindexが小さい方を複製して返す
            Obj o = new Obj();
            Obj src = null;
            if (o1.a < o2.a) {
                src = o1;
            } else if (o1.a > o2.a) {
                src = o2;
            } else {
                if (o1.i < o2.i) {
                    src = o1;
                } else {
                    src = o2;
                }
            }
            o.i = src.i;
            o.a = src.a;
            return o;
        });
        for (int i = 0; i < n; i++) {
            Obj o = new Obj();
            o.i = i;
            o.a = a[i];
            st.update(i, o);
        }

        StringBuilder sb = new StringBuilder();
        idx = 0;
        for (int i = 0; i < k; i++) {
            // 選択可能区間内の最小値取得
            Obj min = st.query(idx, list.get(i) + 1);
            sb.append(min.a).append(' ');
            idx = min.i + d;
        }
        sb.deleteCharAt(sb.length() - 1);
        System.out.println(sb.toString());
    }

    static class Obj {
        int i, a;
    }

    // 以下ライブラリ

    static class SegTree<T> {
        int n = 2;
        int n2;
        T NA;
        List<T> list;
        BiFunction<T, T, T> func;

        public SegTree(int num, T na, BiFunction<T, T, T> func) {
            while (n < num) n <<= 1;
            n2 = n * 2 - 1;
            NA = na;
            list = new ArrayList<T>(n2);
            for (int i = 0; i < n2; i++) list.add(NA);
            this.func = func;
        }

        void update(int i, T x) {
            i += n - 1;
            list.set(i, x);
            while (i > 0) {
                i = (i - 1) / 2;
                list.set(i, func.apply(list.get(i * 2 + 1), list.get(i * 2 + 2)));
            }
        }

        T query(int l, int r) {
            return query(l, r, 0, 0, n);
        }

        private T query(int l, int r, int k, int beg, int end) {
            if (end <= l || r <= beg) return NA;
            if (l <= beg && end <= r) return list.get(k);
            T v1 = query(l, r, k * 2 + 1, beg, (beg + end) / 2);
            T v2 = query(l, r, k * 2 + 2, (beg + end) / 2, end);
            return func.apply(v1, v2);
        }
    }
}
考察2

後でよく考えたら、PriorityQueueでもできた。1つ選択するごとに使用不可になったものを全部捨てようとするのではなく、次の最小値を得る際に、インデックスが使用可能な左端より小さいものを捨て続けるだけだった。

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) throws Exception {

        // 省略

        if (list.size() < k) {
            System.out.println(-1);
            return;
        }

        // ここまでセグ木解と同じ

        list.add(-1); // 番兵
        Collections.reverse(list);

        PriorityQueue<Obj> que = new PriorityQueue<>((o1, o2) -> {
            // Aの昇順、同じならindexの昇順
            if (o1.a != o2.a) {
                return o1.a - o2.a;
            }
            return o1.i - o2.i;
        });

        StringBuilder sb = new StringBuilder();
        idx = 0;
        for (int i = 1; i <= k; i++) {
            for (int j = list.get(i - 1) + 1; j <= list.get(i); j++) {
                que.add(arr[j]);
            }
            // 選択可能範囲のものが見つかるまで捨てる
            while (true) {
                Obj o = que.poll();
                if (o.i >= idx) {
                    sb.append(o.a).append(' ');
                    idx = o.i + d;
                    break;
                }
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        System.out.println(sb.toString());
    }

    static class Obj {
        int i, a;
    }
}

M - 食堂

問題
15分くらい考えてみたが解けなかったので飛ばし。
最初に好みの料理を食べるまでと、最後に好みの料理を食べた後を単純にシミュレーションするだけで計算量オーバーのような気がして(実際どうかは不明)諦めた。
解説PDFも未解読。

N - ビルの建設

問題
1時間くらいかかったが、ラスト3問の中では唯一時間内に解けた。結果88点。

問題概要

2次元平面上に、各辺がx, y軸に平行な正方形がN個ある。
i番目の正方形は、左下の頂点が(xmin_i, ymin_i)、1辺の長さがD_i、コストがC_i
Q個の点(A_j, B_j)それぞれについて、点を含む(または境界線上に持つ)正方形のコストの和を求めよ。

  • 1 \le N \le 50000
  • 1 \le Q \le 10^5
  • -10^9 \le xmin_i, ymin_i, A_j, B_j \le 10^9
  • 1 \le D_i \le 10^9
  • 0 \le C_i \le 10^9
  • 入力は全て整数
考察
  1. 少なくともQは必ず全体のオーダーに関わる。1クエリ当たりlogくらいで求まる必要がある。
  2. Nが比較的小さい。座標圧縮して二次元累積和を取れば、クエリはO(1)になるが、累積和部分がO(N^2)で無理そう。
  3. 二次元累積和をしても、正方形ともクエリとも関係ない無駄な部分だらけになりそう。関係ある部分だけの情報を持てないか?
  4. xでソートしてx座標の小さい方から見ていき、正方形の境界部分でy座標の情報更新しながら、x座標が対象範囲に含まれるクエリに答えていくことを考える。
  5. x座標を固定したときのy座標に注目すると数直線のようになり、BITを使って各正方形の下端に+C、上端に-Cを加算した上でBまでの和を取得することで、コストの和を取得できる。
  6. 3 \times 10^9のデータは持てないので、yについて座標圧縮する。これでBITのサイズが2N程度に収まる。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeMap;

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 q = Integer.parseInt(sa[1]);

        // キー:正方形の境界線のx座標(左右両側)、値:対応する正方形のリスト
        TreeMap<Integer, List<Toti>> map = new TreeMap<>();
        // キー:正方形の境界線のy座標(上下両側)、値:座標圧縮後の値
        TreeMap<Integer, Integer> zy = new TreeMap<>();
        zy.put(Integer.MIN_VALUE, null); // 番兵

        for (int i = 0; i < n; i++) {
            sa = br.readLine().split(" ");
            Toti o = new Toti();
            o.x = Integer.parseInt(sa[0]);
            o.y = Integer.parseInt(sa[1]);
            o.d = Integer.parseInt(sa[2]) + 1; // 半開区間とする
            o.c = Integer.parseInt(sa[3]);
            zy.put(o.y, null);
            zy.put(o.y + o.d, null);
            List<Toti> list = map.get(o.x);
            if (list == null) {
                list = new ArrayList<>();
                map.put(o.x, list);
            }
            list.add(o);
            list = map.get(o.x + o.d);
            if (list == null) {
                list = new ArrayList<>();
                map.put(o.x + o.d, list);
            }
            list.add(o);
        }
        map.put(Integer.MAX_VALUE, new ArrayList<>(0)); // 番兵

        Query[] qs = new Query[q];
        for (int i = 0; i < q; i++) {
            sa = br.readLine().split(" ");
            Query qu = new Query();
            qu.i = i;
            qu.a = Integer.parseInt(sa[0]);
            qu.b = Integer.parseInt(sa[1]);
            qs[i] = qu;
        }
        br.close();

        // 座標圧縮
        Integer[] tmp = zy.keySet().toArray(new Integer[0]);
        int cnt = 0;
        for (Integer i : tmp) {
            cnt++;
            zy.put(i, cnt);
        }

        // x座標の昇順でソート
        Arrays.sort(qs, (o1, o2) -> o1.a - o2.a);

        BIT bit = new BIT(100001); // 2N以上確保
        int idx = 0;
        // x座標の境界分ループ
        for (int key : map.keySet()) {
            // 正方形の前回の境界 ≦ A < 今回の境界 のクエリに答える
            while (idx < q && qs[idx].a < key) {
                qs[idx].ans = bit.sum(zy.floorEntry(qs[idx].b).getValue());
                idx++;
            }
            // 境界に対応する正方形のコストをBITに反映
            List<Toti> list = map.get(key);
            for (Toti o : list) {
                if (o.x == key) {
                    // これから敷地内に入る正方形分を加算
                    bit.add(zy.get(o.y), o.c);
                    bit.add(zy.get(o.y + o.d), -o.c);
                } else {
                    // 敷地から抜ける正方形分を減算
                    bit.add(zy.get(o.y), -o.c);
                    bit.add(zy.get(o.y + o.d), o.c);
                }
            }
        }

        // クエリをインデックス順にソートし直して結果出力
        Arrays.sort(qs, (o1, o2) -> o1.i - o2.i);
        PrintWriter pw = new PrintWriter(System.out);
        for (int i = 0; i < q; i++) {
            pw.println(qs[i].ans);
        }
        pw.flush();
    }

    static class Toti {
        int x, y, d, c;
    }

    static class Query {
        int i, a, b;
        long ans;
    }

    // 以下ライブラリ

    static class BIT {
        int n;
        long[] arr;

        public BIT(int n) {
            this.n = n;
            arr = new long[n + 1];
        }

        void add(int idx, long val) {
            for (int i = idx; i <= n; i += i & -i) {
                arr[i] += val;
            }
        }

        long sum(int idx) {
            long sum = 0;
            for (int i = idx; i > 0; i -= i & -i) {
                sum += arr[i];
            }
            return sum;
        }
    }
}

O - 可変全域木

問題
解説PDFLCA解がわかってはいたが、ダブリングで実装をしたことがなく、一からやっていたら30~40分かけても終わらず、時間切れ。
次回までにはライブラリ整備しておいて、エキスパートを狙いたい。

問題概要

N頂点M辺の重みつき単純連結無向グラフが与えられる。
iは頂点A_iB_iを結び、重みはC_i
1Mのそれぞれの辺について、その辺を含む最小の重みの全域木の重みを求めよ。

  • 2 \le N \le 10^5
  • N-1 \le M \le min(10^5, N(N-1)/2)
  • 1 \le C_i \le 10^9
考察
  1. とりあえずクラスカル法で最小全域木を求める。
  2. 上記に含まれる辺は、それが答え。
  3. その他の(ABを結ぶ)辺を1つ足してみると、最小全域木上でのパスA-B内で最大の重みをもつ辺を代わりに抜けばよいことがわかる。
  4. そのような辺をどうやって求めるか? LCA絡みの何かをすれば、ALCA間、LCAB間の最大をそれぞれ求めて、両者の最大を取れた気がする。
  5. そのものずばり、もしくはちょっと改変するくらいで使えるライブラリがほしかった。

一応後でACは取れたのだが、ソースは無駄に長いと思うので一旦省略。
その内整備できたら追加するかもしれません。

初記事なのに随分長くなってしまいました。

*1:ScannerやSystem.out.println()だと、十万単位の行数を入出力するとそれだけで1秒近くかかってしまったりするので、比較的お手軽に何割かは速くなるBufferedReaderやPrintWriterを使うようにしている。それ以上の高速化までは知らない。

*2:Stringで扱うと、連結の度に新規オブジェクトが生成され、MLEになる恐れがあるので、StringBuilderを使う。

*3:便宜上スタックと呼ぶが、JavaのStackは古く、Dequeが上位互換のようなので、実際はDequeを使っている。