ABC268をPythonで

# 0 はじめに
## 0-1 記事について
AtCoder Beginner Contest 268の解説です。
Pythonで書きます。
公式な解説とは異なる場合がありますがご了承下さい。
ミス等ございましたらコメント欄よりお知らせ頂ければ幸いです。

# 1 ABC268 解説
## 1-1 個人的な感想
今回は珍しくDが水Diffと、高難易度でした。
Diffは、AとBが灰前半、Cが茶後半、Dが水前半、掛け離れてEFが青後半といった感じです。
私は三冠でしたが、なんとBでFAを取ることが出来ました!!:wink::smirk:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2520428/93561910-d155-02ee-3b8e-b5f12ba2c79f.png)
独り佇んでいる緑コーダーが私です。
爆速三冠(8分半)したのでDiffは緑と水の間くらいで、Ratingは+3でした。
まあ満足といった感じです。
Dは通したかったですが...

## 1-2  A問題 Five Integers

### 問題
整数$A,B,C,D,E$が与えられます。
その中に何種類整数が含まれるか出力して下さい。

### 解説
`A,B,C,D,E`を配列として受け取ってソートするなど、色々な方法が考えられますが、ここでは`set`型を使います。
`set`型とは、集合を扱う型です。使い方についてはネットで調べてみましょう。

配列`[A,B,C,D,E]`を`set`型にした時の要素数(長さ)を出力すればよいです。

<details>
<summary>実装</summary>

```py
L=list(map(int,input().split()))
S=set(L)
print(len(S))
```
</details>


## 1-3  B問題 Prefix?
### 問題
英小文字のみから構成される二つの文字列$S,T$が与えられます。
$S$が$T$の接頭辞か判定して下さい。

### 解説
接頭辞とは、簡単に言えば「その文字列の先頭から`i`文字」です。
例えば、`pyth`は`python`の接頭辞ですが、`ppp`は`python`の接頭辞ではありません。

文字列`S`が`T`の接頭辞であるということは、「`T`の先頭`len(S)`文字が`S`と一致している」と言い換えられます。
それを判定すれば良いです。

ここでは、Pythonのスライスというものを使えば短く書けます。
スライスとは`S[a:b]`などといったもので、この場合、`S`の`a`文字目から`b-1`文字目と同じです。
スライスについては[こちらの記事](https://qiita.com/kaeruair/items/e7f1c08915839ce3c9b4)で詳しく説明されています。

<details>
<summary> 実装</summary>

```py
S=input()
T=input()
head=T[:len(S)]
if S==head:
  print("Yes")
else:
  print("No")
```
</details>

スライスは短く書けるのでとても便利ですが、スライスの一回当たりの計算量は、取得するスライスの長さを`k`として$O(k)$です。
TLEの可能性があるので、容易に`for`文の中で使わないようにしましょう。
(スライスでは、指定した範囲の中に範囲外参照となるものがあればそれを無視しますので、エラーによるREは心配しなくて結構です。)

<font color="Red">(2022/09/11 18:16追記)`startswith`関数というものがあります。
`A.startswith(B)`で、文字列Aが文字列Bで始まっているかを判定できます。即ち、今回の問題はstartswith関数を使えば工夫を施すことなく解けます。
@Kentuc-oh さん、ご指摘ありがとうございます。

<details>
<summary> startswith関数での実装 </summary>

```py
S=input()
T=input()
if T.startswith(S):
  print("Yes")
else:
  print("No")
```
</details>


## 1-4 C問題 Chinese Restaurant
### 問題
回転テーブルの周りに人$0$,人$1$,...人$N-1$が等間隔に並んでいます。
人$i$の前には料理$P_i$が置かれています。
あなたは次の操作を何回でも行う事ができます。
> 回転テーブルを一周の$N$分の$1$だけ回し、人$i$の前にあった料理を人$(i+1) mod N$の前に移動させる。

操作を終えた後に、人$i$は、料理$i$が人$(i-1)modN$か人$i$か人$(i+1)modN$の前にあると喜びます。
喜ぶ人数の最大値を求めて下さい。

### 解説
(喜ぶ人数の最大値をスコアとします)
愚直に、操作を$N$回行った時のそれぞれのスコアを求めるとどうなるでしょうか。
操作は$N$回行われ、$N$人の人を喜んでいるか判定するので、計算量は$O(N^2)$となり、TLEが出てしまいます。
どうすれば良いでしょうか。

考えてみれば、$N$通りのテーブルの状態に対して、$N$人の人が喜ぶ回数の合計は丁度$3N$回しかありません。
$N$人それぞれが、$3$通りの状態で喜ぶので当然ですね。

配列`cnt`を次のように定義します。
> `cnt[i]`は、テーブルが`i`回操作された時に喜ぶ人数

そして次のように工夫して計算します。
> $N$人それぞれについて、人`i`が喜ぶ時の操作の回数を求め、それぞれ`a,b,c`とする。`cnt[a],cnt[b],cnt[c]`を1増やす。

人`i`の喜ぶ時の操作回数は$O(1)$で求まり、それを$N$回計算するので計算量は$O(N)$となり、これなら間に合います。

最後に`L`の最大値を出力すればよいです。

<details>
<summary> 実装 </summary>

```py
N=int(input())
P=list(map(int,input().split()))

L=[0]*N
for i in range(N):
  a=(P[i]-i-1)%N; b=(P[i]-i)%N; c=(P[i]-i+1)%N
  L[a]+=1; L[b]+=1; L[c]+=1

print(max(L))
```
</details>

このように、愚直な計算だと$O(N^2)$掛るものをなんとか$O(N)$に抑えるという技法は高難易度でもよく出題されるので慣れておきましょう。
3問だけで低難易度ですが例題(?)を貼っておきます。
[ABC262-C Min Max Pair](https://atcoder.jp/contests/abc262/tasks/abc262_c)
[ABC210-C Colorful Candies](https://atcoder.jp/contests/abc210/tasks/abc210_c)
[ABC186-D Sum of difference](https://atcoder.jp/contests/abc186/tasks/abc186_d)

## 1-5 D問題 Unique Username

### 問題
以下の条件を全て満たす文字列$X$を一つ出力して下さい。
> $X$は以下の手順で得られる文字列である。
> > $N$個の文字列$S_1,S_2,...S_N$を好きな順番に並べたものを$S'_1,S'_2,...S'_N$とする。そして、$S'$のそれぞれの要素の間に一つ以上の`_`を並べたものを$X$とする。
>
> $3\leq|X|\leq16$である。
> $X$は、$M$個の文字列$T_1,T_2,...T_M$のいずれにも一致しない。

そのような文字列が存在しないならば`-1`を出力して下さい。

### 解説
$X$としてあり得る文字列を全探索してもよさそうです。
1つずつあり得る文字列を探索し、もし$T$に含まれず、条件を満たしていればそれを出力して終了、$T$に含まれるならば続行、という風に全探索できます。

<details>
<summary> 何故全探索できるか? </summary>

見ている文字列が$T$に含まれず条件を満たしていれば出力して、含まれれば続行する訳ですが、その「探索」は最悪の場合でも$M$回しか行われません。
「探索の続行」は、今見ている文字列が$T$に含まれている時に限って行われる訳ですから、そのような文字列が存在すれば、遅くとも$M+1$回目には目当ての文字列を見つけられます。
そのような文字列が存在しない場合でも、探索は$M+1$回以下で済みます。

</details>


では、あり得る文字列をどのように全探索すればいいか考えてみましょう。
まず、$N$個の文字列を並べる、というのは`itertools.permutations`を使えばよいです。
問題は、$N$個の文字列の間の`_`の個数の組の探索です。


個数の組は、$N$個の文字列の順番に関係ありません。
又、個数の組は次のような条件を満たしている必要があります。
> 個数の組を$L$とすると、$L$の総和は、$16-(Sの文字列の長さの総和)$以下
> $L$の要素は全て$1$以上

このような「個数の組」としてあり得る組を探索する訳ですが、メモ化再帰を使って実装します。
`DFS`の引数に、`残りの'_'の個数`と`探索中のindex`を持たせます。

<details>
<summary> 個数の組の探索 </summary>

```py
memo=[[[] for _ in range(17)] for _ in range(17)]
def DFS(rem,depth):
  if memo[rem][depth]:
    return memo[rem][depth]
  if rem<=0:
    return []
  if depth==0:
    return []
  if depth==N-1:
    return [[i] for i in range(1,rem+1)]
  ret=[]
  for i in range(1,rem+1):
    for j in DFS(rem-i,depth+1):
      ret.append([i]+j)
  memo[rem][depth]=ret
  return ret
 
su=16-sum(len(i) for i in S)
L=DFS(su,1)
```

</details>
説明が煩雑となってしまうのでご自身で理解して頂ければ幸いです。ごめんなさい...

これで個数の組を全列挙できたので全探索しましょう。
`permutations(S)`と`L`で二重ループを回します。
$T$に含まれずかつ条件を満たすものがあれば出力して終了、そうでないならば続行します。
あり得るものが見つからなければ`-1`を出力すればいいです。

しかしこれだけではコーナーケースに引っ掛かってしまいます。
何が罠となっているのでしょうか。
その罠とは、$X$の文字数制限です。
例えばこのような時にも`-1`が出力されてしまいます。
<details>
<summary> コーナーケース</summary>

```
1 0
aaaaa
```
</details>

$N=1$の時、`_`の「個数の組」が一つもないので、二重ループでの全探索で何も探索できていないことが原因です。
このコーナーケースはこのように`if`文を書けば解決できます。
<details>
<summary> コーナーケースKiller </summary>

```py
if N==1:
  if 3<=len(S[0])<=16 and S[0] not in T:
    print(S[0])
  else:
    print(-1)
```
</details>

最後に全体の実装を載せます。
特に注意すべき点はありませんが、`T`に要素が含まれるかの判定を頻繁に行うので、`T`を`set`型として受け取っておきましょう。
要素検索は、`set`だと$O(1)$ですが`list`だと$O(M)$掛ってしまうので注意。
また、`sys.setrecursionlimit(10**9)`で再帰呼び出し制限を緩和(?)するのも忘れずに。

<details>
<summary> 全体の実装 </summary>

```py
import sys
sys.setrecursionlimit(10**9)
from itertools import permutations

N,M=map(int,input().split())
S=[input() for _ in range(N)]
T=set([input() for _ in range(M)])

if N==1:
  if 3<=len(S[0])<=16 and S[0] not in T:
    print(S[0])
  else:
    print(-1)
  exit()

memo=[[[] for _ in range(17)] for _ in range(17)]
def DFS(rem,depth):
  if memo[rem][depth]:
    return memo[rem][depth]
  if rem<=0:
    return []
  if depth==0:
    return []
  if depth==N-1:
    return [[i] for i in range(1,rem+1)]
  ret=[]
  for i in range(1,rem+1):
    for j in DFS(rem-i,depth+1):
      ret.append([i]+j)
  memo[rem][depth]=ret
  return ret

su=16-sum(len(i) for i in S)
L=DFS(su,1)

for p in permutations(S):
  for l in L:
    now=""
    for i in range(N-1):
      now+=p[i]
      now+=l[i]*"_"
    now+=p[-1]
    if not 3<=len(now)<=16:
      continue
    if not now in T:
      print(now); exit()

print(-1)
```
</details>

中々実装が重く、コーナーケースも意地悪な問題でした。
私も解けなかったので、集中して実装できるようにしたいです。

# 2 さいごに
最後までお読み頂きありがとうございます。
今回のD問題は最近では中々珍しい問題だったらしいです。
C問題の、$O(N^2)$を$O(N)$にするテクニックは可也重要なので身に付けておきましょう。
以上です。
ありがとうございました。
よい競プロライフを!!
~~いいねお願いします...(切実)~~