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