Loading [MathJax]/extensions/TeX/mathchoice.js

目次

キーエンス プログラミング コンテスト 2021 A,B,C,D,E問題メモ

キーエンス プログラミング コンテスト 2021

A - Two Sequences 2

A - Two Sequences 2

問題

解法

まず、i=1 の時は A1×B1 しかない。

次に Ci1 までが求まっているとして、Ci を考える。

Bi を使うか使わないかで分けて考えると、

この2つを比較して、大きい方を Ci とすればよい。

Python3

B - Mex Boxes

B - Mex Boxes

問題

解法

例えば 0,1,2 が入って 3 が入っていない箱があったら、後はどれだけ大きい数字を入れようとその箱のMEXは 3 である。

次のようにイメージするとよい。

ボールを値別にカウントした状態
ai
5:  oooooooo
4:  
3:  ooo
2:  ooooooooooo
1:  ooooo
0:  oooooooo

ここで、同じ値のボールを1つの箱に入れたり、たとえば3を0,1,2の入っていない箱に入れるのは無駄。
よって、値ごとに左詰にして、縦の1列が1つの箱に入れるボールと考えて問題ない。

すると、MEXを大きくするのに意味のあるボールは結局、小さい方から最小値をとっていった以下のようになる。

5:  
4:  
3:  ooo
2:  ooooo
1:  ooooo
0:  oooooooo

以下の手順を実装するとよい。

Python3

C - Robot on Grid

C - Robot on Grid

問題

解法

DPだけど、通らない空白マスは何でもよい、というのをどう扱うか。

まず、'R','D' などの変な制約のない経路数を求める時の定石として、上と左を足していく以下のDPはよく使われる。

これを踏まえて今回の制約を含めて経路数を考える。例えば右移動を考える(下移動も同様に考えられる)。

なんとなくのイメージとして、以下のようなことを行えばうまくいきそう。

■遷移元の文字が明らかなとき: 移動できるならそのまま, 移動できないなら0。

遷移元のマス
文字/経路数
   R/20     →   20
   D/20     →    0
   X/20     →   20

■遷移元が空白の時、右に移動できるのは'R','X'の2文字なので、2倍して遷移。(下移動も同様)

   _/20     →   40

しかし、このままでは「ある経路において通過しなかった空白マスは、何の文字でもよい」ことが考慮されていない。
答えに反映する際には経路数を 3^{通らなかった空白マス数} 倍する必要がある。

だが通過しなかった空白マス数は経路によって異なる一方、上記のDPではいろんな経路が混ざってしまっているので、うまく求められない。

逆に、答えとなる値を直接求めるようなDPを組むとよい。つまり、以下のDPを定義する。

DP[1][1]=3^{HW-K} としておく。

遷移元の文字が明らかなときはさっきと一緒。

遷移元が空白の時、その遷移ができる文字の種類数は 3→2 になるので、2/3 倍して遷移する。

   _/60     →   40

こうすると、うまく求められる。

\mod{} が素数なので、3^{-1} \times 2 はあらかじめ求めておくとよい。

Python3

D - Choosing Up Sides

D - Choosing Up Sides

問題

解法

ひらめき系? 構築方法自体は何かの過去問にあったような気もする。

条件は2つあるが、「全操作回数 ー 同じグループに入った回数 = 違うグループに入った回数」なので、 一方が満たされればもう一方も満たされるので、実質1個と考えてよい。

操作回数の下限を考える。

以下のような表で同じグループに入った回数を数えることを考えると

i\j  1  2  3  4
1    -
2    -  -
3    -  -  -
4    -  -  -  -

これは、C(2^{N-1}-1) = D(2^N-1) で一致することとなる。
逆に、2^N-12^{N-1}-1 の最大公約数は必ず1なので(ユークリッド互除法の最初の1手を考えるなど)、これ以上は小さくならない。

よって、少なくとも操作は 2^N-1 回は行われないと全てのペアを同数にできない。

これはあくまで下限なので、うまい振り分け方が無くて実際はもっとかかるかもしれないが、 もし 2^N-1 回でできるとしたら、同じグループに入る回数は 2^{N-1}-1 回、違うグループは 2^{N-1} 回ということになる。

どうせできるやろと当たりをつけて考えを進める。

N の答えを求めるにあたり、N-1 の答えは、半数の人のグループ関係については条件を満たしているので、これをうまく使いたい。

N=2 の答えがある。(見やすさのため、A=0,B=1 とおく)

1 2 3 4
-------
0 0 1 1
0 1 0 1
0 1 1 0

N=3 の答えを7回の操作で実現したい。とりあえず最初の3回について横に並べてみる。

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

こうすると、

2,3,4についても同じことがいえ、残る問題は、1と5、2と6、などのペアについての回数が多くなっていることとなる。

7回中、同じグループに入るのは3回。つまり1と5などは、これ以上同じグループに入らない。

ここで、1と5、2と6などがこれ以上同グループにならないようにすることを考えると、 「5~8 については0,1をまるっと反転させた結果を横に並べた」のを追加するとよさそう。

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

ひとまず1を中心に考える。

N=2 の時に、1と、2,3,4の関係は、同グループが1回、違うグループが2回だった。
つまり、N=3 の時に上の図で1と6,7,8との関係は、反転してないのとしたのを合わせるとちょうど目標の3回となる。

また、1と5についても前述のように同グループとなる規定回数を満たしている。

残るは、1,2,3,4について同グループとなった回数が1回ずつ少ないことなので、それを追加すると、どのペアとも3回ずつとなる。

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

今、1と同グループになる回数をもとに説明したが、対称性があり、2,3,4についても同じことがいえる。

もう少し規則的に

(問題文の条件違反ではあるが)全ての人が同グループになる仮想的な操作を追加すると、

1 2 3 4
-------
0 0 0 0  ←
0 0 1 1
0 1 0 1
0 1 1 0

これを田の字に並べ、右下だけ反転したもの(の最初の1行を無視したもの)が、次の答えとなる。

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

仮想的な操作の追加により、全てのペアについて同グループになった回数も、違うグループになった回数も 2^{N-1} 回で揃うので、 反転させたもの、反転させないものを繋げた時、各ペアにつき同じグループに属した回数がそれぞれ2倍になることが少しだけイメージしやすくなる、かも。

このように生成される行列は、アダマール行列と名前が付いていて、任意の2行が互いに垂直になるなどの性質があるらしい。

Python3

E - Greedy Ant

E - Greedy Ant

問題

解法

解説AC。区間DPで解ける。

大まかには美味しいキャンディから取っていくのがよさそうだが、それでは上手くいかない例が入力例1で(優しいことに)紹介されている。

4 3 1 🐜 2 1000 2000 3000

この例では、初手で1を取るのが最適となる。
うかうか3000などを取っていると蟻は2→1000→2000とどんどん美味しいキャンディの方に浸食してきてしまうが、 1を取ることで蟻は3→4の方向に誘導され、2の壁によって美味しいキャンディが後回しになる。

このような現象は初手にのみ起こるわけでも無く、途中で何度でも起こりうる。
また、蟻に隣接するもののみ考えていればいいわけでも無く、「あらかじめある範囲をまとめてごそっと取り除いておく」必要があるケースも考え得る。

N=20
11 10 9 8 7 3 6 2 1 5 🐜 4 1000 2000 3000 4000 5000 6000 7000 8000 9000

この例とか、何も対策しないと蟻は2ターン目に4の壁を突破して、以降は大きい数の取り合いとなり1000~3000は蟻に取られてしまうが、 まず1,2,3を取り除いてやることで、蟻は7→8→…の方に誘導され、人は大きい方から7つを悠々と取ることができる。

この「蟻がまだ4,5あたりにいる段階から、1,2,3を選んで取り除き始める」ことを予見するのはなかなか難しそうに思える。
また、蟻を誘導するためには何個か小さい数字を取ることになるので、その差し引きが、最大から貪欲した場合より得なのか損なのか変わってくる。
さらに、右側を堰き止めて、ある程度右側で大きい数字を取り尽くしたら、堰き止めてたやつを取って、今度は左で堰き止めて……という戦略もあり、 「どこで堰き止めるか」などを固定するような方法は、ちょっと切りが無さそう。

以下の事実を利用すると

なので、実際に蟻の行動範囲を管理などして、1,2あたりに到達したときに、「1,2を取った場合の最終値」と「無視して大きい方から取った最終値」の比較ができると、DPが組めそう。

ただ、十分準備ができないまま蟻が来てしまう場合もある。蟻がそこに到達するまでに、本当に取り除けるのか、位置関係によって変わってくる。

11 10 9 8 7 4 3 2 1 6 🐜 5 1000 2000 3000 4000 5000 6000 7000 8000 9000

5を取られる前に1,2,3,4を取り除くと蟻は7,8,...の方に誘導されるが、
実際には全部取り除くには間に合わず、どうしても5を先に取られてしまう。
(なのでこの場合は、大きい方から取っていくのがよい)

実際に取るのでは無く、「取る権利を1ターンごとにもらう」と考えると上手くいく。

両端に a_0=0,a_{N+1}=0 を番兵として追加しておくとよい。

たとえば、最初の例では、以下の3通りの選択肢がある。

           l     r
i  0   1 2 3     4    5    6    7   8
  (0)  4 3 1 🐜 2 1000 2000 3000  (0)

ただし、 はMAXで更新する操作とする。
k=0 の時は取ることはできないので、何もしない操作のみとなる。

DP[l][r] を求める際には、より外側に範囲を拡張させた DP[l-1][r]DP[l][r+1] が埋まっている必要がある。
つまり、r-l の大きい方から順に埋めていくとよい。

r-l=1 まで埋めたら、i=0~N につき、DP[i][i+1][1] が答えとなる。(人からターンが始まるので、最初に取る権利は1個存在している)

Python3

嘘解法

まるで別の解法というわけではないが、以下の入力で、

2
1 1000

以下の結果を返すコードが通ってしまった。(本当は最初も当然1000)

1
1000
1000

どうも、「最大まで取れる権利を貯めてから、一気に使う」ようなケースが見逃されているらしい。

DP配列で、取る権利の残数 k は、取る権利をためて意味のある \dfrac{N}{2}(切り上げ)個まで考慮すればよいはずと考えた。

この値を lim とすると、配列外参照をしないためには、DP[l][r][lim] の更新をどうすればよいか。

lim まで貯まったんなら、必ず消費する必要があるだろう」と誤った考えを元に、必ず l,r のいずれかを取る、というコードを書いてしまうと、上記の誤ったコードになる。

このDPの遷移は、1つ r-l が大きい前計算結果の k-1,k+1 を参照する。
そのため、(r-l の偶奇)XOR(k の偶奇) によって、遷移元・遷移先のつながりが2つに分かれてしまっている。
この断絶により、N の値によっては、正しい答えが求まらなくなってしまっていた。

ここでは、DP[l][r][lim] を「lim 以上」と見なして、取る権利をさらに蓄積することも可能にすると、問題なくなる。