動態編程i
數組下標越界.
java技術類文章可以關注微信公賬號:碼農工作室
㈡ 用動態規劃方法找出由n個數a【i】(1<=i<=n)組成的序列的一個最長單調遞增子序列
f(i)=max{f(j)+1},c[i]=j(f(j)+1{max}),其中j<i and a[j]<a[i]
f(i)表示若第i個數為最長遞增子序列的末端,此時的最長遞增子序列的長度
c[i]表示令f(i)最大的前一個元素
用這種方法可以找到最大的f(i),通過c[i]的倒推可以得到最長單調遞增子序列
因為對每一個元素找到f(i)的效率為O(i),所以對所有元素找到的時間效率為O(n^2),空間效率為O(3n)
優化:
要能更改f(i)需要兩個條件:
j<i and a[j]<a[i]
f(j)盡可能大
以上兩個條件提示我們後面的值一定要大於前面的值。因此我們試著構建一個遞增的序列。在這個遞增的序列中查找可以更改的f值,使得序列的值盡可能小。
舉例:
有序列
1,5,6,2,3,4,8,5
令d[i]表示f(j)=i中的數中最小的那個的序號
我們有如下的處理
初始:d={}
1:d={1}
2:d={1,2}
3:d={1,2,3}
4:d={1,4,3} (這里:f(4)=f(2)=2,但是a[4]<a[2],如果後面有某個數要作為第三大的數的話,小一點的a[4]顯然更有優勢)
5:d={1,4,5}
6:d={1,4,5,6}
7:d={1,4,5,6,7}
8:d={1,4,5,6,8}
同樣的,在替換或加到序列最後一個之前,存下d-1
這樣就可以通過序列d的最後一項向前倒推到整個序列
因為序列d有序,可以通過二分搜索得到,因此時間效率為O(nlogn),空間效率為O(3n)
㈢ 什麼是動態規劃動態規劃的意義是什麼
動態規劃中遞推式的求解方法不是動態規劃的本質。
我曾經作為省隊成員參加過NOI,保送之後也給學校參加NOIP的同學多次講過動態規劃,我試著講一下我理解的動態規劃,爭取深入淺出。希望你看了我的答案,能夠喜歡上動態規劃。
0. 動態規劃的本質,是對問題狀態的定義和狀態轉移方程的定義。
引自維基網路
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.動態規劃是通過拆分問題,定義問題狀態和狀態之間的關系,使得問題能夠以遞推(或者說分治)的方式去解決。
本題下的其他答案,大多都是在說遞推的求解方法,但如何拆分問題,才是動態規劃的核心。
而拆分問題,靠的就是狀態的定義和狀態轉移方程的定義。
1. 什麼是狀態的定義?
首先想說大家千萬不要被下面的數學式嚇到,這里只涉及到了函數相關的知識。
我們先來看一個動態規劃的教學必備題:
給定一個數列,長度為N,
求這個數列的最長上升(遞增)子數列(LIS)的長度.
以
1 7 2 8 3 4
為例。
這個數列的最長遞增子數列是 1 2 3 4,長度為4;
次長的長度為3, 包括 1 7 8; 1 2 3 等.要解決這個問題,我們首先要定義這個問題和這個問題的子問題。
有人可能會問了,題目都已經在這了,我們還需定義這個問題嗎?需要,原因就是這個問題在字面上看,找不出子問題,而沒有子問題,這個題目就沒辦法解決。
所以我們來重新定義這個問題:
給定一個數列,長度為N,
設為:以數列中第k項結尾的最長遞增子序列的長度.
求 中的最大值.顯然,這個新問題與原問題等價。
而對於來講,都是的子問題:因為以第k項結尾的最長遞增子序列(下稱LIS),包含著以第中某項結尾的LIS。
上述的新問題也可以叫做狀態,定義中的「為數列中第k項結尾的LIS的長度」,就叫做對狀態的定義。
之所以把做「狀態」而不是「問題」 ,一是因為避免跟原問題中「問題」混淆,二是因為這個新問題是數學化定義的。
對狀態的定義只有一種嗎?當然不是。
我們甚至可以二維的,以完全不同的視角定義這個問題:
給定一個數列,長度為N,
設為:
在前i項中的,長度為k的最長遞增子序列中,最後一位的最小值. .
若在前i項中,不存在長度為k的最長遞增子序列,則為正無窮.
求最大的x,使得不為正無窮。這個新定義與原問題的等價性也不難證明,請讀者體會一下。
上述的就是狀態,定義中的「為:在前i項中,長度為k的最長遞增子序列中,最後一位的最小值」就是對狀態的定義。
2. 什麼是狀態轉移方程?
上述狀態定義好之後,狀態和狀態之間的關系式,就叫做狀態轉移方程。
比如,對於LIS問題,我們的第一種定義:
設為:以數列中第k項結尾的最長遞增子序列的長度.設A為題中數列,狀態轉移方程為:
(根據狀態定義導出邊界情況)
用文字解釋一下是:
以第k項結尾的LIS的長度是:保證第i項比第k項小的情況下,以第i項結尾的LIS長度加一的最大值,取遍i的所有值(i小於k)。
第二種定義:
設為:在數列前i項中,長度為k的遞增子序列中,最後一位的最小值設A為題中數列,狀態轉移方程為:
若則
否則:(邊界情況需要分類討論較多,在此不列出,需要根據狀態定義導出邊界情況。)
大家套著定義讀一下公式就可以了,應該不難理解,就是有點繞。
這里可以看出,這里的狀態轉移方程,就是定義了問題和子問題之間的關系。
可以看出,狀態轉移方程就是帶有條件的遞推式。
3. 動態規劃迷思
本題下其他用戶的回答跟動態規劃都有或多或少的聯系,我也講一下與本答案的聯系。
a. 「緩存」,「重疊子問題」,「記憶化」:
這三個名詞,都是在闡述遞推式求解的技巧。以Fibonacci數列為例,計算第100項的時候,需要計算第99項和98項;在計算第101項的時候,需要第100項和第99項,這時候你還需要重新計算第99項嗎?不需要,你只需要在第一次計算的時候把它記下來就可以了。
上述的需要再次計算的「第99項」,就叫「重疊子問題」。如果沒有計算過,就按照遞推式計算,如果計算過,直接使用,就像「緩存」一樣,這種方法,叫做「記憶化」,這是遞推式求解的技巧。這種技巧,通俗的說叫「花費空間來節省時間」。都不是動態規劃的本質,不是動態規劃的核心。
b. 「遞歸」:
遞歸是遞推式求解的方法,連技巧都算不上。
c. "無後效性",「最優子結構」:
上述的狀態轉移方程中,等式右邊不會用到下標大於左邊i或者k的值,這是"無後效性"的通俗上的數學定義,符合這種定義的狀態定義,我們可以說它具有「最優子結構」的性質,在動態規劃中我們要做的,就是找到這種「最優子結構」。
在對狀態和狀態轉移方程的定義過程中,滿足「最優子結構」是一個隱含的條件(否則根本定義不出來)。對狀態和「最優子結構」的關系的進一步解釋,什麼是動態規劃?動態規劃的意義是什麼? - 王勐的回答 寫的很好,大家可以去讀一下。
需要注意的是,一個問題可能有多種不同的狀態定義和狀態轉移方程定義,存在一個有後效性的定義,不代表該問題不適用動態規劃。這也是其他幾個答案中出現的邏輯誤區:
動態規劃方法要尋找符合「最優子結構「的狀態和狀態轉移方程的定義,在找到之後,這個問題就可以以「記憶化地求解遞推式」的方法來解決。而尋找到的定義,才是動態規劃的本質。
有位答主說:
分治在求解每個子問題的時候,都要進行一遍計算
動態規劃則存儲了子問題的結果,查表時間為常數這就像說多加辣椒的菜就叫川菜,多加醬油的菜就叫魯菜一樣,是存在誤解的。
文藝的說,動態規劃是尋找一種對問題的觀察角度,讓問題能夠以遞推(或者說分治)的方式去解決。尋找看問題的角度,才是動態規劃中最耀眼的寶石!(大霧)
㈣ 用C++編寫程序 動態規劃
//==
#include <iostream>
using namespace std;
#include <cmath>
//--
struct Coordinate //頂點
{
float x;
float y;
};
//--
float Mole(Coordinate a,Coordinate b) //求兩點的模值
{
float mole=(a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);
return (float)sqrt(mole);
}
//--
float W(Coordinate a,Coordinate b,Coordinate c) //權函數
{
return Mole(a,b)+Mole(b,c)+Mole(a,c);
}
//--
void Triangle_Dissect(Coordinate v[11],float m[10][10],int s[10][10],int n) //最優剖分
{
for(int i=1;i<n;i++)
{
m[i][i]=0;
}
for(int l=2;l<=n-1;l++)
{
for(i=1;i<=n-l;i++)
{
int j=i+l-1;
m[i][j]=m[i+1][j]+W(v[i-1],v[i],v[j]);
s[i][j]=i;
for(int k=i;k<=j-1;k++)
{
float q=m[i][k]+m[k+1][j]+W(v[i-1],v[k],v[j]);
if(q<m[i][j])
{
m[i][j]=q;
s[i][j]=k;
}
}
}
}
}
//--
bool judge(Coordinate a,Coordinate b,Coordinate v[11],int n) //判斷能否構成凸多邊形
{
float t[11];
int j=0;
int k=0;
for(int i=0;i<n;i++)
{
if(a.x==b.x)
t[i]=v[i].x-a.x;
else if(a.y==b.y)
t[i]=v[i].y-a.y;
else
t[i]=(v[i].y-a.y)/(b.y-a.y)-(v[i].x-a.x)/(b.x-a.x);
}
for(i=0;i<n;i++)
{
if(t[i]>0)
j++;
if(t[i]<0)
k++;
}
if(j==n-2||k==n-2)
return true;
else
return false;
}
//--
bool input(Coordinate v[11],int n) //輸入頂點坐標,並判斷能否構成凸多邊形
{
for(int i=0;i<n;i++)
{
cout<<"輸入頂點v"<<i<<"坐標";
cin>>v[i].x>>v[i].y;
}
int t[11];
for(i=0;i<n;i++)
{
if(i==n-1)
t[i]=judge(v[i],v[0],v,n);
else
t[i]=judge(v[i],v[i+1],v,n);
}
int j=0;
for(i=0;i<n;i++)
{
if(t[i]>0)
j++;
}
if(j==n)
return true;
else
return false;
}
//--
void outputm(float a[10][10],int n) //輸出數組m中保存的數據
{
for(int i=1;i<n;i++)
{
for(int j=1;j<n;j++)
{
if(j<i)
{
cout.width(16);
cout<<"*";
}
else
{
cout.width(16);
cout<<a[i][j];
}
}
cout<<endl;
}
}
//--
void outputs(int a[10][10],int n) //輸出數組s中保存的數據
{
for(int i=1;i<n;i++)
{
for(int j=1;j<n;j++)
{
if(j<=i)
{
cout.width(12);
cout<<"*";
}
else
{
cout.width(12);
cout<<a[i][j];
}
}
cout<<endl;
}
}
//--
void Triangle_Print(int i,int j,int s[10][10]) //輸出最優剖分出的全部三角形
{
if(i==j)
return;
Triangle_Print(i,s[i][j],s);
Triangle_Print(s[i][j]+1,j,s);
cout<<"三角形:v"<<i-1<<"v"<<s[i][j]<<"v"<<j<<endl;
}
//--
void main()
{
int n;
Coordinate v[11];
float m[10][10];
int s[10][10];
cout<<"輸入頂點個數,限定10個"<<endl;
cin>>n;
if(n>10)
cout<<"輸入有誤"<<endl;
else
{
cout<<"輸入各頂點坐標"<<endl;
if(input(v,n))
{
Triangle_Dissect(v,m,s,n);
cout<<endl<<"數組m中記錄的數據為:"<<endl;
outputm(m,n);
cout<<"數組s中記錄的數據為:"<<endl;
outputs(s,n);
cout<<"最優凸多邊形三角剖分為:"<<endl;
Triangle_Print(1,n-1,s);
}
else
cout<<"不能構成凸多邊形"<<endl;
}
}
//===
呵呵 這類問題好難 寫了一整天啊 學到了很多 還是有收獲的
㈤ c語言動態分配編程
malloc是分配內存用的,返回分配區域的指針,指針類型為void。而p的類型為char,類型不匹配錯誤,要進行類型強制轉換。
p =(char *) malloc(strlen(a) +strlen(b) +1);
null是為了檢測內存是否分配成功,malloc分配失敗是返回null,此時賦值給p,所以要檢測p是否為null。大部分情況分配都是成功的。但是為了程序健壯性,這種檢測是需要的。
再說說malloc有什麼用,申請p指針後,系統只分配給你一個指針。但是並沒有分配給你數組存放的空間。malloc就是申請用來存放數組的空間的。
㈥ 動態規劃編程題目
#include<iostream>
using namespace std;
void main()
{
int thing[6]={5,3,7,2,3,4};
int Value[6]={3,6,5,4,3,4};
int maxValue[6];//分別定義3個數組
//分別求出各自總價值
for(int i=0;i<6;i++)
{
maxValue[i]=15/thing[i]*Value[i];
}
for (int j=5;j>=0;j--)//冒泡排序
{
for (int i=0;i<=j;i++)
{
int t;
if(maxValue[i]>maxValue[i+1])
{
t=maxValue[i+1];
maxValue[i+1]=maxValue[i];
maxValue[i]=t;
}
}
}
int p=maxValue[6];//輸出最大數
cout<<p;
}
能力有限。有錯留言
㈦ 詳解動態規劃演算法
其實你可以這么去想。
能用動態規劃解決的問題,肯定能用搜索解決。
但是搜素時間復雜度太高了,怎麼優化呢?
你想到了記憶化搜索,就是搜完某個解之後把它保存起來,下一次搜到這個地方的時候,調用上一次的搜索出來的結果。這樣就解決了處理重復狀態的問題。
動態規劃之所以速度快是因為解決了重復處理某個狀態的問題。
記憶化搜索是動態規劃的一種實現方法。
搜索到i狀態,首先確定要解決i首先要解決什麼狀態。
那麼那些狀態必然可以轉移給i狀態。
於是你就確定了狀態轉移方程。
然後你需要確定邊界條件。
將邊界條件賦予初值。
此時就可以從前往後枚舉狀態進行狀態轉移拉。
㈧ 什麼是動態規劃
動態規劃演算法 概念及意義動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。20世紀50年代初美國數學家R.E.Bellman等人在研究多階段決策過程(multistep decision process)的優化問題時,提出了著名的最優化原理(principle of optimality),把多階段過程轉化為一系列單階段問題,利用各階段之間的關系,逐個求解,創立了解決這類過程優化問題的新方法——動態規劃。1957年出版了他的名著Dynamic Programming,這是該領域的第一本著作。
動態規劃問世以來,在經濟管理、生產調度、工程技術和最優控制等方面得到了廣泛的應用。例如最短路線、庫存管理、資源分配、設備更新、排序、裝載等問題,用動態規劃方法比用其它方法求解更為方便。
雖然動態規劃主要用於求解以時間劃分階段的動態過程的優化問題,但是一些與時間無關的靜態規劃(如線性規劃、非線性規劃),只要人為地引進時間因素,把它視為多階段決策過程,也可以用動態規劃方法方便地求解。
動態規劃程序設計是對解最優化問題的一種途徑、一種方法,而不是一種特殊演算法。不象前面所述的那些搜索或數值計算那樣,具有一個標準的數學表達式和明確清晰的解題方法。動態規劃程序設計往往是針對一種最優化問題,由於各種問題的性質不同,確定最優解的條件也互不相同,因而動態規劃的設計方法對不同的問題,有各具特色的解題方法,而不存在一種萬能的動態規劃演算法,可以解決各類最優化問題。因此讀者在學習時,除了要對基本概念和方法正確理解外,必須具體問題具體分析處理,以豐富的想像力去建立模型,用創造性的技巧去求解。我們也可以通過對若干有代表性的問題的動態規劃演算法進行分析、討論,逐漸學會並掌握這一設計方法。 基本模型
多階段決策過程的最優化問題。
在現實生活中,有一類活動的過程,由於它的特殊性,可將過程分成若干個互相聯系的階段,在它的每一階段都需要作出決策,從而使整個過程達到最好的活動效果。當然,各個階段決策的選取不是任意確定的,它依賴於當前面臨的狀態,又影響以後的發展,當各個階段決策確定後,就組成一個決策序列,因而也就確定了整個過程的一條活動路線,如圖所示:(看詞條圖)
這種把一個問題看作是一個前後關聯具有鏈狀結構的多階段過程就稱為多階段決策過程,這種問題就稱為多階段決策問題。 記憶化搜索 給你一個數字三角形, 形式如下:
1
2 3
4 5 6
7 8 9 10
找出從第一層到最後一層的一條路,使得所經過的權值之和最小或者最大.
無論對與新手還是老手,這都是再熟悉不過的題了,很容易地,我們寫出狀態轉移方程:f(i, j)=a[i, j] + min{f(i+1, j),f(i+1, j + 1)}
對於動態規劃演算法解決這個問題,我們根據狀態轉移方程和狀態轉移方向,比較容易地寫出動態規劃的循環表示方法。但是,當狀態和轉移非常復雜的時候,也許寫出循環式的動態規劃就不是那麼簡單了。
解決方法:
我們嘗試從正面的思路去分析問題,如上例,不難得出一個非常簡單的遞歸過程 :
f1:=f(i-1,j+1); f2:=f(i-1,j);
if f1>f2 then f:=f1+a[i,j] else f:=f2+a[i,j];
顯而易見,這個演算法就是最簡單的搜索演算法。時間復雜度為2^n,明顯是會超時的。分析一下搜索的過程,實際上,很多調用都是不必要的,也就是把產生過的最優狀態,又產生了一次。為了避免浪費,很顯然,我們存放一個opt數組:Opt[i, j] - 每產生一個f(i, j),將f(i, j)的值放入opt中,以後再次調用到f(i, j)的時候,直接從opt[i, j]來取就可以了。於是動態規劃的狀態轉移方程被直觀地表示出來了,這樣節省了思維的難度,減少了編程的技巧,而運行時間只是相差常數的復雜度,避免了動態規劃狀態轉移先後的問題,而且在相當多的情況下,遞歸演算法能更好地避免浪費,在比賽中是非常實用的. 狀態 決策
決策:
當前狀態通過決策,回到了以前狀態.可見決策其實就是狀態之間的橋梁。而以前狀態也就決定了當前狀態的情況。數字三角形的決策就是選擇相鄰的兩個以前狀態的最優值。
狀態:
我們一般在動規的時候所用到的一些數組,也就是用來存儲每個狀態的最優值的。我們就從動態規劃的要訣,也就是核心部分「狀態」開始,來逐步了解動態規劃。有時候當前狀態確定後,以前狀態就已經確定,則無需枚舉.
動態規劃演算法的應用 一、動態規劃的概念
近年來,涉及動態規劃的各種競賽題越來越多,每一年的NOI幾乎都至少有一道題目需要用動態規劃的方法來解決;而競賽對選手運用動態規劃知識的要求也越來越高,已經不再停留於簡單的遞推和建模上了。
要了解動態規劃的概念,首先要知道什麼是多階段決策問題。
1. 多階段決策問題
如果一類活動過程可以分為若干個互相聯系的階段,在每一個階段都需作出決策(採取措施),一個階段的決策確定以後,常常影響到下一個階段的決策,從而就完全確定了一個過程的活動路線,則稱它為多階段決策問題。
各個階段的決策構成一個決策序列,稱為一個策略。每一個階段都有若干個決策可供選擇,因而就有許多策略供我們選取,對應於一個策略可以確定活動的效果,這個效果可以用數量來確定。策略不同,效果也不同,多階段決策問題,就是要在可以選擇的那些策略中間,選取一個最優策略,使在預定的標准下達到最好的效果.
2.動態規劃問題中的術語
階段:把所給求解問題的過程恰當地分成若干個相互聯系的階段,以便於求解,過程不同,階段數就可能不同.描述階段的變數稱為階段變數。在多數情況下,階段變數是離散的,用k表示。此外,也有階段變數是連續的情形。如果過程可以在任何時刻作出決策,且在任意兩個不同的時刻之間允許有無窮多個決策時,階段變數就是連續的。
在前面的例子中,第一個階段就是點A,而第二個階段就是點A到點B,第三個階段是點B到點C,而第四個階段是點C到點D。
狀態:狀態表示每個階段開始面臨的自然狀況或客觀條件,它不以人們的主觀意志為轉移,也稱為不可控因素。在上面的例子中狀態就是某階段的出發位置,它既是該階段某路的起點,同時又是前一階段某支路的終點。
在前面的例子中,第一個階段有一個狀態即A,而第二個階段有兩個狀態B1和B2,第三個階段是三個狀態C1,C2和C3,而第四個階段又是一個狀態D。
過程的狀態通常可以用一個或一組數來描述,稱為狀態變數。一般,狀態是離散的,但有時為了方便也將狀態取成連續的。當然,在現實生活中,由於變數形式的限制,所有的狀態都是離散的,但從分析的觀點,有時將狀態作為連續的處理將會有很大的好處。此外,狀態可以有多個分量(多維情形),因而用向量來代表;而且在每個階段的狀態維數可以不同。
當過程按所有可能不同的方式發展時,過程各段的狀態變數將在某一確定的范圍內取值。狀態變數取值的集合稱為狀態集合。
無後效性:我們要求狀態具有下面的性質:如果給定某一階段的狀態,則在這一階段以後過程的發展不受這階段以前各段狀態的影響,所有各階段都確定時,整個過程也就確定了。換句話說,過程的每一次實現可以用一個狀態序列表示,在前面的例子中每階段的狀態是該線路的始點,確定了這些點的序列,整個線路也就完全確定。從某一階段以後的線路開始,當這段的始點給定時,不受以前線路(所通過的點)的影響。狀態的這個性質意味著過程的歷史只能通過當前的狀態去影響它的未來的發展,這個性質稱為無後效性。
決策:一個階段的狀態給定以後,從該狀態演變到下一階段某個狀態的一種選擇(行動)稱為決策。在最優控制中,也稱為控制。在許多間題中,決策可以自然而然地表示為一個數或一組數。不同的決策對應著不同的數值。描述決策的變數稱決策變數,因狀態滿足無後效性,故在每個階段選擇決策時只需考慮當前的狀態而無須考慮過程的歷史。
決策變數的范圍稱為允許決策集合。
策略:由每個階段的決策組成的序列稱為策略。對於每一個實際的多階段決策過程,可供選取的策略有一定的范圍限制,這個范圍稱為允許策略集合。允許策略集合中達到最優效果的策略稱為最優策略。
給定k階段狀態變數x(k)的值後,如果這一階段的決策變數一經確定,第k+1階段的狀態變數x(k+1)也就完全確定,即x(k+1)的值隨x(k)和第k階段的決策u(k)的值變化而變化,那麼可以把這一關系看成(x(k),u(k))與x(k+1)確定的對應關系,用x(k+1)=Tk(x(k),u(k))表示。這是從k階段到k+1階段的狀態轉移規律,稱為狀態轉移方程。
最優性原理:作為整個過程的最優策略,它滿足:相對前面決策所形成的狀態而言,餘下的子策略必然構成「最優子策略」。
最優性原理實際上是要求問題的最優策略的子策略也是最優。讓我們通過對前面的例子再分析來具體說明這一點:從A到D,我們知道,最短路徑是A�8�1B1�8�1C2�8�1D,這些點的選擇構成了這個例子的最優策略,根據最優性原理,這個策略的每個子策略應是最優:A�8�1B1�8�1C2是A到C2的最短路徑,B1�8�1C2�8�1D也是B1到D的最短路徑……──事實正是如此,因此我們認為這個例子滿足最優性原理的要求。
㈨ 如何用c語言編程實現動態規劃
動態規劃主要是 狀態 & 狀態轉移方程 如果轉移方程寫出來了 程序就自然出來啦
對於這題 dp[i][j] 表示 走到格子 (i,j) 時 的總和最大值 val[i][j] 表示格子 (i, j) 的值
那麼有 dp[i][j] = max( dp[i-1][j] , dp[i][j-1]) + val[i][j];
當然 邊界的時候要特殊考慮一下
for(int i = 1; i <= m ; i++)
for(int j = 1; j <= n; j++)
{
if( i == 1 ) ....;
else if( j == 1 ) ....;
else dp[i][j] = max( dp[i-1][j] , dp[i][j-1]) + val[i][j];
}
最後 dp[m][n] 就是答案啦 ^ ^
㈩ 動態規劃演算法程序例子
給你導彈攔截的吧:
[問題描述]
某國為了防禦敵國的導彈襲擊,發展出一種導彈攔截系統。但是這種導彈攔截系統有一個缺陷:雖然它的第一發炮彈能夠到達任意的高度,但是以後每一發炮彈都不能高於前一發的高度。某天,雷達捕捉到敵國的導彈來襲。由於該系統還在試用階段,所以只有一套系統,因此有可能不能攔截所有的導彈。
輸入導彈依次飛來的高度(雷達給出的高度數據是不大於30000的正整數,每個數據之間至少有一個空格),計算這套系統最多能攔截多少導彈,如果要攔截所有導彈最少要配備多少套這種導彈攔截系統。
[輸入輸出樣例]
INPUT:
389 207 155 300 299 170 158 65
OUTPUT:
6(最多能攔截的導彈數)
2(要攔截所有導彈最少要配備的系統數)
[問題分析]
我們先解決第一問。一套系統最多能攔多少導彈,跟它最後攔截的導彈高度有很大關系。假設a[i]表示攔截的最後一枚導彈是第i枚時,系統能攔得的最大導彈數。例如,樣例中a[5]=3,表示:如果系統攔截的最後一枚導彈是299的話,最多可以攔截第1枚(389)、第4枚(300)、第5枚(299)三枚導彈。顯然,a[1]~a[8]中的最大值就是第一問的答案。關鍵是怎樣求得a[1]~a[8]。
假設現在已經求得a[1]~a[7](註:在動態規劃中,這樣的假設往往是很必要的),那麼怎樣求a[8]呢?a[8]要求系統攔截的最後1枚導彈必須是65,也就意味著倒數第2枚被攔截的導彈高度必須不小於65,則符合要求的導彈有389、207、155、300、299、170、158。假如最後第二枚導彈是300,則a[8]=a[4]+1;假如倒數第2枚導彈是299,則a[8]=a[5]+1;類似地,a[8]還可能是a[1]+1、a[2]+1、……。當然,我們現在求得是以65結尾的最多導彈數目,因此a[8]要取所有可能值的最大值,即a[8]=max{a[1]+1,a[2]+1,……,a[7]+1}=max{a[i]}+1 (i=1..7)。
類似地,我們可以假設a[1]~a[6]為已知,來求得a[7]。同樣,a[6]、a[5]、a[4]、a[3]、a[2]也是類似求法,而a[1]就是1,即如果系統攔截的最後1枚導彈是389,則只能攔截第1枚。
這樣,求解過程可以用下列式子歸納:
a[1]=1
a[i]=max{a[j]}+1 (i>1,j=1,2,…,i-1,且j同時要滿足:a[j]>=a[i])
最後,只需把a[1]~a[8]中的最大值輸出即可。這就是第一問的解法,這種解題方法就稱為「動態規劃」。
第二問比較有意思。由於它緊接著第一問,所以很容易受前面的影響,多次採用第一問的辦法,然後得出總次數,其實這是不對的。要舉反例並不難,比如長為7的高度序列「7 5 4 1 6 3 2」, 最長不上升序列為「7 5 4 3 2」,用多次求最長不上升序列的結果為3套系統;但其實只要2套,分別擊落「7 5 4 1」與「6 3 2」。所以不能用「動態規劃」做,那麼,正確的做法又是什麼呢?
我們的目標是用最少的系統擊落所有導彈,至於系統之間怎麼分配導彈數目則無關緊要,上面錯誤的想法正是承襲了「一套系統盡量多攔截導彈」的思維定勢,忽視了最優解中各個系統攔截數較為平均的情況,本質上是一種貪心演算法,但貪心的策略不對。如果從每套系統攔截的導彈方面來想行不通的話,我們就應該換一個思路,從攔截某個導彈所選的系統入手。
題目告訴我們,已有系統目前的瞄準高度必須不低於來犯導彈高度,所以,當已有的系統均無法攔截該導彈時,就不得不啟用新系統。如果已有系統中有一個能攔截該導彈,我們是應該繼續使用它,還是另起爐灶呢?事實是:無論用哪套系統,只要攔截了這枚導彈,那麼系統的瞄準高度就等於導彈高度,這一點對舊的或新的系統都適用。而新系統能攔截的導彈高度最高,即新系統的性能優於任意一套已使用的系統。既然如此,我們當然應該選擇已有的系統。如果已有系統中有多個可以攔截該導彈,究竟選哪一個呢?當前瞄準高度較高的系統的「潛力」較大,而瞄準高度較低的系統則不同,它能打下的導彈別的系統也能打下,它夠不到的導彈卻未必是別的系統所夠不到的。所以,當有多個系統供選擇時,要選瞄準高度最低的使用,當然瞄準高度同時也要大於等於來犯導彈高度。
解題時用一個數組sys記下當前已有系統的各個當前瞄準高度,該數組中實際元素的個數就是第二問的解答。
[參考程序]
program noip1999_2;
const max=1000;
var i,j,current,maxlong,minheight,select,tail,total:longint;
height,longest,sys:array [1..max] of longint;
line:string;
begin
write('Input test data:');
readln(line); {輸入用字元串}
i:=1;
total:=0; {飛來的導彈數}
while i<=length(line) do {分解出若干個數,存儲在height數組中}
begin
while (i<=length(line)) and (line[i]=' ') do i:=i+1; {過濾空格}
current:=0; {記錄一個導彈的高度}
while (i<=length(line)) and (line[i]<>' ') do {將一個字元串變成數}
begin
current:=current*10+ord(line[i])-ord('0');
i:=i+1
end;
total:=total+1;
height[total]:=current {存儲在height中}
end;
longest[1]:=1; {以下用動態規劃求第一問}
for i:=2 to total do
begin
maxlong:=1;
for j:=1 to i-1 do
begin
if height[i]<=height[j]
then if longest[j]+1>maxlong
then maxlong:=longest[j]+1;
longest[i]:=maxlong {以第i個導彈為結束,能攔截的最多導彈數}
end;
end;
maxlong:=longest[1];
for i:=2 to total do
if longest[i]>maxlong then maxlong:=longest[i];
writeln(maxlong); {輸出第一問的結果}
sys[1]:=height[1]; {以下求第二問}
tail:=1; {數組下標,最後也就是所需系統數}
for i:=2 to total do
begin
minheight:=maxint;
for j:=1 to tail do {找一套最適合的系統}
if sys[j]>height[i] then
if sys[j]<minheight then
begin minheight:=sys[j]; select:=j end;
if minheight=maxint {開一套新系統}
then begin tail:=tail+1; sys[tail]:=height[i] end
else sys[select]:=height[i]
end;
writeln(tail)
end.
[部分測試數據]
輸入1:300 250 275 252 200 138 245
輸出1:
5
2
輸入2:181 205 471 782 1033 1058 1111
輸出2:
1
7
輸入3:465 978 486 476 324 575 384 278 214 657 218 445 123
輸出3:
7
4
輸入4:236 865 858 565 545 445 455 656 844 735 638 652 659 714 845
輸出4:
6
7
夠詳細的吧