Processing math: 100%

AtCoder Regular Contest 104 B,C,D,E問題メモ

AtCoder Regular Contest 104

めっちゃ久々のARC。

B - DNA Sequence

問題

  • 長さ N の、ATCGのいずれかの文字からなる文字列 S が与えられる
  • S の連続する部分文字列のうち、以下の条件を満たすものの個数を求めよ
    • 部分文字列 T1 とすると、好きに並べ替えることによって以下の条件を満たす T2 が作れる
    • 全ての i=1|T1| において、
      • T1i 文字目が 'A' なら T2i 文字目は 'T'
      • T1i 文字目が 'T' なら T2i 文字目は 'A'
      • T1i 文字目が 'C' なら T2i 文字目は 'G'
      • T1i 文字目が 'G' なら T2i 文字目は 'C'
  • 1N5000

解法

要は、「'A'と'T'、'C'と'G'をそれぞれ同数ずつ含む部分文字列」を数えればよい。

2つの要素が絡むとややこしいので、たとえばAとTしかないと単純化する。

Xi を「S を先頭から i 文字目まで見て余っているAの個数」とする。不足分は負で表す。

X0=0 で初期化し、Aが現れたら+1、Tが現れたら-1として更新していく。

   A T A A T T T  T
X 0 1 0 1 2 1 0 -1 -2

すると、「Xi=Xj である」=「Si+1Sj はAとTを同数ずつ含む」となる。この (i,j) のペアの個数を数えればよい。

これは、以下のような Y を作っておいて、順番に Xi を求めていく中で更新していけばよい。

  • Y[j]= これまでに Xi=j となった回数

i について Xi を更新したら、その時の Y[Xi] の値が「(?,i) となるようなペアの個数」となるので、答えに加算する。
その後、Y[Xi] に+1して更新する。

         Y
    X     -2  -1  0  1  2     ペア
始  0             1
A   1             1  1
T   0             2  1          1
A   1             2  2          1
A   2             2  2  1
T   1             2  3  1       2
T   0             3  3  1       2
T  -1          1  3  3  1
T  -2      1   1  3  3  1  _______
                           Ans. 6

今回の問題では、これを2次元で行えばよい。

計算量は、イテレートする部分は O(N) なのだが、Y の二次元配列を確保するところで O(N2) かかる。

Python3

C - Fair Elevator

問題

  • エレベーターが1階から 2N 階まで、1回だけ移動した
  • その間、N 人の人が乗り降りした
    • 2N 個のフロア全てで、乗りまたは降りが1回ずつ発生した
    • エレベーターに同時に乗っていた瞬間がある人達は、移動した階(降りた階-乗った階-1)が全て同じだった
  • N 人の人の乗降記録がある
    • i 番目の人は Ai 階で乗り、Bi 階で降りた
    • ただし、Ai=1Bi=1 の時は、記録が無いことを表す
  • 乗降記録が、上記の説明と矛盾しないか判定せよ
  • 1N100

解法

丁寧な条件分岐をしての全探索。

まず、互いに-1でないのに AiBi だったり、Ai,Bi の間で重複があったらアウト。

以下、そうで無いとする。「移動した階が全て同じ」という条件を考える。

仮に、ある人が1→5階へ移動したとする。2,3,4階で他の人の乗り降りが発生している。

ここで、他の人は同様に4階層移動しなければならないので、「降りた」ことはあり得ない(地下から乗ったことになってしまう)。

1  2  3  4  5  6  7  8  9 10
|---------->|
   |---------->|
      |---------->|
         |---------->|

結局、このようになるしか無い。

すると 18 階は全て埋まったため、この間ではこれ以上乗りも降りも発生しない。次は、9階が先ほどの1階と同じようにして、議論が進められる。

つまり、正しい乗降記録はある連続した偶数長の区間 [i,i+2w) に分けられて、各区間で [i,i+w) は全て乗り、[i+w,i+2w) は全て降り、となるしかない。

この偶数長の区間を、「グループ」とする。
グループの分け方について全探索し、与えられた乗降記録と矛盾しないものがあるか調べる。
左から順番に仮確定し、残りを矛盾無く確定できるか調べていく。

関数 f(L) を「L2N を矛盾無くグループ分けできるか?」とする。以下のように調べればよい。

  • L から 2w の長さのグループを作るとする
  • w=1(L+2w2N) について
    • k=0w1 について、
      • 以下の条件のいずれかに当てはまれば、L からの長さ 2w のグループは不可能
        • L+k 階で降りた人が乗降記録にある
        • L+k+w 階で乗った人が乗降記録にある
        • L+k 階で乗った人と L+k+w 階で降りた人が乗降記録上で異なる
    • もし長さ 2w のグループが可能なら、仮確定し、f(L+2w) へ進む
  • L=2N+1 まで到達したら成功

f(L)1L1 の分け方に関わらないので、メモ化で高速化できる。


これ、嘘解法ですね……。同じ人が2回乗っても許されちゃう。

2
1 4
-1 -1

Noが正しいが、Yesになる

L+k 階で乗った人が指定されていて、さらにその人の降りた階も指定されている場合、L+k+w がその階で無ければ不可能」という判定も必要。

Python3

D - Multiset Mean

問題

  • 正整数 N,K,M が与えられる
  • x=1N のそれぞれについて、以下を求めよ
    • 1N の整数をそれぞれ 0 個以上 K 個以下含む、空で無い多重集合であって、平均が x であるものの個数を M で割った余り
  • 1N,K100
  • 108M109+9
  • M は素数

N=5,K=3x=4 について求める場合、1~5を上限3個まで含む、平均が4の集合の個数。

{4}, {3,5}, {1,5,5,5} などが該当し、全部で27個存在する。

解法

DPなんだけど、上手な高速化が必要となる。

方針

たとえば N=8x=4 について求めたいとすると、全体から 4 を引いて考え、平均を0にすると考えるとやりやすい。

 1  2  3    4    5  6  7  8
           ↓
-3 -2 -1    0    1  2  3  4

すると、以下のような値を事前計算しておくと、

  • DP[i][j]=1i をそれぞれ上限 K 個まで含む多重集合で、総和が j となるものの個数

平均が0となるには、0 より左(13)の総和と、0 より右(14)の総和が打ち消し合ってくれればよいので、以下のように求められる。

  • j=0(DP[3][j]×DP[4][j])

さらに 0 自身も 0K 個含んでいてよいので K+1 倍、ただし空集合が含まれるのでそれは除き、最終的に以下が答えとなる。

  • j=0(DP[3][j]×DP[4][j])×(K+1)1

DPの求め方

いざDPを求めようとすると、これが一筋縄ではいかないことに気付く。

というのも、DP[i][j] において i,j の範囲は以下のようになる。

  • i: x=1 の時などが最小と最大を同時に必要とし、0iN1
  • j: 和の最大値は、i が最大値 N1 の時、(1+2+...+N1)×K=N(N1)2K

つまり、DP の必要とする空間サイズは O(N3K) となる。

そして、この更新は、愚直にやろうとすると骨子は以下のようになる。(添字が範囲外になる場合などは適宜除外する)

DP[0][0] = 1

for i in range(1, N):
    for j in range(0, N*(N-1)*K//2+1):
        for t in range(0, K):
            DP[i][j] += DP[i-1][j-i*t]

これは全体で O(N3K2) のループとなり、枝刈りなどをしてもさすがにTLE。1)

AtCoder-Libraryのconvolutionでも使うのかな? と思ったが、それでも O(N3KlogN2K) であり、効果は薄い。 また、任意MODでのconvolutionは逆元を求めるフェイズで M の値によって多少の時間がかかってしまう場合があり、 わざわざ問題設定で M を任意にしているということは「convolutionじゃないぞ」というメッセージのような気がする。

解説を見ると、以下のような方法を用いて高速化している(難しいことはしていないのだが)。

  • j について順に処理するとき、i 個周期で参照する箇所の多くが被ることになるので、その情報を上手く使う

具体的には、以下のようになる。

※DP[i-1] を単にDP1と表記する

DP[i][j]   = DP1[j] + DP1[j-i] + DP1[j-2i] + ... + DP1[j-Ki]
DP[i][j-i] =          DP1[j-i] + DP1[i-2i] + ... + DP1[j-Ki] + DP1[j-(K+1)i]

この差分だけを足し引きすればよい。

これで、DP の構築が O(N3K) で収まる。

Python3

E - Random LIS

問題

  • 整数列 A1,A2,...,AN が与えられる
  • ここから長さ N の整数列 X を作る
    • i=1N に対して、Xi は独立に 1Ai の中から一様ランダムで選ばれる
  • この X の最長増加部分列(狭義単調増加)の長さの期待値を求めよ
  • 1N6
  • 1Ai109

解法

制約が独特。

分数は考えるのが面倒なので、「全ての作り得る X に対してLISの総和」を求めて、後から作り得る X の個数で割ることにする。 作り得る X の個数は単純に A1×A2×...×AN である。

通常のLISは、「DP[i][j]= A1Ai だけで長さ j のLISを作るのに必要な最小の末尾要素」として前から順に更新すると解ける。

これを拡張して、試しに以下にしてみたらどうだろうか、と考えたが、

  • DP[i][j][k]= A1Ai だけで長さ j のLISを作るのに必要な最小の末尾要素が k になる個数

これは最終的にDPを埋められたとしても、結局、ある j が最終的なLISの長さとしてあり得るのか、途中経過にのみ出現するのか、区別することができない。

想定解は、N6 という制約を利用して、X における N 個の数の大小関係を全て網羅してしまえばよいらしい。

たとえば、N=4 なら (2,0,0,1) は、X2=X3<X4<X1 となるような X を示す。

大小関係さえ分かれば、それに対する「LISの値」と「パターン数」をそれぞれ個別に求められるので、漏れなく全パターンの総和を求めることが可能となる。

大小関係の列挙方法

大小関係 D を、(2,0,0,1) のように 0k1 からなる重複ありの長さ N の数列で表現するとする。

全探索するにあたり、NN 通りを調べてもよいが、たとえば (2,0,0,1)(3,0,0,2)(3,1,1,2) は全て同じ関係なので、これらを重複して数えないようにしなければならない。

先に何通りの値が出てくるか k=1N1 を決めてしまって、kN 通りの数列を全て調べる。この内、使われている値の種類が k に満たないものは除外すると、重複無く数えられる。

k=N の時は順列を全列挙すればよいので、上記の方法は k=N1 まででよい。

使われている値の種類を調べるのにも N かかるとして、列挙にかかる計算量は N(1N+2N+...+(N1)N)+N! に比例し、N=6 でも123810程度である。

また、実際にこの中で有効なのは、順序付きベル数と呼ばれるらしく、4683個しか無いらしい。

もっと上手な列挙の仕方はありそうな気がする。他の人のコードを読もう。

LISの求め方

大小関係の配列のLISをそのまま求めればよい。

パターン数の求め方

要は、D=(2,0,0,1) のような大小関係の列が固定されたとき、(15,3,3,8) のような具体的な数字の当てはめ方を数えたい。
0,1,2 に相当する数がそれぞれ上限いくつまでを使えるのかを整理する。A の対応する位置の最小値を見ればよい。

  • Si=D 上で i に対応する数が、上限いくつまで使用可能か

A=(9,18,12,5) の場合、0 に対応する数は、対応する位置の Ai18,12 なので小さい方の 12 が上限となる。
例の場合、S=(12,5,9) となる。

i について 1TiSi かつ狭義単調増加となるような、各整数の割り当て方 T を数える。

冗長なDP

Ai109 と大きいので、Ai の値をそのまま添字にしたDPは使えないが、まずはそれが可能として考える。

  • DP[i][j]=Si 番目まで考慮して、Ti=j である割り当て方のパターン数

すると、遷移は以下のようになる。

  • DP[i][j]=j1k=1DP[i1][k]jSi の時)
  • DP[i][j]=0j>Si の時)
S   (0)  1   2   3   4   5   6   7   8   9  10  11  12
始   1   0   0   0   0   0   0   0   0   0   0   0   0
12   -   1   1   1   1   1   1   1   1   1   1   1   1
5    -   0   1   2   3   4   0   0   0   0   0   0   0
9    -   0   0   1   3   6  10  10  10  10   0   0   0  → パターン数はこの総和 50 通り

毎回 Si を超えた範囲は0になるという特徴があるが、基本は累積和をとる更新となる。

累積和を開始する地点が、i ごとに1個ずつ後ろにずれていってしまうのが面倒くさい。
ここは「TiTi+1(広義単調増加)も許す」とすれば、常に累積和を1から開始できる。

Si=Sii として新たに S=(12,4,7) とする。これがシフト後の Ti の上限となる。

    →j
S'  (0)  1   2   3   4   5   6   7   8   9  10  11  12
始   1   0   0   0   0   0   0   0   0   0   0   0   0
12   -   1   1   1   1   1   1   1   1   1   1   1   1
4    -   1   2   3   4   0   0   0   0   0   0   0   0   ←1個左シフト
7    -   1   3   6  10  10  10  10   0   0   0   0   0   ←2個左シフト

ところで、このように繰り返し累積和が取られるような更新は、まとめて計算する方法がある。

j>Si で0となってしまうのが不連続的だが、それが発生しない範囲では、i が進む毎に繰り返し累積和が取られるので、

    →j
i  (0)  1   2   3   4   5   6   7   8   ...
0   1   0   0   0   0   0   0   0   0   ...
1   -   1   1   1   1   1   1   1   1   ...
2   -   1   2   3   4   5   6   7   8   ...
3   -   1   3   6  10  15  21  28  36   ...
4   -   1   4  10  20  35  56  84 120   ...

これは、斜めに見ると、二項係数が並ぶパスカルの三角形が現れる。

    1     上の表は、横列を↘方向に見ている感じ
   1 1
  1 2 1
 1 3 3 1
1 4 6 4 1

従って、0になる更新が発生しない範囲では、DP[i][j] の値は i+jCj、さらに DP[i][1]DP[i][j] の総和は i+j+1Cj と、二項係数一発で求められる。

縮約したDP

さて、遷移がわかったところで、添字がでかすぎる問題に対処する。

S に出現する値で区切ることによって、各区間について、値を1個1個保持せずとも全体を上手く計算できないか考えてみる。この場合、区間数はたかだか6箇所となる。

(0, 4]   (4, 7]    (7, 12]
1 2 3 4 | 5 6 7 | 8 9 10 11 12
  • DP[i][s]=i 番目まで考慮して、s 番目の区間の○○(パターン数を上手く計算するために保持する何か)

(4,7] のように、「自分たちは0のまま、自分より下の区間で累積和が更新されており、いざ累積和を求める遷移が発生した際、なんか中途半端な値で開始される」というような区間が、どのように立式すればいいのか一見わかりづらい。

    →j
S'  (0)  1   2   3   4  |  5   6   7  |  8   9  10  11  12
始   1   0   0   0   0  |  0   0   0  |  0   0   0   0   0
12   -   1   1   1   1  |  1   1   1  |  1   1   1   1   1
4    -   1   2   3   4  |  0   0   0  |  0   0   0   0   0
7    -   1   3   6  10  | 10  10  10  |  0   0   0   0   0
12   -   1   4  10  20  | 30  40  50  | 50  50  50  50  50
                          ~~~~~~~~~~
                          ↑どう全体をまとめて表現する?

DPの更新はすぐ左と上の足し算と言い換えられることを考えると、以下のように、前の区間の末尾の値が大きく関係してくることがわかる。

前区間末尾 | 区間
           |   0      0       0        0     ...
         x |   x      x       x        x     ...
         y |  x+y   2x+y    3x+y     4x+y    ...
         z | x+y+z 3x+2y+z 6x+3y+z 10x+4y+z  ...

これを例えば x の係数についてのみ追いかけると、先ほどの二項係数の値がそのまま出てきている。
また、y,z も、x をそれぞれ追従する形で同じ係数が出てくる。

一度、区間全体が0になったら、x,y,z はそれ以降に伝播しないので、情報は捨ててしまってよい。

従って、連続して区間の累積和を取る更新がかけられ続ける限り、i 個前の前区間末尾の値 x は、

  • 区間内の j 番目(0-index)の値に対し、i+jCjx だけ寄与する
  • 区間長を L とすると、区間全体に xL+i+1CLx だけ寄与する

「その区間に連続して累積和を取る更新がかけられ続けている間、0個前、1個前、…の前区間の末尾が何であったか」という情報さえ覚えておけば良いことがわかる。

実際は同じ計算を省略する目的などで、以下の3つの値をDPに持たせた。

  • 「その区間内の合計、その区間末尾の値、現在有効な0個前,1個前,…の前区間の末尾の値リスト」

計算量は、順序付きベル数を FN として、

  • 列挙が O(NN) 未満
  • その中で有効なもの(FN 個)に対して
    • LISは O(NlogN)
    • パターン数の数え上げは O(N2)

全体で O(NN+FNN2) 未満となり、十分間に合う。

Python3

F - Visibility Sequence

問題

解法

1
 

1)
枝刈りして高速な言語なら通るらしい?
programming_algorithm/contest_history/atcoder/2020/1003_arc104.txt · 最終更新: 2020/10/09 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0