NOIP複習篇———枚舉
----------------------------------------------------------------------------------------------------------------
高手的切磋不在於難題,而在於SB算法....NOIP來了,決不能犯SB錯誤
---------------------------------------------------------------------------------------------------------------------------------------------------------
1.1 枚舉算法
#define#
對於無從下手的問題,我們往往刻畫其解空間,使得這個解空間有了上限和下限,這樣,我們可以枚舉這個區間值,使得問題得到解答
#優點#
寫起來很容易,構思簡單,答案准確
#缺點#
時間的長夠你等一輩子
#模板#
function findans(int l,int r) 上限l,下限r
{
int ans;
for(ans=l -) r)
if (ans可行)cnt++; 找到一個答案
}
1.1.2 例題
《打地鼠》
題目描述 Description
打地鼠是這樣的一個遊戲:地面上有一些地鼠洞,地鼠們會不時從洞裏探出頭來很短時間後又縮回洞中。玩家的目標是在地鼠伸出頭時,用錘子砸其頭部,砸到的地鼠越多分數也就越高。
遊戲中的錘子每次只能打一隻地鼠,如果多隻地鼠同時探出頭,玩家只能通過多次揮舞錘子的方式打掉所有的地鼠。你認爲這錘子太沒用了,所以你改裝了錘子,增加了錘子與地面的接觸面積,使其每次可以擊打一片區域。如果我們把地面看做m*n的方陣,其每個元素都代表一個地鼠洞,那麼錘子可以覆蓋R*C區域內的所有地鼠洞。但是改裝後的錘子有一個缺點:每次揮舞錘子時,對於這R*C的區域中的所有地洞,錘子會打掉恰好一隻地鼠。也就是說錘子覆蓋的區域中,每個地洞必須至少有1只地鼠,且如果某個地洞中地鼠的個數大於1,那麼這個地洞只會有1只地鼠被打掉,因此每次揮舞錘子時,恰好有R*C只地鼠被打掉。由於錘子的內部結構過於精密,因此在遊戲過程中你不能旋轉錘子(即不能互換R和C)。
你可以任意更改錘子的規格(即你可以任意規定R和C的大小),但是改裝錘子的工作只能在打地鼠前進行(即你不可以打掉一部分地鼠後,再改變錘子的規格)。你的任務是求出要想打掉所有的地鼠,至少需要揮舞錘子的次數。
Hint:由於你可以把錘子的大小設置爲1*1,因此本題總是有解的。
輸入描述 Input Description
第一行包含兩個正整數m和n;
下面m行每行n個正整數描述地圖,每個數字表示相應位置的地洞中地鼠的數量。
數據範圍及提示 Data Size & Hint
使用2*2的錘子,分別在左上、左下、右上、右下揮舞一次。
對於30%的數據,m,n≤5;
對於60%的數據,m,n≤30;
對於100%的數據,m,n≤100,其他數據不小於0,不大於105。
#分析#
題目的意思是要我們找出最適合的錘子的大小,使得每次打擊都不會打空或者打出界且打完所有地鼠所需要的次數最小。
我們發現,如果求打擊次數,很明顯就是sum/(r*c) 這裏sum表示所有地鼠個數和,r和c分別表示改裝後的錘子的長和寬,那麼我們只需要枚舉r和c即可,但是,在枚舉的時候,要注意不能打空
#代碼#
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=101;
-
-
int g[MaxN][MaxN],allsum=0;
-
int n,m,r,c,ans=0x7fffffff;
-
int tmp[MaxN][MaxN];
-
-
bool ok(){
-
int i,j,i1,j1;
-
for(i=1;i<=n;i++)
-
for(j=1;j<=m;j++)
-
tmp[i][j]=g[i][j];
-
for(i=1;i<=n;i++)
-
for(j=1;j<=m;j++)
-
if(tmp[i][j]){
-
if(i+r<=n+1 && j+c<=m+1){
-
int delta=tmp[i][j];
-
for(i1=0;i1<r;i1++)
-
for(j1=0;j1<c;j1++)
-
tmp[i+i1][j+j1]-=delta;
-
if(tmp[i1+i][j1+j]<0)return false;
-
}
-
else return false;
-
}
-
return true;
-
}
-
-
int main(){
-
scanf("%d%d",&n,&m);
-
for(r=1;r<=n;r++)
-
for(c=1;c<=m;c++)
-
{
-
scanf("%d",&g[r][c]);
-
allsum+=g[r][c];
-
}
-
for(r=1;r<=n;r++)
-
for(c=1;c<=m;c++)
-
{
-
if(allsum%(r*c)==0 && allsum/(r*c)<ans && ok()==true)
-
ans=allsum/(r*c);
-
}
-
cout<<ans;
-
return 0;
-
}
-
《Jam的計數法》
題目描述 Description
Jam是個喜歡標新立異的科學怪人。他不使用阿拉伯數字計數,而是使用小寫英文字母計數,他覺得這樣做,會使世界更加豐富多彩。在他的計數法中,每個數字的位數都是相同的(使用相同個數的字母),英文字母按原先的順序,排在前面的字母小於排在它後面的字母。我們把這樣的“數字”稱爲Jam數字。在Jam數字中,每個字母互不相同,而且從左到右是嚴格遞增的。每次,Jam還指定使用字母的範圍,例如,從2到10,表示只能使用{b,c,d,e,f,g,h,i,j}這些字母。如果再規定位數爲5,那麼,緊接在Jam數字“bdfij”之後的數字應該是“bdghi”。(如果我們用U、V依次表示Jam數字“bdfij”與“bdghi”,則U<V<
span>,且不存在Jam數字P,使U<P<V<
span>)。你的任務是:對於從文件讀入的一個Jam數字,按順序輸出緊接在後面的5個Jam數字,如果後面沒有那麼多Jam數字,那麼有幾個就輸出幾個。
輸入描述 Input Description
有2行,第1行爲3個正整數,用一個空格隔開:
s t w
(其中s爲所使用的最小的字母的序號,t爲所使用的最大的字母的序號。w爲數字的位數,這3個數滿足:1≤s<T<
span>≤26, 2≤w≤t-s )
第2行爲具有w個小寫字母的字符串,爲一個符合要求的Jam數字。
所給的數據都是正確的,不必驗證。
輸出描述 Output Description
最多爲5行,爲緊接在輸入的Jam數字後面的5個Jam數字,如果後面沒有那麼多Jam數字,那麼有幾個就輸出幾個。每行只輸出一個Jam數字,是由w個小寫字母組成的字符串,不要有多餘的空格
樣例輸出 Sample Output
bdghi
bdghj
bdgij
bdhij
befgh
#分析#
題目的大意是給出一串字符,然後求出它的後續排列當且僅當不能有字符越界
我們觀察題目,發現題目就是要我們枚舉一個需要改變的位置,這個位置自然就是t+96+j-w+1
然後對於後幾位處理:str[k]=str[k-1]+1
不要忘記如果找不到相應位置時,則沒有Jam數字了
#代碼#
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=300;
-
-
char str[MaxN];
-
int s,t,w,l;
-
-
int main(){
-
int i;
-
cin>>s>>t>>w;
-
scanf("%s",str);
-
l=strlen(str)-1;
-
for(i=1;i<=5;i++){
-
int j=l;
-
while(j>=0 && str[j]==t+97+j-w)j--;
-
if(j==-1)break;
-
str[j]++;
-
for(int k=j+1;k<=w-1;k++)str[k]=str[k-1]+1;
-
cout<<str<<endl;
-
}
-
return 0;
-
}
《合數和》
題目描述 Description
用戶輸入一個數,然後輸出從1開始一直到這個數爲止(包括這個數)中所有的合數的和。
#分析#
題意:找出n以內的合數,並累加起來,輸出【感覺比較easy...
至於找合數,我們有2種方法:
1)從1~n枚舉,對於每一個數用根號n的時間複雜度判斷是否爲合數,時間複雜度O(nlogn),有可能會TLE,我們還有更好的方法:
2)先用O(n)的篩法篩除n以內的質數,剩餘的就是合數,然後累加,時間複雜度O(2n)
#代碼#
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=1001;
-
-
int p[MaxN],sum,n;
-
-
int getans(){
-
int i,j;
-
memset(p,1,sizeof(p));
-
for(i=2;i<=n;i++)
-
if(p[i])
-
for(j=i*i;j<=n;j+=i)
-
p[j]=0;
-
for(i=2;i<=n;i++)
-
if(!p[i])sum+=i;
-
return sum;
-
}
-
-
int main(){
-
scanf("%d",&n);
-
cout<<getans();
-
return 0;
-
}
1.3 枚舉算法的反思
只有不斷反思才能得到進步,枚舉算法着實好,可惜,時間複雜度過於高,但我們可以優化,不過說實在的,枚舉的優化也確實多。
優化1:二分答案
對於某些題目,它的答案是具有單調性的,所以可以用類似二分查找的方式查找答案,再判斷是否可行,時間複雜度從O(N)降至O(logN),詳情參考《奇怪的函數》
優化2:離散化
對於不必要枚舉的狀態或者解空間,我們可以不必算它,所以在一些題目中,可以離散掉不需要的內容,然後剩下的再進行枚舉,詳情參考《最佳室溫》和《海報》
優化3:搜索
搜索也是對解空間的刻畫,只不過將解空間用解空間樹聯繫起來,然後得到解的有序性,去除了一些不必要的狀態,但是,往往也是會TLE的,搜索同樣也有優化,這在後面的搜索複習中會提到(形如:記憶化搜索,BFS,DFS,PFS)
優化4:動態規劃
對於解空間的處理,我們得到小問題,再不斷將小問題合併,得出原問題的解,這是動態規劃的長處,空間換時間
以得到問題的求解
優化5:數據結構
對於查找類的題目,沒有必要去一一枚舉,轉換,只需要用數據結構來優化浪費的查找過程即可,主要體現在堆上面,詳情參考《dijkstra算法》《prim算法》《合併果子》
----------------------------------------------------------------------------------------------------------------------------------------------------
NOIP複習篇———貪心
-------------------------------------------------------------------------------------------------------------------
高手的切磋不在於難題,而在於SB算法....NOIP來了,決不能犯SB錯誤
---------------------------------------------------------------------------------------------------------------------------------------------------------
1.1 貪心算法
#定義#
對於某個問題,不從宏觀去考慮問題,而是從微觀考慮問題,儘管大多數時候是不奏效的,但也有時候很有效。
#講解#
貪心算法,是指人們爲了解決問題而下意識的爲自己的最大利益而設計的方案,往往題目中會出現“最小”“最大”等關鍵詞,而貪心算法的最大難度莫過於證明貪心和想出貪心策略,對於NOIP普及組的難度,不會考察這點..舉個簡單的例子,人們在穿越草地時往往選擇直線穿過(兩點之間,線段最短),這便是貪心算法。
而貪心算法擁有的一個重要特徵就是——最優子結構
#模板#
function greedy(){
int a[]={問題的子問題};
sort(a+1,a+n+1);
for(i=1;i<=n;i++)
if(a[i]可行)直接記錄,並退出
}
1.2 例題
1.《旅行家的預算》
題目描述 Description
一個旅行家想駕駛汽車以最少的費用從一個城市到另一個城市(假設出發時油箱是空的)。給定兩個城市之間的距離D1、汽車油箱的容量C(以升爲單位)、每升汽油能行駛的距離D2、出發點每升汽油價格P和沿途油站數N(N可以爲零),油站i離出發點的距離Di、每升汽油價格Pi(i=1,2,……N)。計算結果四捨五入至小數點後兩位。如果無法到達目的地,則輸出“No Solution”。
輸入描述 Input Description
第一行D1 C D2 P N
之後N行,每行2個數表示離出發點的距離Di和每升汽油的價格Pi
樣例輸入 Sample Input
275.6 11.9 27.4 2.8 2
102.0 2.9
220.0 2.2
#分析#
這道題目貪心的味道很濃,或者說太濃了,我們希望花費最小,而又給定了加油站的位置,所以選擇權在我們手中,則排除搜索和動態規劃的可能性。
那麼,怎麼貪呢?我們假設自己是那個旅行家,不要忘記了,貪心是從局部考慮,而不是整體! 所以,我們只要考慮當前所在的加油站的決策即可,我們發現:
(1)如果當前加油站有可以到達的油站,我們自然選擇距離最小的(當且僅當當前加油站的油價>可以到達的加油站的油價)
(2)如果當前加油站有可以到達的油站但沒有一個油價較低,我們不妨在這一站加滿油,開往下一站
(3)如果當前加油站沒有可以到達的加油站,則無法到達,輸出"No Soultion"
時間複雜度O(N^2)
#代碼#
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=101;
-
-
int n;
-
double d1,c,go,price_start;
-
double now,ans,minway,d;
-
-
struct oil{
-
double dis,price;
-
friend bool operator< (oil a,oil b){return a.price<b.price;}
-
}s[MaxN];
-
-
int main(){
-
int i,k,p;
-
cin>>d1>>c>>go>>price_start>>n;
-
for(i=1;i<=n;i++)cin>>s[i].dis>>s[i].price;
-
sort(s+1,s+n+1);
-
s[k=0].dis=0;
-
s[k].price=price_start;
-
s[n+1].dis=d1;
-
s[n+1].price=0;
-
while(now<d1){
-
minway=(double)(0x7fffffff);
-
p=-1;
-
for(i=1;i<=n+1;i++)
-
if(i!=k && s[k].price>s[i].price && c*go+s[k].dis>=s[i].dis && minway>(s[i].dis-s[k].dis) && s[i].dis>s[k].dis){
-
minway=s[i].dis-s[k].dis;
-
p=i;
-
}
-
if(p!=-1){
-
ans+=((s[p].dis-s[k].dis)/go-d)*s[k].price;
-
now=s[p].dis;
-
k=p;
-
}
-
else {
-
if(((s[k+1].dis-s[k].dis)/go)>c){cout<<"No Solution";return 0;}
-
ans+=(c-d)*s[k].price;
-
d=c-(s[k+1].dis-s[k].dis)/go;
-
now=s[k+1].dis;
-
k++;
-
}
-
}
-
printf("%.2lf",ans);
-
return 0;
-
}
-
2.《線段覆蓋三步曲第一步——線段覆蓋1》
題目描述 Description
給定x軸上的N(0<N<100)條線段,每個線段由它的二個端點a_I和b_I確定,I=1,2,……N.這些座標都是區間(-999,999)的整數。有些線段之間會相互交疊或覆蓋。請你編寫一個程序,從給出的線段中去掉儘量少的線段,使得剩下的線段兩兩之間沒有內部公共點。所謂的內部公共點是指一個點同時屬於兩條線段且至少在其中一條線段的內部(即除去端點的部分)。
輸入描述 Input Description
輸入第一行是一個整數N。接下來有N行,每行有二個空格隔開的整數,表示一條線段的二個端點的座標。
#分析#
同樣是一道貪心味道很濃的一題(想一想爲什麼?tip:看定義),那麼,怎麼貪呢?
我們挖掘一下題目的最優子結構,首先,從貪心思想(只看局部)我們可以知道,只需要考慮每條線段就行了
那麼,我們設MAX表示當前最長不重疊線段的右端點,很明顯初始時MAX=-oo
這裏要注意一點,要讓線段按右端點排序,再處理,不然就不滿足最優子結構了
#代碼#
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=1000001;
-
-
struct line{
-
int L,R;
-
friend bool operator< (line a,line b){return a.R<b.R;}
-
}a[MaxN];
-
-
int ans,n,i,MAX;
-
-
int main(){
-
freopen("line.in","r",stdin);
-
freopen("line.out","w",stdout);
-
scanf("%d",&n);
-
for(i=1;i<=n;i++){
-
scanf("%d %d",&a[i].L,&a[i].R);
-
if(a[i].L>a[i].R)swap(a[i].L,a[i].R);
-
}
-
sort(a+1,a+n+1);
-
MAX=-(1<<30);
-
for(i=1;i<=n;i++){
-
if(MAX<=a[i].L){
-
ans++;
-
MAX=a[i].R;
-
}
-
}
-
printf("%d",ans);
-
return 0;
-
}
3.《線段覆蓋三步曲第三步——線段覆蓋3》
題目描述 Description
在一個數軸上有n條線段,現要選取其中k條線段使得這k條線段兩兩沒有重合部分(端點可以重合),問最大的k爲多少。
輸入描述 Input Description
輸入格式
輸入文件的第1行爲一個正整數n,下面n行每行2個數字ai,bi,描述每條線段。
數據範圍及提示 Data Size & Hint
數據範圍
對於20%的數據,n≤10;
對於50%的數據,n≤1000;
對於70%的數據,n≤100000;
對於100%的數據,n≤1000000,0≤ai<bi≤1000000。
#分析#
又是一道貪心題,同例2一樣,只不過輸出ans+1即可
【讀者可能好奇爲什麼直接《線段覆蓋3》,因爲《線段覆蓋2》是接下來的動態規劃複習專題將提到的
#代碼#
略
4.《地鼠遊戲》
題目描述 Description
王鋼是一名學習成績優異的學生,在平時的學習中,他總能利用一切時間認真高效地學習,他不但學習刻苦,而且善於經常總結、完善自己的學習方法,所以他總能在每次考試中得到優異的分數,這一切很大程度上是由於他是一個追求效率的人。
但王鋼也是一個喜歡玩的人,平時在學校學習他努力剋制自己玩,可在星期天他卻會抽一定的時間讓自己玩一下,他的爸爸媽媽也比較信任他的學習能力和學習習慣,所以在星期天也不會象其他家長一樣對他抓緊,而是允許他在星期天上午可以自由支配時間。
地鼠遊戲是一項需要反應速度和敏捷判斷力的遊戲。遊戲開始時,會在地板上一下子冒出很多地鼠來,然後等你用榔頭去敲擊這些地鼠,每個地鼠被敲擊後,將會增加相應的遊戲分值。問題是這些地鼠不會傻傻地等你去敲擊,它總會在冒出一會時間後又鑽到地板下面去(而且再也不上來),每個地鼠冒出後停留的時間可能是不同的,而且每個地鼠被敲擊後增加的遊戲分值也可能是不同,爲了勝出,遊戲參與者就必須根據每個地鼠的特性,有選擇地儘快敲擊一些地鼠,使得總的得分最大。
這個極具挑戰性的遊戲王鋼特別喜歡,最近他經常在星期天上午玩這個遊戲,慢慢地他不但敲擊速度越來越快(敲擊每個地鼠所需要的耗時是1秒),而且他還發現了遊戲的一些特徵,那就是每次遊戲重新開始後,某個地鼠冒出來後停留的時間都是固定的,而且他記錄了每個地鼠被敲擊後將會增加的分值。於是,他在每次遊戲開始後總能有次序地選擇敲擊不同的地鼠,保證每次得到最大的總分值。
輸入描述 Input Description
輸入包含3行,第一行包含一個整數n(1<=n<=100)表示有n個地鼠從地上冒出來,第二行n個用空格分隔的整數表示每個地鼠冒出後停留的時間,第三行n個用空格分隔的整數表示每個地鼠被敲擊後會增加的分值(<=100)。每行中第i個數都表示第i個地鼠的信息。
輸出描述 Output Description
輸出只有一行一個整數,表示王鋼所能獲得的最大遊戲總分值。
#分析#
貪心味道很濃【感覺我逗比噠。。。】 題目讓我們求分數最大,由於時間都是需要1s的,所以我們對結構體S排序(按時間從小到大),然後枚舉時間,同時把同一時間的數統統放入堆中,每一秒,我們累加堆頂,堆可以用優先隊列實現。
#代碼#
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=10001;
-
-
struct mice{
-
int time,score;
-
friend bool operator< (mice a,mice b){return a.time<b.time;}
-
}g[MaxN];
-
-
priority_queue<int > q;
-
-
int n,ans=0;
-
-
int main(){
-
freopen("mouse.in","r",stdin);
-
freopen("mouse.out","w",stdout);
-
int i,j,MAX=0;
-
cin>>n;
-
for(i=1;i<=n;i++){cin>>g[i].time;MAX=max(MAX,g[i].time);}
-
for(i=1;i<=n;i++)cin>>g[i].score;
-
sort(g+1,g+n+1);
-
j=n;
-
for(i=MAX;i>=1;i--)
-
{
-
while(j>0 && g[j].time==i){q.push(g[j].score);j--;}
-
if(!q.empty()){ans+=q.top();q.pop();}
-
}
-
cout<<ans;
-
return 0;
-
}
1.3 反思
可以說,貪心的覆蓋範圍很廣,不是一個晚上就能複習完的,不過,要做到舉一反三,從而觸類旁通,弄懂貪心算法的整體,說到底,還是8個字“考慮局部,無視整體”
NOIP複習篇———動態規劃
-------------------------------------------------------------------------------------------------------------------
高手的切磋不在於難題,而在於SB算法....NOIP來了,決不能犯SB錯誤
---------------------------------------------------------------------------------------------------------------------------------------------------------
1.1 動態規劃
#定義#
動態規劃屬於運籌學分類的一種,是解決多階段決策問題的一種手段,其內部結構和搜索很相似:
動態規劃:由已知推到未知
搜索:由未知推到已知
不同的方法在實現上可能會有時間上的大量差別,動態規劃枚舉子問題求最優值,搜索枚舉決策
#技巧#
解決動態規劃問題是每個OIer必備的基礎,作爲只能應付PJ的我也只是略懂...
動態規劃問題求解步驟:
1)對問題進行分析,通過反證法證明問題具有最優子結構和無後效性
2)找到問題的階段
3)根據階段,找到描述階段的量,這個量就組成了狀態
4)思考每一個問題的由來,例如fibnacio數列的每個子問題的關係是:f[n]=f[n-1]+f[n-2],這步就是動態轉移方程
5)實現代碼,有幾個狀態枚舉幾層,逐個描述,然後主體就是動態轉移方程
6)對於無從下手的動態規劃問題,可以先用搜索的理解來找出轉移方程,實在無奈,果斷選擇記憶化搜索
7)對於空間過大的動態規劃,我們可以利用《滾動數組》《離散化》等手段來優化
8)尋找問題的初始階段,如果fibnacio就是f(0)=f(1)=1
#動態規劃的分類#
動態規劃分爲以下幾類:
【1】序列型動態規劃
【2】棋盤型動態規劃
【3】區間型動態規劃
【4】劃分型動態規劃
【5】揹包型動態規劃
【6】狀態壓縮型動態規劃
【7】環形動態規劃
【8】樹形動態規劃
對於PJ的難度,只需要考察【1】【2】【3】【4】【5】【7】
1.2 例題
1.2.1 序列型動態規劃例題——《攔截導彈》(NOIP1999提高組)
題目描述 Description
某國爲了防禦敵國的導彈襲擊,發展出一種導彈攔截系統。但是這種導彈攔截系統有一個缺陷:雖然它的第一發炮彈能夠到達任意的高度,但是以後每一發炮彈都不能高於前一發的高度。某天,雷達捕捉到敵國的導彈來襲。由於該系統還在試用階段,所以只有一套系統,因此有可能不能攔截所有的導彈。
輸入描述 Input Description
輸入導彈依次飛來的高度(雷達給出的高度數據是不大於30000的正整數)
輸出描述 Output Description
輸出這套系統最多能攔截多少導彈,如果要攔截所有導彈最少要配備多少套這種導彈攔截系統。
樣例輸入 Sample Input
7
389 207 155 300 299 170 158 65
【分析】
將題目數學建模:導彈抽象爲點,能連續打抽象爲邊
則得到下圖:
那麼很明顯,題目是要我們求最長連續的一段不上升子序列(第一問),第二問就是最長不下降子序列的長度
那麼,如何求最長不下降(上升)子序列呢?
以不下降舉例:
(1)證明最優子結構:
假設:f(n)與f'(n)無關
則f'(n)的任意一個值都不等於f(n)
而一個導彈的高度取決於前一個導彈的高度
所以矛盾
故假設不成立,原命題成立。
顯然一定具有無後效性
(2)階段:已經處理的導彈的數目
(3)狀態:f(i)表示以第i顆導彈爲結尾的最長不下降子序列
(4)顯然,f(i)=max{f(j)}+1 (2<=i<=n,1<=j<i)
(5)時間複雜度爲O(N^2),空間複雜度O(N),貌似沒辦法優化,但實質上是有的!
我們發現,對於每一個階段的值,有些處理是不必要的,而且整個線段是具有單調性的
所以利用單調隊列進行優化
至此,這道題目已經AC了!
-
-
#include<iostream>
-
#include<cstdio>
-
using namespace std;
-
int a[30000];
-
int n=1,i,j,max1;
-
int f[600000];
-
int main()
-
{
-
while(scanf("%d",&a[n++])!=EOF);
-
n--;
-
f[1]=1;
-
for(i=2;i<=n;i++){f[i]=1;
-
for(j=1;j<=i-1;j++)
-
if(f[j]+1>f[i] && a[j]>=a[i])f[i]=f[j]+1;
-
}
-
max1=0;
-
for(i=1;i<=n;i++)if(f[i]>max1)max1=f[i];
-
cout<<max1-1<<endl;
-
f[1]=1;
-
for(i=2;i<=n;i++){f[i]=1;
-
for(j=1;j<=i-1;j++)
-
if(f[j]+1>f[i] && a[j]<a[i])f[i]=f[j]+1;
-
}
-
max1=0;
-
for(i=1;i<=n;i++)if(f[i]>max1)max1=f[i];
-
cout<<max1;
-
-
return 0;
-
}
-
-
#include <iostream>
-
using namespace std;
-
#include <cstdio>
-
-
const int MaxN=100001;
-
-
int n,i,top=0,x,stack[MaxN];
-
-
int main(){
-
cin>>n;
-
stack[top]=-1;
-
for(i=1;i<=n;i++){
-
cin>>x;
-
if(x>stack[top]){stack[++top]=x;}
-
else
-
{
-
int low=0,high=top,mid;
-
while(low<high){
-
mid=(low+high)>>1;
-
if(x>stack[mid])
-
low=mid+1;
-
else
-
high=mid-1;
-
}
-
stack[low]=x;
-
}
-
}
-
cout<<top;
-
return 0;
-
}
1.2.1 序列型動態規劃例題——《線段覆蓋2》(線段覆蓋三部曲第二部)
題目描述 Description
數軸上有n條線段,線段的兩端都是整數座標,座標範圍在0~1000000,每條線段有一個價值,請從n條線段中挑出若干條線段,使得這些線段兩兩不覆蓋(端點可以重合)且線段價值之和最大。
n<=1000
輸入描述 Input Description
第一行一個整數n,表示有多少條線段。
接下來n行每行三個整數, ai bi ci,分別代表第i條線段的左端點ai,右端點bi(保證左端點<右端點)和價值ci。
數據範圍及提示 Data Size & Hint
數據範圍
對於40%的數據,n≤10;
對於100%的數據,n≤1000;
0<=ai,bi<=1000000
0<=ci<=1000000
【分析】
同樣,可以講線段映射到數軸上(按右端點排序)【算是一種離散化吧】然後進行DP
(1)階段:線段的條數
(2)狀態:f(i)表示前i條線段中任意選擇線段且必須選擇第i條線段的最大價值
(3)決策:f(i)=max{f(j)+val(i)}
到了這裏,題目已經解決了。
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=1001;
-
-
struct line{
-
int L,R,val;
-
friend bool operator< (line a,line b){return a.R<b.R;}
-
}a[MaxN];
-
-
int f[MaxN],n,i,j;
-
-
int main(){
-
scanf("%d",&n);
-
for(i=1;i<=n;i++)
-
scanf("%d%d%d",&a[i].L,&a[i].R,&a[i].val);
-
sort(a+1,a+n+1);
-
for(i=1;i<=n;i++)f[i]=a[i].val;
-
for(i=2;i<=n;i++){
-
for(j=1;j<i;j++)
-
if(a[j].R<=a[i].L && f[j]+a[i].val>f[i])f[i]=a[i].val+f[j];
-
if(f[i]>f[0])f[0]=f[i];
-
}
-
cout<<f[0];
-
return 0;
-
}
1.2.2 區間型動態規劃例題——《石子合併》
題目描述 Description
有n堆石子排成一列,每堆石子有一個重量w[i], 每次合併可以合併相鄰的兩堆石子,一次合併的代價爲兩堆石子的重量和w[i]+w[i+1]。問安排怎樣的合併順序,能夠使得總合並代價達到最小。
輸入描述 Input Description
第一行一個整數n(n<=100)
第二行n個整數w1,w2...wn (wi <= 100)
【分析】
(1)證明最優子結構略
(2)階段:區間
(3)狀態:f(i,j)表示[i,j]這個區間的最大值
(4)決策:f[i][j]=min{f[i][j],f[i][k]+f[k+1][j]+sum[i][j]
-
#include <iostream>
-
using namespace std;
-
#include <cstdio>
-
const int MaxN=101;
-
int n,s[MaxN],f[MaxN][MaxN];
-
int i,j,k,r;
-
int main(){
-
cin>>n;
-
for(i=1;i<=n;i++){
-
cin>>s[i];
-
s[i]+=s[i-1];
-
}
-
for(r=2;r<=n;r++)
-
for(i=1;i<=n-r+1;i++){
-
j=i+r-1;
-
f[i][j]=0x7fffffff;
-
for(k=i;k<j;k++)
-
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
-
}
-
cout<<f[1][n];
-
return 0;
-
}
1.2.2 區間型動態規劃例題——《能量項鍊》(NOIP2006提高組)
題目描述 Description
在Mars星球上,每個Mars人都隨身佩帶着一串能量項鍊。在項鍊上有N顆能量珠。能量珠是一顆有頭標記與尾標記的珠子,這些標記對應着某個正整數。並且,對於相鄰的兩顆珠子,前一顆珠子的尾標記一定等於後一顆珠子的頭標記。因爲只有這樣,通過吸盤(吸盤是Mars人吸收能量的一種器官)的作用,這兩顆珠子才能聚合成一顆珠子,同時釋放出可以被吸盤吸收的能量。如果前一顆能量珠的頭標記爲m,尾標記爲r,後一顆能量珠的頭標記爲r,尾標記爲n,則聚合後釋放的能量爲m*r*n(Mars單位),新產生的珠子的頭標記爲m,尾標記爲n。
需要時,Mars人就用吸盤夾住相鄰的兩顆珠子,通過聚合得到能量,直到項鍊上只剩下一顆珠子爲止。顯然,不同的聚合順序得到的總能量是不同的,請你設計一個聚合順序,使一串項鍊釋放出的總能量最大。
例如:設N=4,4顆珠子的頭標記與尾標記依次爲(2,3)
(3,5) (5,10)
(10,2)。我們用記號⊕表示兩顆珠子的聚合操作,(j⊕k)表示第j,k兩顆珠子聚合後所釋放的能量。則第4、1兩顆珠子聚合後釋放的能量爲:
(4⊕1)=10*2*3=60。
這一串項鍊可以得到最優值的一個聚合順序所釋放的總能量爲
((4⊕1)⊕2)⊕3)=10*2*3+10*3*5+10*5*10=710。
輸入描述 Input Description
第一行是一個正整數N(4≤N≤100),表示項鍊上珠子的個數。第二行是N個用空格隔開的正整數,所有的數均不超過1000。第i個數爲第i顆珠子的頭標記(1≤i≤N),當i<N<
span>時,第i顆珠子的尾標記應該等於第i+1顆珠子的頭標記。第N顆珠子的尾標記應該等於第1顆珠子的頭標記。
至於珠子的順序,你可以這樣確定:將項鍊放到桌面上,不要出現交叉,隨意指定第一顆珠子,然後按順時針方向確定其他珠子的順序。
輸出描述 Output Description
只有一行,是一個正整數E(E≤2.1*109),爲一個最優聚合順序所釋放的總能量。
【分析】
對於環形的題目,我們往往採用的方法是拉兩倍,然後div 2 再找最小f[i][i+n-1]
(1)階段:區間以及劃分
(2)狀態:f(i,j,k)表示[i,j]這個區間的最大值
(3)對於環形問題,最終的答案在n的範圍內尋找
for(i=1;i<=n;i++)
MAX=max(MAX,f[i][i+n-1][m]);
當然,這題同時涉足了兩個領域:區間&環形
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=205;
-
-
int f[MaxN][MaxN];
-
int a[MaxN];
-
int n;
-
-
int main(){
-
int i,j,k,r;
-
cin>>n;
-
for(i=1;i<=n;i++)
-
{
-
cin>>a[i];
-
a[i+n]=a[i];
-
}
-
for(j=2;j<(n<<1);j++)
-
for(i=j-1;i>0 && j-i<n;i--)
-
for(k=i;k<j;k++)
-
f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+a[i]*a[k+1]*a[j+1]);
-
int MAX=0;
-
for(i=1;i<=n;i++)
-
MAX=max(MAX,f[i][i+n-1]);
-
cout<<MAX;
-
return 0;
-
}
1.2.3 劃分型動態規劃例題——《乘積最大》(NOIP2000普及組)
題目描述 Description
今年是國際數學聯盟確定的“2000——世界數學年”,又恰逢我國著名數學家華羅庚先生誕辰90週年。在華羅庚先生的家鄉江蘇金壇,組織了一場別開生面的數學智力競賽的活動,你的一個好朋友XZ也有幸得以參加。活動中,主持人給所有參加活動的選手出了這樣一道題目:
設有一個長度爲N的數字串,要求選手使用K個乘號將它分成K+1個部分,找出一種分法,使得這K+1個部分的乘積能夠爲最大。
同時,爲了幫助選手能夠正確理解題意,主持人還舉了如下的一個例子:
有一個數字串:312, 當N=3,K=1時會有以下兩種分法:
1) 3*12=36
2) 31*2=62
這時,符合題目要求的結果是:31*2=62
現在,請你幫助你的好朋友XZ設計一個程序,求得正確的答案。
輸入描述 Input Description
程序的輸入共有兩行:
第一行共有2個自然數N,K(6≤N≤40,1≤K≤6)
第二行是一個長度爲N的數字串。
輸出描述 Output Description
結果顯示在屏幕上,相對於輸入,應輸出所求得的最大乘積(一個自然數)。
【分析】
對於劃分型動態規劃,往往考慮“定範圍,取特殊”
(1)階段:劃分
(2)狀態:f(i,j)表示前i段字符劃分爲j塊所能得到的最大乘積
(3)決策:f[i][j]=max(f[i][j],f[k][j-1]*merge(k+1,i));
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
typedef long long big;
-
-
string s;
-
int n,k;
-
big f[41][7];
-
-
big merge(int l,int r){
-
big num=0;
-
for(;l<=r;l++)num=num*10+s[l]-'0';
-
return num;
-
}
-
-
int main(){
-
cin>>n>>k;
-
cin>>s;
-
int i,j,ak;
-
for(i=0;i<n;i++)f[i][0]=merge(0,i);
-
for(i=1;i<n;i++)
-
for(j=1;j<=k;j++)
-
for(ak=0;ak<i;ak++)
-
f[i][j]=max(f[i][j],f[ak][j-1]*merge(ak+1,i));
-
cout<<f[n-1][k];
-
return 0;
-
}
1.2.3 劃分型動態規劃例題——《數的劃分》(NOIP2001提高組)
題目描述 Description
將整數n分成k份,且每份不能爲空,任意兩種劃分方案不能相同(不考慮順序)。
例如:n=7,k=3,下面三種劃分方案被認爲是相同的。
1 1 5
1 5 1
5 1 1
問有多少種不同的分法。
輸入描述 Input Description
輸入:n,k (6<n<=200,2<=k<=6)
數據範圍及提示 Data Size & Hint
{四種分法爲:1,1,5;1,2,4;1,3,3;2,2,3;}
【分析】
同樣,劃分型動態規劃:
(1)階段:劃分
(2)狀態:f(i,j)表示數字i被劃分爲j個數的和的方案數
(3)決策:f[i][j]=f[i-j][j-1]+f[i-1][j-1] (i>=j)
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=210;
-
const int MaxM=7;
-
-
int f[MaxN][MaxM];
-
int n,m;
-
-
int main(){
-
cin>>n>>m;
-
int i,j;
-
f[0][0]=1;
-
for(i=1;i<=n;i++)
-
for(j=1;j<=m;j++)
-
if(i>=j)f[i][j]+=f[i-j][j]+f[i-1][j-1];
-
cout<<f[n][m];
-
return 0;
-
}
1.2.3 劃分型動態規劃例題——《數字遊戲》(NOIP2003普及組)
題目描述 Description
丁丁最近沉迷於一個數字遊戲之中。這個遊戲看似簡單,但丁丁在研究了許多天之後卻發覺原來在簡單的規則下想要贏得這個遊戲並不那麼容易。遊戲是這樣的,在你面前有一圈整數(一共n個),你要按順序將其分爲m個部分,各部分內的數字相加,相加所得的m個結果對10取模後再相乘,最終得到一個數k。遊戲的要求是使你所得的k最大或者最小。
例如,對於下面這圈數字(n=4,m=2):
2
4 -1
3
當要求最小值時,((2-1) mod 10)×((4+3) mod 10)=1×7=7,要求最大值時,爲((2+4+3) mod 10)×(-1 mod 10)=9×9=81。特別值得注意的是,無論是負數還是正數,對10取模的結果均爲非負值。
丁丁請你編寫程序幫他贏得這個遊戲。
輸入描述 Input Description
輸入文件第一行有兩個整數,n(1≤n≤50)和m(1≤m≤9)。以下n行每行有個整數,其絕對值不大於104,按順序給出圈中的數字,首尾相接。
輸出描述 Output Description
輸出文件有兩行,各包含一個非負整數。第一行是你程序得到的最小值,第二行是最大值。
【分析】
又是一道環形DP套劃分型DP套區間DP的題目....
《拉兩倍!》
(1)階段:劃分
(2)狀態:f(i,j,k)表示將[i,j]這個區間劃分爲k塊所能得到的最大值
(3)由於要求最大和最小,所以跑兩遍DP即可
-
#include <iostream>
-
#include <algorithm>
-
#include <cstring>
-
#include <cstdio>
-
#include <cstdlib>
-
#include <vector>
-
#include <queue>
-
#include <list>
-
#include <deque>
-
#include <string>
-
using namespace std;
-
-
const int MaxN=51;
-
const int MaxM=10;
-
const int oo=30001;
-
-
int n,m,a[MaxN*2];
-
int fmax[MaxN*2][MaxN*2][MaxM];
-
int fmin[MaxN*2][MaxN*2][MaxM];
-
int p,sum[MaxN*2][MaxN*2];
-
int MAX=-oo,MIN=oo;
-
-
int main(){
-
cin>>n>>m;
-
int i,j,k;
-
for(i=1;i<=n;i++){
-
cin>>a[i];
-
a[n+i]=a[i];
-
}
-
n<<=1;
-
for(i=1;i<=n;i++)
-
for(j=i;j<=n;j++){
-
p=0;
-
for(k=i;k<=j;k++)
-
p+=a[k];
-
p=p%10;
-
if(p<0)p+=10;
-
sum[i][j]=p;
-
}
-
for(i=1;i<=n;i++)
-
for(j=1;j<=n;j++)
-
for(k=1;k<=m;k++)
-
{
-
fmax[i][j][k]=-oo;
-
fmin[i][j][k]=oo;
-
}
-
for(i=1;i<=n;i++)
-
for(j=i;j<=n;j++){
-
fmax[i][j][1]=sum[i][j];
-
fmin[i][j][1]=sum[i][j];
-
}
-
for(i=1;i<=n;i++)
-
for(j=i;j<=n;j++)
-
for(k=2;k<=m;k++)
-
for(p=j-1;p>=i;p--)
-
{
-
fmin[i][j][k]=min(fmin[i][j][k],fmin[i][p][k-1]*sum[p+1][j]);
-
fmax[i][j][k]=max(fmax[i][j][k],fmax[i][p][k-1]*sum[p+1][j]);
-
}
-
n>>=1;
-
for(i=1;i<=n;i++)
-
{
-
MAX=max(MAX,fmax[i][i+n-1][m]);
-
MIN=min(MIN,fmin[i][i+n-1][m]);
-
}
-
cout<<MIN<<endl<<MAX;
-
return 0;
-
}
-