ライブラリ(ほぼ)全解説

はじめに

2020/09/14時点での僕の所持しているライブラリを(一部を除いて)解説していきます
各題目をクリックするとライブラリの該当ページに飛べるので、興味があればどうぞ(verify以上の動作保証はしません)

ライブラリ

avl

  • 遅延評価のないinsert/eraseベースのAVL
  • ノード管理はnewで行う
  • rotate関連をtemplateでやってる事ぐらいしか書くことがない

avl_map

  • std::mapと同じ役割を持つAVL
  • ノード管理は配列+index
  • メモリ使用量が少なく、軽い

binaryindexedtree

  • BIT
  • 特に言うことなし

binarylifting

  • 木のダブリング
  • LCAを返してくれるので、HLDを使うほどではない時に貼ったりする

bitvector

  • conpactなbitvector
  • access/rankのみ
  • waveletmatrixの内部で使う用

compression

  • 座標圧縮
  • 割と使うけど面倒でverifyしていない

convexhulltrick

  • CHT
  • 単調性などを問わず、だいたいの操作にlogNかける実装

dinic

  • 最大流
  • 特に書く事がない

disjointsparsetable

  • Disjoint Sparse Table
  • 脳死で累積和の代わりに貼る事がある

dynamicbitvector

  • 動的なビットベクトル
  • 葉にboolを32個ぐらい詰め込む典型テクをしているのでメモリに優しい
  • バグってる可能性あり

dynamiclazysegmenttree

  • 必要な時にノードを作る遅延セグ木
  • BBSTでいいので使わない

dynamicsegmenttree

  • 必要な時にノードを作るセグ木
  • BBSTでいいので使わない

dynamicwaveletmatrix

  • 動的WaveletMatrix
  • 計算量が重く、メモリも辛い
  • 実装でどこかバグってるらしい
  • 怖いので使いたくないし書き直したくない

eertree

  • eerTree
  • 回文関連がだいたいこれで殴れる

eulertour

  • 前処理を線形時間で行って部分木判定をするやつ
  • オイラーツアーって名前がかなり良くない
  • 使った記憶がない

hashmap

  • 自前ハッシュマップ
  • 速いので時々使う

heavylightdecomposition

  • HLD (heavy path decompositionと書いた方がいい?)
  • よく使う
  • vector<pair<int,int>>のペアで区間を返して、非可換クエリにも対応する

lazysegtree

  • 抽象化がされている遅延セグ木
  • よくある典型的な非再帰実装

lazyskiplist

  • 区間加算などが可能なスキップリスト
  • 挿入削除区間和等、だいたい平衡二分木と同じ操作が可能
  • 定数倍が重くてかなり使いづらい(BBSTの方がよい)

lineartimesparsetable

  • 線形時間構築のSparseTable
  • 線形時間だと言い張るためにこういうデータ構造をよく使う事がある

lowlink

  • 橋と関節点の列挙

matrix

  • 行列ライブラリ
  • あまり機能が多くない、行列累乗に使えるのでまぁこれでも十分

memorypool

  • メモリプール
  • ノード確保をstatic配列にしたりglobalに書いたりするのが嫌なので書いた
  • __buitin_clzでアクセスしてるのがアホでかなり遅い
  • 使う時毎回書き換えてる、いつか消したい負債

mo

  • Mo's algorithm
  • バグらせやすいのであると便利(後述するmo_queryと組み合わせる)

mo_query

  • クエリ情報を渡すとMo's algorithmを走らせてくれる
  • 全クエリ情報がまとめて手に入って便利
  • インターフェースが使いやすくかなり気に入ってる

modint

  • 普通のModInt
  • 逆元やべき乗は別ライブラリに投げてる

persistentdynamiclazysegmenttree

  • 永続動的遅延伝播セグメント木
  • だいたいの場合永続遅延伝播赤黒木でなんとかなるので使わない

persistentunionfind

  • 部分永続UF
  • あまり使わない

primaldual

  • 最小費用流
  • ときどき使う事がある

redblacktree

  • 普通の赤黒木
  • ノード管理はmemorypool(後述する赤黒系は全てそう)
  • merge/splitベース
  • ランダムアクセス、区間和等 よくあるクエリは捌ける

redblacktree_lazy

  • 遅延伝播赤黒木
  • 普通に使う(memorypoolのせいでちょっと遅いので使う時は適宜修正する)

redblacktree_persistent

  • 永続赤黒木
  • コピー&ペースト、PCK20予選10等 持っていて損はない
  • 人々が思ってるより実装が軽い おすすめ

redblacktree_persistent_lazy

  • 永続遅延伝播赤黒木
  • グラフではないを解くのに使える

redblacktree_sset

  • std::setの機能の赤黒木
  • ランダムアクセス付きsetとして使う

rerooting

  • 全方位木DP
  • 気づいたら使い方が分からなくなっていた

rollinghash

  • ロリハ
  • 109+7,109+9をmodにする事が多い
  • まだ実戦で落ちた経験はないのでこのまま使うと思う

scc

  • 強連結成分分解
  • PCKとかICPC系で使う印象

scc_dag

  • sccをしてからdagへの変換までやってくれる
  • 返ってくる構造体が親切で使いやすいと思ってる

segmentset

  • 区間をsetで管理するやつ
  • 使う頻度が低いので毎回使用方法を忘れる

segmenttree

  • セグ木
  • よくある非再帰実装 特に言うことなし

skiplist

  • スキップリスト
  • 実は平衡二分木にできる操作がだいたいできる(あまり知られてない?)
  • 定数倍が遅い

skiplist_sset

  • スキップリストでのset実装
  • あえて使う事はない

sparsetable

  • 普通のSparseTable
  • まぁ時々使う(LCPのテーブルとかに使う事が多い気がする)

splaytree_sset

  • splay tree
  • 速いしインターフェースも分かりやすいので割と使う

substrmatching

  • SA-ISと文字列マッチングの詰め合わせ
  • 時々使うかも

suffixtree

  • SuffixTree+SA+ISA+LCPのいつものやつ
  • これがあればだいたい事足りると思う

treap

  • 普通のTreap
  • merge/splitベース
  • 最近はあまり使わない

trie

  • 普通のtrie
  • インターフェースがカスなので多分今後一度も使わない
  • 結局直書きする事になりがち

twothreetree

  • 2-3木
  • 場合分けがとても多くて実装が辛かった
  • 絶対使わない

twoedgeconnectedcomponents_tree

  • 二重辺連結成分分解をして木にしてくれる
  • 実装で困る事が多いのであると嬉しい

unionfind

  • UF
  • だいたいの機能が網羅されてる
  • よく使う

vanemdeboastree

  • vEB木 いわゆる謎木
  • 実装がシンプルで実際に速い
  • 空間計算量の都合で220ぐらいまでしか使えない

vanemdeboastree2

  • 子をhashmapで管理するvEB木
  • 実測でもsetより速い
  • 値域制約がないのでかなり実用性がある

wavletmatrix

  • succinctではないWaveletMatrix
  • いろいろ使えるし応用もできて好き

xfasttrie

  • x-fast trie
  • 使わない

xfastrie_yft

  • yftの土台用のx-fast trie
  • y-fast trieの土台以上の価値はない

yfasttrie

  • 乱数でBBSTに挿入するかを決めるy-fast trie
  • vEBより遅いので使わない

yfasttrie2

  • 一定サイズでBBSTのmerge/splitをするy-fast trie
  • 前者よりは速い
  • vEBより遅いので使わない

zdd

  • ZDD
  • 競プロで使わない事に気づいて整備をやめた

おわりに

整備しても一生使わないデータ構造が大変を占めていて、かなしくなった

昼食

昼食

 最近はよくBASE BREADを食べています 学内で食べる時は必ずこれです(週に3回ぐらい食べてる)

f:id:shibh308:20200710231932p:plain
完全パン

完全食

 完全食を食べると人間に必要な栄養素がほぼ全部摂取できて、完全になれます

種類

粉とかグミとかパンとかパスタとかいろいろあります

粉のやつ

 時短目的なのにシェイカーで混ぜる手間がかかるの、つらそう(完全食初心者)

グミのやつ

  • 食べる手間がかからない
  • 食べても眠くならない気がする

のでよさそうなんですけど、

  • ちょっと高い
  • 必要な摂取量が多い
  • めっちゃ硬い
  • あまりおいしくない(個人の感想)

のでちょっと微妙そう

パスタのやつ

 完全食なのに食べる手間がめっちゃかかるの、大変そう(エアプ)

パンのやつ

 すぐ食べられるので手軽度がめっちゃ高くて、そこそこ安い(一食400円で完全になれる)
二種類の味があって、チョコはチョコ風味でうれしい(プレーンの方はほぼ無味で自分の唾液がうまく感じる)
基本的に密度の高い雑穀パンみたいな感じで、食感は湿ったダンボールに近かったです。

それ以外

 知りません・・・

感想

 正直あまり美味しくないんですが、「これ食ってれば完全だし・・・」みたいな感じで食べられてうれしいです 一食がパン2個だけで済むので食事にかかる時間が減ってよかった

おすすめリンク

basefood.co.jp

継続購入初回は20%オフでいつでも解約/配達日時変更可能!

スキップリストで区間を管理するやつ

はじめに

 スキップリストで区間の管理をする方法が載ってそうな記事があまりなかったので書きました(雑記事です)

できること

スキップリストは

とかができます。あと永続化もできるっぽいんですが僕はやってません・・・

スキップリスト自体の概要

 Wikipediaに載ってます

ランダムアクセスの方法

 Wikipediaに載ってます

区間管理の方法

 下のようなスキップリストに区間minの情報を持たせる事を考えます。

f:id:shibh308:20200406135001p:plain
スキップリストの例
 このリストからノード部分だけを切り出すとこんな感じにかけて、
f:id:shibh308:20200406135012p:plain
ノード部分を切り出したやつ
 空いてる部分を横に伸ばすとこんな感じになります。
f:id:shibh308:20200406135008p:plain
区間を持たせたやつ
 各ノードごとに下の段の値の最小値を持たせると、こうなります。
f:id:shibh308:20200406135005p:plain
区間minを持たせたやつ

 こうするとなんかセグメント木っぽく見えて、実際セグ木っぽい感じで下図のように区間取得ができます。

f:id:shibh308:20200406140506p:plain
区間 \rm [2, 7]を取得する例

 もう書く事がないのでこの話はここでおわりです・・・

実装

 実装を、しました・・・

提出コード(一点更新+区間min)

提出コード(一点更新+一次関数の合成)

提出コード(区間加算+区間min)

ライブラリ(区間和)

ライブラリ(区間加算)

なぜ広まってないの??????

・見た目に対して実装がダルい

・遅延伝播とかいろいろやるとかなりバグりやすい

・スキップリストに興味持つ人間のうち95割はすでに平衡二分木を持ってそう

・そもそも色々できる事が知られていない

 とかが理由なんじゃない?(適当)

おわり

 区間加算ができる事に気づいた時は絶対流行ると思ったんだけど、予想より実装が楽じゃなかったので微妙な気持ちになった・・・

JOI2019-2020二次予選 参加記

はじめに

 この記事は東京高専プロコンゼミ② Advent Calender 2019 9日目の記事です。

 昨日(12/9)の13:00-16:00にJOI2019/2020の二次予選が開催されました。私は本選参加資格がないのですが、一次予選に参加して通過したため、他の参加者と同様に予選を受ける事ができました。(つくばには行けませんが・・・)

結果は全完で500点でした。


当日の動きについて

 ぼくはコンテストが開始される13:00時点でラーメンを食べていたので、問題文だけ先に読んで後で(適当な場所を探して)コーディングをする予定でした。

C問題

 300点辺りがJOI予選突破ボーダー辺りだと考えていたので、いい感じの難易度っぽい3問目を最初に読みました。
 最初5分ぐらいは解法が分からず、Nから逆に辿ればいいじゃんみたいな事を考えてすぐ捨てるみたいな事をしていましたが、思いつけばおわりです。逆辺張ってBFSすればおわりだと思ったのですが、実際は逆辺を張るまでもなく、桁和を足した先に値を加算すればおわりでした。スマホコーディングができるぐらいので軽実装で終わったのでよかったです。
 コンテスト後のTLにUnionFindを貼って遷移先とマージしてNの含まれる集合の要素数を出力するみたいな解法が生えていて、めっちゃ頭がいいな〜となりました。(UFを貼って解けるの、かなり好きです)

    
    
    #include <bits/stdc++.h>
    using namespace std;
    using i64 = long;
    
    
    signed main() {
    int n;
cin>>n;
vector<int> val(n+1,1);
for(int i=1;i<n;++i){int c=0;
int j=i;while(j){c+=j%10;j/=10;}if(i+c<=n)val[i+c]+=val[i];
}
cout<<val[n]<<endl;
    }


D問題

 dist[mod mでの値][現在指してるマス]で適当にダイクストラをします。JOIっぽくて割と楽しいな〜っていいながら解いてました(辺情報のソースコード埋め込みをしたのですが、それが間違ってないかが怖かった)

#include "bits/stdc++.h"

using namespace std;

using i64 = long long;


signed main(){
    int n, r;
    cin >> n >> r;
    vector<vector<int>> v{
            {0, 1},
            {1, 2},
            {2, 3},
            {4, 5},
            {5, 6},
            {7, 8},
            {8, 9},
            {1, 4},
            {2, 5},
            {3, 6},
            {4, 7},
            {5, 8},
            {6, 9},
    };
    vector<vector<int>> edges(10);
    for(auto &p : v){
        edges[p[0]].emplace_back(p[1]);
        edges[p[1]].emplace_back(p[0]);
    }

    vector<int> dist(n * 10, 1e9);
    dist[0] = 0;
    priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> que;
    que.emplace(0, 0);
    while(!que.empty()){
        int d, p, val, pos;
        tie(d, p) = que.top();
        que.pop();
        if(d != dist[p])
            continue;
        val = p / 10;
        pos = p % 10;
        for(auto& t : edges[pos]){
            if(dist[val * 10 + t] > d + 1){
                dist[val * 10 + t] = d + 1;
                que.emplace(dist[val * 10 + t], val * 10 + t);
            }
        }
        int nex = (val * 10 + pos) % n;
        if(dist[nex * 10 + pos] > d + 1){
            dist[nex * 10 + pos] = d + 1;
            que.emplace(dist[nex * 10 + pos], nex * 10 + pos);
        }
    }
    int ans = 1e9;
    for(int i = 0; i < 10; ++i)
        ans = min(ans, dist[r * 10 + i]);
    cout << ans << endl;
}


E問題

 問題を見てびっくりしました。まさか予選でBNF表記の構文解析が出るとは思わなかった・・・
 考察要素はあまりなくて、あなたは実装ができますかみたいな問題なんですが、やっぱり構文解析は重めで辛いです。じゃんけんの勝敗情報の埋め込みとか、バグる予感しかしなくて大変でした。
 カラオケで適当に書いてたら自然と左結合になって厳しかったです。ただ、演算が全部可換で2文字以上のトークンが存在しないので、入力対象のstringをそのままreverseしたら通りました。

#include "bits/stdc++.h"

using namespace std;

using i64 = long long;

const i64 MOD = 1000000007;

template <i64 mod = MOD>
struct ModInt{
    i64 p;

    ModInt() : p(0){}
    ModInt(i64 x){p = x >= 0 ? x % mod : x + (-x + mod - 1) / mod * mod;}

    ModInt& operator+=(const ModInt& y){p = p + *y - ((p + *y) >= mod ? mod : 0); return *this;}
    ModInt& operator-=(const ModInt& y){p = p - *y + (p - *y < 0 ? mod : 0); return *this;}
    ModInt& operator*=(const ModInt& y){p = (p * *y) % mod; return *this;}
    ModInt& operator%=(const ModInt& y){if(y)p %= *y; return *this;}

    ModInt operator+(const ModInt& y) const{ModInt x = *this; return x += y;}
    ModInt operator-(const ModInt& y) const{ModInt x = *this; return x -= y;}
    ModInt operator*(const ModInt& y) const{ModInt x = *this; return x *= y;}
    ModInt operator%(const ModInt& y) const{ModInt x = *this; return x %= y;}

    friend ostream& operator<<(ostream& stream, const ModInt<mod>& x){
        stream << *x;
        return stream;
    }

    friend ostream& operator>>(ostream& stream, const ModInt<mod>& x){
        stream >> *x;
        return stream;
    }

    ModInt& operator++(){p = (p + 1) % mod; return *this;}
    ModInt& operator--(){p = (p - 1 + mod) % mod; return *this;}

    bool operator==(const ModInt& y) const{return p == *y;}
    bool operator!=(const ModInt& y) const{return p != *y;}

    const i64& operator*() const{return p;}
    i64& operator*(){return p;}

};

using mint = ModInt<>;


signed main(){
    int n;
    string s;
    char tmp;
    cin >> n >> s >> tmp;
    string p = "RSP";
    int target = p.find(tmp);

    reverse(s.begin(), s.end());
    s += ";";
    int c = 0;

    struct Block{
        array<mint, 3> res;
        int r;
    };
    auto merge_plus = [&](Block& l, Block& r){
        vector<vector<int>> magic{
                {0, 0, 2},
                {0, 1, 1},
                {2, 1, 2}
        };

        array<mint, 3> m{mint(0), mint(0), mint(0)};
        for(int i = 0; i < 3; ++i)
            for(int j = 0; j < 3; ++j)
                m[magic[i][j]] += l.res[i] * r.res[j];
        return m;
    };

    auto merge_minus = [&](Block& l, Block& r){
        vector<vector<int>> magic{
                {0, 1, 0},
                {1, 1, 2},
                {0, 2, 2}
        };

        array<mint, 3> m{mint(0), mint(0), mint(0)};
        for(int i = 0; i < 3; ++i)
            for(int j = 0; j < 3; ++j)
                m[magic[i][j]] += l.res[i] * r.res[j];
        return m;
    };

    auto merge_mul = [&](Block& l, Block& r){
        vector<vector<int>> magic{
                {0, 2, 1},
                {2, 1, 0},
                {1, 0, 2}
        };

        array<mint, 3> m{mint(0), mint(0), mint(0)};
        for(int i = 0; i < 3; ++i)
            for(int j = 0; j < 3; ++j)
                m[magic[i][j]] += l.res[i] * r.res[j];
        return m;
    };

    function<Block(int)> expr, term, factor;
    expr = [&](int l){
        auto res = term(l);
        if(s[res.r] == '+'){
            Block ret;
            auto res2 = expr(res.r + 1);
            ret.r = res2.r;
            // TODO
            ret.res = merge_plus(res, res2);
            return ret;
        }
        else if(s[res.r] == '-'){
            Block ret;
            auto res2 = expr(res.r + 1);
            ret.r = res2.r;
            // TODO
            ret.res = merge_minus(res, res2);
            return ret;
        }
        else
            return res;
    };

    term = [&](int l){
        auto res = factor(l);
        if(s[res.r] == '*'){
            Block ret;
            auto res2 = term(res.r + 1);
            ret.r = res2.r;
            ret.res = merge_mul(res, res2);
            // TODO
            return ret;
        }
        return res;
    };

    factor = [&](int l){
        Block ret;
        assert(l < n);
        ret.r = l + 1;
        if(s[l] == 'R')
            ret.res = {mint(1), mint(0), mint(0)};
        else if(s[l] == 'S')
            ret.res = {mint(0), mint(1), mint(0)};
        else if(s[l] == 'P')
            ret.res = {mint(0), mint(0), mint(1)};
        else if(s[l] == '?')
            ret.res = {mint(1), mint(1), mint(1)};
        else{
            ret = expr(l + 1);
            ++ret.r;
        }
        return ret;
    };

    auto res = expr(0);
    assert(res.r == n);
    cout << res.res[target] << endl;
}


A問題

 電車で帰宅中にスマホコーディングをします。90度回転がめんどいな〜って感じだったんですが、問題文をよく見るとどうやって解けばいいかが書いてあったので解けました。

#include <bits/stdc++.h>
    using namespace std;
    using i64 = long;
    
    
    signed main() {
    int n;cin>>n;
vector<string> s(n),t(n);
for(int i=0;i<n;++i)cin>>s[i];
for(int i=0;i<n;++i)cin>>t[i];
int ans=1e9;auto f=[&](int c){int k=0;for(int i=0;i<n;++i)for(int j=0;j<n;++j)if(s[i][j]!=t[i][j])++k;ans=min(ans,k+c);
};
auto rotate=[&](){vector<string> tmp=s;
for(int i=0;i<n;++i)for(int j=0;j<n;++j)s[j][n-1-i]=tmp[i][j];};
f(0);
rotate();f(1);
rotate();f(2);
rotate();f(1);
cout<<ans<<endl;
}


B問題

 「一番遠い場所から戻ってくるといい」みたいな事がサンプルに書いてあるので、それで書きます。考えるのが面倒なので電車内でスマホから脳死で二分探索をすると通りました。

    #include <bits/stdc++.h>

    using namespace std;

    using i64 = long;
    signed main() {
int n;cin>>n;

 vector<pair<i64,i64>>a(n);

for(int i=0;i<n;++i)cin>>a[i].first>>a[i].second;

sort(a.begin(),a.end());

auto f=[&](i64 val){i64 time=a.back().first;for(int i=0;i<n;++i)if(val+(a.back().first-a[i].first)<a[i].second)return false;

return true;

};

i64 ok=1e18,ng=max(a.back().second,a.back().first)-1;while(ok-ng>1){i64 mid=(ok+ng)/2;(f(mid)?ok:ng)=mid;

}cout<<a.back().first+ok<<endl;}


おわりに

 全完できてよかったです。終盤3問の難易度は5-7-8、ボーダーは340-370ぐらいだと思いました。

自前のVMを作って適当な文法の言語を動かしてみる

はじめに

 この記事は東京高専プロコンゼミ① Advent Calender 2019 4日目の記事です。
また、この記事はGigaCode 2019での発表とほぼ同じ内容となっています。 (発表資料)

概要

 最近はコンパイラを作るのが流行りらしいです。そこで今回は流行に乗っかり、簡単な文法の自作言語もどきを動かしてみようと思います。
私のような知識ゼロの初心者がアセンブリに触れるのはかなり厳しいので、VM経由で動かす形式で実装をしていきます。

 制作期間は1週間、バイトコード生成器の実装はGoを、VM本体の実装はC++を用いる事にします。
私は今までGoを触った事が(Hello Worldレベルの事を含めて)一度もないのですが、この機会に触ってお気持ちを掴んでいこうと思います。また、VM側は速度が求められるのでC++で実装していきます。

今回やる事

 今回の実装内容は

で構成されています。それぞれ順番に実装方法などを書いていきたいと思います。

実装

事前にやっておく事

 まずは言語仕様を決めます、とりあえず動けばなんでもいい、言語としての優れた機能を求めない(他言語の劣化でも問題ない)事にして、型は32bit int固定、宣言時やプロトライプ引数の記述時に型を記述しないC言語のような構文を採用する事にしました。関数は実装して、とりあえず構造体やポインタ等は(今は)実装しない事にします。

 また、字句解析や構文解析で必要になるので、EBNF記法のような適当な記法を用いて言語構文を記述しておきます。
バイトコード生成についても同様に、LuaJavaで生成されるバイトコードを参考にしながらオペコードを決めていきます。今回はレジスタマシンベースで動かすので、オペランドがスタックマシンベースと比べて少し多くなります。

字句解析

 構文解析をする前に、まずはプログラムからそれぞれの単語をトークンとして分割していきます。main(x, val)が["main", "(", "x", ",", "val", ")"]に分割されるイメージです。
正直このパートはやるだけ(先頭からマッチングするだけ、面倒な処理も愚直に書くか正規表現に投げればいい)なので、省略します。

構文解析

 字句解析で得られたトークンの列から、事前に決めておいたEBNF記法に沿って抽象構文木を生成していきます。
今回の言語仕様で使ったEBNF記法には左再帰が存在しなかったため、オーソドックスな再帰下降構文解析で普通に実装する事ができます。競技プログラミングなどで構文解析をやった事がある人にとっては(かなり面倒ですが)実装するだけ、そうでない人でも構文解析関連の資料は大量にあるのであまり困らないと思います。

バイトコード生成

 生成された構文木を潜っていきながら、事前に決めておいたバイトコード規則にそってバイトコードを生成していきます。
バイトコードの決め方は色々考える事ができますが、今回はオペコードに6bit、オペランドレジスタ参照部分が9bit、他は余った部分を使う形で1命令32bitとしました。また、もし即値代入等で1命令に33bit以上を使いたい場合は次の4バイトに前の命令の続きを記述できるような形にしました。

VM作成

 ここは本当にやるだけです。書き出されたバイナリファイルを1バイトずつ読んで行きながら、例えばjumpがあったら読む位置を変更したり、代入命令に合わせて(VM内の配列に)値を代入していったりするのを、オペコードの数だけswitch文で場合分けしながら実装していきます。

VM内での動的コンパイル

 VMまで作成できれば十分な速度でプログラムが動作してくれるのですが、ここからさらにプログラムを高速化していきます。
JITコンパイルを採用する事でプログラムの大幅な高速化を図ります。動的にバイナリを生成する手段としては、x86/x64向けのJITアセンブラのライブラリを導入したり、もしくはasm直書き生成などが考えられますが、時間もない状況で新しい事に手を出して消耗するのは得策ではないため、他の方法を探します。

 例えばRubyではVM内でメソッドの呼び出し回数を監視し、一定回数以上呼ばれたメソッドのバイトコードC言語のコードに変換、別スレッドでgccを用いてコンパイルを行い、生成された.soファイルをVM側から動的ロードして仕様する事で高速化をしています。この方法を用いる事で(比較的)簡単に動的コンパイルによる高速化が行えます。
 作成したVMに手を加え、関数の呼び出し回数を保存するようにし、関数が一定回数以上呼ばれた場合にバイトコードC++コードに変換、gccコンパイルしてdlopenとdlsymで動的ロードを行います。 特に今回の実装はレジスタマシンベースのため、オペランド中に出てくる定数の数がかなり多くなります。そのためバイトコードだった部分の定数埋め込みによる恩恵が大きくなり、細かい最適化をかけなくてもかなりの高速化が行えました。

まとめ

 実装量は多かったけど、異常に辛いという訳ではなく、締切に追われながら適度に限界ペースで開発をしている感じがあって楽しかったです。私自身はコードを書くという事自体が好きな人間なので、こうやって短期間に実装量が多い開発をするのは楽しく、定期的に行っていきたいと感じました。

 今回実装した物は言語として見ると本当に機能不足もいい所で、個人レベルでちゃんとした言語の開発をしている方は本当にすごいと感じました。(特に今回はlexer/parser関連のライブラリやLLVMを全く使用していないため実装で詰まらなかったという節もあったので、それらのライブラリの機能をフルに活用して開発するのは本当に大変そうだと思いました)

 最後にリポジトリを置いておきます。まぁもう手を加える事はないと思いますが・・・ github.com

第30回全国高等専門学校プログラミングコンテスト競技部門に参加しました

はじめに

優勝しました。


開発について

 去年のリポジトリを参考にしつつ、汚くなったコードの書き直しも兼ねて新しいリポジトリで開発を始めました。 開発はLinux環境で例年通りC++とQtを使い、基本的なフォルダ構成などは去年と同じものを使う事にしました。


 開発初期に異常なやる気が生えるのはどこも恒例の事だと思うのですが、うちの部門もそんな感じでした。4/1のテーマ発表と同時にリポジトリ作成がされ、Visualizerの仮完成とゲーム進行部分の仮完成が4/3、各エージェント独立の簡単なBeamSearchができたのが4/4らしいです。


 基本的な部分が終了した後はcsv入出力関連の整備とかboostを使ったPythonとの通信とかロクに使わないアルゴリズムの整備とか適当な事をやっていました。5月から7月辺りでちょっと時間をかけて機械学習方面や強化学習をやってみたのですが、いやこれ無理でしょ、みたいな気分になって終わりました。

6月以降になるともう既にやる気を失っていて、8月辺りになると他の行事(SuperConとかJOI夏季セミナーとか)もあって自分はほぼ開発をしていませんでした。戦略を思いついたのは8月の後半あたりでした。

9月の前期期末試験が終わった辺りから流石に危機感を覚えたので、8月に思いついたものを実装して、通信部分を完成させて、10月に入った辺りで暇ができたので余った一週間ぐらいで通信確認+模擬対戦用のサーバーを実装しながら適当にバグ修正と本番向けの練習をして本番に臨みました。


 開発フローとか各自の担当部分なんですが、だいたい適当です。行動情報やフィールド情報のjsonへの変換部分とPythonを用いたサーバーとの通信部分と、あとはUIを中心としたちょっとした修正とか機能追加を他の人に投げたりしました。


アルゴリズムについて

NAPROCKがあるので一応それまで書かない事にしますが、だいたい他チームが想像している通りだと思います。


本選時の様子

比較的真面目な文体で書くのがつらくなってきたので、この辺から適当です

日曜日(本選1日目)

当日に組み合わせ発表があって、予行でもファーストステージでも仙台名取(去年の優勝、決勝でボロ負けした相手)と戦う事になった。実質決勝(?)

予行演習

うちの見立てでは大体半分とか6割ぐらいが通信失敗だろ〜みたいな感じだったんだけど、予行演習をすると7割とかのチームが死んでてびっくりした。
うちのチームは情報取得や通信に失敗する事なく動いたのでよかった。普通に動いて3勝とかすると微妙に注目される気がしたので、人力操作でエージェントを初期配置に戻して踏んだタイルも消す遊びをしてて、ちょっと楽しかった。

ファーストステージ

仙台名取がとてもこわかった。一戦目は相手が数ターン行動に失敗したりしてたから余裕があったんだけど、二戦目はこの大会中でうちが戦った中で一番負ける可能性が高かった気がする。(初戦での点差があったからある程度は負けても一位だったし、二位でも通ったからまぁ問題なかったけど)

月曜日(本選2日目)

前日の対戦結果からだいたいの組み合わせが分かっていたので、それらのチームのファーストステージでの様子とかを確認していた。連絡会議で決勝8エージェント10秒とか書いてあったのでマジかってなった。(8エージェント15秒になると思ってた)

セカンドステージ

前方に与謝野明子がいて面白かった。

準々決勝

ここから同時対戦がなくなって、1対戦を3人で見れるようになったのでけっこう楽になった。

準決勝

相手がすごい強くて、いい勝負だった。公開フィールドの方の勝負の終盤辺りが特に面白くてよかった。(いい感じに領域を潰しあえたので楽しかった)

決勝

非公開フィールド戦の辺りからカメラが後ろに回ってきて、いろいろ微妙な事にならないかなぁって言ってた。非公開フィールドが明らかに領域を取る前提の設計だったんだけど、最終的にはタイル点で数百点レベルの差が取れてよかった。

感想

今年も例年通り(特に事前の情報関連で)たくさんガバがあったと思うんだけど、まぁいつもと比べるとけっこうよかったと思う。

人力で勝った感じがあってとても申し訳ないんだけど、高専プロコンはアルゴリズムコンテストではないと思っているので・・・(人力コンテストです、ごめんね)


これは燃えそうな発言なんだけど、通信関連で失敗しない+30分で書けるビームサーチぐらいの強さがある 辺りが戦える最低限のラインだと思っていて、そうなっていないチームがかなり多かった気がする そもそもこの大会向けに時間をかけて開発しているチームが全然いないんじゃないかと思いました(それはそう?)(うちみたいに半年かける所って実際どのくらいあるんでしょうか、分からない)

あと機械学習とか強化学習の方面をやるチームがとんでもなく多かった、パンフレットを見たらだいたい半分ぐらいのチームがそうで、うちが上手くできてなくて実はめっちゃ強いものが完成する、とかだとアルゴ書いた人間(ぼく)の責任になってしまうのでこわかった。