東工大2022 数学第3問
(1)
軸正の方向と直線 がなす角の大きさはそれぞれ である.
のとき, である.
のとき,直線 の式はそれぞれ以下の通りである.
.
これら二式から を消去すると,
,
.
よって, の 座標は である.
同様に 座標も求めることで, を得る.
したがって, は直線 上に存在し,これは の時も成り立つ.
以上より,求める直線の方程式は である.
(2)
とおくと, より,
のような増減表を得る.
と の大小に注意し(1)の結果をふまえると ,求める道のりは,
直線 の の範囲()
である.
(3)
すべての に対し,
または
が 以上であることを示せば良い.
(2)で求めた の増減表から,
のとき, より, が成り立つ.
のとき, より, が成り立つ.
以上より,題意は示された.
東工大2022 数学第2問
(1)
とおく.
を背理法で示す.
と仮定すると,ある素数 が存在して はすべて の倍数である.
特に, が の倍数であることから, のいずれか一つは の倍数である.
が の倍数であるとして一般性を失わない.
このとき,
より, はともに の倍数である.
特に, が の倍数であることから, のいずれか一つは の倍数である.
が の倍数であるとして一般性を失わない.
このとき, も の倍数となるが, がすべて の倍数となるため に矛盾する.
したがって, である.
(2)
とおく.
より, が成り立つ.
(i)
が の倍数であると仮定すると, はすべて の倍数である.
であり,右辺は の倍数なので, は の倍数である.
であり,右辺は の倍数なので, は の倍数である.
以上より, はすべて の倍数となるが,これは(1)で示した に矛盾する.
したがって, は の倍数でない.
(ii)
が の倍数であると仮定すると,(i)と同様の議論により はすべて の倍数となり に矛盾する.
したがって, は の倍数でない.
(iii)
以上の素数 に対し が の倍数であると仮定すると,(i),(ii)と同様の議論により はすべて の倍数となり に矛盾する.
したがって, は の倍数でない.
(i)〜(iii)より, としてあり得る値は のいずれかである.
実際,
となることから, はすべてあり得る.
以上より,求める値は である.
Beginning of Beginners Contest 002 解説
A問題
各桁の数に興味がある場合には,入力を整数ではなく文字列として受け取ると,実装が簡単になることが多いです.
(別解)入力を整数として受け取り, で割った商と余りを利用する.
N = input() ans = 0 for x in N: ans = max(ans, int(x)) print(ans)
B問題
は見慣れない演算かもしれませんが,ゲームの勝敗を判定するGrundy数の計算などに用いられます. を求めるためには, について順に が に含まれるかどうかを判定し, に含まれないものがあれば を出力し計算を終了すれば良いです.
の計算量を考えてみます. が に含まれるかどうかを判定するために, に含まれる要素1つ1つについて と比較する場合,最悪で の計算量がかかります.しかし,setなどのデータ構造を利用することで,計算量を や に改善することができます.
以上をふまえ,今回の問題を考えてみます. を計算する場合には , を計算する場合には となるので, 個の と 個の を求めるために要する計算量は, または になります. の計算量を改善しない場合には,計算量は となりTLEしてしまいます.
H,W = map(int,input().split()) A = [list(map(int,input().split())) for _ in range(H)] def mex(S): for x in range(1001): if x not in S: return x R = [] for i in range(H): S = set() for j in range(W): S.add(A[i][j]) R.append(mex(S)) C = [] for j in range(W): S = set() for i in range(H): S.add(A[i][j]) C.append(mex(S)) print(*R) print(*C)
C問題
排他的論理和(XOR)は,AND,ORなどと同じ論理演算の仲間です.計算は少しややこしいですが,以下のような性質を持っています.
- 逆元は自分自身:
- 演算の順番を変えても結果は変わらない(結合法則および交換法則が成り立つ)
以上の性質を用いることで, が成り立つことが分かります.
上記のような性質を知らない場合でも,コンテスト中に「競プロ 排他的論理和 性質」のように検索することが大事です.
(別解)各ビットごとに注目する. のうち,第 ビット目が であるものの個数を とする. が奇数のとき, の第 ビット目は であり, が偶数のとき, の第 ビット目は である.
N,K = map(int,input().split()) A = list(map(int,input().split())) ans = K for a in A: ans ^= a print(ans)
D問題
とすると, が成り立ちます.また,各 に対して, とすると が成り立ちます.さらに, はそれぞれ 以下という条件を満たします.よって, が取り得る値は の 通りです.
この解法は,
- 各 に対し, となるための条件は何か?
→ がそれぞれ の倍数であれば良い. - がそれぞれ の倍数であるような は存在するか?
→ ,, それぞれの範囲に の倍数が含まれれば良い.
→ 最小の の倍数である が含まれるかどうかのみを考えれば良い.
という考察から発想に至ることができます.
(別解) は の約数に限られるので, について全探索を行い, の各約数 について が成り立つような が存在するか判定する.この判定は により で可能である. より,前もって 以下の整数について約数列挙を行うことで,計算量を削減することができる.
X,Y,Z = map(int,input().split()) print(min(X,Y,Z))
E問題
多倍長整数を扱うことができるPythonと言えども, のような極めて大きな数を扱うことはできません.しかし,このような大きな数が含まれる場合には, の値は を超えることが予想されます.実際, のとき, より, の値は必ず を超えてしまいます.
よって, のときは-1
を出力し, のときは を通常通り計算すれば良いです.言語によってはオーバーフローに気をつけてください.
from math import gcd def lcm(x,y): return x * y // gcd(x,y) A,B = map(int,input().split()) if B>=60: print(-1) exit() L = 1 for x in range(A,B+1): L = lcm(L,2**x-1) if L>10**18: print(-1) exit() print(L)
F問題
考えられる解法をいくつか紹介します.
解法1
から への並び替えを全探索します.考えられる場合の数は 通りであり, の場合だと 通りにもなるため,TLEしてしまいます.
解法2
の順に, のうちのどのペアに割り当てるかを決めていきます.ただし,すでに2つの要素が決まっているペアについては割り当てないようにします.こうすることで, がペアであるときには必ず が成り立ち, と の区別をなくすことで場合の数の削減につながります(これらのペアは, を考えたときの結果が同じであるため,どちらか一方について考えれば十分です).実際,考えられる場合の数は 通りであり, の場合だと 通りになります.解法1と比べて探索数はかなり減少しましたが,それでもTLEしてしまうと思います.
N,K = map(int,input().split()) X = list(map(int,input().split())) Y_list = [[] for _ in range(N)] # i番目のXをj番目のYに割り当てる再帰関数 def rec(i): if i==2*N: if sum([min(Y) for Y in Y_list])==K: print('Yes') exit() return for j in range(N): if len(Y_list[j])<2: Y_list[j].append(X[i]) rec(i+1) Y_list[j].pop() rec(0) print('No')
解法3
解法2について,ペアの順番を入れ替えても結果は変わらないため, は必ず に割り当てられるようにします.こうすることで, が に割り当てられる場合の探索を削減できます.考えられる場合の数は 通りであり, の場合だと 通りになります.この程度の探索数であれば,ACを獲得できると思います.
N,K = map(int,input().split()) X = list(map(int,input().split())) Y_list = [[] for _ in range(N)] Y_list[0].append(X[0]) # i番目のXをj番目のYに割り当てる再帰関数 def rec(i): if i==2*N: if sum([min(Y) for Y in Y_list])==K: print('Yes') exit() return for j in range(N): if len(Y_list[j])<2: Y_list[j].append(X[i]) rec(i+1) Y_list[j].pop() rec(1) print('No')
解法4
解法2について,各 の割り当てを考える際に,まだ空のペアに割り当てる場合には,空のペアの中で番号が最も小さいものに割り当てるようにします.こうすることで,必ず が成り立つため,ペアの順番による無駄な探索を削減することができます.考えられる場合の数は 通りであり, の場合でも 通りとなり,余裕でACを獲得することができます.
N,K = map(int,input().split()) X = list(map(int,input().split())) Y_list = [] # i番目のXをj番目のYに割り当てる再帰関数 def rec(i): if i==2*N: if sum([min(Y) for Y in Y_list])==K: print('Yes') exit() return for j in range(len(Y_list)): if len(Y_list[j])<2: Y_list[j].append(X[i]) rec(i+1) Y_list[j].pop() if len(Y_list)<N: Y_list.append([X[i]]) rec(i+1) Y_list.pop() rec(0) print('No')
解法5
のうち,どの 個をSakkyさんが購入することになるかを考えます.その後,決められた 個をSakkyさんが購入するようなペアの選び方が実際に存在するかを判定します.この判定は,すべての に対し,
- 価格が安い方から 番目までの商品に注目したとき,Sakkyさんが購入する商品の個数が,購入しない商品の個数以上である
が成り立つかどうかで判定できます.考えられる場合の数は 通りであり, の場合でも 通りとなり,余裕でACを獲得することができます.
from itertools import combinations N,K = map(int,input().split()) X = list(map(int,input().split())) X.sort() for v in combinations(range(2*N),N): ok = True cnt = 0 for i in range(2*N): cnt += 1 if i in v else -1 if cnt<0: ok = False break if ok and sum([X[i] for i in v])==K: print('Yes') exit() print('No')
F2問題
F問題におけるAC解の探索数は,それぞれ
- 解法3: 通り
- 解法4: 通り
- 解法5: 通り
でした. の場合は探索数は小さかったですが, の場合だと
- 解法3:約 通り
- 解法4:約 通り
- 解法5:約 通り
となってしまい,どれもTLEしてしまいます.
解法5では,Sakkyさんが購入する商品の組み合わせ 通りを探索しましたが,中には実際にはペアが構成できず実現不可能な組み合わせも含まれていました.実現可能な組み合わせだけを探索することはできないでしょうか.
解法6
解法5で用いたペアの選び方の判定条件を応用することで,ある 個の商品の組み合わせに対し,Sakkyさんがその 個の商品を購入するようなペアの選び方が存在するための必要十分条件は,
- 長さ の文字列 の第 文字目を,価格が安い方から 番目の商品をSakkyさんが購入するならば
(
,購入しないならば)
とするとき, がvalidな括弧列になっている
であることが分かります.こうすることで,長さ のvalidな括弧列を探索する問題に帰着されます.考えられる場合の数は 通り(カタラン数)であり, の場合でも 通りとなり,ACを獲得することができます.
N,K = map(int,input().split()) X = list(map(int,input().split())) X.sort() def rec(i,c,s): if i==2*N: if s==K: print('Yes') exit() return if c<=2*N-i: rec(i+1,c+1,s+X[i]) if c>0: rec(i+1,c-1,s) rec(0,0,0) print('No')
Beginning of Beginners Contest 001 解説
A問題
が成り立つかどうかで場合分けをすれば良いです.出力文のタイプミスには注意してください(サンプルの出力をコピペすると確実です).
A,B,C = map(int,input().split()) if A+B>C: print('Correct :)') else: print('Wrong :(')
B問題
各 に対して愚直に を計算すると,全体で 回の計算を行う必要があるためTLEしてしまいます.
そこで, であることを利用すれば,計算量を削減し で解くことができます.
N = int(input()) A = list(map(int,input().split())) MOD = 10**9+7 ans = 0 S = 0 for a in A: S += a S %= MOD ans += S ans %= MOD print(ans)
もしくは, であることを利用して で解くことができます.
N = int(input()) A = list(map(int,input().split())) MOD = 10**9+7 ans = 0 for i in range(N): ans += A[i]*(N-i) ans %= MOD print(ans)
C問題
を素因数分解して,2番目に大きい正の約数を求めれば良いです.
以上 以下の整数 について, が で割り切れるかどうかを調べようとすると,最悪の場合に の計算量がかかりTLEしてしまいます.
ここで,仮に が を満たす約数 を持つならば, も の約数であり が成り立ちます.言い換えれば, が 以上 以下のすべての整数で割り切れなければ, は か でしか割り切れない素数であることが分かります.
よって, 以上 以下の整数 について が で割り切れるかどうかを調べれば良いので, の計算量で問題を解くことができます.
また,今回は2番目に大きい正の約数にしか興味がないので,正の約数すべてを列挙する必要はなく,
を答えれば良いです.
N = int(input()) # 小数の丸め誤差を考慮して,念のため√N+1まで調べる for x in range(2,int(N**0.5)+2): if N%x==0: print(N//x) exit() # Nが素数の場合 print(1)
D問題
この問題でやっていることはバブルソートそのものであることに気付ければ,条件を満たすかどうかは転倒数から判定できることが分かります.転倒数とは かつ を満たす の個数であり,愚直に計算すれば の計算量がかかりますが,BITなどのデータ構造を用いれば で求めることができます.
しかし,今回の場合はソート回数が3回までと限定されているので,愚直なシミュレーションを行うことが可能です.数列を順に見ていき入れ替えるべきところ( となっているところ)を見つけたら,そこを入れ替えるという操作を3回まで行うようにすれば良いです.計算量は です.
N = int(input()) h = list(map(int,input().split())) for k in range(3): for i in range(N-1): if h[i]>h[i+1]: h[i],h[i+1] = h[i+1],h[i] break ok = True for i in range(N): if h[i]!=i+1: ok = False break if ok: print('Yes') else: print('No')
E問題
を で割った商と余りをそれぞれ とすると, 円を支払う際には1円硬貨を 枚,10円硬貨を 枚を使用することになります.よって, を満たす の個数を求める問題であると言い換えることが出来ます.
考えられる の値は最大で になるため,全探索ではTLEしてしまいます.
しかし, の値からある程度それぞれの硬貨の使用枚数に目星をつけられることが分かります.
- のとき,10円硬貨を 枚より多く使うと支払額が 円を超えるため,10円硬貨は高々 枚までしか使われず,さらに1円硬貨は高々 枚までしか使われないため,硬貨の使用枚数は 枚より少ない.
- のとき,10円硬貨は最低 枚使う必要があり,加えて1円硬貨または10円硬貨を 枚以上使うことになるので,硬貨の使用枚数は 枚を超える.
以上をふまえると, の範囲のみを調べれば良いことが分かります.この範囲内に整数は高々 個しか存在しないため,全探索が可能です.
N = int(input()) ans = 0 for x in range(max(1,10*N-100),10*N+1): if (x//10)+(x%10)==N: ans+=1 print(ans)
よりスマートな解法を示します.ポイントは,1円硬貨は高々 枚までしか使用されないことと,1円硬貨の使用枚数と が決まれば,10円硬貨の使用枚数と も一意に決まることです.
のとき,
- (1円硬貨を 枚,10円硬貨を 枚)
- (1円硬貨を 枚,10円硬貨を 枚)
︙ - (1円硬貨を 枚,10円硬貨を 枚)
の 通りが考えられます.
のとき,
- (1円硬貨を 枚,10円硬貨を 枚)
- (1円硬貨を 枚,10円硬貨を 枚)
︙ - (1円硬貨を 枚,10円硬貨を 枚)
の 通りが考えられます.
よって,求める答えは です.
N=int(input()) print(min(N+1,10))
F問題
は に関して単調増加になっているので,二部探索で答えを求めることができます.計算量は です.
K,N = map(int,input().split()) l = 1 r = 10**18 while r-l: mid = (l+r)//2 X = mid for i in range(1,K+1): X = ((X-1)//i+1)*i if X>=N: r = mid else: l = mid+1 print(l)
Python初心者のためのABC207
A問題
お好きなやり方でどうぞ.
# 解法1 A,B,C = map(int,input().split()) print(max(A+B,B+C,C+A)) # 解法2 L = list(map(int,input().split())) print(sum(L)-min(L)) # 解法3 L = sorted(list(map(int,input().split()))) print(L[1]+L[2])
B問題
操作回数を とすると, すなわち が成り立つ必要があります.
- のとき,条件をみたす は存在しません.
- のとき,条件をみたす最小の は です.
を用いれば,整数同士の計算のみで完結するため誤差の心配はありません.
A,B,C,D = map(int,input().split()) print((A-1)//(C*D-B)+1 if C*D-B>0 else -1)
C問題
開区間はちょっとだけ幅を狭めた閉区間だと思って処理すれば良いです.制約が と小さいため,すべての区間のペアについて条件を満たすか確認する の全探索が可能です.
N = int(input()) D = [] for _ in range(N): t,l,r = map(int,input().split()) l += 0.1 if t in [3,4] else 0 r -= 0.1 if t in [2,4] else 0 D.append([l,r]) ans = 0 for i in range(N): li,ri = D[i] for j in range(i+1,N): lj,rj = D[j] if li<=rj and lj<=ri: ans += 1 print(ans)
D問題
ここでは,複素数を用いた解法を紹介します.
Step1:特殊ケースの例外処理①
のとき,平行移動のみで と は必ず一致します.以降は の場合を考えます.
Step2:重心に注目した平行移動
公式解説にある通り, と が一致するならば両者の重心も一致しており,回転移動では重心の位置は変わりません.よって, と の重心が一致するような平行移動が必要であり,かつ,それ以外の平行移動は許されないことが分かります.両者の重心が原点に一致するように平行移動させることで,後は回転移動のみを考えれば良いことになります.
Step3:特殊ケースの例外処理②
の代表点( とします)を一つ選び, に含まれる点のうち と一致させる点 ( とします)を決めれば,回転角度は一意に決まります.しかし, が原点と一致する場合には,対応する は原点以外ありえません.そのため, か のいずれか一方にのみ原点が含まれる場合には,その時点で と を一致させることは不可能であると判定できます.また, と の両方に原点が含まれる場合には,それぞれから原点を削除した残りの 点について一致するかどうかを考えれば良いです.後々の操作を考慮して,このようなケースに対応する処理を加えておきます.
Step4:複素数を用いた回転移動
複素数平面を考えることで各点を複素数に対応させます.すなわち, とします.
の代表点 を適当に選びます.なお,
- Step1によって が保証されている
- Step3によって の長さが に減少している可能性がある
を考慮すると,この時点で の長さは必ず 以上であるため, が空だったことによるREの心配はありません.
に対応する の点 を全探索します. を に一致させるような回転移動は, として と一意に定まります.ただし,これが回転移動であるためには が成り立つ必要があります.なお,
- Step3によって が保証されている
を考慮すると,ゼロ除算によるREの心配はありません. が決まれば,後は各 に対して となるかを判定すれば良いです. と それぞれについて同一の点は含まれないという仮定から,対応する点が重複する心配はありません.
※ 実装上の注意
はPythonの複素数型を用いて complex(x,y)
と表されます. を求める際に複素数同士の割り算を行うため,誤差に注意する必要があります.特に, および の処理では完全一致 が求められますが,条件を緩和した などに置き換えてやると良いです.
この解法の計算量は,Step4における の全探索がボトルネックとなり です.
N = int(input()) S = [list(map(int,input().split())) for _ in range(N)] T = [list(map(int,input().split())) for _ in range(N)] # Step1 if N==1: print('Yes') exit() # Step2 cx = 0 cy = 0 for x,y in S: cx += x cy += y cx /= N cy /= N S = [complex(x-cx,y-cy) for x,y in S] cx = 0 cy = 0 for x,y in T: cx += x cy += y cx /= N cy /= N T = [complex(x-cx,y-cy) for x,y in T] # Step3 if 0 in S and 0 not in T or 0 not in S and 0 in T: print('No') exit() if 0 in S and 0 in T: S.remove(0) T.remove(0) # Step4 s0 = S[0] eps = 1e-10 for t0 in T: theta = t0/s0 if abs(abs(theta)-1)>eps: continue ok = True for s in S: ok_s = False for t in T: if abs(theta*s-t)<eps: ok_s = True break if not ok_s: ok = False break if ok: print('Yes') exit() print('No')
E問題
Step1:解法に目処を立てる
「数列が与えられていて という制約なので, の動的計画法(DP)で解きましょう」と言いたいところですが,さすがに乱暴すぎるので思考を少し説明します.DPの本質は「ある問題を解くときに,より規模の小さい問題の答えを利用する」という点にあります.今回の場合,
- 数列 の第 項までを考えようとしたときに,第 項まで,第 項まで,…,第 項までの結果が使えそうな気がする
という直感からDPを選択します.実際にこれが合っているかどうかは以降の考察によって確認されることになるのですが,とりあえずは直感を信じて考察を進めてみます.
Step2:状態数・遷移の定式化
DPの問題が解けるかどうかは,きちんと状態数を決められるかどうかが最初のポイントです.今回の場合,とりあえず問題を見たときに自然に思いつく状態数は
- を見ていることを表す
- に分割していることを表す
の2つがあります.さらに, という制約から計算量は であることも予想がついているので,状態数は高々2つです.よって,おそらく上記の2つが正しい状態数になっていそうです.以降では,
- : を に分割する場合の数
を考えることにします.
次に,状態の遷移を考えましょう.簡単な例として, に対する を考えてみます. の末尾から連続する何項かが に分割されているはずです. に含まれる数の和は の倍数でなければならないことから, として , の2通りが考えられます.
という条件の下, を に分割する場合の数は,残りの を に分割する場合の数 に等しいです.また, という条件の下, を に分割する場合の数は,残りの を に分割する場合の数 に等しいです.よって, と計算できます.
この結果を一般化すると,
が成り立ちます.なお, は が の倍数であるような を表します.以上により,問題をDPとして定式化することができました.
Step3:DPの高速化
を計算するために足し合わせる は最大で 通りあるため,状態数 の各遷移が の計算量を要し,全体では となってTLEしてしまいます.状態数を減らすことは難しそうなので,各遷移の計算量を に抑えられないでしょうか.
ここで,連続部分列の要素和に対する典型テクニックである
と表せることを用います.なお,, です.これにより, は と同値であることが分かります.よって,
- : のうち, を で割った余りが であるような についての の和
を用意すれば, と で計算できます.問題の ですが, を固定すれば の小さい順に で更新していくことが出来ます.
Step4:答えの導出
以上により,計算量 のDPを構成することができました.求めたい答えは です.計算を行うたびに で割った余りを求めることを忘れないようにしましょう.
N = int(input()) A = list(map(int,input().split())) MOD = 10**9+7 dp = [[0]*(N+1) for _ in range(N+1)] dp[0][0] = 1 for j in range(1,N+1): fact = [0]*j fact[0] = dp[0][j-1] S = 0 for i in range(1,N+1): S += A[i-1] S %= j dp[i][j] = fact[S] fact[S] += dp[i][j-1] fact[S] %= MOD ans = 0 for j in range(1,N+1): ans += dp[N][j] ans %= MOD print(ans)
Python初心者のためのABC206
A問題
print文をタイプミスしないように気をつけましょう.サンプルの出力をコピペすると確実です.
N = int(input()) * 108 // 100 if N < 206: print('Yay!') elif N > 206: print(':(') else: print('so-so')
B問題
日目の夜に確認する額が
であることがポイントです.今回の制約 では の範囲を調べれば十分なので全探索が可能です.
N = int(input()) money = 0 for i in range(1,10**5): money += i if money >= N: print(i) exit()
もちろん上記の式を使っても良いです.方程式を解いても良いですが誤差には注意しましょう.
C問題
条件をみたさない組を数えて全体から引くことを考えます.すなわち,
となる の個数を求めて全体の場合の数 から引きます.
この問題,どっかで見たことないですか…?
ABC200-Cを思い出してください.これは
となる の個数を数える問題でした.このときは各要素の個数をリストで管理することで解くことができました.しかし,今回の問題では という制約のため,同じようにリストを使うことができません.その代わりに使えるのがdefaultdict(int)
です.
from collections import defaultdict N = int(input()) A = list(map(int,input().split())) D = defaultdict(int) # 要素aの個数:D[a] for a in A: D[a] += 1 ans = N*(N-1)//2 # Dに記録された各要素の個数を取得(要素自体の値には興味なし) for v in D.values(): ans -= v*(v-1)//2 print(ans)
defaultdict(int)
は,通常のdict
とは違って存在しないキーが呼び出されたときに対応する値を0で初期化する機能を持っています(通常のdict
ではエラーになります).
D問題
Step1:愚直なシミュレーションを考える
求める値は最小の操作回数ですが,とりあえず最適性は忘れて条件を達成できるような手順を考えてみましょう.入力例1では
1 5 3 2 5 2 3 1
という数列が与えられます.愚直に先頭から見ていきましょう.
- は と一致する必要があります.現時点ですでに一致しているので操作は不要です.
- は と一致する必要があります. について一方から他方へ置き換える必要がありそうです.
- は と一致する必要があります. について一方から他方へ置き換える必要がありそうです.
- は と一致する必要があります. について一方から他方へ置き換える必要がありそうです.
- 以降に関しては,対応する値についてすでに調べ終わっている(例えば, に対応するのは )ので,これ以上調べる必要はありません.
よって, がすべて同じ数字のペアになるように操作をすれば良さそうです.
Step2:操作の意味を考える
Step1で操作が必要な数字のペアが求まりました.これらのペアに操作を施すことのアルゴリズム的な解釈を考えます.ここで思いつくのがグラフに置き換えることです(この発想に至るには経験を積んで慣れるしかないです).各数字を頂点としてペアの頂点同士を辺で結ぶことにします.そうすると,各操作は辺を1つ選んで辺が結ぶ2つの頂点を1つにまとめること(辺の縮約)に相当します.
この操作を繰り返して辺が無くなれば条件を達成したことになります.
Step3:最小の操作回数を考える
辺で結ばれた頂点のグループ(連結成分)が複数ある場合,異なるグループ間で操作が行われることはないので,各グループごとに考えればいいです.操作を1回行うと頂点は1つ減ります.また,操作を行うことでグループが分離したり合体することはありません.よって, 頂点からなるグループに対しては必ず 回の操作を行うことになります.以上をふまえると,グラフの連結成分ごとに頂点数を求める問題に帰着されました.
Step4:実装上のテクニック
以上の操作を高速に行うことができるデータ構造がUnion-Find木です.Union-Find木では
- グループの結合(グループの分解は不可能)
- 2つの要素が同グループに属するかの判定
を行うことができます.このようなデータ構造は,あらかじめ手元に準備しておくか,コンテスト中にインターネットで調べて有志が作成したコードを使うと良いです.その際には,必ず競プロ用に作成されたものを使うようにしましょう.そうでないものの中には,高速に動作しなかったり未検証のまま公開されていたりするものがあるので注意です.また,コードによって使い方や機能が違うので,自分に合ったものを選ぶようにしましょう.
def UnionFind(x): (x要素からなるUnion-Find木を構成) N = int(input()) A = list(map(int,input().split())) # union(x,y):xの属するグループとyの属するグループを結合 # same(x,y):xとyが同じグループに属するかを判定 U = UnionFind(200001) ans = 0 for i in range(N): if not U.same(A[i],A[N-1-i]): ans += 1 U.union(A[i],A[N-1-i]) print(ans)
Python初心者のためのABC205
A問題
算数の問題ですね.
A,B = map(int,input().split()) print(A*B/100)
B問題
実際に を昇順に並び替えて と一致するか判定しましょう.
N = int(input()) A = list(map(int,input().split())) A.sort() if A==list(range(1,N+1)): print('Yes') else: print('No')
リストを使って各値の出現回数を管理し, から までの値がそれぞれ1回ずつ現れるかどうかを判定しても良いです.
N = int(input()) A = list(map(int,input().split())) cnt = [0]*(N+1) for a in A: cnt[a] += 1 ok = True for i in range(1,N+1): if cnt[i] != 1: ok = False if ok: print('Yes') else: print('No')
集合を用いても同様の判定ができます.
N = int(input()) A = list(map(int,input().split())) S = set(A) if len(S)==N: print('Yes') else: print('No')
C問題
まず, は負の値を取ることもあり, は正の値に限られる点に注意しましょう. と を比較する問題ですが は共通しているので, を固定した時の関数 の挙動を考えてみましょう.
- が奇数のとき, は狭義単調増加なので が成り立ちます.
- が偶数のとき, は の時は狭義単調増加で, の時は が成り立つので,まとめると が成り立ちます.
以上より, の偶奇に関して場合分けをすると良いことが分かります.
A,B,C = map(int,input().split()) def compare(X,Y): if X>Y: print('>') elif X<Y: print('<') else: print('=') if C%2: compare(A,B) else: compare(abs(A),abs(B))
D問題
各クエリに対して の計算量がかかるとすると,全体の計算量は となります. とそこそこ大きいので, などではTLEしてしまいます*1.何とか定数時間または 時間に抑えられないでしょうか.
小さいケースで実験してみましょう. のとき小さい方から数えて 番目の数を とすると,
1 | 1 | 0 |
2 | 2 | 0 |
3 | 4 | 1 |
4 | 5 | 1 |
5 | 6 | 1 |
6 | 8 | 2 |
7 | 9 | 2 |
8 | 11 | 3 |
9 | 12 | 3 |
︙ | ︙ | ︙ |
のようになります.同様に実験することで,
- のとき
- のとき
- のとき
︙ - のとき
- のとき
が成り立つことが分かります.よって,以下のようなコードでACできます.
from bisect import bisect_right N,Q = map(int,input().split()) A = list(map(int,input().split())) # 二分探索用のリスト L = [A[i]-i for i in range(N)] for _ in range(Q): K = int(input()) X = K + bisect_right(L,K) print(X)
この解法における各クエリに対する計算量は です.
別解として,以下のような二分探索が可能です.
from bisect import bisect_right N,Q = map(int,input().split()) A = list(map(int,input().split())) for _ in range(Q): K = int(input()) l = 1 r = 10**18+10**5 while r-l: mid = (l+r)//2 cnt = mid - bisect_right(A,mid) #mid以下の自然数のうちAに含まれないものの個数 if cnt>=K: r = mid else: l = mid+1 print(l)
この解法における各クエリに対する計算量は です.
Python初心者のためのABC204
A問題
3人の出す手を ( がそれぞれグー,チョキ,パーに対応)とします.この問題は, が与えられたときにあいこになるような を求める問題です.公式解説では,
- は高々9通りしかないので場合分け
- のとき , のとき を出力
という2つの解法を紹介しています.別解として,3人の手があいこになるときは が3の倍数になることを利用します.この性質に気づくことができれば,実装量は上記の2つの解法よりも少なく抑えられます.
x,y = map(int,input().split()) print(-(x+y)%3)
B問題
そのまま実装するだけです.A問題より簡単かもしれません.それぞれの木から収穫する木の実の個数は,生っている木の実が10個以下かどうかif文を使って場合分けしても良いですが,以下のような表し方も可能です.
N = int(input()) A = list(map(int,input().split())) ans = 0 for a in A: ans += max(0, a - 10) print(ans)
C問題
都市を頂点,道を有向辺とするグラフとみなすことができます.各頂点から探索(幅優先探索または深さ優先探索)を行うことで,その頂点をスタート地点とした場合のゴール地点の候補をすべて調べることができます.頂点数を ,辺数を とした場合の全頂点からの探索には の計算量がかかり, は一般には最大で 程度になるため,全体では の計算量を要しTLEしそうな気がします.しかし,今回は という追加制約によって時間制限に間に合うように設定されています.
問題文はよく読みましょう. (スミマセンでした)
from collections import deque N,M = map(int,input().split()) node = [[] for _ in range(N+1)] for _ in range(M): a,b = map(int,input().split()) node[a].append(b) # 幅優先探索 def bfs(v): q = deque() done = [0]*(N+1) # すでに探索済かどうか q.append(v) done[v] = 1 cnt = 1 # 到達可能な頂点数 while q: x = q.popleft() for y in node[x]: if not done[y]: q.append(y) done[y] = 1 cnt += 1 return cnt ans = 0 for v in range(1,N+1): ans += bfs(v) print(ans)
D問題
誤った解法(貪欲法)
料理を時間がかかる順にオーブンを割り当てることを考えます.ある料理を割り当てたいとき,今までに割り当てたられた料理にかかる時間の合計が小さい方のオーブンに割り当てれば良さそうな気がします.
N = int(input()) T = list(map(int,input().split())) T.sort(reverse=True) oven1 = 0 oven2 = 0 for t in T: if oven1 <= oven2: oven1 += t else: oven2 += t ans = max(oven1, oven2) print(ans)
この解法はサンプルケースに対してはACですが,実際に提出するとWAになってしまいます.例えば,以下のようなケースが反例になります.
5
3 3 2 2 2
正解は6分(1つ目のオーブンで3分かかる料理を2つ,2つ目のオーブンで2分かかる料理を3つ調理するのが最適)ですが,上記のプログラムでは7分(1つ目のオーブンで3分,2分,2分の料理,2つ目のオーブンで3分,2分の料理を調理することになる)を出力してしまいます.このように,サンプルケースに対しては正解を出力しても解法が根本的に間違っている場合があるので注意しましょう.
正しい解法
とします.1つ目のオーブンに割り当てる料理の調理時間 が決まると,2つ目のオーブンでの調理時間は自動的に と定まります.よって,考えられるすべての に対して の最小値が答えとなります.すべての を調べるには 典型056 のテクニックが使え,計算量は であるため時間制限にも間に合います.
N = int(input()) T = list(map(int,input().split())) S = sum(T) # 合計調理時間がxになるような料理の割り当て方があるか dp = [0]*(S+1) dp[0] = 1 for t in T: nxt = [0]*(S+1) for x in range(S+1): if dp[x]: nxt[x] = 1 if x+t<=S: nxt[x+t] = 1 dp = nxt ans = 3141592 for x in range(S+1): if dp[x]: ans = min(ans, max(x, S-x)) print(ans)
Python初心者のためのABC203
A問題
if
文を使って条件を素直に実装すると良いです.
a,b,c = map(int,input().split()) if a==b: print(c) elif b==c: print(a) elif c==a: print(b) else: print(0)
ソートを使って比較回数を減らすのも良いでしょう.
l = list(map(int,input().split())) l.sort() if l[1]==l[0]: print(l[2]) elif l[2]==l[1]: print(l[0]) else: print(0)
さらに簡潔に書くことも可能です.
a,b,c = map(int,input().split()) print(0 if (a-b)*(b-c)*(c-a) else a^b^c)
^
はXOR演算子と呼ばれるもので詳細は割愛しますが,上記の回答は
- 結合法則 が成り立つ
という性質を利用しています.
B問題
まず問題を見たときに考えるべきは愚直解(全探索,シミュレーション,等)が可能であるか?ということです.愚直解のメリットは,難しい思考やテクニックは何も必要とせず素直に実装すれば良い点です.一方のデメリットは,計算量が大幅にかかってしまうです.逆に言えば,計算量の課題さえクリアすれば愚直解が可能なので,計算量がどれくらいになるか見積もってみましょう.
今回の場合,愚直解はすべての の組について考える全探索になりますが,高々 通りしかないので計算量的には余裕です.よって,全探索で実装しましょう.問題はi0j
という数値の表し方です.各桁を文字列に変換して結合した後に全体を整数に変換しても良いですが, を使うのが簡単でしょう.
N,K = map(int,input().split()) ans = 0 for i in range(1,N+1): for j in range(1,K+1): ans += 100 * i + j print(ans)
公式解説にある通り,数学の力を借りれば で解くことも可能ですが, の解法を思いついて実装するまでの時間よりも,難しい思考なしで全探索を実行する方が早いと思われます.
C問題
Step 1:愚直解の構成
まずは愚直解(シミュレーション)が可能であるかを考えましょう.太郎君は1円あたり1村進むので,すべての友達からお金を回収できるような場合に最大で 村進みます.よって,シミュレーションは計算量的に不可能です.
Step 2:愚直解の改善
このシミュレーションに「無駄な計算」はないか考えてみましょう.例えば,
太郎君は現在 円持っていて,次の友達がいる村までの距離は である
という状況では,次の友達がいる村にたどり着けないなら 回,たどり着けるなら 回のシミュレーションを行うことになります.しかし,よくよく考えてみると,次の友達がいる村にたどり着けるかどうかは と を比較すれば で判定できます.この判定を行うことで,次に 回または 回のシミュレーションを行うことが分かっていれば,それらを1回のシミュレーションにまとめてしまえば良いです.
この方法により,シミュレーションの計算回数を削減することができます.具体的には,太郎君は見かけ上だと友達がいる村だけを飛び飛びで移動する(もしくは,友達がいる村にたどり着けず友達がいない村でシミュレーションが終了する)ことになるので,計算回数は になります.
シミュレーション自体の計算量は ですが,友達がいる村を番号が小さい順に探索するために の計算量をかけてソートする必要があります.
N,K = map(int,input().split()) friend = [list(map(int,input().split())) for _ in range(N)] friend.sort() X = 0 # 太郎君が現在いる村の番号 M = K # 太郎君の所持金 for a,b in friend: # 太郎君が次の友達がいる村にたどり着くことができるか if a - X <= M: M += -(a - X) + b # (a-X)村進んで(a-X)円失うが友達からb円もらえる X = a else: X += M print(X) exit() # 最後の友達がいる村にたどり着いた後も太郎君は進み続ける X += M print(X)
問題を見たときに最初に愚直解を考える習慣をつけておくと,想定解が愚直解の場合に速やかに実装に取り組めるだけでなく,この問題のように想定解を構成するための原型となる解法を手に入れられる場合があります.
E問題
※難易度は青diffと初心者向きではないですが,解法を思いつきさえずれば実装は比較的簡単なので解説を書きました.
Step 1:愚直解の構成
まずは愚直解(シミュレーション)を考えます. が小さい順にすべての についてシミュレーションを行うので,計算回数は 回です. なので到底間に合いません.
Step 2:愚直解の改善
C問題と同様,このシミュレーションに「無駄な計算」はないか考えてみましょう. 行目に黒のポーンが1つも置かれていないとします.この場合, 行目で白のポーンが置かれている可能性がある の集合を とすると, が成り立ちます.よって,
① 黒のポーンが1つも置かれていない行は削除してシミュレーションを行う
ことで,残る行は高々 個になるため計算量を に減らすことができました.
現在は各行 に対してすべての列 についてシミュレーションを行っていますが,その必要はあるのでしょうか.例えば,0行目については です.1行目への遷移を考えるとき,そもそも に白のポーンが置かれている可能性がなければ からのシミュレーションを行う意味はありません.よって, からのシミュレーションのみを行えば良いことになります.同様に考えることで,
② 行目については に含まれる についてのみシミュレーションを行う
ようにすれば良いです.後の議論から が成り立つため,全体の計算量を に減らすことができます.
現在は白のポーン視点でシミュレーションを行っており,白のポーンが遷移する可能性がある列を探索することで を更新しています.一方で,黒のポーン視点で の更新を考えるとどうなるでしょうか.簡単のため, 行目には 列目にしか黒のポーンが置かれていない状況を考えてみます.すると, なる については
が成り立つことが分かります.よって, から への更新を考える上で変更(追加または削除)が行われる可能性がある要素は のみということになります.1行に複数個の黒のポーンが置かれている場合も同様に考えられるため,
③ 行目については黒のポーンが置かれている列 についてのみ に対する追加または削除を行う
ようにすれば良いです.これにより計算量は に抑えられ時間制限に間に合うようになりました.
Step 3:実装上のテクニック
以上の議論から,①と③をふまえたプログラムを実装すれば良いことになりました.まず,①を実装するにあたっては,
- 黒のポーンが1つ以上置かれている行のみを抽出する
- 各行に置かれている黒のポーンの列番号を管理する
ような機能が必要です.これらはcollections.defaultdict
によって同時に実現できます.defaultdict(func)
は基本的に通常のdict()
と同じですが,
辞書内に存在しないキーが呼び出されたときに,そのキーに対応する要素を
func
で初期化する
という違いがあります.func
には関数が入り,よく用いられるのはint
(0で初期化),list
(空のリストで初期化)などです.今回は集合を管理するためにdefaultdict(set)
を使います.
from collections import defaultdict N,M = map(int,input().split()) D = defaultdict(set) # D[i]:i行目に置かれている黒のポーンの列番号の集合 for _ in range(M): x,y = map(int,input().split()) D[x].add(y)
dict
およびdefaultdict
には以下のようなメソッドがあります.
# 辞書の各要素は「キー:値」 Dict = {1:2, 'A':'B', 'list':[0,0,0]} # 辞書のキーを出力 for k in Dict.keys(): print(k) > 1 > a > list # 辞書の値を出力 for v in Dict.values(): print(v) > 2 > B > [0, 0, 0] # 辞書の要素を出力 for k,v in Dict.items(): print(k, v) > 1 2 > A B > list [0, 0, 0]
今回は,黒のポーンが1つ以上置かれている行の番号を小さい順に取得したいので,sorted(D.keys())
を用いれば良いです.
次に,③の実装を考えます.実際には黒のポーンが1つ以上置かれている行しか考えないため行番号は飛び飛びになる可能性がありますが,便宜上 から への遷移を考えます.その際, は をコピーしてから 行目に置かれている黒のポーンを処理していけば良さそうですが,集合のコピーには計算時間がかかってしまいます.そこで,すべての行で同じ集合 を使い回すようにして,各行に関する遷移では
- に新たに追加される要素
- から削除される要素
を考えるようにします.
from collections import defaultdict N,M = map(int,input().split()) D = defaultdict(set) # D[i]:i行目に置かれている黒のポーンの列番号の集合 for _ in range(M): x,y = map(int,input().split()) D[x].add(y) S = set() # 白のポーンが置かれている可能性がある列番号の集合 S.add(N) for i in sorted(D.keys()): new = set() # Sに新たに追加される列番号 rem = set() # Sから削除される列番号 for j in D[i]: if j not in S and (j-1 in S or j+1 in S): new.add(j) if j in S and j-1 not in S and j+1 not in S: rem.add(j) for j in new: S.add(j) for j in rem: S.remove(j) print(len(S))
辞書のキーのソートの部分がボトルネックとなり,全体の計算量は です.
Python初心者のためのABC202
A問題
サイコロの出た面の数を とすると,反対側の面の数は と表される.
a,b,c = map(int,input().split()) # サイコロの出た面と反対側の面の数 def f(x): return 7 - x ans = f(a) + f(b) + f(c) print(ans)
以下のように簡潔に書くこともできる.
print(21 - sum(map(int,input().split())))
B問題
ポイントは以下の2つである.
- の順番が逆になる
6
は9
に,9
は6
に変換する(それ以外の文字は変わらない)
まず,文字列 の順番を逆にするためには,S = S[::-1]
とするのが最も簡単である.
これはスライスと呼ばれる操作で,文字列だけでなくリストに対しても使うことができる.
S = '0123456' # S[x]からスタートしてS[y]の手前までz文字ごとに出力する print(S[x:y:z]) # S[1]からS[6]の手前まで2文字ごとに出力する print(S[1:6:2]) > '135' # xを省略した場合は最初からスタートする # zが負の場合はSの末尾が最初として扱われる print(S[:4:1]) > '0123' # yを省略した場合は最後まで出力する # zが負の場合はSの先頭が最後として扱われる print(S[5::-2]) > '531' # x,y,zを複数省略することも可能 # 下の例だと,Sの最初から最後までを-1文字ごと=逆向きに1文字ごとに出力する print(S[::-1]) > '6543210'
次に,6
を9
に,9
を6
に変換するためには,Pythonに文字を反転させるような関数はないため,
if
文などを用いて一文字ごとに変換してやれば良い.
S = input() # 文字列を反転 S = S[::-1] ans = '' # '6'を'9'に,'9'を'6'に変換 for s in S: if s=='6': ans += '9' elif s=='9': ans += '6' else: ans += s print(ans)
文字列の変換にはS.replace(a, b)
( 内に現れる文字列 をすべて に変換する)を使っても良い.以下のように簡潔に書くこともできる.
print(input()[::-1].replace('9', '*').replace('6', '9').replace('*', '6'))
ただし,S.replace('6', '9').replace('9', '6')
のようにすると,
変換が左から順に行われるため6
が9
に変換されたあと再度6
に戻ってしまうため注意.
C問題
なる数列 を考えると, をみたす の組の個数を求める問題に帰着される. これは ABC200のC問題 と似たような感じで,各値の出現回数を管理することで解くことができる.
N = int(input()) A = list(map(int,input().split())) B = list(map(int,input().split())) C = list(map(int,input().split())) D = [B[c-1] for c in C] cntA = [0]*(N+1) for x in A: cntA[x] += 1 cntD = [0]*(N+1) for x in D: cntD[x] += 1 ans = 0 for x in range(1,N+1): ans += cntA[x] * cntD[x] print(ans)
D問題
求めたい辞書順 番目の文字列を とする.
入出力例2を見る限り全探索は無理そうなので,上手いやり方を考える必要がある.
文字列を辞書順に並べたとき,前半にa
で始まる文字列が並び,後半にb
で始まる文字列が並ぶのは簡単に予想がつく.
では,具体的に何番目までがa
から始まる文字列なのだろうか?
先頭の文字をa
で固定したとき,文字列の残りは 個のa
と 個のb
が並ぶので,
通り考えられる.
よって,辞書順で
番目から 番目までの文字列は先頭の文字が
a
番目以降の文字列は先頭の文字がb
であることが分かる. したがって, と の比較により の先頭の文字が分かる.
では,2文字目以降はどのようにして考えたら良いだろうか?
の先頭の文字がa
だった場合, の辞書順は残りの 個のa
と 個のb
が並ぶ部分で決まる.
仮にこの部分を とすると,以下の2つ
の「 個の
a
と 個のb
が並ぶ文字列」の中での辞書順
の「 個のa
と 個のb
が並ぶ文字列」の中での辞書順
は一致する.よって,
「 個の
a
と 個のb
が並ぶ文字列」の中での辞書順 番目の文字列の先頭の文字
が, の2文字目である.
の先頭の文字がb
だった場合,
先程と同様に の辞書順は残りの 個のa
と 個のb
が並ぶ部分 で決まる.
しかし, の「 個のa
と 個のb
が並ぶ文字列」の中での辞書順が 番目だとすると,
の「 個の
a
と 個のb
が並ぶ文字列」の中での 辞書順は 番目
になる.これは,「 個のa
と 個のb
が並ぶ文字列」の辞書順を考えると,
a
から始まる文字列が最初に 個並ぶためである.
この点に注意すると, の2文字目は,
「 個の
a
と 個のb
が並ぶ文字列」の中での 辞書順 番目の文字列の先頭の文字
である.
以上のように1文字目,2文字目,…と順に考えることで,辞書順で 番目の文字列の先頭の文字を求める問題に帰着される.
A,B,K = map(int,input().split()) # 二項係数 def comb(n,k): ans = 1 for i in range(k): ans *= n - i ans //= i + 1 return ans ans = '' while True: # AかBの値が0になったら残りの文字は自動的に決まる if A==0: ans += 'b'*B break if B==0: ans += 'a'*A break # 先頭の文字はどっち? if K <= comb(A+B-1, A-1): ans += 'a' A -= 1 else: ans += 'b' K -= comb(A+B-1, A-1) B -= 1 print(ans)