#16/20 BFS: Shortest Reach in a Graph [Cracking the Coding Interview Challenges]

16 BFS: Shortest Reach in a Graph

www.hackerrank.com

以下の形式の入力が与えられる。

2
4 2
1 2
1 3
1
3 1
2 3
2

最初の行には、クエリの数を示す整数が示される。後続の行は、各クエリを次の形式で示す。

  • 最初の行には、グラフ内のノードの数nとエッジの数mのそれぞれの値をスペースで区切りの2つの整数で示す。
  • 後続の各行には、スペースで区切られた2つの整数が含まれており、ノードからノードに接続するエッジが示される。
  • 最後の行には、開始ノードのインデックスを示す1つの整数が示される。

出力は、開始ノードから各ノードへの距離を出力する。1つのエッジの距離は6、エッジで繋がっていないノードへの距離は-1 とする。

上記入力の場合、解答は以下となる。

6 6 -1
-1 6

solution

この問題は、無向グラフの探索問題である。 幅優先探索(BFS)アルゴリズムで解いている。以下、rubyでの解。

class Node
    def initialize
        @neighbors = []
    end
    
    def addNeighbor(id)
        @neighbors << id if !@neighbors.index(id)
    end
    
    def getNeighbors
        return @neighbors
    end
end

class Graph
    DIST = 6
    
    def initialize
        @nodes = []
    end
    
    def addNode(node)
        @nodes << node
    end
    
    def shortestReach(startId)
        dist = Array.new(@nodes.size, -1)
        dist[startId] = 0
        queue = []
        queue << startId
   
        while (!queue.empty?)
            nodeId = queue.shift
            @nodes[nodeId].getNeighbors.each do |neighbor|
                if dist[neighbor] == -1
                    queue << neighbor
                    dist[neighbor] = dist[nodeId] + DIST
                end
            end
        end
        return dist
    end
    
end

input = $stdin.read.split("\n")
q = input.shift.to_i

q.times do
    n, edges = input.shift.split(" ").map(&:to_i)
    graph, nodes = Graph.new, []

    n.times { nodes << Node.new }
    
    for i in 0..edges-1
        ary = input.shift.split(" ").map(&:to_i)
        idx1, idx2 = ary[0] - 1, ary[1] - 1
        nodes[idx1].addNeighbor(idx2) if nodes[idx1]
        nodes[idx2].addNeighbor(idx1) if nodes[idx2]
    end
    
    nodes.each { |node| graph.addNode(node) }
    
    startId = input.shift.to_i - 1
    dist = graph.shortestReach(startId)
    dist.delete_at(startId)
    puts dist.join(" ") # output answer here
end

グラフ理論

グラフ理論 - Wikipedia

BFS(breadth first search: 幅優先探索)とは

幅優先探索 - Wikipedia

感想

BFSアルゴリズム自体は難しくないが、有向or無向、深さ優先or幅優先、訪問済みを無視するかどうか、など一歩処理を間違えると期待する答えが得られなくなるので注意する。

#15/20 DFS: Connected Cell in a Grid [Cracking the Coding Interview Challenges]

#15 DFS: Connected Cell in a Grid

www.hackerrank.com

以下のような入力が与えられる。

4
4
1 1 0 0
0 1 1 0
0 0 1 0
1 0 0 0

1行目にn行、2行目にm列、それ以降はn行m列分のマトリックスがスペース区切りで与えられる。 マトリックス内で、1のセルが隣接している(縦横斜め)領域を調べ、マトリックス内での最大領域数を出力する。

上記入力の場合、1が隣接している最大領域は5となるので、5を出力する。

5

solution

再帰ありDFS(depth first search)で解いた。以下、rubyでの解。

一度訪問した箇所を再度調べる必要はないので、訪問済みの位置には0をセットしている。

#!/bin/ruby

class Solution

    def initialize(matrix)
        @matrix = matrix
    end

    def getBiggestResion
        matrix = @matrix.clone
        maxSize = 0

        for row in 0..(matrix.size - 1)
            for column in 0..(matrix[row].size - 1)
                if matrix[row][column] == 1
                    size = getResionSize(matrix, row, column)
                    maxSize = size if maxSize < size
                end
            end
        end
        return maxSize
    end
    
    private
        def getResionSize(matrix, row, column)
            return 0 if row < 0 || column < 0 || row >= matrix.size || column >= matrix[row].size
            return 0 if matrix[row][column] == 0

            size, matrix[row][column] = 1, 0
            for r in (row - 1)..(row + 1)
                for c in (column - 1)..(column + 1)
                    if r != row || c != column
                        size += getResionSize(matrix, r, c)
                    end
                end
            end
            return size
        end
end

input = $stdin.read.split("\n")
n, m, matrix = input.shift.to_i, input.shift.to_i, []
input.each_with_index { |v, i| matrix[i] = v.split(" ").map(&:to_i) }
puts (Solution.new(matrix)).getBiggestResion

github.com

DFS(depth-first search: 深さ優先探索)とは

深さ優先探索 - Wikipedia

まとめ

スタックでやる再帰なしより、再帰で書く方が楽だと思う。

都内から江ノ島へ自転車日帰りツーリング

休みに予定がぽっかり空いてしまったので、たまには自転車で日帰り旅でもしようかな、と思い江ノ島に行ってきた。 江ノ島は世田谷区からちょうど往復100kmなので、ロードバイクなら十分日帰り可能だ。

ロードバイク

GIANTのDEFY3。確か今はもうDEFYシリーズは売られてないと思う。 通勤用に2013年に購入して、まだまだ現役。

ルート

google 先生に聞くと、246号線のルートがサジェストされた。まあいいんだけど、往復でルートを変えたかったので、行きは丸子橋を通るルートで、帰りは246号線を走るルートを走ることにした。

往路

*google map のオプションで有料道路と高速道路を不使用にチェック

復路

飯田牧場に寄って、246号線で帰るルート。

*google map のオプションで有料道路と高速道路を不使用にチェック

出発

暑い一日になりそうなので、早朝に出発する。 50kmならゆったりペースでも3時間半あれば着くかな、という見積もり。

6:15 世田谷区(三軒茶屋駅)出発

写真は丸子橋を渡るところ。

f:id:tic40:20170716064118j:plain

9:15 頃、江ノ島海岸到着

f:id:tic40:20170716092656j:plain

この時間はまだまだ人はまばらだけど、昼にかけてどんどん人が増えてきそうな気配がある。

f:id:tic40:20170716092801j:plain

江ノ島入り口

江ノ島の入り口で記念撮影。途中道を間違えたりしながら、ここまで約51kmだった。

江ノ島神社

enoshimajinja.or.jp

奥津宮、中津宮辺津宮の3つがあり、それぞれ3姉妹の女神がお祀りされている。

  • 奥津宮の多紀理比賣命(たぎりひめのみこと)
  • 中津宮の市寸島比賣命(いちきしまひめのみこと)
  • 辺津宮の田寸津比賣命(たぎつひめのみこと)

なるほど。この3女神さらに弁財天女(七福神の一柱でもある弁才天)と称され、才能や財宝を招いてくれるそうだ。 それぞれ参拝をした。

f:id:tic40:20170716102657j:plain

サムエルコッキング苑

サムエル・コッキングというイギリス出身貿易商の庭園らしい。 有料で、あまり興味はわかなかったので入場しなかった。

江の島サムエル・コッキング苑 - Wikipedia

しらす問屋 とびっちょ

人気店。開店30分前に行ってみたが、すでにかなりの人が店前にいる。 整理券の発行機が店頭に置いてあり、60という数字が見え、諦めた。 食べたい人は、早めに発券機で整理券を取っておいた方がいいだろう。

同じように生しらすや海鮮丼を提供している店はたくさんあるのに、開店前に行列ができているのはこの店だけだ。 周り店とどう違うのか。

せっかくなので、店頭売っていた生しらすを買って食べた。500円。おいしいけど、もうちょっと量が欲しかったな。

飯田牧場

自転車乗りが結構集まるみたいで、せっかくなので帰りに寄ってみることにした。 話通り、自転車が沢山止まっていて圧倒された。 シングルのミルクジェラートを注文した。美味しい。ジェラートは最強。

店内にはろんぐらいだぁす、という自転車マンガが置いてあって、知らなかったけどこのマンガに飯田牧場が出てるみたいだ。 所謂聖地だったか。

15時頃には帰宅。

感想

とにかく暑かったけど、久しぶりの自転車旅、最高だった。輪行もそろそろやってみたい。

#14/20 Binary Search: Ice Cream Parlor [Cracking the Coding Interview Challenges]

#14 Binary Search: Ice Cream Parlor

www.hackerrank.com

アイスクリーム屋で、持っているお金を全てを使い切って2個のアイスクリームを購入したい。 その組み合わせを見つける、という問題。

アイスクリーム屋には、様々なフレーバーのアイスがあり、それぞれ値段とユニークなIDが定められている(IDは与えられる入力順に1から始まる)。

以下のような入力が与えられる。

2
4
5
1 4 5 3 2
4
4
2 2 4 3
  1. 1行目はアイスクリーム屋に行く回数
  2. 2行目以降は3行単位で、1.所持金、2.フレーバーの総数、3.フレーバーのコストが繰り返される。

解答は2個のアイスクリームのIDをスペース区切りで、ID昇順で出力する。上記入力場合、解答は以下となる。

1 4
1 2

solution

問題タイトルどおり、バイナリーサーチを使って解いた。

#!/bin/ruby

def binarySearch(ary, key, min, max)
    return -1 if max < min
    
    mid = min + (max - min) / 2
    if key < ary[mid]
        return binarySearch(ary, key, min, mid -1)
    elsif key > ary[mid]
        return binarySearch(ary, key, mid + 1, max)
    else
        return mid
    end
end

def getFlavorId(menu, value, excludeIdx = nil)
    menu.each_with_index do |v, i|
        if v == value
            if excludeIdx && excludeIdx == i
                next
            else
                return i + 1
            end
        end
    end
    return -1
end

def find(menu, money)
    sorted = menu.sort
    len = menu.size
    sorted.each_with_index do |v, i|
        complement = money - v
        result = binarySearch(sorted, complement, i + 1, len - 1)
        if result != -1
            flavor1 = getFlavorId(menu, v)
            flavor2 = getFlavorId(menu, sorted[result], flavor1 - 1)
            return [flavor1, flavor2].sort if flavor2 != -1
        end
    end
    return []
end


input = $stdin.read.split("\n")
trips = input.shift.to_i
trips.times do
    money, n, menu = input.shift.to_i, input.shift.to_i, input.shift.split(" ").map(&:to_i)
    puts find(menu, money).join(" ")
end

github.com

Binary Search(2分探索)とは

二分探索 - Wikipedia

#13/20 DP: Coin Change [Cracking the Coding Interview Challenges]

#13 DP: Coin Change

www.hackerrank.com

タイトルどおり、Dynamic Programming で解く問題。

以下のような入力が与えられる。

4 3
1 2 3
4 3 # {コインの組み合わせで達成したい値} {コインの種類数}
1 2 3 # コインの種類。この場合、価値が1, 2, 3の3種類のコインが存在するということになる

各コインが無限に利用可能である場合、達成したい値(上記の場合4)を達成する組み合わせが何通り存在するかを計算する。

上記の入力の場合

  • 1,1,1,1
  • 1,1,2
  • 2,2
  • 1,3

の4種類の組み合わせがあるため、4を出力すれば良い。

solution

以下 ruby での解。

#!/bin/ruby

def solution(coins, target)
    lenCoins = coins.size
    table = Array.new(lenCoins + 1).map{Array.new(target + 1,0)}
    for i in 0..lenCoins
        table[i][0] = 1
    end

    for i in 1..lenCoins
        for j in 1..target
            table[i][j] = coins[i - 1] <= j ?
                table[i - 1][j] + table[i][j - coins[i -1]]
                : table[i - 1][j]
        end
    end
    return table[lenCoins][target];
end

input = $stdin.read.split("\n")
n, m = input[0].split(" ").map(&:to_i)
coins = input[1].split(" ").map(&:to_i)
puts solution(coins, n)

動的計画法に倣って、まず小さい問題に分割、ボトムアップで順次に解を得ていくというアプローチで解いた。 ソースコード内table配列が全ての部分結果を保持する。

追加するコインの値がtarget値より大きい場合

table[i - 1][j] を参照する 。合計値をオーバーするためこの場合、既に算出している自身より1ワイズ小さいコインの結果を参照する。

追加するコインの値がtarget値以下の場合

table[i - 1][j] + table[i][j - coins[i -1]] を参照する。 自身のコインを追加できる場合、目標値は(target - コインの値)と考えることができるので既に算出済みの table[追加するコインindex][(target - 追加するコインの値)] を加算している。

github.com

Dynamic Programming(動的計画法)とは

動的計画法(どうてきけいかくほう、英: Dynamic Programming, DP)は、計算機科学の分野において、アルゴリズムの分類の1つである。対象となる問題を複数の部分問題に分割し、部分問題の計算結果を記録しながら解いていく手法を総称してこう呼ぶ。

動的計画法 - Wikipedia

感想

動的計画法、すらすらと書けないなあ。解いてみるとすごくシンプルなんだけど。

#12/20 Merge Sort: Counting Inversions [Cracking the Coding Interview Challenges]

#12 Merge Sort: Counting Inversions

www.hackerrank.com

以下のようなインプットが与えられる。

2  
5  
1 1 1 2 2  
5  
2 1 3 1 2
  • 1行目はデータセットの総数
  • 2行目以降はデータセットの総数分だけ以下の入力の繰り返し
    • データの要素の数が整数で与えられる
    • スペース区切りで上記要素の数だけ整数が与えられる

与えられたデータをマージソートし、swapした回数を出力する。 上記のインプットの場合、2個のデータセットが与えられるので、それぞれに対して マージソートを行いswap数を出力する。

0  
4   

solution

以下、rubyでの解。

class Solution

    def initialize(ary)
        @ary = ary
        @swaps = 0
    end

    def mergeSort(ary = @ary)
        return ary if (len = ary.size) <= 1
        ary = ary.clone
        mid = len == 2 ? 1 : len / 2
        list1 = ary.slice(0, mid)
        list2 = ary.slice(mid, len - mid)

        return merge(self.mergeSort(list1), self.mergeSort(list2))
    end

    def getSwaps
        return @swaps
    end

    private
        def merge(list1, list2)
            len1, len2 = list1.size, list2.size
            result = Array.new(len1 + len2)
            a, b, i, j, k = list1[0], list2[0], 0, 0, 0
            loop {
                if a <= b then
                    result[i] = a
                    i += 1
                    j += 1
                    break unless j < len1
                    a = list1[j]
                else
                    # count number of swaps here
                    @swaps += len1 - j
                    result[i] = b
                    i += 1
                    k += 1
                    break unless k < len2
                    b = list2[k]
                end
            }
            while j < len1 do
                result[i] = list1[j]
                i += 1
                j += 1
            end
            while k < len2 do
                result[i] = list2[k]
                i += 1
                k += 1
            end
            return result
        end
end

input = $stdin.read.split("\n")
d = input.shift.to_i
d.times do |v|
    n, ary = input.shift.to_i, input.shift.split(" ").map(&:to_i)
    s = Solution.new(ary)
    s.mergeSort
    puts s.getSwaps
end

github.com

マージソートとは

マージソートは、ソートのアルゴリズムで、既に整列してある複数個の列を1個の列にマージする際に、小さいものから先に新しい列に並べれば、新しい列も整列されている、というボトムアップの分割統治法による。n個のデータを含む配列をソートする場合、最悪計算量O(n log n)である。分割と統合の実装にもよるが、一般に安定なソートを実装できる。インプレースなソートも提案されているが、通常O(n)の外部記憶を必要とする。 (ナイーブな)クイックソートと比べると、最悪計算量は少ない。ランダムなデータでは通常、クイックソートのほうが速い。

マージソート - Wikipedia

pipでWebスクレイピングライブラリScrapyのインストール(Python3)

環境

$ python -V 
Python 3.6.1

$ pip -V
pip 9.0.1

Scrapyとは?

これ

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

pipでScrapyをインストールする

$ pip install Scrapy 
Collecting scrapy
  Using cached Scrapy-1.4.0-py2.py3-none-any.whl
Collecting PyDispatcher>=2.0.5 (from scrapy)
  Using cached PyDispatcher-2.0.5.tar.gz
Collecting queuelib (from scrapy)
  Using cached queuelib-1.4.2-py2.py3-none-any.whl
Collecting w3lib>=1.17.0 (from scrapy)
  Downloading w3lib-1.17.0-py2.py3-none-any.whl
Collecting parsel>=1.1 (from scrapy)
  Using cached parsel-1.2.0-py2.py3-none-any.whl
Collecting service-identity (from scrapy)
  Using cached service_identity-17.0.0-py2.py3-none-any.whl
Collecting pyOpenSSL (from scrapy)
  Using cached pyOpenSSL-17.1.0-py2.py3-none-any.whl
Collecting lxml (from scrapy)
  Downloading lxml-3.8.0-cp36-cp36m-manylinux1_x86_64.whl (7.3MB)
    100% |████████████████████████████████| 7.3MB 79kB/s
Collecting Twisted>=13.1.0 (from scrapy)
  Could not find a version that satisfies the requirement Twisted>=13.1.0 (from scrapy) (from versions: )
No matching distribution found for Twisted>=13.1.0 (from scrapy)

インストールに失敗した。どうやらTwisted >=13.1.0 が必要とのことなのでTwistedをインストールする。

$ pip install Twisted
Collecting Twisted
  Could not find a version that satisfies the requirement Twisted (from versions: )
No matching distribution found for Twisted

が、Twistedのインストールもダメ。仕方がないので、公式のソースからインストールする。

Downloads – Twisted

Twisted 17.5.0 をダウンロードしてインストール。

$ wget https://twistedmatrix.com/Releases/Twisted/17.5/Twisted-17.5.0.tar.bz2
$ tar -jxvf Twisted-17.5.0.tar.bz2
$ cd Twisted-17.5.0.tar.bz2
$ python setup.py install
...
Finished processing dependencies for Twisted==17.5.0

Twistedのインストール完了。再度pipでScrapyのインストールにトライする。

$ pip install Scrapy
...
Successfully installed PyDispatcher-2.0.5 Scrapy-1.4.0 pyasn1-0.2.3 pyasn1-modules-0.0.9 queuelib-1.4.2 service-identity-17.0.0

無事にインストールできた。

$ scrapy
Scrapy 1.4.0 - no active project

Usage:
  scrapy <command> [options] [args]

Available commands:
  bench         Run quick benchmark test
  fetch         Fetch a URL using the Scrapy downloader
  genspider     Generate new spider using pre-defined templates
  runspider     Run a self-contained spider (without creating a project)
  settings      Get settings values
  shell         Interactive scraping console
  startproject  Create new project
  version       Print Scrapy version
  view          Open URL in browser, as seen by Scrapy

  [ more ]      More commands available when run from project directory

Use "scrapy <command> -h" to see more info about a command

PythonによるWebスクレイピング

PythonによるWebスクレイピング