利用動態規劃實現最短路徑和(適合小白看,看不懂你打我,附JS程式碼和C程式碼實現)

Goder_HaHa發表於2020-11-21

要求:利用動態規劃實現最短路徑和。
Example:從起點到終點,只能向右或向上,要求找到最短的路徑。(下圖為了方面大家思考所以標明瞭行列和每段距離,實際情況可能會輸入)
在這裡插入圖片描述動態規劃思想:

把原始問題分解為一系列子問題
求解每個子問題僅一次,並將結果儲存下來,以後用到時直接取,不重複計算,與此同時,演算法效率提高
自底向上計算
適用:對於一類優化問題,可以分為多個相關子問題,子問題的解被重複使用
(可參考:斐波那契數列,爬樓梯問題)

首先我們標好每個座標(中間的就不標了,太亂了看著)
在這裡插入圖片描述我們的目的是從(1,1)到(4,5)
我們可以倒著想,如果我要從(4,5)往回走,發現只能走(4,4)或者(3,5)

(4,4)到(4,5)最短路徑就是1,因為沒別的可選
(3,5)到(4,5)最短路徑也是1
我們給(4,4)這裡儲存的值是1,給(3,5)這裡儲存的值是1

然後考慮(3,4)到終點,從(3,4)到(4,5)只有兩條路:

(3,4)->(4,4)->(4,5)
(3,4)->(3,5)->(4,5)

(3,4)->(4,4)->(4,5)的路徑是3+1=4
(3,4)->(3,5)->(4,5)的路徑是8+1=9
所以從(3,4)到(4,5)最短的路徑就是走(3,4)->(4,4)->(4,5),也就是距離為4的是最短路徑和,所以這裡給(3,4)儲存的值是4

相信大家明白啥意思了,我可以定義一個二維陣列,其值儲存從該點到終點最短的路徑和。
就像我從(1,1)算最短路徑和,我只需要知道,(2,1)和(1,2)這兩個分別到終點的路徑和。(因為我們倒著計算,完全能夠確定(2,1)和(1,2)儲存的值是多少,因為他們本身存的值就是最短路徑和)
我怎麼確定從(1,1)走哪條呢?
只需要確定 (1,1)到(2,1)的距離+(2,1)本身儲存的值。(這裡的話也就是3+a[2][1])
以及(1,1)到(1,2)的距離+(1,2)本身儲存的值。(這裡的話也就是5+a[2][1])
我比較這兩個哪個小,我就在(1,1)儲存上計算結果。

即把原始問題分解為一系列子問題,求解每個子問題僅一次,並將結果儲存下來,以後用到時直接取,不重複計算。

如果沒明白,我再舉個例子
我們之前算了a[4][4] =1,a[3][5]=1,a[3][4]=4;
那a[2][5] = ?
因為最右邊比較特殊,只能向上走(同理最上邊只能向右)。即(2,5)->(3,5)->(4,5),然後求一下距離之和。a[2][5]=2+1=3。

懂了嗎?那我們再算一個a[2][4]應該儲存的最短路徑和。
a[2][4]第一步只可以去a[3][4]或a[2][5]。
這時我們不用去考慮從(3,4)或(2,5)之後該怎麼走,因為我們a[3][4]和a[2][5]已經都計算好了怎麼走路徑最短,以及已經儲存好了最短路徑和。
so: 可以有兩個計算方法:
a[2][4] = 2 + a[3][4] = 2+4 = 6;
a[2][4] =4 + a[2][5] = 4+3 = 7;
選最小的,所以,a[2][4] = min(6,7) = 6;
有同學問為什麼不考慮(2,4)->(3,4)->(3,5)->(4,5)
是這樣的:我們從(3,5)到終點的時候我們在計算a[3][5]的時候已經確定了走哪條是最短的,即(3,4)->(4,4)->(4,5)=4的。而非(3,4)->(3,5)->(4,5)的路徑是8+1=9的。

如果大家懂了就快去用程式碼實現一下。
好啦,上程式碼啦(首先是C的):

# include <stdio.h>

struct S{
    int up;  //該座標向上的距離
    int right;  //該座標向右的距離
    int v;  //本身的值
};

int main(void)
{
    //m向上 n向右  從(1,1)點到(m,n)
    //m、n 可以後期再定義為輸入,這裡指定一下,但以下程式都是用m、m來表示,不會涉及到具體的數。
    int m=4,n=5;
    struct S a[m+1][n+1];
    int i,j;
    for(i=1;i<=m;i++)
    {
        for(j=1;j<=n;j++)
        {
            //輸入的時候,如果沒有向上或向右的,我們手動輸入 0
            scanf("%d",&(a[i][j].up));
            scanf("%d",&(a[i][j].right));
        }
    }

    //compute 演算法部分,重點看一下哦!!!
    a[m][n].v = 0;
    for(i=m;i>0;i--)
    {
        for(j=n;j>0;j--)
        {
            if( m==i && j!=n)  //當m==i時,可以發現會篩選出最上邊的一行  至於j==n時,算的是終點,終點之前已經賦值了,再計算會混亂
            {
                a[i][j].v = a[i][j].right + a[i][j+1].v;
            }
            else if( n==j && i!=m)  //當n==j時,可以發現會篩選出最右邊的一行  至於i==m時,算的是終點,終點之前已經賦值了,再計算會混亂
            {
                a[i][j].v = a[i][j].up + a[i+1][j].v;
            }
            else if(i!=m && j!=n)  //當既不是最上邊也不是最右邊,也不是終點時
            {
                int n1 = a[i][j].up+a[i+1][j].v;
                int n2 = a[i][j].right+a[i][j+1].v;
                //要求的是最短路徑 看一下n1和n2哪個小,就儲存哪個
                a[i][j].v = n1>n2?n2:n1;  
            }
        }
    }

    printf("%d",a[1][1].v);
    return 0;
}
/*
供測試資料:
3 5
1 6
3 7
1 8
3 0
2 1
2 2
2 3
2 4
2 0
1 5
3 6
1 7
3 8
1 0
0 4
0 3
0 2
0 1
0 0
得出最後結果a[1][1].v=12;
*/

再附上一個JS程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebHomework05</title>
    <script type="text/javascript">
        //動態規劃實現最小路徑和

        //這裡的m和n可以手動輸入,利用prompt語句。這裡指定一下值,但下面的程式用的都是m和n,如果需要修改m和n的值只需要修改定義這裡的就行。
        var m = 4;
        var n = 5;
        /*不太會定義結構體,所以定義了三個二維陣列,基本演算法思想是一樣的*/
        //up陣列儲存每個座標的向上的路徑,right儲存每個座標向下的路徑,v陣列利用動態規劃儲存該座標的值到右上終點的最小路徑和
        var up = new Array();
        for(var i=1;i<=m;i++){
            up[i] = new Array();
            for(var j=1;j<=n;j++){
                up[i][j] = 0;
            }
        }

        var right = new Array();
        for(var i=1;i<=m;i++){
            right[i] = new Array();
            for(var j=1;j<=n;j++){
                right[i][j] = 0;
            }
        }

        var v = new Array();
        for(var i=1;i<=m;i++){
            v[i] = new Array();
            for(var j=1;j<=n;j++){
                v[i][j] = Number(0);
            }
        }

        //從[1,1]到[m,n]依次輸入所在座標的up和right值
        for(var i=1;i<=m;i++){
            for(var j=1;j<=n;j++){
                up[i][j] = prompt("Please input 'up'  a["+i+"]["+j+"]");
                right[i][j] = prompt("Please input 'right' a["+i+"]["+j+"]");
            }
        }

        //compute
        v[m][n] = Number(0);
        for(i=m;i>0;i--)
        {
            for(j=n;j>0;j--)
            {
                if( m==i && j!=n)
                {
                    v[i][j] = Number(right[i][j]) + Number(v[i][j+1]);
                }
                else if( n==j && i!=m)
                {
                    v[i][j] = Number(up[i][j]) + Number(v[i+1][j]);
                }
                else if(i!=m && j!=n)
                {
                    var n1 = Number(up[i][j]) + Number(v[i+1][j]);
                    var n2 = Number(right[i][j])+Number(v[i][j+1]);
                    //要求的是最短路徑,所以三元表示式如下:
                    v[i][j] = Number(n1>n2?n2:n1);
                }
            }
        }



/*
        for(var i=1;i<=m;i++){
            for(var j=1;j<=n;j++){
                console.log("i="+i+"  j="+j+"  up="+up[i][j]+"  right="+right[i][j]+"   v="+v[i][j]);
            }
        }
        //由此迴圈可以觀察出 js將prompt語句所賦的值當成字串來處理了,
        //所以我們可以在之前的迴圈賦值語句中呼叫String物件的Number()方法,將字串轉化為數值型
*/
        console.log("the min add = "+Number(v[1][1]));
        //或者是alert("the min add = "+Number(v[1][1]));
        /*供測試的資料:
            3 5
            1 6
            3 7
            1 8
            3 0

            2 1
            2 2
            2 3
            2 4
            2 0

            1 5
            3 6
            1 7
            3 8
            1 0

            0 4
            0 3
            0 2
            0 1
            0 0
            最短路徑和是12,正確,從左下到右上走的分別是3->1->2->2->1->2->1
         */

    </script>
</head>
<body>

</body>
</html>

感謝朋友們能看到這裡。
如果程式不懂記得先看懂流程控制,在看懂每個語句的功能,最後試數。
加油呀!

相關文章