AtCoder Beginner Contest 419 E,F,G 問題メモ

E - Subarray Sum Divisibility

問題文

  • 長さ $N$ の整数列 $A = (A_1, A_2, \ldots, A_N)$ が与えられます。
  • あなたの目的は、以下の操作を繰り返し行うことにより、$A$ のすべての長さ $L$ の連続部分列についてその総和が $M$ の倍数であるようにすることです。
    • $1 \leq i \leq N$ なる整数 $i$ を選び、$A_i$ の値を $1$ 増やす。
  • 目的を達成するまでの操作回数として考えられる最小値を求めてください。

制約

  • $1 \leq N, M \leq 500$
  • $1 \leq L \leq N$
  • $0 \leq A_i \lt M$
  • 入力される値はすべて整数

解法

長さ $L$ の枠を1ずらしたとき、$A_i$ が出て $A_{i+L}$ が入ることになる。
この前後で枠内の総和が $M$ の倍数であることが変わらないようにしないといけない。
つまり操作後の $A$ では $A_{i} \equiv A_{i+L} \mod{M}$ となっていなければいけない。

  i               i+L    M=5
  6  8  5  ...  2  1
 |<------L------>|
    |<------L------>|

もっというと、$A_i$ を「$i$ を $L$ で割ったあまり」によって $0~L-1$ の $L$ 個のグループに分けた時、 操作後の同じグループ内の $A_i \mod{M}$ は、全て一致していないといけない。

L=4
      0   1   2   3   4   5   6   7   8   9  10
A    ○  △  □  ☆  ○  △  □  ☆  ○  △  □  ...

同じ記号は mod M が同じ状態にする必要がある

枠内の総和を $M$ の倍数にすることはとりあえず最後に調整するとして、 ひとまず「各グループ内で mod M を一致させる」ことを目指す。この最小コストは、グループ毎に独立に考えられる。

あるグループを取り出して、$(A_i, A_{i+L}, A_{i+2L},...)$ を $B=(B_0,B_1,B_2...)$ と名付ける(表記を簡潔にするため)。

「グループの mod M は、操作前の $B_i$ のどれかに揃える」としてよい。
そうで無い場合、「$B$ の全てに1を足す」という無駄な操作をおこなっていることになる。

mod M を $B_i$ に揃えるとしたとき、「$B_i$ 以下の要素は $B_i$ に、$B_i$ を超える要素は $B_i+M$ に揃える」のが最適。
そのコストは、$B$ をソートして以下のように求められる。

  • $(B_i 以下の要素数) \times B_i + (B_i を超える要素数) \times (B_i+M) - \sum_i B_i$

これによって、$(揃える値 B_i, コスト C_i)$ という組が $B$ の要素数だけできる。(同じ値が含まれる場合は省略できる)
これを、全てのグループにおいて求める。

グループ0 :  [(3, 8), (5, 9), (6, 8), ...]
グループ1 :  [(0, 7), (2, 10), (5, 12), ...]
        :

後は、1グループからちょうど1個のアイテムを選ぶとして、ナップサックのようなDPをおこなう。

  • $\mathrm{DP}[i,j]:=$ グループ $i$ までを考慮して、揃える値の和 mod M を $j$ にするための最小コスト

アイテム総数は $O(N)$ なので、このDPは $O(NM)$ で動く。

最後に、枠内の総和を $M$ に合わせる。
最後のグループ $L-1$ まで考慮した最終的な結果のうち、 $\mathrm{DP}[L-1,0]$ は、調整無しで枠内の値が $M$ の倍数になっている。

そうでない場合は調整する。
例えば $M=5$ で $j=3$ の場合、あと $2$ だけ、どれかのグループで揃える値を増やす必要がある。

グループで揃える値を $1$ 増やすためには、 グループの要素を全て1増やす必要があるので、 調整は最も要素数が少ないグループでおこなうのがよい。
$\lfloor \frac{N}{L} \rfloor$ が最も要素数の少ないグループの要素数となる。

枠内の総和 mod M が暫定 $j$ である状態からは、あと $M-j$ 増やす必要があるので、 $\mathrm{DP}[L-1,j] + (M-j) \times \lfloor \frac{N}{L} \rfloor$ が、$j$ の場合のコストとなる。

$j=0~M-1$ の中で最小のものが答えとなる。

計算量は $O(N\log{N} + NM)$

Python3

F - All Included

問題文

  • $N$ 個の英小文字列 $S_1,S_2,\ldots,S_N$ および整数 $L$ が与えられます。
  • 長さ $L$ の英小文字列であって、$S_1,S_2,\ldots,S_N$ をすべて部分文字列として含むものの個数を $998244353$ で割った余りを求めてください。

制約

  • $1\leq N\leq 8$
  • $1\leq L\leq 100$
  • $N,L$ は整数
  • $S_i$ は長さが $1$ 以上 $10$ 以下の英小文字からなる文字列
  • $S_i\neq S_j\ (i\neq j)$

解法

$N, |S_i|$ はとても小さい。

ひとまず、Aho-Corasick 法で、各 $S_i$ からなる探索木を作る。

    ,--> a --> ab --> abc
    |
root --> c --> cd --> cde
          |
          `--> ca --> cab --> cabd

このノード数は $O(\sum_i |S_i|)$ で、制約上、rootを合わせて81個以下である。

次のDPをおこなう。

  • $\mathrm{DP}[i, C, s]:=i$ 文字目まで決めて、既に含まれている文字列の集合が $C$ で、接尾辞が $s$ であるものの個数

$s$ が上記のノードのそれぞれに対応する。
「$S_i$ のどれかが完成する可能性がある接尾辞」は Aho-Corasick のノードだけで全てでなので、この状態さえ区別できればよい。

また、$C$ も最大 $N$ bitのbitflagで管理できるので、整数上、状態数は $256$ 個以下である。

各状態から次にどの文字を置くかの遷移が $\sigma=26$ 通りあるので、 全体 $O(L 2^N N \max(|S_i|) \sigma)$ でこのDPは求められる。
単純に制約の上限値を代入すると、これは $5.4 \times 10^7$ 程度となり、間に合う範囲となる。

遷移を考える。各ノードに、以下の情報があれば良い。

  • a) このノードをもって完成する $S_i$ の集合
  • b) 次に文字 $c$ を置いたら、どのノードに遷移するか

a) があれば、完成済みの文字列のbitflag $C$ を適切に更新できる。
探索木の構築時に終端ノードにマークしておけばよい。

b) は、$s$ から次の遷移先を決めるのに必要となる。次の、Aho-Corasick法における探索アルゴリズムが使える。

  • 構築時、各ノードには「その文字列の接尾辞の中で、$S_i$ のどれかの接頭辞に一致する、次に長い文字列 failure」が計算されている。
    • 例) abc に対して、その接尾辞の中で $S_i$ のどれかの接頭辞と一致する次に長い文字列は c
    • 例) cab に対して、その接尾辞の中で $S_i$ のどれかの接頭辞と一致する次に長い文字列は ab

状態 $s$ の時、次の文字として $c$ を置くとする。

  • $s$ から、文字 $c$ に対応する辺が出ているなら、それに従って遷移する。
  • 出ていないなら、$s$ からfailure を1個辿る。辿ったノードから $c$ の辺が出ていたら、その先に遷移する。
  • それも無いなら、さらに failure の failure を辿って、$c$ の辺が出ていたら、その先に遷移する。
  • failure が root までたどり着き、root からも $c$ の辺が無いなら、root に遷移する。

これは、(ノード数 × 次に置く文字26通り) の配列に前計算しておくと、毎回 failure を辿らずに済む。

Python3

G - Count Simple Paths 2

問題文

  • 頂点に $1$ から $N$ の番号がついた $N$ 頂点 $M$ 辺の単純連結無向グラフが与えられます。
  • 各 $k=1,2,\ldots,N-1$ に対して、頂点 $1$ から頂点 $N$ までの単純パスであって、パスに含まれる辺の個数が $k$ であるようなものの個数を求めてください。

制約

  • $2\leq N\leq 2\times 10^5$
  • $N-1\leq M\leq N+20$
  • $1\leq u_i\lt v_i\leq N$
  • 与えられるグラフは単純連結無向グラフ
  • 入力は全て整数

解法

グラフの、丁寧な計算量見積もりが問われる。

一見、そんなんできるかいな、と思いきや制約が特徴的。
$M \le N+20$ ということで、全域木の辺数が $N-1$ 本なので、そこから高々 $21$ 本くらいしか追加の辺は無い。 ほとんど木のようなグラフとなる。

全域木から余る辺数を $K=M-N+1$ とする。

パス数の上限の見積もり

サイクル基底、という考え方があって、

全域木に含まれない辺の数だけ「その辺 + 辺の端点の2頂点を結ぶ全域木上の辺」のサイクルができる。
このサイクルの集合を「サイクル基底」と呼び、グラフ上のサイクルやパスの個数を数えるのに便利な概念となる。

全域木での $1→N$ パスは、一意に決まり、(当然)単純パスである。
このパスを元のグラフに戻してから、サイクル基底をいくつか選んで辺を xor したものも、 また別の $1→N$ パスとなる可能性がある。 (非連結になったり、連結でも同じ頂点を2度以上通過しないとたどれない辺集合ができてしまう場合もあるので、あくまで“可能性”)

逆に言うと、あり得る $1→N$ パスは、このように元のパスへのサイクル基底のXORで作られるものしか存在しない。
数える対象となる $1→N$ パスの個数の上限は、$2^{K}$ 以下であることがわかる。

$2^{21} \simeq 2.1 \times 10^6$ なので「意外と全ての単純パスを列挙しても大丈夫なのでは?」という気になるが、 しかし、まだちょっと計算量的に厳しいので、グラフの縮約をおこなう。

グラフの縮約

  • 袋小路の削除
  • 一本道の縮約

明らかに $1→N$ 単純パスに使われない頂点は、ある程度、前段階で削除できる。
例えば「$1,N$ 以外の次数1の頂点」は、袋小路なので単純パスに含まれ得ない。これは比較的簡単に削除できる。

(また、$1,N$ が同じ側にあるような橋や関節点に関しても、一度通ってしまうと戻れないのでその先を削除できるが、 実装が大変なのと、今回はそこまでしなくても通るので、省略)

さらに、グラフには多数の「次数2の辺」が含まれる場合がある。
これは一本道なので、削除できる。削除した後の辺には、元の距離の重みを持たせておく。

..○--◎--●--●--●--◎--○..  →  ..○--◎--4--◎--○..
      |              |                  |     |
      ○              ○                  ○     ○

この2つの前処理によって、頂点数の上限はぐっと少なくなる。

縮約後のグラフの $1,N$ 以外の頂点数を $n$ 頂点とする。 $1,N$ 以外、頂点の次数は $3$ 以上となっている。
全域木に余分な辺が $K$ 本ある状態は維持されているため、辺数は $n+K+1$ となる。つまり全頂点の次数の総和は $2(n+K+1)$ である。

以上の条件から、以下の式が成立していないといけないので、

  • $\dfrac{2(n+K+1)-2}{n} \ge 3$

整理すると、$n \le 2K$ で抑えられる。

DFS

縮約により、多くとも $2K+2$ まで頂点数が減らせることが分かった。

ここまでくると、普通にDFSで全探索できる。

計算量は以下の要領で見積もれる。

DFSの探索回数(再帰型で書いたときに、dfs() が呼び出される回数)は、 「$\displaystyle \sum_{v} (1からvへの単純パスの総数)$」と一致する。

各 $v$ につき、単純パスは高々 $2^K$ 個であるので、上の式は $(2K+2)2^K$ が上限となる。
これは、$K=21$ を代入して約 $10^8$ となる。
(ちゃんと証明はできてないが、実際にはそこまで綺麗に全頂点、全サイクル基底のXORが単純パスとして成立することも難しいので、実際の探索回数はこれより少なくなると見積もって良さそう?)

Python3

programming_algorithm/contest_history/atcoder/2025/0816_abc419.txt · 最終更新: by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0