AtCoder Beginner Contest 171(ABC171) A~F解説

AtCoder Beginner Contest 171(ABC171)のA~Fまでの解説です。
コンテスト中はA~Eを解き(22:03+1WA)、Fが易しめだったこともありレート微増に終わりました。
問題のアドレスは以下になります↓
https://atcoder.jp/contests/abc171/
公式の解説(Editorial)は以下になります↓
https://img.atcoder.jp/abc171/editorial.pdf

A問題:

入力された文字が大文字か小文字かを判定する問題です。
判定の方法としては、
・入力された文字がいずれかの大文字、または小文字に一致するかを確かめる
・標準で用意された関数を用いて判定する
の2つがあります。
前者は、文字列"ABCDEFGHIJKLMNOPQRSTUVWXYZ"を用意しこのいずれかと一致するかを確かめる、あるいは、入力された文字コードの値が'A'と'Z'の文字コードの間にあるかを確かめることで判定できます。
後者は、大文字であるかを判定する関数(Pythonであればisupper関数)を用いる、あるいは、入力された文字を大文字に変換する関数(Pythonであればupper関数)を用いて元の文字と等しいかを確かめることで判定できます。
後者のほうがお行儀がいいですが、覚えることが増えるうえ他に使い道がないので、前者での判定法を覚えておくのもよいと思います。

B問題:

まず、K種類の果物を1つずつ買ったときの合計価格が最小となるような買い方を考えます。
これはN種類の果物を安い順にK番目まで1つずつ買っていくことで実現できます。
したがって、N種類の果物を安い順にソートし、1番目からK番目までの値を足し合わせることで、求める答えを得ることができます。

C問題:

ルールを元に番号と名前の対応を考えます。
すると、c(a)=1,c(b)=2,…,c(z)=26とおき、与えられた名前をSとすると、Sがn文字のとき、c(S1)*26^n-1+c(S2)*26^(n-2)+…+c(Sn)*26^0として、与えられた名前から番号を求めることができます。
これを逆に見ると、与えられた番号Nについて、(N-1)%26とすることでc(Sn)-1を得ることができ、NをN//26に置き換えて同様の操作を行うことでc(S(n-1))-1が得られます。
これをc(S1)-1が得られるまで、すなわちNが0になるまで続けることで、c(S1)-1,c(S2)-1,…,c(Sn)-1が得られます。
ルールより、c(a)=1,c(b)=2,…,c(z)=26の対応が分かっているので、c(S1)-1,c(S2)-1,…,c(Sn)-1よりS1,S2,…,Snが分かり、求める答えが得られました。
ただし、この方法では名前の文字を下から順に求めているので、答えを出力する際に名前を反転させる必要があることに注意してください。

サンプルコード(Python):
https://atcoder.jp/contests/abc171/submissions/14597556

n=int(input())
ans=''
check='abcdefghijklmnopqrstuvwxyz' #c(a)-1=0,c(b)-1=1,…,c(z)-1=25を対応づける文字列
while n!=0:
 n-=1
 ans+=check[n%26] #(n-1)%26がc(Sk)-1を表すので、上記の文字列を用いてc(Sk)-1からSkを求める
 n//=26
print(ans[::-1])

D問題:

各クエリごとに数列Aの和を求めることを考えると、O(QN)となり間に合いません。
ここで、値がBiである要素をすべてCiに置き換えたとき、数列Aの和がどのように変化するかを考えます。
すると、数列Aの和は(値がBiである要素の個数)*(Ci-Bi)だけ変化することが分かります(∵値がBiでない要素は変化しないためです)。
これから、与えられた数列Aの和をあらかじめ求めておくと、各クエリごとの数列Aの和は、(直前の数列Aの和)+(値がBiである要素の個数)*(Ci-Bi)として求めることができます。
値がBiである要素の個数をクエリごとに数えているとやはりO(QN)となり間に合いませんが、与えられた数列Aの値が1~max(Ai)である要素の個数をあらかじめ数えておくと、クエリごとに(値がCiである要素の個数)=(値がCiである要素の個数)+(値がBiである要素の個数)、(値がBiである要素の個数)=0と更新することで、値がBiである要素の個数をO(1)で得ることができます。
以上により、この問題をO(N+Q)で解くことができました。

サンプルコード(Python):
https://atcoder.jp/contests/abc171/submissions/14550216

import collections

n=int(input())
arr=list(map(int,input().split()))
cnt=collections.defaultdict(int)
for val in arr: #あらかじめ各Aiの個数を数えておく
 cnt[val]+=1
sums=sum(arr) #あらかじめAiの総和を求めておく
q=int(input())
for _ in range(q):
 b,c=map(int,input().split())
 diff=(c-b)*(cnt[b]) #各クエリにおいて数列Aの総和は(値がBiである要素の個数)*(Ci-Bi)だけ変化する
 sums+=diff
 print(sums) 
 cnt[c]+=cnt[b] #値がCi,Biである要素の個数を更新する
 cnt[b]=0

E問題:

i番目の猫のスカーフに書かれた元の数をbiとします。
すると、各aiについて、aiはbiを除いたN-1個の値のxorであることが分かります。
これと、xorの性質としてk xor k=0が成り立つことから、aiとajのxorを取ることを考えます。
すると、上記の性質より、ai xor aj=bi xor bjとなることが分かります(∵biとbj以外のbkは2回xorを取ることになり、結局biとbjのxorのみが残ります)。
ここで、各akはbkを除くN-1個の値のxorであることから、ai xor aj=bi xor bjを用いることで、akを除くN-1個の値のxorを取ることでbkを得られることが分かります。
これを愚直に行うと計算量がO(N^2)となりますが、aiを除くN-1個の値のxorを取ることと、N個の値のxorを取った値とaiのxorを取ることは同値なので、あらかじめN個の値のxorを求めておくことで、この問題をO(N)で解くことができました。

サンプルコード(Python):
https://atcoder.jp/contests/abc171/submissions/14553589

n=int(input())
arr=list(map(int,input().split()))
xors=0
for val in arr: #あらかじめすべての要素のxorを求めておく
 xors^=val
ans=[]
for i in range(n): #各aiについて、ai xor (すべての要素のxor)がi番目に書かれた値に等しい
 ans.append(xors^arr[i])
print(*ans)

F問題:

問題の方針として、
・DP[i][j]:=(i:先頭からi文字目まで見ている、j:i文字目まででSと一致した文字がj文字ある)とDPを定めDPを計算する
・|S|+Kマスにまず|S|文字を与えられた順に置き、残りのK文字を決める数え上げを行う
の2つの方法が浮かびます。
前者は愚直にやるとO(|S|(K+|S|))で、遷移はj=|S|のときDP[i][|S|]=DP[i-1][|S|]*26+DP[i-1][|S|-1]、j=0のときDP[i][0]=DP[i-1][0]*25、それ以外のときDP[i][*]=DP[i-1][*]*25+DP[i-1][*-1]となります。
これを|S|+1行|S|+1列の遷移行列Aとして見ると、この行列を|S|+K乗したときの1行目|S|+1列目の値が分かればこの問題を解くことができることが分かります。
ここで、遷移行列Aについて、O'を1行目1列目の値が1、それ以外の値が0である|S|+1行|S|+1列の行列と定めると、A'=A-O'とすることで、A'を各行について25と1が並ぶ形にすることができます。
これを用いて、A^(|S|+K)=(O'+A')^(|S|+K)を考えることにします。
いまA'*O'=25O'であることとA'^Kの1行目1列目の値が25^Kであることから、A'^K*O'=25^KO'であることが分かります。
さらに、O'*A'は1行目についてA'と一致します。
以上のことから、(O'+A')^(|S|+K)について、A'^|S|~A'^(|S|+K)の係数を具体的に求めることを考えます。
小さいケースで実験することにより、|S|<=n<=|S|+K-1についてA'^nの係数が26^(|S|+K-1-n)に等しいことが分かり、またA'^(|S|+K)の係数が1であることは明らかです。
さらに、A'^nについて、小さいケースで実験することにより、1行目の値が(25+1)^nの第1項~第|S|+1項の値に等しいことが分かり、これからA'^nの1行目|S|+1列目の値はnC(n-|S|)*25^(n-|S|)であることが分かります。
以上より、|S|<=n<=|S|+K-1について、26^(|S|+K-1-n)*nC(n-|S|)*25^(n-|S|)を求め、最後にn=|S|+Kのケースの値、すなわち(|S|+K)C(K)*25^Kを足し合わせることで、求める答えを得ることができます。
さらに考察を進めると、結局(25+1)^(|S|+K)の第|S|+1項から第|S|+K+1項までの値を足し合わせることで答えが得られることが分かります。
以上より前者の方法で問題を解くことができることが分かりましたが、遷移行列の形から1行目|S|列目の値を求めるステップが難しいので、後者についても考えてみます。
後者で問題となりうるのは、同じ文字列を重複して数え上げることです。
例えば、|S|+Kマスに置く|S|文字をo、K文字をxとして表すことにすると、いくつかのoが同じマスにあるとき、それらを重複しないように数え上げる必要があります。
例として、
oxoxx
xooxx
の2つの置き方があるとき、1,2マス目のxがoに等しいとき重複します。
これを逆にいうと、Sの最後の文字を置く場所を固定したときに、oと重なる可能性のあるすべてのxにはo以外の文字をおく必要があり、oと重なる可能性のないすべてのxにはすべての文字を置くことができることになります。
したがって、Sの最後の文字を置く場所を|S|+Kマスの中からすべて試し、それぞれについて、(残りの|S|-1文字を与えられた順に置く場合の数)*25^(K-Sの最後の文字以降にあるマスの個数)*26^(Sの最後の文字以降にあるマスの個数)を、事前に0!~(|S|+k)!とその逆数を求めておくことで、O(|S|+K)でこの問題を解くことができました。
ここで、残りの|S|-1文字を与えられた順に置く場合の数は、(|S|+K-1-Sの最後の文字以降にあるマスの個数)C(N-1)で求めることができます(当然といえば当然ですが、結果的にDPによる解法と同じ計算式が得られます)。

サンプルコード(DPの遷移行列の性質を活かした解法):
https://atcoder.jp/contests/abc171/submissions/14606623

サンプルコード(Python)(数え上げによる解法):
https://atcoder.jp/contests/abc171/submissions/14588874

mod=10**9+7
k=int(input())
s=input()
l=len(s)
fact=[1] #0!~(|S|+k)!の前計算を行う
for i in range(1,k+l+1):
 fact.append((fact[-1]*i)%mod)
revfact=[]
for i in range(k+l+1): #0!~(|S|+k)!の逆数の前計算を行う
 revfact.append(pow(fact[i],mod-2,mod))
pow1=[1]
pow2=[1]
for i in range(1,k+l+1): #25^Kと26^Kの前計算を行う
 pow1.append((pow1[-1]*25)%mod)
 pow2.append((pow2[-1]*26)%mod)
ans=0
for i in range(k+l): #Sの最後の文字を置く場所を|S|+Kマスの中からすべて試す
 coef1=(pow1[k-i]*pow2[i])%mod
 if i<=k:
   coef2=(fact[k+l-1-i]*revfact[l-1]*revfact[k-i])%mod
 else:
   coef2=0
 ans+=coef1*coef2
 ans%=mod
print(ans)

この記事が気に入ったらサポートをしてみませんか?