史萊姆論壇

返回   史萊姆論壇 > 教學文件資料庫 > Hacker/Cracker 及加解密技術文件
忘記密碼?
論壇說明 標記討論區已讀

歡迎您來到『史萊姆論壇』 ^___^

您目前正以訪客的身份瀏覽本論壇,訪客所擁有的權限將受到限制,您可以瀏覽本論壇大部份的版區與文章,但您將無法參與任何討論或是使用私人訊息與其他會員交流。若您希望擁有完整的使用權限,請註冊成為我們的一份子,註冊的程序十分簡單、快速,而且最重要的是--註冊是完全免費的!

請點擊這裡:『註冊成為我們的一份子!』

Google 提供的廣告


 
 
主題工具 顯示模式
舊 2006-05-11, 08:31 PM   #1
psac
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設 常用算法設計方法

常用算法設計方法

一、迭代法

迭代法是用於求方程或方程組近似根的一種常用的算法設計方法。設方程為f(x)=0,用某種數學方法匯出等價的形式x=g(x),然後按以下步驟執行:
(1) 選一個方程的近似根,賦給變數x0;
(2) 將x0的值儲存於變數x1,然後計算g(x1),並將結果存於變數x0;
(3) 當x0與x1的差的絕對值還小於指定的精度要求時,重複步驟(2)的計算。
若方程有根,並且用上述方法計算出來的近似根序列收斂,則按上述方法求得的x0就認為是方程的根。上述算法用C程序的形式表示為:
【算法】迭代法求方程的根
{ x0=初始近似根;
do {
x1=x0;
x0=g(x1); /*按特定的方程計算新的近似根*/
} while ( fabs(x0-x1)>Epsilon);
printf(「方程的近似根是%f\n」,x0);
}
迭代算法也常用於求方程組的根,令
X=(x0,x1,…,xn-1)
設方程組為:
xi=gi(X) (I=0,1,…,n-1)
則求方程組根的迭代算法可描述如下:
【算法】迭代法求方程組的根
{ for (i=0;i<n;i++)
x=初始近似根;
do {
for (i=0;i<n;i++)
y=x;
for (i=0;i<n;i++)
x=gi(X);
for (delta=0.0,i=0;i<n;i++)
if (fabs(y-x)>delta) delta=fabs(y-x);
} while (delta>Epsilon);
for (i=0;i<n;i++)
printf(「變數x[%d]的近似根是 %f」,I,x);
printf(「\n」);
}
具體使用迭代法求根時應注意以下兩種可能發生的情況:
(1) 如果方程無解,算法求出的近似根序列就不會收斂,迭代程序會變成死循環,因此在使用迭代算法前應先考察方程是否有解,並在程序中對迭代的次數給予限制;
(2) 方程雖然有解,但迭代公式選項不當,或迭代的初始近似根選項不合理,也會導致迭代失敗
__________________
http://bbsimg.qianlong.com/upload/01/08/29/68/1082968_1136014649812.gif
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
舊 2006-05-11, 08:31 PM   #2 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

二、窮舉搜尋法

窮舉搜尋法是對可能是解的眾多候選解按某種順序進行逐一枚舉和檢驗,並從眾找出那些符合要求的候選解作為問題的解。
【問題】 將A、B、C、D、E、F這六個變數排成如圖所顯示的三角形,這六個變數分別取[1,6]上的整數,且均不相同。求使三角形三條邊上的變數之和相等的全部解。如圖就是一個解。
程序引入變數a、b、c、d、e、f,並讓它們分別順序取1至6的證書,在它們互不相同的條件下,測試由它們排成的如圖所顯示的三角形三條邊上的變數之和是否相等,如相等即為一種滿足要求的排列,把它們輸出。當這些變數取盡所有的組合後,程序就可得到全部可能的解。細節見下面的程序。
【程序1】
# include <stdio.h>
void main()
{ int a,b,c,d,e,f;
for (a=1;a<=6;a++)
for (b=1;b<=6;b++) {
if (b==a) continue;
for (c=1;c<=6;c++) {
if (c==a)||(c==b) continue;
for (d=1;d<=6;d++) {
if (d==a)||(d==b)||(d==c) continue;
for (e=1;e<=6;e++) {
if (e==a)||(e==b)||(e==c)||(e==d) continue;
f=21-(a+b+c+d+e);
if ((a+b+c==c+d+e))&&(a+b+c==e+f+a)) {
printf(「%6d,a);
printf(「%4d%4d」,b,f);
printf(「%2d%4d%4d」,c,d,e);
scanf(「%*c」);
}
}
}
}
}
}
按窮舉法編寫的程序通常不能適應變化的情況。如問題改成有9個變數排成三角形,每條邊有4個變數的情況,程序的循環重數就要相應改變。
對一組數窮盡所有排列,還有更直接的方法。將一個排列看作一個長整數,則所有排列對應著一組整數。將這組整數按從小到大的順序排列排成一個整數,從對應最小的整數開始。按數列的遞增順序逐一列舉每個排列對應的每個整數,這能更有效地完成排列的窮舉。從一個排列找出對應數列的下一個排列可在當前排列的基礎上作部分調整來實現。倘若當前排列為1,2,4,6,5,3,並令其對應的長整數為124653。要尋找比長整數124653更大的排列,可從該排列的最後一個數位順序向前逐位考察,當發現排列中的某個數位比它前一個數位大時,如本例中的6比它的前一位數位4大,這說明還有對應更大整數的排列。但為了順序從小到大列舉出所有的排列,不能立即調整得太大,如本例中將數位6與數位4交換得到的排列126453就不是排列124653的下一個排列。為了得到排列124653的下一個排列,應從已經考察過的那部分數位中選出比數位大,但又是它們中最小的那一個數位,比如數位5,與數位4交換。該數位也是從後向前考察程序中第一個比4大的數位。5與4交換後,得到排列125643。在前面數位1,2,5固定的情況下,還應選項對應最小整數的那個排列,為此還需將後面那部分數位的排列順序顛倒,如將數位6,4,3的排列順序顛倒,得到排列1,2,5,3,4,6,這才是排列1,2,4,6,5,3的下一個排列。按以上想法編寫的程序如下。
【程序2】
# include <stdio.h>
# define SIDE_N 3
# define LENGTH 3
# define VARIABLES 6
int A,B,C,D,E,F;
int *pt[]={&A,&B,&C,&D,&E,&F};
int *side[SIDE_N][LENGTH]={&A,&B,&C,&C,&D,&E,&E,&F,&A};
int side_total[SIDE_N];
main{}
{ int i,j,t,equal;
for (j=0;j<VARIABLES;j++)
*pt[j]=j+1;
while(1)
{ for (i=0;i<SIDE_N;i++)
{ for (t=j=0;j<LENGTH;j++)
t+=*side[j];
side_total=t;
}
for (equal=1,i=0;equal&&i<SIDE_N-1;i++)
if (side_total!=side_total[i+1] equal=0;
if (equal)
{ for (i=1;i<VARIABLES;i++)
printf(「%4d」,*pt);
printf(「\n」);
scanf(「%*c」);
}
for (j=VARIABLES-1;j>0;j--)
if (*pt[j]>*pt[j-1]) break;
if (j==0) break;
for (i=VARIABLES-1;i>=j;i--)
if (*pt>*pt[i-1]) break;
t=*pt[j-1];* pt[j-1] =* pt; *pt=t;
for (i=VARIABLES-1;i>j;i--,j++)
{ t=*pt[j]; *pt[j] =* pt; *pt=t; }
}
}
從上述問題解決的方法中,最重要的因素就是確定某種方法來確定所有的候選解。下面再用一個示例來加以說明。
【問題】 背包問題
問題描述:有不同價值、不同重量的物品n件,求從這n件物品中選取一部分物品的選項方案,使選物品的總重量不超過指定的限制重量,但選物品的價值之和最大。
設n個物品的重量和價值分別儲存於於陣列w[ ]和v[ ]中,限制重量為tw。考慮一個n元組(x0,x1,…,xn-1),其中xi=0 表示第i個物品沒有選取,而xi=1則表示第i個物品被選取。顯然這個n元組等價於一個選項方案。用枚舉法解決背包問題,需要枚舉所有的選取方案,而根據上述方法,我們只要枚舉所有的n元組,就可以得到問題的解。
顯然,每個份量取值為0或1的n元組的個數共為2n個。而每個n元組其實對應了一個長度為n的二進制數,且這些二進制數的取值範圍為0~2n-1。因此,如果把0~2n-1分別轉化為相應的二進制數,則可以得到我們所需要的2n個n元組。
【算法】
maxv=0;
for (i=0;i<2n;i++)
{ B[0..n-1]=0;
把i轉化為二進制數,儲存於於陣列B中;
temp_w=0;
temp_v=0;
for (j=0;j<n;j++)
{ if (B[j]==1)
{ temp_w=temp_w+w[j];
temp_v=temp_v+v[j];
}
if ((temp_w<=tw)&&(temp_v>maxv))
{ maxv=temp_v;
儲存該B陣列;
}
}
}
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
舊 2006-05-11, 08:32 PM   #3 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

三、遞推法

遞推法是利用問題本身所具有的一種遞推關係求問題解的一種方法。設要求問題規模為N的解,當N=1時,解或為已知,或能非常方便地得到解。能採用遞推法構造算法的問題有重要的遞推性質,即當得到問題規模為i-1的解後,由問題的遞推性質,能從已求得的規模為1,2,…,i-1的一系列解,構造出問題規模為I的解。這樣,程序可從i=0或i=1出發,重複地,由已知至i-1規模的解,通過遞推,獲得規模為i的解,直至得到規模為N的解。
【問題】 階乘計算
問題描述:編寫程序,對給定的n(n≦100),計算並輸出k的階乘k!(k=1,2,…,n)的全部有效數位。
由於要求的整數可能大大超出一般整數的位數,程序用一維陣列儲存於長整數,儲存於長整數陣列的每個元素只儲存於長整數的一位數位。如有m位成整數N用陣列a[ ]儲存於:
N=a[m]×10m-1+a[m-1]×10m-2+ … +a[2]×101+a[1]×100
並用a[0]儲存於長整數N的位數m,即a[0]=m。按上述約定,陣列的每個元素儲存於k的階乘k!的一位數位,並從低位元到高位依次存於陣列的第二個元素、第三個元素……。例如,5!=120,在陣列中的儲存於形式為:
3 0 2 1 ……
首元素3表示長整數是一個3位數,接著是低位元到高位依次是0、2、1,表示成整數120。
計算階乘k!可採用對已求得的階乘(k-1)!連續累加k-1次後求得。例如,已知4!=24,計算5!,可對原來的24累加4次24後得到120。細節見以下程序。
# include <stdio.h>
# include <malloc.h>
# define MAXN 1000
void pnext(int a[ ],int k)
{ int *b,m=a[0],i,j,r,carry;
b=(int * ) malloc(sizeof(int)* (m+1));
for ( i=1;i<=m;i++) b=a;
for ( j=1;j<=k;j++)
{ for ( carry=0,i=1;i<=m;i++)
{ r=(i<a[0]?a+b:a)+carry;
a=r%10;
carry=r/10;
}
if (carry) a[++m]=carry;
}
free(b);
a[0]=m;
}

void write(int *a,int k)
{ int i;
printf(「%4d!=」,k);
for (i=a[0];i>0;i--)
printf(「%d」,a);
printf(「\n\n」);
}

void main()
{ int a[MAXN],n,k;
printf(「Enter the number n: 「);
scanf(「%d」,&n);
a[0]=1;
a[1]=1;
write(a,1);
for (k=2;k<=n;k++)
{ pnext(a,k);
write(a,k);
getchar();
}
}
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
舊 2006-05-11, 08:32 PM   #4 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

四、遞回

遞回是設計和描述算法的一種有力的工具,由於它在複雜算法的描述中被經常採用,為此在進一步介紹其他算法設計方法之前先討論它。
能採用遞回描述的算法通常有這樣的特徵:為求解規模為N的問題,設法將它分解成規模較小的問題,然後從這些小問題的解方便地構造出大問題的解,並且這些規模較小的問題也能採用同樣的分解和綜合方法,分解成規模更小的問題,並從這些更小問題的解構造出規模較大問題的解。特別地,當規模N=1時,能直接得解。
【問題】 編寫計算斐波那契(Fibonacci)數列的第n項函數fib(n)。
斐波那契數列為:0、1、1、2、3、……,即:
fib(0)=0;
fib(1)=1;
fib(n)=fib(n-1)+fib(n-2) (當n>1時)。
寫成遞回函數有:
int fib(int n)
{ if (n==0) return 0;
if (n==1) return 1;
if (n>1) return fib(n-1)+fib(n-2);
}
遞回算法的執行程序分遞推和回歸兩個階段。在遞推階段,把較複雜的問題(規模為n)的求解推到比原問題簡單一些的問題(規模小於n)的求解。例如上例中,求解fib(n),把它推到求解fib(n-1)和fib(n-2)。也就是說,為計算fib(n),必須先計算fib(n-1)和fib(n-2),而計算fib(n-1)和fib(n-2),又必須先計算fib(n-3)和fib(n-4)。依次類推,直至計算fib(1)和fib(0),分別能立即得到結果1和0。在遞推階段,必須要有終止遞回的情況。例如在函數fib中,當n為1和0的情況。
在回歸階段,當獲得最簡單情況的解後,逐級返回,依次得到稍複雜問題的解,例如得到fib(1)和fib(0)後,返回得到fib(2)的結果,……,在得到了fib(n-1)和fib(n-2)的結果後,返回得到fib(n)的結果。
在編寫遞回函數時要注意,函數中的局部變數和參數知識局限於當前使用層,當遞推進入「簡單問題」層時,原來層次上的參數和局部變數便被隱蔽起來。在一系列「簡單問題」層,它們各有自己的參數和局部變數。
由於遞回引起一系列的函數使用,並且可能會有一系列的重複計算,遞回算法的執行效率相對較低。當某個遞回算法能較方便地轉換成遞推算法時,通常按遞推算法編寫程序。例如上例計算斐波那契數列的第n項的函數fib(n)應採用遞推算法,即從斐波那契數列的前兩項出發,逐次由前兩項計算出下一項,直至計算出要求的第n項。
【問題】 組合問題
問題描述:找出從自然數1、2、……、n中任取r個數的所有組合。例如n=5,r=3的所有組合為: (1)5、4、3 (2)5、4、2 (3)5、4、1
(4)5、3、2 (5)5、3、1 (6)5、2、1
(7)4、3、2 (8)4、3、1 (9)4、2、1
(10)3、2、1
分析所列的10個組合,可以採用這樣的遞回思想來考慮求組合函數的算法。設函數為void comb(int m,int k)為找出從自然數1、2、……、m中任取k個數的所有組合。當組合的第一個數位選定時,其後的數位是從餘下的m-1個數中取k-1數的組合。這就將求m個數中取k個數的組合問題轉化成求m-1個數中取k-1個數的組合問題。設函數引入工作陣列a[ ]存放求出的組合的數位,約定函數將確定的k個數位組合的第一個數位放在a[k]中,當一個組合求出後,才將a[ ]中的一個組合輸出。第一個數可以是m、m-1、……、k,函數將確定組合的第一個數位放入陣列後,有兩種可能的選項,因還未去頂組合的其餘元素,繼續遞回去確定;或因已確定了組合的全部元素,輸出這個組合。細節見以下程序中的函數comb。
【程序】
# include <stdio.h>
# define MAXN 100
int a[MAXN];
void comb(int m,int k)
{ int i,j;
for (i=m;i>=k;i--)
{ a[k]=i;
if (k>1)
comb(i-1,k-1);
else
{ for (j=a[0];j>0;j--)
printf(「%4d」,a[j]);
printf(「\n」);
}
}
}

void main()
{ a[0]=3;
comb(5,3);
}
【問題】 背包問題
問題描述:有不同價值、不同重量的物品n件,求從這n件物品中選取一部分物品的選項方案,使選物品的總重量不超過指定的限制重量,但選物品的價值之和最大。
設n件物品的重量分別為w0、w1、…、wn-1,物品的價值分別為v0、v1、…、vn-1。採用遞回尋找物品的選項方案。設前面已有了多種選項的方案,並保留了其中總價值最大的方案於陣列option[ ],該方案的總價值存於變數maxv。當前正在考察新方案,其物品選項情況儲存於陣列cop[ ]。假定當前方案已考慮了前i-1件物品,現在要考慮第i件物品;當前方案已包含的物品的重量之和為tw;至此,若其餘物品都選項是可能的話,本方案能達到的總價值的期望值為tv。算法引入tv是當一旦當前方案的總價值的期望值也小於前面方案的總價值maxv時,繼續考察當前方案變成無意義的工作,應終止當前方案,立即去考察下一個方案。因為當方案的總價值不比maxv大時,該方案不會被再考察,這同時保證函數後找到的方案一定會比前面的方案更好。
對於第i件物品的選項考慮有兩種可能:
(1) 考慮物品i被選項,這種可能性僅當包含它不會超過方案總重量限制時才是可行的。選後,繼續遞回去考慮其餘物品的選項。
(2) 考慮物品i不被選項,這種可能性僅當不包含物品i也有可能會找到價值更大的方案的情況。
按以上思想寫出遞回算法如下:
try(物品i,當前選項已達到的重量和,本方案可能達到的總價值tv)
{ /*考慮物品i包含在當前方案中的可能性*/
if(包含物品i是可以接受的)
{ 將物品i包含在當前方案中;
if (i<n-1)
try(i+1,tw+物品i的重量,tv);
else
/*又一個完整方案,因為它比前面的方案好,以它作為最佳方案*/
以當前方案作為臨時最佳方案儲存;
恢復物品i不包含狀態;
}
/*考慮物品i不包含在當前方案中的可能性*/
if (不包含物品i僅是可男考慮的)
if (i<n-1)
try(i+1,tw,tv-物品i的價值);
else
/*又一個完整方案,因它比前面的方案好,以它作為最佳方案*/
以當前方案作為臨時最佳方案儲存;
}
為了理解上述算法,特舉以下實例。設有4件物品,它們的重量和價值見表:
物品 0 1 2 3
重量 5 3 2 1
價值 4 4 3 1

並設限制重量為7。則按以上算法,下圖表示找解程序。由圖知,一旦找到一個解,算法就進一步找更好的佳。如能判定某個搜尋分支不會找到更好的解,算法不會在該分支繼續搜尋,而是立即終止該分支,並去考察下一個分支。

按上述算法編寫函數和程序如下:
【程序】
# include <stdio.h>
# define N 100
double limitW,totV,maxV;
int option[N],cop[N];
struct { double weight;
double value;
}a[N];
int n;
void find(int i,double tw,double tv)
{ int k;
/*考慮物品i包含在當前方案中的可能性*/
if (tw+a.weight<=limitW)
{ cop=1;
if (i<n-1) find(i+1,tw+a.weight,tv);
else
{ for (k=0;k<n;k++)
option[k]=cop[k];
maxv=tv;
}
cop=0;
}
/*考慮物品i不包含在當前方案中的可能性*/
if (tv-a.value>maxV)
if (i<n-1) find(i+1,tw,tv-a.value);
else
{ for (k=0;k<n;k++)
option[k]=cop[k];
maxv=tv-a.value;
}
}

void main()
{ int k;
double w,v;
printf(「輸入物品種數\n」);
scanf((「%d」,&n);
printf(「輸入各物品的重量和價值\n」);
for (totv=0.0,k=0;k<n;k++)
{ scanf(「%1f%1f」,&w,&v);
a[k].weight=w;
a[k].value=v;
totV+=V;
}
printf(「輸入限制重量\n」);
scanf(「%1f」,&limitV);
maxv=0.0;
for (k=0;k<n;k++) cop[k]=0;
find(0,0.0,totV);
for (k=0;k<n;k++)
if (option[k]) printf(「%4d」,k+1);
printf(「\n總價值為%.2f\n」,maxv);
}
作為對比,下面以同樣的解題思想,考慮非遞回的程序解。為了提高找解速度,程序不是簡單地逐一產生所有候選解,而是從每個物品對候選解的影響來形成值得進一步考慮的候選解,一個候選解是通過依次考察每個物品形成的。對物品i的考察有這樣幾種情況:當該物品被包含在候選解中依舊滿足解的總重量的限制,該物品被包含在候選解中是應該繼續考慮的;反之,該物品不應該包括在當前正在形成的候選解中。同樣地,僅當物品不被包括在候選解中,還是有可能找到比目前臨時最佳解更好的候選解時,才去考慮該物品不被包括在候選解中;反之,該物品不包括在當前候選解中的方案也不應繼續考慮。對於任一值得繼續考慮的方案,程序就去進一步考慮下一個物品。
【程序】
# include <stdio.h>
# define N 100
double limitW;
int cop[N];
struct ele { double weight;
double value;
} a[N];
int k,n;
struct { int flg;
double tw;
double tv;
}twv[N];
void next(int i,double tw,double tv)
{ twv.flg=1;
twv.tw=tw;
twv.tv=tv;
}
double find(struct ele *a,int n)
{ int i,k,f;
double maxv,tw,tv,totv;
maxv=0;
for (totv=0.0,k=0;k<n;k++)
totv+=a[k].value;
next(0,0.0,totv);
i=0;
While (i>=0)
{ f=twv.flg;
tw=twv.tw;
tv=twv.tv;
switch(f)
{ case 1: twv.flg++;
if (tw+a.weight<=limitW)
if (i<n-1)
{ next(i+1,tw+a.weight,tv);
i++;
}
else
{ maxv=tv;
for (k=0;k<n;k++)
cop[k]=twv[k].flg!=0;
}
break;
case 0: i--;
break;
default: twv.flg=0;
if (tv-a.value>maxv)
if (i<n-1)
{ next(i+1,tw,tv-a.value);
i++;
}
else
{ maxv=tv-a.value;
for (k=0;k<n;k++)
cop[k]=twv[k].flg!=0;
}
break;
}
}
return maxv;
}

void main()
{ double maxv;
printf(「輸入物品種數\n」);
scanf((「%d」,&n);
printf(「輸入限制重量\n」);
scanf(「%1f」,&limitW);
printf(「輸入各物品的重量和價值\n」);
for (k=0;k<n;k++)
scanf(「%1f%1f」,&a[k].weight,&a[k].value);
maxv=find(a,n);
printf(「\n選的物品為\n」);
for (k=0;k<n;k++)
if (option[k]) printf(「%4d」,k+1);
printf(「\n總價值為%.2f\n」,maxv);
}
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
舊 2006-05-11, 08:33 PM   #5 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

五、回溯法

回溯法也稱為試探法,該方法首先暫時放棄關於問題規模大小的限制,並將問題的候選解按某種順序逐一枚舉和檢驗。當發現當前候選解不可能是解時,就選項下一個候選解;倘若當前候選解除了還不滿足問題規模要求外,滿足所有其他要求時,繼續擴大當前候選解的規模,並繼續試探。如果當前候選解滿足包括問題規模在內的所有要求時,該候選解就是問題的一個解。在回溯法中,放棄當前候選解,尋找下一個候選解的程序稱為回溯。擴大當前候選解的規模,以繼續試探的程序稱為向前試探。
1、回溯法的一般描述
可用回溯法求解的問題P,通常要能表達為:對於已知的由n元組(x1,x2,…,xn)組成的一個狀態空間E={(x1,x2,…,xn)∣xi∈Si ,i=1,2,…,n},給定關於n元組中的一個份量的一個約束集D,要求E中滿足D的全部約束條件的所有n元組。其中Si是份量xi的定義域,且 |Si| 有限,i=1,2,…,n。我們稱E中滿足D的全部約束條件的任一n元組為問題P的一個解。
解問題P的最樸素的方法就是枚舉法,即對E中的所有n元組逐一地檢測其是否滿足D的全部約束,若滿足,則為問題P的一個解。但顯然,其計算量是相當大的。
我們發現,對於許多問題,所給定的約束集D具有完備性,即i元組(x1,x2,…,xi)滿足D中僅涉及到x1,x2,…,xi的所有約束意味著j(j<i)元組(x1,x2,…,xj)一定也滿足D中僅涉及到x1,x2,…,xj的所有約束,i=1,2,…,n。換句話說,只要存在0≤j≤n-1,使得(x1,x2,…,xj)違反D中僅涉及到x1,x2,…,xj的約束之一,則以(x1,x2,…,xj)為前綴的任何n元組(x1,x2,…,xj,xj+1,…,xn)一定也違反D中僅涉及到x1,x2,…,xi的一個約束,n≥i>j。因此,對於約束集D具有完備性的問題P,一旦檢測斷定某個j元組(x1,x2,…,xj)違反D中僅涉及x1,x2,…,xj的一個約束,就可以肯定,以(x1,x2,…,xj)為前綴的任何n元組(x1,x2,…,xj,xj+1,…,xn)都不會是問題P的解,因而就不必去搜尋它們、檢測它們。回溯法正是針對這類問題,利用這類問題的上述性質而提出來的比枚舉法效率更高的算法。
回溯法首先將問題P的n元組的狀態空間E表示成一棵高為n的帶權有序樹T,把在E中求問題P的所有解轉化為在T中搜尋問題P的所有解。樹T類似於檢索樹,它可以這樣構造:
設Si中的元素可排成xi(1) ,xi(2) ,…,xi(mi-1) ,|Si| =mi,i=1,2,…,n。從根開始,讓T的第I層的每一個結點都有mi個兒子。這mi個兒子到它們的雙親的邊,按從左到右的次序,分別帶權xi+1(1) ,xi+1(2) ,…,xi+1(mi) ,i=0,1,2,…,n-1。照這種構造方式,E中的一個n元組(x1,x2,…,xn)對應於T中的一個葉子結點,T的根到這個葉子結點的路徑上依次的n條邊的權分別為x1,x2,…,xn,反之亦然。另外,對於任意的0≤i≤n-1,E中n元組(x1,x2,…,xn)的一個前綴I元組(x1,x2,…,xi)對應於T中的一個非葉子結點,T的根到這個非葉子結點的路徑上依次的I條邊的權分別為x1,x2,…,xi,反之亦然。特別,E中的任意一個n元組的空前綴(),對應於T的根。
因而,在E中尋找問題P的一個解等價於在T中搜尋一個葉子結點,要求從T的根到該葉子結點的路徑上依次的n條邊相應帶的n個權x1,x2,…,xn滿足約束集D的全部約束。在T中搜尋所要求的葉子結點,很自然的一種方式是從根出發,按深度優先的原則逐步深入,即依次搜尋滿足約束條件的前綴1元組(x1i)、前綴2元組(x1,x2)、…,前綴I元組(x1,x2,…,xi),…,直到i=n為止。
在回溯法中,上述引入的樹被稱為問題P的狀態空間樹;樹T上任意一個結點被稱為問題P的狀態結點;樹T上的任意一個葉子結點被稱為問題P的一個解狀態結點;樹T上滿足約束集D的全部約束的任意一個葉子結點被稱為問題P的一個回答狀態結點,它對應於問題P的一個解。
【問題】 組合問題
問題描述:找出從自然數1、2、……、n中任取r個數的所有組合。
例如n=5,r=3的所有組合為:
(1)1、2、3 (2)1、2、4 (3)1、2、5
(4)1、3、4 (5)1、3、5 (6)1、4、5
(7)2、3、4 (8)2、3、5 (9)2、4、5
(10)3、4、5
則該問題的狀態空間為:
E={(x1,x2,x3)∣xi∈S ,i=1,2,3 } 其中:S={1,2,3,4,5}
約束集為: x1<x2<x3
顯然該約束集具有完備性。


2、回溯法的方法
對於具有完備約束集D的一般問題P及其相應的狀態空間樹T,利用T的層次結構和D的完備性,在T中搜尋問題P的所有解的回溯法可以形象地描述為:
從T的根出發,按深度優先的原則,系統地搜尋以其為根的子樹中可能包含著回答結點的所有狀態結點,而跳過對肯定不含回答結點的所有子樹的搜尋,以提高搜尋效率。具體地說,當搜尋按深度優先原則到達一個滿足D中所有有關約束的狀態結點時,即「啟動」該狀態結點,以便繼續往深層搜尋;否則跳過對以該狀態結點為根的子樹的搜尋,而一邊逐層地向該狀態結點的祖先結點回溯,一邊「殺死」其兒子結點已被搜尋遍的祖先結點,直到遇到其兒子結點未被搜尋遍的祖先結點,即轉向其未被搜尋的一個兒子結點繼續搜尋。
在搜尋程序中,只要所啟動的狀態結點又滿足終結條件,那麼它就是回答結點,應該把它輸出或儲存。由於在回溯法求解問題時,一般要求出問題的所有解,因此在得到回答結點後,同時也要進行回溯,以便得到問題的其他解,直至回溯到T的根且根的所有兒子結點均已被搜尋過為止。
例如在組合問題中,從T的根出發深度優先遍歷該樹。當遍歷到結點(1,2)時,雖然它滿足約束條件,但還不是回答結點,則應繼續深度遍歷;當遍歷到葉子結點(1,2,5)時,由於它已是一個回答結點,則儲存(或輸出)該結點,並回溯到其雙親結點,繼續深度遍歷;當遍歷到結點(1,5)時,由於它已是葉子結點,但不滿足約束條件,故也需回溯。
3、回溯法的一般流程和技術
在用回溯法求解有關問題的程序中,一般是一邊建樹,一邊遍歷該樹。在回溯法中我們一般採用非遞回方法。下面,我們指出回溯法的非遞回算法的一般流程:

在用回溯法求解問題,也即在遍歷狀態空間樹的程序中,如果採用非遞回方法,則我們一般要用到棧的資料結構。這時,不僅可以用棧來表示正在遍歷的樹的結點,而且可以很方便地表示建立孩子結點和回溯程序。
例如在組合問題中,我們用一個一維陣列Stack[ ]表示棧。開始棧空,則表示了樹的根結點。如果元素1進棧,則表示建立並遍歷(1)結點;這時如果元素2進棧,則表示建立並遍歷(1,2)結點;元素3再進去棧,則表示建立並遍歷(1,2,3)結點。這時可以判斷它滿足所有約束條件,是問題的一個解,輸出(或儲存)。這時只要推疊頂端元素(3)出棧,即表示從結點(1,2,3)回溯到結點(1,2)。
【問題】 組合問題
問題描述:找出從自然數1,2,…,n中任取r個數的所有組合。
採用回溯法找問題的解,將找到的組合以從小到大順序存於a[0],a[1],…,a[r-1]中,組合的元素滿足以下性質:
(1) a[i+1]>a,後一個數位比前一個大;
(2) a-i<=n-r+1。
按回溯法的思想,找解程序可以敘述如下:
首先放棄組合數個數為r的條件,候選組合從只有一個數位1開始。因該候選解滿足除問題規模之外的全部條件,擴大其規模,並使其滿足上述條件(1),候選組合改為1,2。繼續這一程序,得到候選組合1,2,3。該候選解滿足包括問題規模在內的全部條件,因而是一個解。在該解的基礎上,選下一個候選解,因a[2]上的3調整為4,以及以後調整為5都滿足問題的全部要求,得到解1,2,4和1,2,5。由於對5不能再作調整,就要從a[2]回溯到a[1],這時,a[1]=2,可以調整為3,並向前試探,得到解1,3,4。重複上述向前試探和向後回溯,直至要從a[0]再回溯時,說明已經找完問題的全部解。按上述思想寫成程序如下:
【程序】
# define MAXN 100
int a[MAXN];
void comb(int m,int r)
{ int i,j;
i=0;
a=1;
do {
if (a-i<=m-r+1
{ if (i==r-1)
{ for (j=0;j<r;j++)
printf(「%4d」,a[j]);
printf(「\n」);
}
a++;
continue;
}
else
{ if (i==0)
return;
a[--i]++;
}
} while (1)
}

main()
{ comb(5,3);
}
【問題】 填字遊戲
問題描述:在3×3個方格的方陣中要填入數位1到N(N≥10)內的某9個數位,每個方格填一個整數,似的所有相鄰兩個方格內的兩個整數之和為質數。試求出所有滿足這個要求的各種數位填法。
可用試探發找到問題的解,即從第一個方格開始,為當前方格尋找一個合理的整數填入,並在當前位置正確填入後,為下一方格尋找可填入的合理整數。如不能為當前方格找到一個合理的可填證書,就要回退到前一方格,調整前一方格的填入數。當第九個方格也填入合理的整數後,就找到了一個解,將該解輸出,並調整第九個的填入的整數,尋找下一個解。
為找到一個滿足要求的9個數的填法,從還未填一個數開始,按某種順序(如從小到大的順序)每次在當前位置填入一個整數,然後檢查當前填入的整數是否能滿足要求。在滿足要求的情況下,繼續用同樣的方法為下一方格填入整數。如果最近填入的整數不能滿足要求,就改變填入的整數。如對當前方格試盡所有可能的整數,都不能滿足要求,就得回退到前一方格,並調整前一方格填入的整數。如此重複執行增強、檢查或調整、檢查,直到找到一個滿足問題要求的解,將解輸出。
回溯法找一個解的算法:
{ int m=0,ok=1;
int n=8;
do{
if (ok) 增強;
else 調整;
ok=檢查前m個整數填放的合理性;
} while ((!ok||m!=n)&&(m!=0))
if (m!=0) 輸出解;
else 輸出無解報告;
}
如果程序要找全部解,則在將找到的解輸出後,應繼續調整最後位置上填放的整數,試圖去找下一個解。相應的算法如下:
回溯法找全部解的算法:
{ int m=0,ok=1;
int n=8;
do{
if (ok)
{ if (m==n)
{ 輸出解;
調整;
}
else 增強;
}
else 調整;
ok=檢查前m個整數填放的合理性;
} while (m!=0);
}
為了確保程序能夠終止,調整時必須保證曾被放棄過的填數序列不會再次實驗,即要求按某種有許模型產生填數序列。給解的候選者設定一個被檢驗的順序,按這個順序逐一形成候選者並檢驗。從小到大或從大到小,都是可以採用的方法。如增強時,先在新位置填入整數1,調整時,找當前候選解中下一個還未被使用過的整數。將上述增強、調整、檢驗都編寫成程序,細節見以下找全部解的程序。
【程序】
# include <stdio.h>
# define N 12
void write(int a[ ])
{ int i,j;
for (i=0;i<3;i++)
{ for (j=0;j<3;j++)
printf(「%3d」,a[3*i+j]);
printf(「\n」);
}
scanf(「%*c」);
}

int b[N+1];
int a[10];
int isprime(int m)
{ int i;
int primes[ ]={2,3,5,7,11,17,19,23,29,-1};
if (m==1||m%2=0) return 0;
for (i=0;primes>0;i++)
if (m==primes) return 1;
for (i=3;i*i<=m
{ if (m%i==0) return 0;
i+=2;
}
return 1;
}

int checkmatrix[ ][3]={ {-1},{0,-1},{1,-1},{0,-1},{1,3,-1},
{2,4,-1},{3,-1},{4,6,-1},{5,7,-1}};
int selectnum(int start)
{ int j;
for (j=start;j<=N;j++)
if (b[j]) return j
return 0;
}

int check(int pos)
{ int i,j;
if (pos<0) return 0;
for (i=0;(j=checkmatrix[pos])>=0;i++)
if (!isprime(a[pos]+a[j])
return 0;
return 1;
}

int extend(int pos)
{ a[++pos]=selectnum(1);
b[a][pos]]=0;
return pos;
}

int change(int pos)
{ int j;
while (pos>=0&&(j=selectnum(a[pos]+1))==0)
b[a[pos--]]=1;
if (pos<0) return –1
b[a[pos]]=1;
a[pos]=j;
b[j]=0;
return pos;
}

void find()
{ int ok=0,pos=0;
a[pos]=1;
b[a[pos]]=0;
do {
if (ok)
if (pos==8)
{ write(a);
pos=change(pos);
}
else pos=extend(pos);
else pos=change(pos);
ok=check(pos);
} while (pos>=0)
}

void main()
{ int i;
for (i=1;i<=N;i++)
b=1;
find();
}
【問題】 n皇后問題
問題描述:求出在一個n×n的棋碟上,放置n個不能互相捕捉的國際象棋「皇后」的所有佈局。
這是來源於國際象棋的一個問題。皇后可以沿著縱橫和兩條斜線4個方向相互捕捉。如圖所顯示,一個皇后放在棋盤的第4行第3列位置上,則棋碟上凡打「×」的位置上的皇后就能與這個皇后相互捕捉。

1 2 3 4 5 6 7 8
× ×
× × ×
× × ×
× × Q × × × × ×
× × ×
× × ×
× ×
× ×
從圖中可以得到以下啟示:一個合適的解應是在每列、每行上只有一個皇后,且一條斜線上也只有一個皇后。
求解程序從空組態開始。在第1列至第m列為合理組態的基礎上,再組態第m+1列,直至第n列組態也是合理時,就找到了一個解。接著改變第n列組態,希望獲得下一個解。另外,在任一列上,可能有n種組態。開始時組態在第1行,以後改變時,順次選項第2行、第3行、…、直到第n行。當第n行組態也找不到一個合理的組態時,就要回溯,去改變前一列的組態。得到求解皇后問題的算法如下:
{ 輸入棋盤大小值n;
m=0;
good=1;
do {
if (good)
if (m==n)
{ 輸出解;
改變之,形成下一個候選解;
}
else 增強當前候選接至下一列;
else 改變之,形成下一個候選解;
good=檢查當前候選解的合理性;
} while (m!=0);
}
在編寫程序之前,先確定邊式棋盤的資料結構。比較直觀的方法是採用一個二維陣列,但仔細觀察就會發現,這種表示方法給調整候選解及檢查其合理性帶來困難。更好的方法乃是盡可能直接表示那些常用的訊息。對於本題來說,「常用訊息」並不是皇后的具體位置,而是「一個皇后是否已經在某行和某條斜線合理地安置好了」。因在某一列上恰好放一個皇后,引入一個一維陣列(col[ ]),值col表示在棋盤第i列、col行有一個皇后。例如:col[3]=4,就表示在棋盤的第3列、第4行上有一個皇后。另外,為了使程序在找完了全部解後回溯到最初位置,設定col[0]的初值為0當回溯到第0列時,說明程序已求得全部解,結束程序執行。
為使程序在檢查皇后組態的合理性方面簡易方便,引入以下三個工作陣列:
(1) 陣列a[ ],a[k]表示第k行上還沒有皇后;
(2) 陣列b[ ],b[k]表示第k列右高左低斜線上沒有皇后;
(3) 陣列 c[ ],c[k]表示第k列左高右低斜線上沒有皇后;
棋盤中同一右高左低斜線上的方格,他們的行號與列號之和相同;同一左高右低斜線上的方格,他們的行號與列號之差均相同。
初始時,所有行和斜線上均沒有皇后,從第1列的第1行組態第一個皇后開始,在第m列col[m]行放置了一個合理的皇后後,準備考察第m+1列時,在陣列a[ ]、b[ ]和c[ ]中為第m列,col[m]行的位置設定有皇后標誌;當從第m列回溯到第m-1列,並準備調整第m-1列的皇后組態時,清除在陣列a[ ]、b[ ]和c[ ]中設定的關於第m-1列,col[m-1]行有皇后的標誌。一個皇后在m列,col[m]行方格內組態是合理的,由陣列a[ ]、b[ ]和c[ ]對應位置的值都為1來確定。細節見以下程序:
【程序】
# include <stdio.h>
# include <stdlib.h>
# define MAXN 20
int n,m,good;
int col[MAXN+1],a[MAXN+1],b[2*MAXN+1],c[2*MAXN+1];

void main()
{ int j;
char awn;
printf(「Enter n: 「); scanf(「%d」,&n);
for (j=0;j<=n;j++) a[j]=1;
for (j=0;j<=2*n;j++) cb[j]=c[j]=1;
m=1; col[1]=1; good=1; col[0]=0;
do {
if (good)
if (m==n)
{ printf(「列\t行」);
for (j=1;j<=n;j++)
printf(「%3d\t%d\n」,j,col[j]);
printf(「Enter a character (Q/q for exit)!\n」);
scanf(「%c」,&awn);
if (awn==』Q』||awn==』q』) exit(0);
while (col[m]==n)
{ m--;
a[col[m]]=b[m+col[m]]=c[n+m-col[m]]=1;
}
col[m]++;
}
else
{ a[col[m]]=b[m+col[m]]=c[n+m-col[m]]=0;
col[++m]=1;
}
else
{ while (col[m]==n)
{ m--;
a[col[m]]=b[m+col[m]]=c[n+m-col[m]]=1;
}
col[m]++;
}
good=a[col[m]]&&b[m+col[m]]&&c[n+m-col[m]];
} while (m!=0);
}
試探法找解算法也常常被編寫成遞回函數,下面兩程序中的函數queen_all()和函數queen_one()能分別用來解皇后問題的全部解和一個解。
【程序】
# include <stdio.h>
# include <stdlib.h>
# define MAXN 20
int n;
int col[MAXN+1],a[MAXN+1],b[2*MAXN+1],c[2*MAXN+1];
void main()
{ int j;
printf(「Enter n: 「); scanf(「%d」,&n);
for (j=0;j<=n;j++) a[j]=1;
for (j=0;j<=2*n;j++) cb[j]=c[j]=1;
queen_all(1,n);
}

void queen_all(int k,int n)
{ int i,j;
char awn;
for (i=1;i<=n;i++)
if (a&&b[k+i]&&c[n+k-i])
{ col[k]=i;
a=b[k+i]=c[n+k-i]=0;
if (k==n)
{ printf(「列\t行」);
for (j=1;j<=n;j++)
printf(「%3d\t%d\n」,j,col[j]);
printf(「Enter a character (Q/q for exit)!\n」);
scanf(「%c」,&awn);
if (awn==』Q』||awn==』q』) exit(0);
}
queen_all(k+1,n);
a=b[k+i]=c[n+k-i];
}
}
採用遞回方法找一個解與找全部解稍有不同,在找一個解的算法中,遞回算法要對當前候選解最終是否能成為解要有回答。當它成為最終解時,遞回函數就不再遞回試探,立即返回;若不能成為解,就得繼續試探。設函數queen_one()返回1表示找到解,返回0表示當前候選解不能成為解。細節見以下函數。
【程序】
# define MAXN 20
int n;
int col[MAXN+1],a[MAXN+1],b[2*MAXN+1],c[2*MAXN+1];
int queen_one(int k,int n)
{ int i,found;
i=found=0;
While (!found&&i<n)
{ i++;
if (a&&b[k+i]&&c[n+k-i])
{ col[k]=i;
a=b[k+i]=c[n+k-i]=0;
if (k==n) return 1;
else
found=queen_one(k+1,n);
a=b[k+i]=c[n+k-i]=1;
}
}
return found;
}
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
舊 2006-05-11, 08:34 PM   #6 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

六、貪婪法

貪婪法是一種不追求最優解,只希望得到較為滿意解的方法。貪婪法一般可以快速得到滿意的解,因為它省去了為找最優解要窮盡所有可能而必須耗費的大量時間。貪婪法常以當前情況為基礎作最優選項,而不考慮各種可能的整體情況,所以貪婪法不要回溯。
例如平時購物找錢時,為使找回的零錢的硬幣數最少,不考慮找零錢的所有各種發表方案,而是從最大面值的幣種開始,按遞減的順序考慮各幣種,先盡量用大面值的幣種,當不足大面值幣種的金額時才去考慮下一種較小面值的幣種。這就是在使用貪婪法。這種方法在這裡總是最優,是因為銀行對其發行的硬幣種類和硬幣面值的巧妙安排。如只有面值分別為1、5和11服務機構的硬幣,而希望找回總額為15服務機構的硬幣。按貪婪算法,應找1個11服務機構面值的硬幣和4個1服務機構面值的硬幣,共找回5個硬幣。但最優的解應是3個5服務機構面值的硬幣。
【問題】 裝箱問題
問題描述:裝箱問題可簡述如下:設有編號為0、1、…、n-1的n種物品,體積分別為v0、v1、…、vn-1。將這n種物品裝到容量都為V的若干箱子裡。約定這n種物品的體積均不超過V,即對於0≤i<n,有0<vi≤V。不同的裝箱方案所需要的箱子數目可能不同。裝箱問題要求使裝盡這n種物品的箱子數要少。
若考察將n種物品的集合分劃成n個或小於n個物品的所有子集,最優解就可以找到。但所有可能劃分的總數太大。對適當大的n,找出所有可能的劃分要花費的時間是無法承受的。為此,對裝箱問題採用非常簡單的近似算法,即貪婪法。該算法依次將物品放到它第一個能放進去的箱子中,該算法雖不能保證找到最優解,但還是能找到非常好的解。不失一般性,設n件物品的體積是按從大到小排好序的,即有v0≥v1≥…≥vn-1。如不滿足上述要求,只要先對這n件物品按它們的體積從大到小排序,然後按排序結果對物品重新編號即可。裝箱算法簡單描述如下:
{ 輸入箱子的容積;
輸入物品種數n;
按體積從大到小順序,輸入各物品的體積;
預置已用箱子鏈為空;
預置已用箱子計數器box_count為0;
for (i=0;i<n;i++)
{ 從已用的第一隻箱子開始順序尋找能放入物品i 的箱子j;
if (已用箱子都不能再放物品i)
{ 另用一個箱子,並將物品i放入該箱子;
box_count++;
}
else
將物品i放入箱子j;
}
}
上述算法能求出需要的箱子數box_count,並能求出各箱子所裝物品。下面的例子說明該算法不一定能找到最優解,設有6種物品,它們的體積分別為:60、45、35、20、20和20服務機構體積,箱子的容積為100個服務機構體積。按上述算法計算,需三隻箱子,各箱子所裝物品分別為:第一隻箱子裝物品1、3;第二隻箱子裝物品2、4、5;第三隻箱子裝物品6。而最優解為兩隻箱子,分別裝物品1、4、5和2、3、6。
若每隻箱子所裝物品用鏈表來表示,鏈表首結點游標存於一個結構中,結構記錄尚剩餘的空間量和該箱子所裝物品鏈表的首游標。另將全部箱子的訊息也構成鏈表。以下是按以上算法編寫的程序。
【程序】
# include <stdio.h>
# include <stdlib.h>
typedef struct ele
{ int vno;
struct ele *link;
} ELE;
typedef struct hnode
{ int remainder;
ELE *head;
Struct hnode *next;
} HNODE;

void main()
{ int n, i, box_count, box_volume, *a;
HNODE *box_h, *box_t, *j;
ELE *p, *q;
Printf(「輸入箱子容積\n」);
Scanf(「%d」,&box_volume);
Printf(「輸入物品種數\n」);
Scanf(「%d」,&n);
A=(int *)malloc(sizeof(int)*n);
Printf(「請按體積從大到小順序輸入各物品的體積:」);
For (i=0;i<n;i++) scanf(「%d」,a+i);
Box_h=box_t=NULL;
Box_count=0;
For (i=0;i<n;i++)
{ p=(ELE *)malloc(sizeof(ELE));
p->vno=i;
for (j=box_h;j!=NULL;j=j->next)
if (j->remainder>=a) break;
if (j==NULL)
{ j=(HNODE *)malloc(sizeof(HNODE));
j->remainder=box_volume-a;
j->head=NULL;
if (box_h==NULL) box_h=box_t=j;
else box_t=boix_t->next=j;
j->next=NULL;
box_count++;
}
else j->remainder-=a;
for (q=j->next;q!=NULL&&q->link!=NULL;q=q->link);
if (q==NULL)
{ p->link=j->head;
j->head=p;
}
else
{ p->link=NULL;
q->link=p;
}
}
printf(「共使用了%d只箱子」,box_count);
printf(「各箱子裝物品情況如下:」);
for (j=box_h,i=1;j!=NULL;j=j->next,i++)
{ printf(「第%2d只箱子,還剩餘容積%4d,所裝物品有;\n」,I,j->remainder);
for (p=j->head;p!=NULL;p=p->link)
printf(「%4d」,p->vno+1);
printf(「\n」);
}
}
【問題】 馬的遍歷
問題描述:在8×8方格的棋碟上,從任意指定的方格出發,為馬尋找一條走遍棋盤每一格並且只經過一次的一條路徑。
馬在某個方格,可以在一步內到達的不同位置最多有8個,如圖所顯示。如用二維陣列board[ ][ ]表示棋盤,其元素記錄馬經過該位置時的步驟號。另對馬的8種可能走法(稱為著法)設定一個順序,如當前位置在棋盤的(i,j)方格,下一個可能的位置依次為(i+2,j+1)、(i+1,j+2)、(i-1,j+2)、(i-2,j+1)、(i-2,j-1)、(i-1,j-2)、(i+1,j-2)、(i+2,j-1),實際可以走的位置盡限於還未走過的和不越出邊界的那些位置。為便於程序的同意處理,可以引入兩個陣列,分別儲存於各種可能走法對當前位置的縱橫增量。
4 3
5 2

6 1
7 0

對於本題,一般可以採用回溯法,這裡採用Warnsdoff原則求解,這也是一種貪婪法,其選項下一出口的貪婪標準是在那些允許走的位置中,選項出口最少的那個位置。如馬的當前位置(i,j)只有三個出口,他們是位置(i+2,j+1)、(i-2,j+1)和(i-1,j-2),如分別走到這些位置,這三個位置又分別會有不同的出口,假定這三個位置的出口個數分別為4、2、3,則程序就選項讓馬走向(i-2,j+1)位置。
由於程序採用的是一種貪婪法,整個找解程序是一直向前,沒有回溯,所以能非常快地找到解。但是,對於某些開始位置,實際上有解,而該算法不能找到解。對於找不到解的情況,程序只要改變8種可能出口的選項順序,就能找到解。改變出口選項順序,就是改變有相同出口時的選項標準。以下程序考慮到這種情況,引入變數start,用於控制8種可能著法的選項順序。開始時為0,當不能找到解時,就讓start增1,重新找解。細節以下程序。
【程序】
# include <stdio.h>
int delta_i[ ]={2,1,-1,-2,-2,-1,1,2};
int delta_j[ ]={1,2,2,1,-1,-2,-2,-1};
int board[8][8];
int exitn(int i,int j,int s,int a[ ])
{ int i1,j1,k,count;
for (count=k=0;k<8;k++)
{ i1=i+delta_i[(s+k)%8];
j1=i+delta_j[(s+k)%8];
if (i1>=0&&i1<8&&j1>=0&&j1<8&&board[I1][j1]==0)
a[count++]=(s+k)%8;
}
return count;
}

int next(int i,int j,int s)
{ int m,k,mm,min,a[8],b[8],temp;
m=exitn(i,j,s,a);
if (m==0) return –1;
for (min=9,k=0;k<m;k++)
{ temp=exitn(I+delta_i[a[k]],j+delta_j[a[k]],s,b);
if (temp<min)
{ min=temp;
kk=a[k];
}
}
return kk;
}

void main()
{ int sx,sy,i,j,step,no,start;
for (sx=0;sx<8;sx++)
for (sy=0;sy<8;sy++)
{ start=0;
do {
for (i=0;i<8;i++)
for (j=0;j<8;j++)
board[j]=0;
board[sx][sy]=1;
I=sx; j=sy;
For (step=2;step<64;step++)
{ if ((no=next(i,j,start))==-1) break;
I+=delta_i[no];
j+=delta_j[no];
board[j]=step;
}
if (step>64) break;
start++;
} while(step<=64)
for (i=0;i<8;i++)
{ for (j=0;j<8;j++)
printf(「%4d」,board[j]);
printf(「\n\n」);
}
scanf(「%*c」);
}
}
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
舊 2006-05-11, 08:35 PM   #7 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

七、分治法

1、分治法的基本思想
任何一個可以用電腦求解的問題所需的計算時間都與其規模N有關。問題的規模越小,越容易直接求解,解題所需的計算時間也越少。例如,對於n個元素的排序問題,當n=1時,不需任何計算;n=2時,只要作一次比較即可排好序;n=3時只要作3次比較即可,…。而當n較大時,問題就不那麼容易處理了。要想直接解決一個規模較大的問題,有時是相當困難的。
分治法的設計思想是,將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。
如果原問題可分割成k個子問題(1<k≤n),且這些子問題都可解,並可利用這些子問題的解求出原問題的解,那麼這種分治法就是可行的。由分治法產生的子問題往往是原問題的較小模式,這就為使用遞回技術提供了方便。在這種情況下,反覆套用分治手段,可以使子問題與原問題檔案類型一致而其規模卻不斷縮小,最終使子問題縮小到很容易直接求出其解。這自然導致遞回程序的產生。分治與遞回像一對孿生兄弟,經常同時套用在算法設計之中,並由此產生許多高效算法。
2、分治法的適用條件
分治法所能解決的問題一般具有以下幾個特徵:
(1)該問題的規模縮小到一定的程度就可以容易地解決;
(2)該問題可以分解為若干個規模較小的相同問題,即該問題具有最優子結構性質;
(3)利用該問題分解出的子問題的解可以合併為該問題的解;
(4)該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。
上述的第一條特徵是絕大多數問題都可以滿足的,因為問題的計算複雜性一般是隨著問題規模的增加而增加;第二條特徵是套用分治法的前提,它也是大多數問題可以滿足的,此特徵反映了遞回思想的套用;第三條特徵是關鍵,能否利用分治法完全取決於問題是否具有第三條特徵,如果具備了第一條和第二條特徵,而不具備第三條特徵,則可以考慮貪心法或動態規劃法。第四條特徵涉及到分治法的效率,如果各子問題是不獨立的,則分治法要做許多不必要的工作,重複地解公共的子問題,此時雖然可用分治法,但一般用動態規劃法較好。
3、分治法的基本步驟
分治法在每一層遞回上都有三個步驟:
(1)分解:將原問題分解為若干個規模較小,相互獨立,與原問題形式相同的子問題;
(2)解決:若子問題規模較小而容易被解決則直接解,否則遞回地解各個子問題;
(3)合併:將各個子問題的解合併為原問題的解。
它的一般的算法設計模式如下:
Divide_and_Conquer(P)
if |P|≤n0
then return(ADHOC(P))
將P分解為較小的子問題P1、P2、…、Pk
for i←1 to k
do
yi ← Divide-and-Conquer(Pi) △ 遞回解決Pi
T ← MERGE(y1,y2,…,yk) △ 合併子問題
Return(T)
其中 |P| 表示問題P的規模;n0為一閾值,表示當問題P的規模不超過n0時,問題已容易直接解出,不必再繼續分解。ADHOC(P)是該分治法中的基本子算法,用於直接解小規模的問題P。因此,當P的規模不超過n0時,直接用算法ADHOC(P)求解。
算法MERGE(y1,y2,…,yk)是該分治法中的合併子算法,用於將P的子問題P1、P2、…、Pk的相應的解y1、y2、…、yk合併為P的解。
根據分治法的分割原則,原問題應該分為多少個子問題才較適宜?各個子問題的規模應該怎樣才為適當?這些問題很難予以肯定的回答。但人們從大量實踐中發現,在用分治法設計算法時,最好使子問題的規模大致相同。換句話說,將一個問題分成大小相等的k個子問題的處理方法是行之有效的。許多問題可以取k=2。這種使子問題規模大致相等的做法是出自一種平衡子問題的思想,它幾乎總是比子問題規模不等的做法要好。
分治法的合併步驟是算法的關鍵所在。有些問題的合併方法比較明顯,有些問題合併方法比較複雜,或者是有多種合併方案;或者是合併方案不明顯。究竟應該怎樣合併,沒有統一的模式,需要具體問題具體分析。
【問題】 大整數乘法
問題描述:
通常,在分析一個算法的計算複雜性時,都將加法和乘法運算當作是基本運算來處理,即將執行一次加法或乘法運算所需的計算時間當作一個僅取決於電腦硬體處理速度的常數。
這個假定僅在電腦硬體能對參加運算的整數直接表示和處理時才是合理的。然而,在某些情況下,我們要處理很大的整數,它無法在電腦硬體能直接表示的範圍內進行處理。若用浮點數來表示它,則只能近似地表示它的大小,計算結果中的有效數位也受到限制。若要精確地表示大整數並在計算結果中要求精確地得到所有位數上的數位,就必須用軟體的方法來實現大整數的算術運算。
請設計一個有效的算法,可以進行兩個n位大整數的乘法運算。
設X和Y都是n位的二進制整數,現在要計算它們的乘積XY。我們可以用小學所學的方法來設計一個計算乘積XY的算法,但是這樣做計算步驟太多,顯得效率較低。如果將每2個1位數的乘法或加法看作一步運算,那麼這種方法要作O(n2)步運算才能求出乘積XY。下面我們用分治法來設計一個更有效的大整數乘積算法。

圖6-3 大整數X和Y的分段
我們將n位的二進制整數X和Y各分為2段,每段的長為n/2位(為簡單起見,假設n是2的冪),如圖6-3所顯示。
由此,X=A2n/2+B,Y=C2n/2+D。這樣,X和Y的乘積為:
XY=(A2n/2+B)(C2n/2+D)=AC2n+(AD+CB)2n/2+BD (1)
如果按式(1)計算XY,則我們必須進行4次n/2位整數的乘法(AC,AD,BC和BD),以及3次不超過n位的整數加法(分別對應於式(1)中的加號),此外還要做2次移位(分別對應於式(1)中乘2n和乘2n/2)。所有這些加法和移位共用O(n)步運算。設T(n)是2個n位整數相乘所需的運算總數,則由式(1),我們有:
(2)
由此可得T(n)=O(n2)。因此,用(1)式來計算X和Y的乘積並不比小學生的方法更有效。要想改進算法的計算複雜性,必須減少乘法次數。為此我們把XY寫成另一種形式:
XY=AC2n+[(A-B)(D-C)+AC+BD]2n/2+BD (3)
雖然,式(3)看起來比式(1)複雜些,但它僅需做3次n/2位整數的乘法(AC,BD和(A-B)(D-C)),6次加、減法和2次移位。由此可得:
(4)
用解遞回方程的套用公式法馬上可得其解為T(n)=O(nlog3)=O(n1.59)。利用式(3),並考慮到X和Y的符號對結果的影響,我們指出大整數相乘的完整算法MULT如下:
function MULT(X,Y,n); {X和Y為2個小於2n的整數,返回結果為X和Y的乘積XY}
begin
S=SIGN(X)*SIGN(Y); {S為X和Y的符號乘積}
X=ABS(X);
Y=ABS(Y); {X和Y分別取絕對值}
if n=1 then
if (X=1)and(Y=1) then return(S)
else return(0)
else begin
A=X的左邊n/2位;
B=X的右邊n/2位;
C=Y的左邊n/2位;
D=Y的右邊n/2位;
ml=MULT(A,C,n/2);
m2=MULT(A-B,D-C,n/2);
m3=MULT(B,D,n/2);
S=S*(m1*2n+(m1+m2+m3)*2n/2+m3);
return(S);
end;
end;
上述二進制大整數乘法同樣可套用於十進制大整數的乘法以提高乘法的效率減少乘法次數。
【問題】 最接近點對問題
問題描述:
在套用中,常用諸如點、圓等簡單的幾何對像代表現實世界中的實體。在涉及這些幾何對象的問題中,常需要瞭解其鄰域中其他幾何對象的訊息。例如,在空中交通控制問題中,若將飛機作為空間中移動的一個點來看待,則具有最大碰撞危險的2架飛機,就是這個空間中最接近的一對點。這類問題是計算幾何學中研究的基本問題之一。下面我們著重考慮平面上的最接近點對問題。
最接近點對問題的提法是:給定平面上n個點,找其中的一對點,使得在n個點的所有點對中,該點對的距離最小。
嚴格地說,最接近點對可能多於1對。為了簡單起見,這裡只限於找其中的一對。
這個問題很容易理解,似乎也不難解決。我們只要將每一點與其他n-1個點的距離算出,找出達到最小距離的兩個點即可。然而,這樣做效率太低,需要O(n2)的計算時間。我們能否找到問題的一個O (nlogn)算法。
這個問題顯然滿足分治法的第一個和第二個適用條件,我們考慮將所給的平面上n個點的集合S分成2個子集S1和S2,每個子集中約有n/2個點,然後在每個子集中遞回地求其最接近的點對。在這裡,一個關鍵的問題是如何實現分治法中的合併步驟,即由S1和S2的最接近點對,如何求得原集合S中的最接近點對,因為S1和S2的最接近點對未必就是S的最接近點對。如果組成S的最接近點對的2個點都在S1中或都在S2中,則問題很容易解決。但是,如果這2個點分別在S1和S2中,則對於S1中任一點p,S2中最多只有n/2個點與它構成最接近點對的候選者,仍需做n2/4次計算和比較才能確定S的最接近點對。因此,依此思法,合併步驟耗時為O(n2)。整個算法所需計算時間T(n)應滿足:
T(n)=2T(n/2)+O(n2)
它的解為T(n)=O(n2),即與合併步驟的耗時同階,顯示不出比用窮舉的方法好。從解遞回方程的套用公式法,我們看到問題出在合併步驟耗時太多。這啟發我們把注意力放在合併步驟上。
為了使問題易於理解和分析,我們先來考慮一維的情形。此時S中的n個點退化為x軸上的n個實數x1、x2、…、xn。最接近點對即為這n個實數中相差最小的2個實數。我們顯然可以先將x1、x2、…、xn排好序,然後,用一次線性掃瞄就可以找出最接近點對。這種方法主要計算時間花在排序上,因此如在排序算法中所證明的,耗時為O(nlogn)。然而這種方法無法直接推廣到二維的情形。因此,對這種一維的簡單情形,我們還是嘗試用分治法來求解,並希望能推廣到二維的情形。
假設我們用x軸上某個點m將S劃分為2個子集S1和S2,使得S1={x∈S | x≤m};S2={x∈S | x>m}。這樣一來,對於所有p∈S1和q∈S2有p<q。
遞回地在S1和S2上找出其最接近點對{p1,p2}和{q1,q2},並設δ=min{|p1-p2|,|q1-q2|},S中的最接近點對或者是{p1,p2},或者是{q1,q2},或者是某個{p3,q3},其中p3∈S1且q3∈S2。如圖1所顯示。

圖1 一維情形的分治法
我們注意到,如果S的最接近點對是{p3,q3},即 | p3-q3 | < δ,則p3和q3兩者與m的距離不超過δ,即 | p3-m | < δ,| q3-m | < δ,也就是說,p3∈(m-δ,m),q3∈(m,m+δ)。由於在S1中,每個長度為δ的半閉區間至多包含一個點(否則必有兩點距離小於δ),並且m是S1和S2的分割點,因此(m-δ,m)中至多包含S中的一個點。同理,(m,m+δ)中也至多包含S中的一個點。由圖1可以看出,如果(m-δ,m)中有S中的點,則此點就是S1中最大點。同理,如果(m,m+δ)中有S中的點,則此點就是S2中最小點。因此,我們用線性時間就能找到區間(m-δ,m)和(m,m+δ)中所有點,即p3和q3。從而我們用線性時間就可以將S1的解和S2的解合併成為S的解。也就是說,按這種分治原則,合併步可在O(n)時間內完成。這樣是否就可以得到一個有效的算法了呢?
還有一個問題需要認真考慮,即分割點m的選取,及S1和S2的劃分。選取分割點m的一個基本要求是由此匯出集合S的一個線性分割,即S=S1∪S2 ,S1∩S2=Φ,且S1 {x | x≤m};S2 {x | x>m}。容易看出,如果選取m=[max(S)+min(S)]/2,可以滿足線性分割的要求。選取分割點後,再用O(n)時間即可將S劃分成S1={x∈S | x≤m}和S2={x∈S | x>m}。然而,這樣選取分割點m,有可能造成劃分出的子集S1和S2的不平衡。例如在最壞情況下,|S1|=1,|S2|=n-1,由此產生的分治法在最壞情況下所需的計算時間T(n)應滿足遞回方程:
T(n)=T(n-1)+O(n)
它的解是T(n)=O(n2)。這種效率降低的現象可以通過份治法中「平衡子問題」的方法加以解決。也就是說,我們可以通過適當選項分割點m,使S1和S2中有大致相等個數的點。自然地,我們會想到用S的n個點的坐標的中位數來作分割點。在選項算法中介紹的選取中位數的線性時間算法使我們可以在O(n)時間內確定一個平衡的分割點m。
至此,我們可以設計出一個求一維點集S中最接近點對的距離的算法pair如下。
Float pair(S);
{ if | S | =2 δ= | x[2]-x[1] | /*x[1..n]存放的是S中n個點的坐標*/
else
{ if ( | S | =1) δ=∞
else
{ m=S中各點的坐標值的中位數;
構造S1和S2,使S1={x∈S | x≤m},S2={x∈S | x>m};
δ1=pair(S1);
δ2=pair(S2);
p=max(S1);
q=min(S2);
δ=min(δ1,δ2,q-p);
}
return(δ);
}
由以上的分析可知,該算法的分割步驟和合併步驟總共耗時O(n)。因此,算法耗費的計算時間T(n)滿足遞回方程:

解此遞回方程可得T(n)=O(nlogn)。

【問題】循環賽行事曆
問題描述:設有n=2k個運動員要進行網球循環賽。現要設計一個滿足以下要求的比賽行事曆:
(1)每個選手必須與其他n-1個選手各賽一次;
(2)每個選手一天只能參賽一次;
(3)循環賽在n-1天內結束。
請按此要求將比賽行事曆設計成有n行和n-1列的一個表。在表中的第i行,第j列處填入第i個選手在第j天所遇到的選手。其中1≤i≤n,1≤j≤n-1。
按分治原則,我們可以將所有的選手分為兩半,則n個選手的比賽行事曆可以通過n/2個選手的比賽行事曆來決定。遞回地用這種一分為二的原則對選手進行劃分,直到只剩下兩個選手時,比賽行事曆的制定就變得很簡單。這時只要讓這兩個選手進行比賽就可以了。

1 2 3 4 5 6 7
1 2 3 4 5 6 7 8
2 1 4 3 6 7 8 5
3 4 1 2 7 8 5 6
1 2 3 4 3 2 1 8 5 6 7
1 2 3 4 5 6 7 8 1 4 3 2
1 2 1 4 3 6 5 8 7 2 1 4 3
1 2 3 4 1 2 7 8 5 6 3 2 1 4
2 1 4 3 2 1 8 7 6 5 4 3 2 1
(1) (2) (3)
圖1 2個、4個和8個選手的比賽行事曆
圖1所列出的正方形表(3)是8個選手的比賽行事曆。其中左上角與左下角的兩小塊分別為選手1至選手4和選手5至選手8前3天的比賽日程。據此,將左上角小塊中的所有數位按其相對位置複製到右下角,又將左下角小塊中的所有數位按其相對位置複製到右上角,這樣我們就分別安排好了選手1至選手4和選手5至選手8在後4天的比賽日程。依此思想容易將這個比賽行事曆推廣到具有任意多個選手的情形。
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
舊 2006-05-11, 08:36 PM   #8 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

八、動態規劃法

經常會遇到複雜問題不能簡單地分解成幾個子問題,而會分解出一系列的子問題。簡單地採用把大問題分解成子問題,並綜合子問題的解匯出大問題的解的方法,問題求解耗時會按問題規模呈冪級數增加。
為了節約重複求相同子問題的時間,引入一個陣列,不管它們是否對最終解有用,把所有子問題的解存於該陣列中,這就是動態規劃法所採用的基本方法。以下先用實例說明動態規劃方法的使用。
【問題】 求兩字元序列的最長公共字元子序列
問題描述:字元序列的子序列是指從給定字元序列中隨意地(不一定連續)去掉若干個字元(可能一個也不去掉)後所形成的字元序列。令給定的字元序列X=「x0,x1,…,xm-1」,序列Y=「y0,y1,…,yk-1」是X的子序列,存在X的一個嚴格遞增上標序列<i0,i1,…,ik-1>,使得對所有的j=0,1,…,k-1,有xij=yj。例如,X=「ABCBDAB」,Y=「BCDB」是X的一個子序列。
給定兩個序列A和B,稱序列Z是A和B的公共子序列,是指Z同是A和B的子序列。問題要求已知兩序列A和B的最長公共子序列。
如採用列舉A的所有子序列,並一一檢查其是否又是B的子序列,並隨時記錄所發現的子序列,最終求出最長公共子序列。這種方法因耗時太多而不可取。
考慮最長公共子序列問題如何分解成子問題,設A=「a0,a1,…,am-1」,B=「b0,b1,…,bm-1」,並Z=「z0,z1,…,zk-1」為它們的最長公共子序列。不難證明有以下性質:
(1) 如果am-1=bn-1,則zk-1=am-1=bn-1,且「z0,z1,…,zk-2」是「a0,a1,…,am-2」和「b0,b1,…,bn-2」的一個最長公共子序列;
(2) 如果am-1!=bn-1,則若zk-1!=am-1,蘊涵「z0,z1,…,zk-1」是「a0,a1,…,am-2」和「b0,b1,…,bn-1」的一個最長公共子序列;
(3) 如果am-1!=bn-1,則若zk-1!=bn-1,蘊涵「z0,z1,…,zk-1」是「a0,a1,…,am-1」和「b0,b1,…,bn-2」的一個最長公共子序列。
這樣,在找A和B的公共子序列時,如有am-1=bn-1,則進一步解決一個子問題,找「a0,a1,…,am-2」和「b0,b1,…,bm-2」的一個最長公共子序列;如果am-1!=bn-1,則要解決兩個子問題,找出「a0,a1,…,am-2」和「b0,b1,…,bn-1」的一個最長公共子序列和找出「a0,a1,…,am-1」和「b0,b1,…,bn-2」的一個最長公共子序列,再取兩者中較長者作為A和B的最長公共子序列。
定義c[j]為序列「a0,a1,…,ai-2」和「b0,b1,…,bj-1」的最長公共子序列的長度,計算c[j]可遞回地表述如下:
(1)c[j]=0 如果i=0或j=0;
(2)c[j]= c[i-1][j-1]+1 如果I,j>0,且a[i-1]=b[j-1];
(3)c[j]=max(c[j-1],c[i-1][j]) 如果I,j>0,且a[i-1]!=b[j-1]。
按此算式可寫出計算兩個序列的最長公共子序列的長度函數。由於c[j]的產生僅依賴於c[i-1][j-1]、c[i-1][j]和c[j-1],故可以從c[m][n]開始,跟蹤c[j]的產生程序,逆向構造出最長公共子序列。細節見程序。
# include <stdio.h>
# include <string.h>
# define N 100
char a[N],b[N],str[N];

int lcs_len(char *a, char *b, int c[ ][ N])
{ int m=strlen(a), n=strlen(b), i,j;
for (i=0;i<=m;i++) c[0]=0;
for (i=0;i<=n;i++) c[0]=0;
for (i=1;i<=m;i++)
for (j=1;j<=m;j++)
if (a[i-1]==b[j-1])
c[j]=c[i-1][j-1]+1;
else if (c[i-1][j]>=c[j-1])
c[j]=c[i-1][j];
else
c[j]=c[j-1];
return c[m][n];
}

char *buile_lcs(char s[ ],char *a, char *b)
{ int k, i=strlen(a), j=strlen(b);
k=lcs_len(a,b,c);
s[k]=』\0』;
while (k>0)
if (c[j]==c[i-1][j]) i--;
else if (c[j]==c[j-1]) j--;
else { s[--k]=a[i-1];
i--; j--;
}
return s;
}

void main()
{ printf (「Enter two string(<%d)!\n」,N);
scanf(「%s%s」,a,b);
printf(「LCS=%s\n」,build_lcs(str,a,b));
}
1、動態規劃的適用條件
任何思想方法都有一定的局限性,超出了特定條件,它就失去了作用。同樣,動態規劃也並不是萬能的。適用動態規劃的問題必須滿足最最佳化原理和無後效性。
(1)最最佳化原理(最優子結構性質)
最最佳化原理可這樣闡述:一個最最佳化原則具有這樣的性質,不論過去狀態和決策如何,對前面的決策所形成的狀態而言,餘下的諸決策必須構成最優原則。簡而言之,一個最最佳化原則的子原則總是最優的。一個問題滿足最最佳化原理又稱其具有最優子結構性質。

圖2
例如圖2中,若路線I和J是A到C的最優路徑,則根據最最佳化原理,路線J必是從B到C的最優路線。這可用反證法證明:假設有另一路徑J』是B到C的最優路徑,則A到C的路線取I和J』比I和J更優,矛盾。從而證明J』必是B到C的最優路徑。
最最佳化原理是動態規劃的基礎,任何問題,如果失去了最最佳化原理的支持,就不可能用動態規劃方法計算。根據最最佳化原理匯出的動態規劃基本方程是解決一切動態規劃問題的基本方法。
(2)無後向性
將各階段按照一定的次序排列好之後,對於某個給定的階段狀態,它以前各階段的狀態無法直接影響它未來的決策,而只能通過現用的這個狀態。換句話說,每個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱為無後效性。
(3)子問題的重疊性
動態規划算法的關鍵在於解決冗余,這是動態規划算法的根本目的。動態規劃實質上是一種以空間換時間的技術,它在實現的程序中,不得不儲存於產生程序中的各種狀態,所以它的空間複雜度要大於其它的算法。選項動態規划算法是因為動態規划算法在空間上可以承受,而搜尋算法在時間上卻無法承受,所以我們捨空間而取時間。
所以,能夠用動態規劃解決的問題還有一個顯著特徵:子問題的重疊性。這個性質並不是動態規劃適用的必要條件,但是如果該性質無法滿足,動態規划算法同其他算法相比就不具備優勢。
2、動態規劃的基本思想
前文主要介紹了動態規劃的一些理論依據,我們將前文所說的具有明顯的階段劃分和狀態轉移方程的動態規劃稱為標準動態規劃,這種標準動態規劃是在研究多階段決策問題時推匯出來的,具有嚴格的數學形式,適合用於理論上的分析。在實際套用中,許多問題的階段劃分並不明顯,這時如果刻意地劃分階段法反而麻煩。一般來說,只要該問題可以劃分成規模更小的子問題,並且原問題的最優解中包含了子問題的最優解(即滿足最優子化原理),則可以考慮用動態規劃解決。
動態規劃的實質是分治思想和解決冗余,因此,動態規劃是一種將問題實例分解為更小的、相似的子問題,並儲存於子問題的解而避免計算重複的子問題,以解決最最佳化問題的算法原則。
由此可知,動態規劃法與分治法和貪心法類似,它們都是將問題實例歸納為更小的、相似的子問題,並通過求解子問題產生一個全局最優解。其中貪心法的當前選項可能要依賴已經作出的所有選項,但不依賴於有待於做出的選項和子問題。因此貪心法自頂向下,一步一步地作出貪心選項;而分治法中的各個子問題是獨立的(即不包含公共的子子問題),因此一旦遞回地求出各子問題的解後,便可自下而上地將子問題的解合併成問題的解。但不足的是,如果當前選項可能要依賴子問題的解時,則難以通過局部的貪心原則達到全局最優解;如果各子問題是不獨立的,則分治法要做許多不必要的工作,重複地解公共的子問題。
解決上述問題的辦法是利用動態規劃。該方法主要套用於最最佳化問題,這類問題會有多種可能的解,每個解都有一個值,而動態規劃找出其中最優(最大或最小)值的解。若存在若干個取最優值的解的話,它只取其中的一個。在求解程序中,該方法也是通過求解局部子問題的解達到全局最優解,但與分治法和貪心法不同的是,動態規劃允許這些子問題不獨立,(亦即各子問題可包含公共的子子問題)也允許其通過自身子問題的解作出選項,該方法對每一個子問題只解一次,並將結果儲存起來,避免每次碰到時都要重複計算。
因此,動態規劃法所針對的問題有一個顯著的特徵,即它所對應的子問題樹中的子問題呈現大量的重複。動態規劃法的關鍵就在於,對於重複出現的子問題,只在第一次遇到時加以求解,並把答案儲存起來,讓以後再遇到時直接引用,不必重新求解。
3、動態規划算法的基本步驟
設計一個標準的動態規划算法,通常可按以下幾個步驟進行:
(1)劃分階段:按照問題的時間或空間特徵,把問題分為若干個階段。注意這若干個階段一定要是有序的或者是可排序的(即無後向性),否則問題就無法用動態規劃求解。
(2)選項狀態:將問題發展到各個階段時所處於的各種客觀情況用不同的狀態表示出來。當然,狀態的選項要滿足無後效性。
(3)確定決策並寫出狀態轉移方程:之所以把這兩步放在一起,是因為決策和狀態轉移有著天然的聯繫,狀態轉移就是根據上一階段的狀態和決策來匯出本階段的狀態。所以,如果我們確定了決策,狀態轉移方程也就寫出來了。但事實上,我們常常是反過來做,根據相鄰兩段的各狀態之間的關係來確定決策。
(4)寫出規劃方程(包括邊界條件):動態規劃的基本方程是規劃方程的通用形式化陳述式。
一般說來,只要階段、狀態、決策和狀態轉移確定了,這一步還是比較簡單的。動態規劃的主要難點在於理論上的設計,一旦設計完成,實現部分就會非常簡單。根據動態規劃的基本方程可以直接遞回計算最優值,但是一般將其改為遞推計算,實現的大體上的框架如下:
標準動態規劃的基本框架
1. 對fn+1(xn+1)啟始化; {邊界條件}
for k:=n downto 1 do
for 每一個xk∈Xk do
for 每一個uk∈Uk(xk) do
begin
5. fk(xk):=一個極值; {∞或-∞}
6. xk+1:=Tk(xk,uk); {狀態轉移方程}
7. t:=φ(fk+1(xk+1),vk(xk,uk)); {基本方程(9)式}
if t比fk(xk)更優 then fk(xk):=t; {計算fk(xk)的最優值}
end;
9. t:=一個極值; {∞或-∞}
for 每一個x1∈X1 do
11. if f1(x1)比t更優 then t:=f1(x1); {按照10式求出最優指標}
12. 輸出t;
但是,實際套用當中經常不顯式地按照上面步驟設計動態規劃,而是按以下幾個步驟進行:
(1)分析最優解的性質,並刻劃其結構特徵。
(2)遞回地定義最優值。
(3)以自底向上的方式或自頂向下的記憶化方法(備忘錄法)計算出最優值。
(4)根據計算最優值時得到的訊息,構造一個最優解。
步驟(1)~(3)是動態規划算法的基本步驟。在只需要求出最優值的情形,步驟(4)可以省略,若需要求出問題的一個最優解,則必須執行步驟(4)。此時,在步驟(3)中計算最優值時,通常需記錄更多的訊息,以便在步驟(4)中,根據所記錄的訊息,快速地構造出一個最優解。

【問題】 凸多邊形的最優三角剖分問題
問題描述:多邊形是平面上一條分段線性的閉曲線。也就是說,多邊形是由一系列首尾相接的直線段組成的。組成多邊形的各直線段稱為該多邊形的邊。多邊形相接兩條邊的連接點稱為多邊形的頂點。若多邊形的邊之間除了連接頂點外沒有別的公共點,則稱該多邊形為簡單多邊形。一個簡單多邊形將平面分為3個部分:被包圍在多邊形內的所有點構成了多邊形的內部;多邊形本身構成多邊形的邊界;而平面上其餘的點構成了多邊形的外部。當一個簡單多邊形及其內部構成一個閉凸集時,稱該簡單多邊形為凸多邊形。也就是說凸多邊形邊界上或內部的任意兩點所連成的直線段上所有的點均在該凸多邊形的內部或邊界上。
通常,用多邊形頂點的逆時針序列來表示一個凸多邊形,即P=<v0,v1,…,vn-1>表示具有n條邊v0v1,v1v2,…,vn-1vn的一個凸多邊形,其中,約定v0=vn 。
若vi與vj是多邊形上不相鄰的兩個頂點,則線段vivj稱為多邊形的一條弦。弦將多邊形分割成凸的兩個子多邊形<vi,vi+1,…,vj>和<vj,vj+1,…,vi>。多邊形的三角剖分是一個將多邊形分割成互不重迭的三角形的弦的集合T。圖1是一個凸多邊形的兩個不同的三角剖分。

(a) (b)
圖1 一個凸多邊形的2個不同的三角剖分
在凸多邊形P的一個三角剖分T中,各弦互不相交且弦數已達到最大,即P的任一不在T中的弦必與T中某一弦相交。在一個有n個頂點的凸多邊形的三角刮分中,恰好有n-3條弦和n-2個三角形。
凸多邊形最優三角剖分的問題是:給定一個凸多邊形P=<v0,v1,…,vn-1>以及定義在由多邊形的邊和弦組成的三角形上的權函數ω。要求確定該凸多邊形的一個三角剖分,使得該三角剖分對應的權即剖分中諸三角形上的權之和為最小。
可以定義三角形上各種各樣的權函數ω。例如:定義ω(△vivjvk)=| vivj |+| vivk |+| vkvj |,其中,| vivj |是點vi到vj的歐氏距離。相應於此權函數的最優三角剖分即為最小弦長三角剖分。
(1)最優子結構性質
凸多邊形的最優三角剖分問題有最優子結構性質。事實上,若凸(n+1)邊形P=<v0,v1 ,…,vn>的一個最優三角剖分T包含三角形v0vkvn,1≤k≤n-1,則T的權為3個部分權的和,即三角形v0vkvn的權,子多邊形<v0,v1,…,vk>的權和<vk,vk+1,…,vn>的權之和。可以斷言由T所確定的這兩個子多邊形的三角剖分也是最優的,因為若有<v0,v1,…,vk>或<vk,vk+1,…,vn>的更小權的三角剖分,將會導致T不是最優三角剖分的矛盾。
(2)最優三角剖分對應的權的遞回結構
首先,定義t[i,j](1≤i<j≤n)為凸子多邊形<vi-1,vi,…,vj>的最優三角剖分所對應的權值,即最優值。為方便起見,設退化的多邊形<vi-1,vi>具有權值0。據此定義,要計算的凸(n+1)邊多邊形P對應的權的最優值為t[1,n]。
t[i,j]的值可以利用最優子結構性質遞回地計算。由於退化的2頂點多邊形的權值為0,所以t[i,i]=0,i=1,2,…,n 。當j一i≥1時,子多邊形<vi-1,vi,…,vj>至少有3個頂點。由最優於結構性質,t[i,j]的值應為t[i,k]的值加上t[k+1,j]的值,再加上△vi-1vkvj的權值,並在i≤k≤j-1的範圍內取最小。由此,t[i,j]可遞回地定義為:

(3)計算最優值
下面描述的計算凸(n+1)邊形P=<v0,v1,…,vn>的三角剖分最優權值的動態規划算法MINIMUM_WEIGHT,輸入是凸多邊形P=<v0,v1,…,vn>的權函數ω,輸出是最優值t[i,j]和使得t[i,k]+t[k+1,j]+ω(△vi-1vkvj)達到最優的位置(k=)s[i,j],1≤i≤j≤n 。
Procedure MINIMUM_WEIGHT(P,w);
Begin
n=length[p]-1;
for i=1 to n do t[i,i]:=0;
for ll=2 to n do
for i=1 to n-ll+1 do
begin
j=i+ll-1;
t[i,j]=∞;
for k=i to j-1 do
begin
q=t[i,k]+t[k+1,j]+ω(△vi-1vkvj);
if q<t[i,j] then
begin
t[i,j]=q;
s[i,j]=k;
end;
end;
end;
return(t,s);
end;
算法MINIMUM_WEIGHT_佔用θ(n2)空間,耗時θ(n3)。
(4)構造最優三角剖分
如我們所看到的,對於任意的1≤i≤j≤n ,算法MINIMUM_WEIGHT在計算每一個子多邊形<vi-1,vi,…,vj>的最優三角剖分所對應的權值t[i,j]的同時,還在s[i,j]中記錄了此最優三角剖分中與邊(或弦)vi-1vj構成的三角形的第三個頂點的位置。因此,利用最優子結構性質並借助於s[i,j],1≤i≤j≤n ,凸(n+l)邊形P=<v0,v1,…,vn>的最優三角剖分可容易地在Ο(n)時間內構造出來。

習題:
1、汽車加油問題:
設有路程長度為L公里的公路上,分佈著m個加油站,它們的位置分別為p(i=1,2,……,m),而汽車油箱加滿油後(油箱最多可以加油k升),可以行駛n公里。設計一個方案,使汽車經過此公路的加油次數盡量少(汽車出發時是加滿油的)。
2、最短路徑:
設有一個網路,要求從某個頂點出發到其他頂點的最短路徑
3、跳馬問題:
在8*8方格的棋碟上,從任意指定的方格出發,為馬尋找一條走遍棋盤每一格並且只經過一次的一條路徑。
4、二叉樹的遍歷
5、背包問題
6、用分治法實現兩個大整數相乘
7、設x1,x2,…,xn是直線上的n個點,若要用服務機構長度的閉區間去覆蓋這n個點,至少需要多少個這樣的服務機構閉區間?
8、用關係「<」和「=」將3個數A、B和C依次排列時,有13種不同的序關係:
A=B=C,A=B<C,A<B=C,A<B<C,A<C<B,A=C<B,B<A=C,
B<A<C,B<C<A,B=C<A,C<A=B,C<A<B,C<A<B。
若要將n個數依序進行排列,試設計一個動態規划算法,計算出有多少鍾不同的序關係。
9、有一種單人玩的遊戲:設有n(2<=n<=200)堆薄片,各堆順序用0至 n-1編號,極端情況,有的堆可能沒有薄片。在遊戲程序中,一次移動只能取某堆上的若干張薄片,移到該堆的相鄰堆上。如指定
I堆k張 k 移到I-1(I>0)堆,和將k 張薄片移至I+1(I<n-1)堆。所以當有兩個堆與 I 堆相鄰 時,I堆原先至少有2k 張薄片;只有一個堆與 I 堆相鄰 時, I 堆原先至少有k張薄片。
遊戲的目標是對給定的堆數,和各堆上的薄片數,按上述規則移動薄片,最終使 各堆的薄片數相同。為了使移動次數較少些,移動哪一堆薄片,和移多少薄片先作以下估算:

ci:I堆的薄片數(0<=I<n,0<=ci<=200);
v:每堆 的平均薄片數;
ai:I堆的相鄰堆可以從I堆得到的薄片數。
估算方法如下:
v=c0+a1-a0 a1=v+a0-c0
v=c1+a0+a2-2a1 a2=v+2a1-a0-c1
…….. ……….
V=ci+ai-1+ai+1-2aI ai+1=v+2ai-ai-1-ci
這裡並不希望準確地求出A0 至an-1,而是作以下處理:若令 a0 為0,能按上述算式計算出 A1至 an-1。程序找出 a 中的最小值,並讓全部a值減去這最小值,使每堆移去的薄片數大於等於0。
實際操作採用以下貪心原則:
(1)每次從第一堆出發順序搜尋每一堆,若發現可從 I堆移走薄片,就完成一次移動。即, I堆的相鄰堆從 I堆取走 ai片薄片。可從I 堆移薄片到相鄰堆取於 I堆薄片數:若I 堆是處於兩端位置( I=0 I=n-1), 要求 ci>=ai ;若 I堆是中間堆,則要求ci>=2ai。
(2)因在ai>0的所有堆中,薄片數最多的堆 在平分程序中被它的相鄰堆取走的薄片數也最多。在用原則(1)搜尋移動時,當發生沒有滿足條件(1)的可移走薄片的堆時,採用本原則,讓在ai>0的所有堆中,薄片數最多的堆被它的相鄰堆取走它的全部薄片。
psac 目前離線  
送花文章: 3, 收花文章: 1630 篇, 收花: 3204 次
 


主題工具
顯示模式

發表規則
不可以發文
不可以回覆主題
不可以上傳附加檔案
不可以編輯您的文章

論壇啟用 BB 語法
論壇啟用 表情符號
論壇啟用 [IMG] 語法
論壇禁用 HTML 語法
Trackbacks are 禁用
Pingbacks are 禁用
Refbacks are 禁用


所有時間均為台北時間。現在的時間是 08:22 PM


Powered by vBulletin® 版本 3.6.8
版權所有 ©2000 - 2020, Jelsoft Enterprises Ltd.


SEO by vBSEO 3.6.1