えびちゃんの日記

えびちゃん(競プロ)の日記です。

PAST #5 J - 長い長い文字列

問題ページ

nagai1mojiretsu

公式解説では後ろからやっていますが、前から解いたので書いておきます。 計算量は公式解説より少し悪化しています。

解法

いままで出力した文字数を y とします(初期値は 0)。 下記のように、順にシミュレートしていきながら y を更新します。

英小文字を読んだときに x == y+1 となったらその文字を出力して終了です。 数字を読んだときに x <= (d+1) * y となったら、xx % y or ya or b は、a0 なら b、そうでなければ a)で置き換えてシミュレーションを最初からやり直します。

上記の条件を満たさなければ、英小文字なら y += 1、数字 d なら y += d * y で更新します。

計算量解析

x = x % y で更新されるときは x が半分以下になります(有名事実)。 x = y で更新されるときは、x % y == 0(更新の条件)かつ x > y(シミュレーション中のループ不変条件)から、x >= 2 * y なので、やっぱり半分以下になります。

シミュレーションをやり直すたびに x が半分以下になることから、計算量は \(O(N\log(X))\) です。

実装

Python での例を挙げてみます。

def main():
    s = input()
    x = int(input())

    while True:
        y = 0
        for c in s:
            if c.islower():
                if x == y+1:
                    print(c)
                    return
                y += 1
            else:  # c.isdigit()
                d = int(c)
                if x <= (d+1) * y:
                    x = x % y or y
                    break
                y += d * y

main()

sum of floor of linear の解説するよ

特に Advent Calendar は関係ない 12 月 13 日の記事です。

これの解説です。

atcoder.jp

以下の二つを参考にしながら考えたことをまとめます。

qiita.com

数式が多めです。もしかして MathJax から KaTeX に移行するべきですか?

(追記 2021/01/22:\(\KaTeX\) に移行しました)

問題概要

一次関数 \(f(x) = ax+b\) の出力を \(m\) で切り捨て除算した値を \(x\in[0, n)\) の整数について足したものを求めてね。

すなわち、以下で定義される \(g(n, m, a, b)\) を出力してね。 \[ g(n, m, a, b) = \sum_{i=0}^{n-1} \left\lfloor\frac{ai+b}{m}\right\rfloor = \sum_{i=0}^{n-1} \left\lfloor\frac{f(i)}{m}\right\rfloor. \]

重要な観察

\(g(n, m, a-m, b)\) と \(g(n, m, a, b-m)\) について調べてみます。 \[ \begin{aligned} g(n, m, a-m, b) &= \sum_{i=0}^{n-1} \left\lfloor\frac{(a-m)\cdot i+b}{m}\right\rfloor \\ &= \sum_{i=0}^{n-1} \left\lfloor\frac{ai+b-im}{m}\right\rfloor \\ &= \sum_{i=0}^{n-1} \left(\left\lfloor\frac{ai+b}{m}\right\rfloor -i\right) \\ &= g(n, m, a, b) - \frac{n(n-1)}{2}. \end{aligned} \] \[ \begin{aligned} g(n, m, a, b-m) &= \sum_{i=0}^{n-1} \left\lfloor\frac{ai+(b-m)}{m}\right\rfloor \\ &= \sum_{i=0}^{n-1} \left(\left\lfloor\frac{ai+b}{m}\right\rfloor -1\right) \\ &= g(n, m, a, b) - n. \end{aligned} \]

さて、これによって以下のこともわかります(所望の回数だけ上記を適用する)。 \[ \begin{aligned} g(n, m, a\bmod m, b\bmod m) &= g(n, m, a, b) - \left\lfloor\frac{a}{m}\right\rfloor \cdot \frac{n(n-1)}{2} - \left\lfloor\frac{b}{m}\right\rfloor \cdot n. \end{aligned} \]

すなわち、\(0\le a\lt m\) および \(0\le b\lt m\) に帰着できます。 あとは、これについて考えていきます。

考察

方針を先に書くと、小さいケースに帰着させて再帰的に解くことを目指します。 ベースケースは、\(g(0, m, a, b) = 0\) です。

いわゆる contribution technique とか 主客転倒 とか呼ばれているテクニックで総和をバラしていきます。ある値を見たときに、それが何回総和に寄与するかを数えるやつです。 \(\gdef\Y{y_{\text{max}}}\)

\(\Y = \lfloor f(n-1)/m\rfloor = \lfloor (a(n-1)+b)/m\rfloor\) とおきます。

(追記 2020/12/29:あるいは \(\Y = \lfloor f(n)/m\rfloor\) とおきます。両方試してみましょう)

\[ \begin{aligned} g(n, m, a, b) &= \sum_{i=0}^{n-1} \left\lfloor\frac{f(i)}{m}\right\rfloor \\ &= \sum_{j=1}^{\Y} \sum_{\substack{0\le i\lt n\\ \lfloor f(i)/m \rfloor=j}} j. \\ \end{aligned} \]

ここで、\(k_j\) を \(\lfloor f(i)/m\rfloor = j\) なる最小の \(i\) とおく*1と、次のように書けます。

(追記 2021/01/10:行間を少しうめました)

\[ \begin{aligned} g(n, m, a, b) &= \sum_{j=1}^{\Y} \sum_{\substack{0\le i\lt n\\ \lfloor f(i)/m \rfloor=j}} j \\ &= \sum_{j=1}^{\Y-1} j\cdot(k_{j+1}-k_j) + \Y\cdot(n-k_{\Y}) \\ &= \sum_{j=1}^{\Y-1} j\cdot k_{j+1} - \sum_{j=1}^{\Y-1} j\cdot k_j + \Y\cdot n - \Y\cdot k_{\Y} \\ &= \sum_{j=2}^{\Y} \,(j-1)\cdot k_j - \sum_{j=1}^{\Y-1} j\cdot k_j + \Y\cdot n - \Y\cdot k_{\Y} \\ &= \sum_{j=2}^{\Y} \,(j-1)\cdot k_j - \sum_{j=1}^{\Y} j\cdot k_j + \Y\cdot n \\ &= \sum_{j=2}^{\Y} \,(j-1)\cdot k_j - 1\cdot k_1 - \sum_{j=2}^{\Y} j\cdot k_j + \Y\cdot n \\ &= - 1\cdot k_1 - \sum_{j=2}^{\Y} - k_j + \Y\cdot n \\ &= \sum_{j=1}^{\Y} -k_j + n\cdot\Y. \end{aligned} \]

さて、\(k_j\) を求めます。 定義から、以下が成り立ちます。

\[ \left\lfloor\frac{f(k_j-1)}{m}\right\rfloor \lt j = \left\lfloor\frac{f(k_j)}{m}\right\rfloor \le \frac{f(k_j)}{m}. \]

ここで \(j \le f(k_j-1)/m\) と仮定すると、\(j\) は整数なので \(\lfloor f(k_j-1)/m\rfloor \ge j\) となり、\(\lfloor f(k_j-1)/m\rfloor \lt j\) に矛盾します。 よって、以下を得ます。

(追記 2021/01/10:2 行目から 3 行目は、左と右の不等式をそれぞれ \(k_j\) について変形してまとめます。)

\[ \begin{aligned} \frac{f(k_j-1)}{m} \lt j \le \frac{f(k_j)}{m}; \\ \frac{a(k_j-1)+b}{m} \lt j \le \frac{ak_j+b}{m}; \\ \frac{mj-b}{a} \le k_j \lt \frac{mj-b}{a}+1; \\ k_j = \left\lceil \frac{mj-b}{a} \right\rceil. \end{aligned} \]

あとは、これを元の式に入れてがんばります。 \(\lceil x\rceil = -\lfloor -x\rfloor\) を使ったり、添字を入れ替えたりします*2

\[ \begin{aligned} g(n, m, a, b) &= \sum_{j=1}^{\Y} -k_j + n\cdot\Y \\ &= n\cdot \Y + \sum_{j=1}^{\Y} - \left\lceil \frac{mj-b}{a} \right\rceil \\ &= n\cdot \Y + \sum_{j=1}^{\Y} + \left\lfloor \frac{-mj+b}{a} \right\rfloor \\ &= n\cdot \Y + \sum_{j=0}^{\Y-1} + \left\lfloor \frac{-m(\Y-j)+b}{a} \right\rfloor \\ &= n\cdot \Y + \sum_{j=0}^{\Y-1} + \left\lfloor \frac{mj+b-m\cdot\Y}{a} \right\rfloor \\ &= n\cdot \Y + g(\Y, a, m, b-m\cdot\Y). \end{aligned} \]

より小さいケースに帰着できました。 念のため、小さくなっていることを確認しておきます。\(0\le a\lt m\)、\(0\le b\lt m\)、\(1\le n\) に注意です。

(追記 2021/01/10:ここの議論は、\(\Y = \lfloor f(n)/m\rfloor\) のときは少し変わると思います。)

\[ \begin{aligned} n - \Y &= n-\left\lfloor\frac{a(n-1)+b}{m}\right\rfloor \\ &\ge n - \frac{a(n-1)+b}{m} \\ &= \frac{mn-an+a-b}{m} \\ &\gt \frac{mn-an+a-m}{m} \\ &= \frac{(m-a)(n-1)}{m} \ge 0; \\ n &\gt \Y. \end{aligned} \]

ここで終わってもいいのですが、もう一工夫です。

上の重要な観察のところで示したように、第 4 引数から第 2 引数を好きな回数だけ引いた値も計算できるので、それをしてみます。 具体的には、\(-(n-1)\) 回だけ引きます。

\[ g(\Y, a, m, a(n-1)+b-m\cdot\Y) = g(\Y, a, m, b-m\cdot\Y) + (n-1)\cdot\Y. \]

すなわち、次の式を得ます。\(\Y = \lfloor f(n-1)/m\rfloor\) を思い出しましょう。

(追記 2021/01/10:\(x\bmod y = x - \lfloor x/y\rfloor\cdot y\) も使います。)

\[ \begin{aligned} g(\Y, a, m, b-m\cdot\Y) &= g(\Y, a, m, a(n-1)+b-m\cdot\Y) - (n-1)\cdot\Y \\ &= g(\Y, a, m, f(n-1)-m\cdot\lfloor f(n-1)/m\rfloor) - (n-1) \cdot \Y \\ &= g(\Y, a, m, f(n-1)\bmod m) -(n-1)\cdot\Y. \end{aligned} \]

これを元の式に戻してゴールです。

\[ \begin{aligned} g(n, m, a, b) &= n\cdot \Y + g(\Y, a, m, b-m\cdot\Y) \\ &= n\cdot \Y + g(\Y, a, m, f(n-1)\bmod m) -(n-1)\cdot\Y \\ &= \lfloor f(n-1)/m\rfloor + g(\lfloor f(n-1)/m\rfloor, a, m, f(n-1)\bmod m). \end{aligned} \]

\(\lfloor f(n-1)/m\rfloor\) と \(f(n-1) \bmod m\) は、コンパイラを信じれば除算命令 1 回でやってくれると思います。

実装

fn floor_sum(n: i64, m: i64, mut a: i64, mut b: i64) -> i64 {
    let mut res = 0;
    if a >= m {
        res += n * (n - 1) / 2 * (a / m);
        a %= m;
    };
    if b >= m {
        res += n * (b / m);
        b %= m;
    };

    let last = a * (n - 1) + b;
    if last >= m {
        let last_div = last / m;
        let last_mod = last % m;
        res += last_div + floor_sum(last_div, a, m, last_mod);
    }
    res
}

適当なループをして非再帰に書き直すことは可能だと思いますが、このくらいならコンパイラに任せた方がいいかもしれません。 ループにしても大して速くならず、無駄に可読性を落とすことが予想されます。

(追記:2020/12/16)

よく考えると、last のところは a * n + b にできて、そうすると last_div を足す必要がなくなって、次のようにできます。 \(\Y\) や上の式変形ででてきた \(n-1\) を \(n\) に置き換えてよいので。

fn floor_sum(n: i64, m: i64, mut a: i64, mut b: i64) -> i64 {
    let mut res = 0;
    if a >= m {
        res += n * (n - 1) / 2 * (a / m);
        a %= m;
    };
    if b >= m {
        res += n * (b / m);
        b %= m;
    };

    let last = a * n + b;
    if last >= m {
        res += floor_sum(last / m, a, m, last % m);
    }
    res
}

なんですが、速くなってくれませんでした(以下の提出コードは諸事情で C++ です)。 提出し直したら速くなったりしてわかりません。ジャッジの気分屋さん。

停止性の証明だけ書き直します。(訂正 2021/01/10:やめました)

冷静に考えると、\(n-\Y\gt 0\) を示さなくても、計算量のところに書くように \(a\) と \(m\) の互除法より長くはループしないことがわかるので十分な気もします。

(追記 2021/01/10:負数に対応させる場合は、/ が切り捨て除算でない言語では注意が必要です。)

計算量

(追記:2020/12/14)

\(m, a\) のみに注目すると互除法のようになっていて、最悪ケースでは \(\log_{\varphi}(\min\{m, a\})\) 回程度の再帰呼び出しをします。 互除法が終わったときの \(\Y\) の値を考えると、\(a = 0\) かつ \(b\lt m\) なので、それ以上の呼び出しはしないことがわかります。 \(\varphi\) は黄金比です。

また前述のように、呼び出しごとに \(n\) が少なくとも \(\frac{(m-a)(n-1)}{m}\) 減ります。 互除法の最悪ケースでは \(a/m\) は \(1/\varphi\) くらいになっているので、ざっくり高々 \(\log_{\varphi}(n)\) 回程度再帰呼び出ししそうな気がします。 怪しそう。たすけて〜。

より詳細に見積もる方法を身につけたいです。変数がいっぱいあって大変。

結局、再帰呼び出しは \(\log_{\varphi}(\min\{m, a, n\})\) 回程度で抑えられて、各々は \(O(1)\) 時間なので、全体として \(O(\log(\min\{m, a, n\}))\) 時間だと思います。

big-O だと隠されちゃうけど、それはそれとしてループ回数を詳しく解析するのは楽しそうなので、できるようになりたいです。

\(n\) に関する評価のきもち

(追記 2021/01/10)

Fibonacci 数列を \(\{F_i\}\) とします。

\(a\) と \(m\) の互除法の最悪ケースでは、 \( (F_i, F_{i-1}) \dots, (F_2, F_1), (F_1, F_0)\) のように動きます。 このとき、\(j\) ステップ後の \(n\) は次のようになっているはずです(\(\pm 1\) とかは追々考える必要があります)。

\[ \left\lfloor \frac{F_{i-j}}{F_{i-j+1}} \dots \left\lfloor \frac{F_{i-1}}{F_i}\cdot n\right\rfloor\dots\right\rfloor \approx \left\lfloor \frac{F_{i-j}}{F_i}\cdot n \right\rfloor. \]

\(n\) が十分小さければ、\(j=i\) となる前にこれは \(0\) になるはずです。 \(F_i が \varphi^i\) くらいであることを考えると、\(\log_{\varphi}(n)\) ステップくらいで \(0\) になりそうです。

\(a\) と \(m\) については互除法の議論から \(\log_{\varphi}(\min\{a, m\})\) ステップくらいで \(0\) になるので、これらの \(\min\) のオーダーになりそうです。

おわり

お疲れさまでした。

うれしい

この高速化の PRACL にマージされました。

うれしい 2

yosupo さんが ACL Practice Contest の 解説 に載せてくれました。

*1:\(0\le b\lt m\) より、各 \(1\le j\le \Y\) について \(k_j\) は存在します。

*2:\(j\) の係数を非負にしたいので。

ICPC 2020 夢地区参加記 3

夢地区参加記 2 は これ

今回は設定が特に謎なのですが、夢なのでそういうものだと思います。 一度ボツにしたものの、変にハードルが上がるとよくないので書きます。

起きた後に軽くメモをしているとはいえ、いくらか日が経っているので正確でない記述があるかもしれません*1

ここから夢です。

今回のラウンドは、国内予選の次の月曜日に開催されています。 なぜか TAB がいなくて、monkukui とえびちゃんでチーム名「python」で出ています。

(ここ非夢:そういえば TAB は元々 Python 勢だった気がするし、えびちゃんも元々 Python 勢だった気がします)

コンテスト

AB をすぐ解いて、その後はずっと冷えていました。終了です(??)

コンテスト後

105 位くらいだったので諦めて忘れて過ごしていたところ、自宅に郵送で順位表が送られてきました。 通過チームの欄に☆がついているのですが、自チームのところにはついていませんでした。

monkukui に「なかったねー」と言ったところ、「え、自分のでは通過扱いになってましたよ」とか言われて謎でした(そもそも何に通過しているのかもよくわかりません)。

あと、13 位くらいに大学名「Hokkaido」*2チーム名「」のチームがあって、変でした。

おわり

怪文書。ゆるしてね。

*1:正確ってなに? そもそも正確な記述が要求されていますか?

*2:Hokkaido University とかではない。他のチームは別に省略されていたわけではない。

非再帰セグ木上の任意始点にぶたん

セグ木は知っているとします。 知らない人は えびちゃんのスライド なり適当な記事なりを読んでください。

セグ木上のにぶたんも上のスライドに載っていますが、もう少し解説したいかなと思います。 上のスライドにある図を見るとイメージが湧きやすいかもしれません。

愚直にやると、セグ木の区間*1取得と、二分探索で、ふたつ log がついてしまいます。 セグ木の実装を非再帰でやって、\([l, r)\) の区間和を \(O(\log(r- l))\) でできるとしても、オーダーは落ちません。

参考までに、\(n = 10^6\) とすると、\(n\log(n)^2\) は \(n\sqrt{n}/2.5\) 程度ですが、\(n\log(n)\) は \(n\sqrt{n}/50\) 程度です。 落とせる log は落としておきたい気がします。

この方針以外の実装もある気がしますが、えびちゃん的にはこれが好きなので、とりあえずこれを紹介します。 気づいたらそのうち宗派が変わっているかもしれません。

定義

セグ木の葉の個数を \(n\) とします。 セグ木の元の配列長が \(n\) ということです。 区間 \([l, r)\) の和を \(a[l, r)\) と書くことにします。

左端を固定するとき

以下を満たす述語 \(p\) が与えられるとします。

  • 単位元 \(e\) に対して \(p(e)\) が true
  • ある \(i\) が存在して \(p(a[l, i))\) が false であれば、任意の \(i \lt j \le n\) に対して \(p(a[l, j))\) が false

このとき、\(p(a[l, i))\) が true かつ、次のいずれかを満たす \(i\) を返します。

  • \(i = n\)
  • \(i < n\) かつ \(p(a[l, i+1))\) が false

右端を固定するとき

以下を満たす述語 \(p\) が与えられるとします。

  • 単位元 \(e\) に対して \(p(e)\) が true
  • ある \(i\) が存在して \(p(a[i, r))\) が false であれば、任意の \(0 \le j \lt i\) に対して \(p(a[j, r))\) が false

このとき、\(p(a[i, r))\) が true かつ、次のいずれかを満たす \(i\) を返します。

  • \(i = 0\)
  • \(i \gt 0\) かつ \(p(a[i-1, r))\) が false

補足

一度 false になると、区間を広げても false であり続ける述語を受け取ります。 条件を満たす最大の区間を求めているのに相当します。 両方の場合において、引数と返り値を昇順に \(l, r\) とすると、\(p(a[l, r))\) は true となります。

\(p(e)\) が true であることを要求しているのは、そうしないと返り値がつらくなるためです。 -1 を返すのはあまりうれしくないし、そもそも単位元の時点で false なら、呼び出す前から答えがわかりますし。

なお、これは ACL のものの定義と同じです。

簡単な場合

まずは、セグ木を完全二分木に限定し、二分探索の始点を左端に固定した場合を考えます。 全区間の和を述語に渡した場合は false になるとします(そうかどうかの判定は述語を高々一回呼べばわかる)。

これは、必要な情報を管理しながら、ノードを下っていくことでできます。 必要な情報というのは、「今まで足されている値」「今見ている添字」くらいだと思います。

適当に非再帰で簡単に実装できます。 たとえば、左に潜るなら i <<= 1 のようなことをすればよいです。

auto x = e;  // 今まで足した値。単位元で初期化
while (i < n) {
  i <<= 1;
  auto tmp = op(x, a[i]);
  if (p(tmp)) {
    // 左の子を足しても大丈夫
    x = tmp;  // 足した結果で上書き
    i += 1;
  } else {
    // 左の子を足すとだめ
    // 特に何もしなくていいね
  }
}
return i - n;

一般の場合(左端を固定)

セグ木を完全二分木に限定せず、二分探索の始点を特に限定しない場合を考えます。始点を \(l\) とします。

セグ木で \([l, n)\) の区間和を求める際に見るノードを、左から順に見ていきます。 このノードたちの添字は、非再帰セグ木のループを使うと簡単に求められます(順序には注意)。

必要に応じて自分で図を書いたり、上のスライドの図を見るといいかもしれません。

これらのノードを順に見ていきながら、「ここまで加えると条件を満たさなくなる」という境界を探します。 このノードは \(O(\log(n))\) 個なので、単に線形探索するだけで問題ありません(ここすき)。

さて、そのようなノードが見つかったとします*2。 重要なこととして、あるノードの子たちを見ると、完全二分木になっています。 このことから、上で示した簡単な場合に帰着されるので、\(O(\log(n))\) 時間でできます。

以上を合わせて、\(O(\log(n))\) 時間で答えを求められます。

一般の場合(右端を固定)

上記で左から見ていたところを、右から見るようにするだけです。 off-by-one とかには注意。

遅延セグ木の場合

必要に応じて伝播しながらやると、同じ感じでできます。

おわり

候補が \(O(\log(n))\) 個なので線形探索をしても \(O(\log(n))\) 時間で済むパートすき。

*1:適宜、和はモノイド積と読み替えてください。

*2:見つからなければ全区間が条件を満たします。

α(n) とお近づきになりたい人のための記事

↓ 勉強し直しました ↓

rsk0315.hatenablog.com


union-find の計算量解析で出てくる \(\alpha(n)\) というのがありますね。 Ackermann 関数の逆関数として知られるやつです。

競プロ er の多くは、次のような認識をしていると思います。

  • 計算量に \(\alpha(n)\) が出てくるなら union-find が関係する以外ないと思っている*1
  • なぜ union-find でそうなるかはよく知らない
  • どういう解析で \(\alpha(n)\) が出てくるのかわからない
  • \(\alpha(n)\) がどんな性質を持っているかもあまり知らない
  • 実質定数だと思っている

最近、少しお勉強する機会があったので、紹介してみます。 えびちゃんもまだちゃんと仲よしになったわけではないです*2

\(\star\) 演算子の導入

唐突ですが、\(\star\) 演算子というのを導入します。 関数 \(f(n)\) に対して、次のような漸化式で定義されます。 ただし、\(n > 1 \implies f(n) < n\) とします。

\[ f^\star(n) = \begin{cases} 0, & \text{if }n \le 1;\\ 1 + f^\star(f(n)), & \text{otherwise}. \end{cases} \]

DP に慣れている人は読みやすいと思いますが、「与えられた \(n\) に \(f\) を適用して \(1\) 以下にするために必要な、最小の適用回数」を返します。 例を挙げてみます。

\(f(n)\) \(f^\star(n)\)
\(n-1\) \(n-1\)
\(n-2\) \(n/2\)
\(n/2\) \(\log(n)\)
\(\sqrt{n}\) \(\log(\log(n))\)
\(\log(n)\) \(\log^\star(n)\)

使い道の例

分割統治を例に出します。

サイズ \(n\) の問題があり、これをサイズ \(f(n)\) の問題 \(n/f(n)\) 個に分け、その結果を高々 \(a\cdot n\) 時間で合わせるときの計算量 \(T(n)\) を考えてみましょう。

\[ \begin{aligned} T(n) \le \begin{cases} 0, & \text{if }n\le 1;\\ an + \frac{n}{f(n)}\cdot T(f(n)), & \text{otherwise}. \end{cases} \end{aligned} \]

これを順に展開していくと、次のようになります。

\[ \begin{aligned} T(n) &\le an + \frac{n}{f(n)}\cdot T(f(n)) \\ &\le an + \frac{n}{f(n)}\cdot \left(a\cdot f(n) + \frac{f(n)}{f(f(n))} \cdot T(f(f(n)))\right) \\ &\le an + an + \frac{n}{f(f(n))} \cdot T(f(f(n))) \\ &\le \dots \\ &\le an + \dots + an + \frac{n}{f^k(n)} \cdot T(f^k(n)) \end{aligned} \]

\(n\) に \(f\) を \(k\) 回適用すると \(1\) になるとします。 \(f^k(n) = 1\) のとき、\(T(f^k(n)) = 0\) となることに注意します。

このとき、\(k\) 個の \(an\) があるので、次のように書けることがわかります。

\[ T(n) \le an\cdot f^\star(n). \]

\(\alpha(n)\) の定義

さっきの表の一部を抜粋して再掲します。

\(f(n)\) \(f^\star(n)\)
\(n-2\) \(n/2\)
\(n/2\) \(\log(n)\)
\(\log(n)\) \(\log^\star(n)\)

ある \(f\) に複数回 \(\star\) を適用した場合の挙動が気になりますね。

そこで、\(\alpha_0(n) = n-2\) とし、\(\alpha_{k+1}(n) = \alpha_k^\star(n)\) と定義します。 念のため補足すると、\(n\) に \(\alpha_k\) を \(\alpha_{k+1}(n)\) 回適用すると初めて \(1\) 以下になるということです。

\[ \begin{aligned} \underbrace{\alpha_k(\alpha_k(\dots(n)\dots))}_{\alpha_{k+1}(n)-1\text{ times}} \gt 1,\\ \underbrace{\alpha_k(\alpha_k(\alpha_k(\dots(n)\dots)))}_{\alpha_{k+1}(n)\text{ times}} \le 1.\\ \end{aligned} \]

そして、\(\alpha(n)\) を次のように定義します*3。 \[ \alpha(n) = \min\{k: \alpha_k(n)\le 2\}. \]

\(\alpha_k(n)\) が \(2\) 以下になるような最小の \(k\) を \(\alpha(n)\) とします。 \(2\) である理由は、\(n\ge 4\) に対して \(\alpha_k(n)\) の最小値が \(2\) であるためです。

値の求め方がわかったことで、以前よりは \(\alpha(n)\) が身近に感じられてきませんか? イメージがつかなければ、手などで計算してみてください。

資料の紹介コーナー

書くのが大変になってきたので、資料の紹介コーナーに移ります。 英語のものばかりですが、英語自体は難しくないと思います。

わかりやすいスライド

  • 図がたくさんある
  • 静的な区間モノイド積が \(\langle O(n\alpha(n)), O(\alpha(n))\rangle\) で解ける話が書いてある
    • disjoint sparse table は \(\langle O(n\log(n)), O(1)\rangle\)
    • \(\langle O(n), O(\alpha(n))\rangle\) でできるらしい
  • union-find の計算量解析も載っている

各種性質などの証明

  • 上の定義とは少し異なる \(\alpha(n)\) が用いられている(後述)
  • \(\alpha(n) = o(\alpha_k(n))\) などが載っている

Weak Epsilon-Nets, Davenport–Schinzel Sequences, and Related Problems

  • 上のリンク先の記事を書いた人の D 論っぽい
  • 1 章と Appendix A に \(\alpha(n)\) の話がある
  • \(\star\) を用いて定義した \(\alpha(n)\) が Ackermann 関数の逆関数になるのは、帰納的に簡単に示せると書かれている
  • \(\alpha(n)\) の亜種(定義が若干異なる)はいくつかあるが、漸近的には同じであることを示す手法が書かれている
  • この辺よくわかってません

定義の異なるやつに関する質問

  • あとでよむ

おわり

ほぼ丸投げ記事ですが、興味を持つ人がいてくれたらうれしいかなと思います。

*1:競プロの文脈なら概ね正しいとは思います。

*2:まだちゃんって誰?ではないです。

*3:Ackermann 関数に直接触れていませんが、それについては後述します。

PAST #4 参加記

考えていたことなどを書きます。

tl; dr

受検前

有料期間中に受けるかどうか考えました。

前提として、既にエキスパートの認定証を持っているので、以下のような葛藤が生じます。

エキスパートを取れた場合:

  • 少しの間、承認欲求が満たせる
  • 数ヶ月ぶんエキスパートの有効期間が延びる

エキスパートを取れなかった場合:

  • それなりのお金を払って無を取得できる*1
  • 気持ちがめちゃくちゃになる

そもそも、値段が安くはないので、気軽には払えません。 お布施という意味では払う方がいい気もするんですけど。

というのがあって、悩んだのですが、無料になってからうけることにしました。

問題公開日

なかなか公開されません。

ツイートをするとすぐ公開されてびっくりしました。

URL をエスパーしてコンテストページでリロードして待っていたのですが、PAST 過去問 の一覧に反映されたのは問題公開より後だった気がします。

はじまるよ

一覧を見ると「構文解析」と書いてあったので、うれしくなりつつも「どうせ構文解析要素は薄いんだろうな」と思いました。

A

3 つの要素の中央値は「最大値と最小値でないもの」なので、以下のように求めました。

let min = a.min(b).min(c);
let max = a.max(b).max(c);
let med = min ^ max ^ a ^ b ^ c;

総和から引いてもいいと思います。

B

特になし。頭が無だったので "ERROR" ではなく "0" とか書いていました。

C

特になし。

D

特になし。

この辺で、作業用 BGM を準備しようと思って(ナメていませんか)、前によく見ていた動画を探そうとしたのですが、見つかりません。 少し調べると、投稿者が動画もアカウントも全て消していたことが判明し、かなり動揺しました。これを書いている今も引きずっています。

E

Rust は標準に順列を生成するのがないので、itertools を使いました。 そういうのがない言語だとどうするのがいいのでしょう。

ここでは長さ \(N \le 5\) の順列を全て生成すればいいので、「\(N\) 進数 \(N\) 桁を全部つくり、各桁が distinct なもののみ見る」とか? よく考えると、この問題では \(26^N\) 通り全部作って判定するのが楽かもしれません。

動揺のせいか、D のコードを投げて全ケース RE になっていました。 PAST #1 でも同じことをした記憶がありますね。

F

特になし。

G

動揺のせいか、「. が見つかったら . に変えて、判定を終えたら # にする」という謎を実装していました。

判定用に union-find を使っていたのですが、自作ライブラリ用の bundler がバグっており、その直しに数分を要しました(あーあ)。

H

特になし。

これ、どう線形で解くんでしょう。

I

尺取りでもいいと思うのですが、にぶたんでもいいと思ったのでにぶたんをしました。 円環は 2 周持つのがセオリーだと思いますが、変に配列外参照をしたりすると嫌なので、念のため 3 周持ちました。

この辺で「あれ? そういえば構文解析たのしみだった。何問題だったかにゃ」と思って、問題一覧を見ると既に AC しており、全てを察してかなり嫌な気分になりました。

J

超頂点を作って Dijkstra をします。

K

\(10^9\) で割った余りとあったので、\(10^9+7\) が MathJax の描画ミスとかで \(10^9\) に見えているのではないかとか疑い、右クリックして確かめました。

転倒数と書いてあるのでセグ木を貼るのかと思いましたが、値が \(20\) 以下とのことなので、そっちで抑えるんだろうなとなりました。

系列二つに対して、「その系列における転倒数」と「各値の出現回数」がわかれば、それらをくっつけた系列に対するそれらの値もわかります。結果として、全体の転倒数もわかります。

これは ACL practice L に似ています。

最初に用意されている系列に対する転倒数と出現回数を求めるパートがありますが、これは「既にできている系列に、長さ \(1\) の系列をくっつける」という操作を考えれば、上の計算方法を流用できることがわかります。

こういうやり方は meldable heap の push を meld でやるやつに似ている気がします。

L

偶奇で分けます。

配列 \(a\) を用意し、クエリ 3(一点加算)によって変化した値を持ちます。 クエリ 1・2 における変化はひとつの変数 \(x\) で持ち、クエリ 1 がきたら足し、クエリ 2 がきたら引きます。

\(a[2i]-a[2i+1]\) の形式で書ける値の個数カウンタと、\(a[2i-1]-a[2i]\) におけるそれを用意します。 出力クエリでは、それらのカウンタで \(x\) や \(-x\) の個数を調べればよく、終わりです。

添字関連でこわれるかなと思ったのですが、一発完動だったのでうれしかったです。

M

なんかこう、賢いをすればいいんだろうなとか、逆順に処理するのが定石だろうなと思いつつ、HL 分解で済ませました。

N

盤面サイズが定数でめずらしい。 サンプルを見ると最大ケースが載っており、いろいろ察します。

値の種類が二つのときの中央値は多数決と同じですね。そういえば最近 MAO の話を聞かない気がします。

包除をするのかな、いや無理かとか思いました。 あと、なぜか縦横がそれぞれ \(60\) くらいだと思っており、「bit DP は無理か〜」と思って棄却しました。

一旦 O を見てしばらく考えて無理になったあと bit DP でいいじゃんとなり、しばらくバグらせるも、なんとか AC できました。 一番上と一番下に 000000 を追加するといい感じになりました。

O

最小費用流?と初手で思って棄却したんですが、冷静に考えるとこれを詰めるべきだったと思います。 区間なのでそれを利用して(スキップするような感じで)辺を張るんでしょうとか思ったのに、なんで辿りつかなかったんでしょうね。

結局、in-place DP っぽく考えたいもののよくわからないとなり、実家になじめていませんねとなりました。

おわり

\(10^9+7\) にしなければ数え上げを出しても ok みたいなのを感じました。

動揺していた序盤と N 以外ではバグらせなかったのはよかったかなと思います。

満点を取るつもりだった人が 94 点で「エキスパートでよかった〜」と言っているの、ダサいですよね。つらい。

*1:厳密には無ではなくて、前のエキスパートが切れてから少しの期間は有効になるものは取得できることが期待されます。

ICPC 2020 国内予選参加記

これは夢日記ではありません*1夢日記これこれ

チームメイトは夢日記と同様、monkukui と TAB とえびちゃんです。 チーム名は tsutaj で、これは去年までチームメイトだった先輩に由来します。

tsutaj 抜きのチームに tsutaj と名付けるの、余因子展開とかで出てくる、行列 \(A\) の \(i\) 行 \(j\) 列抜きを \(A_{ij}\) と書くやつに似ている気がします。

See also: monkukui の参加記TAB の参加記

前日

バチャ をしていました。やや冷え気味ですね。

それからは、有名なライブラリをローカルに落とす遊びをしました(結局使いませんでした)。

去年のことを思い出したりしていました。去年の参加記は これ

当日・本番前

リハーサル

模擬地区のときはリハーサルに出そびれたので、今回はちゃんと出ました。 ちゃんとと言いつつも、AC はしていません。

f:id:rsk0315:20201106204750p:plain
うっかりね

せっかく「まれに見られる悲しい状態」*2になったので、おふざけの画像を生成したりしました。

矢印で表現するやつかわいくてすき。この生物には名前がついているようですね(ふににちゃん?)。

ジャッジ結果の URL を少し改変するといろいろなジャッジ結果を見ることができ、(少なくとも?)20 個あるようです。 プログラムと解答ファイルの両方にプログラムを指定すると以下のような結果になります(ペナなし)。

Program as answer.

You submitted a program source file as an answer file. Enter a file name of an answer file in the box.

両方に解答ファイルを指定すると Correct answer. と言われ、二つ目のケースで困ってしまいます。 たぶん、こういうお助け処理は correct 以外のときだけ行われるのでしょうね。

Binary program. というジャッジ結果もあるようなのですが、C++コンパイルした実行ファイルを提出したり、Java のクラスファイルを提出したりしても Correct answer. になってしまい、条件がよくわかりませんでした。

他にもいろいろ試そうと思っていたんですが、試し終わらないうちにケースを使い切ってしまいました。

そういえば、国内予選のジャッジ結果では、紫と緑が AC 時に出てくる背景色なのですよね。 そのことはわかっているのですが、文字色が赤なので、AC のときも一瞬焦ってしまいます(警告色を感じるので)。

20 種のジャッジ結果たち。 興味ある人がいるかわからないですが、せっかく調べたので載せてみます(載せない方がよかったら消えます)。

Congratulations!

You have successfully finished this problem.

Correct answer.

Proceed to the next data.

Correct answer.

Proceed to the next data.

Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Correct answer, with a program different from the last one.

BAD NEWS: You can no longer finish this problem, because you would need to answer the next data correctly with the same program, yet there are no data left for this problem. Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Correct answer, with a program different from the last one.

Proceed to the next data.

Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Empty answer.

You either specified a wrong file name which does not exist, or an empty file. Enter a correct file name in the box.

Empty program.

You either specified a wrong file name which does not exist, or an empty file. Enter a correct file name in the box.

No answer file.

Enter a file name in the box.

Missing problem.

Select a problem in the list.

No program file.

Enter a file name in the box.

Wrong answer.

Try again.

Wrong answer.

Try again.

You need to correctly answer two more data. Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

Wrong answer.

BAD NEWS: You can no longer finish this problem, because you would need to answer two more data correctly, yet there is only one data left for this problem. Recall that to finish a problem, you need to correctly answer two data in a row, with the same program.

The contest is over.

You can no longer send your answer.

Binary program.

You specified a binary (executable) program or compiled Java class file. Enter a file name of a source program in the box.

Program as answer.

You submitted a program source file as an answer file. Enter a file name of an answer file in the box.

Input as answer.

You submitted an input file as an answer file. Enter a file name of an answer file in the box.

Input as program.

You submitted an input file as a program file. Enter a file name of a program file in the box.

Previous answer.

You submitted an answer to the previous data. You should run your program with a new input file.

Submits in too short time.

The server stopped processing your submit since you made submits in less than 0.5 seconds.

以上 20 種です。

間違ったファイルを指定してしまったとき用のジャッジ結果が用意されていて優しいと思います。 Wrong Answer. 以外はペナがつかないようになっていると思います。

リハーサルおわり

暇なので、バチャをしていました。 肩慣らしに簡単な問題を解こうということで、チームで これ をしました。

えびちゃんが H を AC した後に monkukui が H に RE を投げていたりして面白かったです。 J は苦手そうだったのですが、ひらめいて、書いてすぐ投げました。数秒後に monkukui が WA を投げていて危なかったです。

当日・コンテスト

ざっくりとした戦略は次のような感じです。

  • monkukui が A、えびちゃんが B、TAB が C を見て、各々できたら書く。
  • 構文解析はえびちゃん、幾何は TAB、その他つらそうなのは monkukui に変わる。
  • できるなら一人でやるが適宜話し合う。
  • デバッグが必要になったらえびちゃんに聞く。

A や B でペナを出すとつらいので、油断した実装をしないようにしましょうという話をしたり、「まぁ WA を出しても後半でもう 1 問解いてくれたらいいからね」とか言ったりしました。

問題内容はあまり説明しないので、必要に応じて 問題文 を読んでください。

開始

あの、はじまりません。

no status information for team ###### in contest are found (database broken or not initialized?)

と書かれていたので、たぶん database broken or not initialized だったんだと思います。

「れれれ冷静になっていいい一旦コーチに連絡しましょう」とか言っていました。えびちゃんが一番焦っていたかもしれません。

今度こそ開始

B を読みます。union-find を貼りそうになりますが、必要なくて、std::set で処理して、AC です。 数秒差で A も AC して、5 分くらいで 2 AC です。この時点で 6 位です。

C、え、これ、何するんでしょうね。

D は構文解析らしいので、うれしくなって、読みます。 え、これ、何するんでしょうね。

E は monkukui が解けそうと言ったり、解けてないことがわかったりしました。

正直なところ、2 完でお通夜まであるかなと思いました。 最初は「全チーム高々 2 完で終わるからペナ差で通過でしょ」と思っていたのですが、めちゃくちゃ C が通されていて焦ります。

D は、愚直には階乗通り試せばいいんですが、指数時間に落としたい気持ちです。 階乗モノは bit DP でできがちというのがあったので、変にハマってしまいました。

C が謎なんですが、TAB が愚直を書いて回してくれて、時間は掛かったものの AC してよかったです。 この時点で 68 位とかになっていて、ボーダーギリギリで通過かなと思っていたんですが、どうやら学内 3 位らしく、あーとなりました。

D は TAB と話しながら決め打ちというワードまでは出てたんですが、なかなか時間がかかりました。 解法は以下のような感じです。

最終的な答えとなる変数を決め打ちする。 残った変数について、上記の変数より前に来るか後に来るかを決め打ちする(指数通りある)。 答えの変数には 0、前のものには -1、後のものには +1 を割り振る。 (x<y)min(x, y)(x>y)max(x, y) として計算する。 どちらかの式の値が 0 以外なら寄与分は \(0\)。 両方の式の値が 0 なら valid な順序であり、前のもの・後のものの個数をそれぞれ \(f\), \(b\) とすると、寄与分は \(f!\cdot b!\)。 これを、各決め打ちに対して足し合わせたものが答え。

各順序に対して、それが答えに寄与するのは(ある変数が全体の値となる)1 回だけで、両方の式の値が一致するときのみ加算されるので、うまくいきます。

コンパイルが一発で通り、サンプルも一発で合い、テストケースも二発(国内予選なので)で通り、気持ちよかったです。 頭と手がふわふわしました。 「D、サンプル、通った」「あ、correct って出てる」とか、かなり口が不自由になっていたと思います。

31 位くらいになっていて、さすがに通過したかなと思いました。

TAB が F の概要を説明してくれましたが、頭がふわふわしていたので半分くらいしかわかりませんでした。非常にごめん。

前後関係をあまり覚えていませんが、monkukui が E をがんばっていて、TAB がサポートしたりしていたと思います。 桂馬なので二部グラフになるとか、一つ飛ばしで動くので偶奇で分けていいとか、左右に動く(まっすぐは動かない)のでそこでも半分にできるとか言っていて、賢いなぁとなりました。

monkukui ががんばってデバッグして、えびちゃんが手伝って、結局えびちゃんの PC で計算して、E を AC しました。 この時点で 12 位とかで、夢かと思いました(夢地区ではないです)。

頭がふわふわしていて、Congratulations! を見ながら「あ、E 通ってますね(他人事)」と発言してしまいました。 自分でも意味不明すぎて二の句が継げなかったのですが、想像以上にチームメイトを混乱させてしまったようです(それはそうか)。ごめんなさい。

うっかり「通過したでしょ」と口走ってしまったのですが、ここから弊学の他チーム全てに抜かされると不通過になるはずなので、それらのチームに対して失礼な発言だった気がします*3。ごめんなさい。

F を TAB ががんばっていたもののバグっていそうで、デバッグを手伝うも、うまくいかず、そこで終了しました。

提出した問題については一つも WA を出さなかったのもよかったと思います。

最終的な順位は 18 位でした。

icpcsec.firebaseapp.com

これ、毎年同じ URL なので、来年には見れないんですね。

コンテストおわり

「もう TweetDeck 開いていい?」とか言っていました。やばい人じゃん。

2 完を覚悟したタイミングから各々 1 問ずつ AC していて、喜ばしい気持ちになりました。 あれこれ話し合ったりしてチーム戦っぽさもあったと思います。 特に E は TAB が考察・monkukui が実装・えびちゃんがデバッグ(と提出)をしていて、面白かったです。

今回の国内予選は今までの国内予選で一番気持ちよかったと思います。 理由は以下のものが主です。

  • 4 年連続で通過できたこと
  • 本番中に構文解析を通せたこと
  • 構文解析をバグらせなかったこと
  • 20 位を上回ったこと
  • 5 完できたこと

去年までできなかったことがいろいろできたので、うれしかったです(通過自体はできていたけど 4 年連続は初めてなので)。 三人コーダーのチームなので並列 OK のルールだと有利なのかなとも思いますが、最近の人たちはほとんど三人コーダーだったりするかもしれません。 何にせよ、うちのチームにとってはやりやすかったと思います。

TAB が「あまり仕事できなかった、愚直しか書いてない」と嘆いていましたが、後半 3 問の AC は TAB のおかげだと思います(愚直のやつ含む)。

毎年、終わってしばらくぼんやりしてから「どうやら例年通り通過したらしい」という実感が湧いている気がしますが、今年もそうなりました。

f:id:rsk0315:20201106222615p:plain
自画自讃ではない

tsutaj ありがとう。

おわり

おわりです。やっぱりうれしい。

今年は弊学からは 2 チーム通過はならなかったように見えますが、来年も出場資格がある人はぜひがんばってほしいと思います。

*1:念のためほっぺたをつねっておきました。

*2:ICPC 予選システム ユーザマニュアル にある表現。

*3:それらのチームがここから十分な個数の AC をすることはないと発言しているように聞こえるので。