算法分析

算法分析

基础知识

算法定义:

程序 = 算法 + 数据结构

算法是问题求解的有效策略.是解某一特定问题的一组有穷规则的集合。

算法特征 :

有限性、确定性、输入、输出、能行性

算法复杂性:

算法运行所需要的计算机资源的量
时间复杂性:需要时间资源的量
空间复杂性:需要空间资源的量
渐进上界,记作f(n) = O(g(n))。
渐进下界,记作f(n) = Ω(g(n))。
准确界,记作f(n) = Θ(g(n))。
绝对上界,记作f(n) = o(g(n))。

时间复杂度分析的步骤

1)确定用来表示问题规模的变量;
2)确定算法的基本操作;
3)写出基本操作执行次数的函数(运行时间函数);
4)如果函数依赖输入实例,则分情况考虑:最坏情况、最好情况、平均情况;
5)只考虑问题的规模充分大时函数的增长率,用渐近符号O 、Θ、Ω 、o来表示。
6)常用O和Θ

基本操作频率的统计

一般:
检索和排序问题,可选元素比较操作作为基本操作
矩阵乘法问题,可选数乘作为基本操作
遍历链表问题,可选设置或更新链表指针作为基本操作
图的遍历问题,可选访问图中的顶点的操作作为基本操作

最好情况、最坏情况、平均情况分析

有些算法的运行时间除与问题规模有关外,还与输
入元素的初始排列顺序有关。因此,有3种分析方法:
最好情况:依据输入元素顺序,算法所达到的最短运
行时间。
最坏情况:依据输入元素顺序,算法所达到的最长运
行时间。
平均情况:算法的运行时间取算法所有可能输入的平
均运行时间。

递归算法

递归的概念

一个递归模型是由递归边界和递归体两部分组成,前者确定递归到何时结束,后者确定递归求解时的递推关系。
递归的分类
直接递归:函数直接调用本身。
间接递归:一函数在调用其他函数时,又产生了对自身的调用。
什么时候使用递归?

  1. 问题的定义是递归的
  2. 数据结构是递归的
  3. 问题的求解方法是递归的

递归算法的设计方法

阶乘函数:

阶乘函数可递归地定义为:
在这里插入图片描述
代码实现:

void   main( )
{
        int   n;
        printf("请输入一个整数:");
        scanf("%d",   &n);
        printf("%d!  =  %d \n",   n,   fact(n));
}
int   fact(int  n)
{
        if(n  ==  1)    return(1);
        else  return(n * fact(n-1));
}

如求3的阶乘:
调用
和返回的递归示意图在这里插入图片描述

汉诺塔问题

问题分析:
可以用递归方法求解n个盘子的汉诺塔问题。
基本思想:
1个盘子的汉诺塔问题可直接移动。
n个盘子的汉诺塔问题可递归表示为,首先把上边的n-1个盘子从A柱借助C移到B柱,然后把最下边的一个盘子从A柱移到C柱,最后把移到B柱的n-1个盘子再借助A移到C柱。
对n个盘子从上至下依次编号1,2,…,n。
代码设计:

//伪代码
void Hanoi(int n,char a,char b,char c)
{
  if(n==1)//递归出口
       printf("\t将第%d个盘片从%c移动到%c\n",n,a,c);
  else
  {
       Hanoi(n-1,a,c,b);//把n-1个圆盘从fromPeg借助toPeg移至auxPeg
       printf("\t将第%d个盘片从%c移动到%c\n",n,a,c);
       move(n,a,c);
       Hanoi(n-1,b,a,c)//把n-1个圆盘从auxPeg借助fromPeg移至toPeg
  }
}

递归计算斐波那契序列

斐波那契数列Fib(n)的递归定义是:

在这里插入图片描述
求第n项斐波那契数列的递归函数如下:

long Fib(int n)
{        if(n == 0 || n == 1) return n; //递归出口
          else return Fib(n-1) + Fib(n-2); //递归调用
}

求第n项斐波那契数列的非递归函数如下:

long Fib ( int n )
{
    if ( n==1 )  return 1;
    if ( n==2 )  return 1; 
    long f1,f2,f3;
    f1 = 0, f2 = 1;
    for (int i=3; i<=n; i++) {
        f3 = f2+f1;
        f1 = f2, f2 = f3;
    }
    return f3;
} 

两种方法的时间复杂度分析:
上述循环方式的计算斐波那契数列的函数Fib2(n)的时间复杂度为O(n)。对比循环结构的Fib2(n)和递归结构的Fib(n)可发现:
循环结构的Fib2(n)算法在计算第n项的斐波那契数列时保存了当前已经计算得到的第n-1项和第n-2项的斐波那契数列,因此其时间复杂度为O(n);
而递归结构的Fib(n)算法在计算第n项的斐波那契数列时,必须首先计算第n-1项和第n-2项的斐波那契数列,而某次递归计算得出的斐波那契数列,如Fib(n-1)、Fib(n-2)等无法保存,下一次要用到时还需要重新递归计算,因此其时间复杂度为O(2n) 。

递归寻找线性表最大元素

问题描述:寻找线性表中最大的数据元素。
解决的基本思想:
将线性表分解为{a1a2……am}和{am+1……an}两个子表,分别求取子表的最大值ai和aj,比较ai和aj,得到其最大值就是整个线性表的最大值。
而子表中求取最大值的方法和原表相同。如此不断分解,直到表中只剩下一个数据元素(就是该子表的最大值)。

算法实现:

#include<stdio.h>
int Max(int L[],int i,int j)//求顺序表中最大数据元素
{
  int mid; int max,max1,max2;
  if(i==j)
     max=L[i];//递归出口
  else
  {
    mid=(i+j)/2;
    max1=Max(L,i,mid);//递归调用
    max2=Max(L,mid+1,j);//递归调用
    max=(max1>max2)?max1:max2;
  }
  return max;
}

函数调用与返回的过程

1.函数调用
当在一个函数的运行期间调用另一个函数时,在运行该被调用函数之前,需先完成三项任务:
a)将返回地址、所有实参等信息传递给被调用函数保存;
b)为被调用函数的局部变量分配存储区;
c)将控制转移到被调用函数的入口。
2.函数返回
从被调用函数返回调用函数之前,应该完成下列三项任务:
a)保存被调函数的计算结果;
b)释放被调函数保存局部变量的数据区;
c)依照被调函数保存的返回地址将控制转移到调用函数。

借助堆栈消除任何递归

因为堆栈的后进先出特点正好和递归函数的运行特点相吻合。所以原理上讲,任何递归都可以转换为非递归的算法。
利用栈可以将任何递归函数转换成非递归的形式.

分治策略

基本思想
对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小),则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题, 然后将各子问题的解合并,得到原问题的解。这种算法设计策略叫做分治法(divide and conquer)。
分治法在每一层递归上由三个步骤组成:
(1) 划分(divide):将原问题分解为若干规模较小、 相互独立、 与原问题形式相同的子问题。
(2) 解决(conquer): 若子问题规模较小,则直接求解;否则递归求解各子问题。
(3) 合并(combine): 将各子问题的解合并为原问题的解。
在这里插入图片描述

分治法的适用条件

分治法所能解决的问题一般具有以下几个特征:
1.该问题的规模缩小到一定的程度就可以容易地解决;
2.该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质
3.利用该问题分解出的子问题的解可以合并为该问题的解;
4.该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
Markdown将文本转换为 HTML

折半查找(二分搜索)

给定已按升序排好序的n个元素a[0:n-1],现要在这n个元素中找出一特定元素x。
分析:
该问题的规模缩小到一定的程度就可以容易地解决;
该问题可以分解为若干个规模较小的相同问题;
分解出的子问题的解可以合并为原问题的解;
分解出的各个子问题是相互独立的。
算法实现:



int bsearch(int b[], int x, int L, int R)
{    
  int mid;
  if(L > R) return(-1);
  mid = (L + R)/2;
  if(x == b[mid])      return mid;
  else if(x < b[mid])    return bsearch(b, x, L, mid-1);
  else return bsearch(b,x,mid+1,R);
}

算法复杂度分析:
每执行一次算法的while循环, 待搜索数组的大小减少一半。因此,在最坏情况下,while循环被执行了O(log2n) 次。循环体内运算需要O(1) 时间,因此整个算法在最坏情况下的计算时间复杂性为O(log2n) 。

合并排序

基本思想:将待排序元素分成大小大致相同的2个子集合,分别对2个子集合进行排序,最终将排好序的子集合合并成为所要求的排好序的集合。
在这里插入图片描述
算法实现:

void MergeSort(Type a[], int left, int right)
   {
      if (left<right) {//至少有2个元素
      int i=(left+right)/2;  //取中点
      MergeSort(a, left, i);
      MergeSort(a, i+1, right);
      merge(a, b, left, i, right);  //合并到数组b
      copy(a, b, left, right);    //复制回数组a
      }
   }

合并函数merge()的实现思想:
在这里插入图片描述
算法 (伪代码)Merge(B[0…p-1],C[0…q-1]),A[0…p+q-1]):

//将两个有序数组合并成一个有序数组
//输入:两个有序数组B[0..p-1]和C[0..q-1]
//输出:非降序列数组A-0+q-1]
i=0;  j=0;  k=0;
while (i<p and j<q do){
    if (B[i] ≤ C[j]) {
        A[k]=B[i];i=i+1;}
    else{
        A[k]=C[j];j=j+1;}
    k=k+1;
}
if (i=p)
    copy C[j..q-1] to A[k...p+q-1]
else
    copy B[j..p-1] to A[k...p+q-1]

合并排序时间复杂度:
最坏时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
辅助空间:O(n)

快速排序

基本思想:
选取A的某个元素t=A[s],然后将其他元素重新排列,使A[0…n-1]中所有在t以前出现的元素都小于或等于t,而在t之后出现的元素都大于或等于t。
划分元素(中轴)的选取:
可以随机选取,最简单的选择策略是数组第一个元素
划分的实现思想
同时从左、右开始扫描,左边找到一个比划分元素大的,右边找到一个比划分元素小的,然后交换两个元素的位置。
在这里插入图片描述
算法实现(伪代码):

算法  Quicksort(A[l...r])
//用Quicksort对子数组排序
//输入:数组A[0..n-1]中的子数组
//输出:非降序的子数组A[l...r]
if (l<r){
    s  Partition(A[l...r]);
    Quicksort(A[l...s-1]);
    Quicksort(A[s+1...r]);
}
算法 Partition(A[l..r])
//以第一个元素作为中轴,划分数组
//输入:数组A[l..r],l,r为左右下标
//输出:A[l..r]的一个分区,返回划分点位置
pA[l];
il; jr+1;
reapeat
    reapeat jj-1 until A[j] ≤p;
    reapeat ii+1 until A[i] ≥p;
    swap(A[i],A[j]);
until i ≥ j
swap(A[l],A[j]);
return j;

在快速排序中,记录的比较和交换是从两端向中间进行的,关键字较大的记录一次就能交换到后面单元,关键字较小的记录一次就能交换到前面单元,记录每次移动的距离较大,因而总的比较和移动次数较少。
快速排序算法的性能取决于划分的对称性。通过修改算法partition,可以设计出采用随机选择策略的快速排序算法。在快速排序算法的每一步中,当数组还没有被划分时,可以在a[p:r]中随机选出一个元素作为划分基准,这样可以使划分基准的选择是随机的,从而可以期望划分是较对称的。
时间复杂度分析:
最坏时间复杂度:O(n2)
平均时间复杂度:O(nlogn)
辅助空间:O(n)或O(logn)

找最大值与最小值

含有n个不同元素的集合中同时找出它的最大值和最小值
如果我们将分治策略用于此问题,每次将问题分成大致相等的两部分,分别在这两部分中找出最大值与最小值,再将这两个子问题的解组合成原问题的解,就可得到该问题的分治算法。算法描述如下:

//伪代码
REC-MAXMIN (i,j,fmax,fmin)
{ 
     if i=j 
         then fmax ← fmin ← A[i] 
     if i=(j-1) 
              then if A[i]>A[j] 
                             then fmax ← A[i]   
                                    fmin ← A[j] 
                             else fmax ← A[j] 
                                     fmin ← A[i] 
              else mid ← [(i+j)/2] 
                      REC-MAXMIN(i,mid,gmax,gmin) 
                      REC-MAXMIN(mid+1, j, hmax,hmin) 
                      fmax ← max{gmax,hmax} 
                      fmin ←min{gmin,hmin} 
}

贪心策略

贪心算法的特点:
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。
贪心算法不能对所有问题都得到整体最优解。
在许多情况下,应用贪心算法能够得到整体最优解;在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
贪心算法中,较大子问题的解恰好包含了较小子问题的解作为子集。
贪心算法在某一步决定优化函数的最大或最小值时,不考虑子问题的计算结果,而是根据当时情况采取“只顾眼前”的贪心策略
最优化问题数学模型描述:
目标函数、约束条件、可行解、最优解
贪心算法的设计要素:
最优子结构性质
贪心选择性质
贪心法的正确性问题

活动安排问题

活动实例:s[i] 活动开始时间 f[i] 活动结束时间
在这里插入图片描述

策略:早完成的活动先安排(如上图)。
把活动按照截止时间从小到大排序,使得f1 ≤ f2 ≤… ≤fn ,然后从前向后挑选,只要与前面选择的活动相容,便将这项活动选入最大相容集合A。
由于输入的活动以其完成时间的非减序排列,所以算法greedySelector每次总是选择具有最早完成时间的相容活动加入集合A中。直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。
解上图:
首先选定活动1,其结束时间f[1]=4。
然后因s[4] = 5≥4,选定活动4,这时f[4] = 7。
又因s[8] = 8≥7,选定活动8,这时f[8] = 11。
最后因s[11] = 12≥11,选定活动11。

template<class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[]) {
    A[1] = true;
    int j = 1;
    for (int i=2;i<=n;i++) {
        if (s[i]>=f[j]) { 
            A[i]=true; 
            j=i; 
        }
        else A[i]=false;
    }
}

各活动起始时间与结束时间分别存储于数组s和f中,且f中活动按结束时间非递减排序,即f1 ≤f2 ≤… ≤fn。
时间复杂度O(n)
预排序O(nlogn)

背包问题

问题描述:
给定n个物品和一个背包。物品i的重量为wi,价值为vi,背包容量为C。问如何选择装入背包中的物品,使得装入背包的物品的价值最大?
在装入背包时,每种物品i只有两种选择,装入或者不装入,既不能装入多次,也不能只装入一部分。因此,此问题称为0-1背包问题。
如果在装入背包时,物品可以切割,即可以只装入一部分,这种情况下的问题称为背包问题。
这两类问题都具有最优子结构性质,极为相似,但背包问题可用贪心算法求解,而0-1背包问题却不能用贪心算法求解。
0-1背包问题与背包问题:
0-1背包问题可描述为:给定c>0, wi>0, vi>0, 1 ≤ i ≤ n,
在这里插入图片描述
背包问题可描述为:给定c>0, wi>0, vi>0, 1 ≤ i ≤ n,
在这里插入图片描述
背包问题的形式描述:

在这里插入图片描述
背包问题的贪心策略:
1.以价值作为度量
2.以容量作为量度标准
3.最优的度量标准(按照物品的单位效益值)
首先计算每种物品单位重量的价值vi/wi,然后,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。若将这种物品全部装入背包后,背包内的物品总重量未超过C,则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直地进行下去,直到背包装满为止。
算法实现:

void Knapsack(int n, float M, float v[], float w[], float x[]) {
    Sort (n, v, w);
    int i;
    for (i=1; i<=n; i++) x[i]=0;
    float c = M;
    for (i=1; i<=n; i++) {
          if (w[i] > c) break;
          x[i]=1;
          c -= w[i];
    }
    if (i<=n) x[i]=c/w[i];
}

对于0-1背包问题,贪心选择之所以不能得到最优解是因为在这种情况下,它无法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值降低了。
(动态规划)
算法knapsack的主要计算时间在于将各种物品依其单位重量的价值从大到小排序。因此,算法的计算时间上界为O(nlogn)。
为了证明算法的正确性,还必须证明背包问题具有贪心选择性质。

最优装载问题

问题描述:
有一批集装箱要装上一艘载重量为c的轮船。其中集装箱i的重量为wi。最优装载问题要求确定在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。
0-1背包问题的特例。具有贪心选择性质和最优子结构性质
在这里插入图片描述
将集装箱按从轻到重排序,轻者先装。

template<class Type>
void Loading(int x[],  Type w[], Type c, int n) {
    int* t = new int[n+1];
    Sort(w, t, n);
    for (int i = 1; i <= n; i++) x[i] = 0;
    for (int i = 1; i <= n && w[t[i]] <= c; i++) {
        x[t[i]] = 1; 
        c -= w[t[i]];
    }
}

算法loading的主要计算量在于将集装箱依其重量从小到大排序,故算法所需的计算时间为 O(nlogn)。

Prim算法(最小生成树)

特点:(归并顶点)
设:N =(V , E)是个连通网,
另设U为最小生成树的顶点集,TE为最小生成树的边集。
构造步骤:
(1)初始状态: U ={u0 },( u0 ∈V ), TE={ },
(2)从E中选择顶点分别属于U、V-U两个集合、且权值最小的边( u0, v0),将顶点v0归并到集合U中,边(u0, v0)归并到TE中;
(3)直到U=V为止。此时TE中必有n-1条边,
T=(V,{TE})就是最小生成树。
在这里插入图片描述
Prim算法特点: 将顶点归并,与边数无关,适于稠密网。
故采用邻接矩阵作为图的存储表示。
[注]:在最小生成树的生成过程中,所选的边都是
一端在V-U中,另一端在U中。
设计思路:
增设一辅助数组Closedge[ n ], 每个数组分量都有两个域:
在这里插入图片描述
要求:使Colsedge[i]. lowcost = min( ( u ,vi ) ) u 属于 U
struct {
VertexType adjvex; // U集中的顶点序号
VRType lowcost; // 边的权值
} closedge[MAX_VERTEX_NUM] ;
在这里插入图片描述
显然, Prim算法的时间效率=O(n2)
adjvex的类型是顶点名称的类型。集合U和Y-U中都是顶点的名称。行i的内容是下标。
算法实现步骤:

  1. 初始化辅助数组closedge(lowcost和adjvex);
  2. 输出顶点u0,将顶点u0加入集合U中;
  3. 重复执行下列操作n-1次
    3.1 在lowcost中选取最短边,取adjvex中对应的顶点序号k;
    3.2 输出顶点k和对应的权值及对应的另外一个顶点;
    3.3 将顶点k加入集合U中;
    3.4 调整数组closedge中的lowcost和adjvex;
//伪代码
void MiniSpanTree_P ( MGraph G, VertexType u) 
{  //用普里姆算法从顶点u出发构造网G的最小生成树
   k = LocateVex ( G, u ); 
   for ( j=0; j<G.vexnum; ++j )  // 辅助数组初始化
      if (j!=k)  
          closedge[j] = { u, G.arcs[k][j].adj };  
   closedge[k].lowcost = 0;      // 初始,U={u}
   for (i=0; i<G.vexnum; ++i) //选择其余G.vexnum-1个顶点
  {    k = minimum(closedge);  
                  // 求出T的下一个结点:第k顶点
      printf (closedge[k].adjvex, G.vexs[k]); 
                    // 输出生成树上一条边和对应的顶点
     closedge[k].lowcost = 0;    // 第k顶点并入U集
     for (j=0; j<G.vexnum; ++j)  //修改其它顶点的最小边
     if (G.arcs[k][j].adj < closedge[j].lowcost)
           //新顶点并入U后重新选择最小边
      closedge[j] = { G.vexs[k], G.arcs[k][j].adj }; 
  }//for
}// MiniSpanTree

Kruskal算法(最小生成树)

基本思想:在保证无回路的前提下依次选出权重较小的n – 1条边。
贪心策略:如果(i, j)是E中尚未被选中的边中权重最小的,并且(i, j)不会与已经选择的边构成回路,于是就选择 (i, j)。
若边(i, j) 的两个端点i和j属于同一个连通分支,则选择(i, j) 会造成回路,反之则不会造成回路
因此初始时将图的n个顶点看成n个孤立分支。
kruskal算法的数据结构:
1.结构数组e[]表示图的边,e[i].u、e[i].v和e[i].w分别表示边i的两个端点及其权重。
2.函数Sort(e, w)将数组e按权重w从小到大排序。
3.一个连通分支中的顶点表示为一个集合。
4.函数Initialize(n)将每个顶点初始化为一个集合
5.函数Find(u)给出顶点u所在的集合。
6.函数Union(a, b)给出集合a和集合b的并集。
7.重载算符!=判断集合的不相等。
在这里插入图片描述
Kruskal算法实现:

Kruskal(int n, *e)  {
  Sort(e, w); //将边按权重从小到大排序
  initialize(n); //初始时每个顶点为一个集合
  k = 1; //k累计已选边的数目,
  j = 1;  //j为所选的边在e中的序号
  while (k < n) //选择n – 1条边
    {a = Find(e[j].u); b = Find(e[j].v); 
   //找出第j条边两个端点所在的集合
     if (a != b) {T[k] = j; Union(a, b);k=k+1;}
   //若不同,第j条边放入树中并合并这两个集合
     j++ }} //继续考察下一条边

Kruskal与Prim算法的复杂度

Prim算法为两重循环,外层循环为n次,内层循环为O(n),因此其复杂性为O(n2)。
Kruskal算法中,设边数为e,则边排序的时间为O(eloge),最多对e条边各扫描一次,每次确定边的时间为O(loge),所以整个时间复杂性为O(eloge)。
当e = Ω(n2)时,Kruskal算法要比Prim算法差;
当e = ο(n2)时,Kruskal算法比Prim算法好得多。

单源最短路径(Dijkstra算法)

给定一个图G = (V, E),其中每条边的权是一个非负实数。另外给定V中的一个顶点v,称为源。求从源v到所有其它各个顶点的最短路径。
单源最短路径问题的贪心选择策略:选择从源v出发目前用最短的路径所到达的顶点,这就是目前的局部最优解。

单元最短路径的贪心算法:
基本思想:首先设置一个集合S;用数组dis[]来记录v到S中各点的目前最短路径长度。然后不断地用贪心选择来扩充这个集合,并同时记录或修订数组dis[];直至S包含所有V中顶点。
贪心选择:一个顶点u属于S当且仅当从v到u的最短路径长度已知。
初始化:S中仅含有源v。

Dijkstra算法:
Dijkstra 算法的做法是:
由近到远逐步计算,每次最近的顶点的距离就是它的最短路径长度。
然后再从这个最近者出发。即依据最近者修订到各顶点的距离,然后再选出新的最近者。
如此走下去,直到所有顶点都走到。

Dijkstra算法举例

在这里插入图片描述
由数组dis[i]可知:从顶点1到顶点2、3、4、5的最短通路的长度分别为10、50、30和60。

算法实现:

Procedure Dijkstra {
(1) S:={1}; //初始化S
(2) for i:= 2  to n do //初始化dis[]
(3) dis[i] =C[1, i] ; //初始时为源到顶点i一步的距离
(4) for i :=1  to  n do {
(5)    从V-S中选取一个顶点u使得dis[u]最小;
(6)    将u加入到S中;//将新的最近者加入S
(7)    for w∈V-S do  //依据最近者u修订dis[w]
(8)    dis[w] := min(dis[w] , dis[u]+C[u ,w])  
        }
    }

Dijkstra算法的计算复杂性:

Dijkstra 算法有两层循环,外层循环为n次,内层有两个循环:一个是选出最小的u(第5行),另一个是修订disw,内层循环的时间为O(n)。
因此Dijkstra算法的时间复杂度为 O(n2)。
Dijkstra 算法能求出从源到其它各顶点的最短通路的长度,但是却并没有给出其最短通路。
对Dijkstra 算法做适当的修改便可求出最短通路。

哈夫曼编码

问题:通讯过程中需将传输的信息转换为二进制码.由于英文字母使用频率不同,若频率高的字母对应短的编码,频率低的字母对应长的编码,传输的数据总量就会降低.要求找到一个编码方案,使传输的数据量最少.

利用哈夫曼树可以构造一种不等长的二进制编码,并且构造所得的哈夫曼编码是一种最优前缀编码,即使所传电文的总长度最短。
例:假设有5个符号以及它们的频率:
A B C D E
6 7 2 5 9
求前缀码。
在这里插入图片描述

回溯法

引言:
理论上
寻找问题的解的一种可靠的方法是首先列出所有候选解,然后依次检查每一个,在检查完所有或部分候选解后,即可找到所需要的解。
但是
当候选解数量有限并且通过检查所有或部分候选解能够得到所需解时,上述方法是可行的。
若候选解的数量非常大(指数级,大数阶乘),即便采用最快的计算机也只能解决规模很小的问题。
于是
回溯和分枝限界法是比较常用的对候选解进行系统检查两种方法。
按照这两种方法对候选解进行系统检查通常会使问题的求解时间大大减少(无论对于最坏情形还是对于一般情形) 。
可以避免对很大的候选解集合进行检查,同时能够保证算法运行结束时可以找到所需要的解。
通常能够用来求解规模很大的问题。

回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。
回溯法的基本步骤
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
常用剪枝函数
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。
解空间树:
子集树通常有2n个叶结点,结点总数为2n+1-1。需Ω(2n)计算时间。
排列树通常有n!个叶结点。因此遍历排列树需要Ω(n!)计算时间。
满m叉树通常有mn个叶结点,需Ω(mn)计算时间。

0-1背包问题

0-1背包问题
n=3, C=30, w={16, 15, 15}, v={45,25,25}
开始时,
Cr=C=30,V=0
C为容量,Cr为剩余空间,V为价值。
A为唯一活结点,也是当前扩展结点。
注意:只用到了约束函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
扩展到G以后……
Cr=30,V=0,活结点为A、C、G,G为当前扩展结点
扩展G,先到达N,N是叶结点,且25<50,不是最优解,又N不可扩展,返回到G
再扩展G到达O,O是叶结点,且0<50,不是最优解,又O不可扩展,返回到G
G没有可扩展结点,成为死结点,返回到C
C没有可扩展结点,成为死结点,返回到A
A没有可扩展结点,成为死结点,算法结束
最优解X=(0,1,1),最优值50。

解空间:
bag:array[1…maxn] of 0…1;
其中,bag[k]=1表示第k个物品要放
0表示第k个物品不放
约束条件:
第k个物品确定放置方法后,背包所剩重量足够放第K个物品
wei-bag[k]*w[k]>=0 {wei:背包还能装的重量}

状态树:子集树
求最优值:
Best:=0; 表示最大价值
当一组可行解产生后,得到当前总价值count,
if count>best then best:=count
利用这种方法记录最优解!如果还要记录最优时的物品取法应:
if count>best then{
best:=count;
for i:=1 to n do y[i]:=bag[i];}
算法分析:
本题可以用递归求解:
设当前有N个物品,容量为M;
这些物品要么选,要么不选,我们假设选的第一个物品编号为i(1到i-1号物品不选),问题又可以转化为有N-i个物品(即第i+1~N号物品),容量为M-Wi的子问题……如此反复下去,然后在所有可行解中选一个效益最大的便可。
回溯(状态恢复)后,需恢复的状态有:
Bag[k]
背包可装的物品重量:wei:=wei+w[k]*i
已装物品的价值总和:count:=count-v[k]*i
算法实现:

//伪代码
procedure work(k,wei:integer);
    var i,j:integer;
    for i:=1 downto 0 do
        if  (wei-w[k]*i>=0) 
          {
           bag[k]:=i;
           count:=count+v[k]*i;
           if (k=n) and (count>best)
             {best:=count;
              for j:=1 to n do y[j]:=bag[j];
             }
           if k<n then work(k+1,wei-w[k]*i);
           count:=count-v[k]*i;    {状态恢复}
           }
     

旅行商问题

问题描述(只用到了限界函数)
每个城市一遍,最后回到住地的路线,使总的路程最短。
该问题是一个NP完全问题, 有(n-1)!条可选路线
最优解(1,3,2,4,1),最优值25
在这里插入图片描述
回溯算法将用深度优先方式从根节点开始,通过搜索解空间树发现一个最小耗费的旅行。
一个可能的搜索为 ABCFL。L点,旅行1-2-3-4-1作为当前最好的旅行被记录下来,耗费 59。
从L点回溯到活节点F,F没有未被检查的孩子,所以它成为死节点,回溯到 C点。
C变为E-节点,向前移动到G,然后是M。这样构造出了旅行1-2-4-3-1,它的耗费是66。不比当前的最佳旅行好,抛弃它并回溯到G,然后是C,B。
从B点,搜索向前移动到D,然后是H,N。这个旅行1-3-2-4-1的耗费是25,比当前的最佳旅行好,把它作为当前的最好旅行。
从N点,搜索回溯到H,然后是D。在D点,再次向前移动,到达O点。
如此继续下去,可搜索完整个树,得出1-3-2-4-1是最少耗费的旅行,耗费值为25。

装载问题

问题描述
一批共n个集装箱要装上2艘载重量分别为C1和C2的轮船,其中集装箱i的重量为wi,且∑wi≤C1+C2
要求确定是否有一个合理的装载方案可将n个集装箱装上2艘轮船。
如果一个给定装载问题有解,采用下面的策略可得到最优装载方案:
(1)首先将第一艘轮船尽可能装满
选取子集,重量和最接近C1。
(2)将剩余的集装箱装上第二艘轮船。
例如
n=3,c1=c2=50,且w=[10,40,40]时,集装箱1、2装第一艘船,3装第二艘船。
w=[20,40,40]时,无可行解。
当∑wi =c1+c2时,两艘船的装载问题等价于子集之和(sum-of-subset)问题,即有n个数字,要求找到一个子集(如果存在的话)使它的和为c1。
当c1=c2 且∑wi=2c1 时,两艘船的装载问题等价于分割问题(partition problem) ,即有n个数字ai , ( 1≤i≤n),要求找到一个子集(若存在),使得子集之和为∑ai/2。
分割问题和子集之和问题都是NP-复杂问题。

问题分析
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,重量之和最接近轮船载重量。
由此可知,装载问题等价于以下特殊的0-1背包问题。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

算法设计:

解空间:子集树
可行性约束(剪枝)函数:W X总和小于等于C1
限界函数(不选择当前元素)
当前重量 + 剩余集装箱重量 ≤ 当前最优载重量
CW+R ≤ BestW
用回溯法设计解装载问题的O(2n)计算时间算法,某些情况下优于动态规划算法。

算法实现:

template <class Type >
Type Maxloading(type w[], type c,int n)
{
    loading <Type> X;
    //初始化X
    X. w=w;  //集装箱重量数组
    X. c=c;   //第一艘船载重量
    X. n=n;//集装箱数
    X. bestw=0; //当前最优载重
    X. cw=0;//当前载重量
    X. r=0; //剩余集装箱重量 
    for (int i=1; i<=n; i++)
        X. r +=w[i]
  //计算最优载重量
    X.Backtrack(1)return X.bestw;
 }

算法MaxLoading调用递归函数Backtrack(1)实现回溯搜索。

template<classType>
void Loading<Type>::Backtrack(int i)
{ / /搜索第i层结点
    if (i>n) {//到达叶结点
         bestw=cw;
         return}
    //搜索子树
    r - = w[i];
    if (cw+w[i]]<=c){ //x[i]=1左孩子
         cw += w[i];    
         Backtrack (i+1);
         cw - = w[i]; }
     if (cw+r > bestw){ //控制剪去右子树
         Backtrack(i+1);
    r+=w[i] 
    }

构造最优解:

if (i > n) {// 在叶节点上
 for (int j = 1; j <= n; j++)  bestx[j] = x[j];//最优解
 bestw = cw; return;}

算法描述:

在这里插入图片描述

N皇后问题

问题描述:
在一个n*n的国际象棋棋盘上放置n个皇后,使得它们中任意2个之间都不互相“攻击”,即任意2个皇后不可在同行、同列、同斜线上。
输出N,⑴求N皇后问题的一种放法;
⑵求N皇后问题的所有放法;
分析:
N=4时,下图是一组解:
在这里插入图片描述
要素一: 解空间
一般想法:利用二维数组,用[i,j]确定一个皇后位置!
优化:利用约束条件,只需一维数组即可!
x:array[1…n] of integer;
x[i]:i表示第i行皇后
x[i]表示第i行上皇后放第几列
要素二:约束条件
不同行:数组x的下标保证不重复
不同列:x[i]<>x[j] (i<=n,j<=n;i<>j)
不同对角线:abs(x[i]-x[j])<>abs(i-j)
填到第K行时,就与前1~(K-1)行都进行比较
Function Place(k:integer):boolean;
place:=true;
for j←1 to k-1 do
if |k-j|=|x[j]-x[k]| or x[j]=x[k] then
place:= false
要素三:状态树
将搜索过程中的每个状态用树的形式表示出来!
画出状态树对书写程序有很大帮助!
在这里插入图片描述

在这里插入图片描述

程序结束条件:
一组解:设标志,找到一解后更改标志,以标志做为结束循环的条件。
所有解:k=0
判断约束函数:
Function Place(k:integer):boolean;
place:=true;
for j←1 to k-1 do
if |k-j|=|x[j]-x[k]| or x[j]=x[k] then
place:= false
程序实现:
回溯算法可用非递归和递归两种方法实现!
非递归写法:

Nqueens()
{
     x[1]0
     k ← 1
     while k>0 do
        {
            x[k] ← x[k] +1
            while x[k]<=n and (not place(k)) do
                      x[k] ← x[k] +1
             if  x[k]<=n then
                         if k=n  then  sum ← sum+1
                         else   {
                                      k ← k+1
                                      x[k]0
                                   }
                else k ← k-1
        }
  }

算法描述:
产生一种新放法
冲突,继续找,直到找到不冲突----不超范围
if 不冲突 then k<nk+1
k=n一组解
if 冲突 then 回溯

递归写法:

procedure try(k:byte);
 var i:byte;
 begin
  for i:=1 to n do  {每层均有n种放法}
   if place(k) then   {寻找放置皇后的位置}
    begin    
     x[k]:=i;   {放置皇后)
     if k=n then print  {n个皇后都放置好,输出}
    {若只想找一组解,halt}
     else try(k+1); {继续递归放置下一个皇后}
    end;
 end;

图的m着色问题

1 .基本概念
图的m可着色判定问题
给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。
该图的色数
若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色,则称这个数m为该图的色数。
可着色优化问题
求图的色数m的问题称为图的m可着色优化问题。
在这里插入图片描述
在这里插入图片描述

2 问题分析
解向量
(x1, x2, … , xn)表示顶点i所着颜色x[i]
可行性约束函数
顶点i与已着色的相邻顶点颜色不重复。
子集树:

在这里插入图片描述
算法描述:

void Color::Backtrack(int t)
{ if (t>n) {
  sum++;
  for (int i=1; i<=n; i++) cout << x[i] << ' ';
  cout << endl;
  }
  else
    for (int i=1;i<=m;i++) {
      x[t]=i;
      if (Ok(t)) Backtrack(t+1);
    }
}
bool Color::Ok(int k) // 检查颜色可用性
{ for (int j=1;j<=n;j++)
    if ((a[k][j]==1)&&(x[j]==x[k])) return false;
  return true;
}

分支限界法

搜索算法类型:
基于枚举策略的搜索
深度优先搜索
广度优先搜索
优化+枚举的搜索
回溯算法=深度优先搜索+剪枝策略
分支限界算法=广度优先搜索+剪枝策略
步骤:
1.将根结点加入队列中
2.接着从队列中取出首结点,使其成为当前扩展结点,一次性生成它的所有孩子结点,判断孩子结点是舍弃还是保存。舍弃那些不可能导致可行解或最优解的孩子结点,其余的结点则保存在队列中
3.重复上述扩展过程,直到找到问题的解或队列为空时为止。
注意:
每一个活结点最多只有一次机会成为扩展结点。
分类(根据活结点表的维护方式)
队列式分支限界法
优先队列式分支限界法
分支限界法的一般解题步骤为:
定义问题的解空间
确定问题的解空间组织结构(树或图)
搜索解空间。搜索前要定义判断标准(约束函数或限界函数),如果选用优先队列式分支限界法,则必须确定优先级。

0-1背包问题

考虑实例n=4,w=[3,5,2,1],v=[9,10,7,4],C=7。
定义问题的解空间:
该实例的解空间为(x1,x2,x3,x4),xi=0或1(i=1,2,3,4)。
确定问题的解空间组织结构:
该实例的解空间是一棵子集树,深度为4。
搜索解空间:
约束条件:总wx<=C
限界条件: cp+rp>bestp

cp初始值为0;rp初始值为所有物品的价值之和;bestp表示当前最优解,初始值为0。
当cp>bestp时,更新bestp为cp。
图解:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
应用分支限界法的关键问题
(1)如何确定合适的限界函数
(2)如何组织待处理结点表
(3)如何确定最优解中的各个分量

旅行商问题

问题描述:
某售货员要到若干城市去推销商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过每个城市一次,最后回到驻地的路线,使总的路程(或总旅费)最小。

在这里插入图片描述
问题的解空间(x1,x2,x3,x4),其中令S={1,2,3,4}, x1=1,x2∈S-{x1},x3∈S-{x1,x2},x4 ∈ S-{x1,x2,x3}。
解空间的组织结构是一棵深度为4的排列树。
搜索:
约束条件g[i][j]!=∞,其中g是该图的邻接矩阵;
限界条件:cl<bestl,其中cl表示当前已经走的路径长度,初始值为0;bestl表示当前最短路径长度,初始值为∞。

队列式分支限界法:
在这里插入图片描述
在这里插入图片描述

优先队列式分支限界法:
优先级:活结点所对应的已经走过的路径长度cl,长度越短,优先级越高。
在这里插入图片描述
在这里插入图片描述

算法优化:
使用贪心策略计算近似最优解zl;
使用zl的值作为bestl的初始值;
优先级:活结点的cl,cl越小,优先级越高;
限界条件:cl<bestl,

布线问题

问题描述:
在N*M的方格阵列中,指定一个方格的中点为a,另一个方格中的中点为b,问题要求找出a到b的最短布线方案(即最短路径)。布线时只能沿直线或直角,不能走斜线。黑色的单元格代表不可以通过的封锁方格。如下图:

在这里插入图片描述

问题分析
1.将方格抽象为顶点,中心方格和相邻四个方向(上、下、左、右)能通过的方格用一条边连起来。这样,可以把问题的解空间定义为一个图。
2.该问题是特殊的最短路径问题,特殊之处在于用布线走过的方格数代表布线的长度,布线时每布一个方格,布线长度累加1。
3.只能朝上、下、左、右四个方向进行布线

在这里插入图片描述

分支限界法与回溯法的比较

相同点
均需要先定义问题的解空间,确定的解空间组织结构一般都是树或图。
在问题的解空间树上搜索问题解。
搜索前均需确定判断条件,该判断条件用于判断扩展生成的结点是否为可行结点。
搜索过程中必须判断扩展生成的结点是否满足判断条件,如果满足,则保留该扩展生成的结点,否则舍弃。

不同点
搜索目标:回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。
搜索方式不同:回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树
扩展方式不同:在回溯法搜索中,扩展结点一次生成一个孩子结点,而在分支限界法搜索中,扩展结点一次生成它所有的孩子结点。

动态规划

动态规划方法的关键在于正确地写出基本的递推关系式和恰当的边界条件。
动态规划算法的基本要素:
1.最优子结构
2.重叠子问题
3.备忘录方法
动态规划基本步骤:
1.找出最优解的性质,并刻划其结构特征。
2.递归地定义最优值。
3.以自底向上的方式计算出最优值。
4.根据计算最优值时得到的信息,构造最优解。

多段图最短路径

问题描述:
设图G=(V, E)是一个带权有向连通图,如果把顶点集合V划分成k个互不相交的子集Vi(2≤k≤n, 1≤i≤k),
使得E中的任何一条边(u, v),必有u∈Vi,v∈Vi+m(1≤i<k, 1<i+m≤k),
则称图G为多段图,称s∈V1为源点,t∈Vk为终点。多段图的最短路径问题是求从源点到终点的最小代价路径。

由于多段图将顶点划分为k个互不相交的子集,所以,多段图划分为k段,每一段包含顶点的一个子集。
不失一般性,将多段图的顶点按照段的顺序进行编号,同一段内顶点的相互顺序无关紧要。
假设图中的顶点个数为n,则源点s的编号为0,终点t的编号为n-1,并且,对图中的任何一条边(u, v),顶点u的编号小于顶点v的编号。
在这里插入图片描述

对多段图的边(u, v),用cuv表示边上的权值,将从源点s到终点t的最短路径记为d(s, t),
则从源点0到终点9的最短路径d(0, 9)由下式确定:
d(0, 9)=min{c01+d(1, 9), c02+d(2, 9), c03+d(3, 9)}

这是最后一个阶段的决策,它依赖于d(1, 9)、d(2, 9)和d(3, 9)的计算结果,而
d(1, 9)=min{c14+d(4, 9), c15+d(5, 9)}
d(2, 9)=min{c24+d(4, 9), c25+d(5, 9), c26+d(6, 9)}
d(3, 9)=min{c35+d(5, 9), c36+d(6, 9)}
依次往下推导(此处省略)

下面考虑多段图的最短路径问题的填表形式。
用一个数组cost[n]作为存储子问题解的表格,cost[i]表示从顶点i到终点n-1的最短路径,数组path[n]存储状态,path[i]表示从顶点i到终点n-1的路径上顶点i的下一个顶点。则:
cost[i]=min{cij+cost[j]} (i≤j≤n且顶点j是顶点i的邻接点)
(式1)
path[i]=使cij+cost[j]最小的j (式2)

在这里插入图片描述

数字三角形

在这里插入图片描述
上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,和最大的路径称为最佳路径。你的任务就是求出最佳路径上的数字之和。
注意:路径上的每一步只能从一个数走到下一层上和它最近的左边的数或者右边的数。
在这里插入图片描述

程序实现:

#include <stdio.h>
#include <memory.h>
#define MAX_NUM 100
int D[MAX_NUM + 10][MAX_NUM + 10];
int N;
int aMaxSum[MAX_NUM + 10][MAX_NUM + 10];
int MaxSum( int r, int j)
{
    if( r == N )    return D[r][j];
    if( aMaxSum[r+1][j] == -1 ) //如果MaxSum(r+1, j)没有计算过
        aMaxSum[r+1][j] = MaxSum(r+1, j);
    if( aMaxSum[r+1][j+1] == -1)
        aMaxSum[r+1][j+1] = MaxSum(r+1, j+1);
    if( aMaxSum[r+1][j] > aMaxSum[r+1][j+1] )
        return aMaxSum[r+1][j] +D[r][j];
    return aMaxSum[r+1][j+1] + D[r][j];
}

int main(void)
{
    int m;
    scanf("%d", & N);
    //将 aMaxSum 全部置成-1, 开始时所有的 MaxSum(r, j)都没有算过
    memset(aMaxSum, -1, sizeof(aMaxSum));
    for( int i = 1; i <= N; i ++ )
        for( int j = 1; j <= i; j ++ )
            scanf("%d", & D[i][j]);
    printf("%d", MaxSum(1, 1));
    return 0;
}

程序分析:
这种将一个问题分解为子问题递归求解,并且将中间结果保存以避免重复计算的办法,就叫做“动态规划”。动态规划通常用来求最优解,能用动态规划解决的求最优解问题,必须满足,最优解的每个局部解也都是最优的。以上题为例,最佳路径上面的每个数字到底部的那一段路径,都是从该数字出发到达到底部的最佳路径。
实际上,递归的思想在编程时未必要实现为递归函数。在上面的例子里,有递推公式:
在这里插入图片描述
因此,不需要写递归函数,从aMaxSum**[N-1]这一行元素开始向上逐行递推,就能求得aMaxSum[1][1]的值了。**

投资分配问题

现有数量为a(万元)的资金,计划分配给n 个工厂,用于扩大再生产。
假设:xi 为分配给第i 个工厂的资金数量(万元) ;gi(xi)为第i 个工厂得到资金后提供的利润值(万元)。
问题是如何确定各工厂的资金数,使得总的利润为最大。

在这里插入图片描述
令:fk(x) = 以数量为x 的资金分配给前k 个工厂,所得到的最大利润值。
用动态规划求解,就是求 fn(a) 的问题。
当 k=1 时, f1(x) = g1(x) (因为只给一个工厂)
当1<k≤n 时,其递推关系如下:
设:y 为分给第k 个工厂的资金(其中 0≤y ≤ x ),此时还剩 x - y(万元)的资金需要分配给前 k-1 个工厂,如果采取最优策略,则得到的最大利润为fk-1(x-y) ,因此总的利润为:
gk(y) + fk-1(x-y)
在这里插入图片描述
在这里插入图片描述

0-1背包问题

关键问题:找出动态规划函数
0/1背包问题可以看作是决策一个序列(x1, x2, …, xn),对任一变量xi的决策是决定xi=1还是xi=0。在对xi-1决策后,已确定了(x1, …, xi-1),在决策xi时,问题处于下列两种状态之一:
(1)背包容量不足以装入物品i,则xi=0,背包不增加价值;
(2)背包容量可以装入物品i。
在(2)的状态下,物品i有两种情况,装入(则xi=1)或不装入(则xi=0)。在这两种情况下背包价值的最大者应该是对xi决策后的背包价值。

令V(i, j)表示在前i(1≤i≤n)个物品中能够装入容量为j(1≤j≤C)的背包中的物品的最大值,则可以得到如下动态规划函数:

在这里插入图片描述
式1表明:把前面i个物品装入容量为0的背包和把0个物品装入容量为j的背包,得到的价值均为0。
式2的第一个式子表明:如果第i个物品的重量大于背包的容量,则物品i不能装入背包,则装入前i个物品得到的最大价值和装入前i-1个物品得到的最大价值是相同的。

第二个式子表明:如果第i个物品的重量小于背包的容量,则会有以下两种情况:
1.如果第i个物品没有装入背包,则背包中物品的价值就等于把前i-1个物品装入容量为j的背包中所取得的价值。
2.如果把第i个物品装入背包,则背包中物品的价值等于把前i-1个物品装入容量为j-wi的背包中的价值加上第i个物品的价值vi;
显然,取二者中价值较大者作为把前i个物品装入容量为j的背包中的最优解。

在这里插入图片描述

算法实现:

设n个物品的重量存储在数组w[n]中,价值存储在数组v[n]中,背包容量为C,数组V[n+1][C+1]存放迭代结果,
其中V[i][j]表示前i个物品装入容量为j的背包中获得的最大价值,数组x[n]存储装入背包的物品,
动态规划法求解0/1背包问题的算法如下:

int KnapSack(int n,int w[],int v[])
{
   for(i=0;i<=n;i++) //初始化第0行
       V[i][0]=0;
   for(j=0;j<=C;j++) //初始化第0行
       V[0][j]=0;
   for(i=1;i<=n;i++)
       for(j=1;j<=C;j++)
          if(j<w[i]) V[i][j]=V[i-1][j];
          else  V[i][j]=max(V[i-1][j],V[i-1][j-w[i]+V[i]);
   j=C;  //求装入背包的物品
   for(i=n;i>0;i--)
   {
      if(V[i][j]>V[i-1][j])
      {
          x[i]=1;
          j=j-w[i];
      }
      else x[i]=0;
   }
   return V[n][C]  //返回背包取得的最大值   
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值