動態規劃入門篇

本文整理轉載自:Hawstein’s Blog

動態規劃入門篇

什麼是動態規劃??

動態規劃是拆分問題,定義問題的狀態和狀態之間的關係,使得問題能夠以遞推或者說分治的方式去解決問題。(詳情請點這裏)。因此,使用動態規劃求解問題時我們需要找到某個轉態的最優解,在這個狀態的幫助下找到下一個狀態的最優解。

狀態是什麼並且如何定義它?

通俗的說,狀態是來描述子問題的解,或者說子問題的定義。

現通過一個例子來講解什麼狀態和如何定義它。

假設我們有若干枚1元,3元,5元的硬幣,如何使用最少的硬幣湊夠11元?

在求解這個問題或者說動態規劃求解的問題時,首先我們思考一個問題,如何用最少的硬幣湊夠i 元(i<11 ),?
爲什麼要問這個問題呢?有兩個原因:
1. 當我們需要一個大的問題時,總是習慣把問題的規模變小,這樣更容易分析和討論
2. 這個規模比較小的問題與原問題是同質的,除了規模變小了之外其他都是一樣的,本質上它還是同一個問題。

現在我們來求解這個規模較小的問題:
i=0,及如何使用最少的硬幣湊夠0元?因爲沒有0元的硬幣。因此,我們需要0個硬幣(也許你會問?我們都沒有0元的硬幣問這個問題不是很傻嗎?彆着急,這個思路有利於我們理清楚動態規劃在做什麼)。爲了敘述方便我們使用數學符號來進行表示,dp(i)=j 表示湊夠i 元要使用j 個硬幣。因此有, dp(0)=0
i=1, 因爲只有面值爲1元的硬幣,因此我們拿一個1元的硬幣,接下來我們只需要湊夠0元就好了。及dp(1)=1+dp(11)=1+dp(0)=1
i=2, 仍然只有1元的硬幣可以使用,我們拿一個1元的硬幣,接下來我們只需要湊夠2 - 1 = 1元。及dp(2)=1+dp(21)=1+dp(1)=2
i=3,我們有1元和3元的硬幣可以使用。因此,我們有兩種方案:第一種,拿一個1元的硬幣,及dp(3)=1+dp(31)=1+dp(2)=3 。第二種:拿一個3元的硬幣,及dp(3)=1+dp(33)=1+dp(0)=1 。因爲我們要選最少。所以dp(3)=min(1+dp(2),1+dp(0))=1
OK, 取了這麼多的值,講了這麼多具體的東西,是時候讓來說點抽象的,從上面的文字中,我們要抽出動態規劃中兩個最重要的概念: 狀態狀態轉換方程
上面的文字中:dp(i) 表示湊夠i元至少使用的硬幣數我們稱之爲轉態,而 dp(3)=min(1+dp(2),1+dp(0))=1 稱之爲轉態轉換方程。對於轉態轉換方程我們需要更加的抽象化及dp(i)=min(1+dp(1vj)),and(1vj)>=0 其中vj 表示第j個硬幣的面值。

有了狀態和狀態轉換方程,那麼這個問題也就容易解決了。當然,Talk is cheap,show me the code!
代碼如下:

#include<iostream>
#include<vector>
#include<limits.h>

using namespace std;

/*brief 求湊夠n元至少需要多少枚硬幣數
* param n : 湊夠n元
* param value : 硬幣值數組
* return : 硬幣數
*/
int countMin(int n, vector<int> value)
{
    //首先設置Min的元素爲最大值,便於比較
    vector<int> Min(n+1, INT_MAX);
    Min[0] = 0; // 對於湊夠0元的硬幣數
    // 採用自頂向上的方法求解
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j != value.size(); ++j) {
            if (value[j] <= i && Min[i] >= 1 + Min[i-value[j]])
                Min[i] = 1 + Min[i-value[j]];
        }
    }
    return Min[n];
}

int main()
{
    vector<int> value{1, 3, 5};  //幣值
    cout << countMin(11, value) << endl;
    return 0
}

下圖是當i從0到11時的解:
這裏寫圖片描述
從上圖可以得出,湊夠11元至少需要3枚硬幣。

最長遞增子序列列子

上面討論一個非常非常簡單的例子,現在來一個稍微複雜的問題,如何找到狀態和狀態轉換方程。

給定一個有N個數的數列,A[1], A[2],..., A[N]。求出最長遞增子序列的長度(LIS: Longest Increasing  Subsequence)。

正如上面講的,面對動態規劃的問題。首先,我們要定義一個“狀態”來代表它的子問題,並且找到它的解。注意:在大部分的情況下,某個轉態只與前面的狀態有關,並且獨立於後面的狀態。

我們繼續按照上題的思路一步一步來找到“狀態”和“狀態轉換方程”。假設我們考慮求A[1], A[2],…, A[i], 其中i < N 的最長遞增子序列的長度,那麼這個問題變成了原問題的一個子問題,並且與原問題的本質是相同的只是規模減小了而以。我們定義d(i)表示長度爲i的最長遞增子序列。因此,d(i)就是原問題的狀態。狀態定義好了之後,我們需要找到狀態之間的轉換方程。
爲了更好的敘述方便,我們假設數列爲:

5,3,4,8,6,7

根據上面定義的狀態,我們可以得到:
- i = 1 時, LIS長度d(1) = 1 (序列5)。
- i = 2 時, LIS長度d(2) = 1 (序列3; 3前面沒有比3更小的數字)。
- i = 3 時, LIS長度d(3) = 2 (序列3, 4: 4前面只有一個個比它更小的3所以d(3) = 1 + d(2) = 2)。
- i = 4 時, LIS長度d(4) = 3(序列3, 4 , 8: 8前面有3個比它小的數字因此d(4) = max{dp(1), dp(2), dp(3)} + 1。
很顯然,分析到這裏,狀態裝換方程已經明顯了,如果我們知道了dp(1), …, dp(i-1), 那麼dp(i)可以通過下面的狀態轉換方程得到

dp(i) = max(1, dp(j) + 1) if j < i and A[j] < A[i]

其中邊界條件爲dp(1) = 1;
分析完了,上圖:(第二列表示前i個數中LIS的長度, 第三列表示,LIS中到達當前這個數的上一個數的下標,根據這個可以求出LIS序列)

這裏寫圖片描述

code :

#include<iostream>
#include<vector>

using namespace std;

/** \brief 求數組A的最長遞增子序列的長度
 * \param 數組
 * \return LIS長度
 */     
int longestIncrSubseq(vector<int> A)
{
    int n = A.size();
    vector<int> res(n, 1);

    for (int i = 1; i < n; ++i) {
        for (int j = 0; j < i; ++j) {
            if (A[j] <= A[i]) {
                res[i] = max(res[i], res[j] + 1);
            }
        }
    }
    return res[n-1];
}

int main()
{
    vector<int> value{5,3,4,8,6,7};
    cout << longestIncrSubseq(value) << endl;

    return 0;
}

TopCoder ZigZag

看了上面兩道題之後,讓我們繼續對動態規劃的題目進行討論,題目來源於TopCoder, Inc

問題描述

給定一個序列,如果這個序列的連續數字之間的差是嚴格的正負交替的話,那麼我們稱這個序列爲zig-zag序列。對於第一個差及第二數減去第一個數(如果存在的話)可以是正的也可以是負的。如果一個序列的元素小於2的話,那麼這個序列也是zig-zag序列。
例如 1,7,4,9,2,5 是一個zig-zag序列,因爲他們之間的差(6,-3,5,-7,3)是嚴格的正負交替。相反, 1,4,7,2,5 和1,7,4,5,5 不是zig-zag序列。 對於第一個序列,因爲它的第一第二差值(3, 3)都是正的。對於第二個序列因爲他們最後一個差值是0。

現在,給定一個整形的序列,返回這個序列最長的zig-zag子序列的長度。子序列可以從最原始的序列中刪除一些元素,但是對於沒有刪除的元素需要保持他們的順序不變。
例如:
0)
{ 1, 7, 4, 9, 2, 5 }
返回: 6
整個序列都是zig-zag序列

1)
{ 1, 17, 5, 10, 13, 15, 10, 5, 16, 8 }
返回: 7
對於這個序列存在幾個子序列擁有相同的長度7。 其中一個爲:1,17,10,13,10,16,8.

2)
{ 44 }
返回: 1

3)
{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }
返回: 2

4)
{ 70, 55, 13, 2, 99, 2, 80, 80, 80, 80, 100, 19, 7, 5, 5, 5, 1000, 32, 32 }
返回: 8

5)
{ 374, 40, 854, 203, 203, 156, 362, 279, 812, 955, 600, 947, 978, 46, 100, 953,
670, 862, 568, 188, 67, 669, 810, 704, 52, 861, 49, 640, 370, 908, 477, 245, 413,
109, 659, 401, 483, 308, 609, 120, 249, 22, 176, 279, 23, 22, 617, 462, 459, 244 }
返回: 36

爲敘述方便,我們假設序列爲A[1], A[2], …, A[n]
首先,我們要找到原始題目的”狀態”, 及假設序列A[1], A[2], …, A[i]的最長zig-Zag子序列的最大長度爲dp(i)。現在狀態定義好的之後,我們需要找到狀態轉換方程。

對於序列{ 1, 17, 5, 10, 13, 15, 10, 5, 16, 8 }來說

根據上面狀態的定義,我們可以得到

  • i = 1 時,dp(1) = 1 (序列1)
  • i = 2 時,dp(2) = 2 (序列1, 17)
  • i = 3 時,dp(3) = 3 (序列1, 17, 5,因爲:(517)×(171)<0 , dp(3) = 1 + dp(2) = 3)
  • i = 4 時,dp(4) = 4 (序列1, 17, 5, 10, 因爲:(105)×(517)<0 , dp(4) = 1 + dp(3))
  • i = 5 時,dp(5) = 4(序列1,17,5, 10 或者序列1, 17, 5, 13。對於第一種情況,因爲(1310)×(105)>0 , 所以dp(4) = dp(3)。 對於第二種情況(135)×(513)<0 dp(5) = 1 + dp(3))
    很顯然,分析到這裏狀態轉換方程已經很明確了。如果我們知道了dp(1), … , dp(i-1) ,那麼dp(i)可以通過下面的狀態轉換方程得到:
dp(i) = max{dp(i), dp(j) + 1 if (A[i]-A[j]*(A[j]-A[j-1]) < 0) else dp(j)}

從定義可以看出i2 時dp(i)至少爲2, 並且有dp(1) = 1
code

#include<iostream>
#include<vector>

using namespace std;

int longestZigZag(vector<int> sequence)
{
    int n = sequence.size();
    vector<int> res(n, 2); //對於長度大於2的序列其最長zig-zag序列的長度至少爲2
    res[0] = 1;//對於只有一個元素
    //因此,我們可以從第三個元素開始計算
    for (int i = 2; i < n; ++i) {
        for (int j = 1; j < i; ++j) {
            if ((sequence[i] - sequence[j])*(sequence[j] - sequence[j-1]) < 0 
                && (1 + res[j]) > res[i])
                res[i] = 1 + res[j];
            if (res[j] > res[i])
                res[i] = res[j];
        }
    }
    return res[n-1];
}

int main()
{
    vector<int> sequence{ 1, 17, 5, 10, 13, 15, 10, 5, 16, 8 };
    cout << longestZigZag(sequence) << endl;

    return 0;
}

TopCoder BadNeighbors

趁熱打鐵,讓我們繼續討論此類問題,題目來源於TopCoder, Inc

Problem Statement

The old song declares “Go ahead and hate your neighbor”, and the residents of Onetinville have taken those words to heart. Every resident hates his next-door neighbors on both sides. Nobody is willing to live farther away from the town’s well than his neighbors, so the town has been arranged in a big circle around the well. Unfortunately, the town’s well is in disrepair and needs to be restored. You have been hired to collect donations for the Save Our Well fund.

Each of the town’s residents is willing to donate a certain amount, as specified in the int[] donations, which is listed in clockwise order around the well. However, nobody is willing to contribute to a fund to which his neighbor has also contributed. Next-door neighbors are always listed consecutively in donations, except that the first and last entries in donations are also for next-door neighbors. You must calculate and return the maximum amount of donations that can be collected.

Constraints
- donations contains between 2 and 40 elements, inclusive.
- Each element in donations is between 1 and 1000, inclusive.

Examples
0)
{ 10, 3, 2, 5, 7, 8 }
Returns: 19
The maximum donation is 19, achieved by 10+2+7. It would be better to take 10+5+8 except that the 10 and 8 donations are from neighbors.

1)
{ 11, 15 }
Returns: 15

2)
{ 7, 7, 7, 7, 7, 7, 7 }
Returns: 21

3)
{ 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 }
Returns: 16

4)
{ 94, 40, 49, 65, 21, 21, 106, 80, 92, 81, 679, 4, 61,
6, 237, 12, 72, 74, 29, 95, 265, 35, 47, 1, 61, 397,
52, 72, 37, 51, 1, 81, 45, 435, 7, 36, 57, 86, 81, 72 }
Returns: 2926

題目的意思是:給定一個序列從中挑選出一些數字所挑選的這些數字要求不相鄰,使得這些數字的和最大,其中認爲第一個數字和最後一個數字是相鄰的。

爲了敘述方便,假設這序列爲{A[1], A[2], …, A[n]}。
對於這個問題,首先我們來看簡單版本沒有環的及第一個元素與最後一個元素不相鄰。
首先,我們定義這個問題的狀態,假設A[1], …, A[i]的最大捐贈爲dp(i)。
爲了分析我們使用 { 10, 3, 2, 5, 7, 8 } 這個序列。

基於上面所假設的狀態我們可以得到
- i = 1 時 dp(1) = 10 (選10);
- i = 2 時 dp(2) = 10 (選10, 因爲3 < 10);
- i = 3 時 dp(3) = 12 (選10, 2。如果不選2的話,我們可以從10, 3序列中選擇得到10, 如果選2的話,我們只能從序列10中選得到12,因此dp(3) = 12)
- i = 4時 dp(4) = 15 (選10, 5。選5的話,我們還可以從10,3中選。如果不選5的話,我們可以從10, 3, 2中選擇,因此。dp(4) = max(dp(3), A[4] + dp(2));
基於上面的分析,我們可以得到狀態方程爲
dp(i) = max(A[i] + dp(i-2), dp(i-1)); (注意:這是無環序列的狀態轉換方程)

現在,我們繼續來討論有環序列的問題。對於有環和無環問題其問題的狀態還是一樣的及假設序列A[1], A[2], …, A[i]的最大捐贈爲dp(i)。
我們仍然以序列 { 10, 3, 2, 5, 7, 8 } 爲例。因此我們可以得到:

  • i = 1 時 dp(1) = 10 (選10)
  • i = 2 時 dp(2) = 10 (選10)
  • i = 3 時 dp(3) = 10 (對於i = 3 仍然還是隻有兩種情況,及考慮與不考慮A[3] = 2的問題。如果我們考慮A[3] = 2的話,我們只能選擇2因爲2與10和3都相鄰。如果不考慮2的話,我們可以從10,3中進行選着得到10,因此dp(3) = max(2, 10) = 10)。
  • i = 4 時 dp(4) = 12 (如果考慮5的話,我們只能從{3}進行選擇得到8,那麼就變成了從無環序列{3, 2, 5}選取最大的問題。如果不考慮5的話,我們可以從{10,3,2}進行選着得到12。那麼就變成從無環序列{10,3, 2}選取最大的問題。因此,dp(4) = max(8, 12) = 12)。
    似乎分析到這裏,問題已經變得很明確了。對於dp(i)的討論與無環序列一樣還是選與不選A[i]兩種情況。

    對於有環圖來說,需要考慮兩種情況:
    case 1: 如果考慮A[i]的話, 那麼問題就變成了從無環序列{A[1], …, A[i-2], A[i-1], A[i]}中選取最大的問題。
    case 2: 如果不考慮A[i]的話,那麼問題就變成了從無環序列{A[0], A[1], …., A[i-1]}中選取最大的問題。
    dp(i) = max{case1, case2};

    Talk is cheap,show me the code!

#include<iostream>
#include<vector>

using namespace std;

/** brief: 無環序列的最大捐贈
** param a: 序列的起始索引
** param b: 序列的結束索引的下一個索引,及索引不包含b
** param arr: 序列
** return : 返回最大捐贈和
*/
int solve(int a, int b, vector<int> &arr)
{
    int n = b - a;
    vector<int> dp(n, 0);
    dp[0] = arr[a];
    for (int i = 1; i + a < b; ++i) {
        if ((i - 2) < 0)
            dp[i] = max(arr[a+i], dp[i-1]);
        else
            dp[i] = max(arr[a+i] + dp[i-2], dp[i-1]);
    }
    return dp[n-1];
}

/**
*返回序列donations的最大捐贈。其中序列是有環的
**/
int maxDonations(vector<int> donations)
{
    int n = donations.size();
    int a = solve(1,n,donations);
    int b = solve(0,n-1,donations);
    return max(a, b);
}

int main()
{
    vector<int> donations { 10, 3, 2, 5, 7, 8 };
    cout << maxDonations(donations) << endl;
    return 0;
}

TopCoder FlowerGarden

既然都已經看到這裏了,那麼嘗試解決下面這道題吧!,題目來源於TopCoder, Inc

Problem Statement

You are planting a flower garden with bulbs to give you joyous flowers throughout the year. However, you wish to plant the flowers such that they do not block other flowers while they are visible.

You will be given a int[] height, a int[] bloom, and a int[] wilt. Each type of flower is represented by the element at the same index of height, bloom, and wilt. height represents how high each type of flower grows, bloom represents the morning that each type of flower springs from the ground, and wilt represents the evening that each type of flower shrivels up and dies. Each element in bloom and wilt will be a number between 1 and 365 inclusive, and wilt[i] will always be greater than bloom[i]. You must plant all of the flowers of the same type in a single row for appearance, and you also want to have the tallest flowers as far forward as possible. However, if a flower type is taller than another type, and both types can be out of the ground at the same time, the shorter flower must be planted in front of the taller flower to prevent blocking. A flower blooms in the morning, and wilts in the evening, so even if one flower is blooming on the same day another flower is wilting, one can block the other.

You should return a int[] which contains the elements of height in the order you should plant your flowers to acheive the above goals. The front of the garden is represented by the first element in your return value, and is where you view the garden from. The elements of height will all be unique, so there will always be a well-defined ordering.

Constraints
- height will have between 2 and 50 elements, inclusive.
- bloom will have the same number of elements as height
- wilt will have the same number of elements as height
- height will have no repeated elements.
- Each element of height will be between 1 and 1000, inclusive.
- Each element of bloom will be between 1 and 365, inclusive.
- Each element of wilt will be between 1 and 365, inclusive.
- For each element i of bloom and wilt, wilt[i] will be greater than bloom[i].

Examples
0)
{5,4,3,2,1}
{1,1,1,1,1}
{365,365,365,365,365}
Returns: { 1, 2, 3, 4, 5 }
These flowers all bloom on January 1st and wilt on December 31st. Since they all may block each other, you must order them from shortest to tallest.

1)
{5,4,3,2,1}
{1,5,10,15,20}
{4,9,14,19,24}
Returns: { 5, 4, 3, 2, 1 }
The same set of flowers now bloom all at separate times. Since they will never block each other, you can order them from tallest to shortest to get the tallest ones as far forward as possible.

2)
{5,4,3,2,1}
{1,5,10,15,20}
{5,10,15,20,25}
Returns: { 1, 2, 3, 4, 5 }
Although each flower only blocks at most one other, they all must be ordered from shortest to tallest to prevent any blocking from occurring.

3)
{5,4,3,2,1}
{1,5,10,15,20}
{5,10,14,20,25}
Returns: { 3, 4, 5, 1, 2 }
The difference here is that the third type of flower wilts one day earlier than the blooming of the fourth flower. Therefore, we can put the flowers of height 3 first, then the flowers of height 4, then height 5, and finally the flowers of height 1 and 2. Note that we could have also ordered them with height 1 first, but this does not result in the maximum possible height being first in the garden.

4)
{1,2,3,4,5,6}
{1,3,1,3,1,3}
{2,4,2,4,2,4}
Returns: { 2, 4, 6, 1, 3, 5 }

5)
{3,2,5,4}
{1,2,11,10}
{4,3,12,13}
Returns: { 4, 5, 2, 3 }

發佈了49 篇原創文章 · 獲贊 22 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章