AtCoder Regular Contest 148 B,C,D問題メモ
B - dp
問題
'd' と 'p' のみからなる文字列 $S$ が与えられる
以下の手順で得られる文字列を「$T$ を180度回転させた文字列」とする
1回だけ、$S$ の好きな部分文字列を180度回転させたものに置き換えることができる(しなくてもいい)
作れる辞書順最小の文字列を答えよ
$1 \le |S| \le 5000$
解法
なるべく先頭から続く'd'を多くするのが正義。
「最初に出てきた'p'」の位置を、回転させる区間の左端 $L$ に設定して問題ない。
dddppdppppd
L
区間の右端を'p'にして回転させることで、$L$ からさらに1個以上の 'd' を続けさせることができる
最初の'p'より右に設定したら、先頭の'd'の個数は初期のまま変わらないので、↑より必ず少なくなる
最初の'p'より左に設定しても、追加で続けさせることのできる個数は変わらないので、単にせっかくの先頭の'd'を削るだけで意味が無い
区間の右端 $R$ は、以下の2通りが考えられる。
ddd ppp d pppp d pp dd pppp d
L 2 1 1
2.が見落としがち?だが、こいつだけは「区間全てを'd'にする」ことができるので、その後に続く'd'を含めることができ、最適となる場合がある。
ddd pp dddddddddddddddddddddddddddddddd pppp d pppp d
~~
ここだけをひっくり返すのがいい
可能性のある箇所を全て試しても、その箇所が $O(|S|)$、1回の判定で $O(|S|)$ なので、$O(|S|^2)$ で通る。
Python3
from itertools import groupby
def solve(n, s):
l = -1
rrr = []
max_p = 0
special_r = -1
i = 0
for idx, (c, itr) in enumerate(groupby(s)):
lt = len(list(itr))
if c == 'p':
if l == -1:
l = i
special_r = i + lt
elif max_p == lt:
rrr.append(i + lt)
elif max_p < lt:
rrr = [i + lt]
max_p = lt
i += lt
ans = s
rrr.append(special_r)
for r in rrr:
tmp = [s[:l]]
for c in reversed(s[l:r]):
if c == 'd':
tmp.append('p')
else:
tmp.append('d')
tmp.append(s[r:])
ans = min(ans, ''.join(tmp))
return ans
n = int(input())
s = input()
print(solve(n, s))
C - Lights Out on Tree
問題
頂点 $1$ を根とした、$N$ 頂点の木が与えられる
各頂点には1枚ずつコインが置かれている
以下の操作を好きなだけ行える
$Q$ 個のクエリに答えよ
頂点集合 $S=\{v_1,v_2,...\}$ が与えられる
コインの初期状態として、$S$ に含まれる頂点は表、他は裏から始める
全てのコインを裏にするために必要な最小操作回数を求める
$2 \le N \le 2 \times 10^5$
$1 \le Q \le 2 \times 10^5$
($Q$ 回のクエリにおける $|S|$ の和) $\le 2 \times 10^5$
解法
言い換え
簡単な場合として木でなく直線だった時を考える。操作は以下のようになる。
このような操作は、隣り合う箇所との差分(同じか異なるか。XORと捉えてもよい)に注目すると「そこと1つ前の差分の状態のみを変更し、他は変えない」といえる。
1 2 3 4 5 6 7
(1) 1 0 0 0 1 1 0 0:表 1:裏
^ ^ ^
従って操作回数は「表と裏が隣接する部分」の個数に一致する。
今回、最終的には全て裏にしないといけないが、
頂点 $1$ の親には必ず裏である頂点 $0$ が隠れていると考えると上手く表現できる。
木に戻しても似たようなことが言えて、1回の操作は
「自身と親との差分の状態のみを変更する」ので、答えはそのような箇所の個数と一致する。
答えの求め方
上記を踏まえると、以下のようなアルゴリズムが考えられる。
正しい答えは求まるが、これだとTLEとなる。
もし $v$ がめっちゃ多くの頂点と隣接するハブ頂点で、$Q$ 回のクエリでそればかり指定されると、
調べる $u$ の個数が $O(NQ)$ となってしまうからである。
高速化する。
ハブ頂点をたとえば「隣接頂点が $\sqrt{N}$ 個以上の頂点」として分類する。
さらに、各頂点の隣接リストも「ハブ頂点」「ハブ頂点以外」の2つに分類しておく。
木におけるハブ頂点の個数は少ないので、隣接する「ハブ頂点」はどの頂点も高が知れている。
隣接する「ハブ頂点以外」が、ハブ頂点は多い状態となっている。
$S$ に含まれる頂点 $v$ につき、
$v$ がハブ頂点なら
$v$ がハブ頂点以外なら
こうすると、1回のクエリで辿る上限が $O(|S|\sqrt{N}))$ となり、クエリ全体の $|S|$ の総和を $M$ として $O(M \sqrt{N})$ となる。
(後から考えると、親だけ別に管理して「隣接頂点はとりあえず全て足す」「親も $S$ に含まれたら双方で足しすぎなので2引く」でよかったね)
Python3
n, q = map(int, input().split())
links = [set() for _ in range(n + 1)]
ppp = list(map(int, input().split()))
for i, p in enumerate(ppp, start=2):
links[i].add(p)
links[p].add(i)
links[0].add(1)
links[1].add(0)
thr = max(500, int(n ** 0.5))
is_hub = [len(l) > thr for l in links]
hub_links = [set() for _ in range(n + 1)]
for i in range(n + 1):
rms = set()
for u in links[i]:
if is_hub[u]:
rms.add(u)
hub_links[i].add(u)
for i in range(n + 1):
links[i] -= hub_links[i]
for _ in range(q):
m, *vvv = map(int, input().split())
v_set = set(vvv)
ans = 0
for v in vvv:
if is_hub[v]:
ans += len(links[v])
for u in hub_links[v]:
if u not in v_set:
ans += 1
else:
for u in links[v]:
if u not in v_set:
ans += 1
for u in hub_links[v]:
if u in v_set:
ans -= 1
else:
ans += 1
print(ans)
D - mod M Game
問題
黒板に $2N$ 個の整数 $A_1,...,A_{2N}$ がある。また、整数 $M$ がある
先攻と後攻でゲームをする
ルール
黒板の数が全て無くなるまで、「残っている中から数を $1$ 個選び、消す」ことを繰り返す
終了時点で、「先攻が消した数の和」と「後攻が消した数の和」それぞれの $M$ で割ったあまりを計算する
両者が一致していれば後攻の勝ち、それ以外は先攻の勝ち
どちらが必勝か答えよ
$1 \le N \le 2 \times 10^5$
$2 \le M \le 10^9$
$0 \le A_i \lt M$
解法
最終局面を考える
残った数が2つになった時、先攻がどちらを取っても負けてしまうのは、どのような状況だろうか?
これまでの和を $a,b$、残っている数を $c,d$ とする。
$a+c \equiv b+d \mod{M}$ かつ $a+d \equiv b+c \mod{M}$ が同時に成り立つ。
整理すると、$2c \equiv 2d \mod{M}$ となる。このような状況に後攻は持ち込みたい。
$M$ が奇数の時
$2c \equiv 2d \mod{M}$ となる $(c,d)$ を考える。
$2c,2d$ は $2M$ 未満なので、$c \neq d$ の場合、$2c+M=2d$ である(大小関係がどちらというのはあるが)。
$M$ が奇数なら偶奇が合わないので明らかに矛盾しており、結局、$c=d$ の場合しか無いことがわかる。
従ってそれまでの和 $a,b \mod{M}$ も同じである必要があり、1つ少ない状態に帰着できる。
再帰的に考えると、後攻は「全ての数が偶数個ずつある場合のみ、先攻と同じ数を取り続けることで勝てる」ことがわかる。
$M$ が偶数の時
$c \neq d$ の場合でも $2c \equiv 2d \mod{M}$ に解があり得る。
$M=6$ のとき、$(0,3),(1,4),(2,5)$ という、$(i, i+\frac{M}{2})$ という組が $(c,d)$ になりえる。
このような組が残っていた場合、それまでの和 $a,b$ も
ちょうど $\frac{M}{2}$ だけ離れていた場合に、両者の和 $\mod{M}$ が同じとなる。
大小関係はどちらでもよい。
つまり、$\frac{M}{2}$ だけ離れた組が $(0,3,1,4)$ のように偶数個あれば、「0なら3、1なら4」のように先攻が選んだペアの片割れを選び続ければ、後攻が勝てると言うことになる。
もちろん、同じ数($c=d$)がある場合はそれで打ち消してもいい。
$\frac{M}{2}$ だけ離れている2数をペアとして、ペア毎に考えればよい。
ペア $(A_i, B_i)$ がそれぞれ $p,q$ 個あったとする
$p,q$ がともに偶数なら、ペアの中だけで完結して先攻と後攻の和に差を生じさせないことができる
$p,q$ の偶奇が違うと、後攻は先攻と同じペアを選び続けることができない
$p,q$ がともに奇数なら、その中だけでは $\frac{M}{2}$ の差が生じることになる
個数カウントして、上記の通りに調べればよい。
$p,q$ の偶奇が違うようなペアがあると先攻必勝という証明
数は偶数個あるので、どちらかが余るようなペアは偶数個存在するはずである。
この余る数を上手く組み合わせることで、和が同じになって後攻が勝ってしまわないか?
結論から言うと、それはない。
よって、
先攻は余る数 $x$ をとりあえず取る
後攻が選んできた数にペアが残っていたらペアを取り、先攻操作後に両者の和が $x$ だけ異なった状態を維持する
後攻が余る(ペアのない)数 $y$ を選んできたら、また余る数を適当に取る
そのようにしていく内に、余る数が残り2個という状態になる
先攻は、現状の両者の差と、残る数を考慮し、和が同じにならない方を選ぶことができる
よって先攻必勝である。
Python3
from collections import Counter
def solve(n, m, aaa):
cnt = Counter(aaa)
if m % 2 == 1:
if all(c % 2 == 0 for c in cnt.values()):
return 1
else:
return 0
h = m // 2
checked = set()
xor = 0
for a, c in cnt.items():
if a in checked:
continue
b = (a + h) % m
if b in cnt:
d = cnt[b]
checked.add(b)
if c % 2 != d % 2:
return 0
if c % 2 == 1:
xor ^= 1
else:
if c % 2 == 1:
return 0
if xor == 0:
return 1
else:
return 0
n, m = map(int, input().split())
aaa = list(map(int, input().split()))
print(['Alice', 'Bob'][solve(n, m, aaa)])