1)算法
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每条指令表示一个或多个操作。
算法特性:
2)算法设计
一个好的算法应考虑多个目标,包括正确性、可读性、健壮性和高效性等。
算法设计技术主要有分治法、动态规划法、贪心法、回溯法、分支限界法,概率算法和近似算法等。
3)算法分析
算法分析技术的主要内容:
4)算法表示
算法表示方法:
1)时间复杂度
由于时间复杂度与空间复杂度分别对算法占用的时间和空间资源进行分析,计算方法相似,且空间复杂度分析相对简单,因此主要讨论时间复杂度。算法时间复杂度分析主要是分析算法的运行时间。
建立以输入规模n为自变量的函数 T(n)来表示时间复杂度。根据不同的输入,将算法复杂度分析分为3种情况:
最佳情况:执行时间最少的输入
最坏情况:执行时间最多的输入
平均情况:算法的平均运行时间。考虑的是每种输入极其输入的概率。平均情况分析按以下3个步骤进行:
pi 第i类输入发生的概率,ci 第i类输入的执行时间,输入分为 n 类。
2)渐进符号
以输入规模n为自变量建立的时间复杂度实际上还是较复杂的,可以对该函数做进一步的抽象,仅考虑运行时间的增长率或称为增长的重量。当输入规模大到只有与运行时间的增长量级有关时,就是在研究算法的渐进效率。下面介绍3种常用的标准方法来简化算法的渐进分析:
O记号。定义为:给定一个函数 g(n),O(g(n)) = { f(n): 存在正常数 c 和 n₀,当n≥n₀,有 f(n)≤cg(n) },如图所示。O(g(n))表示一个函数集合,往往用该记号求出算法的渐进上界。
Ω记号。定义为:给定一个函数 g(n),Ω(g(n)) = { f(n): 存在正常数 c 和 n₀,当n≥n₀,有 f(n)≥cg(n) },如图所示。Ω(g(n))表示一个函数集合,往往用该记号求出算法的渐进下界。
⊙记号。定义为:给定一个函数 g(n),⊙(g(n)) = { f(n): 存在正常数 c₁、c₂ 和 n₀,当n≥n₀,有 c₁g(n)≤f(n)≤c₂g(n) },如图所示。⊙(g(n))表示一个函数集合,往往用该记号求出算法的渐进上界和渐进下界,即渐进紧致界。
由上述定义可知, f(n)=⊙(g(n)) 当且仅当 f(n)=O(g(n)) 和 f(n)=Ω(g(n))
3)递归式
递归算法的时间复杂度:
展开法:将递归式中等式右边的项根据递归式进行替换,称为展开。展开后的项再被展开,如此下去,直到得到一个求和表达式,得到结果。
当n=1时T(n)=1,当n>1时T(n)= T(n-1)+n,求时间复杂度
T(n)= T(n-1)+n
T(n-1)= T(n-2)+n-1
...
T(2) = T(1) + 2
根据展开法原则, T(n) = 1+2+...+n = n(n+1)/2=O(n²)
代换法:先猜测一个较小值,再用数学归纳法证明猜测的正确性,这种方法比较难用。
递归树法
主方法:也称主定理,给出了求解以下形式的递归式的快速方法
T(n)=aT(n/e)+f(n)
其中 a≥1 e≥1 ,f(n)是一个渐进的正函数。T(n) 可能有如下渐进紧致界:
1)递归的概念
递归 = 递推 + 回溯。
递归是指子程序(或函数)直接或间接调用自己,是一种描述问题和解决问题的常用方法。
递归有两个基本要是:
例如阶乘函数:当n=0时n!=1,当n>0时 n!=n(n-1)!。n=0为边界条件, n(n-1)! 是递归体。n!可以递归地计算如下:
int factorial(int num) {
if(num == 0) {
return 1;
}
if(num > 0) {
return num * factorial(num-1);
}
}
2)分治法的基本思想
分治和递归就像孪生兄弟。分治法的思想是将一个难以直接解决的大问题分解成一些规模较小的相同问题,这些问题互相独立,分而治之。
一般来说,分治法在每一层递归上都有3个步骤:
3)分支算法经典实例
归并排序算法
归并排序算法是成功应用分治法的一个完美例子,其基本思想是将待排序元素分成大小相同的两个子序列,分别对子序列进行排序,最终将拍好序的子序列合并为所要求的的序列。归并算法按照分治法的3个步骤:
void mergeSort(int[] arr,int left,int right,int[] temp) {
if (left
例如我们使用例子调用函数
public static void main(String[] args) {
int []arr = {1,3,6,2,3,7,8,10};
int[] tmp = new int[arr.length];
MyTest myTest = new MyTest();
System.out.println(Arrays.toString(arr));
myTest.mergeSort(arr, 0, arr.length-1, tmp);
System.out.println(Arrays.toString(arr));
}
数组{1,3,6,2,3,7,8,10} 长度为8,初始传入参数为mergeSort(arr, 0, 7, tmp)
,调用的逻辑如下:
mergeSort(arr, 0, 7, tmp)
,未到达终止条件。计算 mid = 3,需向左分解(递推) 即 mergeSort(arr, 0, 3, tmp)
mergeSort(arr, 0, 3, tmp)
,未到达终止条件。计算 mid = 1,需向左分解(递推) 即 mergeSort(arr, 0, 1, tmp)
mergeSort(arr, 0, 1, tmp)
,未到达终止条件。计算 mid = 0,需向左分解(递推) 即 mergeSort(arr, 0, 0, tmp)
mergeSort(arr, 0, 0, tmp)
,到达终止条件,需返回(回溯)到mergeSort(arr, 0, 1, tmp)
中;mergeSort(arr, 0, 1, tmp)
,继续执行向右分解(递推)即mergeSort(arr, 1, 1, tmp)
mergeSort(arr, 1, 1, tmp)
,到达终止条件,需返回(回溯)到mergeSort(arr, 0, 1, tmp)
中;mergeSort(arr, 0, 1, tmp)
,继续执行 merge(),合并 数组0,1下标元素。合并完成后需返回(回溯)到mergeSort(arr, 0, 3, tmp)
mergeSort(arr, 0, 3, tmp)
,继续执行向右分解(递推)即mergeSort(arr, 2, 3, tmp)
mergeSort(arr, 2, 3, tmp)
,未到达终止条件。计算 mid = 2,需向左分解(递推) 即 mergeSort(arr, 2, 2, tmp)
mergeSort(arr, 2, 2, tmp)
,到达终止条件,需返回(回溯)到mergeSort(arr, 2, 3, tmp)
中;mergeSort(arr, 2, 3, tmp)
,继续执行向右分解(递推)即mergeSort(arr, 3, 3, tmp)
mergeSort(arr, 3, 3, tmp)
,到达终止条件,需返回(回溯)到mergeSort(arr, 2, 3, tmp)
中;mergeSort(arr, 2, 3, tmp)
,继续执行 merge(),合并 数组2,3下标元素。合并完成后需返回(回溯)到mergeSort(arr, 0, 3, tmp)
mergeSort(arr, 0, 3, tmp)
,继续执行 merge(),合并 数组1,2,3,4下标元素。合并完成后需返回(回溯)到mergeSort(arr, 0, 7, tmp)
,至此 0,7以完成前半部分的排序。mergeSort(arr, 0, 7, tmp)
,继续执行向右分解(递推)即mergeSort(arr, 4, 7, tmp)
。mergeSort(arr, 0, 7, tmp)
,继续执行 merge(),合并 数组1,2,3,4,5, 6, 7下标元素。完成整个排序最大子段和问题
给定长度为n的整数序列,a[1...n], 求[1,n]某个子区间[i , j]使得a[i]+…+a[j]和最大。例如(-2,11,-4,13,-5,2)的最大子段和为20,所求子区间为[2,4]。
分治策略如下:
int maxsum(int arr[], int left, int right)//返回左闭右开区间的最大连续和
{
int sum = 0;
int i;
if (left == right) { //分解到单个整数,不可继续分解
if (arr[left] > 0) { return arr[left]; }
else { return 0; }
}
// 从 left 和 right 的中间分解数组
int mid = (left + right) / 2;
int leftsum = maxsum(arr, left,mid);
int rightsum = maxsum(arr, mid+1, right);
//判断是情形1、情形2还是情形3
int s1=0; // 左侧最优值
int lefts = 0;
for(i = mid; i >= left; i--){
lefts += arr[i];
if (lefts > s1) { s1 = lefts; }
}
int s2=0; // 右侧最优值
int rights = 0;
for(i = mid+1; i <= right; i++){
rights += arr[i];
if (rights > s2) { s2 = rights; }
}
sum = s1+s2;
//情形1
if (sum < leftsum) { sum = leftsum; }
//情形2
if (sum < rightsum) { sum = rightsum; }
//如果不是情形1、情形2,则是情形3
return sum;
}
动态规划法与分治法类似,其基本思想也是将待求解问题分成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。
设计一个动态规划法步骤:
对于一个给定问题,若具有以下性质,可以考虑使用动态规划法来求解:
背包问题
给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi。问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
问题分析
面对每个物品,我们只有选择拿(1)取或者不拿(0)两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。 把物品随机排成一排,标记为1、 2、 3……,从1号物品开始依次判断是否装包,面对当前物品有两种情况:
该物品的重量大于背包的容量,装不下,只能选择不装
该物品的重量小于背包的容量,可以装下,但是否要装,需要进一步判断,因为可能存在这样一种情况:要装该物品,就必须拿出之前装的一个物品,而这时可能会出现如下不同情况:
- 【拿掉之前的一个物品并装进当前物品后 背包的总价值】<【不拿掉之前的物品且不装当前物品 背包的总价值】
- 【拿掉之前的一个物品并装进当前物品后 背包的总价值】>【不拿掉之前的物品且不装当前物品 背包的总价值】
举例说明:有2个物品,重量数组w={7,6},价值数组v={3,9},背包容量为8。1号物品重量7小于背包容量8,放进背包,此时背包价值为3,到2号物品时,2号物品重量3也小于背包容量8,但是如果要把2号物品放进背包,就要把1号物品从背包中拿出,此时就要比较两种情况下背包的价值哪个更大,max(2号物品不放进背包,2号物品放进背包(隐含着要把1号物品取出背包))=max(3,9)=9,因此选择拿出1号物品,放进2号物品。
当然背包容量也有可能不用取出之前的物品可以直接放下当前物品,此时肯定是放要比不放价值更大。 因此,当物品重量小于背包总容量,也就是背包可以装下该物品时,要判断装之前和装之后背包的总价值来决定是否要装该物品。
因此,通过判断当前物品是否装包而计算当前问题的最优解时,是要用到上一个子问题的最优解的(通过判断上一个物品是否装包而计算得到),也就是说,如果当前物品不装进背包,那么上一个子问题的最优解就是当前状态的最优解,如果当前物品装包后的价值大于不装的价值,那么当前问题的最优解就是当前物品装进背包后产生的价值,这个值=上一个子问题中背包容量为【背包总容量减去当前物品重量】的情况下的最优解+当前物品价值。总之,当前问题的最优解求解过程依托于上一个子问题的各个状态下的最优解,所以在求当前问题的最优解之前要先求出之前的所有情况下的最优解。也就是要先求子问题的最优解。这里就需要用到动态规划的方法。
动态规划
动态规划(Dynamic Programming,DP) 与分治法的区别在于划分的子问题是有重叠的,解过程中对于重叠的部分只要求解一次,记录下结果,减少了重复计算过程。
另外,DP在求解一个问题最优解时,不是固定的计算合并某些子问题的解,而是根据各子问题的解的情况选择其中最优的。
动态规划求解具有以下性质:
- 最优子结构性质:最优解包含了其子问题的最优解,不是合并所有子问题的解,而是找最优的一条解线路,选择部分子最优解来达到最终的最优解。
- 子问题重叠性质:先计算子问题的解,再由子问题的解去构造问题的解(由于子问题存在重叠,把子问题解记录下来为下一步使用,这样就可以从备忘录中读取)。其中备忘录先记录初始状态。
求解过程
定义一个二维数组
m[n][C]
,每个元素代表一个状态,m[i][j]表示前 i 个物品放入容量为 j 的背包所能获得的最大价值,我们可以很容易分析得出m[i][j]的计算方法:
初始状态:初始状态都为0,表示前0个物品无论放入多大的背包价值都为0,容量为0的背包无论多大价值的物品都无法装进去;
转移函数
if(w(i)>j) m[i][j]=m[i-1][j]; else m[i][j]=max(m[i-1][j],m[i-1][j-w(i)]+v(i)); /* 最后一行代码就是根据“为了容量为C的背包中物品总价值最大化,第i件物品应该放入背包中吗”转化来的。v(i)表示第i件物品的价值,w(i)表示第i件物品的重量。m[i-1][j]表示不将这件物品放进背包的背包的总价值,m[i-1][j-v(i)]+w(i)表示将第i件物品放进背包后背包的总价值,比较两者,取最大值作为最终的选择。 */
假设有6个物品:
价值数组v = {8, 10, 6, 3, 7, 2}, 重量数组w = {4, 6, 2, 2, 5, 1}, 求背包容量C = 12时对应的
m[i][j]
数组。
代码
package org.cj;
import java.util.Arrays;
public class MyTest1 {
public static void main(String[] args) {
MyTest1 myTest1 = new MyTest1();
int w[] = {2,3,4,5,9}; //物品重量
int v[] = {3,4,5,8,10}; //物品价值
int num = w.length;//物品个数为5
int W = 20; //背包重量
int table[][] = myTest1.knapsack(W, w, v, num); //计算背包问题最优解的值
System.out.println("最优解的值:"+table[num][W]);
//输出存储表
int n = String.valueOf(table[num][W]).length();
for (int i = 0; i <= num; i++) {
for (int j = 0; j <= W; j++) {
System.out.print(String.format(" %"+n+"d", table[i][j]));
}
System.out.println("\n");
}
System.out.println("最优解为:");
int[] x = myTest1.knapsackDp(table, W, w, v, num);
for (int i=0;i c) { //第k件物品放不进去 此时背包的价值 = 判断完上一件物品之后背包的价值
table[k][c] = table[k - 1][c];
} else {
int value1 = table[k - 1][c - wTemp[k]] + vTemp[k]; //放入第k件物品后 背包总价值 = 先给这件物品留出空间,剩余的背包大小能装进的最大价值 + 这件物品的价值
int value2 = table[k - 1][c]; //不放入第k件物品 背包总价值 = 不用给这件物品留出空间,当前背包大小能装进的最大价值(就是判断完上一件物品之后背包的价值)
table[k][c] = value1 > value2 ? value1:value2;
}
}
}
return table;
}
/**
* 查找最优解
* table - 存储表
* W - 背包重量
* w - 物品重量
* v - 物品价值
* len - 物品个数
*/
int[] knapsackDp(int table[][], int W, int w[], int v[], int len) {
int k; // 第k个物品
int x[] = new int[len];
//初始化表,将重量和价值数组前面都加上一个0
int wTemp[] = new int[len+1];
wTemp[0] = 0;
System.arraycopy(w,0, wTemp,1,len);
int vTemp[] = new int[len+1];
vTemp[0] = 0;
System.arraycopy(v,0, vTemp,1,len);
//填表
for (k=len;k>1;k--) {
if (table[k][W] == table[k-1][W]) { //重量为W的最优选择的背包中不包含该物品
x[k-1] = 0;
}else{ //重量为W的最优选择的背包中包含该物品
x[k-1] = 1;
W -= w[k-1];
}
}
if (table[1][W] == 0) { //第一个物品不放入背包
x[0]=0;
} else{ //第一个物品放入背包
x[0]=1;
}
return x;
}
}
贪心法是一种对某些求最优解问题的更简单、更迅速的技术。用贪心法设计算法的特点是一步一步地进行,常以当前情况为基础根据某个优化测试作最优选择,而不考虑各种的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间,它采用自定向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求的问题化为一个规模更小的子问题,通过每一步的选择,可得到问题的一个最优解,虽然每一步都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪心法不要回溯。
贪心算法基本求解思路:
确定问题能否用贪心策略求解;一般来说,适用于贪心策略求解的问题具有以下特点:
public class MyTest3 {
public static void main(String[] args) {
MyTest3 myTest1 = new MyTest3();
int w[] = {35,30,60,50,40,10,25}; //物品重量
int v[] = {10,40,30,50,35,40,30}; //物品价值
int W = 150; //背包重量
myTest1.knapsackGreedy(W, w,v);
}
/**
* 贪婪算法实现背包问题求解
* @param capacity 背包容量
* @param weights 各个物品的重量
* @param values 各个物品的价值
*/
private void knapsackGreedy(int capacity,int weights[],int values[]) {
int n=weights.length; //物品的数量
Double[] r=new Double[n]; //保存性价比的数组
int [] index=new int[n]; //保存按性价比排序的物品的下标
//计算得到各个物品的性价比
for (int i = 0; i < n; i++) {
r[i]=(double)values[i]/weights[i];
index[i]=i; //初始化各个物品的默认性价比排序
}
//对各个物品的性价比进行排序
for(int i=0;i
回溯法有"通用的解题法"之称,用它可以系统地搜索一个问题的所有解或任一解。回溯法是一个既带有系统性有带有跳跃性的搜索算法。它在包含问题的所有解空间树中,按照深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定不包含问题的解。若肯定不包含,则跳过对以该结点为根结点的子树的系统搜索,逐层向其祖先结点回溯;否则就进入该子树,继续按深度优先的策略进行搜索。回溯法在用来求问题的所有解时要回溯到根,且根结点的所有子树都已被搜索才结束;而用来求问题的任一解时,只要搜索到问题的一个解就可以结束。这种深度优先的方式系统地搜索问题的解的方法称为回溯法,它适用于解一些组合数较大的问题。
回溯法的算法框架:
问题的解空间
在应用回溯法解问题时,首先应明确定义问题的解空间。问题的解空间至少包含问题的一个(最优)解。例如,对于n种可选物品的 0-1 背包问题,其解空间由长度为n的 0-1 向量组成。该解空间包含了对变量的所有可能的 0-1 赋值。当 n=3 时,其解空间是{ (0,0,0), (0,1,0), (0,0,1), (1,0,0), (0,1,1), (1,0,1), (1,1,0), (1,1,1) } 。
定义了解空间后,还应将解空间很好地组织起来,使得回溯法能方便地搜索整个解空间。通常将解空间表示为树或图的形式。例如,对于n=3的0-1背包问题,其解空间用一个完全二叉树表示,如下图。
解空间的第i层到第i+1层边上的标号给出了遍历的值。从树根到叶子的任一路径表示解空间的一个元素。例如,从根结点到结点H对应于解空间中的元素(1,1,1)
回溯法的基本思想
确定了解空间的组织结构后,回溯法从开始结点(出发),以深度优先的方式搜索整个解空间。这个开始结点就称为一个活结点,同时也称为当前的扩展结点。在当前的扩展结点处,搜索向纵深方向移动至一个新结点。这个新节点就成为新的活动结点,并成为当前扩展结点。如果在当前扩展结点处不能再向纵深方向移动,则当前的扩展结点就成为死结点。此时,应该往回移动(回溯)至最新一个活动结点处,并使这个结点成为当前的扩展结点。回溯法即以这种工作方式递归地在解空间中搜索,直到所要求的解或解空间中已无活动结点为止。
例如,n=3 时的 0-1 背包问题,考虑下面的具体实例:w=[16,15,15],p=[45,25,25], c=30(w-重量 p-价值 c-背包容量)。从上图的根结点开始搜索其解空间。
按此方法继续搜索,可搜索整个解空间。所搜结束后找到的最好解就是 0-1背包问题的最优解。
综上所述,运用回溯法通常包含以下3个步骤:
回溯法的算法框架
非递归方式:
BackTracking(X)
1 计算解 X 第一个元素的候选集合 S
2 k ← 1
3 while k>0 do
4 while sₖ ≠ φ do
5 xₖ ← Sₖ 中的下一个元素
6 Sₖ ← Sₖ - {xₖ}
7 if X = {x₁,x₂,...,xₖ} 是问题的解
8 then 输出 X
9 k ← k+1
10 计算解 X 的第 k 个元素的候选集合 Sₖ
11 k ← k -1
递归方式:
BackTrackingDFS(X, k)
1 if X = {x₁,x₂,...,xₖ} 是问题的解
2 then 输出 X
3 else k ← k+1
4 计算解 X 的第 k 个元素的候选集合 Sₖ
5 while sₖ ≠ φ do
6 xₖ ← Sₖ 中的下一个元素
7 Sₖ ← Sₖ - {xₖ}
8 BackTrackingDFS(X, k)
9 return
回溯法的界限函数
问题的解空间往往很大,为了有效地进行搜索,需要在搜索的过程中对某些结点进行剪枝,而对哪些结点进行剪枝,需要设计界限函数来判断。因此,界限函数的设计是回溯法的一个核心问题,也是一个很难的问题。设计界限函数的通用的直到原则是尽可能多和尽可能早地“杀掉”不可能产生最优解的活结点。好的界限函数可以大大减少问题的搜索空间,从而大大提高算法的效率。下面通过例子来说明。
0- 背包问题 : 给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi。问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
上图已给出 0-1 背包问题的解空间树的示例。在该问题中,目标是为了得到最大价值,因此可以杀掉哪些不可能产生最大价值的活结点。那么,如何判断哪些结点扩展后不能产生最大价值呢?考虑贪心策略,先对物品按其单位重量从大到小排序,对搜索树中的某个节点,如F,已经确定了某些 X(i) ,1≤i≤k,而其他的 X(i),k+1≤i≤n 待定。此时可以将 0-1 背包问题松弛为背包问题,求从F点扩展下去,计算能获得的最大价值,若该价值比当前已获得某个可行解的值要小,则该结点不扩展。
若所有物品已经按其单位重量价值从大到小排序。假设 k 个物品是否放入背包以及确定,现在考虑在当前背包的剩余容量下,若是背包问题,那么能获得的最大价值是多少?即求背包物品的价值上限。代码如下:
/**
* 回溯法的01背包
*
* @author anLA
*
*/
public class BagFBack {
private MyElement[] myelements; // 封装的物品
private float s; // 背包容量
private float nowWeight = 0; // 记录当前以拿重量
private float nowPrice = 0; // 记录当前以拿价格
private float betterValue; // 记录最多的价格
/*
* 构造方法,用于初始化各个变量
*/
public BagFBack(float[] w, float[] v, float s) {
myelements = new MyElement[w.length];
for (int i = 0; i < w.length; i++) {
myelements[i] = new MyElement();
myelements[i].v = v[i];
myelements[i].w = w[i];
}
this.s = s;
// 对数组进行价值排序,系统的是从小到大的,但我讲MyElement改了,使得是从大到小
Arrays.sort(myelements);
System.out.println("物品价值" + " " + "物品重量");
for (int i = 0; i < myelements.length; i++) {
System.out.print(myelements[i].v + " " + myelements[i].w);
System.out.println();
}
}
public void traceBack(int t) {
if (t >= myelements.length) {
// 已经遍历到最下一层,也就是最后一个
System.out.println("找到方法");
betterValue = nowPrice;
System.out.println("最终拿到: " + betterValue);
output(myelements);
return;
}
// 首先进入走左子树
if (nowWeight + myelements[t].w < s) {
// 进入左子树
nowWeight += myelements[t].w;
nowPrice += myelements[t].v;
myelements[t].take = true;
traceBack(t + 1);
// 还原现场
nowWeight -= myelements[t].w;
nowPrice -= myelements[t].v;
myelements[t].take = false;
}
// 进入右子树,以及要进入的条件
if (bound(t + 1) > betterValue) {
traceBack(t + 1);
}
}
// 输出方法,用于输出
public void output(MyElement[] myelements2) {
System.out.print("拿重量为这些的物品:");
for (int i = 0; i < myelements2.length; i++) {
if (myelements2[i].take) {
System.out.print(myelements2[i].w + " ");
}
}
}
/**
* 用于计算右边的,如果右边大些,就直接进入
*
* @param i
* @return
*/
public float bound(int i) {
// 计算上界
float cleft = s - nowWeight;
float bound = nowPrice;
// 以物品单位价值递减顺序装入物品
while (i < myelements.length && cleft > myelements[i].v) {
cleft -= myelements[i].w;
bound += myelements[i].v;
i++;
myelements[i].take = true;
}
// // 如果最后一个不能整个放下去,那就装满背包,此问题讨论01背包,要么装,要么不装,所以不用加这一句
// if (i < myelements.length) {
// bound += (myelements[i].v / myelements[i].w) * cleft;
// }
return bound;
}
/**
* 封装为物品的类
*
* @author anLA
*
*/
class MyElement implements Comparable {
float w;
float v;
boolean take = false;
// 更改实现的方法,方便调用系统函数
@Override
public int compareTo(Object o) {
if (v / w < ((MyElement) o).v / ((MyElement) o).w) {
return 1; // 注意,此处主要用于排序,从大到小排序,所以故意反
} else {
return -1;
}
}
}
public static void main(String[] args) {
float[] w = { 3.4f, 2.5f, 6f, 4f, 9.0f };
float[] v = { 3f, 2.5f, 5f, 9f, 6.2f };
float s = 10;
BagFBack bagFBack = new BagFBack(w, v, s);
// 从第0层开始回溯
bagFBack.traceBack(0);
}
}
分支界限法类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。但在一般情况下,分支界限法与回溯法的求解目标不同。回溯法的求解是找出T中满足约束条件的所有解,而分支界限法的求解目标是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的,即某种意义下的最优解。
由于求解的目标不同,导致分支界限法与回溯法在解空间树 T 上的搜索方式也不同。回溯法以深度优先的方式搜索解空间树 T,而分支界限法以广度优先或以最小耗费方式搜索解空间树 T。分支界限法的搜索策略是每一个活结点只有一次机会成为扩展结点,活动结点一旦称为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,哪些导致不可行解或非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。此后,从活结点表中取下一节点称为当前扩展结点,并重复上述结点扩展过程。这个过程一致持续到找到所需的解或活节点表为空时为止。人们已经利用分支界限法解决了大量离散最优化的实际问题。
与回溯法相似,界限函数的设计是分支界限法的一个核心问题,也是一个很难的问题,如何设计界限函数来有效地减小搜索空间是应用分支界限法要考虑的问题。
根据从活结点表中选择下一扩展结点的不同方式,可将分支界限法分为机制不同的类型。最常用的有两种:
优先队列中规定的优先级通常用一个与该节点相关的数值p来表示。节点优先级的高低与 p 相关。最大优先队列规定 p 值较大的节点优先级较高。在算法实现时,通常用一个最大堆起来实现最大优先队列,用最大堆的 Deletemax 操作抽取堆中下一个节点称为当前扩展结点。类似的,最小队列规定 p 值较小的节点优先级较高。在算法实现时,通常用一个最小堆起来实现最小优先队列,用最小堆的 Deletemin 操作抽取堆中下一个节点称为当前扩展结点。
例如 n=3 时,0-1 背包问题的一个实例: w=[16,15,15],p=[45,25,25],c=30,其解空间如图:
用队列分支界限法解此问题时,用一个队列来存储活结点表。算法从根节点A触发。
前面讨论的算法对于所有合理的输入都给出正确的输出,概率算法将这一条件放宽,把随机性的算则加入到算法中。在算法执行某些步骤时,可以随机地选择下一步该如何进行,同时允许结果以较小的概率出现错误,并以此为代价,获得算法运行时间的大幅度减少。概率算法的一个基本特征是对所求解问题的同一实例用同一概率算法求解两次,可能得到完全不同的效果。这两次求解所需时间甚至所得到的的结果可能会有相当大的差别。如果一个问题没有有效的确定型算法可以在合理的时间内给出解,但是该问题能接受小概率错误,那么采用概率算法就可以快速找到这个问题的解。
一般情况下,概率算法具有以下基本特征:
概率算法大致分为4类:数值概率算法、蒙特卡罗算法、拉斯维加斯算法和舍伍德算法。
迄今为止,所有的难解问题都没有多项式时间算法,采用回溯法和分支界限法等算法设计技术可以相对有效的解决这类问题。然而,这些算法的时间性能常常是无法保证的。近似算法是解决难解问题的一种有效策略,其基本思想是放弃求最优解,而用近似最优解代替最优解,以换取算法设计上的简化和时间复杂度的降低。近似算法是这样一个过程“虽然它可能找不到一个最优解,但它总会给带求解的问题提供一个解。为了具有实用性,近似算法必须能够给出算法所产生的解与最优解之间的差别或者比例的一个界限,它保证任意一个实例的近似最优解与最优解之间相差的程度。显然,这个差别越小,近似算法越具有实用性。
衡量近似算法性能最重要的标准有以下两个:
在当今的大数据时代,数挖掘、机器学习和人工智能这些名词在我们的生活、工作和学习中已经是耳熟能详的词汇。我们需要各种技术来分析爆炸式增长的各类数据,以发现隐含在这些数据中有价值的信息和知识。作为一门交叉学科,数据挖掘利用机器学习方法对多种数据,包括数据库、数据仓库数据、Web数据等进行分析和挖掘。数据挖掘的核心是算法,其主要功能包括分类、回归、关联规则和聚类等。
优化技术是一种以数学为基础,用于求解各种工程问题优化解的应用技术。作为一个重要的科学分支,它一直受到人们的广泛重视,并在诸多工程领域迅速得到推广和应用,如系统控制、人工智能、识别模式、生产调度、VLSI技术和计算机工程等。鉴于实际工程问题的复杂性、约束性、非线性、多级小、建模困难等难点,寻求一种适合于大规模并具有智能特征的算法是一个主要目标和研究方向。20世纪80年代以来,一些新颖的优化算法,如人工神经网络、混沌、遗传算法、进化规划、模拟退货、紧急搜索机器混合花策略等,通过某些自然现象或过程而发展得到,其思想和内容设计数学、物理学、生物进化、人工智能、神经科学和统计学等方面,为解决复杂问题提供了思路和手段。