あるぱかのブログ

AtCoder茶(レート578)

AtCoder Beginner Contest 350(Promotion of AtCoderJobs)

A - Past ABCs

リンク:A - Past ABCs (atcoder.jp)

概要:文字列 Sの後ろ3文字が数字であり、この数字が001~315、317~349の範囲であるか判定してね

文字列 Sは6文字与えられますが頭から3文字は"ABC"であること、後ろ3文字は数字の羅列であることが保証されています。

後ろ3文字を数字として扱って上記の条件を満たすか判定すればOKです。

(S=ABC000の可能性を忘れて1WA叩いたのは自分だけでいい・・・)

#ABC350 B - Dentist Aoki
from collections import Counter
N, Q = map(int, input().split())
T = list(map(int, input().split()))
num = Counter(T)
ans = N
for key in num:
    if num[key] % 2 == 1:
        ans -= 1
print(ans)  

提出リンク:提出 #52558777 - AtCoder Beginner Contest 350(Promotion of AtCoderJobs)

B - Dentist Aoki

リンク:B - Dentist Aoki (atcoder.jp)

概要:青木くんの奇行シリーズ。こんな歯医者にかかりたくない。

同じ歯に対して何回治療行為が行われたかを判定して、回数が偶数なら歯が生えている状態、奇数なら歯が抜かれた状態となります。

なので度数分布を取って各歯の治療回数を2で割った余りを見て歯の増減を計算していきます。

#ABC350 B - Dentist Aoki
from collections import Counter
N, Q = map(int, input().split())
T = list(map(int, input().split()))
num = Counter(T)
ans = N
for key in num:
    if num[key] % 2 == 1:
        ans -= 1
print(ans)  

提出リンク:提出 #52558777 - AtCoder Beginner Contest 350(Promotion of AtCoderJobs)

C - Sort

リンク:C - Sort (atcoder.jp)

概要:配列 Aに対して2つの要素を選んで位置を交換することで小さい順に並べ替えたいです。必要な操作を出力してね

操作は最小回数である必要は無いので、1を1番目に、2を2番目に、・・・という方針で解きました。

並び変える数字がどこにあるかをindex()で探すと毎回 O(N)の計算量になりTLEします。

 O(1)で探したいので連想配列を使いました。

方針は間違っていないのですが大バグりしてしまい終了3分前になんとかACできました。

連想配列 numの一方のみを並べ替えて Aはそのまま使っており、並び替えたはずが実際には変わっていないので変なことになっていた
提出 #52606088 - AtCoder Beginner Contest 350(Promotion of AtCoderJobs)

数字の並び Aと位置を記録した配列 posの両方をしっかりスワップしていきましょう。

Pythonにはswap関数は用意されていないので、

a, b = b, a

のようにして位置を交換します。

#ABC350 C - Sort
from collections import defaultdict
N = int(input())
A = [0] + list(map(int, input().split()))
pos = [0] * (N+1)
for i in range(N+1):
    pos[A[i]] = i
ans = []
for i in range(1, N+1):
    if A[i] != i:
        ans.append((i, pos[i]))
        tmp = A[i]
        A[i], A[pos[i]] = A[pos[i]], A[i]
        pos[i], pos[tmp] = pos[tmp], pos[i]
n = len(ans)
print(n)
for a, b in ans:
  print(a, b)

提出リンク:提出 #52639001 - AtCoder Beginner Contest 350(Promotion of AtCoderJobs)

D - New Friends

リンク:D - New Friends (atcoder.jp)

概要:XとYは友達、YとZは友達のときにXとZを友達とする操作を行える最大数を出力してね

友達の友達の友達の・・・を辿れるだけ辿ってグループとします。

グループの人数が N人ならば交友関係の数は \dfrac{N \times (N-1)}{2}なので、これから元々あった交友関係の数を引けば答えとなります。

無向グラフを作成してBFSでグループの人数を把握し、上記の計算で答えを出します。

計算量は O(N+M)だと思います。

#ABC350 D - New Friends
from collections import deque
N, M = map(int, input().split())
G = [[] for i in range(N+1)]
for _ in range(M):
    a, b = map(int, input().split())
    G[a].append(b)
    G[b].append(a)
visited = [True] + [False for i in range(N)]
def bfs(G, start):
    q = deque()
    q.append(start)
    group = []
    group.append(start)
    visited[start] = True
    while q:
        pos = q.popleft()
        for i in G[pos]:
            if visited[i]:
                continue
            else:
                q.append(i)
                group.append(i)
                visited[i] = True
    return len(group)
ans = 0 - M
for i in range(1, N+1):
    if visited[i] == True:
        continue
    else:
        n = bfs(G, i)
        ans += (n*(n-1)) // 2
print(ans)  

提出リンク:提出 #52661681 - AtCoder Beginner Contest 350(Promotion of AtCoderJobs)

グループの人数を知りたいのでUnionFindでも同じことができます。

(他の方々の提出を見るとUnionFindを使う人のほうが多そうでした)

UnionFind部分は誰かの提出を元にしているので内容や計算量あんまりわかってないです。

#ABC350 D - New Friends
class UnionFind():
    """
    集合を木と捉え、各要素にその根を持たせる
    Union : 2つの木を合併する
    Find : 要素の根を返す
    """
    def __init__(self, n) -> None:
        """
        parameter
            n : 要素数
        """
        self.n = n
        self.parents = [-1]*n
        self.rank = [0]*n
    def find(self, x):
        """
        xの根を返す
        """
        if self.parents[x]==-1:
            return x
        else:
            # 値を更新しながら根を探す
            # 深さは最大でlog(n)なので、再帰関数でok
            self.parents[x] = self.find(self.parents[x])
            return self.parents[x]
    def union(self, x, y):
        """
        xを含む木とyを含む木を合併する
        """
        x = self.find(x)
        y = self.find(y)
        if x==y:
            return
        if self.rank[x] > self.rank[y]:
            x, y = y, x
        self.parents[x] = y
        if self.rank[x] == self.rank[y]:
            self.rank[y] += 1
N, M = map(int, input().split())
uf = UnionFind(N)
for _ in range(M):
    a, b = map(int, input().split())
    a -= 1
    b -= 1
    uf.union(a, b)
size = [0]*N
for i in range(N):
    size[uf.find(i)] += 1
ans = 0 - M
for i in size:
    ans += (i * (i-1)) // 2
print(ans)  

提出リンク:提出 #52622719 - AtCoder Beginner Contest 350(Promotion of AtCoderJobs)

結果

C問題で大いに躓きました。

A~Cの3完117分(WA×4含)でした

初めてのレート降下。

C問題でもたついてなければD問題まで4完できていたかもしれない・・・

 

 

 

 

 

精進(2024/4/19)

最近のABCからD問題を解く

HUAWEI Programming Contest 2024(AtCoder Beginner Contest 342) D - Square Pair

リンク:D - Square Pair (atcoder.jp)

概要:配列 Aの中から2つ選んで掛け算したときに積が平方数になる組み合わせ数を数えてね

 N \leq 2 \times 10^{5}から2つ選ぶ組み合わせ数は _N C _2 = \dfrac{N \times (N-1)}{2}で最大 2 \times 10^{12}になるので愚直にやるとTLEになります。

本番では全く見当もつかなかったので解説で方針だけ確認しました。

  • 配列 Aの各要素についてこれを割り切る最大の平方数 d_{A_i}を求める( A_i=0 のときは0とする)
  •  \dfrac{A_i}{d_{A_i}}が同じになる組み合わせ数を数え上げる

 \dfrac{A_i}{d_{A_i}} \dfrac{A_j}{d_{A_j}}が平方数であることの証明は記載ありませんでしたが、おそらくこういうことだと思います。

  •  \dfrac{A_i}{d_{A_i}}の約数に平方数が含まれない
  • 約数に平方数を持たない数同士を掛けて平方数にするには両者が同じ数字でなければならない

この方針により問題を配列 D D_i = \dfrac{A_i}{d_{A_i}})の度数分布と組み合わせ数の計算に変換でき、計算量を O(N \sqrt(A_i))にできました。

 Aに0があるときが厄介で、0に対しては何を掛けても0になり題意を満たしてしまうので特別な処理が必要になります。

(0の処理で3回くらいWAしました・・・)

#ABC342 D - Square Pair
#解説
from collections import Counter
N = int(input())
A = list(map(int, input().split()))
D = [0]*N
z = 0
for i in range(N):
#Ai=0なら0を返す
    if A[i] == 0:
        D[i] = 0
        z += 1
#それ以外は割り切れる最大の平方数で割った商を返す
  else:
#探索範囲は√AiまででOK
        for j in range(1, int(A[i]**0.5)+1):
            if A[i] % j**2 == 0:
                d = j**2
        D[i] = A[i] // d
ans = 0
num = Counter(sorted(D))
for key in num:
    if key == 0:
        for i in range(1, num[key]+1):
            ans += N-i
    else:
        ans += (num[key] * (num[key] - 1)) // 2
print(ans)  

提出リンク:提出 #52513574 - HUAWEI Programming Contest 2024(AtCoder Beginner Contest 342)

トヨタ自動車プログラミングコンテスト2024#4(AtCoder Beginner Contest 348) D - Medicines on Grid

リンク:D - Medicines on Grid (atcoder.jp)

概要:エネルギー管理しながら二次元盤面のゴールにたどり着けるか判定してね

BFSのいい練習になりました。

これを機に二次元盤面とグラフのBFSのテンプレートを作ったので次からは安心!

公式解説の解法1の通り以下の方針で解きます。

  • スタートおよび薬のあるマスを始点としてBFS、エネルギーの続くうちに他の薬orゴールに到達可能か判定して、可能なら有効グラフに辺を追加する
  • 上記のグラフでBFSしてスタートからゴールが続いているか判定して答えを出力する

私がやった間違いとして

初めて100行近いコード書きました。疲れた・・・

 from collections import deque
#二次元盤面BFSで各マスへの最短距離を書いた二次元配列distを返す
def BFS_2Dgrid(grid,start):
    H = len(grid)
    W = len(grid[0])
    DX = [1,-1,0,0]
    DY = [0,0,1,-1]
    sx, sy = start
    gx, gy = goal
    q = deque()
    q.append(start)
    dist = [[-1 for i in range(W)] for j in range(H)]
    dist[sx][sy] = 0
    while q:
        x, y = q.popleft()
        for dx, dy in zip(DX,DY):
            xt, yt = x+dx, y+dy
            if xt < 0 or H-1 < xt or yt < 0 or W-1 < yt:
                continue
            if grid[xt][yt] == '#':
                continue
            if dist[xt][yt] > -1:
                continue
            else:
                dist[xt][yt] = dist[x][y] + 1
                q.append((xt,yt))
    return dist
#1次元グラフ上のBFSでゴールへの最短距離を返す
def BFS_1Dgraph(graph,start,goal):
    N = len(graph)
    q = deque()
    q.append(start)
    dist = [-1 for i in range(N)]
    dist[start] = 0
    while q:
        pop = q.popleft()
        if pop == goal:
            break
        else:
            for i in G[pop]:
                if dist[i] > -1:
                    continue
                else:
                    dist[i] = dist[pop] + 1
                    q.append(i)
    return dist[goal]
#二次元盤面の受け取り
H, W = map(int,input().split())
grid = []
for i in range(H):
    A = input()
    if 'S' in A:
        start = (i, A.index('S'))
    if 'T' in A:
        goal = (i, A.index('T'))
    grid.append(A)
#薬マスの座標を受け取り
N = int(input())
medicine = []
energy = []
for i in range(N):
    r, c, e = map(int, input().split())
    r -= 1
    c -= 1
    medicine.append((r, c))
    energy.append(e)
#スタートとゴールに薬が無い場合はエネルギー0の薬があることにする
(スタートにない時点でNoを出力してexit()でいいが気づいていない顔)
if start not in medicine:
    medicine.append(start)
    energy.append(0)
if goal not in medicine:
    medicine.append(goal)
    energy.append(0)
#薬マス間のグラフをつくる
G = [[] for i in range(len(medicine))]
for i in range(len(medicine)):
    grid_start = medicine[i]
    en = energy[i]
    dist = BFS_2Dgrid(grid,grid_start)
    for j in range(len(medicine)):
        if i == j:
            continue
        else:
            x, y = medicine[j]
            if 0 <= dist[x][y] and dist[x][y] <= en:
                G[i].append(j)
#グラフ上でスタートゴールがつながっているか判定
s = medicine.index(start)
g = medicine.index(goal)
if BFS_1Dgraph(G,s,g) >= 0:
    print('Yes')
else:
  print('No')

提出リンク:提出 #52292164 - トヨタ自動車プログラミングコンテスト2024#4(AtCoder Beginner Contest 348)

 

 

 

 

AtCoder Beginner Contest 349

A - Zero Sum Game

リンク:A - Zero Sum Game (atcoder.jp)

概要: N-1人の勝ち点をみて N人目の勝ち点を計算してね

2人が戦って勝てば勝ち点1、負ければ勝ち点-1になるので、全員の勝ち点の総和を取ると必ず0になります(問題のタイトル通りです)。

そのため N-1人の勝ち点総和を計算して、0から引く(もしくは-1を掛ける)ことで N人目の勝ち点が分かります。

#ABC349 A - Zero Sum Game
N = int(input())
A = list(map(int, input().split()))
ans = -sum(A)
print(ans)  

提出リンク:提出 #52295867 - AtCoder Beginner Contest 349

B - Commencement

リンク:B - Commencement (atcoder.jp)

概要:文字列 Sに登場する英小文字の数が以下の条件を満たすか判定してね

 S にちょうど  i 回現れる文字はちょうど 0 種類またはちょうど2 種類ある

言い回しが難しいですが、各英小文字が文字列 Sに登場する回数を数えて度数分布を作成、さらに度数分布に登場する数の度数分布を作って0と2以外があればNG、という方針で押し通せそうです。

以下のコードでは文字の度数分布を n、登場回数の度数分布を numとしています。

(以下のコードでは nの作成にCounter、 numの作成にdefaultdictを使用していますが、 numについてもCounter(num)で同じことができるので本来はCounterだけで済みます)

例えば入力例1:commencementに対して n numは以下のようになります。

n = Counter({'m': 3, 'e': 3, 'c': 2, 'n': 2, 'o': 1, 't': 1})
num = defaultdict(int, {2: 2, 1: 2, 3: 2})

 numを見るとたしかに1回登場する文字が2種類(o,t)、2回登場する文字が2種類(c,n)、3回登場する文字が2種類(m,e)あることが分かります。

#ABC349 B - Commencement
from collections import Counter
from collections import defaultdict
S = input()
n = Counter(S)
num = defaultdict(int)
for key in n:
    num[n[key]] += 1
ans = 'Yes'
for key in num:
    if num[key] != 2:
        ans = 'No'
print(ans)  

提出リンク:提出 #52305590 - AtCoder Beginner Contest 349

C - Airport Code

リンク:C - Airport Code (atcoder.jp)

概要:文字列 Tを小文字に直したときに文字列 Sの部分文字列(連続でなくてOK)であるか判定してね

 Tは3文字で確定なので1文字目を持って Sを走査していき、一致する文字があれば1文字目はクリア、2文字目について同様に・・・という流れでいけそうです。

 |S| \leq 10^{5}ということですが二重ループを使う場面は無いので特に気にする必要は無いです。

部分文字列ということなのでおそらく S=abcdefに対して T=FDAみたいな取り方はNGなんだろうと思います。

#ABC349 C - Airport Code
S = input()
T = input()
T = T.lower()
flag_1 = False
flag_2 = False
flag_3 = False
l1 = 0
l2 = 0
for i in range(len(S)):
    if S[i] == T[0]:
        flag_1 = True
        l1 = i
        break
for i in range(l1+1, len(S)):
    if S[i] == T[1]:
        flag_2 = True
        l2 = i
        break
if T[2] == 'x':
    flag_3 = True
else:
    for i in range(l2+1, len(S)):
        if S[i] == T[2]:
            flag_3 = True
ans = 'No'
if flag_1:
    if flag_2:
        if flag_3:
            ans = 'Yes'
print(ans)  

提出リンク:提出 #52317824 - AtCoder Beginner Contest 349

D - Divide Interval

なにがなにやらさっぱり・・・

E - Weighted Tic-Tac-Toe

リンク:E - Weighted Tic-Tac-Toe (atcoder.jp)

概要:配点付き三目並べをして勝利するプレイヤーを教えてね

今回はD、E両方とも450点でした。

こちらは実装が重そうなもののやるべきことは明確そうに思います。

お気持ちとしては

1ターン目高橋君は盤面の得点最大を取る

 複数あれば盤面中央を優先的にとる

2ターン目以降は以下の優先度で行動する

 自分のビンゴ完成

 相手のリーチつぶし

 得点最大をとる

 得点最大が負の値ならリーチを作りに行く

 (取得済のマスから縦、横(、斜め)を見て相手がいない列かつ得点最大のマスをとる)
 ターン終了時にビンゴチェック、完成していたらゲーム終了
 ビンゴなしでゲーム終了したときは得点の総和で勝負

になるんですが1時間ではちょっとしんどいですね・・・と思い椅子温めタイム終了としました。

後日じっくり時間かけて実装だけはしてみようと思います。

結果

いつも通りA~Cの3完21分で4386位でした。

D問題よりE問題のほうが正答率低いんですね。

レート更新されたら追記します。

 

トヨタ自動車プログラミングコンテスト2024#4(AtCoder Beginner Contest 348)

A - Penalty Kick

リンク:A - Penalty Kick (atcoder.jp)

概要:3の倍数のときだけ×を、そのほかのときはoとした長さ Nの文字列を出力してね

空の文字列を用意して起き、1~Nまで順番に走査して3で割った余りが0となったときに×を、それ以外はoを文字列の末尾に付け足していきます。

range(N)だと0~N-1までを走査することになるので、range(1, N+1)とします。

#ABC348 A - Penalty Kick
N = int(input())
#空の文字列
ans = ''
for i in range(1, N+1):
#3の倍数のときだけ×を出す
    if i%3 == 0:
        ans += 'x'
    else:
        ans += 'o'
print(ans)  

提出リンク:提出 #52062289 - トヨタ自動車プログラミングコンテスト2024#4(AtCoder Beginner Contest 348)

B - Farthest Point

リンク:B - Farthest Point (atcoder.jp)

概要: N個の座標が与えられるので、各座標から最も遠い位置にある座標の番号を N行出力してね

 N \leq 100なので全探索しても N^{2}=10,000回の計算で済むため愚直に全探索します。

ユークリッド距離の計算をいっぱいするので関数として定義しておくと楽になります。

一方の座標を固定してもう一方の座標を小さい順に見て距離を計算していく中で最大値を更新します。

もし距離が同じ場合は番号の小さい座標を出す必要があるので最大値を更新する条件に注が必要です。( \geqではなく  \gtを使用)

またプログラムでは少数の扱いが苦手らしいので、距離の計算を本来 \sqrt{(x_1-x_2)^{2}+(y_1-y_2)^{2}}とするべきところ、ルートを取らずに済ませています。

今回は座標の最大値が1000なので2乗のままでも桁数がとんでもないことにはならないので問題ない(はず)です。

#ABC348 B - Farthest Point
def dist(A, B):
    x1, y1 = A[0], A[1]
    x2, y2 = B[0], B[1]
#平方根を取らず2乗のまま扱う
    d = (x1-x2)**2+(y1-y2)**2
    return d
N = int(input())
P = []
#座標の受け取り
for i in range(N):
    x, y = map(int, input().split())
    P.append((x,y))
#一方の座標を固定して
for i in range(N):
    mx = 0
#もう一方の座標を全探索
    for j in range(N):
        if dist(P[i], P[j]) > mx:
            mx = dist(P[i], P[j])
#1-indexedに合わせる
          mx_node = j+1
    print(mx_node)  

提出リンク:提出 #52072957 - トヨタ自動車プログラミングコンテスト2024#4(AtCoder Beginner Contest 348)

C - Colorful Beans

リンク:色ごとに一番まずい物のまずさを比べて一番マシなものを出力してね

概要:C - Colorful Beans (atcoder.jp)

題意の理解にすこし時間がかかりました。

やりたいこととしては

・同じ色の中で味の数値が最小のものを見つける

 →dictを用意して各色の最も不味いときの数値を記録する

・複数の色がある中で上記が最大のものを選ぶ

 →上記のdictの最大値を返す

ということになろうかと思います。

#ABC348 C - Colorful Beans
from collections import defaultdict
N = int(input())
#0で初期化したdictを用意
num = defaultdict(int)
for i in range(N):
  A, C = map(int, input().split())
#初回は味にかかわらず記録
  if num[C] == 0:
        num[C] = A
#2回目以降は記録より不味いときだけ値を更新
    elif num[C] > A:
        num[C] = A
mx = 0
#全色の最マズを並べて一番マシなものを出力
for key in num:
    mx = max(mx, num[key])
print(mx)  

提出リンク:提出 #52081817 - トヨタ自動車プログラミングコンテスト2024#4(AtCoder Beginner Contest 348)

D - Medicines on Grid

リンク:D - Medicines on Grid (atcoder.jp)

概要:二次元盤面に壁や薬があるのでゴールできるか判定してね

二次元盤面の迷路=最短距離=BFSだけなら良かったのですがエネルギーを管理しないといけないのでバグらせ散らかして80分間椅子を温めることに成功・・・

公式解説の解法2の方針で特に工夫もなくやろうとしてたのでたぶんバグをとれてもTLEしてたと思います。

下のように脇道にそれて薬を回収して元の道に戻る、なんかの場合もあり得るなあなんて考えて無駄にごちゃらせたように思います。

この機に二次元盤面BFSの関数準備しようかな、と思いつつ直近で緑diff以下にBFSやDFSを見た気がしないのでたぶんやらないです()

結果

A~Cまで3完22分で4,866位、レート更新して548→559に微増です。

 

精進(2024/4/1)

最近のABCからD問題を解く

AtCoder Beginner Contest 341 D - Only one of two

リンク:D - Only one of two (atcoder.jp)

概要:正整数 N, Mのうち一方のみで割り切れる正整数の小さいほうから K番目を出力してね

この問題では以下の3つの考え方を使用します。ひとつひとつはそれほど難しくない概念ですが組み合わさることで緑diffの問題になるのすき。

ユークリッドの互除法

・包除原理(ベン図的なアレ)

・答えで二分探索

まず、「 N, Mのどちらか一方でのみ割り切れる」、という文言を「 N, Mのいずれかで割り切れる数から N, Mの両方で割り切れる数を引く」と読み替えます。

さらに「 N, Mの両方で割り切れる数」は「 N, Mの最小公倍数で割り切れる数」と表現できます。

最小公倍数を高速に求めるためにユークリッドの互除法を使います。

アルゴリズムの詳しい説明は他所にいくらでもあるのでそちらに任せます)

ユークリッドの互除法により求めることができる最大公約数を gcd(N, M)と表すと最小公倍数は \dfrac{N \times M}{gcd(N, M)}で計算できます。

例として N=6,M=21とすると gcd(6,21)=3、最小公倍数= \dfrac{6 \times 21}{3}=42と計算でき正しいことがわかります。

下記の提出コードでは自分で関数gcdを定義していますが、標準ライブラリmathの中に関数gcdが存在するのでfrom math import gcdで使っても構わないですしそのほうが処理も早いです。

(なんなら最小公倍数を求めるlcmという関数もあります)

次にベン図的なアレの考え方について、

 X以下の正整数で N,Mのいずれか一方のみで割り切れる数の個数は下の図から \lfloor \dfrac{X}{N} \rfloor +  \lfloor \dfrac{X}{M} \rfloor - 2 \times \lfloor \dfrac{X}{gcd(N,M)} \rfloor で表すことができます。

 \lfloor \dfrac{X}{gcd(N,M)} \rfloor 部分が重なっているため2回引いている)

最後に二分探索ですが、二分探索の中点 \dfrac{L+R}{2}が答えであると仮定して上記で求めた数が Kより多いか少ないかを考えることで効率的に探索を行うことができます。

#ユークリッドの互除法をgcdとして関数定義
def gcd(a, b):
    if b == 0:
        return a
    else:
        return gcd(b, a%b)
N, M, K = map(int, input().split())
L = 1
R = 10**19
#最小公倍数
x = N*M // gcd(N, M)
#二分探索
while L< R:
  mid = (L+R) // 2
#包除原理
    ans = mid//N + mid//M - 2*(mid//x)
    if ans < K:
        L = mid+1
    else:
        R = mid
#ループを抜けるとL=R=答えになっている
#print(L, mid, R)
print(R)

提出リンク:提出 #51700930 - トヨタ自動車プログラミングコンテスト2024#2(AtCoder Beginner Contest 341)

AtCoder Beginner Contest 344 D - String Bags

リンク:D - String Bags (atcoder.jp)

概要:袋から文字列を取ったり取らなかったりして目的の文字列 Tをつくってね

取ったり取らなかったりということで動的計画法を使います。動的計画法は必殺技っぽいのですき。いまのところ一番好きなアルゴリズムかもしれない。

何袋目まで見たか、何文字目まで完成したかで表を作って中身にはコストを記入します。

ターゲット文字列は最大100文字、袋から1回取り出すごとにコスト1なのでコストの最大値は100、ということで二次元配列 dpを10,000くらいで初期化したのちに dp_{0,0}=0として初期状態とします。

入力例1の場合では下図のようになります。

 dpの遷移式は以下のようになります。

とりあえず dp[i][j]=dp[i-1][j]で上書きする。(今見ている袋について操作を行わなかった状態)

そのあとで T[j-ls:j] = S[k]ならば dp[i][j]=min(dp[i-1][j-ls]+1, dp[i][j])

(袋から取り出した文字列 S[k]の長さを lsとして)

この dp_iの初期化と最小値上書きの順番で3時間くらい悩みました。

入力例1については下図のように遷移していきます。

 dp[2][4]は袋1からab、袋2からcdを取り出しても到達できますが、袋1からabcdを取り出した場合のほうがコストが小さいのでそちらが採用されています。

最終的な解答は以下になります。

鉄則本で勉強した影響で動的計画法だけは1-indexedで書くクセがつきました。

#ABC344 D - String Bags
T = input()
N = int(input())
n = len(T)
#dpを10,000で初期化
dp = [[10000 for i in range(n+1)] for j in range(N+1)]
dp[0][0] = 0
for i in range(1, N+1):
    A, *S = map(str, input().split())
    A = int(A)
    for j in range(n+1):
#操作を行わなかったとしてdp[i]をdp[i-1]で上書き
        dp[i][j] = dp[i-1][j]
        for k in range(A):
          ls = len(S[k])
#条件を満たす場合にコストの更新
            if j-ls >= 0 and T[j-ls:j] == S[k]:
              dp[i][j] = min(dp[i][j], dp[i-1][j-ls]+1)
if dp[-1][-1] < 10000:
    print(dp[-1][-1])
else:
  print(-1)

提出リンク:提出 #51942668 - トヨタ自動車プログラミングコンテスト2024#3(AtCoder Beginner Contest 344)

 

 

 

AtCoder Beginner Contest 347

A - Divisible

リンク:A - Divisible (atcoder.jp)

概要:配列 Aのうち Kの倍数であるものについて \dfrac{A_i}{K}を出力してね

 A_iを順番に見て Kで割った余りが0であれば \dfrac{A_i}{K}を計算して空の配列 ansに格納、最後に半角スペース区切りで出力します。

#ABC347 A - Divisible
N, K = map(int, input().split())
A = list(map(int, input().split()))
ans = []
for i in range(N):
    if A[i] % K == 0:
        ans.append(A[i]//K)
print(*ans, sep = ' ')  

提出リンク:提出 #51802776 - AtCoder Beginner Contest 347

ちなみに配列 ansを用意しなくても以下のように末尾を改行ではなく半角スペース(' ')として出力してもいいみたいです。

(末尾に不要な半角スペースがつくので本番では怖くて避けました)

#ABC347 A - Divisible
N, K = map(int, input().split())
A = list(map(int, input().split()))
for i in range(N):
    if A[i] % K == 0:
        print(A[i]//K, end = ' ')  

提出リンク:提出 #51880006 - AtCoder Beginner Contest 347

B - Substring

リンク:B - Substring (atcoder.jp)

概要:文字列 Sの部分文字列としてあり得るものが何種類か出力してね

 Sは最大100文字ということで、1文字取り出す場合は100通り、2文字なら99通り・・・と考えると最大 \dfrac{N \times (N+1)}{2}で5000通りくらいで済みそうなので全探索します。

二重for文で始点と終点を指定してスライスで部分文字列を取り出し、セット ansにaddしていきます。

最後にセットの長さ=部分文字列の種類数を出力します。

初めからセットで持っておくほうが速いですが、初めは配列として最後にセットを取る方法でも問題ないと思います。

#ABC347 B - Substring
S = input()
ans = set()
for i in range(len(S)):
    for j in range(1, len(S)+1):
        if S[i:j] != '':
            ans.add(S[i:j])
print(len(ans))  

提出リンク:提出 #51814826 - AtCoder Beginner Contest 347

C - Ideal Holidays

リンク:C - Ideal Holidays (atcoder.jp)

概要:配列 Dがすべて休日になる可能性があるか判定してね

3回WA叩きましたが何とか時間内にACできました。

予定の数 N = 2 \times 10^{5}、休日と平日 A,B \leq 10^{9}なのですべての予定を1日ずつずらして休日に収まるか判定する方法は使えなさそうです。

なのでお気持ちとしては予定のある日が幅 Aに収まるかどうか判定できればよさそうです。

・最初の予定を仮に休日初日として( D_i - D_0を計算して)、 A+Bで割ることで1週間にまとめる

・上記の値を仮に xとして、休日であればそのまま、その週の休日からはみ出す場合は A+Bを引いて休日の何日前という表現に変える

・この処理を D_0除く N-1回行う中で最大値と最小値を記録しておく

・最大値と最小値の幅が休日の日数 Aに収まればYesを出力する

#ABC347 C - Ideal Holidays
N, A, B = map(int, input().split())
D = list(map(int, input().split()))
E = []
mx = 0
mn = 10**10
for i in range(1,N):
    x = (D[i]-D[0])%(A+B)
    if x >= A:
        x = x-(A+B)
    mx = max(mx, x)
    mn = min(mn, x)
    E.append(x)
if mx - mn < A:
    print('Yes')
else:
  print('No')  

提出リンク:提出 #51865968 - AtCoder Beginner Contest 347

ちなみにWA2回は休日の何日前の考え方を思いつく前で、3回目は出力時の判定を \leq Aにしていたことが原因でした。

結果

A~Cの3完、WA3回のペナルティ含めて98分でした。

パフォーマンス856で2回連続の緑パフォ、レートは501→548と順調に伸びました。

C問題はもうちょっと早く発想できていれば、D問題はビット演算に慣れていれば解けたのかな?

いずれにしても茶色中位~緑下位の問題に慣れていないので数こなすのがよさそうです。

 

 

 

 

 

 

ユニークビジョンプログラミングコンテスト2024 春(AtCoder Beginner Contest 346)

コンディション不調により後日バーチャル参加

A - Adjacent Product

リンク:A - Adjacent Product (atcoder.jp)

概要:配列 Aに対して配列 Bの各要素 B_i = A_i \times A_{i+1}を出力してね

そのまま Aを0~Nまで走査して B_iを計算して配列を作成、最後にスペース区切りで出力します。

#ABC346 A - Adjacent Product
N = int(input())
A = list(map(int, input().split()))
B = [0] * (N-1)
for i in range(N-1):
    B[i] = A[i] * A[i+1]
print(*B, sep = ' ')

提出リンク:提出 #51677468 - ユニークビジョンプログラミングコンテスト2024 春(AtCoder Beginner Contest 346)

B - Piano

リンク:B - Piano (atcoder.jp)

概要:wbwbwwbwbwbwが無限に続く文字列 Sに対して、wの数 Wとbの数 Bから作成可能な文字列が Sの部分文字列となりうるか判定してね。

題名の通りピアノの白鍵と黒鍵の並びですね。

1周期(ド~シ)に鍵盤が12個あるので、まずは1周期以下になるよう W Bを引いていきます。

1周期以下に収めると Bの範囲は0~5になるので、この範囲で部分文字列としてありえる組み合わせを全部(といっても最大6通り)書いてしまえばOKです。

例えば処理後の B=1のとき Wが0~3の範囲(b、wb、bw、wwb、wbw、bww、wwbw、wbww)であれば成立します。

以下のコードでは W-Bで場合分けしていますが、上記のように Bの数に着目すると考えやすいと思います。

#ABC346 B - Piano
W, B = map(int, input().split())
ans = 'No'
#問題の文字列が1周期以下になるよう調節
while W+B >= 12:
    W -= 7
    B -= 5
#考えられる組み合わせを全列挙して判定
if W - B == 0:
    if B <= 5:
        ans = 'Yes'
elif W - B == 1 or W - B == 2:
    ans = 'Yes'
elif W - B == 3:
    if B == 2 or B == 3:
        ans = 'Yes'
elif W - B == -1:
    if B == 1 or B == 2 or B == 3:
        ans = 'Yes'
print(ans)  

提出リンク:提出 #51678570 - ユニークビジョンプログラミングコンテスト2024 春(AtCoder Beginner Contest 346)

C - Σ

リンク:C - Σ (atcoder.jp)

概要: K以下で配列 Aに含まれない数字だけを足し上げて出力してね

 Aに含まれない、ということは K以下の数字すべて足したものから[tex :A]に一度以上現れる数字の和を引けばいいと読み替えることができます。

 K以下の自然数の総和は \dfrac{K \times (K+1)}{2}で計算できます。

 Aの重複を削除したのちに A_i \leq Kのものを足し上げて先の総和から引くことで答えとなります。

下記のコードではソートを入れていますが不要だと思います。

#ABC346 C - Σ
N, K = map(int, input().split())
A = list(map(int, input().split()))
#K以下の自然数の総和
tot = K*(K+1)//2
#setで重複削除
B = sorted(set(A))
s = 0
#Ai<=Kのみ足し上げる
for i in B:
    if i <= K:
        s += i
#総和から引き算して出力
ans = tot - s
print(ans)

提出リンク:提出 #51678770 - ユニークビジョンプログラミングコンテスト2024 春(AtCoder Beginner Contest 346)

結果

A~Cの3完51分程度で本番順位だと5,500位くらいでしょうか。

B問題で苦戦しました。

今回は本戦不参加なのでレート変動無しです。