APIO2022に参加しました!!

APIO2022に参加しました!!


 幸いにも今年は募集枠が多かったため、今年のAPIO(Asia Pacific Informatics Olympiad)2022Egyptに出場することができました。競技の感想について残します。コードにおけるマクロに関しては

k1suxu.hatenablog.com

の「テンプレートの作製」の欄をご参照ください。
書くだけ書いて投稿してなかったことに気付いたので2年越しに公開しました。


当日の流れ

競技前

 当日はゆっくり9時ごろに起きて競技開始時間の13:00に向けてご飯を食べたりしました。

競技開始

 APIOでは、問題、サンプル、各種コードなどがzipファイルなどにまとまった状態で掲示されるので、とりあえずそれらをダウンロードしてフォルダに入れました。
 とりあえず問題を一通り読みます。marsの問題文の意味が分からなくて焦りながら、とりあえずgame, permの読解に移ります。とりあえず問題設定は把握したのですぐに考察に移りました。以下は自分が解いた問題の順番とその解法です。


perm 91.36点

 これは部分列のうち単調増加であるような数が K個となるような順列 Aを構成してほしいということです。こういった構築系は二進数にするのがいいという典型があるのでとりあえず二進数で考えてみると、ある時点でどの要素よりも大きいような要素を右に入れると条件を満たすような部分列の個数は2倍になって(手順1)(1bit右にずらす)、右に入れてから左に入れると元の通り数を Xとすると、 2X+1となる(手順2)(1bitずらしてから+1)ことがわかりました。ということでビットが立っている位置を見つけたら手順2を、そうでなければ手順1を行うようにするとうまく構成できます。実際には再帰的に求めることで構成しました。
提出コードは以下です。

vector<int> construct_permutation(ll k) {
    k--;

    if(k == 0) return {};
    
    if(k == 1) return {0};

    vector<int> ret;

    if(k%2 == 1) {
        vector<int> ret = construct_permutation(k/2+1);
        ret.push_back(ret.size());
        return ret;
    }else {
        vector<int> ret = construct_permutation(k);
        ret.insert(ret.begin(), ret.size());
        return ret;
    }

    assert(false);
    return {};
}


game 30点

 部分点の1~3については毎回DFSをすればいいことはすぐにわかったので実装しました。まず、initが呼び出されるタイミングで、初期状態のテレポーターを示す辺を張り、add_teleporterが呼ばれるたびに辺を追加しDFSによって判定しました。判定においては、0\leqq{i}\leqq{j}\lt kなる頂点組 (i, j)であって、 jから一つ以上の辺を通って iに行けるようなものが存在するかどうかを判定できればいいです。より具体的には頂点0からDFSをし、現在の経路内にある頂点をsetで管理し、頂点番号が kよりも小さいような頂点に来たらset.findで既出判定を行うことで判定できます。
提出コードは以下です。

vvi g;
vector<bool> can;
int nn, kk;

void init(int n, int k) {
    g.resize(n);
    nn = n;
    kk = k;
    FOR(k-1) g[i].push_back(i+1);
    can.resize(n, false);
}

vector<bool> done;
set<int> st;

int dfs(int v) {
    if(can[v]) return true;

    done[v] = true;
    st.insert(v);
    for(auto e : g[v]) {
        if(e < kk && st.find(e) != st.end()) return can[v] = true;
        else if(!done[e]) {
            if(dfs(e) == 1) return can[v] = true;
        }
    }

    st.erase(v);
    return can[v];
}

int add_teleporter(int u, int v) {
    done.assign(nn, false);
    st.clear();
    g[u].push_back(v);
    return dfs(0);
}


mars 14点

 ここにきて問題文の意味が理解できなかったmarsの読解に入ります。簡潔に言えば、「連結成分の個数を数えたいけどメモリの制限が厳しいからデータをうまく圧縮してね」という問題です。普段こういった形の問題を解く機会はなかったので、読解に苦戦してしまいましたが、非常に勉強になりました。
 簡単にわかることとして、盤面の情報をbitで表したものを左上に左上に渡していけばいいのでその方針で実装すると14点が取れました。連結成分の個数は簡潔にBFSで数え上げました。
提出コードは以下です。

ll cal(string s, int n) {
    vector<vector<bool>> grid(n, vector<bool>(n, false));
    rep(i, n) {
        rep(j, n) {
            if(s[i*n+j] == '1') grid[i][j] = true;
        }
    }

    vector<vector<int>> dist(n, vector<int>(n, -1));

    auto bfs = [&](int X, int Y) -> void {
        dist[X][Y] = 0;
        queue<pii> que;
        que.emplace(X, Y);
        while(que.size()) {
            int x, y;
            tie(x, y) = que.front();
            que.pop();

            rep(d, 4) {
                int nx = x + dx[d];
                int ny = y + dy[d];

                if(nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                if(dist[nx][ny] < 0 && grid[nx][ny]) {
                    dist[nx][ny] = dist[nx][ny] + 1;
                    que.emplace(nx, ny);
                }
            }
        }
    };

    ll ans = 0;

    rep(i, n) {
        rep(j, n) {
            if(grid[i][j] && dist[i][j] < 0) {
                bfs(i, j);
                ans++;
            }
        }
    }

    return ans;
}

string to_bit(ll n) {
    string ret(100, '0');
    rep(i, 60) {
        if(n >> i & 1) ret[i] = '1';
    }
    return ret;
}

string process(vector<vector<string>> a, int i, int j, int k, int n) {
    int dim = 2*k+1;
    string ret(100, '0');
    rep(id, dim) {
        rep(jd, dim) {
            rep(ii, 3) {
                rep(jj, 3) {
                    ret[(id + ii) * (dim + 2) + (jd + jj)] = a[ii][jj][id * dim + jd];
                }
            }
        }
    }

    // cout << ret << endl;

    if(k == n - 1) {
        return to_bit(cal(ret, dim+2));
    }else {
        return ret;
    }

    assert(false);
    return "";
}

 marsに関してはこれ以上の部分点を取ることはできませんでした。


game 60点

 30点を取ったときの考察にある「 i\leqq{j}\lt kなる頂点組 (i, j)であって、 jから一つ以上の辺を通って iに行けるようなもの」という部分を高速化したいなと思いました。ここで、説明の都合上、順辺を張ったグラフを G, 逆辺を張ったグラフを G'とします。ある頂点 vについて G上で到達可能な頂点のうち、 {0\leqq{i}} \lt kを満たすものの最小値を min_vとし、 G'上で到達可能な頂点のうち、 {0\leqq{i}} \lt kを満たすものの最大値を max_vとします。
するとこの問題は、 min_v\leqq{max_v}を満たすような頂点 vが存在するかというものに帰着します。
 あとはうまいこと必要部分のみを更新するように実装するだけです、少し複雑ですが以下の実装を参考にしてください。
 正直この提出で60点をとれるとは思っていなかったのですが、 k\leqq{1000}がうまく効いてくれたおかげで得点を伸ばすことができました。

vvi g, rg;
vector<int> from, to;
int nn, kk;

void init(int n, int k) {
    g.resize(n);
    rg.resize(n);
    nn = n;
    kk = k;
    FOR(k-1) g[i].push_back(i+1);
    FOR(k-1) rg[i+1].push_back(i);
    
    to.assign(n, INF);
    from.assign(n, -1);
    FOR(k) from[i] = i;
    FOR(k-1) to[i] = i+1;
}


void dfs1(int v, int now, int& mnto) {
    from[v] = now;
    chmin(mnto, to[v]);

    for(auto e : g[v]) {
        if(from[e] < now) {
            dfs1(e, now, mnto);
        }
    }
}

void dfs2(int v, int now, int& mxfrom) {
    to[v] = now;
    chmax(mxfrom, from[v]);

    for(auto e : rg[v]) {
        if(to[e] > now) {
            dfs2(e, now, mxfrom);
        }
    }
}

int add_teleporter(int u, int v) {
    if(u < v && v < kk) return 0;

    if(u == v) {
        if(u < kk) {
            return 1;
        }else {
            return 0;
        }
    }

    g[u].push_back(v);
    rg[v].push_back(u);

    int mxfrom = -1, mnto = INF;

    if(v < kk) {
        if(to[u] > min(to[v], v)) {
            dfs2(u, min(to[v], v), mxfrom);
        }
    }else {
        if(to[u] > to[v]) {
            dfs2(u, to[v], mxfrom);
        }
    }
    if(u < kk) {
        if(from[v] < max(from[u], u)) {
            dfs1(v, max(u, from[u]), mnto);
        }
    }else {
        if(from[v] < from[u]) {
            dfs1(v, from[u], mnto);
        }
    }

    if(to[u] <= mxfrom || mnto <= from[u] || to[v] <= mxfrom || mnto <= from[v]) {
        return 1;
    }

    return 0;
}


競技終了

 ここまで合計得点 14+60+91.36=165.36で競技が終了しました。最終順位は国内16位(同順位5人)銅メダル相当という結果になりました。
ちなみに、permは既出だったらしいです(悔しい) atcoder.jp
 私にとってはこれが最後の情報オリンピック関連大会だったので、いい結果が残せてよかったです。大学ではICPCにも出たいですね。

青色コーダー(AtCoder)になりました!!

 初めまして、趣味で競技プログラミングをやっているk1suxuと申します。言語は主にC++を使用しています。先日のAtCoder Beginner Contest 251にて、幸いにも初黄パフォを出すことができ、念願の入青を果たしました。自分の進捗を残すためにもブログに書き起こしてみます。


目次


現在のAtCoderの進捗状況

最近は一気に青色を解き進めた後に少しずつ黄色を解いています。

青色になるまでに学んだこと

 この章では、私が青色になるまでに学んだ知識について列挙しています。一部抜けがあるかもしれませんがご了承ください。 また、色ごとの勉強方法やその色の時期に何を学んだかなどについては、次の章をご参照ください。

1.数学的な知識

青色になるまでに学んだ数学的な知識は以下の通りです。

高校数学

 競技プログラミングを始めたのが高校2年生の4月であって、今も高校生なので、特に追加で学んだことはないのですが、競技プログラミングで重要だと思う分野は以下です。

・ベクトル(平面、空間)

・初等幾何(三角関数三平方の定理、多角形の面積など)

・初等関数(n次関数や三角関数、指数対数関数などといった関数の概形に関する知識)

・方程式(問題文に対して適切に未知数を定め、計算式にまとめる能力がしばしば求められます)

不定方程式(不定方程式の考え方を用いて、二つの未知数に対して一つを全探索してもう一つを求めるというような考察が必要となることがあります)

・数え上げ(場合の数)(いわゆる写像12相などです)

・確率(条件付確率や包除原理など)

・期待値(特に期待値の線形性) 私の苦手分野です。

・数列(等差数列、等比数列、漸化式、特性方程式、一般項や総和、級数の公式など)

複素数平面(図形問題を複素数平面上で解くとうまくいったりします)

その他

・行列の基礎的な知識(行列を用いて連立方程式の解を求める方法、行列式、転置、逆行列)

群論における基礎、特に演算子の定義やモノイドなど

・オーダー表記(計算機科学?)による計算量、メモリ量の見積もり

グラフ理論の基礎

パスカルの三角形

フェルマーの小定理(逆元の計算に必要です)

・拡張ユークリッドの互除法(不定方程式の解を一つ求めます)


2.アルゴリズム・データ構造

青色になるまでに学んだアルゴリズム・データ構造は以下の通りです。

DP

・配るDP, 貰うDP, 区間DP, bitDP, 期待値DP, 桁DP, 文字列DPなど

・LCS(Longest Common Subsequences)

・LIS(Longest Increasing Subsequences)

数学

・MOD上での各種計算、高速累乗(繰り返し二乗法)

・逆元の計算

パスカルの三角形を用いたnCrの計算

・エラトステネスの篩

・Euler Phi

・転倒数

・スライド最小値・最大値

探索法

・順列全探索

・bit全探索

・二分探索

・三分探索

・半分全列挙

グラフ理論

〇最短経路探索

・BFS, DFS

Dijkstra

・Warshall Floyd

・Bellman_Ford

〇BFS, DFSの応用知識

・Euler Tour

・01BFS

・BFS Tree, DFS Tree

・強連結成分分解 Strongly Connected Components(SCC)

・二部グラフ判定

・LowLink グラフ上の橋、関節点の検出

・木の直径

〇フロー

・最大流(最小カット)問題(Ford_Fulkerson法、Dinic法) まだコンテストで使ったことはないです。

〇素集合データ構造

・Union Find

・Weighted Union Find 重み付きUnion Findのことです

木構造に関するクエリ

LCA (Lowest Common Ancestor)

・Topological Sort (DAGの判定にも使えます)

・無向最小全域木(Kruskal法、Prim法)

区間に対するクエリを高速に答えるデータ構造など

・BIT(Binary Indexed Tree)

・Fenwick Tree

・セグメント木、遅延セグメント木

・Mo's Algorithm

文字列

・Z_Algorithm

・Rolling_Hash

・ランレングス圧縮

 これらのアルゴリズムは引数を何らかの配列にすることで文字列以外にも適用できます。 例えばランレングス圧縮の場合、c++では以下のように実装できます。

その他

・累積和

・imos法

・座標圧縮

・マンハッタン距離の性質(45度回転)

・しゃくとり法

・調和級数を用いた計算量解析


2.ライブラリの整備

テンプレートの作製

 コンテストに出るにあたりテンプレートファイルを作成しました。現在使用しているものは以下です。特にfor文をマクロで短縮するのはおすすめです。

現在使用しているライブラリ

 前述したアルゴリズムたちにあるものはほぼすべてライブラリ化しています。その他持っているライブラリは以下です。

・構造体:有理数(分数)
・構造体:行列
素因数分解
・商(整数除算)の列挙
・幾何ライブラリ
->多角形の面積や内外判定など多数


色ごとの勉強

 この章では、私が青色になるまでの色の遷移とその時々にしていた勉強について記述しています。具体的に何を学んだかについては前章をご参照ください。

灰~茶(2021年4月24日~2021年6月13日)

 私がプログラミングそのものの勉強を始めたのは2021年の3月辺りで、その約1か月後に友人に教えてもらったことをきっかけにAtCoderに登録し、競技プログラミングを始めました。当時私はPython競技プログラミングしており、多重配列の入力の受け取りや初期化に苦労してた記憶があります。初参加のABC199では2完にとどまりました(当時の私は計算量という概念を知らず、C問題で一生TLEしていました...)。この頃は特に競技プログラミング向けの勉強はしておらず、ひたすらプログラミングの基礎に触れるという感じでした。計算量について少しだけ学びましたが、「ソートって時間かかるんだ。へ~。」程度にしか思っていませんでした。もともとタイピングはそれなりに速かったので、簡単な問題を早く解くことのみに専念していました。

茶~緑(2021年6月13日~2021年9月11日)

 茶色になってからも数か月は、灰色を虚無埋めしていました。7月ごろになり、学校が夏休みに入ったころ、E869120さんの「レッドコーダーが教える、競プロ・AtCoder上達のガイドライン」という記事を見たことをきっかけにアルゴリズムの面白さに引き込まれました(本当におすすめでとても為になる記事です。ぜひ検索してみてください)。そこからはBFSやDFSなどの基礎的なアルゴリズムを学びつつ、AOJ(AiZU ONLINE JUDGE)などを利用してライブラリの整備などをしていました。そうしている間に緑色になりました。

 ここまでで最短経路アルゴリズム、nCr, nPr, nHrなどのライブラリが整いました。

緑~水(2021年9月11日~2022年1月30日)

 ちょうど緑色になったころ、日本情報オリンピック(JOI)の存在を知り応募しました。一次予選突破が確定したあたりからはJOIの過去問をといていました。私にとっては最初で最後のJOIだったので、この頃はJOI二次予選突破を第一目標に精進していました。今年は例年よりも定員が多かったため、無事に二次予選を突破し、本選に進むことができました。

 JOIが一段落ついたので高校二年生の冬休みはAtCoderに傾倒することにしました。むやみやたらに簡単な問題を解いても成長しないだろうということは経験でわかっていたので、自分の得意不得意やコンテストでの動きについて分析してみることにしました。分析してみた結果当時の私はDPに弱く、早解きはできるが、difficulty*1が緑色以上の問題に対してACできていないということがわかりました。また、グラフの問題に強いという傾向も見て取れました。

 分析の結果を踏まえてDPを扱う問題を解こうと思いEDPCを少し解き進めました。これにより、DPの基礎を学ぶことができました。また、緑色以上の問題を安定して解けるようになることを目標にABC中の緑色の問題を埋めてみることにしました。緑色が安定してきたら、次に水色へと移りました。この頃から、ScrapBoxにて解いた問題の感想や考察の手順、振り返りを始めました。これを始めてから実力が一気に伸びた気がします(知識と解法の点と点が線で結ばれるような感覚です)。水色を60問ほど埋めたあたりでついに水色になれました!!

 この時点で、現在そろっているライブラリの7割ほどは完成しています。具体的には(フローライブラリ, fenwick_tree, rolling_hash, 幾何ライブラリの一部, mo's algorithm, 行列ライブラリ + α以外です)。

水~青(2022年1月30日~2022年5月14日)

 水色になった後はひたすら青問題を埋めました。知らないアルゴリズムが出てくるということはあまりなく、典型考察力*2が鍛えられたという感想です。そのままScrap Boxで問題の感想を書くことを続けつつ精進していると順調にレートが伸びていき2022年5月14日についに青色になることができました。

問題を解く度の振り返りの勧め

 前述した通り私は、競技プログラミングの問題を解いたら、常にScrap Boxを用いてその問題についてメモを行うようにしています。具体的には以下のようなものです(ABC107_D_Median_of_Medians)。

*注意*ABC107 D Median of Mediansのネタバレを含みます。見たくない方は飛ばしてください!!



 これを行うことにより、自分の思考の過程をアウトプットすることになるので、考察がその場しのぎではなく、ほかの問題への応用も可能な状態になります。また、自分の持っている知識たちのかけ合わせが文字になって表れるので、複雑な典型考察を言語化、簡略化して覚えることができます。

これからの予定

 とりあえずは、学業をおろそかにしない程度に精進していこうと思います。最近はABCの黄色diffの問題を解き進めていますがやっぱり難しいですね、また色変したら記事にしようかと思います。ここまで読んでいただきありがとうございました。またここに色変記事を投稿できるよう頑張ります!!

*1:AtCoder Problemsさんが算出提供してくださっている、AtCoder内の各問題の難易度をAtCoder Ratingで示したようなものです。

*2:特に、座標圧縮してfenwick_treeのような、よく使うけど知らないと難しい。というような考察がパッとできるようになりました