算法设计与分析 - 李春葆 - 第二版 - pdf->word v1

   1 1.11 章─概论
   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.    证明以下关系成立:
  131)10n2-2n=(n2)
  142)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*318=2*3*311=11。设计一个算法求 n 这样分解后各个质因数出现的次数,采用 vector 向量存放结果。
  21 12.    有一个整数序列,所有元素均不相同,设计一个算法求相差最小的元素对的个数。如序列 4123 的相差最小的元素对的个数是 3,其元素对是(12),(23),
  2234)。
  23 13.    有一个 map<stringint>容器,其中已经存放了较多元素。设计一个算法求出其中重复的 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  #include  
  34 bool isPrime1(int n) //方法 1 
  35 {    for (int i=2;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)。
  522)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  #include  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) 
  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 所示。
  961.1    程序执行结果
  97 8.    解:采用前后字符判断方法,对应的程序如下:
  98 #include  #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 所示。
 1181.2    程序执行结果
 119 9.    解:先将 a 中元素递增排序,然后从两端开始进行判断。对应的程序如下:
 120 #include  #include  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//区间中存在两个或者以上元素 
 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 所示。
 1511.3 程序执行结果
 152 10.    解:采用集合 set<int>存储整数序列,集合中元素默认是递增排序的,再采用二路归并算法求它们的交集。对应的程序如下:
 153 #include  #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 所示。
 1921.4 程序执行结果
 193 11.    解:对于正整数 n,从 i=2 开始查找其质因数,ic 记录质因数 i 出现的次数,当找到这样质因数后,将(i,ic)作为一个元素插入到 vector 容器 v 中。最后输出 v。对应的算法如下:
 194 #include  #include  using namespace std; 
 195 struct NodeType        //vector 向量元素类型 
 196 {    int p;            //质因数 
 197      int pc;        //质因数出现次数 
 198 }; 
 199 void solve(int n,vector &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 &v)    //输出 v 
 220 {    vector::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 v; 
 227      int n=100; 
 228      printf("n=%d\n",n); 
 229      solve(n,v); 
 230      disp(v); 
 231 } 
 232 上述程序的执行结果如图 1.5 所示。
 2331.5 程序执行结果
 234 12.    解:先递增排序,再求相邻元素差,比较求最小元素差,累计最小元素差的个数。对应的程序如下:
 235 #include  #include  #include  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) 
 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 
 2591.6 程序执行结果
 260 13.    解:对于 map<stringint>容器 mymap,设计另外一个 map<intint>容器 tmap, 将前者的 value 作为后者的关键字。遍历 mymap,累计 tmap 中相同关键字的次数。一个参考程序及其输出结果如下:
 261 #include  #include  #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<intint>::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 所示。
 2781.7 程序执行结果
 279 14.    解:采用 map<intint>容器 mymap 存放求解结果,第一个分量存放质因数,第二个分量存放质因数出现次数。对应的程序如下:
 280 #include  #include  
 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 所示。
 3081.8 程序执行结果
 309 15.    解:栈容器不能顺序遍历,为此创建一个临时 tmpst 栈,将 st 的 k 个元素出栈并进栈到 tmpst 中,再出栈 tmpst 一次得到第 k 个元素,最后将栈 tmpst 的所有元素出栈并进栈到 st 中。对应的程序如下:
 310 #include  #include  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//出栈 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 所示。
 3501.9    程序执行结果
 351 1.22 章─递归算法设计技术
 352 1.2.1    练习题
 353 1.    什么是直接递归和间接递归?消除递归一般要用到什么数据结构?
 354 2.    分析以下程序的执行结果:
 355 #include  
 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  
 4244 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 
 4571.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 11 478  
 479 
 480 5 n
 481  
 482 
 483 11 484  
 485 
 486 5 n
 487  
 488 
 489 11 490  
 491 
 492 5 n
 493  
 494 T1(n)=
 495  
 496        ≈
 497  
 498  = 
 499  
 500         
 501         
 502 11 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  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 所示。
 5461.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  #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 所示。
 5741.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 所示。
 5951.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 所示。
 6181.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 所示。
 6471.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 所示。
 6721.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 所示。
 7031.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 所示。
 7321.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 
 7631.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 所示。
 7951.20 程序执行结果
 796  
 797 1.33 章─分治法
 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      return1; 
 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 return1; 
 829 } 
 830 C.
 831 int binarySearch (int a[], int n, int x) 
 832 {    int low=0, high=n-1; 
 833      while(low1) 
 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 return1; 
 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      return1; 
 858 } 
 859 4.    快速排序算法是根据分治策略来设计的,简述其基本思想。
 860 5.    假设含有 n 个元素的待排序的数据 a 恰好是递减排列的,说明调用 QuickSort(a, 0,n-1)递增排序的时间复杂度为 O(n2)。
 861 6.    以下哪些算法采用分治策略:
 8621)    堆排序算法
 8632)    二路归并排序算法
 8643)    折半查找算法
 8654)    顺序查找算法
 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≤ii。设计一个算法以 O(log2n)时间找到这个下标 i。
 877 16.    请你模仿二分查找过程设计一个三分查找算法。分析其时间复杂度。
 878 17.    对于大于 1 的正整数 n,可以分解为 n=x1*x2*…*xm,其中 xi≥2。例如,n=12 时有 8 种 不 同 的 分 解 式 :12=1212=6*212=4*312=3*412=3*2*212=2*612=2*3*212=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[]={12345}为例说明。选项 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.    答:适合并行计算的问题通常表现出以下特征:
 8911)    将工作分离成离散部分,有助于同时解决。例如,对于分治法设计的串行算法,可以将各个独立的子问题并行求解,最后合并成整个问题的解,从而转化为并行算法。
 8922)    随时并及时地执行多个程序指令。
 8933)    多计算资源下解决问题的耗时要少于单个计算资源下的耗时。
 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  
 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 所示。
 9301.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  
 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 所示。
 9601.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 所示。
10021.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 所示。
10281.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)    当 xdata
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 (xdata) 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 所示。
10581.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  
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]//这样的元素只能在右区间中出现 
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 所示。
10901.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  
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 所示。
11291.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=44=2*2
1133  
1134 
1135 f(6)=3,分解式为:6=66=2*36=3*2,即 f(6)=f(1)+f(2)+f(3)
1136 以此类推,可以看出 f(n)为 n 的所有因数的不同分解式个数之和,即 f(n)=
1137 ∑    (/)。对应的程序如下:
1138 #include  #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 所示。
11551.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//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.44 章─蛮力法
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),若 iaj,则就为一个逆序对。例如数组(31452)的逆序对有<31>,<32>,<42>,<52>。设计一个算法采用蛮力法求 A 中逆序对的个数即逆序数。
1193 5.    对于给定的正整数 n(n>1), 采用蛮力法求 1!+2!+…+n!,并改进该算法提高效率。
1194 6.    有一群鸡和一群兔,它们的只数相同,它们的脚数都是三位数,且这两个三位数的各位数字只能是 012345。设计一个算法用蛮力法求鸡和兔的只数各是多
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=424+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  #include  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) 
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;i1;i++) 
1236           for (int j=i+1;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≤50≤b,c,e,f≤5,采
12666 重循环,求出鸡只数 x1=y/2(y 是 2 的倍数),兔只数 x2=z/4(z 是 4 的倍数),当
1267 x1=x2 时输出结果。对应的程序如下:
1268 #include  
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 所示。
12961.29  程序执行结果
1297 7.    解:设该三位数为 x=abc,有 1≤a≤90≤b,c≤9,满足 c>a,a>b, a+b+c=a*b*c。对应的程序如下:
1298 #include  
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 所示。
13141.30 程序执行结果
1315 8.    解:设该年级的人数为 x,租船数为 y。因为每只船坐 10 人正好多出 2 个座位,则x=10*y-2;因为每只船多坐 2 人即 12 人时可少租 1 只船(没有说恰好全部座位占满),有 x+z=12*(y-1),z 表示此时空出的座位,显然 z<12。让 y 从 1100(实际上 y 取更大范围的结果是相同的)、z 从 011 枚举,求出最大的 x 即可。对应的程序如下:
1316 #include  
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 所示。
13301.31 程序执行结果
1331 9.    解:采用蛮力法求出一个正整数 n 的各位数字和 sum1,以及 n 的所有质因数的数字和 sum2,若 sum1=sum2,即为 Smitch 数。从用户输入的 n 开始枚举,若是 Smitch
1332 数,输出,本次结束,否则 n++继续查找大于 n 的最小 Smitch 数。对应的完整程序如下:
1333 #include  
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  #define MAXN 51 
1373 //问题表示int n; 
1374 char board[MAXN][MAXN]; 
1375 int getMaxArea()                //蛮力法求解算法 
1376 {    int maxArea=0; 
1377      for (int j=0; j) 
1378      {    int countj=1; 
1379           for (int i=1; 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) 
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  #include  using namespace std; 
1401 vectorint> > ps;                        //存放全排列 
1402 void Insert(vector<int> s,int a[],int i,vectorint> > &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 {    vectorint> > ps1;                       //临时存放子排列 
1415      vectorint> >::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//循环添加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 {    vectorint> >::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 所示。
14461.32  程序执行结果
1447 1.55 章─回溯法
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[]={12345},采用例 5.4 的回溯法求全排列,以 12 开头的排列一定最先出现吗?为什么?
1462 8.    考虑 n 皇后问题,其解空间树为由 12、…、n 构成的 n!种排列所组成。现用回
1463  
1464 溯法求解,要求:
14651)    通过解搜索空间说明 n=3 时是无解的。
14662)    给出剪枝操作。
14673)    最坏情况下在解空间树上会生成多少个结点?分析算法的时间复杂度。
1468 9.    设计一个算法求解简单装载问题,设有一批集装箱要装上一艘载重量为 W 的轮船,其中编号为 i(0≤i≤n-1)的集装箱的重量为 wi。现要从 n 个集装箱中选出若干装上轮船,使它们的重量之和正好为 W。如果找到任一种解返回 true,否则返回 false1469 10.    给定若干个正整数a0、a0 、…、an-1 ,从中选出若干数,使它们的和恰好为k, 要求找选择元素个数最少的解。
1470 11.    设计求解有重复元素的排列问题的算法,设有 n 个元素 a[]={a0,a1,…,an-1), 其中可能含有重复的元素,求这些元素的所有不同排列。如 a[]={112},输出结果是
1471112),(121),(211)。
1472 12.    采用递归回溯法设计一个算法求1~n的n个整数中取出m个元素的排列,要求每个元素最多只能取一次。例如,n=3,m=2的输出结果是(12),(13),(21),
147323),(31),(32)。
1474 13.    对于n皇后问题,有人认为当n为偶数时,其解具有对称性,即n皇后问题的解个数恰好为n/2皇后问题的解个数的2倍,这个结论正确吗?请编写回溯法程序对n=461475 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 结点扩展出来的叶子结点,它们就是以 12 开头的排列。
1485  
1486 
1487  
1488 
14891.33 部分解空间树
1490 8.    答:(1)n=3 时的解搜索空间如图 1.34 所示,不能得到任何叶子结点,所有无解。
14912)    剪枝操作是任何两个皇后不能同行、同列和同两条对角线。
14923)    最坏情况下每个结点扩展 n 个结点,共有 nn 个结点,算法的时间复杂度为
1493 O(nn)。
1494 
1495 
1496 
14971.34 3 皇后问题的解搜索空间
1498 9.    解:用数组 w[0..n-1]存放 n 个集装箱的重量,采用类似判断子集和是否存在解的方法求解。对应完整的求解程序如下:
1499 #include  
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)   
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 所示。
15441.35 程序执行结果
1545 10.    解:这是一个典型的解空间为子集树的问题,采用子集树的回溯算法框架。当找到一个解后通过选取的元素个数进行比较求最优解 minpath。对应的完整程序如下:
1546 #include  #include  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) 
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 所示。
15811.36    程序执行结果
1582 11.    解:在回溯法求全排列的基础上,增加元素的重复性判断。例如,对于 a[]={11583 12},不判断重复性时输出(112),(121),(112),(121),
1584211),(211),共 6 个,有 3 个是重复的。重复性判断是这样的,对于在扩展 a[i]时,仅仅将与 a[i..j-1]没有出现的元素 a[j]交换到 a[i]的位置,如果出现,对应的排列已经在前面求出了。对应的完整程序如下:
1585 #include  
1586 bool ok(int a[],int i,int j)    //ok用于判别重复元素 
1587 {    if (j>i) 
1588      {    for(int k=i;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) 
1609      printf("%3d",a[j]); printf("\n"); 
1610      else     
1611      {     for (int j=i;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;i1;i++) 
1624           printf("%d ",a[i]); 
1625      printf("%d)的所有不同排列:\n",a[n-1]); 
1626      dfs(a,n,0); 
1627 } 
1628 上述程序的执行结果如图 1.37 所示。
16291.37 程序执行结果
1630 12.    解:采用求全排列的递归框架。选取的元素个数用 i 表示(i 从 1 开始),当 i>m时达到一个叶子结点,输出一个排列。为了避免重复,用 used 数组实现,used[i]=0 表示没有选择整数 i,used[i]=1 表示已经选择整数 i。对应的完整程序如下:
1631 #include  #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 所示。
16691.38    程序执行结果
1670 13.    解:这个结论不正确。验证程序如下:
1671 #include  #include  #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//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所示。从执行结果看出结论是不正确的。
17181.39 程序执行结果
1719 14.    解:假设给定的无向图有 n 个顶点(顶点编号从 0 到 n-1),采用邻接矩阵数组 a
17200/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  #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]//找到一个有效的顶点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) 
1771      {    printf(" 从顶点%d出发的哈密顿回路:\n",v); 
1772           count=1; 
1773  
1774           Hamiltonian(v);        //从顶点v出发 
1775      } 
1776 } 
1777 上述程序对如图 1.40 所示的无向图求从每个顶点出发的哈密顿回路,程序执行结果如图 1.41 所示。
1778 
17791.40    一个无向图
17801.41    程序执行结果
1781 1.66 章─分枝限界法
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,物品重量为(4753),物品价值为(401798 422512),背包最大载重量 W=10,给出采用优先队列式分枝限界法求最优解的过程。
1799 8. 有一个流水作业调度问题,n=4,a[]={51097},b[]={7598},给出采用优先队列式分枝限界法求一个解的过程。
1800 9.    有一个含 n 个顶点(顶点编号为 0~n-1)的带权图,采用邻接矩阵数组 A 表示, 采用分枝限界法求从起点 s 到目标点 t 的最短路径长度,以及具有最短路径长度的路径条数。
1801 10.    采用优先队列式分枝限界法求解最优装载问题。给出以下装载问题的求解过程和结果:n=5,集装箱重量为 w=(52643),限重为 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.    答:求解过程如下:
18101)根结点 1 进队,对应结点值:e.i=0,e.w=0,e.v=0,e.ub=76,x:[0000]。
18112)    出队结点 1:左孩子结点 2 进队,对应结点值:e.no=2,e.i=1,e.w=4, e.v=40,e.ub=76,x:[1000];右孩子结点 3 进队,对应结点值:e.no=3,e.i=1, e.w=0,e.v=0,e.ub=57,x:[0000]。
18123)    出队结点 2:左孩子超重;右孩子结点 4 进队,对应结点值:e.no=4,e.i=2, e.w=4,e.v=40,e.ub=69,x:[1000]。
18134)    出队结点 4:左孩子结点 5 进队,对应结点值:e.no=5,e.i=3,e.w=9, e.v=65,e.ub=69,x:[1010];右孩子结点 6 进队,对应结点值:e.no=6,e.i=3, e.w=4,e.v=40,e.ub=52,x:[1000]。
1814  
18155)    出队结点 5:产生一个解,maxv= 65,bestx:[1010]。
18166)    出队结点 3:左孩子结点 8 进队,对应结点值:e.no=8,e.i=2,e.w=7, e.v=42,e.ub=57,x:[0100];右孩子结点 9 被剪枝。
18177)    出队结点 8:左孩子超重;右孩子结点 10 被剪枝。
18188)    出队结点 6:左孩子结点 11 超重;右孩子结点 12 被剪枝。
18199)    队列空,算法结束,产生的最优解:maxv= 65,bestx:[1010]。
1820 8.    答:求解过程如下:
18211)根结点 1 进队,对应结点值:e.i=0,e.f1=0,e.f2=0,e.lb=29,    x:[0001822  
1823 0]。
1824  
18252)    出队结点 1:扩展结点如下:
1826 进队(j=1):结点 2,e.i=1,e.f1=5,e.f2=12,e.lb=27,x:[1000]。进队(j=2):结点 3,e.i=1,e.f1=10,e.f2=15,e.lb=34,x:[2000]。进队(j=3):结点 4,e.i=1,e.f1=9,e.f2=18,e.lb=29,x:[3000]。进队(j=4):结点 5,e.i=1,e.f1=7,e.f2=15,e.lb=28,x:[4000]。
18273)    出队结点 2:扩展结点如下:
1828 进队(j=2):结点 6,e.i=2,e.f1=15,e.f2=20,e.lb=32,x:[1200]。进队(j=3):结点 7,e.i=2,e.f1=14,e.f2=23,e.lb=27,x:[1300]。进队(j=4):结点 8,e.i=2,e.f1=12,e.f2=20,e.lb=26,x:[1400]。
18294)    出队结点 8:扩展结点如下:
1830 进队(j=2):结点 9,e.i=3,e.f1=22,e.f2=27,e.lb=31,x:[1420]。进队(j=3):结点 10,e.i=3,e.f1=21,e.f2=30,e.lb=26,x:[1430]。
18315)    出队结点 10,扩展一个 j=2 的子结点,有 e.i=4,到达叶子结点,产生的一个解
1832  
1833 是 e.f1=31,e.f2=36,e.lb=31,x=[1432]。
1834 该解对应的调度方案是:第 1 步执行作业 1,第 2 步执行作业 4,第 3 步执行作业3,第 4 步执行作业 2,总时间=361835 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  #include  using namespace std; #define MAX 11 
1844 #define INF 0x3f3f3f3f 
1845 //问题表示 
1846 int A[MAX][MAX]={                        //一个带权有向图 
1847  
1848 
1849           {014,INF,INF}, 
1850           {INF,0,INF,15}, 
1851           {INF,INF,0,INF,1}, 
1852           {INF,INF,203}, 
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 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//比较找最优解 
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//检查e的所有相邻顶点 
1883                     if (A[e.vno][j]!=INF && A[e.vno][j]!=0) //顶点e.vno到顶点j有边 
1884                     {    if (e.length+A[e.vno][j]//剪枝 
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 所示。
19061.39 程序执行结果
1907 10.    解:采用优先队列式分枝限界法求解。设计优先队列priority_queue,并设计优先队列的关系比较函数 Cmp,指定按结点的 ub 值进行比较,即 ub 值越大的结点越先出队。对应的完整程序如下:
1908 #include  #include  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.ub0]>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,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]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 所示。
20001.40    程序执行结果
2001 1.77 章─贪心法
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.    使用教材介绍的动态规划方法可求解任意 01 背包问题
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[]={123},B[]={324},结果为 3;若 A[]={3431},B[]={1226},结果为 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 
20291.41 每一步的走向
2030 两个略有不同的 5×6 的数字矩阵的最小路径如图 1.42 所示,只有最下面一行的数不同。右边矩阵的路径利用了第一行与最后一行相邻的性质。
2031 输入:包含多个矩阵,每个矩阵的第一行为两个数 m 和 n,分别表示矩阵的行数和列数,接下来的 m×n 个整数按行优先的顺序排列,即前 n 个数组成第一行,接下的 n 个数组成第 2 行,依此类推。相邻整数间用一个或多个空格分隔。注意这些数不一定是正数。输入中可能有一个或多个矩阵描述,直到输入结束。每个矩阵的行数在 110 之间,列数在 1100 之间。
2032 输出:对每个矩阵输出两行,第一行为最小整数之和的路径,路径由 n 个整数组成, 表示路径经过的行号,如果这样的路径不止一条,输出字典序最小一条。
2033 
2034 3    4    1    2    8    6
20351.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={322},v={744},W=4 时,由于 7/3 最大,若按题目要求的方法,只能取第一个,收益是 7。而此实例的最大的收益应该是 8,取第 23
2057 个物品。
2058 8.    解:用结构体数组 A 存放硬币数据,A[i].v 存放硬币 i 的面额,A[i].c 存放硬币 i 的枚数。采用贪心思路,首先将数组 A 按面额递减排序,再兑换硬币,每次尽可能兑换面额大的硬币。对应的完整程序如下:
2059 #include  #include  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) 
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 所示。
20921.43 程序执行结果
2093 9.    解:采用贪心方法求解。用 a[0..k]存放 n 的分解结果:
20941)    n≤4 时可以验证其分解成几个正整数的和的乘积均小于 n,没有解。
20952)    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 ① m2:将 m 平均分解到 a[k..i](对应的分解数个数为 m)中,即从 a[k]开始往前的分解数增加 1(也是贪心的思路,分解数越大加 1 和乘积也越大)。
2097 ② m=k+2:将 a[0..k-1] (对应的分解数个数为 k)的每个分解数增加 1,剩下的 2 增加到 a[k]中,即 a[k]增加 22098 对应的完整程序如下:
2099 #include  #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]开始往前的数+1 
2126           {    for (i=0; 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) 
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 
21531.44 程序执行结果
2154 10.    解:采用贪心思路,首先按体重递增排序;再考虑前后的两个人(最轻者和最重者),分别用 i、j 指向:若 w[i]+w[j]≤C,说明这两个人可以同乘(执行 i++,j--),否则 w[j]单乘(执行 j--),若最后只剩余一个人,该人只能单乘。
2155 对应的完整程序如下:
2156 #include  #include  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 
21961.45 程序执行结果
2197 11.    解:采用贪心思路。每次都在还未安排的容量最大的会议室安排尽可能多的参会人数,即对于每个会议室,都安排当前还未安排的会议中,参会人数最多的会议。若能容纳下,则选择该会议,否则找参会人数次多的会议来安排,直到找到能容纳下的会议。
2198 对应的完整程序如下:
2199 #include  #include  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  #include <string.h> #include  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  #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) 
2302                a[i][j]+= minValue(i,j); 
2303      min=a[0][0]; 
2304      minRow=0; 
2305      for (i=1; i//在第一列查找最小代价的行 
2306           if (a[i][0]<min) 
2307           {    min=a[i][0]; 
2308                minRow=i; 
2309           } 
2310      for (j=0; j) 
2311      {    printf("%d",minRow+1); 
2312           if (j1) printf(" "); 
2313           minValue(minRow, j); 
2314      } 
2315      printf("\n%d\n",min);   
2316 } 
2317 void main() 
2318 { 
2319      solve(); 
2320 } 
2321 
2322 1.88 章─动态规划
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.    下列算法中哪些属于动态规划算法?
23381)    顺序查找算法
23392)    直接插入排序算法
23403)    简单选择排序算法
23414)    二路归并排序算法
2342 9.    某个问题对应的递归模型如下:
2343 f(1)=1
2344 f(2)=2
2345 f(n)=f(n-1)+f(n-2)+…+f(1)+1    当 n>22346 可以采用如下递归算法求解:
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  
236911
2370 12.    一个机器人只能向下和向右移动,每次只能移动一步,设计一个算法求它从
237100)移动到(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 两者的不同点如下:
23871)    求解方式不同,动态规划法是自底向上的,有些具有最优子结构性质的问题只能用动态规划法,有些可用贪心法。而贪心法是自顶向下的。
23882)    对子问题的依赖不同,动态规划法依赖于各子问题的解,所以应使各子问题最优,才能保证整体最优;而贪心法依赖于过去所作过的选择,但决不依赖于将来的选择, 也不依赖于子问题的解。
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  #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  #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.    解:设从(00)移动到(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  #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 程》第 88.5 节。对应的完整程序如下:
2488 #include  #include <string.h> #include  #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 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 《教程》第 88.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  #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 
26441.46 程序的一次执行结果
2645 13. 解:本题目的思路是求 arr1 和 arr2 字符串的最长公共子序列,基本过程参见《教程》第 88.5 节。对应的完整程序如下:
2646 
2647 然后再用递归思想,逐一输出,得到的就是最后答案。
2648 #include  #include <string.h> #include  #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 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 所示。
27111.46    程序的一次执行结果
2712 1.99 章─图算法设计
2713 1.9.1    练习题
2714 1.    以下不属于贪心算法的是( )。
2715 A.Prim 算法    B.Kruskal 算法    C.Dijkstra 算法    D.深度优先遍历
2716 2.    一个有 n 个顶点的连通图的生成树是原图的最小连通子图,且包含原图中所有 n 个顶点,并且有保持图联通的最少的边。最大生成树就是权和最大生成树,现在给出一个无向带权图的邻接矩阵为{{04503},{40423},{54020},{02201},{33010}},其中权为 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)表示单位运输费用。给出从 123 位置运输货物到位置 6 的最小费用最大流的过程。
2726 10.    本教程中的 Dijkstra 算法采用邻接矩阵存储图,算法时间复杂度为 O(n2)。请你从各个方面考虑优化该算法,用于求源点 v 到其他顶点的最短路径长度。
2727 11.    有一个带权有向图 G(所有权为正整数),采用邻接矩阵存储。设计一个算法求其中的一个最小环。
2728 
27291.47    一个运输网络
2730 1.9.2    练习题参考答案
2731 1.    答:D。
2732 2.    答:采用类似 Kurskal 算法来求最大生成树,第 1 步取最大边(02),第 2 步取
2733 边(01),第 3 步取边(04),第 4 步取最大边(13),得到的权和为 14。答案为 D。
2734 3.    答:这 2 条权值最小的边一定包含在所有的最小生成树中,因为按 Kurskal 算法一定首先选中这 2 条权值最小的边。如果有 3 条权值最小的边,就不一定了,因为首先选中这 3 条权值最小的边有可能出现回路。
2735 4.    答:TSP 问题不满足最优子结构性质,如(01230)是整个问题的最优解,但(0120)不一定是子问题的最优解。
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,它到 123 位置运输费用为 0,容量分别为到 123 位置运输容量和,如图 1.48 所示,起点 s=0,终点 t=62745 
27461.48 添加一个虚拟起点的运输网络
2747 首先初始化 f 为零流,最大流量 maxf=0,最小费用 mincost=0,采用最小费用最大流算法求解过程如下:
27481)    k=0,求出 w 如下:
2749 
2750 
2751 
2752 
2753 求出从起点 0 到终点 6 的最短路径为 0146,求出最小调整量=4,f[4][6]调整为 4,f[1][4]调整为 4,f[0][1]调整为 4,mincost=20,maxf=427542)    k=1,求出 w 如下:
2755 
2756 
2757 
2758 
2759 求出从起点 0 到终点 6 的最短路径为 0246,求出最小调整量=3,f[4][6]调整
2760  
27617,f[2][4]调整为 3,f[0][2]调整为 3,mincost=44,maxf=4+3=727623)    k=2,求出 w 如下:
2763 
2764 
2765 
2766 
2767 求出从起点 0 到终点 6 的最短路径为 0346,求出最小调整量=1,f[4][6]调整为 8,f[3][4]调整为 1,f[0][3]调整为 1,mincost=53,maxf=7+1=827684)    k=3,求出 w 如下:
2769 
2770 
2771 
2772 
2773 求出从起点 0 到终点 6 的最短路径为 0356,求出最小调整量=2,f[5][6]调整为 2,f[3][5]调整为 2,f[0][3]调整为 3,mincost=83,maxf=8+2=1027745)    k=4,求出 w 如下:
2775 
2776 
2777 
2778 
2779 求出从起点 0 到终点 6 的最短路径为 0156,求出最小调整量=6,f[5][6]调整为 8,f[1][5]调整为 6,f[0][1]调整为 10,mincost=179,maxf=10+6=1627806)    k=5,求出 w 如下:
2781 
2782 
2783 
2784 
2785 求出从起点 0 到终点 6 的最短路径为 0156,求出最小调整量=1,f[5][6]调整
2786  
2787 
27889,f[2][5]调整为 1,f[0][2]调整为 4,mincost=195,maxf=16+1=1727897)    k=6,求出的 w 中没有增广路径,调整结束。对应的最大流如下:
2790 
2791 
2792 
2793 
2794 
2795 最终结果,maxf=17,mincost=195。即运输的最大货物量为 17,对应的最小总运输费用为 1952796 10.    解:从两个方面考虑优化:
27971)    在 Dijkstra 算法中,当求出源点 v 到顶点 u 的最短路径长度后,仅仅调整从顶点 u 出发的邻接点的最短路径长度,而教程中的 Dijkstra 算法由于采用邻接矩阵存储图, 需要花费 O(n)的时间来调整顶点 u 出发的邻接点的最短路径长度,如果采用邻接表存储图,可以很快查找到顶点 u 的所有邻接点并进行调整,时间为 O(MAX(图中顶点的出
2798 度))。
27992)    求目前一个最短路径长度的顶点 u 时,教科书上的 Dijkstra 算法采用简单比较方法,可以改为采用优先队列(小根堆)求解。由于最多 e 条边对应的顶点进队,对应的时间为 O(log2e)。
2800 对应的完整程序和测试数据算法如下:
2801 #include "Graph.cpp" 
2802 #include  #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 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,0sizeof(S)); 
2818      p=G->adjlist[v].firstarc; 
2819      for (i=0;in;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;in-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;in;++i) 
2859           if (i!=v) 
2860                printf(" 到顶点%d: %d\n",i,dist[i]); 
2861 } 
2862 void main() 
2863 {    int A[MAXV][MAXV]={ 
2864           {0466,INF,INF,INF}, 
2865           {INF,01,INF,7,INF,INF}, 
2866           {INF,INF,0,INF,64,INF}, 
2867           {INF,INF,20,INF,5,INF}, 
2868           {INF,INF,INF,INF,0,INF,6}, 
2869           {INF,INF,INF,INF,108}, 
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 
28861.49  程序执行结果
2887 其中 Dijkstra 算法的时间复杂度为 O(n(log2e+MAX(顶点的出度)),一般图中最大顶点出度远小于 e,所以进一步简化时间复杂度为 O(nlog2e)。
2888 11.    有一个带权有向图 G(所有权为正整数),采用邻接矩阵存储。设计一个算法求其中的一个最小环。
2889 解:利用 Floyd 算法求出所有顶点对之间的最短路径,若顶点 i 到 j 有最短路径,而图中又存在顶点 j 到 i 的边,则构成一个环,在所有环中比较找到一个最小环并输出。对应的程序如下:
2890 #include "Graph.cpp"                    //包含图的基本运算算法#include  
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) 
2909           for (j=0;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) 
2924           for (j=0;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//依次考察所有顶点 
2932      {    for (i=0;i) 
2933                for (j=0;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 
29621.50  程序执行结果
2963 1.1010 章─计算几何
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 
29791.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(11)。
2986 排序后:a0(11) a1(81) a2(94) a3(54) a4(87) a5(56) a10(710) a9(35) a6(37) a7(410) a8(16) a11(03)。
2987 先将 a0(11)进栈,a1(81)进栈,a2(94)进栈。处理点 a3(54):a3(54)进栈。
2988 处理点 a4(87):a3(54)存在右拐关系,退栈,a4(87)进栈。
2989  
2990 处理点 a5(56):a5(56)进栈。
2991 处理点 a10(710):a5(56)存在右拐关系,退栈,a10(710)进栈。处理点 a9(35):a9(35)进栈。
2992 处理点 a6(37):a9(35)存在右拐关系,退栈,a6(37)进栈。处理点 a7(410):a6(37)存在右拐关系,退栈,a7(410)进栈。处理点 a8(16):a8(16)进栈。
2993 处理点 a11(03):a11(03)进栈。
2994 结果:n=8,凸包的顶点:a0(11) a1(81) a2(94) a4(87) a10(710) a7(410) a8(16) a11(03)。
2995 2.    答:求解过程如下:
2996 排序前:(11) (81) (94) (54) (87) (56) (37) (410) (16) (35) (710)
2997 (03)。按 x 坐标排序后:(03) (11) (16) (37) (35) (410) (54) (56) (710)
2998 (81) (87) (94)。按 y 坐标排序后:(11) (81) (03) (54) (94) (35) (16) (56) (37) (87) (410) (710)。
29991)中间位置 midindex=5,左部分:(03) (11) (16) (37) (35) (410);右部分:(54) (56) (710) (81) (87) (94);中间部分点集为 (03) (37) (410) (54) (56) (710) (87)。
30002)求解左部分:(03) (11) (16) (37) (35) (410)。
3001 中间位置=2,划分为左部分 1:(03) (11) (16),右部分 1:(37) (35) (410) 处理左部分 1:点数少于 4:求出最近距离=2.23607,即(03)和(11)之间的距离。处理右部分 1:点数少于 4:求出最近距离=2,即(37)和(35)之间的距离。
3002 再考虑中间部分(中间部分最近距离=2.23)求出左部分 d1=230033)求解右部分:(54) (56) (710) (81) (87) (94)。
3004 中间位置=8,划分为左部分 2:(54) (56) (710),右部分 2:(81) (87) (93005  
3006 4)。
3007  
3008 处理左部分 2:点数少于 4,求出最近距离=2,即 (54)和(56)之间的距离。
3009 处理右部分 2:点数少于 4,求出最近距离=3.16228,即(81)和(94)之间的距离。再考虑中间部分(中间部分为空)求出右部分 d2=230104)求解中间部分点集:(03) (37) (410) (54) (56) (710) (87)。求出最
3011  
3012 近距离 d3=53013 最终结果为:d=MIN{d1,d2,d3)=23014 3.    答:采用旋转卡壳法求出两个最远点对是(11)和(710),最远距离为
3015 10.823016 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 
30221.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 
30321.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 所示。
30661.55  程序执行结果
3067 1.1111 章─计算复杂性理论
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(32)和 f(23)的瞬像演变过程。
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(32)时,输入带上的初始信息为 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,计算结果为 131242)执行 f(23)时,输入带上的初始信息为 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,计算结果为 03157 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)
316210100010 的后继函数值的瞬像演变过程如下:
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 其结果为 101000113184  
3185 1.1212 章─概率算法和近似算法
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.    给定能随机生成整数 15 的函数 rand5(),写出能随机生成整数 17 的函数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()产生 6789,…,262728293025 个整数,每个整数 x 出现的概率相等,取前面 3*7=21 个整数,舍弃后面的 4 个整数,将{678}转化成 1,{91011}转化成 2,以此类推,即由 y= (x-3)/3 为最终结果。对应的程序如下:
3220 #include  
3221 #include  
3222 #include                     //包含产生随机数的库函数 
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 所示。
32431.56    程序执行结果

你可能感兴趣的:(算法设计与分析 - 李春葆 - 第二版 - pdf->word v1)