算法是一組(有限個)規則,它為某個特定問題提供了解決問題的運算序列;也就是計算機解題的過程,在這個過程中,無論是形成解題思路還是編寫程序, 都是在實施某種算法;前者是推理實現的算法,后者是操作實現的算法。下面就常用的一些算法來進行分析。
計算機解題的核心是算法沒汁。一個算法應該具有以下五個重要特征:(1)有窮性:一個算法必須保證執行有限步之后結束;(2)確切性:算法的每一步驟必須確切定義;(3)輸入:一個算法有0個或多個輸入,所謂0個輸入是指算法本身定出初始條件;(4)輸出: 一個算法有一個或多個輸出,以反映對輸人數據加工后的結果,沒有輸出的算法是毫無意義的;(5)能行性:算法原則上能夠精確地運行,做有限次即可完成。
常見的基本算法:
1)遞推法,遞推法實際上是一種遞推關系,就是為了得到問題的解,把它推到比原問題簡單的問題求解,可分為順推法和倒推法。
i.順推法,就是先找到遞推關系式,然后從初始條件出發,一步步地按遞推關系式遞推,直至求出最終結果。
例:求斐波那契數列第10項。
已知斐波那契數列第1項為1,第2項也為2,遞推關系是當前項等于前2項之和。F[n]=f[n-1]+f[n-2] (n≥3)。
F[1]:=1;
F[2]:=2
for m:=3 to 10 do
f[m]:=f[m-1]+f[m-2];
writeln(f[10]);
ii.倒推法,就是在不知道初始條件的情況下,經某種遞推關系而獲知問題的解,再倒過來,推知它的初始條件。
例:一輛重型卡車想穿過800公里的沙漠,已知卡車每公里耗油1升,卡車總載油能力為400升,顯然卡車裝滿一次油是無法穿越沙漠的,因此卡車司機需要在沿途建立幾個儲油點,使卡車能夠順利穿越沙漠。試問司機需要沿途建立幾個儲油點?每個儲油點存儲多少汽油,才能讓卡車以消耗最少的汽油穿越沙漠。
用倒推法來解決這個問題,從終點向起點倒推,逐一求出每個貯油點的位置及存油量。為了消耗最少的汽油,最后一個儲油點應該離終點400公里,且此處儲油400升,即儲油點m=1處離終點dis[1]=400公里,儲油oil[1]=400升;為了在m=1處儲400升汽油,卡車最少從儲油點m=2處開兩趟載滿油的車到儲油點m=1處,則儲油點m=2處儲油oil[2]=400*2=800升,離終點dis[2]=dis[1]+400/3公里(儲油點m=2處到儲油點m=1處開兩趟需要開三次路程);為了在儲油點m=2處儲800升汽油,卡車最少從儲油點m=3處開兩趟載滿油的車到儲油點m=2處,則儲油點m=3處儲油oil[3]=400*3=1200升,離終點dis[3]=dis[2]+400/5公里(儲油點m=3處到儲油點m=2處開三趟需要開五次路程);依次類推,為了在m=k處儲oil[k]=k*400升汽油,卡車最少從m=k+1處開k趟滿載車至m=k處,則儲油點m=k+1處儲油oil[k+1]=(k+1)*400,從m=k+1處至m=k處兩地往返2k+1次路程,用總耗油最省方法要求400升,則儲油點離終點dis[k+1]=dis[k]+400/(2*k+1),最后,反推至m=n至起點距離為800-dis[n],儲油為n*400升,為了在m=n處儲n*400升汽油,卡車最少從起點開n+1次滿載車至m=n處,需要開2*n+1次路程,總耗油量為(800-dis[n])*(2*n+1),即起點儲油為oil[n]+(800-dis[n])*(2*n+1)=n*400+(800-dis[n])*(2*n+1)。
程序段如下:
k:=1;
dis[1]:=400; {從m=1處開始倒推}
oil[1]:=400
repeat
dis[k+1]:=dis[k]+400/(2*k+1);
k:=k+1;
oil[k]:=k*400;
until dis[k]>=800;
dis[k]:=800;
oil[k]:=(k-1)*400+(800-dis[k])*(2*k+1);
for m:=0 to k-1 do
writeln(800-dis[k-m],oil[k-m]);
2)遞歸法,遞歸算法本質上將較復雜的處理歸結為簡單處理,直到最簡單的處理,在處理的遞歸調用過程中,系統用堆棧把每次調用的中間結果保存在棧區,直至求出最簡值,然后返回調用函數,返回過程中,中間結果相繼出棧恢復,它的執行效率相對比較低。
例:求斐波那契數列第10項。
已知斐波那契數列第1項為1,第2項也為2,關系式是當項等于前2項之和,F[n]=f[n-1]+f[n-2] (n≥3)。
function fib(n:integer):integer;
begin
if n=1 then fib:=1
else if n=2 then fib=2
else fib:=fib(n-1)+fib(n-2)
end;
使用時調用fib(10)求第10項的值
3)窮舉法,從是將所有存在的條件都測試一次,求出正確的結果,它的執行效率是最低的。
例:背包問題,有一個背包,可以放Wm公斤物品,現有n件物品,第I件物品的重量是Wi公斤,價值為Vi元(1≤i≤n),現要求在包內放在若干件物品,使包內物品總價值最大。
使用窮舉法,將物品放入包所有可能情況(2n-1)都計算一遍,再考慮背包的最大存放重量Wm,在滿足條件的情況下,計算包內物品的總價值,求解出總價值最大的情況。
使用一個N位二進制的計數器,來表示N個物品放入包的情況,第i位上的0表示第i件沒有放入包,1表示第i件放入包。根據物品情況計算物品的總重量,若沒有超過包的限重,記錄下此情況下的總價值,使用提高門檻法判斷此時情況是否為已測試條件下總價值最大,若是則記錄下此時的總價值,從1測試到2n-1種的情況,此算法的執行效率最低,但一定可以求出最優解。
4)貪心法,從問題的某一個初始解出發逐步逼近給定的目標,以盡可能快的求得更好的解。當達到某算法中的某一步不能再繼續前進時,算法停止。該算法存在問題:i.不能保證求得的最后解是最佳的;ii.不能用來求最大或最小解問題;iii.只能求滿足某些約束條件的可行解的范圍。
用貪心法來求背包問題,為了使包內物品的總價值最大,先計算出所有物品的單位價值,然后從單位價值從高到低依次選擇物品放入包,在不超過限重的情況下盡可能放更多的物品。
例:背包最大限重100公斤,各物品重量、價值如下表:
物品
1
2
3
4
5
6
7
重量(公斤)
35
30
60
50
40
10
25
價值(元)
10
40
30
50
35
40
30
單位價值(元/公斤)
0.29
1.33
0.5
1
0.88
4
1.2
按局部貪心的方法,各物品單位價值從高到低分別6、2、7、4、5、3、1,選取物品6、物品2、物品7、物品1,總重量恰好100公斤,總價值有120元。而事實上這并不是最優解,最優解是物品6、物品2、物品4,總重量雖然為90公斤,但總價值有130元。因此使用局部貪心的方法難以達到全局最優解動態規劃設計方法可以解決,一般采用動態規劃設計方法解決此問題。
5)分治法,對問題分而治之,將原問題一次或者多次分成N個規模較小的而結構與原問題相似的問題,遞歸地解這些子問題,然后合并其結果就得到原問題的解。用分治法求解一個問題,所需的時間是由子問題的個數、大小以及把這個問題分解為子問題所需的工作問題來確定的。當N=2又稱為二分法。
例:對序列A[1]、A[2]…A[n]進行合并排序。
輸入:n,A[1]、A[2] …A[n]
輸出:排序后的A[1]、A[2] …A[n]
合并排序的算法就是二分法,先將N個元素分解成兩個子序列,再用合并排序法對兩個子序列分別排序,如果子序列長度不為1的話,再進行分解成兩個子序列,然后再用合并排序法對兩個子序列分別排序,當其長度為1時遞歸結束,然后將兩個子序列合并,返回遞歸調用,再合并…最后就得到結果。
若序列為(5,3,17,9,4,1,8)進行合并排序的過程為:
6)模擬法,模擬法就是模擬某個過程,通過改變數學模型的各種參數,進而觀察變更這些參數所引起過程狀態的變化。一般題目給定或者隱含某一概率。設計者利用隨機函數和取整函數設定某一范圍的隨機值,將符合概率的隨機值作為參數。然后根據這一模擬的數學模型展開算法。
例:求圓周率,在一個單位正方形有1/4圓弧,隨機產生N個坐標點,統計出現在圓弧內的點的次數M,根據概率,當N足夠大時,出現的次數M/N接近π/4,通過計算M/N*4的值來近似求出π。
假計N為10000次
randomize;
n:=0;
m:=0;
while n<10000 do
begin
x:=random;
y:=random;
if x*x+y*y<=1 then m:=m+1;
n:=n+1;
end
writeln(m/n*4);
7)簡單搜索,搜索算法按搜索的方式分有兩類,一類是深度優先搜索,一類是廣度優先搜索。我們知道,深度搜索編程簡單,程序簡潔易懂,空間需求也比較低,但是這種方法的時間復雜度往往是指數級的,倘若不加優化,其時間效率簡直無法忍受;而廣度優先搜索雖然時間復雜度比前者低一些,但需要龐大的空間需求量。
例:在“走迷宮”的時候,一般回溯法(深度優先搜索)思路是這樣的:
a、這個方向有路可走,我沒走過
b、往這個方向前進
c、是死胡同,往回走,回到上一個路口
d、重復第一步,直到找著出口
按廣度優先搜索思路是:
a、走一步,將下一步所有可走且沒有走過的路口放入隊列
b、從隊列取下一個路口
c、重復第一步,直到找著出口
回溯法很容易實現,但是當迷宮的規模很大時,回溯法的缺點便暴露無遺:搜索耗時極巨。我們可不可以在向某個方向前進時,先一步判斷出這樣走會不會走到死胡同里呢?這樣一來,搜索的時間不就可以減少了嗎?使用剪枝的算法,其實就跟走迷宮避開死胡同差不多。若我們把搜索的過程看成是對一棵樹的遍歷,那么剪枝顧名思義,就是將樹中的一些“死胡同”,不能到達我們需要的解的枝條“剪”掉,以減少搜索的時間。搜索算法,絕大部分需要用到剪枝。然而,不是所有的枝條都可以剪掉,這就需要通過設計出合理的判斷方法,以決定某一分支的取舍。
例:求解九宮數字魔方。在搜索解過程中,我們分析因為每行、列的三個數相加均為15,則可將1~9個數分為三組1~3、4~6,7~9,從每組中取出一個組成一行或一列,再分析,組成和為15的組合只有8種,1+5+9、2+6+7、1+6+8、3+4+8、2+4+9、3+5+7、2+5+8、4+5+6,再分析,可以發現九宮最中間的數必為5,通過這樣的分析,就可以從搜索過程中剪去許多沒有用的枝條,從而大大提高搜索效率。
8)動態規劃,動態規劃是建立在最優原則的基礎上。采用動態規劃方法,可以優雅而高效地解決許多用貪心算法或分治法無法解決的問題。和貪心算法一樣,在動態規劃中,可將一個問題的解決方案視為一系列決策的結果。不同的是,在貪心算法中,每采用一次貪心準則便做出一個不可撤回的決策,而在動態規劃中,還要考察每個最優決策序列中是否包含一個最優子序列。
動態規劃的設計過程一般分為三個步驟:
a、 按照最優解的結構設計狀態轉移方程
b、按照自上而下或者自下而上的方式計算最優解的值
c、 按照求解途徑給出最優解的形成過程
例:某人購置了一輛卡車,從事個體運輸業務。給定以下各有關數據:
R[t],t=1,2,...,k,表示已使用過 t 年的卡車,再工作一年所得的運費,它隨 t 的增加而減少,k (k≤20) 年后卡車已無使用價值.。 U[t],t=1,...,k,表示已使用過 t 年的卡車,再工作一年所需的維修費, 它隨 t 的增加而增加。C[t],t=1,2,...,k,表示已使用過 t 年的舊卡車,賣掉舊車,買進新車,所需的凈費用,它隨 t 的增加而增加。以上各數據均為實型,單位為"萬元"。
設某卡車已使用過 t 年,
① 如果繼續使用,則第 t+1 年回收額為 R[t]-U[t],
② 如果賣掉舊車,買進新車,則 第 t+1 年回收額為 R[0]-U[0]-C[t] . 該運輸戶從某年初購車日起,計劃工作 N (N<=20) 年,N 年后不論車的狀態如何,不再工作。為使這 N 年的總回收額最大,應在哪些年更新舊車? 假定在這 N年內,運輸戶每年只用一輛車,而且以上各種費用均不改變。輸入: 用文件輸入已知數據,格式為:
第 1 行: N (運輸戶工作年限)
第 2 行: k (卡車最大使用年限,k≤20 )
第 3 行: R[0] R[1] ... R[k]
第 4 行: U[0] U[1] ... U[k]
第 5 行: C[0] C[1] ... C[k]
輸出: 用文本文件按以下格式輸出結果:
第 1 行: W ( N 年總回收額 )
第 2--N+1 行: 每行輸出 3 個數據:
年序號 ( 從 1 到 N 按升序輸出 );
是否更新 ( 當年如果更新,輸出 1,否則輸出 0);
當年回收額 ( N 年回收總額應等于 W ).
例: 設給定以下數據:
N=4, k=5,
i: 0 1 2 3 4 5
R[i]: 8 7 6 5 4 2
U[i]: 0.5 1 2 3 4 5
C[i]: 0 2 3 5 8 10
則正確的輸出應該是
24.5
1 0 7.5
2 1 5.5
3 1 5.5
4 0 6.0
【分析】這是動態規劃的一個典型的例題.由題意可知,用過t年的卡車,繼續使用一年的收益為d[t]=R[t]-U[t],更換新車后一年的收益為e[t]=R[0]-U[0]-C[t]。我們采用倒推分析的方法.F[j,t]表示已經使用了t年的卡車,在第j年不論繼續使用還是更新,到第N年為止,可能得到的最大收益。規定當j>N時,F[j,t]≡0。如果在第j年更新,則收益為p=e[t]+F[j+1,1]; 如果仍使用舊車,則收益為 q=d[t]+F[j+1,t+1]。這里,e[t]或d[t]為第j年的收益,F[j+1,1]或F[j+1,t+1]為從第j+1年到第N年在不同條件下的最大收益.顯然,F[j,t]=Max(p,q).這就是所需要的計算公式.在下面的程序中,數組g[j,t]用于記錄使用過t年的車,在第j年的選擇方案,g[j,t]=1表示更換新車,g[j,t]=0表示仍使用舊車.