编程5大算法总结--概念加实例

分治法,动态规划法,贪心算法这三者之间有类似之处,比如都需要将问题划分为一个个子问题,然后通过解决这些子问题来解决最终问题。但其实这三者之间的区别还是蛮大的。

贪心是则可看成是链式结构

回溯和分支界限为穷举式的搜索,其思想的差异是深度优先和广度优先

一:分治算法

一、基本概念

在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。n=2时,只要作一次比较即可排好序。n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。 具体可以:

分解(Divide):将原问题分解成一系列子问题;
解决(conquer):递归地解各个子问题。若子问题足够小,则直接求解;
合并(Combine):将子问题的结果合并成原问题的解。
合并排序(merge sort)是一个典型分治法的例子。其对应的直观的操作如下:

分解:将n个元素分成各含n/2个元素的子序列;
解决:用合并排序法对两个子序列递归地排序;
合并:合并两个已排序的子序列以得到排序结果。

二、基本思想及策略

分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。

三、分治法适用的情况

分治法所能解决的问题一般具有以下几个特征:

1) 该问题的规模缩小到一定的程度就可以容易地解决

2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。

3) 利用该问题分解出的子问题的解可以合并为该问题的解;

4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。

第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;

第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、

第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法

第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好

四、分治法的基本步骤

分治法在每一层递归上都有三个步骤:

step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;

step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题

step3 合并:将各个子问题的解合并为原问题的解。

它的一般的算法设计模式如下:

Divide-and-Conquer(P)

1. if |P|≤n0

2. then return(ADHOC(P))

3. 将P分解为较小的子问题 P1 ,P2 ,…,Pk

4. for i←1 to k

5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi

6. T ← MERGE(y1,y2,…,yk) △ 合并子问题

7. return(T)

其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。

实例:

 [找出伪币] 给你一个装有1 6个硬币的袋子。1 6个硬币中有一个是伪造的,并且那个伪造的硬币比真的硬币要轻一些。你的任务是找出这个伪造的硬币。

为了帮助你完成这一任务,将提供一台可用来比较两组硬币重量的仪器,利用这台仪器,可以知道两组硬币的重量是否相同。比较硬币1与硬币2的重量。假如硬币1比硬币2轻,则硬币1是伪造的;假如硬币2比硬币1轻,则硬币2是伪造的。这样就完成了任务。假如两硬币重量相等,则比较硬币3和硬币4。同样,假如有一个硬币轻一些,则寻找伪币的任务完成。假如两硬币重量相等,则继续比较硬币5和硬币6。按照这种方式,可以最多通过8次比较来判断伪币的存在并找出这一伪币。 

另外一种方法就是利用分而治之方法。假如把1 6硬币的例子看成一个大的问题。第一步,把这一问题分成两个小问题。随机选择8个硬币作为第一组称为A组,剩下的8个硬币作为第二组称为B组。这样,就把1 6个硬币的问题分成两个8硬币的问题来解决。第二步,判断A和B组中是否有伪币。可以利用仪器来比较A组硬币和B组硬币的重量。假如两组硬币重量相等,则可以判断伪币不存在。假如两组硬币重量不相等,则存在伪币,并且可以判断它位于较轻的那一组硬币中。最后,在第三步中,用第二步的结果得出原先1 6个硬币问题的答案。若仅仅判断硬币是否存在,则第三步非常简单。无论A组还是B组中有伪币,都可以推断这1 6个硬币中存在伪币。因此,仅仅通过一次重量的比较,就可以判断伪币是否存在。 

现在假设需要识别出这一伪币。把两个或三个硬币的情况作为不可再分的小问题。注意如果只有一个硬币,那么不能判断出它是否就是伪币。在一个小问题中,通过将一个硬币分别与其他两个硬币比较,最多比较两次就可以找到伪币。这样,1 6硬币的问题就被分为两个8硬币(A组和B组)的问题。通过比较这两组硬币的重量,可以判断伪币是否存在。如果没有伪币,则算法终止。否则,继续划分这两组硬币来寻找伪币。假设B是轻的那一组,因此再把它分成两组,每组有4个硬币。称其中一组为B1,另一组为B2。比较这两组,肯定有一组轻一些。如果B1轻,则伪币在B1中,再将B1又分成两组,每组有两个硬币,称其中一组为B1a,另一组为B1b。比较这两组,可以得到一个较轻的组。由于这个组只有两个硬币,因此不必再细分。比较组中两个硬币的重量,可以立即知道哪一个硬币轻一些。较轻的硬币就是所要找的伪币。

五、分治法的复杂性分析

一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:

T(n)= k T(n/m)+f(n)

通过迭代法求得方程的解:

递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。通常假定T(n)是单调上升的,从而当 mi≤n<mi+1时,T(mi)≤T(n)<T(mi+1)。

 

六、可使用分治法求解的一些经典问题

(1)二分搜索

(2)大整数乘法

(3)Strassen矩阵乘法

(4)棋盘覆盖

(5)合并排序

(6)快速排序

(7)线性时间选择

(8)最接近点对问题

(9)循环赛日程表

(10)汉诺塔

 

七、依据分治法设计程序时的思维过程

实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。

1、一定是先找到最小问题规模时的求解方法

2、然后考虑随着问题规模增大时的求解方法

3、找到求解的递归函数式后(各种规模或因子),设计递归程序即可。

应用之归并排序:

package sort;

public class mergerSort2 {

static void merge(int arr[], int L, int M, int R) {//合并算法
    int LEFT_SIZE = M - L;//left array size
    int RIGHT_SIZE = R - M + 1;
//    int left[LEFT_SIZE];
//    int right[RIGHT_SIZE];
    int left[] = new int[LEFT_SIZE];
    int right[] = new int[RIGHT_SIZE];
    int i, j, k;
    
    // 1. Fill in the left sub array
    for (i=L; i<M; i++) {
        left[i-L] = arr[i];//sub array begin from 0
    }
    // 2. Fill in the right sub array
    for (i=M; i<=R; i++) {
        right[i-M] = arr[i];//sub array begin from 0
    }
    
    // 3. Merge into the original array
    i = 0;  j = 0;  k = L;//k point to the left array
    while (i < LEFT_SIZE && j < RIGHT_SIZE) {//没有到达子矩阵的顶端,这时比较左右矩阵如果左面小arr[k]位置放入左面值,即达到取晓得放入新的矩阵,i,j,k指针各自加上1
        if (left[i] < right[j]) {
            arr[k] = left[i];
            i++;
            k++;
        }
        else {
            arr[k] = right[j];
            j++;
            k++;
        }
    }
    
    while (i < LEFT_SIZE) {//一边到达了顶端,那么如果另一方还没到直接放入新矩阵
        arr[k] = left[i];
        i++;
        k++;
    }
    while (j < RIGHT_SIZE) {
        arr[k] = right[j];
        j++;
        k++;
    }
}

static void mergeSort(int arr[], int L, int R) {//分治+归并!!!!
    if (L == R) {//分支的结束为只有一个
        return;
    }
    else {
        int M = (L + R) / 2;
        mergeSort(arr, L, M);
        mergeSort(arr, M+1, R);
        merge(arr, L, M+1, R);
    }
}

public static void main(String[] args) 	
{
    int arr[] = {6, 8, 10, 9, 4, 5, 2, 7,3};
    int L = 0;
    int R = arr.length;
    mergeSort(arr, L, R);
    
    int i;
    for (i=0; i<=R; i++) {
        System.out.println(arr[i]);
    }
 }
	
}

 

 

二:动态规划算法

一、基本概念

动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

二、基本思想与策略

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

三、适用的情况

能采用动态规划求解的问题的一般要具有3个性质:

(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

四、求解的基本步骤

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

图1 动态规划决策过程示意图

(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

(1)分析最优解的性质,并刻画其结构特征。

(2)递归的定义最优解。

(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值

(4)根据计算最优值时得到的信息,构造问题的最优解

五、算法实现的说明

动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

使用动态规划求解问题,最重要的就是确定动态规划三要素:

(1)问题的阶段 (2)每个阶段的状态

(3)从前一个阶段转化到后一个阶段之间的递推关系。

递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

六、动态规划算法基本框架

代码

1 for(j=1; j<=m; j=j+1) // 第一个阶段

2    xn[j] = 初始值;

3

4  for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段

5    for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式

6      xi[j]=j=max(或min){g(xi-1[j1:j2]), ……, g(xi-1[jk:jk+1])};

8

9 t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案

10

11 print(x1[j1]);

12

13 for(i=2; i<=n-1; i=i+1)

15 { 

17      t = t-xi-1[ji];

18

19      for(j=1; j>=f(i); j=j+1)

21         if(t=xi[ji])

23              break;

25 }

实例:背包问题(from http://www.cnblogs.com/xy-kidult/archive/2013/03/25/2970313.html)

背包问题具体例子:假设现有容量10kg的背包,另外有3个物品,分别为a1,a2,a3。物品a1重量为3kg,价值为4;物品a2重量为4kg,价值为5;物品a3重量为5kg,价值为6。将哪些物品放入背包可使得背包中的总价值最大?

  这个问题有两种解法,动态规划和贪婪算法。本文仅涉及动态规划。

  先不套用动态规划的具体定义,试着想,碰见这种题目,怎么解决?

  首先想到的,一般是穷举法,一个一个地试,对于数目小的例子适用,如果容量增大,物品增多,这种方法就无用武之地了。

  其次,可以先把价值最大的物体放入,这已经是贪婪算法的雏形了。如果不添加某些特定条件,结果未必可行。

  最后,就是动态规划的思路了。先将原始问题一般化,欲求背包能够获得的总价值,即欲求前i个物体放入容量为m(kg)背包的最大价值c[i][m]——使用一个数组来存储最大价值,当m取10,i取3时,即原始问题了。而前i个物体放入容量为m(kg)的背包,又可以转化成前(i-1)个物体放入背包的问题。下面使用数学表达式描述它们两者之间的具体关系。

  表达式中各个符号的具体含义。

  w[i] :  第i个物体的重量;

  p[i] : 第i个物体的价值;

  c[i][m] : 前i个物体放入容量为m的背包的最大价值;

  c[i-1][m] : 前i-1个物体放入容量为m的背包的最大价值;

  c[i-1][m-w[i]] : 前i-1个物体放入容量为m-w[i]的背包的最大价值;

  由此可得:

      c[i][m]=max{c[i-1][m-w[i]]+pi , c[i-1][m]}(下图将给出更具体的解释)

《编程5大算法总结--概念加实例》

    根据上式,对物体个数及背包重量进行递推,列出一个表格(见下表),表格来自(http://blog.csdn.net/fg2006/article/details/6766384?reload) ,当逐步推出表中每个值的大小,那个最大价值就求出来了。推导过程中,注意一点,最好逐行而非逐列开始推导,先从编号为1的那一行,推出所有c[1][m]的值,再推编号为2的那行c[2][m]的大小。这样便于理解。

《编程5大算法总结--概念加实例》

代码:


//该代码片段来自于: http://www.sharejs.com/codes/java/7212
package algorithm.dynamicplan; 
public class Knapsack { 
       
    /** 背包重量  */ 
    private int weight; 
       
    /** 背包物品价值  */ 
    private int value; 
    /***
     * 构造器
     */ 
    public Knapsack(int weight, int value) { 
        this.value = value; 
        this.weight = weight; 
    } 
    public int getWeight() { 
        return weight; 
    } 
       
    public int getValue() { 
        return value; 
    } 
       
    public String toString() { 
        return "[weight: " + weight + " " + "value: " + value + "]";   
    } 
}  
 
背包问题求解:
/**
 * 求解背包问题:
 * 给定 n 个背包,其重量分别为 w1,w2,……,wn, 价值分别为 v1,v2,……,vn
 * 要放入总承重为 totalWeight 的箱子中, 
 * 求可放入箱子的背包价值总和的最大值。
 * 
 * NOTE: 使用动态规划法求解 背包问题
 * 设 前 n 个背包,总承重为 j 的最优值为 v[n,j], 最优解背包组成为 b[n];
 * 求解最优值:
 * 1. 若 j < wn, 则 : v[n,j] = v[n-1,j];
 * 2. 若  j >= wn, 则:v[n,j] = max{v[n-1,j], vn + v[n-1,j-wn]}。
 * 
 * 求解最优背包组成:
 * 1. 若 v[n,j] > v[n-1,j] 则 背包 n 被选择放入 b[n], 
 * 2. 接着求解前 n-1 个背包放入 j-wn 的总承重中, 
 *    于是应当判断 v[n-1, j-wn] VS v[n-2,j-wn], 决定 背包 n-1 是否被选择。
 * 3. 依次逆推,直至总承重为零。
 *    
 *    重点: 掌握使用动态规划法求解问题的分析方法和实现思想。
 *    分析方法: 问题实例 P(n) 的最优解S(n) 蕴含 问题实例 P(n-1) 的最优解S(n-1);
 *              在S(n-1)的基础上构造 S(n) 
 *    实现思想: 自底向上的迭代求解 和 基于记忆功能的自顶向下递归
 */ 
package algorithm.dynamicplan; 
import java.util.ArrayList; 
public class KnapsackProblem { 
       
    /** 指定背包 */ 
    private Knapsack[] bags; 
       
    /** 总承重  */ 
    private int totalWeight; 
       
    /** 给定背包数量  */ 
    private int n; 
       
    /** 前 n 个背包,总承重为 totalWeight 的最优值矩阵  */ 
    private int[][] bestValues; 
       
    /** 前 n 个背包,总承重为 totalWeight 的最优值 */ 
    private int bestValue; 
       
    /** 前 n 个背包,总承重为 totalWeight 的最优解的物品组成 */ 
    private ArrayList<Knapsack> bestSolution; 
       
    public KnapsackProblem(Knapsack[] bags, int totalWeight) { 
        this.bags = bags; 
        this.totalWeight = totalWeight; 
        this.n = bags.length; 
        if (bestValues == null) { 
            bestValues = new int[n+1][totalWeight+1]; 
        } 
    } 
       
    /**
     * 求解前 n 个背包、给定总承重为 totalWeight 下的背包问题
     * 
     */ 
    public void solve() { 
           
        System.out.println("给定背包:"); 
        for(Knapsack b: bags) { 
            System.out.println(b); 
        } 
        System.out.println("给定总承重: " + totalWeight); 
           
        // 求解最优值 
        for (int j = 0; j <= totalWeight; j++) { 
            for (int i = 0; i <= n; i++) { 
               
                if (i == 0 || j == 0) { 
                    bestValues[i][j] = 0; 
                }    
                else  
                { 
                    // 如果第 i 个背包重量大于总承重,则最优解存在于前 i-1 个背包中, 
                    // 注意:第 i 个背包是 bags[i-1] 
                    if (j < bags[i-1].getWeight()) { 
                        bestValues[i][j] = bestValues[i-1][j]; 
                    }    
                    else  
                    { 
                        // 如果第 i 个背包不大于总承重,则最优解要么是包含第 i 个背包的最优解, 
                        // 要么是不包含第 i 个背包的最优解, 取两者最大值,这里采用了分类讨论法 
                        // 第 i 个背包的重量 iweight 和价值 ivalue 
                        int iweight = bags[i-1].getWeight(); 
                        int ivalue = bags[i-1].getValue(); 
                        bestValues[i][j] =  
                            Math.max(bestValues[i-1][j], ivalue + bestValues[i-1][j-iweight]);       
                    } // else 
                } //else          
           } //for 
        } //for 
           
        // 求解背包组成 
        if (bestSolution == null) { 
            bestSolution = new ArrayList<Knapsack>(); 
        } 
        int tempWeight = totalWeight; 
        for (int i=n; i >= 1; i--) { 
           if (bestValues[i][tempWeight] > bestValues[i-1][tempWeight]) { 
               bestSolution.add(bags[i-1]);  // bags[i-1] 表示第 i 个背包 
               tempWeight -= bags[i-1].getWeight(); 
           } 
           if (tempWeight == 0) { break; } 
        } 
        bestValue = bestValues[n][totalWeight]; 
    } 
       
    /**
     * 获得前  n 个背包, 总承重为 totalWeight 的背包问题的最优解值
     * 调用条件: 必须先调用 solve 方法
     * 
     */ 
    public int getBestValue() {  
        return bestValue; 
    } 
       
    /**
     * 获得前  n 个背包, 总承重为 totalWeight 的背包问题的最优解值矩阵
     * 调用条件: 必须先调用 solve 方法
     * 
     */ 
    public int[][] getBestValues() { 
           
        return bestValues; 
    } 
       
    /**
     * 获得前  n 个背包, 总承重为 totalWeight 的背包问题的最优解值矩阵
     * 调用条件: 必须先调用 solve 方法
     * 
     */ 
    public ArrayList<Knapsack> getBestSolution() { 
        return bestSolution; 
    } 
       
} 
 
背包问题测试:
view sourceprint?
package algorithm.dynamicplan; 
   
public class KnapsackTest { 
       
    public static void main(String[] args) { 
           
        Knapsack[] bags = new Knapsack[] { 
                new Knapsack(2,13), new Knapsack(1,10), 
                new Knapsack(3,24), new Knapsack(2,15), 
                new Knapsack(4,28), new Knapsack(5,33), 
                new Knapsack(3,20), new Knapsack(1, 8) 
        }; 
        int totalWeight = 12; 
        KnapsackProblem kp = new KnapsackProblem(bags, totalWeight); 
           
        kp.solve(); 
        System.out.println(" -------- 该背包问题实例的解: --------- "); 
        System.out.println("最优值:" + kp.getBestValue());  
        System.out.println("最优解【选取的背包】: "); 
        System.out.println(kp.getBestSolution()); 
        System.out.println("最优值矩阵:"); 
        int[][] bestValues = kp.getBestValues(); 
        for (int i=0; i < bestValues.length; i++) { 
            for (int j=0; j < bestValues[i].length; j++) { 
                System.out.printf("%-5d", bestValues[i][j]); 
            } 
            System.out.println(); 
        } 
    } 
}  
//动态规划法总结:
//
//1. 动态规划法用于求解非最优化问题:
//
//当问题实例P(n)的解由子问题实例的解构成时,比如 P(n) = P(n-1) + P(n-2) [斐波那契数列] ,而 P(n-1) 和 P(n-2)可能包含重合的子问题,
//可以使用动态规划法,通过自底向上的迭代,求解较小子问题实例的解,并作为求解较大子问题实例的解的基础。关键思想是: 避免对子问题重复求解。
//比如: 求斐波那契数 F(5):
//F(5)  = F(4) + F(3);
//子问题: F(4) = F(3) + F(2) 
//         F(3) = F(2) + F(1);
//         F(2) = F(1) + F(0)
//         F(2) = F(1) + F(0);
//
//子问题: F(3) = F(2) + F(1)
//
//          F(2) = F(1) + F(0)
//
//由上面的计算过程可知,如果单纯使用递归式,则子问题 F(2) 被重复计算了2次;当问题实例较大时,这些重复的子问题求解就会耗费大量不必要的时间。
//若使用动态规划法,将 F(2) 的值存储起来,当后续计算需要的时候,直接取出来, 就可以节省不少时间。
//另一个比较典型的例子是: 求解二项式系数  C(n, k) = C(n-1, k) + C(n-1, k-1)
//2. 动态规划法求解最优化问题:
//当问题实例P(n) 的最优解 可以从 问题实例 P(n-1) 的最优解 构造出来时,可以采用动态规划法,一步步地构造最优解。
//关键是掌握动态规划法求解问题时的分析方法,如何从问题导出 解的递推式。 实际上,当导出背包问题的递归式后,后来的工作就简单多了、
//如何分析背包问题,导出其最优解的递推式,我觉得,这才是最关键的地方!问题分析很重要!

 

三:贪心算法

一、基本概念:

所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。

贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。

所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。

 

二、贪心算法的基本思路:

1.建立数学模型来描述问题。

2.把求解的问题分成若干个子问题。

3.对每一子问题求解,得到子问题的局部最优解。

4.把子问题的解局部最优解合成原来解问题的一个解。

 

三、贪心算法适用的问题

贪心策略适用的前提是:局部最优策略能导致产生全局最优解。

实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。

 

四、贪心算法的实现框架

从问题的某一初始解出发;

while (能朝给定总目标前进一步)

{

利用可行的决策,求出可行解的一个解元素;

}

由所有解元素组合成问题的一个可行解;

 

五、贪心策略的选择

因为用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。

 

六、例题分析

下面是一个可以试用贪心算法解的题目,贪心解的确不错,可惜不是最优解。

[背包问题]有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。

要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。

物品 A B C D E F G

重量 35 30 60 50 40 10 25

价值 10 40 30 50 35 40 30

分析:

目标函数: ∑pi最大

约束条件是装入的物品总重量不超过背包容量:∑wi<=M( M=150)

(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?

(2)每次挑选所占重量最小的物品装入是否能得到最优解?

(3)每次选取单位重量价值最大的物品,成为解本题的策略。

值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。

贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。

可惜的是,它需要证明后才能真正运用到题目的算法中。

一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。

对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:

(1)贪心策略:选取价值最大者。反例:

W=30

物品:A B C

重量:28 12 12

价值:30 20 20

根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。

(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。

(3)贪心策略:选取单位重量价值最大的物品。反例:

W=30

物品:A B C

重量:28 20 10

价值:28 20 10

根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。

实例:活动安排from http://blog.csdn.net/lican19911221/article/details/24657581

/**
 * 活动安排问题(贪心算法)
 * @author Lican
 *
 */
public class ActionOrder {
	public int greedySelector(int[] s,int[] f,boolean[] a){
		int n=s.length-1;
		a[1]=true;//第一个活动被选中
		int j=1;
		int count=1;//被选中活动的数量,默认第一个活动被选中
		for(int i=2;i<=n;i++){
			if(s[i]>=f[j]){//下一个活动开始时间大于大于等于上一个活动结束时间
				a[i]=true;
				j=i;
				count++;
			}
			else{
				a[i]=false;
			}
		}
		return count;
	}
	public static void main(String[] args) {
		int s[]={-1,1,3,0,5,3,5,6,8,8,2,12};//默认下标从1开始(已非减序排好序),初始的-1无用
		int f[]={-1,4,5,6,7,8,9,10,11,12,13,14};
		boolean[] a=new boolean[s.length];
		ActionOrder ac = new ActionOrder();
		int counts=ac.greedySelector(s, f, a);
		System.out.println("活动集合中最大相容活动数量为:"+counts);
		for(int i=1;i<=s.length-1;i++){
			if(a[i]){
				System.out.println("第"+i+"活动被选中,其开始时间为:"+s[i]+",结束时间为:"+f[i]);
			}
		}
	}
}

 

四:回溯法

1、概念

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

2、基本思想

在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。

若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。

而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

3、用回溯法解题的一般步骤:

(1)针对所给问题,确定问题的解空间:

首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。

(2)确定结点的扩展搜索规则

(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

4、算法框架

(1)问题框架

设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。

(2)非递归回溯框架

   1: int a[n],i;

   2: 初始化数组a[];

   3: i = 1;

   4: while (i>0(有路可走)   and  (未达到目标))  // 还未回溯到头

   5: {

   6:     if(i > n)                                              //搜索到叶结点

   7:     {  

   8:           搜索到一个解,输出;

   9:     }

  10:     else                                                   //处理第i个元素

  11:     {

  12:           a[i]第一个可能的值;

  13:           while(a[i]在不满足约束条件且在搜索空间内)

  14:           {

  15:               a[i]下一个可能的值;

  16:           }

  17:           if(a[i]在搜索空间内)

  18:          {

  19:               标识占用的资源;

  20:               i = i+1;                              //扩展下一个结点

  21:          }

  22:          else

  23:         {

  24:               清理所占的状态空间;            //回溯

  25:               i = i –1;

  26:          }

  27: }

(3)递归的算法框架

回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:

   1: int a[n];

   2: try(int i)

   3: {

   4:     if(i>n)

   5:        输出结果;

   6:      else

   7:     {

   8:        for(j = 下界; j <=上界; j=j+1)  //枚举i所有可能的路径

   9:        {

  10:            if(fun(j))                 //满足限界函数和约束条件

  11:              {

  12:                 a[i] = j;

  13:               …                         // 其他操作

  14:                 try(i+1);

  15:               回溯前的清理工作(如a[i]置空值等);

  16:               }

  17:          }

  18:      }

  19: }

 

五:分支限界法

一、基本描述

类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。但在一般情况下,分支限界法与回溯法的求解目标不同。回溯法的求解目标是找出T中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解

(1)分支搜索算法

所谓“分支”就是采用广度优先的策略,依次搜索E-结点的所有分支,也就是所有相邻结点,抛弃不满足约束条件的结点,其余结点加入活结点表。然后从表中选择一个结点作为下一个E-结点,继续搜索。

选择下一个E-结点的方式不同,则会有几种不同的分支搜索方式。

1)FIFO搜索

2)LIFO搜索

3)优先队列式搜索

(2)分支限界搜索算法

二、分支限界法的一般过程

由于求解目标不同,导致分支限界法与回溯法在解空间树T上的搜索方式也不相同。回溯法以深度优先的方式搜索解空间树T,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树T

分支限界法的搜索策略是:在扩展结点处,先生成其所有的儿子结点(分支),然后再从当前的活结点表中选择下一个扩展对点。为了有效地选择下一扩展结点,以加速搜索的进程,在每一活结点处,计算一个函数值(限界),并根据这些已计算出的函数值,从当前活结点表中选择一个最有利的结点作为扩展结点,使搜索朝着解空间树上有最优解的分支推进,以便尽快地找出一个最优解。

分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。问题的解空间树是表示问题解空间的一棵有序树,常见的有子集树和排列树。在搜索问题的解空间树时,分支限界法与回溯法对当前扩展结点所使用的扩展方式不同。在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,那些导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被子加入活结点表中。此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所求的解或活结点表为空时为止。

三、回溯法和分支限界法的一些区别

有一些问题其实无论用回溯法还是分支限界法都可以得到很好的解决,但是另外一些则不然。也许我们需要具体一些的分析——到底何时使用分支限界而何时使用回溯呢?

回溯法和分支限界法的一些区别:

方法对解空间树的搜索方式 存储结点的常用数据结构 结点存储特性常用应用

回溯法深度优先搜索堆栈活结点的所有可行子结点被遍历后才被从栈中弹出找出满足约束条件的所有解

分支限界法广度优先或最小消耗优先搜索队列、优先队列每个结点只有一次成为活结点的机会找出满足约束条件的一个解或特定意义下的最优解

 

    原文作者:五大常用算法
    原文地址: https://blog.csdn.net/basycia/article/details/51848871
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞