C++——動態規劃入門 筆記1

其實一直沒分清動態規劃和貪心以及遞歸的區別,然而這周的作業是動態規劃專題(╯‵□′)╯︵┻━┻,我用貪心寫,不知道WA了多少次(╥╯^╰╥)。看到別人寫的動態規劃的題解還看不懂,╮(╯▽╰)╭。

然後今天在B站上/*看動畫片的時候*/發現有很多up主上傳了關於動態規劃的視頻,怎麼說呢,豁然開朗(注:這裏不是打廣告,B站上出了鬼畜還是有很多東西的,畢竟連考研數學都有(`´ー))。

首先,援引百度百科上的介紹/*大家可以無視這一段,畢竟百度百科是本着讓本來就懂的人看的,對於我這種完全不懂的小白並沒有什麼用*/:

動態規劃(dynamicprogramming)運籌學的一個分支,是求解決策過程(decisionprocess)最優化的數學方法。20世紀50年代初美國數學家R.E.Bellman等人在研究多階段決策過程(multistepdecision process)的優化問題時,提出了著名的最優化原理(principleof optimality),把多階段過程轉化爲一系列單階段問題,利用各階段之間的關係,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。1957年出版了他的名著《DynamicProgramming》,這是該領域的第一本著作。

動態規劃一般可分爲線性動規,區域動規,樹形動規,揹包動規四類。

舉例:

線性動規:攔截導彈,合唱隊形,挖地雷,建學校,劍客決鬥等;

區域動規:石子合併, 加分二叉樹,統計單詞個數,炮兵佈陣等;

樹形動規:貪吃的九頭龍,二分查找樹,聚會的歡樂,數字三角形等;

揹包問題:01揹包問題,完全揹包問題,分組揹包問題,二維揹包,裝箱問題,擠牛奶(同濟ACM1132題)等;

應用實例:

最短路徑問題 項目管理,網絡流優化等;

POJ動態規劃題目列表:

容易:
  1018,1050,1083,1088,1125,1143,1157,1163,1178,1179,1189,1191,1208,1276,1322,1414,1456,1458,1609,1644,1664,1690,1699,1740,1742,1887,1926,1936,1952,1953,1958,1959,1962,1975,1989,2018,2029,2039,2063,2081,2082,2181,2184,2192,2231,2279,2329,2336,2346,2353,2355,2356,2385,2392,2424

不易:
  1019,1037,1080,1112,1141,1170,1192,1239,1655,1695,1707,1733(區間減法加並查集),1737,1837,1850,1920(加強版漢羅塔),1934(全部最長公共子序列),1964(最大矩形面積,O(n*m)算法),2138,2151,2161,2178

推薦:
  1015,1635,1636,1671,1682,1692,1704,1717,1722,1726,1732,1770,1821,1853,1949,2019,2127,2176,2228,2287,2342,2374,2378,2384,2411

基本結構

多階段決策問題中,各個階段採取的決策,一般來說是與時間有關的,決策依賴於當前狀態,又隨即引起狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,故有動態的含義,稱這種解決多階段決策最優化問題的方法爲動態規劃方法。

動態規劃基本模型

根據上例分析和動態規劃的基本概念,可以得到動態規劃的基本模型如下:

(1)確定問題的決策對象。(2)對決策過程劃分階段。(3)對各階段確定狀態變量 (4)根據狀態變量確定費用函數和目標函數。 (5)建立各階段狀態變量的轉移過程,確定狀態轉移方程。

狀態轉移方程的一般形式:

一般形式: U:狀態; X:策略
  順推:f[Uk]=opt{f[Uk-1]+L[Uk-1,Xk-1]}其中,L[Uk-1,Xk-1] 狀態Uk-1通過策略Xk-1到達狀態Uk 的費用 初始f[U1];結果:f[Un]

倒推:
  f[Uk]=opt{f[Uk+1]+L[Uk,Xk]}
  L[Uk,Xk] 狀態Uk通過策略Xk到達狀態Uk+1 的費用
  初始f[Un];結果:f(U1)

動態規劃適用條件

任何思想方法都有一定的侷限性,超出了特定條件,它就失去了作用。同樣,動態規劃也並不是萬能的。適用動態規劃的問題必須滿足最優化原理和無後效性。

1.最優化原理(最優子結構性質) 最優化原理可這樣闡述:一個最優化策略具有這樣的性質,不論過去狀態和決策如何,對前面的決策所形成的狀態而言,餘下的諸決策必須構成最優策略。簡而言之,一個最優化策略的子策略總是最優的。一個問題滿足最優化原理又稱其具有最優子結構性質。

2.無後效性將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過當前的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱爲無後效性。

3.子問題的重疊性 動態規劃將原來具有指數級時間複雜度的搜索算法改進成了具有多項式時間複雜度的算法。其中的關鍵在於解決冗餘,這是動態規劃算法的根本目的。動態規劃實質上是一種以空間換時間的技術,它在實現的過程中,不得不存儲產生過程中的各種狀態,所以它的空間複雜度要大於其它的算法。

——以上全部來自百度百科(鏈接https://baike.baidu.com/item/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/529408?fr=aladdin

然後迴歸正題

 

其實歸根結底,動態規劃與貪心和遞歸還是有一定關係的。

 

舉例:

斐波拉切數列

大家對這個數列應該都不陌生,先粗略得列一下該數列的前幾項:

  1 12 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181……

這個數列的特點是從第三項起,每一項都是前兩項之和。

所以,理所當然的,我們會想到遞歸解法

代碼如下:

int fab(int n)
{
	if(n == 1 || n == 2)//即第一項和第二項的值都爲1
	{
		return 1;
	}
	else//如果n>2,那麼每一項都是前兩項之和,但是前兩項萬一沒有算,就會再往前找兩項
	{
		return fab(n - 1) + fab(n - 2);//直到找到了n==1和n==2
	}
}

    這個就是最經典的遞歸解法。那麼,既然問題解決了,爲什麼還要討論動態規劃呢?

原因很簡單,舉個栗子,如果我們要算fab(6),我們需要幹什麼?

由遞歸的知識,我們知道,要想知道fab(6),我們要建立如下解答樹:

fab(6)

                         

                    fab(5)                              fab(4)

                                                         

            fab(4)          fab(3)              fab(3)          fab(2)

            ↙↘            ↙↘                ↙↘

        fab(3)  fab(2)  fab(2)  fab(1)      fab(2)  fab(1)

我們可能覺得,這也不麻煩啊,但是,這纔到fab(6),如果是fab(7),fab(8),……,fab(n)呢?通過對解答樹的觀察,我們可以很容易發現,有很多數被算了不止一次,當n足夠大的時候,遞歸的時間複雜度會很大,達到O(2^n),很容易TLE,而這也是我們不願意看到的。

因此,我們需要一種方法——即每個數只算一次,這個數會被計算機記住(無論計算機用什麼辦法記住),而在之後的計算中,這個數會被直接調用,不需多次運算,如果可以實現的話,時間複雜度會顯著降低,從O(2^n)降至O(n)

一下代碼來自: http://m.blog.csdn.net/u011499425/article/details/52705544

動態規劃的備忘錄方法:

int fab(int n, int m[])
{
	if(m[n]){
		return m[n];
	}
	else{
		m[n] = fab(n - 1, m) + fab(n - 2, m);
		return m[n];
	}
}

int memFab(int n)
{
	int m[SIZE];
	int i;
	m[1] = 1;
	m[2] = 1;
	for(i = 3; i < SIZE; ++i){
		m[i] = 0;
	}
	return fab(n, m);
}

動態規劃的迭代方法:

int fab(int n)
{
	int f1, f2, f3, i;
	if(n == 1 || n == 2){
		return 1;
	}
	else{
		f1 = 1;
		f2 = 1;
		for(i = 2; i < n; ++i){
			f3 = f1 + f2;
			f1 = f2;
			f2 = f3;
		}
		return f3;
	}
}

-------------------------------------------以上(有輕微改動)

順便附上另一個討論遞歸和動歸時間的:https://segmentfault.com/a/1190000007927865

給定一個數列,長度爲N,求這個數列的最長上升(遞增)子數列(LIS)的長度.
1 7 2 8 3 4爲例。這個數列的最長遞增子數列是 1 2 3 4,長度爲4
次長的長度爲3, 包括 1 7 8; 1 2 3 .

據說,這個問題是討論DP必備的問題。

這個題英文版應該是這個//因爲恰好有這個的代碼,所以偷個懶

Longest Ordered Subsequence

A numericsequence of ai is ordered if a1 < a2< ... < aN. Let the subsequence of the given numericsequence ( a1, a2, ..., aN)be any sequence ( ai1, ai2, ..., aiK),where 1 <= i1 < i2 < ... < iK<= N. For example, sequence (1, 7, 3, 5, 9, 4, 8) has orderedsubsequences, e. g., (1, 7), (3, 4, 8) and many others. All longest orderedsubsequences are of length 4, e. g., (1, 3, 5, 8).

Your program, when given the numeric sequence, must find the length of itslongest ordered subsequence.

Input

The first lineof input file contains the length of sequence N. The second line contains theelements of sequence - N integers in the range from 0 to 10000 each, separatedby spaces. 1 <= N <= 1000

Output

Output file mustcontain a single integer - the length of the longest ordered subsequence of thegiven sequence.

Sample Input

7

1 7 3 5 9 4 8

Sample Output

4

先放代碼:

 

#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
int a[1005],dp[1005],n;
int LIS()  
{  
    int i,j,ans,m;  
    dp[1] = 1;  
    ans = 1;  //無論如何,至少滿足要求的數列長度爲1
    for(i = 2;i<=n;i++)  //兩個循環——比較的關鍵步驟
    {  
        m = 0;  
        for(j = 1;j<i;j++)  
        {  
            if(dp[j]>m && a[j]<a[i])//先將第一項與m比較若第一項>0且滿足在dp[i]之前較小的有dp[j]
            m = dp[j];//將dp[j]的值賦給m,之後就是新的dp[j]與dp[i]的比較了
        }
        dp[i] = m+1;
        if(dp[i]>ans)
        ans = dp[i];//得到最長子序列
    }  
    return ans;
}

int main()
{
    int i;
    while(~scanf("%d",&n))
    {
        for(i = 1;i<=n;i++)
        scanf("%d",&a[i]);
        printf("%d\n",LIS());

    }
  
    return 0;
}

一開始,我做這題的想法是很單純的,就是先輸入一個數列,然後從第一項開始與後面各項比較,如果後面有某項比第一項大,就計數+1,然後把這個某項按第一項相同操作辦法直到最後一項,然而本地過了,提交好像是WA還是TLE之類的記不清了。

其實,這道題就是一個裸的一維DP…………(╯‵□′)╯︵┻━┻

最後附上一個在360doc上看到的引入的例子,個人覺得寫得不錯(鏈接:http://www.360doc.com/content/13/0601/00/8076359_289597587.shtml

動態規劃算法通常基於一個遞推公式及一個或多個初始狀態。當前子問題的解將由上一次子問題的解推出。使用動態規劃來解題只需要多項式時間複雜度,因此它比回溯法、暴力法等要快許多。

現在讓我們通過一個例子來了解一下DP的基本原理。

首先,我們要找到某個狀態的最優解,然後在它的幫助下,找到下一個狀態的最優解。

狀態代表什麼及如何找到它?

狀態"用來描述該問題的子問題的解。原文中有兩段作者闡述得不太清楚,跳過直接上例子。

如果我們有面值爲1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元? (表面上這道題可以用貪心算法,但貪心算法無法保證可以求出解,比如1元換成2元的時候)

首先我們思考一個問題,如何用最少的硬幣湊夠i(i<11)?爲什麼要這麼問呢?兩個原因:1.當我們遇到一個大問題時,總是習慣把問題的規模變小,這樣便於分析討論。 2.這個規模變小後的問題和原來的問題是同質的,除了規模變小,其它的都是一樣的,本質上它還是同一個問題(規模變小後的問題其實是原問題的子問題)

好了,讓我們從最小的i開始吧。當i=0,即我們需要多少個硬幣來湊夠0元。由於135都大於0,即沒有比0小的幣值,因此湊夠0元我們最少需要0個硬幣。 (這個分析很傻是不是?彆着急,這個思路有利於我們理清動態規劃究竟在做些什麼。) 這時候我們發現用一個標記來表示這句湊夠0元我們最少需要0個硬幣。會比較方便,如果一直用純文字來表述,不出一會兒你就會覺得很繞了。那麼,我們用d(i)=j來表示湊夠i元最少需要j個硬幣。於是我們已經得到了d(0)=0,表示湊夠0元最小需要0個硬幣。當i=1時,只有面值爲1元的硬幣可用,因此我們拿起一個面值爲1的硬幣,接下來只需要湊夠0元即可,而這個是已經知道答案的,即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。當i=2時,仍然只有面值爲1的硬幣可用,於是我拿起一個面值爲1的硬幣,接下來我只需要再湊夠2-1=1元即可(記得要用最小的硬幣數量),而這個答案也已經知道了。所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到這裏,你都可能會覺得,好無聊,感覺像做小學生的題目似的。因爲我們一直都只能操作面值爲1的硬幣!耐心點,讓我們看看i=3時的情況。當i=3時,我們能用的硬幣就有兩種了:1元的和3元的( 5元的仍然沒用,因爲你需要湊的數目是3元!5元太多了親)。既然能用的硬幣有兩種,我就有兩種方案。如果我拿了一個1元的硬幣,我的目標就變爲了:湊夠3-1=2元需要的最少硬幣數量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。這個方案說的是,我拿31元的硬幣;第二種方案是我拿起一個3元的硬幣,我的目標就變成:湊夠3-3=0元需要的最少硬幣數量。即d(3)=d(3-3)+1=d(0)+1=0+1=1.這個方案說的是,我拿13元的硬幣。好了,這兩種方案哪種更優呢?記得我們可是要用最少的硬幣數量來湊夠3元的。所以,選擇d(3)=1,怎麼來的呢?具體是這樣得到的:d(3)=min{d(3-1)+1, d(3-3)+1}

OK,碼了這麼多字講具體的東西,讓我們來點抽象的。從以上的文字中,我們要抽出動態規劃裏非常重要的兩個概念:狀態和狀態轉移方程。

上文中d(i)表示湊夠i元需要的最少硬幣數量,我們將它定義爲該問題的"狀態",這個狀態是怎麼找出來的呢?我在另一篇文章 動態規劃之揹包問題(一)中寫過:根據子問題定義狀態。你找到子問題,狀態也就浮出水面了。最終我們要求解的問題,可以用這個狀態來表示:d(11),即湊夠11元最少需要多少個硬幣。那狀態轉移方程是什麼呢?既然我們用d(i)表示狀態,那麼狀態轉移方程自然包含d(i),上文中包含狀態d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。沒錯,它就是狀態轉移方程,描述狀態之間是如何轉移的。當然,我們要對它抽象一下,

d(i)=min{ d(i-vj)+1 },其中i-vj>=0vj表示第j個硬幣的面值;

此外,通過追蹤我們是如何從前一個狀態值得到當前狀態值的,可以找到每一次我們用的是什麼面值的硬幣。比如,從上面的圖我們可以看出,最終結果d(11)=d(10)+1(面值爲1),而d(10)=d(5)+1(面值爲5),最後d(5)=d(0)+1 (面值爲5)。所以我們湊夠11元最少需要的3枚硬幣是:1元、5元、5元。

注意:原文中這裏本來還有一段的,但我反反覆覆讀了幾遍,大概的意思我已經在上文從i=0i=3的分析中有所體現了。作者本來想講的通俗一些,結果沒寫好,反而更不好懂,所以這段不翻譯了。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章