はまやんはまやんはまやん

hamayanhamayan's blog

Safety Journey [AtCoder Beginner Contest 212 E]

https://atcoder.jp/contests/abc212/tasks/abc212_e

前提知識

解説

https://atcoder.jp/contests/abc212/submissions/24697855

難しくなってくる。今回の問題はまずDPが分かっていることは前提で、その上で高速化をしていく必要がある。

なぜDPが思いつくか

毎回難しい部分で、mod上での数え上げと制約からDPが思い浮かんでDPを考えていくと解けたと言う他ない。
解説などでDPで解きます。と書いてあっても、実際解いている過程でマジでそんな感じなんだと思う。
恐らく、今回のABCではtouristは全問そんな感じだったんだろう。

DPで解く

以下のようなDPで解いていく。

dp[day][lst] := day日間の旅が終了していて最後に都市lstにいるときの旅の組み合わせ数

これが解ければdp[K][1]が答えになることは分かるだろう。
愚直に計算しようとすると、dp[day][lst]について都市lstから移動可能な都市nxtへ
dp[day+1][nxt] += dp[day][lst]のような感じで遷移していく感じになる。
遷移回数がこれだと、状態がO(N2)で遷移がO(N)なのでO(N3)となり間に合わない。

制約をついて高速化する

今回の制約には実は弱い部分があって、Mの最大数が5000であることである。
ここが怪しい部分でここから高速化を導くことができる。

普通にDPの遷移をやろうと思うと、各都市から遷移可能な都市をすべて確認するような感じになるので、
各日で遷移数はO(N2)になる。これは間に合わない。
だが、遷移に「使われない辺」の遷移に着目してみると、これはM本の辺なのでO(M)で済む。
ここが重要な改善ポイント。

DP高速化へ

DP高速化の前段階として良く行われる、配るDPから貰うDPへの変換をしよう。
現状は配るDPとなっており、遷移元のdp[day][lst]を全探索して、遷移可能なdp[day+1][nxt]にその値を足しこんでいく。
逆にdp[day+1][nxt]を全探索して、遷移元dp[day][lst]を足しこんでいくような実装に変えてみよう。

dp[day+1][nxt] = sum{nxtから遷移可能なlst} dp[day][lst]

このような実装をやると貰うDPへの変換が完了する。
この遷移可能というのを遷移できないに変換すると、このような感じになる。

dp[day+1][nxt] = (dp[day][lst]の全部の組み合わせ) - sum{nxtから遷移不可能なlst} dp[day][lst]

逆を取って全体から引くような感じになる。
この全部の組み合わせはnxtのループに入る前に変数totとして計算しておく。
nxtから遷移不可能な先というのは、指定された辺と自分自身への辺の2種類があるので、その分を引く。
こうすると、遷移の回数は全部まとめると(ならしで)N+2M回になるので間に合う。

これで全体O(N(N+M))でAC.

int N, M, K;
vector<int> E[5050];
mint dp[5010][5010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M >> K;
    rep(i, 0, M) {
        int U, V; cin >> U >> V;
        U--; V--;
        E[U].push_back(V);
        E[V].push_back(U);
    }
    
    dp[0][0] = 1;
    rep(day, 0, K) {
        mint tot = 0;
        rep(lst, 0, N) tot += dp[day][lst];
        rep(nxt, 0, N) {
            dp[day + 1][nxt] = tot - dp[day][nxt];
            fore(lst, E[nxt]) dp[day + 1][nxt] -= dp[day][lst];
        }
    }

    cout << dp[K][0] << endl;
}

Querying Multiset [AtCoder Beginner Contest 212 D]

https://atcoder.jp/contests/abc212/tasks/abc212_d

前提知識

  • priority_queueなど

解説

https://atcoder.jp/contests/abc212/submissions/24696926

今回の問題は、以下のような条件を持つデータ構造を使える、持っていることが解くために必要なこととなる。

  • 高速に要素を入れることができる
  • 高速に既に入っている最小値の要素を参照して、かつ、取り出すことができる

これを満たすデータ構造として優先度付きキューがあり、自分の実装もそれを使っている。

問題にどう立ち向かうか

「全体に何かする」という問題は割と出るクエリ問題であり、その場合にoffset的なものを使って高速化する
というのは頻出方針である。
この方針をいかに早く出せるかがキーとなる。
自分は多く見てきたので、この発想をすぐに出せた。

offsetを管理しながら解いていく

offsetのひらめきが一番難しいと思うが、そこはクリアしているものとして以下説明していく。
以下のようなデータを保持しながらクエリをさばいていく

  • 最小値が先頭に来るような優先度付きキュー que
  • キューに入っている要素に足される数 offset

offsetの立ち位置がよくわからないかもしれないが、例えばque = {2,4,9}が入っていて、offset=10となっているならば、
これはキューに実質{12, 14, 19}が入っているものとして考えるということである。
このようにoffsetを定義することで、2番目のクエリが高速に処理できる。

クエリ操作

クエリ1

数の追加なので、queにXを入れればいいが、キューの中身は実質+offsetになっていることになる。
よって、そのまま入れるとX+offsetが入っていることになってしまうので、X-offsetを入れることで、
Xが入っているようにする。

クエリ2

全体に数を足すので、これは丁度offsetに数を足せばいい。

クエリ3

queから最小の要素を取り出してくればいい。
取り出すときに+offsetするのを忘れずに。

今回はあまりに自明すぎて、気にならないかもしれないが、+offsetという制約が入っているせいで
大小関係が変化しないかという部分も考慮しておく必要がある。
今回は変化しないので、問題ない。

template<typename T> using min_priority_queue = priority_queue<T, vector<T>, greater<T>>;
int Q;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> Q;

    ll offset = 0;
    min_priority_queue<ll> que;

    rep(_, 0, Q) {
        int T; cin >> T;
        
        if (T == 1) {
            ll X; cin >> X;
            que.push(X - offset);
        }
        else if (T == 2) {
            ll X; cin >> X;
            offset += X;
        }
        else {
            ll ans = que.top() + offset;
            que.pop();
            printf("%lld\n", ans);
        }
    }
}

Min Difference [AtCoder Beginner Contest 212 C]

https://atcoder.jp/contests/abc212/tasks/abc212_c

前提知識

  • 二分探索, lower_bound

解説

https://atcoder.jp/contests/abc212/submissions/24696602

想定解法とは異なる解法ですが、達成したい目標は同じで、より高速化した解法が想定解法となります。
二分探索、それかlower_boundについて理解できていることを前提として書きます。

まずは全探索を考えてみる

愚直に解いていくことを考えると、iとjを全探索して、abs(A[i] - B[j])の最小値を取れば答えになる。
こうすると、全組合せ数は4*1010通りとなるので、間に合わない。
なお、間に合う目安は107通りくらいである。
この解法はオーダー記法にするとO(N2)なので、高速化していく必要がある。

高速化方針

このような2要素を全探索する解法の高速化の方針として、1つの要素は全探索で、もう片方を最適化するというものがある。
今回はこれが適用でき、iは全探索することにして、jの選択を最適化していくことにしよう。

iを全探索するので、A[i]が分かっているときにより最適なj、ないし、B[j]はどういうものかを考える。
絶対値が最も小さくなるのが最適なものなので、B[j]はA[i]に最も違いものが高速に取り出せれば良い。

配列Bの中でA[i]に最も近いものは?

この問題を解決するのに、二分探索、それを内部で使っているlower_bound関数を利用する。
C++にある関数であるが、以下のような機能を持つ。

lower_bound(begin, end, x) := ソート済み配列の[begin,end)の範囲でx以上となる最小の要素のイテレーションを返す

C++にしかなく、イテレーションとは?という話はあると思うが、ざっくり添え字と考えた場合は以下のような動きになる。

1 2 3 4 5 6  
===========  
2 4 5 7 7 9  
  
lower_bound(1, 6, 5) = 3  
lower_bound(1, 6, 6) = 4  
lower_bound(1, 6, 7) = 4  

こんな感じ。この検索は内部的に二分探索が使われているので計算量はO(logN)となる。
注意として、オーダー記法になじみが無い場合は、大体logN回くらいの検索回数になっていると思っておこう。
使われる配列はソート済みである必要があるので、配列Bはソートしておこう。
(この時にソートしても答えに影響ないことを確認すること。今回はない)

さて、lower_boundで検索する数をA[i]にすれば、A[i]以上で最小の要素が何かが高速に分かる。
A[i]以上で最小の要素が得られたら、それよりも大きい数は絶対値は離れていくはずなので考慮する必要はない。
だが、A[i]未満で絶対値が最小の数が存在するかもしれない。
なので、得られた要素の1個前の要素も答えの候補となるので、それも計算してあげよう。

このようにしてやるとjの候補は2通りに絞られる(かつ、絞る計算も早い)ので、以前のM通りに比べて大量に候補を減らすことができ、間に合う。

int N, M, A[201010], B[201010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> M;
    rep(i, 0, N) cin >> A[i];
    rep(i, 0, M) cin >> B[i];

    sort(A, A + N);
    sort(B, B + M);

    int ans = inf;
    rep(i, 0, N) {
        int j = lower_bound(B, B + M, A[i]) - B;
        if (j < M) chmin(ans, abs(A[i] - B[j]));
        if (0 <= j - 1) chmin(ans, abs(A[i] - B[j - 1]));
    }
    cout << ans << endl;
}

Weak Password [AtCoder Beginner Contest 212 B]

https://atcoder.jp/contests/abc212/tasks/abc212_b

解説

https://atcoder.jp/contests/abc212/submissions/24695908

問題で言われている条件をそれぞれ判定して実装しよう。

入力が数として与えられるので整数型で保持したくなるが、
今回の問題では桁に分けて条件を判定していくので、各桁毎に分けて自分は実装している。
こういう数を桁ごとに分ける場合はstringで取得してみるのもよい選択肢となる。
その後、数の配列に改めて変換していて、文字を数に変換する場合は標準関数を使ってもよいし、'0'で引くことでも実現できる。

条件1

こちらはすべて等しいかどうかの判定であるが、隣接しているもの同士が等しいことを判定すれば、
A=BかつB=CならばA=Cとなるので、全部等しいことが判定できる。

条件2

X[i]+1==X[i+1]であることを判定すればいいのだが、9の次が0になる部分が少し厄介。
これは10で周期をもつという風に言い換えることができ、計算的には10で割った余りとすればよい。
よって、(X[i]+1)%10 == X[i+1]であることを判定しよう。

string _X;
int X[4];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> _X;
    rep(i, 0, 4) X[i] = _X[i] - '0';

    // rule 1
    if (X[0] == X[1] && X[1] == X[2] && X[2] == X[3]) {
        cout << "Weak" << endl;
        return;
    }

    // rule 2
    bool cmp01 = ((X[0] + 1) % 10) == X[1];
    bool cmp12 = ((X[1] + 1) % 10) == X[2];
    bool cmp23 = ((X[2] + 1) % 10) == X[3];
    if (cmp01 && cmp12 && cmp23) {
        cout << "Weak" << endl;
        return;
    }

    cout << "Strong" << endl;
}

Red Polyomino [AtCoder Beginner Contest 211 E]

https://atcoder.jp/contests/abc211/tasks/abc211_e

前提知識

解説

https://atcoder.jp/contests/abc211/submissions/24516205

入力例3を見ると答えの最大数はあまり大きくないので、正しい候補を列挙すればよさそうに見える。
全探索だろうという方針は見えるかもしれないが、実装が今回は問題だと思う。

状態をビットで表現する

まずは状態管理はbitで管理することにしよう。
64箇所について赤にするかしないか2通りであるが、これはunsigned long longを使えばギリギリ入れ込むことができる。
(signedでもできるんかな?ビットで見る分にはできそうだし、llのままでもいいかも)

BFSで列挙していく

BFSによる遷移をK回行うことで、1回の遷移で隣接マスに着色するようにする。
1回目は隣接判定がいらないので、別途やったほうがいい。
自分の実装では1回目だけ別にしている。

隣接マスを塗る場合は、塗られているマス基準で考えるよりも、次塗ろうとしている場所を全通り試して、
そこの隣接に塗られているマスがあるかどうかを見ていくといい。
ビット演算とマスに対する操作に慣れていればそれほど難しくない。

bfsで列挙していくときはかぶっている状態を何回も評価してしまうと遅くなるので、一般的なqueueではなく
setを使ってqueueの役目をさせた。

最後に残った状態が答えの状態となり、その個数が答え。

typedef unsigned long long ull;
int N, K;
string S[8];
int dx[4] = { 0, 1, 0, -1 }, dy[4] = { -1, 0, 1, 0 };
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> K;
    rep(i, 0, N) cin >> S[i];

    set<ull> que;
    rep(y, 0, N) rep(x, 0, N) if (S[y][x] == '.') que.insert(1ull << (y * N + x));

    rep(k, 1, K) {
        set<ull> nxt;
        fore(cu, que) {
            rep(y, 0, N) rep(x, 0, N) if (S[y][x] == '.' && !(cu & (1ull << (y * N + x)))) {
                bool ok = false;
                rep(d, 0, 4) {
                    int xx = x + dx[d];
                    int yy = y + dy[d];

                    if (0 <= xx && xx < N && 0 <= yy && yy < N) {
                        if (S[yy][xx] == '.' && (cu & (1ull << (yy * N + xx)))) {
                            ok = true;
                        }
                    }
                }
                if (ok) nxt.insert(cu | (1ull << (y * N + x)));
            }
        }
        swap(que, nxt);
    }

    int ans = que.size();
    cout << ans << endl;
}

Nevus [第七回 アルゴリズム実技検定 I]

https://atcoder.jp/contests/past202107-open/tasks/past202107_i

前提知識

解説

https://atcoder.jp/contests/past202107-open/submissions/24478121

幾何問題である。複素数を使って座標を保持しながら、幾何的な操作を行っていくのがいい。
幾何問題はその場でのライブラリ整備はだいぶきついので、ライブラリを作っておく必要がある。
書き下してみると、今回必要となる要素結構多いですね…

  • 座標保持
  • 中点計算
  • 平行移動
  • 中心からの距離
  • 回転角の計算
  • 回転操作

平行移動と回転操作

2つ合わさってくると、複雑なので、どちらかを先に行ってしまって、あとは片方だけというのが理想である。
今回はこの理想を実現することができる。

移動前の頂点についての情報は何一つ分かっていないのだが、実は1つ特徴的な点を作り出すことができる。
それが、目と目の中点である。
移動前の目と目の中点は原点となっているので、移動後の目と目の中点を原点に合わせてやれば、
平行移動を行うことができる。

よって、最初に移動後の目と目の中点を計算し、それが原点になるように平行移動をしておく。
複素数で座標を持っていれば、平行移動はベクトルの加減算なので、中心の座標を全部から引いてやればいい。
これでだいぶ楽になった。

回転移動…の前に

回転移動を次は考えるが、どれだけ回転したかを考えるには、基準となる2つの点を用意する必要がある。
依然として、移動前の座標は何一つ分かっていないように見えるが、実は、平行移動によってEが計算できるようになっている。

開店前の目は原点からの距離がEであるが、原点を中心とした回転移動では距離は変わらないので、
平行移動後の目と原点との距離はEになっていることが分かる。
これでEが求められると移動前の右目の座標(-E,0)が分かるので、移動後と比べてみるとどれだけ回転しているかが
計算可能になった。

回転移動

後は回転角を求めて、すべてのほくろをそれで回転させると答えが得られる。
自分の実装では原点を中心として、移動後の右目の位置から移動前の右目の位置(-E,0)への回転角を求めて、
それを全部のほくろに適用している。
ライブラリが整備されていればこの辺はそれほど難しくない。
///////////////////////// writeup2 end *

int N;
vector<P> eyes, dots;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    rep(i, 0, 2) {
        int x, y; cin >> x >> y;
        eyes.push_back(P(x, y));
    }
    rep(i, 0, N) {
        int x, y; cin >> x >> y;
        dots.push_back(P(x, y));
    }

    // 平行移動
    P center = (eyes[0] + eyes[1]) / 2;
    rep(i, 0, 2) eyes[i] -= center;
    rep(i, 0, N) dots[i] -= center;

    double E = abs(eyes[0]);

    // 回転移動
    double rad = argumentAntiClockwise(P(0, 0), eyes[0], P(-E, 0));
    rep(i, 0, N) {
        P ans = rotate(dots[i], rad);
        printf("%.10f %.10f\n", real(ans), imag(ans));
    }
}

Computer [第七回 アルゴリズム実技検定 O]

https://atcoder.jp/contests/past202107-open/tasks/past202107_o

前提知識

解説

https://atcoder.jp/contests/past202107-open/submissions/24477460

最初の考察方針というのはだいぶパフォーマンスに影響すると思う。
今回の問題も制約が普通で、色々方針が見えそうではある。
DPが正しい方針であるが、それをなんで見出したかといわれるととても困ってしまう。
前置きはいいとして、解説に入ろう。

性質①

まずは重要な性質を取り出しておく。
それは入力に対して適切な変換を施すことで、Bを狭義単調増加にすることができるという部分である。
仮にB[i]≧B[i+1]になっている部分があったとすると、i日目を通過できているということはB[i]円以上の
コンピュータを持っているということであり、i+1日目は必ず通過できることになる。
よって、i+1日目は考慮する必要が無く、ただ所持金が増えるだけの日として、それ以降のBが狭義単調増加している日に
マージしてしまおう。

A B  
===  
2 4  
1 3  
6 8  

となっていたら2日目は何もしなくてもお金が入ってくるので3日目とまとめてしまっていいので、

A B  
===  
2 4  
7 8  

のように変換できる。

この部分がまず分かっていないとこれ以降を理解することは難しい。
(理解はできても実装で困るかも)
自分の実装では前処理として、pre関数にてBが狭義単調増加になるように入力を変換している。
入力によっては最後に何もしなくても所持金が増える場合があるので、その分をpostとして別途分けている。

これ以降は、Bは狭義単調増加していると前提をして説明を進める。

性質②

行動として「好きな金額を支払ってコンピュータを購入できる」という部分が自由度が高い。
自由度が高いと選択肢が増えて取り扱いにくいので、少し考えてみる。
コンピュータを購入する意味は仕事を終えるためなので、仕事をこなすのに必要な金額以上のコンピュータを買う必要性はない。
加えて、自分が今持っているコンピュータより安いコンピュータを買う必要性もない。

よって、もう一つ重要な性質として、とある状況で購入するコンピュータの金額は、今持っているコンピュータより高額で、
配列Bのいずれかの金額に絞ってよいということになる。
これで少なくとも遷移はO(N)におさめることができるようになる。

DPテーブル

問題のDPテーブルは以下のようになる。

dp[i] := i日目の夜の仕事を終えた(その夜に必要なコンピュータを購入済み)ときの所持金の最大値

遷移は結構複雑。

dp[i]からdp[j]への遷移について考えてみよう。
dp[i]の状態ではi日目の夜に必要なコンピュータを持っているので、次の日はこのままでは仕事ができない。
だが、遷移先はdp[j]であるので、i+1日目の昼に買うコンピュータはj日目に必要なものになる。
よって、遷移できる条件はj日目のコンピュータの金額 ≦ dp[i] + (i+1日目の朝もらえるお金)となる。
もし、この時に買うことができるならば、j日目まではコンピュータを買わずに済むため、購入後の所持金は

dp[i] + (i+1~j日目の朝もらえるお金) - (j日目のコンピュータの金額)

となる。
高速化のために累積和を使って朝もらえる金額をうまく変換しておこう。

dp[i] + (~j日目の朝もらえるお金) - (~i日目の朝もらえるお金) - (j日目のコンピュータの金額)

これがdp[j]の候補となる。
ここまで理解できていると嬉しいが、これを貰うDPで実装したものが
提出 #24476595 - 第七回 アルゴリズム実技検定
になる。dpだけ1-indexedで、それ以外が0-indexedでなんのことか分からないかもしれないが、参考程度に見てみてほしい。
まずはここまでくればO(N2)が達成できる。
ここまでわかってくればいい所まで来ている。

DP高速化

今回高速化したい部分というのは、

  • j日目のコンピュータの金額 ≦ dp[i] + (i+1日目の朝もらえるお金) という条件をiが満たす中で
  • dp[i] + (~j日目の朝もらえるお金) - (~i日目の朝もらえるお金) - (j日目のコンピュータの金額)の最大値

2番目の部分では貰うDPだとjについては固定なので、dp[i] - (~i日目の朝もらえるお金)の最大値が欲しいということになる。

これにはいくつか解決法がある。
自分はpriority_queue, multisetで実装をしたが、色々やり方がありそうではある。

priority_queue, multiset

自分の実装では以下のような関数を用意して実装した。

  • add(i):dp[i]についてデータ構造に追加
  • cut(limit):満たすべき条件を考慮して、現在のデータ構造から候補を削る
  • getOpt(): 条件を満たす中でのdp[i] - (~i日目の朝もらえるお金)の最大値

今回この手法が使えるのは、「j日目のコンピュータの金額」が狭義単調増加しているからである。
priority_queueを使って条件に使用されるdp[i] + (i+1日目の朝もらえるお金)を保持しておき、日付が増えるにつれて、
「j日目のコンピュータの金額」が増えていくので、priority_queueの先頭(最小)を見ながら条件を満たさないものを
あぶりだして候補から消していく。
候補についてはmultisetで別途管理して、priority_queueから除外されたらmultisetからも消すようにする。
multisetでは最大値を持ってきたいdp[i] + (i+1日目の朝もらえるお金)を入れておき、消せるし、最大値も取れるしという
状況にしておく。

int N; ll A[201010]; int B[201010];
ll dp[201010];
ll post = 0;
ll tot[201010];
//---------------------------------------------------------------------------------------------------
void pre() {
    int NN = 1;
    int ma = B[0]; ll tot = 0;
    rep(i, 1, N) {
        tot += A[i];
        if (ma < B[i]) {
            A[NN] = tot;
            B[NN] = B[i];
            NN++;

            ma = B[i];
            tot = 0;
        }
    }
    post = tot;
    N = NN;
}
//---------------------------------------------------------------------------------------------------
template<typename T> using min_priority_queue = priority_queue<T, vector<T>, greater<T>>;
min_priority_queue<pair<ll, ll>> contains;
multiset<ll> candicates;
void add(int idx) {
    contains.push({ dp[idx] + A[idx], dp[idx] + A[idx] - tot[idx] });
    candicates.insert(dp[idx] + A[idx] - tot[idx]);
}
void cut(ll limit) {
    while (!contains.empty()) {
        auto to = contains.top();

        if (to.first < limit) {
            contains.pop();
            candicates.erase(candicates.find(to.second));
        }
        else return;
    }
}
ll getOpt() {
    auto ite = candicates.rbegin();
    if (ite == candicates.rend()) return -infl;
    return *ite;
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    rep(i, 0, N) cin >> A[i] >> B[i];

    pre();

    tot[0] = A[0];
    rep(i, 1, N) tot[i] = tot[i - 1] + A[i];
    tot[N] = tot[N - 1] + post;

    rep(i, 0, N + 1) dp[i] = -infl;
    dp[0] = 0;
    add(0);
    rep(j, 1, N + 1) {
        cut(B[j - 1]);
        ll opt = getOpt();
        if (-infl < opt) {
            dp[j] = opt + tot[j - 1] - B[j - 1];
            add(j);
        }
    }

    ll ans = -1;
    if (-infl < dp[N]) ans = dp[N] + post;

    cout << ans << endl;
}