けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

AtCoder ABC 249 C - Just K (茶色, 300 点)

ビット全探索もついに茶色 diff ですね!

予備知識

ビット全探索の知識があると解きやすいです!

drken1215.hatenablog.com

問題概要

英小文字のみからなる  N 個の文字列  S_{1}, S_{2}, \dots, S_{N} が与えられます。

これらの文字列の中から、いくつかの文字列を選びます。

こうして選んだ文字列のうち、ちょうど  K 個に含まれているような文字の種類を数えます。その種類数として考えられる最大値を求めてください。

制約

  •  1 \le K \le N \le 15

 

一目で bit 全探索!!

問題の意味がパッと分かりづらいですね。でも種類数がナンタラのところがパッと分からなかったとしても、「この問題が bit 全探索で解けそうだ」ということは一目でわかります。それは


  •  N 個のものから、いくつか選んだ結果を最適化する問題だということ
  • 制約が  N \le 15 というように、思わせぶりであること

の 2 つが理由です!!!

まず 1 個目についてですが、競プロではまさに  N 個のものからいくつか選んだ結果を最適化する問題が数多いです。ナップサック問題もそうですね。

そういう問題は、制約さえ小さければ、必ず bit 全探索で解けるのです。そう思っているところに「 N \le 15」という制約があるのですから、bit 全探索確定です!!

bit 全探索

 N 個のものからいくつか選んで得られる集合は  2^{N} 通りあります。そのような集合たちをビットで表します。 そして bit 全探索は、次のように書けます。

for (int bit = 0; bit < (1 << N); ++bit) {
    // ビットで表すと bit になるような集合を調べる
}

たとえば N = 8bit = 0b01100111 のとき、8 個のもの (0〜7) のうち 0, 1, 2, 5, 6 番目のものを選ぶような場合を表します。ビット表現では、右端の桁から順に i 桁目が 1 のときは「i 番目のものを選ぶ」ことを表し、i 桁目が 0 のときは「i 番目のものを選ばない」ことを表します。

なお、調べる集合の個数は  2^{N} 通りあります。多くの場合、各集合を評価するのに要する計算量は  O(N) ですので、全体の計算量は  O(N 2^{N}) になることが多いです。各集合の評価により多くの時間がかかりときは、全体の計算量もより大きくなります。

集合 bit を調べる

最後に、今回の問題において、 N 個のもののうち、ビット表現で bit と表される集合が条件を満たすかどうかを調べる部分を詰めます。

'a' から 'z' までの 26 種類の各文字について、それが何回登場するかを数えてあげます。そして、ちょうど  K 回登場するものが何文字あるかを数えましょう。

たとえば次のように実装できます。bit に対して、そのスコア ( K 回登場する文字の個数) を評価する関数を実装します。

int evaluate(int bit, int N, int K, const vector<string>& S) {
    int res = 0;

    // num[c] := 文字 c (0〜26 で表す) の登場回数
    vector<int> num(26, 0);
    for (int i = 0; i < N; ++i) {
        // bit に S[i] が含まれないならスキップ
        if (!(bit & (1 << i))) continue;

        // S[i] に含まれる文字をカウントしていく
        for (char c : S[i]) num[c - 'a']++;
    }
    
    // num[c] = K となる文字 c を数える
    for (int c = 0; c < 26; ++c) if (num[c] == K) ++res;

    return res;
}

コード

計算量は、英小文字の種類数を  A としたとき、

  • bit の個数: 2^{N} 通り
  • 関数 evaluate(bit) O(AN)

ですので、全体として  O(AN2^{N}) となります。今回は  A = 26,  N \le 15 ですので十分間に合います。

#include <bits/stdc++.h>
using namespace std;

int evaluate(int bit, int N, int K, const vector<string>& S) {
    int res = 0;

    // num[c] := 文字 c (0〜26 で表す) の登場回数
    vector<int> num(26, 0);
    for (int i = 0; i < N; ++i) {
        // bit に S[i] が含まれないならスキップ
        if (!(bit & (1 << i))) continue;

        // S[i] に含まれる文字をカウントしていく
        for (char c : S[i]) num[c - 'a']++;
    }
    
    // num[c] = K となる文字 c を数える
    for (int c = 0; c < 26; ++c) if (num[c] == K) ++res;

    return res;
}
int main() {
    int N, K;
    cin >> N >> K;
    vector<string> S(N);
    for (int i = 0; i < N; ++i) cin >> S[i];

    // ビット全探索
    int res = 0;
    for (int bit = 0; bit < (1 << N); ++bit) {
        res = max(res, evaluate(bit, N, K, S));
    }
    cout << res << endl;
}

パズルとアルゴリズムのコラボ本を書きました!

1. はじめに

お久しぶりです!
けんちょん本のけんちょんです。

最近はアルゴリズムがとても盛り上がっていますね。今回新たなアルゴリズム本を上梓させていただくことになりました!

発売予定日は 2022/4/20 です。一部大型書店では、もうすでに並んでいるはずです。今回の記事では、この本を通してお届けしたいメッセージや、想定読者、内容などについて簡単に紹介させていただきます。

amazon ページへのリンク

f:id:drken1215:20220413211445p:plain

 

2. 本書の内容と対象読者

2-1. 本書の内容

百聞は一見に如かずということで、まずは目次構成をお見せします!

第 1 章:アルゴリズム入門

  • 第 1 話:「テンパズル」 〜 力まかせ探索
  • 第 2 話:「小町算」 〜 再帰関数
  • 第 3 話:「虫食算」 〜 枝刈り

第 II 章:グラフアルゴリズム

第 III 章:発展的なアルゴリズム

  • 第 7 節:「15 パズル」 〜 反復深化 A*
  • 第 8 節:「4 × 4 オセロ」 〜 α-β 探索
  • 第 9 節:「編集距離」 〜 動的計画法
  • 第 10 節:「ドミノタイリング」 〜 マッチング法

f:id:drken1215:20220413124240p:plain

端的に言うと、


  • 誰もが馴染みのある代表的なパズルを楽しみながら、
  • パズルのソルバー作りをとおして、
  • アルゴリズムの考え方にも一通り習熟していく

ことを目指した書籍です。とにかく眺めているだけで楽しくなるような内容を目指しました!!

 

ページサンプル 1 (迷路の紹介)

f:id:drken1215:20220413212313p:plain

ページサンプル 2 (覆面算を作る!)

f:id:drken1215:20220413212323p:plain

ページサンプル 3 (数独を作る!)

f:id:drken1215:20220413212332p:plain

ページサンプル 4 (迷路ソルバーを油分け算に応用!)

f:id:drken1215:20220413212548p:plain

ページサンプル 5 (ドミノタイリング)

f:id:drken1215:20220413212824p:plain

2-2. 対象読者

本書は、「パズルが好きな方」と「アルゴリズムを学びたい方」のどちらも楽しめるものを目指しました。

  • 純粋にパズルが好きな方
  • パズルのソルバー作りに関心のある方
  • 大学のレポートで数独などのプログラムを作る必要のある方
  • アルゴリズムを一通り楽しく学びたい方
  • アルゴリズムがどんなものか雰囲気で感じたい方

などなど、さまざまな方に楽しんでいただけると思います!

予備知識として、C++ の文法に一通り習熟していることを仮定しています。しかしながらプログラミングの経験のない方も楽しめるように、ページを眺めているだけでもパズルを解くアルゴリズムの考え方が掴めるように、大量の図を用いて解説しました。

 

3. アルゴリズムと他分野のコラボ

最近アルゴリズムがとても流行っています。今後もさらに何冊ものアルゴリズム本が出版されると思います!

今後さらにアルゴリズムが広まるために大事なことは、アルゴリズムがさまざまな分野とコラボすることだと考えています。米田優峻さんによる「数学 x アルゴ」本はとても画期的でしたね。

amazon ページへのリンク

本書は、パズルとアルゴリズムのコラボを目指します。これまでも、パズル的なアルゴリズムの本はいくつか出ています。しかしながら、数独・虫食算・迷路といった、ザ・パズルとも言うべきパズルを題材にしたアルゴリズムの本は (僕の知る限り) 出ていませんでした。

そこで今回は、ザ・パズルを題材として、アルゴリズム設計技能をひととおり磨ける書籍を目指しました!

f:id:drken1215:20220413223744p:plain

 

4. 「パズル × アルゴリズム」に込めた想い

「パズル」と「アルゴリズム」を結びつけることは、一見ごく自然なようで、実はとても難しいことでした。なぜならパズルの世界では、「パズルは決まった解き方ではなく、問題ごとに工夫を凝らして解くのが楽しい」という考え方が強くあります。

一方アルゴリズムは、「その問題に対するどんな入力データに対しても、同じやり方で答えを導く」ものです。パズルをコンピュータで解くというのは、一歩間違えれば、パズルの魅力を損いかねない考え方を含むものにも思えます。

たとえば数独ソルバーを作るとき、人間が数独を解くための手筋をトレースしたソルバーは広く受け入れられますが、バックトラッキングで解くソルバーに対しては推奨されない場面をよく目にします。

f:id:drken1215:20220413222002p:plain

しかし本書の執筆を通して、実際にパズルのソルバーを作っていくうちに、パズルのことをよく知らなければよいソルバーは作れないことにしばしば気付きました。

パズルのソルバーを作ることは、単にパズルの問題を解く作業をコンピュータに任せるというだけではなく、パズルに対する理解を深める営みでもあるのです!

それだけではありません。パズルを解くアルゴリズムを考えるということは、もうそれ自体が一つのパズルなのです。そんな新たなパズルの楽しさが存分に伝われば幸いです。

 

5. おわりに

最後になりますが、本書を執筆するにあたっては、たくさんの方のお世話になりました。改めて感謝の気持ちでいっぱいです。

アルゴリズムにはものすごいポテンシャルがあると信じています。今から 600 年も前に印刷技術が発明されたことで、識字率が大きく向上したことで世界が大きく変わったように、アルゴリズムを楽しむ方が増えることで世界が大きく変わる。そんな未来に思いを馳せています。個人的な話で恐縮ですが、アルゴ式もそんな未来を思い描きながら創っています。

本書を通して、パズルが好きな方が「アルゴリズムを考えるというパズル」に関心を抱いたり、アルゴリズムが好きな方がパズルの世界を楽しんだりするキッカケを作れたならば、とても嬉しいです!!

AtCoder ABC 246 G - Game on Tree 3 (黄色, 600 点)

めっちゃ面白い問題だった!

問題概要

頂点 0 を根とする頂点数  N の根付き木が与えられます。頂点 0 以外の頂点  v には数値  A_{v} が書かれています。今、頂点 0 にコマが置いてあります。

高橋くんと青木くんが次の動作を交互に繰り返します。

  • 青木くんは、好きな頂点を選んで、その頂点の値を 0 にする
  • 高橋くんは、コマを子頂点のいずれかに移動させる。移動できないときはその場でゲームを終了する

高橋くんは任意のタイミングでゲームを終了できます。そしてゲーム終了した時のコマの置かれた頂点の数値が、ゲームのスコアとなります。

高橋くんはスコアの最大化を目指し、青木くんはスコアの最小化を目指します。双方が最善を尽くしたときのゲームのスコアを求めてください。

制約

  •  2 \le N \le 10^{5}

考えたこと

まず真っ先に二分探索したいと思った。そうすれば、白黒ゲームになる。つまり、各頂点が白または黒に塗られていて、

  • 高橋くんは黒頂点に着地することを目指す
  • 青木くんは黒頂点を白く塗りつぶすことで、高橋くんが黒頂点に着地するのを阻止することを目指す

というゲームになる。このように、二分探索することで、ゲームだったりグラフだったり盤面だったりを白黒にするケースはよくある気がする。

白黒ゲームの解き方

まず思ったのは、高橋くんが勝てる場合、勝つ直前は下図のように「コマのある頂点の子頂点のうち、2 個以上が黒色」という状況にある。

1 個だけが黒色だと、先に青木くんに白色にされて勝てない。でも 2 個が黒色なら青木くんが両方を白色にする前に高橋くんが黒色頂点に着地できるのだ。

そしてさらに言えば、3 個が黒色なら、コマがもう一歩手前にあっても間に合う。

この状況を一般化して、各頂点の「余裕値」を求められそうだ。つまり次のような値を計算できそうだ。


dp[v] ← 頂点  v にコマがある状態から、青木くんに何手先に操作されても、高橋くんが黒頂点に着地できるか


たとえば、

  • 黒頂点自体の余裕値は 1 (すでに着地していることから)
  • 子頂点のうちの 2 個が黒頂点であるような頂点の余裕値は 1
  • 子頂点のうちの 3 個が黒頂点であるような頂点の余裕値は 2

となる。もっと一般に、次のような木 DP で余裕値が求められる。

dp[v] =  \max(0, \sum_{c : v の子頂点} dp[c]  - 1) + (頂点  v が黒頂点 ? 1 : 0)

そして dp[0] が 1 以上なら高橋くんの勝ち、0 なら青木くんの勝ち。この DP に要する計算量は  O(N) となる。

よって二分探索法全体を合わせると、全体の計算量は  A = \max_{v} A_{v} として、 O(N \log A) と評価できる。

コード

#include <bits/stdc++.h>
using namespace std;
using Graph = vector<vector<int>>;

int main() {
    int N;
    cin >> N;
    vector<int> A(N, 0);
    for (int i = 1; i < N; ++i) cin >> A[i];
    Graph G(N);
    for (int i = 0; i < N-1; ++i) {
        int u, v;
        cin >> u >> v;
        --u, --v;
        G[u].push_back(v);
        G[v].push_back(u);
    }

    long long low = -1, high = 1LL<<60;
    while (high - low > 1) {
        long long x = (low + high) / 2;

        auto dfs = [&](auto self, int v, int p = -1) -> int {
            int res = 0;
            for (auto ch : G[v]) {
                if (ch == p) continue;
                res += self(self, ch, v);
            }
            return max(0, res - 1) + (A[v] >= x);
        };

        if (dfs(dfs, 0) >= 1) low = x;
        else high = x;
    }
    cout << low << endl;
}

別解

二分探索ではない方法で  O(N \log N) で求められる方法がある。さっきの DP で言えば、

dp[v][x] ← 数値が  x 以上である頂点を黒頂点であるとした場合の、頂点  v の余裕値

とする。配列 dp[v] の値は、 x に応じて単調減少となるが、変化する部分は高々  O(N) 箇所しかないのだ。

よって dp[v] の値は、変化点も含めて、高々  O(N) 次元ベクトルで管理できる。

木 DP を組むときに、マージテクを使い、さらにヒープのマージを高速にできる meldable heap を用いることで  O(N \log N) の計算量となる。

詳細は Nachia さんのユーザ解説にて

atcoder.jp

AtCoder ABC 246 F - typewriter (青色, 500 点)

包除原理を学べる問題!

問題概要

 N 個の文字列  S_{1}, S_{2}, \dots, S_{N} が与えられます。次の手順によって作れる長さ  L の文字列の個数を 998244353 で割ったあまりを求めてください。

  1.  k = 1, 2, \dots, N のいずれかを選ぶ
  2. 文字列  S_{k} に含まれる文字のみを使って、長さ  L の文字列を作る

制約

  •  1 \le N \le 18
  •  1 \le L \le 10^{9}

包除原理へ

たとえば  N = 2 L = 2 で文字列が "ab"、"ac" である場合は

  • "ab" から作れるのは、"aa", "ab", "ba", "bb" の 4 通り
  • "ac" から作れるのは、"aa", "ac", "ca", "cc" の 4 通り
  • 両方から作れるのは、"aa" の 1 通り

ということで、4 + 4 - 1 = 7 通りと求められます。より一般に  N = 2 の場合は、

(S[0] から作れる文字列の個数)
+ (S[1] から作れる文字列の個数)
- (S[0], S[1] の両方から作れる文字列の個数)

を計算すればよいことになります。

なお、S[0] から作れる文字列の個数は、S[0] に含まれる文字の種類数を  m として  m^{L} 通りとなります。

S[0], S[1] から作れる文字列の個数は、S[0] と S[1] にともに含まれる文字の種類数を  m として  m^{L} 通りとなります。

一般の  N の場合

 N = 2 の場合を拡張して、一般の  N の場合は次のように求められます。


文字列  S_{1}, S_{2}, \dots, S_{N} の空でない部分集合  T 2^{N} - 1 通り考えられる。そのそれぞれに対して、

  • 集合  T に含まれる文字列のいずれからも、作れる長さ  L の文字列の個数を  f(T) としたとき
    •  T の要素数が奇数ならば  f(T) を加算
    •  T の要素数が偶数ならば  f(T) を減算

なお、 f(T) の計算は、 T に含まれる文字列のすべてに含まれる文字の個数を  m として、

 f(T) = m^{L}

と計算できます。

コード

計算量は、各部分集合  T に対して

  • すべてに含まれる文字の個数  m の計算: O(N)
  •  m^{L} の計算: O(\log L)

ですので、全体として  O(2^{N}(N + \log L)) となります。

#include <bits/stdc++.h>
using namespace std;

const int MOD = 998244353;
long long modpow(long long a, long long n, long long mod) {
    long long res = 1;
    while (n > 0) {
        if (n & 1) res = res * a % mod;
        a = a * a % mod;
        n >>= 1;
    }
    return res;
}

int main() {
    int N, L;
    cin >> N >> L;
    vector<int> S(N, 0);
    for (int i = 0; i < N; ++i) {
        string str;
        cin >> str;
        for (char c : str) S[i] |= 1 << (c - 'a');
    }

    long long res = 0;
    for (int bit = 1; bit < (1 << N); ++bit) {
        int tmp = (1 << 26) - 1;
        for (int i = 0; i < N; ++i) {
            if (bit & (1 << i)) tmp &= S[i];
        }
        int m = __builtin_popcount(tmp);
        long long mL = modpow(m, L, MOD);

        if (__builtin_popcount(bit) & 1) res = (res + mL) % MOD;
        else res = (res + MOD - mL) % MOD;
    }
    cout << res << endl;
}

AtCoder ABC 246 E - Bishop 2 (水色, 500 点)

迷路の最短路問題なので BFS でやりたくなるが、まともにやると  O(N^{3}) で TLE してしまう!!

頂点の持ち方を工夫して 0-1 BFS で解く!
別解として枝刈り BFS も。

drken1215.hatenablog.com

問題概要

 N \times N のグリッドが与えられます。各マスは通路 (文字 . で表される) と壁 (文字 # で表される) のいずれかです。

今あなたは座標 (Ax, Ay) にいます。ここから「ビショップの移動」を繰り返して座標 (Bx, By) に到達したいとします。そのための最小手数を答えてください。到達不可能な場合は -1 と出力してください。

なおビショップの移動とは「斜め四方向に、壁にぶつからない限りはどこまでも移動できる」というものです。壁を通り抜けたり、盤面外に出たりすることはできません。

制約

  •  2 \le N \le 1500

BFS では間に合わない

「迷路の最短路」は BFS の典型問題です。ただし通常の「迷路の最短路」問題では、1 回で移動できるマスが「上下左右に隣接する 4 マス」しかないのに対して、今回の問題では 1 回で  O(N) 個のマスに移動できうそうです。

BFS の計算量はグラフの辺数  E に対して  O(E) です。1 回で移動できる箇所が  O(N) となると、グラフの辺数は  O(N^{3}) になります。よって単なる BFS だと、計算量は  O(N^{3}) となって間に合いません。

 

ノードに「移動方向」を持たせて 1 マスずつ移動する

そこで、「1 回の移動で斜めにどこまでも進める」というのを次のように読み替えることにします。


  • 各頂点が (縦方向座標, 横方向座標, 前回の移動方向) を表すグラフを考える
  • 各頂点に対して斜め四方向に進んだ 4 マスにのみ辺を張る
    • 移動方向が前回のものと一致するときは「辺のコスト:0」
    • 移動方向が前回のものと異なるときは「辺のコスト:1」

こうしてできるグラフ上で、(Ax, Ay, 任意) を始点、(Bx, By, 任意) を終点とした最短経路問題を解けばよいことになります!!

こうしてグラフの辺数が  O(N^{2}) で抑えられることになりました。ただし元々のグラフでは辺のコストがすべて 1 だとみなせたのに対して、今回は辺のコストは 0 と 1 がありうることになります。

普通の BFS ではダメなので、Dijkstra 法や 0-1 BFS を使います。

 

0-1 BFS

0-1 BFS は、辺のコストが 0, 1 のみに限ることを利用して、Dijkstra 法における priority_queue の部分を deque にすることで計算量を落とす手法です。Dijkstra 法の計算量  O(V + E \log V) が、0-1 BFS では  O(V + E) に落ちます。

辺の重みが 0 と 1 のみに限られるとき、Dijkstra 法を回したときの priority_queue の中身は常に「最大値と最小値との差が 1 以下」という状態が保たれます。  

よって、priority_queue の中身は高々 2 種類の値しか同時に共存しないことになります。priority_queue のような高級なデータ構造を使う必要はありません。代わりに


両端キュー deque を使う


という方法が考えられます。そして辺  e = (u, v) について緩和するとき、

  • 辺のコストが 0 の場合:deque の先頭に push
  • 辺のコストが 1 の場合:deque の末尾に push

というようにすればよいことになります。deque の両端に push する操作は  O(1) でできるので、priority_queue を使うのに比べて計算量が落ちます。

今回の場合、計算量は  O(N^{2}) になります。

 

コード

#include <bits/stdc++.h>
using namespace std;

const int INF = 1 << 29;
const vector<int> dx = {1, 1, -1, -1};
const vector<int> dy = {1, -1, 1, -1};

template<class T> inline bool chmin(T& a, T b) {
    if (a > b) { 
        a = b; 
        return true; 
    } 
    return false;
}

int main() {
    // 入力
    int N, Ax, Ay, Bx, By;
    cin >> N >> Ax >> Ay >> Bx >> By;
    --Ax, --Ay, --Bx, --By;
    vector<string> S(N);
    for (int i = 0; i < N; ++i) cin >> S[i];

    // 頂点数は (N x N x 4)
    vector<vector<vector<int>>> dp(N, vector<vector<int>>(N, vector<int>(4, INF)));
    deque<tuple<int,int,int>> que;

    // 初期条件
    for (int dir = 0; dir < 4; ++dir) {
        dp[Ax][Ay][dir] = 1;  // 最初の 1 手をノーコストにするため
        que.push_back({Ax, Ay, dir});
    }

    // 0-1 BFS
    while (!que.empty()) {
        auto [x, y, dir] = que.front();
        que.pop_front();
        
        for (int ndir = 0; ndir < 4; ++ndir) {
            int nx = x + dx[ndir], ny = y + dy[ndir];
            if (nx < 0 || nx >= N || ny < 0 || ny >= N) continue;
            if (S[nx][ny] == '#') continue;

            int cost = (ndir != dir);
            if (chmin(dp[nx][ny][ndir], dp[x][y][dir] + cost)) {
                if (cost == 0) que.push_front({nx, ny, ndir});
                else que.push_back({nx, ny, ndir});
            }
        }
    }
    int res = INF;
    for (int dir = 0; dir < 4; ++dir) chmin(res, dp[Bx][By][dir]);
    cout << (res < INF ? res : -1) << endl;
}

別解:枝刈り BFS

さきほど、通常の BFS では TLE してしまうと書いたが、少し工夫することで通すことができる。

通常の BFS では、今いるマスから、斜め四方に壁にぶつからない限りどこまでも行く遷移を試す。

下のコードのような感じ。このコードは、移動方向 dir を決めた上で、その方向に壁にぶつかるまで遷移する様子を表している。

while (true) {
      x += dx[dir], y += dy[dir];
      if (x < 0 || x >= N || y < 0 || y >= N) break;
      if (S[x][y] == '#') break;
                
      if (chmin(dp[x][y], cur_dist + 1)) {
             que.push({x, y});
      } 
}

枝刈り BFS は、次のように「もともとの距離値がより小さいようなマスに到達したら打ち切る」ようにするだけ!!!

while (true) {
      x += dx[dir], y += dy[dir];
      if (x < 0 || x >= N || y < 0 || y >= N) break;
      if (S[x][y] == '#') break;

      if (dp[x][y] < cur_dist + 1) break;
                
      if (chmin(dp[x][y], cur_dist + 1)) {
             que.push({x, y});
      } 
}

たったこれだけの工夫で劇的に早くなる。

コード

#include <bits/stdc++.h>
using namespace std;

const int INF = 1 << 29;
const vector<int> dx = {1, 1, -1, -1};
const vector<int> dy = {1, -1, 1, -1};

template<class T> inline bool chmin(T& a, T b) {
    if (a > b) { 
        a = b; 
        return true; 
    } 
    return false;
}

int main() {
    // 入力
    int N, Ax, Ay, Bx, By;
    cin >> N >> Ax >> Ay >> Bx >> By;
    --Ax, --Ay, --Bx, --By;
    vector<string> S(N);
    for (int i = 0; i < N; ++i) cin >> S[i];

    // 頂点数は (N x N)
    vector<vector<int>> dp(N, vector<int>(N, INF));
    queue<pair<int,int>> que;

    // 初期条件
    dp[Ax][Ay] = 0;
    que.push({Ax, Ay});

    // 枝刈り BFS
    while (!que.empty()) {
        auto [cx, cy] = que.front();
        que.pop();
        int cur_dist = dp[cx][cy];
        for (int dir = 0; dir < 4; ++dir) {
            int x = cx, y = cy;
            while (true) {
                x += dx[dir], y += dy[dir];
                if (x < 0 || x >= N || y < 0 || y >= N) break;
                if (S[x][y] == '#') break;

                if (dp[x][y] < cur_dist + 1) break;
                
                if (chmin(dp[x][y], cur_dist + 1)) {
                    que.push({x, y});
                } 
            }
        }
    }
    cout << (dp[Bx][By] < INF ? dp[Bx][By] : -1) << endl;
}