贪心、回溯、动态规划算法求“变形”杨辉三角的最短路径

1、背景需求:

“杨辉三角”不知道你听说过吗?我们现在对它进行一些改造。每个位置的数字可以随意填写,经过某个数字只能到达下面一层相邻的两个数字。

假设你站在第一层,往下移动,我们把移动到最底层所经过的所有数字之和,定义为路径的长度。请你编程求出从顶层移动到底层的最短路径长度

 咱们今天通过三种算法思想来解这个题:分别是回溯、动态回归、贪心等算法。

这三种算法思想各有自己适应的场景,简要的说:

贪心:一条路走到黑,就一次机会,只能哪边看着顺眼走哪边。不一定能求得满足要求的值。

回溯:一条路走到黑,无数次重来的机会,还怕我走不出来 (Snapshot View)

动态规划:拥有上帝视角,手握无数平行宇宙的历史存档, 同时发展出无数个未来 (Versioned Archive View)

2、数据结构描述这个三角形

思考:用什么样的数据结构来描述这个三角形呢?

合适的数据结构会放求解算法更便捷、合适。看到这个结构的时候,我一开始想到的是,结构很像二叉树,正准备用二叉树来描述,画了三层之后,发现不合适。思考了一会,用二维数组描述

// 变形的杨辉三角的数据描述
const triangleElements = [
    [5],
    [7,8],
    [2,3,4],
    [4,9,6,1],
    [7,8,9,4,5]
];

上述二维数组也就是用行、列来描述,直观图:

3、回溯方式解:

回溯的方式来解,就是把每一种可能的路径都暴力走一遍,记录下最小值。一般情况下,回溯用递归的方式实现。除了叶子节点外,每个节点往下走,都有两个走法:往左走(下一个列column值跟当前节点的列column值一样),往右走:(下一个列column值等于当前节点列值+1)

(没有做“备忘录”缓存优化)

// 变形的杨辉三角的数据描述
const triangleElements = [[5], [7,8], [2,3,4], [4,9,6,1], [7,8,9,4,5] ];
let minLen = Infinity; // 记录最小值,用来比较每种走法的结果,得出最短路径
/**
 * 回溯方式解决【变形杨辉三角】最短路径:用递归方式,把每一条可能的路径都走完,找到最短路径
 * @param {*} rowIndex, 当前行元素的index,
 * @param {*} columnIndex , 当前列元素Index
 * @param {*} len , 当前长度总和
 * @param {*} route,记录路径 
 * @returns 
 */
function findMinLenBT(rowIndex=0, columnIndex=0, len=triangleElements[0][0], route=[triangleElements[0][0]]){
    console.log(`findMinLenBT :: enter, rowIndex = ${rowIndex}, columnIndex = ${columnIndex}, len = ${len}, minLen = ${minLen}, route = ${route}`)
    // 退出条件
    if(rowIndex === triangleElements.length-1){
        if(minLen > len){
            minLen = len; // 找到最小值
        }
        return;
    }    
    const row = triangleElements[rowIndex]; // 当前行
    for(let column=0; column<row.length; column++){
        const nextRowIndex = rowIndex + 1
        if(nextRowIndex < triangleElements.length){
            // 这个判断很关键,每次只处理该列元素的值就好,因为每个元素往下只有两个元素可走
            if(column === columnIndex){
                const nextRow = triangleElements[nextRowIndex];
                const leftRote = [...route]; // copy 一份路径记录
                leftRote.push(nextRow[columnIndex])
                // 走左边,行加1,列不变
                findMinLenBT(rowIndex+1, columnIndex, len + nextRow[columnIndex], leftRote)
                // 走右边,行加1,列也加1
                const rightRote = [...route];
                rightRote.push(nextRow[columnIndex+1])
                findMinLenBT(rowIndex+1, columnIndex+1, len + nextRow[columnIndex+1], rightRote)
            }
        }
    }
}

// 测试一下上述算法
function test1(){
    findMinLenBT()    
    console.log(`test1 :: minLen = ${minLen}`) // minLen = 22
}
test1()

4、动态规划方式解:

动态规划常用的一个思考方式是:基于回溯暴力搜索(递归)画出递归树(部分即可),分析是否适合用动态规划,然后以表格的方式来记录每一个阶段的状态(当前值)。

画状态转移表:我们以当前走到的行、列来记录状态状态,将走到当前状态节点的路径总和记录在表格中,有部分单元格会计算两次,记录较小值的那一次。直接看状态表:

 上述状态转移表画清楚之后,将其翻译成代码就容易一些了:

/**
 * 创建一个表格(二维数组),主要用来生成状态二维数组
 * @param {*} rows 
 * @param {*} columns 
 * @param {*} defaultValue 
 * @returns 
 */
function createTableArr(rows, columns, defaultValue =0){
    console.log(`createTableArr :: enter, rows = ${rows}, columns = ${columns}, defaultValue = ${defaultValue}`);
    const table = [];
    for(let i=0; i<rows; i++){
        const rowArr = new Array(columns)
        rowArr.fill(defaultValue)
        table.push(rowArr)
    }
    return table;
}

// 变形杨辉三角的数据描述:
const triangleElements = [[5],[7,8],[2,3,4],[4,9,6,1],[7,8,9,4,5]];

/**
 * 用动态规划方式求【变形杨辉三角】最短路径
 * @param {*} data 
 * @returns 
 */
function findMinLenDP(data) {
    const sideCont = data.length;
    const states =  createTableArr(sideCont, sideCont, 0);
    states[0][0] = data[0][0]; // 特殊处理一下开始状态
    // 开始动态规划
    for(let rowIndex = 0; rowIndex< sideCont -1; rowIndex ++ ){
        const row = data[rowIndex];
        const nextRowIndex = rowIndex+1;
        const nextRow = data[nextRowIndex];
        for(let column=0; column<row.length; column++){
            const current = states[rowIndex][column];
            // 看左下角的值
            const left = current + nextRow[column];
            if(states[nextRowIndex][column] === 0){
                states[nextRowIndex][column] = left
            }else{
                if(states[nextRowIndex][column] > left){
                    states[nextRowIndex][column] = left
                }
            }
            // 看右下角的值
            const right = current + nextRow[column + 1];
            if(states[nextRowIndex][column+1] === 0) {
                states[nextRowIndex][column+1] = right
            }else{
                if(states[nextRowIndex][column+1] > right) {
                    states[nextRowIndex][column +1] = right
                }
            }
        }
        
    }
    const totalMin = Math.min(...states[sideCont -1]); // 找到最短路径 , 值是22
    const totalMinIndex = states[sideCont -1].findIndex(v => v===totalMin); // 找到最短路径和的状态索引
    console.log(`findMinLenDP :: got result, totalMin = ${totalMin}, totalMinIndex = ${totalMinIndex}`);
    console.log(states);
    return {
        totalMin,
    }    
}


function  test2(){
    findMinLenDP(triangleElements)
}
test2()

上面已经计算出最小值,如果要查找出其所走的节点路径要怎么做呢?

这里其实只要用状态表结合原始数据表就很容易算出来。每一个状态都是从上一个状态推导出来的,可以通过最终状态反推路径(可能有多条,这里只找一条):先在状态表(左侧)中找到最优解的索引,用最优解的值减去数据表(右侧)对应位上的数据,就得到上一个状态,依次类推。需要注意当前已经处于边缘位置的场景。处于边缘位置,上一个状态的列索引就是为0,或者自己的列索引-1(当前状态节点位于三角形的边上)

撸代码(完整代码): 

// 变形杨辉三角数据描述
const triangleElements = [[5],[7,8],[2,3,4],[4,9,6,1],[7,8,9,4,5]];
/**
 * 用动态规划方式求【变形杨辉三角】最短路径值,同时计算出所经过的路径节点
 * @param {*} data 
 * @returns 
 */
 function findMinLenDP2(data) {
    const sideCont = data.length;
    const states =  createTableArr(sideCont, sideCont, 0);
    states[0][0] = data[0][0];
    // 开始动态规划
    for(let rowIndex = 0; rowIndex< sideCont -1; rowIndex ++ ){
        const row = data[rowIndex];
        const nextRowIndex = rowIndex+1;
        const nextRow = data[nextRowIndex];
        for(let column=0; column<row.length; column++){
            const current = states[rowIndex][column];
            // 看左下角的值
            const left = current + nextRow[column];
            if(states[nextRowIndex][column] === 0){
                states[nextRowIndex][column] = left
            }else{
                if(states[nextRowIndex][column] > left){
                    states[nextRowIndex][column] = left
                }
            }
            // 看右下角的值
            const right = current + nextRow[column + 1];
            if(states[nextRowIndex][column+1] === 0) {
                states[nextRowIndex][column+1] = right
            }else{
                if(states[nextRowIndex][column+1] > right) {
                    states[nextRowIndex][column +1] = right
                }
            }
        }
        
    }
    const totalMin = Math.min(...states[sideCont -1]); // 找到最短路径,值为22
    const totalMinIndex = states[sideCont -1].findIndex(v => v===totalMin); // 找到最短路径和的状态索引,值为3
    console.log(`findMinLenDP2 :: got result, totalMin = ${totalMin}, totalMinIndex = ${totalMinIndex}`);

    // 倒推该最优解所经过的节点
    let min = totalMin;
    let minIndex = totalMinIndex;
    const route = [ data[sideCont -1][minIndex] ];
    for(let rowIndex = sideCont -1; rowIndex > 0; rowIndex --){
        let preStateRow = states[rowIndex -1];
        let preDataRow = data[rowIndex -1];
        let preIndex = 0;
        if(minIndex ===0){
            preIndex = 0
        }else if(minIndex === preDataRow.length){
            preIndex = minIndex - 1;
        }else{
            // 父节点有两个,两个值可能是一样的,为了方便起见,就只找一条路径就好。
            let pLeft = minIndex-1, pRight = minIndex;
            if(min === preStateRow[pLeft] + data[rowIndex][minIndex]){
                preIndex = pLeft
            }else{
                preIndex = pRight
            }
        }
        min = preStateRow[preIndex];
        minIndex = preIndex;
        route.push(data[rowIndex -1][minIndex]);
    }
    // 反转一下路径,从头开始看
    route.reverse()
    console.log(`findMinLenDP2 :: end, route = ${route}`);
    console.log(states);
    return {
        totalMin,
        route,
    }
}

function  test3(){
    findMinLenDP2(triangleElements)
}

最短路径值为22,所经过的节点为:5,8,4,1,4 

5、贪心算法解

下面看看贪心算法如何求解。贪心算法比较简单,为啥放到最后来讲,主要是他跟动态规划的逻辑很像。贪心算法就是从头开始,每一步都只取当前阶段的最优解,但是,结果并不一定是整体的最优解。只看眼前利益,不看整体利益。

贪心算法实际上是动态规划算法的一种特殊情况。它解决问题起来更加高效,代码实现也更加简洁。不过,它可以解决的问题也更加有限。它能解决的问题需要满足三个条件,最优子结构、无后效性和贪心选择性(这里我们不怎么强调重复子问题)。

“贪心选择性”的意思是,通过局部最优的选择,能产生全局的最优选择。每一个阶段,我们都选择当前看起来最优的决策,所有阶段的决策完成之后,最终由这些局部最优解构成全局最优解。

贴分析图:

 撸代码,求的值为25:

// 变形杨辉骚叫的数据描述:
const triangleElements = [[5],[7,8],[2,3,4],[4,9,6,1],[7,8,9,4,5]];

/**
 * 用贪心算法思路求解【变形杨辉三角】最短路径
 * @param {*} data 
 */
function findMinLenTX(data){
    const route = [];
    let currentColumn = 0;
    let minLen =  data[0][0]; // 特殊处理初始值。
    route.push(data[0][0]);
    for(let rowIndex=0; rowIndex < data.length -1; rowIndex++) {
        const nextDataRowIdex = rowIndex + 1;
        const nextDataRow = data[nextDataRowIdex];
        const left = nextDataRow[currentColumn];
        const right = nextDataRow[currentColumn + 1];
        // 决策下一步往哪走: 每一个阶段都选小的
        if(left > right){
            route.push(right);
            minLen += right
            currentColumn = currentColumn + 1;
        }else{
            route.push(left);
            minLen += left
        }
    }
    // minLen = 25
    console.log(`findMinLenTX :: end, minLen = ${minLen}, route = ${route}`)
    return minLen
}

function  test4(){
    findMinLenTX(triangleElements)
}

test4();

 6,结果比较:

在该示例中:

回溯的方式求得最短路径为:22

动态规划方式求得最短路径为:22

贪心算法求得最短路径为:25

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值