PythonのライブラリをC++で作成する(3)

いよいよ本題、C++でMatrixの掛け算をするPythonのライブラリを作ります。

まず、リストをstd::vectorに変換します。

PyObject *mul(PyObject *self, PyObject *args) {
    PyObject    *obj1, *obj2;
    long    D;
    if(!PyArg_ParseTuple(args, "OOl", &obj1, &obj2, &D))
        ...

まず、このように引数のタプルからPyObjectを取り出します。このobj1、obj2がリストになっています。そして、このリストから要素を取り出します。以下は、リストの要素が整数の場合です。

    const int   M = PyList_Size(obj_list);
    for(int j = 0; j < M; ++j) {
        PyObject    *obj = PyList_GetItem(obj_list, (Py_ssize_t)j);
        const long  d = PyLong_AsLong(obj);
        v.push_back(d);     // std::vector<long>
    }

タプルの取り出し方とだいたい同じですね。
Matrixはこれを2層で行えばいいだけです。簡単ですね。

次に、std::vectorPythonのリストに変換します。

    const size_t    L = v.size();   // v : std::vector<long>
    PyObject    *obj_list = PyList_New(L);
    for(size_t i = 0U; i < L; ++i)
        PyList_SET_ITEM(obj_list, i, PyLong_FromLong(v[i]));
    return obj_list;

長さLのリストオブジェクトを作り、longをPyObjectに変換して、リストにセットします。

リストオブジェクト - Python 3.9.1 ドキュメント

コードは、

# setup.py
from distutils.core import setup, Extension

setup(name = 'mymatrix', version = '1.0.0', \
    ext_modules = [Extension('mymatrix', ['matrix.cpp'])])
// matrix.h
#include <Python.h>
#include <vector>

typedef std::vector<long>   Vec;
typedef std::vector<Vec>    Matrix;

static Vec make_vector(PyObject *obj_list);
static Matrix make_matrix(PyObject *args);
static PyObject *mul(PyObject *self, PyObject *args);
static Matrix mul_core(const Matrix& A, const Matrix& B, long D);
static Matrix transpose(const Matrix& A);
static PyObject *make_python_matrix(const Matrix& A);
static PyObject *make_python_vector(const Vec& v);
// matrix.cpp
#include <iostream>
#include "matrix.h"

using namespace std;

Vec make_vector(PyObject *obj_list) {
    if(!PyList_Check(obj_list))
        return Vec(0);
    
    Vec v;
    const int   M = PyList_Size(obj_list);
    for(int j = 0; j < M; ++j) {
        PyObject    *obj = PyList_GetItem(obj_list, (Py_ssize_t)j);
        if(!PyLong_Check(obj)) {
            printf("vector[%d] is not long.\n", j);
            return Vec(0);
        }
        
        const long  d = PyLong_AsLong(obj);
        v.push_back(d);
    }
    
    return v;
}

Matrix make_matrix(PyObject *obj_table) {
    if(!PyList_Check(obj_table))
        return Matrix(0);
    
    const int   L = PyList_Size(obj_table);
    Matrix  X(L);
    for(int i = 0; i < L; ++i) {
        PyObject    *obj = PyList_GetItem(obj_table, (Py_ssize_t)i);
        Vec v = make_vector(obj);
        if(v.empty())
            return Matrix(0);
        X[i] = v;
    }
    
    return X;
}

PyObject *mul(PyObject *self, PyObject *args) {
    PyObject    *obj1, *obj2;
    long    D;
    if(!PyArg_ParseTuple(args, "OOl", &obj1, &obj2, &D))
        return make_python_matrix(Matrix(0));
    
    const Matrix    A = make_matrix(obj1);
    const Matrix    B = make_matrix(obj2);
    Matrix  C = mul_core(A, B, D);
    return make_python_matrix(C);
}

Matrix mul_core(const Matrix& A, const Matrix& B, long D) {
    const size_t    H = A.size();
    const size_t    M = B.size();
    const size_t    W = B.front().size();
    
    Matrix  C(H, Vec(W));
    for(size_t i = 0U; i < H; ++i) {
        for(size_t j = 0U; j < W; ++j) {
            for(size_t k = 0U; k < M; ++k)
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % D;
        }
    }
    return C;
}

PyObject *make_python_matrix(const Matrix& A) {
    const size_t    H = A.size();
    PyObject    *obj_mat = PyList_New(H);
    for(size_t i = 0U; i < H; ++i) {
        PyObject    *obj_list = make_python_vector(A[i]);
        PyList_SET_ITEM(obj_mat, i, obj_list);
    }
    return obj_mat;
}

PyObject *make_python_vector(const Vec& v) {
    const size_t    L = v.size();
    PyObject    *obj_list = PyList_New(L);
    for(size_t i = 0U; i < L; ++i)
        PyList_SET_ITEM(obj_list, i, PyLong_FromLong(v[i]));
    return obj_list;
}

static PyObject *print_matrix(PyObject *self, PyObject *args) {
    PyObject    *obj_table;
    if(!PyArg_ParseTuple(args, "O", &obj_table))
        return make_python_matrix(Matrix(0));
    
    Matrix  X = make_matrix(obj_table);
    for(auto p = X.begin(); p != X.end(); ++p) {
        for(auto q = p->begin(); q != p->end(); ++q)
            cout << *q << ' ';
        cout << endl;
    }
    
    return PyLong_FromLong(0);
}

static PyMethodDef mymatrixMethods[] = {
    { "print_matrix", print_matrix, METH_VARARGS },
    { "mul", mul, METH_VARARGS },
    { NULL }
};

static struct PyModuleDef mymatrix = {
    PyModuleDef_HEAD_INIT, "mymatrix", "Python3 C API Module",
    -1, mymatrixMethods
};

PyMODINIT_FUNC PyInit_mymatrix() {
    return PyModule_Create(&mymatrix);
}

Python側は、

import mymatrix

def mat_pow(M, e, D):
    if e == 1:
        return M
    elif e % 2 == 1:
        return mymatrix.mul(M, mat_pow(M, e-1, D), D)
    else:
        A = mat_pow(M, e/2, D)
        return mymatrix.mul(A, A, D)

ここが最初のコードと違うだけです。

実行してみましょう。

1134
256330968
206.85379028320312

そんなに変わりませんね。なぜでしょう。工夫する余地が2つあります。

PythonのライブラリをC++で作成する(2)

C++で簡単なPythonのライブラリを作ります。
整数同士を掛け算する関数です。

さっそくコードを示します。

# setup.py
from distutils.core import setup, Extension

setup(name = 'mymath', version = '1.0.0', \
    ext_modules = [Extension('mymath', ['mymath.cpp'])])
// mymath.cpp
#include <Python.h>

static PyObject *mul(PyObject *self, PyObject *args) {
    int a, b;
    if (!PyArg_ParseTuple(args, "ii", &a, &b))
        return NULL;
    
    const int   c = a * b;
    return Py_BuildValue("i", c);
}

static PyMethodDef mymathMethods[] = {
    { "mul", mul, METH_VARARGS },
    { NULL }
};

static struct PyModuleDef mymath = {
    PyModuleDef_HEAD_INIT, "mymath", "Python3 C API Module",
    -1, mymathMethods
};

PyMODINIT_FUNC PyInit_mymath() {
    return PyModule_Create(&mymath);
}

ポイントは型変換です。
Pythonからやってくる値はPyObjectで、返す値もPyObjectなので、PyObjectをintに変換して、計算した結果のintをPyObjectに変換します。

まず、PyObjectをintに変換する部分です。

    int a, b;
    if (!PyArg_ParseTuple(args, "ii", &a, &b))
        return NULL;

タプルなので、PyObjectを一つずつ取り出して、それをintにしてもよいです。

    PyObject    *obj1 = PyTuple_GetItem(args, 0);
    PyObject    *obj2 = PyTuple_GetItem(args, 1);
    if(!PyLong_Check(obj1))
        return Py_None;
    if(!PyLong_Check(obj2))
        return Py_None;
    const int   a = PyLong_AsLong(obj1);
    const int   b = PyLong_AsLong(obj2);

タプルオブジェクト (tuple object) - Python 3.9.1 ドキュメント
整数型オブジェクト (integer object) - Python 3.9.1 ドキュメント

上のように、PyArg_ParseTupleを使うと一度にintを2つ取り出せます。
PyArg_ParseTupleの第2引数がどの型に変換するかを指定しています。"ii"だと2つともintに変換します。

引数の解釈と値の構築 - Python 3.9.1 ドキュメント

実行してみましょう。

$ python
Python 3.7.4 (default, Aug 13 2019, 20:35:49)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mymath
>>> mymath.mul(2, 3)
6

次の例だと、

>>> mymath.mul(100000, 100000)
1410065408

32ビット符号付き整数なので、ちゃんとオーバーフローしていますね。

>>> (100000*100000+2**31)%2**32-2**31
1410065408

あってます。

PythonのライブラリをC++で作成する(1)

PythonのライブラリをC++で作ります。
例えば、次のような問題を考えます。

H×Wの長方形があり、そこに1×2の長方形を隙間なく敷き詰めます。敷き詰め方は何通りあるでしょう。

2×3なら、

f:id:inamori:20210106195416p:plain

の2通りです。
さて、この問題はどのように解くでしょう。この手の問題は、状態遷移を考えます。左から順に詰めていきます。そして、縦に見て最も左が空いている場所に長方形を埋めます。最も左が空いている場所が複数あれば、上から詰めます。空の状態から一つ長方形を詰めた状態は次の2つのみです。

f:id:inamori:20210106200031p:plain

この時、全て埋まっている列は消した状態を考えます。H = 3で考えて、例えば、

f:id:inamori:20210106201615p:plain

のようにします。これを(2, 0, 1)、もしくはもう少し簡単に201と表します。000から始まって、HW/2個長方形を敷いて000になっていたらH×Wが隙間なく敷き詰められたことになります。

状態遷移は、

000 -> 200, 110
200 -> 220, 100
220 -> 000
110 -> 001
100 -> 120, 000
001 -> 201, 000
120 -> 011
201 -> 110
011 -> 100

となり、これを行列にして、この行列のW/2乗を計算すればいいわけです。

コードは次のようになりました。

# coding: utf-8
from collections import Counter
import sys
import time


#################### Matrix ####################

def mat_mul(A, B, D):
    H, M, W = len(A), len(B), len(B[0])
    C = [ [0] * W for _ in range(H) ]
    for i in range(H):
        for j in range(W):
            C[i][j] = int(sum(A[i][k] * B[k][j] for k in range(M)) % D)
    return C

def mat_pow(M, e, D):
    if e == 1:
        return M
    elif e % 2 == 1:
        return mat_mul(M, mat_pow(M, e-1, D), D)
    else:
        A = mat_pow(M, e/2, D)
        return mat_mul(A, A, D)


#################### process ####################

def f(H, W, D):
    def normalize(diagram):
        min_l = min(diagram)
        return tuple(row - min_l for row in diagram)
    
    def nexts(diagram):
        y = next(y for y, row in enumerate(diagram) if row == 0)
        yield diagram[:y] + (2,) + diagram[y+1:]
        if y+1 < H and diagram[y+1] == 0:
            yield diagram[:y] + (1, 1) + diagram[y+2:]
    
    def walk(diagram):
        for diagram1 in nexts(diagram):
            diagram2 = normalize(diagram1)
            if diagram2 not in set_diagram:
                set_diagram.add(diagram2)
                walk(diagram2)
    
    def make_graph(H):
        graph = Counter()       # { (orig, dest): counter }
        for diagram in set_diagram:
            for diagram1 in nexts(diagram):
                graph[(diagram, normalize(diagram1))] += 1
        return graph
    
    def make_matrix(H):
        graph = make_graph(H)
        M = [ [ graph[(orig, dest)] for orig in set_diagram ]
                                    for dest in set_diagram ]
        row_to_index = dict((diagram, i) for i, diagram in enumerate(set_diagram))
        return (M, row_to_index)
    
    init_diagram = (0,)*H
    set_diagram = set([init_diagram])
    walk(init_diagram)
    print(len(set_diagram))
    M, row_to_index = make_matrix(H)
    M_pow = mat_pow(M, W*H/2, D)
    i = row_to_index[init_diagram]
    return M_pow[i][i]


#################### main ####################

t0 = time.time()
D = 10**9+7
H = int(sys.argv[1])
W = int(sys.argv[2])
print(f(H, W, D))
print(time.time() - t0)

これを9×100, D = 10**9 + 7で計算してみましょう。

1134
256330968
1756.864093542099

最初の1134は、行列の大きさです。約30分かかっていますね。競技プログラミングでは、こんなに行列が大きくなることはふつうないです。
さて、計算時間はこの行列の掛け算がほとんど全てです。この手のリストを使う計算は、PyPyにすると速くなります。

1134
256330968
277.08021235466003

6.3倍速くなりました。
Project Eulerでは、PyPyは事実上の標準です。PyPyを使いましょう。
もっと速くするために、C++によるライブラリを作ります。

Microsoft Solitaire Collection

Windowsユーザにはおなじみのゲームかと思うが、この中にイベントというのがあって、大勢が24時間以内に一斉に行ってスコアとタイムを競う。
これに初めてちゃんと参加したところ59万人中で25位だった。本当だろうか。
全ての面をクリアすると、最終的にはタイムの比較になるので、考えている暇はない。5つのカードゲームが得意でなければならないが、一つ苦手なゲームがあるので、これをやりこまないといけない。