Quora Programming Challenge 2021

www.quora.com

Division 1に参加して、247/700点。 100位台。

今年初めて開催で来年もあるかもしれないので、色々と書いておく。 端的に言うと、実装が重くてつらい。

Quora Programming Challenge

Quoraは最近良く目にするようになったQ&Aサイト。 Stack Overflowに比べて回答が少ないがその分回答に気合いが入っているように思う。

トップページにリンクを貼ろうと思ったが……トップページはログインフォームが出てくるだけなのか。 こんな感じのサイト。

jp.quora.com

CEOが競プロガチ勢らしい。

Google Code Jamのように複数ラウンドに分かれていたりはしない一発勝負。 ただし、タイムゾーンを考慮して、Division 1とDivision 2に分かれている。 どちらかに一方に出ても良いし、両方に出ても良い。 どちらも10以内なら賞金、50以内ならTシャツ。

タイムゾーンに配慮していると言っているけど、これ日本がとても不利じゃない? Division 1は日本時間の23時から翌日3時、Division 2が同日の10時から14時。 両方出ればその分有利。 普通に1回だけの開催ならば睡眠時間を適当にズラすが、3時から10時の間にピタリと睡眠時間を持ってくるはつらい。 ということでDivision 2はスルーした。

問題はDivisionごとに全7問で全て100点。 簡単でも難しくても点数が同じなので、順位表の解いている人の人数から簡単な問題を見極める必要がある。 でも、コンテストサイトに順位表が無い。 1時間ごとくらいにトップ50位までの順位表が貼り出される。

誤答ペナルティは無し。 提出数と間隔にちょっと制限はあるが気にするほどではない。

制約の小さなテストケースを通すことによる部分点が多い。 今となってはこの方式も珍しい。

私のコンテスト中の様子と回答

1問目: digest

Quoraの記事推薦システムが題材。 企業コンテストでこういう問題良いね。

ユーザーがユーザーをフォローすることもできるし、ユーザーが記事をフォローすることもできる。 記事には記事を作成したユーザーが存在する。 これらの関係から、記事S_kのユーザーU_iに対するスコアは、

\sum_{j=1}^m a\left(U_i, U_j\right)\times b\left(U_j, S_k\right)

と計算される。 関数abの値はユーザー同士やユーザーの記事とのフォローしているかどうかとかの関係から決まる。 これで各ユーザーについてスコア上位3件の記事を推薦しろという問題。

ユーザー数と記事数がどちらも最大200。制限時間が0.5秒。 O\left(n^{3}\right)通るかな? 通るよね。 abの値を事前に計算しておく。

実装が面倒でバグって時間が溶ける。 計算が複雑だからサンプルケースと見比べるの大変。 何とか修正して通った。

#include <iostream>
#include <vector>
#include <algorithm>
#include <utility>
using namespace std;

int main()
{
    int n, m;
    cin>>n>>m;
    vector<int> creator(n);
    for (int &c: creator)
        cin>>c, c--;
    int p, q;
    cin>>p>>q;
    vector<vector<bool>> FU(m, vector<bool>(m));
    for (int x=0; x<p; x++)
    {
        int i, j;
        cin>>i>>j;
        FU[i-1][j-1] = true;
    }
    vector<vector<bool>> FS(m, vector<bool>(n));
    for (int x=0; x<q; x++)
    {
        int i, k;
        cin>>i>>k;
        FS[i-1][k-1] = true;
    }

    vector<vector<int>> a(m, vector<int>(m, -1));
    for (int i=0; i<m; i++)
        a[i][i] = 0;
    for (int i=0; i<m; i++)
        for (int j=0; j<m; j++)
            if (FU[i][j] && a[i][j]==-1)
                a[i][j] = 3;
    for (int i=0; i<m; i++)
        for (int j=0; j<n; j++)
            if (FS[i][j] && a[i][creator[j]]==-1)
                a[i][creator[j]] = 2;
    for (int i=0; i<m; i++)
        for (int j=0; j<m; j++)
            for (int k=0; k<n; k++)
                if (FS[i][k] && FS[j][k] && a[i][j]==-1)
                    a[i][j] = 1;
    for (int i=0; i<m; i++)
        for (int j=0; j<m; j++)
            if (a[i][j]==-1)
                a[i][j] = 0;

    vector<vector<int>> b(m, vector<int>(n));
    for (int i=0; i<m; i++)
        for (int j=0; j<n; j++)
            if (creator[j]==i)
                b[i][j] = 2;
            else if (FS[i][j])
                b[i][j] = 1;

    for (int i=0; i<m; i++)
    {
        vector<pair<int, int>> S;
        for (int j=0; j<n; j++)
        {
            int s = 0;
            if (creator[j]==i || FS[i][j])
                s = -1;
            else
                for (int k=0; k<m; k++)
                    s += a[i][k]*b[k][j];
            S.push_back(make_pair(-s, j));
        }
        sort(S.begin(), S.end());
        for (int j=0; j<3; j++)
            cout<<(j==0 ? "" : " ")<<S[j].second+1;
        cout<<endl;
    }
}

2問目: escape

グリッド上の迷路に閉じ込められてしまったので、SからEまで移動して脱出したい。 何体かのガードがいて、各ガードiは(マンハッタン距離で)d_{i}の範囲の好きな場所に移動できる。 捕まらないように脱出する最小ステップは?

ガードの移動可能な範囲を壁にしてしまえば良い。 各ガードについて求めていると間に合わないので、同時に求める。 移動範囲の広いガードは先に、狭いガードは後に時間差で置いていき、1マスずつ広げていくイメージ。

これは素直に通った。

#include <iostream>
#include <vector>
#include <string>
#include <utility>
using namespace std;

int main()
{
    int n, m, k;
    cin>>n>>m>>k;
    vector<string> M(n);
    for (string &s: M)
        cin>>s;
    vector<vector<pair<int, int>>> G(n*m+1);
    for (int i=0; i<k; i++)
    {
        int r, c, d;
        cin>>r>>c>>d;
        G[d].push_back(make_pair(r-1, c-1));
    }

    int dx[] = {0, 0, 1, -1};
    int dy[] = {1, -1, 0, 0};

    vector<pair<int, int>> T;
    for (int s=0; s<=n*m; s++)
    {
        vector<pair<int, int>> P;
        P.swap(T);

        for (auto p: P)
            for (int d=0; d<4; d++)
            {
                int tx = p.second + dx[d];
                int ty = p.first + dy[d];
                if (M[ty][tx]!='#')
                {
                    M[ty][tx] = '#';
                    T.push_back(make_pair(ty, tx));
                }
            }

        for (auto p: G[n*m-s])
            if (M[p.first][p.second]!='#')
            {
                M[p.first][p.second] = '#';
                T.push_back(p);
            }
    }

    int sx = -1;
    int sy = -1;
    int ex = -1;
    int ey = -1;
    for (int y=0; y<n; y++)
        for (int x=0; x<m; x++)
        {
            if (M[y][x]=='S')
                sx = x,
                sy = y;
            if (M[y][x]=='E')
                ex = x,
                ey = y;
        }
    if (sx==-1 || ex==-1)
    {
        cout<<"IMPOSSIBLE"<<endl;
        return 0;
    }

    vector<vector<int>> D(n, vector<int>(m, -1));
    D[sy][sx] = 0;
    T.clear();
    T.push_back(make_pair(sy, sx));

    for (int s=1; s<=n*m; s++)
    {
        vector<pair<int, int>> P;
        P.swap(T);

        for (auto p: P)
            for (int d=0; d<4; d++)
            {
                int tx = p.second + dx[d];
                int ty = p.first + dy[d];
                if (M[ty][tx]!='#' && D[ty][tx]==-1)
                {
                    D[ty][tx] = s;
                    T.push_back(make_pair(ty, tx));
                }
            }
    }

    if (D[ey][ex]==-1)
    {
        cout<<"IMPOSSIBLE"<<endl;
        return 0;
    }

    cout<<D[ey][ex]<<endl;
}

3問目: students

n\times n2\leq n \leq 3\times 10^{3})のグリッドに生徒が敷き詰められている。 4近傍隣接のうちm0\leq m \leq min\left(2n(n-1), 2\times 10^{5}\right))組はおしゃべりをしている。 おしゃべりしている生徒同士を含まないように生徒は最大何人選べる?

二部グラフの最大独立集合。

どうやって求めるんだっけ? 直感的には最大マッチングを求めて何とかするのだろうが……。 ググる

www.slideshare.net

こんなん考えても分かるわけがないな。 ググって良かった。 けんちょんさんありがとう。

持っているライブラリーはマッチング数しか返さなかったので、ポチポチ修正。 ライブラリゲー止めてくれ。 まあ、修正が必要になると、ACLではなく自分でライブラリを書いていたのが生きてくる。 自分で書いたコードのほうが修正がしやすい。

提出すると47/100。 Memory Limit Exceededで落ちていた。 制限は256 MiBらしい。

たしかにけっこうギリギリか……。 型をsigned charに変えてもダメ。 mの上限がちょっと小さいので、おしゃべりしていない生徒を除外して、おしゃべりしている生徒だけでグラフを作ってみる。 こういう非本質的なところで手間を掛けさせるのは止めてくれ~

今度はTime Limit Exceeded。 最大流のアルゴリズムをDinicに貼り替えてみたら、バグった。 諦め。

今気が付いたけど、これ、cinを止めれば通ったか……?

#include <vector>
#include <algorithm>
#include <functional>

using namespace std;

/*
void maxflow(vector<vector<int>> E_, vector<vector<int>> &F_, int s, int t)
{
    int n = (int)E_.size();

    vector<vector<int>> E(n), R(n), Forg(n);
    vector<vector<int>> F(n);
    for (int i=0; i<n; i++)
    for (int j=0; j<(int)E_[i].size(); j++)
    {
        int v = E_[i][j];
        R[i].push_back(int(E[v].size()));
        R[v].push_back(int(E[i].size()));
        E[i].push_back(v);
        E[v].push_back(i);
        F[i].push_back(F_[i][j]);
        Forg[i].push_back(j);
        F[v].push_back(0);
        Forg[v].push_back(-1);
    }
    vector<bool> U(n);
    function<int (int, int)> dfs = [&](int p, int f)
    {
        if (p==t)
            return f;
        U[p] = true;
        for (int i=0; i<(int)E[p].size(); i++)
        if (!U[E[p][i]] && F[p][i]>0)
        {
            int t = dfs(E[p][i], min(f, (int)F[p][i]));
            if (t>0)
            {
                F[p][i] -= t;
                F[E[p][i]][R[p][i]] += t;
                return t;
            }
        }
        return 0;
    };
    
    int flow = 0;
    while (true)
    {
        for (int i=0; i<n; i++)
            U[i] = false;
        int f = dfs(s, 0x7fffffff);
        if (f==0)
            break;
        flow += f;
    }

    //return flow;
    for (int i=0; i<n; i++)
        for (int j=0; j<(int)Forg[i].size(); j++)
            if (Forg[i][j]!=-1)
                F_[i][Forg[i][j]] = F[i][j];
}
*/

template<class T>
T maxflow(vector<vector<int>> E_, vector<vector<T>> C_, int s, int t)
{
    int n = (int)E_.size();
    vector<vector<int>> E(n), R(n), Corg(n);
    vector<vector<T>> C(n);
    T mx = {};
    for (int i=0; i<n; i++)
    for (int j=0; j<(int)E_[i].size(); j++)
    {
        int v = E_[i][j];
        R[i].push_back((int)E[v].size());
        R[v].push_back((int)E[i].size());
        E[i].push_back(v);
        E[v].push_back(i);
        C[i].push_back(C_[i][j]);
        Corg[i].push_back(j);
        C[v].push_back(T());
        Corg[v].push_back(-1);
        mx = max(mx, C_[i][j]);
    }

    vector<int> D;
    vector<int> I;

    auto bfs = [&]()
    {
        D = vector<int>(n, -1);
        vector<int> Q;
        D[s] = 0;
        Q.push_back(s);
        int q = 0;
        while (q<(int)Q.size())
        {
            int v = Q[q++];
            for (int i=0; i<(int)E[v].size(); i++)
            if (C[v][i]>0 && D[E[v][i]]==-1)
            {
                D[E[v][i]] = D[v]+1;
                Q.push_back(E[v][i]);
            }
        }
    };

    function<T(int,T)> dfs = [&](int v, T f)
    {
        if (v==t)
            return f;
        for (int &i=I[v]; i<(int)E[v].size(); i++)
        if (C[v][i]>0 && D[v]<D[E[v][i]])
        {
            T d = dfs(E[v][i], min(f, C[v][i]));
            if (d>0)
            {
                C[v][i] -= d;
                C[E[v][i]][R[v][i]] += d;
                return d;
            }
        }
        return T();
    };

    T flow = {};
    while (true)
    {
        bfs();
        if (D[t]==-1)
            break;
        I = vector<int>(n, 0);
        while (true)
        {
            T f = dfs(s, mx);
            if (f == T())
                break;
            flow += f;
        }
    }
    //return flow;
    for (int i=0; i<n; i++)
        for (int j=0; j<(int)Corg[i].size(); j++)
            if (Corg[i][j]!=-1)
                C_[i][Corg[i][j]] = C[i][j];
    return 0;
}

#include <iostream>
#include <queue>

int main()
{
    int n, m;
    cin>>n>>m;
    vector<int> x1(m), y1(m), x2(m), y2(m);
    for (int i=0; i<m; i++)
    {
        cin>>x1[i]>>y1[i]>>x2[i]>>y2[i];
        x1[i]--, y1[i]--, x2[i]--, y2[i]--;
    }

    //  https://www.slideshare.net/drken1215/ss-86894312
    vector<vector<int>> M(n, vector<int>(n, -1));
    for (int i=0; i<m; i++)
    {
        M[y1[i]][x1[i]] = 0;
        M[y2[i]][x2[i]] = 0;
    }
    int C = 0;
    for (int y=0; y<n; y++)
        for (int x=0; x<n; x++)
            if (M[y][x]>=0)
                M[y][x] = C++;
    vector<bool> O(C);
    for (int y=0; y<n; y++)
        for (int x=0; x<n; x++)
            if (M[y][x]>=0)
                O[M[y][x]] = (y+x)%2!=0;

    vector<int> e1(m), e2(m);   //  even, odd
    for (int i=0; i<m; i++)
    {
        e1[i] = M[y1[i]][x1[i]];
        e2[i] = M[y2[i]][x2[i]];
        if (O[e1[i]])
            swap(e1[i], e2[i]);
    }

    vector<vector<int>> E(C+2);
    vector<vector<int>> F(C+2);
    for (int i=0; i<m; i++)
    {
        E[e1[i]].push_back(e2[i]);
        F[e1[i]].push_back(1);
    }
    for (int i=0; i<C; i++)
        if (!O[i])
        {
            E[C].push_back(i);
            F[C].push_back(1);
        }
        else
        {
            E[i].push_back(C+1);
            F[i].push_back(1);
        }

    maxflow(E, F, C, C+1);
    for (int i=0; i<C+2; i++)
        E[i].clear();
    vector<int> EC(C);
    vector<bool> FF(C);
    for (int i=0; i<C; i++)
        if (!O[i])
            FF[i] = true;
    for (int i=0; i<m; i++)
    {
        if (F[e1[i]][EC[e1[i]]]!=0)
        {
            E[e2[i]].push_back(e1[i]);
            FF[e1[i]] = false;
        }
        else
            E[e1[i]].push_back(e2[i]);
        EC[e1[i]]++;
    }

    queue<int> Q;
    for (int i=0; i<C; i++)
        if (FF[i])
            Q.push(i);
    while (!Q.empty())
    {
        int q = Q.front();
        Q.pop();
        for (int e: E[q])
            if (!FF[e])
            {
                FF[e] = true;
                Q.push(e);
            }
    }

    int ans = 0;
    for (int i=0; i<C; i++)
        if (!O[i] && FF[i] ||
            O[i] && !FF[i])
            ans++;
    for (int y=0; y<n; y++)
        for (int x=0; x<n; x++)
            if (M[y][x]<0)
                ans++;
    cout<<ans<<endl;
}


/*
int main()
{
    int n, m;
    cin>>n>>m;
    vector<int> x1(m), y1(m), x2(m), y2(m);
    for (int i=0; i<m; i++)
    {
        cin>>x1[i]>>y1[i]>>x2[i]>>y2[i];
        x1[i]--, y1[i]--, x2[i]--, y2[i]--;
    }

    //  https://www.slideshare.net/drken1215/ss-86894312
    vector<vector<int>> E(n*n+2);
    vector<vector<signed char>> F(n*n+2);
    for (int i=0; i<m; i++)
    {
        int p1 = y1[i]*n+x1[i];
        int p2 = y2[i]*n+x2[i];
        if ((x1[i]+y1[i])%2!=0)
            swap(p1, p2);
        E[p1].push_back(p2);
        F[p1].push_back(1);
    }
    for (int y=0; y<n; y++)
        for (int x=0; x<n; x++)
            if ((x+y)%2==0)
            {
                E[n*n].push_back(y*n+x);
                F[n*n].push_back(1);
            }
            else
            {
                E[y*n+x].push_back(n*n+1);
                F[y*n+x].push_back(1);
            }

    maxflow(E, F, n*n, n*n+1);
    for (int i=0; i<n*n; i++)
        E[i].clear();
    vector<int> EC(n*n);
    vector<bool> FF(n*n);
    for (int y=0; y<n; y++)
        for (int x=0; x<n; x++)
            if ((x+y)%2==0)
                FF[y*n+x] = true;
    for (int i=0; i<m; i++)
    {
        int p1 = y1[i]*n+x1[i];
        int p2 = y2[i]*n+x2[i];
        if ((x1[i]+y1[i])%2!=0)
            swap(p1, p2);
        if (F[p1][EC[p1]]==0)
        {
            E[p2].push_back(p1);
            FF[p1] = false;
        }
        else
            E[p1].push_back(p2);
        EC[p1]++;
    }

    queue<int> Q;
    for (int y=0; y<n; y++)
        for (int x=0; x<n; x++)
            if (FF[y*n+x])
                Q.push(y*n+x);
    while (!Q.empty())
    {
        int q = Q.front();
        Q.pop();
        for (int e: E[q])
            if (!FF[e])
            {
                FF[e] = true;
                Q.push(e);
            }
    }

    int ans = 0;
    for (int y=0; y<n; y++)
        for (int x=0; x<n; x++)
            if ((x+y)%2==0 && FF[y*n+x] ||
                (x+y)%2!=0 && !FF[y*n+x])
                ans++;
    cout<<ans<<endl;
}
*/

3問目: walls

公開された順位表で解いている人数が少なかったのでスルー。

4問目: tourism

木が与えられる。 頂点では金がもらえる。 辺では金が取られる。 移動元と移動先のクエリが複数与えられ、それぞれについて、金が尽きないように移動できる初期所持金の最小値を求める。

各頂点xについて、xから根に移動するときに必要な初期所持金と、根からxに移動するときに必要な初期所持金と、xから根に移動したときに所持金がいくらになっているかを求めておけば、各クエリがO(n)で処理できる。

書いてみたけどサンプルケースが合わない → あ、経由するのは根ではなく最小共通祖先か。

だいたい差分計算ができそうだが、必要な金の最小値を取っているところがあり、これはSegment Tree的なことをしないといけない。 この時点で残り10分。 解けるわけがない。 終わり。

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;

int main()
{
    int n, m;
    cin>>n>>m;
    vector<long long> x(n);
    for (long long &t: x)
        cin>>t;
    vector<vector<int>> E(n);
    vector<vector<long long>> T(n);
    for (int i=0; i<n-1; i++)
    {
        int v, w;
        long long t;
        cin>>v>>w>>t;
        v--, w--;
        E[v].push_back(w);
        T[v].push_back(t);
        E[w].push_back(v);
        T[w].push_back(t);
    }

    vector<long long> MF(n);    //  MF[x]: needed money from x to 0
    function<void(int, int, long long)> f1 = [&](int c, int p, long long t)
    {
        if (p>=0)
            MF[c] = max(0LL, t-x[c]+MF[p]);
        for (int i=0; i<(int)E[c].size(); i++)
            if (E[c][i]!=p)
                f1(E[c][i], c, T[c][i]);
    };
    f1(0, -1, 0);

    vector<long long> MT(n);    //  MT[x]: needed money from 0 to x
    function<void(int, int, long long)> f2 = [&](int c, int p, long long m)
    {
        if (p>=0)
            MT[c] = max(0LL, max(MT[p], -m));
        for (int i=0; i<(int)E[c].size(); i++)
            if (E[c][i]!=p)
                f2(E[c][i], c, m+x[c]-T[c][i]);
    };
    f2(0, -1, 0);

    vector<long long> MS(n);
    function<void(int, int, long long)> f3 = [&](int c, int p, long long m)
    {
        if (p>=0)
        {
            m += x[c];
            MS[c] = m;
        }
        for (int i=0; i<(int)E[c].size(); i++)
            if (E[c][i]!=p)
                f3(E[c][i], c, m-T[c][i]);
    };
    f3(0, -1, 0);

    for (int i=0; i<n; i++)
        cout<<i<<" "<<MF[i]<<" "<<MT[i]<<" "<<MS[i]<<endl;

    for (int i=0; i<m; i++)
    {
        int a, b;
        cin>>a>>b;
        a--, b--;
        //cout<<MF[a]+max(0LL, MT[b]-MS[a])<<endl;
        cout<<MF[a]+max(0LL, MT[b]-MS[a])<<" "<<MF[a]<<" "<<MT[b]<<" "<<MS[a]<<endl;
    }
}

日本ディープラーニング協会G検定でGoogle検索はありなのか?

2020年第3回試験で合格した✌

G検定とは、一般社団法人日本ディープラーニング協会(JDLA)が主催するディープラーニングに関する2個の試験のうちの簡単なほう。 公式サイトに、

ディープラーニングの基礎知識を有し、適切な活用方針を決定して、事業活用する能力や知識を有しているかを検定する。

G検定とは - 一般社団法人日本ディープラーニング協会【公式】

と書かれているように、実際にディープラーニングの実装を行う人ではなく、企画担当者やマネージャー向けの試験でしょう。

難易度はわりと低く、ある程度の前提知識があれば、公式テキストに一通り目を通して、あとはググれば何とかなると思う。 「2時間で約200問、1問あたり36秒。そんな時間は無い」という話もあるけど、10秒で分かる問題が3割、公式テキストの索引を引くか検索するかで30秒くらいですぐに答えが分かる問題が2割ずつ、あと2割は1-2分時間を掛けてしっかりググって、(何割で合格かは非公開だけど)残り1割は別に落としても良いだろ、くらいの時間感覚だった。

一方で、広く浅くの試験なので、検索と資料の参照を封じられると一気に難易度が上がる。 ディープラーニングを使って仕事をしている人でも落ちるんじゃないかと思う。 今では誰も使っていないような古いモデルも把握していますか? ネオコグニトロンって知っていますか? フォルマントという言葉は? 個人情報取扱事業者に課せられる義務のうち努力義務は何? 「Ethically Aligned Design」を提唱したのはどこの団体? みたいな感じ。 頭を使う問題は無くてググればすぐに分かるけど、そんなに色々覚えてないよと。

ということで、試験合格のためには検索スキルが重要であり、「G検定はG(oogle)検索検定」などと揶揄されていたりもする。

ここまで読んで「え? 試験なのにネット検索ありなの?」と思いますよね。 「検索したら楽勝だったわw」とツイートしたら大炎上すると思いますよね。 私は思いました。

でも、受験者のブログやTwitterを見ていると、みんな当たり前のように検索の話をしている。 いちいちネットで検索するよりも、頻出用語をまとめたテキストファイルを作っておくという手もあるらしい。 それを売っている人もいた。

「へー、検索ありなんだ」と思って念のために、↑の公式サイトの試験概要を見ても、そんなことは一言も書かれていない。 申し込んだ後のメールとかでの案内に書いてあるのかとも思ったけど、特に無し。 逆に、受験開始前に同意させられる受験規約には、

なりすまし、カンニング、試験中に援助を受けたりするなど不正行為があったと判断される場合、もしくはそのような行為が疑われる場合は結果を取り消される可能性があります。

http://docs.jdla-exam.org/OperationManual_Examinees_JP.pdf#page=40

と書かれている。特に定義が無ければ、一般的に、「カンニング」というのは試験中のネット検索や資料の参照を含むはず。

5ch。

35名無し検定1級さん2020/07/16(木) 19:13:14.17ID:LWGsH0Yq

>>34

一方通行の正義を押し付けるんじゃなくて、ちゃんとルール読んだり、JDLA公式の動画とか見ような。

JDLA公式の質問会でグーグル検索については「してよい」と見解が出てるんだわ。

【JDLA】G検定 part2

この動画が見つからん。

これかなぁ。

www.youtube.com

19:56

できればですね。 僕らオンラインでこれやりたかった。 そうするとやっぱりどうしてもカンニングというかですね、手元に資料があったりとかする中で受けてくださいというものになりますので、 そうすると、本当に分かっているかどうかというのを1問30秒ぐらいになりますけど、パッとパッと出てくるような状況にあるかを確認したいので、こういうった制限時間の中でたくさん答えていただくというような形でG検定を実施しています。

22:46

司会「検索することも意味があるということですよね?」

理事・事務局長「そこで得た知識も自分のものにしていただけたらという風に思っています」

司会「結局、最新の情報を入手する方法が身に付いているかということもあるということですよね。試験中に検索ができるということは」

理事・事務局長「その通りですね、はい」(小声なのでよく聞き取れず)

試験中に検索することが前提の話の流れだけど、「検索して良いですか?」「良いですよ」とはっきり言っているわけではないんだよな……。

www.youtube.com

協会主催の合格体験談オンラインセミナーでは、合格者が当たり前のように試験中の検索やカンニングペーパーの作成に言及している。

ということで、「Google検索はありなのか?」の答えは、探した限りでは、はっきり文章でOKと言っているものは無い。 が、試験の主催も試験中に皆が検索や資料の参照をしていることは認識していて、咎めるつもりは無さそう。 まずは公式のイベントで合格体験談を話していた人の合格を取り消せという話になるので、Twitterなどで「検索して合格した」とツイートしたところで合格を取り消されることも無いだろう。 たぶん。 検索に関して公式がどこかに書いているのを見つけた人がいたらコメントででも教えてほしい。

はっきりしていない状態は良くないと思う。 こういう情報を知らずに受けた人は検索しても良いとは思わないだろうから、大きく不利になる。 「検索ありなのかな?」と調べていたら、「自宅でのオンライン受験なのでバレません」みたいなことを書いているブログもあった。 その理屈だと、カンニングと同様に規約で禁止されている代理受験もありになる。 さすがに代理受験は試験主催者としては止めてほしいだろう。 ちなみに、試験はブラウザなのだけど問題文のコピーはできないように対策されている。 なので、検索するときは手打ち。 これをコピーできるようにするブラウザ拡張があったら便利なので、そういうのも出てきかねない。

検索したら10秒で分かることを詰め込むような試験勉強はしたくないし、それをどれだけ覚えているかで合否が決まる資格に意味があるとは思わないので、試験で検索ができること自体は良いと思う。 運営は「試験中にインターネットを見るのも資料を参照するのもOK」というのを試験の要項にはっきりと書いてほしい。

CTFのPwnableの本の改訂版を技術書典9で頒布しています

紙は明日(9月22日23時59分)まで。 電子版は会期終了後も頒布継続できるらしい。

techbookfest.org

紹介やサンプルページなど。

sanya.sweetduet.info

前回の記事。

kusano-k.hatenablog.com

File stream oriented programmingとか、glibc 2.31までの内容とかを追加した。

おかげで、InterKosenCTF 2020のFables of aeSOPは簡単に解けた。

qiita.com

以下、細々とメモ。

別の本にするのか改訂版にするのか

元々は、5月のコミックマーケットC98で別の本を頒布しようと思って、問題を作っていた。 新型コロナウイルスでC98の中止が決定されたので放り投げた。 このまま放置はもったいないので、オンライン開催の技術書典9の話を聞いて、もう1回取りかかった。

1冊にするには分量がちょっと少ない。 Twitterで「同人誌をシリーズにしても、基本的に前の本を買った人しか後の本は買わないから先細りだよ」みたいなツイートを見かける。 「いや、○○2を見かけて、1と一緒に買ったことがあるぞ」とも思ったけど、まあ、そういう人は少なそうというのは分かる。 2だけで完結するなら良いけど、1も必要な本で1だけ完売となるとつらいし、その辺の管理も面倒そうだな……と前の本にこの内容を追加してまとめることにした。

実際、技術系の同人誌は「第○版」みたいなのをときどき見かける。 これをやっている人は旧版の在庫をどうしているんだろうなぁ。 1回のイベントで、最初は旧版を頒布して、途中から新版に切り替えたら文句を言われそう。 私の場合、今は在庫が無いからちょうど良いと思っていたけど、これは勘違いで、在庫が出てきたわ。

glibc 2.27とglibc 2.31の共存

2020年4月23日Ubuntu 20リリース。

CTFの問題サーバーはUbuntuのLTSで動いていることが多いと思う。 この本で扱っている問題もUbuntuUbuntuのバージョンによってglibcのバージョンも異なり、問題の解き方も変わる。 Ubuntu 18はglibc 2.27、Ubuntu 20はglibc 2.31。

github.com

予定通り5月に出していたなら、Ubuntu 18でも良かったかもしれないが、20リリース半年後に古いバージョンで出したくはない。 「ま、攻撃コードをちょっと修正すればUbuntu 20で動くだろ」と思ったら甘かった。 (私では)どうやっても解けなくなる問題があったり、問題で解説したいところ以外が面倒になったりする。

qiita.com

glibc 2.31の解説もするならば、同じプログラムをglibc 2.27とglibc 2.31でそれぞれ解くのも面白そうな気がしてきた。 ということで、glibc 2.27とglibc 2.31の両方の環境を用意する必要がある。 しかし、問題サーバーを2個動かすのは面倒。 1個のサーバーでプログラムごとにglibc 2.27とglibc 2.31を指定したい。

Dockerで動かしている問題サーバーの中で、さらにDockerを動かす(Docker in Docker)ことも考えたけれど、何かと面倒らしい。

システムのlibcとは別のlibcを置いたディレクトリを環境変数LD_LIBRARY_PATHに指定すれば、別のlibcを読み込ませることができる。 でも、ldとバージョンがあっていないと起動中に落ちる。 そしてldのパスはプログラムに絶対パスで埋め込まれているので、LD_LIBRARY_PATHが効かない。 ldのパスを書き換えれば良くて、patchelfというコマンドでそれができる。 ついでにライブラリを読み込むディレクトリも指定できるので、LD_LIBRARY_PATHも不要になる。

patchelf --set-rpath /lib227/ --set-interpreter /lib227/ld-2.27.so program

CTFの出題者、libcを配布するついでにldも配布してくれないかな。 そうしたら問題サーバーと同じ環境で問題のプログラムを動かせる。

印刷所

これまで同人誌の印刷はポプルスに頼んでいた。

技術書典曰く、オペレーションの都合上、搬入は1サークル1箱でQRコードを貼り付けろと。ポプルスに「1箱に詰めて、QRコードを貼り付けてくれ🙏」と頼んでやってくれるか分からないし面倒、一度自宅を経由するにしても、そんなでかい段ボール箱どうするんだ……。ということで、別の印刷所も使ってみるかと、提携印刷所の日光企画に頼むことにした。なお、この条件は「自宅から」送付する場合だけの話で、提携以外の印刷所でもこの辺の作業は不要だった。

ページ数が増えたことによって印刷代がシビアになってくる。日光企画の料金表を見てみると、高ぇ……。同人誌の原価の話がたびたび炎上するけど、利益0か赤字では……と思っていたら、これはベースの価格で、早めに入稿するだけで20~40%割引になる。pixivプレミアム会員なら5%割引。プレミアム会員でなくても申込み直前に会員になればOK。プレミアム会員費540円は誤差。さらに、指定の見積もりサービスで見積もりして、クレジットカード払いではなく銀行振り込みすればさらに5%割引(この5%は、元の値段ではなく、早期入稿などの割引を適用した後の価格がベース)。ということで、最終的な値段はあまり変わらない。

あと、冊数のに対しての印刷代の上がり方も印刷所によって全然違う。オンデマンドとオフセットでどちらが安くなるかとかも違ってきそう。「どこもたいして変わらないだろ」と思っていたけど、安く高品質で印刷したかったら色々と調べないとダメだな。

AMスクリーニングとFMスクリーニング

ということで、ほぼ同じ表紙を異なる印刷所に頼んだので、比較ができる。 1枚目がポプルス、2枚目が日光企画。 どちらもオンデマンドで、「高精細表紙」みたいなオプションは無し。 カメラのホワイトバランスは同じなので実際の色合いの違いもこんな感じ(再販などで違う印刷所のものと色合いを揃えたければ、前の本も送れば何とかしてくれるらしい)。

細かい粗い以前に、網点が規則的かランダムで見た目が全然違うような……。 これがAMスクリーニングとFMスクリーニングか。 こういうことをブツクサ言っていると、紙の本を買ってくれる人がいなくなりそうだけど、まあよっぽど目を近づけて見なければ分からん。

ポプルスがFMスクリーニング、日光企画がAMスクリーニング(オンデマンド印刷でもこの用語を使うのかは知らないけど)。 以前の技術書典で買った「ねこのしっぽの4色フルカラーオフセット印刷スクリーン線数比較本」に載っていた。

www.shippo.co.jp

この本、AMスクリーニング15線~800線、FMスクリーニング70μ~20μで実際に印刷されていて面白い。 これは印刷所にしか作れない本(少なくとも、ねこのしっぽは線数の指定は受け付けていないらしいので)。

AMスクリーニングとは、点の位置を固定して点のサイズで濃さを変える方式。 線数が1インチに並ぶ点の個数。 「細かければ細かいほど良いんでしょ。800線で印刷してくれ」と素人は思うけれど、線数が高くなると多階調がなめらかに表現できなくなるらしい。 800線を見ても違いが分からないが……。 たぶん、点が細かいとサイズのコントロールが難しいのでしょう。

FMスクリーニングは点をランダムに配置し、点の個数で濃さを変える方式。 FMスクリーニングのほうが良さそうだが……この本曰く、

FMスクリーニングに関しては同人誌印刷業界では採用している会社が多いのですが印刷業界的には主流ではなく、過去の流行のような扱いになっていまして、製版機メーカーでも開発を止めてしまっているが現状です。

だそうで。 FMスクリーニングには、濃いところが潰れやすくなったり、淡いところで点の間隔が広がってしまうデメリットがあるらしい。 AM印刷の技術向上で、線数を上げられるようになったから、もうFMスクリーニングは要らないということなのだろうか。

天下一 Game Battle Contest 2020参加記

tenka1.klab.jp

5,000円ゲット 🎉

住所を連絡したときの返信で「ブログ記事とか書いてね!」と言われていて、すっかり忘れていたので、今さら書く。

概要

KLab株式会社主催のゲームAIコンテスト。

去年までは天下一プログラマーコンテストということで、競技プログラミングのコンテストだったけれど、今年はゲームAIになったらしい。 ゲームAIコンテストというとCODE VSがあった。 CODE VSはAI作成期間が数週間あってガッツリ時間が取られるけれど、天下一のほうは4時間の短期決戦。

AtCoderができる前のプログラミングコンテストとかGoogleくらいしかやっていない2009年にKLabは1回コンテストを開いていた。 懐かしい。 過去のコンテスト一覧に載っていないな。 黒歴史なのか?

suztomo.hatenadiary.org

ゲームのルール

冒頭のリンク先から、参加者でなくても見られるはず。

コンテスト時間は4時間しかないので、シンプル。 でもちゃんとバランスが取れていてゲームになる。 これはホントすごい。 別に5,000円をもらったから褒めているわけではない。

1分ごとに20x20マスの正方形が与えられて、参加者は正方形の領域を指定してマスを取っていく。 各マスには点数が決まっていて、マスの点数/取った人数 の点数が1分ごとの最後に入る。 基本はこれだけ。ただし、

  • 1辺がmの正方形の領域を確保するのに (m+1)*(m+1)*125ミリ秒掛かる
    • m*mではない
    • m=1なら0.5秒/マス、m=2なら0.281秒/マス、m=3なら0.222秒/マス
    • mが大きいところではたいした差は無いものの、小さいところだと差が大きくて、なるべく大きく取りたくなる
  • 「マスの点数/取った人数」は、4近傍連結成分内の最小値が、連結成分全体に適用される
    • 点数の低いマスが1個でも含まれていると大損なので、なるべく小さく取りたくなる
    • 点数が高いマスには人が集まるからどうせ最後は一定になるんじゃないの?
      • →高いマスは点数が下がるかもしれないが、低いマスは低いままなのでちゃんと避けないといけない
  • 1時間ごとに点数が10倍になっていく
    • 最後の1時間は最初の1時間の1,000倍
    • 最初のほうはスコアを気にせず試行錯誤できる
    • でも、わりと接戦になるので、2時間目から3時間目あたりは手を抜けない
    • でも、この辺で一旦プログラムを止めてでも、改良できれば最後の点数の伸びが違ってくるよなぁ、で悩ましい

WebAPIを叩いてゲームに参加する。 領域を確保するAPIを叩いて、指定された時間が経過する前にもう1回APIを叩くと、時間が経過するまでAPI内部で待って処理を実行してくれる。 プログラム中でウェイトを入れる処理を書かなくて良くなるので、これはありがたい。 WebAPIを叩いて戦うとなると、「まず運営のサーバーへのレイテンシが小さいサーバーを借ります」となりそうなところ、そういうのもあまり要らない。 すごい。

1個の盤面を全参加者で共有して戦うのは良いのだけど、サブアカウントを量産して自分が取っていないマスだけを取るというチートができちゃうんじゃないかというのがちょっと気になった。 まあ、そういう奴がいたらBANすれば良いだけか。

そういえば、3Dでおしゃれな感じの人力で操作できるビジュアライザもあったのだけど、ほとんど使っていないな。 最初に感覚を掴むのにちょっと触って、それ以降は放置。 状況の確認もWebAPIを叩いていた。 なぜかというと、状況確認のAPIにもレートリミットがあって、ビジュアライザが毎秒(だったっけ?)1回消費してしまうから。 何とかしてほしいところではあるものの、AIとレートを共有していないとビジュアライザのほうのAPIをAIから叩くというズルができてしまうから難しい。

私のAI

import os
import random
import time
from typing import List, Tuple
from urllib.request import urlopen

GAME_SERVER = os.getenv('GAME_SERVER', 'https://contest.gbc-2020.tenka1.klab.jp')
TOKEN = os.getenv('TOKEN', 'xxxx')

def call_api(x) -> str:
    with urlopen(f'{GAME_SERVER}{x}') as res:
        return res.read().decode()


def calc_score(stage: List[List[int]], num_claim: List[List[int]], my_claim: List[List[int]]) -> float:
    visited = [[False for _ in range(20)] for _ in range(20)]

    def f(r, c) -> Tuple[float, int]:
        if r < 0 or r >= 20 or c < 0 or c >= 20 or my_claim[r][c] == 0 or visited[r][c]:
            return 1e+300, 0
        visited[r][c] = True
        r1 = stage[r][c] / num_claim[r][c]
        r2 = 1
        for r3, r4 in (f(r+1, c), f(r-1, c), f(r, c+1), f(r, c-1)):
            r1 = min(r1, r3)
            r2 += r4
        return r1, r2

    score = 0.0
    for i in range(20):
        for j in range(20):
            x, y = f(i, j)
            score += x * y
    return score

def main():
    while True:
        game_resp = call_api('/api/game')
        game_id, remaining_ms = list(map(int, game_resp.split()))

        if game_id < 0:
            break

        stage_resp = call_api(f'/api/stage/{game_id}').split('\n')
        assert stage_resp[0] == '20'
        stage = [list(map(int, x.split(' '))) for x in stage_resp[1:21]]

        while True:
            areas_resp = call_api(f'/api/areas/{TOKEN}/{game_id}').split('\n')
            if areas_resp[0] == 'too_many_request':
                time.sleep(0.5)
                continue
            assert areas_resp[0] == 'ok'
            num_claim = [list(map(int, x.split(' '))) for x in areas_resp[1:21]]
            my_claim = [list(map(int, x.split(' '))) for x in areas_resp[21:41]]

            score = calc_score(stage, num_claim, my_claim)
            num = 0
            for i in range(20):
                for j in range(20):
                     num += my_claim[i][j]
            print(f'game_id: {game_id}  score: {score}  num: {num}')

            best = -1.0
            r, c, size = -1, -1, -1
            base = calc_score(stage, num_claim, my_claim)
            for k in range(1, 4):
                for i in range(21-k):
                    for j in range(21-k):
                        for ii in range(k):
                            for jj in range(k):
                                if my_claim[i+ii][j+jj]==0:
                                    num_claim[i+ii][j+jj] += 1
                                my_claim[i+ii][j+jj] += 1
                        s = (calc_score(stage, num_claim, my_claim)-base)/k
                        if num<64:
                            s += random.gauss(100, 10)
                        for ii in range(k):
                            for jj in range(k):
                                my_claim[i+ii][j+jj] -= 1
                                if my_claim[i+ii][j+jj]==0:
                                    num_claim[i+ii][j+jj] -= 1
                        if s>best:
                            best = s
                            r = i
                            c = j
                            size = k
            if r==-1:
                break
            print(f"{r}-{c}-{size}")
            claim_resp = call_api(f'/api/claim/{TOKEN}/{game_id}/{r}-{c}-{size}').split('\n')[0]
            if claim_resp == 'game_finished':
                break
            assert claim_resp == 'ok'

        while int(call_api('/api/game').split()[0]) == game_id:
            time.sleep(0.5)

if __name__ == "__main__":
    while True:
        try:
            main()
        except:
            pass
        time.sleep(1)

頑張っているように見えるかもしれないが、書いたのはbest = -1.0からprint(f"{r}-{c}-{size}")の間だけ。 他はサンプルコードそのまま。 サンプルコードでは、ここが未取得の1x1マスをランダムに取得するようになっていた。

1辺の長さ1-3の正方形を全ての場所に置いて、一番スコアが高いところ(スコアを計算する関数はサンプルにある)に置くだけ。 1x1だけ取っていくのが良さそうだけど、点数が高いところは結局下がって、2x2とか3x3で取ったほうが良さそう盤面になることが多かった。

大きい正方形のほうが点数が高くなりがちなものの時間が掛かるという代償がある。 なので、1辺の長さで割っている。 これ何が正しいのか分からなかった。 1辺の長さで割ることが正しいなら1-3ではなく4とか5とかも試せるはずだけど、range(1, 4)range(1, 5)にしたらスコアが下がった。

みんな同じ事を考えて、最初に同じ所を取って点数がガクっと下がったら嫌なので、一応乱数で少しばらしてみた。 効果は分からん。

たいしたことは何もやってないな。 いつもQiitaを使っているのでQiitaに書こうと思ったが、記事のプログラミング要素が少なすぎて怒られそうなので、はてなに書いた😇

遠くの銀河は大きく見えるという話

これ。

冷静に考えると、「光が広がるってどういうこと?」ってなるよね。重力レンズならば、一部が大きく見える分周囲は小さくなって整合性が取れるけれど、宇宙の膨張は等方的だからどこの方向を見ても大きく見えるはずで、「しわ寄せはどこに行くんだ?」という話になるよね。リプライや引用リツイートで「『猫の恩返し』のバロンのセリフだ!」と盛り上がっているが、そんな文学的な話ではなく、科学的な話が気になる。

ということで調べた。

答え。(現在の位置や赤方偏移的な意味で)遠くにある銀河は、(視差的な意味で)近くに見える。近くにあるのだから大きく見える。

参考文献

Closer Than They Appear | by Brian Koberlein

画像の出典。詳しい説明は無し。

The Case for a Positive Lambda-Term - V. Sahni & A. Starobinsky

ここが詳しい……が、読み切れていない。

“宇宙の果て”が137億光年でない理由 ― 宇宙は 400 億光年先まで見えている ―

赤方偏移と距離の関係が載っている。

距離とは?

そもそも、どうやって距離を測るのか。部屋の中くらいのスケールならば、人間の目は2個あるので距離感が分かるし、メジャーで測れば良い。カッコイイのだとレーザー距離計がある。

宇宙スケールではどうするのか?というところで、宇宙の距離梯子という概念がある。

月や近くの惑星規模ならレーザーで測れる。地球人が得られる最大の目の間隔であるところの地球の公転直径で数百光年までなら測れる。古いアニメなどで宇宙規模の距離を表わすのに光年の代わりにパーセクが出てきて、「なぜこんな面倒な定義の距離を使うんだ?」と思っていたけど、距離の測定方法が年周視差だからなのか。「距離によって左目と右目の見え方が変わるよね」という仕組みで距離が測れるのはここまで。

それ以上は、宇宙の天体の中で(見た目の明るさではなく)絶対的な明るさが分かるものを探すという話になるらしい。本当は明るい星が暗く見えたら、その星は遠い。これで測れるのが、数十億光年まで。これ以上は超新星爆発といえども暗すぎて無理なのだろう。

ここから先は赤方偏移。遠くにあるものほど赤く見える。星の色は様々だけれど、銀河くらい星が集まっていれば一定になるだろうという話だろうか。どうせこの距離では、個々の星は見えず、銀河しか見えないし。なお、Wikipediaに書いてあるように、ドップラー効果宇宙論赤方偏移は別である。遠くの星は早く遠ざかっているからドップラー効果で赤く見えるよねということ(だけ)ではない。光の経路全体で宇宙の膨張によるドップラー効果積分すれば宇宙論赤方偏移になるのかな? ということで、遠方の天体については赤方偏移しか方法が無く、本当の距離(とは?)と赤方偏移の対応もあいまい。だから、遠い天体の距離は赤方偏移で表わすらしい。

光年という距離の単位があるのだから、光の速度は有限である。数十億光年先の天体は数十億年前の姿。遠くの宇宙を観察するということは、昔の宇宙を観察するということである。距離の話をするときには、ある天体が光を発したときの距離なのか、(観察できない)現在の距離なのかを区別する必要がある。

赤方偏移と現在の距離と光を発したときの距離

これを知らなくて混乱したしビックリした。「現在遠くにある天体ほど赤方偏移が大きい」は正しい。「現在遠くにある天体ほど光を発したのは過去である」も正しい。「光を発した時点で遠くにあった天体ほど赤方偏移が大きい」は間違っている。「現在遠くにある天体ほど光を発した時点の距離が遠い」も間違い。

f:id:kusano_k:20200804002636p:plain

https://www.sci-museum.jp/files/pdf/study/research/2010/pb20_061-064.pdf

「初めの位置」だけ、赤方偏移が大きい領域では減少している。

現在314億光年の距離にある銀河A、171億光年にある銀河B、4億光年にある銀河Cの様子を図示するとこんな感じだろうか。

上記のPDFの表から、抜粋して適当に値を丸めるとこうなる。

z T億年 x(現在)億光年 x0億光年
0.03 4 4 4
2.00 103 171 57
10.00 132 314 29

T億年前に距離x0億光年で発した光を我々は見ており、現在はx億光年の距離にある。

132億年前に銀河Aが光を発する。この時点で銀河Aは29億光年の位置にある。

f:id:kusano_k:20200804013143p:plain

103億年前に銀河Bが光を発する。宇宙は膨張しており、この時点で銀河Bは57億光年の位置にある。ここら辺と光速度不変の法則がどう関係してくるのかがいまいち分からないのだけど、宇宙がうにょ~んと伸びているのだから、銀河Aの発した光が、132億年前に地球から見た銀河Aの位置より遠くにいくこともあるのだろう。たぶん。

f:id:kusano_k:20200804012202p:plain

4億年前(≒現在)に銀河Cが光を発する。そして、地球に銀河A, B, Cの発した光が届く。

f:id:kusano_k:20200804012234p:plain

地球からは、それぞれ132億年前、103億年前、4億年前の銀河A, B, Cの姿が見えている。その見かけ上の位置は、光を発した時点の位置のはず。銀河Cは当然すぐ近くだが、銀河Bは銀河Aよりも遠くなる。

私が瞳孔間距離数億光年で赤外線を見ることのできる目を手に入れて、遠くの銀河を青いほうから赤いほうに辿っていくと、最初は赤い銀河ほど遠くにあり、ある地点(赤方偏移z=2くらい)から今度は赤ければ赤いほど近くに見えるようになるはずである。

距離と見かけの大きさ

ここまで納得してしまえば後は簡単で、現在遠くにある銀河は近くに見えるのだから、見かけの大きさが大きいというだけの話である。

「あつまれ どうぶつの森」の花の色をコンプするぞメモ

あつまれどうぶつの森 花の遺伝子情報 | hyperWiki

各花は3個のSNP(バラだけ4個)を持っている。 塩基は0と1の2種類。 例えば、00-00-01の花と00-11-01の花が交配すると、00-01-0000-01-0100-01-11の3通りの遺伝子の花が生まれる可能性がある。 1個目のSNPは両親がどちらも00なので00、2個目は一方の親からは0を他方の親からは1を受け継ぐので常に01、3個目は全ての組合わせがありうる。

交配作業は何日もかかり、混乱しそうなので、あらかじめメモしておく。 青バラ以外はそんなに面倒ではない。

赤、白、黄色の花は種から育てるのが前提。 野生のものだとすでに遺伝子が混ざっている可能性がある。 また、交配する時点で周囲の花が全て交配済みだと、交配できずに同じ遺伝子の花が生まれてしまう。 2個ずつペアにして周囲の8近傍は空けるペア埋めが安全。

バラ

オレンジ

赤と黄色で、オレンジが50%。

白と白で、25%が紫。

赤と赤で、25%が黒。

ピンク

赤と白で、50%がピンク。

これ。こんなの自分では導き出せない。考え方としては、分かるのは花の色だけで遺伝子は分からないので、狙っている遺伝子しか特定の色にならないような組合わせを探しているのだと思う。

種の遺伝子は、赤が00-00-11-01、黄が00-11-00-00、白は01-00-00-00

それぞれの畑で狙っている色は、

花畑 遺伝子
1 11-00-00-00(25%)
2 01-01-00-00(50%)
3
11-01-00-00(25%、欲しいのはこっち)
11-00-00-00(25%、こっちは花畑5でオレンジが咲かない)
4 オレンジ 00-01-01-00(50%)
5 オレンジ 01-11-01-00(6.25%、花畑3の紫が半々と仮定)
6
11-11-01-00(12.5%)
11-11-11-00(6.25%)
7 11-11-11-00(25%)

チューリップ

オレンジ

赤と黄で、50%がオレンジ。

ピンク

赤と白で、50%がピンク。

赤と赤で、25%が黒。

上で作ったオレンジ同士で、6.25%が紫。

ユリ

オレンジ

赤と黄で、50%がオレンジ。

赤と赤で、50%が黒。 ちなみに、25%でピンク。

ピンク

赤と白で、50%がピンク。

ヒヤシンス

オレンジ

赤と黄で、50%がオレンジ。

白と白で、25%が青。

ピンク

赤と白で、50%がピンク。

上のオレンジ同士で、6.25%が紫。

キク

白と白で、25%が紫。

ピンク

赤と白で、100%ピンク。

キクだけ緑がある。

赤と黄を交配させて産まれた黄同士で、6.25%が緑。

パンジー

赤黄色

なぜパンジーだけオレンジではないのだろう。 花全体がオレンジ色なのではなくて、赤い部分と黄色い部分があるから?

赤と黄で、100%オレンジ。

白と白で、25%が青。

赤と上で作った青を交配させて産まれた赤同士で、6.25%が紫。

アネモネ

アネモネは種が、赤、白、オレンジ。

白と白で、25%が青。

ピンク

赤とオレンジで、100%ピンク。

赤と上で作った青を交配させて生まれた赤同士で、6.25%が紫。

コスモス

オレンジ

赤と黄で、100%オレンジ。

ピンク

赤と白で、100%ピンク。

上で作ったオレンジ同士で、5%くらい? オレンジの遺伝子が00-01-0101-01-01の2種類あって、黒になるのは00-11-1101-11-11だけ。 11-11-11は赤になってしまう。

脳を鍛える大人のNintendo Switchトレーニング(Switch版脳トレ)

www.nintendo.co.jp

頭が良くなりたい。 人生楽しくなりそう。 一昔前ならば「頭が良い」というのは知識があることだっただろうけれど、このインターネット時代ググれば知識は得られるので、人と差を付けるならばいわゆる頭の回転の速さだろうか。 ということで脳トレを買った。

今年の1月から初めてわりと毎日マメにトレーニングをしていたけれど、3月末に飽きてしまい、そこからは「数独」と「漢字合成」だけをやっていた。 その数独も全部解き終わった。 もうトレーニングはしなそうなのでメモを残しておく。

頭は良くなったのか? 「脳年齢チェック」の推移はこんな感じ。

f:id:kusano_k:20200505025920j:plain:w360

f:id:kusano_k:20200505025922j:plain:w360

f:id:kusano_k:20200505025927j:plain:w360

最初にちょっと伸びたが、これは慣れただけだろう。 その後もわずかずつは良くなっているものの、そんなに変わった感じはしない。 2回に1回くらいは「20歳」を叩き出せるくらいになるんじゃないかと思ったけど、そんなことはなかった。 リングフィットアドベンチャーなら体力や筋肉が多少は付くのに、脳を鍛えるのは難しいんだな……。

物理版のおまけに付いてきた(単品でも買える)タッチペンが良い感じ。 昔買ったタッチペンは先端がゴムで書きづらくて、指のほうがマシだと思ったけど、これは先端のゴムが金属の網のようなもので覆われていて良く滑る。

お手軽

対戦ゲームと、「毎日トレーニング」の一部のゲーム。 対戦する相手がいないのでやっていない。

毎日トレーニング(トレーニング)

色々なトレーニングがある。

漢字合成

漢字が苦手で「徒歩級」(出来が乗り物で評価される。最低は「徒歩級」、最高は「ロケット級」)になることが多いけれど、これは好き。 漢字のパーツが与えられて、組み合わせて作れる漢字を答える。

最初のうちは簡単。

f:id:kusano_k:20200504233938j:plain:w360

「次」。

最後のほうは難しい。

f:id:kusano_k:20200504234026j:plain:w360

これで、「衝」が作れる。

全問解くのに掛かった時間で評価され、パスしたら+20秒なので、20秒考えても無理そうならパスをするという手はある。 難しい漢字も容赦なく出てくる。 小学生が泣くぞ。 解きまくって漢字を覚えればスコアは伸びそうだが、同じ日には同じ問題しか出てこない。 もちろん2回目のスコアは記録には残らない。

数独

初級28問、中級36問、上級36問の100問。 解くのに掛かった時間は記録されるものの、「徒歩級」とか評価されることはないのでゆっくり考えられる。

「アシスト機能」があって、使うと1箇所数字を書くごとに正否が判定される。 間違えたまま進めてしまって、解けなくなることが防げる……が、間違えたときにペナルティがあり、数字が誤認識されてうっかりそのまま決定してペナルティを喰らってムカついたので使っていない。

インターフェースはこんな感じ。

f:id:kusano_k:20200505033409j:plain:w360

入る可能性のある数字を書いていくのが面倒だから自動化してほしいが……それでは意味が無いか。

上級ともなるとけっこう難しい。 バックトラックが必要になることまでは無いものの、「3は横に並んだ2マスのどちらかには入るから、この行の他のマスの3の可能性は消える」くらいは普通に出てくる。

瞬間記憶

この記事を書くのにスクショを撮ろうと思ったら、スクショ禁止だった。 ズルができないようになっているのかw

数個から十数個の数字が一瞬表示されて、消えた後に小さい数字があったところから順にマスをタッチ。 全部正解すると数字の個数が増え、不正解なら減る。

表示される数字が1~9なので9個が上限かと思いきや、10個以上にもなる。 そのときは10以上の数字も出てくる。

結果表示のグラフの上限が90個。 4個から開始して全10問。 4+5+…+13=85 だから、最高85個かな。

名曲演奏

楽譜と1オクターブ分の鍵盤が表示されるので演奏。 これもプレイ中は撮影禁止だった。 基本クラシックだから権利関係の問題とか無さそうなのに……。

f:id:kusano_k:20200505035051j:plain:w360

2曲だけ細菌撲滅ドクターマリオ)の曲が入っている。 任天堂のゲームの曲をもっと入れてほしかった。 「クラシックなんて知らないよ」と思っていたけれど、だいたいの曲はどこかで聞いたことのある曲だった。

間違った鍵盤を押したときだけではなく、伴奏(と楽譜の進み)に遅れたときも減点。 ノーミスは何曲かできたけれど、100点は無理だった。

新聞音読

昭和の新聞記事を音読。 これも撮影禁止。 厳しいな。

別に音声認識で判定されるわけではなく、読み終えたのは自己申告なので、ズルしようと思えばできる。 たまに気になる記事があるけれど、ゆっくり読めないので後から読み直す機能がほしい。

全60記事で、2週目に入ったのに、なぜか読んだ記事数が59/60。 なぜだろう。 名曲演奏は1日に2回以上やると好きな曲を選べるのに、こちらは好きな記事を選ぶ機能は無し。

計算25

f:id:kusano_k:20200505040022j:plain:w360

足し算・引き算・掛け算(割り算は無し)を25問。

脳を鍛えている感じがある。 手が答えを書いている間に、次の計算をする感じで、ときどき20秒を切れた。

計算100

こっちは100問。 他はそんなことないのに、計算○○だけは2バージョンある。

人数数え

中の見えない家に人が出入りして、最終的に人が何人になったかを当てる。 全6問。

f:id:kusano_k:20200505040601j:plain:w360

これは得意。 だいたい全問正解できる。 むしろ最初のほうが簡単すぎてぼうっとしてしまいミスる。 「車はスピードを出したほうが眠くならなくて安全なんだよ」と怖いことを言っている人がいたけれど、気持ちが分かった。

2問目で人が出入りする時間が長く(回数が多く)なり、3問目で出入りが同時に行われるようになり、5問目でテンポが上がり、6問目は最後に煙突から出入りしたり、家の前を人が横切ったりする。

人が同時に出入りするときは、足し引きを2回するのではなく、出入りした人数の差分を家の人数に加えるほうが楽。 例えば、家に3人いて6人入って4人出るならば、3+6=9, 9-4=5ではなく、6-4=2, 3+2=5。 6-4の部分は計算をしなくても視覚的に分かる。

答えは1人以上9人以下。 家にいる人数は常に0人以上9人以下。 これを知っていると、うっかり見逃したときに助かる場合があるかもしれない。

指体操

指示された手の形を作る。 IRセンサーが活用されている。

これも撮影禁止。 なぜ……。

基本は、グー、チョキ、パー。 最後だけ、鉄砲とか👍とかが出てくる。 8個1セットで、1セットの前半4個と後半4個は同じ。 後半4個はいちいち画面を見ないで、脳内で処理する気持ちになると速くなる……ような気がする。

IRセンサーの画像を出しているのは上手いなと思う。 ユーザーが手の位置を良い感じに調整してくれる。

指計算

指数字で足し算引き算。

これも撮影禁止。 IRセンサーに猥褻物を写してネットに上げられるのとかを気にしているのかな。 IRセンサーの画像を見ても何も分からないと思うが……。

判定をそうとう緩くしているっぽく、たまに曖昧な手の形で数問連続して正解判定されてしまうのがちょっと残念。 まあ、正解を出しているのに認識されないよりはマシか。

指体操と指計算は、JoyConを外して画面の向きを横向きに変える必要がある。 「画面の向きを縦に戻して」と言われるのを無視して、指体操と指計算を続けてやると手間が省ける。

二重課題

ハードルを越えながら値が最高の数字をタッチ。

f:id:kusano_k:20200505042800j:plain:w360

ちなみにハードルの他に頭上に鳥が飛んでいて、鳥が飛んでいるときにジャンプしてしまうのも減点。 後半は数字が動いたりしてイライラする。

最初は「こんなん絶対無理だろ」と思ったけれど、数日で慣れて普通にできるようになった。 人間の脳はすごい。

直前写真

f:id:kusano_k:20200505044139j:plain:w360

「一つ前の写真」はキノコなので、この場合テントウムシではなくキノコの写真を選ぶ。 後半は左右反転が入ってきてなかなか意地が悪い。

タッチしようとした瞬間には脳は自由になっているわけで、そのときに絵を覚えれば良い。 ワーキングメモリを鍛えるトレーニングとのことだけど、鍛えられている感があまり無い。

細菌撲滅

ドクターマリオ

f:id:kusano_k:20200505044955j:plain:w360

ただ、コントローラーではなくカプセルをペンで掴んで動かす。 当然(?)上に動かすことはできない。 また、そのうちカプセルが2個同時に落ちてくるようになる。

これはトレーニングではなく、前頭葉をリラックスさせる効果があるらしい。 リラックスするか??? スーファミの「すーぱーぷよぷよ」に「とことんぷよぷよ」という相手も無しにひたすらぷよぷよをするモードがあった。 だんだんスピードが速くなっていって、最高速度だとけっこうなテンポで落ちてくるのだけど、この状態になると意外と何も考えていないというか、瞑想をしているような感覚はあった。 でも、ぷよぷよは適当に4個繋げれば良いだけだが、ドクターマリオはカプセルが固い(下に落ちない)し、縦か横に並べないといけないし、頭を使うと思うんだよな……。 ドクターマリオに慣れていないだけで、慣れたらリラックスできるのかな。

特に評価があるわけでも無いので、ほとんど遊んでいない。

ワーキングメモリーレーニン

アップデートで追加。 アップデート後に脳年齢チェックで20歳を取るか、「毎日トレーニング」に隠れている教授をタッチする裏技で解放。

f:id:kusano_k:20200506003218j:plain:w360

超能力の測定に使うゼナーカードの5種類の記号に三角形を加えた6個の記号(○、┼、川、□、☆、△)が出てきて、N個前の記号を答えていく。 例えば、△、△、□、○、┼と順番に覚えるのは良いとして、この後に☆が出てきたときに、最初の△を捨てて、△、□、○、┼、☆と覚え直すのがつらい。 トレーニングにはなりそうだけど、つらいのであまりやってない。 負荷が強すぎて良くないのか、1日に3回までの制限がある。

普通に名前を付けたときに最初の1文字が被らないので、○=ま(まる)、┼=じ(じゅうじ)、川=な(なみ)、□=し(しかく)、☆=ほ(ほし)、△=さ(さんかく)と覚えていた。 上の例ならば「ささしまじ」。 ただ、これだと「し」と「じ」で混乱したりする。 他のトレーニングを見るに私は数字が得意らしいので、数字に紐付けることにしたらもうちょっと覚えられるようになった。 ○=0(0っぽい)、┼=2(線が2本なので)、□=4(四角形)、☆=5(五芒星)、△=3(三角形)、川=2(2が余った)。

脳年齢チェック

脳年齢を測定できる。 1日何回でもチェックできるけれど、記録が残るのは最初の1回のみ。 年齢は若いほど良く、20歳が最高。

抑制力、処理速度、短期記憶の3個の評価軸で、それぞれにいくつかの問題があってランダムに選ばれる。 私は短期記憶が致命的にダメで、後の2個が20歳なのに短期記憶が50歳とかが良くあった。 PCばかり使っていてクリップボードに頼っているからだろうか。

後出勝負テスト(抑制力)

じゃんけん。 ただし、「勝ってください」と「負けてください」と言われる場合がある。 つい勝ってしまおうとするのを「抑制」できるかどうかということなのだろうか。

でも良く考えてみると、普段じゃんけんをしているときは、手を出した後で勝者敗者を判定するのであって、相手の手を見て勝てる手を出したりはしないよな。 なぜこんなに難しいのだろう。

私は、ハサミ、紙、石をイメージすると少し楽になった。

IRセンサーを使う。 「後出勝負テスト」が選ばれた場合は、最初に「今JoyConは使えますか?」と聞かれるので、「使えない」と言えば他のテストになる。

順番線引テスト(抑制力)

f:id:kusano_k:20200505234429j:plain:w360

スクショの通り。

つい数字だけや英字だけで線を結びそうになるのを抑制できるかどうかだろうか。

最高数字テスト(抑制力)

「二重課題」の下半分。 ミスっても秒数が加算されるペナルティは無くて、一瞬タッチができなくなるだけ。

計算25テスト(処理速度)

「計算25」と同じ。

高速数えテスト(処理速度)

1から120まで声に出して数える。 子供の頃に風呂で「100まで数えてから上がりなさい」と言われて高速で数えたのを思い出す。 終わりは自己申告。

息継ぎの時間がもったいないので、なるべく小さな声にして吐く息を少なくすると速くなる。

連続減算テスト(短期記憶)

f:id:kusano_k:20200506000300j:plain:w360

93 → 85 → 77 → … → 13 → 5 となる。 書き始めると最初の数字が消され、それ以降に書いた数字もすぐに消えるので、覚えておく必要がある。

f:id:kusano_k:20200506000308j:plain:w360

とはいえ、あまり短期記憶を使っている気がしない。 要求されるのは処理速度じゃないのか? なので私はこれが得意で、短期記憶がこれならば総合20歳が狙える。

単語記憶テスト(短期記憶)

平仮名3文字の単語が30個が提示され、それを覚えて何個思い出したかがテストされる。 次の「5×5記憶テスト」と異なり、回答済みの単語が画面に残らないのがつらい。 回答済みの単語をきっかけにすることができないし、どの単語を回答したかを覚えておく(あるいは記憶から綺麗に消す)必要がある。

人間の記憶はPCと違ってシーケンシャルに読み出すことができず、何らかのきっかけが必要で、それが難しい。 ということで、「つくえ」「せいと」「こっき」(校庭にあった気がする)「たから」(子供は宝探しが好き)があったら小学校繋がりとか、出てきた単語に無理矢理にでも何らかの繋がりを見つけると楽。 最後数秒でいくつかの単語を一時的に覚えてダメ押し。 記憶時間終了時にポーンという音が鳴るのだけど、「この音に記憶を消す作用があるんじゃないか!?」と思うくらい記憶が飛ぶときがあるけど。

5×5記憶テスト(短期記憶)

5×5のマス目に1から25の数字が並んでいるものを記憶し、各マスにどの数字があったかを答える。

要は1~25の順列なので、左上のマスから順にどの数字が書かれていたかを覚える方法と、1から順にどのマスに書かれていたかを覚える方法がある。 私は後者のほうが覚えられた。 人間は場所に関する記憶が強いというやつだろうか。 あと、1から順に覚えていきつつ25とか24とかの場所も覚えておくと、なぜか別腹のようで、わりと覚えられる。 覚えている分を全部書き終えたら、空いているマスに書いていない数字を適当に書いていけばちょっと水増しできる。