Eloquent JavaScript #07# Project: A Robot

注释即笔记:

const roads = [
    "Alice's House-Bob's House", "Alice's House-Cabin",
    "Alice's House-Post Office", "Bob's House-Town Hall",
    "Daria's House-Ernie's House", "Daria's House-Town Hall",
    "Ernie's House-Grete's House", "Grete's House-Farm",
    "Grete's House-Shop", "Marketplace-Farm",
    "Marketplace-Post Office", "Marketplace-Shop",
    "Marketplace-Town Hall", "Shop-Town Hall"
];

function buildGraph(edges) {
    // graph的存储格式类似于
    // graph.place_a: [place_b, place_c]
    // 表示由a能够直接到达b或者c
    let graph = Object.create(null);

    function addEdge(from, to) {
        // 首先判断该起点有没有被添加
        if(graph[from] == null) {
            graph[from] = [to];
        } else {
            graph[from].push(to);
        }
    }

    // 'Alice's House-Bob's House'.split("-")
    // → ['Alice's House', 'Bob's House']
    for(let [from, to] of edges.map(r => r.split("-"))) {
        addEdge(from, to);
        addEdge(to, from);
    }

    return graph;
}

const roadGraph = buildGraph(roads);

/**
 * 不要条件反射地把每个概念都搞成对象,
 * 这样的程序通常是难以理解和维护的。
 * 相反,用最小的数值集合来描述村庄&机器人
 * 状态就可以了。
 */

class VillageState {
    constructor(place, parcels) {
        this.place = place;
        this.parcels = parcels;
    }

    move(destination) {
        // move表示机器人的一次移动。
        // 首先检查是否存在一条从当前位置this.place到目的地destination的路
        // 如果不存在就说明不是合法的移动,返回旧的VillageState。
        // 如果存在,就更新机器人的当前位置,也就是更新VillageState.place
        // 为destination,当然同时需要更新包裹的状态:1-还没被机器人拿到
        // 的包裹不去管它,2-拿到的包裹则更新当前位置place(目的地address不改变)
        // 3-最后过滤掉已经送到的包裹(目的地就在本地)
        // PS. 整个move方法实际上是重造了一个VillageState,并没改变旧的
        // VillageState对象
        if(!roadGraph[this.place].includes(destination)) {
            return this;
        } else {
            let parcels = this.parcels.map(p => {
                if(p.place != this.place) return p;
                return {
                    place: destination,
                    address: p.address
                };
            }).filter(p => p.place != p.address);
            return new VillageState(destination, parcels);
        }
    }
}

/**
 * 可以用Object.freeze冻结对象
 * 所有对该对象属性的写入操作会被忽略
 * 这需要计算机做一些额外工作
 * let object = Object.freeze({value: 5});
    object.value = 10;
    console.log(object.value);
    // → 5
 */

/**
 * But the most important limit 
 * on what kind of systems we can build 
 * is how much we can understand. 
 * Anything that makes your code easier 
 * to understand makes it possible 
 * to build a more ambitious system.
 * 写程序最重要的限制是我们能够理解多少
 */

// robot实际上是一个函数接口,
// 该函数输入state和memory
// 输出决策action用于实际移动
// 这样写就能够动态更改策略了。
function runRobot(state, robot, memory) {
    for(let turn = 0;; turn++) {
        if(state.parcels.length == 0) {
            console.log(`Done in ${turn} turns`);
            break;
        }
        let action = robot(state, memory);
        state = state.move(action.direction);
        memory = action.memory;
        console.log(`Moved to ${action.direction}`);
    }
}

function randomPick(array) {
    let choice = Math.floor(Math.random() * array.length);
    return array[choice];
}

// 最愚蠢但有效的策略--随机决定下一个方向。
// 虽然这里只有一个参数,但是js允许传
// 入更多(或者更少)的参数。在这里,传入的
// memeory被忽略了
function randomRobot(state) {
    return {
        direction: randomPick(roadGraph[state.place])
    };
}

// 初始化
VillageState.random = function(parcelCount = 5) {
    let parcels = [];
    for(let i = 0; i < parcelCount; i++) {
        let address = randomPick(Object.keys(roadGraph));
        let place;
        do {
            place = randomPick(Object.keys(roadGraph));
            // 包裹的起点和终点不可以是同一个地方
        } while (place == address);
        parcels.push({
            place,
            address
        });
    }
    return new VillageState("Post Office", parcels);
};

// runRobot(VillageState.random(), randomRobot);
// 版本一,步数不稳定

// runRobotAnimation(VillageState.random(), randomRobot); 
// 作者写的动画版本,相当直观酷炫。。

// 第二个策略:事先指定一条可以通过所有地点的路线
// 走两遍就可以确保投递所有邮件
const mailRoute = [
    "Alice's House", "Cabin", "Alice's House", "Bob's House",
    "Town Hall", "Daria's House", "Ernie's House",
    "Grete's House", "Shop", "Grete's House", "Farm",
    "Marketplace", "Post Office"
];

// [a,b,c].slice(1) 
// → [b,c]
// [a,b,c].slice(1, 2) 
// → [b] // 包括start不包括end
function routeRobot(state, memory) {
    if(memory.length == 0) {
        memory = mailRoute;
    }
    // memory类似于队列
    // 等价: return {direction: memory.shift(), memory: memory}
    return {
        direction: memory[0],
        memory: memory.slice(1)
    };
}

// runRobot(VillageState.random(), routeRobot, []);
// 版本二,最多26步

/**
 * The problem of finding a route 
 * through a graph is a typical search problem. 
 * We can tell whether a given solution (a route) 
 * is a valid solution, but we can’t directly compute
 * the solution the way we could for 2 + 2. 
 * Instead, we have to keep creating potential solutions 
 * until we find one that works.
 */

// 返回一点到另一点的最短路线,参考:C++ 电路布线/最短路径问题
function findRoute(graph, from, to) {
    let work = [{at: from, route: []}]; // 其实也是个队列
    for(let i = 0; i < work.length; i++) {
        let {at, route} = work[i]; // 原来还可以这样赋值。。
        for(let place of graph[at]) {
            // 搜索四周围,如果找到了目的地就直接+1返回。
            if(place == to) return route.concat(place);
            if(!work.some(w => w.at == place)) { // 判断点是否已经入队
                work.push({at: place, route: route.concat(place)});
            }
        }
    }
}

function goalOrientedRobot({place, parcels}, route) {
    // 首先判断当前制定的路线走完没有,
    // 走完就重新制定下一条路线
    // 逐个包裹处理(当然也有可能顺
    // 路完成其它包裹的fetch和投递)
    if(route.length == 0) {
        let parcel = parcels[0];
        if(parcel.place != place) {
            // 制定取包裹路线
            route = findRoute(roadGraph, place, parcel.place);
        } else {
            // 制定投递路线
            route = findRoute(roadGraph, place, parcel.address);
        }
    }
    return {direction: route[0], memory: route.slice(1)};
}


runRobot(VillageState.random(), goalOrientedRobot, []);
// 版本三,平均十来步的样子

 

Exercises

① Measuring a robot

function testRobot(state, robot, memory) {
    for(let turn = 0;; turn++) {
        if(state.parcels.length == 0) {
            return turn;
            break;
        }
        let action = robot(state, memory);
        state = state.move(action.direction);
        memory = action.memory;
    }
}

function compareRobots(robot1, memory1, robot2, memory2) {
    let tasks = [];
    for (let i = 0; i != 100; ++i) {
        tasks.push(VillageState.random());
    }
    let total1 = 0, total2 = 0;
    for (let task of tasks) {
        total1 += testRobot(task, robot1, memory1);
        total2 += testRobot(task, robot2, memory2);
    }
    console.log(`average turns: robot1 ${total1 / 100}, robot2 ${total2 / 100}`);
}

compareRobots(routeRobot, [], goalOrientedRobot, []);
// → average turns: robot1 18.07, robot2 15.03

- - -- - - - - -- -- -- - - - - -- -          -- - - 

② Robot efficiency

没做任何优化的穷举,而且还用递归。。。6个以上包裹浏览器应该会直接崩溃掉。。

/**
  max表示一个包裹要被处理的次数
  arr为长度为包裹数量的全0数组
  arr[i]表示第i个包裹被处理的次数
   当arr为[max, max, max, ...]时
   表示所有包裹已经被处理完
   返回的sequences包含处理包裹的
   所有顺序集合
 */
function makeSequences(max, arr) {
    
    const sequences = [];
    
    const fillArrWith = (max, arr, start, sequence) => {
        // 填充起始点
        arr[start] += 1;
        sequence.push(start);
        // 判断是否已经填充满
        let sum = 0;
        for (let x of arr) {
            sum += x;
        };
        if (sum == max * arr.length) {
            sequences.push(sequence);
            return;
        }
        // 寻找下一个填充点
        for (let i = 0; i != arr.length; ++i) {
            if (arr[i] < max) fillArrWith(max, arr.slice(), i, sequence.slice()); 
        }        
    };
    
    for (let i = 0; i != arr.length; ++i) {
        fillArrWith(max, arr.slice(), i, []);
    }    
    
    return sequences;
}



/**
   把生成的序列转化为具体业务相关的表达。
   得到route并不是实际的route 而是可能不相邻的地点
  routes包含所有能够完成任务的路线
 */
function sequencesToRoutes(sequences, {place, parcels}) {
    const routes = [];
    
    const flag = parcels.map(() => 0); // 用于之后拷贝用
    // 逐个序列处理
    for (let sequence of sequences) {
        let route = [place]; // 添加起点
        let localFlag = flag.slice(); // 标记包裹的状态
        for (let num of sequence) {
            if (localFlag[num] == 0) { // 第一次处理包裹num:到place取包裹
                localFlag[num]++; // 包裹num已取,这样可以保证某包裹的place一定优先于该包裹的address入队
                if (route[route.length - 1] != parcels[num].place) { // 避免出现两个连续重复place
                    route.push(parcels[num].place);
                }                
            } else { // 第二次处理包裹num: 送包裹,去该包裹的目的地
                if (route[route.length-1] != parcels[num].address) {
                    route.push(parcels[num].address);
                }                        
            }        
        }
        routes.push(route);
    }
    
    return routes;
}

/**
 * 计算单个路线需要的最短步数
 * turnsMap用于保存已经计算的两点值,避免重复计算
 */
function turnsOfRoute(route, turnsMap=new Map()) {
    let totalTurns = 0;
    for (let i = 0; i != route.length - 1; ++i) {
        // 两点、两点处理。
        let routeKey = route[i].concat(route[i + 1]);
        let turns = turnsMap.get(routeKey);
        if (turns != undefined) {
            totalTurns += turns;
        } else {
            turns = findRoute(roadGraph, route[i], route[i + 1]).length;
            // 计算 a到b 的最小步数 ↑
            // 保存计算结果 ↓
            turnsMap.set(routeKey, turns);
            routeKey = route[i + 1].concat(route[i]); // a到b和b到a的最短路是一样的。
            turnsMap.set(routeKey, turns);
            
            totalTurns += turns;
        }
    }
    return totalTurns;
}

/**
 * 寻找最短路线
 */
function shortestRoute(routes) {
    let min = Infinity;
    let tempRoute;
    let turnsMap = new Map(); // 用于保存已经计算的两点值,避免重复计算
    for (let route of routes) {
        let turns = turnsOfRoute(route, turnsMap);
        if (turns < min) {
            min = turns;
            tempRoute = route; // 保存最短路线
        }
    }
    
    
    // 将最短路线转化为相邻的可以实际移动的地点序列
    let result = [];    
    for (let i = 0; i != tempRoute.length - 1; ++i) {
        // 仍然是两点、两点处理
        let midRoute = findRoute(roadGraph, tempRoute[i], tempRoute[i + 1]);
        if (result[result.length - 1] != midRoute[0]) { // 避免出现两个连续重复place
            result = result.concat(midRoute);
        } else {
            result = result.concat(midRoute.shift());
        }
    }
    return result;
}

function simpleRobot({place, parcels}, route) {
    if(route.length == 0) {
        let sequences = makeSequences(2, parcels.map(() => 0)); // 转化成一种比较抽象的东西。。。
        let routes = sequencesToRoutes(sequences, {place, parcels}); 
        route = shortestRoute(routes);
    }
    return {direction: route[0], memory: route.slice(1)};
}

//runRobot(VillageState.random(), simpleRobot, []);
// 版本四,穷举。。 平均 10.64
//runRobotAnimation(VillageState.random(), simpleRobot, []);

- - -- - - - - -- -- -- - - - - -- -          -- - - 

③ Persistent group

结果没错,实现上有点跑偏。。(英语阅读能力不足造成的)

class PGroup {
    add(x) {
        let result = Object.create(PGroup.prototype);
        if (this.container == undefined) {
            this.container = [];
        }
        
        if (!this.container.includes(x)) {
            result.container = this.container.concat(x);    
          } else {
              result.container = this.container;
          }
        
        return result;
    }

    delete(x) {
        let result = Object.create(PGroup.prototype);
        if (this.container != undefined) {
            result.container = this.container.filter(a => !(a === x));    
        }
        return result;
    }

    has(x) {
        if (this.container != undefined) {
            return this.container.includes(x);
        }
        return false;
    }
}

PGroup.empty = Object.create(PGroup.prototype);

let a = PGroup.empty.add("a");
let ab = a.add("b");
let b = ab.delete("a");

console.log(b.has("b"));
// → true
console.log(a.has("b"));
// → false
console.log(b.has("a"));
// → false

 

转载于:https://www.cnblogs.com/xkxf/p/9562872.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值