見出し画像

Pythonの便利な関数4つ+αを一題で復習 (ABC326 D - ABC Puzzle)

はじめに

競技プログラミングの話です。Pythonには便利な関数が標準で備えられています。上手に使えると、できることが広がってとても楽しいです。複雑な処理をほんの僅かなコードで書けることもあります。コーディング速度もきっと向上するでしょう。

しかしながら、関数を使う機会が少ないままだとなかなか覚えられないものです。使いこなすためには、くりかえし触れて慣れる必要があるでしょう。

この記事では、次の4つの関数を学びます。

  • itertools.product : 複数のリストの直積を作る

  • any : 1つでも真があればTrueを返す

  • enumerate : リストのインデックスと要素をまとめて取得する

  • map : リストの要素に関数・処理を適用する

とても身近な関数が1つ含まれているかもしれませんが……これら4つの関数を、一題で復習できる問題があります。パナソニックグループ プログラミングコンテスト2023(AtCoder Beginner Contest 326)のD問題です。この問題の解説コードを、4つのパートに分けて読み解いていきます。

パートごとに、便利な関数を使わずに同じ処理ができるコードを紹介します。基本的な関数を用いて書き直すことで、関数の意味を直感的に理解できると思います。より詳細を知りたい場合は、文末の『参照』をご活用ください。

AtCoder灰〜緑向けの内容です。ただし、リスト内包表記ジェネレータ式の基礎知識を前提としています。必要に応じて、以下のリンクなどをご参照ください。


問題 

$${N \times N}$$ の空のマス目があります。文字 A,B,C から1文字を選んで、それぞれのマス目に書き込むことができます。文字を書き込まずに空のままでもOKです。

このとき、以下の条件を満たすことができるか判定して、可能な場合は書き込み方を一つ示してください。

  • 各行・各列に A,B,C がちょうど1個含まれる。

  • 各行の左端の文字を縦読みすると、文字列$${R}$$と一致する。

  • 各列の上端の文字を横読みすると、文字列$${C}$$と一致する。

制約

  • $${3 \le N \le 5}$$

  • $${R,C}$$は A,B,C からなる長さ$${N}$$の文字列

解説コードを読み解く

kyopro_friendsさんのユーザ解説とコードを全面的に参考にしています。
最初に、コード全体を一度引用します。

import itertools
N=int(input())
R=input()
C=input()

def head(s):
  for c in s:
    if c!=".":
      return c

for a,b,c in itertools.product(itertools.permutations(range(N)),repeat=3):
  if any(ai==bi or bi==ci or ci==ai for ai,bi,ci in zip(a,b,c)): continue
  S=[["."]*N for _ in range(N)]
  for i,j in enumerate(a): S[i][j]="A"
  for i,j in enumerate(b): S[i][j]="B"
  for i,j in enumerate(c): S[i][j]="C"
  if "".join(map(head,S))==R and "".join(map(head,zip(*S)))==C:
    print("Yes")
    print(*map(lambda s:"".join(s),S),sep="\n")
    exit()
print("No")

このコードを4つのパートに分けて読んでいきます。

(1) itertools.productで全探索

「各行・各列に A,B,C がちょうど1個含まれる」という条件を使って順列全探索をします。

import itertools

for a,b,c in itertools.product(itertools.permutations(range(N)),repeat=3):

で、全パターンを回すことができます。ここで、順列 $${a}$$ の$${i}$$番目の値$${j}$$は、$${i}$$行$${j}$$列に文字 A を配置することに対応しています。$${b, c}$$ についても同様です。

この問題ではマス目に入る文字は3種類で固定されているので、productの繰り返し回数も常に3回です。そのため、forループを3つ重ねればproductの代用ができます。

from itertools import permutations

# 順列全探索 productなしで
for a in permutations(range(N)) :
    for b in permutations(range(N)) :
        for c in permutations(range(N)) :

(参考:itertools.permutationsを重ねて使う方法もあります。ループを回す回数がすこし減ります。順列$${a, b, c}$$はそれぞれ異なる必要があることを利用しています。)

from itertools import permutations

# 順列全探索 productなしで
P = permutations(range(N))
for a, b, c in permutations(P,3) :

(2) anyで枝刈り

(1)のproductによる順列全探索には、同じ位置に別の文字が重複する場合が含まれています。各マス目には一文字しか置けないので、重複を枝刈りします。

  if any(ai==bi or bi==ci or ci==ai for ai,bi,ci in zip(a,b,c)): continue

anyの引数には、()を省略したジェネレータ式が使われています。見た目もすっきりしていますし、リスト内包表記よりも高速になるそうです。

anyを使わないとしたら、一例として以下のコードが考えられます。

# 枝刈り anyなしで
    flag = False
    for ai,bi,ci in zip(a,b,c) :
        if ai==bi or bi==ci or ci==ai :
            flag = True
            break
    if flag : continue

(3) enumerateで順列から配置を作る

さて、順列 $${a,b,c}$$ から具体的な文字の配置 $${S}$$ を作ります。

    S = [["."]*N for _ in range(N)]
    for i,j in enumerate(a) : S[i][j] = "A"
    for i,j in enumerate(b) : S[i][j] = "B"
    for i,j in enumerate(c) : S[i][j] = "C"

enumerateが便利です。繰り返しになりますが、順列$${a}$$の$${i}$$番目の値$${j}$$は、$${i}$$行$${j}$$列に文字 A を配置することに対応しています。

ここはfor文と添字でもシンプルに書けます。

# 配列を作る enumerateなしで
    S = [["."]*N for _ in range(N)]
    for i in range(N) :
        S[i][a[i]] = "A"
        S[i][b[i]] = "B"
        S[i][c[i]] = "C"

(4) mapで縦読み・横読みの判定と出力

$${S}$$ が縦読みと横読みの条件を満たしているかを判定して、合致した場合に$${S}$$を出力して終了します。

まず、関数 head を定義します。

def head(s):
  for c in s:
    if c!=".":
      return c

リストなどを受け取って、空マス "." でない最初の文字を返す関数です。この関数をmapと組み合わせることで、判定を行います。

  if "".join(map(head,S))==R and "".join(map(head,zip(*S)))==C:
    print("Yes")
    print(*map(lambda s:"".join(s),S),sep="\n")
    exit()

mapの第一引数には、組み込み関数だけでなく、自分で定義した関数やラムダ式を入れることができます。

mapの代わりとして、リスト内包表記でも同じ処理ができます。

# 判定と出力 mapなしで
    r = "".join([head(s) for s in S])
    c = "".join([head(s) for s in zip(*S)])
    if r == R and c == C :
        print("Yes")
        ans = ["".join(s) for s in S]
        print(*ans,sep="\n")
        exit()

振り返り・まとめ

mapを入力以外で使ったことがなかったので、解説コード中の

"".join(map(head,S))==R

の部分がとても印象に残った問題でした。調べるうちに、リスト内包表記とジェネレータ式の働きや、便利な関数の使い方についても学ぶことができたので、記事としてまとめてみました。

この記事がよい復習の機会となれば幸いです。

参照

Pythonの文法はnkmkさんのサイトを参考にしています。いつもお世話になっています。感謝。

また、これら4つの関数を使わずに、代替方法でまとめたACコードはこちらです。

以上です。最後まで読んでいただき、ありがとうございました。

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