演算法之美:動態規劃

來源:互聯網
上載者:User

前言

和分治法一樣,動態規劃(dynamic programing)是通過組合子問題的解而解決整個問題的。注意這裡的programing翻譯成立規劃而不是編程。維基百科上寫道

This is also usually done in a tabular form by iteratively generating solutions to bigger and bigger subproblems by using the solutions to small subproblems.

這說明動規的關鍵是table,是子問題。而且動規的子問題是相互關聯的。而分治演算法是將問題劃分成相對獨立的子問題,遞迴的解決所有子問題,

然後合并子問題成為最終的結果。在這個過程中,分治法會做很多不必要的工作,即重複地求解公用子問題。

動規對每個子問題之求解一遍,並且將其結果儲存在表中,從而避免了子問題被重複計算。

適用範圍

1.動態規劃通常情況下應用於最佳化問題,這類問題一般有很多個可行的解,每個解有一個值,而我們希望從中找到最優的答案。

2.該問題必須符合無後效性。即目前狀態是曆史的完全總結,過程的演變不再受此前各種狀態及決策的影響。

簡單動態規劃問題



最大子數組和問題

題目

一個有N個整數元素的一位元組(A[0], A[1],...,A[n-1], A[n]),這個數組當然有很多子數組,那麼數組之和的最大值是什麼呢?

這是一道很簡單的題目,但是要想寫出時間複雜度為O(n)的最優解法還是需要仔細推敲下的。

例如有數組 int A[5] = {-1, 2, 3, -4, 2};

合格子數組為  2,3  即答案為 5;

再明確一下題意

    1.子數組必須是連續的。

            2.不需要返回子數組的具體位置。

    3.數組中包含:正整數,零,負整數。

例如

數組: {1, -2, 3, 5, -3, 2}   傳回值為 8

數組: {0, -2, 3, 5, -1, 2}   傳回值為 9

數組: {-9, -2, -3, -5, -6}   傳回值為 -2  注意子數組不可為空

首先我們看看最直接的窮舉法

int MaxSubString(int* A, int n){  int max = min;  //初始值為負無窮大  int sum;  for(int i = 0; i < n; i++)  {    sum = 0;    for(int j = i; j < n; j++)    {      sum += A[j];      if(sum > max)        max = sum;    }  }  return max;}

這種方法最直接,當也是最耗時的,他的時間複雜度為O(n^2);

問題分析

可以最佳化嗎?答案是肯定的,可以考慮數組的第一個元素,以及最大的一段數組(A[i], ..., A[j]),和A[0]的關係,有一下幾種情況:

    1. 當0 = i = j 時,元素A[0]本身構成和最大的一段;

    2. 當0 = i < j 時,和最大的一段以A[0]開始;

    3. 當0 < i 時, 元素A[0]和最大的一段沒有關係。

從上面3中情況可以看出。可以將一個大問題(N個元素數組)轉化為一個較小的問題(N-1個元素的數組)。假設已經知道(A[1], ...,A[n-1])中和最大的一段數組之和為All[1],並且已經知道

(A[1],...,A[n-1])中包含A[1]的和最大的一段數組為Start[1]。那麼不難看出 (A[0], ..., A[n])中問題的解All[0] = max{ A[0], A[0] + start[1], All[1] }。通過這樣的分析,可以看出這個問題

無有效性,可以用動態規劃來解決。

解決方案

int MaxSubString(int* A, int n){        int Start = A[n - 1];        int All = A[n - 1];        for(int i = n - 2; i >= 0; i--)    //從後向前遍曆,反之亦可。        {                Start = max( A[i], A[i] + Start);                All = max(Start, All);        }        return All[0];                       //All[0] 中存放結果}

我們通過動規演算法解決該問題不僅效率很高(時間複雜度為O(n)),而且極其簡便。




01背包問題

題目

這題非常有名,只要是電腦專業的應該都有聽說過。有N件物品和一個容量為V的背包。第i件物品的體積是c[i],價值是v[i]。求解將哪些物品裝入背包可使價值總和最大。

我們把題目具體下, 有5個商品,背包的體積為10,他們的體積為 c[5] = {3,5,2,7,4};  價值為 v[5] = {2,4,1,6,5};


問題分析

這是最基礎的背包問題,特點是:每種物品僅有一件,可以選擇放或不放。可以將背包問題的求解看作是進行一系列的決策過程,即決定哪些物品應該放入背包,哪些不放入背包。

如果一個問題的最優解包含了物品n,即Xn = 1,那麼其餘X1, X2, .....,Xn-1 一定構成子問題1,2,.....,n-1在容量C - cn時的最優解。如果這個最優解不包含物品n,即Xn = 0;

那麼其餘 X1, X2.....Xn-1一定構成了子問題 1,2,....n-1在容量C時的最優解。  //請各位仔細品味這幾句話

根據以上分析最優解的結構遞迴定義問題的最優解    f[i][v] = max{ f[i-1][v] , f[i-1][v - c[i]] + v[i]}

解決方案

#include<iostream>#define max(a,b) ((a) > (b) ? a : b)int c[5] = {3,5,2,7,4};int v[5] = {2,4,1,6,5};int f[6][10] = {0};//f[i][v] = max{ f[i-1][v] , f[i-1][v - c[i]] + w[i]}int main(){for(int i = 1; i < 6; i++)for(int j = 1; j < 10 ;j++){if(c[i] > j)//如果背包的容量,放不下c[i],則不選c[i]f[i][j] = f[i-1][j];        else{f[i][j] = max(f[i-1][j], f[i-1][j - c[i]] + v[i]);//轉移方程式}}std::cout<<f[5][9];return 0;}

01背包問題是最基本的動態規劃問題,也是最經典,最易懂的。所以請讀者仔細推敲這段代碼。它包含了背包問題中設計狀態、方程的最基本思想。



矩陣連乘問題

題目

給定n個矩陣{A1,A2,...,An},其中Ai與Ai+1是可乘的,i = 1,2, ...n-1。考慮這n個矩陣的乘積。由於競爭乘法滿足結合律,故計算矩陣的連乘有許多不同的計算次序。

這種計算次序可以用加括弧的方式確定。若一個矩陣連乘的計算次序完全確定,這是就說該連乘已完全加括弧。

例如,矩陣連乘A1 *A2 *A3 *A4 可以有5種完全加括弧的方式:(A1 *(A2 *(A3 *A4 ))), (A1 *((A2 *A3) *A4)),((A1 *A2 )*(A3 *A4)),

(((A1 *A2 )*A3 )*A4)。每種加括弧的方式確定了一個計算的次序。不同的計算次序與矩陣連乘的計算量有密切的關係。關於矩陣如何相乘這裡我就不贅述了請看about matrix 。

考慮3個矩陣{A1,A2,A3}連乘的例子,假設這3個矩陣的維數分別為 10×100, 100×5, 5×50。若按照((A1*A2)*A3)計算,則計算次數為10×100×5 + 10×5×50 = 7500

若按(A1*(A2*A3))計算,則計算次數為   100×5×50 + 10×100×50 = 75000。第1種方法的計算次數是後者的10倍!由此可以看出,不同的加括弧方式確定不同的計算次序對

矩陣乘法的運算量影響是巨大的。

矩陣連乘為題定義如下:給定n個矩陣{A1,A2,...,An},矩陣A1的維數為pi-1×pi, i = 1,2, ..., n,如何給矩陣連乘A1*A2*....*An完全加上括弧使用矩陣乘法中計算次數最少。

問題分析

若用窮舉法,能夠證明需要指數時間來求解。但是時間代價高昂。現在考慮用動態規劃來求解連乘問題。

為方便起見用Ai...j表示矩陣乘法Ai*Ai+1*....Aj的結果。其中i<j。那麼Ai*Ai+1*.....Aj一定在Ak與Ak+1之間被分裂。i <= k < j。問題Ai*Ai+1 ... Aj完全加括弧的開銷等於計算矩陣

Ai...k 與計算 Ak+1...j的開銷,再加上他們的結果相乘的開銷。問題的最優子結構可以描述如下:假定問題Ai*Ai+1*...Aj被完全加括弧的最優方式是在Ak與Ak+1之間被分裂,那麼分裂

之後,最優解Ai*Ai+1*....Aj中的子鏈Ai*Ai+1....Ak一定是問題Ai*Ai+1*...*Ak的最優加括弧方式。同樣,最優解Ak+1*Ak+2*...Aj的子鏈一定是問題Ak+1*Ak+2*...Aj最優加括弧方式。

根據上面分析,設m[i,j]表示計算Ai...j所需的最小計算次數    m[i,j] = min{m[i,k]+m[k+1,j]+pi-1pKpj }

解決方案

#include<iostream>void main(){        int m[8][8], min;        int r[8] = {10, 20, 50, 1, 100, 4, 20, 2};     /* 矩陣維數 */         /* 初始化 */       memset(m,0,sizeof(m));        /* 每此增量加一 */        for (int l = 1; l < 7; l++)        {              /* 對於差值為 l 的兩個元素 */              for (int i = 1; i <= 7 - l; i++)              {                  j = i + l;                  /* 求其最小組合方式 */                  min = m[i][i] + m[i+1][j] + r[i-1] * r[i] * r[j];                  middle[i][j] = i;                  for (int k = i + 1; k < j; k++)                 {                       if (min > m[i][k] + m[k+1][j] + r[i-1] * r[k] * r[j])         {                               min = m[i][k] + m[k+1][j] + r[i-1] *r[k]* r[j];                                        middle[i][j] = k;                        }                  }                        m[i][j] = min;             }        }        std::cout<<m[1][N];}

由以上代碼可以很容易看出演算法的時間複雜度為O(n^3)。即便如此也比窮舉法的指數級時間複雜度快。

尾聲

動態規劃絕對不是一兩篇文章可以講清楚的。當然也不是通過一兩道題目可以完全學會。學習的關鍵是用動規的思想去想問題,去設計狀態轉移方程式。

動態規劃還有很多變形,如狀態壓縮,樹形等等。這些題目雖然很難,但是工作學習之餘,一邊聽歌,一邊推敲。也是人生一大快事

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.