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

AtCoder Regular Contest 199 (Div. 1) A,B,C問題メモ

A - Flip Row or Col 2

問題文

  • N×N 行列 A=(Ai,j) (1i,jN) および長さ N の整数列 R=(R1,R2,,RN),C=(C1,C2,,CN) が与えられます. A の各要素は 0 または 1 であり,R,C の各要素は 0 以上 N4 未満です.
  • あなたは好きな行・列を選んで、その行・列全体をフリップする(0と1を置き換える)ことができます。
  • フリップする行と列をうまく選ぶことで操作後の A が以下の条件を満たすことが可能かどうか判定してください。
    • i=1,2,,N に対して,Ai 行目の要素の総和 Nj=1Ai,jRi である.
    • j=1,2,,N に対して,Aj 列目の要素の総和 Ni=1Ai,jCj である.
  • 可能ならばフリップする列と行の組の一例を出力してください.
  • T 個のテストケースが与えられるので,それぞれについて答えてください.

制約

  • 1T105
  • 1N1000
  • Ai,j{0,1}
  • 0Ri,Cj<N4
  • T,N,Ri,Cj は整数
  • 1 つの入力ファイルに含まれる N2 の総和は 106 以下

解法

Ri,Cj<N4 という制約がどう見ても際立っているのだが、どう活かせばよいかわからなかった。

実は、R,C との合致以前に、そもそも行・列フリップを繰り返して作れる「全ての行・列について総和が N4 未満」となるような盤面は、高々1通りしかない。

略証

よって、「全ての行・列について総和が N4 未満」という状態を1つ作れれば、 それが R,C と合致するかを調べればよい。

具体的な構築方法も、N4 を上手く使わないとなかなか思いつきづらいが、

まず、行に操作して、1列目を全て $0$ にする。
10101      01010
11110      00001
01111  →  01111
10011      01100
00110      00110

こうすると、今後、行について操作するのは N4 回未満に限られる。 (1列目が N4 以上になってはいけないので)

よって、この時点で1列に'1'が N2 個以上ある列は反転させなければいけないし、N2 未満の列は反転させてはいけない。フリップする列が一意に決まる。

 vvv
01010      00100
00001      01111
01111  →  00001
01100      00010
00110      01000

その後、N4 個以上ある行をフリップすればよい。
A が「全ての行・列について総和が N4 未満」とすることが可能であれば、そのようになっている。

00100      00100
01111      10000
00001  →  00001
00010      00010
01000      01000

Python3

B - Adjacent Replace

問題文

  • 長さ N の非負整数列 A=(A1,A2,,AN) および非負整数 K が与えられます.
  • 以下の操作を 104 回以下行うことで A1=K にすることが可能かどうか判定してください.
    • 整数 i (1iN1) を選ぶ.x=AiAi+1 としたとき,Ai,Ai+1 をそれぞれ x で置き換える.
    • ここで, はビット単位 XOR 演算です.
  • 可能な場合,そのような操作手順を一つ出力してください.
  • T 個のテストケースが与えられるので,それぞれについて答えてください.

制約

  • 1T50
  • 3N60
  • 0Ai,K<260
  • 入力は全て整数

解法

基本的な方針は早々に分かって、後は場合分けを頑張るだけやし時間かければいけるやろ!で挑戦したけど、 個々の場合分けに対する実装が遅すぎて間に合わなかった。どうやって素早く実装できるの。。。

使うindexの決定

まず、隣接は関係なく「A から好きな要素を抽出して、総XORで K を作ってください」という問題は典型であり、 XOR基底を求めることで可能不可能を判定できる。

基底を求める際に、「この基底は、元の A のどれとどれをXORして作られたもの」という情報も計算しておけば、 具体的な構築方法の一例も求められる。

これにより、そもそも A をどのように組み合わせても K を作れない場合は“No”。
以下、作れるとする。

上記の方法で見つけた K の構築方法の1つを I=(i1,i2,...,im) として元の問題に戻ると、 「操作を繰り返して、A1 を、Ai1Ai2...Aim にする」とよいことがわかる。

じゃあ、実際に可能か?を考える。

使わない要素が2つ連続している箇所がある場合
a   b   c   d   e   f    o: 使う
o   x   x   o   x   o    x: 使わない

構築方法の一例
a   b   c   d   e   f
      x
a  bc  bc   d   e   f    同じ箇所に2回操作することで
      x                  隣接2要素を0にできる
a   0   0   d   e   f
          x              dをずらして d,f の間を2つ以上開ける
a   0   d   d   e   f
              xx         2回の操作で0にする
a   0   d   0   0   f
          x   x   x      右から順に
a   0  df  df   f   f
  x   x                  右から順に
adf adf df df   f   f

使わない要素が2つ連続している箇所があったら、その2つを0にして、使う要素を移動できる。
他に使わない要素が1つしか連続していない箇所があっても、↑の移動で順にずらして、2つ以上の連続にすることができる。

使う要素が2つ連続している箇所がある場合
a   b   c   d   e   f    o: 使う
o   x   o   o   x   o    x: 使わない

構築方法の一例
a   b   c   d   e   f    ともに使うc,dを1つにまとめると、
          x              どちらか一方は「使わない要素」と見なして良くなる
a   b  cd  cd   e   f
      xx
a   0   0  cd   e   f    後の流れは、1つめの例の3行目以降と同様

最初、使わない要素が2つ連続してる箇所が無い場合でも、使う要素が2つ以上連続してる箇所があれば、 その連続箇所の端の2要素を1つにまとめると、使わない箇所の2つの連続を作ることができる。

どちらも無い場合
a   b   c   d   e   f    o: 使う
o   x   o   x   o   x    x: 使わない
  or
x   o   x   o   x   o

使用・不使用が最後まで交互に並ぶ状態のみ該当する。

この場合は不可能。どうしても初手で o と x が混ざり合ってしまい、それを分離する方法が無い。

ただし、I の他にも K を作り出せるindexの組がある場合は、そちらでできる可能性がある。

他に K を作り出せる方法があるかどうかは、XOR基底に“0”が存在する(という言い方は厳密には正しくない? まぁ、掃き出し法の過程で最後に余った0が出てくる、という意味)のと同値。
“0”を作るためのindexの組の1つ J=(j1,j2,...) と、I の、集合としてのXOR(片方にのみ含まれる要素からなる集合)が I の他に K を作り出せる方法の1つとなる。

ただし、作り方が2通りあっても、どちらもダメ、という場合もあるので注意。

6 21             1,4,16 か 2,8,31 でしか 21 を作れないが
1 2 4 8 16 31    どちらもダメ

不可能な状態は2通りしかないので、XOR基底に“0”が2個以上存在すれば、4通り以上の K を作る方法があり、必ず可能な状態にできる。
高々2通りの J を試せばよい。

構築方法

まず、コーナーケースとして、使う要素が0個の状態を考える。
これはつまり、K=0 ということなので、i=1 に2回操作すれば簡単にできる。

以降、使う要素は1個以上あるとする。

K を作るための要素 I=(i1,...,im) に対し、右端の Aim から降順に、I に含まれるindexの要素だけをXOR合成しながら A1 まで運んでいきたい。
ひとまず、A1 まで運ばずとも「使う要素を全て合成し、i1 の位置に K を作る」ところまでを目指す。

そのために、「im の右に、使わない要素(バッファ)が2個以上ある」初期状態を目指す。
使わない要素が1個しか連続していない箇所を上手く乗り越えるには、右側に2個のバッファを要するからである。 なお、間が0個、2個以上、の場合はバッファは不要。

右から合成していくための初期状態として目指す形

○   ... o x x ]    o: 最も右で使う要素
×   ..... o x ]    ]: 配列の終わり
×   ....... o ]
b を a にXOR合成するために、その間に使わない要素が何個あるか、で場合分け

0個
...  a  b             そのまま操作すればよい
... ab ab

1個
...  a  x  b  x  x    a,b:使う要素
...  a  x  b  0  0    x:使わない要素
...  a  x  b  b  0
...  a  0  0  b  0    b の右にバッファが2個あれば
... ab ab  b  b  0    b を、間にある x を含めず a と合成できる

2個以上
...  a  x  x  x  b    間の使わない要素を全て0にした上で、
...  a  0  0  0  b    降順に操作して b を運んでいける。
... ab ab  b  b  b

最初から右側にバッファが2個以上あればよいが、そうで無い場合、事前に調整して作っておく必要がある。
調整の方法を考える。

使う要素のうち、最も右にあるもののindexを im とする。バッファが2個未満という前提で、im=N1 または im=N である。
最も右にある使わない要素の連続箇所を u、使う要素の連続箇所を v とする。

どちらも存在しない場合はあり得ない。(使用不使用が交互に並ぶ不可能配置となり、判定の時点ではじかれているはずなので)

・u が存在する場合:
    u 以降の使う要素を、左から順番に左に寄せていける。
    ... x x a x b x c    a,b,c: 使う要素
    ... 0 0 a x b x c    x: 使わない要素
    ... a a a x b x c
    ... a 0 0 0 b x c
    ... a b b b b x c
    ... a b 0 0 0 0 c
    ... a b c c c c c    ←最も左以外のcは使わない要素なので
    ... a b c x x x x    ←このように見なせ、条件を満たす。
    
    なお、a,b,c をどこまで左に移動させるかは、
    上記の例のように「隙間無く並べる」だけでなく
    「この時点で合成までしてしまう」「それぞれ2個ずつ左に寄せる」などでもよい。お好みで。

・u が存在せず、v が存在する場合
  ・v が、最も右の使う要素を含む場合
    ・右にバッファが1個ある場合
        ...  a  b  x ]
        ... ab ab  x ]    ←この、右側のabは使わない要素なので
        ... ab  x  x ]    ←このように見なせ、条件を満たす。

    ・バッファが0個で、使う要素が3つ以上連続している場合
        ...  a   b   c ]
        ...  a  bc  bc ]    末尾から順に操作していけばよい。
        ...abc abc  bc ]
        ...abc   x   x ]    ←このように見なせ、条件を満たす。
        
    ・バッファが0個で、使う要素の連続が2個のみの場合
        ...  x  b  c ]
        ...  x bc bc ]    ちょっとややこしいが、
        ...  0  0 bc ]    一旦右にまとめてから左に移動させる
        ... bc bc bc ]
        ... bc  x  x ]
  
  ・v が、最も右の使う要素より左にある場合
    ...  a  b  x  c  x  d
    ... ab ab  x  c  x  d
    ... ab  x  x  c  x  d    ←このように見なせるので、uが存在する場合に帰着

これで事前調整して、末尾から I に含まれるindexだけを合成していき、i1 の位置に K を作れたとする。
これを i=1 まで運ぶ作業も、だいたいは上記と同じ感じで、

i1=1
既に完成している

i=2
x  o  x  x ...    N=3 の時は判定問題の時点で不可能とはじかれているため、
x  o  0  0 ...    N>=4 で、使う要素の右側には2個以上の使わない要素の連続がある。
x  o  o  0 ...
0  0  o  0 ...    一旦右に待避させることで、
o  o  o  0 ...    左側に2個以上の使わない要素の連続を作る。

i1>=3
x  x  x  o ...    2個以上の使わない要素の連続があれば、
0  0  0  o ...    操作を繰り返して0にできるので、
o  o  o  o ...    後は降順に操作して、使う要素を運んでいける。

これで完成である。

操作回数を見積もる。以下の場合にかさみそう。

  • u が存在する場合」で、uA の左端にあり、各要素を左端にはるばる集める場合
  • ほとんど交互で、右端だけバッファが2個ある状態

前者は、使用不使用が交互に並ぶ中の使う要素(N2 個程度)の各 Ai それぞれについて、O(i) 個の使わない区間を 0 に舗装してから O(i) かけて運ぶ必要がある。定数倍を考慮すると、2i+2 以内には可能なので、これを飛び飛びで総和を取っていくと、だいたい N22 程度と見積もれる。

後者は、1つ使わない要素を乗り越えるのに 8 回の操作が必要なので、4N 程度と見積もれる。

いずれにしても、十分余裕を持って 104 回以内に収めることができる。

Python3

C - Circular Tree Embedding

問題文

  • この問題では,頂点に 1,2,,N の番号が付けられ,辺に番号が付いていない N 頂点の木を単に N 頂点の木と呼ぶことにします.
  • N 頂点の木 T および (1,2,,N) の順列 Q=(Q1,Q2,,QN) が次の条件を満たすとき,順列 Q は木 T の 良い順列 であると呼びます.
    • 円周上に N 個の点 1,2,,N がこの順で反時計回りに等間隔で配置されている.
    • T の各辺 {u,v} に対して 2Qu,Qv を結ぶ線分を書き込む.
    • このとき,書き込まれた N1 本の線分のうちどの 2 本を選んでも,それらは端点を除いて共有点を持たない.
  • M 個の (1,2,,N) の順列 P1=(P11,P12,,P1N),P2=(P21,P22,,P2N),,PM=(PM1,PM2,,PMN) が与えられます.
  • P1,P2,,PM が全て良い順列となるような N 頂点の木の個数を 998244353 で割った余りを求めて下さい.

制約

  • 2N,M500
  • P1,P2,,PM(1,2,,N) の順列
  • 入力は全て整数

解法

問題設定を理解するのがまず難しい。

満たすべき条件

頂点番号は 0-indexed に読み替える。

P の逆順列(Q[Pi]=i となるような Q)を求めて、Q の順に円周上に並べる。

i  0 1 2 3 4 5 6 7
P  5 2 4 6 7 0 1 3
Q  5 6 1 7 2 0 3 4
    ↓               他のPも同様に
  ⑤ ⑥             ③ ④      ⓪ ②
④     ①         ②     ⑤  ③     ④
③     ⑦         ⓪     ⑦  ⑦     ⑤
  ⓪ ②             ⑥ ①      ① ⑥

N 頂点の木に対して「円を1つ選び、周上の対応する番号の位置に頂点を置いていく」操作を考える。
この時「どの円を選んで操作しても、辺が交差しない」ような木の個数が答えとなる。

ひとまず、最大の⑦を根とした木で考える。
(通常は⓪を根とすることが多いが、今回は後述のDPで⑦を根とした方がやりやすい)

条件を満たす木は、「木の全ての部分木について『含まれる頂点集合がどの円周上でも連続している』」ことが必要十分条件となる。

      ⑦       部分木の頂点集合(1頂点のみのを除く)
    /  \       ②⓪③
   ⑤    ⑥      ⑤②⓪③④
  /  \   |      ⑥①
 ②  ④  ①      ⑦⑤②⓪③④⑥①
 /\            これらは全て、どの円周上でも連続している
⓪③           (集合内での順序は円ごとに変わってよい)
               →この木は条件を満たす

ある部分木をなす頂点集合 U の間に、部分木の外にある頂点 v があり連続が途切れていたら、 vU のどの頂点とも結ばれない(それ以外の頂点と結ばれる必要がある)のに、U 同士は結ばれる必要があり、どうしても交差してしまう。

U = {0,1,2,3},   v = 4

   :        :    ⓪①②③が部分木を為す場合、
   ⓪--------③    どうしても(⓪①)と(②③)の間は結ばれることになり、
     ①----②'     ④は他の頂点に結びに行けなくなる。
        ④

数えあげ

ある部分木に対し、含まれる頂点集合がどの円周上でも連続しているものを「条件★を満たす」とする。
また、ある「条件★を満たす部分木」の集合のうち、どの円周上でも含まれる頂点が連続しているものを「条件☆を満たす部分森」と呼ぶことにする。

たとえば上例の木では、②⓪③と④はそれぞれ条件★を満たす部分木で、 さらに②⓪③④も各円周上で連続しているので、{②⓪③, ④}は「条件☆を満たす部分森」である。

根を除く N1 頂点集合から作れる「条件☆を満たす部分森の個数」が答えとなる。
この森のそれぞれと根を結ぶことで木が完成する。
(全 N 頂点からなる条件★を満たす部分木を数えると、構造が同じでも根を区別するため、重複が発生する)

順列上で連続していることが条件であることから、区間DPで数え上げることを考える。
ただ、「区間」が何を示すものなのか、まだちょっと曖昧である。

各順列 Q に以下の改変を行う。

  • 1つめの順列を、頂点を置換して、⓪①②…⑦ の順にする
  • 残りの順列にも同じ置換を施した上で、⑦が最後になるように巡回シフトする
i    0 1 2 3 4 5 6 7
P1   5 2 4 6 7 0 1 3
Q1   5 6 1 7 2 0 3 4
     ↓                 置換
Q'1  0 1 2 3 4 5 6 7

Q2   3 4 5 7 1 6 0 2
     ↓                 同じマッピングで置換
     6 7 0 3 2 1 5 4
     ↓                 7が末尾になるように巡回シフト
Q'2  0 3 2 1 5 4 6 7

1つめの順列をこうすることで「条件★を満たす部分木」となるためには「頂点番号が連続するものからなる」が必要条件となり、 「区間 [l,r)」を「頂点番号 l,l+1,...,r1 の頂点集合」と自然な形で定義することができる。

また円環で考えると、⑦の位置が中ほどにあると順列の端を跨ぐ部分の処理に困るが、 端にすることでそこで切り開いて数列と考えてよくなる。
以下、円周上での連続は、数列上での連続という判定に置き換えられる。

まず、以下を求める。

  • ok[i,j]:=i,i+1,...,j1 が、全ての順列上で連続しているか(bool)

これは、全ての順列の全ての区間について調べて間に合う。

まず、ok[i,j] を全て True で初期化する。
また、着目中の順列 Q における値 k のindexを、Rk とする。

順列 Q 上で (l,l+1,...,r1) が連続しているかの判定を行いたい場合、 max(Rl,Rl+1,...,Rr1)min(Rl,Rl+1,...,Rr1)=rl1 かどうかを調べればよい。
違反していたら ok[l,r] を False にする。
l から r=l+1,l+2,...,N と1つずつ拡張していけば、1つ区間を伸ばすのに O(1)、全体 O(N2M) で調べられる。

これをもとに、以下のDPをおこなう。

  • DP1[l,r]:= 区間 [l,r) の頂点からなる、条件★を満たす部分木の個数
  • DP2[l,r]:= 区間 [l,r) の頂点からなる、条件☆を満たす部分森の個数
    • それぞれは、部分木の根がどれかも区別する。

初期状態は以下のようにしておく。
長さ0と長さ1の区間は、1通りであることは自明なので、最初から埋めておく。

  • DP1[i,i]=DP1[i,i+1]=1
  • DP2[i,i]=DP2[i,i+1]=1
  • その他は0

長さ2以上の区間 [l,r) について更新を考える。これより短い区間のDPは全て埋まっているとする。

ok[l,r]=True であれば、単一の部分木が作れる。
根を m=l,l+1,...,r1 とした場合の部分木の個数は、DP2[l,m]×DP2[m+1,r] となる。

また、複数の部分木からなる部分森については、1つめの部分木がどこまで続くかを全探索する。

  • DP1[l,r]=r1m=lDP2[l,m]×DP2[m+1,r](※ok[l,r]=True の場合。Falseの場合、0
  • DP2[l,r]=DP1[l,r]+r2m=l+1DP2[l,m]×DP2[m,r]

としてDPを埋めていき、DP2[0,N1] が答えとなる。

Python3

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