一、 递归与分治
1) 定义:程序直接或间接调用自身的编程技巧称为递归算法,而且递归分为直接调用和间接调用,它通常将复杂的问题层层转化为一个与原问题相似的但是规模相对较小的问题来求解。分治策略是对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同。分治法的适用条件:
i. 问题的规模缩小到一定的程度就可以容易地解决;
ii. 可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
iii. 利用该问题分解出的子问题的解可以合并为该问题的解;
iv. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
分治策略就是递归算法的一个分支而已。
2) 要求以及注意事项:递归需要有边界条件、递归前进段和递归返回段。
i. 当边界条件不满足时,递归前进;
ii. 当边界条件满足时,递归返回。
所以递归算法最重要的就是递归出口即边界条件,如果没有递归出口的话,那么递归就会一直进行下去。当然递归也会有一些缺点,例如:递归算法解题的运行效率较低,而且在递归调用过程中,系统为每一层的返回点、局部变量等开辟了堆栈来存储。递归次数过多容易造成堆栈溢出等。
3) 个人见解:在解决递归问题的之前,要先理清思路,确定递归的出口以及递归前进的条件。递归算法也没用快速掌握的方法,只能通过多做一些题目,熟悉这类题目的套路,也要多看看别人的见解以及做题的方法。
4) 经典例题:递归中最经典的例子就是FIBONACCI数列了
int fib[50]; //采用数组保存中间结果
void fibonacci(int n)
{
fib[0]= 1;
fib[1] =1;
for (int i=2; i<=n; i++)
fib[i] =fib[i-1]+fib[i-2];
}
这个代码是采用了数组保存中间结果的方法,如果给定的n已经存储了的话,那么直接返回就可以了,不用再递归算了。这是用牺牲空间来换取时间的方法。
二、 贪心算法
1) 定义:在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。从中可以看出:贪心算法只是求每一步的最优值,即是某种意义上的局部最优解。贪心标准就是每一步选择最优解的标准。
2) 要求以及注意事项:如果要使用贪心策略解题,需要解决两个问题:该题是否适合于用贪心策略求解(因为并不是所有问题都可以用贪心算法解决的,也不能保证总是有效的);如何选择贪心标准,以得到问题的最优/较优解(如果没用贪心标准的话,那么就不能确定每一步的最优/较优解)。贪心算法和动态规划的区别:在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择;在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解出这个选择后产生的相应的子问题。
3) 个人见解:当我们用贪心算法解决题目的时候应该首先要注意这道题目是不是能用贪心算法解决。当我们确定能用贪心算法解决时,然后再确定贪心标准,然后根据贪心标准确定每一步的最优/较优标准。
4) 经典例子:背包问题(利用贪心算法计算背包问题,同时得到解向量),按性价比从高到底的顺序选取物品,而且物品可以分割。
数据结构:struct bag{
int w; //物品的重量
int v; //物品的价值
doublex; //装入背包的量,0≤x≤1
int index; //物品编号
double c; //性价比
}a[1001]; //存放物品的数组
排序因子(按性价比降序):bool cmp(bag a, bag b){
return a.c>= b.c;}
使用标准模板库函数排序(最好使用stable_sort()函数,在性价比相同时保持输入的顺序):sort(a,a+n, cmp);
double knapsack(int n, bag a[], double c)
{
double cleft = c;
int i = 0;
double b = 0;
while(i<n && a[i].w<=cleft)
{
cleft -= a[i].w;
b += a[i].v;
//物品原先的序号是a[i].index,全部装入背包
a[a[i].index].x = 1.0;
i++;
}
if (i<n) {
a[a[i].index].x = 1.0*cleft/a[i].w;
b += a[a[i].index].x*a[i].v;
}
return b;
}
三、 回溯算法
1) 定义:回溯法是一种组织搜索的一般技术,有“通用的解题法”之称,用它可以系统的搜索一个问题的所有解或任一解。它的基本做法是用深度优先的方式搜索,即有系统性又有跳跃性。
2) 要求以及注意事项:用回溯法求解时,要明确定义问题的解空间。而且应至少包含问题的一个(最优)解。与解空间树相关的定义:
i.活结点:如果已生成一个结点而它的所有儿子结点还没有全部生成,则这个结点叫做活结点。
ii.扩展结点:当前正在生成其儿子结点的活结点叫扩展结点(正扩展的结点)。
iii.死结点:不再进一步扩展或者其儿子结点已全部生成的结点就是死结点。在回溯法搜索解空间树时,
通常采用两种策略(剪枝函数)避免无效搜索以提高回溯法的搜索效率:
i. 用约束函数在扩展结点处减去不满足约束条件的子树
ii. 用限界函数减去不能得到最优解的子树。
3) 基本过程:从根结点开始,深度优先遍历整个解空间。
i.这个开始结点成为一个活结点,同时成为当前的扩展结点。在当前的扩展结点,搜索向深度方向进入一个新的结点。这个新结点成为一个新的活结点,并成为当前的扩展结点。
ii.若在当前扩展结点处不能再向深度方向移动,则当前的扩展结点成为死结点,即该活结点成为死结点。此时回溯到最近的一个活结点处,并使得这个活结点成为当前的扩展结点。
iii.回溯法以这样的方式递归搜索整个解空间(树),直至满足中止条件。
4) 个人见解:回溯算法还是有难度的,所以需要下多点功夫。首先要将知识点看透,尤其是回溯的基本过程学会,然后做一些实际的题目来练习。而且要熟练掌握深度优先遍历的知识点。
5) 经典例子:0-1背包问题(回溯算法) :给定一个物品集合s={1,2,3,…,n},物品i的重量是wi,其价值是vi,背包的容量为W,即最大载重量不超过W。在限定的总重量W内,我们如何选择物品,才能使得物品的总价值最大。
输入:第一个数据是背包的容量为c(1≤c≤1500),第二个数据是物品的数量为n(1≤n≤50)。接下来n行是物品i的重量是wi,其价值为vi。所有的数据全部为整数,且保证输入数据中物品的总重量大于背包的容量。当c=0时,表示输入数据结束。
输出:对每组测试数据,输出装入背包中物品的最大价值.
输入样例 | 输出样例 |
50 3 | 220 |
10 60 | |
30 120 | |
20 100 | |
0 |
令cw(i)表示目前搜索到第i层已经装入背包的物品总重量,即部分解(x1,x2 , …, xi)的重量:
对于左子树, xi =1 ,其约束函数为:
若constraint(i)>W,则停止搜索左子树,否则继续搜索。
对于右子树,为了提高搜索效率,采用上界函数Bound(i)剪枝。
令cv(i)表示目前到第i层结点已经装入背包的物品价值:
令r(i)表示剩余物品的总价值:
则限界函数Bound(i)为:
假设当前最优值为bestv,若Bound(i)<bestv,则停止搜索第i层结点及其子树,否则继续搜索。显然r(i)越小, Bound(i)越小,剪掉的分支就越多。
为了构造更小的r(i) ,将物品以单位重量价值比di=vi/wi递减的顺序进行排列:
d1≥d2≥… ≥dn
对于第i层,背包的剩余容量为W-cw(i),采用贪心算法把剩余的物品放进背包,根据贪心算法理论,此时剩余物品的价值已经是最优的,因为对剩余物品不存在比上述贪心装载方案更优的方案。
数据结构:#define NUM 100
int c; //背包的容量
int n; //物品的数量
int cw; //当前重量
int cv; //当前价值
int bestv; //当前最优价值
//描述每个物品的数据结构
struct Object{
intw; //物品的重量
intv; //物品的价值
doubled; //物品的单位重量价值比
}Q[NUM]; //物品的数组
对物品以单位重量价值比递减排序的因子是:
bool cmp(Object a, Object b)
{
if(a.d>=b.d) return true;
else return false;
}
物品的单位重量价值比是在输入数据时计算的:
for(int i=0; i<n; i++)
{
scanf("%d%d",&Q[i].w,&Q[i].v);
Q[i].d = 1.0*Q[i].v/Q[i].w;
}
使用C++标准模板库的排序函数sort()排序:
sort(Q, Q+n, cmp);
//形参i是回溯的深度,从0开始
voidbacktrack(int i)
{
//到达叶子结点时,更新最优值
if(i+1>n) {bestv = cv; return;}
//进入左子树搜索
if(cw+Q[i].w<=c)
{
cw += Q[i].w;
cv += Q[i].v;
backtrack(i+1);
cw -= Q[i].w;
cv -= Q[i].v;
}
//进入右子树搜索
if(Bound(i+1)>bestv) backtrack(i+1);
}
//形参i是回溯的深度
int Bound(int i)
{
int cleft =c-cw; //背包剩余的容量
int b = cv; //上界
//尽量装满背包
while (i<n&& Q[i].w<=cleft)
{
cleft -= Q[i].w;
b += Q[i].v;
i++;
}
//剩余的部分空间也装满
if(i<n) b += 1.0*cleft*Q[i].v/Q[i].w;
returnb;
}
四、 分支限界算法
1) 定义:分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。基本思想是对有约束条件的最优化问题的所有可行解(数目有限)空间进行搜索。
2) 要求以及注意事项:搜索前要确定目标值的上下界,边搜索边减掉搜索树的某些分支,提高搜索效率;在搜索时,绝大部分需要用到剪枝。“剪枝”是搜索算法中优化程序的一种基本方法,需要通过设计出合理的判断方法,以决定某一分支的取舍;关于节点的算法:在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。关于节点的选择:FIFO(First In First Out)分支限界算法和最小耗费或最大收益分支限界算法
3) 基本过程:该算法在具体执行时,把全部可行的解空间不断分割为越来越小的子集(称为分支),并为每个子集内的解的值计算一个下界或上界(称为限界)。在每次分支后,对凡是界限超出已知可行解值那些子集不再做进一步分支。这样,解的许多子集(即搜索树上的许多结点)就可以不予考虑,从而缩小了搜索范围。这一过程一直进行到找出可行解为止,该可行解的值不大于任何子集的界限。最后求得最优解。
4) 个人见解:通过定义会觉得分支限界和回溯法有些类似,但是两者是不同的,回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。在分支限界的运算过程中剪枝是很重要的一个点,能大大提高算法的效率。
5) 经典例子:单源最短路径问题:给定带权有向图G=(V,E),其中每条边的权是非负实数。给定V中的一个顶点,称为源。现在要计算从源到所有其它各顶点的最短路长度,这里路的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。
输入:第一行是顶点个数n,第二行是边数edge;接下来edge行是边的描述:from,to,d,表示从顶点from到顶点to的边权是d。后面是若干查询,从顶点s到顶点t。
输出:给出所有查询,从顶点s到顶点t的最短距离。如果从顶点s不可达到顶点t,则输出“No path!”。
数据结构:#define inf 1000000 //∞
#define NUM 100
int n; //图G的顶点数
int edge; //图G的边数
int c[NUM][NUM]; //图G的邻接矩阵
int prev[NUM]; //前驱顶点数组
int dist[NUM]; //从源顶点到各个顶点最短距离数组
//优先队列的元素
struct MinHeapNode {
//排序算法,升序
friendbool operator < (const MinHeapNode& a,
const MinHeapNode&b)
{
if(a.length> b.length) returntrue;
elsereturn false;
}
int i; //结点编号
intlength; //结点路径的长度
};
void ShortestPaths(int v)
{//形参v是起始结点
priority_queue<MinHeapNode, vector<MinHeapNode>,
less<MinHeapNode>> H;
MinHeapNodeE;
E.i = v; //定义源结点v为初始扩展结点
E.length =0;
dist[v] =0;
while(true) //搜索问题的解空间
{
for (int j= 1; j <= n; j++) //扩展所有子结点
//剪枝,沿结点i到结点j有路,并且能够取得更优的路径长度
if ((c[E.i][j]<inf) && (E.length+c[E.i][j]<dist[j]))
{
dist[j] = E.length+c[E.i][j];
prev[j] = E.i;
MinHeapNode N; //构造队列元素
N.i=j;
N.length = dist[j];
H.push(N); //加入到优先队列H中
}
if (H.empty()) break; //队列为空
else {
E = H.top(); //取出队列的头元素
H.pop(); } //删除队列的头元素
}
}
五、 动态规划
1) 定义:动态规划是解决多阶段决策问题的一种方法,多阶段决策问题:如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策,其指导思想为在做每一步决策时,列出各种可能的局部解依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。以每一步都是最优的来保证全局是最优的。
2) 相关概念:阶段:据空间顺序或时间顺序对问题的求解划分阶段。状态:描述事物的性质,不同事物有不同的性质,因而用不同的状态来刻画。对问题的求解状态的描述是分阶段的。决策:根据题意要求,对每个阶段所做出的某种选择性操作。状态转移方程:用数学公式描述与阶段相关的状态间的演变规律。最优性原理:不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。最优决策序列的子序列,一定是局部最优决策子序列。包含有非局部最优的决策子序列,一定不是最优决策序列。
3) 个人见解:一般求最优性的问题基本上都是从总体直接求的,而动态规划其实就是从局部来求最忧解,你的每一步决策只和你的上一步的决策有关。所以解决此类问题和一般的最优问题是不一样的。
4) 相关例题:最长上升子序列:一个数的序列bi,当b1< b2 < ... < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... <iK <= N。
输入数据:输入的第一行是序列的长度N (1 <= N<= 1000)。第二行给出序列中的N 个整数,这些整数的取值范围都在0 到10000。
输出要求:最长上升子序列的长度。
过程:假定MaxLen(k)表示以ak做为“终点”的最长上升子序列的长度,那么:
MaxLen (1) = 1
MaxLen (k) = Max { MaxLen (i):1<i < k 且 ai < ak且 k≠1 } +1
实际实现的时候,可以不必编写递归函数,因为从 MaxLen(1)就能推算出MaxLen(2),有了MaxLen(1)和MaxLen(2)就能推算出MaxLen(3)……
int b[MAX_N + 10];
int aMaxLen[MAX_N +10];
int main()
{
int i, j, N;
scanf("%d", & N);
for( i = 1;i <= N;i ++ )
scanf("%d", & b[i]);
aMaxLen[1] = 1;
for( i = 2; i <=N; i ++ )
{ //求以第i 个数为终点的最长上升子序列的长度
int nTmp = 0; //记录第i 个数左边子序列最大长度
for( j = 1; j < i; j ++ )
{ //搜索以第i 个数左边数为终点的最长上升子序列长度
if( b[i] > b[j]
算法分析与设计论文
以大学生程序设计竞赛为例
姓 名: 刘震
学 号:20153880
专 业:信息与计算科学
学 校:山东农业大学
授课老师:费玉奎
一、 递归与分治
1) 定义:程序直接或间接调用自身的编程技巧称为递归算法,而且递归分为直接调用和间接调用,它通常将复杂的问题层层转化为一个与原问题相似的但是规模相对较小的问题来求解。分治策略是对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同。分治法的适用条件:
i. 问题的规模缩小到一定的程度就可以容易地解决;
ii. 可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
iii. 利用该问题分解出的子问题的解可以合并为该问题的解;
iv. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
分治策略就是递归算法的一个分支而已。
2) 要求以及注意事项:递归需要有边界条件、递归前进段和递归返回段。
i. 当边界条件不满足时,递归前进;
ii. 当边界条件满足时,递归返回。
所以递归算法最重要的就是递归出口即边界条件,如果没有递归出口的话,那么递归就会一直进行下去。当然递归也会有一些缺点,例如:递归算法解题的运行效率较低,而且在递归调用过程中,系统为每一层的返回点、局部变量等开辟了堆栈来存储。递归次数过多容易造成堆栈溢出等。
3) 个人见解:在解决递归问题的之前,要先理清思路,确定递归的出口以及递归前进的条件。递归算法也没用快速掌握的方法,只能通过多做一些题目,熟悉这类题目的套路,也要多看看别人的见解以及做题的方法。
4) 经典例题:递归中最经典的例子就是FIBONACCI数列了
int fib[50]; //采用数组保存中间结果
void fibonacci(int n)
{
fib[0]= 1;
fib[1] =1;
for (int i=2; i<=n; i++)
fib[i] =fib[i-1]+fib[i-2];
}
这个代码是采用了数组保存中间结果的方法,如果给定的n已经存储了的话,那么直接返回就可以了,不用再递归算了。这是用牺牲空间来换取时间的方法。
二、 贪心算法
1) 定义:在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。从中可以看出:贪心算法只是求每一步的最优值,即是某种意义上的局部最优解。贪心标准就是每一步选择最优解的标准。
2) 要求以及注意事项:如果要使用贪心策略解题,需要解决两个问题:该题是否适合于用贪心策略求解(因为并不是所有问题都可以用贪心算法解决的,也不能保证总是有效的);如何选择贪心标准,以得到问题的最优/较优解(如果没用贪心标准的话,那么就不能确定每一步的最优/较优解)。贪心算法和动态规划的区别:在动态规划算法中,每步所做的选择往往依赖于相关子问题的解,因而只有在解出相关子问题后,才能做出选择;在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择,然后再去解出这个选择后产生的相应的子问题。
3) 个人见解:当我们用贪心算法解决题目的时候应该首先要注意这道题目是不是能用贪心算法解决。当我们确定能用贪心算法解决时,然后再确定贪心标准,然后根据贪心标准确定每一步的最优/较优标准。
4) 经典例子:背包问题(利用贪心算法计算背包问题,同时得到解向量),按性价比从高到底的顺序选取物品,而且物品可以分割。
数据结构:struct bag{
int w; //物品的重量
int v; //物品的价值
doublex; //装入背包的量,0≤x≤1
int index; //物品编号
double c; //性价比
}a[1001]; //存放物品的数组
排序因子(按性价比降序):bool cmp(bag a, bag b){
return a.c>= b.c;}
使用标准模板库函数排序(最好使用stable_sort()函数,在性价比相同时保持输入的顺序):sort(a,a+n, cmp);
double knapsack(int n, bag a[], double c)
{
double cleft = c;
int i = 0;
double b = 0;
while(i<n && a[i].w<=cleft)
{
cleft -= a[i].w;
b += a[i].v;
//物品原先的序号是a[i].index,全部装入背包
a[a[i].index].x = 1.0;
i++;
}
if (i<n) {
a[a[i].index].x = 1.0*cleft/a[i].w;
b += a[a[i].index].x*a[i].v;
}
return b;
}
三、 回溯算法
1) 定义:回溯法是一种组织搜索的一般技术,有“通用的解题法”之称,用它可以系统的搜索一个问题的所有解或任一解。它的基本做法是用深度优先的方式搜索,即有系统性又有跳跃性。
2) 要求以及注意事项:用回溯法求解时,要明确定义问题的解空间。而且应至少包含问题的一个(最优)解。与解空间树相关的定义:
i.活结点:如果已生成一个结点而它的所有儿子结点还没有全部生成,则这个结点叫做活结点。
ii.扩展结点:当前正在生成其儿子结点的活结点叫扩展结点(正扩展的结点)。
iii.死结点:不再进一步扩展或者其儿子结点已全部生成的结点就是死结点。在回溯法搜索解空间树时,
通常采用两种策略(剪枝函数)避免无效搜索以提高回溯法的搜索效率:
i. 用约束函数在扩展结点处减去不满足约束条件的子树
ii. 用限界函数减去不能得到最优解的子树。
3) 基本过程:从根结点开始,深度优先遍历整个解空间。
i.这个开始结点成为一个活结点,同时成为当前的扩展结点。在当前的扩展结点,搜索向深度方向进入一个新的结点。这个新结点成为一个新的活结点,并成为当前的扩展结点。
ii.若在当前扩展结点处不能再向深度方向移动,则当前的扩展结点成为死结点,即该活结点成为死结点。此时回溯到最近的一个活结点处,并使得这个活结点成为当前的扩展结点。
iii.回溯法以这样的方式递归搜索整个解空间(树),直至满足中止条件。
4) 个人见解:回溯算法还是有难度的,所以需要下多点功夫。首先要将知识点看透,尤其是回溯的基本过程学会,然后做一些实际的题目来练习。而且要熟练掌握深度优先遍历的知识点。
5) 经典例子:0-1背包问题(回溯算法) :给定一个物品集合s={1,2,3,…,n},物品i的重量是wi,其价值是vi,背包的容量为W,即最大载重量不超过W。在限定的总重量W内,我们如何选择物品,才能使得物品的总价值最大。
输入:第一个数据是背包的容量为c(1≤c≤1500),第二个数据是物品的数量为n(1≤n≤50)。接下来n行是物品i的重量是wi,其价值为vi。所有的数据全部为整数,且保证输入数据中物品的总重量大于背包的容量。当c=0时,表示输入数据结束。
输出:对每组测试数据,输出装入背包中物品的最大价值.
输入样例 | 输出样例 |
50 3 | 220 |
10 60 | |
30 120 | |
20 100 | |
0 |
令cw(i)表示目前搜索到第i层已经装入背包的物品总重量,即部分解(x1,x2 , …, xi)的重量:
对于左子树, xi =1 ,其约束函数为:
若constraint(i)>W,则停止搜索左子树,否则继续搜索。
对于右子树,为了提高搜索效率,采用上界函数Bound(i)剪枝。
令cv(i)表示目前到第i层结点已经装入背包的物品价值:
令r(i)表示剩余物品的总价值:
则限界函数Bound(i)为:
假设当前最优值为bestv,若Bound(i)<bestv,则停止搜索第i层结点及其子树,否则继续搜索。显然r(i)越小, Bound(i)越小,剪掉的分支就越多。
为了构造更小的r(i) ,将物品以单位重量价值比di=vi/wi递减的顺序进行排列:
d1≥d2≥… ≥dn
对于第i层,背包的剩余容量为W-cw(i),采用贪心算法把剩余的物品放进背包,根据贪心算法理论,此时剩余物品的价值已经是最优的,因为对剩余物品不存在比上述贪心装载方案更优的方案。
数据结构:#define NUM 100
int c; //背包的容量
int n; //物品的数量
int cw; //当前重量
int cv; //当前价值
int bestv; //当前最优价值
//描述每个物品的数据结构
struct Object{
intw; //物品的重量
intv; //物品的价值
doubled; //物品的单位重量价值比
}Q[NUM]; //物品的数组
对物品以单位重量价值比递减排序的因子是:
bool cmp(Object a, Object b)
{
if(a.d>=b.d) return true;
else return false;
}
物品的单位重量价值比是在输入数据时计算的:
for(int i=0; i<n; i++)
{
scanf("%d%d",&Q[i].w,&Q[i].v);
Q[i].d = 1.0*Q[i].v/Q[i].w;
}
使用C++标准模板库的排序函数sort()排序:
sort(Q, Q+n, cmp);
//形参i是回溯的深度,从0开始
voidbacktrack(int i)
{
//到达叶子结点时,更新最优值
if(i+1>n) {bestv = cv; return;}
//进入左子树搜索
if(cw+Q[i].w<=c)
{
cw += Q[i].w;
cv += Q[i].v;
backtrack(i+1);
cw -= Q[i].w;
cv -= Q[i].v;
}
//进入右子树搜索
if(Bound(i+1)>bestv) backtrack(i+1);
}
//形参i是回溯的深度
int Bound(int i)
{
int cleft =c-cw; //背包剩余的容量
int b = cv; //上界
//尽量装满背包
while (i<n&& Q[i].w<=cleft)
{
cleft -= Q[i].w;
b += Q[i].v;
i++;
}
//剩余的部分空间也装满
if(i<n) b += 1.0*cleft*Q[i].v/Q[i].w;
returnb;
}
四、 分支限界算法
1) 定义:分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。基本思想是对有约束条件的最优化问题的所有可行解(数目有限)空间进行搜索。
2) 要求以及注意事项:搜索前要确定目标值的上下界,边搜索边减掉搜索树的某些分支,提高搜索效率;在搜索时,绝大部分需要用到剪枝。“剪枝”是搜索算法中优化程序的一种基本方法,需要通过设计出合理的判断方法,以决定某一分支的取舍;关于节点的算法:在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。关于节点的选择:FIFO(First In First Out)分支限界算法和最小耗费或最大收益分支限界算法
3) 基本过程:该算法在具体执行时,把全部可行的解空间不断分割为越来越小的子集(称为分支),并为每个子集内的解的值计算一个下界或上界(称为限界)。在每次分支后,对凡是界限超出已知可行解值那些子集不再做进一步分支。这样,解的许多子集(即搜索树上的许多结点)就可以不予考虑,从而缩小了搜索范围。这一过程一直进行到找出可行解为止,该可行解的值不大于任何子集的界限。最后求得最优解。
4) 个人见解:通过定义会觉得分支限界和回溯法有些类似,但是两者是不同的,回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。在分支限界的运算过程中剪枝是很重要的一个点,能大大提高算法的效率。
5) 经典例子:单源最短路径问题:给定带权有向图G=(V,E),其中每条边的权是非负实数。给定V中的一个顶点,称为源。现在要计算从源到所有其它各顶点的最短路长度,这里路的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。
输入:第一行是顶点个数n,第二行是边数edge;接下来edge行是边的描述:from,to,d,表示从顶点from到顶点to的边权是d。后面是若干查询,从顶点s到顶点t。
输出:给出所有查询,从顶点s到顶点t的最短距离。如果从顶点s不可达到顶点t,则输出“No path!”。
数据结构:#define inf 1000000 //∞
#define NUM 100
int n; //图G的顶点数
int edge; //图G的边数
int c[NUM][NUM]; //图G的邻接矩阵
int prev[NUM]; //前驱顶点数组
int dist[NUM]; //从源顶点到各个顶点最短距离数组
//优先队列的元素
struct MinHeapNode {
//排序算法,升序
friendbool operator < (const MinHeapNode& a,
const MinHeapNode&b)
{
if(a.length> b.length) returntrue;
elsereturn false;
}
int i; //结点编号
intlength; //结点路径的长度
};
void ShortestPaths(int v)
{//形参v是起始结点
priority_queue<MinHeapNode, vector<MinHeapNode>,
less<MinHeapNode>> H;
MinHeapNodeE;
E.i = v; //定义源结点v为初始扩展结点
E.length =0;
dist[v] =0;
while(true) //搜索问题的解空间
{
for (int j= 1; j <= n; j++) //扩展所有子结点
//剪枝,沿结点i到结点j有路,并且能够取得更优的路径长度
if ((c[E.i][j]<inf) && (E.length+c[E.i][j]<dist[j]))
{
dist[j] = E.length+c[E.i][j];
prev[j] = E.i;
MinHeapNode N; //构造队列元素
N.i=j;
N.length = dist[j];
H.push(N); //加入到优先队列H中
}
if (H.empty()) break; //队列为空
else {
E = H.top(); //取出队列的头元素
H.pop(); } //删除队列的头元素
}
}
五、 动态规划
1) 定义:动态规划是解决多阶段决策问题的一种方法,多阶段决策问题:如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策,其指导思想为在做每一步决策时,列出各种可能的局部解依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。以每一步都是最优的来保证全局是最优的。
2) 相关概念:阶段:据空间顺序或时间顺序对问题的求解划分阶段。状态:描述事物的性质,不同事物有不同的性质,因而用不同的状态来刻画。对问题的求解状态的描述是分阶段的。决策:根据题意要求,对每个阶段所做出的某种选择性操作。状态转移方程:用数学公式描述与阶段相关的状态间的演变规律。最优性原理:不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。最优决策序列的子序列,一定是局部最优决策子序列。包含有非局部最优的决策子序列,一定不是最优决策序列。
3) 个人见解:一般求最优性的问题基本上都是从总体直接求的,而动态规划其实就是从局部来求最忧解,你的每一步决策只和你的上一步的决策有关。所以解决此类问题和一般的最优问题是不一样的。
4) 相关例题:最长上升子序列:一个数的序列bi,当b1< b2 < ... < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... <iK <= N。
输入数据:输入的第一行是序列的长度N (1 <= N<= 1000)。第二行给出序列中的N 个整数,这些整数的取值范围都在0 到10000。
输出要求:最长上升子序列的长度。
过程:假定MaxLen(k)表示以ak做为“终点”的最长上升子序列的长度,那么:
MaxLen (1) = 1
MaxLen (k) = Max { MaxLen (i):1<i < k 且 ai < ak且 k≠1 } +1
实际实现的时候,可以不必编写递归函数,因为从 MaxLen(1)就能推算出MaxLen(2),有了MaxLen(1)和MaxLen(2)就能推算出MaxLen(3)……
int b[MAX_N + 10];
int aMaxLen[MAX_N +10];
int main()
{
int i, j, N;
scanf("%d", & N);
for( i = 1;i <= N;i ++ )
scanf("%d", & b[i]);
aMaxLen[1] = 1;
for( i = 2; i <=N; i ++ )
{ //求以第i 个数为终点的最长上升子序列的长度
int nTmp = 0; //记录第i 个数左边子序列最大长度
for( j = 1; j < i; j ++ )
{ //搜索以第i 个数左边数为终点的最长上升子序列长度
if( b[i] > b[j] )
{
if( nTmp < aMaxLen[j] )
nTmp = aMaxLen[j];
}
}
aMaxLen[i] = nTmp + 1;
}
int nMax =-1;
for( i = 1;i <= N;i ++ )
if( nMax < aMaxLen[i])
nMax = aMaxLen[i];
printf("%d\n", nMax);
return 0;
}
)
{
if( nTmp < aMaxLen[j] )
nTmp = aMaxLen[j];
}
}
aMaxLen[i] = nTmp + 1;
}
int nMax =-1;
for( i = 1;i <= N;i ++ )
if( nMax < aMaxLen[i])
nMax = aMaxLen[i];
printf("%d\n", nMax);
return 0;
}