Loading [MathJax]/jax/output/CommonHTML/jax.js

AtCoder Regular Contest 194 (Div. 2) A,B,C,D,E問題メモ

AtCoder Regular Contest 194 (Div. 2)

ちょっと悩みつつ、でも時間内に答えに辿り着ける、というくらいの難易度が一番参加してて楽しい。 もちろんそれに当てはまる難易度帯は人によって変わるが、Div.2は自分にとってちょうどよく、Div.1と分けることで頻度が増えたことはありがたい。

A - Operations on a Stack

問題文

  • 長さ N の整数列 (A1,A2,,AN) が与えられます。また、列 S があり、S ははじめは空列です。
  • i=1,2,,N の順に、それぞれの i について、下記の 2 つの操作のうちちょうど 1 つを行います。
    • S の末尾に Ai を要素として追加する。
    • S の末尾の要素を削除する。ただし S が空のときは、この操作を行うことを選択できない。
  • すべての操作を終えた後の、S の要素の総和としてあり得る最大値を出力してください。

制約

  • 1N2×105
  • 109Ai109
  • 入力はすべて整数

解法

i 番目の操作でpopを選択したら、Ai はaddできないという点が特徴的。

この操作でできる S は、以下に言い換えられる。

  • A から連続する2個の要素を削除して間を詰める、ということを好きなだけおこなった残り
  • (2個の内、前者を一度addしてpopした要素、後者をpopしたために追加されなかった要素と考える)

先頭から採用するか捨てるか決めていくDPを考える。
Ai を採用しようと思うと、その直前に採用したのが Ai1,Ai3,... など、奇数個前である状態のみから遷移できる。

i のたびに全ての“奇数個前”を確認していくとTLEするので、DPにはこの累積Maxを持たせるようにすればよい。

  • DP[i]:=Ai までを採用か不採用か決め、最後の採用が i(0) である状態の合計最大値
  • 初期状態・遷移
    • DP[1]=,DP[0]=0
    • DP[i]=max(DP[i1]+Ai,DP[i2])

DP[N] が答え。

Python3

B - Minimum Cost Sort

問題文

  • (1,2,,N) の順列 P=(P1,P2,,PN) が与えられます。
  • 高橋君は P に対して次の操作を繰り返し(0回でも良い)行う事ができます。
    • 1iN1 をみたす整数を 1 つ選ぶ。コスト i を支払い、PiPi+1 を交換する。
  • P を昇順にソートするために必要なコストの総和の最小値を求めてください。

制約

  • 2N2×105
  • (P1,P2,,PN)(1,2,,N) の順列
  • 入力はすべて整数

解法

「隣り合う要素の交換を繰り返して昇順にソートする」といえばバブルソート。
今回は交換位置によってコストが変わるので、なるべく i が小さい左の方で交換したい。

バブルソートに関する前提知識として、以下がある。

  • バブルソートの最小swap回数は転倒数と一致する
  • 「(大,小) が隣接する箇所をswapして (小,大) にする」という操作を繰り返せば、操作順に関係なく最小回数でのソートを達成できる

答えの下限を見つけ、その下限を達成する方法を見つけるという考察過程を取る。

i より左にある Pi より大きな値(つまり、j<i かつ Pi<Pj を満たす j)の個数を L(i) として、 i につき L(i) 回のswapは必須である。
また、1回のswapで1しか左に行かないため、i1,i2,...,iL(i) のコストは必ずかかる。

つまり、全体では Ni=1L(i)j=1ij は必ずかかる。

ここからコストが増えてしまう場合というのは、 まだ「i より左にあり、Pi より大きな値」が残っているのに「i より右にあり、Pi より小さな値」との関係性を解消するために Pi をswapして右に動かしてしまう操作である。以降の Pi を左に持っていく操作コストが1ずつ増える。

逆に言うと、複数の (大,小) の隣接箇所がある場合、左から操作を優先すれば上記のようなことは発生せず、下限を達成できる。

L(i) は、転倒数と同じような要領で FenwickTree などで求められる。

Python3

C - Cost to Flip

問題文

  • 01 のみからなる 2 つの長さ N の整数列 A=(A1,A2,,AN)B=(B1,B2,,BN) が与えられます。
  • A に対して下記の操作を好きな回数(0 回でも良い)だけ行うことができます。
    1. まず、1iN を満たす整数 i を選び、Ai​ の値を反転する(元の値が 0 ならば 1 に、1 ならば 0 に変更する)。
    2. その後、操作のコストとして、Nk=1AkCk 円を支払う。
  • 上記の手順 2. におけるコストの計算には、手順 1. で変更が加えられた後の A を用いることに注意してください。
  • AB に一致させるために支払う合計費用の最小値を出力してください。

制約

  • 1N2×105
  • Ai,Bi{0,1}
  • 1Ci106
  • 入力はすべて整数

解法

(Ai,Bi) の組で Ci を4通りに分類する。i の順番は実質関係ないので、分類ごとにソートする。

20
1 1 1 1 0 0 1 1 0 0 0 1 0 1 0 1 1 0 1 0
0 0 0 1 1 1 0 1 1 0 0 0 0 0 0 1 0 1 0 0
52 73 97 72 54 15 79 67 13 55 65 22 36 90 84 46 1 2 27 8
↓
(0,0): 8, 36, 55, 65, 84
(0,1): 2, 13, 15, 54
(1,0): 1, 22, 27, 52, 73, 79, 90, 97
(1,1): 46, 67, 72

AB にするのに変更が必要なのは (0,1)(1,0) で、このコストを安くするには以下の操作が最適である。

  • (1,0) のコストが高い方から 1→0 にする
  • その後、(0,1) のコストが低い方から 0→1 にする
■上から順に操作

  組  Ci  コスト
(1,0) 97  (1,0)の未操作分+(1,1)の全て  1+22+27+52+73+79+90 + 46+67+72
(1,0) 90  (1,0)の未操作分+(1,1)の全て  1+22+27+52+73+79    + 46+67+72
(1,0) 79  (1,0)の未操作分+(1,1)の全て  1+22+27+52+73       + 46+67+72
  :
(1,0)  1  (1,0)の未操作分+(1,1)の全て                        46+67+72
(0,1)  2  (0,1)の既操作分+(1,1)の全て  2                   + 46+67+72
(0,1) 13  (0,1)の既操作分+(1,1)の全て  2+13                + 46+67+72
  :
(0,1) 54  (0,1)の既操作分+(1,1)の全て  2+13+15+54          + 46+67+72
                                     └──総コストはこれの総和───┘

コストは以下の和となる。「G0,0:=(0,0) の組に分類された Ci を昇順ソートした列」とする。(他の組も同様)

  • G0,1 の、先頭からの累積和の総和
  • G1,0 の、先頭からの累積和の総和(末尾を除く)
  • G1,1 の総和 × (|G0,1|+|G1,0|)

ところで、(1,1) は「一旦0にしておいて、後から1に戻す」とした方がコストが安くなる場合もある。(この操作を★とする)

(1,1) に分類されたある値 x に対して★の操作をする場合、 各 G を以下のようしてからコストを求めたものと言い換えられる。

  • G1,1 から x を除き、G0,1,G1,0 の双方に(昇順を保ちつつ)x を追加する

この変更後の各 Gi,j を、Gi,j とする。 コストは、G0,1,G1,0 の中で x 以下である値の個数をそれぞれ k,l として、

  • x を 1→0 にするとき、sum(G0,1[1k])+sum(G1,1) のコストがかかる
  • x を 0→1 にするとき、sum(G1,0[1l])+x+sum(G1,1) のコストがかかる
  • x が0になっている間、つまり k+l 回分の操作で、x のコストがかからなくなる

これで差分更新できる。
G0,1,G1,0 の個数と値のそれぞれに対する FenwickTree などで管理できる。

複数の値に対して★をするときも、1つずつ順番に差分を考えていけばよい。

ただ、G1,1 の全ての部分集合について★を試すわけにもいかない。
G1,1 に含まれる任意の2値 x,y について「x,y 以外の G1,1 の要素に対する★操作の有無が同じで、x にのみ★をおこなう場合と、y にのみ★をおこなう場合では、x にのみおこなった方が必ずよい」という関係性が成り立つなら、その順に試していくことができる。

直感的には「値が大きい順」がよさそうで、実際にそれは正しいが、証明しようとすると少し難しい。

ともかく、G1,1 の要素を大きい方から★にした場合のコストを差分で計算していき、その中でコスト最小のものが答えとなる。

ただ、差分の変化を表す関数は凸ではない?
凸なら「途中で差分が非負になったら、もう減少させられなくなったということなので打ち切る」が成り立つはずだが、それだとWAだった。
途中で非負になろうと、全てを試すことにしたらACした。この辺、ちゃんと原因を理解できてない。

Python3

D - Reverse Brackets

問題文

  • 長さ N の正しい括弧列 S が与えられます。
  • あなたは以下の操作を何度でも行うことができます。
    • S の連続部分文字列のうち、正しい括弧列であるものを 1 個選び、反転させる。
  • ここで Sl 文字目から r 文字目までからなる連続部分文字列を反転させるとは、以下を行うことを指します。
    • lir を満たす整数 i に対して同時に SiSl+ri が ( なら ) に、) なら ( に置き換える。(ここでの反転は、通常の反転の定義とは異なる点に注意してください。)
  • 操作終了時にあり得る S の個数を 998244353 で割ったあまりを求めてください。

制約

  • 1N5000
  • |S|=N
  • S は正しい括弧列

解法

この問題における反転とは、「文字列を画像としてみた場合に鏡映しにする」操作を指す。

()(()()) → (()())()

この操作で、対応する “(” と “)” のペアが変わることはない。

ところで、括弧列は木構造に変換することができる。
1つの頂点は、対応する “(” と “)” のペアを表す。
子は、親の括弧ペアの中に内包される括弧列を表す。子の並び順にも意味があり、括弧列に戻す際には左から順に並べるとする。

()(()()(()))  ↔    ○←ルートとなる仮想頂点
                  /  \
                 ○    ○
                     /|\
                    ○ ○ ○
                          |
                          ○

操作とは、この木においては以下のように言い換えられる。

  • 子を持つ任意の頂点 v を選ぶ
  • v の、1個以上の連続した子の並びを選ぶ
  • その子達の並びを(孫以降の並びも含め)反転させる

操作を繰り返せば、各頂点において子は好きな順に並べ替えることが可能である。
よって、v の子の集合を Cv として、|Cv|! 通りの並べ方がある……?

というのは誤りで、部分木が「同型」であるものは反転させても変わらない。

   ○               ○
 /  \           /  \       同型でない子の順番を入れ替えるのは、
○    ○     →  ○    ○      対応する括弧列も変化する
    /|\           /|\
   ○ ● ●         ○ ● ●
         |            |
         ○            ○

   ○               ○
 /  \           /  \       同型の子の順番を入れ替えても
○    ○     →  ○    ○      対応する括弧列は変わらない
    /|\           /|\
   ● ● ○         ● ● ○
         |               |
         ○               ○

木と括弧列の対応関係という意味では子の並び順も意味があると言ったが、 今は「子を並べ替えた上で同じにできる2つの部分木」を同一視したいため、 子の並び順は無視した同型判定を行う必要がある。

ある頂点の子の各部分木が、互いに同型であるかを判定したい。
同型のものは同じ値になるような上手いハッシュがあると嬉しい。
部分木のハッシュ化について、以下のような記事がある。

  • 1からの連番に対応する乱数 R1,R2,... を決めておく。(木の高さくらいまで)
  • 葉のハッシュ値は 1 とする。
  • 子を持つ頂点 v のハッシュ値は、
    • v の部分木の高さ(最も深い葉からの距離)を d とする。
    • 子の各ハッシュ値 h1,h2,...,hk に対し、(Rd+h1)(Rd+h2)...(Rd+hk)v のハッシュ値とする。

とすると、衝突確率が 1/mod の良いハッシュ値が得られるらしい。

これを元に木DPをおこなうことで答えが求められる。

  • DP[v]:=v 以下の部分木から操作により作れる、対応する括弧列の種類数
  • DP[v]=uCvDP[u]×()

同型を同一視した子の並べ方の個数は、例えばある頂点の6つの子のうち、3つが同型、2つがまた別の同型だった場合は 6!3!2! のように求められる。

同型判定は、今回は制約が小さく O(N2) が間に合うので、ハッシュ値でなく衝突の危険性のない固有の値で持っても間に合う。 例えば部分木を括弧列に戻して状態を表すとして、「子を並べ替えたものの中で辞書順最小のもの」を標準形とし、この標準形が同じかどうかで判定するなど。

Python3

E - Swap 0^X and 1^Y

問題文

  • 0 と 1 のみからなる長さ N の文字列 S,T と、正整数 X,Y が与えられます。
  • i=1,2,,N について、Si 文字目を Si で表します。
  • 「下記の操作 A と操作 B のどちらかを行う」ことを好きな回数( 0 回でも良い)だけ繰り返すことで、ST に一致させることができるかどうかを判定してください。
    • (操作 A)1iN(X+Y)+1,Si=Si+1==Si+X1= 0, Si+X=Si+X+1==Si+X+Y1= 1 を満たす整数 i を選び、Si,Si+1,,Si+Y1 をそれぞれ 1 に、Si+Y,Si+Y+1,,Si+Y+X1 をそれぞれ 0 に変更する。
    • (操作 B)1iN(X+Y)+1,Si=Si+1==Si+Y1= 1, Si+Y=Si+Y+1==Si+Y+X1= 0 を満たす整数 i を選び、Si,Si+1,,Si+X1 をそれぞれ 0 に、Si+X,Si+X+1,,Si+X+Y1 をそれぞれ 1 に変更する。
    • ※要は、X 個の 0 のカタマリと Y 個の 1 のカタマリをスワップする。

制約

  • 1N5×105
  • 1X,YN
  • S,T はそれぞれ 0 と 1 のみからなる長さ N の文字列
  • 入力はすべて整数

解法

D問題の木の同型判定の発想と似ているが、何らかの標準形を決めてやると、S,T の標準形が同じかどうかで判定できる。

標準形とは、「一定のルールで操作を繰り返したら、互いに変換可能な列は全部、同じ形に行き着くよ!」というようなルールを定めたときの、最終的に行き着く同じ形のこと。
本問題の場合、おこなった操作は逆順にたどることで元に戻せるので、 もし標準形が同じなら ST という経緯で変換できることがわかる。

まず、ST に含まれる“0”と“1”の個数が違えば絶対無理なのでNo。以下、同じとする。

連続している個数が重要なので、ランレングス圧縮する。
その上で、各連続成分の長さを X,Y で割った商と余りを d,m として、 [(0,d1,m1),(1,d2,m2),(0,d3,m3),...] のような形で持つようにしておく。
この列を、とりあえず「商余ランレングス圧縮列」とでもしておく。

01列に問題の操作をおこなったとき、商余ランレングス圧縮列に対してはどのような操作に言い換えられるか考える。

mi=0 である i は、自身を飛び越えさせることで、di1di+1 間で値を移すことができる。

X=3 Y=2
  0000    1111  00000000  ⇔   0      1111  00000000000
(0,1,1) (1,2,0) (0,2,2)     (0,0,1) (1,2,0) (0,3,2)
   ~               ~           ~               ~
                          ⇔ 0000000  1111  00000
                            (0,2,1) (1,2,0) (0,1,2)
                               ~               ~

その際、さらに mi+1=0 だと、統合してランレングス圧縮列の項数を減らせる。

  0000    1111  000000    111    →  0000000000   1111111
(0,1,1) (1,2,0) (0,2,0) (1,1,1)       (0,3,1)     (1,3,1)
   a       b       c       d            a+c         b+d

逆に、mi0,mi+10 の箇所で移動させても、間に m=0 である2つの項が間に新たにできるだけである。
特に、この操作により、外側の「…」の部分で操作できないものが操作できるようにはならない。

... 00000   11111 ...   ⇔  ... 00      11     000     111 ...
   (0,1,2) (1,2,1)           (0,0,2) (1,1,0) (0,1,0) (1,1,1)

おこなえる操作はこれで全てである。
なんとなく、標準形を作る際は統合できるものはしておいた方が良さそうなことを考えると、3番目の操作は意味が無く、実質2つである。

以上を踏まえて、次のような標準形が考えられる。

  • 項数を最小化する(m=0 が2つ以上続く箇所を統合する)
  • その上で、di だけを取り出した列を、辞書順最小化する(寄せられる0,1は右に寄せる)

全て mi=0 である場合が若干例外的で、“0000…001111..11” と “1111…110000..00” と どちらを標準形とするかが定義されないが、この場合は “0000…001111..11” で統一する。

それ以外の場合、項数最小化によって、m=0 が2つ続く箇所はなくなる。あとは

  0000    1111   00000   111111 0000000  →   0      1111     00    111111 0000000000000
(0,1,1) (1,2,0) (0,1,2) (1,3,0) (0,2,1)    (0,0,1) (1,2,0) (0,0,2) (1,3,0) (0,4,1)

のように、mi10,mi=0,mi+10 という箇所が残るので、 このような箇所の d をできるだけ右に寄せると形が一意に定まり、標準形として利用できる。

Python3

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