DISCO presents ディスカバリーチャンネル コードコンテスト2021 本戦 参加記

2021/03/28に行われたDISCO presents ディスカバリーチャンネル コードコンテスト2021 本戦に参加してきました。

今年はコロナウイルス感染拡大の影響で全然オンサイトがなく、なんと最後に参加したものは去年のDDCC2020本戦でした。

kotatsugame.hatenablog.com

なかなか面白いめぐり合わせですね。この状況下で開催を決断してくださった運営の方々、また感染症対策などに気を使いつつコンテストを進行してくださったスタッフの皆さん、ありがとうございました。

ちなみに、念の為予選の成績を書いておくと、30分ちょっとで全完でした。

今年はコード部門がないので、本戦開始時刻も遅くなりました。具体的には、午前13時開始なので、朝にはいくらか余裕があります。昨年は始発の新幹線に乗りましたからね……。

とはいえ、今年は僕が富山県に帰省中なので、仙台駅から向かった昨年と比べて(駅までの移動も合わせ)移動時間は増えます。なかなかちょうどよい列車がなかったこともあって、今年は指定席特急券を買い、速い列車で行くことにしました。僕は22卒の対象に含まれるので、交通費も支給されます。というわけで、09:08富山駅発11:20東京駅着のかがやきに乗りました。

新幹線の中で本戦参加者のTwitterリストを作りました。

twitter.com

東京駅から品川駅を経由して大森海岸駅まで向かいます。うっかりグリーン車の車両に乗ってしまい、わざわざ普通車に通り抜けさせてもらいました。こういうのは問答無用でグリーン車乗車券を買わされるものだと思っていたので、人の温かみに触れた気がしました。

品川駅でJRと京急の乗り換えに失敗し、検索して出てきたものより1本遅れてしまいましたが、無事到着。駅の近くで食事をしようと考え、ふとTLで目にしたなか卯に行ってみると、4人くらいの競プロerが集まっていました。5人目は流石にマズいので、挨拶だけして食事は別の席でしました。

僕「これって勝手に帰っていいんですか」

kort0nさん「(券売機を指しつつ)お金払ったでしょ」

僕「……(納得して退店)」

ディスコ本社に向かうも、受付開始の12:30までは入れないようだったので、横のイトーヨーカドーに向かいます。その途中すれ違ったkyopro_friendsさんに声を掛けていただき、2人で少しだけうろつきましたが、特に何か見るわけでもなく、すぐディスコ本社前に向かいました。本社前には他にも所在なげに立ち尽くしている競プロerが複数人いて、そのうち自宅番兵さん等と挨拶しているうちに、受付開始時間になりました。

コンテスト前

検温・マスク交換・アルコール消毒して、名札を受け取り、コンテスト会場に向かいます。交通費領収書もこのとき渡すのですが、封筒に入れなければならないところを用意していなかったので、急遽クリアファイルをもらって名前を書くことになりました。少し時間を食って他の人を待たせてしまい申し訳ない……。

コンテスト会場に用意されていた席は、上下以外の4方がビニールに覆われていてびっくりしました。企業紹介映像を見る限り、このようなブースは普段の業務でも使用しているようで、なかなか大変そうだなあという気持ちになりました。

机に用意されていたTシャツを着ていると、インタビューを申し込まれたので、しばらく答えていました。

これまでのDDCCへの参加経験を聞かれ、昨年本戦に出たということ、また予選は詳しく覚えていないが、2016年から競プロを始めたので、それ以降のものには出ているんじゃないかと答えました。DDCCもちょうど2016年から始まったということで、これも面白いめぐり合わせです。後から確認したところ、2016年と2017年の予選には参加していたようです。

また、この記事の冒頭でも書きましたが、昨年のDDCCから丸1年オンサイトがなかったことにも触れ、開催していただけてありがたいということも言いました。

本戦にかける意気込みを聞かれて、「昨年は装置に自分のプログラムを実装することができなかったので、今年はそこまで進みたい」ということを答えたのですが、今年はシミュレーションで点数を取れば全員実装できるということを知ってちょっと拍子抜けしました。まあ30人しかいなければそんなものなんですかね。

その後説明・会社紹介があって、いよいよ問題公開・コーディングです。

シミュレータ問題

問題は、簡単にいえば次のようなものです。制約の細かい値(例えば穴の詳細等)は、忘れたものもあるので、気にしないでください。

長方形の平面がある。ある辺の中心にボール射出装置があり、平面上に10個の穴がある。

ボール射出装置の向きと平面の傾きをある程度操作できるので、ボールを射出して穴に入れ、後述するスコアを最大化する。

スコアは、穴に入ったボールの個数×ボールが1つ以上入った穴の個数で計算される。 ただし、1つの穴に10個ボールが入るとそれ以上その穴にボールを入れてもスコアには影響しなくなる。

ボール射出装置や平面の操作のパラメータ、射出するボールの個数、その後の次の動作までの待ち時間をセットで指定して、装置を動かすことを繰り返す。 ボール射出装置や平面の操作のために1.7secかかり、ボールを1つ発射するのに0.5secかかる。

まずは60分コーディングし、シミュレータ上でスコアを競います。シミュレータでは、制限時間は100secで、待ち時間が終了するとその時点で穴に入っていないボールは無視されます。

まず、ボール射出装置の角度だけで狙える穴だけ狙うことにします。待ち時間は、最も遠い穴まで転がることを考えても2.1secあれば十分で、このとき1.7+0.5×10+2.1=8.8[sec]となり、10個の穴をそれぞれ狙って10個ずつボールを打っても制限時間に間に合います。

テストケース5つは、予選と同様事前に公開されたものをコードに貼り付ける形です。実は1つだけ、ボール射出装置の角度だけでは狙えない位置にある穴が存在しましたが、それ以外は大丈夫なようです。コーディングして提出すると、3070点になりました。

これは予想していたよりかなり低いスコアです。適当に提出を繰り返して何故か調べてみたところ、どうやら奥の穴を狙って打ったのに手前の穴に吸い込まれてしまっていたようでした。当たり前すぎる……。これをなんとかするためには、平面を傾けて狙う必要がありそうです。

傾いた平面における、球の傾き方向への加速度の式は、問題ページに書いてありました。傾きを\thetaとして\frac 5 7g\sin\thetaらしいです。係数が何から出ているのかよくわかりませんが、これの物理学的導出に注意を払っている時間はなく、僕は久しぶりに扱う等加速度運動にドキドキしていました。

とりあえず平面をボール射出装置(の初期状態の方向)から見て左右に傾けるだけにすることにします。最初に打ち出す角度と到達させたい場所が分かれば、平面の傾きをarcsinで求められることがわかりました。平面の傾きは、支持する軸を上下させて操作するという制約上、それほど大きく傾けられるわけではありません。適当に三分探索などして、平面の傾きが打ち出す角度が制限内に収まるものを探しました。

これで、以前に射出した角度に近いものを使用しないようにしてコーディングしたところ、3260点が出ました。思ったより伸びませんでしたが、ここでシミュレータ問題の時間終了です。

実機への実装

シミュレータ問題の順位が発表されました。僕は2位だったようです。beetさんなどの話を聞く限り、その下はだいたい3060点で団子になっているような感じでした。少しでも改善できたことが良かったのでしょう。2位なので、実機実装は2番目に行えます。

まず30分のコーディング時間が与えられ、そこで最後に提出したコードが実機に実装されます。実機の穴の情報もシミュレータ問題と同様のフォーマットで与えられますが、制約に変更がありました。

待ち時間が終了しても穴に入っていないボールが消えたりしないこと、また台を手前に向けて傾けることが可能になったことは、シミュレータで再現が難しかった部分の制限撤廃でしょう。最も大きい変更は、制限時間が60secになったことです。これによって、ボールを全て打ち切ることが不可能になりました。

実機では、ボール射出装置から見てある穴の後ろに隠れてしまう穴が4つ程度存在するようでした。テストで動かしてみる機会は1度しかなく、10発ずつ打っていると時間がなくなってしまうため、とりあえずの軌道だけ確認しようと1発ずつ打つことにしました。これは結構特徴的だったようです。

それで動かしてみたところ、他の穴に隠れていない穴には順当に入りましたが、傾けて打ったボールは1つが狙い通りに入り、もう1つ穴に弾かれて別の穴に入っただけでした。どうやら遠くの穴を狙うとかなりブレてしまうようです。

これをメモして、他の人の動作をしばらく見ていましたが、装置の不具合などもあって順調に進まず、結構暇を持て余していました。この時間はコーディング不可なので、手で少し計算するくらいしかやることがなくて、あとはなろう小説を読んだりしていました。

本番前

また30分与えられてコーディングし、ここでの最終提出を再度動かして順位を決めます。

先程見たように、遠くの穴を狙うとブレている、もっと詳しく言えば曲がりすぎてしまうようだったので、そこを補正しました。これは「頭で考えた値を埋め込むことを禁止する」というルールに抵触しそうだなと思いましたが、それっぽいif文で押し切りました。

他にも、ボールを射出してからの待ち時間を0secにしてしまったが故に、まだ転がっているボールがあるのに平面を傾けてしまいあらぬところに転がっていった人がかなりいたので、その点も調整しました。具体的には、平面を操作せずに狙える穴だけまとめて待ち時間0secで狙い、そこからしばらく待って、転がりきってから平面を操作し始めるという感じにしました。

傾けずに狙える穴へは変わらず10発打ちますが、傾けて穴を狙うのは難しいので、10発ではなくそれぞれ3発だけにして、時間制限のクリアとともに全ての穴に入れることでのスコアアップを狙いました。

本番

人生終了

穴を狙うときの射出装置の傾き同士の差が、より大きくなるようにしたのですが、それで台を傾けたときの誤差が大きくなったのでしょうか、全然入りませんでした。補正する値を適当に決めたのも良くなかったのでしょう。見返してみると謎の軌道で狙っている穴もあって、よくわかりません。

スコアは420点で、最終的に22位だったようです。同点が3人いて、タイブレークにはシミュレータ問題の順位が使用されました。

コンテスト後

実はスケジュールが遅れた影響で予定していた新幹線に間に合わない可能性が出てきて、本番実装を一番最初に行ってもらった上、最後の10人くらい(と表彰式)を見ずに帰宅しました。

ディスコ本社の看板を撮影していると、玄関口まで送ってくださったスタッフさんが声を掛けてくださって、写真を撮ってもらいました。

夜はぽかーん懐古DPさんと食事する予定を立てていましたが、時間に余裕がなく失敗。一人で食べることになりました。前に碧黴さんと行った東京ラーメンストリートのつけ麺屋に入ろうとしましたが、微妙に並んでいたので、諦めて2軒隣の豚骨ラーメン屋に入りました。

夜は20:12東京駅発22:57富山駅着のはくたかに乗りました。東京・上野・大宮で結構人が乗ってきましたが、幸い2人がけの席を1人で使えるくらいだったので、安心してパソコンを出し、ARCに出場しました。別に隣に人がいたからと言ってARCに出ないわけはないのですが、まあ気を使わなくて済むのはよいことです。

ARCはEまで速解きして温まりました。

コード

コードを公開しておきます。

#include<iostream>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
int XYin[20]={0,450,85,575,340,700,170,700,0,700,-340,700,-280,825,255,950,0,950,-340,950};
pair<int,int>XY[10];
int elapsed;
double calc(int x,int y,double th)
{
    double t=y/(500*cos(th));
    double dx=x-tan(th)*y;
    dx/=t*t/2*5/7*9.8e3;
    return asin(dx);
}
vector<double>S;
void disp(double th,int id,int N,int Tm)
{
    int x=XY[id].first,y=XY[id].second;
    double ph=calc(x,y,th);
    int up=round(tan(ph)*400*1e3);
    int theta=round(th*180/M_PI*1e3);
    if(abs(up)>25000)
    {
        cerr<<"ERROR"<<endl;
    }
    cout<<0<<","<<up<<","<<-up<<","<<theta<<","<<N<<","<<Tm<<endl;
    elapsed+=N*500+Tm+1700;
    //cerr<<elapsed<<endl;
}
bool usd[10];
void disp(int id,int N)
{
    int x=XY[id].first,y=XY[id].second;
    double pre=-M_PI/4;
    const double EPS=1e-2;
    sort(S.begin(),S.end());
    double ansth,ansph;
    double absmn=0;
    for(double t:S)
    {
        double thl=pre+EPS;
        double thr=t-EPS;
        if(thl<thr)
        {
            for(int _=0;_<50;_++)
            {
                double th1=(thl*2+thr)/3,th2=(thl+thr*2)/3;
                double ph1=calc(x,y,th1);
                double ph2=calc(x,y,th2);
                if(abs(ph1)<abs(ph2))thr=th2;
                else thl=th1;
            }
            double th=(thl+thr)/2;
            double ph=calc(x,y,th);
            int up=round(tan(ph)*400*1e3);
            int theta=round(th*180/M_PI*1e3);
            if(abs(up)>25000)continue;
            double mn=9e9;
            for(int i=0;i<10;i++)if(usd[i])
            {
                int tx=XY[i].first,ty=XY[i].second;
                double phi=calc(tx,ty,th);
                mn=min(mn,abs(ph-phi));
            }
            if(absmn<mn)
            {
                absmn=mn;
                ansth=th;
                ansph=ph;
            }
        }
        pre=t;
    }
    if(absmn==9e9)cerr<<"ERROR"<<endl;
    double th=ansth;
    if(th!=0&&y>800)
    {
        if(th>0)th+=5e-3;
        else th-=5e-3;
    }
    double ph=calc(x,y,th);
    int up=round(tan(ph)*400*1e3);
    int theta=round(th*180/M_PI*1e3);
    int Tm=ceil(y*2/cos(th));
    cout<<0<<","<<up<<","<<-up<<","<<theta<<","<<N<<","<<Tm<<endl;
    elapsed+=N*500+Tm+1700;
    //cerr<<elapsed<<endl;
    usd[id]=true;
    S.push_back(th);
}
int main()
{
    for(int i=0;i<10;i++)
    {
        XY[i]=make_pair(XYin[i*2],XYin[i*2+1]);
    }
    vector<pair<double,int> >FST;
    for(int i=0;i<10;i++)
    {
        int x=XY[i].first,y=XY[i].second;
        double th=atan2(x,y);
        bool fn=false;
        for(double t:S)if(abs(t-th)<0.03)fn=true;
        if(fn)
        {
        }
        else
        {
            usd[i]=true;
            FST.push_back(make_pair(th,i));
            S.push_back(th);
        }
    }
    for(int i=0;i<FST.size();i++)
    {
        int Tm=0;
        if(i+1==FST.size())
        {
            double th=FST[i].first;
            Tm=ceil(XY[i].second*2/cos(th));
        }
        disp(FST[i].first,FST[i].second,10,Tm);
    }
    S.push_back(M_PI/4);
    for(int i=0;i<10;i++)if(!usd[i])
    {
        disp(i,3);
    }
}

週記(2021/03/15-2021/03/21)

03/15(月)

週記を投稿してから少しハーメルンを読み、午前9時半に就寝。

午前11時、前日シャワーを浴びなかったことによる頭皮の異常なかゆみに耐え切れず、いったん起床してシャワーを浴びた。そうすると目が覚めたので、生活リズムを無理やり整えるためにも起き続けておくことにした。しかし今度は微妙な眠気でなんのやる気も起きない。

昨日のARC114Eを解いた。「各直線が使われる確率の和(+1)が全体の期待値」という考察、この一言に700点がついているという感じで、本当にすごい問題。一言聞くだけでたちどころに了解できてしまう考察なのだが、コンテスト中は解けなかった。

適当に縮めたあと、この問題では最速コードも狙ってみることにした。1からNまでの逆元を求めるのには、有名なO(N)の実装があるが、変数による剰余算や配列のシーケンシャルでないアクセスが入って遅そうに感じる。ここで\frac{1}{i}=\frac{(i-1)!}{i!}を利用してみることにした。これはO(N+log mod)だが、適当に埋め込めばlog modは消せる。しかし配列を2本使うのはやはりまずかったのか、遅くなってしまった。例えば、普通のO(N)の実装ではmod/imod%iが両方出てきているが、適切に最適化がかかっていれば、これはdivmod相当のことができて1回の演算でどちらも取得できる。やはり見た目よりは高速に動作するのだろう。

逆元の和を取る部分は、事前に累積和を計算しておくことで対応できる。ほかにも剰余を取る演算を引き算に直したり、入出力を自前で書いたものにして、そこそこ速そうなコードが完成した。少し提出して様子を見ると、Clangを使ったほうが速いようなので、それで提出を繰り返す。現在の最速は8msだったわけだが、数回の提出で無事7msを引き当てることに成功した。しかし別の提出について実行時間の内容を見てみたところ、7ms以上を記録したケースが1つしか存在しないものを発見した。これは6msも狙えるのでは!?となって、そこから20回弱の提出を繰り返し、なんとか6msを引き当てた。本当に運が良ければ5msも出ないことはなさそうだが、ジャッジアップデート後のAtCoderの実行時間はブレブレなので、これくらいで止めにしておこう。

atcoder.jp

C問題のコードゴルフをする。解説の式は、k=i-jと置くとこうなる。

\displaystyle N M^N-\sum_{x=1}^M\sum_{k=1}^{N-1}(N-k)(M-x)^{k-1}M^{N-k-1}

2つ目の\sumから先をWolframAlphaに投げると、うまいこと閉じた式が帰ってくる。

\displaystyle N M^N-\sum_{x=1}^M\frac{(M-x)^N-N(M-x)M^{N-1}+(N-1)M^N}{x^2}

M-xでループしたりしてみたが、結局このままのほうが短くなるようだ。N M^N\sumの中に入れて、

\displaystyle \sum_{x=1}^M\frac{M^{N-1}(M+Nx(x-1))-(M-x)^N}{x^2}

atcoder.jp

昨日のTROC20のC問題の解法が間違っていた。全部1のときは常にYESらしい。僕の解法(実際に試してみる)だと、少なくとも2x4の盤面でNOを出力してしまう。

そのあと、ARC114Aのコードゴルフをしたあたりで本格的に眠気が耐えられなくなり、午後6時くらいに1時間の目覚ましをかけて昼寝することにした。この目覚ましで意識を取り戻すまではよかったのだが、さらに1時間伸ばして二度寝した結果、今度の目覚ましには気づけず、日付が変わるまでぐっすり寝てしまった。

03/16(火)

午前2時半起床。あまりよろしくない生活リズム。うっかりハーメルンを読み始めてしまい、そのくせ午前7時半くらいに再度寝落ちした。次に起きたのは正午で、これはICPC 1日目の予定が始まる直前だった。ここからのことは参加記の1日目の部分に書いてある。

kotatsugame.hatenablog.com

夜作っていた2Dセグ木ライブラリについて、もう少し詳しく書いておく。これは、先週のICPCチーム練で出会った問題を解こうとしていた。

残り時間はIと格闘していた。明らかに計算量がヤバそうな解を書いてTLEを出しまくるも通らず、このタイミングで参加してきたゆきのんさんに2Dセグ木では?と言われてうしさんのライブラリをコピペしてきたが使い方がよくわからず、提出が数秒間に合わなかった。ちなみにTLEした。コンテスト後もしばらく格闘していたが、ずっとtest 5でTLEしていてつらくなったので通らないまま諦めた。

週記(2021/03/08-2021/03/14) - kotatsugameの日記

この日使っていたうしさんの2Dセグ木は、HWが105オーダーの時に必要な箇所だけ作るタイプのセグメント木だったようだ。そうではなく、HW個ちゃんと作るタイプの2Dセグ木もあるようで、今日はこちらを実装した。dat[i][j]が、iが対応する区間の行掛けるjが対応する区間の列の値となる。演算の順番は保存できないため、可換性が要求される。1点更新と2次元区間取得はどちらもO(\log H\log W)となる。

algoogle.hadrori.jp

ここにある実装を参考にした。「単純にセグメント木を拡張したもの」とだけ書いてあったので、具体的なアルゴリズムは実装から読み解くことになったのだが、わかってしまえば確かに「単純に拡張したもの」にしか見えない。上の実装は再帰関数によるものだが、通常のセグメント木とほとんど同じロジックで非再帰にできる。結局、僕の実装では以下のようになった。ACL準拠のつもりで関数opはテンプレート引数になっている。

library/segtree_2D.cpp at master · kotatsugame/library · GitHub

これを使って、先週のICPCチーム練のIに再挑戦した。数回の挑戦の後、入力部を少し変えたら1996msで通った。

ヤバすぎ。

FastIOもライブラリ化しておこうと思ったが、設計を考えながら布団に入ったら(?)寝落ちしてしまった。午後10時だった。

03/17(水)

午前3時半起床。昨日と同じくベッドでハーメルンを読んでいたが、今日の予定は昨日よりさらに早いため、万が一にも寝落ちするとまずい。布団を出てTechFULのイベントを進めることにした。TCB34は今日から開催されている。

なんとか全完した。現時点では1位だが……。このセットだと結構ペナが出ると思うのだが、これからイベント終了までどこまで下がるのだろうか。この週記が公開される頃にはイベントが終了しているので、今回もここに解法を書いておこう。

1から5問目はよい。6問目はいくらか迷走したが、小さいほうから(K+1)/2個に1個ずつ取っていくことになる。7問目は後ろから見た。ひたすら面倒。8問目はすべての問題の中で最も簡単だが、セグメント木を使うという1点からここに配置するしかなかったのだろう。9問目は、絶対値をmaxに言い換えて左から見たセグ木と右から見たセグ木で別々に持つ。頭がなかったので1WA。

10問目は問題名を見た瞬間からbinary trieを使うのだろうとは思っていて、事前に準備しておくつもりだったが、9問目でWAを出したのでヤケクソになって突撃。見た瞬間binary trie+Mo's algorithmのO(\sqrt{N}(N+Q)\log \max X)が思い浮かんで、それ以外の方法が全然わからなかったので、書いた。最初に提出したものは初期化ミスで全ケースWAだった。2回目に提出したものはTLE。ここで、Mo's algorithmの影響で同じ値を何度もbinary trieに出し入れしているため、アクセスするノードを前計算しておくことにすると、最遅ケースで3.5secになり、通った。

かなりつらい。TechFULのジャッジは弱い、というのはわかっていたので、最初からできるだけの高速化をしておけと言われればその通りなのだが……。もうちょっとオーダーの良い解法があるのだろうか。ほかの人がどうやって解いたのか知りたい。

これをきっかけとしてbinary trieのライブラリを作ることにした。bit幅をテンプレート引数で受け取るようにする。keyを表す型は、unsigned long long固定ではなく、bit幅に応じてunsigned intと切り替えて使い分けるようにしたいが、このようなことは可能だろうか?TLに質問を投げたところ、えびちゃんさんからconditionalというものを教えてもらった。C++11かららしい。

std::conditional - cppreference.com

これを使って少し書いていたら、ICPCの2日目が始まった。コンテストとそのあとの懇親会については参加記の2日目の部分に書いておいた。昨日の日記にもリンクを貼ったが、ここにも貼っておこう。

kotatsugame.hatenablog.com

そういえば、昨日の夜格闘していたICPC練のI問題についてだが、こるとんさんのチームも最近同じセットを走ったようなので、懇親会で聞いてみたのだった。どうやらlog 1つの解法があって、ただ2Dセグ木によるlog 2つでも通らないことはないだろう、というコメントがkoosagaから出ていたらしい。

夜、参加記を書いていたが、こどふぉが始まっていたことに気づいたのでなんとなく参加した。CF #708 Div.2である。途中から眠気がすごくて大変なことになったが、何とか全完した。

Aから面倒だが、適当にやってよさそう。Bは2WAしてキレそうになったが、よく見ると(M+1)/2であるべきところがM/2になっていた。最初のWAのあと、この部分も確認したはずなのだが……。このあたりで自分が思ったより疲れていることを自覚した。C1は気合いで場合分けしたらできた。C2も気合いで場合分けしそうになったが、ふと、3個残して他を全部1にするとC1に帰着できることに気づいた。TLでは面白い問題であると評価されていたが、確かに、誘導としてのC1がないとハマってしまったかもしれない。

この時点でDはsolvedが2人とかだったので飛ばし、E1に行った。E1は自明。その後Dに戻った。メモリ制限が厳しいタイプの問題のようだ。dpをするとき、配列を使いまわすことで次元を1つ減らすテクを使えば、この制限自体は簡単にクリアできそうな気がする。適当に書いたらサンプルが合わなかったが、よく見るとサンプルの出力と説明に書いてある値が異なっている。説明を信じることにすれば、僕の出力は正しいようだったので、提出してみたところ、1ケース目のサンプルで落ちてしまった。これはペナルティに含まれないはずなので、まあよい。サンプルを合わせて再度提出すると、今度は4ケース目でWAが出た。これの解消には非常に苦しんだが、試行錯誤の過程でセグメント木を使用する必要がないことに気付いたため、実行時間が大幅に短くなったのはよかった。眠気に抗いながら考えて、何とかHackケースを見つけることができてAC。メモリも実行時間も申し分ない。

E2はかなり速く解法にたどり着くことができたが、コーディングも終盤に差し掛かったあたりでふと残り時間を見ると、コンテストがあと2分くらいになっていて非常に焦った。急いで書き上げてサンプルを確認し提出したところ、残り時間が増えてびっくり。これは、途中でコンテストが15分伸びたのに、ページをリロードしなかったせいで起こった勘違いだったようだ。

コンテストが終わってから再度参加記を書く作業に戻り、午前2時半に投稿。午前3時就寝。

03/18(木)

午後2時起床。

懇親会でkotamanegi氏に聞いてみたところ、似たような問題設定を考えていたこと、またこのような問題設定が好きであるからC問題に手を付けたと言っていた。僕も同様に、このような問題設定(または問題名「Short Coding」)に好奇心を刺激されたので手を付けた。確かに問題の見た目的に重そうではあるものの、この問題に興味を持つようなチームが2つも存在してしまった、というのがbeet_aizu氏の戦略がうまくいかなかった原因だったのだろう。そうでなければ後回しにすべき問題に見える、というのには頷ける。

ベッドの上から動かないまま寝たりなろうを読んだりしていたら、午後9時半になってしまった。起きて食事。今日は板チョコレート。そのあと、しばらく自分の本名で検索して遊んでいた。自分の本名というのは検索してはいけないワードとしてよく挙げられるものであるが、僕の場合はマイナスイメージのあるような検索結果が出ないため、かなり気軽に検索できる。

Twitter Web Appのフォローボタンが、これまでは未フォロー⇔白だったのに、既フォロー⇔白になっていた。なんで色を、よりにもよって真逆にしてしまったのか。全然慣れない。

ECR106があったので出た。4完、5完時点では1位だったが、Fに手間取って4位。Gは手も足も出なかったのでなろうを読んでいた。

Aはよい。Bは、最も右にある00と最も左にある11のインデックスを比較した。Cは何回曲がるかを全探索すればよい。

Dはちょっと難しい。数abに対し、g=\gcd(a,b)a'=\frac{a}{g}b'=\frac{b}{g}とおくと、ca'b'g-dg=xが得られる。よってa'b'=\frac{x/g+d}{c}gを決め打ったとき、左辺の2数は互いに素であればよいため、右辺の素因数を(指数ごと)a'b'のどちらに振り分けるか?という問題になる。さすがに毎回素因数分解していては間に合わないだろうから、素因数の種類数を2\times 10^7まで求めておくことにした。gxの約数を全探索するが、まあこれくらいなら制限時間に収まるだろう。実際収まったが、前計算に800ms使っているのでちょっとひやひやしていた。

Eはパッと見難しいが、初期値としていろんなところに1を入れたdp配列でdpすると求まる。片方の文字列の長さが0になってしまうケースは、dp遷移で排除できなさそうなので、別個に計算して引いておいた。Fは木DP。部分木に対して、その根からたどれる一番深い頂点の深さをkeyにして場合の数を数え、直径がkを超えないようにマージしていく。深さk/2以下はいくつでもマージしていいので一気に計算して、k/2より深い場合は、その深さを達成する子(これは全部試す)とそれ以外の子に分けて計算する。mod上の除算が必要になって、間に合うだろうと適当に出したらTLEしてしまった。ちゃんと左と右から累積積を持つことでAC。

Gは、そもそもクエリ形式になる前の最初の問題すら解けなかったが、本当に有名問題だったらしい。

noimi.hatenablog.com

結局、パスの端点がユニークで、パスに含まれる辺が交互に塗られていればよいはずだ。パスの端点のユニーク性については被ったら適当に繋げればよくて、hash全部の和と現在答えに足されているhashだけの和を持てば色のflipができる。パスを閉じてループにするような辺を追加するときだけ、パス全体を取り除いてしまうことにする。復元も難しいが、頂点ではなく辺に対してそれに繋がる辺番号を持たせておけばよい。微妙なミスでWAを出して原因の原因の原因を特定するのに非常に苦労したが、なんとか通った。重いのは出力を毎回flushしているから。

https://codeforces.com/contest/1499/submission/110399277

ゴミ出しをし、布団に入ってなろうを読んでいた。午前9時半就寝。

03/19(金)

午後3時、学生支援課活動支援係からの電話に気づいて起床。どうやら部員名簿にある人数と別の書類に書かれた人数に大きな隔たりがあるらしい。今現在の部員名簿を再度提出することになった。

昨日から読んでいた「奇運のファンタジア」の最新話に追いついた。

https://ncode.syosetu.com/n8357fd/

天才経営者、とか大企業に成長、とか書いてあったので、法人としてのチートみたいなことを期待して読んだのだが、個人のチートに重きを置かれていてちょっと見込み違い。ただ、丁寧なテンプレ展開で非常に心地よく読める作品だった。

そのまま午後5時半に寝落ちして、次に起きたのが午前2時だった。昼間受けた電話で、活動支援係の方に、夜のうちに書類を出しますと言っていたのだが、すでに夜中になってしまっている。慌てて作成を始める。

部員名簿は新歓期間の終了時に更新したものを作ったはずだが……とサークルのGoogle Driveを探して、12/26に作成した書類を発見した。これにはちゃんと正しい人数が記載されているように見える。この日の日記にも次のように書かれている。

今年度の新歓期間は12月の半ばまであったが、それが終了したので、サークルの名簿などに更新がある場合報告せよとのメールが来ていた。名簿を更新して提出した。

週記(2020/12/21-2020/12/27) - kotatsugameの日記

しかし、いざ送信済みメールを探してみると、提出のメールがない。どうやら作成だけして提出をすっかり忘れていたらしいことが分かった。これはひどい失敗だ。なぜこのようなことが起こったのかさっぱりわからないので、再発防止策も取れない。とりあえず、今年に入ってから入部してくださった2名の新入部員をさらに付け足して提出しておいた。

寝ている間に行われたyukicoderの問題を解く。A、Bはよい。Cは面倒だが、まあやればできる。Dはタグに尺取り法と見えてしまったが、これは設定でゆるふわモードにしているからで、変える気はない。E以降はsolved数が少ないので止めておくことにした。

水曜日にちょっと書いてから放置されていたbinary trieを完成させた。ほかの人のライブラリを参考に、適当に必要そうな機能を盛り込んでおいた。binary trieに限らずtrie木一般に、子のノードをポインタで持つ実装をよく見るが、Library-CheckerのSet Xor-Minではvectorとそのインデックスで持つ実装のほうが速かったので、そちらを採用した。trie木だけのシンプルなコードのためvectorのメモリ空間の再確保が行われないから速い、というだけかもしれない。

library/binarytrie.cpp at master · kotatsugame/library · GitHub

さらにFastIOを作った。

library/FastIO.cpp at master · kotatsugame/library · GitHub

いろいろ仮定してできるだけ速くなりそうな書き方をしている。例えば、空白や改行が2つ以上連続したりすればそれだけで壊れてしまうし、数値の途中に変な文字が紛れ込んでいても気づけないかもしれない。わざわざFastIOを使うのだから、どうせオンラインジャッジ上で正しい入力のときだけ動けばよくて、それより余計な比較などをできるだけ省こうという算段だ。設計はclayのものを大いに参考にした。

布団でなろうを読む。いろいろな作品の冒頭だけ読んで投げ出すということを繰り返していた。1作、面白いと感じるものを見つけて読了した。「異世界で美少女になった俺、地球に戻れたので裏垢やってみる。」。

https://ncode.syosetu.com/n6063gu/

ABCに向けて、さすがに寝ないとまずい。午後3時半就寝。

03/20(土)

午後6時、地震で起床。頭の真上にあるエアコンを見て、これが落ちてきたらまずいな、と思ってベッドの足元のほうに避難し、布団を引っ被って揺れが収まるのを待っていた。前回の地震の際、本棚に取り付けてある突っ張り棒のゆるみに気づいて直していたおかげか、今回は特に本が落ちてきそうになることはなかった。

机の下にもぐっていると、本棚がグラグラ揺れているのに気づき、思わず押さえに行ってしまった。

週記(2021/02/08-2020/02/14) - kotatsugameの日記

宮城県の沿岸部に津波注意報が出たらしいが、おそらく自分が今住んでいる場所は沿岸部ではないだろうと考えて、そのまま二度寝した。さすがに眠すぎた。

午後7時から30分おきの目覚ましを聞きつつ、午後8時半起床。ギリギリまで寝られたので良かった。部屋の中を確認したが、モニターがずれているくらいで特に地震の影響はないようだった。

食事してABC196に参加した。全完18位だった。A、Bはよい。Cも全探索してよい。Dは、一行持つbitDPを書いたのだが、うっかり4Wくらいの計算回数になってしまってTLEした。1行の場合がまずいので、1行目だけ見なくてもよいとわかる部分を見ない3WにしてAC。Eは適当に関数合成。Fはこれ↓で、(a-b)^2を展開した。解説によれば、a xor ba(1-b)+(1-a)bと式変形できるらしい。僕の解法は2乗の項の処理がちょっと面倒。

?(任意の1文字)を含む文字列のマッチングがFFTでできるという話を覚えていたので、試した。?を0に、その他の文字をユニークな正整数に対応させて、Σ_{i-j=k}a_ib_j(a_i-b_j)^2を計算する。

週記(2020/11/30-2020/12/06) - kotatsugameの日記

コードゴルフをする。A問題は、まずbashで解いて、全完後にAWKで書くと縮むことに気づいた。そのあと、この日記を書いている間にさらにAWKで縮められていた。getlineを使う発想が出てきた試しがない。実際に書いたことがないため、どのようなときに縮むのか?という嗅覚が育っていないのだろう。B問題はsedだと思っていたがVimに負けた。7Bの解は、削除しようとしたときに何も削除しなかったらレジスタの状態が変わらないのを利用していて面白いと感じられたが、普通にdwで取れたようだ。6B。7Bの面白さに夢中になっていて気づけなかった。C問題はAWKが短いと思ったらdcに負けた。同じくdcで取り返したところ、等号付き不等号の等号が外せたらしく、再度取られた。26B。これも単純なミスで非常に悔しい。

DEFはRuby。Dは判定方法が面白い。畳が覆う2マスの番号をペアにしてリストアップしておき、そこからA個取り出し、覆うマスに重複がないか見る。これはCythonの判定方法を移植しただけ。なぜコードゴルフにおいて頑なにPython系の言語を使おうとするのか……。Eは、clampLRをリストで持つと覿面に短くなった。clampに渡すときはsplat演算子で一発だし、更新のときもLRは同じ処理なのでmap!でできる。Fは嘘bitset解法が通る間に縮めておいた。

今回は僕がやってることで、僕からすればデメリットが一切なくてただ嬉しいだけだったが、ほかの人がこれをやってるのを僕が見ると、ちょっとどころではなくモヤモヤしてしまうだろう。より一般に、八百長コードゴルフみたいなことをしたくないという僕のささやかなプライドが理由となり、今後も僕がAtCoderで作問作業に関わることはない。

布団でハーメルンを読もうとしたら素早く寝落ちした。午前8時半。

03/21(日)

午後2時、目を覚ます。寝落ちする前に読んでいたハーメルンを開いてしまい、そのままずっと読み続けていた。全体の話数の半分くらいで本編が終了し、残りは番外編という位置づけであることがタイトルからわかる。とりあえず本編終了まで流し読みしたが、求めていたものとは違った。

syosetu.org

現代社会で乙女ゲームの悪役令嬢をするのはちょっと大変」というなろうに似た作品を読みたいと思い、「財閥」というキーワードで検索して出てきた作品。実際にある架空の財閥の栄枯盛衰を描く話なのだが、物語というよりかは資料という側面が強いように感じられた。さすがにそういう設定だけ食べて生きていけるほど鍛えられてはいない……。

午後4時、さすがにもう少し寝ておかないとまずいという気持ちになったので、寝た。午後5時半から30分おきの目覚ましがかかっていて、午後7時のもので何とか起床に成功。コンビニに出かけて食事を買おうとしたが、時間帯の問題かかなり売り切れていてびっくりした。食べてしばらくするとARC115。

ARC115はギリギリ5完でパフォーマンス2628、レートは2665→2662(-3)。世知辛い。

Aから難しいが、しばらく考えていると、なんとなく__builtin_parityが異なるペアを数えればよさそうというのがわかってくる。これは頑張れば証明できそうだが、どうにも焦って頭が回らないので、提出することにした。AC。BはTakahashi's Information。Cはよくわからないが、ある数がxと決定した時にそれの倍数すべてにmax(*,x+1)を作用させたら通った。この時点で15分。

atcoder.jp

Dは難しい。45分考えてちっとも進まなかったので、Eに飛んだ。Eは、まず109要素のdp配列を考えてみると、おおよそdp<-sum(dp)-dpのような遷移をすることがわかる。Aの値によってdp配列の後ろの要素が増えたり減ったりするが、ある程度の区間は常に同じ値になる。これを区間ごとにシミュレートすればよい、ということまではすぐわかった。実はこれは一次関数が作用素の遅延セグメント木に言い換えられるようだが、コンテスト中は思いつかず、必死にstackをいじって値を合わせようと格闘していた。改めて思い返してみれば、一番最初だけsum(dp)の代わりに1を使わなければいけないことにハマっていたのだろうと思える。符号を交互に切り替えて足す操作を累積和で再現するのは最初から上手くいっていたが、左端のoff-by-oneエラーに苦しめられた。40分くらいかけて何とか合わせたら通った。残り20分。Dに戻る。

閉路の個数に着目すべきだと考えた。極小な閉路(これにはサイクル基底という用語があるようだ)の個数を使って云々、を考えていたが、全探索するコードの出力と考えている結果がどうにも合わない。もう少し単純なグラフの出力を観察してみようとしていくらか試していると、どうやら、すでに連結な頂点同士を結ぶ辺を追加すると値が2倍されるらしいということが分かってきた。いったんこれを認めることにすると、あとは森に対する答えがわかればよい。パスの長さを変えて観察していたが、ここで閃く。6頂点のパスと6頂点の木を与えてみると、どちらも出力が同じであることが分かった。木なら頂点数だけから答えがわかるようだ。どうやって答えを計算するのかも少し悩んだが、少し考えると頂点のうちいくつか選ぶ組み合わせの個数であることに気づく。ここまでわかった段階で、さかのぼって実験で確かめたいくつかの事実の確からしさというものが上昇したのを感じられた。もう時間もないので証明は一切考えずそのまま書くことにする。

UFで連結成分のサイズと全域森に含まれない辺の本数を数える。連結成分ごとに組み合わせの値を記録した数列を作って、全部を畳み込みで掛け合わせ、さらに要素を2の何乗倍かする。サンプル2が合わなくて焦ったが、偶数インデックスだけ抜き出した列を計算していたせいで出力するときのアクセス箇所がずれていたようだ。何とか修正して投げるとACした。残り1分もなかった。

残り20分で3完から5完まで駆け上ったはいいものの、レートを上げるには遅すぎたらしい。自分はDで45分間何をしていたのだろうか。こういうところで詰まるとダメなんだなあ。また、D問題は自前のUFとACLのmodint/convolutionを併用したので、oj-bundleで展開した後さらにACLをincludeパスに含めるという手間がかかってかなり焦ってしまった。こういうのも一発でできるようにしておきたいが、さておき常にACLをincludeパスに含めるとCFなどでもそのまま提出してしまいそうで怖い。oj-bundleACLも展開しようとすると、include"atcoder/*.hpp"などとhppを付けたりダブルクォーテーションで書かなければならないようで、ちょっと面倒。

ARCの直後にCF 709 div.1があったので出た。3完270位で2744→2649(-95)。これはひどい

Aは可能な日が少ない友人から一気に割り当てていったらWA。可能な日が最も多い友人について、ほかの友人が可能でない日から先に(m+1)/2日割り当てると通った。可能な友人が最も少ない日から貪欲に割り当ててもよかったようだ。Bはqueueと双方向リストでシミュレート。これも最後のほうの処理でミスして1WA。Cはセグメント木を書きそうになったが、区間クエリの右端が必ず全体の右端なので、priority_queueで書ける。値のほうを優先度にして持つpriority_queueも作っておいて、消えた要素は適宜インデックスに対するフラグを立てて読み飛ばせばよい。

Dは、3つ組(u,v,l)について、コストcの辺u->wを使って(w,v,l-c)に書き換えるという操作を何度も繰り返せばよいと考えた。当然vのほうからも縮めていく。lの大きいものから順に更新していきたいのでpriority_queueで見ていく。……ところがMLEしてしまった。priority_queueに入る要素数が多すぎるのだろうか。しばらく試しても通らなかったので、優先度書き換えできるとよいと考え、えびちゃんさんの記事からFibonacciヒープを盗んできて貼り付けてみた。今度はTLEした。本当に絶望。そのまま通らずにコンテスト終了。

rsk0315.hatenablog.com

TCB34が終了した。6位だった。1位は理論値らしい。このセットで理論値出すのはさすがにヤバい。埋められない差を感じてしまう。10問目のbinary trie+Mo's algorithmは、考えたもののTLEしたので諦めたという人がいくらか見られた。もっとちゃんとした解法があるらしい。1つだけTLで見たのは、使える魔法の区間の右端でソートして、左端についてはbinary trieで条件を満たすような魔法のインデックスの最小値を求めて比較することで判定する、というもの。これは非常に面白く感じられる。binary trieというのはつまるところ必要な箇所しか作らないセグメント木であると見れて、そのとき当然区間minクエリに答えられるし、さらに全体にxorできる性質も変わらない。

ARCのコードゴルフをした。AはPerly/1//&1がいい感じ。Bはよくわからないが、とりあえずOctaveで解いておいた。Cは1 2 2 3 3 3 3 4...という出力をする解法が天才的。Rakuでmsbを使う。Eはぽかーん懐古DPさんの解が強い。僕のコンテスト中のACコードから頑張って変形してみたが、今何イテレーション目かによって答えの符号を変えることで累積和を単純なものに落とし込めるようで、そこからさらに和の取り方を変えると、区間の両端を持たなくてよくなったり累積和がいらなくなってpopしたstackの要素を足し合わせればよくなったりする。

ICPCアジア地区横浜大会 2020 参加記

2021/03/16-2021/03/17に行われたICPCアジア地区横浜大会に出場しました。チームはAobayama_dropoutで、メンバーは碧黴さん、ゆきのんさん、僕です。

今年は新型コロナウイルスの感染拡大の影響で、オンラインでの開催となりました。それに伴い、ネット上の情報を自由に参照できたり、事前に用意したコード片を使用してもよいことになっていました。

チーム内でのコミュニケーションはSlackのみを利用し、すべてテキストベースで行いました。音声通話をしようかとの案も出ましたが、なんとなく消極的になっているうちに立ち消えていました。

コンテスト前

Aobayama_sugarstepのチーム練に便乗する形で練習していました。ありがとうございました。

1日目(03/16)

生活リズムが前にずれていて困っていました。午前2時半に目を覚まして、そこから眠れなくなって布団でゴロゴロしていたのですが、結局午前7時半に再度寝てしまったようで、次に起きたのが正午でした。なんとかセーフ。

prefixがAobayama_であるチームが2つ存在したので、チームごとの行動の際に混じったことがあったようです。が、それ以外は特に問題なく、学生証のチェックが終了しました。

開会式が終わって、practiceが始まりました。碧黴さんがA、ゆきのんさんがBを読むので、僕はCとDを読みます。どうやら昨年と全く同じ問題のようです。Cは二重階乗のオーダーで探索することを覚えていましたが、Dはあんまり覚えていなかったので、Dから解くことにしました。最短経路だけ取り出したグラフを考えたとき、ある辺について、そこを迂回するような経路が存在するか判定できれば良いです。橋の検出ライブラリを貼れば楽だったようですが、言いかえが思いつきませんでした。今回は始点からの距離を並べた列を作り、imos法で辺に対応する区間に加算して、対応する区間が2本以上の辺で覆われているかどうかで判定することにしました。一発AC。Cも適当に書いてAC。この時点でAとBも通っていたので、全完です。

practiceで速さを求めて全完しても虚しさがすごいです。そのあと、適当にWAやTLEを出してみて、何となくジャッジの挙動を確認できた気持ちになります。例えばreturn 1;でREになること、longが64bit整数であることなどは重要でした。

practiceの後に少しZoomでお話があって、今日の予定は終了です。夜は2Dセグ木のライブラリを作ったりしていましたが、午後10時くらいに寝落ちしてしまいました。

2日目(03/17)

午前3時半くらいに目を覚まします。しばらく布団でゴロゴロしていたのですが、今日は二度寝するとまずいので、起きることにしました。朝は食事した後TechFULのイベント問題を解いたりして過ごしていました。

Zoomに入る時間を間違えてギリギリになりましたが、何とか最終連絡を聞くことができて、いよいよコンテストです。

コンテスト

問題文:http://icpc.iisf.or.jp/past-icpc/regional2020/problems-2020.pdf

順位表:https://icpcsec.firebaseapp.com/standings/

C問題

碧黴さんがA、ゆきのんさんがBを読みます。僕は残りの問題を適当に開いて訳す感じで動こうかなと思ったのですが、C問題の問題名が「Short Coding」だったので、これは解くしかあるまい、としばらく時間を使って考えることにしました。

しばらく唸っていると、右手法(あるいは左手法)が使えることに気づきます。つまり、右手法を実際に再現するようなプログラムを書けば、どんな盤面でも必ずゴールできることになります。おそらく、かなり短いプログラムで再現可能で、それより短いプログラムについては全探索するというのが問題の意図なのでしょう。実際に手でいろいろ試していると、次のような5行のプログラムが完成しました。

RIGHT
IF-OPEN 5
LEFT
GOTO 2
FORWARD

サンプル出力が最大で5行であることからも、これが最短っぽいです。それ未満の行数のプログラムを全探索するコードを書いてサンプルを試してみると、出力される行数が一致していることがわかります。よさそうなので提出しましたが、WA。配列外参照していることに気づいたので直しましたが、またもやWA。コードをじっくり見ていると、右に曲がるときと左に曲がるときで方向を表す変数に足す値が逆になっていることに気づきました。そこを修正するとAC。45分でした。

CのFAはonionsの43分なので、ギリギリ負けたことになります。改めて以前のサンプルに対する出力を確認してみると、サンプル1から正しくない解を出力していることに気づきます。行数だけじゃなくてちゃんと内容にまで気を配っていれば……と後悔することしきりでした。しばらくはこれが頭にこびりついて離れなくなりました。

G問題

僕がCを解いている間にBが通っていました。Aはちょっと炎上していそうですが、あんまり触れずに次の問題に進みます。Dを読んで概要を共有しました。シンプルな問題ですが、とっかかりがつかめないので、次の問題に進むことにします。この時点ではGとHが2チームに、Jが4、5チームに解かれていたと思います。ゆきのんさんがすでにJを読んでいるので、僕はGを読むことにしました。ゆきのんさんはそのままJを書き始めていました。

Gの概要を共有してからしばらく考えていると、あるしきい値に対してそれ以下の頂点の数x、連結成分の個数aと、それより大きな頂点の数y、連結成分の個数bが与えられたとき、a+b-1<=min(x,y)が必要条件であることに気づきます。通され具合からこれが十分条件でもあると決めつけることにして、とりあえず実装します。

2021/03/18追記:解説でもこの部分の証明は飛ばされていましたが、そのとき話していた熨斗袋さんに帰納法が回るとの指摘を頂いたことを思い出しました。a+b>=3のときは、2つ以上の頂点からなる連結成分が必ず存在することがわかるので、その連結成分を適当につなげることにします。そのあと使った頂点を無視することで、不等式の両辺から1引いた問題に帰着できます。

xyを求めるのは簡単です。abを求めるのには、しきい値をずらしながら一つのUFを更新するのではうまくいきませんが、しきい値以下の頂点としきい値より大きい頂点に分けてそれぞれ計算すればうまくいきそうです。そのようにして実装してみるとサンプルが合ったので提出、AC。78分でした。この数分前にJも通っていました。

H問題

Aのコードが共有されてきました。改めて以前のやり取りを読み返してみてもどういうロジックなのかよくわからなかったので、各座標(x,y,z)について3方向の影を調べてブロックの存在判定をし、最後に条件を満たしているかチェックする、という方針に書き換えてもらうことにします。入力される文字列と座標の対応がややこしいですが、座標軸に合わせて考えることにすると、それぞれどの方向に軸が存在するか描かれた図を使うことで比較的簡単に変換の式が得られます。

書き換えてもらっている間にIを読んで概要を共有します。数え上げですがすぐにはわかりません。ゆきのんさんがHを読んでいて、そちらの概要も共有されてきました。こっちはクエリに答える問題なので、とっかかりは楽そうです。Hをしばらく考えることにします。

素因数ごとに独立に考えるのは明らかです。103以下の素数の個数を数えると168個でしたが、168本くらいならセグメント木を持ってもよさそうな気分になります。素因数の指数を小さいほうから3つ持つようなセグメント木を書けば、実際に答えにおけるその素因数の指数を計算することができそうです。定数倍が怖いですが、TLが10secもあるので多分大丈夫だろうという気持ちになりました。

残るは103以上の素数についてです。これは、区間の先頭k+1項に含まれるような素数しか候補となりえないため、それぞれチェックしてよさそうです。チェックの方法についてですが、事前に103以下の素因数を持たないようにしておいた数列に対して区間==を計算するようなセグメント木を作ってその上で二分探索すれば、ある素因数が出現する区間を求めることができるので、k+1回繰り返せば間k個を抜いたような区間の右端が求まります。

この時点でAが通っていたと思います。解けたので書くことにします。サンプルが貧弱で、103以上の素数が必要となるケースが存在しなかったので、自分で適当に作って足しました。しかし依然貧弱なままだったようで、例えば等号付き不等号の等号が抜けていたり、l+i項目を見ようとしてi項目を見てしまったりなど残念なミスで2WAを重ねました。結局147分時点でAC。

I問題

E問題の概要が共有されて、見覚えがあるという話がされていました。確かに僕も見たことがある気がします。

後からTLで知りましたが、JAG夏合宿2018day1Eらしいです。これは確か、どこか海外のICPC予選セットだったと思います。なのでAOJを探しても収録されていないわけなんですね。

Eを解こうとして、円の半径をにぶたんすれば良いと分かったのですがWA、多角形が円の半分に偏るコーナーケースにゆきのんさんが気付いたのが残り15分時点で、実装できませんでした…。

JAG夏合宿・ACPC2018 参加記 - kotatsugameの日記

角度を足して1周ぶんになるかを見ること、円の片側に多角形が寄った時の処理が難しいこと、などが思い出されました。適当に立式して二分探索すれば解けそうであることが分かったので、ゆきのんさんに任せて、自分はIに挑みます。

まず、入退室が両方記録されているIDは無視してよいです。それ以外を考えます。

制約がn<=5000なので、1次元足したdpをするのだろうなという気持ちになります。入室の際にIDがわからない人にIDを割り振ってしまうと、入室と退室でそれぞれ1つずつ情報を持たなければならない気がします。ここで、退室のときに入室まで遡って割り振ることにしてみると、情報が1つだけでよいような気がします。まだIDが割り振られていない人が室内に何人いるか、の情報を足す1次元で持つことにして、実際に書いて様子を見ます。直感的に、それ以外に必要になるような値はすべて適切な前計算と組み合わせて導出できる気がします。

dp[i][j]で、今i番目の記録を処理していて、室内にIDが割り振られていない人がj人いる場合の通り数を表すことにします。まず入室処理についてですが、これは、IDが割り振られている人が入室するか(dp[i+1][j]+=dp[i][j])、そうでない人が入室するか(dp[i+1][j+1]+=dp[i][j])の違いになります。特に係数が掛かったりはしません。

次に退室処理についてです。退室する人のIDがわかっているときは、室内にいるIDが割り振られていない人のうち1人を選んでIDを割り振ることになるため、係数にjがかかってdp[i+1][j-1]+=dp[i][j]*jとなります。IDがわからないまま退室するときは幾分複雑で、

  • 室内にいるIDが割り振られている人が退室する

現時点での室内の人数kが前計算でわかるので、dp[i][j]+=dp[i][j]*(k-j)です。

  • 室内にいるIDが割り振られていない人が退室する

室内にいるIDが割り振られていない人のうち1人を選んでIDを割り振るのは、先ほどと同じです。ただ今回は割り振るIDにも自由度があります。入退室のどちらにも記録されていなかったIDの個数のうち、現在すでに使われているIDの個数(つまり、IDが割り振られていないまま入室して、IDがわからないタイミングで退室していった人数)を求めましょう。IDが割り振られていないまま入室して、現在すでに退室していった人の人数は(これまでに入室したIDが割り振られていない人)-j人です。IDがわかるタイミングで退室するときは、必ずIDが割り振られていないまま入室した人から選ばれることがわかりますから、IDがわからないタイミングで退室していった人数は先ほどの値からさらに引いて(これまでに入室したIDが割り振られていない人)-j-(これまでに退室したIDが割り振られている人)人です。

かっこで示した値や「入退室のどちらにも記録されていなかったIDの個数」は前計算でわかるので、割り振るIDの自由度rが求まり、dp[i][j-1]+=dp[i][j]*j*rです。

これでサンプルを試すと合いました。この問題のサンプルもかなり貧弱に感じられますが、とりあえず提出してみると、AC。189分でした。

E問題

円の片側に多角形が寄っているとき微妙に変な値が出る、とのことで、ゆきのんさんのコードが共有されていました。適当に口出ししながらK問題を読みます。適当なdpを考えて実装したらサンプルが合ったので、チームメイトに何も言わずに提出したのですが、WAでした。いくつか証明をしていない性質を仮定して解いたので、仮定が間違っていたんだなあとはわかりますが、具体的にどういうケースで壊れるのかはわかりません。K問題は頑張っても通りそうにないので、E問題を通すことに集中することにしました。

コードをコピペしてきて手元で動かしていろいろやっていたら、サンプルが合ったので提出しました。するとゆきのんさんもほとんど同時に提出していたらしく、どちらもACしました。239分でした。両方通ったからよかったものの、例えば僕のコードがWAで、しかも微妙に僕のほうが早く出していた、みたいなことになったら目も当てられません。ここははっきりとコミュニケーション不足が表れました。

ゆきのんさんは誤差に苦しんで、結局acosasinに変更したらサンプルが合ったそうです。僕はacosのままでしたが通ったので、よくわかりません。Heno Worldも誤差で苦しんだという話をしていたのですが、僕自身はそういう感じがしないので、どこかの実装方針が違ったんだろうと思っています。しかし自分のコードもよく見るとEPSとEPSを10倍した値の2種類を使い分けている箇所があるので、たまたま1回目の提出で当たりを引いただけなのかもしれません。

残り

K問題を解いていました。現在の方針は、文字列の先頭にTのprefixを繋げていくdpで、繋げるときのスコア計算をZ-algorithmで行うものです。つなげる先がどのような文字列かわからない状態でスコア計算をするのはかなり怪しいですが、Tが繰り返されていると考えて大丈夫だろうと思い実装していました。しかしWAが取れないので、どうやら大丈夫ではないようです。しかしどのようなケースで落ちるのかが全然わからないため、ojでランダムケースによるテストをしてみました。すると、うまく愚直解と異なる答えが出るものが見つかりました。

bbcbbbcc
8

とりあえず、Tが繰り返されていると考えるスコア計算が実際にダメであることがわかりました。ここで、先頭に繋げるprefixの長さは広義単調増加するだろうと考え、現在どの長さのprefixを繋げたかも持つ3乗のdpを書いてみました。今度は上のケースにも通りましたが、当然TLEしてしまい、高速化をしようにもうまくいかず、そのままコンテストが終了しました。

コンテスト後

Gather Townに集まって話しながら解説を聞きました。K問題はオートマトン上でdpするらしいです。そう言われると見たことあるなあという気持ちになってしまうのですが、次見たときに解けるかどうかはちょっと怪しいです。その後YesNoを見ました。自分のチームのKへの提出が通っていないのは知っているのですが、実際Noとなる瞬間を目にすると非常に残念になります。

途中送った顔写真が公開されるフェーズがあったのですが、僕は(ほとんど故意に)送り忘れていたので、いらすとやの画像に差し変わっていました。みんな提出していないものだと予想していたのですが、かなりの人数が自分の顔面を提出していて本当にびっくりしました。

YouTubeLiveの放送が終了した後は、Gather Townが閉まるまでずっと喋っていました。途中からピクトセンスを始めたのですが、通話しながら遊ぶのは非常に楽しかったです。小学生みたいな下ネタで笑うのも楽しいです。

結果

8完10位でした。例年と単純に比較すれば良い成績であると喜ぶところですが、今年は並列可能になったり海外チームがいなかったりネット検索が可能だったりといろいろ勝手が違うので、ちょっとモヤモヤはします。とりあえず3年間順位が単調減少しているので、来年は1桁順位を取れるよう頑張りたいです。

週記(2021/03/08-2021/03/14)

03/08(月)

先週日曜日はWriteUpをいくらか書いたら力尽きた。コードゴルフ大会中は週記を書く作業もストップしていたが、溜めてしまったものを書いて投稿する気力もなく、午前7時に就寝。

午後7時に起床。まだ眠かったので再入眠し、結局午後11時半まで寝た。これでもまだ眠い気がする。寝すぎただけかもしれない。

まずWriteUpの残りを書いた。次に、先週の週記を完成させつつ、タイミングを見失っていたICPCチーム紹介ドキュメントに関連する記事を投稿し、模擬地区予選に関する記事も書いて投稿した。

kotatsugame.hatenablog.com

kotatsugame.hatenablog.com

模擬地区予選に関して。実装の分担は常々大事だと思っていて、これは3並列が可能になった今、さらに重要度を増したはずだが、へのkのチームはほとんどへのk一人で解いて実装してしまったらしい。これは本当にすごいことで、並列することの意味みたいなのを考えさせられる。

週記を投稿したらもう外が明るくなってきていた。

コードゴルフ大会中にやろうと思っていたことをする。具体的には、この週末にあったコンテストのコードゴルフである。ABC194BとAGC052Aを縮めた。

yukicoderが今年もエイプリルフールコンテストを開催するらしい。一昨年出題したが、今年も出題しようと思い立つ。1問、以前から何となく考えていた(というほどでもない、星1くらいの)問題を出そうと思って、ストーリーを練っている最中、致命的な間違いに気づいた。はさみ将棋の勝利条件を、コマが相手陣地の最奥まで進むことだと勘違いしていたのだ。ストーリーの文章が完成してから気づいたので、没になってしまった文章はTwitterに放流した。

考えていた問題は、次のようなものだ:盤面に、飛車の動きができる駒が相手と自分のもの1つずつある。相手と自分はそれぞれ盤を挟むように向かい合っているが、このとき、自分の駒を一番向こう、相手側の端まで進めることが勝利条件である。自分と相手の駒が最初にあるマス目がそれぞれ入力されるので、自分が先手としてどちらが勝つか判定せよ。ただしパスはできない。

最初の状態ですでにどちらかが勝利条件を満たしている場合を考えないこととすれば、後は、自分が1手で相手側まで進めるか判定すればよい。1手で相手側まで進めない場合は、横にずれる必要があって、その時相手が勝つ。……嘘である。この場合も、相手駒の目の前まで進むことで、相手はじりじり後退するしかない状況に追い込まれる。これは今書いている最中に気づいた。

結局、自分が初手で1歩でも前に進めることが自分が勝利する必要十分条件になるようだ。

さて、この問題は全然ダメだったが、さらに1問ひねり出すことに成功した。僕は面白いと感じるが、ほかの人がどう思うかはわからない。問題文や想定解をパパッと書いて、%20さんにテスターをお願いするDMを送っておく。

まだ寝る気にならないので、AHC001に手を付ける。初日にコードゴルフのための解を提出してから手つかずだった。この日提出していたのは、最初1x1を割り当てておいて、適当に選んだ長方形を伸ばしたり平行移動したりする解。0→46969906846。

途中ハーメルンを読んでいた。

syosetu.org

寝る前に確認したら早速最短コードが取られていた。ABC194Bである。-1B。

午後8時就寝。

03/09(火)

生活リズムの狭間に飲まれて消えた。

03/10(水)

午前3時半起床。布団でスマホを触っていたが、1時間くらいでまた寝た。午前7時に再度目を覚ました。

ラノベを1冊読んだ。「自称Fランクのお兄さまがゲームで評価される学園の頂点に君臨するそうですよ?」10巻。この巻はお兄さまの活躍があんまりなくて、次巻に向けて力をためているような感じだった。単体としてみると少し消化不良。早く無双してほしい。

なろうの「現代社会で乙女ゲームの悪役令嬢をするのはちょっと大変」を最後に読んでから3か月くらい経っていた。

https://ncode.syosetu.com/n3297eu/

更新分を読もうとしたのだが、このなろうは後から以前の話の間に差し込むような更新をするため、いちいち題名を見て更新分を探す必要がある(一応、新しいものは〇年〇月〇日投稿と書いてある)。作者曰く、まず大まかに書いてから、以前の分を修正するような書き方をしているそうだ。更新してくれるだけでありがたいのだが、ちょっと面倒だなと思ってしまうのはしょうがない。さかのぼって確認していたら、記憶に残っているシーンなどが出てきて、結局読み返す形になった。

yukicoderの問題は、%20さんがテスターを引き受けてくださるそうなので、リンクを送っておいた。

午前10時くらいにまた寝落ち。今度は午後5時に目を覚ました。

%20さんが問題を解いてフィードバックをくださったので、それを元に問題文を修正する。僕自身微妙だなと思っていた点についてもご意見をいただき、1時間半くらいで完成。yukicoder公式のDMで出題してほしい旨を伝えると、すぐ反映してくださった。これで完成である。

AGC052Aも最短コードを取られていた。こちらは-4Bと大敗を喫した。

今日は早めの時間にCF #706 div.1がある。レジってしばらく「現代社会で乙女ゲームの悪役令嬢をするのはちょっと大変」を読んでいると、始まった。

Aは、2乗してソートし、大きいもの同士をペアにする。これは未証明で出したが、後から考えてみれば、ペアをつなぐ線分が交差しないようにしていると考えることで明らか。Bは難しい。丹念に考えると、左右の下り坂の長さが等しくかつ偶数で、その2つが全体に存在する下り坂のうちstrictに最大のものである必要がある。strictという条件から答えは0か1になるようだ。Cはギャグ。mod 3で引いて適当につなぐとできる。ここまで解いた時点では3位だった。

Dは、f(i,i)は明らかで、f(i,j)(ただしi!=j)を求めることを考える。まずijを結ぶ最短経路は1通りである必要があって、このとき数え上げる対象のBFS treeはijの最短経路上から木が生えた形をしている。ある頂点kがどこから生えた木に属するかはdist(i,k)-dist(j,k)を見ればわかって、これでグループ分けしたそれぞれの木についてf(i,i)を求める感じで計算すればよい。

Eは、どちらのチームが多くカードを持っているかを見て、カードが少ないほうのチームは全部使い、多いほうのチームは適当に2周してならせばよい。ところがMLEとコーディングミスで2WAしたところでコンテストが終了してしまった。

結果は4完でシステス後27位。レートは2658→2759(+101)でhighestを更新した。Eを後から提出したら通ったので、あと1分あればもっと良い順位が得られたようだ。非常に残念。この点数の問題がこんな簡単なわけないだろと思って、あんまり真剣にコーディングミスを直してはいなかった。もっと急げばよかった。

パスタを食べて、今日も写真をTwitterに投稿したのだが、どうやらうまく保存できていなかったらしく、前回のパスタの写真を再度投稿してしまった。食べている本人も気づかないほど似通った写真なのでどうしようもない。スマホで写真を撮った直後に別のアプリに切り替えるとうまく保存できないことがあるようで、最近は結構写真を撮るのに失敗している。

先週金曜日のyukicoderの問題を解いた。Eは頑張って数え上げる。mod pにおける切り捨て除算が、余りを引いて割ることで行える事実は何となく面白く感じる。Fはループがあったら総xorが0でなければならない、などと考えていたのだが、冷静になるとそのような判定は必要なくて、BFSでもDFSでも適当に決めていけばよい。Gは似たような問題設定なのにFとは全然違う解法になって面白かった。50列ぶんのbitをintで持っているのに気づかず、しばらく首をかしげていた。

今日もマラソンをする。前回の続きから、今日は平行移動する操作をやめ、代わりにほかの長方形を押して伸ばすような操作1本に絞ったうえで焼きなまし法をやってみた。vectorを使っていたのを固定長のグローバル配列に直すと結構ループ回数が増えてよかった。exp関数をテイラー展開して高速化、というのを目にしたので、適当に3次で打ち切ってみたが、あんまり速くはならなかった。1次だとさすがにまずそうで、2次は負の数のexpを取るときにやたら大きな値が出てしまいそう。後は、30分ごとに適当にパラメータをいじって投げると伸びたり伸びなかったりして、それにばっかり夢中になって本質的な改善は一切できていない。46969906846→48039849007。

途中でCF problemsを触っていた。div.4というのが1回だけ開催されているようなので埋めた。

ほかには、DDCC予選を通過したというメールが届いていたので、本選に参加するためにGoogle formに回答したりもしていた。bits/stdc++.hatcoder/をincludeした人たちが落ちていて、面白いコンテストだなあと感じた。交通費の計算について、とりあえず仙台駅からにしておいたが、もしかしたら帰省先の実家から直接東京に行くことになるかもしれない。この場合はどうなるのだろう、富山から東京に行くほうが少し高いようで、別にその分増やしてくれとは言わないから、せめて仙台→東京でないような領収書でも受理してくれないだろうか。

正午、布団に入って「無冠の棋士、幼女に転生する」というなろうを読んでいた。

https://ncode.syosetu.com/n1783ex/

午後3時までかかってちょうど最新話までたどり着いたところで寝落ち。

03/11(木)

生活リズムの狭間に飲まれて消えた。

……火曜日も同じことを書いた気がする。さすがに自分でも違和を感じるので、ここで今週の睡眠を振り返っておこう。便宜上木曜日の欄に書いているだけで、今週一週間分の睡眠を見ている。

時刻 日記において属する日付
先週日曜日
月0700 就寝
月2330 起床
月曜日
火2000 就寝
水0330 起床、1時間で再度就寝
水0700 起床
水曜日
水1000 就寝
水1700 起床
水曜日
木1500 就寝
金0230 起床
金曜日
金1530 就寝
土0030 起床
金曜日
土0930 就寝
土1030 起床
土曜日
日0000 就寝
日0400 起床
日曜日
日0700 就寝
日1300 起床
日曜日

どう考えても水曜日が長すぎる。また、金曜日と土曜日の切れ目がそこじゃないだろみたいなところにある。本来は僕が起きてから寝るまでを1日としていたのだが、今週は寝落ちして変な時間に起きることが続いた結果細切れの睡眠になってしまい、崩壊した。

03/12(金)

午前2時半起床。

寝る前に開いていた話の最後まで目を通して、これで最新話に追いついたことになる。

https://ncode.syosetu.com/n1783ex/

将棋の話だとどうしても「りゅうおうのおしごと!」を比較対象にしてしまいがち。「熱い」というキーワードが出てきたのでそこに注目が行ってしまった。頭を使う対戦ゲームで、プレイヤーを傍から見ているといかにも冷静沈着といった面持ちだが、内心は情熱に燃えている、というような対比を強調して「熱い」という言葉を使うのは別に「りゅうおうのおしごと!」に限った話ではなく、僕がそれしか知らないだけかもしれない。

ラソンをする。前回焼きなましたが、どうしてもいつも目標面積に全然届かない長方形が出てくる。そこで、最初に少しだけ山登り法をして、満足度がある値を下回るような長方形を抜き出し、それだけを対象とした山登り法をしたものを初期解にしてみた。これで2億点くらい伸びた。こりゃいいやと思って、抜き出す長方形は隣り合わないようなものだけにするとか、山登り法じゃなくてここでも焼きなまし法をするとかしてみたが、これらは逆にスコアが悪化してしまった。結局それぞれにかける時間を弄り始めてしまい、そのあとはずっとそれしかしなかった。3回の計算をそれぞれ0.1秒、0.3秒、4.55秒にしたものが一番スコアが高くなった。48039849007→48273067872。

ラノベを読んだ。「りゅうおうのおしごと!」14巻。非常に良い。もう何を言ってもネタバレになりそうなので、何とも言えない。12巻の終わりでも触れられていたが、いよいよ主人公が強くて僕としては嬉しい。

布団でハーメルンを読んでいたら、午後3時半に寝落ちした。

午前0時半起床。

DMでとがさんにコードゴルフのお題を振られていたので、少し考えてやり取りしていた。フォルダ内のファイルを1つずつあるコマンドに渡し、そこの標準エラー出力から特定の文字列を含む行を抜き出し、書かれた数字に対していくつか計算をして総和を取るという問題。もっと具体的に言えば、フォルダ内のmp3ファイルの再生時間の総和を秒単位で求めたいときのシェルスクリプトについて。

最初のコードは、for文でファイル名についてループし、grepで抜き出して(sedでフィールドを区切れるよう空白に置き換え)AWKで計算し、さらにfor文の外側でもう一度AWKを使って総和を求めていた。これらのうち、for文以外のことはすべてAWKでできる。grep/pattern/{}でよいし、フィールドを区切る文字はFS=charで変更できる。さらに、AWKを2回呼び出さずとも総和を持っておいてENDブロックで出力すればよい。

for文をどうにかしたい。コマンドの中には、ファイルを複数一気に処理できるようなものが存在するが、今回使うものはそうではないようだ。まずxargsで書いてみた。空白を含むファイル名を持つファイルが存在するようで、さらにクォートする必要があったが、何とか動いた。ls|xargs -I{} -n1 command "{}" 2&>1 |awk...だ。これだとsedで前後に文字列を入れs///eしたほうが短くなった。さらに2&>1 |awk...というのは|&awk...でよいということをとがさんが調べてきて、これで一旦は完成とした。しばらく後に、xargsのオプションがいくつか無駄であることに気づいた。ls|xargs -I@ command "@"|&awk...として終了。

シャワーを浴びて、深夜のコンビニに行ってみた。弁当とサラダとパンとおにぎりとお菓子を買って帰宅。通行人がいてびっくりした。

溜めていた日記を書いていたら朝になってしまった。また少しマラソンをする。ビームサーチというものがあるらしいので、試してみたが、これが驚くほど効果がなかった。まず試行回数が全然稼げない。さらに、すぐ局所解にハマって身動きができなくなってしまう。初期解を焼きなましで作ってからビームを打つとよいのではないか?とも考えたが、一切スコアが改善されなくてたまげた。コーディングの合間にもパラメータを適当に変えた解を投げ続けていたが、ここでは更新なし。というかシステムテスト形式なので、プレテストのスコアを執拗に気にしているのは良くないのだろうとは自覚している。ただ、バグが発覚したのはよかった。満足度がある値を下回るような長方形を抜き出していたが、ここで1つも抜き出さなかった場合にゼロ除算が発生するようだ。

さて、現在世間的には土曜日で、夜はABCがあるため、いったん寝ておく必要がある。しかし午前10時半からICPCのチーム練がある。起きられるだろうと自分を信じることにして、強気の午前9時半就寝。

03/13(土)

午前10時半起床。ICPCチーム練習の開始と同時に起きても、チーム練習に遅刻することに変わりがないことに改めて気づかされた。急いでバチャにレジって問題を解く。解きながらタイミングを見計らって昨日買ってきたパンやおにぎりを食べていた。

https://codeforces.com/gym/102920

8完。今日は碧黴さんとやっていたが、結局僕が全問書いてしまった。英語問題文を読んで概要を共有してくれる人が1人いるとかなり楽。碧黴さんはE問題をずっとやっていたが、問題文の冒頭、ストーリーに紛れ込んだ本質的な制約を見落としてしまったらしく、その状態で概要を聞いた僕も一緒に考えていたが全然わからなくてびっくりした。これは問題文が悪いよ。

解いた問題は、順番にBCJGHEAL。

Bはやるだけ。Cは、アパートがある頂点を適当に選んで木DFSする。自分を頂点とする部分木がアパートを含むような頂点を数えればよい。Jは\mathbb{F}_2上の掃き出し法。Gは、出力が小数点以下1桁固定であることに気づかず1WA、long longを%dで出力しているのに気づかず2WA(このとき別のバグも直した)、最後にTLEを食らって合計4ペナだった。scanfでlong longを106個読んでいるのが遅いらしく、getcharを使って書き直したら通った。後から聞いたらcin高速化だと問題なく通ったらしい。許せない。

Hはbitset。ここでEの制約見落としに気づき、AC。前から見て場合分け。Aは気合いで実装する。線分の端点が別の線分に乗らない制約で大助かりだった。今いる格子点と今目指している格子点を持つ実装。まず1周回ってみる。1周分の距離が求まったら、進むべき距離をそれで割った余りに置き換えておくと、残り1周未満で答えがわかる。

Lは非常に面白かったので、少しスペースを取って話しておこう。ちなみに、また106要素の入出力なので、今回は先にgetcharを書いておいた。まあ本題はそこではない。

問題概要は次の通り:2\le n\le 10^6要素の数列h、ただし1\le h_i\le 10^6、が与えられる。\displaystyle \max_{1\le i\lt j\le n}(h_i+h_j)(j-i)を求めよ。

例えば、あるペア(i,j)を考えたとき、i'\lt i \land h_{i'}\ge h_iなるi'が存在するなら、明らかにペア(i',j)を考えたほうがよい。こうしてみると、左右から累積maxだけ残した配列だけ考えてよいことになる。このとき、うまく尺取り法のようにして計算できないか考えたのだが、それ自体はうまくいかない。しかしその過程で良い性質を示すことに成功し、それがACにつながった。

i\lt i'\land h_i\lt h_{i'}j\lt j'\land h_j\gt h_{j'}を考える。ii'は左から累積maxだけ残した配列に属し、jj'は右からのそれに属する。ここで、(i,j)の値より(i,j')の値のほうが大きかったとしよう。つまり(h_i+h_j)(j-i)\lt(h_i+h_{j'})(j'-i)だ。このとき、(i',j)の値のほうが(i',j')の値より大きくなる、つまり(h_{i'}+h_j)(j-i')\gt(h_{i'}+h_{j'})(j'-i')となるようなことがあるだろうか?もし大きくなることがあるなら、尺取り法をしようとしても後戻りする可能性があってできない。式変形してみよう。

展開して整理するとh_ij-h_ji-h_ij'+h_{j'}i\lt 0\lt h_{i'}j-h_ji'-h_{i'}j'+h_{j'}i'になる。移項してまとめると(h_i-h_{i'})(j-j')+(h_{j'}-h_j)(i-i')\lt 0になる。ここで登場する4つの項は、最初に置いた時の大小関係からすべて正であることがわかる。つまり左辺は正なので、矛盾。したがって、(i,j)の値より(i,j')の値のほうが大きいとき、必ず(i',j)の値より(i',j')の値のほうが大きい。

勢いあまって尺取り法を書いて2WAくらい出したが、やはりダメであった。各iに対してjを全部舐める必要性はどうしても消えない。ここで天啓が降りる。あるiに対してjを全部舐め、最大となるjを求めたとき、i'\lt iに対してはj'\le jだけ、もう半分も同様に逆の半分のみ調べるだけでよいのではないだろうか。これはiで半分に切っていくことでO(n \log n)になりそうである。書いて提出するとACした。

まったくの別件を経由して後から知ったことだが、これはmonotone minimaそのものであったようだ。名前だけ聞いたことがあって、なんとなく敬遠していたのだが、自力で再発明できたので嬉しい。monotone minima、何するものぞ……。

残り時間はIと格闘していた。明らかに計算量がヤバそうな解を書いてTLEを出しまくるも通らず、このタイミングで参加してきたゆきのんさんに2Dセグ木では?と言われてうしさんのライブラリをコピペしてきたが使い方がよくわからず、提出が数秒間に合わなかった。ちなみにTLEした。コンテスト後もしばらく格闘していたが、ずっとtest 5でTLEしていてつらくなったので通らないまま諦めた。

ICPC練の合間にもまたマラソンで適当にパラメータを変えたものを投げていたら、焼きなましの終了温度を10分の1にしたところで微妙にスコアが更新された。嬉しくないと言ったら嘘になる。48273067872→48275288137。

CF 707 div.1に出た。3完で2759→2744(-15)。

Aはいつぞやの僕が解けなかった300点の、値がdistinctでないもの。distinctでないものは適当に場合分けすることで消せて、結局メインはこの問題とほとんど同じ。出力の順番をミスして1WA。

atcoder.jp

Bは面倒。数列の値がdistinctなので、怒られない日は少ない。簡単のため、n>=mであるとする。まず、0<=i<nに対して、その日からm日間とbm日間を合わせたときに怒られる回数を数えておく。この数列をgcd(n,m)項ごとに見て和をとることで、lcm(n,m)日間で怒られる回数がわかる。これはn/gcd(n,m)ステップでできる。kをその値で割った余りに置き換えて、同様の配列を用いてm日ごとに怒られる回数を見ながら進んでいく。こちらもn/gcd(n,m)ステップ未満となる。最後に、1日ずつ見て細かいところを合わせる。これはmステップ未満となる。

Cはカス。Bを見て操作を逆順にたどる。最後にソートする列は[0,n)がきれいに並んでいる必要があるが、その前の列は、最後の列において等しい値が続いた部分だけでよくなる。ちゃんと並んでいる箇所というのをbitsetで管理しておけば、bitwise andを利用してその列でソートできるかを判定することが可能になる。O(nm2)の64倍高速化……のはずが、TLEした。何がTLEしているのかわからないまま速そうな書き方に直したり判定をもうちょっとちゃんとしたりして、最終的にbitsetであることを使わない解にまで至った(並んでいない箇所を見なくてよくなった時に使う列の候補に追加する)のだが、7ペナ生やして依然TLEのままであった。

ここで、1500x1500の行列をscanfで読み込んでいることに気づく。getcharで書き直したところ、通った。本当に許せない。scanfがいついかなる時も速いと思っていて、CFでは常にscanfを使っているのだが、この日はそれがひたすら裏目に出ている。しかもつい数時間前にキレていた個所で再度キレることになって目も当てられない。この7ペナがなければレートは上がっていたはずだ。

https://codeforces.com/blog/entry/88590?#comment-770387

GNU C++17(64)だとscanfのほうがcin高速化より遅いらしい。本当にキレています。

ABC195に出た。6位。なんの賞も得られない。

Aはよい。Bはよくわからなかったので、個数を1から106まで全探索した。Cもよくわからない。適当に書き、適当にいじってサンプルを合わせたら通った。Dはかなり迷ったものの自分の直感を信じて貪欲に取ったら通った。証明はしていない。Eはメモ化再帰。こういうのを何も考えずに無理やりdpで書き始めるのではなく、メモ化再帰するという選択肢が出るようになったのは成長。手番の人が勝つかを返そうとして混乱していたが、よく見るとターンが交互ではなかったので、素直に高橋君が勝つかを返した。Fはおもむろにraku -e'say +grep &is-prime,^72'と実行すると20と出たのでbitDP。このコードは72未満の素数の個数をカウントするRakuのコード片で、is-primeというのがかなり使い勝手が良くて重宝している。ちなみに105くらいでもう実行時間がとんでもないことになる……と思ったが、手元のRakuをバージョンアップしていたことでかなり高速になったようだ。

コードゴルフはAとBとC。Aは最初のコードが16Bで最短。Bは全完後にちゃんと式変形してfloorceilのO(1)で解いておいた。Cは……謎。全部dcである。

合間合間でラノベを1冊読んだ。「ねえ、もっかい寝よ?」2巻。設定に全然惹かれていないことに気づいてしまったので、かなり適当に読んだ。

ちょうど真夜中ぐらいに、布団に倒れこんで寝た。

03/14(日)

午前4時、目を覚ます。atgolferの更新が流れてきた。新手であるように見えたので、飛び起きて他の最短コードを確認。どうやらほかに影響はなさそうである。いくらかコードゴルフされたものがこれ。

atcoder.jp

これまでのメモ化再帰は、メモ用のHashを用意してh={};f=->x{h[x]||=...のように書いていた。これをHashオブジェクトのnewでやってしまおうという発想が驚き。非常に美しいコードゴルフのテクニックであると感じられる。存在するキーにアクセスした場合は当然メモされた値が返されるが、存在しないキーにアクセスしたときに呼び出されるブロックで、デフォルト値を計算すると見せかけて問題を解いている。これは本当にすごい。

他にもE問題のコードゴルフが行われていたり、C問題が-4Bされていたりしたが、見ても粗はなさそうだった。D問題を縮めて、再度布団に戻る。ハーメルンを読み続けていたが、夜のARCに向けてさすがに寝ないとまずいと感じたので午前7時くらいに再入眠。

今度は午後1時くらいに起きた。午後2時からyukicoderの1時間コンテストに出た。2完。またBのFAを取った。

Bはよくわからないが適当に二分探索したら3700msで通ってFAだった。

週記(2021/02/22-2021/02/28) - kotatsugameの日記

Aはよい。僕は(0,0)(0,1)を聞いた。何を思ったか、(0,0)(0,100)をいったん考えておきながら却下してしまった。ユークリッド距離の2乗が返されると聞いて混乱していたが、冷静になるとsqrtを取ったことにしてよくて、ちゃんと円の交点になる。BはPを降順ソートして、チケットを使う場合は貪欲に使っていく。今何個目の商品を見ていて、どちらのチケットをどれだけ使ったかさえ持っておけば、もう片方の残り枚数もわかるという寸法だ。Cはずっと考えていたがわからなかった。解説を読んで、コードを読んで、ようやく理解した。

僕がコンテスト中にたどり着いた計算は、次のようなもの。おそらく解説でO(HW \min\{H,W\})とされているものであろう(このHWNのことだろう)。

\displaystyle \sum_{0\le h,2N-K-h\le N}\binom{N}{h}\binom{N}{2N-K-h}(-1)^K\sum_{a=0}^h\sum_{b=0}^{2N-K-h}\binom{ab}{M}\binom{h}{a}\binom{2N-K-h}{b}(-1)^{a+b}

この\binom{ab}{M}が厄介で、分解できない。正解は、最初の\sumを分解することだったようだ。

\binom{N}{h}\binom{h}{a}\binom{N}{a}\binom{N-a}{h-a}とする。bも同様に\binom{N}{b}\binom{N-b}{2N-K-h-b}\binom{N-a}{h-a}\binom{N-b}{2N-K-h-b}を考えよう。0\le a,b\le Nを決め打ったときに、a\le hb\le 2N-K-hから0\le h-a\le 2N-K-a-bがわかる。同様に0\le 2N-K-h-b\le 2N-K-a-b。よって\binom{(N-a)+(N-b)}{(h-a)+(2N-K-h-b)}=\binom{2N-a-b}{2N-K-a-b}について、2N-K-a-bN-aN-bからそれぞれいくつずつ持ってこられたかに対応するh-a2N-K-h-bが存在する。したがってhに関する和は\binom{2N-a-b}{2N-K-a-b}という二項係数に吸収される。結局

\displaystyle \sum_{a=0}^N\sum_{b=0}^N\binom{ab}{M}\binom{N}{a}\binom{N}{b}\binom{2N-a-b}{2N-K-a-b}(-1)^{K+a+b}

ただし、2N-K-a-b\lt 0の場合は\binom{2N-a-b}{2N-K-a-b}=0としておく。ab\lt Mのときの\binom{ab}{M}もそうである。

今朝縮めたABC-Dが取られていた。終端なしのrangeを完全に忘れていたのはまずい。reject!を活用するのは上手い。

そのあとはARCまでずっとハーメルンを読み続けていた。もうそろそろ最新話に追いつきそう。

午後8時にAHC001が終了。人々の解法を眺める。焼きなましをやるのはよくて、最初のうちは目標面積を小さく見積もっておくことや、焼きなましの遷移にいったん長方形を破壊するようなものを入れるのが効くらしい。あとは、全体をずらしつつ面積が小さい長方形を引き伸ばす、というのも僕の解法を発展させていったうちの1つと見れるだろう。長方形を分割していく方針の人も結構いたようだが、そちらは正直よくわかっていない。

ARC114に出た。遅い3完で306th、パフォーマンス2102、レートは2714→2665(-49)。ひどい、その一言に尽きる。

Aはよい。昨日も見た。Bもよい。サイクルを数えようとして、ちょっと立ち止まって連結成分の個数でよいことに気づけたのはファインプレー。ここまで7分、6位だったらしい。Cにすべてを破壊された。

最後20分でEに特攻し、脆くも敗れ去った。がりがり式変形している最中、今計算しているのが「ありうるすべての手順における操作回数の和」であることに気づいていたが、何らかの値で割ることで修正可能だと思っていた。不可能だった。

ボロボロのメンタルのまま、直後のTROC20に出た。ABCDFの5完25位でレートは2602→2588(-14)。初めてレートを落とした。

Aはやるだけ。Bは天才パズル1で、必ず2N-3本引けるので答えは2N-3-M。Cは天才パズル2で、0しかない場合はNO、0と1が両方ある場合は(2<=N,Mより)YES、1しかない場合は適当に左上から順に操作してみる。(2021/03/15追記:嘘。本当は常に可能。)Dは天才パズル3で、ランレングス圧縮して3個以下になるかN<=4であることがYESになる必要十分条件。Cはシンプルに難しいし、Dはずっと具体的な形を考えていたせいで0と1が切り替わる場所の個数に着目できなかった。最終的に何かそれっぽい解法を思いついて、とりあえず実装しようとした段階で実質ランレングス圧縮して3個以下になることを判定していると気づいた。ほとんど運試しのように出したが、通って嬉しい。CとDに合わせて1時間かけた。

そのあとEを考えていたが、解けず、Fのほうが解かれていたのでそちらに移った。各頂点についてとりうる値の最大値と最小値を管理する。条件が重なったりして、2つの頂点のうち片方が使えなくなったとき、もう片方の頂点の値が決まる。1つ決まると条件が連鎖するが、そこで決まっていく頂点はBFSで見ることにする。最後に残った条件とそれに関連する頂点たちが問題。一切証明していないが、およそmax-min-max-のように取ることになって、今見ている頂点たちはmax!=minであるから、条件は2部グラフをなしている必要があると考えた。そのように実装するとサンプルが合うので深く考えずに提出。するとREが出た。残り5分だが落ち着くことを意識して見ていくと、条件の番号から頂点の番号を参照するのを忘れて、条件の番号をそのまま使っている個所を発見した。直したが、サンプルを試すのが面倒に感じられたのでそのまま提出。祈るような気持ちでF5を連打した。結果はAC、残り3分だった。

GはOEISにあるらしい。面白いコンテストサイトですね。

ハーメルンを読む。ここ最近読んでいたものの、最新話に追いついた。

syosetu.org

TLで「主人公がVtuberになる」という言及がなされていたのを見つけて読み始めたが、冒頭は全然そんな感じではない。基本は主人公が地球人類の発展を見守り続ける話で、気まぐれにVtuberをやっているだけという話だったようだ。ちょうど現在更新されている部分。それはそうと「主人公が地球人類の発展を見守る」設定は僕の大好物であるため、むさぼるように読んでしまった。むやみやたらにテンプレ無双するだけのシーンが挟まれるが、全体的な流れは非常に良い。ただ文章が非常に残念なことになっているので、おすすめはしにくい。厨二というわけではないのが救いか。250話以上書いて、多少は改善されたような気もするが、相変わらずぶつ切りの文章である。

昨日のICPC練のIは他チームの解答が見れなくて困っていた。ホスフィンにAobayama_sugarstepのコードを教えてもらった。

mine691.hatenablog.com

週記(2021/03/01-2021/03/07)

03/01(月)

日曜日の31時に週記を投稿してから、寿司打を少しやった。久しぶりに高級コースで+10,000した。手元を撮影しようと思ったがスマホの三脚などないしなあ……で諦めかけたが、ふとWebカメラが使えることに気づく。モニタ上部に設置したまま限界まで下に向けると、いい感じに机の天板だけが映る(後から気づいたが、これは動画編集でどうにでもなることだった)。それで、適当にお手軽コースを撮影して、上下反転して撮影されているのを直してTwitterに投稿した。

動画が終わってから8秒くらい無の時間があって悲しい。聞くところによれば、オブジェクトの最初と最後を切り落として先頭に詰めただけだと、元の長さ分の尺が確保されてしまうのではないかとの話だった。

午前10時半に布団に入ってしばらくハーメルンやなろうを読んでいたが、どうにも腹が減る。仕方なく食事をし、さらに1時間程度なろうを読んで、午後0時半に寝落ちした。

午後8時半くらいに目を覚ました。今週からサークル解説会は春休みなので、思うさま寝ていられた。午前0時半くらいまでそのまま布団でなろうを読んでいたが、メールを確認すると4年セミナーの配属が決定されていたらしいので、確認のために布団を出てパソコンに向かった。

4年次ゼミの割り振りについて、現在僕が第一希望にしているゼミは定員より多い人数が希望しているため、何人かを第二希望以降に回す必要がある。

週記(2021/02/01-2021/02/07) - kotatsugameの日記

こういうことがあって、周りの人の取得単位数など気にもしたことがなかったためアンケートの内容を見て怖がっていたのだが、無事第一希望のゼミに配属された。ホスフィン曰く、僕が希望しているゼミに人が集中しているとはいっても、そこで扱う4冊の教科書のうち僕が希望しているもの以外を目当てにしている人が多いのではないか、とのことであったから、そういうことなのだろうと思っておく。

2月が終わっていたので、また読書冊数の集計をしてツイートしておいた。

午前1時、また布団に戻ってなろうを読み始めたが、午前2時くらいにすぐ寝落ちしたようだ。

03/02(火)

午前7時起床。またなろうを読み続ける。せっかく朝に起きたのだから生協に行って本の受け取りでもしようかと考えたが、どうにも体が動かず、午後7時くらいまでなろうを読み続けた挙句また寝落ちした。

昨日から今日の午前9時半くらいまではこの作品を読んでいた。

https://ncode.syosetu.com/n2296ez/

これは「うちの脳内コンピューターが俺を勝たせようとしてくる」の作者の別作品なのでいずれ読もうとしていたのだが、どうにも設定や内容が肌に合わなかった。知識チートでNAISEIする話、まではよかったのだが、平凡な主人公が曖昧な知識で物事を推し進めようとしているのが合わなかったのだろう。身の丈に合わないことをしている、と感じてしまって、おしまいだった。知識チートもので、Wikipediaか何かのように正確かつ広汎な知識を披露する主人公が揶揄されることもあるようだが、僕はそうやって振り切ってくれたほうが好きである。

そのあとはこの作品を読んでいた。

https://ncode.syosetu.com/n0859fa/

この日記を書いている時点で100話くらいか。本編完結まであと50話もないが、読み終わるとは限らない。しかして今ここでこれまでの感想を書く気もない。

午後7時に寝落ちして、起きたのは午後11時半だった。サークルのバチャを寝坊した……と思ったが、slackを見ても今日開催された形跡がない。さらに今日はECR105があるが、あと5分もないし諦めるかな……と思ったところ、10分こどふぉったという情報が流れてきた。これは何らかの思し召しか。参加を決めて急いで準備した。

ABCD4完で終わり。直前まで寝ていたにせよ、これはあまりにひどすぎる。

Aは全探索。Bもちょっとひねって、四隅の置き方を24通り全探索すればよい。Cは難しいが、押すときはあるスペシャルマスの直前まで押してよい(わざわざ少し離れたところで止めなくてよい)ことが何となくわかるので、それを全探索する。そこまで押したとき、どこの箱まで影響して動くかは尺取り方のように計算できる。それ以降動かない箱のうちいくつがもともとスペシャルマスにあるかというのは、後ろからカウントを累積すればわかる。手元ではサンプルが合ったのに提出すると1ケース目で落ちて首を傾げていたが、custom invocationで実行してみたところ配列外参照していることに気づいた。1ケース目で落ちたのでペナが付かなかったのはいいこと。Dは答えが必ず存在するという制約がなくて微妙に不安だが、答えが存在しなかった場合の処理が書かれていないので大丈夫だろうと適当に提出したら通った。

Eは、サンプルの3つ目の答えを見てループでもOKだと勘違いしてしまい、解けなかった。結局双方向に辺がある頂点ペアが存在しないとNOで、さらにその辺に書かれた文字が一致していなければkが偶数の時NO、それ以外はYESというギャグであった。Fは考えていない。

寝ている間に取られていた最短コードを取り返したりもしていた。

atcoder.jp

布団に入ってから4時間半ほどかけてハーメルンの「輝きたくて」を一気読みした。

syosetu.org

これも「うちの脳内コンピューターが俺を勝たせようとしてくる」の作者の別作品で、今連載中のもの。この作者は毎日更新してくれるのが本当にすごい。今日の朝読んでいた作品は途中で読むのを止めてしまったが、こちらは肌に合ったのか、非常に面白く感じられた。主人公は自己評価低めで、主人公視点でしか語られない序盤は与えられたチートで一転突破みたいな感想を覚えるが、前作同様次第に主人公の生身のスペックも人間辞めてるレベルであることが明らかになってきて僕としては非常に好ましい。Vtuberものも好きなのでさらに良い。

そのあとハーメルンVtuberものを探していた。3つくらい序盤をちょっと確認して、1つ、文章も読みにくくなく良さげなものを見つけたが、8話にしてエタっていそう。僕もちょっとゆっくり実況を作ったが、創作を続けられずエタるのはどうしようもないことが体感できたので、ただただ涙を呑むしかない。

syosetu.org

チュウニズムに「LOVE EAST」が収録されるという告知が流れてきた。最高すぎる。本当に良いので、皆さんも良くなりましょう。

そのあとなろうに移ってさらに別の作品を少し読み、午後3時就寝。

03/03(水)

午後11時半起床。今日もサークルのバチャはなかったようだ。正直な話をすると、昨日の時点で主催者がバチャを忘れているであろうことは何となくわかっていたが、しかし生活リズムの関係上今週は出られそうにない僕が指摘してもなあ……と思ってなんとなくモジモジしているうちに僕も忘れてしまっていた。

食事してすぐ布団に戻り、昨夜(昼?)読んでいたなろうの続きを読む。

ちょっとTLを眺めているとウマ娘ハルウララについての話が流れてきて、読んだだけで感動してすぐに泣いてしまった。涙もろすぎる。

午前4時くらいに寝落ちしたらしい。今日は活動時間4時間半、そのほとんどを布団の上で過ごしており、まさに虚無の具現化とも言うべき1日であった。

03/04(木)

午前8時起床。ちょっと眠いが、起きていることにする。しかし午前中はパソコンの前で固まっているだけだったので、シャワーを浴びて切り替える。今日はチュウニズムのアップデートの日なので、午後はゲームセンターに行くことにする。

いったん生協に寄ってパンと注文していた本を買う。↓のときの本が1冊遅れて発送されていて、まだ受け取っていなかった。

また大学生協で本を注文しておく。前回注文してから新たに発売された新刊と、前回は在庫切れで注文できなかったが今見ると復活していた分、計4冊。

週記(2021/02/15-2021/02/21) - kotatsugameの日記

天気がいいので街までは自転車で移動した。久しぶりに乗ると太ももがパンパンになって、筋力の衰えを感じさせられた。

ゲームセンターには10時間くらい滞在して、55クレ使った。最初5クレほど新曲のAJ埋めをして、そのあと9時間くらいかけて「LOVE EAST」の理論値粘着をした。

120回くらいプレイしたはず。1落ちが4回、2落ちが5回、3落ち以上は数えていない。1日中プレイしてようやく出たが、別に譜面が難しいわけではなく、ただ僕が普段から何でもないところで赤を出してばかりだからである。僕にとってはこれが理論値2譜面目であった。粘着を始めたときはスピード9.5、判定0.0で、そこからいろいろ試して結局スピード10.0、判定-0.2に落ち着いた。昔SDVXで判定を弄ったとき、どれだけ早くしても自分がそれに慣れたらさらに早く押してしまうため全然nearが減らなかったというトラウマがあって、ちょっと腰が引けていたが、まあこの粘着の間だけなら何とかなるだろうと値を変えた。それより効果が大きかったのがスピードを上げたことで、判定は粘着後にもとに戻したが、スピードはこれからもこのままでやっていこうと思う。

本当に好きな曲なので飽きることはないし、譜面も体力を使わないので、立ちっぱなしであることにさえ目をつぶれば疲れはしない。金銭もこの際問題にならない。ただ、これだけの好条件があってすら理論値粘着は辛かった。自分には早いのではないかと思いもしたが、諦めたくなかったので、自分の精神力に寄らず粘着するため「全ランが埋まるか閉店するまでプレイし続ける」ことに決めた。結果として全ラン98人目に滑り込めていいことずくめ。

f:id:kotatsugame:20210309030721p:plain
47小節、51小節

ここで赤を出しまくった。この部分だけ見ても理論値通過できたのは10回くらいではなかっただろうか。このままではいけないと思って、注意深く観察したところ、普段はFASTが多いところこの部分ではLATEが出ているらしい。具体的には、47小節の振り下ろしが遅く、スライド始点で赤が出ていることが分かった。注意深く曲を聞いてみると、確かに僕が思っているのとテンポが少しずれていることに気づいた。かといって単純に早めると今度は手前のタップで赤が出るようになって、本当にどうしようもなかった。

疲れ果てて帰宅。「影の実力者になりたくて!」の更新が2年ぶりくらいに来ていてびっくりした。

https://ncode.syosetu.com/n0611em/

午前3時半就寝。

03/05(金)

午前10時、宅配が届いて起床。ICPC関連グッズだった。その中にチーム紹介ドキュメントも含まれていた。

メイキングは下書きですでに完成しており、あとは投稿するだけだった。夜のほうがいいだろうと思ってしばらく待っていたが、そのままタイミングを逃してしまった。2021/03/09に投稿した。

kotatsugame.hatenablog.com

そのあと少ししてから新春TechFULの商品であるトラックボールマウスが届いた。

最初は使い勝手に戸惑ったが、今はそこそこ慣れてきた。少しだけマインスイーパをやってみたが、3画面を移動するためのマウスポインタの速度とマインスイーパを正確にプレイするマウスポインタの速度は全然両立しない。今使っているキーボードもマウスも、競プロの商品として得たものであることに気づいた。面白い。

午前11時からTSG-KMC合同コードゴルフ大会に外部からの参加者として参加した。以降午前9時までずっとコードゴルフしていた。この日提出したのは次の言語たちであった。かっこで注釈が付けられている問題について、取られたのはこの日ではなくもっと後のことかもしれない。そのあたりの時系列を復元する気力はない。現在の最短コードを更新しないと提出できない仕様のため、手を付けた言語はもっと多かったかもしれないが、もはや記憶が定かではない。ずっとコードゴルフしていたので現実感も薄い。

  • Perl(取られた)
  • Python3(取られた)
  • Go
  • Ruby
  • AWK(取られた)
  • D
  • sed
  • Haskell(チームメイトに短縮された)
  • GolfScript
  • Crystal(取られた)
  • F#(取られた)
  • wake
  • Perl6
  • Ballerina
  • Rail
  • Octave
  • Lua(取られた)
  • Erlang
  • bash(pureのほうは取られた)
  • SQLite3(チームメイトに短縮された)
  • Verilog(取られた)
  • 05AB1E(取られた)
  • gs2(取られた)
  • COBOL
  • Elixir
  • Racket(取られた)
  • Zsh(取られた)
  • Kotlin
  • Vim
  • m4(取られた)
  • Makefile(取られた)

午前9時就寝。

03/06(土)

午後1時起床。

午後2時からDDCC予選に参加した。サンプルがあるのに気づかず提出しWAを生やしてしまったりもしたが、30分前後で全完。やるだけの問題4問用意して何がしたいんだろう。もうどんな問題があったかも覚えていない。

コードゴルフに戻る、が、途中ABC194に参加したのでそれを先に書いておこう。50分全完3WAで66位。

Aは問題を読めず2WA出した。Bも全然題意が把握できず結構時間がかかった。Cは明らか。Dは最初sum N/iではなくprod N/iを求めてしまい1WA。何を考えていたのかわからない。Eはやるだけ。Fは桁DPをもとにして考えた結果、Nより小さくなることが確定した桁で残りの出現パターンも包除原理で数えてしまう実装をし、DPがいらなくなった。代わりに16^2くらいの定数倍がついて1900msだった。

総じて頭が回っていない。コードゴルフに意識を支配されすぎていて、また睡眠時間も減らしてしまったため、非常につらい。明日のAGCが怖すぎる。

ABCのほうのコードゴルフは、ACDを取ってBは取られてEFは手つかずといった感じか。Cは分散のN^2倍なのでOctaveが強いかと考えたのだが、精度の問題だろうか、大きなケースが全然合わなかった。正直あまり考えていないので、すべての問題について縮む余地は大いにある。

さて、DDCC予選とABC以外の時間はずっとコードゴルフをしていた。この日提出したのは次の言語たちであった。昨日提出して今日も提出している言語は、つまり縮めたということである。

午前6時就寝。

03/07(日)

正午起床。午後1時からICPC模擬地区大会があった。これについては参加記を書いた。

kotatsugame.hatenablog.com

コードゴルフ大会は今日の午後9時までであった。ICPC模擬地区大会が終わってから3時間ばかり、最後の仕上げとして取り組んだ。今日提出した言語は次の通り。

  • Jelly

といっても1つしかない。残りの時間は、Makefileと><>(fish)と格闘していた。Makefileのほうは入力を3行ごとに処理する方法が、><>のほうは答えを求めるアルゴリズムが全然違っていて、負け。

終了直後からAGC052があったので出た。ABの2完78分で87位、パフォーマンス2698、レートは2716→2714(-2)。相変わらず頭が回っていなかったものの、崖に助けられた。さらにB問題も最後の解の判定が微妙に足りておらず、yosupoさんのケースで落ちる。

Aは、パズルとして面白くはあるのだろうが、Ratedに出されると泣くしかない問題。45分かかった。0をN個と1をN個と0または1を並べるのだろうな、ということまでは問題を見た瞬間に分かった。そこから文字列の両端は0..00..1と……という風に4パターンあって、このうち3種類しか出現し得ないことを利用するのだと考えていたが、0..01..1が両方出る入力に対する答えを作れなくて苦労した。文字列の形を、末尾に続く0または1の個数という形でもっと詳しく見れば、うまく証明できることに気づいてびっくり。

翻ってBは非常に面白かった。辺に関する条件を頂点に関する条件に言い換えるステップが面白い。この方針が降ってきたとき、直観として正しいと確信した。実際に実験すると頂点の値のswapになっていて、一気に目の前が開けたような心地がした。そこから解のチェックをするステップで、これまで謎に包まれていたNが奇数という条件が突如姿を現して、見事に伏線を回収されたような驚きがあった。詰めが甘かったので、それだけは心残り。完璧に解いておきたかった。

AGC後に、コードゴルフ大会のWriteUpを書いた。以前の大会のものはここから見られる。2021/03/15以降には、今回の大会のものもここから飛べるようになるはずだ。

github.com

さて、僕は最終的に121言語のうち40言語(bashは外部コマンドの実行の可不可で2種類ある)に提出し、21言語の最短コードを保持していた。相変わらず以前に書いた経験のある言語や汎用言語ばっかりだが、まあ数字にしてみればかなり頑張っていたのではないか。その中から5言語、特に面白いと感じたものについて、WriteUpの内容をここにも書いておこう。短縮ポイントを見つけられた場合などはぜひ教えてほしい。

コンテストリンク:Esolang Codegolf Contest #7

問題

入力

  • 入力には 16 個のテストケースが含まれている。
  • 各ケースでは、文字 0, 1 からなる長さ 3 の文字列が 3 個、改行区切りで与えられる。
  • 各ケースの最後には改行が与えられる。

出力

  • 各ケースについて、次の問題を解け。

    入力を 3 行 3 列の将棋盤とみなす。 文字 1 は対応するマスに歩が 1 個あることを、文字 0 は対応するマスに駒がないことを表す。 二歩であるとは、2 個以上の歩が置かれている列が将棋盤に存在することとする。 (3 個の歩が置かれている列でも二歩になることに注意せよ。)

  • 二歩であれば 1 を、そうでなければ 0 を出力せよ。
  • 文字 0, 1 はちょうど 16 個出力せよ。
  • 出力された文字列に含まれる空白文字(改行含む)は無視される。
  • 文字 0, 1 および空白文字以外の文字を出力してはいけない。
  • 提出するプログラムは、入力されうるすべてのテストケースに正しく出力できるものでなければならない。つまり確率解を禁止する。

制約

  • どのテストケースも 1 の個数は 2 以上 4 以下である。
  • 入力されうるテストケースの集合は、1 の個数が 2 以上 4 以下である盤面の集合と一致する。
  • 1 の個数が 1 以下または 5 以上である盤面に対しては、プログラムは正しく動作しなくてもよい。

APL (47B)

{+/'23'∈⍕ω}¨+/16 3⍴⍎⍕⎕ARG[6]
)OFF

数値として足して/2|3/をしている。右から順に

  • ⎕ARG[6]:入力
  • :to_stringのはずだがよくわかっていない。ないと動かない。
  • :eval
  • 16 3⍴:16行3列に成形
  • +/:sum(行ごと)
  • ¨:行ごとに左の関数を適用

{}が無名関数で、引数はωに入る。

  • :to_string
  • '23'∈:elem。[2を含むか 3を含むか]という列が得られる。
  • +/:sum

最終的に01の列になって、変数に入れたりしていないため出力される。

Cubix (21B)

pa^<IIaqbI/biO<@..?:,
  pa
  ^<
IIaqbI/b
iO<@..?:
  ,.
  ..

スタック型の言語だが、ほとんどの演算がスタックの中身を消費しないのは特徴的か。例えば加算をすると、popしたオペランドを再度pushしてから演算結果をさらにpushする。

上のような配置の2x2x2のキューブ上をIPが動いていく。スタートは3行1列目のIで、方向は右向きだ。 3x3x3に詰め込んで満足していたが、ふと2x2x2を考えてみたら詰め込めてしまってびっくりした。

アルゴリズムは、3行を数値として読んで(x&y|(x|y)&z)!=0である。 スタックの中身を消費しないことを利用してIIaqbIapb:,Oと書ける。 x&yを計算してから、いったんqでスタックの底に退避させ、後でpで持ってきている。 !=0の代わりに、:dupして,で切り捨て除算している。実装上0/0は0らしい。

次に、どのようにIPが動くのか見ていこう。

まず3行1列目のIから右方向にIIaqbI/と進む。 /で進行方向が変わる。右方向に進んでいたので、次は上方向になり、立方体上面、1行4列目のaに上から進入する。 上面では<^によってUターンのようなことをしながらapを実行する。1行4列目のpから上に進むと、今度は3行8列目のbに上から進入することになる。 そのままb:と進み、今度は6行3列目に下から進入する。.はnopで、途中<で進行方向を変えつつ,Oと進む。

これでケース1つぶんの入出力が完了した。無限ループになってはいけないので、実行を継続するか判定する必要がある。 iで1文字読んで、改行コードか-1(EOF)かで判定できる。 先ほどの続きから、4行1列目のi、さらに左に行くと今度は4行8列目に回り込んで:?と進む。:dupなので関係ない。?が条件分岐になる。 ?にたどり着いたとき、スタックのトップが0より小さいなら左に、0より大きいなら右に曲がる。 先ほど読んだ文字の文字コードが見られ、改行コードなら右に、EOFなら左に曲がることになる。

左向きに進んでいるので、右に曲がると上方向に進むことになる。また/で反射して右方向に進み、そのまま3行1列目のIに左から進入することになる。確かにループしている。 逆に、左に曲がると下方向に進むことになる。今度は6行4列目に下から進入し、そのまま上がって@にたどり着き、実行が終了する。

コードゴルフとしては、2x2x2に収めたほかにも、下面にnopが多くあらわれるように工夫している。 2x2x6文字に足りない分は勝手にnopで埋められるので、下面の3つのnopはコードに書かなくてもよい。

Jelly (24B->21B)

ƈƈFпOs12µs4PS%9>1)

アルゴリズムは、1ケース12文字(改行を含む)の文字コードを順にabcdefghijklとしたとき(aei+bfj+cgk+dhl)%9>1である。 左から読んでいこう。

  • ƈƈFп:入力を全部読むおまじない。第5回大会のWriteUpから持ってきたものが1B改善できた。元はƈƈ¹Ð¿であるが、この¹はただの恒等関数のくせに2Bも使っている。適当に1Bのmonadを試してみたところ、Fでうまく動いてくれた。Fの意味はリストのFlattenである。
  • O:ord。
  • s12:リストを12個ごとに区切り、リストのリストにする。
  • µ:関数定義を区切る。ここから右は左とは独立した関数になる。

区切られた関数はmonadである。s12で作った12要素のリスト[a,b,c,d,e,f,g,h,i,j,k,l]が引数となることを考えよう。

  • s4:4要素ごとに区切ったリストのリストにする。[[a,b,c,d],[e,f,g,h],[i,j,k,l]]
  • P:総積。積は、リストに対しては内積になるらしい。[aei,bfj,cgk,dhl]
  • S:総和。aei+bfj+cgk+dhl
  • %9:9で割ったあまり。
  • >1:1より大きいか否か。

  • )µ€エイリアス。関数定義を区切り、で左の要素に対してmappingしている。 つまり、先ほどのµより左にあるリストのリストに対して、今見たmonadが要素ごとに適用されている。 )が信じられないくらい偉い。µという2B文字とという3B文字が合わさって1B文字になっていて笑いが止まらない。

ところで、赤チームのコードは見た目的に全然違うものだった。そちらを参考に21Bまで縮めたので、後学のために読んでおこう。

ɠṭɠ,ɠ¤OPS%9>0¢

アルゴリズムは変わっていない。 ɠで1行読み込んでいるが、このとき改行が消えるため、OPS%9したあと>1ではなく>0になっている。

ɠṭɠ,ɠ¤を読もう。先頭のɠniladなので、以下これに対して関数が適用されていく。 は左オペランドを右オペランドにリストとして足す。左オペランドɠだが、右オペランドについて少し注意する必要がある。 順当に読めばすぐ右にあるɠだが、少し先の¤でパーサの挙動が変わり、実際にはɠ,ɠが右オペランドになっている。

¤は、直前にパースしたコマンド列の後ろからいくつか切り取って、niladと1つ以上のlinkが繋がったパターンを1つのniladとしてまとめる役割を持っている。 今回はɠというniladというlinkがまとめられている。複数の切り取り方がある場合は、最も短いものが採用されるので、ここで切られる。

ɠ,ɠは2行読んで2要素のリストにしたniladなので、それに最初のɠが足されて、全体として["efg","ijk","abc"]のようになる。 あとは先ほどと同様OPS%9>0で答えが得られる。

以上で、ɠṭɠ,ɠ¤OPS%9>0が1ケース分の入力を読んで計算するniladであることが分かった。 最後に¢で直前のlinkが(niladとして)参照されている。直前のリンクというものは存在しないため、この場合現在見ているlinkが参照されるようだ。 結果的に、いわばmain再帰のような形のコードとなって無限ループが実現されている。(これは僕の解釈なので、より良い説明があればぜひ教えてほしい。) 最終的にEOFを読んで落ちる。

sed (20B)

N
N
h
G
/1...1/c1
c0

3行を連結し、2倍して/1...1/を探している。

  • N:次の行を読む。2回実行するので、合計で3行、つまりケースごとに以下の処理を行うことになる。
  • h:上書きホールド。以前のホールドスペースの内容を削除し、現在のパターンスペースの内容を入れる。
  • G:ホールドスペースの内容をパターンスペースに追加する。これで文字列が2倍されたことになる。
  • /1...1/c1/1...1/にマッチしたら1を出力して(残りの処理をせず)次の行の処理に進む。
  • c0:前の行のマッチに失敗した場合のみこの行に到達する。0を出力して次の行に進む。

Vim (32B)

qq3JYPJr0/1...1\|\%#
xVpjq15@qZZ

qq..qでマクロを作り、15@qで15回(実行しながら定義するため1回減っている)繰り返す。 以下マクロを詳しく見ていく。処理開始の時点では1ケース3行のうち1行目にいて、処理が終わったら次のケースの1行目にいる、という状態を保つ。

  • 3J:3行連結
  • YPJ:ヤンクして直上に貼り付け、即座に下の行と連結する。これで行が2倍になったことになる。
  • r0:カーソル下の1文字(Jの直後のため、必ず空白文字)を0に置き換える。
  • /1...1\|\%#/1...1/を探す。マッチすれば、カーソルは最初の1の上に動く。マッチしなければ次の\%#だが、これは現在のカーソル位置であるため、カーソルの移動は起こらない。マッチに失敗するとマクロの実行が止まってしまうので、必ず成功させるためのもの。末尾の/は省略できる。

この時点でカーソル直下の文字が出力すべき値になっている。

  • xVp:カーソル直下の1文字を削除しつつレジスタに入れ、ビジュアルモードで1行全体を選択してレジスタの中身で上書きする。
  • j:1行下、つまり次のケースに進む。

最後のjで最終行のさらに下に進もうとすると勝手にマクロが止まるので、qq..q15@qではなくqq..@qq@q再帰のようにしてもよい。これはループ回数が3桁以上になると縮む。 AtCoderVimが追加されてからたった10か月の間に100問以上の最短コードが生まれていて、知見が溜まりまくり。

ICPC模擬地区予選 2020 参加記

2021/03/07に開催された模擬地区予選にチームAobayama_dropoutで参加しました。 メンバーは変わらず碧黴さん・ゆきのんさん・僕です。

模擬国内で使用するユーザ名とパスワードは、各チームに3組ずつ配布されました。直前になって適当に割り当てを決め、コンテスト開始です。

序盤(C問題)

特に何も打ち合わせがないまま、シュッと始まりました。碧黴さんがA、ゆきのんさんがBを読むらしいので、僕はCを読みます。

Cは挿入DPができそうな見た目をしています。同じ値の要素をまとめて、個数を見つつ遷移をしていきます。DP配列としてi<j<kかつa_i>a_k>a_jのうちjだけがある状態、jkがある状態、すべてある状態、の3つを考えます。1つ目は必ず1通りで、3つ目は値が1種類しかありません。2つ目については、条件を満たすjkの組のうちjが最も大きいものを考えることになるため、これだけは配列で持ちます。

これで適当に遷移を書いてみたところ、O(N^3)でした。サンプルを試すと一発で合ったので提出、AC。39分で、2番目のACだったようです。

E問題とB問題

僕がCを通している間に碧黴さんがAを通し、D問題の概要を共有してくれていました。パッと見フローですが、どうにもグラフが想像できなかったので、Eを読みます。明らかに牛ゲーなので、碧黴さんに伝えて実装してもらいます。

ゆきのんさんがB問題で詰まっていたので、これも概要を教えてもらい、priority_queueで中央値を動的に管理する方針を立てて伝えます。これは左右のサイズを注意深く観察しないとすぐバグるイメージなので、ちょっと面倒そうだなと感じて細かい実装までは伝えませんでしたが、ゆきのんさんも同様の解法を見たことがあるらしく、伝わりました。

F問題

Fを読みます。円を左右に分けてCHTっぽくやれば解けると考えましたが、ここで嘘をつきました。実際に式を立てればCHTであることがわかるのですが、僕はそうではなく「曲がっていても直線っぽく扱える」と感じてしまい、スライド最小値で実装してしまいました。

ここで碧黴さんのEがREを出したらしく、コードが共有されてきます。見てバグを指摘したところ、またRE。再度見るとまだバグが残っていて、これを修正してもらうとやっと通りました。

またゆきのんさんがBを通し、Iを読んでいるようで、概要が共有されてきます。そのまま期待値DPをする方針も立ったらしく、掃き出し法をするライブラリを提供しました。

さて、Fですが、実装が間違っているのではないかと疑っていろいろ適当に書き換えた結果4WAくらいしていました。さすがに考察を疑い、結果まずいケースを発見しました。ある円をある円が覆っているとき、その2つがqueueの先頭でずっと比較され続けるのですが、その間に下から別の円が上がってくる可能性があります。

結局一から書き直すことになって、CHTという方針そのものを捨て、円をソートして交点を調べ、最大値が変わる境界を保存して、クエリには二分探索で答えるコードを書きました。提出すると一発AC、185分でした。

G問題

僕がFを通すより先にゆきのんさんのIが通りました。そのまま3人でGを読みます。僕はもう全然頭が回っていませんでしたが、ゆきのんさんが半分全列挙を疑いました。その方針で考えてみたところ、右半分を先に列挙して、左の数が10の何乗倍されるかによって保存するテーブルを分ければ、解けることがわかりました。

僕が実装して提出しますが、RE。コードを共有してみると、すぐにREを出すケースが作成されてきました。

123456789123456789123456789123456789
47

どうやらテーブルのサイズが1足りていなかったようです。増やして提出しなおすとAC!243分でした。

残り時間

最後に全員でKを考えていましたが、全然ダメでした。今振り返ってみれば、全然集中していなかった気もします。

結果

12位でした。全体的にあんまり集中できていなかった気がしますが、これはここ数日コードゴルフ大会にどっぷり浸かっていて、睡眠時間などを削ってしまっていたからだということにしておきます。

実装の分担は結構できていたのではないかと思います。

ICPC2020チーム紹介ドキュメントのメイキング

kotatsugame.hatenablog.com

gist.github.com

https://ideone.com/BrtL8u

今回はQuineのAAコードを作ってみました。

1.AAを用意する

tool-taro.com

前回と同じく、またこのサイトを使ってAAを作ります。元となった画像はガウリールドロップアウトのアニメ公式サイトから拾ってきました。↓のページの4枚目の画像です。著作権は……よくわかりません。

gabdro.com

このままAA変換サイトに投げてもよいのですが、輪郭をちゃんと取り出すために下準備をします。輪郭を認識する……機械学習だな!とはならず、温かみのある手作業です。ペイントで300%くらいに拡大し、マウスを握りしめて縁取りして、内部を白で埋めます。何度か試してみて、つぶれがちだったアホ毛・眉毛・口はちょっと太くしておきました。

f:id:kotatsugame:20210228081646j:plain
微妙に残った元の色が哀愁を誘う

ideoneのフォントでいい感じに見えるように縦横比を調整します(1敗)。ここでいう1敗とは、全ての工程が完了してから縦横比を調整していないことに気づいた、という意味です。

AA変換サイトに投げたものがこれです。サイズは80を指定しました。これはチーム紹介ドキュメントの制約で、幅が半角75文字以内である必要があったからです(1敗)。ここでいう1敗とは、全ての工程が完了してから幅が制限をオーバーしていることに気づいた、という意味です。上の敗北とは別の回です。

f:id:kotatsugame:20210228083245p:plain
第一段階

https://ideone.com/lPnJYs

まず、`を空白に置換し、それ以外すべての文字を#に置換します。また、頭のてっぺんの線を付け足します。そして、幅が半角75文字に収まるように、左右から少しだけ行を削除します。幅を80に指定してAAに変換しましたが、実際は79文字でした。アホ毛を少し縮めて右から-2行、あとは左から-2行しておきます。顔が真ん中に来るようにしました。

さらに後々のため、右目の下2行が9文字・7文字になるようにしておきます。

f:id:kotatsugame:20210228084045p:plain
第二段階

https://ideone.com/2LgmEH

2.ランレングス圧縮する

今回は出力をAAにするにあたって、ランレングス圧縮した数列をもとに構築するアルゴリズムを採用しました。白と黒しかないので、それが交互に出現するようにゼロ埋めしておきます。行の幅をすべて等しくしておけば、改行の出力は75回に1回行うと決め打ってよいです。数列は、各値に65を足したものを文字コードとして見た文字列にして保持します。運よくバックスラッシュが出現しなかったので、特にエスケープなどを考える必要はありません。

次のようなプログラムで文字列を圧縮しました。

ans=[]
while s=gets
  s.chomp!
  a=[]
  t=" #"
  c=0
  75.times{|i|
    t[a.size%2]==s[i] ? c+=1 : (a<<c;c=1)
  }
  a<<c
  a<<0 if a.size%2==1
  ans+=a
end
A=65
puts"WARNING"if ans.include?(92-A)
puts ans.map{|e|(e+A).chr}.join

3.Rubyコードを書く

「あなたの知らない超絶技巧プログラミングの世界」という本を読みます。

https://www.amazon.co.jp/o/ASIN/4774176435/gihyojp-22

4-1-1項(p.137)に、アスキーアートQuineの基本構造が載っています。これを使いました。

eval$s=%w(
s=%(eval$s=%w(#$s))*"";
# 成形して出力
)*""

成形して出力するところさえ考えればよいです。次のようになりました。(一か所(){}が変わっていますが、コードの内容に違いはありません。)

s=%(eval$s=%w{#$s}*"").chars;
a="圧縮された文字列";
t=[32.chr]*897;
l=0;
a.bytes{|c|s,t=t,s;c.downto(66){$><<s.shift;(l+=1)%75==0&&puts}}

どのようなコードを経てこれにたどり着いたかは記録していませんが、最初に書いたコードからかなり変わったことは覚えています。このコードの文字数がちゃんとAAの#の文字数より少なくなるようにコードゴルフしたからです。それでも微妙に収まらなかったので、先ほどのAAから孤立した空白や#をいくつか削除することで、圧縮後の文字列が短くなるようにしました。最終的に、以下のようなAAを使いました。

f:id:kotatsugame:20210228094947p:plain
第三段階

https://ideone.com/KL1HCq

4.AAにする

先ほどのコードをそのまま実行すれば、AAのQuineが手に入ります。つまり、わざわざ手で空白を入れたりして最初の状態を作る必要はないということです(これも「あなたの知らない超絶技巧プログラミングの世界」に書いてあることです)。

ここで、先ほどのコードに少し無意味な変数を足し、順番を入れ替え、圧縮された文字列を2つに分割して、次のようなコードを実行します。

eval$s=%w{
a="圧縮された文字列の先頭";
Aobayama_=t=[32.chr]*897;
l=0;
s=dropout=%(eval$s=%w{#$s}*"").chars;
(a+"圧縮された文字列の末尾").bytes{|c|s,t=t,s;c.downto(66){$><<s.shift;(l+=1)%75==0&&puts}}
}*""

僕が所属するICPCのチーム名はAobayama_dropoutです。これがQuineの中に見えていると、より技巧的に感じられることが期待されます。そこで、Aobayama_dropoutという変数を足しました。最初にAAを作ったとき、右目の下2行が9文字・7文字になるようにしていましたが、そこにちょうどこのAobayama_(9文字)とdropout(7文字)が来れば、全体のだいたい真ん中あたりにチーム名が見えることになります。

試行錯誤の結果、圧縮された文字列の最後3文字だけを分割すれば、ちょうど狙った位置に変数名が来てくれることがわかりました。

5.完成!

f:id:kotatsugame:20210228100549p:plain
実行結果とともに

https://ideone.com/BrtL8u

6.余談

大きな幅で作ってしまったものは、Aobayama_dropoutに加えTOHOKU_UnivもQuineに含めることができました。せっかくなのでそちらも載せておきますが、縦横比を修正する前のため、キャプチャはideoneではなく僕の手元の環境になります。

f:id:kotatsugame:20210228101350p:plain
右目の下半分に注目

https://ideone.com/8bii4H

7.手元に届いてみると……

f:id:kotatsugame:20210305110836j:plain
潰れちゃった

悲しいなあ