算法设计与分析 - 李春葆 - 第二版 - html v2
1 .1 第 1 章─概论 1.1.1 练习题 1 . 下列关于算法的说法中正确的有( )。 Ⅰ Ⅱ Ⅲ Ⅳ . 求解某一类问题的算法是唯一的 . 算法必须在有限步操作之后停止 . 算法的每一步操作必须是明确的,不能有歧义或含义模糊 . 算法执行后一定产生确定的结果 A. 1 个 B.2 个 C.3 个 D.4 个 2 . T ( n ) 表示当输入规模为 n 时的算法效率,以下算法效率最优的是( )。 A. T ( n )= T ( n - 1)+1 , T (1)=1 C. T ( n )= T ( n /2)+1 , T (1)=1 B. T ( n )= 2 n 2 D. T ( n )=3 n log 2 n 3 4 . 什么是算法?算法有哪些特征? . 判断一个大于 2 的正整数 n 是否为素数的方法有多种,给出两种算法,说明其中 一种算法更好的理由。 5 . 证明以下关系成立: ( ( 1 ) 10 n 2 - 2 n = ( n 2 ) 2 ) 2 n + 1 = (2 n ) 6 7 . 证明 O( f ( n ))+O( g ( n ))=O(max{ f ( n ) , g ( n )}) 。 . 有一个含 n ( n >2 )个整数的数组 a ,判断其中是否存在出现次数超过所有元素一 半的元素。 8 9 . 一个字符串采用 string 对象存储,设计一个算法判断该字符串是否为回文。 . 有一个整数序列,设计一个算法判断其中是否存在两个元素和恰好等于给定的整 数 k 。 0. 有两个整数序列,每个整数序列中所有元素均不相同。设计一个算法求它们的公 共元素,要求不使用 STL 的集合算法。 1. 正整数 n ( n >1 )可以写成质数的乘积形式,称为整数的质因数分解。例如, 2=2*2*3 , 18=2*3*3 , 11=11 。设计一个算法求 n 这样分解后各个质因数出现的次数,采 用 vector 向量存放结果。 2. 有一个整数序列,所有元素均不相同,设计一个算法求相差最小的元素对的个 数。如序列 4 、 1 、 2 、 3 的相差最小的元素对的个数是 3 ,其元素对是( 1 , 2 ),( 2 , 3 ), 3 , 4 )。 3. 有一个 map<string , int> 容器,其中已经存放了较多元素。设计一个算法求出其 中重复的 value 并且返回重复 value 的个数。 1 1 1 1 ( 1 1 1 4. 重新做第 10 题,采用 map 容器存放最终结果。 5. 假设有一个含 n ( n >1 )个元素的 stack<int> 栈容器 st ,设计一个算法出栈从栈顶 到栈底的第 k ( 1 ≤ k ≤ n )个元素,其他栈元素不变。 算法设计 1 .1.2 练习题参考答案 答: 由于算法具有有穷性、确定性和输出性,因而Ⅱ、Ⅲ、Ⅳ正确,而解决某一 类问题的算法不一定是唯一的。答案为 C 。 答: 选项 A 的时间复杂度为 O( n ) 。选项 B 的时间复杂度为 O( n 2 ) 。选项 C 的时间 复杂度为 O(log 2 n ) 。选项 D 的时间复杂度为 O( n log 2 n ) 。答案为 C 。 答: 算法是求解问题的一系列计算步骤。算法具有有限性、确定性、可行性、输 入性和输出性 5 个重要特征。 . 答: 两种算法如下: 1 . 2 . 3 . 4 # # include <stdio.h> include <math.h> bool isPrime1(int n) // 方法 1 { for (int i=2;i<n;i++) if (n%i==0) return false; return true; } bool isPrime2(int n) // 方法 2 { for (int i=2;i<=(int)sqrt(n);i++) if (n%i==0) return false; return true; } void main() { int n=5; printf("%d,%d\n",isPrime1(n),isPrime2(n)); } 方法 1 的时间复杂度为 O( n ) ,方法 2 的时间复杂度为 n ,所以方法 2 更好。 . 答: ( 1 )当 n 足够大时, (10 n 2 - 2 n )/( n 2 )=10 ,所以 10 n 2 - 2 n = ( n 2 ) 。 2 ) 2 n + 1 =2*2 n = (2 n ) 。 . 证明: 对于任意 f 1 (n) ∈ O( f ( n )) ,存在正常数 c 1 和正常数 n 1 ,使得对所有 n ≥ n 1 , 有 f 1 ( n ) ≤ c 1 f ( n ) 。 类似地,对于任意 g 1 ( n ) ∈ O( g ( n )) ,存在正常数 c 2 和自然数 n 2 ,使得对所有 n ≥ n 2 , 5 ( 6 有 g 1 ( n ) ≤ c 2 g ( n ) 。 令 c 3 =max{ c 1 , c 2 } , n 3 =max{ n 1 , n 2 } , h ( n )= max{ f ( n ) , g ( n )} 。 则对所有的 n ≥ n 3 ,有: f 1 ( n ) + g 1 ( n ) ≤ c 1 f ( n ) + c 2 g ( n ) ≤ c 3 f ( n )+ c 3 g ( n )= c 3 ( f ( n )+ g ( n )) ≤ c 3 2max{ f ( n ) , g ( n )}=2 c 3 h ( n )=O(max{ f ( n ) , g ( n )}) 。 . 解: 先将 a 中元素递增排序,再求出现次数最多的次数 maxnum ,最后判断是否满 足条件。对应的程序如下: 7 # # include <stdio.h> include <algorithm> using namespace std; 2 第 1 章 概论 bool solve(int a[],int n,int &x) { sort(a,a+n); // 递增排序 // 出现次数最多的次数 int maxnum=0; int num=1; int e=a[0]; for (int i=1;i<n;i++) { if (a[i]==e) { num++; if (num>maxnum) { maxnum=num; x=e; } } else { e=a[i]; num=1; } } if (maxnum>n/2) return true; else return false; } void main() { int a[]={2,2,2,4,5,6,2}; int n=sizeof(a)/sizeof(a[0]); int x; if (solve(a,n,x)) printf(" 出现次数超过所有元素一半的元素为 %d\n",x); else printf(" 不存在出现次数超过所有元素一半的元素 \n"); } 上述程序的执行结果如图 1.1 所示。 图 1.1 程序执行结果 8 . 解: 采用前后字符判断方法,对应的程序如下: # # include <iostream> include <string> using namespace std; bool solve(string str) // 判断字符串 str 是否为回文 { int i=0,j=str.length()-1; while (i<j) if (str[i]!=str[j]) return false; { 3 算法设计 i++; j--; return true; void main() } } { cout << " 求解结果 " << endl; string str="abcd"; cout << " " << str << (solve(str)?" 是回文 ":" 不是回文 ") << endl; string str1="abba"; cout << " " << str1 << (solve(str1)?" 是回文 ":" 不是回文 ") << endl; } 上述程序的执行结果如图 1.2 所示。 图 1.2 程序执行结果 9 . 解: 先将 a 中元素递增排序,然后从两端开始进行判断。对应的程序如下: # # include <stdio.h> include <algorithm> using namespace std; bool solve(int a[],int n,int k) { sort(a,a+n); int i=0, j=n-1; while (i<j) // 递增排序 // 区间中存在两个或者以上元素 { if (a[i]+a[j]==k) return true; else if (a[i]+a[j]<k) i++; else j--; } return false; } void main() int a[]={1,2,4,5,3}; { int n=sizeof(a)/sizeof(a[0]); printf(" 求解结果 \n"); int k=9,i,j; if (solve(a,n,k,i,j)) printf(" 存在 : %d+%d=%d\n",a[i],a[j],k); else printf(" 不存在两个元素和为 %d\n",k); int k1=10; if (solve(a,n,k1,i,j)) printf(" 存在 : %d+%d=%d\n",a[i],a[j],k1); 4 第 1 章 概论 else printf(" 不存在两个元素和为 %d\n",k1); } 上述程序的执行结果如图 1.3 所示。 图 1.3 程序执行结果 1 0. 解: 采用集合 set<int> 存储整数序列,集合中元素默认是递增排序的,再采用二 路归并算法求它们的交集。对应的程序如下: # # include <stdio.h> include <set> using namespace std; void solve(set<int> s1,set<int> s2,set<int> &s3) // 求交集 s3 { set<int>::iterator it1,it2; it1=s1.begin(); it2=s2.begin(); while (it1!=s1.end() && it2!=s2.end()) { if (*it1==*it2) { s3.insert(*it1); + +it1; ++it2; } else if (*it1<*it2) + +it1; else + +it2; } } void dispset(set<int> s) // 输出集合的元素 { set<int>::iterator it; for (it=s.begin();it!=s.end();++it) printf("%d ",*it); printf("\n"); } void main() { int a[]={3,2,4,8}; int n=sizeof(a)/sizeof(a[0]); set<int> s1(a,a+n); int b[]={1,2,4,5,3}; int m=sizeof(b)/sizeof(b[0]); set<int> s2(b,b+m); set<int> s3; solve(s1,s2,s3); printf(" 求解结果 \n"); printf(" s1: "); dispset(s1); 5 算法设计 printf(" s2: "); dispset(s2); printf(" s3: "); dispset(s3); } 上述程序的执行结果如图 1.4 所示。 图 1.4 程序执行结果 1 1. 解: 对于正整数 n ,从 i =2 开始查找其质因数, ic 记录质因数 i 出现的次数,当找 到这样质因数后,将( i , ic )作为一个元素插入到 vector 容器 v 中。最后输出 v 。对应的 算法如下: # # include <stdio.h> include <vector> using namespace std; struct NodeType //vector 向量元素类型 // 质因数 { int p; int pc; // 质因数出现次数 } ; void solve(int n,vector<NodeType> &v)// 求 n 的质因数分解 { int i=2; int ic=0; NodeType e; do { if (n%i==0) { ic++; n=n/i; } else { if (ic>0) { e.p=i; e.pc=ic; v.push_back(e); } ic=0; i++; } } while (n>1 || ic!=0); } void disp(vector<NodeType> &v) // 输出 v { vector<NodeType>::iterator it; for (it=v.begin();it!=v.end();++it) printf(" 质因数 %d 出现 %d 次 \n",it->p,it->pc); } 6 第 1 章 概论 void main() { vector<NodeType> v; int n=100; printf("n=%d\n",n); solve(n,v); disp(v); } 上述程序的执行结果如图 1.5 所示。 图 1.5 程序执行结果 1 2. 解: 先递增排序,再求相邻元素差,比较求最小元素差,累计最小元素差的个 数。对应的程序如下: # # # include <iostream> include <algorithm> include <vector> using namespace std; int solve(vector<int> &myv) // 求 myv 中相差最小的元素对的个数 // 递增排序 { sort(myv.begin(),myv.end()); int ans=1; int mindif=myv[1]-myv[0]; for (int i=2;i<myv.size();i++) { if (myv[i]-myv[i-1]<mindif) { ans=1; mindif=myv[i]-myv[i-1]; } else if (myv[i]-myv[i-1]==mindif) ans++; } return ans; } void main() { int a[]={4,1,2,3}; int n=sizeof(a)/sizeof(a[0]); vector<int> myv(a,a+n); cout << " 相差最小的元素对的个数 : " << solve(myv) << endl; } 上述程序的执行结果如图 1.6 所示。 7 算法设计 图 1.6 程序执行结果 1 3. 解: 对于 map<string , int> 容器 mymap ,设计另外一个 map<int , int> 容器 tmap , 将前者的 value 作为后者的关键字。遍历 mymap ,累计 tmap 中相同关键字的次数。一个 参考程序及其输出结果如下: # # # include <iostream> include <map> include <string> using namespace std; void main() { map<string,int> mymap; mymap.insert(pair<string,int>("Mary",80)); mymap.insert(pair<string,int>("Smith",82)); mymap.insert(pair<string,int>("John",80)); mymap.insert(pair<string,int>("Lippman",95)); mymap.insert(pair<string,int>("Detial",82)); map<string,int>::iterator it; map<int,int> tmap; for (it=mymap.begin();it!=mymap.end();it++) tmap[(*it).second]++; map<int , int>::iterator it1; cout << " 求解结果 " << endl; for (it1=tmap.begin();it1!=tmap.end();it1++) cout << " " << (*it1).first << ": " << (*it1).second << " 次 \n"; } 上述程序的执行结果如图 1.7 所示。 图 1.7 程序执行结果 1 4. 解: 采用 map<int , int> 容器 mymap 存放求解结果,第一个分量存放质因数,第 二个分量存放质因数出现次数。对应的程序如下: # # include <stdio.h> include <map> using namespace std; void solve(int n,map<int,int> &mymap) // 求 n 的质因数分解 { int i=2; int ic=0; do { if (n%i==0) { ic++; n=n/i; } 8 第 1 章 概论 else { if (ic>0) mymap[i]=ic; ic=0; i++; } } while (n>1 || ic!=0); } void disp(map<int,int> &mymap) // 输出 mymap { map<int,int>::iterator it; for (it=mymap.begin();it!=mymap.end();++it) printf(" 质因数 %d 出现 %d 次 \n",it->first,it->second); } void main() { map<int,int> mymap; int n=12345; printf("n=%d\n",n); solve(n,mymap); disp(mymap); } 上述程序的执行结果如图 1.8 所示。 图 1.8 程序执行结果 1 5. 解: 栈容器不能顺序遍历,为此创建一个临时 tmpst 栈,将 st 的 k 个元素出栈并 进栈到 tmpst 中,再出栈 tmpst 一次得到第 k 个元素,最后将栈 tmpst 的所有元素出栈并进 栈到 st 中。对应的程序如下: # # include <stdio.h> include <stack> using namespace std; int solve(stack<int> &st,int k) // 出栈第 k 个元素 { stack<int> tmpst; int e; for (int i=0;i<k;i++) // 出栈 st 的 k 个元素并进 tmpst 栈 { e=st.top(); st.pop(); tmpst.push(e); } e=tmpst.top(); // 求第 k 个元素 tmpst.pop(); while (!tmpst.empty()) // 将 tmpst 的所有元素出栈并进栈 st { st.push(tmpst.top()); tmpst.pop(); 9 算法设计 } return e; } void disp(stack<int> &st) // 出栈 st 的所有元素 { while (!st.empty()) { printf("%d ",st.top()); st.pop(); } printf("\n"); } void main() { stack<int> st; printf(" 进栈元素 1,2,3,4\n"); st.push(1); st.push(2); st.push(3); st.push(4); int k=3; int e=solve(st,k); printf(" 出栈第 %d 个元素是 : %d\n",k,e); printf("st 中元素出栈顺序 : "); disp(st); } 上述程序的执行结果如图 1.9 所示。 图 1.9 程序执行结果 1 .2 第 2 章─递归算法设计技术 1.2.1 练习题 1 2 . 什么是直接递归和间接递归?消除递归一般要用到什么数据结构? . 分析以下程序的执行结果: # include <stdio.h> void f(int n,int &m) { if (n<1) return; else { printf(" 调用 f(%d,%d) 前 ,n=%d,m=%d\n",n-1,m-1,n,m); n--; m--; f(n-1,m); printf(" 调用 f(%d,%d) 后 :n=%d,m=%d\n",n-1,m-1,n,m); } 1 0 第 1 章 概论 } void main() { int n=4,m=4; f(n,m); } 3 . 采用直接推导方法求解以下递归方程: T (1)=1 T ( n )= T ( n - 1)+ n 当 n >1 4 . 采用特征方程方法求解以下递归方程: H (0)=0 H (1)=1 H (2)=2 H ( n )= H ( n - 1)+9 H ( n - 2) - 9 H ( n - 3) 当 n >2 5 . 采用递归树方法求解以下递归方程: T (1)=1 T ( n )=4 T ( n /2)+ n 当 n >1 6 . 采用主方法求解以下题的递归方程。 T ( n )=1 T ( n )=4 T ( n /2)+ n 2 当 n =1 当 n >1 7 8 . 分析求斐波那契 f(n) 的时间复杂度。 . 数列的首项 a 1 =0 ,后续奇数项和偶数项的计算公式分别为 a 2 n =a 2 n - 1 +2 , a 2 n + 1 =a 2 n - 1 + a 2 n - 1 ,写出计算数列第 n 项的递归算法。 9 . 对于一个采用字符数组存放的字符串 str ,设计一个递归算法求其字符个数(长 度)。 1 0. 对于一个采用字符数组存放的字符串 str ,设计一个递归算法判断 str 是否为回 文。 1 1 1 1. 对于不带头结点的单链表 L ,设计一个递归算法正序输出所有结点值。 2. 对于不带头结点的单链表 L ,设计一个递归算法逆序输出所有结点值。 3. 对于不带头结点的非空单链表 L ,设计一个递归算法返回最大值结点的地址(假 设这样的结点唯一)。 4. 对于不带头结点的单链表 L ,设计一个递归算法返回第一个值为 x 的结点的地 址,没有这样的结点时返回 NULL 。 1 1 1 5. 对于不带头结点的单链表 L ,设计一个递归算法删除第一个值为 x 的结点。 6. 假设二叉树采用二叉链存储结构存放,结点值为 int 类型,设计一个递归算法求 二叉树 bt 中所有叶子结点值之和。 7. 假设二叉树采用二叉链存储结构存放,结点值为 int 类型,设计一个递归算法求 二叉树 bt 中所有结点值大于等于 k 的结点个数。 8. 假设二叉树采用二叉链存储结构存放,所有结点值均不相同,设计一个递归算法 求值为 x 的结点的层次(根结点的层次为 1 ),没有找到这样的结点时返回 0 。 1 1 1 1 算法设计 1 .2.2 练习题参考答案 答: 一个 f 函数定义中直接调用 f 函数自己,称为直接递归。一个 f 函数定义中调 用 g 函数,而 g 函数的定义中调用 f 函数,称为间接递归。消除递归一般要用栈实现。 答: 递归函数 f ( n , m ) 中, n 是非引用参数, m 是引用参数,所以递归函数的状态为 1 . 2 . ( n )。程序执行结果如下: 调用 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 3 . 解: 求 T ( n ) 的过程如下: T ( n )= T ( n - 1)+ n =[T( n - 2)+ n - 1)]+ n =T( n - 2)+ n +( n - 1) = = = = T( n - 3)+ n +( n - 1)+( n - 2) … T(1)+ n +( n - 1)+ … +2 n +( n - 1)+ + … +2+1= n ( n +1)/2=O( n 2 ) 。 4 . 解: 整数一个常系数的线性齐次递推式,用 x n 代替 H ( n ) ,有: x n = x n - 1 +9 x n - 2 - 9 x n - 3 , 两边同时除以 x n - 3 ,得到: x 3 = x 2 +9 x - 9 ,即 x 3 - x 2 - 9 x+ 9=0 。 x 3 - x 2 - 9 x+ 9= x ( x 2 - 9) - ( x 2 - 9)=( x - 1)( x 2 - 9)=( x - 1)( x +3)( x - 3)=0 。得到 r 1 =1 , r 2 = - 3 , r 3 =3 则递归方程的通解为: H ( n )= c 1 + c 2 ( - 3) n + c 3 3 n 代入 H (0)=0 ,有 c 1 + c 2 + c 3 =0 代入 H (1)=1 ,有 c 1 - 3 c 2 +3 c 3 =1 代入 H (2)=2 ,有 c 1 +9 c 2 +9 c 3 =2 ( ‒ 1 ) n ‒ 1 1 ‒ 4 。 n ‒ 1 求出: c 1 = - 1/4 , c 2 = - 1/12 , c 3 =1/3 , H ( n )= c 1 + c 2 ( - 3) n + c 3 3 n = ( + 1 ) 3 4 5 . 解: 构造的递归树如图 1.10 所示,第 1 层的问题规模为 n ,第 2 的层的子问题的 问题规模为 n /2 ,依此类推,当展开到第 k +1 层,其规模为 n /2 k =1 ,所以递归树的高度为 log 2 n +1 。 第 1 层有 1 个结点,其时间为 n ,第 2 层有 4 个结点,其时间为 4( n /2)=2 n ,依次类推,第 k 层有 4 k - 1 个结点,每个子问题规模为 n /2 k - 1 ,其时间为 4 k - 1 ( n /2 k - 1 )=2 k - 1 n 。叶子结点的个数为 n 个,其时间为 n 。将递归树每一层的时间加起来,可得: log 2 n T ( n )= n +2 n + … + 2 k - 1 n + … + n ≈ ꢀ ∗ 2 =O( n 2 ) 。 1 2 第 1 章 概论 n n ( n /2) ( n /2) ( n /2) ( n /2) 2 n 2 高度 h 为 log n +1 n /2 2 ) ( n /2 2 ) ( n /2 2 ) ( n /2 2 ) ( 2 2 n … … … … 1 1 1 1 n 图 1.10 一棵递归树 6 . 解: 采用主方法求解,这里 a =4 , b =2 , f ( n )= n 2 。 log a log 4 b 2 = ꢀ l o g b a 因此, ꢀ = n 2 ,它与 f ( n ) 一样大,满足主定理中的情况( 2 ),所以 T ( n )= O( ꢀ log 2 n) =O( n 2 log 2 n ) 。 7 . 解: 设求斐波那契 f ( n ) 的时间为 T( n ) ,有以下递推式: T(1)=T(2) T(n)=T( n - 1)+T( n - 2)+1 当 n >2 其中, T( n ) 式中加 1 表示一次加法运算的时间。 不妨先求 T 1 (1)=T 1 (2)=1 , T 1 ( n )=T 1 ( n - 1)+T 1 ( n - 2) ,按《教程》例 2.14 的方法可以求 出: n n n 1 5 1 5 T 1 ( n )= 1 1 ≈ 1 1 5 2 = 5 2 5 2 5 n 所以 T( n )=T 1 ( n )+1 ≈ 1 1 5 2 +1=O( φ n ) ,其中 φ = 1 5 2 。 5 8 . 解: 设 f ( m ) 计算数列第 m 项值。 当 m 为偶数时,不妨设 m =2 n ,则 2 n - 1= m - 1 ,所以有 f ( m )= f ( m - 1)+2 。 当 m 为奇数时,不妨设 m =2 n +1 ,则 2 n - 1= m - 2 , 2 n = m - 1 ,所以有 f ( m )= f ( m - 2)+ f ( m - 1 ) - 1 。 对应的递归算法如下: int f(int m) { if (m==1) return 0; if (m%2==0) return f(m-1)+2; else return f(m-2)+f(m-1)-1; } 9 . 解: 设 f (str) 返回字符串 str 的长度,其递归模型如下: f (str)=0 当 *str='\0' 时 f (str)= f (str+1)+1 其他情况 对应的递归程序如下: 1 3 算法设计 # include <iostream> using namespace std; int Length(char *str) // 求 str 的字符个数 { if (*str=='\0') return 0; else return Length(str+1)+1; } void main() { char str[]="abcd"; cout << str << " 的长度 : " << Length(str) << endl; } 上述程序的执行结果如图 1.11 所示。 图 1.11 程序执行结果 1 0. 解: 设 f (str , n ) 返回含 n 个字符的字符串 str 是否为回文,其递归模型如下: f (str , n )=true 当 n =0 或者 n =1 时 当 str[0] ≠ str[ n - 1] 时 其他情况 f (str , n )=flase f (str , n )= f (str+1 , n - 2) 对应的递归算法如下: # # include <stdio.h> include <string.h> bool isPal(char *str,int n) //str 回文判断算法 { if (n==0 || n==1) return true; if (str[0]!=str[n-1]) return false; return isPal(str+1,n-2); } void disp(char *str) { int n=strlen(str); if (isPal(str,n)) printf(" %s 是回文 \n",str); else printf(" %s 不是回文 \n",str); } void main() { printf(" 求解结果 \n"); disp("abcba"); disp("a"); disp("abc"); } 1 4 第 1 章 概论 上述程序的执行结果如图 1.12 所示。 图 1.12 程序执行结果 1 1. 解: 设 f (L) 正序输出单链表 L 的所有结点值,其递归模型如下: f (L) ≡ 不做任何事情 当 L=NULL f (L) ≡ 输出 L - >data; f (L - >next); 当 L ≠ NULL 时 对应的递归程序如下: # include "LinkList.cpp" // 包含单链表的基本运算算法 // 正序输出所有结点值 void dispLink(LinkNode *L) { if (L==NULL) return; else { printf("%d ",L->data); dispLink(L->next); } } void main() { int a[]={1,2,5,2,3,2}; int n=sizeof(a)/sizeof(a[0]); LinkNode *L; CreateList(L,a,n); printf(" 正向 L: "); dispLink(L); printf("\n"); Release(L); // 由 a[0..n-1] 创建不带头结点的单链表 // 销毁单链表 } 上述程序的执行结果如图 1.13 所示。 图 1.13 程序执行结果 1 2. 解: 设 f (L) 逆序输出单链表 L 的所有结点值,其递归模型如下: f (L) ≡ 不做任何事情 当 L=NULL f (L) ≡ f (L - >next); 输出 L - >data 当 L ≠ NULL 时 对应的递归程序如下: # include "LinkList.cpp" void Revdisp(LinkNode *L) if (L==NULL) return; // 包含单链表的基本运算算法 // 逆序输出所有结点值 { 1 5 算法设计 else { Revdisp(L->next); printf("%d ",L->data); } } void main() { int a[]={1,2,5,2,3,2}; int n=sizeof(a)/sizeof(a[0]); LinkNode *L; CreateList(L,a,n); printf(" 反向 L: "); Revdisp(L); printf("\n"); Release(L); } 上述程序的执行结果如图 1.14 所示。 图 1.14 程序执行结果 1 3. 解: 设 f (L) 返回单链表 L 中值最大结点的地址,其递归模型如下: f (L) = L 当 L 只有一个结点时 f (L) = MAX{ f (L - >next) , L->data} 其他情况 对应的递归程序如下: # include "LinkList.cpp" // 包含单链表的基本运算算法 LinkNode *Maxnode(LinkNode *L) // 返回最大值结点的地址 { if (L->next==NULL) return L; // 只有一个结点时 else { } LinkNode *maxp; maxp=Maxnode(L->next); if (L->data>maxp->data) return L; else return maxp; } void main() int a[]={1,2,5,2,3,2}; int n=sizeof(a)/sizeof(a[0]); { LinkNode *L,*p; CreateList(L,a,n); p=Maxnode(L); printf(" 最大结点值 : %d\n",p->data); Release(L); 1 6 第 1 章 概论 } 上述程序的执行结果如图 1.15 所示。 图 1.15 程序执行结果 1 4. 解: 设 f (L , x ) 返回单链表 L 中第一个值为 x 的结点的地址,其递归模型如下: f (L , x ) = NULL f (L , x ) = L 当 L=NULL 时 当 L ≠ NULL 且 L - >data= x 时 其他情况 f (L , x ) = f (L - >next , x ) 对应的递归程序如下: # include "LinkList.cpp" // 包含单链表的基本运算算法 LinkNode *Firstxnode(LinkNode *L,int x) // 返回第一个值为 x 的结点的地址 { if (L==NULL) return NULL; if (L->data==x) return L; else return Firstxnode(L->next,x); } void main() { int a[]={1,2,5,2,3,2}; int n=sizeof(a)/sizeof(a[0]); LinkNode *L,*p; CreateList(L,a,n); int x=2; p=Firstxnode(L,x); printf(" 结点值 : %d\n",p->data); Release(L); } 上述程序的执行结果如图 1.16 所示。 图 1.16 程序执行结果 1 5. 解: 设 f (L , x ) 删除单链表 L 中第一个值为 x 的结点,其递归模型如下: f (L , x ) ≡ 不做任何事情 当 L=NULL f (L , x ) ≡ 删除 L 结点, L=L - >next f (L , x ) ≡ f (L - >next , x ) 当 L ≠ NULL 且 L - >data= x 其他情况 对应的递归程序如下: 1 7 算法设计 # include "LinkList.cpp" // 包含单链表的基本运算算法 void Delfirstx(LinkNode *&L,int x) // 删除单链表 L 中第一个值为 x 的结点 { if (L==NULL) return; if (L->data==x) { LinkNode *p=L; L=L->next; free(p); } else Delfirstx(L->next,x); } void main() { int a[]={1,2,5,2,3,2}; int n=sizeof(a)/sizeof(a[0]); LinkNode *L; CreateList(L,a,n); printf(" 删除前 L: "); DispList(L); int x=2; printf(" 删除第一个值为 %d 的结点 \n",x); Delfirstx(L,x); printf(" 删除后 L: "); DispList(L); Release(L); } 上述程序的执行结果如图 1.17 所示。 图 1.17 程序执行结果 1 6. 解: 设 f (bt) 返回二叉树 bt 中所有叶子结点值之和,其递归模型如下: f (bt)=0 当 bt=NULL f (bt)=bt - >data 当 bt ≠ NULL 且 bt 结点为叶子结点 其他情况 f (bt)= f (bt - >lchild)+ f (bt - >rchild) 对应的递归程序如下: # include "Btree.cpp" // 包含二叉树的基本运算算法 int LeafSum(BTNode *bt) // 二叉树 bt 中所有叶子结点值之和 { if (bt==NULL) return 0; if (bt->lchild==NULL && bt->rchild==NULL) return bt->data; int lsum=LeafSum(bt->lchild); int rsum=LeafSum(bt->rchild); return lsum+rsum; } void main() 1 8 第 1 章 概论 { BTNode *bt; Int a[]={5,2,3,4,1,6}; Int b[]={2,3,5,1,4,6}; // 先序序列 // 中序序列 int n=sizeof(a)/sizeof(a[0]); bt=CreateBTree(a,b,n); // 由 a 和 b 构造二叉链 bt printf(" 二叉树 bt:"); DispBTree(bt); printf("\n"); printf(" 所有叶子结点值之和 : %d\n",LeafSum(bt)); DestroyBTree(bt); // 销毁树 bt } 上述程序的执行结果如图 1.18 所示。 图 1.18 程序执行结果 1 7. 解: 设 f (bt , k ) 返回二叉树 bt 中所有结点值大于等于 k 的结点个数,其递归模型 如下: f (bt , k )=0 当 bt=NULL f (bt , k )= f (bt - >lchild , k )+ f (bt - >rchild , k )+1 f (bt , k )= f (bt - >lchild , k )+ f (bt - >rchild , k ) 当 bt ≠ NULL 且 bt - >data ≥ k 其他情况 对应的递归程序如下: # include "Btree.cpp" // 包含二叉树的基本运算算法 // 大于等于 k 的结点个数 int Nodenum(BTNode *bt,int k) { if (bt==NULL) return 0; int lnum=Nodenum(bt->lchild,k); int rnum=Nodenum(bt->rchild,k); if (bt->data>=k) return lnum+rnum+1; else return lnum+rnum; } void main() { BTNode *bt; Int a[]={5,2,3,4,1,6}; Int b[]={2,3,5,1,4,6}; int n=sizeof(a)/sizeof(a[0]); bt=CreateBTree(a,b,n); // 由 a 和 b 构造二叉链 bt printf(" 二叉树 bt:"); DispBTree(bt); printf("\n"); int k=3; printf(" 大于等于 %d 的结点个数 : %d\n",k,Nodenum(bt,k)); DestroyBTree(bt); // 销毁树 bt } 上述程序的执行结果如图 1.19 所示。 1 9 算法设计 图 1.19 程序执行结果 1 8. 解: 设 f (bt , x , h ) 返回二叉树 bt 中 x 结点的层次,其中 h 表示 bt 所指结点的层 次,初始调用时, bt 指向根结点, h 置为 1 。其递归模型如下: f (bt , x , h )=0 当 bt=NULL f (bt , x , h )= h 当 bt ≠ NULL 且 bt - >data=x 当 l = f (bt - >lchild , x , h +1) ≠ 0 其他情况 f (bt , x , h ) = l f (bt , x , h ) = f (bt - >rchild , x , h +1) 对应的递归程序如下: # include "Btree.cpp" // 包含二叉树的基本运算算法 int Level(BTNode *bt,int x,int h) // 求二叉树 bt 中 x 结点的层次 { // 初始调用时: bt 为根, h 为 1 if (bt==NULL) return 0; if (bt->data==x) return h; // 找到 x 结点,返回 h else { int l=Level(bt->lchild,x,h+1); // 在左子树中查找 if (l!=0) return l; // 在左子树中找到,返回其层次 l else return Level(bt->rchild,x,h+1);// 返回在右子树的查找结果 } } void main() { BTNode *bt; Int a[]={5,2,3,4,1,6}; Int b[]={2,3,5,1,4,6}; int n=sizeof(a)/sizeof(a[0]); bt=CreateBTree(a,b,n); // 由 a 和 b 构造二叉链 bt printf(" 二叉树 bt:"); DispBTree(bt); printf("\n"); int x=1; printf("%d 结点的层次 : %d\n",x,Level(bt,x,1)); DestroyBTree(bt); // 销毁树 bt } 上述程序的执行结果如图 1.20 所示。 图 1.20 程序执行结果 2 0 第 1 章 概论 1 .3 第 3 章─分治法 1 .3.1 练习题 分治法的设计思想是将一个难以直接解决的大问题分割成规模较小的子问题,分 别解决子问题,最后将子问题的解组合起来形成原问题的解。这要求原问题和子问题 )。 A. 问题规模相同,问题性质相同 1 . ( B. 问题规模相同,问题性质不同 C. 问题规模不同,问题性质相同 D. 问题规模不同,问题性质不同 2 . 在寻找 n 个元素中第 k 小元素问题中,如快速排序算法思想,运用分治算法对 n 个元素进行划分,如何选择划分基准?下面( )答案解释最合理。 A. 随机选择一个元素作为划分基准 B. 取子序列的第一个元素作为划分基准 C. 用中位数的中位数方法寻找划分基准 D. 以上皆可行。但不同方法,算法复杂度上界可能不同 3 . 对于下列二分查找算法,以下正确的是( )。 A. int binarySearch(int a[], int n, int x) { int low=0, high=n-1; while(low<=high) { int mid=(low+high)/2; if(x==a[mid]) return mid; if(x>a[mid]) low=mid; else high=mid; } return – 1; } B. int binarySearch(int a[], int n, int x) { int low=0, high=n-1; while(low+1!=high) { int mid=(low+high)/2; if(x>=a[mid]) low=mid; else high=mid; } if(x==a[low]) return low; else return – 1; } C. int binarySearch (int a[], int n, int x) { int low=0, high=n-1; while(low<high-1) { int mid=(low+high)/2; 2 1 算法设计 if(x<a[mid]) high=mid; else low=mid; } if(x==a[low]) return low; else return – 1; } D. int binarySearch(int a[], int n, int x) { if(n > 0 && x >= a[0]) { int low = 0, high = n-1; while(low < high) { int mid=(low+high+1)/2; if(x < a[mid]) high=mid-1; else low=mid; } if(x==a[low]) return low; } return – 1; } 4 5 . 快速排序算法是根据分治策略来设计的,简述其基本思想。 . 假设含有 n 个元素的待排序的数据 a 恰好是递减排列的,说明调用 QuickSort( a , 0 , n - 1) 递增排序的时间复杂度为 O( n 2 ) 。 6 . 以下哪些算法采用分治策略: ( ( ( ( 1 )堆排序算法 2 )二路归并排序算法 3 )折半查找算法 4 )顺序查找算法 7 8 . 适合并行计算的问题通常表现出哪些特征? . 设有两个复数 x = a + bi 和 y = c + di 。复数乘积 xy 可以使用 4 次乘法来完成,即 xy =( ac - bd )+( ad + bc ) i 。设计一个仅用 3 次乘法来计算乘积 xy 的方法。 9 1 1 1 . 有 4 个数组 a 、 b 、 c 和 d ,都已经排好序,说明找出这 4 个数组的交集的方法。 0. 设计一个算法,采用分治法求一个整数序列中的最大最小元素。 1. 设计一个算法,采用分治法求 x n 。 2. 假设二叉树采用二叉链存储结构进行存储。设计一个算法采用分治法求一棵二叉 树 bt 的高度。 1 3. 假设二叉树采用二叉链存储结构进行存储。设计一个算法采用分治法求一棵二叉 树 bt 中度为 2 的结点个数。 1 4. 有一种二叉排序树,其定义是空树是一棵二叉排序树,若不空,左子树中所有结 点值小于根结点值,右子树中所有结点值大于根结点值,并且左右子树都是二叉排序树。 现在该二叉排序树采用二叉链存储,采用分治法设计查找值为 x 的结点地址,并分析算法 的最好的平均时间复杂度。 2 2 第 1 章 概论 1 5. 设有 n 个互不相同的整数,按递增顺序存放在数组 a [0.. n - 1] 中,若存在一个下标 i ( 0 ≤ i < n ),使得 a [ i ]= i 。设计一个算法以 O(log 2 n ) 时间找到这个下标 i 。 1 6. 请你模仿二分查找过程设计一个三分查找算法。分析其时间复杂度。 1 7. 对于大于 1 的正整数 n ,可以分解为 n = x 1 * x 2 * … * x m ,其中 x i ≥ 2 。例如, n =12 时 有 8 种不同的分解式: 12=12 , 12=6*2 , 12=4*3 , 12=3*4 , 12=3*2*2 , 12=2*6 , 2=2*3*2 , 12=2*2*3 ,设计一个算法求 n 的不同分解式个数。 8. 设计一个基于 BSP 模型的并行算法,假设有 p 台处理器,计算整数数组 a [0.. n - 1] 的所有元素之和。并分析算法的时间复杂度。 1 1 1.3.2 练习题参考答案 1 2 3 . 答: C 。 . 答: D 。 . 答: 以 a []={1 , 2 , 3 , 4 , 5} 为例说明。选项 A 中在查找 5 时出现死循环。选项 B 中在查找 5 时返回 - 1 。选项 C 中在查找 5 时返回 - 1 。选项 D 正确。 4. 答: 对于无序序列 a [low..high] 进行快速排序,整个排序为“大问题”。选择其中的 一个基准 base= a [ i ] (通常以序列中第一个元素为基准),将所有小于等于 base 的元素移动 到它的前面,所有大于等于 base 的元素移动到它的后面,即将基准归位到 a [ i ] ,这样产生 a [low.. i - 1] 和 a [ i +1..high] 两个无序序列,它们的排序为“小问题”。当 a [low..high] 序列只 有一个元素或者为空时对应递归出口。 所以快速排序算法就是采用分治策略,将一个“大问题”分解为两个“小问题”来求 解。由于元素都是在 a 数组中,其合并过程是自然产生的,不需要特别设计。 5 . 答: 此时快速排序对应的递归树高度为 O( n ) ,每一次划分对应的时间为 O( n ) ,所 以整个排序时间为 O( n 2 ) 。 6 7 . 答: 其中二路归并排序和折半查找算法采用分治策略。 . 答: 适合并行计算的问题通常表现出以下特征: ( 1 )将工作分离成离散部分,有助于同时解决。例如,对于分治法设计的串行算 法,可以将各个独立的子问题并行求解,最后合并成整个问题的解,从而转化为并行算 法。 ( ( 2 )随时并及时地执行多个程序指令。 3 )多计算资源下解决问题的耗时要少于单个计算资源下的耗时。 8 . 答: xy =( ac - bd )+(( a + b )( c + d ) - ac - bd ) i 。由此可见,这样计算 xy 只需要 3 次乘法(即 ac 、 bd 和 ( a + b )( c + d ) 乘法运算)。 答: 采用基本的二路归并思路,先求出 a 、 b 的交集 ab ,再求出 c 、 d 的交集 cd , 最后求出 ab 和 cd 的交集,即为最后的结果。也可以直接采用 4 路归并方法求解。 0. 解: 采用类似求求一个整数序列中的最大次大元素的分治法思路。对应的程序如 9 . 1 下: # # # include <stdio.h> define max(x,y) ((x)>(y)?(x):(y)) define min(x,y) ((x)<(y)?(x):(y)) 2 3 算法设计 void MaxMin(int a[],int low,int high,int &maxe,int &mine) // 求 a 中最大最小元素 { if (low==high) // 只有一个元素 // 只有两个元素 // 有两个以上元素 { maxe=a[low]; mine=a[low]; } else if (low==high-1) { maxe=max(a[low],a[high]); mine=min(a[low],a[high]); } else { int mid=(low+high)/2; int lmaxe,lmine; MaxMin(a,low,mid,lmaxe,lmine); int rmaxe,rmine; MaxMin(a,mid+1,high,rmaxe,rmine); maxe=max(lmaxe,rmaxe); mine=min(lmine,rmine); } } void main() { int a[]={4,3,1,2,5}; int n=sizeof(a)/sizeof(a[0]); int maxe,mine; MaxMin(a,0,n-1,maxe,mine); printf("Max=%d, Min=%d\n",maxe,mine); } 上述程序的执行结果如图 1.21 所示。 图 1.21 程序执行结果 1 1. 解: 设 f ( x , n )= x n ,采用分治法求解对应的递归模型如下: f ( x , n )= x 当 n =1 f ( x , n )= f ( x , n /2)* f ( x , n /2) f ( x , n )= f ( x , ( n - 1)/2)* f ( x , ( n - 1)/2)* x 当 n 为偶数时 当 n 为奇数时 对应的递归程序如下: # include <stdio.h> double solve(double x,int n) // 求 x^n { double fv; if (n==1) return x; if (n%2==0) { fv=solve(x,n/2); return fv*fv; } 2 4 第 1 章 概论 else { fv=solve(x,(n-1)/2); return fv*fv*x; } } void main() { double x=2.0; printf(" 求解结果 :\n"); for (int i=1;i<=10;i++) printf(" %g^%d=%g\n",x,i,solve(x,i)); } 上述程序的执行结果如图 1.22 所示。 图 1.22 程序执行结果 1 2. 解: 设 f (bt) 返回二叉树 bt 的高度,对应的递归模型如下: f (bt)=0 当 bt=NULL f (bt)=MAX{ f (bt - >lchild) , f (bt - >rchild)}+1 其他情况 对应的程序如下: # include "Btree.cpp" // 包含二叉树的基本运算算法 // 求二叉树 bt 的高度 int Height(BTNode *bt) { if (bt==NULL) return 0; int lh=Height(bt->lchild); int rh=Height(bt->rchild); if (lh>rh) return lh+1; else return rh+1; // 子问题 1 // 子问题 2 // 合并 } void main() { BTNode *bt; Int a[]={5,2,3,4,1,6}; Int b[]={2,3,5,1,4,6}; int n=sizeof(a)/sizeof(a[0]); bt=CreateBTree(a,b,n); // 由 a 和 b 构造二叉链 bt printf(" 二叉树 bt:"); DispBTree(bt); printf("\n"); printf("bt 的高度 : %d\n",Height(bt)); DestroyBTree(bt); // 销毁树 bt } 2 5 算法设计 上述程序的执行结果如图 1.23 所示。 图 1.23 程序执行结果 1 3. 解: 设 f (bt) 返回二叉树 bt 中度为 2 的结点个数,对应的递归模型如下: f (bt)=0 当 bt=NULL f (bt)= f (bt - >lchild)+ f (bt - >rchild)+1 f (bt)= f (bt - >lchild)+ f (bt - >rchild) 若 bt ≠ NULL 且 bt 为双分支结点 其他情况 对应的算法如下: # include "Btree.cpp" // 包含二叉树的基本运算算法 // 求 bt 中度为 2 的结点个数 int Nodes(BTNode *bt) { int n=0; if (bt==NULL) return 0; if (bt->lchild!=NULL && bt->rchild!=NULL) n=1; return Nodes(bt->lchild)+Nodes(bt->rchild)+n; } void main() { BTNode *bt; Int a[]={5,2,3,4,1,6}; Int b[]={2,3,5,1,4,6}; int n=sizeof(a)/sizeof(a[0]); bt=CreateBTree(a,b,n); // 由 a 和 b 构造二叉链 bt printf(" 二叉树 bt:"); DispBTree(bt); printf("\n"); printf("bt 中度为 2 的结点个数 : %d\n",Nodes(bt)); DestroyBTree(bt); // 销毁树 bt } 上述程序的执行结果如图 1.24 所示。 图 1.24 程序执行结果 1 4. 解: 设 f (bt , x ) 返回在二叉排序树 bt 得到的值为 x 结点的地址,若没有找到返回 空,对应的递归模型如下: f (bt , x )=NULL 当 bt=NULL f (bt , x )=bt 当 bt ≠ NULL 且 x =bt - >data 当 x >bt - >data f (bt , x )= f (bt - >lchild , x ) 2 6 第 1 章 概论 f (bt , x )= f (bt - >rchild , x ) 当 x <bt - >data 对应的程序如下: # include "Btree.cpp" // 包含二叉树的基本运算算法 BTNode *Search(BTNode *bt,Int x) // 在二叉排序树 bt 查找的值为 x 结点 { if (bt==NULL) return NULL; if (x==bt->data) return bt; if (x<bt->data) return Search(bt->lchild,x); else return Search(bt->rchild,x); } void main() { BTNode *bt; Int a[]={4,3,2,8,6,7,9}; Int b[]={2,3,4,6,7,8,9}; int n=sizeof(a)/sizeof(a[0]); bt=CreateBTree(a,b,n); // 构造一棵二叉排序树 bt printf(" 二叉排序树 bt:"); DispBTree(bt); printf("\n"); int x=6; BTNode *p=Search(bt,x); if (p!=NULL) printf(" 找到结点 : %d\n",p->data); else printf(" 没有找到结点 \n",x); DestroyBTree(bt); // 销毁树 bt } 上述程序的执行结果如图 1.25 所示。 图 1.25 程序执行结果 Search(bt , x ) 算法采用的是减治法,最好的情况是某个结点左右子树高度大致相同, 其平均执行时间 T ( n ) 如下: T ( n )=1 当 n =1 当 n >1 T ( n )= T ( n /2)+1 可以推出 T ( n )=O(log 2 n ) ,其中 n 为二叉排序树的结点个数。 15. 解: 采用二分查找方法。 a [ i ]= i 时表示该元素在有序非重复序列 a 中恰好第 i 大。 对于序列 a [low..high] , mid=(low+high)/2 ,若 a [mid]=mid 表示找到该元素;若 a [mid]>mid 说明右区间的所有元素都大于其位置,只能在左区间中查找;若 a [mid]<mid 说明左区间 的所有元素都小于其位置,只能在右区间中查找。对应的程序如下: # include <stdio.h> int Search(int a[],int n) int low=0,high=n-1,mid; // 查找使得 a[i]=i { 2 7 算法设计 while (low<=high) { mid=(low+high)/2; if (a[mid]==mid) return mid; else if (a[mid]<mid) low=mid+1; // 查找到这样的元素 // 这样的元素只能在右区间中出现 // 这样的元素只能在左区间中出现 else high=mid-1; } return -1; } void main() { int a[]={-2,-1,2,4,6,8,9}; int n=sizeof(a)/sizeof(a[0]); int i=Search(a,n); printf(" 求解结果 \n"); if (i!=-1) printf(" 存在 a[%d]=%d\n",i,i); else printf(" 不存在 \n"); } 上述程序的执行结果如图 1.26 所示。 图 1.26 程序执行结果 1 6. 解: 对于有序序列 a [low..high] ,若元素个数少于 3 个,直接查找。若含有更多的 元素,将其分为 a [low..mid1 - 1] 、 a [mid1+1..mid2 - 1] 、 a [mid2+1..high] 子序列,对每个子序 列递归查找,算法的时间复杂度为 O(log 3 n ) ,属于 O(log 2 n ) 级别。对应的算法如下: # include <stdio.h> int Search(int a[],int low,int high,int x) // 三分查找 { if (high<low) return -1; // 序列中没有元素 else if (high==low) // 序列中只有 1 个元素 { if (x==a[low]) return low; else return -1; } if (high-low<2) { if (x==a[low]) // 序列中只有 2 个元素 return low; else if (x==a[low+1]) return low+1; else 2 8 第 1 章 概论 return -1; } int length=(high-low+1)/3; int mid1=low+length; int mid2=high-length; if (x==a[mid1]) // 每个子序列的长度 return mid1; else if (x<a[mid1]) return Search(a,low,mid1-1,x); else if (x==a[mid2]) return mid2; else if (x<a[mid2]) return Search(a,mid1+1,mid2-1,x); else return Search(a,mid2+1,high,x); } void main() { int a[]={1,3,5,7,9,11,13,15}; int n=sizeof(a)/sizeof(a[0]); printf(" 求解结果 \n"); int x=13; int i=Search(a,0,n-1,x); if (i!=-1) printf(" a[%d]=%d\n",i,x); else printf(" 不存在 %d\n",x); int y=10; int j=Search(a,0,n-1,y); if (j!=-1) printf(" a[%d]=%d\n",j,y); else printf(" 不存在 %d\n",y); } 上述程序的执行结果如图 1.27 所示。 图 1.27 程序执行结果 1 7. 解: 设 f ( n ) 表示 n 的不同分解式个数。有: f (1)=1 ,作为递归出口 f (2)=1 ,分解式为: 2=2 f (3)=1 ,分解式为: 3=3 f (4)=2 ,分解式为: 4=4 , 4=2*2 2 9 算法设计 f (6)=3 ,分解式为: 6=6 , 6=2*3 , 6=3*2 ,即 f (6)= f (1)+ f (2)+ f (3) 以此类推,可以看出 f ( n ) 为 n 的所有因数的不同分解式个数之和,即 f ( n )= ∑ ꢀ ꢁ ꢂ = 0 ꢃ ( ꢀ ꢄ ꢂ ) 。对应的程序如下: # # include <stdio.h> define MAX 101 int solve(int n) { if (n==1) return 1; // 求 n 的不同分解式个数 else { int sum=0; for (int i=2;i<=n;i++) if (n%i==0) sum+=solve(n/i); return sum; } } void main() { int n=12; int ans=solve(n); printf(" 结果 : %d\n",ans); } 上述程序的执行结果如图 1.28 所示。 图 1.28 程序执行结果 1 8. 解: 对应的并行算法如下: int Sum(int a[],int s,int t,int p,int i) // 处理器 i 执行求和 { int j,s=0; for (j=s;j<=t;j++) s+=a[j]; return s; } int ParaSum(int a[],int s,int t,int p,int i) { int sum=0,j,k=0,sj; for (j=0;j<p;j++) //for 循环的各个子问题并行执行 { sj=Sum(a,k,k+n/p-1,p,j); k+=n/p; } sum+=sj; return sum; } 每个处理器的执行时间为 O( n / p ) ,同步开销为 O( p ) ,所以该算法的时间复杂度为 O( n / p + p ) 。 3 0 第 1 章 概论 1 .4 第 4 章─蛮力法 1.4.1 练习题 1 2 3 . 简要比较蛮力法和分治法。 . 在采用蛮力法求解时什么情况下使用递归? . 考虑下面这个算法,它求的是数组 a 中大小相差最小的两个元素的差。请对这个 算法做尽可能多的改进。 # # define INF 99999 define abs(x) (x)<0?-(x):(x) // 求绝对值宏 int Mindif(int a[],int n) { int dmin=INF; for (int i=0;i<=n-2;i++) for (int j=i+1;j<=n-1;j++) { int temp=abs(a[i]-a[j]); if (temp<dmin) dmin=temp; } return dmin; } 4 . 给定一个整数数组 A =( a 0 , a 1 ,… a n - 1 ) ,若 i < j 且 a i > a j ,则 < a i , a j > 就为一个逆序 对。例如数组( 3 , 1 , 4 , 5 , 2 )的逆序对有 <3 , 1> , <3 , 2> , <4 , 2> , <5 , 2> 。设计一 个算法采用蛮力法求 A 中逆序对的个数即逆序数。 5 . 对于给定的正整数 n ( n >1 ) , 采用蛮力法求 1!+2!+ … + n ! ,并改进该算法提高效 率。 6 . 有一群鸡和一群兔,它们的只数相同,它们的脚数都是三位数,且这两个三位数 的各位数字只能是 0 、 1 、 2 、 3 、 4 、 5 。设计一个算法用蛮力法求鸡和兔的只数各是多 少?它们的脚数各是多少? 7 . 有一个三位数,个位数字比百位数字大,而百位数字又比十位数字大,并且各位 数字之和等于各位数字相乘之积,设计一个算法用穷举法求此三位数。 . 某年级的同学集体去公园划船,如果每只船坐 10 人,那么多出 2 个座位;如果每 只船多坐 2 人,那么可少租 1 只船,设计一个算法用蛮力法求该年级的最多人数? 已知:若一个合数的质因数分解式逐位相加之和等于其本身逐位相加之和,则称 这个数为 Smith 数。如 4937775=3*5*5*65837 ,而 3+5+5+6+5+8+3+7=42 , +9+3+7+7+7+5=42 ,所以 4937775 是 Smith 数。求给定一个正整数 N ,求大于 N 的最小 Smith 数。 输入:若干个 case ,每个 case 一行代表正整数 N ,输入 0 表示结束 8 9 . 4 输出:大于 N 的最小 Smith 数 输入样例: 4 0 937774 样例输出: 3 1 算法设计 4 937775 1 0. 求解涂棋盘问题。小易有一块 n * n 的棋盘,棋盘的每一个格子都为黑色或者白 色,小易现在要用他喜欢的红色去涂画棋盘。小易会找出棋盘中某一列中拥有相同颜色的 最大的区域去涂画,帮助小易算算他会涂画多少个棋格。 输入描述:输入数据包括 n +1 行:第一行为一个整数 n ( 1 ≤ n ≤ 50 ),即棋盘的大 小,接下来的 n 行每行一个字符串表示第 i 行棋盘的颜色, 'W' 表示白色, 'B' 表示黑色。 输出描述:输出小易会涂画的区域大小。 输入例子: 3 BWW BBB BWB 输出例子: 3 1 1. 给定一个含 n ( n >1 )个整数元素的 a ,所有元素不相同,采用蛮力法求出 a 中所 有元素的全排列。 1.4.2 练习题参考答案 1 . 答: 蛮力法是一种简单直接地解决问题的方法,适用范围广,是能解决几乎所有 问题的一般性方法,常用于一些非常基本、但又十分重要的算法(排序、查找、矩阵乘法 和字符串匹配等),蛮力法主要解决一些规模小或价值低的问题,可以作为同样问题的更 高效算法的一个标准。而分治法采用分而治之思路,把一个复杂的问题分成两个或更多的 相同或相似的子问题,再把子问题分成更小的子问题直到问题解决。分治法在求解问题 时,通常性能比蛮力法好。 2 . 答: 如果用蛮力法求解的问题可以分解为若干个规模较小的相似子问题,此时可 以采用递归来实现算法。 . 解: 上述算法的时间复杂度为 O( n 2 ) ,采用的是最基本的蛮力法。可以先对 a 中元 素递增排序,然后依次比较相邻元素的差,求出最小差,改进后的算法如下: 3 # # include <stdio.h> include <algorithm> using namespace std; int Mindif1(int a[],int n) { sort(a,a+n); // 递增排序 int dmin=a[1]-a[0]; for (int i=2;i<n;i++) { int temp=a[i]-a[i-1]; if (temp<dmin) dmin=temp; } return dmin; } 3 2 第 1 章 概论 上述算法的主要时间花费在排序上,算法的时间复杂度为 O( n log 2 n ) 。 4 . 解: 采用两重循环直接判断是否为逆序对,算法的时间复杂度为 O(n2) ,比第 3 章 实验 3 算法的性能差。对应的算法如下: int solve(int a[],int n) // 求逆序数 { int ans=0; for (int i=0;i<n-1;i++) for (int j=i+1;j<n;j++) if (a[i]>a[j]) ans++; return ans; } 5 . 解: 直接采用蛮力法求解算法如下: long f(int n) // 求 n! { long fn=1; for (int i=2;i<=n;i++) fn=fn*i; return fn; } long solve(int n) // 求 1!+2!+…+n! { long ans=0; for (int i=1;i<=n;i++) ans+=f(i); return ans; } 实际上, f ( n )= f ( n - 1)* n , f (1)=1 ,在求 f ( n ) 时可以利用 f ( n - 1) 的结果。改进后的算法如 下: long solve1(int n) // 求 1!+2!+…+n! { long ans=0; long fn=1; for (int i=1;i<=n;i++) { fn=fn*i; ans+=fn; } return ans; } 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 时输出结果。对应的程序如下: # include <stdio.h> void solve() int a,b,c,d,e,f; { int x1,x2,y,z; for (a=1;a<=5;a++) for (b=0;b<=5;b++) for (c=0;c<=5;c++) 3 3 算法设计 for (d=1;d<=5;d++) for (e=0;e<=5;e++) for (f=0;f<=5;f++) { y=a*100+b*10+c; z=d*100+e*10+f; if (y%2!=0 || z%4!=0) continue; // 鸡脚数 // 兔脚数 x1=y/2; // 鸡只数 // 兔只数 x2=z/4; if (x1==x2) printf(" 鸡只数 :%d, 兔只数 :%d, 鸡脚数 :%d, 兔脚数 :%d\n",x1,x2,y,z); } } void main() { printf(" 求解结果 \n"); solve(); } 上述程序的执行结果如图 1.29 所示。 图 1.29 程序执行结果 7 . 解: 设该三位数为 x = abc ,有 1 ≤ a ≤ 9 , 0 ≤ b , c ≤ 9 ,满足 c > a , a > b , a + b + c = a * b * c 。对应的程序如下: # include <stdio.h> void solve() { int a,b,c; for (a=1;a<=9;a++) for (b=0;b<=9;b++) for (c=0;c<=9;c++) { if (c>a && a>b && a+b+c==a*b*c) printf(" %d%d%d\n",a,b,c); } } void main() 3 4 第 1 章 概论 { } printf(" 求解结果 \n"); solve(); 上述程序的执行结果如图 1.30 所示。 图 1.30 程序执行结果 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 即可。对应的程序如下: # include <stdio.h> int solve() { int x,y,z; for (y=1;y<=100;y++) for (z=0;z<12;z++) if (10*y-2==12*(y-1)-z) x=10*y-2; return x; } void main() { printf(" 求解结果 \n"); printf(" 最多人数 :%d\n",solve()); } 上述程序的执行结果如图 1.31 所示。 图 1.31 程序执行结果 9 . 解: 采用蛮力法求出一个正整数 n 的各位数字和 sum1 ,以及 n 的所有质因数的数 字和 sum2 ,若 sum1=sum2 ,即为 Smitch 数。从用户输入的 n 开始枚举,若是 Smitch 数,输出,本次结束,否则 n ++ 继续查找大于 n 的最小 Smitch 数。对应的完整程序如 下: # include <stdio.h> int Sum(int n) // 求 n 的各位数字和 { int sum=0; while (n>0) 3 5 算法设计 { } sum+=n%10; n=n/10; return sum; } bool solve(int n) // 判断 n 是否为 Smitch 数 { int m=2; int sum1=Sum(n); int sum2=0; while (n>=m) { if (n%m==0) // 找到一个质因数 m { n=n/m; sum2+=Sum(m); } else m++; } if (sum1==sum2) return true; else return false; } void main() { int n; while (true) { scanf("%d",&n); if (n==0) break; while (!solve(n)) n++; printf("%d\n",n); } } 1 0. 解: 采用蛮力法,统计每一列相邻相同颜色的棋格个数 countj ,在 countj 中求最 大值。对应的程序如下: # # / include <stdio.h> define MAXN 51 / 问题表示 int n; char board[MAXN][MAXN]; int getMaxArea() // 蛮力法求解算法 { int maxArea=0; for (int j=0; j<n; j++) { int countj=1; for (int i=1; i<n; i++) // 统计第 j 列中相同颜色相邻棋格个数 { } if (board[i][j]==board[i-1][j]) countj++; countj=1; else 3 6 第 1 章 概论 if (countj>maxArea) maxArea=countj; } return maxArea; } int main() { scanf("%d",&n); for (int i=0;i<n;i++) scanf("%s",board[i]); printf("%d\n",getMaxArea()); return 0; } 1 1. 解: 与《教程》中求全排列类似,但需要将求 1 ~ n 的全排列改为按下标 0 ~ n - 1 求 a 的全排列(下标从 0 开始)。采用非递归的程序如下: # # include <stdio.h> include <vector> using namespace std; vector<vector<int> > ps; // 存放全排列 void Insert(vector<int> s,int a[],int i,vector<vector<int> > &ps1) / / 在每个集合元素中间插入 i 得到 ps1 vector<int> s1; { vector<int>::iterator it; for (int j=0;j<=i;j++) // 在 s( 含 i 个整数 ) 的每个位置插入 a[i] { s1=s; it=s1.begin()+j; s1.insert(it,a[i]); ps1.push_back(s1); // 求出插入位置 // 插入整数 a[i] // 添加到 ps1 中 } } void Perm(int a[],int n) // 求 a[0..n-1] 的所有全排列 // 临时存放子排列 { vector<vector<int> > ps1; vector<vector<int> >::iterator it; vector<int> s,s1; // 全排列迭代器 s.push_back(a[0]); ps.push_back(s); // 添加 {a[0]} 集合元素 // 循环添加 a[1] ~ a[n-1] //ps1 存放插入 a[i] 的结果 for (int i=1;i<n;i++) { ps1.clear(); for (it=ps.begin();it!=ps.end();++it) Insert(*it,a,i,ps1); ps=ps1; // 在每个集合元素中间插入 a[i] 得到 ps1 } } void dispps() vector<vector<int> >::reverse_iterator it; // 输出全排列 ps { // 全排列的反向迭代器 // 排列集合元素迭代器 vector<int>::iterator sit; for (it=ps.rbegin();it!=ps.rend();++it) { for (sit=(*it).begin();sit!=(*it).end();++sit) printf("%d",*sit); printf(" "); 3 7 算法设计 } printf("\n"); } void main() { int a[]={2,5,8}; int n=sizeof(a)/sizeof(a[0]); printf("a[0 ~ %d] 的全排序如下 :\n ",n-1); Perm(a,n); dispps(); } 上述程序的执行结果如图 1.32 所示。 图 1.32 程序执行结果 1 .5 第 5 章─回溯法 1 .5.1 练习题 . 回溯法在问题的解空间树中,按( )策略,从根结点出发搜索解空间树。 A. 广度优先 B. 活结点优先 C. 扩展结点优先 D. 深度优先 . 关于回溯法以下叙述中不正确的是( )。 1 2 A. 回溯法有“通用解题法”之称,它可以系统地搜索一个问题的所有解或任意解 B. 回溯法是一种既带系统性又带有跳跃性的搜索算法 C. 回溯算法需要借助队列这种结构来保存从根结点到当前扩展结点的路径 D. 回溯算法在生成解空间的任一结点时,先判断该结点是否可能包含问题的解,如果 肯定不包含,则跳过对该结点为根的子树的搜索,逐层向祖先结点回溯 3. 回溯法的效率不依赖于下列哪些因素( )。 A. 确定解空间的时间 B. 满足显约束的值的个数 D. 计算限界函数的时间 C. 计算约束函数的时间 4 . 下面( )函数是回溯法中为避免无效搜索采取的策略。 A. 递归函数 B. 剪枝函数 C. 随机数函数 D. 搜索函数 5 6 . 回溯法的搜索特点是什么? . 用回溯法解 0/1 背包问题时,该问题的解空间是何种结构?用回溯法解流水作业调 度问题时,该问题的解空间是何种结构? . 对于递增序列 a []={1 , 2 , 3 , 4 , 5} ,采用例 5.4 的回溯法求全排列,以 1 、 2 开头 的排列一定最先出现吗?为什么? 考虑 n 皇后问题,其解空间树为由 1 、 2 、…、 n 构成的 n ! 种排列所组成。现用回 7 8 . 3 8 第 1 章 概论 溯法求解,要求: ( ( ( 1 )通过解搜索空间说明 n =3 时是无解的。 2 )给出剪枝操作。 3 )最坏情况下在解空间树上会生成多少个结点?分析算法的时间复杂度。 设计一个算法求解简单装载问题,设有一批集装箱要装上一艘载重量为 W 的轮 9 . 船,其中编号为 i ( 0 ≤ i ≤ n - 1 )的集装箱的重量为 w i 。现要从 n 个集装箱中选出若干装上 轮船,使它们的重量之和正好为 W 。如果找到任一种解返回 true ,否则返回 false 。 1 0. 给定若干个正整数 a 0 、 a 0 、…、 a n - 1 ,从中选出若干数,使它们的和恰好为 k , 要求找选择元素个数最少的解。 1. 设计求解有重复元素的排列问题的算法,设有 n 个元素 a []={ a 0 , a 1 ,…, a n - 1 ) , 其中可能含有重复的元素,求这些元素的所有不同排列。如 a []={1 , 1 , 2} ,输出结果是 1 , 1 , 2) ,( 1 , 2 , 1 ),( 2 , 1 , 1 )。 2. 采用递归回溯法设计一个算法求 1 ~ n 的 n 个整数中取出 m 个元素的排列,要求每个 元素最多只能取一次。例如, n =3 , m =2 的输出结果是( 1 , 2 ),( 1 , 3 ),( 2 , 1 ), 2 , 3 ),( 3 , 1 ),( 3 , 2 )。 3. 对于 n 皇后问题,有人认为当 n 为偶数时,其解具有对称性,即 n 皇后问题的解个 数恰好为 n /2 皇后问题的解个数的 2 倍,这个结论正确吗?请编写回溯法程序对 n =4 、 6 、 、 10 的情况进行验证。 4. 给定一个无向图,由指定的起点前往指定的终点,途中经过所有其他顶点且只经 1 ( 1 ( 1 8 1 过一次,称为哈密顿路径,闭合的哈密顿路径称作哈密顿回路( Hamiltonian cycle )。设计 一个回溯算法求无向图的所有哈密顿回路。 1.5.2 练习题参考答案 1 2 . 答: D 。 . 答: 回溯算法是采用深度优先遍历的,需要借助系统栈结构来保存从根结点到当 前扩展结点的路径。答案为 C 。 3 4 5 . 答: 回溯法解空间是虚拟的,不必确定整个解空间。答案为 A 。 . 答: B 。 . 答: 回溯法在解空间树中采用深度优先遍历方式进行解搜索,即用约束条件和限 界函数考察解向量元素 x [ i ] 的取值,如果 x [ i ] 是合理的就搜索 x [ i ] 为根结点的子树,如果 x [ i ] 取完了所有的值,便回溯到 x [ i - 1] 。 6 . 答: 用回溯法解 0/1 背包问题时,该问题的解空间是子集树结构。用回溯法解流水 作业调度问题时,该问题的解空间是排列树结构。 答: 是的。对应的解空间是一棵排列树,如图 1.33 所示给出前面 3 层部分,显然 最先产生的排列是从 G 结点扩展出来的叶子结点,它们就是以 1 、 2 开头的排列。 7 . 3 9 算法设计 A 5 1 3 2 4 B C D E F 2 5 3 4 G H I J 图 1.33 部分解空间树 8 . 答: ( 1 ) n =3 时的解搜索空间如图 1.34 所示,不能得到任何叶子结点,所有无 解。 ( ( 2 )剪枝操作是任何两个皇后不能同行、同列和同两条对角线。 3 )最坏情况下每个结点扩展 n 个结点,共有 n n 个结点,算法的时间复杂度为 O( n n ) 。 (*,*,*) ( 1,*,*) 1,3,*) (2,*,*) (3,*,*) (3,1,*) ( 图 1.34 3 皇后问题的解搜索空间 9 . 解: 用数组 w [0.. n - 1] 存放 n 个集装箱的重量,采用类似判断子集和是否存在解的 方法求解。对应完整的求解程序如下: # # / include <stdio.h> define MAXN 20 / 问题表示 // 最多集装箱个数 int n=5,W; int w[]={2,9,5,6,3}; int count; // 全局变量,累计解个数 // 求解简单装载问题 void dfs(int tw,int rw,int i) { if (i>=n) // 找到一个叶子结点 { if (tw==W) count++; // 找到一个满足条件的解 , 输出它 } else { // 尚未找完 rw-=w[i]; if (tw+w[i]<=W) dfs(tw+w[i],rw,i+1); if (tw+rw>=W) dfs(tw,rw,i+1); // 求剩余的集装箱重量和 // 左孩子结点剪枝:选取满足条件的集装箱 w[i] // 选取第 i 个集装箱 // 右孩子结点剪枝:剪除不可能存在解的结点 // 不选取第 i 个集装箱 , 回溯 } } bool solve() // 判断简单装载问题是否存在解 4 0 第 1 章 概论 { count=0; int rw=0; for (int j=0;j<n;j++) rw+=w[j]; // 求所有集装箱重量和 rw //i 从 0 开始 dfs(0,rw,0); if (count>0) return true; else return false; } void main() { printf(" 求解结果 \n"); W=4; printf(" W=%d 时 %s\n",W,(solve()?" 存在解 ":" 没有解 ")); W=10; printf(" W=%d 时 %s\n",W,(solve()?" 存在解 ":" 没有解 ")); W=12; printf(" W=%d 时 %s\n",W,(solve()?" 存在解 ":" 没有解 ")); W=21; printf(" W=%d 时 %s\n",W,(solve()?" 存在解 ":" 没有解 ")); } 本程序执行结果如图 1.35 所示。 图 1.35 程序执行结果 1 0. 解: 这是一个典型的解空间为子集树的问题,采用子集树的回溯算法框架。当找 到一个解后通过选取的元素个数进行比较求最优解 minpath 。对应的完整程序如下: # # include <stdio.h> include <vector> using namespace std; / / 问题表示 int a[]={1,2,3,4,5}; int n=5,k=9; // 设置为全局变量 // 存放最优解 vector<int> minpath; / / 求解结果表示 int minn=n; // 最多选择 n 个元素 // 输出一个解 void disppath() { } printf(" 选择的元素 :"); for (int j=0;j<minpath.size();j++) printf("%d ",minpath[j]); printf(" 元素个数 =%d\n",minn); 4 1 算法设计 void dfs(vector<int> path,int sum,int start) // 求解算法 { if (sum==k) // 如果找到一个解,不一定到叶子结点 { if (path.size()<minn) { minn=path.size(); minpath=path; } return; } if (start>=n) return; // 全部元素找完,返回 // 不选择 a[start] // 选择 a[start] dfs(path,sum,start+1); path.push_back(a[start]); dfs(path,sum+a[start],start+1); } void main() { vector<int> path; //path 存放一个子集 dfs(path,0,0); printf(" 最优解 :\n"); disppath(); } 上述程序的执行结果如图 1.36 所示。 图 1.36 程序执行结果 1 1. 解: 在回溯法求全排列的基础上,增加元素的重复性判断。例如,对于 a []={1 , 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 ] 的位置,如果出现,对应的排 列已经在前面求出了。对应的完整程序如下: # include <stdio.h> bool ok(int a[],int i,int j) //ok 用于判别重复元素 { if (j>i) { for(int k=i;k<j;k++) if (a[k]==a[j]) return false; } return true; } void swap(int &x,int &y) // 交换两个元素 { int tmp=x; x=y; y=tmp; } void dfs(int a[],int n,int i) { if (i==n) // 求有重复元素的排列问题 4 2 第 1 章 概论 { for(int j=0;j<n;j++) printf("%3d",a[j]); printf("\n"); } else { for (int j=i;j<n;j++) if (ok(a,i,j)) // 选取与 a[i..j-1] 不重复的元素 a[j] { swap(a[i],a[j]); dfs(a,n,i+1); swap(a[i],a[j]); } } } void main() { int a[]={1,2,1,2}; int n=sizeof(a)/sizeof(a[0]); printf(" 序列 ("); for (int i=0;i<n-1;i++) printf("%d ",a[i]); printf("%d) 的所有不同排列 :\n",a[n-1]); dfs(a,n,0); } 上述程序的执行结果如图 1.37 所示。 图 1.37 程序执行结果 1 2. 解: 采用求全排列的递归框架。选取的元素个数用 i 表示( i 从 1 开始),当 i > m 时达到一个叶子结点,输出一个排列。为了避免重复,用 used 数组实现, used[ i ]=0 表示 没有选择整数 i , used[ i ]=1 表示已经选择整数 i 。对应的完整程序如下: # # # # include <stdio.h> include <string.h> define MAXN 20 define MAXM 10 int m,n; int x[MAXM]; bool used[MAXN]; void dfs(int i) //x[1..m] 存放一个排列 // 求 n 个元素中 m 个元素的全排列 { if (i>m) for (int j=1;j<=m;j++) printf(" %d",x[j]); printf("\n"); { // 输出一个排列 4 3 算法设计 } else { for (int j=1;j<=n;j++) { if (!used[j]) { used[j]=true; // 修改 used[i] x[i]=j; //x[i] 选择 j dfs(i+1); // 继续搜索排列的下一个元素 // 回溯:恢复 used[i] used[j]=false; } } } } void main() { n=4,m=2; memset(used,0,sizeof(used)); printf("n=%d,m=%d 的求解结果 \n",n,m); dfs(1); // 初始化为 0 //i 从 1 开始 } 上述程序的执行结果如图 1.38 所示。 图 1.38 程序执行结果 1 3. 解: 这个结论不正确。验证程序如下: # # # include <stdio.h> include <stdlib.h> define MAXN 10 int q[MAXN]; bool place(int i) // 测试第 i 行的 q[i] 列上能否摆放皇后 //j=1 ~ i-1 是已放置了皇后的行 { int j=1; if (i==1) return true; while (j<i) { if ((q[j]==q[i]) || (abs(q[j]-q[i])==abs(j-i))) / 该皇后是否与以前皇后同列,位置 (j,q[j]) 与 (i,q[i]) 是否同对角线 return false; / j++; } return true; } 4 4 第 1 章 概论 int Queens(int n) // 求 n 皇后问题的解个数 { int count=0,k; int i=1; // 计数器初始化 //i 为当前行 q[1]=0; //q[i] 为皇后 i 的列号 while (i>0) { q[i]++; // 移到下一列 while (q[i]<=n && !place(i)) q[i]++; if (q[i]<=n) { if (i==n) count++; // 找到一个解计数器 count 加 1 else { i++;; q[i]=0; } } else i--; // 回溯 } return count; } void main() { printf(" 验证结果如下 :\n"); for (int n=4;n<=10;n+=2) if (Queens(n)==2*Queens(n/2)) printf(" n=%d: 正确 \n",n); else printf(" n=%d: 错误 \n",n); } 上述程序的执行结果如图 1.39 所示。从执行结果看出结论是不正确的。 图 1.39 程序执行结果 1 4. 解: 假设给定的无向图有 n 个顶点(顶点编号从 0 到 n - 1 ),采用邻接矩阵数组 a ( 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 皇后问 题的非递归回溯框架类似)的完整程序如下: # # include <stdio.h> define MAXV 10 4 5 算法设计 / / 求解问题表示 int n=5; // 图中顶点个数 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}}; // 邻接矩阵数组 / / 求解结果表示 int x[MAXV]; int count; void dispasolution() // 输出一个解路径 { for (int i=0;i<=n-1;i++) printf("(%d,%d) ",x[i],x[i+1]); printf("\n"); } bool valid(int i) // 判断顶点第 i 个顶点 x[i] 的有效性 //x[i-1] 到 x[i] 没有边,返回 false { if (a[x[i-1]][x[i]]!=1) return false; for (int j=0;j<=i-1;j++) if (x[i]==x[j]) return false; // 顶点 i 重复出现,返回 false return true; } void Hamiltonian(int v) // 求从顶点 v 出发的哈密顿回路 // 存放起点 { x[0]=v; int i=1; x[i]=-1; while (i>0) // 从顶点 -1+1=0 开始试探 // 尚未回溯到头,循环 { x[i]++; while (!valid(i) && x[i]<n) x[i]++; // 试探一个顶点 x[i] if (x[i]<n) // 找到一个有效的顶点 x[i] // 达到叶子结点 { if (i==n-1) { if (a[x[i]][v]==1) { x[n]=v; // 找到一个解 printf(" 第 %d 个解 : ",count++); dispasolution(); } } else { i++; x[i]=-1; } } else i--; // 回溯 } } void main() { printf(" 求解结果 \n"); for (int v=0;v<n;v++) { printf(" 从顶点 %d 出发的哈密顿回路 :\n",v); count=1; 4 6 第 1 章 概论 Hamiltonian(v); // 从顶点 v 出发 } } 上述程序对如图 1.40 所示的无向图求从每个顶点出发的哈密顿回路,程序执行结果 如图 1.41 所示。 1 3 2 0 4 图 1.40 一个无向图 图 1.41 程序执行结果 1 .6 第 6 章─分枝限界法 1 .6.1 练习题 . 分枝限界法在问题的解空间树中,按( )策略,从根结点出发搜索解空间树。 A. 广度优先 B. 活结点优先 C. 扩展结点优先 D. 深度优先 . 常见的两种分枝限界法为( )。 A. 广度优先分枝限界法与深度优先分枝限界法 1 2 4 7 算法设计 B. 队列式( FIFO )分枝限界法与堆栈式分枝限界法 C. 排列树法与子集树法 D. 队列式( FIFO )分枝限界法与优先队列式分枝限界法 3 . 分枝限界法求解 0/1 背包问题时,活结点表的组织形式是( )。 A. 小根堆 B. 大根堆 C. 栈 D. 数组 4 . 采用最大效益优先搜索方式的算法是( )。 A. 分支界限法 B. 动态规划法 C. 贪心法 D. 回溯法 D. 随机 5 . 优先队列式分枝限界法选取扩展结点的原则是( )。 A. 先进先出 B. 后进先出 C. 结点的优先级 . 简述分枝限界法的搜索策略。 有一个 0/1 背包问题,其中 n =4 ,物品重量为( 4 , 7 , 5 , 3 ),物品价值为( 40 , 6 7 . 4 2 , 25 , 12 ),背包最大载重量 W =10 ,给出采用优先队列式分枝限界法求最优解的过程。 . 有一个流水作业调度问题, n =4 , a []={5 , 10 , 9 , 7} , b []={7 , 5 , 9 , 8} ,给出采 用优先队列式分枝限界法求一个解的过程。 有一个含 n 个顶点(顶点编号为 0 ~ n - 1 )的带权图,采用邻接矩阵数组 A 表示, 8 9 . 采用分枝限界法求从起点 s 到目标点 t 的最短路径长度,以及具有最短路径长度的路径条 数。 1 0. 采用优先队列式分枝限界法求解最优装载问题。给出以下装载问题的求解过程和 结果: n =5 ,集装箱重量为 w = ( 5 , 2 , 6 , 4 , 3 ),限重为 W =10 。在装载重量相同时,最 优装载方案是集装箱个数最少的方案。 1.6.2 练习题参考答案 1 2 3 4 5 6 . 答: A 。 . 答: D 。 . 答: B 。 . 答: A 。 . 答: C 。 . 答: 分枝限界法的搜索策略是广度优先遍历,通过限界函数可以快速找到一个解 或者最优解。 7 . 答: 求解过程如下: ( ( 1 )根结点 1 进队,对应结点值: e.i=0 , e.w=0 , e.v=0 , e.ub=76 , x :[0 , 0 , 0 , 0] 。 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] 。 ( 3 )出队结点 2 :左孩子超重;右孩子结点 4 进队,对应结点值: e.no=4 , e.i=2 , e.w=4 , e.v=40 , e.ub=69 , x :[1 , 0 , 0 , 0] 。 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] 。 4 8 第 1 章 概论 ( ( 5 )出队结点 5 :产生一个解, maxv= 65 , bestx:[1 , 0 , 1 , 0] 。 6 )出队结点 3 :左孩子结点 8 进队,对应结点值: e.no=8 , e.i=2 , e.w=7 , e.v=42 , e.ub=57 , x :[0 , 1 , 0 , 0] ;右孩子结点 9 被剪枝。 ( ( ( 7 )出队结点 8 :左孩子超重;右孩子结点 10 被剪枝。 8 )出队结点 6 :左孩子结点 11 超重;右孩子结点 12 被剪枝。 9 )队列空,算法结束,产生的最优解: maxv= 65 , bestx:[1 , 0 , 1 , 0] 。 8 . 答: 求解过程如下: ( 1 )根结点 1 进队,对应结点值: e.i=0 , e.f1=0 , e.f2=0 , e.lb=29 , x : [0 , 0 , 0 , 0 ] 。 ( 2 )出队结点 1 :扩展结点如下: 进队( 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] 。 ( 3 )出队结点 2 :扩展结点如下: 进队( 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] 。 ( 4 )出队结点 8 :扩展结点如下: 进队( 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] 。 ( 5 )出队结点 10 ,扩展一个 j =2 的子结点,有 e.i=4 ,到达叶子结点,产生的一个解 是 e.f1=31 , e.f2=36 , e.lb=31 , x =[1 , 4 , 3 , 2] 。 该解对应的调度方案是:第 1 步执行作业 1 ,第 2 步执行作业 4 ,第 3 步执行作业 3 ,第 4 步执行作业 2 ,总时间 =36 。 解: 采用优先队列式分枝限界法求解,队列中结点的类型如下: 9 . struct NodeType { int vno; // 顶点的编号 int length; // 当前结点的路径长度 bool operator<(const NodeType &s) const // 重载 < 关系函数 { return length>s.length; } //length 越小越优先 } ; 从顶点 s 开始广度优先搜索,找到目标点 t 后比较求最短路径长度及其路径条数。对 应的完整程序如下: # # include <stdio.h> include <queue> using namespace std; # # / define MAX 11 define INF 0x3f3f3f3f / 问题表示 int A[MAX][MAX]={ // 一个带权有向图 4 9 算法设计 { { { { { 0 , 1 , 4 , INF , INF} , INF , 0 , INF , 1 , 5} , INF , INF , 0 , INF , 1} , INF , INF , 2 , 0 , 3} , INF , INF , INF , INF , INF} }; int n=5; / / 求解结果表示 int bestlen=INF; int bestcount=0; struct NodeType // 最优路径的路径长度 // 最优路径的条数 { int vno; // 顶点的编号 int length; // 当前结点的路径长度 bool operator<(const NodeType &s) const // 重载 > 关系函数 { return length>s.length; } //length 越小越优先 } ; void solve(int s , int t) // 求最短路径问题 // 定义 2 个结点 { NodeType e , e1; priority_queue<NodeType> qu; e.vno=s; // 定义一个优先队列 qu // 构造根结点 e.length=0; qu.push(e); // 根结点进队 while (!qu.empty()) // 队不空循环 { e=qu.top(); qu.pop(); if (e.vno==t) // 出队结点 e 作为当前结点 //e 是一个叶子结点 // 比较找最优解 { if (e.length<bestlen) { bestcount=1; bestlen=e.length; // 保存最短路径长度 } else if (e.length==bestlen) bestcount++; } else { //e 不是叶子结点 for (int j=0; j<n; j++) // 检查 e 的所有相邻顶点 if (A[e.vno][j]!=INF && A[e.vno][j]!=0) // 顶点 e.vno 到顶点 j 有边 { if (e.length+A[e.vno][j]<bestlen) // 剪枝 { e1.vno=j; e1.length=e.length+A[e.vno][j]; qu.push(e1); // 有效子结点 e1 进队 } } } } } void main() int s=0 , t=4; solve(s , t); if (bestcount==0) { printf(" 顶点 %d 到 %d 没有路径 \n" , s , t); else { printf(" 顶点 %d 到 %d 存在路径 \n" , s , t); 5 0 第 1 章 概论 printf(" 最短路径长度 =%d ,条数 =%d\n" , bestlen , bestcount); / / 输出: 5 3 } } 上述程序的执行结果如图 1.39 所示。 图 1.39 程序执行结果 1 0. 解: 采用优先队列式分枝限界法求解。设计优先队列 priority_queue<NodeType> ,并设计优先队列的关系比较函数 Cmp ,指定按结点的 ub 值进 行比较,即 ub 值越大的结点越先出队。对应的完整程序如下: # # include <stdio.h> include <queue> using namespace std; # / define MAXN 21 // 最多的集装箱数 / 问题表示 int n=5; int W=10; int w[]={0,5,2,6,4,3}; // 集装箱重量 , 不计下标 0 的元素 / / 求解结果表示 int bestw=0; // 存放最大重量 , 全局变量 // 存放最优解 , 全局变量 int bestx[MAXN]; int Count=1; // 搜索空间中结点数累计 , 全局变量 typedef struct { int no; int i; // 结点编号 // 当前结点在解空间中的层次 // 当前结点的总重量 // 当前结点包含的解向量 // 上界 int w; int x[MAXN]; int ub; } NodeType; struct Cmp // 队列中关系比较函数 { bool operator()(const NodeType &s,const NodeType &t) { return (s.ub<t.ub) || (s.ub==t.ub && s.x[0]>t.x[0]); / /ub 越大越优先 , 当 ub 相同时 x[0] 越小越优先 } } ; void bound(NodeType &e) // 计算分枝结点 e 的上界 //r 为剩余集装箱的重量 { int i=e.i+1; int r=0; while (i<=n) { r+=w[i]; i++; } e.ub=e.w+r; 5 1 算法设计 } void Loading() // 求装载问题的最优解 { NodeType e,e1,e2; // 定义 3 个结点 priority_queue<NodeType,vector<NodeType>,Cmp > qu; // 定义一个优先队列 qu e.no=Count++; e.i=0; // 设置结点编号 // 根结点置初值 , 其层次计为 0 e.w=0; for (int j=0; j<=n; j++) e.x[j]=0; // 初始化根结点的解向量 bound(e); // 求根结点的上界 // 根结点进队 qu.push(e); while (!qu.empty()) // 队不空循环 { e=qu.top(); qu.pop(); if (e.i==n) // 出队结点 e 作为当前结点 //e 是一个叶子结点 { if ((e.w>bestw) || (e.w==bestw && e.x[0]<bestx[0]))// 比较找最优解 { bestw=e.w; // 更新 bestw for (int j=0;j<=e.i;j++) bestx[j]=e.x[j]; // 复制解向量 e.x->bestx } } else { //e 不是叶子结点 if (e.w+w[e.i+1]<=W) // 检查左孩子结点 // 设置结点编号 // 建立左孩子结点 { e1.no=Count++; e1.i=e.i+1; e1.w=e.w+w[e1.i]; for (int j=0; j<=e.i; j++) e1.x[j]=e.x[j]; // 复制解向量 e.x->e1.x e1.x[e1.i]=1; // 选择集装箱 i e1.x[0]++; bound(e1); qu.push(e1); // 装入集装箱数增 1 // 求左孩子结点的上界 // 左孩子结点进队 } e2.no=Count++; e2.i=e.i+1; e2.w=e.w; // 设置结点编号 // 建立右孩子结点 for (int j=0; j<=e.i; j++) // 复制解向量 e.x->e2.x e2.x[j]=e.x[j]; e2.x[e2.i]=0; bound(e2); // 不选择集装箱 i // 求右孩子结点的上界 if (e2.ub>bestw) qu.push(e2); // 若右孩子结点可行 , 则进队 , 否则被剪枝 } } } void disparr(int x[],int len) // 输出一个解向量 // 输出最优解 { for (int i=1;i<=len;i++) printf("%2d",x[i]); } void dispLoading() { printf(" X=["); 5 2 第 1 章 概论 disparr(bestx,n); printf("], 装入总价值为 %d\n",bestw); } void main() { Loading(); printf(" 求解结果 :\n"); dispLoading(); // 输出最优解 } 上述程序的执行结果如图 1.40 所示。 图 1.40 程序执行结果 1 .7 第 7 章─贪心法 1 .7.1 练习题 . 下面是贪心算法的基本要素的是( )。 A. 重叠子问题 B. 构造最优解 C. 贪心选择性质 . 下面问题( )不能使用贪心法解决。 A. 单源最短路径问题 B. n 皇后问题 采用贪心算法的最优装载问题的主要计算量在于将集装箱依其重量从小到大排 序,故算法的时间复杂度为( )。 A.O( n ) B.O( n 2 ) . 关于 0/ 1 背包问题以下描述正确的是( )。 1 D. 定义最优解 2 C. 最小花费生成树问题 D. 背包问题 3 . C.O( n 3 ) D.O( n log 2 n ) 4 A. 可以使用贪心算法找到最优解 B. 能找到多项式时间的有效算法 C. 使用教材介绍的动态规划方法可求解任意 0 - 1 背包问题 D. 对于同一背包与相同的物品,做背包问题取得的总价值一定大于等于做 0/1 背包问 题 5 . 一棵哈夫曼树共有 215 个结点,对其进行哈夫曼编码,共能得到( )个不同的码 字。
更多精彩