基本情報技術者試験のPythonサンプル問題を解く

はじめに

本記事では、IPAより公表された、基本情報技術者試験(FE)におけるPythonのサンプル問題(link)について解説します。

Pythonによる出題は令和2年度春期試験から実施されるようです。

問題の概要

  • 命令列を解釈実行することによって様々な図形を描くプログラムである。
  • マーカは,現在の位置座標と進行方向の角度を情報としてもつ。
  • 命令列は,命令を;で区切った文字列である。
    • F<val>: <val>の分だけ前進して現在の位置座標を更新する。
    • T<val>: <val>の分だけ回転して角度情報を更新する。
    • R<val>: <val>の分だけE0命令までの命令列を繰り返す。

設問1

a

命令列αは以下の通りです。

R3;R4;F100;T90;E0;T100;E0;

この命令列のうち、特にR4;F100;T90;E0;の部分は現在位置を起点にして長さ100の正方形を描く命令列です。

また、命令列のうち、R3; ... T100;E0;の部分は、...の処理の後に前に100進む処理を3回繰り返す処理です。

つまり、命令列全体は「長さ100の正方形を描いた後、前に100進む」という処理を3回繰り返すものになります。

そのため、命令列αが実行し終わった時点でのマーカの位置は、図3の内の②であり、進行方向は右向き(x軸の正方向)となります。

b

1辺の長さが100の正五角形を描くためには、以下のような処理を行えば良いことがわかります。

- 5回繰り返す
    - 前に100進む
    - 適切な角度回転する

これを問題の命令列に則った書式で書き直すと以下のようになります。

R5;F100;T???;E0;

ここで、回転する角度は以下の画像に示す赤の角度になります。正五角形なので、ここの角度は72度です。

f:id:Szarny:20191225125026p:plain

よって命令列は以下の通りとなります。

R5;F100;T72;E0;

設問2

c

関数parseの処理を分解すると以下のように考えることができます。

- ";" で区切る
- 区切った各要素のうち先頭の命令コードを取り出す
- 区切った各要素のうち先頭の命令コードを除いた数値パラメタを取り出す

問題文のソースコードにおいて、;で区切る処理(s.split(';'))と、各要素のうち先頭の命令コードを取り出す処理(x[0])はすでに記述されているため、残っているのは、各要素のうち先頭の命令コードを除いた数値パラメタを取り出す処理です。

def parse(s):
    return [(x[0], [ c ]) for x in s.split(';')]

数値パラメタは各要素の先頭の1文字を除いたものであるため、ここではxの2文字目以降を表すx[1:]が適切です。

d

関数forwardの処理は以下の通りです。

def forward(self, val):
    # 度数法で表した角度を,ラジアンで表した角度に変換
    rad = math.radians(self.angle)
    dx = val * [ d1 ]
    dy = val * [ d2 ]
    x1, y1, x2, y2 = [ e ], self.x + dx, self.y + dy
    # (x1,y1)と(x2,y2)を結ぶ線分を描画
    plt.plot([x1, x2], [y1, y2], color='black', linewidth=2)
    self.x, self.y = x2, y2

ここで、dx,dyはそれぞれx軸方向及びy軸方向それぞれの軸における、現在の座標から目的とする座標に移動するために必要な座標の差分だと考えられます。

dxがx軸方向における座標の差分であるならば、その値は進む量 * cos(角度)で求められます。これはd1に当てはまります。

また、dyがy軸方向における座標の差分であるならば、その値は進む量 * sin(角度)で求められます。これはd2に当てはまります。

e

関数forwardにおけるex1, y1に代入されています。

plt.plot([x1, x2], [y1, y2], color='black', linewidth=2)という処理を考慮すれば、x1,y1はそれぞれ移動前のx座標とy座標であると考えられます。

よって、eには移動前のx座標とy座標であるself.x, self.yを当てはめれば良いです。

f

関数drawの処理は以下の通りです。

def draw(s):
insts = parse(s)
    marker = Marker()
    stack = []
    opno = 0
    while opno < len(insts):
        print(stack) # <- β>
        code, val = insts[opno]
        if code == 'F':
            marker.forward([ f ])
        elif code == 'T':
            marker.turn([ f ])
        elif code == 'R':
            stack.append({'opno': opno, 'rest': [ f ]})
        elif code == 'E':
            if stack[-1]['rest'] [ g ]:
                [ h ]
                stack[-1]['rest'] -= 1
            else:
                [ i ]
            opno += 1
    marker.show()

fは、移動時や回転時におけるパラメタとして利用されています。そのため、単純に数値パラメタを代入すれば良いです。

code, val = insts[opno]において、実行すべき命令の命令コードと数値パラメタが代入されているため、fにはvalを当てはめれば良いです。

g

gには特定の条件が当てはまります。

特に関係のある部分を抽出してみます。

elif code == 'E':
    if stack[-1]['rest'] [ g ]:
        [ h ]
        stack[-1]['rest'] -= 1
    else:
        [ i ]

この条件分岐は、実行する命令がEである命令、つまりループの終端に到達した時に実行されます。

ここで、Eに到達した時に考えなければならないことは「ループが終わるか、さらに繰り返すか」です。

さて、stack[-1]['rest'] [ g ]という条件がTrueであった場合、stack[-1]['rest'] -= 1という処理が実行されています。これは、ループの残り回数をデクリメントする処理です。つまり、こちら側に分岐した際には、まだループが終わっていないと考えられます(終わっているならばループ回数をデクリメントする必要がないからです)。

以上を考慮すると、stack[-1]['rest'] [ g ]は「ループがまだ終わっていない」を意味する条件であると考えられます。「ループがまだ終わっていない」は「今のループの残り回数がまだ1回より多く残っている」という解釈ができます。

よって、条件はstack[-1]['rest'] > 1であると考えられます。

h

gにて述べた通り、こちら側の分岐はまだループが終わっていない時の分岐です。ループが終わっていない時に行わなければならない処理は以下の通りです。

  • ループの残り回数をデクリメントする
  • 実行する命令をループの先頭に戻す

この内、「ループの残り回数をデクリメントする」はすでに行われているため、「実行する命令をループの先頭に戻す」処理が必要であると考えられます。

実行する命令は変数opnoによって管理されており、実行中のループの先頭命令は、スタックとして用いられているリストの末尾の要素であるstack[-1]に格納されている辞書のopnoキーの値として保持されています。

よってhopno = stack[-1]['opno']であるとわかります。

i

gにて述べた通り、こちら側の分岐はループが終了した際の分岐です。ループが終わった際には、そのループを表す辞書をスタックから取り除く必要があります。

ここで、現在のループを表す辞書はスタックとして用いられているリストの末尾の要素であるstack[-1]に格納されています。

そのため、stack[-1]を取り除く処理であるstack.pop()を実行すれば良いです。

おわりに

基本情報技術者試験(FE)におけるPythonのサンプル問題を行いました。

他の言語よりも比較的敷居が低いため、Pythonの選択者がかなり増えるんじゃないかなあと個人的に思っています。