1 1.1 第1章─概論 2 3 1.1.1 練習題 4 1. 下列關於算法的說法中正確的有( )。 5 Ⅰ.求解某一類問題的算法是惟一的 6 Ⅱ.算法必須在有限步操做以後中止 7 Ⅲ.算法的每一步操做必須是明確的,不能有歧義或含義模糊 8 Ⅳ.算法執行後必定產生肯定的結果 9 A. 1個 B.2個 C.3個 D.4個 10 2. T(n)表示當輸入規模爲n時的算法效率,如下算法效率最優的是( )。 11 A.T(n)= T(n-1)+1,T(1)=1 B.T(n)= 2n2 12 C.T(n)= T(n/2)+1,T(1)=1 D.T(n)=3nlog2n 13 3. 什麼是算法?算法有哪些特徵? 14 4. 判斷一個大於2的正整數n是否爲素數的方法有多種,給出兩種算法,說明其中 15 一種算法更好的理由。 16 5. 證實如下關係成立: 17 (1)10n2-2n=(n2) 18 (2)2n+1=(2n) 19 6. 證實O(f(n))+O(g(n))=O(max{f(n),g(n)}) 。 20 7. 有一個含n(n>2)個整數的數組a,判斷其中是否存在出現次數超過全部元素一 21 半的元素。 22 8. 一個字符串採用string對象存儲,設計一個算法判斷該字符串是否爲迴文。 23 9. 有一個整數序列,設計一個算法判斷其中是否存在兩個元素和剛好等於給定的整 24 數k。 25 10. 有兩個整數序列,每一個整數序列中全部元素均不相同。設計一個算法求它們的公 共元素,要求不使用STL的集合算法。 26 11. 正整數n(n>1)能夠寫成質數的乘積形式,稱爲整數的質因數分解。例如, 12=2*2*3,18=2*3*3,11=11。設計一個算法求n這樣分解後各個質因數出現的次數,採 用vector向量存放結果。 27 12. 有一個整數序列,全部元素均不相同,設計一個算法求相差最小的元素對的個 數。如序列四、1、2、3的相差最小的元素對的個數是3,其元素對是(1,2),(2,3), (3,4)。 28 13. 有一個map<string,int>容器,其中已經存放了較多元素。設計一個算法求出其 中重複的value而且返回重複value的個數。 29 14. 從新作第10題,採用map容器存放最終結果。 30 15. 假設有一個含n(n>1)個元素的stack<int>棧容器st,設計一個算法出棧從棧頂 31 到棧底的第k(1≤k≤n)個元素,其餘棧元素不變。 32 33 算法設計 34 35 1.1.2 練習題參考答案 36 1. 答:因爲算法具備有窮性、肯定性和輸出性,於是Ⅱ、Ⅲ、Ⅳ正確,而解決某一 37 類問題的算法不必定是惟一的。答案爲C。 38 2. 答:選項A的時間複雜度爲O(n)。選項B的時間複雜度爲O(n2)。選項C的時間 39 複雜度爲O(log2n)。選項D的時間複雜度爲O(nlog2n)。答案爲C。 40 3. 答:算法是求解問題的一系列計算步驟。算法具備有限性、肯定性、可行性、輸 41 入性和輸出性5個重要特徵。 42 4. 答:兩種算法以下: 43 #include <stdio.h> 44 #include <math.h> 45 bool isPrime1(int n) //方法1 46 { for (int i=2;i<n;i++) 47 if (n%i==0) 48 return false; 49 return true; 50 } 51 bool isPrime2(int n) //方法2 52 { for (int i=2;i<=(int)sqrt(n);i++) 53 if (n%i==0) 54 return false; 55 return true; 56 } 57 void main() 58 { int n=5; 59 printf("%d,%d\n",isPrime1(n),isPrime2(n)); 60 } 61 方法1的時間複雜度爲O(n),方法2的時間複雜度爲n,因此方法2更好。 62 5. 答:(1)當n足夠大時,(10n2-2n)/( n2)=10,因此10n2-2n=(n2)。 63 (2)2n+1=2*2n=(2n)。 64 6. 證實:對於任意f1(n)∈O(f(n)) ,存在正常數c1和正常數n1,使得對全部n≥n1, 65 有f1(n)≤c1f(n) 。 66 相似地,對於任意g1(n)∈O(g(n)) ,存在正常數c2和天然數n2,使得對全部n≥n2, 67 有g1(n)≤c2g(n) 。 68 令c3=max{c1,c2},n3=max{n1,n2},h(n)= max{f(n),g(n)} 。 69 則對全部的n≥n3,有: 70 f1(n) +g1(n)≤c1f(n) + c2g(n)≤c3f(n)+c3g(n)=c3(f(n)+g(n)) 71 ≤c32max{f(n),g(n)}=2c3h(n)=O(max{f(n),g(n)})。 72 7. 解:先將a中元素遞增排序,再求出現次數最多的次數maxnum,最後判斷是否滿 73 足條件。對應的程序以下: 74 #include <stdio.h> 75 #include <algorithm> 76 using namespace std; 77 78 2 79 第1章 概論 80 81 bool solve(int a[],int n,int &x) 82 { sort(a,a+n); //遞增排序 83 int maxnum=0; //出現次數最多的次數 84 int num=1; 85 int e=a[0]; 86 for (int i=1;i<n;i++) 87 { if (a[i]==e) 88 { num++; 89 if (num>maxnum) 90 { maxnum=num; 91 x=e; 92 } 93 } 94 else 95 { e=a[i]; 96 num=1; 97 } 98 } 99 if (maxnum>n/2) 100 return true; 101 else 102 return false; 103 } 104 void main() 105 { int a[]={2,2,2,4,5,6,2}; 106 int n=sizeof(a)/sizeof(a[0]); 107 int x; 108 if (solve(a,n,x)) 109 printf("出現次數超過全部元素一半的元素爲%d\n",x); else 110 printf("不存在出現次數超過全部元素一半的元素\n"); } 111 上述程序的執行結果如圖1.1所示。 112 113 114 115 116 圖1.1 程序執行結果 117 8. 解:採用先後字符判斷方法,對應的程序以下: 118 #include <iostream> 119 #include <string> 120 using namespace std; 121 bool solve(string str) //判斷字符串str是否爲迴文 { int i=0,j=str.length()-1; 122 while (i<j) 123 { if (str[i]!=str[j]) 124 return false; 125 126 3 127 128 算法設計 129 130 i++; j--; 131 } 132 return true; 133 } 134 void main() 135 { cout << "求解結果" << endl; 136 string str="abcd"; 137 cout << " " << str << (solve(str)?"是迴文":"不是迴文") << endl; 138 string str1="abba"; 139 cout << " " << str1 << (solve(str1)?"是迴文":"不是迴文") << endl; } 140 上述程序的執行結果如圖1.2所示。 141 142 143 144 145 146 圖1.2 程序執行結果 147 9. 解:先將a中元素遞增排序,而後從兩端開始進行判斷。對應的程序以下: 148 #include <stdio.h> 149 #include <algorithm> 150 using namespace std; 151 bool solve(int a[],int n,int k) 152 { sort(a,a+n); //遞增排序 153 int i=0, j=n-1; 154 while (i<j) //區間中存在兩個或者以上元素 155 { if (a[i]+a[j]==k) 156 return true; 157 else if (a[i]+a[j]<k) 158 i++; 159 else 160 j--; 161 } 162 return false; 163 } 164 void main() 165 { int a[]={1,2,4,5,3}; 166 int n=sizeof(a)/sizeof(a[0]); 167 printf("求解結果\n"); 168 int k=9,i,j; 169 if (solve(a,n,k,i,j)) 170 printf(" 存在: %d+%d=%d\n",a[i],a[j],k); 171 else 172 printf(" 不存在兩個元素和爲%d\n",k); 173 int k1=10; 174 if (solve(a,n,k1,i,j)) 175 printf(" 存在: %d+%d=%d\n",a[i],a[j],k1); 176 4 177 第1章 概論 178 179 else 180 printf(" 不存在兩個元素和爲%d\n",k1); } 181 上述程序的執行結果如圖1.3所示。 182 183 184 185 186 187 圖1.3 程序執行結果 188 10. 解:採用集合set<int>存儲整數序列,集合中元素默認是遞增排序的,再採用二 189 路歸併算法求它們的交集。對應的程序以下: 190 #include <stdio.h> 191 #include <set> 192 using namespace std; 193 void solve(set<int> s1,set<int> s2,set<int> &s3) //求交集s3 194 { set<int>::iterator it1,it2; 195 it1=s1.begin(); it2=s2.begin(); 196 while (it1!=s1.end() && it2!=s2.end()) 197 { if (*it1==*it2) 198 { s3.insert(*it1); 199 ++it1; ++it2; 200 } 201 else if (*it1<*it2) 202 ++it1; 203 else 204 ++it2; 205 } 206 } 207 void dispset(set<int> s) //輸出集合的元素 208 { set<int>::iterator it; 209 for (it=s.begin();it!=s.end();++it) 210 printf("%d ",*it); 211 printf("\n"); 212 } 213 void main() 214 { int a[]={3,2,4,8}; 215 int n=sizeof(a)/sizeof(a[0]); 216 set<int> s1(a,a+n); 217 int b[]={1,2,4,5,3}; 218 int m=sizeof(b)/sizeof(b[0]); 219 set<int> s2(b,b+m); 220 set<int> s3; 221 solve(s1,s2,s3); 222 printf("求解結果\n"); 223 printf(" s1: "); dispset(s1); 224 225 5 226 227 228 printf(" s2: "); dispset(s2); printf(" s3: "); dispset(s3); } 229 上述程序的執行結果如圖1.4所示。 230 231 232 算法設計 233 234 235 236 237 238 239 圖1.4 程序執行結果 240 11. 解:對於正整數n,從i=2開始查找其質因數,ic記錄質因數i出現的次數,當找 241 到這樣質因數後,將(i,ic)做爲一個元素插入到vector容器v中。最後輸出v。對應的 算法以下: 242 #include <stdio.h> 243 #include <vector> 244 using namespace std; 245 struct NodeType //vector向量元素類型 246 { int p; //質因數 247 int pc; //質因數出現次數 248 }; 249 void solve(int n,vector<NodeType> &v) //求n的質因數分解 250 { int i=2; 251 int ic=0; 252 NodeType e; 253 do 254 { if (n%i==0) 255 { ic++; 256 n=n/i; 257 } 258 else 259 { if (ic>0) 260 { e.p=i; 261 e.pc=ic; 262 v.push_back(e); 263 } 264 ic=0; 265 i++; 266 } 267 } while (n>1 || ic!=0); 268 } 269 void disp(vector<NodeType> &v) //輸出v 270 { vector<NodeType>::iterator it; 271 for (it=v.begin();it!=v.end();++it) 272 printf(" 質因數%d出現%d次\n",it->p,it->pc); 273 } 274 275 6 276 277 278 void main() 279 { vector<NodeType> v; 280 int n=100; 281 printf("n=%d\n",n); 282 solve(n,v); 283 disp(v); 284 } 285 上述程序的執行結果如圖1.5所示。 286 287 第1章 概論 288 289 290 291 292 293 圖1.5 程序執行結果 294 12. 解:先遞增排序,再求相鄰元素差,比較求最小元素差,累計最小元素差的個 295 數。對應的程序以下: 296 #include <iostream> 297 #include <algorithm> 298 #include <vector> 299 using namespace std; 300 int solve(vector<int> &myv) //求myv中相差最小的元素對的個數 301 { sort(myv.begin(),myv.end()); //遞增排序 302 int ans=1; 303 int mindif=myv[1]-myv[0]; 304 for (int i=2;i<myv.size();i++) 305 { if (myv[i]-myv[i-1]<mindif) 306 { ans=1; 307 mindif=myv[i]-myv[i-1]; 308 } 309 else if (myv[i]-myv[i-1]==mindif) 310 ans++; 311 } 312 return ans; 313 } 314 void main() 315 { int a[]={4,1,2,3}; 316 int n=sizeof(a)/sizeof(a[0]); 317 vector<int> myv(a,a+n); 318 cout << "相差最小的元素對的個數: " << solve(myv) << endl; 319 } 320 上述程序的執行結果如圖1.6所示。 321 322 323 324 325 326 7 327 328 算法設計 329 330 圖1.6 程序執行結果 331 13. 解:對於map<string,int>容器mymap,設計另一個map<int,int>容器tmap, 332 將前者的value做爲後者的關鍵字。遍歷mymap,累計tmap中相同關鍵字的次數。一個 參考程序及其輸出結果以下: 333 #include <iostream> 334 #include <map> 335 #include <string> 336 using namespace std; 337 void main() 338 { map<string,int> mymap; 339 mymap.insert(pair<string,int>("Mary",80)); 340 mymap.insert(pair<string,int>("Smith",82)); 341 mymap.insert(pair<string,int>("John",80)); 342 mymap.insert(pair<string,int>("Lippman",95)); 343 mymap.insert(pair<string,int>("Detial",82)); 344 map<string,int>::iterator it; 345 map<int,int> tmap; 346 for (it=mymap.begin();it!=mymap.end();it++) 347 tmap[(*it).second]++; 348 map<int,int>::iterator it1; 349 cout << "求解結果" << endl; 350 for (it1=tmap.begin();it1!=tmap.end();it1++) 351 cout << " " << (*it1).first << ": " << (*it1).second << "次\n"; 352 } 353 上述程序的執行結果如圖1.7所示。 354 355 356 357 358 359 360 圖1.7 程序執行結果 361 14. 解:採用map<int,int>容器mymap存放求解結果,第一個份量存放質因數,第 362 二個份量存放質因數出現次數。對應的程序以下: 363 #include <stdio.h> 364 #include <map> 365 using namespace std; 366 void solve(int n,map<int,int> &mymap) //求n的質因數分解 367 { int i=2; 368 int ic=0; 369 do 370 { if (n%i==0) 371 { ic++; 372 n=n/i; 373 } 374 8 375 第1章 概論 376 377 else 378 { if (ic>0) 379 mymap[i]=ic; 380 ic=0; 381 i++; 382 } 383 } while (n>1 || ic!=0); 384 } 385 void disp(map<int,int> &mymap) //輸出mymap 386 { map<int,int>::iterator it; 387 for (it=mymap.begin();it!=mymap.end();++it) 388 printf(" 質因數%d出現%d次\n",it->first,it->second); } 389 void main() 390 { map<int,int> mymap; 391 int n=12345; 392 printf("n=%d\n",n); 393 solve(n,mymap); 394 disp(mymap); 395 } 396 上述程序的執行結果如圖1.8所示。 397 398 399 400 401 402 403 圖1.8 程序執行結果 404 15. 解:棧容器不能順序遍歷,爲此建立一個臨時tmpst棧,將st的k個元素出棧並 405 進棧到tmpst中,再出棧tmpst一次獲得第k個元素,最後將棧tmpst的全部元素出棧並進 棧到st中。對應的程序以下: 406 #include <stdio.h> 407 #include <stack> 408 using namespace std; 409 int solve(stack<int> &st,int k) //出棧第k個元素 410 { stack<int> tmpst; 411 int e; 412 for (int i=0;i<k;i++) //出棧st的k個元素並進tmpst棧 413 { e=st.top(); 414 st.pop(); 415 tmpst.push(e); 416 } 417 e=tmpst.top(); //求第k個元素 418 tmpst.pop(); 419 while (!tmpst.empty()) //將tmpst的全部元素出棧並進棧st 420 { st.push(tmpst.top()); 421 tmpst.pop(); 422 9 423 424 算法設計 425 426 } 427 return e; 428 } 429 void disp(stack<int> &st) //出棧st的全部元素 { while (!st.empty()) 430 { printf("%d ",st.top()); 431 st.pop(); 432 } 433 printf("\n"); 434 } 435 void main() 436 { stack<int> st; 437 printf("進棧元素1,2,3,4\n"); 438 st.push(1); 439 st.push(2); 440 st.push(3); 441 st.push(4); 442 int k=3; 443 int e=solve(st,k); 444 printf("出棧第%d個元素是: %d\n",k,e); 445 printf("st中元素出棧順序: "); 446 disp(st); 447 } 448 上述程序的執行結果如圖1.9所示。 449 450 451 452 453 454 圖1.9 程序執行結果 455 1.2 第2章─遞歸算法設計技術 456 457 1.2.1 練習題 458 1. 什麼是直接遞歸和間接遞歸?消除遞歸通常要用到什麼數據結構? 459 2. 分析如下程序的執行結果: 460 #include <stdio.h> 461 void f(int n,int &m) 462 { if (n<1) return; 463 else 464 { printf("調用f(%d,%d)前,n=%d,m=%d\n",n-1,m-1,n,m); 465 n--; m--; 466 f(n-1,m); 467 printf("調用f(%d,%d)後:n=%d,m=%d\n",n-1,m-1,n,m); 468 } 469 10 470 第1章 概論 471 472 } 473 void main() 474 { int n=4,m=4; 475 f(n,m); 476 } 477 3. 採用直接推導方法求解如下遞歸方程: 478 T(1)=1 479 T(n)=T(n-1)+n 當n>1 480 4. 採用特徵方程方法求解如下遞歸方程: 481 H(0)=0 482 H(1)=1 483 H(2)=2 484 H(n)=H(n-1)+9H(n-2)-9H(n-3) 當n>2 485 5. 採用遞歸樹方法求解如下遞歸方程: 486 T(1)=1 487 T(n)=4T(n/2)+n 當n>1 488 6. 採用主方法求解如下題的遞歸方程。 489 T(n)=1 當n=1 490 T(n)=4T(n/2)+n2 當n>1 491 7. 分析求斐波那契f(n)的時間複雜度。 492 8. 數列的首項a1=0,後續奇數項和偶數項的計算公式分別爲a2n=a2n-1+2,a2n+1=a2n- 493 1+a2n-1,寫出計算數列第n項的遞歸算法。 494 9. 對於一個採用字符數組存放的字符串str,設計一個遞歸算法求其字符個數(長 495 度)。 496 10. 對於一個採用字符數組存放的字符串str,設計一個遞歸算法判斷str是否爲回 497 文。 498 11. 對於不帶頭結點的單鏈表L,設計一個遞歸算法正序輸出全部結點值。 499 12. 對於不帶頭結點的單鏈表L,設計一個遞歸算法逆序輸出全部結點值。 500 13. 對於不帶頭結點的非空單鏈表L,設計一個遞歸算法返回最大值結點的地址(假 501 設這樣的結點惟一)。 502 14. 對於不帶頭結點的單鏈表L,設計一個遞歸算法返回第一個值爲x的結點的地 503 址,沒有這樣的結點時返回NULL。 504 15. 對於不帶頭結點的單鏈表L,設計一個遞歸算法刪除第一個值爲x的結點。 505 16. 假設二叉樹採用二叉鏈存儲結構存放,結點值爲int類型,設計一個遞歸算法求 二叉樹bt中全部葉子結點值之和。 506 17. 假設二叉樹採用二叉鏈存儲結構存放,結點值爲int類型,設計一個遞歸算法求 二叉樹bt中全部結點值大於等於k的結點個數。 507 18. 假設二叉樹採用二叉鏈存儲結構存放,全部結點值均不相同,設計一個遞歸算法 求值爲x的結點的層次(根結點的層次爲1),沒有找到這樣的結點時返回0。 508 509 510 11 511 512 算法設計 513 514 1.2.2 練習題參考答案 515 1. 答:一個f函數定義中直接調用f函數本身,稱爲直接遞歸。一個f函數定義中調 516 用g函數,而g函數的定義中調用f函數,稱爲間接遞歸。消除遞歸通常要用棧實現。 517 2. 答:遞歸函數f(n,m)中,n是非引用參數,m是引用參數,因此遞歸函數的狀態爲 518 (n)。程序執行結果以下: 519 調用f(3,3)前,n=4,m=4 520 調用f(1,2)前,n=2,m=3 521 調用f(0,1)後,n=1,m=2 522 調用f(2,1)後,n=3,m=2 523 3. 解:求T(n)的過程以下: 524 T(n)=T(n-1)+n=[T(n-2)+n-1)]+n=T(n-2)+n+(n-1) 525 =T(n-3)+n+(n-1)+(n-2) 526 =… 527 =T(1)+n+(n-1)+…+2 528 =n+(n-1)+ +…+2+1=n(n+1)/2=O(n2)。 529 4. 解:整數一個常係數的線性齊次遞推式,用xn代替H(n),有:xn=xn-1+9xn-2-9xn-3, 530 兩邊同時除以xn-3,獲得:x3=x2+9x-9,即x3-x2-9x+9=0。 531 x3-x2-9x+9=x(x2-9)-(x2-9)=(x-1)(x2-9)=(x-1)(x+3)(x-3)=0。獲得r1=1,r2=-3,r3=3 532 則遞歸方程的通解爲:H(n)=c1+c2(-3)n+c33n 533 代入H(0)=0,有c1+c2+c3=0 534 代入H(1)=1,有c1-3c2+3c3=1 535 代入H(2)=2,有c1+9c2+9c3=2 536 求出:c1=-1/4,c2=-1/12,c3=1/3,H(n)=c1+c2(-3)n+c33n=((‒1) 。 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 12 556 557 558 559 560 561 562 高度h爲log2n+1 563 564 565 566 567 568 569 570 (n/22) 571 …… 572 573 574 575 576 577 (n/2) 578 579 (n/22) 580 … 581 582 第1章 概論 583 584 n 585 (n/2) (n/2) (n/2) 586 587 (n/22) 588 … 589 590 591 592 593 n 594 2n 595 596 22n 597 598 1 1 n 599 圖1.10 一棵遞歸樹 600 6. 解:採用主方法求解,這裏a=4,b=2,f(n)=n2。 601 所以,𝑛= =n2,它與f(n)同樣大,知足主定理中的狀況(2),因此T(n)= 602 log2n) 603 7. 解:設求斐波那契f(n)的時間爲T(n),有如下遞推式: 604 T(1)=T(2) 605 T(n)=T(n-1)+T(n-2)+1 當n>2 606 其中,T(n)式中加1表示一次加法運算的時間。 607 不妨先求T1(1)=T1(2)=1,T1(n)=T1(n-1)+T1(n-2),按《教程》例2.14的方法能夠求 608 出: 609 n n 610 T1(n)= ≈ = 611 612 因此T(n)=T1(n)+1≈ +1=O(φn),其中φ=。 613 8. 解:設f(m)計算數列第m項值。 614 當m爲偶數時,不妨設m=2n,則2n-1=m-1,因此有f(m)=f(m-1)+2。 615 當m爲奇數時,不妨設m=2n+1,則2n-1=m-2,2n=m-1,因此有f(m)=f(m-2)+f(m- 616 1)-1。 617 對應的遞歸算法以下: 618 int f(int m) 619 { if (m==1) return 0; 620 if (m%2==0) 621 return f(m-1)+2; 622 else 623 return f(m-2)+f(m-1)-1; 624 } 625 9. 解:設f(str)返回字符串str的長度,其遞歸模型以下: 626 f(str)=0 當*str='\0'時 627 f(str)=f(str+1)+1 其餘狀況 628 對應的遞歸程序以下: 629 13 630 631 算法設計 632 633 #include <iostream> 634 using namespace std; 635 int Length(char *str) //求str的字符個數 636 { if (*str=='\0') 637 return 0; 638 else 639 return Length(str+1)+1; 640 } 641 void main() 642 { char str[]="abcd"; 643 cout << str << "的長度: " << Length(str) << endl; } 644 上述程序的執行結果如圖1.11所示。 645 646 647 648 649 圖1.11 程序執行結果 650 10. 解:設f(str,n)返回含n個字符的字符串str是否爲迴文,其遞歸模型以下: 651 f(str,n)=true 當n=0或者n=1時 652 f(str,n)=flase 當str[0]≠str[n-1]時 653 f(str,n)=f(str+1,n-2) 其餘狀況 654 對應的遞歸算法以下: 655 #include <stdio.h> 656 #include <string.h> 657 bool isPal(char *str,int n) //str迴文判斷算法 658 { if (n==0 || n==1) 659 return true; 660 if (str[0]!=str[n-1]) 661 return false; 662 return isPal(str+1,n-2); 663 } 664 void disp(char *str) 665 { int n=strlen(str); 666 if (isPal(str,n)) 667 printf(" %s是迴文\n",str); 668 else 669 printf(" %s不是迴文\n",str); 670 } 671 void main() 672 { printf("求解結果\n"); 673 disp("abcba"); 674 disp("a"); 675 disp("abc"); 676 } 677 678 14 679 680 681 上述程序的執行結果如圖1.12所示。 682 683 第1章 概論 684 685 686 687 688 689 690 圖1.12 程序執行結果 691 11. 解:設f(L)正序輸出單鏈表L的全部結點值,其遞歸模型以下: 692 f(L) ≡ 不作任何事情 當L=NULL 693 f(L) ≡ 輸出L->data; f(L->next); 當L≠NULL時 694 對應的遞歸程序以下: 695 #include "LinkList.cpp" //包含單鏈表的基本運算算法 696 void dispLink(LinkNode *L) //正序輸出全部結點值 697 { if (L==NULL) return; 698 else 699 { printf("%d ",L->data); 700 dispLink(L->next); 701 } 702 } 703 void main() 704 { int a[]={1,2,5,2,3,2}; 705 int n=sizeof(a)/sizeof(a[0]); 706 LinkNode *L; 707 CreateList(L,a,n); //由a[0..n-1]建立不帶頭結點的單鏈表 printf("正向L: "); 708 dispLink(L); printf("\n"); 709 Release(L); //銷燬單鏈表 710 } 711 上述程序的執行結果如圖1.13所示。 712 713 714 715 716 圖1.13 程序執行結果 717 12. 解:設f(L)逆序輸出單鏈表L的全部結點值,其遞歸模型以下: 718 f(L) ≡ 不作任何事情 當L=NULL 719 f(L) ≡ f(L->next); 輸出L->data 當L≠NULL時 720 對應的遞歸程序以下: 721 #include "LinkList.cpp" //包含單鏈表的基本運算算法 722 void Revdisp(LinkNode *L) //逆序輸出全部結點值 723 { if (L==NULL) return; 724 15 725 726 727 else 728 { Revdisp(L->next); 729 printf("%d ",L->data); 730 } 731 } 732 void main() 733 { int a[]={1,2,5,2,3,2}; 734 int n=sizeof(a)/sizeof(a[0]); 735 LinkNode *L; 736 CreateList(L,a,n); 737 printf("反向L: "); 738 Revdisp(L); printf("\n"); 739 Release(L); 740 } 741 上述程序的執行結果如圖1.14所示。 742 743 744 算法設計 745 746 747 748 749 圖1.14 程序執行結果 750 13. 解:設f(L)返回單鏈表L中值最大結點的地址,其遞歸模型以下: 751 f(L) = L 當L只有一個結點時 752 f(L) = MAX{f(L->next),L->data} 其餘狀況 753 對應的遞歸程序以下: 754 #include "LinkList.cpp" //包含單鏈表的基本運算算法 755 LinkNode *Maxnode(LinkNode *L) //返回最大值結點的地址 756 { if (L->next==NULL) 757 return L; //只有一個結點時 758 else 759 { LinkNode *maxp; 760 maxp=Maxnode(L->next); 761 if (L->data>maxp->data) 762 return L; 763 else 764 return maxp; 765 } 766 } 767 void main() 768 { int a[]={1,2,5,2,3,2}; 769 int n=sizeof(a)/sizeof(a[0]); 770 LinkNode *L,*p; 771 CreateList(L,a,n); 772 p=Maxnode(L); 773 printf("最大結點值: %d\n",p->data); 774 Release(L); 775 776 16 777 778 779 } 780 上述程序的執行結果如圖1.15所示。 781 782 第1章 概論 783 784 785 786 787 圖1.15 程序執行結果 788 14. 解:設f(L,x)返回單鏈表L中第一個值爲x的結點的地址,其遞歸模型以下: 789 f(L,x) = NULL 當L=NULL時 790 f(L,x) = L 當L≠NULL且L->data=x時 791 f(L,x) = f(L->next,x) 其餘狀況 792 對應的遞歸程序以下: 793 #include "LinkList.cpp" //包含單鏈表的基本運算算法 794 LinkNode *Firstxnode(LinkNode *L,int x) //返回第一個值爲x的結點的地址 795 { if (L==NULL) return NULL; 796 if (L->data==x) 797 return L; 798 else 799 return Firstxnode(L->next,x); 800 } 801 void main() 802 { int a[]={1,2,5,2,3,2}; 803 int n=sizeof(a)/sizeof(a[0]); 804 LinkNode *L,*p; 805 CreateList(L,a,n); 806 int x=2; 807 p=Firstxnode(L,x); 808 printf("結點值: %d\n",p->data); 809 Release(L); 810 } 811 上述程序的執行結果如圖1.16所示。 812 813 814 815 816 圖1.16 程序執行結果 817 15. 解:設f(L,x)刪除單鏈表L中第一個值爲x的結點,其遞歸模型以下: 818 f(L,x) ≡ 不作任何事情 當L=NULL 819 f(L,x) ≡ 刪除L結點,L=L->next 當L≠NULL且L->data=x 820 f(L,x) ≡ f(L->next,x) 其餘狀況 821 對應的遞歸程序以下: 822 823 17 824 825 算法設計 826 827 #include "LinkList.cpp" //包含單鏈表的基本運算算法 828 void Delfirstx(LinkNode *&L,int x) //刪除單鏈表L中第一個值爲x的結點 { if (L==NULL) return; 829 if (L->data==x) 830 { LinkNode *p=L; 831 L=L->next; 832 free(p); 833 } 834 else 835 Delfirstx(L->next,x); 836 } 837 void main() 838 { int a[]={1,2,5,2,3,2}; 839 int n=sizeof(a)/sizeof(a[0]); 840 LinkNode *L; 841 CreateList(L,a,n); 842 printf("刪除前L: "); DispList(L); 843 int x=2; 844 printf("刪除第一個值爲%d的結點\n",x); 845 Delfirstx(L,x); 846 printf("刪除後L: "); DispList(L); 847 Release(L); 848 } 849 上述程序的執行結果如圖1.17所示。 850 851 852 853 854 855 圖1.17 程序執行結果 856 16. 解:設f(bt)返回二叉樹bt中全部葉子結點值之和,其遞歸模型以下: 857 f(bt)=0 當bt=NULL 858 f(bt)=bt->data 當bt≠NULL且bt結點爲葉子結點 859 f(bt)=f(bt->lchild)+f(bt->rchild) 其餘狀況 860 對應的遞歸程序以下: 861 #include "Btree.cpp" //包含二叉樹的基本運算算法 862 int LeafSum(BTNode *bt) //二叉樹bt中全部葉子結點值之和 863 { if (bt==NULL) return 0; 864 if (bt->lchild==NULL && bt->rchild==NULL) 865 return bt->data; 866 int lsum=LeafSum(bt->lchild); 867 int rsum=LeafSum(bt->rchild); 868 return lsum+rsum; 869 } 870 void main() 871 872 18 873 第1章 概論 874 875 { BTNode *bt; 876 Int a[]={5,2,3,4,1,6}; //先序序列 877 Int b[]={2,3,5,1,4,6}; //中序序列 878 int n=sizeof(a)/sizeof(a[0]); 879 bt=CreateBTree(a,b,n); //由a和b構造二叉鏈bt printf("二叉樹bt:"); DispBTree(bt); printf("\n"); printf("全部葉子結點值之和: %d\n",LeafSum(bt)); 880 DestroyBTree(bt); //銷燬樹bt 881 } 882 上述程序的執行結果如圖1.18所示。 883 884 885 886 887 888 圖1.18 程序執行結果 889 17. 解:設f(bt,k)返回二叉樹bt中全部結點值大於等於k的結點個數,其遞歸模型 890 以下: 891 f(bt,k)=0 當bt=NULL 892 f(bt,k)=f(bt->lchild,k)+f(bt->rchild,k)+1 當bt≠NULL且bt->data≥k 893 f(bt,k)=f(bt->lchild,k)+f(bt->rchild,k) 其餘狀況 894 對應的遞歸程序以下: 895 #include "Btree.cpp" //包含二叉樹的基本運算算法 896 int Nodenum(BTNode *bt,int k) //大於等於k的結點個數 897 { if (bt==NULL) return 0; 898 int lnum=Nodenum(bt->lchild,k); 899 int rnum=Nodenum(bt->rchild,k); 900 if (bt->data>=k) 901 return lnum+rnum+1; 902 else 903 return lnum+rnum; 904 } 905 void main() 906 { BTNode *bt; 907 Int a[]={5,2,3,4,1,6}; 908 Int b[]={2,3,5,1,4,6}; 909 int n=sizeof(a)/sizeof(a[0]); 910 bt=CreateBTree(a,b,n); //由a和b構造二叉鏈bt 911 printf("二叉樹bt:"); DispBTree(bt); printf("\n"); 912 int k=3; 913 printf("大於等於%d的結點個數: %d\n",k,Nodenum(bt,k)); 914 DestroyBTree(bt); //銷燬樹bt 915 } 916 上述程序的執行結果如圖1.19所示。 917 918 919 19 920 921 算法設計 922 923 924 925 926 927 928 圖1.19 程序執行結果 929 18. 解:設f(bt,x,h)返回二叉樹bt中x結點的層次,其中h表示bt所指結點的層 930 次,初始調用時,bt指向根結點,h置爲1。其遞歸模型以下: 931 f(bt,x,h)=0 當bt=NULL 932 f(bt,x,h)=h 當bt≠NULL且bt->data=x 933 f(bt,x,h) =l 當l=f(bt->lchild,x,h+1)≠0 934 f(bt,x,h) =f(bt->rchild,x,h+1) 其餘狀況 935 對應的遞歸程序以下: 936 #include "Btree.cpp" //包含二叉樹的基本運算算法 937 int Level(BTNode *bt,int x,int h) //求二叉樹bt中x結點的層次 938 { //初始調用時:bt爲根,h爲1 939 if (bt==NULL) return 0; 940 if (bt->data==x) //找到x結點,返回h 941 return h; 942 else 943 { int l=Level(bt->lchild,x,h+1); //在左子樹中查找 944 if (l!=0) //在左子樹中找到,返回其層次l 945 return l; 946 else 947 return Level(bt->rchild,x,h+1);//返回在右子樹的查找結果 948 } 949 } 950 void main() 951 { BTNode *bt; 952 Int a[]={5,2,3,4,1,6}; 953 Int b[]={2,3,5,1,4,6}; 954 int n=sizeof(a)/sizeof(a[0]); 955 bt=CreateBTree(a,b,n); //由a和b構造二叉鏈bt 956 printf("二叉樹bt:"); DispBTree(bt); printf("\n"); 957 int x=1; 958 printf("%d結點的層次: %d\n",x,Level(bt,x,1)); 959 DestroyBTree(bt); //銷燬樹bt 960 } 961 上述程序的執行結果如圖1.20所示。 962 963 964 965 966 967 圖1.20 程序執行結果 968 20 969 第1章 概論 1.3 第3章─分治法 970 971 1.3.1 練習題 972 1. 分治法的設計思想是將一個難以直接解決的大問題分割成規模較小的子問題,分 973 別解決子問題,最後將子問題的解組合起來造成原問題的解。這要求原問題和子問題 974 ( )。 975 A.問題規模相同,問題性質相同 976 B.問題規模相同,問題性質不一樣 977 C.問題規模不一樣,問題性質相同 978 D.問題規模不一樣,問題性質不一樣 979 2. 在尋找n個元素中第k小元素問題中,如快速排序算法思想,運用分治算法對n 980 個元素進行劃分,如何選擇劃分基準?下面( )答案解釋最合理。 981 A.隨機選擇一個元素做爲劃分基準 982 B.取子序列的第一個元素做爲劃分基準 983 C.用中位數的中位數方法尋找劃分基準 984 D.以上皆可行。但不一樣方法,算法複雜度上界可能不一樣 985 3. 對於下列二分查找算法,如下正確的是( )。 986 A. 987 int binarySearch(int a[], int n, int x) 988 { int low=0, high=n-1; 989 while(low<=high) 990 { int mid=(low+high)/2; 991 if(x==a[mid]) return mid; 992 if(x>a[mid]) low=mid; 993 else high=mid; 994 } 995 return –1; 996 } 997 B. 998 int binarySearch(int a[], int n, int x) 999 { int low=0, high=n-1; 1000 while(low+1!=high) 1001 { int mid=(low+high)/2; 1002 if(x>=a[mid]) low=mid; 1003 else high=mid; 1004 } 1005 if(x==a[low]) return low; 1006 else return –1; 1007 } 1008 C. 1009 int binarySearch (int a[], int n, int x) 1010 { int low=0, high=n-1; 1011 while(low<high-1) 1012 { int mid=(low+high)/2; 1013 21 1014 1015 算法設計 1016 1017 if(x<a[mid]) 1018 high=mid; 1019 else low=mid; 1020 } 1021 if(x==a[low]) return low; 1022 else return –1; 1023 } 1024 D. 1025 int binarySearch(int a[], int n, int x) 1026 { if(n > 0 && x >= a[0]) 1027 { int low = 0, high = n-1; 1028 while(low < high) 1029 { int mid=(low+high+1)/2; 1030 if(x < a[mid]) 1031 high=mid-1; 1032 else low=mid; 1033 } 1034 if(x==a[low]) return low; 1035 } 1036 return –1; 1037 } 1038 4. 快速排序算法是根據分治策略來設計的,簡述其基本思想。 1039 5. 假設含有n個元素的待排序的數據a剛好是遞減排列的,說明調用QuickSort(a, 1040 0,n-1)遞增排序的時間複雜度爲O(n2)。 1041 6. 如下哪些算法採用分治策略: 1042 (1)堆排序算法 1043 (2)二路歸併排序算法 1044 (3)折半查找算法 1045 (4)順序查找算法 1046 7. 適合並行計算的問題一般表現出哪些特徵? 1047 8. 設有兩個複數x=a+bi和y=c+di。複數乘積xy可使用4次乘法來完成,即 1048 xy=(ac-bd)+(ad+bc)i。設計一個僅用3次乘法來計算乘積xy的方法。 1049 9. 有4個數組a、b、c和d,都已經排好序,說明找出這4個數組的交集的方法。 1050 10. 設計一個算法,採用分治法求一個整數序列中的最大最小元素。 1051 11. 設計一個算法,採用分治法求xn。 1052 12. 假設二叉樹採用二叉鏈存儲結構進行存儲。設計一個算法採用分治法求一棵二叉 1053 樹bt的高度。 1054 13. 假設二叉樹採用二叉鏈存儲結構進行存儲。設計一個算法採用分治法求一棵二叉 1055 樹bt中度爲2的結點個數。 1056 14. 有一種二叉排序樹,其定義是空樹是一棵二叉排序樹,若不空,左子樹中全部結 1057 點值小於根結點值,右子樹中全部結點值大於根結點值,而且左右子樹都是二叉排序樹。 如今該二叉排序樹採用二叉鏈存儲,採用分治法設計查找值爲x的結點地址,並分析算法 的最好的平均時間複雜度。 1058 1059 22 1060 第1章 概論 1061 1062 15. 設有n個互不相同的整數,按遞增順序存放在數組a[0..n-1]中,若存在一個下標 i(0≤i<n),使得a[i]=i。設計一個算法以O(log2n)時間找到這個下標i。 1063 16. 請你模仿二分查找過程設計一個三分查找算法。分析其時間複雜度。 1064 17. 對於大於1的正整數n,能夠分解爲n=x1*x2*…*xm,其中xi≥2。例如,n=12時 1065 有8種不一樣的分解式:12=12,12=6*2,12=4*3,12=3*4,12=3*2*2,12=2*6, 1066 12=2*3*2,12=2*2*3,設計一個算法求n的不一樣分解式個數。 1067 18. 設計一個基於BSP模型的並行算法,假設有p臺處理器,計算整數數組a[0..n-1] 的全部元素之和。並分析算法的時間複雜度。 1068 1.3.2 練習題參考答案 1069 1. 答:C。 1070 2. 答:D。 1071 3. 答:以a[]={1,2,3,4,5}爲例說明。選項A中在查找5時出現死循環。選項B 1072 中在查找5時返回-1。選項C中在查找5時返回-1。選項D正確。 1073 4. 答:對於無序序列a[low..high]進行快速排序,整個排序爲「大問題」。選擇其中的 1074 一個基準base=a[i](一般以序列中第一個元素爲基準),將全部小於等於base的元素移動 到它的前面,全部大於等於base的元素移動到它的後面,即將基準歸位到a[i],這樣產生 a[low..i-1]和a[i+1..high]兩個無序序列,它們的排序爲「小問題」。當a[low..high]序列只 1075 有一個元素或者爲空時對應遞歸出口。 1076 因此快速排序算法就是採用分治策略,將一個「大問題」分解爲兩個「小問題」來求 1077 解。因爲元素都是在a數組中,其合併過程是天然產生的,不須要特別設計。 1078 5. 答:此時快速排序對應的遞歸樹高度爲O(n),每一次劃分對應的時間爲O(n),所 1079 以整個排序時間爲O(n2)。 1080 6. 答:其中二路歸併排序和折半查找算法採用分治策略。 1081 7. 答:適合並行計算的問題一般表現出如下特徵: 1082 (1)將工做分離成離散部分,有助於同時解決。例如,對於分治法設計的串行算 1083 法,能夠將各個獨立的子問題並行求解,最後合併成整個問題的解,從而轉化爲並行算 1084 法。 1085 (2)隨時並及時地執行多個程序指令。 1086 (3)多計算資源下解決問題的耗時要少於單個計算資源下的耗時。 1087 8. 答:xy=(ac-bd)+((a+b)(c+d)-ac-bd)i。因而可知,這樣計算xy只須要3次乘法(即 1088 ac、bd和(a+b)(c+d)乘法運算)。 1089 9. 答:採用基本的二路歸併思路,先求出a、b的交集ab,再求出c、d的交集cd, 1090 最後求出ab和cd的交集,即爲最後的結果。也能夠直接採用4路歸併方法求解。 1091 10. 解:採用相似求求一個整數序列中的最大次大元素的分治法思路。對應的程序如 1092 下: 1093 #include <stdio.h> 1094 #define max(x,y) ((x)>(y)?(x):(y)) 1095 #define min(x,y) ((x)<(y)?(x):(y)) 1096 1097 23 1098 1099 算法設計 1100 1101 void MaxMin(int a[],int low,int high,int &maxe,int &mine) //求a中最大最小元素 { if (low==high) //只有一個元素 1102 { maxe=a[low]; 1103 mine=a[low]; 1104 } 1105 else if (low==high-1) //只有兩個元素 1106 { maxe=max(a[low],a[high]); 1107 mine=min(a[low],a[high]); 1108 } 1109 else //有兩個以上元素 1110 { int mid=(low+high)/2; 1111 int lmaxe,lmine; 1112 MaxMin(a,low,mid,lmaxe,lmine); 1113 int rmaxe,rmine; 1114 MaxMin(a,mid+1,high,rmaxe,rmine); 1115 maxe=max(lmaxe,rmaxe); 1116 mine=min(lmine,rmine); 1117 } 1118 } 1119 void main() 1120 { int a[]={4,3,1,2,5}; 1121 int n=sizeof(a)/sizeof(a[0]); 1122 int maxe,mine; 1123 MaxMin(a,0,n-1,maxe,mine); 1124 printf("Max=%d, Min=%d\n",maxe,mine); 1125 } 1126 上述程序的執行結果如圖1.21所示。 1127 1128 1129 1130 1131 1132 圖1.21 程序執行結果 1133 11. 解:設f(x,n)=xn,採用分治法求解對應的遞歸模型以下: 1134 f(x,n)=x 當n=1 1135 f(x,n)=f(x,n/2)*f(x,n/2) 當n爲偶數時 1136 f(x,n)=f(x,(n-1)/2)*f(x,(n-1)/2)*x 當n爲奇數時 1137 對應的遞歸程序以下: 1138 #include <stdio.h> 1139 double solve(double x,int n) //求x^n 1140 { double fv; 1141 if (n==1) return x; 1142 if (n%2==0) 1143 { fv=solve(x,n/2); 1144 return fv*fv; 1145 } 1146 1147 24 1148 第1章 概論 1149 1150 else 1151 { fv=solve(x,(n-1)/2); 1152 return fv*fv*x; 1153 } 1154 } 1155 void main() 1156 { double x=2.0; 1157 printf("求解結果:\n"); 1158 for (int i=1;i<=10;i++) 1159 printf(" %g^%d=%g\n",x,i,solve(x,i)); } 1160 上述程序的執行結果如圖1.22所示。 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 圖1.22 程序執行結果 1171 12. 解:設f(bt)返回二叉樹bt的高度,對應的遞歸模型以下: 1172 f(bt)=0 當bt=NULL 1173 f(bt)=MAX{f(bt->lchild),f(bt->rchild)}+1 其餘狀況 1174 對應的程序以下: 1175 #include "Btree.cpp" //包含二叉樹的基本運算算法 int Height(BTNode *bt) //求二叉樹bt的高度 1176 { if (bt==NULL) return 0; 1177 int lh=Height(bt->lchild); //子問題1 1178 int rh=Height(bt->rchild); //子問題2 1179 if (lh>rh) return lh+1; //合併 1180 else return rh+1; 1181 } 1182 void main() 1183 { BTNode *bt; 1184 Int a[]={5,2,3,4,1,6}; 1185 Int b[]={2,3,5,1,4,6}; 1186 int n=sizeof(a)/sizeof(a[0]); 1187 bt=CreateBTree(a,b,n); //由a和b構造二叉鏈bt 1188 printf("二叉樹bt:"); DispBTree(bt); printf("\n"); 1189 printf("bt的高度: %d\n",Height(bt)); 1190 DestroyBTree(bt); //銷燬樹bt 1191 } 1192 1193 25 1194 1195 1196 1197 上述程序的執行結果如圖1.23所示。 1198 1199 1200 算法設計 1201 1202 1203 1204 1205 1206 圖1.23 程序執行結果 1207 13. 解:設f(bt)返回二叉樹bt中度爲2的結點個數,對應的遞歸模型以下: 1208 f(bt)=0 當bt=NULL 1209 f(bt)=f(bt->lchild)+f(bt->rchild)+1 若bt≠NULL且bt爲雙分支結點 1210 f(bt)=f(bt->lchild)+f(bt->rchild) 其餘狀況 1211 對應的算法以下: 1212 #include "Btree.cpp" //包含二叉樹的基本運算算法 1213 int Nodes(BTNode *bt) //求bt中度爲2的結點個數 1214 { int n=0; 1215 if (bt==NULL) return 0; 1216 if (bt->lchild!=NULL && bt->rchild!=NULL) 1217 n=1; 1218 return Nodes(bt->lchild)+Nodes(bt->rchild)+n; 1219 } 1220 void main() 1221 { BTNode *bt; 1222 Int a[]={5,2,3,4,1,6}; 1223 Int b[]={2,3,5,1,4,6}; 1224 int n=sizeof(a)/sizeof(a[0]); 1225 bt=CreateBTree(a,b,n); //由a和b構造二叉鏈bt 1226 printf("二叉樹bt:"); DispBTree(bt); printf("\n"); 1227 printf("bt中度爲2的結點個數: %d\n",Nodes(bt)); 1228 DestroyBTree(bt); //銷燬樹bt 1229 } 1230 上述程序的執行結果如圖1.24所示。 1231 1232 1233 1234 1235 1236 圖1.24 程序執行結果 1237 14. 解:設f(bt,x)返回在二叉排序樹bt獲得的值爲x結點的地址,若沒有找到返回 1238 空,對應的遞歸模型以下: 1239 f(bt,x)=NULL 當bt=NULL 1240 f(bt,x)=bt 當bt≠NULL且x=bt->data 1241 f(bt,x)=f(bt->lchild,x) 當x>bt->data 1242 1243 26 1244 第1章 概論 1245 f(bt,x)=f(bt->rchild,x) 當x<bt->data 1246 對應的程序以下: 1247 #include "Btree.cpp" //包含二叉樹的基本運算算法 1248 BTNode *Search(BTNode *bt,Int x) //在二叉排序樹bt查找的值爲x結點 { if (bt==NULL) return NULL; 1249 if (x==bt->data) return bt; 1250 if (x<bt->data) return Search(bt->lchild,x); 1251 else return Search(bt->rchild,x); 1252 } 1253 void main() 1254 { BTNode *bt; 1255 Int a[]={4,3,2,8,6,7,9}; 1256 Int b[]={2,3,4,6,7,8,9}; 1257 int n=sizeof(a)/sizeof(a[0]); 1258 bt=CreateBTree(a,b,n); //構造一棵二叉排序樹bt 1259 printf("二叉排序樹bt:"); DispBTree(bt); printf("\n"); 1260 int x=6; 1261 BTNode *p=Search(bt,x); 1262 if (p!=NULL) 1263 printf("找到結點: %d\n",p->data); 1264 else 1265 printf("沒有找到結點\n",x); 1266 DestroyBTree(bt); //銷燬樹bt 1267 } 1268 上述程序的執行結果如圖1.25所示。 1269 1270 1271 1272 1273 1274 圖1.25 程序執行結果 1275 Search(bt,x)算法採用的是減治法,最好的狀況是某個結點左右子樹高度大體相同, 1276 其平均執行時間T(n)以下: 1277 T(n)=1 當n=1 1278 T(n)=T(n/2)+1 當n>1 1279 能夠推出T(n)=O(log2n),其中n爲二叉排序樹的結點個數。 1280 15. 解:採用二分查找方法。a[i]=i時表示該元素在有序非重複序列a中剛好第i大。 1281 對於序列a[low..high],mid=(low+high)/2,若a[mid]=mid表示找到該元素;若a[mid]>mid 說明右區間的全部元素都大於其位置,只能在左區間中查找;若a[mid]<mid說明左區間 1282 的全部元素都小於其位置,只能在右區間中查找。對應的程序以下: 1283 #include <stdio.h> 1284 int Search(int a[],int n) //查找使得a[i]=i 1285 { int low=0,high=n-1,mid; 1286 27 1287 1288 算法設計 1289 1290 while (low<=high) 1291 { mid=(low+high)/2; 1292 if (a[mid]==mid) //查找到這樣的元素 1293 return mid; 1294 else if (a[mid]<mid) //這樣的元素只能在右區間中出現 low=mid+1; 1295 else //這樣的元素只能在左區間中出現 high=mid-1; 1296 } 1297 return -1; 1298 } 1299 void main() 1300 { int a[]={-2,-1,2,4,6,8,9}; 1301 int n=sizeof(a)/sizeof(a[0]); 1302 int i=Search(a,n); 1303 printf("求解結果\n"); 1304 if (i!=-1) 1305 printf(" 存在a[%d]=%d\n",i,i); 1306 else 1307 printf(" 不存在\n"); 1308 } 1309 上述程序的執行結果如圖1.26所示。 1310 1311 1312 1313 1314 1315 圖1.26 程序執行結果 1316 16. 解:對於有序序列a[low..high],若元素個數少於3個,直接查找。若含有更多的 1317 元素,將其分爲a[low..mid1-1]、a[mid1+1..mid2-1]、a[mid2+1..high]子序列,對每一個子序 列遞歸查找,算法的時間複雜度爲O(log3n),屬於O(log2n)級別。對應的算法以下: 1318 #include <stdio.h> 1319 int Search(int a[],int low,int high,int x) //三分查找 1320 { if (high<low) //序列中沒有元素 1321 return -1; 1322 else if (high==low) //序列中只有1個元素 1323 { if (x==a[low]) 1324 return low; 1325 else 1326 return -1; 1327 } 1328 if (high-low<2) //序列中只有2個元素 1329 { if (x==a[low]) 1330 return low; 1331 else if (x==a[low+1]) 1332 return low+1; 1333 else 1334 28 1335 第1章 概論 1336 1337 return -1; 1338 } 1339 int length=(high-low+1)/3; //每一個子序列的長度 int mid1=low+length; 1340 int mid2=high-length; 1341 if (x==a[mid1]) 1342 return mid1; 1343 else if (x<a[mid1]) 1344 return Search(a,low,mid1-1,x); 1345 else if (x==a[mid2]) 1346 return mid2; 1347 else if (x<a[mid2]) 1348 return Search(a,mid1+1,mid2-1,x); 1349 else 1350 return Search(a,mid2+1,high,x); 1351 } 1352 void main() 1353 { int a[]={1,3,5,7,9,11,13,15}; 1354 int n=sizeof(a)/sizeof(a[0]); 1355 printf("求解結果\n"); 1356 int x=13; 1357 int i=Search(a,0,n-1,x); 1358 if (i!=-1) 1359 printf(" a[%d]=%d\n",i,x); 1360 else 1361 printf(" 不存在%d\n",x); 1362 int y=10; 1363 int j=Search(a,0,n-1,y); 1364 if (j!=-1) 1365 printf(" a[%d]=%d\n",j,y); 1366 else 1367 printf(" 不存在%d\n",y); 1368 } 1369 上述程序的執行結果如圖1.27所示。 1370 1371 1372 1373 1374 1375 圖1.27 程序執行結果 1376 17. 解:設f(n)表示n的不一樣分解式個數。有: f(1)=1,做爲遞歸出口 1377 f(2)=1,分解式爲:2=2 1378 f(3)=1,分解式爲:3=3 1379 f(4)=2,分解式爲:4=4,4=2*2 1380 1381 29 1382 1383 算法設計 1384 f(6)=3,分解式爲:6=6,6=2*3,6=3*2,即f(6)=f(1)+f(2)+f(3) 1385 以此類推,能夠看出f(n)爲n的全部因數的不一樣分解式個數之和,即f(n)= 1386 ∑𝑛%𝑖=0𝑓(𝑛/𝑖)。對應的程序以下: 1387 #include <stdio.h> 1388 #define MAX 101 1389 int solve(int n) //求n的不一樣分解式個數 1390 { if (n==1) return 1; 1391 else 1392 { int sum=0; 1393 for (int i=2;i<=n;i++) 1394 if (n%i==0) 1395 sum+=solve(n/i); 1396 return sum; 1397 } 1398 } 1399 void main() 1400 { int n=12; 1401 int ans=solve(n); 1402 printf("結果: %d\n",ans); 1403 } 1404 上述程序的執行結果如圖1.28所示。 1405 1406 1407 1408 1409 圖1.28 程序執行結果 1410 18. 解:對應的並行算法以下: 1411 int Sum(int a[],int s,int t,int p,int i) //處理器i執行求和 1412 { int j,s=0; 1413 for (j=s;j<=t;j++) 1414 s+=a[j]; 1415 return s; 1416 } 1417 int ParaSum(int a[],int s,int t,int p,int i) 1418 { int sum=0,j,k=0,sj; 1419 for (j=0;j<p;j++) //for循環的各個子問題並行執行 1420 { sj=Sum(a,k,k+n/p-1,p,j); 1421 k+=n/p; 1422 } 1423 sum+=sj; 1424 return sum; 1425 } 1426 每一個處理器的執行時間爲O(n/p),同步開銷爲O(p),因此該算法的時間複雜度爲 1427 O(n/p+p)。 1428 1429 30 1430 第1章 概論 1.4 第4章─蠻力法 1431 1432 1.4.1 練習題 1433 1. 簡要比較蠻力法和分治法。 1434 2. 在採用蠻力法求解時什麼狀況下使用遞歸? 1435 3. 考慮下面這個算法,它求的是數組a中大小相差最小的兩個元素的差。請對這個 1436 算法作儘量多的改進。 1437 #define INF 99999 1438 #define abs(x) (x)<0?-(x):(x) //求絕對值宏 1439 int Mindif(int a[],int n) 1440 { int dmin=INF; 1441 for (int i=0;i<=n-2;i++) 1442 for (int j=i+1;j<=n-1;j++) 1443 { int temp=abs(a[i]-a[j]); 1444 if (temp<dmin) 1445 dmin=temp; 1446 } 1447 return dmin; 1448 } 1449 4. 給定一個整數數組A=(a0,a1,…an-1),若i<j且ai>aj,則<ai,aj>就爲一個逆序 1450 對。例如數組(3,1,4,5,2)的逆序對有<3,1>,<3,2>,<4,2>,<5,2>。設計一 個算法採用蠻力法求A中逆序對的個數即逆序數。 1451 5. 對於給定的正整數n(n>1), 採用蠻力法求1!+2!+…+n!,並改進該算法提升效 1452 率。 1453 6. 有一羣雞和一羣兔,它們的只數相同,它們的腳數都是三位數,且這兩個三位數 的各位數字只能是0、1、2、3、4、5。設計一個算法用蠻力法求雞和兔的只數各是多 少?它們的腳數各是多少? 1454 7. 有一個三位數,個位數字比百位數字大,而百位數字又比十位數字大,而且各位 數字之和等於各位數字相乘之積,設計一個算法用窮舉法求此三位數。 1455 8. 某年級的同窗集體去公園划船,若是每隻船坐10人,那麼多出2個座位;若是每 只船多坐2人,那麼可少租1只船,設計一個算法用蠻力法求該年級的最多人數? 1456 9. 已知:若一個合數的質因數分解式逐位相加之和等於其自己逐位相加之和,則稱 這個數爲Smith數。如4937775=3*5*5*65837,而3+5+5+6+5+8+3+7=42, 1457 4+9+3+7+7+7+5=42,因此4937775是Smith數。求給定一個正整數N,求大於N的最小 Smith數。 1458 輸入:若干個case,每一個case一行表明正整數N,輸入0表示結束 1459 輸出:大於N的最小Smith數 1460 輸入樣例: 1461 4937774 1462 0 1463 樣例輸出: 1464 31 1465 1466 算法設計 1467 1468 4937775 1469 10. 求解塗棋盤問題。小易有一塊n*n的棋盤,棋盤的每個格子都爲黑色或者白 1470 色,小易如今要用他喜歡的紅色去塗畫棋盤。小易會找出棋盤中某一列中擁有相同顏色的 最大的區域去塗畫,幫助小易算算他會塗畫多少個棋格。 1471 輸入描述:輸入數據包括n+1行:第一行爲一個整數n(1≤ n≤50),即棋盤的大 小,接下來的n行每行一個字符串表示第i行棋盤的顏色,'W'表示白色,'B'表示黑色。 1472 輸出描述:輸出小易會塗畫的區域大小。 1473 輸入例子: 1474 3 1475 BWW 1476 BBB 1477 BWB 1478 輸出例子: 1479 3 1480 11. 給定一個含n(n>1)個整數元素的a,全部元素不相同,採用蠻力法求出a中所 有元素的全排列。 1481 1.4.2 練習題參考答案 1482 1. 答:蠻力法是一種簡單直接地解決問題的方法,適用範圍廣,是能解決幾乎全部 問題的通常性方法,經常使用於一些很是基本、但又十分重要的算法(排序、查找、矩陣乘法 和字符串匹配等),蠻力法主要解決一些規模小或價值低的問題,能夠做爲一樣問題的更 1483 高效算法的一個標準。而分治法採用分而治之思路,把一個複雜的問題分紅兩個或更多的 相同或類似的子問題,再把子問題分紅更小的子問題直到問題解決。分治法在求解問題 1484 時,一般性能比蠻力法好。 1485 2. 答:若是用蠻力法求解的問題能夠分解爲若干個規模較小的類似子問題,此時可 以採用遞歸來實現算法。 1486 3. 解:上述算法的時間複雜度爲O(n2),採用的是最基本的蠻力法。能夠先對a中元 素遞增排序,而後依次比較相鄰元素的差,求出最小差,改進後的算法以下: 1487 #include <stdio.h> 1488 #include <algorithm> 1489 using namespace std; 1490 int Mindif1(int a[],int n) 1491 { sort(a,a+n); //遞增排序 1492 int dmin=a[1]-a[0]; 1493 for (int i=2;i<n;i++) 1494 { int temp=a[i]-a[i-1]; 1495 if (temp<dmin) 1496 dmin=temp; 1497 } 1498 return dmin; 1499 } 1500 1501 32 1502 第1章 概論 1503 1504 上述算法的主要時間花費在排序上,算法的時間複雜度爲O(nlog2n)。 1505 4. 解:採用兩重循環直接判斷是否爲逆序對,算法的時間複雜度爲O(n2),比第3章 1506 實驗3算法的性能差。對應的算法以下: 1507 int solve(int a[],int n) //求逆序數 1508 { int ans=0; 1509 for (int i=0;i<n-1;i++) 1510 for (int j=i+1;j<n;j++) 1511 if (a[i]>a[j]) 1512 ans++; 1513 return ans; 1514 } 1515 5. 解:直接採用蠻力法求解算法以下: 1516 long f(int n) //求n! 1517 { long fn=1; 1518 for (int i=2;i<=n;i++) 1519 fn=fn*i; 1520 return fn; 1521 } 1522 long solve(int n) //求1!+2!+…+n! 1523 { long ans=0; 1524 for (int i=1;i<=n;i++) 1525 ans+=f(i); 1526 return ans; 1527 } 1528 實際上,f(n)=f(n-1)*n,f(1)=1,在求f(n)時能夠利用f(n-1)的結果。改進後的算法如 1529 下: 1530 long solve1(int n) //求1!+2!+…+n! 1531 { long ans=0; 1532 long fn=1; 1533 for (int i=1;i<=n;i++) 1534 { fn=fn*i; 1535 ans+=fn; 1536 } 1537 return ans; 1538 } 1539 6. 解:設雞腳數爲y=abc,兔腳數爲z=def,有1≤a,d≤5,0≤b,c,e,f≤5,採 用6重循環,求出雞隻數x1=y/2(y是2的倍數),兔只數x2=z/4(z是4的倍數),當 x1=x2時輸出結果。對應的程序以下: 1540 #include <stdio.h> 1541 void solve() 1542 { int a,b,c,d,e,f; 1543 int x1,x2,y,z; 1544 for (a=1;a<=5;a++) 1545 for (b=0;b<=5;b++) 1546 for (c=0;c<=5;c++) 1547 1548 33 1549 1550 算法設計 1551 1552 for (d=1;d<=5;d++) 1553 for (e=0;e<=5;e++) 1554 for (f=0;f<=5;f++) 1555 { y=a*100+b*10+c; //雞腳數 1556 z=d*100+e*10+f; //兔腳數 1557 if (y%2!=0 || z%4!=0) 1558 continue; 1559 x1=y/2; //雞隻數 1560 x2=z/4; //兔只數 1561 if (x1==x2) 1562 printf(" 雞隻數:%d,兔只數:%d,雞腳數:%d, 兔腳數:%d\n",x1,x2,y,z); 1563 } 1564 } 1565 void main() 1566 { printf("求解結果\n"); 1567 solve(); 1568 } 1569 上述程序的執行結果如圖1.29所示。 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 圖1.29 程序執行結果 1584 7. 解:設該三位數爲x=abc,有1≤a≤9,0≤b,c≤9,知足c>a,a>b, 1585 a+b+c=a*b*c。對應的程序以下: 1586 #include <stdio.h> 1587 void solve() 1588 { int a,b,c; 1589 for (a=1;a<=9;a++) 1590 for (b=0;b<=9;b++) 1591 for (c=0;c<=9;c++) 1592 { if (c>a && a>b && a+b+c==a*b*c) 1593 printf(" %d%d%d\n",a,b,c); 1594 } 1595 } 1596 void main() 1597 1598 34 1599 1600 1601 { printf("求解結果\n"); 1602 solve(); 1603 } 1604 上述程序的執行結果如圖1.30所示。 1605 1606 第1章 概論 1607 1608 1609 1610 1611 1612 圖1.30 程序執行結果 1613 8. 解:設該年級的人數爲x,租船數爲y。由於每隻船坐10人正好多出2個座位,則 1614 x=10*y-2;由於每隻船多坐2人即12人時可少租1只船(沒有說剛好所有座位佔滿),有 x+z=12*(y-1),z表示此時空出的座位,顯然z<12。讓y從1到100(實際上y取更大範圍 的結果是相同的)、z從0到11枚舉,求出最大的x便可。對應的程序以下: 1615 #include <stdio.h> 1616 int solve() 1617 { int x,y,z; 1618 for (y=1;y<=100;y++) 1619 for (z=0;z<12;z++) 1620 if (10*y-2==12*(y-1)-z) 1621 x=10*y-2; 1622 return x; 1623 } 1624 void main() 1625 { printf("求解結果\n"); 1626 printf(" 最多人數:%d\n",solve()); 1627 } 1628 上述程序的執行結果如圖1.31所示。 1629 1630 1631 1632 1633 1634 圖1.31 程序執行結果 1635 9. 解:採用蠻力法求出一個正整數n的各位數字和sum1,以及n的全部質因數的數 1636 字和sum2,若sum1=sum2,即爲Smitch數。從用戶輸入的n開始枚舉,如果Smitch 1637 數,輸出,本次結束,不然n++繼續查找大於n的最小Smitch數。對應的完整程序如 1638 下: 1639 #include <stdio.h> 1640 int Sum(int n) //求n的各位數字和 1641 { int sum=0; 1642 while (n>0) 1643 35 1644 1645 算法設計 1646 1647 { sum+=n%10; 1648 n=n/10; 1649 } 1650 return sum; 1651 } 1652 bool solve(int n) //判斷n是否爲Smitch數 1653 { int m=2; 1654 int sum1=Sum(n); 1655 int sum2=0; 1656 while (n>=m) 1657 { if (n%m==0) //找到一個質因數m 1658 { n=n/m; 1659 sum2+=Sum(m); 1660 } 1661 else 1662 m++; 1663 } 1664 if (sum1==sum2) 1665 return true; 1666 else 1667 return false; 1668 } 1669 void main() 1670 { int n; 1671 while (true) 1672 { scanf("%d",&n); 1673 if (n==0) break; 1674 while (!solve(n)) 1675 n++; 1676 printf("%d\n",n); 1677 } 1678 } 1679 10. 解:採用蠻力法,統計每一列相鄰相同顏色的棋格個數countj,在countj中求最 大值。對應的程序以下: 1680 #include <stdio.h> 1681 #define MAXN 51 1682 //問題表示 1683 int n; 1684 char board[MAXN][MAXN]; 1685 int getMaxArea() //蠻力法求解算法 1686 { int maxArea=0; 1687 for (int j=0; j<n; j++) 1688 { int countj=1; 1689 for (int i=1; i<n; i++) //統計第j列中相同顏色相鄰棋格個數 1690 { if (board[i][j]==board[i-1][j]) 1691 countj++; 1692 else 1693 countj=1; 1694 } 1695 1696 36 1697 第1章 概論 1698 1699 if (countj>maxArea) 1700 maxArea=countj; 1701 } 1702 return maxArea; 1703 } 1704 int main() 1705 { scanf("%d",&n); 1706 for (int i=0;i<n;i++) 1707 scanf("%s",board[i]); 1708 printf("%d\n",getMaxArea()); 1709 return 0; 1710 } 1711 11. 解:與《教程》中求全排列相似,但須要將求1~n的全排列改成按下標0~n-1 求a的全排列(下標從0開始)。採用非遞歸的程序以下: 1712 #include <stdio.h> 1713 #include <vector> 1714 using namespace std; 1715 vector<vector<int> > ps; //存放全排列 1716 void Insert(vector<int> s,int a[],int i,vector<vector<int> > &ps1) 1717 //在每一個集合元素中間插入i獲得ps1 1718 { vector<int> s1; 1719 vector<int>::iterator it; 1720 for (int j=0;j<=i;j++) //在s(含i個整數)的每一個位置插入a[i] 1721 { s1=s; 1722 it=s1.begin()+j; //求出插入位置 1723 s1.insert(it,a[i]); //插入整數a[i] 1724 ps1.push_back(s1); //添加到ps1中 1725 } 1726 } 1727 void Perm(int a[],int n) //求a[0..n-1]的全部全排列 1728 { vector<vector<int> > ps1; //臨時存放子排列 1729 vector<vector<int> >::iterator it; //全排列迭代器 1730 vector<int> s,s1; 1731 s.push_back(a[0]); 1732 ps.push_back(s); //添加{a[0]}集合元素 1733 for (int i=1;i<n;i++) //循環添加a[1]~a[n-1] 1734 { ps1.clear(); //ps1存放插入a[i]的結果 1735 for (it=ps.begin();it!=ps.end();++it) 1736 Insert(*it,a,i,ps1); //在每一個集合元素中間插入a[i]獲得ps1 1737 ps=ps1; 1738 } 1739 } 1740 void dispps() //輸出全排列ps 1741 { vector<vector<int> >::reverse_iterator it; //全排列的反向迭代器 1742 vector<int>::iterator sit; //排列集合元素迭代器 1743 for (it=ps.rbegin();it!=ps.rend();++it) 1744 { for (sit=(*it).begin();sit!=(*it).end();++sit) 1745 printf("%d",*sit); 1746 printf(" "); 1747 1748 37 1749 1750 算法設計 1751 1752 } 1753 printf("\n"); 1754 } 1755 void main() 1756 { int a[]={2,5,8}; 1757 int n=sizeof(a)/sizeof(a[0]); 1758 printf("a[0~%d]的全排序以下:\n ",n-1); Perm(a,n); 1759 dispps(); 1760 } 1761 上述程序的執行結果如圖1.32所示。 1762 1763 1764 1765 1766 1767 圖1.32 程序執行結果 1768 1.5 第5章─回溯法 1769 1770 1.5.1 練習題 1771 1. 回溯法在問題的解空間樹中,按( )策略,從根結點出發搜索解空間樹。 1772 A.廣度優先 B.活結點優先 C.擴展結點優先 D.深度優先 1773 2. 關於回溯法如下敘述中不正確的是( )。 1774 A.回溯法有「通用解題法」之稱,它能夠系統地搜索一個問題的全部解或任意解 1775 B.回溯法是一種既帶系統性又帶有跳躍性的搜索算法 1776 C.回溯算法須要藉助隊列這種結構來保存從根結點到當前擴展結點的路徑 1777 D.回溯算法在生成解空間的任一結點時,先判斷該結點是否可能包含問題的解,若是 1778 確定不包含,則跳過對該結點爲根的子樹的搜索,逐層向祖先結點回溯 1779 3. 回溯法的效率不依賴於下列哪些因素( )。 1780 A.肯定解空間的時間 B.知足顯約束的值的個數 1781 C.計算約束函數的時間 D.計算限界函數的時間 1782 4. 下面( )函數是回溯法中爲避免無效搜索採起的策略。 1783 A.遞歸函數 B.剪枝函數 C.隨機數函數 D.搜索函數 1784 5.回溯法的搜索特色是什麼? 1785 6. 用回溯法解0/1揹包問題時,該問題的解空間是何種結構?用回溯法解流水做業調 1786 度問題時,該問題的解空間是何種結構? 1787 7. 對於遞增序列a[]={1,2,3,4,5},採用例5.4的回溯法求全排列,以一、2開頭 1788 的排列必定最早出現嗎?爲何? 1789 8. 考慮n皇后問題,其解空間樹爲由一、2、…、n構成的n!種排列所組成。現用回 1790 1791 38 1792 第1章 概論 1793 1794 溯法求解,要求: 1795 (1)經過解搜索空間說明n=3時是無解的。 1796 (2)給出剪枝操做。 1797 (3)最壞狀況下在解空間樹上會生成多少個結點?分析算法的時間複雜度。 1798 9. 設計一個算法求解簡單裝載問題,設有一批集裝箱要裝上一艘載重量爲W的輪 1799 船,其中編號爲i(0≤i≤n-1)的集裝箱的重量爲wi。現要從n個集裝箱中選出若干裝上 輪船,使它們的重量之和正好爲W。若是找到任一種解返回true,不然返回false。 1800 10. 給定若干個正整數a0、a0 、…、an-1 ,從中選出若干數,使它們的和剛好爲k, 要求找選擇元素個數最少的解。 1801 11. 設計求解有重複元素的排列問題的算法,設有n個元素a[]={a0,a1,…,an-1), 其中可能含有重複的元素,求這些元素的全部不一樣排列。如a[]={1,1,2},輸出結果是 (1,1,2),(1,2,1),(2,1,1)。 1802 12. 採用遞歸回溯法設計一個算法求1~n的n個整數中取出m個元素的排列,要求每一個 元素最多隻能取一次。例如,n=3,m=2的輸出結果是(1,2),(1,3),(2,1), (2,3),(3,1),(3,2)。 1803 13. 對於n皇后問題,有人認爲當n爲偶數時,其解具備對稱性,即n皇后問題的解個 數剛好爲n/2皇后問題的解個數的2倍,這個結論正確嗎?請編寫回溯法程序對n=4、6、 8、10的狀況進行驗證。 1804 14. 給定一個無向圖,由指定的起點前往指定的終點,途中通過全部其餘頂點且只經 過一次,稱爲哈密頓路徑,閉合的哈密頓路徑稱做哈密頓迴路(Hamiltonian cycle)。設計 一個回溯算法求無向圖的全部哈密頓迴路。 1805 1.5.2 練習題參考答案 1806 1. 答:D。 1807 2. 答:回溯算法是採用深度優先遍歷的,須要藉助系統棧結構來保存從根結點到當 1808 前擴展結點的路徑。答案爲C。 1809 3. 答:回溯法解空間是虛擬的,沒必要肯定整個解空間。答案爲A。 1810 4. 答:B。 1811 5. 答:回溯法在解空間樹中採用深度優先遍歷方式進行解搜索,即用約束條件和限 1812 界函數考察解向量元素x[i]的取值,若是x[i]是合理的就搜索x[i]爲根結點的子樹,若是 1813 x[i]取完了全部的值,便回溯到x[i-1]。 1814 6. 答:用回溯法解0/1揹包問題時,該問題的解空間是子集樹結構。用回溯法解流水 做業調度問題時,該問題的解空間是排列樹結構。 1815 7. 答:是的。對應的解空間是一棵排列樹,如圖1.33所示給出前面3層部分,顯然 最早產生的排列是從G結點擴展出來的葉子結點,它們就是以一、2開頭的排列。 1816 1817 1818 1819 1820 1821 39 1822 1823 1824 1825 1826 1 1827 1828 1829 算法設計 1830 1831 A 1832 23 4 1833 1834 1835 1836 1837 1838 5 1839 1840 2 1841 1842 B C 1843 34 5 1844 1845 D E F 1846 G H I J 1847 圖1.33 部分解空間樹 1848 8. 答:(1)n=3時的解搜索空間如圖1.34所示,不能獲得任何葉子結點,全部無 1849 解。 1850 (2)剪枝操做是任何兩個皇后不能同行、同列和同兩條對角線。 1851 (3)最壞狀況下每一個結點擴展n個結點,共有nn個結點,算法的時間複雜度爲 1852 O(nn)。 1853 (*,*,*) 1854 1855 1856 1857 (1,*,*) 1858 1859 (1,3,*) 1860 1861 1862 1863 1864 1865 (2,*,*) 1866 1867 1868 1869 1870 (3,*,*) 1871 1872 (3,1,*) 1873 1874 圖1.34 3皇后問題的解搜索空間 1875 9. 解:用數組w[0..n-1]存放n個集裝箱的重量,採用相似判斷子集和是否存在解的 1876 方法求解。對應完整的求解程序以下: 1877 #include <stdio.h> 1878 #define MAXN 20 //最多集裝箱個數 1879 //問題表示 1880 int n=5,W; 1881 int w[]={2,9,5,6,3}; 1882 int count; //全局變量,累計解個數 1883 void dfs(int tw,int rw,int i) //求解簡單裝載問題 1884 { if (i>=n) //找到一個葉子結點 1885 { if (tw==W) //找到一個知足條件的解,輸出它 1886 count++; 1887 } 1888 else //還沒有找完 1889 { rw-=w[i]; //求剩餘的集裝箱重量和 1890 if (tw+w[i]<=W) //左孩子結點剪枝:選取知足條件的集裝箱w[i] 1891 dfs(tw+w[i],rw,i+1); //選取第i個集裝箱 1892 if (tw+rw>=W) //右孩子結點剪枝:剪除不可能存在解的結點 1893 dfs(tw,rw,i+1); //不選取第i個集裝箱,回溯 1894 } 1895 } 1896 bool solve() //判斷簡單裝載問題是否存在解 1897 1898 40 1899 第1章 概論 1900 1901 { count=0; 1902 int rw=0; 1903 for (int j=0;j<n;j++) //求全部集裝箱重量和rw rw+=w[j]; 1904 dfs(0,rw,0); //i從0開始 1905 if (count>0) 1906 return true; 1907 else 1908 return false; 1909 } 1910 void main() 1911 { printf("求解結果\n"); 1912 W=4; 1913 printf(" W=%d時%s\n",W,(solve()?"存在解":"沒有解")); W=10; 1914 printf(" W=%d時%s\n",W,(solve()?"存在解":"沒有解")); W=12; 1915 printf(" W=%d時%s\n",W,(solve()?"存在解":"沒有解")); W=21; 1916 printf(" W=%d時%s\n",W,(solve()?"存在解":"沒有解")); } 1917 本程序執行結果如圖1.35所示。 1918 1919 1920 1921 1922 1923 1924 圖1.35 程序執行結果 1925 10. 解:這是一個典型的解空間爲子集樹的問題,採用子集樹的回溯算法框架。當找 1926 到一個解後經過選取的元素個數進行比較求最優解minpath。對應的完整程序以下: 1927 #include <stdio.h> 1928 #include <vector> 1929 using namespace std; 1930 //問題表示 1931 int a[]={1,2,3,4,5}; //設置爲全局變量 1932 int n=5,k=9; 1933 vector<int> minpath; //存放最優解 1934 //求解結果表示 1935 int minn=n; //最多選擇n個元素 1936 void disppath() //輸出一個解 1937 { printf(" 選擇的元素:"); 1938 for (int j=0;j<minpath.size();j++) 1939 printf("%d ",minpath[j]); 1940 printf("元素個數=%d\n",minn); 1941 } 1942 1943 41 1944 1945 算法設計 1946 1947 void dfs(vector<int> path,int sum,int start) //求解算法 1948 { if (sum==k) //若是找到一個解,不必定到葉子結點 { if (path.size()<minn) 1949 { minn=path.size(); 1950 minpath=path; 1951 } 1952 return; 1953 } 1954 if (start>=n) return; //所有元素找完,返回 1955 dfs(path,sum,start+1); //不選擇a[start] 1956 path.push_back(a[start]); //選擇a[start] 1957 dfs(path,sum+a[start],start+1); 1958 } 1959 void main() 1960 { vector<int> path; //path存放一個子集 1961 dfs(path,0,0); 1962 printf("最優解:\n"); 1963 disppath(); 1964 } 1965 上述程序的執行結果如圖1.36所示。 1966 1967 1968 1969 1970 1971 圖1.36 程序執行結果 1972 11. 解:在回溯法求全排列的基礎上,增長元素的重複性判斷。例如,對於a[]={1, 1973 1,2},不判斷重複性時輸出(1,1,2),(1,2,1),(1,1,2),(1,2,1), (2,1,1),(2,1,1),共6個,有3個是重複的。重複性判斷是這樣的,對於在擴 展a[i]時,僅僅將與a[i..j-1]沒有出現的元素a[j]交換到a[i]的位置,若是出現,對應的排 列已經在前面求出了。對應的完整程序以下: 1974 #include <stdio.h> 1975 bool ok(int a[],int i,int j) //ok用於判別重複元素 1976 { if (j>i) 1977 { for(int k=i;k<j;k++) 1978 if (a[k]==a[j]) 1979 return false; 1980 } 1981 return true; 1982 } 1983 void swap(int &x,int &y) //交換兩個元素 1984 { int tmp=x; 1985 x=y; y=tmp; 1986 } 1987 void dfs(int a[],int n,int i) //求有重複元素的排列問題 1988 { if (i==n) 1989 1990 42 1991 第1章 概論 1992 1993 { for(int j=0;j<n;j++) 1994 printf("%3d",a[j]); 1995 printf("\n"); 1996 } 1997 else 1998 { for (int j=i;j<n;j++) 1999 if (ok(a,i,j)) //選取與a[i..j-1]不重複的元素a[j] { swap(a[i],a[j]); 2000 dfs(a,n,i+1); 2001 swap(a[i],a[j]); 2002 } 2003 } 2004 } 2005 void main() 2006 { int a[]={1,2,1,2}; 2007 int n=sizeof(a)/sizeof(a[0]); 2008 printf("序列("); 2009 for (int i=0;i<n-1;i++) 2010 printf("%d ",a[i]); 2011 printf("%d)的全部不一樣排列:\n",a[n-1]); 2012 dfs(a,n,0); 2013 } 2014 上述程序的執行結果如圖1.37所示。 2015 2016 2017 2018 2019 2020 2021 2022 圖1.37 程序執行結果 2023 12. 解:採用求全排列的遞歸框架。選取的元素個數用i表示(i從1開始),當i>m 2024 時達到一個葉子結點,輸出一個排列。爲了不重複,用used數組實現,used[i]=0表示 沒有選擇整數i,used[i]=1表示已經選擇整數i。對應的完整程序以下: 2025 #include <stdio.h> 2026 #include <string.h> 2027 #define MAXN 20 2028 #define MAXM 10 2029 int m,n; 2030 int x[MAXM]; //x[1..m]存放一個排列 2031 bool used[MAXN]; 2032 void dfs(int i) //求n個元素中m個元素的全排列 2033 { if (i>m) 2034 { for (int j=1;j<=m;j++) 2035 printf(" %d",x[j]); //輸出一個排列 2036 printf("\n"); 2037 2038 43 2039 2040 算法設計 2041 2042 } 2043 else 2044 { for (int j=1;j<=n;j++) 2045 { if (!used[j]) 2046 { used[j]=true; //修改used[i] 2047 x[i]=j; //x[i]選擇j 2048 dfs(i+1); //繼續搜索排列的下一個元素 used[j]=false; //回溯:恢復used[i] 2049 } 2050 } 2051 } 2052 } 2053 void main() 2054 { n=4,m=2; 2055 memset(used,0,sizeof(used)); //初始化爲0 2056 printf("n=%d,m=%d的求解結果\n",n,m); 2057 dfs(1); //i從1開始 2058 } 2059 上述程序的執行結果如圖1.38所示。 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 圖1.38 程序執行結果 2071 13. 解:這個結論不正確。驗證程序以下: 2072 #include <stdio.h> 2073 #include <stdlib.h> 2074 #define MAXN 10 2075 int q[MAXN]; 2076 bool place(int i) //測試第i行的q[i]列上可否擺放皇后 2077 { int j=1; 2078 if (i==1) return true; 2079 while (j<i) //j=1~i-1是已放置了皇后的行 2080 { if ((q[j]==q[i]) || (abs(q[j]-q[i])==abs(j-i))) 2081 //該皇后是否與之前皇后同列,位置(j,q[j])與(i,q[i])是否同對角線 return false; 2082 j++; 2083 } 2084 return true; 2085 } 2086 44 2087 第1章 概論 2088 2089 int Queens(int n) //求n皇后問題的解個數 2090 { int count=0,k; //計數器初始化 2091 int i=1; //i爲當前行 2092 q[1]=0; //q[i]爲皇后i的列號 2093 while (i>0) 2094 { q[i]++; //移到下一列 2095 while (q[i]<=n && !place(i)) 2096 q[i]++; 2097 if (q[i]<=n) 2098 { if (i==n) 2099 count++; //找到一個解計數器count加1 2100 else 2101 { 2102 i++;; q[i]=0; 2103 } 2104 } 2105 else i--; //回溯 2106 } 2107 return count; 2108 } 2109 void main() 2110 { printf("驗證結果以下:\n"); 2111 for (int n=4;n<=10;n+=2) 2112 if (Queens(n)==2*Queens(n/2)) 2113 printf(" n=%d: 正確\n",n); 2114 else 2115 printf(" n=%d: 錯誤\n",n); 2116 } 2117 上述程序的執行結果如圖1.39所示。從執行結果看出結論是不正確的。 2118 2119 2120 2121 2122 2123 2124 圖1.39 程序執行結果 2125 14. 解:假設給定的無向圖有n個頂點(頂點編號從0到n-1),採用鄰接矩陣數組a 2126 (0/1矩陣)存放,求從頂點v出發回到頂點v的哈密頓迴路。採用回溯法,解向量爲 2127 x[0..n],x[i]表示第i步找到的頂點編號(i=n-1時表示除了起點v外其餘頂點都查找了), 初始時將起點v存放到x[0],i從1開始查找,i>0時循環:爲x[i]找到一個合適的頂點, 當i=n-1時,若頂點x[i]到頂點v有邊對應一個解;不然繼續查找下一個頂點。若是不能 爲x[i]找到一個合適的頂點,則回溯。採用非遞歸回溯框架(與《教程》中求解n皇后問 題的非遞歸回溯框架相似)的完整程序以下: 2128 #include <stdio.h> 2129 #define MAXV 10 2130 2131 45 2132 2133 算法設計 2134 2135 //求解問題表示 2136 int n=5; //圖中頂點個數 2137 int a[MAXV][MAXV]={{0,1,1,1,0},{1,0,0,1,1},{1,0,0,0,1},{1,1,0,0,1},{0,1,1,1,0}}; //鄰接矩陣數組 2138 //求解結果表示 2139 int x[MAXV]; 2140 int count; 2141 void dispasolution() //輸出一個解路徑 2142 { for (int i=0;i<=n-1;i++) 2143 printf("(%d,%d) ",x[i],x[i+1]); 2144 printf("\n"); 2145 } 2146 bool valid(int i) //判斷頂點第i個頂點x[i]的有效性 2147 { if (a[x[i-1]][x[i]]!=1) //x[i-1]到x[i]沒有邊,返回false 2148 return false; 2149 for (int j=0;j<=i-1;j++) 2150 if (x[i]==x[j]) //頂點i重複出現,返回false 2151 return false; 2152 return true; 2153 } 2154 void Hamiltonian(int v) //求從頂點v出發的哈密頓迴路 2155 { x[0]=v; //存放起點 2156 int i=1; 2157 x[i]=-1; //從頂點-1+1=0開始試探 2158 while (i>0) //還沒有回溯到頭,循環 2159 { x[i]++; 2160 while (!valid(i) && x[i]<n) 2161 x[i]++; //試探一個頂點x[i] 2162 if (x[i]<n) //找到一個有效的頂點x[i] 2163 { if (i==n-1) //達到葉子結點 2164 { if (a[x[i]][v]==1) 2165 { x[n]=v; //找到一個解 2166 printf(" 第%d個解: ",count++); 2167 dispasolution(); 2168 } 2169 } 2170 else 2171 { 2172 i++; x[i]=-1; 2173 } 2174 } 2175 else 2176 i--; //回溯 2177 } 2178 } 2179 void main() 2180 { printf("求解結果\n"); 2181 for (int v=0;v<n;v++) 2182 { printf(" 從頂點%d出發的哈密頓迴路:\n",v); 2183 count=1; 2184 2185 46 2186 第1章 概論 2187 2188 Hamiltonian(v); //從頂點v出發 2189 } 2190 } 2191 上述程序對如圖1.40所示的無向圖求從每一個頂點出發的哈密頓迴路,程序執行結果 如圖1.41所示。 2192 1 2193 2194 2195 0 2196 2197 2198 2199 3 2200 2201 2202 2 2203 2204 2205 2206 4 2207 2208 圖1.40 一個無向圖 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 圖1.41 程序執行結果 2227 1.6 第6章─分枝限界法 2228 2229 1.6.1 練習題 2230 1. 分枝限界法在問題的解空間樹中,按( )策略,從根結點出發搜索解空間樹。 2231 A.廣度優先 B.活結點優先 C.擴展結點優先 D. 深度優先 2232 2. 常見的兩種分枝限界法爲( )。 2233 A.廣度優先分枝限界法與深度優先分枝限界法 2234 2235 47 2236 2237 算法設計 2238 2239 B.隊列式(FIFO)分枝限界法與堆棧式分枝限界法 2240 C.排列樹法與子集樹法 2241 D.隊列式(FIFO)分枝限界法與優先隊列式分枝限界法 2242 3. 分枝限界法求解0/1揹包問題時,活結點表的組織形式是( )。 2243 A.小根堆 B.大根堆 C.棧 D.數組 2244 4. 採用最大效益優先搜索方式的算法是( )。 2245 A.分支界限法 B.動態規劃法 C.貪心法 D.回溯法 2246 5. 優先隊列式分枝限界法選取擴展結點的原則是( )。 2247 A.先進先出 B.後進先出 C.結點的優先級 D.隨機 2248 6. 簡述分枝限界法的搜索策略。 2249 7. 有一個0/1揹包問題,其中n=4,物品重量爲(4,7,5,3),物品價值爲(40, 2250 42,25,12),揹包最大載重量W=10,給出採用優先隊列式分枝限界法求最優解的過程。 8. 有一個流水做業調度問題,n=4,a[]={5,10,9,7},b[]={7,5,9,8},給出採 2251 用優先隊列式分枝限界法求一個解的過程。 2252 9. 有一個含n個頂點(頂點編號爲0~n-1)的帶權圖,採用鄰接矩陣數組A表示, 2253 採用分枝限界法求從起點s到目標點t的最短路徑長度,以及具備最短路徑長度的路徑條 數。 2254 10. 採用優先隊列式分枝限界法求解最優裝載問題。給出如下裝載問題的求解過程和 結果:n=5,集裝箱重量爲w=(5,2,6,4,3),限重爲W=10。在裝載重量相同時,最 優裝載方案是集裝箱個數最少的方案。 2255 1.6.2 練習題參考答案 2256 1. 答:A。 2257 2. 答:D。 2258 3. 答:B。 2259 4. 答:A。 2260 5. 答:C。 2261 6. 答:分枝限界法的搜索策略是廣度優先遍歷,經過限界函數能夠快速找到一個解 2262 或者最優解。 2263 7. 答:求解過程以下: 2264 (1)根結點1進隊,對應結點值:e.i=0,e.w=0,e.v=0,e.ub=76,x:[0,0,0,0]。 2265 (2)出隊結點1:左孩子結點2進隊,對應結點值:e.no=2,e.i=1,e.w=4, 2266 e.v=40,e.ub=76,x:[1,0,0,0];右孩子結點3進隊,對應結點值:e.no=3,e.i=1, 2267 e.w=0,e.v=0,e.ub=57,x:[0,0,0,0]。 2268 (3)出隊結點2:左孩子超重;右孩子結點4進隊,對應結點值:e.no=4,e.i=2, 2269 e.w=4,e.v=40,e.ub=69,x:[1,0,0,0]。 2270 (4)出隊結點4:左孩子結點5進隊,對應結點值:e.no=5,e.i=3,e.w=9, 2271 e.v=65,e.ub=69,x:[1,0,1,0];右孩子結點6進隊,對應結點值:e.no=6,e.i=3, 2272 e.w=4,e.v=40,e.ub=52,x:[1,0,0,0]。 2273 48 2274 第1章 概論 2275 2276 (5)出隊結點5:產生一個解,maxv= 65,bestx:[1,0,1,0]。 2277 (6)出隊結點3:左孩子結點8進隊,對應結點值:e.no=8,e.i=2,e.w=7, 2278 e.v=42,e.ub=57,x:[0,1,0,0];右孩子結點9被剪枝。 2279 (7)出隊結點8:左孩子超重;右孩子結點10被剪枝。 2280 (8)出隊結點6:左孩子結點11超重;右孩子結點12被剪枝。 2281 (9)隊列空,算法結束,產生的最優解:maxv= 65,bestx:[1,0,1,0]。 2282 8. 答:求解過程以下: 2283 (1)根結點1進隊,對應結點值:e.i=0,e.f1=0,e.f2=0,e.lb=29, x:[0,0,0, 2284 0]。 2285 (2)出隊結點1:擴展結點以下: 2286 進隊(j=1):結點2,e.i=1,e.f1=5,e.f2=12,e.lb=27,x:[1,0,0,0]。 2287 進隊(j=2):結點3,e.i=1,e.f1=10,e.f2=15,e.lb=34,x:[2,0,0,0]。 2288 進隊(j=3):結點4,e.i=1,e.f1=9,e.f2=18,e.lb=29,x:[3,0,0,0]。 2289 進隊(j=4):結點5,e.i=1,e.f1=7,e.f2=15,e.lb=28,x:[4,0,0,0]。 2290 (3)出隊結點2:擴展結點以下: 2291 進隊(j=2):結點6,e.i=2,e.f1=15,e.f2=20,e.lb=32,x:[1,2,0,0]。 2292 進隊(j=3):結點7,e.i=2,e.f1=14,e.f2=23,e.lb=27,x:[1,3,0,0]。 2293 進隊(j=4):結點8,e.i=2,e.f1=12,e.f2=20,e.lb=26,x:[1,4,0,0]。 2294 (4)出隊結點8:擴展結點以下: 2295 進隊(j=2):結點9,e.i=3,e.f1=22,e.f2=27,e.lb=31,x:[1,4,2,0]。 2296 進隊(j=3):結點10,e.i=3,e.f1=21,e.f2=30,e.lb=26,x:[1,4,3,0]。 2297 (5)出隊結點10,擴展一個j=2的子結點,有e.i=4,到達葉子結點,產生的一個解 2298 是e.f1=31,e.f2=36,e.lb=31,x=[1,4,3,2]。 2299 該解對應的調度方案是:第1步執行做業1,第2步執行做業4,第3步執行做業 2300 3,第4步執行做業2,總時間=36。 2301 9. 解:採用優先隊列式分枝限界法求解,隊列中結點的類型以下: 2302 struct NodeType 2303 { int vno; //頂點的編號 2304 int length; //當前結點的路徑長度 2305 bool operator<(const NodeType &s) const //重載<關係函數 2306 { return length>s.length; } //length越小越優先 2307 }; 2308 從頂點s開始廣度優先搜索,找到目標點t後比較求最短路徑長度及其路徑條數。對 應的完整程序以下: 2309 #include <stdio.h> 2310 #include <queue> 2311 using namespace std; 2312 #define MAX 11 2313 #define INF 0x3f3f3f3f 2314 //問題表示 2315 int A[MAX][MAX]={ //一個帶權有向圖 2316 2317 49 2318 2319 算法設計 2320 2321 {0,1,4,INF,INF}, 2322 {INF,0,INF,1,5}, 2323 {INF,INF,0,INF,1}, 2324 {INF,INF,2,0,3}, 2325 {INF,INF,INF,INF,INF} }; 2326 int n=5; 2327 //求解結果表示 2328 int bestlen=INF; //最優路徑的路徑長度 2329 int bestcount=0; //最優路徑的條數 2330 struct NodeType 2331 { int vno; //頂點的編號 2332 int length; //當前結點的路徑長度 2333 bool operator<(const NodeType &s) const //重載>關係函數 2334 { return length>s.length; } //length越小越優先 2335 }; 2336 void solve(int s,int t) //求最短路徑問題 2337 { NodeType e,e1; //定義2個結點 2338 priority_queue<NodeType> qu; //定義一個優先隊列qu 2339 e.vno=s; //構造根結點 2340 e.length=0; 2341 qu.push(e); //根結點進隊 2342 while (!qu.empty()) //隊不空循環 2343 { e=qu.top(); qu.pop(); //出隊結點e做爲當前結點 2344 if (e.vno==t) //e是一個葉子結點 2345 { if (e.length<bestlen) //比較找最優解 2346 { bestcount=1; 2347 bestlen=e.length; //保存最短路徑長度 2348 } 2349 else if (e.length==bestlen) 2350 bestcount++; 2351 } 2352 else //e不是葉子結點 2353 { for (int j=0; j<n; j++) //檢查e的全部相鄰頂點 2354 if (A[e.vno][j]!=INF && A[e.vno][j]!=0) //頂點e.vno到頂點j有邊 { if (e.length+A[e.vno][j]<bestlen) //剪枝 2355 { e1.vno=j; 2356 e1.length=e.length+A[e.vno][j]; 2357 qu.push(e1); //有效子結點e1進隊 2358 } 2359 } 2360 } 2361 } 2362 } 2363 void main() 2364 { int s=0,t=4; 2365 solve(s,t); 2366 if (bestcount==0) 2367 printf("頂點%d到%d沒有路徑\n",s,t); 2368 else 2369 { printf("頂點%d到%d存在路徑\n",s,t); 2370 2371 50 2372 第1章 概論 2373 2374 printf(" 最短路徑長度=%d,條數=%d\n", bestlen,bestcount); //輸出:5 3 2375 } 2376 } 2377 上述程序的執行結果如圖1.39所示。 2378 2379 2380 2381 2382 2383 圖1.39 程序執行結果 2384 10. 解:採用優先隊列式分枝限界法求解。設計優先隊列 2385 priority_queue<NodeType>,並設計優先隊列的關係比較函數Cmp,指定按結點的ub值進 行比較,即ub值越大的結點越先出隊。對應的完整程序以下: 2386 #include <stdio.h> 2387 #include <queue> 2388 using namespace std; 2389 #define MAXN 21 //最多的集裝箱數 2390 //問題表示 2391 int n=5; 2392 int W=10; 2393 int w[]={0,5,2,6,4,3}; //集裝箱重量,不計下標0的元素 2394 //求解結果表示 2395 int bestw=0; //存放最大重量,全局變量 2396 int bestx[MAXN]; //存放最優解,全局變量 2397 int Count=1; //搜索空間中結點數累計,全局變量 2398 typedef struct 2399 { int no; //結點編號 2400 int i; //當前結點在解空間中的層次 2401 int w; //當前結點的總重量 2402 int x[MAXN]; //當前結點包含的解向量 2403 int ub; //上界 2404 } NodeType; 2405 struct Cmp //隊列中關係比較函數 2406 { bool operator()(const NodeType &s,const NodeType &t) 2407 { return (s.ub<t.ub) || (s.ub==t.ub && s.x[0]>t.x[0]); 2408 //ub越大越優先,當ub相同時x[0]越小越優先 2409 } 2410 }; 2411 void bound(NodeType &e) //計算分枝結點e的上界 2412 { int i=e.i+1; 2413 int r=0; //r爲剩餘集裝箱的重量 2414 while (i<=n) 2415 { r+=w[i]; 2416 i++; 2417 } 2418 e.ub=e.w+r; 2419 2420 51 2421 2422 算法設計 2423 2424 } 2425 void Loading() //求裝載問題的最優解 2426 { NodeType e,e1,e2; //定義3個結點 2427 priority_queue<NodeType,vector<NodeType>,Cmp > qu; //定義一個優先隊列qu 2428 e.no=Count++; //設置結點編號 2429 e.i=0; //根結點置初值,其層次計爲0 2430 e.w=0; 2431 for (int j=0; j<=n; j++) //初始化根結點的解向量 2432 e.x[j]=0; 2433 bound(e); //求根結點的上界 2434 qu.push(e); //根結點進隊 2435 while (!qu.empty()) //隊不空循環 2436 { e=qu.top(); qu.pop(); //出隊結點e做爲當前結點 2437 if (e.i==n) //e是一個葉子結點 2438 { if ((e.w>bestw) || (e.w==bestw && e.x[0]<bestx[0])) //比較找最優解 { bestw=e.w; //更新bestw 2439 for (int j=0;j<=e.i;j++) 2440 bestx[j]=e.x[j]; //複製解向量e.x->bestx 2441 } 2442 } 2443 else //e不是葉子結點 2444 { if (e.w+w[e.i+1]<=W) //檢查左孩子結點 2445 { e1.no=Count++; //設置結點編號 2446 e1.i=e.i+1; //創建左孩子結點 2447 e1.w=e.w+w[e1.i]; 2448 for (int j=0; j<=e.i; j++) 2449 e1.x[j]=e.x[j]; //複製解向量e.x->e1.x 2450 e1.x[e1.i]=1; //選擇集裝箱i 2451 e1.x[0]++; //裝入集裝箱數增1 2452 bound(e1); //求左孩子結點的上界 2453 qu.push(e1); //左孩子結點進隊 2454 } 2455 e2.no=Count++; //設置結點編號 2456 e2.i=e.i+1; //創建右孩子結點 2457 e2.w=e.w; 2458 for (int j=0; j<=e.i; j++) //複製解向量e.x->e2.x 2459 e2.x[j]=e.x[j]; 2460 e2.x[e2.i]=0; //不選擇集裝箱i 2461 bound(e2); //求右孩子結點的上界 2462 if (e2.ub>bestw) //若右孩子結點可行,則進隊,不然被剪枝 qu.push(e2); 2463 } 2464 } 2465 } 2466 void disparr(int x[],int len) //輸出一個解向量 2467 { for (int i=1;i<=len;i++) 2468 printf("%2d",x[i]); 2469 } 2470 void dispLoading() //輸出最優解 2471 { printf(" X=["); 2472 2473 52 2474 第1章 概論 2475 2476 disparr(bestx,n); 2477 printf("],裝入總價值爲%d\n",bestw); 2478 } 2479 void main() 2480 { Loading(); 2481 printf("求解結果:\n"); 2482 dispLoading(); //輸出最優解 } 2483 上述程序的執行結果如圖1.40所示。 2484 2485 2486 2487 2488 2489 圖1.40 程序執行結果 2490 1.7 第7章─貪心法 2491 2492 1.7.1 練習題 2493 1. 下面是貪心算法的基本要素的是( )。 2494 A.重疊子問題 B.構造最優解 C.貪心選擇性質 D.定義最優解 2495 2. 下面問題( )不能使用貪心法解決。 2496 A.單源最短路徑問題 B.n皇后問題 C.最小花費生成樹問題 D.揹包問題 2497 3. 採用貪心算法的最優裝載問題的主要計算量在於將集裝箱依其重量從小到大排 2498 序,故算法的時間複雜度爲( )。 2499 A.O(n) B.O(n2) C.O(n3) D.O(nlog2n) 2500 4. 關於0/ 1揹包問題如下描述正確的是( )。 2501 A.可使用貪心算法找到最優解 2502 B.能找到多項式時間的有效算法 2503 C.使用教材介紹的動態規劃方法可求解任意0-1揹包問題 2504 D.對於同一揹包與相同的物品,作揹包問題取得的總價值必定大於等於作0/1揹包問 2505 題 2506 5. 一棵哈夫曼樹共有215個結點,對其進行哈夫曼編碼,共能獲得( )個不一樣的碼 2507 字。 2508 A.107 B.108 C.214 D.215 2509 6. 求解哈夫曼編碼中如何體現貪心思路? 2510 7. 舉反例證實0/1揹包問題若使用的算法是按照vi/wi的非遞減次序考慮選擇的物 2511 品,即只要正在被考慮的物品裝得進就裝入揹包,則此方法不必定能獲得最優解(此題說 明0/1揹包問題與揹包問題的不一樣)。 2512 2513 53 2514 2515 算法設計 2516 2517 8. 求解硬幣問題。有1分、2分、5分、10分、50分和100分的硬幣各若干枚,現 在要用這些硬幣來支付W元,最少須要多少枚硬幣。 2518 9. 求解正整數的最大乘積分解問題。將正整數n分解爲若干個互不相同的天然數之 和,使這些天然數的乘積最大。 2519 10. 求解乘船問題。有n我的,第i我的體重爲wi(0≤i<n)。每艘船的最大載重量均 爲C,且最多隻能乘兩我的。用最少的船裝載全部人。 2520 11. 求解會議安排問題。有一組會議A和一組會議室B,A[i]表示第i個會議的參加人 數,B[j]表示第j個會議室最多能夠容納的人數。當且僅當A[i]≤B[j]時,第j個會議室可 以用於舉辦第i個會議。給定數組A和數組B,試問最多能夠同時舉辦多少個會議。例 如,A[]={1,2,3},B[]={3,2,4},結果爲3;若A[]={3,4,3,1},B[]={1,2,2, 6},結果爲2. 2521 12. 假設要在足夠多的會場裏安排一批活動,n個活動編號爲1~n,每一個活動有開始 時間bi和結束時間ei(1≤i≤n)。設計一個有效的貪心算法求出最少的會場個數。 2522 13. 給定一個m×n的數字矩陣,計算從左到右走過該矩陣且通過的方格中整數最小的 路徑。一條路徑能夠從第1列的任意位置出發,到達第n列的任意位置,每一步爲從第i 列走到第i+1列相鄰行(水平移動或沿45度斜線移動),如圖1.41所示。第1行和最後一 行看做是相鄰的,即應當把這個矩陣當作是一個捲起來的圓筒。 2523 2524 2525 2526 2527 2528 2529 2530 2531 圖1.41 每一步的走向 2532 兩個略有不一樣的5×6的數字矩陣的最小路徑如圖1.42所示,只有最下面一行的數不 2533 同。右邊矩陣的路徑利用了第一行與最後一行相鄰的性質。 2534 輸入:包含多個矩陣,每一個矩陣的第一行爲兩個數m和n,分別表示矩陣的行數和列 2535 數,接下來的m×n個整數按行優先的順序排列,即前n個數組成第一行,接下的n個數 組成第2行,依此類推。相鄰整數間用一個或多個空格分隔。注意這些數不必定是正數。 輸入中可能有一個或多個矩陣描述,直到輸入結束。每一個矩陣的行數在1到10之間,列 數在1到100之間。 2536 輸出:對每一個矩陣輸出兩行,第一行爲最小整數之和的路徑,路徑由n個整數組成, 表示路徑通過的行號,若是這樣的路徑不止一條,輸出字典序最小一條。 2537 2538 2539 3 4 1 2 8 6 2540 6 1 8 2 7 4 2541 5 9 3 9 9 5 2542 8 4 1 3 2 6 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 3 4 1 2 8 6 2555 6 1 8 2 7 4 2556 5 9 3 9 9 5 2557 8 4 1 3 2 6 2558 3 7 2 1 2 3 2559 54 2560 第1章 概論 2561 2562 6 1 8 2 7 4 2563 5 9 3 9 9 5 2564 8 4 1 3 2 6 2565 3 7 2 8 6 4 2566 輸出結果: 2567 1 2 3 4 4 5 2568 16 2569 1.7.2 練習題參考答案 2570 1. 答:C。 2571 2. 答:n皇后問題的解不知足貪心選擇性質。答案爲B。 2572 3. 答:D。 2573 4. 答:因爲揹包問題能夠取物品的一部分,因此總價值必定大於等於作0/1揹包問 2574 題。答案爲D。 2575 5. 答:這裏n=215,哈夫曼樹中n1=0,而n0=n2+1,n=n0+n1+n2=2n0-1, 2576 n0=(n+1)/2=108。答案爲B。 2577 6. 答:在構造哈夫曼樹時每次都是將兩棵根結點最小的樹合併,從而體現貪心的思 2578 路。 2579 7. 證實:例如,n=3,w={3,2,2},v={7,4,4},W=4時,因爲7/3最大,若按題 目要求的方法,只能取第一個,收益是7。而此實例的最大的收益應該是8,取第二、3 個物品。 2580 8. 解:用結構體數組A存放硬幣數據,A[i].v存放硬幣i的面額,A[i].c存放硬幣i的 枚數。採用貪心思路,首先將數組A按面額遞減排序,再兌換硬幣,每次儘量兌換面額 大的硬幣。對應的完整程序以下: 2581 #include <stdio.h> 2582 #include <algorithm> 2583 using namespace std; 2584 #define min(x,y) ((x)<(y)?(x):(y)) 2585 #define MAX 21 2586 //問題表示 2587 int n=7; 2588 struct NodeType 2589 { int v; //面額 2590 int c; //枚數 2591 bool operator<(const NodeType &s) 2592 { //用於按面額遞減排序 2593 return s.v<v; 2594 } 2595 }; 2596 NodeType A[]={{1,12},{2,8},{5,6},{50,10},{10,8},{200,1},{100,4}}; 2597 int W; 2598 //求解結果表示 2599 int ans=0; //兌換的硬幣枚數 2600 void solve() //兌換硬幣 2601 2602 55 2603 2604 算法設計 2605 2606 { sort(A,A+n); //按面額遞減排序 2607 for (int i=0;i<n;i++) 2608 { int t=min(W/A[i].v,A[i].c); //使用硬幣i的枚數 if (t!=0) 2609 printf(" 支付%3d面額: %3d枚\n",A[i].v,t); W-=t*A[i].v; //剩餘的金額 2610 ans+=t; 2611 if (W==0) break; 2612 } 2613 } 2614 void main() 2615 { W=325; //支付的金額 2616 printf("支付%d分:\n",W); 2617 solve(); 2618 printf("最少硬幣的個數: %d枚\n",ans); 2619 } 2620 上述程序的執行結果如圖1.43所示。 2621 2622 2623 2624 2625 2626 2627 2628 圖1.43 程序執行結果 2629 9. 解:採用貪心方法求解。用a[0..k]存放n的分解結果: 2630 (1)n≤4時能夠驗證其分解成幾個正整數的和的乘積均小於n,沒有解。 2631 (2)n>4時,把n分拆成若干個互不相等的天然數的和,分解數的個數越多乘積越 2632 大。爲此讓n的分解數個數儘量多(體現貪心的思路),把n分解成從2開始的連續的 天然數之和。例如,分解n爲a[0]=2,a[1]=3,a[2]=4,…,a[k]=k+2(共有k+1個分解 2633 數),用m表示剩下數,這樣的分解直到m≤a[k]爲止,即m≤k+2。對剩下數m的處理分 爲以下兩種狀況: 2634 ① m<k+2:將m平均分解到a[k..i](對應的分解數個數爲m)中,即從a[k]開始往前 的分解數增長1(也是貪心的思路,分解數越大加1和乘積也越大)。 2635 ② m=k+2:將a[0..k-1] (對應的分解數個數爲k)的每一個分解數增長1,剩下的2增 加到a[k]中,即a[k]增長2。 2636 對應的完整程序以下: 2637 #include <stdio.h> 2638 #include <string.h> 2639 #define MAX 20 2640 //問題表示 2641 int n; 2642 //求解結果表示 2643 2644 56 2645 第1章 概論 2646 2647 int a[MAX]; //存放被分解的數 2648 int k=0; //a[0..k]存放被分解的數 2649 void solve() //求解n的最大乘積分解問題 2650 { int i; 2651 int sum=1; 2652 if (n<4) //不存在最優方案,直接返回 2653 return; 2654 else 2655 { int m=n; //m表示剩下數 2656 a[0]=2; //第一個數從2開始 2657 m-=a[0]; //減去已經分解的數 2658 k=0; 2659 while (m>a[k]) //若剩下數大於最後一個分解數,則繼續分解 2660 { k++; //a數組下標+1 2661 a[k]=a[k-1]+1; //按二、三、4遞增順序分解 2662 m-=a[k]; //減去最新分解的數 2663 } 2664 if (m<a[k]) //若剩下數小於a[k],從a[k]開始往前的數+1 2665 { for (i=0; i<m; i++) 2666 a[k-i]+=1; 2667 } 2668 if (m==a[k]) //若剩下數等於a[k],則a[k]的值+2,以前的數+1 { a[k]+=2; 2669 for (i=0; i<k; i++) 2670 a[i]+=1; 2671 } 2672 } 2673 } 2674 void main() 2675 { n=23; 2676 memset(a,0,sizeof(a)); 2677 solve(); 2678 printf("%d的最優分解方案\n",n); 2679 int mul=1; 2680 printf(" 分解的數: "); 2681 for (int i=0;i<=k;i++) 2682 if (a[i]!=0) 2683 { printf("%d ",a[i]); 2684 mul*=a[i]; 2685 } 2686 printf("\n 乘積最大值: %d\n",mul); 2687 } 2688 上述程序的執行結果如圖1.44所示。 2689 2690 2691 2692 2693 2694 2695 2696 57 2697 2698 算法設計 2699 2700 圖1.44 程序執行結果 2701 10. 解:採用貪心思路,首先按體重遞增排序;再考慮先後的兩我的(最輕者和最重 2702 者),分別用i、j指向:若w[i]+w[j]≤C,說明這兩我的能夠同乘(執行i++,j--),不然 w[j]單乘(執行j--),若最後只剩餘一我的,該人只能單乘。 2703 對應的完整程序以下: 2704 #include <stdio.h> 2705 #include <algorithm> 2706 using namespace std; 2707 #define MAXN 101 2708 //問題表示 2709 int n=7; 2710 int w[]={50,65,58,72,78,53,82}; 2711 int C=150; 2712 //求解結果表示 2713 int bests=0; 2714 void Boat() //求解乘船問題 2715 { sort(w,w+n); //遞增排序 2716 int i=0; 2717 int j=n - 1; 2718 while (i<=j) 2719 { if(i==j) //剩下最後一我的 2720 { printf(" 一艘船: %d\n",w[i]); 2721 bests++; 2722 break; 2723 } 2724 if (w[i]+w[j]<=C) //先後兩我的同乘 2725 { printf(" 一艘船: %d %d\n",w[i],w[j]); 2726 bests++; 2727 i++; 2728 j--; 2729 } 2730 else //w[j]單乘 2731 { printf(" 一艘船: %d\n",w[j]); 2732 bests++; 2733 j--; 2734 } 2735 } 2736 } 2737 void main() 2738 { printf("求解結果:\n"); 2739 Boat(); 2740 printf("最少的船數=%d\n",bests); 2741 } 2742 上述程序的執行結果如圖1.45所示。 2743 2744 2745 2746 58 2747 第1章 概論 2748 2749 2750 2751 2752 2753 2754 2755 2756 圖1.45 程序執行結果 2757 11. 解:採用貪心思路。每次都在還未安排的容量最大的會議室安排儘量多的參會 2758 人數,即對於每一個會議室,都安排當前還未安排的會議中,參會人數最多的會議。若能容 納下,則選擇該會議,不然找參會人數次多的會議來安排,直到找到能容納下的會議。 2759 對應的完整程序以下: 2760 #include <stdio.h> 2761 #include <algorithm> 2762 using namespace std; 2763 //問題表示 2764 int n=4; //會議個數 2765 int m=4; //會議室個數 2766 int A[]={3,4,3,1}; 2767 int B[]={1,2,2,6}; 2768 //求解結果表示 2769 int ans=0; 2770 void solve() //求解算法 2771 { sort(A,A+n); //遞增排序 2772 sort(B,B+m); //遞增排序 2773 int i=n-1,j=m-1; //從最多人數會議和最多容納人數會議室開始 2774 for(i;i>=0;i--) 2775 { if(A[i]<=B[j] && j>=0) 2776 { ans++; //不知足條件,增長一個會議室 2777 j--; 2778 } 2779 } 2780 } 2781 void main() 2782 { solve(); 2783 printf("%d\n",ans); //輸出2 2784 } 2785 12. 解:與《教程》例7.2相似,會場對應蓄欄,只是這裏僅僅求會場個數,即最大 兼容活動子集的個數。對應的完整程序以下: 2786 #include <stdio.h> 2787 #include <string.h> 2788 #include <algorithm> 2789 using namespace std; 2790 #define MAX 51 2791 //問題表示 2792 struct Action //活動的類型聲明 2793 2794 59 2795 2796 算法設計 2797 2798 { int b; //活動起始時間 2799 int e; //活動結束時間 2800 bool operator<(const Action &s) const //重載<關係函數 2801 { if (e==s.e) //結束時間相同按開始時間遞增排序 2802 return b<=s.b; 2803 else //不然按結束時間遞增排序 2804 return e<=s.e; 2805 } 2806 }; 2807 int n=5; 2808 Action A[]={{0},{1,10},{2,4},{3,6},{5,8},{4,7}}; //下標0不用 2809 //求解結果表示 2810 int ans; //最少會場個數 2811 void solve() //求解最大兼容活動子集 2812 { bool flag[MAX]; //活動標誌 2813 memset(flag,0,sizeof(flag)); 2814 sort(A+1,A+n+1); //A[1..n]按指定方式排序 2815 ans=0; //會場個數 2816 for (int j=1;j<=n;j++) 2817 { if (!flag[j]) 2818 { flag[j]=true; 2819 int preend=j; //前一個兼容活動的下標 2820 for (int i=preend+1;i<=n;i++) 2821 { if (A[i].b>=A[preend].e && !flag[i]) 2822 { preend=i; 2823 flag[i]=true; 2824 } 2825 } 2826 ans++; //增長一個最大兼容活動子集 2827 } 2828 } 2829 } 2830 void main() 2831 { solve(); 2832 printf("求解結果\n"); 2833 printf(" 最少會場個數: %d\n",ans); //輸出4 2834 } 2835 13. 解:採用貪心思路。從第1列開始每次查找a[i][j]元素上、中、下3個對應數中 的最小數。對應的程序以下: 2836 #include <stdio.h> 2837 #define M 12 2838 #define N 110 2839 int m=5, n=6; 2840 int a[M][N]={{3,4,1,2,8,6},{6,1,8,2,7,4},{5,9,3,9,9,5},{8,4,1,3,2,6},{3,7,2,8,6,4}}; 2841 int minRow,minCol; 2842 int minValue(int i, int j) 2843 //求a[i][j]有方上、中、下3個數的最小數,同時要把行標記錄下來 2844 { int s = (i == 0) ? m - 1 : i - 1; 2845 int x = (i == m - 1) ? 0 : i + 1; 2846 2847 60 2848 第1章 概論 2849 2850 minRow = s; 2851 minRow = a[i][j+1] < a[minRow][j+1] ? i : minRow; 2852 minRow = a[x][j+1] < a[minRow][j+1] ? x : minRow; 2853 minRow = a[minRow][j+1] == a[s][j+1] && minRow > s ? s : minRow; 2854 minRow = a[minRow][j+1] == a[i][j+1] && minRow > i ? i : minRow; 2855 minRow = a[minRow][j+1] == a[x][j+1] && minRow > x ? x : minRow; 2856 return a[minRow][j+1]; 2857 } 2858 void solve() 2859 { int i,j,min; 2860 for (j=n-2; j>=0; j--) 2861 for (i=0; i<m; i++) 2862 a[i][j]+= minValue(i,j); 2863 min=a[0][0]; 2864 minRow=0; 2865 for (i=1; i<m; i++) //在第一列查找最小代價的行 2866 if (a[i][0]<min) 2867 { min=a[i][0]; 2868 minRow=i; 2869 } 2870 for (j=0; j<n; j++) 2871 { printf("%d",minRow+1); 2872 if (j<n-1) printf(" "); 2873 minValue(minRow, j); 2874 } 2875 printf("\n%d\n",min); 2876 } 2877 void main() 2878 { 2879 solve(); 2880 } 2881 1.8 第8章─動態規劃 2882 1.8.1 練習題 2883 1. 下列算法中一般以自底向上的方式求解最優解的是( )。 2884 A.備忘錄法 B.動態規劃法 C.貪心法 D.回溯法 2885 2. 備忘錄方法是( )算法的變形。 2886 A.分治法 B.回溯法 C.貪心法 D.動態規劃法 2887 3. 下列是動態規劃算法基本要素的是( )。 2888 A.定義最優解 B.構造最優解 C.算出最優解 D.子問題重疊性質 2889 4. 一個問題可用動態規劃算法或貪心算法求解的關鍵特徵是問題的( )。 2890 A.貪心選擇性質 B.重疊子問題 C.最優子結構性質 D.定義最優解 2891 5. 簡述動態規劃法的基本思路。 2892 6. 簡述動態規劃法與貪心法的異同。 2893 2894 61 2895 2896 算法設計 2897 2898 7. 簡述動態規劃法與分治法的異同。 2899 8. 下列算法中哪些屬於動態規劃算法? 2900 (1)順序查找算法 2901 (2)直接插入排序算法 2902 (3)簡單選擇排序算法 2903 (4)二路歸併排序算法 2904 9. 某個問題對應的遞歸模型以下: 2905 f(1)=1 2906 f(2)=2 2907 f(n)=f(n-1)+f(n-2)+…+f(1)+1 當n>2時 2908 能夠採用以下遞歸算法求解: 2909 long f(int n) 2910 { if (n==1) return 1; 2911 if (n==2) return 2; 2912 long sum=1; 2913 for (int i=1;i<=n-1;i++) 2914 sum+=f(i); 2915 return sum; 2916 } 2917 但其中存在大量的重複計算,請採用備忘錄方法求解。 2918 10. 第3章中的實驗4採用分治法求解半數集問題,若是直接遞歸求解會存在大量重 2919 復計算,請改進該算法。 2920 11. 設計一個時間複雜度爲O(n2)的算法來計算二項式係數Cnk(k≤n)。二項式係數 2921 Cnk的求值過程以下: 2922 𝐶0𝑖=1 2923 𝐶𝑖 2924 𝑗 2925 𝑖‒1 2926 12. 一個機器人只能向下和向右移動,每次只能移動一步,設計一個算法求它從 2927 (0,0)移動到(m,n)有多少條路徑。 2928 13. 兩種水果雜交出一種新水果,如今給新水果取名,要求這個名字中包含了之前兩 2929 種水果名字的字母,而且這個名字要儘可能短。也就是說之前的一種水果名字arr1是新水果 名字arr的子序列,另外一種水果名字arr2也是新水果名字arr的子序列。設計一個算法求 2930 arr。 2931 例如:輸入如下3組水果名稱: 2932 apple peach 2933 ananas banana 2934 pear peach 2935 輸出的新水果名稱以下: 2936 2937 62 2938 第1章 概論 2939 2940 appleach 2941 bananas 2942 pearch 2943 1.8.2 練習題參考答案 2944 1. 答:B。 2945 2. 答:D。 2946 3. 答:D。 2947 4. 答:C。 2948 5. 答:動態規劃法的基本思路是將待求解問題分解成若干個子問題,先求子問題的 2949 解,而後從這些子問題的解獲得原問題的解。 2950 6. 答:動態規劃法的3個基本要素是最優子結構性質、無後效性和重疊子問題性 2951 質,而貪心法的兩個基本要素是貪心選擇性質和最優子結構性質。因此二者的共同點是都 要求問題具備最優子結構性質。 2952 二者的不一樣點以下: 2953 (1)求解方式不一樣,動態規劃法是自底向上的,有些具備最優子結構性質的問題只 2954 能用動態規劃法,有些可用貪心法。而貪心法是自頂向下的。 2955 (2)對子問題的依賴不一樣,動態規劃法依賴於各子問題的解,因此應使各子問題最 2956 優,才能保證總體最優;而貪心法依賴於過去所做過的選擇,但決不依賴於未來的選擇, 也不依賴於子問題的解。 2957 7. 答:二者的共同點是將待求解的問題分解成若干子問題,先求解子問題,而後再 從這些子問題的解獲得原問題的解。 2958 二者的不一樣點是:適合於用動態規劃法求解的問題,分解獲得的各子問題每每不是相 互獨立的(重疊子問題性質),而分治法中子問題相互獨立;另外動態規劃法用表保存已 求解過的子問題的解,再次碰到一樣的子問題時沒必要從新求解,而只需查詢答案,故可獲 得多項式級時間複雜度,效率較高,而分治法中對於每次出現的子問題均求解,致使一樣 的子問題被反覆求解,故產生指數增加的時間複雜度,效率較低。 2959 8. 答:判斷算法是否具備最優子結構性質、無後效性和重疊子問題性質。(2)、(3) 和(4)均屬於動態規劃算法。 2960 9. 解:設計一個dp數組,dp[i]對應f(i)的值,首先dp的全部元素初始化爲0,在計 算f(i)時,若dp[0]>0表示f(i)已經求出,直接返回dp[i]便可,這樣避免了重複計算。對應 的算法以下: 2961 long dp[MAX]; //dp[n]保存f(n)的計算結果 2962 long f1(int n) 2963 { if (n==1) 2964 { dp[n]=1; 2965 return dp[n]; 2966 } 2967 if (n==2) 2968 { dp[n]=2; 2969 return dp[n]; 2970 63 2971 2972 算法設計 2973 2974 } 2975 if (dp[n]>0) return dp[n]; 2976 long sum=1; 2977 for (int i=1;i<=n-1;i++) 2978 sum+=f1(i); 2979 dp[n]=sum; 2980 return dp[n]; 2981 } 2982 10. 解:設計一個數組a,其中a[i]=f(i),首先將a的全部元素初始化爲0,當a[i]>0 時表示對應的f(i)已經求出,直接返回就能夠了。對應的完整程序以下: 2983 #include <stdio.h> 2984 #include <string.h> 2985 #define MAXN 201 2986 //問題表示 2987 int n; 2988 int a[MAXN]; 2989 int fa(int i) //求a[i] 2990 { int ans=1; 2991 if (a[i]>0) 2992 return a[i]; 2993 for(int j=1;j<=i/2;j++) 2994 ans+=fa(j); 2995 a[i]=ans; 2996 return ans; 2997 } 2998 int solve(int n) //求set(n)的元素個數 2999 { memset(a,0,sizeof(a)); 3000 a[1]=1; 3001 return fa(n); 3002 } 3003 void main() 3004 { n=6; 3005 printf("求解結果\n"); 3006 printf(" n=%d時半數集元素個數=%d\n",n,solve(n)); 3007 } 3008 11. 解:定義C(i,j)=Cij,i≥j。則有以下遞推計算公式:C(i,j)=C(i-1,j-1)+C(i- 1,j),初始條件爲C(i,0)=1,C(i,i)=1。能夠根據初始條件由此遞推關係計算C(n,k), 即Cnk。對應的程序以下: 3009 #include <stdio.h> 3010 #define MAXN 51 3011 #define MAXK 31 3012 //問題表示 3013 int n,k; 3014 //求解結果表示 3015 int C[MAXN][MAXK]; 3016 void solve() 3017 { int i,j; 3018 3019 64 3020 第1章 概論 3021 3022 for (i=0;i<=n;i++) 3023 { C[i][i]=1; 3024 C[i][0]=1; 3025 } 3026 for (i=1;i<=n;i++) 3027 for (j=1;j<=k;j++) 3028 C[i][j]=C[i-1][j-1]+C[i-1][j]; 3029 } 3030 void main() 3031 { n=5,k=3; 3032 solve(); 3033 printf("%d\n",C[n][k]); //輸出10 3034 } 3035 顯然,solve()算法的時間複雜度爲O(n2)。 3036 12. 解:設從(0,0)移動到(i,j)的路徑條數爲dp[i][j],因爲機器人只能向下和 3037 向右移動,不一樣於迷宮問題(迷宮問題因爲存在後退,不知足無後效性,不適合用動態規 劃法求解)。對應的狀態轉移方程以下: 3038 dp[0][j]=1 3039 dp[i][0]=1 3040 dp[i][j]=dp[i][j-1]+dp[i-1][j] i、j>0 3041 最後結果是dp[m][n]。對應的程序以下: 3042 #include <stdio.h> 3043 #include <string.h> 3044 #define MAXX 51 3045 #define MAXY 51 3046 //問題表示 3047 int m,n; 3048 //求解結果表示 3049 int dp[MAXX][MAXY]; 3050 void solve() 3051 { int i,j; 3052 dp[0][0]=0; 3053 memset(dp,0,sizeof(dp)); 3054 for (i=1;i<=m;i++) 3055 dp[i][0]=1; 3056 for (j=1;j<=n;j++) 3057 dp[0][j]=1; 3058 for (i=1;i<=m;i++) 3059 for (j=1;j<=n;j++) 3060 dp[i][j]=dp[i][j-1]+dp[i-1][j]; 3061 } 3062 void main() 3063 { m=5,n=3; 3064 solve(); 3065 printf("%d\n",dp[m][n]); 3066 } 3067 13. 解:本題目的思路是求arr1和arr2字符串的最長公共子序列,基本過程參見《教 3068 65 3069 3070 算法設計 3071 3072 程》第8章8.5節。對應的完整程序以下: 3073 #include <iostream> 3074 #include <string.h> 3075 #include <vector> 3076 #include <string> 3077 using namespace std; 3078 #define max(x,y) ((x)>(y)?(x):(y)) 3079 #define MAX 51 //序列中最多的字符個數 3080 //問題表示 3081 int m,n; 3082 string arr1,arr2; 3083 //求解結果表示 3084 int dp[MAX][MAX]; //動態規劃數組 3085 vector<char> subs; //存放LCS 3086 void LCSlength() //求dp 3087 { int i,j; 3088 for (i=0;i<=m;i++) //將dp[i][0]置爲0,邊界條件 3089 dp[i][0]=0; 3090 for (j=0;j<=n;j++) //將dp[0][j]置爲0,邊界條件 3091 dp[0][j]=0; 3092 for (i=1;i<=m;i++) 3093 for (j=1;j<=n;j++) //兩重for循環處理arr一、arr2的全部字符 3094 { if (arr1[i-1]==arr2[j-1]) //比較的字符相同 3095 dp[i][j]=dp[i-1][j-1]+1; 3096 else //比較的字符不一樣 3097 dp[i][j]=max(dp[i][j-1],dp[i-1][j]); 3098 } 3099 } 3100 void Buildsubs() //由dp構造從subs 3101 { int k=dp[m][n]; //k爲arr1和arr2的最長公共子序列長度 3102 int i=m; 3103 int j=n; 3104 while (k>0) //在subs中放入最長公共子序列(反向) 3105 if (dp[i][j]==dp[i-1][j]) i--; 3106 else if (dp[i][j]==dp[i][j-1]) j--; 3107 else 3108 { subs.push_back(arr1[i-1]); //subs中添加arr1[i-1] 3109 i--; j--; k--; 3110 } 3111 } 3112 void main() 3113 { cin >> arr1 >> arr2; //輸入arr1和arr2 3114 m=arr1.length(); //m爲arr1的長度 3115 n=arr2.length(); //n爲arr2的長度 3116 LCSlength(); //求出dp 3117 Buildsubs(); //求出LCS 3118 cout << "求解結果" << endl; 3119 cout << " arr: "; 3120 vector<char>::reverse_iterator rit; 3121 3122 66 3123 第1章 概論 3124 3125 for (rit=subs.rbegin();rit!=subs.rend();++rit) 3126 cout << *rit; 3127 cout << endl; 3128 cout << " 長度: " << dp[m][n] << endl; 3129 } 3130 改成以下: 3131 13. 解:本題目的思路是先求arr1和arr2字符串的最長公共子序列,基本過程參見 3132 《教程》第8章8.5節,再利用遞歸輸出新水果取名。 3133 算法中設置二維動態規劃數組dp,dp[i][j]表示arr1[0..i-1](i個字母)和arr2[0..j-1] 3134 (j個字母)中最長公共子序列的長度。另外設置二維數組b,b[i][j]表示arr1和arr2比較 的3種狀況:b[i][j]=0表示arr1[i-1]=arr2[j-1],b[i][j]=1表示arr1[i-1]≠arr2[j-1]而且dp[i- 3135 1][j]>dp[i][j-1],b[i][j]=2表示arr1[i-1]≠arr2[j-1]而且dp[i-1][j]≤dp[i][j-1]。 3136 對應的完整程序以下: 3137 #include <stdio.h> 3138 #include <string.h> 3139 #define MAX 51 //序列中最多的字符個數 3140 //問題表示 3141 int m,n; 3142 char arr1[MAX],arr2[MAX]; 3143 //求解結果表示 3144 int dp[MAX][MAX]; //動態規劃數組 3145 int b[MAX][MAX]; //存放arr1與arr2比較的3種狀況 3146 void Output(int i,int j) //利用遞歸輸出新水果取名 3147 { if (i==0 && j==0) //輸出完畢 3148 return; 3149 if(i==0) //arr1完畢,輸出arr2的剩餘部分 3150 { Output(i,j-1); 3151 printf("%c",arr2[j-1]); 3152 return; 3153 } 3154 else if(j==0) //arr2完畢,輸出arr1的剩餘部分 3155 { Output(i-1,j); 3156 printf("%c",arr1[i-1]); 3157 return; 3158 } 3159 if (b[i][j]==0) //arr1[i-1]=arr2[j-1]的狀況 3160 { Output(i-1,j-1); 3161 printf("%c",arr1[i-1]); 3162 return; 3163 } 3164 else if(b[i][j]==1) 3165 { Output(i-1,j); 3166 printf("%c",arr1[i-1]); 3167 return; 3168 } 3169 else 3170 { Output(i,j-1); 3171 3172 67 3173 3174 算法設計 3175 3176 printf("%c",arr2[j-1]); 3177 return; 3178 } 3179 } 3180 void LCSlength() //求dp 3181 { int i,j; 3182 for (i=0;i<=m;i++) //將dp[i][0]置爲0,邊界條件 3183 dp[i][0]=0; 3184 for (j=0;j<=n;j++) //將dp[0][j]置爲0,邊界條件 3185 dp[0][j]=0; 3186 for (i=1;i<=m;i++) 3187 for (j=1;j<=n;j++) //兩重for循環處理arr一、arr2的全部字符 { if (arr1[i-1]==arr2[j-1]) //比較的字符相同:狀況0 3188 { dp[i][j]=dp[i-1][j-1]+1; 3189 b[i][j]=0; 3190 } 3191 else if (dp[i-1][j]>dp[i][j-1]) //狀況1 3192 { dp[i][j]=dp[i-1][j]; 3193 b[i][j]=1; 3194 } 3195 else //dp[i-1][j]<=dp[i][j-1]:狀況2 3196 { dp[i][j]=dp[i][j-1]; 3197 b[i][j]=2; 3198 } 3199 } 3200 } 3201 void main() 3202 { int t; //輸入測試用例個數 3203 printf("測試用例個數: "); 3204 scanf("%d",&t); 3205 while(t--) 3206 { scanf("%s",arr1); 3207 scanf("%s",arr2); 3208 memset(b,-1,sizeof(b)); 3209 m=strlen(arr1); //m爲arr1的長度 3210 n=strlen(arr2); //n爲arr2的長度 3211 LCSlength(); //求出dp 3212 printf("結果: "); Output(m,n); //輸出新水果取名 3213 printf("\n"); 3214 } 3215 } 3216 上述程序的一次執行結果如圖1.46所示。 3217 3218 3219 3220 3221 3222 3223 3224 68 3225 第1章 概論 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 圖1.46 程序的一次執行結果 3236 13. 解:本題目的思路是求arr1和arr2字符串的最長公共子序列,基本過程參見《教 3237 程》第8章8.5節。對應的完整程序以下: 3238 3239 而後再用遞歸思想,逐一輸出,獲得的就是最後答案。 3240 #include <iostream> 3241 #include <string.h> 3242 #include <vector> 3243 #include <string> 3244 using namespace std; 3245 #define max(x,y) ((x)>(y)?(x):(y)) 3246 #define MAX 51 //序列中最多的字符個數 3247 //問題表示 3248 int m,n; 3249 string arr1,arr2; 3250 //求解結果表示 3251 int dp[MAX][MAX]; //動態規劃數組 3252 vector<char> subs; //存放LCS 3253 void LCSlength() //求dp 3254 { int i,j; 3255 for (i=0;i<=m;i++) //將dp[i][0]置爲0,邊界條件 3256 dp[i][0]=0; 3257 for (j=0;j<=n;j++) //將dp[0][j]置爲0,邊界條件 3258 dp[0][j]=0; 3259 for (i=1;i<=m;i++) 3260 for (j=1;j<=n;j++) //兩重for循環處理arr一、arr2的全部字符 { if (arr1[i-1]==arr2[j-1]) //比較的字符相同 3261 dp[i][j]=dp[i-1][j-1]+1; 3262 else //比較的字符不一樣 3263 dp[i][j]=max(dp[i][j-1],dp[i-1][j]); 3264 } 3265 } 3266 void Buildsubs() //由dp構造從subs 3267 { int k=dp[m][n]; //k爲arr1和arr2的最長公共子序列長度 3268 int i=m; 3269 int j=n; 3270 while (k>0) //在subs中放入最長公共子序列(反向) 3271 if (dp[i][j]==dp[i-1][j]) i--; 3272 else if (dp[i][j]==dp[i][j-1]) j--; 3273 3274 69 3275 3276 算法設計 3277 3278 else 3279 { subs.push_back(arr1[i-1]); //subs中添加arr1[i-1] i--; j--; k--; 3280 } 3281 } 3282 void main() 3283 { cin >> arr1 >> arr2; //輸入arr1和arr2 3284 m=arr1.length(); //m爲arr1的長度 3285 n=arr2.length(); //n爲arr2的長度 3286 LCSlength(); //求出dp 3287 Buildsubs(); //求出LCS 3288 cout << "求解結果" << endl; 3289 cout << " arr: "; 3290 vector<char>::reverse_iterator rit; 3291 for (rit=subs.rbegin();rit!=subs.rend();++rit) 3292 cout << *rit; 3293 cout << endl; 3294 cout << " 長度: " << dp[m][n] << endl; 3295 } 3296 上述程序的一次執行結果如圖1.46所示。 3297 3298 3299 3300 3301 3302 3303 圖1.46 程序的一次執行結果 3304 1.9 第9章─圖算法設計 3305 3306 1.9.1 練習題 3307 1. 如下不屬於貪心算法的是( )。 3308 A.Prim算法 B.Kruskal算法 C.Dijkstra算法 D.深度優先遍歷 3309 2. 一個有n個頂點的連通圖的生成樹是原圖的最小連通子圖,且包含原圖中全部n 3310 個頂點,而且有保持圖聯通的最少的邊。最大生成樹就是權和最大生成樹,如今給出一個 無向帶權圖的鄰接矩陣爲{{0,4,5,0,3},{4,0,4,2,3},{5,4,0,2,0},{0, 2,2,0,1},{3,3,0,1,0}},其中權爲0表示沒有邊。一個圖爲求這個圖的最大生 3311 成樹的權和是( )。 3312 A.11 B.12 C.13 D.14 E.15 3313 3. 某個帶權連通圖有4個以上的頂點,其中剛好有2條權值最小的邊,儘管該圖的 最小生成樹可能有多個,而這2條權值最小的邊必定包含在全部的最小生成樹中嗎?若是 有3條權值最小的邊呢? 3314 3315 70 3316 第1章 概論 3317 3318 4. 爲何TSP問題採用貪心算法求解不必定獲得最優解? 3319 5. 求最短路徑的4種算法適合帶權無向圖嗎? 3320 6. 求單源最短路徑的算法有Dijkstra算法、Bellman-Ford算法和SPFA算法,比較這 3321 些算法的不一樣點。 3322 7. 有人這樣修改Dijkstra算法以便求一個帶權連通圖的單源最長路徑,將每次選擇 3323 dist最小的頂點u改成選擇最大的頂點u,將按路徑長度小進行調整改成按路徑長度大調 整。這樣能夠求單源最長路徑嗎? 3324 8. 給出一種方法求無環帶權連通圖(全部權值非負)中從頂點s到頂點t的一條最長 簡單路徑。 3325 9. 一個運輸網絡如圖1.47所示,邊上數字爲(c(i,j),b(i,j)),其中c(i,j)表示容 量,b(i,j)表示單位運輸費用。給出從一、2、3位置運輸貨物到位置6的最小費用最大流 的過程。 3326 10. 本教程中的Dijkstra算法採用鄰接矩陣存儲圖,算法時間複雜度爲O(n2)。請你從 各個方面考慮優化該算法,用於求源點v到其餘頂點的最短路徑長度。 3327 11. 有一個帶權有向圖G(全部權爲正整數),採用鄰接矩陣存儲。設計一個算法求 其中的一個最小環。 3328 1 3329 (6,4) 3330 2 3331 3332 (4,2) 3333 3334 (3,5) 3335 3336 3337 3338 4 3339 3340 3341 3342 (12,3) 3343 3344 3345 3346 3347 6 3348 3349 3350 3 3351 3352 (3,4) 3353 3354 3355 (1,6) 3356 (2,3) 3357 3358 5 3359 3360 (9,12) 3361 圖1.47 一個運輸網絡 3362 1.9.2 練習題參考答案 3363 1. 答:D。 3364 2. 答:採用相似Kurskal算法來求最大生成樹,第1步取最大邊(0,2),第2步取 3365 邊(0,1),第3步取邊(0,4),第4步取最大邊(1,3),獲得的權和爲14。答案爲 3366 D。 3367 3. 答:這2條權值最小的邊必定包含在全部的最小生成樹中,由於按Kurskal算法一 定首先選中這2條權值最小的邊。若是有3條權值最小的邊,就不必定了,由於首先選中 這3條權值最小的邊有可能出現迴路。 3368 4. 答:TSP問題不知足最優子結構性質,如(0,1,2,3,0)是整個問題的最優 解,但(0,1,2,0)不必定是子問題的最優解。 3369 5. 答:都適合帶權無向圖求最短路徑。 3370 6. 答:Dijkstra算法不適合存在負權邊的圖求單源最短路徑,其時間複雜度爲 3371 O(n2)。Bellman-Ford算法和SPFA算法適合存在負權邊的圖求單源最短路徑,但圖中不能 3372 3373 71 3374 3375 算法設計 3376 3377 存在權值和爲負的環。Bellman-Ford算法的時間複雜度爲O(ne),而SPFA算法的時間復 雜度爲O(e),因此SPFA算法更優。 3378 7. 答:不能。Dijkstra算法本質上是一種貪心算法,而求單源最長路徑不知足貪心選 擇性質。 3379 8. 答:Bellman-Ford算法和SPFA算法適合存在負權邊的圖求單源最短路徑。能夠 將圖中全部邊權值改成負權值,求出從頂點s到頂點t的一條最短簡單路徑,它就是原來 圖中從頂點s到頂點t的一條最長簡單路徑。 3380 9. 答:爲該運輸網絡添加一個虛擬起點0,它到一、2、3位置運輸費用爲0,容量分 別爲到一、2、3位置運輸容量和,如圖1.48所示,起點s=0,終點t=6。 3381 3382 3383 3384 0 3385 3386 3387 (10,0) 3388 (6,0) 3389 3390 1 3391 (6,4) 3392 2 3393 3394 (4,2) 3395 3396 (3,5) 3397 3398 3399 3400 4 3401 3402 3403 3404 (12,3) 3405 3406 3407 3408 3409 6 3410 3411 (3,0) 3412 3413 3414 3415 3 3416 3417 (3,4) 3418 3419 3420 (1,6) 3421 (2,3) 3422 3423 5 3424 3425 (9,12) 3426 圖1.48 添加一個虛擬起點的運輸網絡 3427 首先初始化f爲零流,最大流量maxf=0,最小費用mincost=0,採用最小費用最大流 3428 算法求解過程以下: 3429 (1)k=0,求出w以下: 3430 3431 0 0 0 0 ∞ ∞ ∞ 3432 ∞ 0 ∞ ∞ 2 4 ∞ 3433 ∞ ∞ 0 ∞ 5 4 ∞ 3434 ∞ ∞ ∞ 0 6 3 ∞ 3435 ∞ ∞ ∞ ∞ 0 ∞ 3 3436 ∞ ∞ ∞ ∞ ∞ 0 12 3437 ∞ ∞ ∞ ∞ ∞ ∞ 0 3438 3439 3440 3441 3442 3443 3444 求出從起點0到終點6的最短路徑爲0→1→4→6,求出最小調整量=4,f[4][6]調整 爲4,f[1][4]調整爲4,f[0][1]調整爲4,mincost=20,maxf=4。 3445 (2)k=1,求出w以下: 3446 3447 0 0 0 0 ∞ ∞ ∞ 3448 0 0 ∞ ∞ ∞ 4 ∞ 3449 ∞ ∞ 0 ∞ 5 4 ∞ 3450 ∞ ∞ ∞ 0 6 3 ∞ 3451 ∞ -2 ∞ ∞ 0 ∞ 3 3452 ∞ ∞ ∞ ∞ ∞ 0 12 3453 ∞ ∞ ∞ ∞ -3 ∞ 0 3454 3455 3456 3457 3458 3459 3460 求出從起點0到終點6的最短路徑爲0→2→4→6,求出最小調整量=3,f[4][6]調整 3461 3462 72 3463 第1章 概論 3464 3465 爲7,f[2][4]調整爲3,f[0][2]調整爲3,mincost=44,maxf=4+3=7。 3466 (3)k=2,求出w以下: 3467 3468 0 0 0 0 ∞ ∞ ∞ 3469 0 0 ∞ ∞ ∞ 4 ∞ 3470 0 ∞ 0 ∞ ∞ 4 ∞ 3471 ∞ ∞ ∞ 0 6 3 ∞ 3472 ∞ -2 -5 ∞ 0 ∞ 3 3473 ∞ ∞ ∞ ∞ ∞ 0 12 3474 ∞ ∞ ∞ ∞ -3 ∞ 0 3475 3476 3477 3478 3479 3480 3481 求出從起點0到終點6的最短路徑爲0→3→4→6,求出最小調整量=1,f[4][6]調整 爲8,f[3][4]調整爲1,f[0][3]調整爲1,mincost=53,maxf=7+1=8。 3482 (4)k=3,求出w以下: 3483 3484 0 0 0 0 ∞ ∞ ∞ 3485 0 0 ∞ ∞ ∞ 4 ∞ 3486 0 ∞ 0 ∞ ∞ 4 ∞ 3487 0 ∞ ∞ 0 ∞ 3 ∞ 3488 ∞ -2 -5 -6 0 ∞ 3 3489 ∞ ∞ ∞ ∞ ∞ 0 12 3490 ∞ ∞ ∞ ∞ -3 ∞ 0 3491 3492 3493 3494 3495 3496 3497 求出從起點0到終點6的最短路徑爲0→3→5→6,求出最小調整量=2,f[5][6]調整 爲2,f[3][5]調整爲2,f[0][3]調整爲3,mincost=83,maxf=8+2=10。 3498 (5)k=4,求出w以下: 3499 3500 0 0 0 ∞ ∞ ∞ ∞ 3501 0 0 ∞ ∞ ∞ 4 ∞ 3502 0 ∞ 0 ∞ ∞ 4 ∞ 3503 0 ∞ ∞ 0 ∞ ∞ ∞ 3504 ∞ -2 -5 -6 0 ∞ 3 3505 ∞ ∞ ∞ -3 ∞ 0 12 3506 ∞ ∞ ∞ ∞ -3 -12 0 3507 3508 3509 3510 3511 3512 3513 求出從起點0到終點6的最短路徑爲0→1→5→6,求出最小調整量=6,f[5][6]調整 爲8,f[1][5]調整爲6,f[0][1]調整爲10,mincost=179,maxf=10+6=16。 3514 (6)k=5,求出w以下: 3515 3516 0 ∞ 0 ∞ ∞ ∞ ∞ 3517 0 0 ∞ ∞ ∞ ∞ ∞ 3518 0 ∞ 0 ∞ ∞ 4 ∞ 3519 0 ∞ ∞ 0 ∞ ∞ ∞ 3520 ∞ -2 -5 -6 0 ∞ 3 3521 ∞ -4 ∞ -3 ∞ 0 12 3522 ∞ ∞ ∞ ∞ -3 -12 0 3523 3524 3525 3526 3527 3528 3529 求出從起點0到終點6的最短路徑爲0→1→5→6,求出最小調整量=1,f[5][6]調整 3530 3531 73 3532 3533 算法設計 3534 3535 爲9,f[2][5]調整爲1,f[0][2]調整爲4,mincost=195,maxf=16+1=17。 3536 (7)k=6,求出的w中沒有增廣路徑,調整結束。對應的最大流以下: 3537 3538 0 10 4 3 0 0 0 3539 0 0 0 0 4 6 0 3540 0 0 0 0 3 1 0 3541 0 0 0 0 1 2 0 3542 0 0 0 0 0 0 8 3543 0 0 0 0 0 0 9 3544 0 0 0 0 0 0 0 3545 3546 3547 3548 3549 3550 3551 3552 最終結果,maxf=17,mincost=195。即運輸的最大貨物量爲17,對應的最小總運輸費 用爲195。 3553 10. 解:從兩個方面考慮優化: 3554 (1)在Dijkstra算法中,當求出源點v到頂點u的最短路徑長度後,僅僅調整從頂 3555 點u出發的鄰接點的最短路徑長度,而教程中的Dijkstra算法因爲採用鄰接矩陣存儲圖, 須要花費O(n)的時間來調整頂點u出發的鄰接點的最短路徑長度,若是採用鄰接表存儲 3556 圖,能夠很快查找到頂點u的全部鄰接點並進行調整,時間爲O(MAX(圖中頂點的出 3557 度))。 3558 (2)求目前一個最短路徑長度的頂點u時,教科書上的Dijkstra算法採用簡單比較 3559 方法,能夠改成採用優先隊列(小根堆)求解。因爲最多e條邊對應的頂點進隊,對應的 3560 時間爲O(log2e)。 3561 對應的完整程序和測試數據算法以下: 3562 #include "Graph.cpp" //包含圖的基本運算算法 3563 #include <queue> 3564 #include <string.h> 3565 using namespace std; 3566 ALGraph *G; //圖的鄰接表存儲結構,做爲全局變量 3567 struct Node //聲明堆中結點類型 3568 { int i; //頂點編號 3569 int v; //dist[i]值 3570 friend bool operator<(const Node &a,const Node &b) //定義比較運算符 3571 { return a.v > b.v; } 3572 }; 3573 void Dijkstra(int v,int dist[]) //改進的Dijkstra算法 3574 { ArcNode *p; 3575 priority_queue<Node> qu; //建立小根堆 3576 Node e; 3577 int S[MAXV]; //S[i]=1表示頂點i在S中, S[i]=0表示頂點i在U中 3578 int i,j,u,w; 3579 memset(S,0,sizeof(S)); 3580 p=G->adjlist[v].firstarc; 3581 for (i=0;i<G->n;i++) dist[i]=INF; 3582 while (p!=NULL) 3583 { w=p->adjvex; 3584 3585 74 3586 第1章 概論 3587 3588 dist[w]=p->weight; //距離初始化 3589 e.i=w; e.v=dist[w]; //將v的出邊頂點進隊qu 3590 qu.push(e); 3591 p=p->nextarc; 3592 } 3593 S[v]=1; //源點編號v放入S中 3594 for (i=0;i<G->n-1;i++) //循環直到全部頂點的最短路徑都求出 3595 { e=qu.top(); qu.pop(); //出隊e 3596 u=e.i; //選取具備最小最短路徑長度的頂點u 3597 S[u]=1; //頂點u加入S中 3598 p=G->adjlist[u].firstarc; 3599 while (p!=NULL) //考察從頂點u出發的全部相鄰點 3600 { w=p->adjvex; 3601 if (S[w]==0) //考慮修改不在S中的頂點w的最短路徑長度 if (dist[u]+p->weight<dist[w]) 3602 { dist[w]=dist[u]+p->weight; //修改最短路徑長度 3603 e.i=w; e.v=dist[w]; 3604 qu.push(e); //修改最短路徑長度的頂點進隊 3605 } 3606 p=p->nextarc; 3607 } 3608 } 3609 } 3610 void Disppathlength(int v,int dist[]) //輸出最短路徑長度 3611 { printf("從%d頂點出發的最短路徑長度以下:\n",v); 3612 for (int i=0;i<G->n;++i) 3613 if (i!=v) 3614 printf(" 到頂點%d: %d\n",i,dist[i]); 3615 } 3616 void main() 3617 { int A[MAXV][MAXV]={ 3618 {0,4,6,6,INF,INF,INF}, 3619 {INF,0,1,INF,7,INF,INF}, 3620 {INF,INF,0,INF,6,4,INF}, 3621 {INF,INF,2,0,INF,5,INF}, 3622 {INF,INF,INF,INF,0,INF,6}, 3623 {INF,INF,INF,INF,1,0,8}, 3624 {INF,INF,INF,INF,INF,INF,0}}; 3625 int n=7, e=12; 3626 CreateAdj(G,A,n,e); //創建圖的鄰接表 3627 printf("圖G的鄰接表:\n"); 3628 DispAdj(G); //輸出鄰接表 3629 int v=0; 3630 int dist[MAXV]; 3631 Dijkstra(v,dist); //調用Dijkstra算法 3632 Disppathlength(v,dist); //輸出結果 3633 DestroyAdj(G); //銷燬圖的鄰接表 3634 } 3635 上述程序的執行結果如圖1.49所示。 3636 3637 75 3638 3639 算法設計 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 圖1.49 程序執行結果 3653 其中Dijkstra算法的時間複雜度爲O(n(log2e+MAX(頂點的出度)),通常圖中最大頂點 3654 出度遠小於e,因此進一步簡化時間複雜度爲O(nlog2e)。 3655 11. 有一個帶權有向圖G(全部權爲正整數),採用鄰接矩陣存儲。設計一個算法求 3656 其中的一個最小環。 3657 解:利用Floyd算法求出全部頂點對之間的最短路徑,若頂點i到j有最短路徑,而 3658 圖中又存在頂點j到i的邊,則構成一個環,在全部環中比較找到一個最小環並輸出。對 應的程序以下: 3659 #include "Graph.cpp" //包含圖的基本運算算法 3660 #include <vector> 3661 using namespace std; 3662 void Dispapath(int path[][MAXV],int i,int j) 3663 //輸出頂點i到j的一條最短路徑 3664 { vector<int> apath; //存放一條最短路徑中間頂點(反向) 3665 int k=path[i][j]; 3666 apath.push_back(j); //路徑上添加終點 3667 while (k!=-1 && k!=i) //路徑上添加中間點 3668 { apath.push_back(k); 3669 k=path[i][k]; 3670 } 3671 apath.push_back(i); //路徑上添加起點 3672 for (int s=apath.size()-1;s>=0;s--) //輸出路徑上的中間頂點 3673 printf("%d→",apath[s]); 3674 } 3675 int Mincycle(MGraph g,int A[MAXV][MAXV],int &mini,int &minj) 3676 //在圖g和A中的查找一個最小環 3677 { int i,j,min=INF; 3678 for (i=0;i<g.n;i++) 3679 for (j=0;j<g.n;j++) 3680 if (i!=j && g.edges[j][i]<INF) 3681 { if (A[i][j]+g.edges[j][i]<min) 3682 { min=A[i][j]+g.edges[j][i]; 3683 mini=i; minj=j; 3684 3685 76 3686 第1章 概論 3687 3688 } 3689 } 3690 return min; 3691 } 3692 void Floyd(MGraph g) //Floyd算法求圖g中的一個最小環 { int A[MAXV][MAXV],path[MAXV][MAXV]; 3693 int i,j,k,min,mini,minj; 3694 for (i=0;i<g.n;i++) 3695 for (j=0;j<g.n;j++) 3696 { A[i][j]=g.edges[i][j]; 3697 if (i!=j && g.edges[i][j]<INF) 3698 path[i][j]=i; //頂點i到j有邊時 3699 else 3700 path[i][j]=-1; //頂點i到j沒有邊時 3701 } 3702 for (k=0;k<g.n;k++) //依次考察全部頂點 3703 { for (i=0;i<g.n;i++) 3704 for (j=0;j<g.n;j++) 3705 if (A[i][j]>A[i][k]+A[k][j]) 3706 { A[i][j]=A[i][k]+A[k][j]; //修改最短路徑長度 3707 path[i][j]=path[k][j]; //修改最短路徑 3708 } 3709 } 3710 min=Mincycle(g,A,mini,minj); 3711 if (min!=INF) 3712 { printf("圖中最小環:"); 3713 Dispapath(path,mini,minj); //輸出一條最短路徑 3714 printf("%d, 長度:%d\n",mini,min); 3715 } 3716 else printf(" 圖中沒有任何環\n"); 3717 } 3718 void main() 3719 { MGraph g; 3720 int A[MAXV][MAXV]={ {0,5,INF,INF},{INF,0,1,INF}, 3721 {3,INF,0,2}, {INF,4,INF,0}}; 3722 int n=4, e=5; 3723 CreateMat(g,A,n,e); //創建圖的鄰接矩陣 3724 printf("圖G的鄰接矩陣:\n"); 3725 DispMat(g); //輸出鄰接矩陣 3726 Floyd(g); 3727 } 3728 上述程序的執行結果如圖1.50所示。 3729 3730 3731 3732 3733 3734 3735 3736 77 3737 3738 算法設計 3739 3740 3741 3742 3743 3744 3745 3746 3747 圖1.50 程序執行結果 3748 1.10 第10章─計算幾何 3749 3750 1.10.1 練習題 3751 1. 對如圖1.51所示的點集A,給出採用Graham掃描算法求凸包的過程及結果。 3752 3753 7 3754 0 a 3755 3756 3757 3758 a6 a 4 3759 a8 3760 a9 3761 a 3 a 2 3762 3763 a1 1 3764 a 1 3765 1 3766 3767 1 2 3 4 5 6 78 9 10 3768 3769 3770 3771 3772 3773 3774 3775 3776 圖1.51 一個點集A 3777 2. 對如圖1.51所示的點集A,給出採用分治法求最近點對的過程及結果。 3778 3. 對如圖1.51所示的點集A,給出採用旋轉卡殼法求最遠點對的結果。 3779 4. 對應3個點向量p一、p二、p3,採用S(p1,p2,p3)=(p2-p1)(p3-p1)/2求它們構成的三 3780 角形面積,請問什麼狀況下計算結果爲正?什麼狀況下計算結果爲負? 3781 5. 已知座標爲整數,給出判斷平面上一點p是否在一個逆時針三角形 p1-p2-p3 內部 3782 的算法。 3783 1.10.2 練習題參考答案 3784 1. 答:採用Graham掃描算法求凸包的過程及結果以下: 3785 求出起點a0(1,1)。 3786 排序後:a0(1,1) a1(8,1) a2(9,4) a3(5,4) a4(8,7) a5(5,6) a10(7,10) a9(3,5) 3787 a6(3,7) a7(4,10) a8(1,6) a11(0,3)。 3788 先將a0(1,1)進棧,a1(8,1)進棧,a2(9,4)進棧。 3789 處理點a3(5,4):a3(5,4)進棧。 3790 處理點a4(8,7):a3(5,4)存在右拐關係,退棧,a4(8,7)進棧。 3791 3792 78 3793 第1章 概論 3794 3795 處理點a5(5,6):a5(5,6)進棧。 3796 處理點a10(7,10):a5(5,6)存在右拐關係,退棧,a10(7,10)進棧。 3797 處理點a9(3,5):a9(3,5)進棧。 3798 處理點a6(3,7):a9(3,5)存在右拐關係,退棧,a6(3,7)進棧。 3799 處理點a7(4,10):a6(3,7)存在右拐關係,退棧,a7(4,10)進棧。 3800 處理點a8(1,6):a8(1,6)進棧。 3801 處理點a11(0,3):a11(0,3)進棧。 3802 結果:n=8,凸包的頂點:a0(1,1) a1(8,1) a2(9,4) a4(8,7) a10(7,10) a7(4,10) 3803 a8(1,6) a11(0,3)。 3804 2. 答:求解過程以下: 3805 排序前:(1,1) (8,1) (9,4) (5,4) (8,7) (5,6) (3,7) (4,10) (1,6) (3,5) (7,10) (0,3)。按x座標排序後:(0,3) (1,1) (1,6) (3,7) (3,5) (4,10) (5,4) (5,6) (7,10) (8,1) (8,7) (9,4)。按y座標排序後:(1,1) (8,1) (0,3) (5,4) (9,4) (3,5) (1,6) (5,6) (3,7) (8,7) (4,10) (7,10)。 3806 (1)中間位置midindex=5,左部分:(0,3) (1,1) (1,6) (3,7) (3,5) (4,10);右 部分:(5,4) (5,6) (7,10) (8,1) (8,7) (9,4);中間部分點集爲 (0,3) (3,7) (4,10) (5,4) (5,6) (7,10) (8,7)。 3807 (2)求解左部分:(0,3) (1,1) (1,6) (3,7) (3,5) (4,10)。 3808 中間位置=2,劃分爲左部分1:(0,3) (1,1) (1,6),右部分1:(3,7) (3,5) (4,10) 3809 處理左部分1:點數少於4:求出最近距離=2.23607,即(0,3)和(1,1)之間的距離。 3810 處理右部分1:點數少於4:求出最近距離=2,即(3,7)和(3,5)之間的距離。 3811 再考慮中間部分(中間部分最近距離=2.23)求出左部分d1=2。 3812 (3)求解右部分:(5,4) (5,6) (7,10) (8,1) (8,7) (9,4)。 3813 中間位置=8,劃分爲左部分2:(5,4) (5,6) (7,10),右部分2:(8,1) (8,7) (9, 3814 4)。 3815 處理左部分2:點數少於4,求出最近距離=2,即 (5,4)和(5,6)之間的距離。 3816 處理右部分2:點數少於4,求出最近距離=3.16228,即(8,1)和(9,4)之間的距離。 3817 再考慮中間部分(中間部分爲空)求出右部分d2=2。 3818 (4)求解中間部分點集:(0,3) (3,7) (4,10) (5,4) (5,6) (7,10) (8,7)。求出最 3819 近距離d3=5。 3820 最終結果爲:d=MIN{d1,d2,d3)=2。 3821 3. 答:採用旋轉卡殼法求出兩個最遠點對是(1,1)和(7,10),最遠距離爲 10.82。 3822 4. 答:當三角形p1-p2-p3逆時針方向時,如圖1.52所示,p2-p1在p3-p1的順時針方 向上,(p2-p1)(p3-p1)>0,對應的面積(p2-p1)(p3-p1)/2爲正。 3823 當三角形p1-p2-p3順時針方向時,如圖1.53所示,p2-p1在p3-p1的逆時針方向上, 3824 (p2-p1)(p3-p1)<0,對應的面積(p2-p1)(p3-p1)/2爲負。 3825 3826 3827 3828 79 3829 3830 3831 3832 3833 p3-p1 3834 p1 p2-p1 3835 3836 3837 3838 3839 3840 3841 3842 3843 算法設計 3844 3845 3846 p2-p1 3847 p1 p3-p1 3848 3849 3850 3851 3852 3853 3854 圖1.52 p1-p2-p3逆時針方向圖 圖1.53 p1-p2-p3逆時針方向 3855 5. 答:用S(p1,p2,p3)=(p2-p1)(p3-p1)/2求三角形p一、p二、p3帶符號的的面積。如圖 3856 1.54所示,若S(p,p2,p3)和S(p,p3,p1)和S(p,p1,p2)(3個三角形的方向均爲逆時針 方向)均大於0,表示p在該三角形內部。 3857 p3 3858 3859 3860 p1 3861 3862 3863 3864 p 3865 3866 3867 3868 3869 p2 3870 圖1.54 一個點p和一個三角形 3871 對應的程序以下: 3872 #include "Fundament.cpp" //包含向量基本運算算法 3873 double getArea(Point p1,Point p2,Point p3) //求帶符號的面積 3874 { 3875 return Det(p2-p1,p3-p1); 3876 } 3877 bool Intrig(Point p,Point p1,Point p2,Point p3) //判斷p是否在三角形p1p2p3的內部 { double area1=getArea(p,p2,p3); 3878 double area2=getArea(p,p3,p1); 3879 double area3=getArea(p,p1,p2); 3880 if (area1>0 && area2>0 && area3>0) 3881 return true; 3882 else 3883 return false; 3884 } 3885 void main() 3886 { printf("求解結果\n"); 3887 Point p1(0,0); 3888 Point p2(5,-4); 3889 Point p3(4,3); 3890 Point p4(3,1); 3891 Point p5(-1,1); 3892 printf(" p1:"); p1.disp(); printf("\n"); 3893 printf(" p2:"); p2.disp(); printf("\n"); 3894 printf(" p3:"); p3.disp(); printf("\n"); 3895 printf(" p4:"); p4.disp(); printf("\n"); 3896 3897 80 3898 第1章 概論 3899 3900 printf(" p5:"); p5.disp(); printf("\n"); 3901 printf(" p1p2p3三角形面積: %g\n",getArea(p1,p2,p3)); 3902 printf(" p4在p1p2p3三角形內部: %s\n",Intrig(p4,p1,p2,p3)?"是":"不是"); printf(" p5在p1p2p3三角形內部: %s\n",Intrig(p5,p1,p2,p3)?"是":"不是"); } 3903 上述程序的執行結果如圖1.55所示。 3904 3905 3906 3907 3908 3909 3910 3911 3912 圖1.55 程序執行結果 3913 1.11 第11章─計算複雜性理論 3914 3915 1.11.1 練習題 3916 1. 旅行商問題是NP問題嗎? 3917 A.否 B.是 C.至今尚無定論 3918 2. 下面有關P問題,NP問題和NPC問題,說法錯誤的是( )。 3919 A.若是一個問題能夠找到一個能在多項式的時間裏解決它的算法,那麼這個問題就屬 3920 於P問題 3921 B.NP問題是指能夠在多項式的時間裏驗證一個解的問題 3922 C.全部的P類問題都是NP問題 3923 D.NPC問題不必定是個NP問題,只要保證全部的NP問題均可以約化到它便可 3924 3. 對於《教程》例11.2設計的圖靈機,分別給出執行f(3,2)和f(2,3)的瞬像演變過 3925 程。 3926 4. 什麼是P類問題?什麼是NP類問題? 3927 5. 證實求兩個m行n列的二維矩陣相加的問題屬於P類問題。 3928 6. 證實求含有n個元素的數據序列中求最大元素的問題屬於P類問題。 3929 7. 設計一個肯定性圖靈機M,用於計算後繼函數S(n)=n+1(n爲一個二進制數),並 3930 給出求1010001的後繼函數值的瞬像演變過程。 3931 1.11.2 練習題參考答案 3932 1. 答:B。 3933 2. 答:D。 3934 3. 答:(1)執行f(3,2)時,輸入帶上的初始信息爲000100B,其瞬像演變過程如 3935 3936 81 3937 3938 3939 3940 下: 3941 3942 3943 算法設計 3944 q0000100BBq100100B B0q10100BB00q1100B B001q200BB00q3110B 3945 B0q30110BBq300110Bq3B00110BBq000110BBBq10110BBB0q1110B 3946 BB01q210BBB011q20BBB01q311BBB0q3111BBBq30111BBBq00111B 3947 BBB1q211BBBB11q21BBBB111q2BBBB11q41BBBB1q41BB 3948 BBBq41BBBBBBq4BBBBBBB0q6BBB 3949 最終帶上有一個0,計算結果爲1。 3950 (2)執行f(2,3)時,輸入帶上的初始信息爲001000B,其瞬像演變過程以下: 3951 q0001000BBq001000BB0q11000BB01q2000BB0q31100BBq301100B 3952 q3 B01100BBq001100BBBq11100BBB1q2100BBB11q200BBB1q3100B 3953 BBq31100BBq3B1100BBBq01100BBBBq5100BBBBBq500B 3954 BBBBBq50BBBBBBBq5BBBBBBBBq6 3955 最終帶上有零個0,計算結果爲0。 3956 4. 答:用肯定性圖靈機以多項式時間界可解的問題稱爲P類問題。用非肯定性圖靈 3957 機以多項式時間界可解的問題稱爲NP類問題。 3958 5. 答:求兩個m行n列的二維矩陣相加的問題對應的算法時間複雜度爲O(mn),所 3959 以屬於P類問題。 3960 6. 答:求含有n個元素的數據序列中最大元素的問題的算法時間複雜度爲O(n),所 3961 以屬於P類問題。 3962 7. 解: q0爲初始狀態,q3爲終止狀態,讀寫頭初始時注視最右邊的格。δ動做函數 3963 以下: 3964 δ(q0,0)→(q1,1,L) 3965 δ(q0,1)→(q2,0,L) 3966 δ(q0,B)→(q3,B,R) 3967 δ(q1,0)→(q1,0,L) 3968 δ(q1,1)→(q1,1,L) 3969 δ(q1,B)→(q3,B,L) 3970 δ(q2,0)→(q1,1,L) 3971 δ(q2,1)→(q2,0,L) 3972 δ(q2,B)→(q3,B,L) 3973 求10100010的後繼函數值的瞬像演變過程以下: 3974 B1010001q00BB101000q111BB10100q1011BB1010q10011BB101q100011B 3975 B10q1100011BB1q10100011BBq110100011Bq1B10100011B 3976 q3BB10100011B 3977 其結果爲10100011。 3978 3979 3980 3981 3982 3983 82 3984 第1章 概論 3985 1.12 第12章─機率算法和近似算法 3986 3987 1.12.1 練習題 3988 1. 蒙特卡羅算法是( )的一種。 3989 A.分枝限界算法 B.貪心算法 C.機率算法 D.回溯算法 3990 2. 在下列算法中有時找不到問題解的是( )。 3991 A.蒙特卡羅算法 B.拉斯維加斯算法 C.舍伍德算法 D.數值機率算法 3992 3. 在下列算法中獲得的解未必正確的是( )。 3993 A.蒙特卡羅算法 B.拉斯維加斯算法 C.舍伍德算法 D.數值機率算法 3994 4. 總能求得非數值問題的一個解,且所求得的解老是正確的是( )。 3995 A.蒙特卡羅算法 B.拉斯維加斯算法 C.數值機率算法 D.舍伍德算法 3996 5. 目前能夠採用( )在多項式級時間內求出旅行商問題的一個近似最優解。 3997 A.回溯法 B.蠻力法 C.近似算法 D.都不可能 3998 6. 下列敘述錯誤的是( )。 3999 A.機率算法的指望執行時間是指反覆解同一個輸入實例所花的平均執行時間 4000 B.機率算法的平均指望時間是指全部輸入實例上的平均指望執行時間 4001 C.機率算法的最壞指望事件是指最壞輸入實例上的指望執行時間 4002 D.機率算法的指望執行時間是指全部輸入實例上的所花的平均執行時間 4003 7. 下列敘述錯誤的是( )。 4004 A.數值機率算法通常是求數值計算問題的近似解 4005 B.Monte Carlo總能求得問題的一個解,但該解未必正確 4006 C.Las Vegas算法的必定能求出問題的正確解 4007 D.Sherwood算法的主要做用是減小或是消除好的和壞的實例之間的差異 4008 8. 近似算法和貪心法有什麼不一樣? 4009 9. 給定能隨機生成整數1到5的函數rand5(),寫出能隨機生成整數1到7的函數 4010 rand7()。 4011 1.12.2 練習題參考答案 4012 1. 答:C。 4013 2. 答:B。 4014 3. 答:A。 4015 4. 答:D。 4016 5. 答:C。 4017 6. 答:對機率算法一般討論平均的指望時間和最壞的指望時間,前者指全部輸入實 4018 例上平均的指望執行時間,後者指最壞的輸入實例上的指望執行時間。答案爲D。 4019 7. 答:一旦用拉斯維加斯算法找到一個解,那麼這個解確定是正確的,但有時用拉 4020 斯維加斯算法可能找不到解。答案爲C。 4021 8. 答:近似算法不能保證獲得最優解。貪心算法不必定是近似算法,若是能夠證實 4022 4023 83 4024 4025 算法設計 4026 4027 決策既不受以前決策的影響,也不影響後續決策,則貪心算法就是肯定的最優解算法。 4028 9. 解:經過rand5()*5+rand5()產生6,7,8,9,…,26,27,28,29,30這25個整 4029 數,每一個整數x出現的機率相等,取前面3*7=21個整數,捨棄後面的4個整數,將{6, 7,8}轉化成1,{9,10,11}轉化成2,以此類推,即由y= (x-3)/3爲最終結果。對應的程 序以下: 4030 #include <stdio.h> 4031 #include <stdlib.h> //包含產生隨機數的庫函數 4032 #include <time.h> 4033 int rand5() //產生一個[1,5]的隨機數 4034 { int a=1,b=5; 4035 return rand()%(b-a+1)+a; 4036 } 4037 int rand7() //產生一個[1,7]的隨機數 4038 { int x; 4039 do 4040 { 4041 x=rand5()*5+rand5(); 4042 } while (x>26); 4043 int y=(x-3)/3; 4044 return y; 4045 } 4046 void main() 4047 { srand((unsigned)time(NULL)); //隨機種子 4048 for (int i=1;i<=20;i++) //輸出20個[1,7]的隨機數 4049 printf("%d ",rand7()); 4050 printf("\n"); 4051 } 4052 上述程序的一次執行結果如圖1.56所示。 4053 4054 4055 4056 4057 圖1.56 程序執行結果 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 84