



1.1第1 章─概论
1.1.1练习题
1.下列关于算法的说法中正确的有()。
Ⅰ.求解某一类问题的算法是唯一的
Ⅱ.算法必须在有限步操作之后停止
Ⅲ.算法的每一步操作必须是明确的,不能有歧义或含义模糊
Ⅳ.算法执行后一定产生确定的结果
A. 1 个B.2 个C.3 个D.4 个
2. T(n)表示当输入规模为n时的算法效率,以下算法效率最优的是()。
A.T(n)= T(n-1)+1,T(1)=1 C.T(n)= T(n/2)+1,T(1)=1
B.T(n)= 2n
D.T(n)=3nlog2n
3.什么是算法?算法有哪些特征?
4.判断一个大于2的正整数n是否为素数的方法有多种,给出两种算法,说明其中一种算法更好的理由。
5.证明以下关系成立:
(1)10n-2n=(n)
(2)2=(2)
6.证明O(f(n))+O(g(n))=O(max{f(n),g(n)})。
7.有一个含n(n>2)个整数的数组a,判断其中是否存在出现次数超过所有元素一半的元素。
8.一个字符串采用string 对象存储,设计一个算法判断该字符串是否为回文。
9.有一个整数序列,设计一个算法判断其中是否存在两个元素和恰好等于给定的整数k。
10.有两个整数序列,每个整数序列中所有元素均不相同。设计一个算法求它们的公共元素,要求不使用STL 的集合算法。
11.正整数n(n>1)可以写成质数的乘积形式,称为整数的质因数分解。例如,12=2*2*3,18=2*3*3,11=11。设计一个算法求n这样分解后各个质因数出现的次数,采用vector 向量存放结果。
12.有一个整数序列,所有元素均不相同,设计一个算法求相差最小的元素对的个数。如序列4、1、2、3的相差最小的元素对的个数是3,其元素对是(1,2),(2,3),(3,4)。
13.有一个map<string,int>容器,其中已经存放了较多元素。设计一个算法求出其中重复的value 并且返回重复value 的个数。
14.重新做第10题,采用map 容器存放最终结果。
15.假设有一个含n(n>1)个元素的stack<int>栈容器st,设计一个算法出栈从栈顶到栈底的第k(1≤k≤n)个元素,其他栈元素不变。









算法设计
1.1.2练习题参考答案
1.答:由于算法具有有穷性、确定性和输出性,因而Ⅱ、Ⅲ、Ⅳ正确,而解决某一类问题的算法不一定是唯一的。答案为C。
2.答:选项A的时间复杂度为O(n)。选项B的时间复杂度为O(n)。选项C的时间复杂度为O(log2n)。选项D 的时间复杂度为O(nlog2n)。答案为C。
3.答:算法是求解问题的一系列计算步骤。算法具有有限性、确定性、可行性、输入性和输出性5 个重要特征。
4.答:两种算法如下:
#include <stdio.h>
#include <math.h>
boolisPrime1(intn)//方法1
{for (int i=2;i<n;i++)
if (n%i==0)
return false;
return true;
}
boolisPrime2(intn)//方法2
{for (int i=2;i<=(int)sqrt(n);i++)
if (n%i==0)
return false;
return true;
}
voidmain()
{int n=5;
printf("%d,%d\n",isPrime1(n),isPrime2(n));
}
方法1 的时间复杂度为O(n),方法2的时间复杂度为n,所以方法2 更好。5.答:(1)当n 足够大时,(10n-2n)/( n)=10,所以10n-2n=(n)。
(2)2=2*2=(2)。
6.证明:对于任意f1(n)∈O(f(n)),存在正常数c1 和正常数n1,使得对所有n≥n1,有f1(n)≤c1f(n)。
类似地,对于任意g1(n)∈O(g(n)),存在正常数c2 和自然数n2,使得对所有n≥n2,有g1(n)≤c2g(n)。
令c3=max{c1,c2},n3=max{n1,n2},h(n)= max{f(n),g(n)}。
则对所有的n≥n3,有:
f1(n) +g1(n)≤c1f(n) +c2g(n)≤c3f(n)+c3g(n)=c3(f(n)+g(n))
≤c32max{f(n),g(n)}=2c3h(n)=O(max{f(n),g(n)})。
7.解:先将a 中元素递增排序,再求出现次数最多的次数maxnum,最后判断是否满足条件。对应的程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
2







第1章
概论
boolsolve(inta[],intn,int&x)
{sort(a,a+n);
int maxnum=0;
int num=1;
int e=a[0];
for (int i=1;i<n;i++)
{if (a[i]==e)
{num++;
//递增排序
//出现次数最多的次数
if (num>maxnum)
{maxnum=num;
x=e;
}
}
else
{e=a[i];
num=1;
}
}
if (maxnum>n/2)
return true;
else
return false;
}
voidmain()
{int a[]={2,2,2,4,5,6,2};
int n=sizeof(a)/sizeof(a[0]);
int x;
if (solve(a,n,x))
printf("出现次数超过所有元素一半的元素为%d\n",x); else
printf("不存在出现次数超过所有元素一半的元素\n");
}
上述程序的执行结果如图1.1 所示。
图1.1
程序执行结果
8.解:采用前后字符判断方法,对应的程序如下:#include <iostream>
#include <string>
using namespace std;
boolsolve(stringstr)//判断字符串str是否为回文{int i=0,j=str.length()-1;
while (i<j)
{if (str[i]!=str[j])
return false;
3




算法设计
i++; j--;
}
return true;
}
voidmain()
{cout << "求解结果"<<endl;
string str="abcd";
cout << "" << str<<(solve(str)?"是回文":"不是回文")<< endl; string str1="abba";
cout << "" << str1<<(solve(str1)?"是回文":"不是回文")<<endl;}
上述程序的执行结果如图1.2所示。
图1.2
程序执行结果
9.解:先将a 中元素递增排序,然后从两端开始进行判断。对应的程序如下:#include <stdio.h>
#include <algorithm>
using namespace std;
boolsolve(inta[],intn,intk)
{sort(a,a+n);
int i=0, j=n-1;
while (i<j)
{if (a[i]+a[j]==k)
return true;
//递增排序
//区间中存在两个或者以上元素
else if (a[i]+a[j]<k)
i++;
else
j--;
}
return false;
}
voidmain()
{int a[]={1,2,4,5,3};
int n=sizeof(a)/sizeof(a[0]);
printf("求解结果\n");
int k=9,i,j;
if (solve(a,n,k,i,j))
printf("存在: %d+%d=%d\n",a[i],a[j],k);else
printf("不存在两个元素和为%d\n",k);
int k1=10;
if (solve(a,n,k1,i,j))
printf("存在: %d+%d=%d\n",a[i],a[j],k1);
4







第1章
概论
else
printf("
不存在两个元素和为%d\n",k1);
}
上述程序的执行结果如图1.3 所示。
图1.3
程序执行结果
10.解:采用集合set<int>存储整数序列,集合中元素默认是递增排序的,再采用二路归并算法求它们的交集。对应的程序如下:
#include <stdio.h>
#include <set>
using namespace std;
voidsolve(set<int>s1,set<int>s2,set<int>&s3){set<int>::iteratorit1,it2;
it1=s1.begin(); it2=s2.begin();
while (it1!=s1.end()&& it2!=s2.end())
{if (*it1==*it2)
{s3.insert(*it1);
++it1; ++it2;
}
else if (*it1<*it2)
++it1;
else
++it2;
}
}
//求交集s3
voiddispset(set<int>s)
{set<int>::iteratorit;
//输出集合的元素
for (it=s.begin();it!=s.end();++it)
printf("%d ",*it);
printf("\n");
}
voidmain()
{int a[]={3,2,4,8};
int n=sizeof(a)/sizeof(a[0]);
set<int> s1(a,a+n);
int b[]={1,2,4,5,3};
int m=sizeof(b)/sizeof(b[0]);
set<int> s2(b,b+m);
set<int> s3;
solve(s1,s2,s3);
printf("求解结果\n");
printf("s1: ");dispset(s1);
5




算法设计
printf("
printf("
s2: "); dispset(s2);
s3: "); dispset(s3);
}
上述程序的执行结果如图1.4 所示。
图1.4
程序执行结果
11.解:对于正整数n,从i=2 开始查找其质因数,ic 记录质因数i 出现的次数,当找到这样质因数后,将(i,ic)作为一个元素插入到vector 容器v 中。最后输出v。对应的算法如下:
#include <stdio.h>
#include <vector>
using namespace std;
struct NodeType
{int p;
int pc;
//vector向量元素类型 //质因数
//质因数出现次数
};
voidsolve(intn,vector<NodeType>&v)//求n的质因数分解{int i=2;
int ic=0;
NodeType e;
do
{if (n%i==0)
{ic++;
n=n/i;
}
else
{if (ic>0)
{e.p=i;
e.pc=ic;
v.push_back(e);
}
ic=0;
i++;
}
} while (n>1 || ic!=0);
}
voiddisp(vector<NodeType>&v)//输出v
{vector<NodeType>::iteratorit;
for (it=v.begin();it!=v.end();++it)
printf("质因数%d出现%d次\n",it->p,it->pc);
}
6








第1章
概论
voidmain()
{vector<NodeType>v;
int n=100;
printf("n=%d\n",n);
solve(n,v);
disp(v);
}
上述程序的执行结果如图1.5 所示。
图1.5
程序执行结果
12.解:先递增排序,再求相邻元素差,比较求最小元素差,累计最小元素差的个数。对应的程序如下:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
intsolve(vector<int>&myv)
//求myv中相差最小的元素对的个数
{sort(myv.begin(),myv.end());//递增排序
int ans=1;
int mindif=myv[1]-myv[0];
for (int i=2;i<myv.size();i++)
{if (myv[i]-myv[i-1]<mindif)
{ans=1;
mindif=myv[i]-myv[i-1];
}
else if (myv[i]-myv[i-1]==mindif)
ans++;
}
return ans;
}
voidmain()
{int a[]={4,1,2,3};
int n=sizeof(a)/sizeof(a[0]);
vector<int> myv(a,a+n);
cout << "相差最小的元素对的个数:" << solve(myv) <<endl; }
上述程序的执行结果如图1.6 所示。
7




图1.6
算法设计
程序执行结果
13.解:对于map<string,int>容器mymap,设计另外一个map<int,int>容器tmap,将前者的value 作为后者的关键字。遍历mymap,累计tmap 中相同关键字的次数。一个参考程序及其输出结果如下:
#include <iostream>
#include <map>
#include <string>
using namespace std;
voidmain()
{map<string,int>mymap;
mymap.insert(pair<string,int>("Mary",80));
mymap.insert(pair<string,int>("Smith",82));
mymap.insert(pair<string,int>("John",80));
mymap.insert(pair<string,int>("Lippman",95));
mymap.insert(pair<string,int>("Detial",82));
map<string,int>::iteratorit;
map<int,int> tmap;
for (it=mymap.begin();it!=mymap.end();it++)
tmap[(*it).second]++;
map<int,int>::iteratorit1;
cout << "求解结果"<<endl;
for (it1=tmap.begin();it1!=tmap.end();it1++)
cout << "" << (*it1).first<< ":" << (*it1).second<< "次\n";
}
上述程序的执行结果如图1.7 所示。
图1.7
程序执行结果
14.解:采用map<int,int>容器mymap 存放求解结果,第一个分量存放质因数,第二个分量存放质因数出现次数。对应的程序如下:
#include <stdio.h>
#include <map>
using namespace std;
voidsolve(intn,map<int,int>&mymap)//求n的质因数分解
{int i=2;
int ic=0;
do
{
if (n%i==0)
{ic++;
n=n/i;
}
8







第1章
概论
else
{if (ic>0)
mymap[i]=ic;
ic=0;
i++;
}
} while (n>1 || ic!=0);
}
voiddisp(map<int,int>&mymap)//输出mymap
{map<int,int>::iteratorit;
for (it=mymap.begin();it!=mymap.end();++it)
printf("质因数%d出现%d次\n",it->first,it->second);
}
voidmain()
{map<int,int>mymap;
int n=12345;
printf("n=%d\n",n);
solve(n,mymap);
disp(mymap);
}
上述程序的执行结果如图1.8 所示。
图1.8
程序执行结果
15.解:栈容器不能顺序遍历,为此创建一个临时tmpst 栈,将st 的k个元素出栈并进栈到tmpst 中,再出栈tmpst一次得到第k 个元素,最后将栈tmpst 的所有元素出栈并进栈到st 中。对应的程序如下:
#include <stdio.h>
#include <stack>
using namespace std;
intsolve(stack<int>&st,intk)
{stack<int> tmpst;
int e;
for (int i=0;i<k;i++)
{e=st.top();
st.pop();
tmpst.push(e);
}
e=tmpst.top();
tmpst.pop();
while (!tmpst.empty())
{st.push(tmpst.top());
tmpst.pop();
//出栈第k个元素
//出栈st的k个元素并进tmpst栈//求第k个元素
//将tmpst的所有元素出栈并进栈st9




}
return e;
}
voiddisp(stack<int>&st)
{while (!st.empty())
{printf("%d ",st.top());
st.pop();
}
printf("\n");
}
voidmain()
{stack<int> st;
算法设计
//出栈st的所有元素
printf("进栈元素1,2,3,4\n");st.push(1);
st.push(2);
st.push(3);
st.push(4);
int k=3;
int e=solve(st,k);
printf("出栈第%d个元素是: %d\n",k,e);printf("st中元素出栈顺序: "); disp(st);
}
上述程序的执行结果如图1.9 所示。
图1.9
程序执行结果
1.2第2 章─递归算法设计技术
1.2.1练习题
1.什么是直接递归和间接递归?消除递归一般要用到什么数据结构?2.分析以下程序的执行结果:
#include <stdio.h>
voidf(intn,int&m)
{if (n<1) return;
else
{
}
printf("调用f(%d,%d)前,n=%d,m=%d\n",n-1,m-1,n,m);n--; m--;
f(n-1,m);
printf("调用f(%d,%d)后:n=%d,m=%d\n",n-1,m-1,n,m);
10







第1章
概论
}
voidmain()
{int n=4,m=4;
f(n,m);
}
3.采用直接推导方法求解以下递归方程:T(1)=1
T(n)=T(n-1)+n当n>1
4.采用特征方程方法求解以下递归方程:H(0)=0
H(1)=1
H(2)=2
H(n)=H(n-1)+9H(n-2)-9H(n-3)当n>2 5.采用递归树方法求解以下递归方程:T(1)=1
T(n)=4T(n/2)+n当n>1
6.采用主方法求解以下题的递归方程。
T(n)=1
当n=1
T(n)=4T(n/2)+n当n>1
7.分析求斐波那契f(n)的时间复杂度。
8.数列的首项a1=0,后续奇数项和偶数项的计算公式分别为a2n=a2n-1+2,a2n+1=a2n- 1+a2n-1,写出计算数列第n项的递归算法。
9.对于一个采用字符数组存放的字符串str,设计一个递归算法求其字符个数(长度)。
10.对于一个采用字符数组存放的字符串str,设计一个递归算法判断str 是否为回文。
11.对于不带头结点的单链表L,设计一个递归算法正序输出所有结点值。
12.对于不带头结点的单链表L,设计一个递归算法逆序输出所有结点值。
13.对于不带头结点的非空单链表L,设计一个递归算法返回最大值结点的地址(假设这样的结点唯一)。
14.对于不带头结点的单链表L,设计一个递归算法返回第一个值为x 的结点的地址,没有这样的结点时返回NULL。
15.对于不带头结点的单链表L,设计一个递归算法删除第一个值为x 的结点。
16.假设二叉树采用二叉链存储结构存放,结点值为int 类型,设计一个递归算法求二叉树bt 中所有叶子结点值之和。
17.假设二叉树采用二叉链存储结构存放,结点值为int 类型,设计一个递归算法求二叉树bt 中所有结点值大于等于k 的结点个数。
18.假设二叉树采用二叉链存储结构存放,所有结点值均不相同,设计一个递归算法求值为x 的结点的层次(根结点的层次为1),没有找到这样的结点时返回0。
11



























算法设计
1.2.2练习题参考答案
1.答:一个f 函数定义中直接调用f 函数自己,称为直接递归。一个f 函数定义中调用g 函数,而g 函数的定义中调用f 函数,称为间接递归。消除递归一般要用栈实现。
2.答:递归函数f(n,m)中,n是非引用参数,m是引用参数,所以递归函数的状态为(n)。程序执行结果如下:
调用f(3,3)前,n=4,m=4
调用f(1,2)前,n=2,m=3
调用f(0,1)后,n=1,m=2
调用f(2,1)后,n=3,m=2
3.解:求T(n)的过程如下:
T(n)=T(n-1)+n=[T(n-2)+n-1)]+n=T(n-2)+n+(n-1)
=T(n-3)+n+(n-1)+(n-2)
=…
=T(1)+n+(n-1)+…+2
=n+(n-1)+ +…+2+1=n(n+1)/2=O(n)。
4.解:整数一个常系数的线性齐次递推式,用x
n-33232
代替H(n),有:x=x+9x-9x,
x-x-9x+9=x(x-9)-(x-9)=(x-1)(x-9)=(x-1)(x+3)(x-3)=0。得到r1=1,r2=-3,r3=3则递归方程的通解为:H(n)=c1+c2(-3)+c33
代入H(0)=0,有c1+c2+c3=0
代入H(1)=1,有c1-3c2+3c3=1
代入H(2)=2,有c1+9c2+9c3=2
求出:c1=-1/4,c2=-1/12,c3=1/3,H(n)=c1+c2(-3)+c33=(
n‒1
4
+ 13
1
‒。
5.解:构造的递归树如图1.10 所示,第1 层的问题规模为n,第2 的层的子问题的问题规模为n/2,依此类推,当展开到第k+1层,其规模为n/2=1,所以递归树的高度为log2n+1。
第1层有1个结点,其时间为n,第2层有4个结点,其时间为4(n/2)=2n,依次类推,第k 层有4个结点,每个子问题规模为n/2,其时间为4(n/2)=2n。叶子结点的个数为n 个,其时间为n。将递归树每一层的时间加起来,可得:
logn
T(n)=n+2n+…+ 2n+…+n≈𝑛∗2=O(n)。
12
































第1章
概论
n
n
(n/2)
(n/2)
(n/2)
(n/2)
2n
高度h 为log2n+1
(n/2)
…
1
(n/2)
…
1
(n/2)
…
1
(n/2)
…
1
2n
n
图1.10一棵递归树
6.解:采用主方法求解,这里a=4,b=2,f(n)=n。
logalog4
因此,𝑛=𝑛=n,它与f(n)一样大,满足主定理中的情况(2),所以T(n)=O( loga
𝑛log2n)=O(nlog2n)。
7.解:设求斐波那契f(n)的时间为T(n),有以下递推式:
T(1)=T(2)
T(n)=T(n-1)+T(n-2)+1
当n>2
其中,T(n)式中加1表示一次加法运算的时间。
不妨先求T1(1)=T1(2)=1,T1(n)=T1(n-1)+T1(n-2),按《教程》例2.14的方法可以求
出:
T1(n)=
1
5
15
1
5
15
115≈=
所以T(n)=T1(n)+1≈
1
5
15
+1=O(φ),其中φ=。
2
8.解:设f(m)计算数列第m项值。
当m 为偶数时,不妨设m=2n,则2n-1=m-1,所以有f(m)=f(m-1)+2。
当m 为奇数时,不妨设m=2n+1,则2n-1=m-2,2n=m-1,所以有f(m)=f(m-2)+f(m-1)-1。
对应的递归算法如下:
intf(int m)
{if (m==1) return0;
if (m%2==0)
return f(m-1)+2;
else
return f(m-2)+f(m-1)-1;
}
9.解:设f(str)返回字符串str的长度,其递归模型如下:
f(str)=0
f(str)=f(str+1)+1
当*str='\0'时
其他情况
对应的递归程序如下:
13




#include <iostream>
using namespace std;
intLength(char*str)
{if (*str=='\0')
return 0;
else
return Length(str+1)+1;
}
voidmain()
{char str[]="abcd";
算法设计
//求str的字符个数
cout << str << "的长度:"<<Length(str) << endl; }
上述程序的执行结果如图1.11 所示。
图1.11
程序执行结果
10.解:设f(str,n)返回含n 个字符的字符串str是否为回文,其递归模型如下:
f(str,n)=true
f(str,n)=flase
f(str,n)=f(str+1,n-2)
当n=0 或者n=1 时当str[0]≠str[n-1]时其他情况
对应的递归算法如下:
#include <stdio.h>
#include <string.h>
boolisPal(char*str,intn)//str回文判断算法{if (n==0 ||n==1)
return true;
if (str[0]!=str[n-1])
return false;
return isPal(str+1,n-2);
}
voiddisp(char*str)
{int n=strlen(str);
if (isPal(str,n))
printf("%s是回文\n",str);
else
printf("%s不是回文\n",str);
}
voidmain()
{printf("求解结果\n");
disp("abcba");
disp("a");
disp("abc");
}
14








第1章
概论
上述程序的执行结果如图1.12 所示。
图1.12
程序执行结果
11.解:设f(L)正序输出单链表L 的所有结点值,其递归模型如下:
f(L)≡ 不做任何事情
f(L)≡ 输出L->data; f(L->next);对应的递归程序如下:#include "LinkList.cpp"
当L=NULL
当L≠NULL 时
//包含单链表的基本运算算法
voiddispLink(LinkNode*L)//正序输出所有结点值{if (L==NULL)return;
else
{printf("%d ",L->data);
dispLink(L->next);
}
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L;
CreateList(L,a,n);printf("正向L: ");dispLink(L); printf("\n");
Release(L);
//由a[0..n-1]创建不带头结点的单链表 //销毁单链表
}
上述程序的执行结果如图1.13 所示。
图1.13
程序执行结果
12.解:设f(L)逆序输出单链表L 的所有结点值,其递归模型如下:
f(L)≡ 不做任何事情
f(L)≡ f(L->next);输出L->data对应的递归程序如下:
#include "LinkList.cpp"
voidRevdisp(LinkNode*L)
{if (L==NULL)return;
当L=NULL
当L≠NULL 时
//包含单链表的基本运算算法//逆序输出所有结点值
15




else
{Revdisp(L->next);
printf("%d ",L->data);
}
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L;
CreateList(L,a,n);
printf("反向L: ");
Revdisp(L); printf("\n");
Release(L);
}
上述程序的执行结果如图1.14 所示。
图1.14
算法设计
程序执行结果
13.解:设f(L)返回单链表L 中值最大结点的地址,其递归模型如下:
f(L) = L
f(L) =MAX{f(L->next),L->data}对应的递归程序如下:#include "LinkList.cpp"
当L 只有一个结点时其他情况
//包含单链表的基本运算算法
LinkNode*Maxnode(LinkNode*L)//返回最大值结点的地址{if (L->next==NULL)
return L;
else
{LinkNode *maxp;
maxp=Maxnode(L->next);
if (L->data>maxp->data)
return L;
else
return maxp;
}
}
voidmain()
{int a[]={1,2,5,2,3,2};
//只有一个结点时
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L,*p;
CreateList(L,a,n);
p=Maxnode(L);
printf("最大结点值:%d\n",p->data); Release(L);
16








第1章
概论
}
上述程序的执行结果如图1.15 所示。
图1.15
程序执行结果
14.解:设f(L,x)返回单链表L 中第一个值为x 的结点的地址,其递归模型如下:
f(L,x) = NULL
f(L,x) = L
f(L,x) =f(L->next,x) 对应的递归程序如下:
当L=NULL 时
当L≠NULL 且L->data=x 时其他情况
#include "LinkList.cpp"
LinkNode*Firstxnode(LinkNode*L,intx) {if (L==NULL)return NULL;
if (L->data==x)
return L;
else
return Firstxnode(L->next,x);
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L,*p;
CreateList(L,a,n);
int x=2;
p=Firstxnode(L,x);
printf("结点值: %d\n",p->data);Release(L);
}
上述程序的执行结果如图1.16 所示。
//包含单链表的基本运算算法
//返回第一个值为x的结点的地址
图1.16
程序执行结果
15.解:设f(L,x)删除单链表L 中第一个值为x 的结点,其递归模型如下:
f(L,x)≡ 不做任何事情
f(L,x)≡ 删除L 结点,L=L->nextf(L,x)≡ f(L->next,x)
对应的递归程序如下:
当L=NULL
当L≠NULL 且L->data=x 其他情况
17




#include "LinkList.cpp"
算法设计
//包含单链表的基本运算算法
voidDelfirstx(LinkNode*&L,intx)//删除单链表L中第一个值为x的结点{if (L==NULL)return;
if (L->data==x)
{LinkNode *p=L;
L=L->next;
free(p);
}
else
Delfirstx(L->next,x);
}
voidmain()
{int a[]={1,2,5,2,3,2};
int n=sizeof(a)/sizeof(a[0]);
LinkNode *L;
CreateList(L,a,n);
printf("删除前L:"); DispList(L);
int x=2;
printf("删除第一个值为%d的结点\n",x);
Delfirstx(L,x);
printf("删除后L:"); DispList(L);
Release(L);
}
上述程序的执行结果如图1.17 所示。
图1.17
程序执行结果
16.解:设f(bt)返回二叉树bt 中所有叶子结点值之和,其递归模型如下:
f(bt)=0
f(bt)=bt->data
f(bt)=f(bt->lchild)+f(bt->rchild) 对应的递归程序如下:#include "Btree.cpp"
intLeafSum(BTNode*bt)
{if (bt==NULL)return 0;
当bt=NULL
当bt≠NULL 且bt 结点为叶子结点其他情况
//包含二叉树的基本运算算法
//二叉树bt中所有叶子结点值之和
if (bt->lchild==NULL&& bt->rchild==NULL)
return bt->data;
int lsum=LeafSum(bt->lchild);
int rsum=LeafSum(bt->rchild);
return lsum+rsum;
}
voidmain()
18







第1章
概论
{
BTNode *bt;
Int a[]={5,2,3,4,1,6};//先序序列
Int b[]={2,3,5,1,4,6};//中序序列
int n=sizeof(a)/sizeof(a[0]);bt=CreateBTree(a,b,n);//由a和b构造二叉链bt printf("二叉树bt:");DispBTree(bt);printf("\n"); printf("所有叶子结点值之和:%d\n",LeafSum(bt));
DestroyBTree(bt);
//销毁树bt
}
上述程序的执行结果如图1.18 所示。
图1.18
程序执行结果
17.解:设f(bt,k)返回二叉树bt 中所有结点值大于等于k 的结点个数,其递归模型如下:
f(bt,k)=0
f(bt,k)=f(bt->lchild,k)+f(bt->rchild,k)+1f(bt,k)=f(bt->lchild,k)+f(bt->rchild,k)
对应的递归程序如下:
#include "Btree.cpp"
intNodenum(BTNode*bt,intk)
{if (bt==NULL)return 0;
int lnum=Nodenum(bt->lchild,k);
int rnum=Nodenum(bt->rchild,k);
if (bt->data>=k)
return lnum+rnum+1;
else
return lnum+rnum;
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
当bt=NULL
当bt≠NULL 且bt->data≥k其他情况
//包含二叉树的基本运算算法//大于等于k的结点个数
//由a和b构造二叉链bt
printf("二叉树bt:");DispBTree(bt);printf("\n"); int k=3;
printf("大于等于%d的结点个数:%d\n",k,Nodenum(bt,k));
DestroyBTree(bt);
//销毁树bt
}
上述程序的执行结果如图1.19 所示。
19





算法设计
图1.19
程序执行结果
18.解:设f(bt,x,h)返回二叉树bt中x 结点的层次,其中h 表示bt 所指结点的层次,初始调用时,bt 指向根结点,h 置为1。其递归模型如下:
f(bt,x,h)=0
f(bt,x,h)=h
f(bt,x,h) =l
f(bt,x,h) =f(bt->rchild,x,h+1) 对应的递归程序如下:
当bt=NULL
当bt≠NULL 且bt->data=x当l=f(bt->lchild,x,h+1)≠0其他情况
#include "Btree.cpp"
intLevel(BTNode*bt,intx,int h) {//初始调用时:bt为根,h为1
if (bt==NULL) return0;
if (bt->data==x)
return h;
//包含二叉树的基本运算算法//求二叉树bt中x结点的层次
//找到x结点,返回h
else
{int l=Level(bt->lchild,x,h+1);//在左子树中查找
if (l!=0)
return l;
else
//在左子树中找到,返回其层次l
return Level(bt->rchild,x,h+1);//返回在右子树的查找结果
}
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
//由a和b构造二叉链bt
printf("二叉树bt:");DispBTree(bt);printf("\n"); int x=1;
printf("%d结点的层次: %d\n",x,Level(bt,x,1));
DestroyBTree(bt);
}
上述程序的执行结果如图1.20 所示。
//销毁树bt
图1.20
程序执行结果20






第1章
概论
1.3第3 章─分治法
1.3.1练习题
1.分治法的设计思想是将一个难以直接解决的大问题分割成规模较小的子问题,分别解决子问题,最后将子问题的解组合起来形成原问题的解。这要求原问题和子问题()。
A.问题规模相同,问题性质相同
B.问题规模相同,问题性质不同
C.问题规模不同,问题性质相同
D.问题规模不同,问题性质不同
2.在寻找n 个元素中第k 小元素问题中,如快速排序算法思想,运用分治算法对n个元素进行划分,如何选择划分基准?下面()答案解释最合理。
A.随机选择一个元素作为划分基准
B.取子序列的第一个元素作为划分基准
C.用中位数的中位数方法寻找划分基准
D.以上皆可行。但不同方法,算法复杂度上界可能不同
3.对于下列二分查找算法,以下正确的是()。
A.
intbinarySearch(inta[],intn,int x)
{int low=0, high=n-1;
while(low<=high)
{int mid=(low+high)/2;
if(x==a[mid]) returnmid;
if(x>a[mid]) low=mid;
else high=mid;
}
return –1;
}
B.
intbinarySearch(inta[],intn,int x)
{int low=0, high=n-1;
while(low+1!=high)
{int mid=(low+high)/2;
if(x>=a[mid]) low=mid;
else high=mid;
}
if(x==a[low]) returnlow;
else return –1;
}
C.
intbinarySearch(inta[],intn,intx)
{int low=0, high=n-1;
while(low<high-1)
{int mid=(low+high)/2;
21





算法设计
if(x<a[mid])
high=mid;
else low=mid;
}
if(x==a[low]) returnlow;
else return –1;
}
D.
intbinarySearch(inta[],intn,int x)
{if(n > 0 &&x >= a[0])
{int low = 0,high = n-1;
while(low < high)
{int mid=(low+high+1)/2;
if(x < a[mid])
high=mid-1;
else low=mid;
}
if(x==a[low]) returnlow;
}
return –1;
}
4.快速排序算法是根据分治策略来设计的,简述其基本思想。
5.假设含有n 个元素的待排序的数据a 恰好是递减排列的,说明调用QuickSort(a,0,n-1)递增排序的时间复杂度为O(n)。
6.以下哪些算法采用分治策略:
(1)堆排序算法
(2)二路归并排序算法
(3)折半查找算法
(4)顺序查找算法
7.适合并行计算的问题通常表现出哪些特征?
8.设有两个复数x=a+bi和y=c+di。复数乘积xy 可以使用4 次乘法来完成,即xy=(ac-bd)+(ad+bc)i。设计一个仅用3 次乘法来计算乘积xy 的方法。
9.有4 个数组a、b、c 和d,都已经排好序,说明找出这4 个数组的交集的方法。10.设计一个算法,采用分治法求一个整数序列中的最大最小元素。
11.设计一个算法,采用分治法求x。
12.假设二叉树采用二叉链存储结构进行存储。设计一个算法采用分治法求一棵二叉树bt 的高度。
13.假设二叉树采用二叉链存储结构进行存储。设计一个算法采用分治法求一棵二叉树bt 中度为2 的结点个数。
14.有一种二叉排序树,其定义是空树是一棵二叉排序树,若不空,左子树中所有结点值小于根结点值,右子树中所有结点值大于根结点值,并且左右子树都是二叉排序树。现在该二叉排序树采用二叉链存储,采用分治法设计查找值为x 的结点地址,并分析算法的最好的平均时间复杂度。
22







第1章
概论
15.设有n 个互不相同的整数,按递增顺序存放在数组a[0..n-1]中,若存在一个下标i(0≤i<n),使得a[i]=i。设计一个算法以O(log2n)时间找到这个下标i。
16.请你模仿二分查找过程设计一个三分查找算法。分析其时间复杂度。
17.对于大于1 的正整数n,可以分解为n=x1*x2*…*xm,其中xi≥2。例如,n=12 时有8 种不同的分解式:12=12,12=6*2,12=4*3,12=3*4,12=3*2*2,12=2*6,12=2*3*2,12=2*2*3,设计一个算法求n 的不同分解式个数。
18.设计一个基于BSP 模型的并行算法,假设有p 台处理器,计算整数数组a[0..n-1]的所有元素之和。并分析算法的时间复杂度。
1.3.2练习题参考答案
1.答:C。
2.答:D。
3.答:以a[]={1,2,3,4,5}为例说明。选项A中在查找5 时出现死循环。选项B 中在查找5 时返回-1。选项C 中在查找5 时返回-1。选项D 正确。
4.答:对于无序序列a[low..high]进行快速排序,整个排序为“大问题”。选择其中的一个基准base=a[i](通常以序列中第一个元素为基准),将所有小于等于base 的元素移动到它的前面,所有大于等于base 的元素移动到它的后面,即将基准归位到a[i],这样产生a[low..i-1]和a[i+1..high]两个无序序列,它们的排序为“小问题”。当a[low..high]序列只有一个元素或者为空时对应递归出口。
所以快速排序算法就是采用分治策略,将一个“大问题”分解为两个“小问题”来求解。由于元素都是在a 数组中,其合并过程是自然产生的,不需要特别设计。
5.答:此时快速排序对应的递归树高度为O(n),每一次划分对应的时间为O(n),所以整个排序时间为O(n)。
6.答:其中二路归并排序和折半查找算法采用分治策略。
7.答:适合并行计算的问题通常表现出以下特征:
(1)将工作分离成离散部分,有助于同时解决。例如,对于分治法设计的串行算法,可以将各个独立的子问题并行求解,最后合并成整个问题的解,从而转化为并行算法。
(2)随时并及时地执行多个程序指令。
(3)多计算资源下解决问题的耗时要少于单个计算资源下的耗时。
8.答:xy=(ac-bd)+((a+b)(c+d)-ac-bd)i。由此可见,这样计算xy只需要3次乘法(即ac、bd 和(a+b)(c+d)乘法运算)。
9.答:采用基本的二路归并思路,先求出a、b 的交集ab,再求出c、d 的交集cd,最后求出ab 和cd的交集,即为最后的结果。也可以直接采用4 路归并方法求解。
10.解:采用类似求求一个整数序列中的最大次大元素的分治法思路。对应的程序如下:
#include <stdio.h>
#define max(x,y)((x)>(y)?(x):(y))
#define min(x,y)((x)<(y)?(x):(y))
23





算法设计
voidMaxMin(inta[],intlow,inthigh,int&maxe,int&mine)
//求a中最大最小元素
{if (low==high)
{maxe=a[low];
mine=a[low];
}
else if (low==high-1)
{maxe=max(a[low],a[high]);
mine=min(a[low],a[high]);
}
else
{int mid=(low+high)/2;
//只有一个元素
//只有两个元素
//有两个以上元素
int lmaxe,lmine;
MaxMin(a,low,mid,lmaxe,lmine);
int rmaxe,rmine;
MaxMin(a,mid+1,high,rmaxe,rmine);
maxe=max(lmaxe,rmaxe);
mine=min(lmine,rmine);
}
}
voidmain()
{int a[]={4,3,1,2,5};
int n=sizeof(a)/sizeof(a[0]);
int maxe,mine;
MaxMin(a,0,n-1,maxe,mine);
printf("Max=%d, Min=%d\n",maxe,mine);
}
上述程序的执行结果如图1.21 所示。
图1.21
程序执行结果
11.解:设f(x,n)=x,采用分治法求解对应的递归模型如下:
f(x,n)=x
f(x,n)=f(x,n/2)*f(x,n/2)
f(x,n)=f(x,(n-1)/2)*f(x,(n-1)/2)*x对应的递归程序如下:#include <stdio.h>
doublesolve(doublex,intn)
{double fv;
if (n==1) returnx;
if (n%2==0)
{fv=solve(x,n/2);
return fv*fv;
}
当n=1 当n 为偶数时当n 为奇数时
//求x^n
24







第1章
else
{fv=solve(x,(n-1)/2);
return fv*fv*x;
}
}
voidmain()
{double x=2.0;
printf("求解结果:\n");
for (int i=1;i<=10;i++)
printf("%g^%d=%g\n",x,i,solve(x,i));
}
上述程序的执行结果如图1.22 所示。
概论
图1.22
程序执行结果
12.解:设f(bt)返回二叉树bt 的高度,对应的递归模型如下:
f(bt)=0
f(bt)=MAX{f(bt->lchild),f(bt->rchild)}+1对应的程序如下:
#include "Btree.cpp"
intHeight(BTNode*bt)
{if (bt==NULL)return 0;
int lh=Height(bt->lchild);
int rh=Height(bt->rchild);
if (lh>rh) returnlh+1;
else return rh+1;
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
当bt=NULL
其他情况
//包含二叉树的基本运算算法//求二叉树bt的高度
//子问题1
//子问题2
//合并
//由a和b构造二叉链bt
printf("二叉树bt:");DispBTree(bt);printf("\n"); printf("bt的高度:%d\n",Height(bt));
DestroyBTree(bt);
//销毁树bt
}
25





算法设计
上述程序的执行结果如图1.23 所示。
图1.23
程序执行结果
13.解:设f(bt)返回二叉树bt 中度为2 的结点个数,对应的递归模型如下:
f(bt)=0
f(bt)=f(bt->lchild)+f(bt->rchild)+1f(bt)=f(bt->lchild)+f(bt->rchild)
对应的算法如下:
#include "Btree.cpp"
intNodes(BTNode*bt)
{int n=0;
当bt=NULL
若bt≠NULL 且bt 为双分支结点其他情况
//包含二叉树的基本运算算法//求bt中度为2的结点个数
if (bt==NULL) return0;
if (bt->lchild!=NULL&& bt->rchild!=NULL)
n=1;
return Nodes(bt->lchild)+Nodes(bt->rchild)+n;
}
voidmain()
{BTNode *bt;
Int a[]={5,2,3,4,1,6};
Int b[]={2,3,5,1,4,6};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
//由a和b构造二叉链bt
printf("二叉树bt:");DispBTree(bt);printf("\n"); printf("bt中度为2的结点个数:%d\n",Nodes(bt));
DestroyBTree(bt);
//销毁树bt
}
上述程序的执行结果如图1.24 所示。
图1.24
程序执行结果
14.解:设f(bt,x)返回在二叉排序树bt 得到的值为x 结点的地址,若没有找到返回空,对应的递归模型如下:
f(bt,x)=NULL
f(bt,x)=bt
f(bt,x)=f(bt->lchild,x)
当bt=NULL
当bt≠NULL 且x=bt->data当x>bt->data
26







第1章
概论
f(bt,x)=f(bt->rchild,x) 对应的程序如下:
#include "Btree.cpp"
当x<bt->data
//包含二叉树的基本运算算法
BTNode*Search(BTNode*bt,Int x)//在二叉排序树bt查找的值为x结点{if (bt==NULL)return NULL;
if (x==bt->data)return bt;
if (x<bt->data) returnSearch(bt->lchild,x);
else return Search(bt->rchild,x);
}
voidmain()
{BTNode *bt;
Int a[]={4,3,2,8,6,7,9};
Int b[]={2,3,4,6,7,8,9};
int n=sizeof(a)/sizeof(a[0]);
bt=CreateBTree(a,b,n);
//构造一棵二叉排序树bt
printf("二叉排序树bt:");DispBTree(bt); printf("\n");int x=6;
BTNode *p=Search(bt,x);
if (p!=NULL)
printf("找到结点:%d\n",p->data);
else
printf("没有找到结点\n",x);
DestroyBTree(bt);
//销毁树bt
}
上述程序的执行结果如图1.25 所示。
图1.25
程序执行结果
Search(bt,x)算法采用的是减治法,最好的情况是某个结点左右子树高度大致相同,其平均执行时间T(n)如下:
T(n)=1
T(n)=T(n/2)+1
当n=1
当n>1
可以推出T(n)=O(log2n),其中n 为二叉排序树的结点个数。
15.解:采用二分查找方法。a[i]=i 时表示该元素在有序非重复序列a 中恰好第i 大。对于序列a[low..high],mid=(low+high)/2,若a[mid]=mid 表示找到该元素;若a[mid]>mid说明右区间的所有元素都大于其位置,只能在左区间中查找;若a[mid]<mid说明左区间的所有元素都小于其位置,只能在右区间中查找。对应的程序如下:
#include <stdio.h>
intSearch(inta[],intn)
{int low=0,high=n-1,mid;
//查找使得a[i]=i
27




算法设计
while (low<=high)
{mid=(low+high)/2;
if (a[mid]==mid)//查找到这样的元素
return mid;
else if (a[mid]<mid)//这样的元素只能在右区间中出现 low=mid+1;
else
//这样的元素只能在左区间中出现
high=mid-1;
}
return -1;
}
voidmain()
{int a[]={-2,-1,2,4,6,8,9};
int n=sizeof(a)/sizeof(a[0]);
int i=Search(a,n);
printf("求解结果\n");
if (i!=-1)
printf("存在a[%d]=%d\n",i,i);else
printf("不存在\n");
}
上述程序的执行结果如图1.26 所示。
图1.26
程序执行结果
16.解:对于有序序列a[low..high],若元素个数少于3个,直接查找。若含有更多的元素,将其分为a[low..mid1-1]、a[mid1+1..mid2-1]、a[mid2+1..high]子序列,对每个子序列递归查找,算法的时间复杂度为O(log3n),属于O(log2n)级别。对应的算法如下:
#include <stdio.h>
intSearch(inta[],intlow,inthigh,intx) {if (high<low)
return -1;
else if (high==low)
{if (x==a[low])
return low;
else
return -1;
}
if (high-low<2)
{if (x==a[low])
return low;
else if (x==a[low+1])
return low+1;
else
//三分查找
//序列中没有元素
//序列中只有1个元素
//序列中只有2个元素
28







第1章
概论
return -1;
}
int length=(high-low+1)/3;
int mid1=low+length;
int mid2=high-length;
if (x==a[mid1])
return mid1;
else if (x<a[mid1])
return Search(a,low,mid1-1,x);
else if (x==a[mid2])
return mid2;
else if (x<a[mid2])
return Search(a,mid1+1,mid2-1,x);
else
return Search(a,mid2+1,high,x);
}
voidmain()
{int a[]={1,3,5,7,9,11,13,15};
int n=sizeof(a)/sizeof(a[0]);printf("求解结果\n");
int x=13;
int i=Search(a,0,n-1,x);
if (i!=-1)
printf("a[%d]=%d\n",i,x);
else
printf("不存在%d\n",x);
int y=10;
int j=Search(a,0,n-1,y);
if (j!=-1)
printf("a[%d]=%d\n",j,y);
else
printf("不存在%d\n",y);
}
上述程序的执行结果如图1.27 所示。
//每个子序列的长度
图1.27
程序执行结果
17.解:设f(n)表示n的不同分解式个数。有:f(1)=1,作为递归出口
f(2)=1,分解式为:2=2
f(3)=1,分解式为:3=3
f(4)=2,分解式为:4=4,4=2*2
29





算法设计
f(6)=3,分解式为:6=6,6=2*3,6=3*2,即f(6)=f(1)+f(2)+f(3)
以此类推,可以看出f(n)为n 的所有因数的不同分解式个数之和,即f(n)= ∑𝑓(𝑛/𝑖)。对应的程序如下:
#include <stdio.h>
#define MAX 101
intsolve(int n)
{if (n==1) return1; else
{int sum=0;
//求n的不同分解式个数
for (int i=2;i<=n;i++)
if (n%i==0)
sum+=solve(n/i);
return sum;
}
}
voidmain()
{int n=12;
int ans=solve(n);
printf("结果: %d\n",ans);
}
上述程序的执行结果如图1.28 所示。
图1.28
程序执行结果
18.解:对应的并行算法如下:
intSum(inta[],ints,intt,intp,int i)//处理器i执行求和{int j,s=0;
for (j=s;j<=t;j++)
s+=a[j];
return s;
}
intParaSum(inta[],ints,intt,intp,int i)
{int sum=0,j,k=0,sj;
for (j=0;j<p;j++)
{sj=Sum(a,k,k+n/p-1,p,j);
k+=n/p;
}
sum+=sj;
return sum;
//for循环的各个子问题并行执行
}
每个处理器的执行时间为O(n/p),同步开销为O(p),所以该算法的时间复杂度为O(n/p+p)。
30






第1章
概论
1.4第4 章─蛮力法
1.4.1练习题
1.简要比较蛮力法和分治法。
2.在采用蛮力法求解时什么情况下使用递归?
3.考虑下面这个算法,它求的是数组a 中大小相差最小的两个元素的差。请对这个算法做尽可能多的改进。
#define INF 99999
#define abs(x) (x)<0?-(x):(x)
intMindif(inta[],intn)
{int dmin=INF;
for (int i=0;i<=n-2;i++)
//求绝对值宏
for (int j=i+1;j<=n-1;j++)
{int temp=abs(a[i]-a[j]);
if (temp<dmin)
dmin=temp;
}
return dmin;
}
4.给定一个整数数组A=(a0,a1,…an-1),若i<j且ai>aj,则<ai,aj>就为一个逆序对。例如数组(3,1,4,5,2)的逆序对有<3,1>,<3,2>,<4,2>,<5,2>。设计一个算法采用蛮力法求A 中逆序对的个数即逆序数。
5.对于给定的正整数n(n>1),采用蛮力法求1!+2!+…+n!,并改进该算法提高效率。
6.有一群鸡和一群兔,它们的只数相同,它们的脚数都是三位数,且这两个三位数的各位数字只能是0、1、2、3、4、5。设计一个算法用蛮力法求鸡和兔的只数各是多少?它们的脚数各是多少?
7.有一个三位数,个位数字比百位数字大,而百位数字又比十位数字大,并且各位数字之和等于各位数字相乘之积,设计一个算法用穷举法求此三位数。
8.某年级的同学集体去公园划船,如果每只船坐10 人,那么多出2 个座位;如果每只船多坐2 人,那么可少租1 只船,设计一个算法用蛮力法求该年级的最多人数?
9.已知:若一个合数的质因数分解式逐位相加之和等于其本身逐位相加之和,则称这个数为Smith 数。如4937775=3*5*5*65837,而3+5+5+6+5+8+3+7=42,4+9+3+7+7+7+5=42,所以4937775 是Smith 数。求给定一个正整数N,求大于N 的最小Smith 数。
输入:若干个case,每个case 一行代表正整数N,输入0 表示结束
输出:大于N 的最小Smith数
输入样例:
4937774
0
样例输出:
31




算法设计
4937775
10.求解涂棋盘问题。小易有一块n*n 的棋盘,棋盘的每一个格子都为黑色或者白色,小易现在要用他喜欢的红色去涂画棋盘。小易会找出棋盘中某一列中拥有相同颜色的最大的区域去涂画,帮助小易算算他会涂画多少个棋格。
输入描述:输入数据包括n+1 行:第一行为一个整数n(1≤n≤50),即棋盘的大小,接下来的n 行每行一个字符串表示第i 行棋盘的颜色,'W'表示白色,'B'表示黑色。
输出描述:输出小易会涂画的区域大小。
输入例子:
3
BWW
BBB
BWB
输出例子:
3
11.给定一个含n(n>1)个整数元素的a,所有元素不相同,采用蛮力法求出a 中所有元素的全排列。
1.4.2练习题参考答案
1.答:蛮力法是一种简单直接地解决问题的方法,适用范围广,是能解决几乎所有问题的一般性方法,常用于一些非常基本、但又十分重要的算法(排序、查找、矩阵乘法和字符串匹配等),蛮力法主要解决一些规模小或价值低的问题,可以作为同样问题的更高效算法的一个标准。而分治法采用分而治之思路,把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题直到问题解决。分治法在求解问题时,通常性能比蛮力法好。
2.答:如果用蛮力法求解的问题可以分解为若干个规模较小的相似子问题,此时可以采用递归来实现算法。
3.解:上述算法的时间复杂度为O(n),采用的是最基本的蛮力法。可以先对a中元素递增排序,然后依次比较相邻元素的差,求出最小差,改进后的算法如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
intMindif1(inta[],intn)
{sort(a,a+n);
int dmin=a[1]-a[0];
for (int i=2;i<n;i++)
{int temp=a[i]-a[i-1];
if (temp<dmin)
dmin=temp;
}
return dmin;
}
//递增排序
32






第1章
概论
上述算法的主要时间花费在排序上,算法的时间复杂度为O(nlog2n)。
4.解:采用两重循环直接判断是否为逆序对,算法的时间复杂度为O(n2),比第3章实验3 算法的性能差。对应的算法如下:
intsolve(inta[],intn)
{int ans=0;
for (int i=0;i<n-1;i++)
for (int j=i+1;j<n;j++)
if (a[i]>a[j])
ans++;
return ans;
//求逆序数
}
5.解:直接采用蛮力法求解算法如下:
longf(intn)
{long fn=1;
for (int i=2;i<=n;i++)
fn=fn*i;
return fn;
}
longsolve(intn)
{long ans=0;
for (int i=1;i<=n;i++)
ans+=f(i);
return ans;
//求n!
//求1!+2!+…+n!
}
实际上,f(n)=f(n-1)*n,f(1)=1,在求f(n)时可以利用f(n-1)的结果。改进后的算法如
下:
longsolve1(intn)
{long ans=0;
long fn=1;
for (int i=1;i<=n;i++)
{fn=fn*i;
ans+=fn;
}
return ans;
//求1!+2!+…+n!
}
6.解:设鸡脚数为y=abc,兔脚数为z=def,有1≤a,d≤5,0≤b,c,e,f≤5,采用6 重循环,求出鸡只数x1=y/2(y 是2的倍数),兔只数x2=z/4(z 是4 的倍数),当x1=x2 时输出结果。对应的程序如下:
#include <stdio.h>
voidsolve()
{int a,b,c,d,e,f;
int x1,x2,y,z;
for (a=1;a<=5;a++)
for (b=0;b<=5;b++)
for (c=0;c<=5;c++)
33




算法设计
for (d=1;d<=5;d++)
for (e=0;e<=5;e++)
for (f=0;f<=5;f++)
{y=a*100+b*10+c;
z=d*100+e*10+f;
if (y%2!=0 || z%4!=0)
continue;
x1=y/2;
x2=z/4;
if (x1==x2)
//鸡脚数
//兔脚数
//鸡只数
//兔只数
printf("鸡只数:%d,兔只数:%d,鸡脚数:%d, 兔脚数:%d\n",x1,x2,y,z);
}
}
voidmain()
{printf("求解结果\n");
solve();
}
上述程序的执行结果如图1.29 所示。
图1.29程序执行结果
7.解:设该三位数为x=abc,有1≤a≤9,0≤b,c≤9,满足c>a,a>b,a+b+c=a*b*c。对应的程序如下:
#include <stdio.h>
voidsolve()
{int a,b,c;
for (a=1;a<=9;a++)
for (b=0;b<=9;b++)
for (c=0;c<=9;c++)
{if (c>a && a>b&&a+b+c==a*b*c)
printf("%d%d%d\n",a,b,c);
}
}
voidmain()
34








第1章
概论
{
printf("求解结果\n");solve();
}
上述程序的执行结果如图1.30 所示。
图1.30程序执行结果
8.解:设该年级的人数为x,租船数为y。因为每只船坐10 人正好多出2 个座位,则x=10*y-2;因为每只船多坐2 人即12 人时可少租1 只船(没有说恰好全部座位占满),有x+z=12*(y-1),z 表示此时空出的座位,显然z<12。让y 从1 到100(实际上y取更大范围的结果是相同的)、z从0到11 枚举,求出最大的x 即可。对应的程序如下:
#include <stdio.h>
intsolve()
{int x,y,z;
for (y=1;y<=100;y++)
for (z=0;z<12;z++)
if (10*y-2==12*(y-1)-z)
x=10*y-2;
return x;
}
voidmain()
{printf("求解结果\n");
printf("最多人数:%d\n",solve());
}
上述程序的执行结果如图1.31 所示。
图1.31程序执行结果
9.解:采用蛮力法求出一个正整数n的各位数字和sum1,以及n的所有质因数的数字和sum2,若sum1=sum2,即为Smitch数。从用户输入的n开始枚举,若是Smitch 数,输出,本次结束,否则n++继续查找大于n 的最小Smitch数。对应的完整程序如下:
#include <stdio.h>
intSum(intn)
{int sum=0;
while (n>0)
//求n的各位数字和
35



算法设计
{sum+=n%10;
n=n/10;
}
return sum;
}
boolsolve(intn)//判断n是否为Smitch数
{int m=2;
int sum1=Sum(n);
int sum2=0;
while (n>=m)
{if (n%m==0)//找到一个质因数m
{n=n/m;
sum2+=Sum(m);
}
else
m++;
}
if (sum1==sum2)
return true;
else
return false;
}
voidmain()
{int n;
while (true)
{scanf("%d",&n);
if (n==0) break;
while (!solve(n))
n++;
printf("%d\n",n);
}
}
10.解:采用蛮力法,统计每一列相邻相同颜色的棋格个数countj,在countj中求最大值。对应的程序如下:
#include <stdio.h>
#define MAXN 51
//问题表示
int n;
char board[MAXN][MAXN];
intgetMaxArea()
{int maxArea=0;
for (int j=0; j<n;j++){int countj=1;
//蛮力法求解算法
for (int i=1; i<n;i++)//统计第j列中相同颜色相邻棋格个数 {if (board[i][j]==board[i-1][j])
countj++;
else
countj=1;
}
36






第1章
概论
if (countj>maxArea)
maxArea=countj;
}
return maxArea;
}
intmain()
{scanf("%d",&n);
for (int i=0;i<n;i++)
scanf("%s",board[i]);
printf("%d\n",getMaxArea());
return 0;
}
11.解:与《教程》中求全排列类似,但需要将求1~n 的全排列改为按下标0~n-1 求a 的全排列(下标从0 开始)。采用非递归的程序如下:
#include <stdio.h>
#include <vector>
using namespace std;
vector<vector<int>> ps;
//存放全排列
voidInsert(vector<int>s,inta[],inti,vector<vector<int>> &ps1)//在每个集合元素中间插入i得到ps1
{vector<int>s1;
vector<int>::iteratorit;
for (int j=0;j<=i;j++)
{s1=s;
it=s1.begin()+j;
s1.insert(it,a[i]);
ps1.push_back(s1);
}
}
voidPerm(inta[],intn)
{vector<vector<int>> ps1;
vector<vector<int>>::iterator it;
vector<int> s,s1;
s.push_back(a[0]);
ps.push_back(s);
for (int i=1;i<n;i++)
{ps1.clear();
for (it=ps.begin();it!=ps.end();++it)
Insert(*it,a,i,ps1);
ps=ps1;
}
}
voiddispps()
//在s(含i个整数)的每个位置插入a[i] //求出插入位置
//插入整数a[i]
//添加到ps1中
//求a[0..n-1]的所有全排列
//临时存放子排列
//全排列迭代器
//添加{a[0]}集合元素
//循环添加a[1]~a[n-1]
//ps1存放插入a[i]的结果
//在每个集合元素中间插入a[i]得到ps1 //输出全排列ps
{vector<vector<int>>::reverse_iteratorit;//全排列的反向迭代器
vector<int>::iteratorsit;
//排列集合元素迭代器
for (it=ps.rbegin();it!=ps.rend();++it)
{for (sit=(*it).begin();sit!=(*it).end();++sit)
printf("%d",*sit);
printf("");
37




}
printf("\n");
}
voidmain()
{int a[]={2,5,8};
int n=sizeof(a)/sizeof(a[0]);printf("a[0~%d]的全排序如下:\nPerm(a,n);
dispps();
算法设计
",n-1);
}
上述程序的执行结果如图1.32 所示。
图1.32程序执行结果
1.5第5 章─回溯法
1.5.1练习题
1.回溯法在问题的解空间树中,按()策略,从根结点出发搜索解空间树。
A.广度优先B.活结点优先C.扩展结点优先D.深度优先
2.关于回溯法以下叙述中不正确的是()。
A.回溯法有“通用解题法”之称,它可以系统地搜索一个问题的所有解或任意解
B.回溯法是一种既带系统性又带有跳跃性的搜索算法
C.回溯算法需要借助队列这种结构来保存从根结点到当前扩展结点的路径
D.回溯算法在生成解空间的任一结点时,先判断该结点是否可能包含问题的解,如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向祖先结点回溯
3.回溯法的效率不依赖于下列哪些因素()。
A.确定解空间的时间C.计算约束函数的时间
B.满足显约束的值的个数D.计算限界函数的时间
4.下面()函数是回溯法中为避免无效搜索采取的策略。
A.递归函数B.剪枝函数C.随机数函数D.搜索函数
5.回溯法的搜索特点是什么?
6.用回溯法解0/1 背包问题时,该问题的解空间是何种结构?用回溯法解流水作业调度问题时,该问题的解空间是何种结构?
7.对于递增序列a[]={1,2,3,4,5},采用例5.4的回溯法求全排列,以1、2开头的排列一定最先出现吗?为什么?
8.考虑n 皇后问题,其解空间树为由1、2、…、n 构成的n!种排列所组成。现用回
38






第1章
概论
溯法求解,要求:
(1)通过解搜索空间说明n=3 时是无解的。
(2)给出剪枝操作。
(3)最坏情况下在解空间树上会生成多少个结点?分析算法的时间复杂度。9.设计一个算法求解简单装载问题,设有一批集装箱要装上一艘载重量为W 的轮
船,其中编号为i(0≤i≤n-1)的集装箱的重量为wi。现要从n 个集装箱中选出若干装上轮船,使它们的重量之和正好为W。如果找到任一种解返回true,否则返回false。
10.给定若干个正整数a0、a0 要求找选择元素个数最少的解。
、…、an-1
,从中选出若干数,使它们的和恰好为k,
11.设计求解有重复元素的排列问题的算法,设有n个元素a[]={a0,a1,…,an-1),其中可能含有重复的元素,求这些元素的所有不同排列。如a[]={1,1,2},输出结果是(1,1,2),(1,2,1),(2,1,1)。
12.采用递归回溯法设计一个算法求1~n的n个整数中取出m个元素的排列,要求每个元素最多只能取一次。例如,n=3,m=2的输出结果是(1,2),(1,3),(2,1),(2,3),(3,1),(3,2)。
13.对于n皇后问题,有人认为当n为偶数时,其解具有对称性,即n皇后问题的解个数恰好为n/2皇后问题的解个数的2倍,这个结论正确吗?请编写回溯法程序对n=4、6、8、10的情况进行验证。
14.给定一个无向图,由指定的起点前往指定的终点,途中经过所有其他顶点且只经过一次,称为哈密顿路径,闭合的哈密顿路径称作哈密顿回路(Hamiltoniancycle)。设计一个回溯算法求无向图的所有哈密顿回路。
1.5.2练习题参考答案
1.答:D。
2.答:回溯算法是采用深度优先遍历的,需要借助系统栈结构来保存从根结点到当前扩展结点的路径。答案为C。
3.答:回溯法解空间是虚拟的,不必确定整个解空间。答案为A。
4.答:B。
5.答:回溯法在解空间树中采用深度优先遍历方式进行解搜索,即用约束条件和限界函数考察解向量元素x[i]的取值,如果x[i]是合理的就搜索x[i]为根结点的子树,如果x[i]取完了所有的值,便回溯到x[i-1]。
6.答:用回溯法解0/1 背包问题时,该问题的解空间是子集树结构。用回溯法解流水作业调度问题时,该问题的解空间是排列树结构。
7.答:是的。对应的解空间是一棵排列树,如图1.33所示给出前面3 层部分,显然最先产生的排列是从G 结点扩展出来的叶子结点,它们就是以1、2 开头的排列。
39





















算法设计
A
1
2
3
4
5
B
C
D
E
F
2
3
4
5
G
H
I
图1.33
J
部分解空间树
8.答:(1)n=3 时的解搜索空间如图1.34所示,不能得到任何叶子结点,所有无解。
(2)剪枝操作是任何两个皇后不能同行、同列和同两条对角线。
(3)最坏情况下每个结点扩展n 个结点,共有n个结点,算法的时间复杂度为O(n)。
(*,*,*)
(1,*,*)
(1,3,*)
(2,*,*)
(3,*,*)
(3,1,*)
图1.34
3 皇后问题的解搜索空间
9.解:用数组w[0..n-1]存放n 个集装箱的重量,采用类似判断子集和是否存在解的方法求解。对应完整的求解程序如下:
#include <stdio.h>
#define MAXN 20
//问题表示
int n=5,W;
int w[]={2,9,5,6,3};
int count;
voiddfs(inttw,intrw,inti) {if (i>=n)
{if (tw==W)
count++;
}
else
//最多集装箱个数
//全局变量,累计解个数
//求解简单装载问题
//找到一个叶子结点
//找到一个满足条件的解,输出它//尚未找完
{
}
rw-=w[i];
if (tw+w[i]<=W)
dfs(tw+w[i],rw,i+1);
if (tw+rw>=W)
dfs(tw,rw,i+1);
//求剩余的集装箱重量和
//左孩子结点剪枝:选取满足条件的集装箱w[i] //选取第i个集装箱
//右孩子结点剪枝:剪除不可能存在解的结点 //不选取第i个集装箱,回溯
}
boolsolve()
//判断简单装载问题是否存在解40







第1章
概论
{count=0;
int rw=0;
for (int j=0;j<n;j++)
rw+=w[j];
dfs(0,rw,0);
if (count>0)
return true;
else
return false;
}
voidmain()
{printf("求解结果\n");
//求所有集装箱重量和rw//i从0开始
W=4;
printf("W=%d时%s\n",W,(solve()?"存在解":"没有解"));W=10;
printf("W=%d时%s\n",W,(solve()?"存在解":"没有解"));W=12;
printf("W=%d时%s\n",W,(solve()?"存在解":"没有解"));W=21;
printf("W=%d时%s\n",W,(solve()?"存在解":"没有解"));}
本程序执行结果如图1.35 所示。
图1.35
程序执行结果
10.解:这是一个典型的解空间为子集树的问题,采用子集树的回溯算法框架。当找到一个解后通过选取的元素个数进行比较求最优解minpath。对应的完整程序如下:
#include <stdio.h>
#include <vector>
using namespace std;
//问题表示
int a[]={1,2,3,4,5};
int n=5,k=9;
vector<int> minpath;
//求解结果表示
int minn=n;
voiddisppath()
{printf("选择的元素:");
//设置为全局变量 //存放最优解
//最多选择n个元素//输出一个解
for (int j=0;j<minpath.size();j++)
printf("%d ",minpath[j]);printf("元素个数=%d\n",minn);
}
41




算法设计
voiddfs(vector<int>path,intsum,intstart)//求解算法
{if (sum==k)
{if (path.size()<minn)
{minn=path.size();
minpath=path;
}
return;
}
if (start>=n) return;
dfs(path,sum,start+1);
//如果找到一个解,不一定到叶子结点 //全部元素找完,返回
//不选择a[start]
path.push_back(a[start]);//选择a[start] dfs(path,sum+a[start],start+1);
}
voidmain()
{vector<int>path;
dfs(path,0,0); printf("最优解:\n");disppath();
//path存放一个子集
}
上述程序的执行结果如图1.36 所示。
图1.36
程序执行结果
11.解:在回溯法求全排列的基础上,增加元素的重复性判断。例如,对于a[]={1,1,2},不判断重复性时输出(1,1,2),(1,2,1),(1,1,2),(1,2,1),(2,1,1),(2,1,1),共6个,有3 个是重复的。重复性判断是这样的,对于在扩展a[i]时,仅仅将与a[i..j-1]没有出现的元素a[j]交换到a[i]的位置,如果出现,对应的排列已经在前面求出了。对应的完整程序如下:
#include <stdio.h>
boolok(inta[],inti,intj)//ok用于判别重复元素
{if (j>i)
{for(int k=i;k<j;k++)
if (a[k]==a[j])
return false;
}
return true;
}
voidswap(int&x,int&y)
{int tmp=x;
x=y; y=tmp;
//交换两个元素
}
voiddfs(inta[],intn,inti)//求有重复元素的排列问题{if (i==n)
42







第1章
概论
{for(int j=0;j<n;j++)
printf("%3d",a[j]);
printf("\n");
}
else
{for (int j=i;j<n;j++)
if (ok(a,i,j))//选取与a[i..j-1]不重复的元素a[j] {swap(a[i],a[j]);
dfs(a,n,i+1);
swap(a[i],a[j]);
}
}
}
voidmain()
{int a[]={1,2,1,2};
int n=sizeof(a)/sizeof(a[0]);
printf("序列(");
for (int i=0;i<n-1;i++)
printf("%d ",a[i]);
printf("%d)的所有不同排列:\n",a[n-1]);
dfs(a,n,0);
}
上述程序的执行结果如图1.37 所示。
图1.37
程序执行结果
12.解:采用求全排列的递归框架。选取的元素个数用i 表示(i 从1 开始),当i>m时达到一个叶子结点,输出一个排列。为了避免重复,用used 数组实现,used[i]=0表示没有选择整数i,used[i]=1 表示已经选择整数i。对应的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAXN 20
#define MAXM 10
int m,n;
int x[MAXM];
bool used[MAXN];
voiddfs(inti)
{if (i>m)
{for (int j=1;j<=m;j++)
printf("%d",x[j]);
printf("\n");
//x[1..m]存放一个排列
//求n个元素中m个元素的全排列//输出一个排列
43




算法设计
}
else
{for (int j=1;j<=n;j++)
{if (!used[j])
{used[j]=true;
x[i]=j;
dfs(i+1);
used[j]=false;
}
}
}
}
voidmain()
{n=4,m=2;
memset(used,0,sizeof(used));printf("n=%d,m=%d的求解结果\n",n,m); dfs(1);
}
上述程序的执行结果如图1.38 所示。
//修改used[i]
//x[i]选择j
//继续搜索排列的下一个元素//回溯:恢复used[i]
//初始化为0
//i从1开始
图1.38
程序执行结果
13.解:这个结论不正确。验证程序如下:#include <stdio.h>
#include <stdlib.h>
#define MAXN 10
int q[MAXN];
bool place(int i)
{int j=1;
if (i==1) returntrue;
while (j<i)
//测试第i行的q[i]列上能否摆放皇后 //j=1~i-1是已放置了皇后的行
{if ((q[j]==q[i])|| (abs(q[j]-q[i])==abs(j-i)))
//该皇后是否与以前皇后同列,位置(j,q[j])与(i,q[i])是否同对角线 return false;
j++;
}
return true;
}
44







第1章
概论
int Queens(int n)
{int count=0,k;
int i=1;
q[1]=0;
while (i>0)
{q[i]++;
//求n皇后问题的解个数//计数器初始化
//i为当前行
//q[i]为皇后i的列号//移到下一列
while (q[i]<=n &&!place(i))
q[i]++;
if (q[i]<=n)
{if (i==n)
count++;//找到一个解计数器count加1 else
{
i++;; q[i]=0;
}
}
else i--;
}
return count;
}
void main()
{printf("验证结果如下:\n");for (int n=4;n<=10;n+=2)
//回溯
if (Queens(n)==2*Queens(n/2))
printf("n=%d: 正确\n",n);
else
printf("n=%d: 错误\n",n);
}
上述程序的执行结果如图1.39所示。从执行结果看出结论是不正确的。
图1.39
程序执行结果
14.解:假设给定的无向图有n个顶点(顶点编号从0 到n-1),采用邻接矩阵数组a(0/1 矩阵)存放,求从顶点v出发回到顶点v 的哈密顿回路。采用回溯法,解向量为x[0..n],x[i]表示第i步找到的顶点编号(i=n-1 时表示除了起点v 外其他顶点都查找了),初始时将起点v 存放到x[0],i 从1 开始查找,i>0时循环:为x[i]找到一个合适的顶点,当i=n-1 时,若顶点x[i]到顶点v有边对应一个解;否则继续查找下一个顶点。如果不能为x[i]找到一个合适的顶点,则回溯。采用非递归回溯框架(与《教程》中求解n 皇后问题的非递归回溯框架类似)的完整程序如下:
#include <stdio.h>
#define MAXV 10
45



//求解问题表示 int n=5;
算法设计
//图中顶点个数
int a[MAXV][MAXV]={{0,1,1,1,0},{1,0,0,1,1},{1,0,0,0,1},{1,1,0,0,1},{0,1,1,1,0}};
//邻接矩阵数组
//求解结果表示
int x[MAXV];
int count;
voiddispasolution()
{for (int i=0;i<=n-1;i++)
//输出一个解路径
printf("(%d,%d) ",x[i],x[i+1]);
printf("\n");
}
boolvalid(inti)
//判断顶点第i个顶点x[i]的有效性
{if (a[x[i-1]][x[i]]!=1)//x[i-1]到x[i]没有边,返回false
return false;
for (int j=0;j<=i-1;j++)
if (x[i]==x[j])//顶点i重复出现,返回falsereturn false;
return true;
}
voidHamiltonian(intv)
{x[0]=v;
int i=1;
x[i]=-1;
while (i>0)
{x[i]++;
//求从顶点v出发的哈密顿回路//存放起点
//从顶点-1+1=0开始试探
//尚未回溯到头,循环
while (!valid(i)&& x[i]<n)
x[i]++;
if (x[i]<n)
//试探一个顶点x[i]
//找到一个有效的顶点x[i]
{if (i==n-1)//达到叶子结点
{if (a[x[i]][v]==1)
{x[n]=v;//找到一个解
printf("第%d个解:",count++);dispasolution();
}
}
else
{
i++; x[i]=-1;
}
}
else
i--;
//回溯
}
}
void main()
{printf("求解结果\n");
for (int v=0;v<n;v++)
{printf("从顶点%d出发的哈密顿回路:\n",v); count=1;
46












第1章
概论
Hamiltonian(v);
//从顶点v出发
}
}
上述程序对如图1.40所示的无向图求从每个顶点出发的哈密顿回路,程序执行结果如图1.41 所示。
1
0
3
2
4
图1.40一个无向图
图1.41
程序执行结果
1.6第6 章─分枝限界法
1.6.1练习题
1.分枝限界法在问题的解空间树中,按()策略,从根结点出发搜索解空间树。A.广度优先B.活结点优先C.扩展结点优先D.深度优先
2.常见的两种分枝限界法为()。
A.广度优先分枝限界法与深度优先分枝限界法
47



算法设计
B.队列式(FIFO)分枝限界法与堆栈式分枝限界法
C.排列树法与子集树法
D.队列式(FIFO)分枝限界法与优先队列式分枝限界法
3.分枝限界法求解0/1 背包问题时,活结点表的组织形式是()。
A.小根堆B.大根堆C.栈
4.采用最大效益优先搜索方式的算法是()。A.分支界限法B.动态规划法C.贪心法
D.数组
D.回溯法
5.优先队列式分枝限界法选取扩展结点的原则是()。
A.先进先出B.后进先出C.结点的优先级D.随机
6.简述分枝限界法的搜索策略。
7.有一个0/1 背包问题,其中n=4,物品重量为(4,7,5,3),物品价值为(40,42,25,12),背包最大载重量W=10,给出采用优先队列式分枝限界法求最优解的过程。
8.有一个流水作业调度问题,n=4,a[]={5,10,9,7},b[]={7,5,9,8},给出采用优先队列式分枝限界法求一个解的过程。
9.有一个含n个顶点(顶点编号为0~n-1)的带权图,采用邻接矩阵数组A表示,采用分枝限界法求从起点s 到目标点t 的最短路径长度,以及具有最短路径长度的路径条数。
10.采用优先队列式分枝限界法求解最优装载问题。给出以下装载问题的求解过程和结果:n=5,集装箱重量为w=(5,2,6,4,3),限重为W=10。在装载重量相同时,最优装载方案是集装箱个数最少的方案。
1.6.2练习题参考答案
1.答:A。
2.答:D。
3.答:B。
4.答:A。
5.答:C。
6.答:分枝限界法的搜索策略是广度优先遍历,通过限界函数可以快速找到一个解或者最优解。
7.答:求解过程如下:
(1)根结点1 进队,对应结点值:e.i=0,e.w=0,e.v=0,e.ub=76,x:[0,0,0,0]。(2)出队结点1:左孩子结点2 进队,对应结点值:e.no=2,e.i=1,e.w=4,
e.v=40,e.ub=76,x:[1,0,0,0];右孩子结点3进队,对应结点值:e.no=3,e.i=1,e.w=0,e.v=0,e.ub=57,x:[0,0,0,0]。
(3)出队结点2:左孩子超重;右孩子结点4 进队,对应结点值:e.no=4,e.i=2,e.w=4,e.v=40,e.ub=69,x:[1,0,0,0]。
(4)出队结点4:左孩子结点5 进队,对应结点值:e.no=5,e.i=3,e.w=9,e.v=65,e.ub=69,x:[1,0,1,0];右孩子结点6进队,对应结点值:e.no=6,e.i=3,e.w=4,e.v=40,e.ub=52,x:[1,0,0,0]。
48






第1章
概论
(5)出队结点5:产生一个解,maxv= 65,bestx:[1,0,1,0]。
(6)出队结点3:左孩子结点8 进队,对应结点值:e.no=8,e.i=2,e.w=7,e.v=42,e.ub=57,x:[0,1,0,0];右孩子结点9被剪枝。
(7)出队结点8:左孩子超重;右孩子结点10 被剪枝。
(8)出队结点6:左孩子结点11 超重;右孩子结点12 被剪枝。
(9)队列空,算法结束,产生的最优解:maxv= 65,bestx:[1,0,1,0]。
8.答:求解过程如下:
(1)根结点1 进队,对应结点值:e.i=0,e.f1=0,e.f2=0,e.lb=29,x:[0,0,0,0]。
(2)出队结点1:扩展结点如下:
进队(j=1):结点2,e.i=1,e.f1=5,e.f2=12,e.lb=27,x:[1,0,0,0]。
进队(j=2):结点3,e.i=1,e.f1=10,e.f2=15,e.lb=34,x:[2,0,0,0]。进队(j=3):结点4,e.i=1,e.f1=9,e.f2=18,e.lb=29,x:[3,0,0,0]。
进队(j=4):结点5,e.i=1,e.f1=7,e.f2=15,e.lb=28,x:[4,0,0,0]。
(3)出队结点2:扩展结点如下:
进队(j=2):结点6,e.i=2,e.f1=15,e.f2=20,e.lb=32,x:[1,2,0,0]。进队(j=3):结点7,e.i=2,e.f1=14,e.f2=23,e.lb=27,x:[1,3,0,0]。进队(j=4):结点8,e.i=2,e.f1=12,e.f2=20,e.lb=26,x:[1,4,0,0]。(4)出队结点8:扩展结点如下:
进队(j=2):结点9,e.i=3,e.f1=22,e.f2=27,e.lb=31,x:[1,4,2,0]。进队(j=3):结点10,e.i=3,e.f1=21,e.f2=30,e.lb=26,x:[1,4,3,0]。(5)出队结点10,扩展一个j=2的子结点,有e.i=4,到达叶子结点,产生的一个解
是e.f1=31,e.f2=36,e.lb=31,x=[1,4,3,2]。
该解对应的调度方案是:第1 步执行作业1,第2 步执行作业4,第3步执行作业3,第4 步执行作业2,总时间=36。
9.解:采用优先队列式分枝限界法求解,队列中结点的类型如下:
struct NodeType
{int vno;
int length;
bool operator<(constNodeType &s)const {return length>s.length;}
//顶点的编号
//当前结点的路径长度//重载<关系函数 //length越小越优先
};
从顶点s 开始广度优先搜索,找到目标点t 后比较求最短路径长度及其路径条数。对应的完整程序如下:
#include <stdio.h>
#include <queue>
using namespacestd;
#define MAX 11
#define INF 0x3f3f3f3f
//问题表示
int A[MAX][MAX]={
//一个带权有向图
49



{0,1,4,INF,INF},
{INF,0,INF,1,5},
{INF,INF,0,INF,1},{INF,INF,2,0,3},
{INF,INF,INF,INF,INF} };
int n=5;
//求解结果表示
int bestlen=INF;
int bestcount=0;
struct NodeType
{int vno;
int length;
算法设计
//最优路径的路径长度//最优路径的条数
//顶点的编号
//当前结点的路径长度
bool operator<(constNodeType &s)const//重载>关系函数
{return length>s.length;}
};
voidsolve(ints,intt)
{NodeType e,e1;priority_queue<NodeType>qu;
e.vno=s;
e.length=0;
qu.push(e);
while (!qu.empty())
{e=qu.top();qu.pop();
if (e.vno==t)
{if (e.length<bestlen)
{bestcount=1;
bestlen=e.length;
}
else if (e.length==bestlen)
bestcount++;
}
else
{for (int j=0;j<n;j++)
//length越小越优先
//求最短路径问题
//定义2个结点
//定义一个优先队列qu//构造根结点
//根结点进队
//队不空循环
//出队结点e作为当前结点//e是一个叶子结点
//比较找最优解
//保存最短路径长度
//e不是叶子结点
//检查e的所有相邻顶点
if (A[e.vno][j]!=INF&& A[e.vno][j]!=0)//顶点e.vno到顶点j有边{if (e.length+A[e.vno][j]<bestlen)//剪枝
{e1.vno=j;
e1.length=e.length+A[e.vno][j];
qu.push(e1);
}
}
}
}
}
voidmain()
{int s=0,t=4;
solve(s,t);
if (bestcount==0)
printf("顶点%d到%d没有路径\n",s,t); else
{printf("顶点%d到%d存在路径\n",s,t);
50
//有效子结点e1进队







第1章
概论
printf("最短路径长度=%d,条数=%d\n", bestlen,bestcount);//输出:5 3
}
}
上述程序的执行结果如图1.39 所示。
图1.39
程序执行结果
10.解:采用优先队列式分枝限界法求解。设计优先队列priority_queue<NodeType>,并设计优先队列的关系比较函数Cmp,指定按结点的ub值进行比较,即ub 值越大的结点越先出队。对应的完整程序如下:
#include <stdio.h>
#include <queue>
using namespace std;
#define MAXN 21
//问题表示
int n=5;
int W=10;
int w[]={0,5,2,6,4,3};//求解结果表示
int bestw=0;
int bestx[MAXN];
int Count=1;
typedef struct
{int no;
int i;
int w;
int x[MAXN];
int ub;
} NodeType;
struct Cmp
//最多的集装箱数
//集装箱重量,不计下标0的元素//存放最大重量,全局变量
//存放最优解,全局变量
//搜索空间中结点数累计,全局变量//结点编号
//当前结点在解空间中的层次
//当前结点的总重量
//当前结点包含的解向量
//上界
//队列中关系比较函数
{bool operator()(constNodeType&s,const NodeType&t)
{return (s.ub<t.ub)|| (s.ub==t.ub&& s.x[0]>t.x[0]);//ub越大越优先,当ub相同时x[0]越小越优先
}
};
voidbound(NodeType&e)
{int i=e.i+1;
int r=0;
while (i<=n)
{r+=w[i];
i++;
}
e.ub=e.w+r;
//计算分枝结点e的上界
//r为剩余集装箱的重量51



}
voidLoading()
{NodeType e,e1,e2;
算法设计
//求装载问题的最优解//定义3个结点
priority_queue<NodeType,vector<NodeType>,Cmp> qu;//定义一个优先队列qu
e.no=Count++;
e.i=0;
e.w=0;
for (int j=0; j<=n;j++) e.x[j]=0;
bound(e);
qu.push(e);
while (!qu.empty())
{e=qu.top();qu.pop();
if (e.i==n)
//设置结点编号
//根结点置初值,其层次计为0//初始化根结点的解向量
//求根结点的上界
//根结点进队
//队不空循环
//出队结点e作为当前结点//e是一个叶子结点
{if ((e.w>bestw)|| (e.w==bestw&& e.x[0]<bestx[0]))//比较找最优解
{bestw=e.w;
//更新bestw
for (int j=0;j<=e.i;j++)
bestx[j]=e.x[j];//复制解向量e.x->bestx
}
}
else
{if (e.w+w[e.i+1]<=W)
{e1.no=Count++;
e1.i=e.i+1;
//e不是叶子结点 //检查左孩子结点 //设置结点编号 //建立左孩子结点
e1.w=e.w+w[e1.i];
for (int j=0; j<=e.i;j++)
e1.x[j]=e.x[j];//复制解向量e.x->e1.x
e1.x[e1.i]=1;
e1.x[0]++;
bound(e1);
qu.push(e1);
}
e2.no=Count++;
e2.i=e.i+1;
//选择集装箱i
//装入集装箱数增1//求左孩子结点的上界//左孩子结点进队
//设置结点编号
//建立右孩子结点
e2.w=e.w;
for (int j=0; j<=e.i;j++)//复制解向量e.x->e2.xe2.x[j]=e.x[j];
e2.x[e2.i]=0;
bound(e2);
if (e2.ub>bestw)
qu.push(e2);
}
}
}
voiddisparr(intx[],intlen)
{for (int i=1;i<=len;i++)
printf("%2d",x[i]);
}
voiddispLoading()
{printf("X=[");
//不选择集装箱i
//求右孩子结点的上界
//若右孩子结点可行,则进队,否则被剪枝//输出一个解向量
//输出最优解
52









第1章
概论
disparr(bestx,n);
printf("],装入总价值为%d\n",bestw); }
voidmain()
{Loading();
printf("求解结果:\n");dispLoading();
}
上述程序的执行结果如图1.40 所示。
//输出最优解
图1.40
程序执行结果
1.7第7 章─贪心法
1.7.1练习题
1.下面是贪心算法的基本要素的是()。
A.重叠子问题B.构造最优解C.贪心选择性质D.定义最优解
2.下面问题()不能使用贪心法解决。
A.单源最短路径问题B.n 皇后问题C.最小花费生成树问题D.背包问题
3.采用贪心算法的最优装载问题的主要计算量在于将集装箱依其重量从小到大排序,故算法的时间复杂度为()。
A.O(n)
B.O(n)
C.O(n)
D.O(nlog2n)
4.关于0/1背包问题以下描述正确的是()。
A.可以使用贪心算法找到最优解
B.能找到多项式时间的有效算法
C.使用教材介绍的动态规划方法可求解任意0-1 背包问题
D.对于同一背包与相同的物品,做背包问题取得的总价值一定大于等于做0/1 背包问
题
5.一棵哈夫曼树共有215个结点,对其进行哈夫曼编码,共能得到()个不同的码
字。
A.107
B.108
C.214
D.215
6.求解哈夫曼编码中如何体现贪心思路?
7.举反例证明0/1背包问题若使用的算法是按照vi/wi 的非递减次序考虑选择的物品,即只要正在被考虑的物品装得进就装入背包,则此方法不一定能得到最优解(此题说明0/1 背包问题与背包问题的不同)。
53















































































算法设计
8.求解硬币问题。有1分、2分、5 分、10 分、50 分和100 分的硬币各若干枚,现在要用这些硬币来支付W 元,最少需要多少枚硬币。
9.求解正整数的最大乘积分解问题。将正整数n分解为若干个互不相同的自然数之和,使这些自然数的乘积最大。
10.求解乘船问题。有n 个人,第i个人体重为wi(0≤i<n)。每艘船的最大载重量均为C,且最多只能乘两个人。用最少的船装载所有人。
11.求解会议安排问题。有一组会议A 和一组会议室B,A[i]表示第i 个会议的参加人数,B[j]表示第j 个会议室最多可以容纳的人数。当且仅当A[i]≤B[j]时,第j个会议室可以用于举办第i 个会议。给定数组A和数组B,试问最多可以同时举办多少个会议。例如,A[]={1,2,3},B[]={3,2,4},结果为3;若A[]={3,4,3,1},B[]={1,2,2,6},结果为2.
12.假设要在足够多的会场里安排一批活动,n 个活动编号为1~n,每个活动有开始
时间bi
和结束时间ei(1≤i≤n)。设计一个有效的贪心算法求出最少的会场个数。
13.给定一个m×n的数字矩阵,计算从左到右走过该矩阵且经过的方格中整数最小的路径。一条路径可以从第1 列的任意位置出发,到达第n 列的任意位置,每一步为从第i 列走到第i+1 列相邻行(水平移动或沿45 度斜线移动),如图1.41所示。第1 行和最后一行看作是相邻的,即应当把这个矩阵看成是一个卷起来的圆筒。
图1.41每一步的走向
两个略有不同的5×6的数字矩阵的最小路径如图1.42 所示,只有最下面一行的数不同。右边矩阵的路径利用了第一行与最后一行相邻的性质。
输入:包含多个矩阵,每个矩阵的第一行为两个数m和n,分别表示矩阵的行数和列数,接下来的m×n 个整数按行优先的顺序排列,即前n 个数组成第一行,接下的n 个数组成第2 行,依此类推。相邻整数间用一个或多个空格分隔。注意这些数不一定是正数。输入中可能有一个或多个矩阵描述,直到输入结束。每个矩阵的行数在1到10 之间,列数在1 到100 之间。
输出:对每个矩阵输出两行,第一行为最小整数之和的路径,路径由n 个整数组成,表示路径经过的行号,如果这样的路径不止一条,输出字典序最小一条。
3 |
4 |
1 |
2 |
8 |
6 |
6 |
1 |
8 |
2 |
7 |
4 |
5 5 |
9 6 |
3 |
9 |
9 |
5 |
83 |
44 1 |
18 6 |
3 |
2 |
6 |
3 |
7 |
2 |
8 |
6 |
4 |
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 |
1 |
2 |
3 |
54






第1章
概论
6 1 8 2 7 4
5 9 3 9 9 5
8 4 1 3 2 6
3 7 2 8 6 4
输出结果:
1 2 3 4 4 5
16
1.7.2练习题参考答案
1.答:C。
2.答:n皇后问题的解不满足贪心选择性质。答案为B。
3.答:D。
4.答:由于背包问题可以取物品的一部分,所以总价值一定大于等于做0/1背包问题。答案为D。
5.答:这里n=215,哈夫曼树中n1=0,而n0=n2+1,n=n0+n1+n2=2n0-1,n0=(n+1)/2=108。答案为B。
6.答:在构造哈夫曼树时每次都是将两棵根结点最小的树合并,从而体现贪心的思路。
7.证明:例如,n=3,w={3,2,2},v={7,4,4},W=4 时,由于7/3最大,若按题目要求的方法,只能取第一个,收益是7。而此实例的最大的收益应该是8,取第2、3个物品。
8.解:用结构体数组A 存放硬币数据,A[i].v 存放硬币i 的面额,A[i].c 存放硬币i 的枚数。采用贪心思路,首先将数组A按面额递减排序,再兑换硬币,每次尽可能兑换面额大的硬币。对应的完整程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
#define min(x,y)((x)<(y)?(x):(y))
#define MAX 21
//问题表示
int n=7;
struct NodeType
{int v;
int c;
//面额
//枚数
bool operator<(constNodeType &s)
{
//用于按面额递减排序
return s.v<v;
}
};
NodeType A[]={{1,12},{2,8},{5,6},{50,10},{10,8},{200,1},{100,4}};
int W;
//求解结果表示
int ans=0;
voidsolve()
//兑换的硬币枚数//兑换硬币
55




{sort(A,A+n);
算法设计
//按面额递减排序
for (int i=0;i<n;i++)
{int t=min(W/A[i].v,A[i].c);//使用硬币i的枚数 if (t!=0)
printf("支付%3d面额:%3d枚\n",A[i].v,t);
W-=t*A[i].v;
ans+=t;
if (W==0) break;
}
}
voidmain()
{W=325;
//剩余的金额
//支付的金额
printf("支付%d分:\n",W);
solve();
printf("最少硬币的个数:%d枚\n",ans);}
上述程序的执行结果如图1.43 所示。
图1.43
程序执行结果
9.解:采用贪心方法求解。用a[0..k]存放n 的分解结果:
(1)n≤4 时可以验证其分解成几个正整数的和的乘积均小于n,没有解。
(2)n>4 时,把n分拆成若干个互不相等的自然数的和,分解数的个数越多乘积越大。为此让n 的分解数个数尽可能多(体现贪心的思路),把n分解成从2开始的连续的自然数之和。例如,分解n 为a[0]=2,a[1]=3,a[2]=4,…,a[k]=k+2(共有k+1 个分解数),用m表示剩下数,这样的分解直到m≤a[k]为止,即m≤k+2。对剩下数m 的处理分为如下两种情况:
①m<k+2:将m平均分解到a[k..i](对应的分解数个数为m)中,即从a[k]开始往前的分解数增加1(也是贪心的思路,分解数越大加1 和乘积也越大)。
②m=k+2:将a[0..k-1](对应的分解数个数为k)的每个分解数增加1,剩下的2 增加到a[k]中,即a[k]增加2。
对应的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAX 20
//问题表示
int n;
//求解结果表示
56







第1章
概论
int a[MAX];
int k=0;
voidsolve()
{int i;
int sum=1;
if (n<4)
return;
else
{int m=n;
a[0]=2;
m-=a[0];
k=0;
while (m>a[k])
{k++;
//存放被分解的数
//a[0..k]存放被分解的数
//求解n的最大乘积分解问题
//不存在最优方案,直接返回
//m表示剩下数
//第一个数从2开始
//减去已经分解的数
//若剩下数大于最后一个分解数,则继续分解 //a数组下标+1
a[k]=a[k-1]+1;//按2、3、4递增顺序分解
m-=a[k];
}
if (m<a[k])
//减去最新分解的数
//若剩下数小于a[k],从a[k]开始往前的数+1
{for (i=0; i<m;i++)
a[k-i]+=1;
}
if (m==a[k])
{a[k]+=2;
//若剩下数等于a[k],则a[k]的值+2,之前的数+1
for (i=0; i<k; i++)
a[i]+=1;
}
}
}
voidmain()
{n=23;
memset(a,0,sizeof(a));
solve();
printf("%d的最优分解方案\n",n);int mul=1;
printf("分解的数:");
for (int i=0;i<=k;i++)
if (a[i]!=0)
{printf("%d ",a[i]);
mul*=a[i];
}
printf("\n乘积最大值:%d\n",mul); }
上述程序的执行结果如图1.44 所示。
57



图1.44
算法设计
程序执行结果
10.解:采用贪心思路,首先按体重递增排序;再考虑前后的两个人(最轻者和最重者),分别用i、j 指向:若w[i]+w[j]≤C,说明这两个人可以同乘(执行i++,j--),否则w[j]单乘(执行j--),若最后只剩余一个人,该人只能单乘。
对应的完整程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
#define MAXN 101
//问题表示
int n=7;
int w[]={50,65,58,72,78,53,82};
int C=150;
//求解结果表示
int bests=0;
voidBoat()
{sort(w,w+n);
int i=0;
int j=n - 1;
while (i<=j)
{if(i==j)
//求解乘船问题//递增排序
//剩下最后一个人
{printf("一艘船:%d\n",w[i]); bests++;
break;
}
if (w[i]+w[j]<=C)//前后两个人同乘
{printf("一艘船:%d%d\n",w[i],w[j]); bests++;
i++;
j--;
}
else
//w[j]单乘
{printf("一艘船:%d\n",w[j]); bests++;
j--;
}
}
}
voidmain()
{printf("求解结果:\n");
Boat();
printf("最少的船数=%d\n",bests);
}
上述程序的执行结果如图1.45 所示。
58







第1章
概论
图1.45
程序执行结果
11.解:采用贪心思路。每次都在还未安排的容量最大的会议室安排尽可能多的参会人数,即对于每个会议室,都安排当前还未安排的会议中,参会人数最多的会议。若能容纳下,则选择该会议,否则找参会人数次多的会议来安排,直到找到能容纳下的会议。
对应的完整程序如下:
#include <stdio.h>
#include <algorithm>
using namespace std;
//问题表示
int n=4;
int m=4;
int A[]={3,4,3,1};
int B[]={1,2,2,6};//求解结果表示 int ans=0;
voidsolve()
{sort(A,A+n);
sort(B,B+m);
//会议个数 //会议室个数
//求解算法//递增排序 //递增排序
int i=n-1,j=m-1;//从最多人数会议和最多容纳人数会议室开始
for(i;i>=0;i--)
{if(A[i]<=B[j]&& j>=0)
{ans++;//不满足条件,增加一个会议室
j--;
}
}
}
voidmain()
{solve();
printf("%d\n",ans);//输出2
}
12.解:与《教程》例7.2类似,会场对应蓄栏,只是这里仅仅求会场个数,即最大兼容活动子集的个数。对应的完整程序如下:
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAX 51
//问题表示
struct Action
//活动的类型声明59



{int b;
int e;
算法设计
//活动起始时间 //活动结束时间
bool operator<(constAction &s) const//重载<关系函数
{if (e==s.e)
return b<=s.b;
else
return e<=s.e;
}
//结束时间相同按开始时间递增排序//否则按结束时间递增排序
};
int n=5;
Action A[]={{0},{1,10},{2,4},{3,6},{5,8},{4,7}};//下标0不用 //求解结果表示
int ans;
voidsolve()
{bool flag[MAX];
memset(flag,0,sizeof(flag));
sort(A+1,A+n+1);
ans=0;
for (int j=1;j<=n;j++)
{if (!flag[j])
{flag[j]=true;
int preend=j;
//最少会场个数
//求解最大兼容活动子集//活动标志
//A[1..n]按指定方式排序//会场个数
//前一个兼容活动的下标
for (int i=preend+1;i<=n;i++)
{if (A[i].b>=A[preend].e&& !flag[i]){preend=i;
flag[i]=true;
}
}
ans++;
//增加一个最大兼容活动子集
}
}
}
voidmain()
{solve();
printf("求解结果\n");
printf("最少会场个数:%d\n",ans);//输出4
}
13.解:采用贪心思路。从第1 列开始每次查找a[i][j]元素上、中、下3 个对应数中的最小数。对应的程序如下:
#include <stdio.h>
#define M 12
#define N 110
int m=5, n=6;
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}};
int minRow,minCol;
intminValue(inti,intj)
//求a[i][j]有方上、中、下3个数的最小数,同时要把行标记录下来
{
int s = (i == 0)? m - 1 : i - 1; int x = (i == m -1)?0:i+1;
60






第1章
概论
minRow = s;
minRow = a[i][j+1]< a[minRow][j+1]?i:minRow;
minRow = a[x][j+1]< a[minRow][j+1]?x:minRow;
minRow = a[minRow][j+1]== a[s][j+1]&& minRow > s?s:minRow;minRow = a[minRow][j+1]== a[i][j+1]&& minRow > i?i:minRow;minRow = a[minRow][j+1]== a[x][j+1]&& minRow > x?x:minRow;return a[minRow][j+1];
}
voidsolve()
{int i,j,min;
for (j=n-2; j>=0;j--)
for (i=0; i<m; i++)
a[i][j]+= minValue(i,j);
min=a[0][0];
minRow=0;
for (i=1; i<m; i++)
if (a[i][0]<min)
{min=a[i][0];
minRow=i;
}
for (j=0; j<n; j++)
{printf("%d",minRow+1);
if (j<n-1) printf("");
minValue(minRow,j);
}
printf("\n%d\n",min);
}
voidmain()
{
solve();
}
//在第一列查找最小代价的行
1.8第8 章─动态规划
1.8.1练习题
1.下列算法中通常以自底向上的方式求解最优解的是()。
A.备忘录法B.动态规划法C.贪心法D.回溯法2.备忘录方法是()算法的变形。
A.分治法
B.回溯法
C.贪心法D.动态规划法
3.下列是动态规划算法基本要素的是()。
A.定义最优解B.构造最优解C.算出最优解D.子问题重叠性质4.一个问题可用动态规划算法或贪心算法求解的关键特征是问题的()。
A.贪心选择性质B.重叠子问题C.最优子结构性质D.定义最优解5.简述动态规划法的基本思路。
6.简述动态规划法与贪心法的异同。
61













算法设计
7.简述动态规划法与分治法的异同。
8.下列算法中哪些属于动态规划算法?
(1)顺序查找算法
(2)直接插入排序算法
(3)简单选择排序算法
(4)二路归并排序算法
9.某个问题对应的递归模型如下:
f(1)=1
f(2)=2
f(n)=f(n-1)+f(n-2)+…+f(1)+1
可以采用如下递归算法求解:longf(intn)
{if (n==1) return1;
if (n==2) return2;
long sum=1;
for (int i=1;i<=n-1;i++)
sum+=f(i);
return sum;
当n>2 时
}
但其中存在大量的重复计算,请采用备忘录方法求解。
10.第3 章中的实验4采用分治法求解半数集问题,如果直接递归求解会存在大量重复计算,请改进该算法。
11.设计一个时间复杂度为O(n)的算法来计算二项式系数Cn(k≤n)。二项式系数Cn的求值过程如下:
𝐶=1
𝐶=1
𝑗𝑗‒1𝑗
𝑖𝑖‒1𝑖‒1
当i≥j
12.一个机器人只能向下和向右移动,每次只能移动一步,设计一个算法求它从(0,0)移动到(m,n)有多少条路径。
13.两种水果杂交出一种新水果,现在给新水果取名,要求这个名字中包含了以前两种水果名字的字母,并且这个名字要尽量短。也就是说以前的一种水果名字arr1 是新水果名字arr 的子序列,另一种水果名字arr2 也是新水果名字arr 的子序列。设计一个算法求arr。
例如:输入以下3组水果名称:
apple peach
ananas banana
pear peach
输出的新水果名称如下:
62






第1章
概论
appleach
bananas
pearch
1.8.2练习题参考答案
1.答:B。
2.答:D。
3.答:D。
4.答:C。
5.答:动态规划法的基本思路是将待求解问题分解成若干个子问题,先求子问题的解,然后从这些子问题的解得到原问题的解。
6.答:动态规划法的3个基本要素是最优子结构性质、无后效性和重叠子问题性质,而贪心法的两个基本要素是贪心选择性质和最优子结构性质。所以两者的共同点是都要求问题具有最优子结构性质。
两者的不同点如下:
(1)求解方式不同,动态规划法是自底向上的,有些具有最优子结构性质的问题只能用动态规划法,有些可用贪心法。而贪心法是自顶向下的。
(2)对子问题的依赖不同,动态规划法依赖于各子问题的解,所以应使各子问题最优,才能保证整体最优;而贪心法依赖于过去所作过的选择,但决不依赖于将来的选择,也不依赖于子问题的解。
7.答:两者的共同点是将待求解的问题分解成若干子问题,先求解子问题,然后再从这些子问题的解得到原问题的解。
两者的不同点是:适合于用动态规划法求解的问题,分解得到的各子问题往往不是相互独立的(重叠子问题性质),而分治法中子问题相互独立;另外动态规划法用表保存已求解过的子问题的解,再次碰到同样的子问题时不必重新求解,而只需查询答案,故可获得多项式级时间复杂度,效率较高,而分治法中对于每次出现的子问题均求解,导致同样的子问题被反复求解,故产生指数增长的时间复杂度,效率较低。
8.答:判断算法是否具有最优子结构性质、无后效性和重叠子问题性质。(2)、(3)和(4)均属于动态规划算法。
9.解:设计一个dp 数组,dp[i]对应f(i)的值,首先dp 的所有元素初始化为0,在计算f(i)时,若dp[0]>0表示f(i)已经求出,直接返回dp[i]即可,这样避免了重复计算。对应的算法如下:
long dp[MAX];//dp[n]保存f(n)的计算结果
longf1(intn)
{if (n==1)
{dp[n]=1;
return dp[n];
}
if (n==2)
{dp[n]=2;
return dp[n];
63





算法设计
}
if (dp[n]>0) returndp[n];
long sum=1;
for (int i=1;i<=n-1;i++)
sum+=f1(i);
dp[n]=sum;
return dp[n];
}
10.解:设计一个数组a,其中a[i]=f(i),首先将a的所有元素初始化为0,当a[i]>0时表示对应的f(i)已经求出,直接返回就可以了。对应的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAXN 201
//问题表示
int n;
int a[MAXN];
intfa(inti)
{int ans=1;
if (a[i]>0)
return a[i];
for(int j=1;j<=i/2;j++)
ans+=fa(j);
a[i]=ans;
return ans;
}
intsolve(int n)
{memset(a,0,sizeof(a));
a[1]=1;
return fa(n);
}
voidmain()
{n=6;
//求a[i]
//求set(n)的元素个数
printf("求解结果\n");
printf("n=%d时半数集元素个数=%d\n",n,solve(n));
}
11.解:定义C(i,j)=Ci
,i≥j。则有如下递推计算公式:C(i,nj)=C(i-1,j-1)+C(i-
1,j),初始条件为C(i,0)=1,C(i,i)=1。可以根据初始条件由此递推关系计算C(n,k),
n
即Cn。对应的程序如下:
#include <stdio.h>
#define MAXN 51
#define MAXK 31
//问题表示
int n,k;
//求解结果表示
int C[MAXN][MAXK];
voidsolve()
{int i,j;
64







第1章
概论
for (i=0;i<=n;i++)
{C[i][i]=1;
C[i][0]=1;
}
for (i=1;i<=n;i++)
for (j=1;j<=k;j++)
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
voidmain()
{n=5,k=3;
solve();
printf("%d\n",C[n][k]);//输出10
}
显然,solve()算法的时间复杂度为O(n)。
12.解:设从(0,0)移动到(i,j)的路径条数为dp[i][j],由于机器人只能向下和向右移动,不同于迷宫问题(迷宫问题由于存在后退,不满足无后效性,不适合用动态规划法求解)。对应的状态转移方程如下:
dp[0][j]=1
dp[i][0]=1
dp[i][j]=dp[i][j-1]+dp[i-1][j]
i、j>0
最后结果是dp[m][n]。对应的程序如下:
#include <stdio.h>
#include <string.h>
#define MAXX 51
#define MAXY 51
//问题表示
int m,n;
//求解结果表示
int dp[MAXX][MAXY];
voidsolve()
{int i,j;
dp[0][0]=0;
memset(dp,0,sizeof(dp));
for (i=1;i<=m;i++)
dp[i][0]=1;
for (j=1;j<=n;j++)
dp[0][j]=1;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
dp[i][j]=dp[i][j-1]+dp[i-1][j];
}
voidmain()
{m=5,n=3;
solve();
printf("%d\n",dp[m][n]);
}
13.解:本题目的思路是求arr1 和arr2字符串的最长公共子序列,基本过程参见《教
65



程》第8 章8.5 节。对应的完整程序如下:#include <iostream>
#include <string.h>
#include <vector>
#include <string>
using namespace std;
#define max(x,y)((x)>(y)?(x):(y))
#define MAX 51
//问题表示
int m,n;
string arr1,arr2;
//求解结果表示
int dp[MAX][MAX];
vector<char> subs;
voidLCSlength()
{int i,j;
for (i=0;i<=m;i++)
dp[i][0]=0;
for (j=0;j<=n;j++)
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
算法设计
//序列中最多的字符个数
//动态规划数组
//存放LCS
//求dp
//将dp[i][0]置为0,边界条件
//将dp[0][j]置为0,边界条件
//两重for循环处理arr1、arr2的所有字符
{if (arr1[i-1]==arr2[j-1])//比较的字符相同
dp[i][j]=dp[i-1][j-1]+1;
else
//比较的字符不同
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
}
}
voidBuildsubs()
{int k=dp[m][n];
int i=m;
int j=n;
while (k>0)
//由dp构造从subs
//k为arr1和arr2的最长公共子序列长度
//在subs中放入最长公共子序列(反向)
if (dp[i][j]==dp[i-1][j])i--;
else if (dp[i][j]==dp[i][j-1])j--;
else
{subs.push_back(arr1[i-1]);//subs中添加arr1[i-1] i--; j--; k--;
}
}
voidmain()
{cin >> arr1>> arr2; m=arr1.length();
n=arr2.length();
LCSlength();
Buildsubs();
cout << "求解结果"<<endl;
cout << "arr:";vector<char>::reverse_iteratorrit;
//输入arr1和arr2 //m为arr1的长度 //n为arr2的长度 //求出dp
//求出LCS
66






第1章
for (rit=subs.rbegin();rit!=subs.rend();++rit)
cout << *rit;
cout << endl;
cout << "长度:"<<dp[m][n]<< endl;
概论
}
改为如下:
13.解:本题目的思路是先求arr1 和arr2 字符串的最长公共子序列,基本过程参见《教程》第8 章8.5节,再利用递归输出新水果取名。
算法中设置二维动态规划数组dp,dp[i][j]表示arr1[0..i-1](i 个字母)和arr2[0..j-1](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]。
对应的完整程序如下:
#include <stdio.h>
#include <string.h>
#define MAX 51
//问题表示
int m,n;
char arr1[MAX],arr2[MAX];
//求解结果表示
int dp[MAX][MAX];
int b[MAX][MAX];
void Output(int i,intj)
{if (i==0 &&j==0)
return;
if(i==0)
{Output(i,j-1);
printf("%c",arr2[j-1]);
return;
}
else if(j==0)
{Output(i-1,j);
printf("%c",arr1[i-1]);
return;
}
if (b[i][j]==0)
{Output(i-1,j-1);
printf("%c",arr1[i-1]);
return;
}
else if(b[i][j]==1)
{Output(i-1,j);
printf("%c",arr1[i-1]);
return;
}
else
{Output(i,j-1);
//序列中最多的字符个数
//动态规划数组
//存放arr1与arr2比较的3种情况//利用递归输出新水果取名
//输出完毕
//arr1完毕,输出arr2的剩余部分
//arr2完毕,输出arr1的剩余部分
//arr1[i-1]=arr2[j-1]的情况
67



算法设计
printf("%c",arr2[j-1]);
return;
}
}
void LCSlength()
{int i,j;
for (i=0;i<=m;i++)
dp[i][0]=0;
for (j=0;j<=n;j++)
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
{if (arr1[i-1]==arr2[j-1])
{dp[i][j]=dp[i-1][j-1]+1;
b[i][j]=0;
}
else if (dp[i-1][j]>dp[i][j-1])
{dp[i][j]=dp[i-1][j];
b[i][j]=1;
}
else
{dp[i][j]=dp[i][j-1];
b[i][j]=2;
}
}
}
void main()
{int t;
printf("测试用例个数:");scanf("%d",&t);
while(t--)
{scanf("%s",arr1);
scanf("%s",arr2);
memset(b,-1,sizeof(b));
m=strlen(arr1);
n=strlen(arr2);
LCSlength();
printf("结果: ");Output(m,n); printf("\n");
}
}
上述程序的一次执行结果如图1.46 所示。
//求dp
//将dp[i][0]置为0,边界条件
//将dp[0][j]置为0,边界条件
//两重for循环处理arr1、arr2的所有字符 //比较的字符相同:情况0
//情况1
//dp[i-1][j]<=dp[i][j-1]:情况2
//输入测试用例个数
//m为arr1的长度
//n为arr2的长度
//求出dp
//输出新水果取名
68







第1章
概论
图1.46
程序的一次执行结果
13.解:本题目的思路是求arr1 和arr2字符串的最长公共子序列,基本过程参见《教程》第8 章8.5 节。对应的完整程序如下:
然后再用递归思想,逐一输出,得到的就是最后答案。
#include <iostream>
#include <string.h>
#include <vector>
#include <string>
using namespace std;
#define max(x,y)((x)>(y)?(x):(y))
#define MAX 51
//问题表示
int m,n;
string arr1,arr2;
//求解结果表示
int dp[MAX][MAX];
vector<char> subs;
voidLCSlength()
{int i,j;
for (i=0;i<=m;i++)
dp[i][0]=0;
for (j=0;j<=n;j++)
dp[0][j]=0;
for (i=1;i<=m;i++)
for (j=1;j<=n;j++)
//序列中最多的字符个数
//动态规划数组
//存放LCS
//求dp
//将dp[i][0]置为0,边界条件
//将dp[0][j]置为0,边界条件
//两重for循环处理arr1、arr2的所有字符
{if (arr1[i-1]==arr2[j-1])//比较的字符相同
dp[i][j]=dp[i-1][j-1]+1;
else
//比较的字符不同
dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
}
}
voidBuildsubs()
{int k=dp[m][n];
int i=m;
int j=n;
while (k>0)
//由dp构造从subs
//k为arr1和arr2的最长公共子序列长度
//在subs中放入最长公共子序列(反向)
if (dp[i][j]==dp[i-1][j])i--;
else if (dp[i][j]==dp[i][j-1])j--;
69




算法设计
else
{subs.push_back(arr1[i-1]);//subs中添加arr1[i-1] i--; j--; k--;
}
}
voidmain()
{cin >> arr1>> arr2; m=arr1.length();
n=arr2.length();
LCSlength();
Buildsubs();
//输入arr1和arr2 //m为arr1的长度 //n为arr2的长度 //求出dp
//求出LCS
cout << "求解结果"<<endl;
cout << "arr:";
vector<char>::reverse_iteratorrit;
for (rit=subs.rbegin();rit!=subs.rend();++rit)
cout << *rit;
cout << endl;
cout << "长度:"<<dp[m][n]<< endl;
}
上述程序的一次执行结果如图1.46所示。
图1.46
程序的一次执行结果
1.9第9 章─图算法设计
1.9.1练习题
1.以下不属于贪心算法的是()。
A.Prim 算法B.Kruskal 算法C.Dijkstra算法D.深度优先遍历2.一个有n 个顶点的连通图的生成树是原图的最小连通子图,且包含原图中所有n
个顶点,并且有保持图联通的最少的边。最大生成树就是权和最大生成树,现在给出一个无向带权图的邻接矩阵为{{0,4,5,0,3},{4,0,4,2,3},{5,4,0,2,0},{0,2,2,0,1},{3,3,0,1,0}},其中权为0 表示没有边。一个图为求这个图的最大生成树的权和是()。
A.11
B.12
C.13
D.14
E.15
3.某个带权连通图有4个以上的顶点,其中恰好有2 条权值最小的边,尽管该图的最小生成树可能有多个,而这2 条权值最小的边一定包含在所有的最小生成树中吗?如果有3 条权值最小的边呢?
70
















第1章
概论
4.为什么TSP 问题采用贪心算法求解不一定得到最优解?
5.求最短路径的4 种算法适合带权无向图吗?
6.求单源最短路径的算法有Dijkstra 算法、Bellman-Ford 算法和SPFA 算法,比较这些算法的不同点。
7.有人这样修改Dijkstra 算法以便求一个带权连通图的单源最长路径,将每次选择dist 最小的顶点u改为选择最大的顶点u,将按路径长度小进行调整改为按路径长度大调整。这样可以求单源最长路径吗?
8.给出一种方法求无环带权连通图(所有权值非负)中从顶点s 到顶点t的一条最长简单路径。
9.一个运输网络如图1.47 所示,边上数字为(c(i,j),b(i,j)),其中c(i,j)表示容量,b(i,j)表示单位运输费用。给出从1、2、3 位置运输货物到位置6的最小费用最大流的过程。
10.本教程中的Dijkstra 算法采用邻接矩阵存储图,算法时间复杂度为O(n)。请你从各个方面考虑优化该算法,用于求源点v 到其他顶点的最短路径长度。
11.有一个带权有向图G(所有权为正整数),采用邻接矩阵存储。设计一个算法求其中的一个最小环。
1
(4,2)
(6,4)
4
(12,3)
2
(3,5)
6
(3,4)
(1,6)
5
(9,12)
3
(2,3)
图1.47
一个运输网络
1.9.2练习题参考答案
1.答:D。
2.答:采用类似Kurskal 算法来求最大生成树,第1 步取最大边(0,2),第2步取边(0,1),第3步取边(0,4),第4步取最大边(1,3),得到的权和为14。答案为D。
3.答:这2 条权值最小的边一定包含在所有的最小生成树中,因为按Kurskal算法一定首先选中这2 条权值最小的边。如果有3 条权值最小的边,就不一定了,因为首先选中这3 条权值最小的边有可能出现回路。
4.答:TSP 问题不满足最优子结构性质,如(0,1,2,3,0)是整个问题的最优解,但(0,1,2,0)不一定是子问题的最优解。
5.答:都适合带权无向图求最短路径。
6.答:Dijkstra 算法不适合存在负权边的图求单源最短路径,其时间复杂度为O(n)。Bellman-Ford 算法和SPFA 算法适合存在负权边的图求单源最短路径,但图中不能
71
















算法设计
存在权值和为负的环。Bellman-Ford 算法的时间复杂度为O(ne),而SPFA算法的时间复杂度为O(e),所以SPFA 算法更优。
7.答:不能。Dijkstra算法本质上是一种贪心算法,而求单源最长路径不满足贪心选择性质。
8.答:Bellman-Ford 算法和SPFA算法适合存在负权边的图求单源最短路径。可以将图中所有边权值改为负权值,求出从顶点s 到顶点t的一条最短简单路径,它就是原来图中从顶点s 到顶点t的一条最长简单路径。
9.答:为该运输网络添加一个虚拟起点0,它到1、2、3 位置运输费用为0,容量分别为到1、2、3 位置运输容量和,如图1.48 所示,起点s=0,终点t=6。
1
(4,2)
(10,0)
(6,4)
4
(12,3)
0
(6,0)
2
(3,5)
6
(3,0)
(3,4)
(1,6)
5
(9,12)
3
(2,3)
图1.48
添加一个虚拟起点的运输网络
首先初始化f 为零流,最大流量maxf=0,最小费用mincost=0,采用最小费用最大流算法求解过程如下:
(1)k=0,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
∞ |
0 |
∞ |
∞ |
2 |
4 |
∞ |
∞ |
∞ |
0 |
∞ |
5 |
4 |
∞ |
∞ |
∞ |
∞ |
0 |
6 |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
求出从起点0 到终点6 的最短路径为0→1→4→6,求出最小调整量=4,f[4][6]调整为4,f[1][4]调整为4,f[0][1]调整为4,mincost=20,maxf=4。
(2)k=1,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
∞ |
∞ |
0 |
∞ |
5 |
4 |
∞ |
∞ |
∞ |
∞ |
0 |
6 |
3 |
∞ |
∞ |
-2 |
∞ |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
求出从起点0 到终点6 的最短路径为0→2→4→6,求出最小调整量=3,f[4][6]调整
72






第1章
概论
为7,f[2][4]调整为3,f[0][2]调整为3,mincost=44,maxf=4+3=7。(3)k=2,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
∞ |
∞ |
∞ |
0 |
6 |
3 |
∞ |
∞ |
-2 |
-5 |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
求出从起点0 到终点6 的最短路径为0→3→4→6,求出最小调整量=1,f[4][6]调整为8,f[3][4]调整为1,f[0][3]调整为1,mincost=53,maxf=7+1=8。
(4)k=3,求出w如下:
0 |
0 |
0 |
0 |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
∞ |
0 |
∞ |
3 |
∞ |
∞ |
-2 |
-5 |
-6 |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
求出从起点0 到终点6 的最短路径为0→3→5→6,求出最小调整量=2,f[5][6]调整为2,f[3][5]调整为2,f[0][3]调整为3,mincost=83,maxf=8+2=10。
(5)k=4,求出w如下:
0 |
0 |
0 |
∞ |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
∞ |
0 |
∞ |
∞ |
∞ |
∞ |
-2 |
-5 |
-6 |
0 |
∞ |
3 |
∞ |
∞ |
∞ |
-3 |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
-12 |
0 |
求出从起点0 到终点6 的最短路径为0→1→5→6,求出最小调整量=6,f[5][6]调整为8,f[1][5]调整为6,f[0][1]调整为10,mincost=179,maxf=10+6=16。
(6)k=5,求出w如下:
0 |
∞ |
0 |
∞ |
∞ |
∞ |
∞ |
0 |
0 |
∞ |
∞ |
∞ |
∞ |
∞ |
0 |
∞ |
0 |
∞ |
∞ |
4 |
∞ |
0 |
∞ |
∞ |
0 |
∞ |
∞ |
∞ |
∞ |
-2 |
-5 |
-6 |
0 |
∞ |
3 |
∞ |
-4 |
∞ |
-3 |
∞ |
0 |
12 |
∞ |
∞ |
∞ |
∞ |
-3 |
-12 |
0 |
求出从起点0 到终点6 的最短路径为0→1→5→6,求出最小调整量=1,f[5][6]调整
73



算法设计
为9,f[2][5]调整为1,f[0][2]调整为4,mincost=195,maxf=16+1=17。(7)k=6,求出的w中没有增广路径,调整结束。对应的最大流如下:
0 |
10 |
4 |
3 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
4 |
6 |
0 |
0 |
0 |
0 |
0 |
3 |
1 |
0 |
0 |
0 |
0 |
0 |
1 |
2 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
8 |
0 |
0 |
0 |
0 |
0 |
0 |
9 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
最终结果,maxf=17,mincost=195。即运输的最大货物量为17,对应的最小总运输费用为195。
10.解:从两个方面考虑优化:
(1)在Dijkstra 算法中,当求出源点v 到顶点u 的最短路径长度后,仅仅调整从顶点u 出发的邻接点的最短路径长度,而教程中的Dijkstra算法由于采用邻接矩阵存储图,需要花费O(n)的时间来调整顶点u 出发的邻接点的最短路径长度,如果采用邻接表存储图,可以很快查找到顶点u 的所有邻接点并进行调整,时间为O(MAX(图中顶点的出度))。
(2)求目前一个最短路径长度的顶点u 时,教科书上的Dijkstra 算法采用简单比较方法,可以改为采用优先队列(小根堆)求解。由于最多e 条边对应的顶点进队,对应的时间为O(log2e)。
对应的完整程序和测试数据算法如下:
#include "Graph.cpp"
#include <queue>
#include <string.h>
using namespace std;
ALGraph *G;
struct Node
{int i;
int v;
//包含图的基本运算算法
//图的邻接表存储结构,作为全局变量 //声明堆中结点类型
//顶点编号
//dist[i]值
friend bool operator<(constNode &a,const Node &b)//定义比较运算符 {returna.v >b.v;}
};
voidDijkstra(intv,intdist[])//改进的Dijkstra算法
{ArcNode *p;
priority_queue<Node>qu;
Node e;
int S[MAXV];
//创建小根堆
//S[i]=1表示顶点i在S中,S[i]=0表示顶点i在U中
int i,j,u,w;
memset(S,0,sizeof(S));
p=G->adjlist[v].firstarc;
for (i=0;i<G->n;i++)dist[i]=INF;
while (p!=NULL)
{w=p->adjvex;
74






第1章
概论
dist[w]=p->weight;
e.i=w; e.v=dist[w];
qu.push(e);
p=p->nextarc;
}
S[v]=1;
for (i=0;i<G->n-1;i++)
//距离初始化
//将v的出边顶点进队qu
//源点编号v放入S中
//循环直到所有顶点的最短路径都求出
{e=qu.top();qu.pop();//出队e
u=e.i;
S[u]=1;
p=G->adjlist[u].firstarc;
while (p!=NULL)
{w=p->adjvex;
if (S[w]==0)
//选取具有最小最短路径长度的顶点u
//顶点u加入S中
//考察从顶点u出发的所有相邻点
//考虑修改不在S中的顶点w的最短路径长度
if (dist[u]+p->weight<dist[w])
{dist[w]=dist[u]+p->weight;//修改最短路径长度 e.i=w; e.v=dist[w];
qu.push(e);//修改最短路径长度的顶点进队
}
p=p->nextarc;
}
}
}
voidDisppathlength(intv,intdist[])//输出最短路径长度
{printf("从%d顶点出发的最短路径长度如下:\n",v);
for (int i=0;i<G->n;++i)
if (i!=v)
printf("到顶点%d:%d\n",i,dist[i]);
}
voidmain()
{int A[MAXV][MAXV]={
{0,4,6,6,INF,INF,INF},
{INF,0,1,INF,7,INF,INF},
{INF,INF,0,INF,6,4,INF},
{INF,INF,2,0,INF,5,INF},
{INF,INF,INF,INF,0,INF,6},
{INF,INF,INF,INF,1,0,8},
{INF,INF,INF,INF,INF,INF,0}};
int n=7, e=12;
CreateAdj(G,A,n,e);printf("图G的邻接表:\n");DispAdj(G);
int v=0;
int dist[MAXV];
Dijkstra(v,dist);
Disppathlength(v,dist);
DestroyAdj(G);
//建立图的邻接表 //输出邻接表
//调用Dijkstra算法//输出结果
//销毁图的邻接表
}
上述程序的执行结果如图1.49 所示。
75




算法设计
图1.49程序执行结果
其中Dijkstra 算法的时间复杂度为O(n(log2e+MAX(顶点的出度)),一般图中最大顶点出度远小于e,所以进一步简化时间复杂度为O(nlog2e)。
11.有一个带权有向图G(所有权为正整数),采用邻接矩阵存储。设计一个算法求其中的一个最小环。
解:利用Floyd 算法求出所有顶点对之间的最短路径,若顶点i 到j 有最短路径,而图中又存在顶点j到i的边,则构成一个环,在所有环中比较找到一个最小环并输出。对应的程序如下:
#include "Graph.cpp"
//包含图的基本运算算法
#include <vector>
using namespace std;
voidDispapath(intpath[][MAXV],inti,intj) //输出顶点i到j的一条最短路径
{vector<int>apath;
int k=path[i][j];
apath.push_back(j);
while (k!=-1 && k!=i)
{apath.push_back(k);
k=path[i][k];
}
apath.push_back(i);
//存放一条最短路径中间顶点(反向)//路径上添加终点
//路径上添加中间点
//路径上添加起点
for (int s=apath.size()-1;s>=0;s--)//输出路径上的中间顶点printf("%d→",apath[s]);
}
intMincycle(MGraphg,intA[MAXV][MAXV],int&mini,int&minj)//在图g和A中的查找一个最小环
{int i,j,min=INF;
for (i=0;i<g.n;i++)
for (j=0;j<g.n;j++)
if (i!=j && g.edges[j][i]<INF)
{if (A[i][j]+g.edges[j][i]<min)
{min=A[i][j]+g.edges[j][i];
mini=i; minj=j;
76






第1章
概论
}
}
return min;
}
voidFloyd(MGraphg)
{int A[MAXV][MAXV],path[MAXV][MAXV];
int i,j,k,min,mini,minj;
for (i=0;i<g.n;i++)
for (j=0;j<g.n;j++)
{A[i][j]=g.edges[i][j];
//Floyd算法求图g中的一个最小环
if (i!=j && g.edges[i][j]<INF)
path[i][j]=i;
else
path[i][j]=-1;
}
for (k=0;k<g.n;k++)
{for (i=0;i<g.n;i++)
for (j=0;j<g.n;j++)
//顶点i到j有边时
//顶点i到j没有边时
//依次考察所有顶点
if (A[i][j]>A[i][k]+A[k][j])
{A[i][j]=A[i][k]+A[k][j];//修改最短路径长度 path[i][j]=path[k][j];//修改最短路径
}
}
min=Mincycle(g,A,mini,minj);
if (min!=INF)
{printf("图中最小环:");
Dispapath(path,mini,minj);printf("%d, 长度:%d\n",mini,min);
}
else printf("图中没有任何环\n");
}
voidmain()
{MGraph g;
//输出一条最短路径
int A[MAXV][MAXV]={{0,5,INF,INF},{INF,0,1,INF},
{3,INF,0,2}, {INF,4,INF,0}};
int n=4, e=5;
CreateMat(g,A,n,e);printf("图G的邻接矩阵:\n");DispMat(g);
Floyd(g);
//建立图的邻接矩阵//输出邻接矩阵
}
上述程序的执行结果如图1.50 所示。
77





算法设计
图1.50程序执行结果
1.10第10章─计算几何
1.10.1练习题
1.对如图1.51 所示的点集A,给出采用Graham 扫描算法求凸包的过程及结果。
10
9
8
7
6
5
4
3
2
1
a8
a11
a9
a0
a6
a7
a5
a3
a10
a4
a1
a2
12345
678910
图1.51一个点集A
2.对如图1.51 所示的点集A,给出采用分治法求最近点对的过程及结果。
3.对如图1.51 所示的点集A,给出采用旋转卡壳法求最远点对的结果。
4.对应3 个点向量p1、p2、p3,采用S(p1,p2,p3)=(p2-p1)(p3-p1)/2求它们构成的三角形面积,请问什么情况下计算结果为正?什么情况下计算结果为负?
5.已知坐标为整数,给出判断平面上一点p 是否在一个逆时针三角形p1-p2-p3的算法。
1.10.2练习题参考答案
内部
1.答:采用Graham 扫描算法求凸包的过程及结果如下:
求出起点a0(1,1)。
排序后:a0(1,1)a1(8,1)a2(9,4)a3(5,4)a4(8,7)a5(5,6)a10(7,10)a9(3,5) a6(3,7) a7(4,10)a8(1,6)a11(0,3)。
先将a0(1,1)进栈,a1(8,1)进栈,a2(9,4)进栈。
处理点a3(5,4):a3(5,4)进栈。
处理点a4(8,7):a3(5,4)存在右拐关系,退栈,a4(8,7)进栈。
78






第1章
概论
处理点a5(5,6):a5(5,6)进栈。
处理点a10(7,10):a5(5,6)存在右拐关系,退栈,a10(7,10)进栈。
处理点a9(3,5):a9(3,5)进栈。
处理点a6(3,7):a9(3,5)存在右拐关系,退栈,a6(3,7)进栈。
处理点a7(4,10):a6(3,7)存在右拐关系,退栈,a7(4,10)进栈。
处理点a8(1,6):a8(1,6)进栈。
处理点a11(0,3):a11(0,3)进栈。
结果:n=8,凸包的顶点:a0(1,1)a1(8,1)a2(9,4)a4(8,7)a10(7,10)a7(4,10) a8(1,6) a11(0,3)。
2.答:求解过程如下:
排序前:(1,1)(8,1)(9,4)(5,4)(8,7)(5,6)(3,7)(4,10)(1,6)(3,5)(7,10)(0,3)。按x 坐标排序后:(0,3)(1,1)(1,6)(3,7)(3,5)(4,10)(5,4)(5,6)(7,10) (8,1)(8,7)(9,4)。按y 坐标排序后:(1,1)(8,1)(0,3)(5,4)(9,4)(3,5)(1,6) (5,6) (3,7) (8,7) (4,10) (7,10)。
(1)中间位置midindex=5,左部分:(0,3)(1,1)(1,6)(3,7)(3,5)(4,10);右部分:(5,4)(5,6)(7,10)(8,1)(8,7)(9,4);中间部分点集为(0,3)(3,7)(4,10)(5,4) (5,6) (7,10) (8,7)。
(2)求解左部分:(0,3) (1,1) (1,6) (3,7) (3,5)(4,10)。
中间位置=2,划分为左部分1:(0,3)(1,1)(1,6),右部分1:(3,7) (3,5) (4,10)处理左部分1:点数少于4:求出最近距离=2.23607,即(0,3)和(1,1)之间的距离。处理右部分1:点数少于4:求出最近距离=2,即(3,7)和(3,5)之间的距离。再考虑中间部分(中间部分最近距离=2.23)求出左部分d1=2。
(3)求解右部分:(5,4) (5,6) (7,10) (8,1) (8,7)(9,4)。
中间位置=8,划分为左部分2:(5,4)(5,6)(7,10),右部分2:(8,1)(8,7)(9,4)。
处理左部分2:点数少于4,求出最近距离=2,即(5,4)和(5,6)之间的距离。处理右部分2:点数少于4,求出最近距离=3.16228,即(8,1)和(9,4)之间的距离。再考虑中间部分(中间部分为空)求出右部分d2=2。
(4)求解中间部分点集:(0,3)(3,7)(4,10)(5,4)(5,6)(7,10)(8,7)。求出最近距离d3=5。
最终结果为:d=MIN{d1,d2,d3)=2。
3.答:采用旋转卡壳法求出两个最远点对是(1,1)和(7,10),最远距离为10.82。
4.答:当三角形p1-p2-p3
逆时针方向时,如图1.52 所示,p2-p1
在p3-p1
的顺时针方
向上,(p2-p1)(p3-p1)>0,对应的面积(p2-p1)(p3-p1)/2为正。
当三角形p1-p2-p3
顺时针方向时,如图1.53 所示,p2-p1
在p3-p1
的逆时针方向上,
(p2-p1)(p3-p1)<0,对应的面积(p2-p1)(p3-p1)/2为负。
79























算法设计
p1
p3-p1
p2-p1
p1
p2-p1
p3-p1
图1.52
p1-p2-p3
逆时针方向图
图1.53p1-p2-p3
逆时针方向
5.答:用S(p1,p2,p3)=(p2-p1)(p3-p1)/2求三角形p1、p2、p3 带符号的的面积。如图1.54 所示,若S(p,p2,p3)和S(p,p3,p1)和S(p,p1,p2)(3 个三角形的方向均为逆时针方向)均大于0,表示p 在该三角形内部。
p3
p1
p
p2
图1.54
一个点p 和一个三角形
对应的程序如下:
#include "Fundament.cpp"
//包含向量基本运算算法
doublegetArea(Pointp1,Pointp2,Pointp3)//求带符号的面积
{
return Det(p2-p1,p3-p1);
}
boolIntrig(Pointp,Pointp1,Pointp2,Pointp3)//判断p是否在三角形p1p2p3的内部{double area1=getArea(p,p2,p3);
double area2=getArea(p,p3,p1);
double area3=getArea(p,p1,p2);
if (area1>0 && area2>0&& area3>0)
return true;
else
return false;
}
voidmain()
{printf("求解结果\n");
Point p1(0,0);
Point p2(5,-4);
Point p3(4,3);
Point p4(3,1);
Point p5(-1,1);
printf("p1:");p1.disp(); printf("\n");
printf("p2:");p2.disp(); printf("\n");
printf("p3:");p3.disp(); printf("\n");
printf("p4:");p4.disp(); printf("\n");
80







第1章
概论
printf("
printf("
printf("
printf("
p5:"); p5.disp();printf("\n");
p1p2p3三角形面积:%g\n",getArea(p1,p2,p3));
p4在p1p2p3三角形内部:%s\n",Intrig(p4,p1,p2,p3)?"是":"不是");p5在p1p2p3三角形内部:%s\n",Intrig(p5,p1,p2,p3)?"是":"不是");
}
上述程序的执行结果如图1.55 所示。
图1.55程序执行结果
1.11第11章─计算复杂性理论
1.11.1练习题
1.旅行商问题是NP 问题吗?
A.否
B.是
C.至今尚无定论
2.下面有关P 问题,NP问题和NPC 问题,说法错误的是()。
A.如果一个问题可以找到一个能在多项式的时间里解决它的算法,那么这个问题就属于P 问题
B.NP 问题是指可以在多项式的时间里验证一个解的问题
C.所有的P 类问题都是NP问题
D.NPC 问题不一定是个NP 问题,只要保证所有的NP问题都可以约化到它即可
3.对于《教程》例11.2 设计的图灵机,分别给出执行f(3,2)和f(2,3)的瞬像演变过程。
4.什么是P 类问题?什么是NP类问题?
5.证明求两个m 行n 列的二维矩阵相加的问题属于P 类问题。
6.证明求含有n 个元素的数据序列中求最大元素的问题属于P 类问题。
7.设计一个确定性图灵机M,用于计算后继函数S(n)=n+1(n为一个二进制数),并给出求1010001 的后继函数值的瞬像演变过程。
1.11.2练习题参考答案
1.答:B。
2.答:D。
3.答:(1)执行f(3,2)时,输入带上的初始信息为000100B,其瞬像演变过程如
81



算法设计
下:
q0000100BB0q30110BBB01q210B
Bq100100BBq300110BBB011q20B
B0q10100Bq3B00110BBB01q311B
B00q1100B
B001q200B
B00q3110B
Bq000110B
BBq10110B
BB0q1110B
BB0q3111B
BBq30111B
BBq00111B
BBB1q211B
BBB11q21B
BBB111q2B
BBB11q41B
BBB1q41BB
BBBq41BBB
BBBq4BBBB
BBB0q6BBB
最终带上有一个0,计算结果为1。
(2)执行f(2,3)时,输入带上的初始信息为001000B,其瞬像演变过程如下:
q0001000B
Bq001000B
B0q11000B
B01q2000B
B0q31100B
Bq301100B
q3B01100BBBq31100B
Bq001100BBq3B1100B
BBq11100BBBq01100B
BB1q2100BBBBq5100B
BB11q200BBBBBq500B
BB1q3100B
BBBBBq50B
BBBBBBq5B
BBBBBBBq6
最终带上有零个0,计算结果为0。
4.答:用确定性图灵机以多项式时间界可解的问题称为P 类问题。用非确定性图灵机以多项式时间界可解的问题称为NP类问题。
5.答:求两个m 行n列的二维矩阵相加的问题对应的算法时间复杂度为O(mn),所以属于P 类问题。
6.答:求含有n个元素的数据序列中最大元素的问题的算法时间复杂度为O(n),所以属于P 类问题。
7.解:q0 为初始状态,q3为终止状态,读写头初始时注视最右边的格。δ 动作函数如下:
δ(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)
求10100010 的后继函数值的瞬像演变过程如下:
B1010001q00B
B101000q111B
B10100q1011B
B1010q10011B
B101q100011B
B10q1100011B
B1q10100011B
Bq110100011B
q1B10100011B
q3BB10100011B
其结果为10100011。
82






第1章
概论
1.12第12章─概率算法和近似算法
1.12.1练习题
1.蒙特卡罗算法是()的一种。
A.分枝限界算法B.贪心算法C.概率算法D.回溯算法
2.在下列算法中有时找不到问题解的是()。
A.蒙特卡罗算法B.拉斯维加斯算法C.舍伍德算法D.数值概率算法3.在下列算法中得到的解未必正确的是()。
A.蒙特卡罗算法B.拉斯维加斯算法C.舍伍德算法D.数值概率算法4.总能求得非数值问题的一个解,且所求得的解总是正确的是()。
A.蒙特卡罗算法B.拉斯维加斯算法C.数值概率算法D.舍伍德算法5.目前可以采用()在多项式级时间内求出旅行商问题的一个近似最优解。
A.回溯法
B.蛮力法
C.近似算法D.都不可能
6.下列叙述错误的是()。
A.概率算法的期望执行时间是指反复解同一个输入实例所花的平均执行时间
B.概率算法的平均期望时间是指所有输入实例上的平均期望执行时间
C.概率算法的最坏期望事件是指最坏输入实例上的期望执行时间
D.概率算法的期望执行时间是指所有输入实例上的所花的平均执行时间
7.下列叙述错误的是()。
A.数值概率算法一般是求数值计算问题的近似解
B.Monte Carlo 总能求得问题的一个解,但该解未必正确
C.Las Vegas 算法的一定能求出问题的正确解
D.Sherwood 算法的主要作用是减少或是消除好的和坏的实例之间的差别
8.近似算法和贪心法有什么不同?
9.给定能随机生成整数1 到5 的函数rand5(),写出能随机生成整数1 到7 的函数rand7()。
1.12.2练习题参考答案
1.答:C。
2.答:B。
3.答:A。
4.答:D。
5.答:C。
6.答:对概率算法通常讨论平均的期望时间和最坏的期望时间,前者指所有输入实例上平均的期望执行时间,后者指最坏的输入实例上的期望执行时间。答案为D。
7.答:一旦用拉斯维加斯算法找到一个解,那么这个解肯定是正确的,但有时用拉斯维加斯算法可能找不到解。答案为C。
8.答:近似算法不能保证得到最优解。贪心算法不一定是近似算法,如果可以证明
83




算法设计
决策既不受之前决策的影响,也不影响后续决策,则贪心算法就是确定的最优解算法。9.解:通过rand5()*5+rand5()产生6,7,8,9,…,26,27,28,29,30 这25个整
数,每个整数x 出现的概率相等,取前面3*7=21 个整数,舍弃后面的4 个整数,将{6,7,8}转化成1,{9,10,11}转化成2,以此类推,即由y=(x-3)/3 为最终结果。对应的程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
intrand5()
{int a=1,b=5;
return rand()%(b-a+1)+a;
}
intrand7()
{int x;
do
{
x=rand5()*5+rand5();
} while (x>26);
int y=(x-3)/3;
return y;
}
voidmain()
{srand((unsigned)time(NULL));
for (int i=1;i<=20;i++)
printf("%d ",rand7());
printf("\n");
//包含产生随机数的库函数
//产生一个[1,5]的随机数
//产生一个[1,7]的随机数//随机种子
//输出20个[1,7]的随机数
}
上述程序的一次执行结果如图1.56 所示。
图1.56
程序执行结果
84