スクリプト3(どこでも篩)
このページはマス旅の一部です。
PCがあればすぐできる「スクリプト」を使った数学を考えましょう。
今回は、PCで則プログラミングできる使い方をさぐる、「どこでも篩(ふるい)」。
1.どこでも篩
WindowsのコマンドプロンプトやLinuxのターミナルにも、プログラミング言語レベルの構文(条件分岐など)があることはあまり知られていません。
Chromebookでもメニューで選ぶだけでLinux環境が入ります。
Windows(PowerShell)を略して PS、Linux(LinuxのTerminalのbash)を略して BS と書きます。
プロンプトのあとに入力データを書いています。出力は [OUT] からの行です。// からはコメント行です。
<if文が使える>
前回は大量のまとめ計算(反復計算)をやりました。
次に大事になるのが、計算の結果を見て「お宝」だけをゲットすることです。それが「条件判断」です。
最初から偶数だけを作ることもできますが、まずは自然数を並べておいて、
後から「3の倍数だけを取り出す」といったフィルター(篩)をかける方法を考えてみましょう。
現代の条件文の典型である「if」が、スクリプトでもそのまま使えます。
3で割った余りが0であることを判定するには、C言語などと同じく `i % 3 == 0` と書きます。
for構文が `for ... ; do ... ; done` だったように、
if構文は `if ... ; then ... ; fi` という形になります(doがthenに、doneがfiになった構造ですね)。
//BS
for i in {1..20}; do if (( i % 3 == 0 )); then echo -n "$i "; fi ; done; echo ""
[OUT]3 6 9 12 15 18
//PS
//PSでは foreach ($i in 1..20) と構文を合わせ、if文も if (...) { ... } と書きます。
//比較演算子は == ではなく -eq を使います。
foreach ($i in 1..20) { if ($i \% 3 -eq 0){ Write-Host "$i " -NoNewline } }; Write-Host ""
<論理演算を使う>
篩(ふるい)にかけるということは「論理」を使うということです。基本は NOT, AND, OR です。
「3の倍数 または 5の倍数」
「3の倍数 かつ 5の倍数」
「3の倍数でも 5の倍数でもない」を抜き出してみましょう。
BSでは NOTは `!`、ANDは `&&`、ORは `||` というおなじみの書き方です。
//BS
// 3の倍数 または 5の倍数 (OR)
for i in {1..20}; do if (( i % 3 == 0 || i % 5 == 0 )); then echo -n "$i "; fi; done; echo ""
[OUT] 3 5 6 9 10 12 15 18 20
// 3の倍数 かつ 5の倍数 (AND)
for i in {1..20}; do if (( i % 3 == 0 && i % 5 == 0 )); then echo -n "$i "; fi; done; echo ""
[OUT] 15
// 3の倍数でも 5の倍数でもない (ド・モルガンの法則)
for i in {1..20}; do if (( i % 3 != 0 && i % 5 != 0 )); then echo -n "$i "; fi; done; echo ""
for i in {1..20}; do if (( !(i % 3 == 0 || i % 5 == 0) )); then echo -n "$i "; fi; done; echo ""
[OUT] 1 2 4 7 8 11 13 14 16 17 19
PSでは演算子に独自スタイルを使います。
NOTは `-not`、ANDは `-and`、ORは `-or` です。
比較も `==` ではなく `-eq`、
`!=` は `-ne`、`>=` は `-ge`、`<=` は `-le` と書きます。
//PS
foreach ($i in 1..20) { if ($i % 3 -eq 0 -or $i % 5 -eq 0) { Write-Host "$i " -NoNewline } }
foreach ($i in 1..20) { if ($i % 3 -eq 0 -and $i % 5 -eq 0) { Write-Host "$i " -NoNewline } }
foreach ($i in 1..20) { if ($i % 3 -ne 0 -and $i % 5 -ne 0) { Write-Host "$i " -NoNewline } }
<else と分岐(FizzBuzz)>
条件に合わないものを分けるには `else` が必要です。
BSでは `; fi` の前に `; else ...` を、PSでは `else { ... }` を追加します。
3分岐以上(多重分岐)も可能です。
BSでは Python のように `; elif` を使い、PSでは `elseif` を使います。
名作プログラミング問題「FizzBuzz」
(15の倍数は FizzBuzz、3の倍数は Fizz、5の倍数は Buzz、それ以外は数字)を書いてみましょう。
//BS
for i in {1..30}; do if ((i % 15 == 0)); then echo "FizzBuzz"; elif ((i % 3 == 0)); then echo "Fizz"; elif ((i % 5 == 0)); then echo "Buzz"; else echo $i; fi; done
[OUT]
1
2
Fizz
4
Buzz
...
29
FizzBuzz
//PS
foreach ($i in 1..30) { if ($i \% 15 -eq 0) { "FizzBuzz" } elseif ($i % 3 -eq 0) { "Fizz" } elseif ($i \% 5 -eq 0) { "Buzz" } else { $i } }
<世界のナベアツ>
「3で割り切れるか、3のつく数字のときだけアホ(変顔)になる」条件を考えます。
「3がつく」という文字列判定がポイントです。
BSでは `*3*` というワイルドカードパターン(パターンマッチ構文 `[[ ... ]]`)を使います。
//BS
for i in {1..20}; do if ((i % 3 == 0)) || [[ $i == *3* ]]; then echo "変顔"; else echo $i; fi; done
//PSでは -like "*3*" 演算子を使います。
//PS
foreach ($i in 1..20) { if ($i % 3 -eq 0 -or "$i" -like "*3*") { "変顔" } else { $i } }
ここまでは共通のプログラミング的思考で進めてきました。
ここで少し各環境の「独自路線」の面白さを覗いてみましょう。
<独自路線編>
BS(Linux)には、データ処理専用のプログラミング言語 `awk` が標準で備わっています。
データを流し込み、条件フィルター(Where句のような処理)をかけられるのが特徴です。
seq コマンド(連番生成)とパイプライン `|` を組み合わせると、驚くほどスッキリ書けます。
// 3の倍数抽出
seq 1 20 | awk '$1 % 3 == 0 { printf "%s ", $1 } END { print "" }'
// 世界のナベアツ(awkの3項・パターンマッチ構文)
seq 1 20 | awk '$1 % 3 == 0 || $1 ~ /3/ { print "変顔"; next } { print $1 }'
PSではパイプラインで流れてくる「現在のオブジェクト」を `$_`(これ)で参照します。
「`Where-Object { 条件 }`」で篩(ふるい)にかけ、「`ForEach-Object { 処理 }`」で出力します。
// 3の倍数抽出
1..20 | Where-Object { $_ \% 3 -eq 0 } \vert{} ForEach-Object { Write-Host "$_ " -NoNewline }; Write-Host ""
// FizzBuzz
1..20 | ForEach-Object { if ($_ \% 15 -eq 0) { "FizzBuzz" } elseif ($_ % 3 -eq 0) { "Fizz" } elseif ($_ \% 5 -eq 0) { "Buzz" } else { $_ } }
<本題:エラトステネスの篩(素数抽出)>
いよいよ数学の「篩」の代名詞、エラトステネスの篩に挑戦です!
BSには素因数分解を行う `factor` コマンドがあります。
```text
factor 10 ➔ 10: 2 5 (フィールド数 NF=3)
factor 5 ➔ 5: 5 (フィールド数 NF=2 ➔ 素数の証拠!)
factor 8 ➔ 8: 2 2 2 (フィールド数 NF=4)
# factorを出力し、パイプラインで、素数条件(NF==2)の数の第2フィールド(素数自身)をprintする。
for i in {2..50}; do factor $i; done | awk 'NF==2{print $2}'
//PS
//BSと同じくパイプライン(メイン)で流します。チェック条件は{...}の中です。
$nに「そいつ($_)」を読み取らせた上で、1..nの範囲で一時的なミニパイプラインを作ります。
そのミニパイプラインで流れた「こいつ($_)」が「そいつ$n」を割り切る回数(.Count)が2というのがチェック条件ですね。だから、簡単にまとめるとメインパイプラインで流れた数について、約数2という条件をつけて出力するということです。
2..50 | Where-Object { $n=$_; (1..$n | Where-Object { $n % $_ -eq 0 }).Count -eq 2 }
まあ、エラトステネスの篩のアルゴリズムという意味ではちょっと違いますが、
今まで篩にかけても消えてないものは拾い出す。
つまり1回も消されなかったのだから、1で消すとすべての消えるから1以外で数で消しますね。
だから、1以外に自分を消すものが、自分しかない。つまり、約数が2個の数、素数を取り出してます。
数論でgeogebraのKeepIfが大活躍!
2.GeoGebraでのコード化
手続き型で条件判定を行ったターミナルに対して、
GeoGebraではリスト演算コマンド KeepIf を使ってスマートに篩をかけます。
//ただの自然数列2..50のリストnumsを作ります。
nums = Sequence(k, k, 2, 50)
// アプローチ1:IsPrimeコマンド(直接素数判定)
primes0 = KeepIf(IsPrime(k), k, nums)
// アプローチ2:約数の個数が2個のものを残す(Divisorsは約数の「個数」を返します)
primes1 = KeepIf(Divisors(k) == 2, k, nums)
さらに、GeoGebraなら「お宝探し」の先へ進めます。
# 双子素数(差が2である素数のペア)を抽出
twin_primes = Zip( (p, p + 2), p , KeepIf( IsPrime(x) ∧ IsPrime(x + 2), x, nums) )
グラフィックスビューに直線 y = x + 2 を引けば、
双子素数ペアが直線上に美しく団子状に並びますね!