🔙
🔝

【paiza問題集 解説】
スタック・キューメニュー応用編

満員エレベーター

満員エレベーター

STEP: 1 3 つのスタック

3 つのスタック

    N = int(input())
    
    stacks = [[] for _ in range(3)]
    for _ in range(N):
        query = iter(input().split())
        Q, S = next(query), int(next(query)) - 1
        
        if Q == 'push':
            X = int(next(query))
            stacks[S].append(X)
        elif Q == 'pop' and stacks[S]:
            stacks[S].pop()
    
    for stack in stacks:
        if stack:
            print(*stack[::-1], sep='\n')
    

    スタックを3つ用意するのですが、3つの変数に用意するよりは1つのリストに3つ用意したほうが扱い易いのでそのようにします。

    クエリはイテレータとして受け取り、 next() 関数を使って割り振ります。

    'push' については スタックS に X を追加するだけで済みますが、'pop' は問題文に、

    『スタックの要素が存在しない場合は何もしない。』

    とありますが、条件には、

    『空のスタックから要素を取り出すような pop は与えられないことが保証されている。』

    とあります。プログラム例では問題文のほうに従って書きましたが、条件に書かれているほうも受け入れて書くなら、

        else:
            stacks[S].pop()
    

    とだけで正しく動作するはずです。


    最後の画面出力ですが、スタックに入っている要素を末尾から取り出していきますので逆順にします。スタックの中身はなくなりますが、単に要素がなくなるまで .pop() で取り出しても良いでしょう。

    ただし要素が空の場合は空行が表示されてしまいますので、要素があるかどうかを でチェックして表示されないようにする必要があります。

STEP: 2 レンタルビデオ店

レンタルビデオ店

    N = int(input())
    X = [int(input()) for _ in range(N)]
    K = int(input())
    
    for _ in range(K):
        query = iter(input().split())
        Q = next(query)
        
        if Q == 'rent':
            X.pop()
        else:
            X.append(next(query))
    
    print(*X[::-1], sep='\n')
    

    入力される値は『開店時に棚にあるビデオの管理番号が末尾から順に与えられます。』
    条件に『棚が空の状態で rent は与えられないことが保証されている』と、親切設計です。

    その為にプログラムがとてもシンプルに書き上げられます。注意点も何も無くて解説しようがありません。😅

STEP: 3 warp

warp

    N, K = map(int, input().split())
    to = [list(map(int, input().split())) for _ in range(N)]
    
    route = [1]
    for i in range(K):
        next_to = to[route[-1] - 1][i]
        if next_to == -1:
            route.pop()
        else:
            route.append(next_to)
    
        print(route[-1])
    

    問題の意味がわかるまで苦労しました。😓
    問題の入力例2を利用して解説します。

    2
        5 5
        2 3 4 5 -1
        -1 -1 -1 -1 -1
        3 3 3 3 3
        1 2 3 4 5
        5 5 5 5 5
    

    N はマスの数 = 入力行数、K はワープ回数です。マス 1 から開始します。

    マス = 1 2 3 4 5 ←   は現在地

    1 回目のワープ先のマスは 2 です。

      2  3  4  5 -1
     -1 -1 -1 -1 -1
      3  3  3  3  3
      1  2  3  4  5
      5  5  5  5  5
    route = [1] ← 2 を PUSH
    route = [1, 2]
    マス = 1 2 3 4 5 ← 2 にワープする

    print(route[-1])
    2

    2 回目のワープ先は -1 になっていますので、1つ前のマスに戻ります。

      2  3  4  5 -1
     -1 -1 -1 -1 -1
      3  3  3  3  3
      1  2  3  4  5
      5  5  5  5  5
    route = [1, 2] ← -1 なので POP
    route = [1]
    マス = 1 2 3 4 5 ← 1つ前の 1 にワープする

    print(route[-1])
    1

    3 回目のワープ先のマスは 4 です。

      2  3  4  5 -1
     -1 -1 -1 -1 -1
      3  3  3  3  3
      1  2  3  4  5
      5  5  5  5  5
    route = [1] ← 4 を PUSH
    route = [1, 4]
    マス = 1 2 3 4 5 ← 4 にワープする

    print(route[-1])
    4

    4 回目のワープ先のマスは 4 です。

      2  3  4  5 -1
     -1 -1 -1 -1 -1
      3  3  3  3  3
      1  2  3  4  5
      5  5  5  5  5
    route = [1, 4] ← 4 を PUSH
    route = [1, 4, 4]
    マス = 1 2 3 4 5 ← 4 にワープ(?)する

    print(route[-1])
    4

    5 回目のワープ先のマスは 5 です。

      2  3  4  5 -1
     -1 -1 -1 -1 -1
      3  3  3  3  3
      1  2  3  4  5
      5  5  5  5  5
    route = [1, 4, 4] ← 5 を PUSH
    route = [1, 4, 4, 5]
    マス = 1 2 3 4 5 ← 5 にワープする

    print(route[-1])
    5

    【結果】

    2
    1
    4
    4
    5

    何に苦労してたかって、最後に route の中身を順に出力するものと思い込んで、どうしても出力例のとおりにならなくてずっと考えてました。😅
    ワープしたらすぐにそのマスを画面に出力すれば良いだけだったことに気付くのに二日かかりましたよ。🤣

STEP: 4 積読

積読

    N = int(input())
    
    stack = []
    for _ in range(N):
        Q, X = input().split()
        
        if Q == 'buy_book':
            stack.append(int(X))
            continue
    
        y = int(X)
        while y > 0:
            y -= stack.pop()
    
        if y:
            stack.append(abs(y))
        # 読むべき残りページ数が 0 の時は本を戻さない
    
    print(len(stack))
    print(sum(stack))
    

    もう一息プログラムを短くできそうな感じがするのですが、今の私の頭ではこれが限界です。これを上回る良いプログラムが書けたなら、このプログラムに向かってドヤ顔したり鼻で嗤ってやったりしてください。👍

    y というのは 'read Y' の 'Y' のことです。今回読むべきページ数です。
    次の stack.pop() は、一番上に積んである本の未読のページ数です。
    今回読むべきページ数 y、例えば 1000ページとして、本の未読ページ数が 500ページだった場合、

    1000 - 500 = 500 ← 残り読むべきページ数は 500ページ

    となり、の条件式 y > 0True なので、もう1冊本を取って読みます。
    次の本の未読ページが 800ページだった場合、

    500 - 800 = -300 ← 300ページ読み切れなかった

    となり、の条件式 y > 0False となってループを抜けます。

    if y: は、本が読み切れなかった場合は残り300ページとして本の山に戻………します。😓
    読み切れなかったときは必ず y が負数になっているので、これを正数にします。abs(y)
    必ず負数になっているので -y でも大丈夫です。

    y0、つまり本一冊をぴったり読み切った時は読み切った本を本の山に返しませんので何もしません。

FINAL問題 満員エレベーター

満員エレベーター

    N, X = map(int, input().split())
    
    weights = [0]
    len_weights = 0
    for _ in range(N):
        query = iter(input().split())
        Q = next(query)
        
        if Q == 'ride':
            N = int(next(query))
            for _ in range(N):
                w = weights[-1] + int(next(query))
                if w <= X:
                    weights.append(w)
                    len_weights += 1
        else:
            K = int(next(query))
            for _ in range(K):
                weights.pop()
                len_weights -= 1
    
    print(len_weights)
    print(weights[-1])
    

    この問題はスタックを使うとかえって面倒になると思うのですが。😓
    無理矢理にも思えなくもないプログラムになってしまいました。美しくない・・・。

    weights に人数分の総重量が累積和状になって入っています。この最後の要素が現在エレベータに乗っている人たちの総重量です。この要素と次に乗ってくる人の体重を足して、重量オーバーしていないかどうかをチェックします。

    乗員人数は len() 関数を使っても良いのですが、一応乗り降りの都度カウントするようにしました。

カードゲーム

カードゲーム

STEP: 1 3 つのキュー

3 つのキュー

    from collections import deque
    
    N = int(input())
    
    queues = [deque() for _ in range(3)]
    for _ in range(N):
        query = iter(input().split())
    
        Q = next(query)
        if Q == 'push':
            S, X = int(next(query)) - 1, int(next(query))
            queues[S].append(X)
        else:
            S = int(next(query)) - 1
            queues[S].popleft()
    
    for queue in queues:
        for q in queue:
            print(q)
    

    2 つのキュー」の3つ版です。そちらを参考にすればこちらのほうが簡単ですので解けると思います。

    最後、空のリストを画面出力すると空行になりますので、出力のしかたに注意してください。

STEP: 2 品出し

品出し

    from collections import deque
    
    N = int(input())
    
    queue = deque()
    for _ in range(N):
        query = iter(input().split())
    
        Q = next(query)
        if Q == 'add':
            num = int(next(query))
            queue.append(num)
        else:
            X = int(next(query))
            for _ in range(X):
                queue.popleft()
    
    print(*queue, sep='\n')
    

    『 X 個購入する』ところ以外はこれまでと同じ流れでプログラムが組めます。

STEP: 3 キューじゃんけん

キューじゃんけん

    from collections import deque
    
    def judge(p, k):
        if p == k:
            return paiza_win, kyoko_win
        elif janken[p] == k:
            return paiza_win+1, kyoko_win
        else:
            return paiza_win, kyoko_win+1
    
    
    N, K = map(int, input().split())
    Q = zip(*[input().split() for _ in range(N)])
    queue = [deque(tpl) for tpl in Q]
    
    janken = {'R': 'S', 'S': 'P', 'P': 'R'}  # {勝ち: 負け}
    paiza_win, kyoko_win = 0, 0
    for _ in range(K):
        C = input().split()
        q = [queue[i].popleft() for i in range(2)]
        paiza_win, kyoko_win = judge(*q)
    
        for i in range(2):
            if C[i] == 'push':
                queue[i].append(q[i])
    
    if paiza_win == kyoko_win:
        print('draw')
    elif paiza_win > kyoko_win:
        print('paiza')
    else:
        print('kyoko')
    

    余裕ぶっこいてたら急に難易度が上がりましたね。😓
    今回は、

    1. 転置
    2. タプルからリストへの変換
    3. じゃんけんの手の判定
    4. キュー

    プログラム例での組み方の場合ですが、この4つを使います。

    1番の転置は、paiza君と kyokoさんのそれぞれのじゃんけんの手が勝負ごとに与えられています。これを転置して、それぞれ paiza君の手の順番と kyokoさんの手の順番にリスト化します。

    例)
    P R
    S S
    R P

    これを、

    P S R ← paiza君の手
    R S P ← kyokoさんの手

    とします。これを後に deque にしてエンキュー・デキューできるようにします。

    転置については「2章 二次元リスト」で学習できます。

    2番は、zip() の戻り値がタプルになって返ってくるのですが、この後にキューで要素を追加したり取り出したりしますので、deque に変換します。

    queue = [deque(tpl) for tpl in Q]
    

    3番の手の判定ですが、

    janken = {'R': 'S', 'S': 'P', 'P': 'R'}  # {勝ち: 負け}
    

    キーが paiza君が勝つ手、値が kyokoさんの負けの手となっています。これを使って judge() 関数内の、

    elif janken[p] == k:
        return paiza_win+1, kyoko_win
    

    に当てはめると「 paiza君が勝つ手の == kyokoさんの負けの手」となった時には paiza君の勝ち と判定されるようになります。
    pk を反対にすれば、 kyokoさんが勝つ手の値と paiza君の負けの手で判定することができるようになります。

    あいこの判定は最初に済ませていますので、残りは else文に kyokoさんが勝ちとなった処理を書くだけです。

    def judge(p, k):
        if p == k:
            return paiza_win, kyoko_win
        elif janken[p] == k:
            return paiza_win+1, kyoko_win
        else:
            return paiza_win, kyoko_win+1
    
    for i in range(2):
        if C[i] == 'push':
            queue[i].append(q[i])
    

    勝負に使ったブロックをどうするかです。取り出したブロックを「破棄する」はとくに何もしないので書きません。'push' の時は『キューの末尾に挿入する』とありますので、そのようにします。

    最後に結果を画面に出力して完了です。

STEP: 4 満員エスカレーター

満員エスカレーター

    from collections import deque
    
    N, X = map(int, input().split())
    
    persons = deque()
    weight = 0
    len_persons = 0
    for _ in range(N):
        query = iter(input().split())
        Q = next(query)
        
        if Q == 'ride':
            N = int(next(query))
            for _ in range(N):
                w = int(next(query))
                if weight + w <= X:
                    persons.append(w)
                    weight += w
                    len_persons += 1
        else:
            K = int(next(query))
            for _ in range(K):
                weight -= persons.popleft()
                len_persons -= 1
    
    print(len_persons)
    print(weight)
    

    ここまでくると解説すべきところもありませんね。スタック・キューを使わなくても組めますし、使わないほうが簡単明瞭だったりすることもありましたし。😅
    応用編とありますが、ほとんどが前メニューの延長でした。

    逆にスタック・キューを使ったほうがラクな場面もあったりします。けど、あまり意識しなくても普段から .append().pop() を使っていれば自然とスタックになっていたりしますし、 .pop(0) とか .insert(0, a) を多用するところがあったら不自然に感じて deque を思い出すと思います。スタック・キューという言葉すら頭に浮かばない程に。

    クエリが複雑だったせいで難しく感じたり、あまり理解できなかったとしても、この先も .append().pop() を当たり前に使う Python3 でプログラムを組んでいけば、いつか「あ、これがスタックだったのか」と気付く時が来るでしょう。

    スタックがわかればキューもわかる!

FINAL問題 カードゲーム

カードゲーム

    from collections import deque
    
    class Game:
        def __init__(self):
            self.cards = []
            self.turn = 0
    
        def has_keycard(self):
            self.turn += 1
            self.run_query()
            return any(self.cards)
    
        def run_query(self):
            query, x = self.input_event()
            for _ in range(int(x)):
                getattr(self, query)()
    
        def input_event(self):
            event = input().split()
            if self.turn == 1:
                event = ['draw', 5]
            return event
    
        def draw(self):
            self.cards.append(self.deck.pop())
        
        def discard(self):
            self.grave_yard.append(self.deck.pop())
    
        def return_from_the_graveyard(self):
            self.deck.appendleft(self.grave_yard.pop())
    
        def exclude(self):
            self.exclusion.append(self.deck.pop())
    
        def return_from_the_exclusion(self):
            self.deck.appendleft(self.exclusion.pop())
    
        @classmethod
        def initialize(cls, n):
            # 下 <--- [ 山札 ] --- > 上
            cls.deck = deque([False] * 40)
            cls.deck[-n] = True
            cls.grave_yard = []
            cls.exclusion = []
        
    
    def setup():
        N, K = map(int, input().split())
        Game.initialize(N)
        paiza = Game()
    
        return K, paiza
    
    
    def main(K, paiza):
        result = 'Lose'
        for _ in range(K):
            if paiza.has_keycard():
                result = paiza.turn
                break
    
        print(result)
            
    
    if __name__ == '__main__':
        main(*setup())
    

    Bランク問題としてふさわしい複雑さです。順を追って解説します。

    まず setup() 関数とクラスメソッドです。クラスメソッドには「山札」「墓地」「除外」の山を用意します。山札以外は空の状態です。
    山札の上(末尾)から N 番目にキーカードを配置します。

    @classmethod
    def initialize(cls, n):
        # 下 <--- [ 山札 ] --- > 上
        cls.deck = deque([False] * 40)  # 山札
        cls.deck[-n] = True  # 末尾から n 番目にキーカードを配置する
        cls.grave_yard = []  # 墓地
        cls.exclusion = []  # 除外
    

    paiza君のインスタンス変数には paiza君の手札とターン数を持たせています。ターン数は main() 関数の でもカウントできますが、わかりやすくインスタンス内でカウントすることにします。

    def __init__(self):
        self.cards = []
        self.turn = 0
    

    次に main() 関数を一時飛ばして has_keycard() メソッドから説明します。

    def has_keycard(self):
        self.turn += 1
        self.run_query()
        return any(self.cards)
    

    このメソッドはクエリ処理をしたあと、paiza君の手札にキーカードがあるかどうかをチェックします。

    まず run_query() メソッドを実行します。

    def run_query(self):
        query, x = self.input_event()
        for _ in range(int(x)):
            getattr(self, query)()
    
    def input_event(self):
        event = input().split()
        if self.turn == 1:
            event = ['draw', 5]
        return event
    

    input_event() メソッドで入力から得た値を返します。その時、'game_start' の時だけ、山札から5枚カードを取る指示に変えて返します。これは draw() メソッドを5回実行することと同等ですので、そのように値を返します。

    再び run_query() メソッドに戻り、値を queryx に振り分けて 文を実行します。この文では getattr(self, query)() を x 回実行します。

    getattr() は簡単に言えば、ただの文字列 query を関数名(メソッド名も可)として変換する機能です。

  • getattr(オブジェクト, str)

    str
    「文字列 = 関数名」
    「文字列 = メソッド名」

    と、なること。

  • 今回の例で言うと、オブジェクトは paiza です。インスタンスをターゲットとしていますので、オブジェクトは self です。
    str はそのインスタンスのメソッド名と同じ文字列であること。
    最後に引数を、無ければ () だけを付ければ正常に処理できるはずです。

    この方法を使えば、どんなにイベントの数が多かろうとこの一文だけで済んでしまいます。イベント名とメソッド名を一致させることだけ注意すれば、文よりも簡単に思えてくるでしょう。

    そしてメインディッシュのクエリ処理です。問題にも書かれていますが、各メソッドの機能を言葉にしてみます。

    def draw(self):
        self.cards.append(self.deck.pop())
    

    山札からカードを1枚取って、手札に追加する。


    def discard(self):
        self.grave_yard.append(self.deck.pop())
    

    山札からカードを1枚取り、そのカードを墓地の山の一番上に置く。


    def return_from_the_graveyard(self):
        self.deck.appendleft(self.grave_yard.pop())
    

    墓地の山からカードを1枚取り、そのカードを山札の一番下に置く。


    def exclude(self):
        self.exclusion.append(self.deck.pop())
    

    山札のカードを1枚取り、そのカードを除外の山の一番上に置く。


    def return_from_the_exclusion(self):
        self.deck.appendleft(self.exclusion.pop())
    

    除外の山からカードを1枚取り、そのカードを山札の一番下に置く。


    これらメソッドはそれぞれ1枚分の処理しかしていません。それを run_query() 内の で x 回処理するようにしています。

    def run_query(self):
        query, x = self.input_event()
        for _ in range(int(x)):
            getattr(self, query)()
    

    これらの処理をすべて終えると has_keycard() メソッドへ戻ります。

    def has_keycard(self):
        self.turn += 1
        self.run_query()
        return any(self.cards)
    

    main() 関数に戻るのですが、戻り値として現在 paiza君の手札にキーカードがあるかをチェックします。any() 関数は、引数の中に1つでも True がある場合は True を返し、全て False の場合は False を返します。

    def main(K, paiza):
        result = 'Lose'
        for _ in range(K):
            if paiza.has_keycard():
                result = paiza.turn
                break
    
        print(result)
    

    結果出力のための result には初め 'Lose' を代入して初期化しておきます。

    .has_keycard() メソッドから返ってきた値を の評価にかけ、もしキーカードを持っていたなら現在のターン数を result に再代入し、ループを抜けます。

    そして最終結果を画面に出力して完了となります。お疲れさまでした。🏄

    クラス型がまだよくわからないようでしたら、このプログラム例を参考に手続型で組んでみてください。作っていくうちに意味がわかってくると思います。