LoginSignup
45
19

More than 1 year has passed since last update.

【 #ゆめみからの挑戦状 ★第5弾】解答の紹介と総括

Last updated at Posted at 2022-09-15

ツイート貼りすぎてページロード重いですがご容赦ください

問題

@Yametaro さんメインで定期開催していた #ゆめみからの挑戦状 ですが,今回は私から PHP に関連する問題として出題させていただきました。

<?php

$in = [
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
];

// 以下と同じ配列を $in から作って出力してください
//
// [
//     '1st' => 'one',
//     '2nd' => 'two',
//     '3rd' => 'three',
//     '4th' => 'four',
//     '5th' => 'five',
//     '6th' => 'six',
//     '7th' => 'seven',
//     '8th' => 'eight',
//     '9th' => 'nine',
//     '10th' => 'ten',
//     '11th' => 'eleven',
//     '12th' => 'twelve',
//     '13th' => 'thirteen',
//     '14th' => 'fourteen',
//     '15th' => 'fifteen',
// ]

直近が壮大な大喜利問題でしたが,今回は割と真面目です😂
難易度を極端に上げてもアレなので,「あんまり PHP に詳しくなくても一応解ける」ぐらいに調整しました。

「数値添字配列」と「連想配列」が区別されずにぐっちゃぐちゃになる PHP らしい問題です。
(内部的には速度最適化のために区別されているらしいですが,ユーザからは見えにくい部分です)

思考のポイント

今回問われる観点は以下の 4 つです。

  • どのように再帰もしくは平坦化するか
  • どのように 1st 2nd 3rd のような数字付き省略形序数詞を判定するか
  • どのように値とキーを反転させるか
  • どのようにソートさせるか

よく見られたパターン・個人的に紹介したいパターンについて記載します。
面倒なので今回はマニュアルとかへのリンクは割愛します

どのように再帰もしくは平坦化するか

array_walk_recursive に任せる

一番 PHP らしい方法ですね。圧倒的にこれを使った解答が多かったです。

「ネストしている末端部分のキーバリューペアのみを取りたい」

その要求をドンピシャで叶えてくれます。但し,新しい配列をリターンするのではなく,既存配列を手続き的に書き換えるデザインとなっており,新しい変数に入れるためにはコールバックに use での参照変数が必要になるなど,微妙に癖が強い部分はありますね。

<?php

$in = [
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
];

$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
    $out[$key] = $value;
});

echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
    "2nd": "two",
    "four": "4th",
    "three": "3rd",
    "one": "1st",
    "10th": "ten",
    "6th": "six",
    "5th": "five",
    "seven": "7th",
    "fourteen": "14th",
    "11th": "eleven",
    "8th": "eight",
    "thirteen": "13th",
    "12th": "twelve",
    "nine": "9th",
    "15th": "fifteen"
}
*/

RecursiveArrayIterator RecursiveIteratorIterator に任せる

array_walk_recursive の設計上の問題点を克服し,値をリターンする使い方が出来る方法です。 PHP 標準の再帰イテレータの機能を使っています。 これもそこそこ使用者が居たように見えました。

PHP 8.1 から ... がイテレータの展開に対応し,めちゃくちゃ気持ちよく書けるようになりました。

<?php

$in = [
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
];

// PHP 8.1 からはなんとこれでいい!
$out = [...new RecursiveIteratorIterator(new RecursiveArrayIterator($in))];

// PHP 8.0 まではこう
//   - 第 2 引数に false を渡すと array_merge っぽいキー合成になる (... と同じ)
//   - 第 2 引数に true を渡すと array_replace っぽいキー合成になる
// 今回は文字列キーしかないのでどちらでも同じ
//
// $out = iterator_to_array(new RecursiveIteratorIterator(new RecursiveArrayIterator($in)), false);

echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
    "2nd": "two",
    "four": "4th",
    "three": "3rd",
    "one": "1st",
    "10th": "ten",
    "6th": "six",
    "5th": "five",
    "seven": "7th",
    "fourteen": "14th",
    "11th": "eleven",
    "8th": "eight",
    "thirteen": "13th",
    "12th": "twelve",
    "nine": "9th",
    "15th": "fifteen"
}
*/

array_filter array_replace を使った合成

今回は「無限にネストした配列に対応できるようにしてください」とは一言も言っていません。1段階ネストまでならこれで簡単に捌けます。

<?php

$in = [
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
];

$out = array_replace(array_filter($in, 'is_scalar'), ...array_filter($in, 'is_array'));
echo json_encode($out, JSON_PRETTY_PRINT);
/*
さっきまでとは少し順番が違います
{
    "three": "3rd",
    "10th": "ten",
    "5th": "five",
    "seven": "7th",
    "thirteen": "13th",
    "12th": "twelve",
    "nine": "9th",
    "2nd": "two",
    "four": "4th",
    "one": "1st",
    "6th": "six",
    "fourteen": "14th",
    "11th": "eleven",
    "8th": "eight",
    "15th": "fifteen"
}
*/

配列だけを抜き取ったあと 1 段階平坦化して配列以外のものとくっつける,という方法です。

実は今回自分が最初に思いついたものがこれでした。いやー最初はこれ思いついたときキレイだなって思ったんですよ!でも ... 使ったイテレータ展開の方法に長さで負けてたら立場がないわ,PHP 8.1 から強化されたの忘れてたわ🥺

foreach による愚直なループ

説明不要。あんまりかっこよくないので,これを使った方は別の面で芸術点が欲しいところです(何の話だ)

どのように 1st 2nd 3rd のように数字付き省略形序数詞を判定するか

1文字目が数値文字かどうかを見る

初見で度肝を抜かれました。実務だと Uninitialized string offset エラーを引くリスクがあるからあんまり使いたくはないですね。

<?php

var_dump(is_numeric('1st'[0])); // true
var_dump(is_numeric('first'[0])); // false

var_dump(ctype_digit('1st'[0])); // true
var_dump(ctype_digit('first'[0])); // false

preg_match を使っている方も多数いらっしゃいましたが,このへんは PHP に精通している人なら正規表現を使わずに捌く部分ですね。

整数型にキャストする

わりとこっちがオーソドックスなんじゃないかなと思います。 以下の説明中の (bool) キャストは省略してもいいです。

<?php

var_dump((bool)(int)('1st')); // true
var_dump((bool)(int)('first')); // false

var_dump(boolval(intval('1st'))); // true
var_dump(boolval(intval('first'))); // false

var_dump((int)'1st' > 0); // true
var_dump((int)'first' > 0); // false

// var_dump('1st' > 0); // true
// var_dump('first' > 0); // ← PHP 8.0 以降と 7.4 までで動作が変わっています!要注意

stringint の曖昧比較は PHP 8.0 で破壊的変更が入ったので注意してください。

ctype_alpha の否定を使う

「全てがアルファベットではない」 → 「少なくとも数字が 1 つ以上含まれている」

<?php

var_dump(!ctype_alpha('1st')); // true
var_dump(!ctype_alpha('first')); // false

今回に限りこういう方法も使えますね。

どのように値とキーを反転させるか

array_walk_recursive または foreach ループの中で if または三項演算子

最も愚直な方法。

<?php

$in = [
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
];

$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
    if ((int)$value) {
        $out[$value] = $key;
    } else {
        $out[$key] = $value;
    }
});

echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
    "2nd": "two",
    "4th": "four",
    "3rd": "three",
    "1st": "one",
    "10th": "ten",
    "6th": "six",
    "5th": "five",
    "7th": "seven",
    "14th": "fourteen",
    "11th": "eleven",
    "8th": "eight",
    "13th": "thirteen",
    "12th": "twelve",
    "9th": "nine",
    "15th": "fifteen"
}
*/

少しオシャレな書き方だと,こういうのもありました。

$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
    (int)$value ? $out[$value] = $key : $out[$key] = $value;
});
$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
    $out[(int)$value ? $value : $key] = (int)$value ? $key : $value;
});
$out = [];
array_walk_recursive($in, function ($value, $key) use (&$out) {
    $out += (int)$value ? [$value => $key] : [$key => $value];
});

亜種1: min max で比較する

こちらは解答からピックアップ。初見でかなりびっくりしました。少し分かりやすいように書き下します。

<?php

$in = [
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
];

$out = [];
array_walk_recursive($in, function (...$pair) use (&$out) {
    $out[min($pair)] = max($pair);
});

echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
    "2nd": "two",
    "4th": "four",
    "3rd": "three",
    "1st": "one",
    "10th": "ten",
    "6th": "six",
    "5th": "five",
    "7th": "seven",
    "14th": "fourteen",
    "11th": "eleven",
    "8th": "eight",
    "13th": "thirteen",
    "12th": "twelve",
    "9th": "nine",
    "15th": "fifteen"
}
*/

なるほど, 数字がアルファベットよりも文字コードが若い ことを利用しているんですね!これは賢い
ゴルフ専用なのでプロダクションでは使えませんが,なかなかエレガントだと思いました。

亜種2: 引数を sort する

@tadsan 氏の解答自体はこの記事最後の大喜利部門で取り上げているのですが,他にも工夫点があって見落としていたので追記で紹介します。

<?php

$in = [
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
];

$out = [];
array_walk_recursive($in, function (...$pair) use (&$out) {
    sort($pair);
    $out[$pair[0]] = $pair[1];
});

echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
    "2nd": "two",
    "4th": "four",
    "3rd": "three",
    "1st": "one",
    "10th": "ten",
    "6th": "six",
    "5th": "five",
    "7th": "seven",
    "14th": "fourteen",
    "11th": "eleven",
    "8th": "eight",
    "13th": "thirteen",
    "12th": "twelve",
    "9th": "nine",
    "15th": "fifteen"
}
*/

考え方は min max と同じですが,(最後のステップでソートを使うのに)こんなところでもソートが出てくるのが面白いですね。

平坦化済みの配列に対して array_flip を適用する

array_flip という関数はエントリ全体の反転を行ってくれます。それ故に,反転してほしいものだけをまとめて取り出して後から合成という手法を採ることができます。

<?php
$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator([
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
]))];

// 値が 1st, 2nd, ... のものだけを取り出してキー反転
// 値が first, second, ... のものだけを取り出す
// これらを合成
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');

echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
    "4th": "four",
    "3rd": "three",
    "1st": "one",
    "7th": "seven",
    "14th": "fourteen",
    "13th": "thirteen",
    "9th": "nine",
    "2nd": "two",
    "10th": "ten",
    "6th": "six",
    "5th": "five",
    "11th": "eleven",
    "8th": "eight",
    "12th": "twelve",
    "15th": "fifteen"
}
*/

どのようにソートさせるか

ksortSORT_REGULAR SORT_NATURAL を指定

PHP のソート関数,めちゃくちゃ大量にあって選ぶのに困りますが, ちょうど ksort() がキーを基準としてキーバリューペアをソートする関数になります。しかし,そのままでは 10th 以上の値の順序がおかしくなってしまいます。 文字列では "1" "10" "2" という順序になるためです。

<?php

$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator([
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
]))];
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');

ksort($out);
echo json_encode($out, JSON_PRETTY_PRINT);
/*
{
    "10th": "ten",
    "11th": "eleven",
    "12th": "twelve",
    "13th": "thirteen",
    "14th": "fourteen",
    "15th": "fifteen",
    "1st": "one",
    "2nd": "two",
    "3rd": "three",
    "4th": "four",
    "5th": "five",
    "6th": "six",
    "7th": "seven",
    "8th": "eight",
    "9th": "nine"
}
*/

そこで, SORT_NUMERIC SORT_NATURAL いずれかのフラグをつけると比較方法を変えることができ,この問題に対処できます。

フラグ 方式
SORT_NUMERIC (int) キャストされた状態でソート
SORT_NATURAL strnatcmp の自然順比較でソート
<?php

$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator([
    ['2nd' => 'two', 'four' => '4th'],
    'three' => '3rd',
    ['one' => '1st'],
    '10th' => 'ten',
    ['6th' => 'six'],
    '5th' => 'five',
    'seven' => '7th',
    ['fourteen' => '14th', '11th' => 'eleven'],
    ['8th' => 'eight'],
    'thirteen' => '13th',
    '12th' => 'twelve',
    'nine' => '9th',
    ['15th' => 'fifteen'],
]))];
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');

ksort($out, SORT_NUMERIC);
echo json_encode($out, JSON_PRETTY_PRINT);

ksort($out, SORT_NATURAL);
echo json_encode($out, JSON_PRETTY_PRINT);

/*
{
    "1st": "one",
    "2nd": "two",
    "3rd": "three",
    "4th": "four",
    "5th": "five",
    "6th": "six",
    "7th": "seven",
    "8th": "eight",
    "9th": "nine",
    "10th": "ten",
    "11th": "eleven",
    "12th": "twelve",
    "13th": "thirteen",
    "14th": "fourteen",
    "15th": "fifteen"
}
*/

ほか, uksort でコールバック関数として "strnatcmp" を指定する方法もあります。 SORT_NATURAL を使った方法と動作的には全く同じです。

uksort($out, 'strnatcmp');

解答紹介

模範解答以外は大喜利メインでいきます。

問題作成者推奨解答

$x = [...new RecursiveIteratorIterator(new RecursiveArrayIterator($in))];
$out = array_flip(array_filter($x, 'intval')) + array_filter($x, 'ctype_alpha');
ksort($out, SORT_NATURAL);

一般的部門 (+大喜利になりきれなかった部門)

愚直な考え方も大事です

1行に押し込めばいいって問題じゃないよ!

上に同じ。 ctype_alpha に気づいたところはいいね!

array_walk_recursive 部門なら模範解答

配列で += 操作してくれる人好きです
配列結合演算子, array_merge, array_replace を徹底比較 - Qiita

$iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($in));

$tmp = array_fill(1, iterator_count($iterator), "");
foreach($iterator as $key => $value) {
    if (preg_match("/^([0-9]+)/", $key, $match) === 1) {
        $index = $match[1];
        $counter = $key;
        $number = $value;
    } else {
        preg_match("/^([0-9]+)/", $value, $match);
        $index = $match[1];
        $counter = $value;
        $number = $key;
    }
    
    $tmp[$index] = [$counter => $number];
}

$result = array_reduce($tmp, "array_merge", []);
var_dump($result);

こういうのバケツソートって言うんですよね

でぃーほりさんが初めて array_flip 使った解答出してくれたので嬉しかった

array_walk_recursive を限界まで削った形

読みにくいけどやってることは普通なので一般部門! array_reduce 使った解答はこれが初観測

match 式は好きです

$it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($in));
$flatNum = iterator_to_array(new \RegexIterator($it, '/^\d/'));
$flatAlpha = iterator_to_array(new \RegexIterator($it, '/^[^\d]/'));
$out = array_merge(array_flip($flatNum), $flatAlpha);
ksort($out, SORT_NATURAL);
var_dump($out);

RegexIterator に芸術点を加算

大喜利部門

モダン PHP をぶっ壊す!!!

唐突な strtotime 見たとき腹抱えて笑いました。わざわざ序数詞を日付にしなくてもどうにかなるよw

こういう問題出すとソースコードコメント解析して解答を eval 錬成するマン絶対おるんよ

ドMかな?

$ys = [];
foreach ($in as $k => $xs)
  if ([] < $xs)
    foreach ($xs as $l => $x)
      $ys[(int)[$x, $l][$l < $x]] = [[$x, $l], [$l, $x]][$l < $x];
  else
    $ys[(int)[$xs, $k][$k < $xs]] = [[$xs, $k], [$k, $xs]][$k < $xs];
$zs = [];
for ($i = true; $ys[$i] ?? false; $i += true)
  $zs[$ys[$i][false]] = $ys[$i][true];

echo "[\n";
foreach ($zs as $k => $z) {
  echo "    '{$k}' => '{$z}',\n";
}
echo "]\n";

別方面のドM。これはすごい。 関数コールも数値リテラルも 1 つもないようです。 これは PHP を熟知していないと出来ない技。

$in を使って」の条件は満たしているので合格です

全部 json_encode() してパースマンが数人居ました。 json_decode() 職人してる人は加点します

preg_match_all('/"(\d+)\w+"/', $json = json_encode($in), $m);
$out = [];
$date = new DateTimeImmutable;
for($i = max(1, min($m[1])), $max = min(31, max($m[1])); $i <= $max; $i++) {
  $key = $date->setDate(2022, 1, $i)->format('jS');
  if(preg_match('/("(\w+)":)?"'.$key.'"(:"(\w+)")?/', $json, $m) && $value = $m[2] ?: $m[4]) {
    $out[$key] = $value;
  }
}
print_r($out);

JSON + 日付職人の合わせ技。これはすごい

ob_start() + ob_get_clean(),この上ないぐらい PHP らしくて好き

こちらは var_export()

http_build_query()

ごめん読めない

配列と可変長引数の交換が忙しすぎる

SplPriorityQueue!久しぶりに名前聞いたな

シェル芸を我慢できなかった人

こちらもただのシェル芸…かと思ったら,よく見たら スリープソート の実装でした。プロセス開いてるほうがインパクト強すぎて最初気づかなかったw

PHP はプロセス開かないと JavaScript みたいな遅延処理を実現できないんだなぁ…

乱数のシードをわざわざ見つけてきた人。 これが一番びっくりした

$in をどっかに忘れてきた人たち。 @rana_kualu さんのはさすがに草

職場部門

表彰

優勝

@akebi_mh さん

めっちゃいっぱい回答してくれて感激です。 min() max() の交換が目からウロコでした。

準優勝(追加)

@tadsan さん

せっかくスリープソートで実装してくれたので追加で表彰します!
(最初気づかなくてすみません

ナイスゴルフ賞

@rana_kualu さん

ゴルフといえばこの方の印象。問題文読んでない人他にもたくさんいたので細かいことは気にしないです(気にしろよ)

ナイスアルゴリズム賞

@tyabu12 さん

乱数調整に度肝を抜かれました。

PHP エキスパート賞(追加)

@nsfisis さん

難易度の高い縛りプレイに拍手。
難易度高いなぁと改めて思ったので追加しました!

45
19
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
19