最近気づいたので書きます(有名かも?)
有名な等式として ( は正整数)があります。これを証明します。
整数 と実数 に対して
であることを利用します。 を整数として
ですが、 の後でだけ を適用すると が得られ、 の両方の後で を適用すると が得られます。このふたつが同値で、右辺が整数であることから、右辺は等しいです。
雑にいうと、操作の途中で floor をとったりとらなかったりしてよい、という感じです。 なども同様に示せますね。
最近気づいたので書きます(有名かも?)
有名な等式として ( は正整数)があります。これを証明します。
整数 と実数 に対して
であることを利用します。 を整数として
ですが、 の後でだけ を適用すると が得られ、 の両方の後で を適用すると が得られます。このふたつが同値で、右辺が整数であることから、右辺は等しいです。
雑にいうと、操作の途中で floor をとったりとらなかったりしてよい、という感じです。 なども同様に示せますね。
間違っている部分やもっと簡単にできる部分などがあれば教えていただけると助かります。
XOR 演算子は で書く。
非負整数の(有限)集合 に対して全体の XOR を とも書く。
非負整数の(有限)集合 に対して (何個か選んで作れる XOR 全体の集合)と書く。
を 行列とみたときの基底(XOR 基底)の つを と書く。
の要素はワードサイズに収まることを仮定する。ワードサイズを と書く。
とする。
つまり、作れる XOR はどれも、作る方法の数が同じである。*1
No.2672 Subset Xor Sum - yukicoder
その bit が立っている要素が にない場合、明らかに
そうでない場合、その bit が立っているものと立っていないものは半々である。つまり、 である( の bit 目を と表した)。
xor の掃き出しすごい簡単に出来るんですね
— 熨斗袋 (@noshi91) 2019年11月30日
vector<int> basis;
for(int e : a){
for(int b : basis)
chmin(e, e ^ b);
if(e)
basis.push_back(e);
}
これで数列 a の基底が basis に入る
このアルゴリズムを実行すると、msb*2 が distinct な基底が得られる。行列の言葉でいうと、(行を降順ソートすれば)階段行列になる。 の要素 つを処理するのに 時間。
[4] のアルゴリズムを少し変形すると、msb が distinct なだけでなく
を満たす基底が得られる(具体的なアルゴリズムはこの節の一番下にある問題の解説を参照)。行列の言葉でいうと、(行を降順ソートすれば)簡約行列になる。基底を計算するパートの計算量は [4] と同じ。
この基底をソートする(昇順に とする)。すると、各 を選ぶか選ばないかを で表したものが の 進表記と対応する。すなわち、 の要素を昇順に とすると、 のとき となる。クエリ 時間。
verify に使える・より詳細な解説が書いてある:G - Partial Xor Enumeration
単純な二分探索で自明に 時間になるが、もっと高速になる。
[5] の基底をとる。基底を降順に見ていき、「選んでも を超えないなら選ぶ」ことを繰り返す。クエリ 時間。
今あるどの bit よりも上の余っている bit を つ選び、全部の値で立てておく。その bit が なら偶数個、 なら奇数個選んだといえる。 を昇順に並べたとき、 番目はこの bit が に、 番目はこの bit が になる([3] の事実も参照)。
(偶数個選んで作れる XOR の集合)と書く。 に対して とすると、 となる。
No.803 Very Limited Xor Subset - yukicoder
ARC のネタバレ注意
E - Rearrange and Adjacent XOR
今あるどの bit よりも上の余っている bit を つ選び、 のその bit を立てたものを とする。 を考えると、 のうち先程新たに立てた bit が立っているもの、すなわち昇順で 番目のものが対応物([3] の事実も参照)。
最大だけなら、後で扱う問題の解説 に書いてある方法で求まる:[4] または [5] の基底を降順に見て、 を繰り返す。
任意の 番目で同様のことができるかは不明。
[8] 1. と [6] を組み合わせるとできる。
非負整数列 と が与えられる。 を のもとで選べるとき、 の最大値を求めよ(あるいは、そのような選び方はないと判定せよ)。
(記号をこの記事用に変えています)
いったん を許して考える。
、 とすると、求めるものは
となる。 未満となる境界を [6] の方法で求める。 番目が 未満で最大であるとすると、条件を満たす選び方に対応する 進数が、区間 になっている。つまり、境界を求めるのに使った [5] の基底の下 bit を取り直して とすると、求めるものは
となる。(ここで であることが計算量的に効いてくる。)(ここで先程許した を考える。 以外で条件を満たせないのは、 で、 に対応する が の自明な 通りしかない場合。[2] より、 かどうかで判定できる。)
から、対応する 進数が区間 にある選び方をしたとき、選んだ整数と の総 XOR の最大値 とする。答は である。 番目を選ぶかどうかで場合分けして再帰する。
のとき
のとき
やっていることは公式解説と多分同じ(再帰をループで書いているだけ)。計算量も同じく になる。
*1:教えていただきありがとうございます https://twitter.com/Sophia_maki/status/1766876738024300696
range chmin でググるとなぜか beats の話しかヒットしないので
区間 chmin 一点取得 だけならもっと簡単にできます。(当たり前ですが chmin を chmax に替えても可)
ABC179 解法 • knshnbのブログ の F のようにすればできます。
区間積などを呼び出すと壊れますが、呼び出さないことで解決
双対セグメント木 - HackMD あたりを読みましょう(面白いです)
競プロにもありそうな教育的な問題だと思いました。
の並べ替え について、 となる の個数を とする。すべての並べ替え について を足し合わせた値を求めよ。
を、 であるような の順列 の個数とします。
のときは撹乱順列の個数で、包除原理から とわかります(母関数による導出もあります:指数型母関数入門 – 37zigenのHP)。
一般の に対しては、一致する箇所 個を選んだ後、残り 個の場所で撹乱順列を作ると考えて、 となります。
答えは (で としたもの)です。
母関数を使って考察します。まずは撹乱順列の個数 の母関数を考えましょう。 であり、撹乱順列の個数はその累積和なので をかけたものが母関数になります。つまり です。
次に の母関数を考えましょう。このままでは考えにくいので、 と変形します(このように冪乗を下降冪の級数で表したときの係数は 第 2 種スターリング数 です)。すると
となります(負の階乗の逆数や負冪の係数は とみなしておきます)。よって答えは
となります。(補足:2 行目から 3 行目は FPS の積の定義、3 行目から 4 行目は 倍が累積和)
を に、 を に一般化したときの答えを とすると、 です( は第 2 種スターリング数)。これを(mod 998 とかで)計算する競プロの問題だと思うことにします。[多項式・形式的べき級数] 高速に計算できるものたち | maspyのHP を参考にすると、次のことがわかります。
固定、クエリ に対して を求める:「第 2 種 Stirling 数」の項の「前者」の方法で を で前計算して累積和をとることで、各クエリについて(階乗の計算を除いて) で求まる。
固定、 に対して を求める: がすべての について足されることから Bell 数になって、 で求まる(「Bell 数」の項も参照)。
固定、 に対して を求める:「第 2 種 Stirling 数」の項の「後者」を参考にすると を 次まで求めればよい。これは を Polynomial Taylor Shift した後 を代入すればよいので(「Polynomial Taylor Shift」および「特殊な合成」の項を参照)、(階乗の計算を除いて)全体 で求まる。
SBT についてざっくり理解して、次の機能を実装できるようになります。
ABC333 G - Nearest Fraction のような問題を SBT 上の探索で解く
新規性は特になく、既存の記事を自分用にまとめた程度の内容になっています。
気持ち寄りの説明が多めで、議論がやや雑な部分があるかもしれません。
Stern–Brocot Tree (SBT) は、次のように定められる、(無限に続く)完全二分木です。
Stern–Brocot Tree には次の重要な性質があります。
二分探索木である。つまり、 である。(cf. 加比の理)
根から右の子に 回、左の子に 回、、( が奇数 ? 右の子 : 左の子) に 回進んだ頂点が対応する有理数は、連分数 として表される。
逆に、各頂点に対応する正の有理数は相異なる。(1. からわかる。あるいは、正の有理数の連分数展開は に限定すれば一意なので、と思ってもよい)
根から始めて、有理数 に対応する頂点に到達するまでのパスを考える。「右の子/左の子 に 回進む」ことをひとまとまりの操作とみなしたときの操作回数(パスの連長圧縮の長さ、2. での の値 *2)は である。(2. からわかる。連分数展開は実質的に互除法なので)
頂点 の子孫の集合は、開区間 になる( は とみなす)。(気持ちとしては、左の子に 回進むと 、右の子に 回進むと で、 とするとそれぞれ 、)
有理数から SBT 上のパス(の連長圧縮)を得る問題です。性質 2. を利用して連分数展開すればよいです。(性質 5. より、出力は多くなりません)
SBT 上のパス(の連長圧縮)から有理数を得る問題ですが、素直に根から辿ると有理数だけでなく頂点の 整数 を得ることができます(そう実装すると後々楽です)。(これも性質 5. より、入力は多くなりません)
つの有理数の LCA(に対応する有理数)を求める問題です。 つの有理数を ENCODE_PATH したのち、根から辿っていってどこまで一致するか見ればよいです。性質 5. より間に合います。
有理数 の祖先であって深さ の頂点(に対応する有理数)を求める問題です。これも を ENCODE_PATH したのち、根から深さ まで辿っていけば性質 5. より間に合います。
有理数が与えられて、その子孫の下限と上限を求める問題です。性質 6. そのものです。ENCODE_PATH してから DECODE_PATH すると が得られるので、それが答えです。
SBT 上を探索する話です。
より一般に、次の形で考えましょう。(この定式化は [2], [3] をかなり参考にしています)
関数 は単調性がある。すなわち、ある が存在して、 といった形になっている(等号・不等号や true/false は入れ替わってもよい)。このとき、境界 を分母・分子が 以下の有理数で近似したい。 *3 を満たす のうち、 および が最小になるものを求めよ。
性質 1. を利用して、二分探索木を探索することを考えます。性質 6. より、探索の途中で頂点 にいるとき、 は を満たしています。よって、素直に二分探索木を探索して、 を超えたら打ち切る、とすると で解けます。性質 5. を利用して、「その方向にどれだけ進むか」を二分探索 *4 すれば速くなります(log ふたつに見えて実は です。証明は [2])。
実装で自分が苦労した細かい部分を解説します。まず、頂点 に到達したとき、そこからはもう進まずに を答えとする条件は または です。 は、左および右に 個進んだときの および だからです。
同様に、進む深さ を二分探索するときの の上限 は、(左に進むのであれば) または を満たす最小の 、というようにして決めることができます *5。
こういったことは分母・分子が 以下という制限が強い場面でなければ(例:[2] のように、真の値が有理数で、かつ分母・分子がある値以下であることが示せる場合)気にしなくてもあまり問題はありませんが、今回の問題のような場面では出力が を超えてしまうバグの原因になるので、注意して実装する必要があります。
クリックして展開
#include <iostream> #include <vector> #include <functional> #include <cassert> using namespace std; // stern-brocot tree namespace sbt { // (p, q, r, s) から (isleft ? 左 : 右) に d 個進んだ頂点を返す template<class T> tuple<T, T, T, T> child(T d, T p, T q, T r, T s, bool isleft) { if (isleft) r += d * p, s += d * q; else p += d * r, q += d * s; return make_tuple(p, q, r, s); } // (p, q, r, s) の親を返す // (p, q, r, s) が根なら (0, 0, 0, 0) を返す template<class T> tuple<T, T, T, T> parent(T p, T q, T r, T s) { if (p == 0 && q == 1 && r == 1 && s == 0) return make_tuple(0, 0, 0, 0); if (p < r || q < s) r -= p, s -= q; else p -= r, q -= s; return make_tuple(p, q, r, s); } // 1/1 から p/q へのパスが右に a_1 回、左に a_2 回、… としたときの a を返す // (a_1, …, a_k + 1) は p/q の連分数展開になっている template<class T> vector<T> encode_path(T p, T q) { vector<T> a; if (p < q) { a.emplace_back(0); swap(p, q); } while (p != 1) { a.emplace_back(p / q); p %= q; swap(p, q); } if (!a.empty()) { if (a.back() == 1) a.pop_back(); else a.back()--; } return a; } // 1/1 から右に a_1 回、左に a_2 回、… 移動した頂点 (p, q, r, s) を返す // (a_1, …, a_k + 1) は (p+r)/(q+s) の連分数展開になっている template<class T> tuple<T, T, T, T> decode_path(const vector<T> &a) { T p = 0, q = 1, r = 1, s = 0; for (int i = 0; i < (int)a.size(); i++) tie(p, q, r, s) = child(a[i], p, q, r, s, i & 1); return make_tuple(p, q, r, s); } // p/q と r/s の LCA (f, g, h, k) を返す template<class T> tuple<T, T, T, T> lca(T p, T q, T r, T s) { vector<T> a = encode_path(p, q), b = encode_path(r, s); int n = min(a.size(), b.size()); T f = 0, g = 1, h = 1, k = 0; for (int i = 0; i < n; i++) { T c = min(a[i], b[i]); tie(f, g, h, k) = child(c, f, g, h, k, i & 1); if (a[i] != b[i]) break; } return make_tuple(f, g, h, k); } // p/q の祖先であって深さが d のノード (f, g, h, k) を返す // 存在しなければ (0, 0, 0, 0) template<class T> tuple<T, T, T, T> ancestor(T d, T p, T q) { vector<T> a = encode_path(p, q); T f = 0, g = 1, h = 1, k = 0; for (int i = 0; i < (int)a.size(); i++) { T c = min(d, a[i]); tie(f, g, h, k) = child(c, f, g, h, k, i & 1); d -= c; if (d == 0) break; } return d == 0 ? make_tuple(f, g, h, k) : make_tuple(0, 0, 0, 0); } // p/q の子孫の下限 f/g と上限 h/k を返す // p/q に対応する頂点 (f, g, h, k) でもある template<class T> tuple<T, T, T, T> range(T p, T q) { return decode_path(encode_path(p, q)); } // judge: [0, ∞] → {true, false} // judge は単調性を持つ、すなわち、ある実数 α を境に true と false が切り替わる // 分母・分子が max_value 以下の有理数のうち、α に最も近いもの (下側: p/q, 上側: r/s) を返す template<class T> tuple<T, T, T, T> search(const function<bool(T, T)> &judge, const T &max_value) { T p = 0, q = 1, r = 1, s = 0; const bool judge_01 = judge(0, 1), judge_10 = judge(1, 0); assert(judge_01 != judge_10); while (p + r <= max_value && q + s <= max_value) { const bool judge_now = judge(p + r, q + s); const bool isleft = judge_now ^ judge_01; T maxd; if (isleft) maxd = p == 0 ? (max_value - s) / q : min((max_value - r) / p, (max_value - s) / q); else maxd = s == 0 ? (max_value - p) / r : min((max_value - q) / s, (max_value - p) / r); T dl = 1, dr = 2; while (dr <= maxd) { auto [np, nq, nr, ns] = child(dr, p, q, r, s, isleft); if (judge(np + nr, nq + ns) != judge_now) break; dl = dr, dr = min(2 * dr, maxd + 1); } while (dr - dl > 1) { T dm = dl + (dr - dl) / 2; auto [np, nq, nr, ns] = child(dm, p, q, r, s, isleft); if (judge(np + nr, nq + ns) == judge_now) dl = dm; else dr = dm; } tie(p, q, r, s) = child(dl, p, q, r, s, isleft); } return make_tuple(p, q, r, s); } } // 使用例 (https://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1208) int main() { while (true) { int p, n; cin >> p >> n; if (p == 0 && n == 0) break; const function<bool(int, int)> judge = [&](int x, int y) -> bool { return y * y * p < x * x; }; auto [u, v, x, y] = sbt::search(judge, n); cout << x << "/" << y << " " << u << "/" << v << endl; } }
[1] Stern–Brocot tree - Wikipedia
競プロでは「周期性」を利用して解く問題がたまに出ます。例:
長さ の数列 が与えられる()。 から始めて、 という置き換えを 回行ったときの の値を求めよ。
漸化式 で定まる数列 に対し、 を求めよ。
難しくはないのですが、毎回書くと意外と頭を壊しがちなので、抽象化してライブラリにしてみました。この記事ではそのライブラリについて書きます。なお、問題の解法自体は既知として進めます。
これらの問題は次のように抽象化できます。
モノイド およびインデックスの集合 があり、各インデックス にはデータ が対応づけられている。また、インデックス列 が漸化式
で定まっている。これについて、先頭からの区間積
を求めるクエリを処理したい( がクエリで与えられる)。
これは、
となる が存在する(特に最小のものをとる)。つまり、インデックスは ρ のような形で周期性を持ち、非循環部分の長さが 、循環部分の長さが である
に対して冪 を高速に求められる(簡単のため、この冪演算およびその他必要な演算は で行えるものとする)
という条件のもとで、前計算 、クエリ で処理できる。
「冪が高速に計算できるモノイド」である必要があるのは、( が大きいとき)
と変形して計算するためです*1。クエリ にできるのは、クエリで計算する区間積が スタートのものと スタートのものしかなく前計算できるためです。
Period<Index, Data, next_index, get_data, op, e, power> pe(start);
*2
Index
(演算子 !=
が定義されている必要があります)Data
Index next_index(Index x)
Data get_data(Index x)
Data op(Data a, Data b)
Data e()
Data power(Data a, long long n)
を定義する必要があります。start
は最初のインデックス です。
Data query(long long K)
を返します。
既に訪れたかどうかを配列で管理するのが最も思いつきやすいと思いますが、Index
の型によっては連想配列を用いる必要が出てきてしまいます。フロイドの循環検出法 *3 を用いることでこれを回避しています。
限界高速化とかはしていません。
#include <iostream> #include <vector> using namespace std; template <typename Index, typename Data, Index (*next_index)(Index), Data (*get_data)(Index), Data (*op)(Data, Data), Data (*e)(), Data (*power)(Data, long long)> struct Period { private: vector<Data> dat, prod_head, prod_tail; int lambda, mu; Data head, tail; public: Period(Index start) { Index a = start, b = start; do { dat.emplace_back(get_data(a)); a = next_index(a); b = next_index(next_index(b)); } while (a != b); mu = dat.size(); Index c = start; while (a != c) { dat.emplace_back(get_data(a)); a = next_index(a); c = next_index(c); } lambda = (int)dat.size() - mu; prod_head.resize((int)dat.size() + 1); prod_tail.resize((int)dat.size() + 1); prod_head[0] = e(), prod_tail[lambda] = e(); for (int i = 1; i < (int)prod_head.size(); i++) prod_head[i] = op(prod_head[i - 1], dat[i - 1]); for (int i = lambda + 1; i < (int)prod_tail.size(); i++) prod_tail[i] = op(prod_tail[i - 1], dat[i - 1]); head = prod_head[lambda]; tail = prod_tail[lambda + mu]; } Data query(long long k) { if (k <= (int)dat.size()) return prod_head[k]; Data mid = prod_tail[lambda + (k - lambda) % mu]; return op(op(head, power(tail, (k - lambda) / mu)), mid); } };
漸化式 で定まる数列 に対し、 を求めよ。
をインデックス にして、 は をそのまま返すようにします。二項演算 は整数の足し算 にすればよいです。このとき power
は掛け算(個数倍)になります。
using Index = long long; using Data = long long; int M; Index next_index(Index x) { return x * x % M; } Data get_data(Index x) { return x; } Data op(Data a, Data b) { return a + b; } Data e() { return 0; } Data power(Data a, long long n) { return n * a; } int main() { long long N; int X; cin >> N >> X >> M; Period<Index, Data, next_index, get_data, op, e, power> pe(X); cout << pe.query(N) << endl; }
長さ の数列 が与えられる()。 から始めて、 という置き換えを 回行ったときの の値を求めよ。
各操作の後の の値をインデックス(兼データ)にします。二項演算 op
がややトリッキーです。直感的には「最も右のもの」としたいですが、このままだと単位元がないので困ってしまいます。単位元 e
を「意味のないデータ」にしておいて、op
は「意味のあるデータのうち、最も右のもの(なければ『意味のないデータ』)」を返すようにするとよいです。
using Index = int; using Data = int; vector<int> A; Index next_index(Index x) { return A[x]; } Data get_data(Index x) { return x; } Data e() { return -1; } Data op(Data a, Data b) { return b == e() ? a : b; } Data power(Data a, long long) { return a; } int main() { int N; long long K; cin >> N >> K; A.resize(N); for (int i = 0; i < N; i++) { cin >> A[i]; A[i]--; } Period<Index, Data, next_index, get_data, op, e, power> pe(0); cout << pe.query(K + 1) + 1 << endl; }
JAG 夏合宿 2023 に参加した。
ICPC のチームは Magical Fish で、メンバーは自分と magsta, confeito の 3 人。今回 confeito 君に合宿に誘われたので行くことにした。(本当は 3 人で行きたかったけど magsta 君が来れなかったみたいで残念)
ぎりぎりに家を出たら電車が遅延して 10 分遅刻(すみません……)。
チームのもう 1 人として kumakuma さんに加わっていただいた。戦略としては、kumakuma さんは英語がかなり得意らしいので長めの問題文担当 + US 配列使いらしいので基本的に実装はしないで自分か confeito 君がやる、という感じになった。
confeito 君が A を読む。自分は B を読もうとしたが長そうだったので、kumakuma さんと交代して C を読むことにした。C を読むと一瞬で解けた(というか ABC で既出なのを思い出した)ので書いてすぐ通る。A も簡単らしいので書いてもらい、すぐ通る。
B は大変らしいということで、シンプルそうな E を読む。一瞬むずそうに見えたけど最適戦略が貪欲っぽい? ということで 2 人に相談、正しそうということになる。細かい解法がすぐに出てこなかったが kumakuma さんが二分探索でできるというのを出してくれて、たしかにとなる。実装して AC。後で考えたら(ソート後は)差分更新で線形でいけるけど、にぶたんのほうがバグらせにくそう。
その後は F ができそうという話になる。DAG の最大パスは EDPC にあって、一般の有向グラフだと最大パスは解けないけど今回の問題は「ウォークを取ってきてそこに含まれる distinct な頂点の数」という解釈になる(はず)なので強連結成分の中を好きなだけぐるぐる回れるから、SCC して頂点に重みがついた DAG とみなした後同様の DP で解ける。とすると出次数が 以下という制約は何に使うんだ? と一瞬疑ったけどそのまま書き始めてしまった(これがよくなかった)。kumakuma さんの蟻本から SCC を写経(蟻本が昔の本すぎて範囲 for すら使ってなくてやや困った)、DP はメモ化再帰が楽そうだからそれをすることにして書き終えた。サンプルも試して提出すると TLE、よく見たらメモ化になっていない。提出、TLE。この辺で と TL 1 sec に気づく。やめてくれ〜 入力を scanf にしたけどだめで、そもそも SCC で再帰 2 回するのが重くて出次数 以下を活かした定数倍がより高速な解法が必要なのでは? という話になって一旦後回しに。
K が解けるらしい。問題概要と解法(DP)を kumakuma さんから聞く。えそれ DP じゃだめでダイクストラでは? と一瞬なったけど話し合ったところ自分が勘違いしていて、DP であっている。サッと実装して AC。
confeito 君が G 解けたというので実装を任せて、kumakuma さんと 2 人で F を詰める。出次数 以下なことからサイクルから辺が出ていくことはない、よって普通に DP した後サイクルだったらサイクル長を足した値を考えればよさそうで、queue を使うサイクル検出を DP しながらやる、という解法が生えた。定数倍がまだ不安だが、これ以上のものは出なかった。G の実装がまだ終わらないらしいので順位表で解かれている J も見る。既視感があって、ABC に 想定で既出 + 誰かが で解ける旨を言っていたような気がするところまで思い出したけど、肝心の 解法を知らず、ちょっと考えたけどよくわからず、諦めた。
G サンプルが合わないらしい。問題概要と解法を聞くとかなり茨の道なことをやっていて、さらに勘違いがあったらしく MLE しそう。一旦諦めて F をやることに。実装して出したけど TLE、グラフを持つのが vector<vector<int>>
ではなく vector<int>
でいい、とかもやったけどだめだった。結局 ACEK の 4 完で 17 位、激冷え。
G は DP の値に First / Second / Draw を持たせることばかり考えていたけど、実は普通に得点の差を評価値にしてよくてこれで状態数が削れる。これは気づくべきだった……。
F は confeito 君によると JOI 典型らしく、じゃあ F と G の担当逆にすべきだったねという話になった。サイクルが高々 1 個なのを利用するの、なるほどね。(しかしそこまでの定数倍高速化を要求するのはちょっと不快だなあという気持ちもあり)
夕食をとったり解説を聞いたりした後は部屋で交流した。入浴を済ませた後談話室で ABC に出た。机が低かったり他の団体の声が響いたりしてあまり集中できなかった。SSRS さんのタイピングが速すぎてびびっていた。
部屋に戻って、作問の話とかをしつつ就寝。
1 日目チームを組んでいただいた kumakuma さんは 2 日目からソロ参加(元々ソロ参加の予定だったが 1 日目 PC を忘れたらしい)ということで、2 日目は漁師さんとチームを組んでいただいた。
昨日の反省を活かして、ある程度難しい問題は実装を始める前に人に相談しようということにした。
A を見るといかにも FPS 使いそうな 998 で、自分がやることに。ざっと眺めると他にも 998 がいっぱいあって、これは modint 書いたほうがよさそうという話になる。漁師さんに modint を写経してもらっている間に A が解けて AC。
順位表で解かれている C を見る。簡単。confeito 君と 2 人で詰めて AC。
confeito 君が B を、漁師さんが D, E を読んでいて、両方からいろいろ聞いた。confeito 君によると B はいけそうらしいので D, E を考えるけどよくわからず。
順位表で解かれている H を読んで問題概要を伝えると confeito 君から天才考察が返ってくる。コンビネーションを H でも B でも使うっぽいので H の実装ついでに写経する。あとは和が で積が な 2 数のパートだけちょっと詰めて実装、AC。
confeito 君が B を実装している間に順位表で解かれている K を漁師さんと 2 人で考える。超頂点作ってダイクストラかなーとかいろいろ考えたけどよくわからず。B を通して戻ってきた confeito 君(FA、すごい!)に助けを求めると、値に の配列を持つ DP をすると遷移回数がいい感じに抑えられるらしい。たしかにじゃん。実装してもらって AC。この人天才か?
このあたりで順位がだいぶよかった。順位がいいということは逆に解く問題の見極めを順位表に頼れないということでもある。残っている問題で唯一 AC が出ている F を考える。括弧列まではすぐ言い換えられて、カタラン数の証明が思い出せればちょっと進展がありそうだと思ったけど思い出せなくて苦しんでいた。(実際はこれがわかっても解けなかった気がする)
J に AC が出る。読むとゲーム。通したのは上位チームではない。とすると実験エスパーで解けるやつじゃねということになって、漁師さんが実験を書いてくれた。自分は F を考え続けるけど進展なし。
実験が書けたらしいので結果を見にいく。長方形に A, B をプロットすると見やすいという話になって(ここ地味に偉い)、見ると大きいところでは規則性がありそう。 と がコーナーなので なら愚直、そうでなければ規則性、のコードを漁師さんが書いて提出。WA。実験結果を とかじゃなくて とかで表示すると、 のケースが処理できていないことに気づく。ここを自分が詰めて書いてもらって AC。
F を考え続けるけど進展がない。
confeito 君が L 解けた気がするらしい。聞くと MCF でできそうとのこと。これを正しいと思ってしまった(実際は嘘だったけど、指摘できなかった。とてもよくない)。漁師さんに kactl の MCF を写経してもらうと、謎の機能を使っていてコンパイルが通らない。こうならないように持ち込むライブラリはちゃんと把握しておくべきですね。Gifted Infants の MCF を見ると変なことはしていなさそうでコンパイルが通った。いざ実装、というところで confeito 君が解法の誤りに気づく。聞くとたしかに嘘だ。残り 10 分くらい、ここから立て直すのは厳しい。結局 ABCHJK の 6 完で 5 位。だいぶいい順位。この日は confeito 君がかなり強かったという印象。
解説を聞くと F も L も激ヤバでびっくり。チャンスがあったとしたら D, E なのかなあ。まあ今の実力だとこの 6 完が限度という気もする。
談話室でボドゲをした後、部屋で ARC に出た。B が通せなくて激冷え・青落ちで険しい気持ちになった。
その後談話室で Kite_kuma さんと将棋を指した。tokusakurai さんとも指したかったが時間がなくて残念。また機会があればよろしくお願いします。
部屋では 1 行ずつ作問をして遊んだ。滅茶苦茶な問題になっても面白いし、それに混じってたまに良さげな雰囲気の問題が生えても面白いので、レクとしていいかもしれない。nonon さんの参加記 に問題が載っているのでよければ考えてみてください。
3 日目はチームが組めず、2 人でやることになった。
confeito 君が A を、自分が B を読む。A は簡単ですぐ通る。B も簡単なはずだが、実装にちょっと手間取ってしまった。
C の概要を聞く。「過半数が同じ」が条件ということの正当性を 2 人で確認。confeito 君曰くこういう数え上げは自信ないらしいので自分が考えることに。その間 I を考えてもらう。
C を立式すると解けた気になる。998 が他にもあるので modint とコンビネーションを写経して(結果的には modint 書くべきだったかは怪しい)、実装。この modint を普段使いしていないせいでなぜかコンパイルが通らなかったりして手間取る。実装すると合わない。よく確認すると立式が間違っていることに気づく。この式だと今の解法では解けないので一旦後回しに。
I を考える。いろいろ議論すると (
を prique にためて )
がきたら「まだペアになっていない (
と組ませる」「もうペアになった )
と交代」のどっちかをやるという感じになりそう。たぶん正しいとは思ったがちょっと自信がなかった。confeito 君が実装。
その間に順位表で解かれている J を見るとド典型。C とか I より先にこっちやればよかった。
I を提出、WA。実装に細かいミスがあるのか方針がそもそも嘘なのかよくわからないので、コードを印刷してリカバリしてもらっている間に J を実装。実装中に DP だとうまくいかなくて 01BFS にする必要があることに気づく。実装重くないか心配されたけど、B でも BFS をやったこともあり大丈夫だった。AC。この間に I のバグに気づいてもらって I もすぐ AC。
あとは C, E と心中するしかないかなという感じ。C はあれから進展がなく、E を 2 人で考える。valid な順番だと区間を広げていく感じになることに confeito 君が気づいて、それを逆から見て立式すると 左から 個、右から 個取ったときのなんかの積の総和 という感じになりそう。あとはこれが線形になればいいんだけど、わからない。自分は C と E を行ったり来たりしたが、最後までわからなかった。結局 ABIJ の 4 完で 14 位、激冷え 2。
2 人だと 3 人のときに比べて明らかに思考が凝り固まっているのを感じた。やっぱり 3 人いるべきだなあ。
C 分割統治はなるほどなあ。ただもしコンテスト中に通すなら二項係数こねこねをしないといけなかった気がする。
E はもっとうまい考え方をすると自然に線形になるっぽい。2 乗を高速化することに固執しすぎたのがよくなかったかも。
コンテスト時間が 3 日間合計で 3 + 5 + 5 (+ 1.67 + 2) = 16.67 時間というとても濃い合宿で、競プロモチベがとても高まった。来年は絶対に国内予選を通過したい。
最後になりますが、このような素晴らしい会を運営してくださった JAG の皆様や関係者の方々、また交流していただいた皆さん、誠にありがとうございました。