タブーサーチ(TS)で解くOneMax問題をJavaScriptで書いてみた

※個人の練習用です。アルゴリズムの正確性は担保しません。
ここでいうTSは、タブーサーチ(Tabu Search)のことです。

## タブーサーチ(TS)とは

メタヒューリスティックの探索アルゴリズムの一つである。
TSに関する説明は[Wiki](https://ja.wikipedia.org/wiki/%E3%82%BF%E3%83%96%E3%83%BC%E3%82%B5%E3%83%BC%E3%83%81)または[こちらのレビュー論文](http://www.co.mi.i.nagoya-u.ac.jp/~yagiura/papers/tabu-kaisetsu-2008.pdf)に割愛する。

## JSで書いてみた

TSは探索アルゴリズムの中でも割とシンプルなものなので、
JSでも簡単に書ける。

``` javascript
// タブーリストの長さを設定
const TABU_LIST_MAX_LENGTH = 7;
// One-Max 問題の長さを設定
const ARRAY_WIDTH = 100; //e.g. 4 = [0, 0, 0, 0]
// One-Max なので値は2種類（0,1)
const VALUE_LENGTH = 2; // 0 || 1
// 最大探索回数
const MAX_COUNT = 100;
// 一回あたりの探索区間
const SEARCH_WIDTH = 1;
// 近隣の数
const NEIGHBORS_LENGTH = 10;
// 初期値生成
const INITIAL_VALUE = setValue(VALUE_LENGTH, ARRAY_WIDTH);

console.log("The max width is: ", ARRAY_WIDTH);
console.log("The first value is: ", INITIAL_VALUE);
console.log("The sum of first value is: ", sum(INITIAL_VALUE));

// FIFO
function fifo(tabuList, newAns){
    if(tabuList.length >= TABU_LIST_MAX_LENGTH){
        tabuList.shift() | tabuList.push(newAns);
    }else{
        tabuList.push(newAns);
    }
    console.log("The new tabu list is: ", tabuList);
    return tabuList;
}

function getMaxFromArray(arr){
    return Math.max.apply(null, arr);
}

function getMaxFromNeighborsArr(neighborsArr){
    maxCallback = (max, cur) => Math.max(max , cur);
    return neighborsArr.map(x=>sum(x)).reduce(maxCallback, -Infinity);
}

function getMaxNeighbor(neighbors){
    maxValue = getMaxFromNeighborsArr(neighbors);
    return neighbors.find(neighbor => sum(neighbor) == maxValue);
}

function getBestFromTabuList(tabuList){
    return getMaxFromArray(tabuList);
}

function getWideRandom(max, width){
    let result = [];
    for(let i=width; i>0; i--){
        let num = getRandom(max);
        if(result.includes(num)){
            i++;
        }else{
            result.push(num);
        }
    }
    return result;
}

function getRandom(max){
    return Math.floor(Math.random()*Math.floor(max));
}

function setValue(num, width){
    let result = []
    for(let i=0; i< width; i++){
        result.push(getRandom(num));
    };
    return result;
}

function toggle(num){
    return num == 0 ? 1 : 0;
}

function wideToggle(newAns, posArr){
    posArr.forEach(pos => {
        newAns[pos] = toggle(newAns[pos]);
    })
    return newAns;
}

function sum(valueArr){
    return valueArr.reduce(function(sum, val){
            return sum + val;
        }, 0);
}

function pickNeighbors(currentAns, length){
    let neighbors = [];
    for(let i=length; i>0; i--){
        let neighborArr = Array.from(currentAns);
        let posArr = getWideRandom(ARRAY_WIDTH, SEARCH_WIDTH);
        neighborArr = wideToggle(neighborArr, posArr);
        neighbors.push(neighborArr);
    }
    return neighbors;
}

function pickNeighbor(newAns, neighborsLength){
    neighbors = pickNeighbors(newAns, neighborsLength);
    return getMaxNeighbor(neighbors);
}

function hasValueInTabuList(tabuList, newAns){
    return tabuList.includes(answer => answer == newAns) === true;
}

function compareAnswers(oldAnsArr, newAnsArr, tabuList){
    return sum(newAnsArr) > sum(oldAnsArr) && hasValueInTabuList(tabuList, sum(newAnsArr)) === false;
}

// メイン関数
function find(start){
    let tabuList = [sum(start)];
    let latestAns = Array.from(start);

    for(let i=0;i<MAX_COUNT;i++){
        console.log("Count: ", i);
        let tempNeighbor = pickNeighbor(latestAns, NEIGHBORS_LENGTH);
        console.log("The sum of best neighbor's value is: ", sum(tempNeighbor));

        if(compareAnswers(latestAns, tempNeighbor, tabuList)){
            console.log("Add into tabu list cuz NEW VALUE more then OLD VALUE");
            tabuList = fifo(tabuList, sum(tempNeighbor));
            latestAns = Array.from(tempNeighbor);
        }
    }
    return  getBestFromTabuList(tabuList);
}

// 実行
console.log("the best answer is:",find(INITIAL_VALUE));
```