註釋即筆記:瀏覽器
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, []); // 版本三,平均十來步的樣子
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
- - -- - - - - -- -- -- - - - - -- - -- - - 函數
沒作任何優化的窮舉,並且還用遞歸。。。6個以上包裹瀏覽器應該會直接崩潰掉。。fetch
/** 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, []);
- - -- - - - - -- -- -- - - - - -- - -- - - 優化
結果沒錯,實現上有點跑偏。。(英語閱讀能力不足形成的)ui
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