い、い、、いっくし!!

各記事はkwskってコメントすると詳しく書き直します

AtCoder Practice BをJavascriptで解いてみる

※ネタ記事です。息抜きにでもなれば幸いです。

最近はJavascriptに慣れすぎて、他の言語を使うのがおっくうになってしまった。 今回はJavascriptAtCoderのPractice B問題を解いてみようとおもう。

問題の原文は是非公式でみて欲しいが要約するとこうだ。

  1. 長さ26の配列を100回の大小比較でソートせよ
  2. 長さ5の配列を7回の大小比較でソートせよ

幸いJavascriptには既に配列のソートが用意されている。 というわけでこうだ。

const array = [{label: 'A'}, {label:'b'}, .... ];

const compare = (a, b) => {
  return query(a, b) === '<' ? 1 : -1;
};

const result = array.sort(compare);

とおもったらWA(Wrong Answer)の嵐。

ソートが出来ていないわけではないが、ここでソート回数制限に気が付く

たしかNode.jsのソート (Array.prototype.sort)はクイックソートで実装されているはず。 試しにテストをつくって実行してみる。

const array = new Array(26);
for(let i=0; i<array.length; i++) array[i] = Math.random();

let count = 0;
const compare = (a, b) => {
  count++;
  return a-b;
};

array.sort(compare);
console.log(count);

そうすると、だいたい120~160程度の比較が実行されている。 ものぐさせずに計算量を調べてみる。 クイックソートの計算量は最良で n×ln(n) だが最悪で n2 になる。 n = 5 なら 8 ~ 25, n=26 なら 85 ~ 676 になる。

8?! と思うも知れないが、これはあくまでも計算量のオーダーであって、実計算量とはまた異なる。 想像してみればわかるが3つのものを、クイックソートする時に、最良は比較2回でソートは完了する。 3 × ln 3 = 3.296 ... と比較してもわかるが、実際の計算量はそのオーダーから定数が足し引きされる。

というわけで、この問題は最悪計算量を優先して、ヒープソートでも実装すればいいということだ。 で、こうなりました。

Array.prototype.bucket = function (callback) {
    if (!callback) callback = (a, b) => {
        if (a < b) return 1;
        if (a > b) return -1;
        return 0;
    }
    if (typeof callback !== 'function') throw new Error('callback require type of function');

    const full = [];

    const push = (sub, array) => {
        if (sub.length <= 1) full.push(array.concat(sub));
        else {
            for (let i = 0; i < sub.length; i++) {
                const t = sub.filter((x, j) => j !== i);
                push(t, array.concat([sub[i]]));
            }
        }
    };
    push(this.map((x, i) => i), []);
    //    console.log(full);

    const pairs = [];
    for (let i = 0; i < this.length; i++) {
        for (let j = i + 1; j < this.length; j++) {
            pairs.push([i, j]);
        }
    }

    // console.log(pairs);

    const findIndex = (array, value) => {
        let i = 0;
        while (array[i++] !== value);
        return i - 1;
    };

    const run = (pair, current) => {
        if (current.length <= 1) return current[0];
        const result = pair.map(x => {
            return Math.abs(current.length / 2 - current.filter(y => y.indexOf(x[0]) - y.indexOf(x[1]) > 0).length);
        });
        const min = Math.min.apply(null, result);
        const index = findIndex(result, min);

        const [a, b] = [pair[index][0], pair[index][1]];
        const t = callback(this[a], this[b]) || -1;

        return run(
            pair.filter(x => x.indexOf(a) < 0 || x.indexOf(b) < 0),
            current.filter(x => (x.indexOf(a) - x.indexOf(b)) * t > 0)
        );
    };

    const index = run(pairs, full);
    if (!index) throw new Error('can not sort');
    return index.map(x => this[x]);
};

まず、与えられた配列のソート結果の全候補を列挙する。 変数 full がこれに相当し、配列の長さが3なら [[0,1,2] [0,2,1] [1,0,2] [1,2,0] [2,0,1] [2,1,0]] となる。

次に、実際に比較する「2つの数字の組み合わせ」を列挙する。 これは変数 pairs で次のようになる。 [[0, 1] [0, 2] [1, 2]]

ここまででまだ一度も比較を実行していない。 これだけそろえば、どの数字を比較すれば、最も次の候補を減らせるかを探索すればいい。

例えば [0,1] のペアで比較をすると、先の full を2つに分割されて、 0 > 1 :: [0,1,2], [0,2,1], [2,0,1] 0 < 1 :: [1,0,2], [1,2,0], [2,1,0] となる。

実際に比較した結果をもとに候補を絞っていけばいい。

具体的には配列 [100, 7, 256] が与えられたとき、各ペアに対して

[0,1] で比較すると次の候補は 1. [0,1,2], [0,2,1], [2,0,1] 2. [1,0,2], [1,2,0], [2,1,0]

[0,2] で比較すると次の候補は 1. [0,1,2], [0,2,1], [1,0,2] 2. [1,2,0], [2,0,1], [2,1,0]

[1,2] で比較すると次の候補は 1. [0,1,2], [1,0,2], [1,2,0] 2. [0,2,1], [2,0,1], [2,1,0]

となり、どのペアで比較しても、候補は二分される。 [0,1]で比較すると、実際の数字は 100 > 7 であるため、次の候補は [[1,0,2], [1,2,0], [2,1,0]]に絞られる。

2回目の比較をしたときに候補の増減を調べると

[0,1] で比較すると次の候補は 1. [] 2. [1,0,2], [1,2,0], [2,1,0]

[0,2] で比較すると次の候補は 1. [1,0,2] 2. [1,2,0], [2,1,0]

[1,2] で比較すると次の候補は 1. [1,0,2], [1,2,0] 2. [2,1,0]

となる。ここで、優先すべきは最悪計算量を減らすことであるため、比較の結果のばらつぎが なるべく少ない比較のペアを選びたい。[0, 2]と[1,2] ではどちらもばらつきは 1 であるためどちらを選んでもいい。 実際に[0,2]で比較をすれば 100 < 256 であるため、次の候補は [[1,2,0], [2,1,0]] に絞られる。

このような戦略で行けば、常に最小の比較回数で、ソートが完了する。 試しに次のコードで比較回数を調べてみる。

require('../lib/ArrayPrototype');

const n = 10;
const a = new Array(n);
for (let i = 0; i < n; i++) a[i] = ({ name: `N${i}`, value: Math.random() });

let count = 0;
const b = a.bucket((a, b) => {
    count++;
    return a.value - b.value
});

console.log(`Count: ${count}`);
b.map(x => console.log(x.value));

長さ5の配列を100回ソートしてみたが、必要な比較回数は、6~7 (ave. 6.97)となり、 目論見通り7回の比較で終了することが確かめられた。 入力に制限がない配列のソート方法としては最小の計算量となるはずだ。

それでは、提出してみよう

f:id:ixsiid:20180201232840p:plain

アルゴリズムを文字だけで説明するのって難しい 難しくない?