A*算法求解八数码(九数码)问题(JavaScript版)

网上的代码大多使用C++实现。由于要实现可视化的缘故,因此使用HTML+JS重新实现了一遍。

TEST_MODE 表示使用测试样例进行测试,设置为False即可实现随机输入。

MODE代表使用的启发函数,1表示放错位置个数作为估值函数;2代表曼哈顿距离。

isNineMode代表九数码问题。

s代表开始状态。

e代表结束状态。

AStarAlgorithm函数是A*算法的主体,其他的函数用于作业要求的输出显示,可以忽略。

算法中还实现了JS版的优先队列,但是效率不算高,可以考虑使用堆算法进行优化。

A*算法的Javascript代码

// index.js
var s = [0,1,2,3,4,5,6,7,8];
var e = [1,2,3,8,0,4,7,6,5];
var finalRoad;
var isNineMode = false;
var MODE = 1;
var expandCount = 0;
var S0 = 0;
var TEST_MODE = true;

// 测试用状态
function testData(){
    var data = [[6,1,5,0,2,7,3,8,4],
                [0,8,7,5,3,4,1,2,6],
                [6,3,2,4,5,7,1,0,8],
                [3,8,0,1,4,2,6,7,5],
                [4,1,5,0,3,2,7,6,8],
                [6,7,8,0,3,4,1,2,5],
                [2,1,7,4,8,3,5,0,6],
                [3,4,7,5,6,2,1,8,0],
                [3,8,1,5,0,7,6,2,4],
                [7,8,1,5,6,2,4,3,0]];
    var len = data.length;
    return data[Math.floor(Math.random()*len)];
};

window.onload = function() {
    initial();
}

function initial() {
    if(TEST_MODE) {
        s = testData();
    }
    else{
        s = [0,1,2,3,4,5,6,7,8];
        s.sort(function(){
            return 0.5 - Math.random();
        });
    }
    showInitial(s);
}

document.getElementById("reset8").onclick = function() {
    isNineMode = false;
    MODE = 1;
    initial();
}

document.getElementById("reset9").onclick = function() {
    isNineMode = true;
    MODE = 1;
    initial();
}

document.getElementById("begin8_1").onclick = function() {
    MODE = 1;
    S0 = 0;
    isNineMode = false;
    AStarAlgorithm();
}

document.getElementById("begin8_2").onclick = function() {
    MODE = 2;
    S0 = 0;
    isNineMode = false;
    AStarAlgorithm();
}

document.getElementById("begin9_1").onclick = function() {
    MODE = 1;
    S0 = 0;
    isNineMode = true;
    AStarAlgorithm();
}

document.getElementById("begin9_2").onclick = function() {
    MODE = 2;
    S0 = 0;
    isNineMode = true;
    AStarAlgorithm();
}

function AStarAlgorithm() {
    console.clear();
    var a = new AStar();
    a.setMode(MODE);
    a.init(s, e);
    if(a.getStart().canSolved(a.getEnd())){
        a.AStarAlgorithm();
        a.display(a.getEnd());
        finalRoad = a.getRoad();
        show(0);
    }
    else{
        alert("此状态下无解");
    }
}

function show(i) {
    if(i == 0){
        document.getElementById("result").textContent = "";
        document.getElementById("result").textContent += '总拓展节点数: ' + expandCount + '\n\n';
    }
    if(i < finalRoad.length) {
        if(finalRoad[i].getFValue() > S0) {
            alert("步骤 "+i+" 不满足条件 f(n)≤ f*(S0)");
        }
        if(i >= 1 && MODE == 1 &&
		   finalRoad[i-1].getHValue() > 1+finalRoad[i].getHValue()) {
            alert("步骤 "+i+" 不满足条件 h(ni)≤ 1+h(nj)");
        }
        for(var j = 0; j < finalRoad[i].getCodeMap().length; j++) {
            var b = 'pos'+j;
            document.getElementById(b).style.opacity = 1;
            if(!isNineMode && finalRoad[i].getCodeMap()[j] == 0){
                document.getElementById(b).style.opacity = 0;
            }
            else{
                document.getElementById(b).textContent = "" + finalRoad[i].getCodeMap()[j];
            }
        }
        document.getElementById("result").textContent += "步骤 " + i + ':\n';
        document.getElementById("result").textContent += "评估函数值(F): " + finalRoad[i].getFValue() + "\n";
        if(MODE == 1) {
           document.getElementById("result").textContent += "评估函数值(H): " + finalRoad[i].getHValue() + "\n";
        }
        document.getElementById("result").textContent += "估值最小节点:  " + finalRoad[i].getCodeMap().toString() + '\n\n';
        i++;
        setTimeout('show(' + i +')', 250);
    }
}

function showInitial(m) {
    for(var j = 0; j < m.length; j++) {
        var b = 'pos'+j;
        document.getElementById(b).style.opacity = 1;
        if(!isNineMode && m[j] == 0){
            document.getElementById(b).style.opacity = 0;
        }
        else{
            document.getElementById(b).textContent = "" + m[j];
        }
    }
}

// Astar part

// 创建一个优先队列数据结构
function PriorityQueue() {
    var items = [];
    // 向队列尾部添加一个(或多个)新的项
    this.push = function(element){
        if(this.isEmpty()) {
            items.push(element);
        }
        else {
            var flag = false;
            for(var i = 0; i < items.length; i++){
                if(items[i].getFValue() >= element.getFValue()) {
                    items.splice(i, 0, element);
                    flag = true;
                    break;
                }
            }
            if(!flag){
                items.push(element);
            }
        }
    };
    // 移除队列的第一(即排在队列最前面的)项,并返回被移除的元素
    this.pop = function(){
        return items.shift();
    };
    // 返回队列中第一个元素——最先被添加,也将是最先被移除的元素。队列不做任何变动
    this.top = function(){
        return items[0];
    };
    // 如果队列中不包含任何元素,返回 true ,否则返回 false
    this.isEmpty = function(){
        return items.length == 0;
    };
    // 清空队列
    this.clear = function(){
        items = [];
    };
    // 返回队列包含的元素个数,与数组的 length 属性类似
    this.size = function(){
        return items.length;
    };
    this.print = function(){
        console.log(items.toString());
    };
}

function State() {
    var codeMap = [];
    var parent = null;
    var hValue, gValue, fValue, zeroPos;

    this.init = function(code) {
        hValue = gValue = fValue = 0;
        parent = null;
        for(var i = 0; i < code.length; i++) {
            codeMap.push(code[i]);
            if(code[i] == 0){
                zeroPos = i;
            }
        }
    };

    this.initWithState = function(sta) {
        this.init(sta.getCodeMap());
        this.setAllValueWithState(sta);
    };

    this.setParent = function(sta) {
        parent = sta;
    };

    this.setCodeMap = function(code) {
        codeMap = code;
    };

    this.setZeroPos = function(z) {
        zeroPos = z;
    };

    this.setGValue = function(g) {
        gValue = g;
    };

    this.setFValue = function() {
        fValue = gValue + hValue;
    }

    this.getParent = function() {
        return parent;
    };

    this.getFValue = function() {
        return fValue;
    };

    this.getGValue = function() {
        return gValue;
    };

    this.getHValue = function() {
        return hValue;
    };

    this.getCodeMap = function() {
        return codeMap;
    };

    this.getZeroPos = function() {
        return zeroPos;
    };

    this.setHValue1 = function(src) {
        hValue = 0;
        for(var i = 0; i < codeMap.length; i++) {
            if((isNineMode || codeMap[i] != 0) && codeMap[i] == src.getCodeMap()[i]) {
                hValue++;
            }
        }
        hValue = isNineMode? 9 - hValue : 8 - hValue;
    };

    this.setHValue2 = function(src) {
        hValue = 0;
        for(var i = 0; i < codeMap.length; i++) {
            for(var j = 0; j < src.getCodeMap().length; j++) {
                if((isNineMode || codeMap[i] != 0) && codeMap[i] == src.getCodeMap()[j]) {
                    hValue += Math.abs(parseInt((i-j)/3)) + Math.abs(i % 3 - j % 3);
                }
            }
        }
    };

    this.setAllValue = function(src, depth, mode = 1, isSetG = true) {
        if(mode == 1) {
            this.setHValue1(src);
        } else {
            this.setHValue2(src);
        }
        if(isSetG){
            gValue = depth;
        }
        fValue = gValue + hValue;
    };

    this.setAllValueWithState = function(sta) {
        hValue = sta.getHValue();
        gValue = sta.getGValue();
        zeroPos = sta.getZeroPos();
        this.setFValue();
    };

    this.isEqual = function(src) {
        for(var i = 0; i < codeMap.length; i++) {
            if(src.getCodeMap()[i] != codeMap[i]){
                return false;
            }
        }
        return true;
    };

    this.canSolved = function(tar) {
        var res1 = 0, res2 = 0;
        for(var i = codeMap.length-1; i >= 1; i--) {
            for(var j = i-1; j >= 0; j--) {
                if(codeMap[i] != 0 && codeMap[j] != 0
				   && codeMap[j] > codeMap[i]) {
                    res1++;
                }
                if(tar.getCodeMap()[i] != 0 && tar.getCodeMap()[j] != 0
				   && tar.getCodeMap()[j] > tar.getCodeMap()[i]) {
                    res2++;
                }
            }
        }
        return (res1%2) == (res2%2);
    };

    this.showMap = function() {
        console.log(codeMap);
    }
};

var AStar = function() {
    var OpenTable = [], ClosedTable = [];
    var start = null;
    var end = null;
    var road = [];
    var mode = 1;

    this.init = function(startMap, endMap) {
        start = new State();
        start.init(startMap);
        end = new State();
        end.init(endMap);
    };

    this.isInTable = function(table, src) {
        for(var i = 0; i < table.length; i++) {
            if(table[i].isEqual(src)){
                return i;
            }
        }
        return -1;
    };

    this.setMode = function(m) {
        mode = m;
    };

    this.deleteFromOpenTable = function(src) {
        var i = 0;
        for(i = 0; i < OpenTable.length; i++) {
            if(OpenTable[i].isEqual(src)){
                break;
            }
        }
        OpenTable.splice(i,1);
    };

    this.AStarAlgorithm = function() {
        start.setAllValue(end, 0, mode, true);
        var OpenQueue = new PriorityQueue();
        OpenTable.push(start);
        OpenQueue.push(start);
        var move = [[0,1], [1,0], [0,-1], [-1,0]];
        expandCount = 0;

        while(OpenTable.length > 0) {
            var tmp = OpenQueue.top();
            if(end.isEqual(tmp)) {
                end.setParent(tmp.getParent());
                end.setAllValueWithState(tmp);
                console.log("总扩展节点数:", expandCount);
                S0 = end.getFValue();
                return true;
            }

            OpenQueue.pop();
            ClosedTable.push(tmp);
            this.deleteFromOpenTable(tmp);
            console.log("Open表节点数量:", OpenTable.length);
            console.log("评估值最小节点:", tmp.getCodeMap().toString());
            for(var i = 0; i < 4; i++) {
                var y = tmp.getZeroPos() % 3;
                var x = (tmp.getZeroPos() - y) / 3;
                if(x + move[i][0] < 0 || x + move[i][0] >= 3 ||
                   y + move[i][1] < 0 || y + move[i][1] >= 3) {
                    continue;
                }
                var zPos = (x + move[i][0]) * 3 + (y+move[i][1]);

                if(zPos < 0 || zPos >= tmp.getCodeMap().length) {
                    continue;
                }

                var newState = new State();
                newState.initWithState(tmp);
                var newCodeMap = newState.getCodeMap();
                newCodeMap[tmp.getZeroPos()] = newCodeMap[zPos];
                newCodeMap[zPos] = 0;
                newState.setCodeMap(newCodeMap);
                newState.setParent(tmp);
                newState.setZeroPos(zPos);
                newState.setGValue(tmp.getGValue()+1);

                var flag = this.isInTable(OpenTable, newState);
                var flag0 = this.isInTable(ClosedTable, newState);

                if(flag >= 0) {
                    if(newState.getGValue() < OpenTable[flag].getGValue()){
                        OpenTable[flag].setGValue(newState.getGValue());
                        OpenTable[flag].setParent(newState.getParent());
                        OpenTable[flag].setFValue();
                        newState = null;
                    }
                }
                else if(flag0 >= 0) {
                    if(newState.getGValue() < ClosedTable[flag0].getGValue()){
                        ClosedTable[flag0].setGValue(newState.getGValue());
                        ClosedTable[flag0].setParent(newState.getParent());
                        ClosedTable[flag0].setFValue();
                        newState = null;
                        OpenQueue.push(ClosedTable[flag0]);
                        OpenTable.push(ClosedTable[flag0]);
                        ClosedTable.splice(flag0, 1);
                    }
                }
                else {
                    newState.setAllValue(end, 0, mode, false);
                    OpenQueue.push(newState);
                    OpenTable.push(newState);
                }
                expandCount++;
                if(OpenTable.length >= 15000){
                    alert("该问题有解,但是用此算法无法在有限空间内求解");
                    return false;
                }
            }
        }
        return false;
    };

    this.display = function(p) {
        if(p != null) {
            this.display(p.getParent());
            road.push(p);
        }
    };

    this.getRoad = function() {
        return road;
    }

    this.getStart = function() {
        return start;
    };

    this.getEnd = function() {
        return end;
    };
};

简单界面的HTML代码

<!DOCTYPE html>
<html>
<head>
    <title>A*算法</title>
</head>
<body>
    <h1 id="title">A* 算法</h1>
    <div id="container">
        <button id="pos0" class="numblock">
            1
        </button><button id="pos1" class="numblock">
            2
        </button><button id="pos2" class="numblock">
            3
        </button><button id="pos3" class="numblock">
            4
        </button><button id="pos4" class="numblock">
            5
        </button><button id="pos5" class="numblock">
            6
        </button><button id="pos6" class="numblock">
            7
        </button><button id="pos7" class="numblock">
            8
        </button><button id="pos8" class="numblock">
            &nbsp;
        </button>
    </div>
    <div id="controller">
        <button id="begin8_1">八数码(h1)</button>
        <button id="begin8_2">八数码(h2)</button>
        <button id="begin9_1">九数码(h1)</button>
        <button id="begin9_2">九数码(h2)</button>
    </div>
    <div id="resetcontroller">
        <button id="reset8">重置八数码</button>
        <button id="reset9">重置九数码</button>
    </div>
    <div id="resultcontainer">
        <h4 id="resTitle">输出结果显示</h4>
        <textarea id="result" cols="50" rows="25" readonly="true" resize="false"></textarea>
    </div>

    <script type="text/javascript" src="index.js"></script>

    <style type="text/css">
        .numblock {
            display: inline-block;
            width: 80px;
            height: 80px;
            margin: 1px;
            padding: 0;
            font-size: 26pt;
            outline: none;
            background-color: white;
            border-radius: 10px;
            border-color: lightgrey;
        }

        #title {
            width: 400px;
            margin: 40px auto;
            position: relative;
            text-align: center;
        }

        #container {
            width: 260px;
            height: 260px;
            margin: 0 auto;
            position: relative;
            text-align: center;
        }

        #controller {
            width: 500px;
            margin: 20px auto;
            position: relative;
            text-align: center;
        }

        #controller > button {
            background-color: rgb(46,205,113);
            color: white;
            border-radius: 10px;
            border: 2px;
            width: 100px;
            height: 30px;
            cursor: pointer;
            outline: none;
        }

        #resetcontroller {
            width: 400px;
            margin: 20px auto;
            position: relative;
            text-align: center;
        }

        #resetcontroller > button {
            background-color: rgb(76,139,245);
            color: white;
            border-radius: 10px;
            border: 2px;
            width: 100px;
            height: 30px;
            cursor: pointer;
            outline: none;
        }

        #resultcontainer{
            margin: 5px auto;
            position: relative;
            text-align: center;
        }

        #result {
            resize: none;
        }
    </style>
</body>
</html>
A*算法求解八数码问题 1、A*算法基本思想: 1)建立一个队列,计算初始结点的估价函数f,并将初始结点入队,设置队列头和尾指针。 2)取出队列头(队列头指针所指)的结点,如果该结点是目标结点,则输出路径,程序结束。否则对结点进行扩展。 3)检查扩展出的新结点是否与队列中的结点重复,若与不能再扩展的结点重复(位于队列头指针之前),则将它抛弃;若新结点与待扩展的结点重复(位于队列头指针之后),则比较两个结点的估价函数中g的大小,保留较小g值的结点。跳至第五步。 4)如果扩展出的新结点与队列中的结点不重复,则按照它的估价函数f大小将它插入队列中的头结点后待扩展结点的适当位置,使它们按从小到大的顺序排列,最后更新队列尾指针。 5)如果队列头的结点还可以扩展,直接返回第二步。否则将队列头指针指向下一结点,再返回第二步。 2、程序运行基本环境: 源程序所使用编程语言:C# 编译环境:VS2010,.net framework 4.0 运行环境:.net framework 4.0 3、程序运行界面 可使用程序中的test来随机生成源状态与目标状态 此停顿过程中按Enter即可使程序开始运行W(n)部分; 此停顿部分按Enter后程序退出; 4、无解问题运行情况 这里源程序中是先计算源状态与目标状态的逆序对的奇偶性是否一致来判断是否有解的。下面是无解时的运行画面: 输入无解的一组源状态到目标状态,例如: 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 8 7 0 运行画面如下: 5、性能比较 对于任一给定可解初始状态,状态空间有9!/2=181440个状态;当采用不在位棋子数作为启发函数时,深度超过20时,算法求解速度较慢; 其中启发函数P(n)与W(n)的含义如下: P(n): 任意节点与目标结点之间的距离; W(n): 不在位的将牌数; 源状态 目标状态 P(n) 生成节点数 W(n) 生成节点数 P(n) 扩展节点数 W(n) 扩展节点数 2 8 3 1 6 4 7 0 5 1 2 3 8 0 4 7 6 5 11 13 5 6 1 2 3 8 0 4 7 6 5 0 1 3 8 2 4 7 6 5 6 6 2 2 4 8 2 5 1 6 7 0 3 7 4 2 8 5 6 1 3 0 41 79 22 46 6 2 5 8 7 0 3 1 4 0 3 6 7 1 8 4 5 2 359 10530 220 6769 7 6 3 1 0 4 8 5 2 2 8 7 1 3 4 6 5 0 486 8138 312 5295 下图是解决随机生成的100中状态中,P(n)生成函数的生成节点与扩展节点统计图: 由上图可知,P(n)作为启发函数,平均生成节点数大约在1000左右,平均扩展节点数大约在600左右; 下图是解决随机生成的100中状态中,W(n)生成函数的生成节点与扩展节点统计图: 由上图可知,W (n)作为启发函数,平均生成节点数大约在15000左右,是P(n)作为启发函数时的平均生成节点的15倍;W (n)作为启发函数,平均扩展节点数大约在10000左右,是P(n)作为启发函数时的平均扩展节点的15倍; 下图是解决随机生成的100中状态中,两个生成函数的生成节点与扩展节点统计图: 由上述图表可以看到,将P(n)作为启发函数比将W(n)作为启发函数时,生成节点数与扩展节点数更稳定,相比较来说,采用P(n)作为启发函数的性能比采用W(n)作为启发函数的性能好。 6、源代码说明 1)AStar-EightDigital-Statistics文件夹:用来随机生成100个状态,并对这100个状态分别用P(n)与W(n)分别作为启发函数算出生成节点以及扩展节点,以供生成图表使用;运行界面如下: 2)Test文件夹:将0-8这9个数字随机排序,用来随机生成源状态以及目标状态的;运行界面如下: 3)AStar-EightDigital文件夹:输入源状态和目标状态,程序搜索出P(n)与W(n)分别作为启发函数时的生成节点数以及扩展节点数,并给出从源状态到目标状态的移动步骤;运行界面如下: 提高了运行速度的几处编码思想: 1、 在维护open以及close列表的同时,也维护一个类型为hashtable的open以及close列表,主要用来提高判断当前节点是否在open列表以及close列表中出现时的性能; 2、 对于每个状态,按照从左到右,从上到下,依次将数字拼接起来,形成一个唯一标识identify,通过该标识,可以直接判断两个状态是否是同一个状态,而不需要循环判断每个位置上的数字是否相等 3、 在生成每个状态的唯一标识identify时,同时计算了该状态的空格所在位置,通过空格所在位置,可以直接判断能否进行上移、下移、左移、右移等动作; 4、 只计算初始节点的h值,其它生成的节点的h值是根据当前状态的h值、移动的操作等计算后得出的,规则如下: a) 采用W(n)这种方式,不在位置的将牌数,共有以下3中情况: i. 该数字原不在最终位置上,移动后,在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值-1 ii. 该数字原在最终位置上,移动后,不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 +1 iii. 该数字原不在最终位置上,移动后,还是不在其最终位置上 这种情况下,生成的子节点的h值= 父节点的h值 iv. 该数字原在最终位置上,移动后,还在其最终位置 这种情况不存在 b) 采用P(n)这种方式,节点与目标距离,可通过下面3步完成 i. 首先计算在原位置时,与目标位置的距离,命名为Distance1 ii. 移动后,计算当前位置与目标位置的距离,命名为Distance2 iii. 计算子节点的h值: 子节点的h值 = 父节点的h值- Distance1+ Distance2 5、 在任意状态中的每个数字和目标状态中同一数字的相对距离就有9*9种,可以先将这些相对距离算出来,用一个矩阵存储,这样只要知道两个状态中同一个数字的位置,就可查出它们的相对距离,也就是该数字的偏移距离;例如在一个状态中,数字8的位置是3,在另一状态中位置是7,那么从矩阵的3行7列可找到2,它就是8在两个状态中的偏移距离。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZTao-z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值