第32回シェル芸勉強会 Q7 の解答

前置き

今日は第32回シェル芸勉強会のQ7です。

第32回シェル芸勉強会の問題は次のURLにあります。
【問題のみ】jus共催 第32回全くインスタ映えしないシェル芸勉強会 | 上田ブログ

思考を忠実にアウトプットすることを目標にしています。

Q7 問題(上記URLからコピー)

次のファイルについて、次の処理をやってください。

ある数字について、上下左右の数字どれか1つに0が含まれる場合は0、そうでなければ1にする。
次に、上下左右の数字どれか1つに1が含まれる場合は1、そうでなければ0にする。

$ cat image.txt
00010000000000001000000
00000000111111111110000
01111110011111111110001
00011110001111111111000
10011110001111111111000
00000001000000001000000

正解の出力を示します。

00000000000000001000000
00000000001111111100000
00001100011111111110000
00011110001111111111000
00001100000111111110000
00000000000000001000000

問題を見た感想・補足

ライフゲームっぽい。ただ、ワンライナーでやるのはしんどそう。

解答1

cat image.txt | sed 's#.#& #g' | awk '{for(i=1;i<=NF;i++){print NR","i","$i}}' | awk -F, '{a[$1][$2]=$3}END{for(k1 in a){for(k2 in a[k1]){print k1","k2","(a[k1-1][k2]&&a[k1+1][k2]&&a[k1][k2-1]&&a[k1][k2+1])}}}' | awk -F, '{a[$1][$2]=$3}END{for(k1 in a){for(k2 in a[k1]){print k1","k2","(a[k1-1][k2]||a[k1+1][k2]||a[k1][k2-1]||a[k1][k2+1])}}}' | sed 's#.*,##' | tr -d \\n | fold -w 23 | awk 1

解説

とりあえず縦横の数を数えます。

$ cat image.txt | wc
      6       6     144
$ cat image.txt | sed 1\!d | grep -o . | wc
     23      23      46

どうやら 6x23のようです。

2次元配列っぽいデータだとワンライナーで処理しづらいので、座標-値に変換します。(以下の例だとCSVライクに出力します)

$ cat image.txt | sed 's#.#& #g' | awk '{for(i=1;i<=NF;i++){print NR","i","$i}}' | head
1,1,0
1,2,0
1,3,0
1,4,1
1,5,0
1,6,0
1,7,0
1,8,0
1,9,0
1,10,0

で、次の

ある数字について、上下左右の数字どれか1つに0が含まれる場合は0、そうでなければ1にする。

の結果は座標ごとに、
y_{ij} = x_{(i-1)j} \&\& x_{i(j-1)} \&\& x_{(i+1)j} \&\& x_{i(j+1)}
を求めればいいです。

cat image.txt | sed 's#.#& #g' | awk '{for(i=1;i<=NF;i++){print NR","i","$i}}' | awk -F, '{a[$1][$2]=$3}END{for(k1 in a){for(k2 in a[k1]){print k1","k2","(a[k1-1][k2]&&a[k1+1][k2]&&a[k1][k2-1]&&a[k1][k2+1])}}}'

ザ・ゴリ押しですが、awkの2次元配列に入れて(結局2次元配列に入れてる。。。)論理積を取ります。
awkは未定義変数(a[0][1]など(1オリジンのため0が未定義))を参照したときに0になるので、この問題としては良くないですが、今回のデータには辺に1が3つ並んでいないので大丈夫です。

最後の

次に、上下左右の数字どれか1つに1が含まれる場合は1、そうでなければ0にする。

も同様にゴリ押しして、整形すれば答えになります。(今回は論理和です。)

$ cat image.txt | sed 's#.#& #g' | awk '{for(i=1;i<=NF;i++){print NR","i","$i}}' | awk -F, '{a[$1][$2]=$3}END{for(k1 in a){for(k2 in a[k1]){print k1","k2","(a[k1-1][k2]&&a[k1+1][k2]&&a[k1][k2-1]&&a[k1][k2+1])}}}' | awk -F, '{a[$1][$2]=$3}END{for(k1 in a){for(k2 in a[k1]){print k1","k2","(a[k1-1][k2]||a[k1+1][k2]||a[k1][k2-1]||a[k1][k2+1])}}}' | sed 's#.*,##' | tr -d \\n | fold -w 23 | awk 1
00000000000000001000000
00000000001111111100000
00001100011111111110000
00011110001111111111000
00001100000111111110000
00000000000000001000000

まとめ

awkによるゴリ押ししか形になりませんでした。
ただ、2次元配列を2回以上パイプするには座標にしておくのが便利でした。

# ホントはtee >(grep -e $((1-1))),$((1)) -e $((1+1))),$((1)) -e $((1))),$((1-1)) -e $((1))),$((1+1)) | cut -d, -f3 ....) | tee ... をブレース展開で大量に作りたかったですが、最初のgrepでブレース展開すると座標に戻すにはどうすればいいかわかりませんでした。。。

感想

xy座標に変換したらイケそうと思ったが、結局思いつかなかった。
awkでゴリ推した他にも、foldマジックナンバーを渡していたり、雑にawk 1をしてるのが少し嫌