美文网首页
使用动态规划解决复杂地形下的正方形区域选址问题

使用动态规划解决复杂地形下的正方形区域选址问题

作者: HoPGoldy | 来源:发表于2020-07-12 13:38 被阅读0次

    需求

    假设有一块区域(例如 50 * 50),区域被划分成无数的正方形地块,地块有三种状态,分别是:平原、沼泽、山地。现在要在整个区域内选出一个给定边长的正方形区域,要求如下:

    • 目标区域内不可以有山地地块。
    • 目标区域要尽可能少的包含沼泽地块。

    需要完成一个函数,参数为地形数据 map(一个二维数字数组,元素的值分别为:0 平原、1 沼泽、2 山地)和目标正方形区域的边长 targetLength,返回最适合建造的区域的中心点坐标(若目标区域边长为偶数则可返回小数坐标)。

    思路

    建模

    接下来我们分析一下题目,完整的算法在本文最下面。由于需求分为 寻找不包含地块的区域(有效正方形)尽可能少的包含沼泽,所以我们可以建立两个模型:

    • dp[i][j].len 代表以坐标 [i][j](i 为纵坐标,j 为横坐标,下同)为右下角时所能生成的最大正方形的边长。
    • dp[i][j].swamp 代表以坐标 [i][j] 为右下角,[0][0] 为左上角的矩形区域内的沼泽数量之和(伪前缀和)。

    首先我们通过 dp[i][j].len 得出该点代表的正方形是否符合给定的正方形区域边长,如果符合的话再通过 dp[i][j].swamp 求出区域内的沼泽数量,并与当前的最小沼泽数量比较,如果更小的话那自己即为新的结果。

    状态转移

    首先我们先来找 dp[i][j].len 的状态转移方程,由于求有效正方形只跟山地和平原有关,所以我们先忽略掉沼泽,如下图,每个地块中的数字即为对应的 dp[i][j].len

    因为我们规定 dp[i][j].len 就是以该点坐标为正方形的 右下角 时所能生成最大正方形的边长。所以可以发现该点的值是和其上方、左上、左侧三个节点的值相关的:

    并且由于大家都是正方形,所以本节点的正方形大小取决于这三个中 边长最小 的节点。例如上图,左侧的节点是山地,所以其代表的正方形边长为 0(其作为右下角不能生成正方形 )。那么黄色节点所能生成的最大正方形边长就是 0 + 1 = 1(他自己为一个独立的正方形 )。由此,我们就可以得到其状态转移方程:

    dp[i][j].len = Math.min(dp[i - 1][j - 1].len, dp[i][j - 1].len dp[i - 1][j].len) + 1 
    

    接下来我们来看一下 dp[i][j].swamp 怎么推导,想要算当前到左上角的所有沼泽数量,依旧是需要左侧、上方、左上三块区域内的沼泽数量。如下图,若要求红色区域内的沼泽数量,那么就是 蓝色区域沼泽数量 + 紫色区域沼泽数量 - 黄色区域沼泽数量 + 自己是否为沼泽因为黄色区域被重复计算了一次,所以需要减去 )。

    所以,dp[i][j].swamp 的状态转移方程如下:

    dp[i][j].swamp =  dp[i - 1][j].swamp + dp[i][j - 1].swamp - dp[i - 1][j - 1].swamp + (map[i][j] === 1 ? 1 : 0)
    

    通过这种方式,我们也就可以逆推出指定边长正方形中的沼泽数量,如下:。

    红色区域内的沼泽数量就等同于 dp[i][j].swamp - 蓝色区域沼泽数量 - 紫色区域沼泽数量 + 黄色区域沼泽数量,即:

    // currentSwamp 代表以 [i][j] 为右下角 以 dp[i][j].len 为边长的正方形中的沼泽数量
    currentSwamp = dp[i][j].swamp -  dp[i - 1][j].swamp -  dp[i][j - 1].swamp +  dp[i - 1][j - 1].swamp
    

    通过上面三个方程我们可以看出,左上、上、左三个节点的值扮演了非常重要的角色,所以我们可以对其进行抽象,我们把 dp[i][j].lendp[i][j].swamp 方程中的三个节点看作是获取右下角边长为 1 的正方形相邻的三个节点,就可以通过把 右下角正方形边长 提取成参数的形式,来统一三个方程中相邻节点的获取过程:

    翻译成代码并添加越界检查之后就是如下函数:

    /**
     * 获取状态转移所需的三个相邻节点
     * 
     * @param {object[][]} dp 状态集
     * @param {number} i 目标正方形右下角的 y 坐标
     * @param {number} j 目标正方形右下角的 x 坐标
     * @param {number} len 右下角正方形的边长
     */
    function getOtherArea(dp, i, j, len) {
        // 越界时的默认值
        const nullNode = { len: 0, swamp: 0 }
        // 检查索引是否小于零,小于零就返回默认值
        return {
            topLeft: (i - len > -1 && j - len > -1) ? dp[i - len][j - len] : nullNode,
            top: (i - len > -1) ? dp[i - len][j] : nullNode,
            left: (j - len > -1) ? dp[i][j - len] : nullNode,
        }
    }
    

    之后,我们按照刚开始提到的流程,把这三部分代码按顺序装在一起,然后添加个通过右下角和边长获取正方形中心坐标的辅助函数,就可以完成如下的最终解法了.

    完整算法

    /**
     * 寻找可行的最佳选址
     * 
     * @param {number[][]} map 二维数组,代表了地图信息,元素为数字,对应关系为:0 平原、1 沼泽、2 山地
     * @param {number} targetLength 建筑区域的边长
     * @returns {[number, number][]} 二位数组,包含了所有最佳选址的中央点的 xy 坐标
     */
    function getBasePos(map, targetLength) {
        let dp = Array(map.length).fill().map(_ => [])
        // 合适的结果集
        let result = []
        // 结果集里对应的沼泽数量
        let minSwamp = Infinity
    
        // 遍历所有地块
        for (let i = 0; i < map.length; i ++) {
            for (let j = 0; j < map[i].length; j ++) {
                const { topLeft, top, left } = getOtherArea(dp, i, j, 1)
    
                // 生成当前位置的状态
                dp[i][j] = {
                    // 以当前位置为右下角,可以生成的最大正方形的边长
                    len: map[i][j] === 2 ? 0 : (Math.min(topLeft.len, top.len, left.len) + 1),
                    // 以当前位置为右下角,[0][0] 为左上角的区域内所有的沼泽数量
                    swamp: top.swamp + left.swamp - topLeft.swamp + (map[i][j] === 1 ? 1 : 0)
                }
    
                // 发现该正方形已经可以满足要求了
                if (dp[i][j].len >= targetLength) {
                    // 获取正方形右上侧的三个区域
                    const { topLeft, top, left } = getOtherArea(dp, i, j, targetLength)
                    // 计算出当前区域内的沼泽数量
                    const currentSwamp = dp[i][j].swamp - top.swamp - left.swamp + topLeft.swamp
    
                    // 对比沼泽数量并更新结果
                    if (currentSwamp < minSwamp) {
                        minSwamp = currentSwamp
                        result = [ getCenterBybottomRight(i, j, targetLength) ]
                    }
                    else if (currentSwamp === minSwamp) result.push(getCenterBybottomRight(i, j, targetLength))
                }
            }
        }
    
        return result
    }
    
    /**
     * 获取状态转移所需的三个相邻节点
     * 
     * @param {object[][]} dp 状态集
     * @param {number} i 目标正方形右下角的 y 坐标
     * @param {number} j 目标正方形右下角的 x 坐标
     * @param {number} len 正方形的边长
     */
    function getOtherArea(dp, i, j, len) {
        // 越界时的默认值
        const nullNode = { len: 0, swamp: 0 }
        // 检查索引是否小于零,小于零就返回默认值
        return {
            topLeft: (i - len > -1 && j - len > -1) ? dp[i - len][j - len] : nullNode,
            top: (i - len > -1) ? dp[i - len][j] : nullNode,
            left: (j - len > -1) ? dp[i][j - len] : nullNode,
        }
    }
    
    /**
     * 获取该正方形中心点的坐标
     * 
     * @param {number} i 正方形右下角的 y 坐标
     * @param {number} j  正方形右下角的 x 坐标
     * @param {number} len 正方形的边长
     * @returns {[ number, number ]} [0] 为中央点 x 坐标,[1] 为 y 坐标
     */
    function getCenterBybottomRight(i, j, len) {
        return [
            i - (len / 2) + 0.5,
            j - (len / 2) + 0.5,
        ]
    }
    

    随机测试用例

    // 随机地图的宽
    const MAP_WIDTH = 20
    // 随机地图的长
    const MAP_HEIGHT = 25
    // 目标建筑的边长
    const BASE_LENGTH = 3
    
    // 生成随机地图
    const getRandMap = function(width, height) {
        // 提高 roll 中平原的概率,不然太难形成大片空地了
        const randSet = [ 0, 0, 0, 1, 2 ]
        return Array(height).fill().map(_ => Array(width).fill().map(_ => {
            return randSet[Math.floor(Math.random() * randSet.length)]
        }))
    }
    
    // 生成地图并进行查找
    const randMap = getRandMap(MAP_WIDTH, MAP_HEIGHT)
    const result = getBasePos(randMap, BASE_LENGTH)
    
    // 绘制地图,▢ 平原、▤ 沼泽、▣ 山地
    console.log(randMap[0].map((_, i) => i < 10 ? `${i}  ` : `${i} `).join(''))
    console.log(randMap.map((line, index) => line.map(val => [ '▢', '▤', '▣' ][val]).join('  ') + `  ${index}`).join('\n'))
    // 打印结果
    console.log("\nresult:", result.map(pos => `${pos[0]},${pos[1]}`).join('   '))
    

    测试结果:

    相关文章

      网友评论

          本文标题:使用动态规划解决复杂地形下的正方形区域选址问题

          本文链接:https://www.haomeiwen.com/subject/ecowcktx.html