動態規劃求最長遞增子序列(longest increasing subsequence)
1,什麼是動態規劃?
在現實生活中,有一類活動的過程,由於它的特殊性,可將過程分成若干個互相聯絡的階段,在它的每一階段都需要作出決策,從而使整個過程達到最好的活動效果。當然,各個階段決策的選取不是任意確定的,它依賴於當前面臨的狀態,又影響以後的發展,當各個階段決策確定後,就組成一個決策序列,因而也就確定了整個過程的一條活動路線,這種把一個問題看作是一個前後關聯具有鏈狀結構的多階段過程就稱為多階段決策過程,這種問題就稱為多階段決策問題。多階段的決策問題,就是要在所有可能採取的策略中選取一個最優的策略,以便得到最佳的效果。動態規劃是一種求解多階段決策問題的系統技術!
一般來說,只要問題可以劃分成規模更小的子問題,並且原問題的最優解中包含了子問題的最優解,則可以考慮用動態規劃解決。動態規劃與分治的異同?
相同點:把一個問題分割成子問題,通過組合子問題的解而求得原問題的解。
不同點:分治的子問題不重疊,而動態規劃的子問題重疊。
動態規劃與貪心的異同?
相同點:所求問題都具有最優子結構。
不同點:貪心自頂向下使用最優子結構 ,先做選擇,在當時看起來是最優的選擇,最終求得一個最優解。
動態規劃自底向上,先求解子問題的最優解,再做選擇,遞迴求問題的最優解。
2,什麼是最長遞增子序列?
比如陣列:5,6,7,0,10的最長遞增子序列就是6,6,7,10它是可以不連續的。
3,怎麼利用動態規劃?
想當然:要求A[0,,,i]的最長遞增子序列,則先要求A[0,,,j-1]的最長遞增子序列,然後再判斷A[0,,,j-1]的最長遞增子序列的最大元素是否大於A[i],然後再判斷使用!
這樣是不行的:想想陣列5,6,7,0,1,2,3
正確想法:對於動態規劃問題,往往存在遞推解決方法,這個問題也不例外。要求長度為i的序列的Ai{a1,a2,……,ai}最長遞增子序列,需要先求出序列Ai-1{a1,a2,……,ai-1}中以各元素(a1,a2,……,ai-1)作為最大元素的最長遞增序列,然後把所有這些遞增序列與ai比較。
基本方法:時間代價為O(n2);
#include "stdafx.h" #include <iostream> #include <ctime> #include <Windows.h> using namespace std; //基本方法O(n2) void LongestISA(int *a, int length) { int *tail = new int[length];//tail[j]表示以j為尾的LISA的長度 int *pre = new int[length];//pre[i]表示以i元素為最大元素的LISA的前驅元素 int max = 1;//表示最長長度 int k;//表示LISA的末尾元素的位置 int i; for (i = 0; i < length; i++) { tail[i] = 1; pre[i] = i; } for (i = 1; i < length; i++) { for (int j = 0; j < i; j++) { if (a[j] < a[i] && tail[j] + 1 > tail[i])//比如7,8,9,0,10這個陣列, { //若不加第二個條件,求tail[4]會是什麼樣? tail[i] = tail[j] + 1; pre[i] = j; if (max < tail[i]) { max = tail[i]; k = i; } } } } cout << "最長遞增子序列長度為:" << max << endl; //int *Result = new int[max]; //i = max - 1; //while (pre[k] != k) //{ // Result[i--] = a[k]; // k = pre[k]; //} //Result[i] = a[k]; //cout << "最長遞增子序列為:" << endl; //for (i = 0; i < max; i++) //{ // cout << Result[i] << ' '; //} delete[] pre; delete[] tail; //delete[] Result; }
改進方法:我們會發現下面一個特性:
用min_tail[i]表示長度為i+1的遞增序列中的最大元素最小的序列末尾元素的位置,
假設當前輸入陣列用A表示,則有A[min_tail[0]] < A[min_tail[1]] < .... < A[min_tail[max - 1]]這一性質,
為什麼呢?
因為長度為i的遞增序列中肯定有長度為i-1的遞增序列,而min_tail[i-1]表示的是長度為i-1的遞增序列中最大元素最小的那個的位置,所以這個位置所在的數肯定要小於或等於長度為i的遞增序列中的那個長度為i-1的遞增序列的最大元素,即肯定要小於長度為i的遞增序列中的最大元素。
有了這個特性,我們只要求出所有的min_tail元素,就得到了最長遞增子序列。
怎麼利用這一性質呢?
比如當前求到的最大長度用max表示,已經求出的min_tail有min_tail[0],,,,min_tail[max - 1];
現在來了一個數組元素A[i],我們先用二分查詢在A[min_tail[0]]到A[min_tail[max - 1]]中找到A[i]所在位置,
分幾種情況:
若A[i]大於當前的A[min_tail[max - 1]]表示遞增序列的長度增加了,變成了max+1,新的min_tail[max - 1]應當為i;
若A[i]在中間位置,比如A[min_tail[j]] < A[i] < A[min_tail[j+1]]則表示存在一個長度為j+1的遞增序列,且它的最大元素要比當前的A[min_tail[j+1]]小,所以要更新min_tail[j+1]為i;
若A[i]小於A[min_tail[0]],則更新A[min_tail[0]];
若找到有等於A[i]的,則不做處理,因為求得是遞增序列。
具體程式碼如下:
//下邊是改進的演算法O(nlogn)
int Bsearch(int *a, int *b, int low, int high, int key)
{
int i = low, j = high;
while (i < j)
{
int mid = (i + j)/2;
if (key > a[b[mid]])
{
i = mid + 1;
}
else
if (key < a[b[mid]])
{
j = mid - 1;
}
else
{
return -1;//表示相等
}
}
if (a[b[i]] == key)
{
return -1;
}
else
{
return i;
}
}
//關鍵就是求min_tail陣列
void LISA(int *a, int length)
{
int *min_tail = new int[length];//min_tail[i]表示長度為i+1的遞增序列中末尾值最小的
//遞增序列末尾元素的位置
int *pre = new int[length];//pre[i]表示i元素所在遞增序列中的前驅元素
int max = 1;
min_tail[0] = 0;
int i;
for (i = 0; i < length; i++)
{
pre[i] = i;
}
for (i = 1; i < length; i++)
{
if (max == 1)
{
if (a[i] < a[min_tail[0]])
{
min_tail[0] = i;
}
else
if (a[i] > a[min_tail[0]])
{
min_tail[1] = i;
max++;
pre[i] = min_tail[0];
}
}
else
{
int Result = Bsearch(a, min_tail, 0, max - 1, a[i]);//傳入的引數是陣列下標
//比較的是a[min_tail[下標]],因為a[min_tail[i]]是嚴格有序陣列,返回的是下標
if (Result == 0)//返回情況分四種情況
{
if (a[min_tail[Result]] > a[i])
{
min_tail[0] = i;
}
else
{
min_tail[1] = i;
pre[i] = min_tail[0];
}
}
else
if (Result == max - 1)
{
if (a[min_tail[Result]] > a[i])
{
min_tail[Result] = i;
//pre[i] = min_tail[Result - 1];//注意前驅元素位置是這個
}
else
{
pre[i] = min_tail[max - 1];
max++;
min_tail[max - 1] = i;
}
}
else
if (Result == -1)
{
}
else
if (a[min_tail[Result]] < a[i])
{
min_tail[Result + 1] = i;
pre[i] = min_tail[Result];
}
else
{
min_tail[Result] = i;
pre[i] = min_tail[Result - 1];
}
}
}
cout << "最長遞增子序列長度為:" << max << endl;
//int *Result = new int[max];
//i = max - 1;
//int j = max - 1;
//int k = min_tail[j];
//while (pre[k] != k)
//{
// Result[i--] = a[k];
// k = pre[k];
//}
//Result[i] = a[k];
//cout << "最長遞增子序列為:" << endl;
//for (i = 0; i < max; i++)
//{
// cout << Result[i] << ' ';
//}
delete[] pre;
delete[] min_tail;
//delete[] Result;
}
int _tmain(int argc, _TCHAR* argv[])
{
//int a[] = {35,36,39,3,15,27,6,42};
cout << "輸入陣列規模!" << endl;
int num;
cin >> num;
int *a = new int[num];
srand((int)time(0));
for (int i = 0; i < num; i++)
{
a[i] = rand()%5001;
//cout << a[i] << ' ';
}
cout << "基本方法結果為:" << endl;
double start1 = GetTickCount();
LongestISA(a, num);
double end1 = GetTickCount();
cout << "時間為:" << end1 - start1 << "毫秒!" << endl;
cout << "改進方法結果為:" << endl;
double start2 = GetTickCount();
LISA(a, num);
double end2 = GetTickCount();
cout << "時間為:" << end2 - start2 << "毫秒!" << endl;
system("pause");
return 0;
}
100000個隨機數兩個方法的比較結果為:
還有一些可用動態規劃解決的為題:
比如:裝配線排程問題:即怎麼是機車最快出去;
有了這個遞迴解,就可以自底向上一步一步求解,知道最終求出機車所走路線。
還有就是矩陣鏈乘法問題和求最長公共子序列問題。