ABC223H - Xor Query

着想は公式解説とほぼ同じなのでかなり簡潔に記します。

問題概要

atcoder.jp

考えたこと

掃き出し法っぽい操作ができればよい。
A_i を区間 [i, i] であると考えて A(i, i) とする。

(k-1)-bit 目まで全て 0 であるような A(l, r) について、r の小さい順に見ていく。

  • A(l, r) の k-bit 目が 0 → 何もしない
  • A(l, r) の k-bit 目が 1 → 同じく k-bit 目が 1 であるような最も近い A(p, q) とマージ

を行うことで、全ての A(l, r) の k-bit 目を 0 にすることができる。
ここで「最も近い」とは、q ≦ r であるような A(p, q) のうち最も p の大きいものをいう。マージすると値は A(l, r) xor A(p, q)、区間は [min(l, p), r] となる。

これにより、k-bit 目まで全て 0 であるような A(l, r) を新たに作成できる。また、既存の A(l, r) は少なくとも 1 回は新たな A(l, r) の作成に寄与しているため、いわゆる情報落ちも発生していない(おそらく長くなるので割愛)。

あとは、X(L, R) に対して L ≦ l かつ r ≦ R であるような任意の A(l, r) を用いて掃き出していけばよい。X(L, R) を 0 にすることができれば X(L, R) が作成可能であると言える。

計算量は各 bit に対して O(n) なので全体で O(n log(max {A})) である。
新たに作成した A(l, r) のソートはマージソートが可能なので log を 1 つ落とせる。

提出コード

定数倍削減をサボったので少し遅め。

atcoder.jp

ARC117 F - Gateau

筆者は(一応)文系なので、数学的な記法、特に集合に関する記法については誤りを含んでいる可能性があります。 ニュアンスで汲み取ってください。 致命的な誤用がある場合はコメントやツイッターで教えてほしいです。

問題概要

$ 2N $ 個のピースからなる円形のケーキにいちごを乗せる。 ピース $ [ i , \, i+N ) $ には合計 $ A _ i $ 個以上のいちごを乗せる必要があるとき、ケーキ全体に乗せるべきいちごの数の最小値を求めよ。
https://atcoder.jp/contests/arc117/tasks/arc117_f

アルゴリズムの概略

証明パートも含めるとかなり長くなるので、何をするのか簡潔に記しておく。

  1. $ A _ {2N-1} $ が最大値となるようにケーキを回転させる
  2. $ [ N , \, 2N ) $ に正のいちごを乗せる操作を $ [ 0 , \, N ) $ に負のいちごを乗せる操作とみなす
  3. $ A _ {i} = A _ {2N-1} \, ( N-1 \leqq i < 2N-1 ) $ となるよう $ [ 0 , \, N ) $ にいちごを乗せる
  4. $ max \, A _ {i} = A _ {2N-1} \, ( 0 \leqq i < N-1 ) $ となるようピース $ 0 $ に正のいちごを乗せる
  5. 負のいちごの総数が $ A _ {2N-1} $ 以下になるまでいちごを取り除く
  6. $ 2A _ {2N-1} - ( $ 負のいちごの総数 $ ) + ( $ 正のいちごの総数 $ ) $ が求める値

1. ケーキの回転

回転させても問題ないことは明らか。
ついでに配列 $ A $ も回転させて、条件文を「ピース $ [ i , \, i+N ) $ には合計 $ A _ {i+N-1} $ 個以上のいちごを乗せる」と言い換えておく。

2. 負のいちごを乗せる

ピース $ i $ にいちごを $ x $ 個乗せる操作を「 $ a \in \lbrace A _ {k} \mid i \leqq k < i+N \rbrace $ を満たす全ての $ a $ を $ -x $ する効果がある」という意味で $ d _ {i} = -x $ と表すこととする。 また、$ d _ {i} $ の連続 $ N $ 個の和を $ S _ {i+N-1} = \left( \sum _ {k=i} ^ {i+N-1} d _ {k} \right) $ とおく。 すると、満たすべき条件は $$ \begin{gather} A _ {i+N-1} + S _ {i+N-1} \leqq 0 \notag \\ \Leftrightarrow A _ {i} + S _ {i} \leqq 0 \end{gather} $$ と表すことができる。

$ i = 2N-1 $ のとき、$ (1) $ より $$ A _ {2N-1} + S _ {2N-1} \leqq 0 $$ である(下図は $ N = 3 $ のとき)。

さて、$ A _ {2N-1} + S _ {2N-1} < 0 $ かつ $ d _ {2N-1} < 0 $ である限り、$ d _ {2N-1} $ に含まれる $ -1 $ を $ d _ {0} $ へ移動することができる。 元の言い方に即して言うと、ピース $ 2N-1 $ に乗っていたいちご $ 1 $ 個をピース $ 0 $ に移動できる $ ( * ) $
(この操作を行うことによって悪影響があるのは $ A _ {2N-1} $ だけであり、操作後も $ (2) $ を満たしている。また、いちごの総数も変わらない。)

また、$ d _ {2N-1} = 0 $ のときは、$ A _ {2N-2} \leqq A _ {2N-1} $ かつ $ S _ {2N-2} = S _ {2N-1} - d _ {2N-1} + d _ {N-1} \leqq S _ {2N-1} $ より $$ A _ {2N-2} + S _ {2N-2} < 0 \notag $$ となるため、先ほどと同様に $ d _ {2N-2} $ から $ d _ {2N-1} $ へ移動することができ、さらに $ d _ {0} $ へ移動できる。 この操作を繰り返すと、 $$ A _ {2N-1} + S _ {2N-1} = 0 $$ となり、すなわち「ピース $ [ N , \, 2N ) $ には合計 $ A _ {2N-1} $ 個ちょうどのいちごが乗っている」状態にできる。 以降、$ (3) $ が常に成り立っているものとする。

ここで、一旦 $ d _ {i} , \, d _ {i+N} $ をともに $ -d _ {i+N} $ することを考える。 この操作を $ [ 0, \, N ) $ で行うと、 $$ \hat{d} _ {i} = \left\{ \begin{align} & d _ {i} - d _ {i+N} & & ( 0 \leqq i < N ) \notag \\ & 0 & & ( N \leqq i < 2N ) \notag \end{align} \right. $$ と変形できる。 そして、$ \hat{S} _ {i+N-1} = \left( \sum _ {k=i} ^ {i+N-1} \hat{d} _ {k} \right) $ とすると、$ \hat{S} _ {i} = S _ {i} - S _ {2N-1} $ であるから、$ (1) \, (3) $ より $$ \begin{gather} A _ {i} + \hat{S} _ {i} + S _ {2N-1} \leqq 0 \notag \\ \Leftrightarrow \hat{S} _ {i} \leqq A _ {2N-1} - A _ {i} \end{gather} $$ を満たすように $ \hat{D} = \lbrace \hat{d} _ {i} \rbrace $ を構築すればよい。

6. 最終目標

ナンバリングは前後するが、求めるべき最小値について先に整理しておく。 $$ \begin{align} ans & = min \, ( - \sum _ {k=0} ^ {2N-1} d _ {k} ) \notag \\ & = min - ( S _ {2N-1} + S _ {N-1} ) \notag \\ & = min - ( 2S _ {2N-1} + \hat{S} _ {N-1} ) \notag \\ & = 2A _ {2N-1} - max \, \hat{S} _ {N-1} \end{align} $$ つまり、$ (4) $ を満たすようにして $ \hat{S} _ {N-1} = \sum \hat{d} _ {k} $ を最大化すればよい。

3. $ [ N-1, \, 2N ) $ の条件を満たす

$ i = 2N-1 $ においては当然 $ (4) $ を満たしている。

各 $ \hat{d} _ {i} $ を最大化するには、$ (4) $ より $$ \hat{S} _ {i} = A _ {2N-1} - A _ {i} \qquad ( N-1 \leqq i < 2N-1 ) $$ であればよい。

$$ \begin{flalign} \hat{d} _ {N-1} & = \hat{S} _ {2N-2} - \hat{S} _ {2N-1} = A _ {2N-1} - A _ {2N-2} & \notag \\ \hat{d} _ {N-2} & = \hat{S} _ {2N-3} - \hat{S} _ {2N-2} = A _ {2N-2} - A _ {2N-3} & \notag \\ \hat{d} _ {N-3} & = \hat{S} _ {2N-4} - \hat{S} _ {2N-3} = A _ {2N-3} - A _ {2N-4} & \notag \\ & \quad \vdots & \notag \\ \hat{d} _ {0} & = \hat{S} _ {N-1} - \hat{S} _ {N} = A _ {N} - A _ {N-1} & \notag \end{flalign} $$

これでひとまずピース $ [ N-1 , \, 2N) $ については $ (4) $ を満たしている。

4. $ \hat{d} _ {0} $ を減少させる

ピース $ [ 0 , \, N-1 ) $ については $ (4) $ の条件を満たしていないことがある。 $ (6) $ を用いて

$$ \begin{align} (4) & \Leftrightarrow A _ {i} + \hat{S} _ {i} \leqq A _ {2N-1} \notag \\ & \Leftrightarrow A _ {i} + ( \hat{S} _ {N-1} - \hat{S} _ {i+N} ) \leqq A _ {2N-1} \notag \\ & \Leftrightarrow A _ {i} + ( A _ {i+N} - A _ {N-1} ) \leqq A _ {2N-1} \notag \\ & \Leftrightarrow A _ {i} + A _ {i+N} \leqq A _ {N-1} + A _ {2N-1} \qquad ( 0 \leqq i < N-1 ) \notag \end{align} $$

であるから、$ max \, ( A _ {i} + A _ {i+N} ) - ( A _ {N-1} + A _ {2N-1} ) $ の値を $ \hat{d} _ {0} $ から減少させておく必要がある。
ここで、「 $ \hat{d} _ {0} $ から減少させる」必要性はないが、 $ ( * ) $ と同様の議論によって「 $ \hat{S} _ {i} < A _ {2N-1} - A _ {i} $ かつ $ \hat{d} _ {i} < 0 $ である限り、$ \hat{d} _ {i} $ から $ \hat{d} _ {i+1} $ へ転嫁できる(以降、「転嫁操作」と呼ぶ)」ことがわかる。 すなわち、とりあえず $ \hat{D} $ の先頭要素である $ \hat{d} _ {0} $ から減らしておけば必要以上に不利益を被ることはない。

5. $ \hat{d} _ {i} $ を調整する

当初 $ \hat{D} $ は、$ \lbrace d _ {i} \rbrace $ から $ [ N , \, 2N ) $ の範囲を $ [ 0 , \, N ) $ に反転させることで構築した。 $ d _ {i} \leqq 0 $ であるから、$ P = \lbrace x \in \hat{D} \mid x > 0 \rbrace $ とすると、 $$ \left( \sum _ { x \in P } x \right) \leqq - S _ {2N-1} = A _ {2N-1} $$ が条件である。 この条件を満たさない場合、任意の $ \hat{d} _ {i} \in P $ を減少させなければならない。

さて、$ [ 0 , \, N-1 ) $ において転嫁操作を行えなくなるまで繰り返したとき、$ 0 \leqq \hat{S} _ {i} \leqq \hat{S} _ {N-1} $ となる。

少し雑な証明 $ N-1 \leqq i $ については $ (6) $ から常に成立するので、$ i < N-1 $ についてのみ証明する。

$ \hat{S} _ {i} < 0 $ を満たす $ i $ が存在すると仮定する。
そのような $ i $ には $ \hat{d} _ {i} < 0 $ となるものが必ず存在する。 また、$ \hat{S} _ {i} < 0 \leqq A _ {2N-1} - A _ {i} $ となるため、転嫁操作を追加で行うことができ、「もう転嫁操作を行えない」という条件に反する。 したがって、$ 0 \leqq \hat{S} _ {i} $ は常に成立する。

$ \hat{S} _ {i} > \hat{S} _ {N-1} $ を満たす $ i $ が存在すると仮定する。
$ \hat{S} _ {i} + \hat{S} _ {i+N} = \hat{S} _ {N-1} $ より $ \hat{S} _ {i+N} < 0 $ となる。 しかし、$ (6) $ より $ \hat{S} _ {i+N} = A _ {2N-1} - A _ {i+N} \geqq 0 $ であったはずなので、$ \hat{d} _ {i-1} $ から $ \hat{d} _ {i} $ への転嫁操作が行われたことになる。 すると、その転嫁操作の前は $ \hat{d} _ {i-1} < 0 $ であり、$ \hat{S} _ {i+N-1} = \hat{d} _ {i-1} + \hat{S} _ {i+N} < 0 $ となって、同様の議論によって $ \hat{d} _ {i-2} $ から $ \hat{d} _ {i-1} $ への転嫁操作が行われたことになる。 これを繰り返すと、全ての $ j \, ( < i ) $ について $ \hat{d} _ {j} < 0 $ であったことになり、$ \hat{S} _ {i} \leqq 0 $ に反する。 したがって、$ \hat{S} _ {i} \leqq \hat{S} _ {N-1} $ も常に成立する。

すなわち、$ l < i < r $ として、任意の $ \hat{d} _ {i} < 0 $ について $ \hat{d} _ {l} , \, \hat{d} _ {r} > 0 $ となる $ l , \, r $ が存在する。

ここで、$ M = \lbrace x \in \hat{D} \mid x < 0 \rbrace $ として、任意の $ \hat{d} _ {i} \in M $ を $ +1 $ する操作を考える。

$ \hat{d} _ {i} < 0 $ かつ転嫁操作をこれ以上行えないことから、$ \hat{S} _ {i} = A _ {2N-1} - A _ {i} $ だったはずであり、$ \hat{S} _ {i} + 1 > A _ {2N-1} - A _ {i} $ となって $ (4) $ を満たさなくなる。 そこで $ \hat{d} _ {l} \in \lbrace P \mid l < i \rbrace $ を満たす $ l $ を選んで、$ \hat{d} _ {l} $ を $ -1 $ する必要がある。

また、$ \hat{d} _ {l} > 0 $ より、$ \hat{d} _ {l} $ から $ \hat{d} _ {l+1} $ への転嫁操作は行われていないのだから、$ \hat{S} _ {l+N} $ の値は変化していない。 $ (6) $ より、$ \hat{S} _ {l+N} = A _ {2N-1} - A _ {l+N} $ なので、こちらも同様に $ \hat{S} _ {l+N} + 1 > A _ {2N-1} - A _ {l+N} $ となって $ (4) $ を満たさなくなり、$ \hat{d} _ {r} \in \lbrace P \mid i < r \rbrace $ となる $ r $ を選んで $ -1 $ する必要がある。

この一連の操作によって、$ \hat{S} _ {N-1} = \sum \hat{d} _ {k} $ の値は $ -1 $ される。

続けて、任意の $ \hat{d} _ {j} \in \lbrace M \mid r < j \rbrace $ を $ +1 $ する操作を考えると、今度は $ \hat{S} _ {j} + 1 = A _ {2N-1} - A _ {j} $ となって $ (4) $ を満たしている。 したがって、$ -1 $ する必要があるのは $ \hat{d} _ {t} \in \lbrace P \mid j < t \rbrace $ となる $ t $ のみである。

これを繰り返すと、長さ $ len $ の操作列 $ \lbrace -1 , \, +1 , \, -1 , \, +1 , \, \ldots , \, -1 \rbrace $ によって、$ \hat{S} _ {N-1} $ の値は $ -1 $ されるとともに、$ \left( \sum _ { x \in P } x \right) $ の値も $ -(len+1) / 2 $ できることがわかる。 $ (7) $ が満たされるまでこの(可変長の)操作列を適用したとき、その操作列の個数だけ $ \hat{S} _ {N-1} $ の値が削られて、$ (5) $ に代入して答えが得られる。

計算量

最も重い処理は $ \lbrace -1 , \, +1 , \, -1 , \, +1 , \, \ldots , \, -1 \rbrace $ の最長操作列を作成・適用する部分である。 操作列を適用した後、$ \hat{d} _ {l} , \, \hat{d} _ {r} \in P $ は存在するが $ \hat{d} _ {i} \in M $ が存在しなくなるという場合、$ -1 $ の操作が $ 1 $ つ余分になって非効率になるし、逆に $ \hat{d} _ {l} , \, \hat{d} _ {r} \in M $ は存在するが $ \hat{d} _ {i} \in P $ が存在しなくなる場合は条件 $ (4) $ を満たさなくなる。 このようなときは $ \hat{d} _ {l} $ と $ \hat{d} _ {r} $ のどちらかを操作列から除外する必要がある。

具体的な実装としては、遅延セグメント木を用いて、

  • $ P , \, M $ の連続する区間を併合して $ 1 $ つにする
  • セグメント木の偶数番目の要素を $ P $ 、奇数番目の要素を $ M $ に割り当てて構築する
  • 以下、条件 $ (4) $ が満たされるまで繰り返す
    • 全要素から最小の要素の値だけ減らす(=操作列の適用)
    • 値が $ 0 $ になった要素の前後を併合し、不要な部分は無限大に更新する

とすればよい。少し工夫すれば通常のセグメント木でも問題なく、$ O ( N \log N ) $ で実行可能である。

提出コード

atcoder.jp

天下一プログラマーコンテスト2013予選B C - 天下一ジグソーパズルふたたび

問題概要

まとめるのが面倒なので割愛。

atcoder.jp

考えたこと

直前の行に対して、各ピースが下に凸であるかを状態にもつ DP でできそう。

ただし、遷移させるときに四辺が凹のピース(以下、Concave)と四辺が凸のピース(以下、Convex)をそれぞれいくつ使うかを気にする必要がある。
そのままやると、①遷移前の状態、②遷移後の状態、③Concave と Convex の並び方、で場合分けすることになり、計算量に不安がある上にコードを書くのもかなり面倒な気持ちになる。

ここで、特殊なピース(一辺が直線で三辺が凹のピース。以下、Special)を最大化することを考えると、特別な理由がなければ下に凸になるように配置すれば良いことに気付く。
特別な理由とは、Convex の使用を抑えたい、ということである。

そして、Convex を使わなければならないのは如何なる場合か考えてみると、「1 行 n 列のピースの領域であって、全ての辺が凸であるもの」(以下、n_Convex)を作りたいときである。
このとき、領域内に 1 つの Convex があれば作成可能である(1_Convex = Convex。また、k_Convex に対して、一辺が凹で三辺が凸のピースを接続させると k+1_Convex が作れる。これを繰り返す)。
逆に、n_Convex でない領域を作成するとき、Convex は不要である(n_Convex 内に存在する Convex の一辺を凹にすれば良い)。

よって、直前の行に対して、Concave の並び方さえ決めてしまえば、

  • Concave 間の領域が n_Convex になり得る
    • Convex を 1 つ使って全て下に凸にする
    • Convex を使わずに下に凹である部分を 1 つ作る
  • Concave 間の領域が n_Convex になり得ない
    • 全て下に凸にする

というような DFS で、Convex をいくつ使うかとともに遷移させることができる。

Concave の並び方も DFS を用いて、

  • 直上のピースが下に凸ならば、Concave を置いて右に 2 進む(Concave が隣り合うことはない)
  • 直上のピースの状態にかかわらず、Concave を置かずに右に 1 進む

とすれば良い。このとき、両端には Concave ではなく Special が置かれることに注意する。

計算量の改善

半分全列挙を駆使することでさらに早くなる(と思う)。

パズル全体を上下に分割することで、①遷移回数、②Concave の最大値、③Convex の最大値、がそれぞれ半分になるので、計算量が 1/8 になる。

ただし、上述のアルゴリズムでは「特別な理由がなければ下に凸になるように配置」していたので、分割したパズルを組み合わせる際に凸同士が接触してしまうケースが発生する。
凸部を凹部に変えても Convex の使用量は増えないので、高速ゼータ変換を用いて伝播させた後、凹凸を組み合わせる。

提出コード

atcoder.jp

かなり昔の問題だと公式解説がなくて不便(そのためのユーザ解説?)。

パトリシア木

C++ で競プロ用で良い感じの実装をしてる人が見つからなかったのでここに置いておきます ( が、使用頻度はかなり少ないと思います ) 。

アルゴリズム概要

ja.wikipedia.org

トライ木の一種。
通常のトライ木は単一の文字によってラベリングされているが、パトリシア木は部分文字列によってラベリングされているため、メモリ使用量や探索時間を低減させることができる。

実装方針

ベースとなるトライ木はこちらを参考にした。

トライ木(Trie) | Luzhiled’s memo

追加すべき項目は、

  • 各ノードに対応する文字列の情報を格納する
  • ノードを分割する

である。


ノード分割は、例えば [ patricia ] を [ pat ] [ ricia ] に分割する際の方針として、

  • ノード [ pat ] を新しく作成し、既存ノードを [ ricia ] に変更する
  • 既存ノードを [ pat ] に変更し、ノード [ ricia ] を新しく作成する

の 2 通りが考えられるが、後者の場合は子ノードの情報を書き換える必要があり定数倍が重くなるため、前者の方針で実装した。


文字列情報の型は string でも良いが、今回はノード分割との兼ね合いで deque を用いた。
既存ノードの文字列情報を [ patricia ] から [ ricia ] に変更する際に、deque なら定数時間で前方要素を削除できる。 一方 string だと、例示したような短い文字列なら問題ないが、長い文字列になると書き換えコストがかなり大きくなってしまう。


なお、文字列を削除する操作は使う機会がほぼなさそうなので、実装していない。

実装

#include <bits/stdc++.h>
using namespace std;

template<int char_size>
struct PatriciaNode {
  deque<char> prefix;
  int nxt[char_size];

  int exist;
  vector<int> accept;

  PatriciaNode() : exist(0) {
    memset(nxt, -1, sizeof(nxt));
  }

  PatriciaNode(const string &str, int str_index_left, int str_index_right = -1) : exist(0) {
    memset(nxt, -1, sizeof(nxt));
    if(str_index_right == -1) str_index_right = (int)str.size();
    for(int i = str_index_left; i < str_index_right; ++i) {
      prefix.emplace_back(str[i]);
    }
  }

  int sum() const {
    return exist + (int)accept.size();
  }
};

template<int char_size, int margin>
struct PatriciaTree {
  using Node = PatriciaNode<char_size>;

  vector<Node> nodes;
  int root;

  PatriciaTree() : root(0) {
    nodes.emplace_back();
  }

  void update_direct(int node_index, int id) {
    nodes[node_index].accept.emplace_back(id);
  }

  void update_child(int node_index, int child_index, int id) {
    ++nodes[node_index].exist;
  }

  void split(const string &str, int str_index, int node_index, int nxt_index, int len) {
    int insert_index = (int)nodes.size();
    nodes.emplace_back(str, str_index, str_index + len);
    for(int i = 0; i < len; ++i) {
      nodes[nxt_index].prefix.pop_front();
    }
    nodes[node_index].nxt[str[str_index] - margin] = insert_index;
    nodes[insert_index].nxt[nodes[nxt_index].prefix[0] - margin] = nxt_index;
    nodes[insert_index].exist = nodes[nxt_index].sum();
  }

  void add(const string &str, int str_index, int node_index, int id) {
    if(str_index == (int)str.size()) {
      update_direct(node_index, id);
    } else {
      const int c = str[str_index] - margin;
      if(nodes[node_index].nxt[c] == -1) {
        nodes[node_index].nxt[c] = (int)nodes.size();
        nodes.emplace_back(str, str_index);
      } else {
        int nxt_index = nodes[node_index].nxt[c];
        deque<char> &prefix = nodes[nxt_index].prefix;
        for(int i = 0; i < (int)prefix.size(); ++i) {
          if(str_index + i == (int)str.size() || str[str_index + i] != prefix[i]) {
            split(str, str_index, node_index, nxt_index, i);
            break;
          }
        }
      }
      int nxt_index = nodes[node_index].nxt[c];
      add(str, str_index + (int)nodes[nxt_index].prefix.size(), nxt_index, id);
      update_child(node_index, nxt_index, id);
    }
  }

  void add(const string &str, int id) {
    add(str, 0, 0, id);
  }

  void add(const string &str) {
    add(str, nodes[0].exist);
  }

  void query(const string &str, const function<bool(int, int)> &f, int str_index, int node_index) {
    if(f(str_index, node_index)) return;
    if(str_index == (int)str.size()) {
      return;
    } else {
      int nxt_index = nodes[node_index].nxt[str[str_index] - margin];
      if(nxt_index == -1) return;
      query(str, f, str_index + (int)nodes[nxt_index].prefix.size(), nxt_index);
    }
  }

  void query(const string &str, const function<bool(int, int)> &f) {
    query(str, f, 0, 0);
  }

  int count() const {
    return nodes[0].exist;
  }

  int size() const {
    return (int)nodes.size();
  }
};

注意点

query 操作を行う際に引数となる文字列は既に挿入された文字列であることが前提となっているため、各ノードに格納された文字列情報との比較は先頭 1 文字分しか保証していない。
正確に比較したい場合は、引数となる function 内で比較してほしい ( 一致しない場合は function の返り値を true にすることで query が終了する ) 。

また、query 1 回あたりの最悪計算量は、正確な比較を行わない場合でも O ( f * sqrt N) となり、かなり重たい。
( f : 引数となる function の計算量、N : 挿入した文字列の長さの和 )

検証

atcoder.jp

ARC052 D - 9

問題概要

$ 1 \leqq N \leqq M $ となる整数 $ N $ について、$ K $ で割った余りと、$ 10 $ 進法表記での各桁の和を $ K $ で割った余りが一致するような $ N $ の個数を求めよ。
制約 : $ 1 \leqq K \leqq M \leqq 10^{10} $

考えたこと

$ i $ を $ 1 $ ずつ増やしていくときに、各桁の和 ( $ = S_i $ とする) がどうなるかを考えてみる。

例えば、$ 48 \leqq i \leqq 50 $ について考えると、

  • $ S_{48} = 12 $
  • $ S_{49} = 13 $
  • $ S_{50} = 5 $

となる。基本的には $ S_i $ も $ 1 $ ずつ増えていくが、繰り上がりがあると追加で $ 9 $ 減る。
(表面上は $ 8 $ 減ったように見える。)

一般に、繰り上がりが $ t $ 回あると、 $ S_i $ は追加で $ 9 \times t $ 減るので (証明略) 、$ i = N $ まで $ 1 $ ずつ増やしていったときに起こった繰り上がりの累計回数を $ T_N $ とすると、 $$ N - S_N = 9 \times T_N \notag $$ となる。

問題文の条件は $ N \equiv S_N \pmod K \Leftrightarrow N - S_N \equiv 0 \pmod K $ と言い換えられるので、 $$ 9 \times T_N \equiv 0 \pmod K \notag $$ $$ \Leftrightarrow T_N \equiv 0 \pmod P \quad \left( P = { \tfrac{K}{\mathrm{gcd} \left( K, 9 \right)} } \right) \notag $$ であるような $ N $ の個数を求めればよい。

$ x \equiv 0 \pmod P $ を満たす ( $ = P $ の倍数である) 整数 $ x \left( 0 < x \leqq T_M \right) $ は $ \lfloor \tfrac{T_M}{P} \rfloor $ 個存在する。
また、$ T_N = x $ を満たす $ N $ は、後述する "抜け" に $ x $ が含まれていないならば $ 10 $ 個存在する。
( $ 1 $ の位だけが変化しても、繰り上がりの累計回数 ( $ = T_N $ ) は変わらない。)


さて、数列 $ T = [ T_1 , T_2 , T_3 , \ldots ] $ において、$ T $ は単調増加であり、

  • $ T_{99} = 9 $
  • $ T_{100} = 11 $

であることから、$ T_i = 10 $ となるような $ i $ は存在しない。これは、$ 1 $ の位から $ 10 $ の位への繰り上がりと、$ 10 $ の位から $ 100 $ の位への繰り上がりが同時に起こっている、すなわち、$ T_i $ が一気に $ 2 $ 増えているためである。このように、複数の位で同時に繰り上がると "抜け" が発生する。

この "抜け" は、以下のようなコードで列挙することができる。

vector<long long> a = ( { 10 } );
long long k = 11;  // "抜け" の周期
while(true) {
  vector<long long> b;
  for(int i = 0; i < 10; ++i) {
    for(auto &x : a) {
      b.emplace_back(x + i * k);
    }
  }
  b.emplace_back(b.back() + 1);  // 新たな "抜け"
  a = move(b);
  k = k * 10 + 1;
}

$ P $ の倍数である整数 $ x \left( 0 \leqq x < T_M \right) $ のうち、"抜け" に含まれるものは「実際には存在しない数字」であるので、除外する必要がある。

計算量

$ T_N $ については、$ 10^{i} $ の位から $ 10^{i+1} $ の位への繰り上がりの累計回数は $ \lfloor \tfrac{N}{10^{i+1}} \rfloor $ なので、 $$ T_N = \sum_{k=0}^{\infty} \lfloor \tfrac{N}{10^{k+1}} \rfloor \notag $$ で求められる。計算量は $ O \left( \log M \right) $ 。


$ x $ と "抜け" の重複をカウントする方法は、

  1. "抜け" を列挙して $ x $ と重複しているか判定する
  2. $ x $ を列挙して "抜け" と重複しているか判定する

の $ 2 $ 通りがある。
前者は、列挙パートを愚直に実装すれば $ O \left( M \right) $ であるが、必要な値は「 "抜け" $ \bmod P $ 」であるので、それをメモしてループさせれば $ O \left( P \log M \right) $ で収まる。重複判定パートは $ O \left( 1 \right) $ 。
後者は列挙パートで $ O \left( \tfrac{M}{P} \right) $ 、それぞれについて重複判定パートで $ O \left( \log M \right) $ 。

$ \mathrm{min} \left( P , \tfrac{M}{P} \right) \leqq \sqrt{M} $ であるので、最悪計算量は $ O \left( \sqrt{M} \log M \right) $ で収まる。

詳しくは以下の実装を参照されたい。

実装

#include <bits/stdc++.h>
using namespace std;

void solve() {
  using ll = long long;
  ll k, m; cin >> k >> m;

  k /= gcd(k, 9);

// T_M の計算
  ll cnt = 0;
  ll tmp = m;
  while(tmp /= 10) {
    cnt += tmp;
  }

  constexpr ll div = 1e11 / 9;

// n が "抜け" であるかどうか判定する
  auto f = [&](int n) -> bool {
    ll d = div;
    ++n;
    for(int i=9; i>=0; --i) {
      n %= d;
      d /= 10;
    }
    return n;
  };

// "抜け" を列挙して x と重複しているか判定する
  auto f1 = [&]() -> ll {
    vector<vector<ll>> res(10, vector<ll>(k, 0));
    ll MAX = 10;
    res[0][MAX % k] = 1;
    for(int i=0; (MAX+1)*10<cnt; ++i) {
      for(int j=0; j<k; ++j) {
        if(res[i][j]) {
          for(int t=0; t<10; ++t) {
            res[i+1][(j + t*(MAX+1)) % k] += res[i][j];
          }
        }
      }
      MAX = (MAX+1) * 10;
      res[i+1][MAX % k] += 1;
    }

// (T_M - 1) までに存在する "抜け" をカウントする
// e.g. 2222 + 111 + 66 = 2399 < 2400
    vector<ll> sum(k, 0);
    ll d = div;
    ll r = cnt, add = 0;
    for(int i=9; i>=0; --i) {
      for(int t=0; t<r/d; ++t) {
        for(int j=0; j<k; ++j) {
          if(res[i][j]) {
            sum[(j + add) % k] += res[i][j];
          }
        }
        add += d;  // 見終わった分を加算していく
      }
      r %= d;
      d /= 10;
    }

    ll ret = 9;  // 1 <= N <= 9 は常に成立
    ret += ((cnt - 1) / k - sum[0]) * 10;  // x (0 < x < T_M) は存在すれば常に 10 個ある
    if(!(cnt % k)) ret += f(cnt) * (m % 10 + 1);  // x = T_M は (M % 10 + 1) 個存在する
    return ret;
  };

// x を列挙して "抜け" と重複しているか判定する
  auto f2 = [&]() -> ll {
    ll ret = 9;
    for(ll i=k; i<cnt; i+=k) {
      ret += f(i) * 10;
    }
    if(!(cnt % k)) ret += f(cnt) * (m % 10 + 1);
    return ret;
  };

  cout << (m < 10 ? m : k < 1e5 ? f1() : f2()) << endl;
}

signed main() {
  cin.tie(nullptr);
  ios::sync_with_stdio(false);
  cout << fixed << setprecision(10);
  solve();
  return 0;
}

想定解と比べて非常に面倒なことをしていた。

https://atcoder.jp/contests/arc052/tasks/arc052_d