n-ominoの列挙(Redelmeier's algorithm)

n-ominoとはn個の正方形で構成されるポリオミノ(Wikipedia)

n-ominoの列挙を行うRedelmeier's algorithmをPythonで実装した。

速度検証

反転・回転を考慮しないn-ominoを列挙し、個数を数えた。所要時間は下記の通りだった。

n n-omino elapsed[s]
1 1 0.000000
2 2 0.000000
3 6 0.000000
4 19 0.000000
5 63 0.000000
6 216 0.001007
7 760 0.004043
8 2725 0.015034
9 9910 0.060160
10 36446 0.271756
11 135268 1.160055
12 505861 4.801800
13 1903890 19.925538
14 7204874 83.391903

検証環境

  • Python 3.6.2
  • Windows 10
  • Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz

Redelmeier's algorithm

Counting polyominoes: Yet another attack - ScienceDirectにPDFがある。

アルゴリズムを3行で説明するとこんな感じ。

  • monominoから深さ優先探索的に成長させていくことでn-ominoを全列挙する
  • 探索範囲はmonomino(0, 0)から開始としてy>0 or (y==0 and x>=0)の範囲に限定する
  • 候補点に付番しながら探索を進め、既に選択したいずれかのマスより若いマスを選択しないようにすることで重複を防ぐ

実装

import time
import unittest


def enumerate_n_omino(n: int):
    """
    Redelmeier's algorithm
    fixed(回転・反転を考慮しない)なポリオミノを列挙する。
    """
    if n < 1:
        return []

    UNORDER = -1
    MAX_Y = n
    MAX_X = 2 * n - 1
    X_CENTER = n - 1
    DYDX = ((1, 0), (0, 1), (-1, 0), (0, -1))

    ominos = []

    visited = [[False] * MAX_X for i in range(MAX_Y)]
    visited_queue = []
    order = [[UNORDER] * MAX_X for i in range(MAX_Y)]
    ordered_queue = []
    border = set()

    # (0,0)から探索を始める
    visited_queue.append((0, X_CENTER))
    visited[0][X_CENTER] = True
    order[0][X_CENTER] = 0

    max_visited = 0
    next_order = 1
    max_x = X_CENTER
    min_x = X_CENTER
    max_y = 0

    def dfs(y: int, x: int, last: int) -> None:
        nonlocal next_order, max_visited, ominos, max_x, min_x, max_y

        if last == 0:
            ominos.append(
                [row[min_x:max_x + 1] for row in visited[:max_y + 1]])
            return

        # (y, x)に隣接セルに付番する
        for dy, dx in DYDX:
            ny = y + dy
            nx = x + dx
            if (ny == 0 and nx < n) or not (0 <= ny < MAX_Y and 0 <= nx < MAX_X):
                continue
            if order[ny][nx] == UNORDER:
                order[ny][nx] = next_order
                next_order += 1
                border.add((ny, nx))
                ordered_queue.append((y, x, ny, nx))

        # セルを追加可能な箇所を列挙する
        for ny, nx in list(border):
            # if (ny == 0 and nx < n) or not (0 <= ny < MAX_Y and 0 <= nx < MAX_X):
            #     continue
            if order[ny][nx] > max_visited:
                # 深さ+1
                visited[ny][nx] = True
                visited_queue.append((ny, nx))
                pre_max_visited = max_visited
                max_visited = order[ny][nx]
                border.remove((ny, nx))

                pre_max_y = max_y
                pre_max_x = max_x
                pre_min_x = min_x
                max_y = max(max_y, ny)
                max_x = max(max_x, nx)
                min_x = min(min_x, nx)

                # 再帰
                dfs(ny, nx, last - 1)

                # 深さ-1
                # 遷移先で付番したものをキャンセルする
                while ordered_queue and (ny, nx) == (ordered_queue[-1][0], ordered_queue[-1][1]):
                    _y, _x, py, px = ordered_queue.pop()
                    order[py][px] = UNORDER
                max_y = pre_max_y
                max_x = pre_max_x
                min_x = pre_min_x
                border.add((ny, nx))
                max_visited = pre_max_visited
                visited_queue.pop()
                visited[ny][nx] = False

    dfs(0, X_CENTER, X_CENTER)

    return ominos


class TestEnumerateNOmino(unittest.TestCase):
    def test_enumerate_n_polyomino(self):
        cases = ((1, 1), (2, 2), (3, 6), (4, 19), (5, 63), (6, 216),
                 (7, 760), (8, 2725), (9, 9910), (10, 36446))
        for i, expect in cases:
            with self.subTest(i=i, expect=expect):
                ominos = enumerate_n_omino(i)
                self.assertEqual(len(ominos), expect)


def main():
    ominos = enumerate_n_omino(3)
    for omino in ominos:
        for row in omino:
            print(''.join(['#' if c else '-' for c in row]))
        print()
    print()
    print('|n|n-omino|elapsed[s]|')
    print('|---:|---:|---:|')
    for i in range(1, 15):
        start = time.time()
        ominos = enumerate_n_omino(i)
        elapsed = time.time() - start
        print('|{}|{}|{:04f}|'.format(i, len(ominos), elapsed))


if __name__ == '__main__':
    main()

上記をenumerate_n_omino.pyとして、python -m unittest enumerate_n_omino.pyで列挙個数を検証する。

__main__での実行結果は3-omino(トロミノ・トリオミノ)6種類を列挙する。

#-
##

#
#
#

##
#-

-#
##

##
-#

###

max_y, max_x, min_xは必要のない領域を切り落とすために再帰前後で値を保持している。(このあたりもう少しスッキリかけると嬉しい。)

前回の記事(ポリオミノの敷き詰め問題をDancingLinksとKnuth's Algorithm Xを使って解く)の内容と合わせて、matsu7874/polyominoにライブラリ化したものを置いている。興味があれば使ってみて欲しい。

参考

f:id:matsu7874:20180730000602p:plain

ポリオミノの敷き詰め問題をDancing LinksとKnuth's Algorithm Xを使って解く

ポリオミノの敷き詰め問題を解くプログラムを書いた。効率的に解くためにDancing Linksというデータ構造を実装し、Knuth's Algorithm Xを使用した。

ポリオミノの敷き詰め問題とは

こういうパズルがAmazonで売っている。結構難しい。

このポリオミノの敷き詰めパズルをプログラムで解くために下記の順番で話を進めていく。

  1. exact cover problem
  2. Knuth's Algorithm X
  3. Dancing Links
  4. ポリオミノの敷き詰め問題をexact cover problemとして考える
  5. 実装

exact cover problemとは

要素の集合XXの部分集合の集合SSの部分集合S*について、 Xのそれぞれ要素がS*の要素のうち1つにだけ含まれるとき、S*Xのexact coverという。 X, Sが与えられるのでexact coverであるS*を列挙したい。

例を示す。

要素の集合 X = {0, 1, 2}
Xの部分集合の集合 S = {
    A: {0},
    B: {1},
    C: {0, 1},
    D: {2},
    E: {0, 2},
}
exact cover S* = {A, B, D}, {B, E}, {C, D}

exact cover problemはNP-completeで全探索するしかないらしい。Karp's 21 NP-complete problems - Wikipedia

Knuth's Algorithm Xの分かりやすい解説

exact cover problemを効率よく全探索するアルゴリズムとして、Knuth's Algorithm Xがある。 アルゴリズムの説明は下記が詳しい。

Knuth's Algorithm Xの要点

どの部分集合を使うか深さ優先探索でBackTrackingして解く。

  1. すべての要素が集まっていれば、それが解。
  2. その要素を持っている部分集合が存在しなければ、1段階戻る。
  3. その要素を持っている部分集合が少ないものを選択する。
  4. 選択された部分集合といずれかの要素が重複している部分集合を候補から外す。
  5. 1に戻る。

実装の一部

def algorithm_x(self):
    """solves exact cover problem

    algorithm_x() -> List[List[int]]
    Exact Coverの部分集合のインデックス列を返す。
    """
    result = []

    def _algorithm_x(sss):
        if self.is_empty():
            result.append(set(sss))
        else:
            # 列の選択
            min_header = self.head.right

            header = min_header
            while header is not self.head:
                # 埋めることができない要素がある場合はpopする
                if header.size == 0:
                    return None
                if header.size < min_header.size:
                    min_header = header
                header = header.right

            column_node = min_header.down
            while column_node is not min_header:
                self.cover(column_node)
                sss.append(column_node.row_index)
                _algorithm_x(sss)
                sss.pop()
                self.uncover(column_node)
                column_node = column_node.down

    _algorithm_x([])
    return result

Dancing Linksとは

上記のKnuth's Algorithm Xを効率よく実行するためのデータ構造。2次元で十字方向にリンクを張った双方向リスト。 Dancing Linksの(1), (2)にある通り、あるノードのをリストから切り離すこと、リストに戻すことは簡単に行える。 特に戻す処理でリンクから切り離された側のノードにどこに戻すべきかの情報が残っているところに感動した。

class Node:
    def __init__(self, row_index, column_index):
        self.row_index = row_index
        self.column_index = column_index
        self.head = self
        self.up = self
        self.down = self
        self.left = self
        self.right = self


class Header(Node):
    """
    columnのheader
    """

    def __init__(self, row_index, column_index):
        super().__init__(row_index, column_index)
        self.size = 0


class DancingLinks:
    """
    上下左右にNodeが連なったデータ構造

    行数: |subsets|
    列数: |collection_size|
    """

    def __init__(self, collection_size, subsets):
        self.n = collection_size
        self.m = len(subsets)

        self.head = Header(-1, -1)
        self.headers = []
        for column_index in range(collection_size):
            header = Header(-1, column_index)
            self.insert_header(header)
            self.headers.append(header)

        for row_index, row in enumerate(subsets):
            if not row:
                continue

            row_header = Node(row_index, row[0])
            column_header = self.headers[row[0]]
            self.insert_to_column(column_header, row_header)

            for column_index in row[1:]:
                node = Node(row_index, column_index)
                column_header = self.headers[column_index]
                self.insert_to_column(column_header, node)
                self.insert_to_row(row_header, node)

    def insert_header(self, header):
        """headerをheadの行に追加する。
        """
        header.right = self.head
        header.left = self.head.left
        self.head.left.right = header
        self.head.left = header

    def insert_to_row(self, row_header, node):
        """行にノードを追加する。
        """
        node.right = row_header
        node.left = row_header.left
        row_header.left.right = node
        row_header.left = node

    def insert_to_column(self, column_header, node):
        """列にノードを追加する。
        """
        column_header.size += 1
        node.head = column_header

        node.up = column_header.up
        node.down = column_header
        column_header.up.down = node
        column_header.up = node

    def cover(self, selected_node):
        """
        selected_nodeを含む行を選択する。
        """
        node = selected_node
        while True:
            # column_headerをheadから外す
            header = node.head
            header.left.right = header.right
            header.right.left = header.left

            column_node = header.down
            while column_node is not header:
                # 選べなくなる要素を列から外していく
                row_node = column_node.right
                while row_node is not column_node:
                    row_node.up.down = row_node.down
                    row_node.down.up = row_node.up
                    row_node.head.size -= 1
                    row_node = row_node.right
                column_node = column_node.down
            node = node.right
            if node is selected_node:
                break

    def uncover(self, selected_node):
        """
        selected_nodeを含む行を選択解除する。
        """

        node = selected_node
        while True:
            # column_headerをheadつなげる
            header = node.head
            header.left.right = header
            header.right.left = header

            column_node = header.down
            while column_node is not header:
                row_node = column_node.right
                while row_node is not column_node:
                    row_node.up.down = row_node
                    row_node.down.up = row_node
                    row_node.head.size += 1
                    row_node = row_node.right
                column_node = column_node.down
            node = node.right
            if node is selected_node:
                break

ポリオミノの敷き詰め問題への適用

ポリオミノの敷き詰め問題(=すべてのポリオミノを1回ずつ使って、すべての領域を敷き詰める問題)をexact cover problemとして解釈したい。

ポイント: 「敷き詰める領域」と「使用したポリオミノ」をXの要素とする。

例として3x3の領域にA,B,Cの3種類のポリオミノを敷き詰めるケースを考える。

...
...
...

A
ooo
o..

B
oo
o.

C
oo

各ポリオミノPiについて、回転・裏返しで作ることができるバリエーションパターン(8通り以下)を列挙しViとする。

バリエーションパターンを得る実装は下記。

class Polyomino:
    def __init__(self, form, name='-'):
        self.name = name
        self.form = copy.deepcopy(form)
        self.y_size = len(self.form)
        self.x_size = len(self.form[0])

    def rotate(self, times=1):
        if times == 1:
            self.form = [[self.form[self.y_size - 1 - x][y]
                          for x in range(self.y_size)] for y in range(self.x_size)]
        elif times == 2:
            self.form = [[self.form[self.y_size - 1 - y][self.x_size - 1 - x]
                          for x in range(self.x_size)] for y in range(self.y_size)]
        elif times == 3:
            self.form = [[self.form[x][self.x_size - 1 - y]
                          for x in range(self.y_size)] for y in range(self.x_size)]

        self.y_size = len(self.form)
        self.x_size = len(self.form[0])

    def reverse(self):
        reversed_form = [[False for j in range(
            self.x_size)] for i in range(self.y_size)]
        for y in range(self.y_size):
            for x in range(self.x_size):
                reversed_form[y][self.x_size - 1 - x] = self.form[y][x]
        self.form = reversed_form

    def generate_variations(self):
        """
        () -> List[Polyomino]
        """
        variations = set()
        p = copy.deepcopy(self)
        for _rev in range(2):
            p.reverse()
            for _rot in range(4):
                p.rotate()
                f = copy.deepcopy(p.form)
                variations.add(json.dumps(f))
        return list(map(lambda f: Polyomino(f, self.name), sorted(list([json.loads(f) for f in variations]))))

各バリエーションパターンViについて3x3の敷き詰め領域のどこに置くことができるか列挙する。

3x3の領域に下記のように付番し、Vijをおける位置を探す。

012
345
678
ooo
o--

置ける位置は下記の2通りになる。

  • (0, 1, 2, 3)
  • (3, 4, 5, 6)

同様に他のバリエーションパターンについても列挙する。 これらのバリエーションパターンは1つの同じポリオミノが置かれる可能性のある位置を示している。 1つのポリオミノは一箇所にしか置けないため、(A,B,C)Xの要素(9,10,11)として扱う。 上記の(0, 1, 2, 3), (3, 4, 5, 6)A=9のポリオミノを置くため、(9, 0, 1, 2, 3), (9, 3, 4, 5, 6)と変換し、これらを部分集合Sの要素として扱う。

これですべてのポリオミノを1回ずつ使って、すべての領域を敷き詰める問題がexact cover problemとして扱うことができるようになった。

結果

...
...
...

A
ooo
o..

B
oo
o.

C
oo
[[10, 4, 5, 8], [9, 0, 3, 6, 7], [11, 1, 2]]
ACC
ABB
AAB
[[10, 1, 2, 4], [9, 0, 3, 6, 7], [11, 5, 8]]
ABB
ABC
AAC
[[9, 0, 1, 3, 6], [11, 2, 5], [10, 4, 7, 8]]
AAC
ABC
ABB
[[10, 2, 4, 5], [9, 0, 1, 3, 6], [11, 7, 8]]
AAB
ABB
ACC
[[11, 7, 8], [9, 0, 1, 2, 5], [10, 3, 4, 6]]
AAA
BBA
BCC
[[11, 3, 6], [9, 0, 1, 2, 5], [10, 4, 7, 8]]
AAA
CBA
CBB
[[10, 4, 6, 7], [11, 5, 8], [9, 0, 1, 2, 3]]
AAA
ABC
BBC
[[11, 6, 7], [10, 4, 5, 8], [9, 0, 1, 2, 3]]
AAA
ABB
CCB
[[9, 5, 6, 7, 8], [10, 0, 3, 4], [11, 1, 2]]
BCC
BBA
AAA
[[9, 1, 2, 5, 8], [11, 6, 7], [10, 0, 3, 4]]
BAA
BBA
CCA
[[10, 0, 1, 4], [11, 3, 6], [9, 2, 5, 7, 8]]
BBA
CBA
CAA
[[10, 0, 1, 4], [11, 2, 5], [9, 3, 6, 7, 8]]
BBC
ABC
AAA
[[11, 0, 3], [9, 1, 2, 5, 8], [10, 4, 6, 7]]
CAA
CBA
BBA
[[11, 0, 3], [9, 5, 6, 7, 8], [10, 1, 2, 4]]
CBB
CBA
AAA
[[10, 2, 4, 5], [11, 0, 1], [9, 3, 6, 7, 8]]
CCB
ABB
AAA
[[9, 2, 5, 7, 8], [11, 0, 1], [10, 3, 4, 6]]
CCA
BBA
BAA

16通りの敷き詰め方法が出力されている。Aの置き方8通り×それぞれのAの置き方についてBの置き方2通り×CはAとBを置くと一意に決まる=16通りなのでこれは正しい。

実装

実装はGitHub/matsu7874/polyomino: ポリオミノの敷き詰め問題を解くプログラムに置いてある。

パズル紹介

チョコレートパズル(easy)とプラパズル(hard)は2次元でソーマキューブは3次元(very hard)。

urllibとBeautifulSoupでスクレイピングするときに調べたこと

スクレイピングをするときに調べたことを中心に使い方をまとめる。
Python3.6でリクエスト部分にurllib、パース部分にBeautifulSoup4を使って行った。
基本的なことはQuick Start — Beautiful Soup 4.4.0 documentationに書いてあるので、まず読め。

準備

Installing Beautiful Soup — Beautiful Soup 4.4.0 documentation

pip install beautifulsoup4

呼び出し方

pipではbeautifulsoup4でインストールするが、importはbs4

from bs4 import BeautifulSoup
soup = BeautifulSoup(html, 'html.parser')

パーサーの指定についてはこの記事を参照するといい。

htmlをリクエストするところ

import urllib.error
import urllib.request

def get_html(url):
    try:
        with urllib.request.urlopen(url) as response:
            html = response.read()
    except urllib.error.URLError as e:
        print(e, url)
        html = ''
    return html

単要素の取得はfind()を使う

引数の説明などはfind_all()関数の説明に書いてある。下記のように使える。
返り値は<class 'bs4.element.Tag'>(概要はここ)かNoneになる。

hoge = soup.find(id='hoge')
fuga_text = hoge.find('span', class_='fuga').string

複数要素の取得にはselect()とfind_all()を使う

どちらも返り値は配列。

  • find_all()はキーワードごとに値を指定する形式
  • select()CSS selectorを指定する形式

<div class="hoge fuga">のように複数の値が定義されている場合、find_all(class_='fuga hoge')と指定するとclassの順番が異なるために漏らしてしまう。select('.fuga.hoge')で指定すれば<div class="hoge fuga"><div class="fuga hoge">も両方取得できる。

findは要素が見つからないとNoneを返す

AttributeError: 'NoneType' object has no attribute 'get'とか言われがち。終了させたくない場合は例外書いておく。

try:
    href = soup.find('a.hoge').get('href')
except AttributeError:
    href = ''

その他

  • 呼び出しはtime.sleep(1)つけて毎秒1リクエストにする。
  • 途中で落ちるのが嫌なので、どこまで進んだかログに吐きましょう。

Python3, C++11, JavaScriptでの実行時間計測

Python3, C++11, JavaScriptでの実行時間計測

C++とJSを勉強中なのですが、Python,C++,JSの3言語で実行時間計測をどのように書くのか調べてみました。
サンプルとして$109$回足し算をする時間を計測します。

Python3

まずは普段使っているPython。バージョンはPython 3.5.1。

import time

def DoSomething():
    a = 0
    for i in range(1000000000):
        a += 1

START_TIME = time.time()

DoSomething()

ELAPSED_TIME = time.time() - START_TIME
print('Elapsed Time:', ELAPSED_TIME*1000 ,'[ms]')

C++11

バージョンはg++ (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 5.1.0。
g++ -std=c++0xコンパイルしました。(最適化オプションは付けていません。)

#include <iostream>
#include <chrono>

long DoSomething(){
    long a=0;
    for(int i = 0; i<1000000000; ++i){
        ++a;
    }
    return a;
}

int main(){
    std::chrono::system_clock::time_point  start, end;
    start = std::chrono::system_clock::now();

    DoSomething();

    end = std::chrono::system_clock::now();
    double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    std::cout << "Elapsed Time: " << elapsed << "[ms]" << std::endl;

}

JavaScript

最近競プロで使っているのですが、まだ速いのか遅いのか分からない言語。環境はNode.js v4.4.4。

function DoSomething() {
    var a;
    for (var i = 0; i < 1000000000; ++i){
        ++a;
    }
}

var start = new Date().getTime();

DoSomething();

var end = new Date().getTime();
console.log("Elapsed Time: " + (end - start) + "[ms]");

結果

C++Pythonの40倍速い!
JSもPythonの25倍速い!

言語 実行時間[ms] Python
Python3 91966 1
C++ 2216 0.0241
JS 3646 0.0396

これは強い人がC++を使うのも全くよく分かります。

追記

関数を渡すと実行時間と戻り値を表示してくれる関数にした方が使いやすいので、書き換えました。

def measure_elapsed_time(func):
    START_TIME = time.time()

    res = func()

    ELAPSED_TIME = time.time() - START_TIME

    print('Elapsed Time:', ELAPSED_TIME*1000 ,'[ms]')
    print(res)

if __name__ == '__main__':
    measure_elapsed_time(DoSomething)
template<class F>
void MeasureElapsedTime(F func){
    std::chrono::system_clock::time_point  start, end;
    start = std::chrono::system_clock::now();

    auto res = func();

    end = std::chrono::system_clock::now();
    double elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end-start).count();
    std::cout << "Elapsed Time: " << elapsed << "[ms]" << std::endl;
    std::cout << res << std::endl;
}

int main(){
    MeasureElapsedTime(DoSomething);
}
function measureElapsedTime(func){
    var start = new Date().getTime();

    var res = func();

    var end = new Date().getTime();
    console.log("Elapsed Time: " + (end - start) + "[ms]");
    console.log(res);
}

measureElapsedTime(DoSomething);

Anacondaで環境構築するときの一例

Anaconda便利ですね。WindowsでPython使うとインストールが不便なパッケージがたくさんあって困ります。 Anaconda使いましょう。 今回はソースコードを整形してくれるautopep8を使いたかったので環境を新しく作ってインストールしてみました。

conda create -n py35 python=3.5 anaconda
activate py35
conda install pep8
conda install pip
pip install autopep8
conda list --export > conda_requirements.txt
deactivate

ツイート時刻から生活リズムの可視化をする

もう10年も前の映画になるらしいですが、デスノートでLが被害者の死亡時刻を重ねると容疑者が大学生であることを割り出すシーンがあったのを覚えていますか?
あるいはGitHubでPunch cardって出るじゃないですか。なんとなく格好良くて好きです。
同じようなことがtweet時刻でも出来るのではないかと考え、tweet時刻から生活リズムが見えるか調べてみました。

やったこと

  • Twitterからツイートの投稿時間を取得
  • 曜日・時刻ごとに集計
  • ヒートマップにして表示

結果

  • 人によっては明白に寝てる時間が分かる。(社会人はちゃんと夜ちょっと寝ている)
  • 私の寝ている時間もなんとなく分かる。
  • 冬休みを挟んでしまっているので時間割までは分からない。 f:id:matsu7874:20160122131904p:plain

Twitterからtweet時刻の取得

  • GitHub - sixohsix/twitter: Python Twitter APIを使うと簡単。
  • ツイートの投稿時間は'create_at'です。
  • 'user'の'create_at'と間違えないように気をつけます。
  • 1アクセスで200tweet、最大3200tweetまでしか取得できません。ケチ!

集計

  • 時刻がUTCで与えられますので、Asia/Tokyoにします。

可視化

ソースコード

twitter apiの鍵情報はよく使うので以下の形で別ファイルにまとめている。

# twitter_keys.py
KEYS = {'consumer_key': 'xxxxxxxxxxxxxxxxxxxxxxxxx',
        'consumer_secret': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
        'access_token': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
        'access_secret': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
        }
import twitter
from twitter_keys import KEYS
import seaborn


days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']

t = twitter.Twitter(auth=twitter.OAuth(token=KEYS['access_token'], token_secret=KEYS['access_secret'],
                                       consumer_key=KEYS['consumer_key'], consumer_secret=KEYS['consumer_secret']))

users = ['matsu7874']
for username in users:
    timelog = [[0 for j in range(24)] for i in range(7)]
    total_tweets = 0
    latest_tweet = t.statuses.user_timeline(screen_name=username, count=1)[0]
    max_id = int(latest_tweet['id_str'])
    latest_date = latest_tweet['created_at']
    for i in range(16):
        response = t.statuses.user_timeline(screen_name=username, count=200, max_id=max_id)
        if response == []:
            break
        max_id = int(response[-1]['id_str']) - 1
        oldest_date = response[-1]['created_at']
        for tweet in response:
            s = tweet['created_at'].split()
            day = days.index(s[0])
            h = int(s[3].split(':')[0]) + 9
            if h >= 24:
                h %= 24
                day = (day + 1) % 7
            timelog[day][h] += 1
    for i in range(7):
        print(timelog[i])
        total_tweets += sum(timelog[i])
    seaborn.set()
    ax = seaborn.heatmap(timelog)
    title = str(username) + ' tweet log (n=' + str(total_tweets) +')\n'
    title += 'From ' + oldest_date + '\nTo ' + latest_date
    seaborn.plt.title(title)
    seaborn.plt.ylabel('day')
    seaborn.plt.xlabel('time(Asia/Tokyo)')
    seaborn.plt.yticks(range(7), list(reversed(days)))
    seaborn.plt.savefig(str(username) + '_log.png', dpi=300)

ページング

Working with Timelines | Twitter Developersに書いてあるように、
max_idを直前に処理したツイートのid(_str)-1をmax_idに指定してやれば上手く動きます。

感想

綺麗に真っ白の帯が出る生活がしたい。

もっと気持よくyukicoderで遊ぶためにテストケースを自動実行する。

solorab.net
このブログを読んだんですが、謎の呪文が1行だけ貼ってあって????ってなりました。

テストケースは自動で確かめる

ちょうどTwitterでも強い人はテストケースを自動実行しているようだということを見ていたので、自分もオシャレに自動でテストケース実行したいぞ!と思い、yukicoderからテストケースを一括でダウンロードさせていただいて、手元で一括実行することにしました。めっちゃ気持ちいいです。

yukicoderといえば、スマートサブミットや入力サンプルをコピーボタンがあって、めっちゃ使いやすい競技プログラミング練習サイトですね。ヘルプ - yukicoder

めっちゃ使いやすいyukicoderの唯一不便なところは、一覧の所に出てくるナンバリングとURLのナンバリングが異なる(リダイレクト機能はある)点で、手元でデータを管理するときにどちらかに揃えて管理したいのです。
私は問題一覧の所に出てくるナンバリングが好きなのでそちらに合わせます。

必要なもの

  • Python3.5(最新)
  • urllib3(pipでinstall)
  • beautifulsoup4(pipでinstall)

使い方

  1. 必要なものをインストールする
  2. ↓のソースを適当に名前をつけて保存(y.pyとか)
  3. python y.py 0320あるいはpython y.py 320どっちでも320は問題No.320 眠れない夜に - yukicoderに対応。
  4. 初回はテストケースがどかどか―っと./0320_test/にダウンロードされる。
  5. 各テストケースにたいして0320.py./0320_test/test_in/の各テストケースに対して実行
  6. ./0320_test/test_out/の各テストケースと比較してACとかWAとかTLE(とりあえず5秒)を表示
  7. ACだと気持ちいいい

f:id:matsu7874:20151231012733p:plain

ソース

import sys
import os


def download_testcase(problem_number):
    import urllib3
    import bs4
    http = urllib3.PoolManager()
    url = 'http://yukicoder.me/problems/no/' + str(problem_number)
    contents = http.urlopen('GET', url).data
    soup = bs4.BeautifulSoup(contents.decode('utf-8'), 'html.parser')

    problem_number = ('0000' + str(problem_number))[-4:]
    problem_id = soup.find('div', id='content').get('data-problem-id')
    url = 'http://yukicoder.me/problems/' + str(problem_id) + '/testcase.zip'

    filename = str(problem_number) + '-' + str(problem_id) + '-testcase.zip'

    data = http.urlopen('GET', url)
    if data.status == 200:
        f = open(filename, 'wb')
        f.write(data.data)
        f.close()
    return filename


def unzip(filename, place):
    import zipfile
    if not os.path.exists(place):
        os.mkdir(place)
    zf = zipfile.ZipFile(filename, 'r')
    for f in zf.namelist():
        fn = os.path.basename(place + f)
        dir_name = (place + f).replace(fn, '')
        if not os.path.exists(dir_name):
            os.mkdir(dir_name)

        uzf = open(place + f, 'wb')
        uzf.write(zf.read(f))
        uzf.close()
        print(place + f)
    zf.close()


def run_test(python_file, test_path):
    import time
    import subprocess

    testcases = os.listdir(test_path + 'test_in/')
    if not os.path.exists(test_path + 'my_out/'):
        os.mkdir(test_path + 'my_out/')

    count_testcase = len(testcases)
    count_AC = 0
    count_WA = 0
    count_TLE = 0

    for case in testcases:
        input_file = test_path + '/test_in/' + case
        expected_file = input_file.replace('test_in', 'test_out')
        output_file = input_file.replace('test_in', 'my_out')

        cmd = ['python', python_file, '<', input_file, '>', output_file]

        start_time = time.time()
        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        try:
            stdout_data, stderr_data = p.communicate(timeout=5)
            stdout_data = stdout_data.decode('utf-8')
            stderr_data = stderr_data.decode('utf-8')
        except subprocess.TimeoutExpired:
            p.kill()
            count_TLE += 1
            status = 'TLE'
        except Exception as e:
            raise
        else:
            cmd = ['diff',  expected_file, output_file]
            cp = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            if '' == cp.stdout.decode('utf-8'):
                status = 'AC'
                count_AC += 1
            else:
                status = 'WA'
                count_WA += 1
        elapsed_time = time.time() - start_time
        print(status, str(round(elapsed_time, 7)) + '[s]', end=' ')
        print(input_file.replace(test_path + '/test_in/', ''))

    print('AC', count_AC, '/', count_testcase)
    print('WA', count_WA, '/', count_testcase)
    print('TLE', count_TLE, '/', count_testcase)


if __name__ == '__main__':
    argvs = sys.argv
    problem_number = ('0000'+argvs[1])[-4:]
    current_directory = os.getcwd()
    python_file = '.\\' + problem_number + '.py'
    path = (current_directory + '\\' + problem_number + '_test\\')
    if not os.path.exists(path):
        zip_name = download_testcase(problem_number)
        unzip(zip_name, path)
    run_test(python_file, path)

苦労した所