PyScript①(Hello, World!を表示)

PyScript

PyScriptを使うと、Htmlの中でPythonのコードを実行することができます。

インストール等は必要ありませんが、HTML読み込み時にランライムのロードやコンポーネントの初期化処理があるため数秒待つ必要があります。

サンプルコード

「Hello, World!」を表示するサンプルを作成します。

PyScriptを実行するためには、HTMLのヘッダー部に次の2行を追加します。

1
2
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>

Pythonコードは、py-scriptタグの間に記載します。(7~9行目)

[ソースコード]

helloworld.html
1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<py-script>
print('Hello, World!')
</py-script>
</body>
</html>

[ブラウザ表示]

ブラウザ上に、Pythonコードのprintで表示した文字列‘Hello, World!’を表示することができました。

重複組み合わせ

問題(重複組み合わせ)

$ n $ 種類の品物があり、 $ i $ 番目の品物は $ a_i $ 個あります。

異なる種類の品物同士は区別できますが、同じ種類の品物同士は区別できません。

これらの品物の中から $ m $ 個選ぶ組み合わせの総数を求めて下さい。

[制約]
🔹$ 1 \leqq n \leqq 1000 $
🔹$ 1 \leqq m \leqq 1000 $
🔹$ 1 \leqq a_i \leqq 1000 $

解き方・ソースコード

重複なく数え上げるために、同じ種類の品物を一度に処理します。

漸化式を次のように定義します。

$$ dp[i+1][j] := i 番目までの品物からj個選ぶ組み合わせの総数 $$

$ i $ 番目までの品物から $ j $ 個選ぶためには、$ i - 1 $ 番目までの品物から $ j - k $ 個選んで、$ i $ 番目の品物を $ k $ 加えればよいので

$$ dp[i+1][j] = \sum_{k=0}^{min(j,a[i])}dp[i][j-k] $$

という漸化式が成り立ちます。

この漸化式は計算量が $ O(nm^2) $ となりますが、

$$ \sum_{k=0}^{min(j,a[i])}dp[i][j-k] = \sum_{k=0}^{min(j-1,a[i])}dp[i][j-1-k]+dp[i][j]-dp[i][j-1-a_i] $$

であるので、

$$ dp[i+1][j]=dp[i+1][j-1] + dp[i][j]-dp[i][j-1-a_i] $$

と変形することができ、計算量は$ O(nm) $ となります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#--------- 入力例 ----------
n = 3 # 品物の数
a = [1, 2, 3] # 各品物の個数
m = 3 # 分割数
#---------------------------
memo = {}

# 1つも選ばない方法は常に一通り
for i in range(n + 1):
memo[i, 0] = 1

for i in range(n): # 品物の数に対応するループ
for j in range(1, m + 1): # 分割数に対応するループ
if j - i -a[i] >= 0:
memo[i + 1, j] = memo.get((i + 1, j - 1), 0) + memo.get((i, j), 0) - memo.get((i, j - 1 - a[i]), 0)
else:
memo[i + 1, j] = memo.get((i + 1, j - 1), 0) + memo.get((i, j), 0)

print('解:', memo[m, n])

[実行結果]

解: 6

[1 2 0]、[1 1 1]、[1 0 2]、[0 2 1]、[0 1 2]、[0 0 3]の6パターンあるので、解は6となります。

分割数

問題(分割数)

$ n $ 個の品物を、$ m $ 個以下に分割する方法の総数を求めて下さい。

解き方・ソースコード

n の m 分割 $ a_i ( \sum_{i=1}^{m}a_i=n ) $ を考えてみます。

全ての $ i $ で $ a_i > 0 $ ならば、$ {a_i - 1} $ は $ n - m $ の $ m $ 分割となります。

また、$ a_i = 0 $ となる $ i $ が存在したら、これは $ n $ の $ m - 1 $ 分割となります。

したがって、次のような漸化式を立てることができます。

$$ dp[i][j] = dp[i][j - i] + dp[i - 1][j] $$

この漸化式ならば重複なく分割数を数え上げることができ、計算量は $ 0(mn) $ となります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
#--------- 入力例 -----------
n = 4 # 品物の数
m = 3 # 分割数
#----------------------------
memo = {(0,0): 1}

for i in range(1, m + 1): # 分割数に対応するループ
for j in range(n + 1): # 品物の数に対応するループ
if j - i >= 0:
memo[i, j] = memo.get((i - 1, j), 0) + memo.get((i, j - i), 0)
else:
memo[i, j] = memo[i - 1, j]
print('解:', memo[m, n])

[実行結果]

解: 4

4個の品物を3分割以下にする方法は、[1 1 2]、[1 3]、[2 2]、[4]の4パターンあるので、解は4となります。

逆ポーランド記法

逆ポーランド記法

数学的な演算を簡単にするのに逆ポーランド記法があります。

逆ポーランド記法では、「1 + 2」という書き方を「1 2 +」というように演算子を数字の後ろに記述します。

逆ポーランド記法で表現されたものはスタックで処理しやすいという特徴があります。

先頭から順に読み込んで、であればスタックに積み、演算子であればスタックから値を2つ取り出して計算した結果をまたスタックに積む、という操作を繰り返すだけで計算ができます。

ソースコード

「4 * (8 + 2) - (3 -1) * 4」という数式を逆ポーランド記法で表現すると「4 8 2 + * 3 1 - 4 * -」となります。

これを計算するソースコードは次のようになります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def calc(s):
stack = []
for i in s.split(' '):
# スタックの内容を表示
print(stack)
if i == '+':
# +の時はスタックから数字を2つ取り出して加算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a + b)
elif i == '-':
# -の時はスタックから数字を2つ取り出して減算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a - b)
elif i == '*':
# *の時はスタックから数字を2つ取り出して乗算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a * b)
elif i == '/':
# /の時はスタックから数字を2つ取り出して減算し、結果をスタックに格納する
b, a = stack.pop(), stack.pop()
stack.append(a // b)
else:
# 演算子以外(数字)のときはその値を格納する
stack.append(int(i))
return stack[0]

print('解:', calc('4 8 2 + * 3 1 - 4 * -'))

[実行結果]

[]

[4]

[4, 8]

[4, 8, 2]

[4, 10]

[40]

[40, 3]

[40, 3, 1]

[40, 2]

[40, 2, 4]

[40, 8]

解: 32

解は 32 となり「4 * (8 + 2) - (3 -1) * 4」の計算結果として正しいことが確認できます。

A*(エースター)

A*(エースター)

A*(エースター)アルゴリズムは、ダイクストラ法を発展させたアルゴリズムで、ゴールから遠ざかるような無駄な経路は探索しないように工夫することで処理を高速化します。

解き方・ソースコード

ゴールから遠ざかっていることを判断するため、現在値からゴールへのコストの推定値を考えます。

今回はコストの推定値として、マンハッタン距離が与えられているものとします。(21行目)

マンハッタン距離は、各座標の差の絶対値を使うためどの経路でも同じ距離が得られます。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import heapq

def astar(edges, nodes, goal):
dist = [float('inf')] * len(nodes)
dist[0] = 0
q = []
heapq. heappush( q, [0, [0]])

while len(q) > 0:
_, u = heapq.heappop(q)
last = u[-1]
if last == goal:
return u
for i in edges[last]:
if dist[i[0]] > dist[last] + i[1]:
dist[i[0]] = dist[last] + i[1]
# 推定値を踏まえてコストを算出
heapq.heappush(q, [dist[last] + i[1] + nodes[i[0]], u + [i[0]]])
return []

# コストの推定値
nodes = [ 10, 14, 10, 10, 9, 9, 5, 0, 9, 8, 6, 4, 7, 3 ]

# 辺のリスト(終点とコストのリスト)
edges = [ [[4, 1], [5, 1]],
[[2, 12], [3, 4], [4, 15]],
[[1, 12], [9, 2], [11, 6]],
[[1, 4], [5, 3], [8, 3]],
[[1, 15], [0, 1], [6, 6]],
[[0, 1], [3, 3], [6, 4]],
[[4, 6], [5, 4], [10, 1]],
[[11, 4], [13, 5]],
[[3, 3], [9, 1], [10, 5]],
[[2, 2], [8, 1], [12, 1]],
[[6, 1], [8, 5], [13, 3]],
[[2, 6], [7, 4], [12, 5]],
[[9, 1], [11, 5], [13, 6]],
[[7, 5], [10, 3], [12, 6]] ]

print('解:', astar( edges, nodes, 7))

[実行結果]

解: [0, 5, 6, 10, 13, 7]

A*(エースター)アルゴリズムを使って最短経路を求めることができました。

3目並べ(ミニマックス法)

3目並べ(ミニマックス法)

前回は3目並べをランダムでプレイするコードを実装しました。

今回はミニマックス法を使って3目並べをプレイしてみます。

ミニマックス法は相手が自分にとって最も不利になる手を指すと仮定して、最善の手を探す方法です。

解き方・ソースコード

相手が手番の場合は選択できる手の中から最も評価値の高い手を選び、自分が手番の場合は選択できる手の中から最も評価値の低い手を選びます。

ゲームの評価値としては、勝てば $ 1 $ ポイント、負ければ $ -1 $ ポイント、引き分けであれば $ 0 $ ポイントとします。(minmax関数)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
goal = [0b111000000, 0b000111000, 0b000000111,  # 横に3つ並んだケース
0b100100100, 0b010010010, 0b001001001, # 縦に3つ並んだケース
0b100010001, 0b001010100] # 斜めに3つ並んでケース

# 3つが並んだか判定
def check(player):
for mask in goal:
if player & mask == mask:
return True
return False

# ミニマックス法
def minmax(p1, p2, turn):
if check(p2):
if turn: # 自分の手番のときは勝ち
return 1
else:
return -1 # 相手の手番のときは負け

board = p1 | p2
if board == 0b111111111: # すべて置いたら引き分けで終了
return 0

w = [i for i in range(9) if (board & (1 << i)) == 0]

if turn: # 自分の手番の時は最小値を選ぶ
return min([minmax(p2, p1 | (1 << i), not turn) for i in w])
else: # 相手の手番の時は最大値を選ぶ
return max([minmax(p2, p1 | (1 << i), not turn) for i in w])

# 交互に置く
def play(p1, p2, turn):
if check(p2): # 3つ並んでいたら出力して終了
print('勝負あり')
print([bin(p1), bin(p2)])
return

board = p1 | p2
if board == 0b111111111: # すべて置いたら引き分けで終了
print('引き分け')
print([bin(p1), bin(p2)])
return

# 置ける場所を探す
w = [i for i in range(9) if (board & (1 << i)) == 0]
# 各場所に置いたときの評価値を調べる
r = [minmax(p2, p1 | (1 << i), True) for i in w]
print('選択可能なマス目', w)
print('マス目の評価値', r)

# 評価値が一番高いマス目を取得する
j = w[r.index(max(r))]
print('選択するマス目', j)
print('----------------------------')
play(p2, p1 | (1 << j), not turn) # 手番を入れ替えて次を探す

play(0, 0, True) # プレイ開始

[実行結果]

選択可能なマス目 [0, 1, 2, 3, 4, 5, 6, 7, 8]
マス目の評価値 [0, 0, 0, 0, 0, 0, 0, 0, 0]
選択するマス目 0
----------------------------
選択可能なマス目 [1, 2, 3, 4, 5, 6, 7, 8]
マス目の評価値 [-1, -1, -1, 0, -1, -1, -1, -1]
選択するマス目 4
----------------------------
選択可能なマス目 [1, 2, 3, 5, 6, 7, 8]
マス目の評価値 [0, 0, 0, 0, 0, 0, 0]
選択するマス目 1
----------------------------
選択可能なマス目 [2, 3, 5, 6, 7, 8]
マス目の評価値 [0, -1, -1, -1, -1, -1]
選択するマス目 2
----------------------------
選択可能なマス目 [3, 5, 6, 7, 8]
マス目の評価値 [-1, -1, 0, -1, -1]
選択するマス目 6
----------------------------
選択可能なマス目 [3, 5, 7, 8]
マス目の評価値 [0, -1, -1, -1]
選択するマス目 3
----------------------------
選択可能なマス目 [5, 7, 8]
マス目の評価値 [0, -1, -1]
選択するマス目 5
----------------------------
選択可能なマス目 [7, 8]
マス目の評価値 [0, 0]
選択するマス目 7
----------------------------
選択可能なマス目 [8]
マス目の評価値 [0]
選択するマス目 8
----------------------------
引き分け
['0b10011100', '0b101100011']

結果は引き分けとなりました。

3目並べでは両者が最善の手を選べば、確実に引き分けになります。

3目並べ(ランダム)

3目並べ

3目並べは、3×3のマス目に〇と×を交互に書いて、縦・横・斜めのいずれかに3つを並べたほうが勝ちとなるゲームです。

解き方・ソースコード

ビット演算を使って3目並べを実装してみます。

先手と後手を別々の変数で保持します。

また、2進数の各桁を9か所のマスに割り当てます。

縦3マス、横3マスを2進数の1次元配列に対応づけると、空いているマスは先手と後手の変数に対してOR演算を行うと簡単にチェックできます。
(すべてのマスが埋まっていると、OR演算の結果として全てのビットが1になります。)


勝負の判定は、同じ記号が3つ並んだ場合で行うため、3つ並んだパターンを事前に用意しておきます。(goal変数)

このパターンとAND演算を行った結果が、パターンと同じであれば3つ並んだと判定できます。(check関数)


プレイ方法として、コンピュータ同士の対戦で、マス目の空いているところにランダムに置いていくことにします。
(実行するたびに結果が変わります。)

空いている場所(マス目)は、現在の盤面(双方のOR演算した結果)に対して1桁ずつAND演算を行い、0となったものを探します。(28行目)

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import random

goal = [0b111000000, 0b000111000, 0b000000111, # 横に3つ並んだケース
0b100100100, 0b010010010, 0b001001001, # 縦に3つ並んだケース
0b100010001, 0b001010100] # 斜めに3つ並んでケース

# 3つが並んだか判定
def check(player):
for mask in goal:
if player & mask == mask:
return True
return False

# 交互に置く
def play(p1, p2):
if check(p2): # 3つ並んでいたら出力して終了
print('勝負あり')
print([bin(p1), bin(p2)])
return

board = p1 | p2
if board == 0b111111111: # すべて置いたら引き分けで終了
print('引き分け')
print([bin(p1), bin(p2)])
return

# 置ける場所(マス目)を探す
w = [i for i in range(9) if (board & (1 << i)) == 0]
# ランダムに置く
r = random.choice(w)
play(p2, p1 | (1 << r)) # 手番を入れ替えて次を探す

play(0, 0) # プレイ開始

[実行結果(勝負がつく場合)]

勝負あり
['0b10101000', '0b1000111']

下段が3つ並んでいるので勝負がついています。

[実行結果(引き分けの場合)]

引き分け
['0b10100101', '0b101011010']

全てのマスが埋まっていて、3つ並んでいる箇所がないため引き分けとなります。

ハノイの塔

ハノイの塔

ハノイの塔というパズルを考えます。

ハノイの塔では、次のルールのもとにすべての円盤を移動します。

🔹大きさの異なる複数の円盤があり、小さな円盤の上に大きな円盤を積むことはできない。
🔹円盤を置ける場所は3か所あり、最初は1か所にすべて積まれている。
🔹円盤を1回に1枚ずつ移動し、すべての円盤を別の場所に移動するまでの回数を調べる。

解き方・ソースコード

$ n $ 枚の円盤を移動するの必要な最小の移動回数と、その移動手段を求める方法を考えます。

小さな円盤の上に大きな円盤は積めないので、$ n $ 枚の円盤を移動するには $ n - 1 $ 枚を移動した後に、最大の1枚を移動し、その上に $ n - 1 $ 枚を移動することになります。


円盤を置ける3か所をそれぞれ $ a,b,c $ とし、$ a $ から $ b $ に円盤を移動することを $ a $ -> $ b $ と表現します。

移動に必要なパラメータは、「円盤の残り枚数」「移動元」「移動先」「経由場所」の4つで、これらを引数とする関数を定義し、その中で移動内容を出力します。

処理する枚数は、標準入力としプログラム実行時に変えられるようにしています。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 引数 n 円盤の残り枚数
# 引数 src 移動元
# 引数 dist 移動先
# 引数 via 経由場所
def hanoi(n, src, dist, via):
global cnt
cnt += 1
if n > 1:
# n - 1枚を移動元から経由場所に移す
hanoi(n - 1, src, via, dist)
print(src + ' -> ' + dist)
# n -1枚を経由場所から移動先に移す
hanoi(n - 1, via, dist, src)
else:
print(src + ' -> ' + dist)

n = int(input()) # 円盤の枚数(標準入力)

cnt = 0 # 移動回数
hanoi(n, 'a', 'b', 'c')
print('解(移動回数):', cnt)

[実行結果(円盤の枚数が3の場合)]

a -> b

a -> c

b -> c

a -> b

c -> a

c -> b

a -> b

解(移動回数): 7

[実行結果(円盤の枚数が4の場合)]

a -> c

a -> b

c -> b

a -> c

b -> a

b -> c

a -> c

a -> b

c -> b

c -> a

b -> a

c -> b

a -> c

a -> b

c -> b

解(移動回数): 15

マージソート

マージソート

マージソートは、ソートしたいデータが入ったリストを2つに分割することを繰り返し、すべてがバラバラになった状態から、これらのリストを統合(マージ)する方法です。

この統合する際に、そのリスト内で値が小さい順に並ぶように実装することで、全体が1つのリストになったときには全ての値がソート済みになっています。

ソースコード

マージソートを実装すると下記のようになります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
lst = [13, 7, 9, 11, 5, 7, 2, 4, 15, 6]

def merge_sort(lst):
if len(lst) <= 1:
return lst

mid = len(lst) // 2 # 半分の位置を算出
# 再帰的に分割
left = merge_sort(lst[:mid]) # 左側を分割
right = merge_sort(lst[mid:]) # 右側を分割
# 統合
return merge(left, right)

def merge(left, right):
result = []
i, j = 0, 0

while (i < len(left)) and (j < len(right)):
if left[i] <= right[j]: # 左 ≦ 右 の場合
result.append(left[i]) # 左側から1つ取り出して追加
i += 1
else:
result.append(right[i]) # 右側から1つ取り出して追加
j += 1

# 残りをまとめて追加
if i < len(left):
result.extend(left[i:]) # 左側の残りを追加
if j < len(right):
result.extend(right[j:]) # 右側の残りを追加
return result

print('解:', merge_sort(lst))

[実行結果]

解: [2, 5, 6, 6, 7, 6, 9, 9, 13, 15]

マージソートの特徴

マージソートの特徴として、メモリに入りきらないような大容量のデータにも使えることが挙げられます。

分割した領域のそれぞれでソートできるため、複数のディスク装置にあるデータをそれぞれでソートしておき、それを結合しながらソート済みのデータを作成することも可能です。

枝狩り

問題

都道府県からいくつかを選んで、その人口の合計が1千万人にもっと近い組み合わせとその人口を求めてください。

各都道府県の人口は、2015年(平成27年)の国勢調査に基づきます。

2015年(平成27年)国勢調査 各都道府県人口 - https://www.stat.go.jp/data/kokusei/2015/kekka.html

解き方・ソースコード

単純に全ての組み合わせを調べると、各都道府県を「選ぶ」「選ばない」の2通りがあり、47都道府県で $ 2^{47} $ 通りを調べなければなりません。

これは現実的ではありません。


そこで枝狩りという手法を使います。

枝刈りとは、探索の途中において必要な条件を満たしているかどうかをチェックすることです。

必要な条件を満たしていなければ、そこで処理を中断します。


目的の1000万人を超えた場合、それ以上追加しても近づくことはありません。

また、それまでに調べた値より1000万人とのズレが大きいときも調べる必要はありません。

そこでこれらの条件を踏まえた上で、北海道から順に「選ぶ」「選ばない」のパターンを枝狩りしながら再帰的に探索します。

ズレを求めるには、1000万人との差の絶対値(abs関数)を使います。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 近づける値
goal = 10000000
# 各都道府県の人口(北海道から沖縄まで)
pref = [ 5381733, 1308265, 1279594, 2333899, 1023119, 1123891, 1914039, 2916976,
1974255, 1973115, 7266534, 6222666, 13515271, 9126214, 2304264, 1066328,
1154008, 786740, 834930, 2098804, 2031903, 3700305, 7483128, 1815865,
1412916, 2610353, 8839469, 5534800, 1364316, 963579, 573441, 694352,
1921525, 2843990, 1404729, 755733, 976263, 1385262, 728276, 5101556,
832832, 1377187, 1786170, 1166338, 1104069, 1648177, 1433566 ]

min_total = 0
def search(total, pos):
global min_total
if pos >= len(pref): # 人口データ探索終了
return
if total < goal: # 枝狩り① 目標人口数を超えていない場合
if abs(goal - (total + pref[pos])) < abs(goal - min_total): # 枝狩り② ズレが少ない場合
min_total = total + pref[pos] # 合計値を更新
search(total + pref[pos], pos + 1) # 選ぶ場合(その都道府県の人口を足す)
search(total, pos + 1) # 選ばない場合(その都道府県の人口を足さない)

search(0, 0)
print('解:', min_total)

[実行結果]

解: 10000000

青森県、宮城県、福井県、岐阜県、鳥取県、徳島県、佐賀県、長崎県の人口を合計するとちょうど10000000人になります。