今日も窓辺でプログラム

外資系企業勤めのエンジニアが勉強した内容をまとめておくブログ

LeetCode 200: Number of Islands をUnion-findを使って解く

この記事について

Union-findについて勉強したので、LeetCodeのNumber of Islandsという問題をUnion-findを使用して解いてみます。

Union-findとは?

Union-findは素集合データ構造を操作するためのアルゴリズムで、2つの要素が同じ集合に属しているのかを判定したり、2つの集合をまとめたりする操作を行います。

私もUnion-findは知らなかったのですが、AtCoderが提供しているスライドが非常にわかりやすかったです。

www.slideshare.net

LeetCode 200: Number of Islands

Union-findを理解したところで、早速問題を見てみます。問題はLeetCodeのものをお借りしています。
LeetCode: コーディング面接に向けた練習に使えるサイトの紹介 - 今日も窓辺でプログラム

'1'(陸)と'0'(海)で構成されている2次元のグリッド地図が与えられるので、島の個数を求めよ。島は海に囲まれていて、縦横につながっている陸で作られるものとする。地図の四辺の外側は海であると仮定してよい。

https://leetcode.com/problems/number-of-islands/

また問題文には入力の例として次の2つが示されています。

例1:

11110
11010
11000
00000

島の個数 = 1

例2:

11000
11000
00100
00011

島の個数 = 3

この問題、Union-findの考え方を使って解くことができますので、よかったら挑戦してみてください。

Union-findを使った回答

例2を見てみます。

11000
11000
00100
00011

この入力の島の数は3です。左上の4つの'1'の島、中央の1つの'1'の島、そして右下の2つの'1'の島です。
つまり、上下左右でつながっている陸('1')を順にまとめていって、最終的に残った陸の集合の数が島の数となります。
この、陸をまとめる部分で、Union-findのunionの考え方が適用できます。

全ての陸がまとまったあとは、陸の集合がいくつあるかを数えればOKです。陸の集合の個数は、陸の集合の根の個数(上述のスライド参照)と等しいので、根の個数を数えてあげれば終了です。

ざっくりと疑似コードを示すと次のようになります。

  1. 地図の各グリッドにIDを割り振る。海(='0')に関しては全て-1を、陸(='1')に関しては0以上の整数を一意に与える。
  2. グリッドを横向きと縦向きに走査し、陸地が隣り合っている場合は2つの陸地を1つにまとめる (union)
  3. 陸の集合の個数を、その根の個数を数えることによって求める

私の実装例はこんな感じになります。
ステップ1でuidという配列にIDを割り当てていき、uidに対してunionやfindの操作を行っています。

class Solution {
public:
    // Union-findのfindの定義。uid[n]の根を求める。
    int findRoot(vector<int>& uid, int n) {
        if (uid[n] == n) {
            return n;
        } else {
            // 経路圧縮の処理。ランクを使った効率化は今回は実装しない。
            uid[n] = findRoot(uid, uid[n]);
            return uid[n];
        }
    }

    // Union-findのUnionの定義。xとyを一つの集合にまとめる。
    void unify(vector<int>& uid, int x, int y) {
        x = findRoot(uid, x);
        y = findRoot(uid, y);

        if (x == y) { return; }
        uid[y] = x;
    }

    int numIslands(vector<vector<char>>& grid) {
        if ((grid.size() == 0) || (grid[0].size() == 0)) { return 0; }

        auto m = grid.size();
        auto n = grid[0].size();
        vector<int> uid(m * n, -1); 

        // ステップ1:uidに初期IDを割り当てる。
        // 海は全て-1で、陸にはその位置のインデックスをもとに0以上のIDを与える。
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    uid[i * n + j] = i * n + j;
                }
            }
        }

        // ステップ2-1: 横方向に陸が2つ続いている場合は、それらを一つの集合にまとめる
        for (int i = 0; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if ((grid[i][j - 1] == '1') && (grid[i][j] == '1')) {
                    unify(uid, i * n + j - 1, i * n + j);
                }
            }
        }

        // ステップ2-2: 縦方向に陸地が2つ続いている場合は、それらを一つの集合にまとめる
        for (int j = 0; j < n; j++) {
            for (int i = 1; i < m; i++) {
                if ((grid[i - 1][j] == '1') && (grid[i][j] == '1')) {
                    unify(uid, (i - 1) * n + j, i * n + j);
                }
            }
        }

        // ステップ3: 陸の集合の根の個数を数える。
        // IDが0以上だと陸であり、なおかつ陸のインデックスとIDが等しいものが陸の集合の根である。
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                auto idx = i * n + j;
                if ((uid[idx] >= 0) && (uid[idx] == idx)) {
                    ans++;
                }
            }
        }

        return ans;
    }
};

先ほどのスライドによると、経路圧縮のみを実装した場合はfindやunionの計算量はO(logN)となるようですので、それを信じるとこのアルゴリズムの時間計算量はO(N logN)となります。
このコードを提出すると6ms程度で走るので、まあまあ悪くない数字なんじゃないでしょうか。

LeetCode 392: Is Subsequence

この記事で扱う問題

LeetCodeの392 Is Subsequenceという問題を解きます。

文字列 s と t があるとき、sがtの部分列 (subsequene)かどうかを判定せよ。

sとtは英語の小文字のみを含む文字列で、tは長い(50万文字)こともあるが、sは短い文字列(100文字以下)であると仮定してよい。

例1:
s = "abc", t = "ahbgdc"
結果→true

例2:
s = "axc", t = "ahbgdc"
結果→false.

https://leetcode.com/problems/is-subsequence/


LeetCode自体の解説記事はこちら:
LeetCode: コーディング面接に向けた練習に使えるサイトの紹介 - 今日も窓辺でプログラム

続きを読む

LeetCode 338: Counting Bits

この記事で扱う問題

LeetCodeのCounting Bitsという問題を解きます。問題を適当日本語訳で引用すると、

負でない整数 num が与えられる。 0 <= i <= num を満たす全ての i について、iの2進数表現に含まれる1の数を計算して配列として出力せよ。

例:num = 5 の場合、戻り値は [0,1,1,2,1,2] となる。

https://leetcode.com/problems/counting-bits/

といった感じです。

LeetCode自体の解説記事はこちら:
LeetCode: コーディング面接に向けた練習に使えるサイトの紹介 - 今日も窓辺でプログラム

続きを読む

日経平均が日中どのくらい変動するかをTensorFlowで予測する (今までのまとめ)

この記事について

少し時間が空いてしましましたが、日経平均予測シリーズの続編です。
ニューラルネットワークを用いて、ある日の日経平均の終値が当日の始値と比べて「上がる」か「下がる」か「ほぼ変わらない」かを予測します。

今まで数回の記事で日経平均に関する予測を行ってきましたが、実装にバグがあり数値が正しくないことが判明しました。
これまでやってきた層数の調整や評価をもう一度やり直すことになったので、せっかくなのでその全過程を1記事にまとめなおしておこう、という記事です。

続きを読む