算法学习笔记

算法学习笔记

一、递归与分治策略

分治法思想:将一个大的问题,分解成小的相同问题,分而治之。递归:利用子问题,得出原问题解。

1、全排列问题

img

假设要求1,2,3,4的全排列:

  1. 第一个位置有四种选择,1,2,3,4。假设选择4作为第一位,接下来就变成求{1,2,3}的全排列问题。
  2. {1,2,3}全排列问题:假设选择3,那么就变成{1,2}的全排列问题。
  3. {1,2}全排列问题:假设选择1,那么只剩下2,也就是当排列元素个数为1的时候,说明当前的一种排列解法求完了。
template<class Type>
void Perm(Type list[], int start, int end){
    if(start == end){	//说明当前需要排列的元素个数为1,可以输出该排列解法
        for(int i=0; i<=end; i++)
            cout<<list[i];
        cout<<endl;
    }
    else{	//需要选择一个元素,然后求Perm(start+1,end)问题
        for(int i=start; i<=end; i++){
            Swap(list[start], list[i]);	//为当前位置选择一个元素
            Perm(list, start+1, end);	//求后续元素的全排列
            Swap(list[start], list[i]);	//还原,保证前面全排列求解,不影响原数组。
        }
    }
}
template<class Type>
void Swap(Type &a, Type &b){
    ElementType temp = a;
    a = b;
    b = temp;
}

2、二分搜索技术(二分查找)

对排好序的数组进行二分查找,最坏时间复杂度O(logN)。

//参数:数组,查找的数,数组元素个数
template<class Type>
int BinarySearch(Type a[], const Type &X, int n){
    int left = 0; int right = n-1;
    while(left <= right){
        int middle = (left + right)/2;
        if(x == a[middle])
            return middle;
        if(x > a[middle])
            left = middle+1;
        else
            right = middle-1;
    }
    return -1;	//未找到x
}

3、归并排序

时间复杂度:O(NlogN)。最大的缺点是需要额外的空间,而且执行过程中需要复制过来复制过去。实际应用中一般不用于内排序,适合外排序。

3.1 第一步:实现有序子列的归并

有序子列归并

// L=左边起始位置,R=右边起始位置,RightEnd=右边终点位置
void Merge(Type A[], Type TmpA[], int L, int R, int RightEnd){
    int LeftEnd = R - 1;	//左边终点位置。假设左右两列挨着
    int Tmp = L; 			//临时数组的当前插入位置
    int NumElements = RightEnd - L + 1;	//元素总量
    
    while(L <= LeftEnd && R <= RightEnd){
        if(A[L] <= A[R]) TmpA[Tmp++] = A[L++];
        else			 TmpA[Tmp++] = A[R++];
    }
    while(L <= LeftEnd)	//复制剩下的
        TmpA[Tmp++] = A[L++];
    while(R <= RightEnd)
        TmpA[Tmp++] = A[R++];
    
    //复制回原数组,因为此时L变化,可用RightEnd倒着复制
    for(i=0; i<NumElements; i++, RightEnd--)
        A[RightEnd] = TmpA[RightEnd];
}

3.2 归并排序的递归算法

递归左右

//分而治之
void MSort(Type A[], Type TmpA[], int L, int RightEnd){
    int Center;
    if(L < RightEnd){
        Center = (L + RightEnd)/2;
        MSort(A, TmpA, L, Center);
        MSort(A, TmpA, Center+1, RightEnd);
        Merge(A, TmpA, L, Center+1, RightEnd);
    }
}

//统一函数接口
void Merge_sort(Type A[], int N){
    Type *TmpA;
    TmpA = malloc(N * sizeof(ElementType));
    if(TmpA != NULL){
        MSort(A, TmpA, 0, N-1);
        free(TmpA);
    }
    else Error("空间不足");
}

不在Merge里创建TmpA的原因:会不断的申请释放空间。

3.3 归并排序的非递归算法

非递归归并

//一趟归并的函数
//length是当前有序子列的长度,初始为1
void Merge_pass(Type A[], Type TmpA[], int N,int length){	
    //每次要跳过两段,去找下一段
    //i <= N-2*length是为了剩个尾巴,因为有可能不是2个子列,要特别处理
    for(i=0; i <= N-2*length; i += 2*length)
        //归并两个序列,[i,i+length-1]和[i+length, i+2*length-1]
        //Merge1是把A中元素归并到TmpA
        Merge1(A, TmpA, i, i+length, i+2*length-1);
    
    if(i+length < N)	//说明还有2个子列
        Merge1(A, TmpA, i, i+length, N-1);
    else	//最后只剩1个子列
        for(j=i; j<N; j++)	TmpA[j] = A[j];
}

//统一的接口
void Merge_sort(Type A[], int N){
    int length = 1;	//初始化子序列长度
    Type *TmpA;
    TmpA = malloc(N * sizeof(ElementType));
    if(TmpA != NULL){
        while(length < N){
            Merge_pass(A, TmpA, N, length);	//第一次有序数列存在TmpA里
            length *= 2;
            Merge_pass(TmpA, A, N, length);	//第二次存回A里
            length *= 2;
        }
        free(TmpA);
    }
    else Error("空间不足");
}

4、快速排序

//分而治之
template<class Type>
void QuickSort(Type A[], int N){
    if(N < 2) return;	//递归结束条件
    //选取主元
    pivot = 从A[]中选一个主元;
    //将A[]分成两个子集
    A1 = {a ≤ pivot};
    A2 = {a ≥ pivot};
    //递归调用QuickSort
    QuickSort(A1,N1);
    QuickSort(A2,N2);
}

决定快速排序的关键是:1、怎么选取主元;2、怎么划分成两个子集

4.1 选主元

最佳情况:该主元能刚好平分数组,这种时间复杂度就能是O(NlogN)。

最糟糕情况:数组元素全划分在主元的某一侧,将会导致O(N2)。

选主元的方式:

  1. 直接选第一个当主元。最糟糕的情况是O(N2)。等于白搭。
  2. 随机取pivot。但是rand()函数不便宜
  3. 取头、中、尾三个数中的中位数
template<class Type>
Type Median3(Type A[], int Left, int Right){
    int Center = (Left + Right)/2;
    //实现 A[Left] <= A[Center] <= A[Right]
    if(A[Left] > A[Center])
        Swap(&A[Left], &A[Center]);
    if(A[Left] > A[Right])
        Swap(&A[Left], &A[Right]);
    if(A[Center] > A[Right])
        Swap(&A[Center], &A[Right]);
    Swap(&A[Center], &A[Right-1]);	//将pivot藏到右边
    //只需要考虑 A[Left+1]...A[Right-2]
    //因为这种选主元的过程实际上已经考虑了三个数的位置了
    return A[Right-1];	//返回pivot
}

4.2 子集划分

子集划分1

假设是用中位数的方法取主元。如上图,6是主元放在最右边。定义两侧的指针–i和j。移动比较。i 遇到比6大的就会停下,j 遇到比6小的就会停下。然后Swap。

子集划分

终止情况:i , j移动到已经划分完的区域停下,此时下标 j < i。然后主元和 i 所在的位置交换。

如果有元素正好等于pivot怎么办?

  • 停下来交换:好处是让主元靠近中间位置,尽量平分数组,实现O(NlogN);坏处是多了很多交换。
  • 不理它,继续移动指针:好处是不用交换;坏处是最糟糕的时候可能导致O(N2)的情况。

为了效率,选择停下来交换这种。

4.3 小规模数据的处理

快速排序的问题:

  • 是递归。。(利用的栈)
  • 对于小规模的数据(例如N不到100)可能还不如插入排序快。

解决方案:

当递归的数据规模充分小,则停止递归,直接在这一层调用简单排序。

在程序中定义一个Cutoff的阈值。

4.4 算法实现

template<class Type>
void QuickSort(Type A[], int Left, int Right){
    if(Cutoff <= Right-Left){	//阈值控制处理方式
        //1.选择主元
        Pivot = Median3(A, Left, Right);

        //2.子集划分
        i = Left; j = Right - 1;
        //实际是要对[Left+1, Right-2]进行划分,Pivot藏在A[Right-1]
        for( ; ; ){
            while(A[++i] < Pivot){}
            while(A[--j] < Pivot){}
            if(i < j)
                Swap(&A[i], &A[j]);
            else break;
        }

        //3.插入主元
        Swap(&A[i], &A[Right-1]);

        //4.递归
        QuickSort(A, Left, i-1);
        QuickSort(A, i+1, Right);
    } else	//规模小,用插入排序解决
        Insertion_Sort(A+Left, Right-Left+1);
}

//统一接口
template<class Type>
void Quick_Sort(Type A[], int N){
    QuickSort(A, 0, N-1);
}

4.5 直接调用库函数

/* 快速排序 - 直接调用库函数 */

#include <stdlib.h>

/*---------------简单整数排序--------------------*/
int compare(const void *a, const void *b){ 
    /* 比较两整数。非降序排列 */
    return (*(int*)a - *(int*)b);
}
/* 调用接口 */ 
qsort(A, N, sizeof(int), compare);
/*---------------简单整数排序--------------------*/


/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/
struct Node {
    int key1, key2;
} A[MAXN];
 
int compare2keys(const void *a, const void *b){ 
    /* 比较两种键值:按key1非升序排列;如果key1相等,则按key2非降序排列 */
    int k;
    if ( ((const struct Node*)a)->key1 < ((const struct Node*)b)->key1 )
        k = 1;
    else if ( ((const struct Node*)a)->key1 > ((const struct Node*)b)->key1 )
        k = -1;
    else { /* 如果key1相等 */
        if ( ((const struct Node*)a)->key2 < ((const struct Node*)b)->key2 )
            k = -1;
        else
            k = 1;
    }
    return k;
}
/* 调用接口 */ 
qsort(A, N, sizeof(struct Node), compare2keys);
/*--------------- 一般情况下,对结构体Node中的某键值key排序 ---------------*/

5、线性时间选择

找出数组a[0, n-1]中第 k 小元素。

//随机挑选划分基准
template<class Type>
int RandomizedPartition(Type a[], int p, int r){//p是起始位置
    int i = Random(p, r);
    Swap(a[i], a[p]);	//起始位置放置挑选出的基准数
    return Partition(a, p, r);
}
//一次划分
template<class Type>
int Partition(Type a[], int p, int r){
    int i=p, j=r+1;
    Type x = a[p];	//x取起始元素,当作基准
    //划分,左小右大
    while(true){
        while(a[++i]<x && i<r);	//找大,因为p是基准,从p+1开始找
        while(a[--j] > x);	//找小,从r开始
        if(i >= j)
            break;
        Swap(a[i], a[j]);	//交换
    }
    a[p] = a[j];	//放入基准数
    a[j] = x;
    return j;
}

//线性时间选择
template<class Type>
Type RandomizedSelect(Type a[], int p, int r, int k){
    if(p == r)
        return a[p];
    int i = RandomizedPartition(a, p, r);	//此时数组划分成三部分a[p:i-1],a[i],a[i+1:r]
    int j = i - p + 1;	//a[p:i] 左部分加基准数的数组元素总个数
    if(k <= j)	//因为找的是第k小,所以用左部分当作判断依据。(划分完后左部分都是小的)
        return RandomizedSelect(a, p, i, k);//再小点
    else
        return RandomizedSelect(a, i+1, r, k-j);//在a[i+1:r]中找第(k-j)小的元素
}

6、最接近点对问题

将所有点划分成两块区域,那么最接近点对有三种可能:1、两点都在左侧区域内;2、两点都在右侧区域内;3、左边一个和右边一个,组成最接近点对。

//分别是按照x坐标排序和按照y坐标排序
class PointX{
public:
    int operator<=(PointX a) const {
        return (x <= a.x);
    }
private:
    int ID;		//点的编号
    float x, y;	//点的坐标
};
class PointY{
public:
    int operator<=(PointY a) const {
        return (y <= a.y);
    }
private:
    int p;		//点在数组X中的坐标
    float x, y;	//点的坐标
};
//-----------------------------------------

//任意两点的距离计算函数
template<class Type>
inline float distance(const Type& u, const Type& v){
    float dx = u.x - v.x;
    float dy = u.y - v.y;
    return sqrt(dx*dx + dy*dy);
}

//统一函数接口
//X[]点集、n元素个数、a和b最接近点对的坐标、d最小距离
bool Cpair2(PointX X[], int n, PointX& a, PointY& b, float& d){
    if(n < 2)
        return false;
    //------------分别按照x坐标和y坐标排序得到的两个数组X[],Y[]---------------
    MergeSort(X, n);	//按照x坐标,从小到大排序
    PointY *Y = new PointY[n];
    for(int i=0; i<n; i++){	//将X[]复制到Y[]
        Y[i].p = i;
        Y[i].x = X[i].x;
        Y[i].y = X[i].y;
    }
    MergeSort(Y, n);	//按照y坐标排序
    //================================================================
    PointY *Z = new PointY[n];
    closest(X, Y, Z, 0, n-1, a, b, d);	//实际处理最接近点对的函数
    delete[] Y;
    delete[] Z;
    return true;
}

//求最接近点对
void closest(PointX X[], PointY Y[], int l, int r, PointX& a, PointY& b, float& d){
    if(r-l == 1){	//只有两个点
        a = X[l];
        b = X[r];
        d = distance(X[l], X[r]);
        return;
    }
    if(r-1 == 2){	//三个点情况
        float d1 = distance(X[l], X[l+1]);
        float d2 = distance(X[l+1], X[r]);
        float d3 = distance(X[l], X[r]);
        if(d1<=d2 && d1<=d3){	//X[l], X[l+1]
            a = X[l];
            b = X[l+1];
            d = d1;
            return;
        }
        if(d2 <= d3){			//X[l+1], X[r]
            a = X[l+1];
            b = X[r];
            d = d2;
            return;
        }
        else{					//X[l], X[r]
            a = X[l];
            b = X[r];
            d = d3;
            return;
        }
    }
    //-------------关键:多于三个点的求解-------------------------
    //-------------1、求左区域、右区域最接近点对-------------------
    int m = (l+r)/2;		//对半划分
    int f = l, g = m+1;		//f左区域起始位置,g右区域起始位置
    //根据x坐标,左右划分
    for(int i=1; i<=r; i++){
        if(Y[i].p > m)		//因为排好序后,p相当于x坐标大小
            Z[g++] = Y[i];
        else
            Z[f++] = Y[i];
    }
    closest(X, Z, Y, 1, m, a, b, d);	//求出左侧最近点对
    float dr;
    PointX ar, br;
    closest(X, Z, Y, m+1, r, ar, br, dr);	//求出右侧最近点对
    if(dr < d){
        a = ar;
        b = br;
        d = dr;
    }
    Merge(Z, Y, l, m, r);	//将Z中的两段[l..m][m+1..r]归并到数组Y中
    //以上的操作相当于利用Z[]数组:
    //1)求出左侧、右侧的最接近点对
    //2)数组Y[]本身是按照y坐标排序好了,现在经过以上操作,又按照x坐标排序。 
    //==========================================================
    
    //------------------2、求一左一右的情况------------------------
    //按照d,目前最小距离,找出所有可能更小的点,放入Z[]中
    int k = 1;
    for(int i=1; i<=r; i++){
        if(fabs(Y[m].x-Y[i].x) < d)
            Z[k++] = Y[i];
    }
    //遍历此时Z[]中的所有点,试图找更小距离
    for(int i=1; i<k; i++){	
        for(int j=i+1; j<k && Z[j].y-Z[i].y<d; j++){
            float dp = distance(Z[i],Z[j]);
            if(dp < d){
                d = dp;
                a = X[Z[i].p];
                b = X[Z[j].p];
            }
        }
    }
}

二、动态规划

动态规划和分治法的区别:动态规划的子问题是有联系的。所以需要保存子问题答案,即填表格式

动态规划适合解决最优化问题,通常有4个步骤:

① 找出最优解的性质,并刻画其结构特征;

② 递归地定义最优值;

③ 以自底向上的方式计算最优值;

④ 根据计算最优值时得到的信息,构造最优解。(如果要最优解的话)

1、矩阵连乘问题

矩阵A (p×r)、矩阵B (r×q)相乘:计算量 = p*q*r。

可以用数组p[]{ p0, p1, … , pn }来表示所有矩阵维度。

1.1 最优子结构

假设矩阵连乘AiAi+1…Aj,简写成A[i:j]。

计算A[1:n]的最优时,最后一步矩阵连乘假设是A[1:k]和A[k+1:n],那么

总计算量最优值 = 计算A[1:k]的最优值+计算A[k+1:n]的最优值+矩阵A[1:k]与A[k+1:n]相乘的计算量。

最优解包含子问题的最优解。这种性质叫最优子结构。

1.2 建立递归关系

m[i][j]来记录计算A[i:j]的最少数乘次数。最优值即m[1][n]。

  1. 当i = j时,m[i][i] = 0;
  2. 当i < j时,m[i][j] = min{ m[i][k] + m[k+1][j] + pi-1pkpj } 分割点k记录在s[i][j]

1.3 计算最优值

//p[]矩阵维度,n个数,m[]记录最优值,s[]记录分割点k
void MatrixChain(int *p, int n, int **m, int **s){
    for(int i=1; i<=n; i++)
        m[i][i] = 0;
    for(int r=2; r<=n; r++){	//矩阵乘法序列长度
        for(int i=1; i<=n-r+1; i++){	//遍历
            int j = i+r-1;	//结尾矩阵标号
            m[i][j] = m[i+1][j] + p[i-1]*p[i]*p[j];	//随便初始化一个值
            s[i][j] = i;
            for(int k=i+1; k<j; k++){	//寻找最优值
                int t = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j];
                if(t < m[i][j]){
                    m[i][j] = t;
                    s[i][j] = k;
                }
            }
        }
    }
}

1.4 构造最优解

s[i][j]表示的是 A[i:j]的最优计算方式是 (A[i:k])(A[k+1:j])。

void Traceback(int i, int j, int **s){
    if(i == j){
        cout<<"A["<<i<<"]";
        return;
    }
    cout<<"(";
    Traceback(i, s[i][j], s);
    Traceback(s[i][j]+1, j, s);
    cout<<")";
}

2、备忘录方法

备忘录:首先记录好每一个子问题。可以有值,可以待定。

备忘录的使用:1、如果子问题没有值时,需要去求解子问题;

​ 2、如果子问题有值时,直接返回该值。

备忘录是自顶向下的过程。和动态规划的共同点是对子问题求解次数都是1次。

感觉就是递归写法(但是多了记忆功能)。

动态规划是从小到大,击破问题。备忘录(递归)是我命令你去找出答案。

//矩阵连乘的备忘录写法
int MemoizedMatrixChain(int n, int **m, int **s){
    for(int i=1; i<=n; i++){
        for(int j=i; j<=n; j++)
            m[i][j] = 0;
        return LookupChain(1, n);
    }
}
int LookupChain(int i, int j){
    if(m[i][j] > 0)
        return m[i][j];
    if(i == j)
        return 0;
    int u = LookupChain(i, i) + LookupChain(i+1, j) + p[i-1]*p[i]*p[j];
    s[i][j] = i;
    for(int k=i+1; k<j; k++){
        int t = LookupChain(i, k) + LookupChain(k+1, j) + p[i-1]*p[k]*p[j];
        if(t < u){
            u = t;
            s[i][j] = k;
        }
    }
    m[i][j] = u;
    return u;
}

一般,所有子问题都需要至少解一次时,用动态规划更好;如果存在部分子问题不需要求解时,用备忘录更好。

3、最长公共子序列

求出两个序列的最长公共子序列。

子序列的产生:一个序列删除某些元素,但是保留原先的递增顺序,得到的序列。

例如:{A,B,C, B,D,A,B}→{B,C,A}或者{B,C,B,A}或者…

3.1 最优子结构

如果 Z 是 Xm和Yn的最长公共子序列。那么 Z 中包含着X,Y的前缀的最长公共子序列。

3.2 递归关系

c[i][j]记录Xi和Yj的最长公共子序列的长度。

c[i][j] 的计算方法: ① i = 0 或 j = 0时,值为0

​ ② 如果当前Xi = Yj , 说明当前字符是属于最长公共子序列的字符,c[i][j] = c[i-1][j-1] + 1;

​ ③ Xi ≠ Yj ,说明应该保持前缀的值,即 c[i][j] = max{ c[i][j-1], c[i-1][j] }

3.3 计算最优值

//c[][]记录最优值,b[][]是个记号,用于求最优解
void LCSLength(int m, int n, char *x, char *y, int **c, int **b){
    int i, j;
    for(i=1; i<=m; i++)		//空序列的情况
        c[i][0] = 0;
    for(i=1; i<=n; i++)		//空序列的情况
        c[0][i] = 0;
    for(i=1; i<=m; i++){
        for(j=1; j<=n; j++){
            if(x[i] == y[j]){
                c[i][j] = c[i-1][j-1] + 1;
                b[i][j] = 1;
            }else if(c[i-1][j] >= c[i][j-1]){
                c[i][j] = c[i-1][j];
                b[i][j] = 2;
            }else{
                c[i][j] = c[i][j-1];
                b[i][j] = 3;
            }
        }
    }
}

3.4 构造最长公共子序列

void LCS(int i, int j, char *x, int **b){
    if(i==0 || j==0)
        return;
    if(b[i][j] == 1){	//本身就是在集合里
        LCS(i-1, j-1, x, b);
        cout<<x[i];
    }
    else if(b[i][j] == 2)
        LCS(i-1, j, x, b);
    else
        LCS(i, j-1, x, b);
}

4、最大子段和

给定n个整数(可能存在负整数,如果全为负整数,最大子段和为0),求其最大子段和。

4.1 递归分治法: O(nlogn)

显然可以平分这个数组。a[1:n/2]和a[n/2+1:n]

① 最大子段和在左半边,a[1:n/2]中;

② 最大子段和在右半边,a[n/2+1:n]中;

③ 最大子段和从中间a[n/2]和a[n/2+1]先左右延伸,左延伸的最大字段和为s1,右延伸的最大子段和为s2, 则总的最大子段和为s1+s2。

int MaxSubSum(int *a, int left, int right){
    int sum = 0;
    if(left == right)	//初值要么是正数,最差也得是0
        sum = a[left]>0 ? a[left] : 0;
    else{
        int center = (left+right)/2;
        int leftSum = MaxSubSum(a, left, center);	//左半边的最大子段和
        int rightSum = MaxSubSum(a, center+1, right);//右半边的最大子段和
        //求跨越两边的情况
        int s1 = 0;
        int lefts = 0;
        for(int i=center; i>=left; i--){
            lefts += a[i];
            if(lefts > s1)
                s1 = lefts;
        }
        int s2 = 0;
        int rights = 0;
        for(int i=center+1; i<=right; i++){
            rights += a[i];
            if(rights > s2)
                s2 = rights;
        }
        sum = s1 + s2;
        //比较三种情况,得出最大值
        if(sum < leftSum)
            sum = leftSum;
        if(sum < rightSum)
            sum = rightSum;
    }
    return sum;
}
//简化调用函数
int MaxSum(int n, int *a){
    return MaxSubSum(a, 1, n);
}

4.2 动态规划 : O(n)

累加:b[j] 是某子段[i:j]的和。当b[j-1] > 0时,b[j] = b[j-1] + a[j];否则b[j] = a[j]。

b[j] = max{b[j-1] + a[j], a[j]}

int MaxSum(int n, int *a){
    int sum = 0, b = 0;
    for(int i=1; i<=n; i++){
        if(b > 0)
            b += a[i];
        else
            b = a[i];
        if(b > sum)
            sum = b;
    }
    return sum;
}

4.3 最大子矩阵和问题

//思想:1)把多维问题向一维问题靠拢; 2)充分使用累加,减少计算
int MaxSum2(int m, int n, int **a){
    int sum = 0;
    int *b = new int[n+1];
    for(int i=1; i<=m; i++){		// 假设最大子矩阵的起始行是i
        for(int k=1; k<=n; k++)		// b[]矩阵初始化为0
            b[k] = 0;
        for(int j=i; j<=m; j++){	// 从i行开始,增加子矩阵的行数,1行,2行...
            for(int k=1; k<=n; k++)	// 每次把列的和,放于b[k]中,(利用之前的列的和)
                b[k] += a[j][k];
            int max = MaxSum(n, b);	// 此时问题变成一维最大子列和问题
            if(max > sum)
                sum = max;
        }
    }
    return sum;
}

4.4 最大m子段和问题

**问题:**求m个子段的最大子段和,这些子段要求不相交。

假设 b(i, j) 表示数组 a[] 的前 j 项中 i 个子段和的最大值。

推导式:b(i, j) = max{ b(i, j-1) + a[j], max{b(i-1, t)} + a[j] }

​ ① b(i, j-1) + a[j] 的意思是【已经有i个子段,且最后一个子段包含a[j-1]和a[j]】。

​ ② max{b(i-1, t)} + a[j] 的意思是【已有i-1个子段,然后第i个子段由 a[j] 开始】。

这种讨论结尾项的方法,很常用。

int MaxSumM(int m, int n, int *a){
    if(n<m || m<1)
        return 0;
    //动态初始化二维数组b[][]
    int **b = new int*[m+1];
    for(int i=0; i<=m; i++)
        b[i] = new int[n+1];
    //两种为0的情况,b[0][j]和b[i][0]
    for(int i=0; i<=m; i++)
        b[i][0] = 0;
    for(int j=0; j<=n; j++)
        b[0][j] = 0;
    //关键部分
    for(int i=1; i<=m; i++){	//当前子段数量
        //当前子段数量为i,所以最极限的情况是剩m-i个元素,所以j可以到达的最大值是n-(m-i)
        for(int j=i; j<=n-(m-i); j++){ 
            if(j > i){
                b[i][j] = b[i][j-1] + a[j];	//这是a[j]加入第i子段的情况
                //这是a[j]自立门户成第i子段的情况,所以需要寻找合适k下的前i-1子段的最大值
                for(int k=i-1; k<j; k++)	
                    if(b[i][j] < b[i-1][k]+a[j])
                        b[i][j] = b[i-1][k]+a[j];
            }
            else		// i==j的情况
                b[i][j] = b[i-1][j-1] + a[j];
        }
    }
    int sum = 0;
    for(int j=m; j<=n; j++)	//组数m是固定的,但可以在元素数量j上动手脚,找最大值。
        if(sum < b[m][j])
            sum = b[m][j];
    return sum;
}

上面写法的化简:因为只用到b数组的 i 行和 i-1 行,所以只需要存储当前行和上一行。

int MaxSumM(int m, int n, int *a){
    if(n<m || m<1)
        return 0;
    int *b = new int[n+1];	//当前值
    int *c = new int[n+1];	//保存上一轮的值
    b[0] = 0;
    c[1] = 0;
    //关键部分
    for(int i=1; i<=m; i++){	//当前子段数量
        b[i] = b[i-1] + a[i];	//第i个子段包含a[i]
        c[i-1] = b[i];			//存一下
        int max = b[i];
        for(int j=i+1; j<= n-m+i; i++){		//a[j]自立门户,成为第i子段
            b[j] = b[j-1]>c[j-1] ? b[j-1]+a[j] : c[j-1]+a[j];	//c[j-1]是上一轮的最大值
            c[j-1] = max;	//此时,需要随时更新c[],保证这一轮c[]能最终获得最大值
            if(max < b[j])
                max = b[j];
        }
        c[n-m+i] = max;	//当前轮结束,也要保存最大值
    }
    int sum = 0;
    for(int j=m; j<=n; j++)
        if(sum < b[j])
            sum = b[j];
    return sum;
}

5、0-1背包问题

5.1 思考

设定:n个物品,物品i的重量为wi,其价值为vi,背包的容量为c。

要求:不允许分割物品,要么完整装入,要么不装。

最优子结构

假设物品x1,在最优解里,那么前一步的最优解在(x2, x3,…, n)中,且此时的背包空间满足 ∑wi ≤ c-w1

递归关系

从后往前递归的顺序。

最优值m(i, j),表示当前背包剩余容量为j可以任意选择i, i+1, …, n塞入背包

两种情况:①假设wi ≤ j,说明可以试试把物品i塞入背包。判断不塞入物品i,和塞入之后的价值区别。

​ 即m(i,j) = Max{ m(i+1, j), m(i+1, j-wi)+vi }

​ 效果:如果能在塞入一件物品,且能产生最大的价值,就是当前最优值。

​ ②假设j ≤ wi,说明当前物品i无法塞入。即m(i, j) = m(i+1, j)

初始可求的值:m(n, j) = vn或者0,取决于物品n是否能塞入。

5.2 实现

5.2.1 普通方法

假设w[]数组全是正整数。

template<class Type>
//此时w[]数组已经从小到大排好序
void Knapsack(Type v, int w, int c, int n, Type** m){
    int jMax = min(w[n]-1, c);
    for(int j=0; j<= jMax; j++)		//想塞物品n,却塞不下
        m[n][j] = 0;
    for(int j=w[n]; j<=c; j++)		//剩余背包容量能装下物品n
        m[n][j] = v[n];
    for(int i=n-1; i>1; i--){
        jMax = min(w[i]-1, c);
        for(int j=0; j<= jMax; j++)		//想塞物品i,却塞不下
        	m[i][j] = m[i+1][j];
        for(int j=w[n]; j<=c; j++)		//剩余背包容量能装下物品i
        	m[i][j] = max(m[i+1][j], m[i+1][j-w[i]]+v[i]);
    }
    //考虑第一件物品,此时没必要用到循环。
    m[1][c] = m[2][c];
    if(c >= w[1])
        m[1][c] = max(m[1][c], m[2][c-w1]+v[1]);
}

//结果最优值求解函数,此时最优值就是m[1][c]

template<class Type>
void Traceback(Type **m, int w, int c, int n, int* x){
    for(int i=1; i<n; i++){
        if(m[i][c] == m[i+1][c])	//当前物品i不塞入
            x[i] = 0;
        else{						//装入物品i
            x[i] = 1;
            c -= w[i];
        }
    }
    x[n] = (m[n][c]) ? 1 : 0;
}

缺点:w[]数组只能是整数,且c很大的时候花费的时间很多。

5.2.2 跳跃点方法

概念

对于确定的i,m(i, j)是一个关于j的单调不减函数。可以由跳跃点唯一确定。

可以用表p[i]来存储函数m(i, j)的全部跳跃点。表p[i]中全部跳跃点(j, m(i, j))根据j的升序排列。

计算方法

p[i]可以通过p[i+1]递归计算得出,初始:p[n+1] = {(0,0)}。

计算方法:函数m(i, j)是m(i+1, j)【即p[i+1]】m(i+1, j-wi)+vi【即q[i+1]】并集

q[i+1] = p[i+1] + (wi, vi) = {(j+wi, m(i, j)+vi)}

求出p[i+1]和q[i+1],需要删掉受控跳跃点:即横坐标大,但是纵坐标小的点。【因为要满足单调不减】。

例如:p[6] = {(0,0)}, (w5, v5) = (4, 6);

​ q[6] = p[6] + (4, 6) = {(4, 6)};

​ p[5] = {(0, 0), (4, 6)};


​ q[5] = p[5] + (5, 4) = {(5, 4), (9, 10)};

​ p[5] ∪ q[5] = {(0, 0), (4, 6), (5, 4), (9, 10)};//发现受控跳跃点[5,4]

​ p[4] = {(0, 0), (4, 6), (9, 10)};


依次求,直到p[1] = {(0, 0), (2, 6), (4, 9), (6, 12), (8, 15)}

​ 此时可以得出最优值为 m(1, c) = (8, 15) //要保证横坐标不超过背包总容量c

代码实现

template<class Type>
//n:物品个数; c:背包容量; v[]:价值; w[]:重量; **p:所有轮的点集; x[]:存储最优解结果的数组
Type Knapsack(int n, Type c, Type v[], Type w[], Type **p, int x[]){
    int *head = new int[n+2];
    head[n+1] = 0;
    //初始化:p[n+1] = {(0,0)}
    p[0][0] = 0;	//p[i]的点(x,y),相当于x=p[i][0],y=p[i][1]
    p[0][1] = 0;
    //left和right表示上一轮点集在p数组中的区间。next是指向p数组下一个插入位置
    int left=0, right=0, next=1;	
    head[n] = 1;
    for(int i=n; i>=1; i--){	//抛开初始化p[n+1],需要求n轮,到p[1]
        int k = left;			//k指向p[i+1]的点
        for(int j=left; j<=right; j++){		//形成q[i+1]的过程,每求一个新的点,就处理一次
            if(p[j][0]+w[i] > c)	//新增点的容量,要保证不超过c
                break;
            Type y = p[j][0]+w[i], m = p[j][1]+v[i];	//q[i+1]的新增点(y,m)即(j,m(i,j))
            //上一轮数组p[i+1]中,比q[i+1]横坐标小的,因为不可能是受控跳跃点,肯定可以保留
            while(k<=right && p[k][0]<y){	
                p[next][0] = p[k][0];
                p[next++][1] = p[k++][1];
            }
            //处理新增点。
            //横坐标与新增点相同的点,取m价值大的,然后保存。
            if(k<=right && p[k][0]==y){
                if(m < p[k][1])
                    m = p[k][1];
                k++;
            }
            if(m > p[next-1][1]){	//为了保证单调不减
                p[next][0] = y;
                p[next++][1] = m;
            }
            
            //处理新增点的下一位,要保证单调增
            while(k<=right && p[k][1]<=p[next-1][1])//这里用的等号,价值相同优先容量小的。
                k++;	//直到遇到一个横坐标大,价值也符合单调增的点。
        }
        //剩余p[i+1]的点直接保留
        while(k <= right){
            p[next][0] = p[k][0];
            p[next++][1] = p[k++][0];
        }
        //记录这轮p[i]的起始位置和结束位置
        left = right+1;
        right = next-1;
        head[i-1] = next;		//head[]存的是【当前轮的最后一位+1】的地址,一直到head[0]
    }//求到p[1],for循环结束
    Traceback(n, w, v, p, head, x);	//打印最优解
    return p[next-1][1];			//输出最优值
}
template<class Type>
void Traceback(int n, Type w[], Type v[], Type **p, int* head, int x[]){
    Type j = p[head[0]-1][0], m = p[head[0]-1][1];	//(j,m)指向p[i]的最优解法
    for(int i=1; i<=n; i++){
        x[i] = 0;		//初始化:该物品没放入背包
        for(int k=head[i+1]; k<=head[i]-1; k++){	//k指向p[i+1]的最优解法
            if(p[k][0]+w[i]==j && p[k][1]+v[i]==m){	//p[i+1]+物品i == p[i],说明放了
                x[i] = 1;
                j = p[k][0];
                m = p[k][1];
                break;
            }
        }
    }
}

凸多边形最优三角剖分

多边形游戏

图像压缩

流水作业调度

最优二叉搜索树

三、贪心算法

贪心算法的基本元素

1、贪心选择性质

  • 整体最优解可以通过一系列局部最优的选择得到。
  • 子问题的局部最优选择,不依赖其他子问题。
  • 动态规划一般自底向上,贪心算法一般自顶向下

2、最优子结构性质

某些问题,贪心能得到最优解,更多时候是近似解。

1、最优装载

采用重量最轻者先装的贪心选择策略。

template<class Type>
void Loading(int x[], Type w[], Type c, int n){
    int* t = new int[n+1];
    Sort(w, t, n);	//t[]数组装的是排好的序号
    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]];
    }
}

2、哈夫曼编码

template<class Type>
class Huffman{
    friend BinaryTree<int> HuffmanTree(Type [], int);	//形成哈夫曼树的函数
public:
    operator Type ()const { return weight; }			//返回权重
private:
    BinaryTree<int> tree;	//二叉树
    Type weight;			//权重
};

template<class Type>
BinaryTree<int> HuffmanTree(Type f[], int n){
    Huffman<Type> *w = new Huffman<Type>[n+1];	//结点数组
    BinaryTree<int> z, zero;
    for(int i=1; i<=n; i++){	//将权重数组,转化成树结点数组赋给w[]
        z.MakeTree(i, zero, zero);	//编号,左孩子,右孩子
        w[i].weight = f[i];
        w[i].tree = z;
    }
    //建立优先队列【最小堆】
    MinHeap<Huffman<Type>>Q(1);
    Q.Initialize(w, n, n);
    //反复合并最小权重的两棵树
    Huffman<Type> x, y;
    for(int i=1; i<n; i++){
        Q.DeleteMin(x);
        Q.DeleteMin(y);
        z.MakeTree(0, x.tree, y.tree);
        x.weight += y.weight;
        x.tree = z;
        Q.Insert(x);
    }
    Q.DeleteMin(x);
    Q.Deactivate(); //删除Q
    delete []w;
    return x.tree;
}

3、单源最短路径(Dijkstra算法)

每次选择最短的路径,有新结点,就更新距离数组。

template<class Type>
//dist[]:源到顶点i的最短路径长度; **c:边的权重; prev[]:前一个点; v:源的下标
void Dijkstra(int n, int v, Type dist[], int prev[], Type **c){
    bool s[maxint];				//是否访问数组
    for(int i=1; i<=n; i++){	//初始化
        dist[i] = c[v][i];
        s[i] = false;
        if(dist[i] == maxint)
            prev[i] = 0;
        else
            prev[i] = v;
    }
    dist[v] = 0; s[v] = true;	//起点
    for(int i=1; i<n; i++){		//n-1轮,解决
        int temp = maxint;
        int u = v;
        //寻找最短,然后确认下一个开通顶点为u
        for(int j=1; j<=n; j++)
            if((!s[j]) && (dist[j]<temp)){	//未走过且能走通
                u = j;
                temp = dist[j];
            }
        s[u] = true;
        //如果经过u点,距离可以更小的话,就更新dist[]数组
        for(int j=1; j<=n; j++){
            if((!s[j]) && (c[u][j]<maxint)){
                Type newdist = dist[u] + c[u][j];
                if(newdist < dist[j]){
                    dist[j] = newdist;
                    prev[j] = u;
                }
            }
        }
    }
}

4、最小生成树

4.1 Prim算法: O(n2)

伪代码:

void Prim(int n, Type **c){
    Tree =;	//最小生成树的边集,初始化为空集
    S = {1};	//一开始选择一个顶点当树的根结点
    while(S != V){	//S集合需要所有顶点
        (i,j)是i∈S且j∈V-S的最小权边;
        Tree = Tree∪{(i,j)};
        S = S∪{j};
    }
}

具体代码:

template<class Type>
void Prim(int n, Type **c){
    Type lowcost[maxint];		//距离
    int closest[maxint];		//老规矩,记录边的前端点,用于输出解
    bool s[maxint];				//是否访问数组
    s[1] = true;				//起点
    for(int i=2; i<=n; i++){	//初始化
        lowcost[i] = c[1][i];
        closest[i] = 1;	
        s[i] = false;
    }
    for(int i=1; i<n; i++){		//n个顶点,需要n-1条边,所以最小生成树的求解要n-1轮
        Type min = inf;			//无穷
        int j = 1;
        //寻找最小权值的边,且是未访问的
        for(int k=2; k<=n; k++){
            if((lowcost[k]<min) && (!s[k])){
                min = lowcost[k];
                j = k;
            }
        }
        cout<<j<<' '<<closest[j]<<endl;
        s[j] = true;	//收录新结点
        //更新距离数组
        for(int k=2; k<=n; k++){
            if((c[j][k]<lowcost[k]) && (!s[k])){
                lowcost[k] = c[j][k];
                closest[k] = j;
            }
        }
    }
}

4.2 Kruskal算法

时间复杂度:O(eloge)。 【e是边数】

伪代码:

void Kruskal(Graph G){
    MST = {};
    while( MST 中不到 V-1 条边 && E中还有边){
        从 E 中取一条权重最小的边E<V,W>;	//最小堆,实现
        将 E<V,W> 从 E 中删除;
        if( E<V,W> 不在 MST 中构成回路)	//并查集
            将E<V,W> 加入MST;
        else
            彻底无视 E<V,W>;
    }
    if( MST 中的不到 V-1 条边)	//说明图不连通
        Error("生成树不存在");
}

具体代码实现:

template<class Type>
class EdgeNode{
    friend ostream& operator<<(ostream&, EdgeNode<Type>);
    friend bool Kruskal(int, int, EdgeNode<Type>*, EdgeNode<Type>*);
    friend void main(void);
public:
    operator Type ()const { return weight; }
private:
    Type weight;	//权重
    int u, v;		//端点
};

template<class Type>
//n:顶点数; e:边数; E[]:边数组;	t[]:最小生成树的边集
bool Kruskal(int n, int e, EdgeNode<Type> E[], EdgeNode<Type> t[]){
    MinHeap<EdgeNode<Type>> H(1);	//最小堆
    H.Initialize(E, e, e);
    UnionFind U(n);					//并查集
    int k = 0;
    while(e && k < n-1){
        EdgeNode<Type> x;
        H.DeleteMin(x);		//取出一条最小权重边
        e--;
        int a = U.Find(x.u);
        int b = U.Find(x.v);
        if(a != b){			//判断是否构成回路
        	t[k++] = x;
            U.Union(a, b);	//a,b因为边<u,v>变成连通的
        }
        H.Deactivate();		//删除最小堆
        return (k == n-1);	//需要n-1条边
    }
}

多机调度问题

四、回溯法

回溯法有“通用的解题法”之称。适合解组合数较大的问题。

用回溯法求解问题时,应明确定义问题的解空间。问题的解空间至少应包含问题的一个(最优)解。

通常将解空间组织成树或图的形式。

通常采用两种策略避免无效搜索:① 约束函数【剪去不满足约束的】;② 限界函数【剪掉得不到最优解的】

一般解题步骤:① 定义解空间;② 确定易于搜索的解空间结构;③ 以深度优先搜索解空间,并且通过剪枝函 数避免无效搜索。

基本格式:

void Backtrack(int t){	//t为递归深度
    if(t > n)
        Output(x);		//输出可行解
    else {
        for(int i=f(n, t); i<=g(n, t); i++){	//遍历当前扩展结点的子树情况
            x[t] = h(i);					//当前可选值
            if(Constraint(t) && Bound(t))	//约束函数和限界函数
                Backtrack(t+1);		//对子树进一步搜索
        }
    }
}

非递归形式:

void Backtrack(void){
    int t = 1;
    while(t > 0){
        if(f(n, t) <= g(n, t)){		//当前结点有子结点,可以继续扩展
            for(int i=f(n,t); i<=g(n,t); i++){
                x[t] = h(i);
                if(Constraint(t) && Bound(t)){
                    if(Solution(t))
                        Output(x);
                    else
                        t++;
                }
            }
        }
        else	//当前结点是叶子结点,可以开始回溯
            t--;
    }
}

子集树:【求子集】

排列树

1表示选择Xi,0表示不选择Xi

void Backtrack(int t){
    if(t > n)
        Output(x);		
    else {
        for(int i=0; i<=1; i++){		//这里特别只有0或1
            x[t] = h(i);					
            if(Constraint(t) && Bound(t))	
                Backtrack(t+1);		
        }
    }
}

排列树:【求n个元素的排列】

排列树

void Backtrack(int t){
    if(t > n)
        Output(x);
    else{
        for(int i=t; i<=n; i++){
            Swap(x[t], x[i]);			//特别:前后的交换包着
            if(Constraint(t) && Bound(t))
                Backtrack(t+1);
            Swap(x[t], x[i]);
        }
    }
}

1、装载问题

分析:

有两艘船,容量为c1和c2。同样是n个物品。

采取策略:尽量先将第一艘船尽可能装满,即全体箱子的子集的重量之和尽可能接近c1。

**约束条件:**子集当前重量不能超过c1。【左子树要考虑,右子树不用考虑(因为x = 0,不塞入)】

**上界函数【剪去不能产生最优解的子树】:**当 cw+r≤bestw 时,可以剪掉该结点的右子树。

​ 【cw是当前载重量,r是剩余箱子的重量】

template<class Type>
class Loading{	//记录子集树的结点信息,减少传给Backtrack的参数
    friend Type MaxLoading(Type[], Type, int);
private:
    void Backtrack(int i);
    int n,			//集装箱数
    	*x,			//当前解
    	*bestx;		//当前最优解
    Type *w,		//重量数组
    	 c,			//第一艘船的容量
    	 cw,		//当前载重量
    	 bestw,		//当前最优载重量
    	 r;			//剩余集装箱的重量和
};
template<class Type>
void Loading<Type>::Backtrack(int i){
    if(i > n){				//到达叶结点
        if(cw > beatw){
            for(int j=1; j<=n; j++)
                bestx[j] = x[j];
            bestw = cw;
        }  
        return;
    }
    //搜索子树
    r -= w[i];
    if(cw+w[i] <= c){		//x[i] = 1
        x[i] = 1;
        cw += w[i];
        Backtrack(i+1);
        cw -= w[i];
    }
    if(cw+r > bestw){		//x[i] = 0
        x[i] = 0
    	Backtrack(i+1);
    }
    r += w[i];
}
template<class Type>
Type MaxLoading(Type w[], Type c, int n, int bestx[]){	//用户调用的接口函数【返回最优值】
    Loading<Type> X;
    //初始化X
    X.x = new int[n+1];
    X.w = w;
    X.c = c;
    X.n = n;
    X.bestx = bestx;
    X.bestw = 0;
    X.cw = 0;
    X.r = 0;			//初始化r
    for(int i=1; i<=n; i++)
        X.r += w[i];
    X.Backtrack(1);		//调用计算函数
    delete[] X.x;
    return X.bestw;
}

2、批处理作业调度

题目:

给定n个作业。每个作业都有两个任务分别在两台机器上完成。每个作业都是先由机器1处理,再交给机器2处理。要求制定最佳作业调度方案,使其完成时间和达到最小。

分析:

需要给n个作业排除某种序列,显然这是个排序树。

代码实现:

class Flowshop{
    friend Flow(int**, int, int[]);
private:
	void Backtrack(int i);
    int **M,		//各作业所需的处理时间
    	*x,			//当前的调度方案	
    				//x[]数组存的是作业编号,所以访问**M,要M[x[i]][1]或M[x[i]][2]
    	*bestx,		//当前最优调度方案
    	*f2,		//机器2完成处理时间
    	f1,			//机器1完成处理时间
    	f,			//完成时间和
    	bestf,		//最优完成时间和
    	n;			//作业个数
};
void Flowshop::Backtrack(int i){
    if(i > n){		//到达叶子结点
        for(int j=1; j<=n; j++)
            bestx[j] = x[j];
        bestf = f;
    }
    else{
        for(int j=i; j<=n; j++){	//当前轮需要挑选放于x[i]的作业
            f1 += M[x[j]][1];	//假设下一个作业是x[j],叠加机器1的用时
            //使用机器2需要比较,
            //作业i-1在机器2的结束时间f2[i-1] 和 当前作业i在机器1的结束时间f1
            f2[i] = ((f2[i-1]>f1) ? f2[i-1] : f1) + M[x[j]][2];
            f += f2[i];
            if(f < bestf){			//当前用时小于最优值,可以继续扩展结点
                Swap(x[i], x[j]);
                Backtrack(i+1);
                Swap(x[i], x[j]);
            }
            //回溯法特点:要还原数据
            //此地不处理f2[i],因为这个数据是回回覆盖的,不会影响。
            f1 -= M[x[j]][1];
            f -= f2[i];
        }
    }
}

int Flow(int **M, int n, int bestx[]){	//用户调用函数
    int ub = INT_MAX;
    Flowshop X;
    X.x = new int[n+1];
    X.f2 = new int[n+1];
    X.M = M;
    X.n = n;
    X.bestx = bestx[];
    X.bestf = ub;
    X.f1 = 0;
    X.f = 0;
    for(int i=0; i<=n; i++){
        X.f2[i] = 0;
        X.x[i] = i;
    }
    X.Backtrack(1);
    delete[] X.x;
    delete[] X.f2;
    return X.bestf;
}

3、n后问题

class Queen{
    friend int nQueen(int);
private:
	bool Place(int k);
    void Backtrack(int t);
    int n,		//皇后个数
    	*x;		//当前解
    long sum;	//当前已找到的可行方案数
};

bool Queen::Place(int k){
    for(int j=1; j<k; j++)	//行已经保证不会冲突
        if((abs(k-j) == abs(x[j]-x[k])) || (x[j] == x[k]))	//斜线 || 同列
            return false;
    return true;
}

void Queen::Backtrack(int t){
    if(t > n)		//求出 t==n
        sum++;
    else{
        for(int i=1; i<=n; i++){	//选列
            x[t] = i;		//第t行,第i列,下子
            if(Place(t))
                Backtrack(t+1);
        }
    }
}
int nQueen(int n){
    Queen X;
    //初始化X
    X.n = n;
    X.sum = 0;
    int *p = new int[n+1];
    for(int i=0; i<=n; i++)
        p[i] = 0;
    X.x = p;
    X.Backtrack(1);
    delete[] p;
    return X.sum;
}

4、0-1背包问题

0-1背包问题是子集选取问题。时间复杂度:O(n2n)

template<class Typew, class Typep>
class Knap{			//结点类
    friend Typep Knapsack(Typep*, Typew*, Typew, int);
private:
	Typep Bound(int i);
    void Backtrack(int i);
    Typew c;					//背包容量
    int n;						//物品数
    Typew *w;					//重量数组
    Typep *p;					//价值数组
    Typew cw;					//当前重量
    Typep cp;					//当前价值
    Typep bestp;				//当前最优价值
};

template<class Typew, class Typep>
void Knap<Typew, Typep>::Backtrack(int i){
    if(i > n){					//到达叶节点
        bestp = cp;
        return;
    }
    if(cw+w[i] <= c){			//左子树可行
        cw += w[i];
        cp += p[i];
        Backtrack(i+1);
        cw -= w[i];
        cp -= p[i];
    }
    if(Bound(i+1) > bestp)		//如果右子树可能出现最优解,进入右子树
        Backtrack(i+1);
}

template<class Typew, class Typep>
Typep Knap<Typew, Typep>::Bound(int i){		//计算上界函数
    Typew cleft = c - cw;			//背包剩余容量
    Typep b = cp;
    while(i<=n && w[i]<=cleft){		//按照【价值/重量】递减序装入背包
        cleft -= w[i];
        b += p[i];
        i++;
    }
    if(i <= n)			//充分利用背包空间
        b += p[i]*cleft/w[i];
    return b;
}

class object{		//物品类
    friend int Knapsack(int*, int*, int, int);
public:
	int operator<=(Object a) const { return (d >= a.d); }
private:
	int ID;		//物品编号
    float d;	//单位重量价值
};

template<class Typew, class Typep>
Typep Knapsack(Typep p[], Typew w[], Typew c, int n){	//用户调用函数,[初始化+调用求解函数]
    Typew W = 0;		//物品总重量
    Typep P = 0;		//物品总价值
    Object *Q = new Object[n];
    for(int i=1; i<=n; i++){
        Q[i-1].Id = i;
        Q[i-1].d = 1.0*p[i]/w[i];
        P += p[i];
        W += w[i];
    }
    if(W <= c)		//能全装入背包,完全没必要求
        return p;
    Sort(Q, n);		//物品按照[单位重量价值]递减序排序
    Knap<Typew, Typep> K;
    K.p = new Typep[n+1];
    K.w = new Typew[n+1];
    for(int i=1; i<=n; i++){
        K.p[i] = p[Q[i-1].ID];
        K.w[i] = w[Q[i-1].ID];
    }
    K.cp = 0;
    K.cw = 0;
    K.c = c;
    K.n = n;
    K.bestp = 0;
    K.Backtrack(1);
    delete[] Q;
    delete[] K.w;
    delete[] K.p;
    return K.bestp;
}

5、最大团问题

团:即完全子图,就是指任意两个顶点之间只有一条路径。

最大团:顶点数最多的团。

时间复杂度:O(n2n)

class Clique{
    friend MaxClique(int**, int[], int);
private:
	void Backtrack(int i);			
    int **a,				//图的邻接矩阵
    	n,					//图的顶点数
    	*x,					//当前解
    	*bestx,				//当前最优解
    	cn,					//当前顶点数
    	bestn;				//当前最大顶点数
};
void Clique::Backtrack(int i){
    if(i > n){
        for(int j=1; j<=n; j++)
            bestx[j] = x[j];
        bestn = cn;
        return;
    }
    //检查顶点i和当前团的连接
    int OK = 1;
    for(int j=1; j<i; j++){
        if(x[j] && a[i][j] == 0){	//顶点i和团的所有顶点不相连
            OK = 0;
            break;
        }
        if(Ok){		//当前顶点i可以加入团,进入左子树
            x[i] = 1;
            cn++;
            Backtrack(i+1);
            x[i] = 0;
            cn--;
        }
        if(cn+n-i > bestn){	//考虑是否进入右子树
            x[i] = 0;
            Backtrack(i+1)
        }
    }
}
int MaxClique(int **a, int v[], int n){
    Clique Y;
    //初始化
    Y.x = new int[n+1];
    Y.a = a;
    Y.n = n;
    Y.cn = 0;
    Y.bestn = 0;
    Y.bestx = v;
    Y.Backtrack(1);
    delete[] Y.x;
    return Y.bestn;
}

6、图的m着色问题

判断一张图是否能用m种颜色涂色,相接的两个顶点的颜色要不同。如果能,给出所有不同着色法;如果不能,给出否定回答。

分析:

这是一棵高度为n+1的完全m叉树,且i层的每个结点都有m个儿子。

代码实现:

class Color{
    friend int mColoring(int, int, int**);
private:
	bool Ok(int k);
    void Backtrack(int i);
    int n,					//图的顶点个数
    	m,					//可用颜色数
    	**a,				//图的邻接矩阵
    	*x;					//当前解
    long sum;				//当前已经找到的涂色方案个数
};
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;
}
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++){	//拿m个颜色涂色
            x[t] = i;
            if(Ok(t))
                Backtrack(t+1);
            x[t] = 0;
        }
    }
}

int mColoring(int n, int m, int **a){
    Color X;
    //初始化
    X.n = n;
    X.m = m;
    X.a = a;
    X.sum = 0;
    int *p = new int[n+1];
    for(int i=0; i<=n; i++)
        p[i] = 0;
    X.x = p;
    X.Backtrack(1);
    delete[] p;
    return X.sum;
}

时间复杂度:O(nmn)

7、旅行售货员问题

旅行售货员问题是个排列树。目标是找到一条回路,费用最小。

template<class Type>
class Traveling{
    friend Type TSP(int**, int[], int, Type);
private:
	void Backtrack(int i);
    int n,					//图的顶点数
    	*x,					//当前解
    	*bestx;				//当前最优解
    Type **a,				//图的邻接矩阵
    	 cc,				//当前费用
    	 bestc,				//当前最优费用
    	 NoEdge;			//无边标记
};
template<class Type>
void Traveling<Type>::Backtrack(int i){
    if(i == n){	//第n步	
        //因为要构成回路,所以第n步,要保证x[n-1]→x[n]和x[n]→x[1]要走得通
        //再考虑是否修改最优值
        //bestc == NoEdge 第一次的方案直接当最优值,不用比较
        if(a[x[n-1]][x[n]] != NoEdge && a[x[n]][1] != NoEdge &&
          			(cc+a[x[n-1]][n]+a[x[n]][1] < bestc || bestc == NoEdge)){
            for(int j=1; j<=n; j++)
                bestx[j] = x[j];
            bestc = cc + a[x[n-1]][n] + a[x[n]][1];
        }
    }
    else{
        for(int j=i; j<=n; j++){
            //是否进入x[j]子树
            if(a[x[i-1][x[j]]] != NoEdge && //能走通
               (cc+a[x[i-1]][x[j]] < bestc || bestc == NoEdge)){//剪枝 || 第一次
                Swap(x[i], x[j]);
                cc += a[x[i-1]][x[i]];
                Backtrack(i+1);
                cc -= a[x[i-1]][x[i]];
                Swap(x[i], x[j]);
            }
        }
    }
}

template<class Type>
Type TSP(Type **a, int v[], int n, Type NoEdge){
    Traveling<Type> Y;
    Y.x = new int[n+1];
    for(int i=1; i<=n; i++)
        Y.x[i] = i;
    Y.a = a;
    Y.n = n;
    Y.bestc = 0;
    Y.bestx = v;
    Y.cc = 0;
    Y.NoEdge = NoEdge;
    Y.Backtrack(2);			//搜索x[2:n]的全排列。因为起点x[1]是固定的
    delete[] Y.x;
    return Y.bestc;
}

时间复杂度:O(n!)

符号三角形问题

圆排列问题

电路板排列问题

连续邮资问题

五、分支限界法

分支限界法与回溯法的区别:

求解目标:

  • 回溯法是为了找出满足约束条件的所有解

  • 分支限界法的目标是找出满足约束条件的一个解,或者是在满足约束条件的解中找出某一目标函数值达到极大或极小的解,即某种意义下的最优解

解法:

  • 回溯法是深度优先
  • 分支界限法是广度优先或者以最小耗费优先

分支限界法的两种形式: 1)队列;2)优先队列。

分支限界法的基本思想:
1)每个活结点就一次机会成为扩展结点;

2)活结点成为扩展结点,会一次性产生所有儿子结点。

1、单源最短路径问题

每次选择当前最短路径的结点进行展开,直到优先队列为空。

template<class Type>
class Graph{
    friend void main(void);
public:
	void ShortestPaths(int);    
private:
	int n,				//图的顶点数
    	*prev;			//前驱顶点数组
    Type **c,			//图的邻接矩阵
    	 *dist;			//最短距离数组
};
template<class Type>
class MinHeapNode{			//最小堆的结点类
    friend Graph<Type>;
public:
	operator int ()const{ return length; }
private:
	int i;			//顶点编号
    Type length;	//当前路径长度
};

template<class Type>
void Graph<Type>::ShortestPaths(int v){		//求解单源最短路径的函数
    MinHeap<MinHeapNode<Type>> H(1000);		//定义最小堆的容量为1000
    MinHeapNode<Type> E;					//定义起始结点
    E.i = v;
    E.length = 0;
    dist[v] = 0;
    while(true){			//搜索问题的解空间
        for(int j=1; j<=n; j++){
            //当前顶点E.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<Type> N;	//创建新的结点塞入优先队列
                N.i = j;
                N.length = dist[j];
                H.Insert(N);
            }
            try{
                H.DeleteMin(E);			//从优先队列取出下一个扩展结点
            } catch (OutOfBounds){		//优先队列为空,跳出循环
                break;
            }
        }
    }
}

2、装载问题

2.1 队列式分支限界法写法

template<class Type>
class QNode{
    friend void EnQueue(Queue<QNode<Type> *> &, Type,
                       int, int, Type, QNode<Type>*, QNode<Type>*&, int*, bool);
    friend Type MaxLoading(Type*, Type, int, int*);
private:
    QNode* parent;			//指向父结点的指针
    bool LChild;			//左儿子标志
    Type weight;			//当前结点的载重量
};

template<class Type>
//入队列操作
/*
	Queue<QNode<Type> *> &Q:队列
 	Type wt:当前载重量
 	int i:当前层数
 	int n:结点总个数
 	Type bestw:当前最优载重量
 	QNode<Type>* E:当前结点
 	QNode<Type>* &bestE:当前最优结点
 	int bestx[]:当前最优解
 	bool ch:是否为左孩子标志
*/
void EnQueue(Queue<QNode<Type> *> &Q, Type wt, int i, int n, Type bestw,
            QNode<Type>* E, QNode<Type>* &bestE, int bestx[], bool ch){
    if(i == n){				//叶子结点
        if(wt == bestw){	//因为外部已经修改bestw,这里才使用的等号
            bestE = E;
            bestx[n] = ch;
        }
        return;
    }
    //非叶子结点
    QNode<Type> *b;
    b = new QNode<Type>;
    b->weight = wt;
    b->parent = E;
    b->LChild = ch;
    Q.Add(b);
}
    
template<class Type>
Type MaxLoading(Type w[], Type c, int n, int bestx[]){	//队列式分支界限法,返回最优装载量
    //初始化
    Queue<QNode<Type>*> Q;
    Q.Add(0);			//同层结点结束标志
    int	i = 1;			//当前扩展结点的层数
    Type Ew = 0,		//当前扩展结点的载重量
    	 bestw = 0,		//当前最优载重量
    	 r = 0;			//剩余集装箱量,用于剪枝 Ew + r <= bestw 可以直接剪掉
    for(int j=2; j<=n; j++)
        r +=w[j];
    QNode<Type> *E = 0,			//当前扩展结点
    			*bestE;			//当前最优扩展结点
    while(true){
        Type wt = Ew + w[i];	//左孩子的载重量
        if(wt <= c){			//左孩子可行
            if(wt > bestw)		//为了实现Ew + r <= bestw剪枝, 需要随时更新最优值
                bestw = wt;
            EnQueue(Q, wt, i, n, bestw, E, bestE, bestx, true);
        }
        if(Ew+r > bestw)	//有剪枝函数,检查右孩子的可行性
            EnQueue(Q, Ew, i, n, bestw, E, bestE, bestx, false);
        Q.Delete(Ew);		//取下一个扩展结点
        if(!E){				//到达同层结点的尾部
            if(Q.IsEmpty())	
                break;
            //如果还有一层结点,插入同层尾部标志,并取下一个扩展结点
            Q.Add(0);
            Q.Delete(E);
            i++;			//进入下一层
            r -= w[i];		//更新剩余集装箱量
        }
        Ew = E->weight;		//新扩展结点的载重量
    }
    //构造当前最优解
    for(int j=n-1; j>0; j--){
        bestx[j] = bestE->LChild;
        bestE = bestE->parent;
    }
    return bestw;
}

2.2 优先队列式分支限界法

优先级定义:当前载重量和剩余集装箱重量之和。

template<class Type>
class bbnode{			//子集树的结点类
    friend void AddLiveNode(MaxHeap<HeapNode<int>> &, bbnode*, Type, bool, int);
    friend int MaxLoading(int*, int, int, int*);
    friend class AdjacencyGraph;
private:
	bbnode* parent;				//指向父结点的指针
    bool LChild;				//左孩子标志
};
template<class Type>
class HeapNode{			//优先队列的结点类
    friend void AddLiveNode(MaxHeap<HeapNode<Type>> &, bbnode*, Type, bool, int);
    friend Type MaxLoading(Type*, Type, int, int*);
public:
	operator Type ()const { return weight; }
private:
	bbnode* ptr;			//指向活结点在子集树中相应结点的指针
    Type uweight;			//活结点优先级
    int level;				//活结点在子集树中的层序号
};

template<class Type>
//将活结点,添加进优先队列
//参数解释:最大堆,当前扩展结点,优先级【当前载重量+剩余载重量】,是否是左孩子,层数
void AddLiveNode(MaxHeap<HeapNode<int>> &H, bbnode* E, Type wt, bool ch, int lev){
    bbnode* b = new bbnode;
    b->parent = E;
    b->LChild = ch;
    HeapNode<Type> N;
    N.uweight = wt;
    N.level = lev;
    N.ptr = b;
    H.Insert(N);
}

template<class Type>
//返回最优载重量,bestx[]是最优解
Type MaxLoading(Type w[], Type c, int n, int bestx[]){
    MaxHeap<HeapNode<Type>> H(1000);
    Type* r = new Type[n+1];			//剩余重量数组
    r[n] = 0;
    for(int j=n-1; j>0; j--)
        r[j] = r[j+1] + w[j+1];
    //初始化
    int i=1;							//当前层数	从1开始,n+1层到达叶子结点
    bbnode* E = 0;						//当前扩展结点
    Type Ew = 0;						//扩展结点的载重量
    //搜索子集树
    while(i != n+1){
        //左孩子可行性分析
        if(Ew+w[i] <= c)
            AddLiveNode(H, E, Ew+w[i]+r[i], true, i+1);
        AddLiveNode(H, E, Ew+r[i], false, i+1);		//右孩子
        HeapNode<Type> N;		//取下一个扩展结点
        H.DeleteMax(N);
        i = N.level;
        E = N.ptr;
        Ew = N.uweight-r[i-1];
    }
    //构造最优解
    for(int j=n; j>0; j--){
        bestx[j] = E->LChild;
        E = E->parent;
    }
    return Ew;
}

优先队列中,n+1层第一个结点必是最优解。

3、0-1背包问题

通过Bound上界函数作为优先级。

class Object{		//物品类
    friend int Knapsack(int*, int*, int, int, int*);
public:
	int operator<=(Object a) const { return (d >= a.d)};
private:
	int ID;
    float d;		//单位重量价值
};
template<class Typew, class Typep> class Knap;
class bbnode{				//子集树的结点类型
    friend Knap<int, int>;
    friend int Knapsack(int*, int*, int, int, int*);
private:
	bbnode* parent;			//父亲结点
    bool LChild;			//左孩子标志
};
template<class Typew, class Typep>
class HeapNode{				//优先队列的结点类
    friend Knap<Typew, Typep>;
public:
	operator Typep() const { return uprofit; }
private:
	Typep uprofit,			//结点的价值上届
    	  profit;			//结点的价值
    Typew weight;			//结点的重量
    int level;				//层序号
    bbnode* ptr;			//子集树中的结点指针
};

template<class Typew, class Typep>
class Knap{					//问题类的定义
    friend Typep Knapsack(Typep*, Typew*, Typew, int, int*);	//用户调用接口
public:
	Typep MaxKnapsack();	//o-1背包问题求解函数
private:
    MaxHeap<HeapNode<Typew, Typep>> *H;	//优先队列
    Typep Bound(int i);				//求解结点的价值上界函数
    void AddLiveNode(Typep up, Typep cp, Typew cw, bool ch, int level);	//活结点入队函数
    bbnode* E;						//指向扩展结点的指针
    Typew c;						//背包容量
    int n;							//物品总数
    Typew* w;						//物品重量数组
    Typep* p;						//物品价值数组
    Typew cw;						//当前重量
    Typep cp;						//当前价值
    int* bestx;						//最优解
};

template<class Typew, class Typep>
Typep Knap<Typew, Typep>::Bound(int i){		//上界函数
    Typew cleft = c-cw;		//剩余容量
    Typep b = cp;			//价值上界
    //根据【单位重量价值】递减序塞入背包
    while(i<=n && w[i]<=cleft){
        cleft -= w[i];
        b += p[i];
        i++
    }
    if(i <= n)	//物品无法完整塞入,塞入部分
        b += p[i]/w[i]*cleft;
    return b;
}

template<class Typew, class Typep>
// 将新的活结点插入到子集树和最大堆中
void Knap<Typew, Typep>::AddLiveNode(Typep up, Typep cp, Typew cw, bool ch, int lev){
    bbnode* b = new bbnode;		//生成子集树中结点
    b->parent = E;
    b->LChild = ch;
    HeapNode<Typew, Typep> N;	//生成最大堆里的结点
    N.uprofit = up;
    N.profit = cp;
    N.weight = cw;
    N.level = lev;
    N.ptr = b;
    H->Insert(N);
}

template<class Typew, class Typep>
Typep Knap<Typew, Typep>::MaxKnapsack(){	//返回最大价值,bestx返回最优解
    H = new MaxHeap<HeapNode<Typew, Typep>>(1000);	//定义最大堆容量为1000
    bestx = new int[n+1];
    //初始化
    int i = 1;
    E = 0;
    cw = cp = 0;
    Typep bestp = 0;		//当前最优值
    Typep up = Bound(1);	//价值上界
    //搜索子集树
    while(i != n+1){		//非叶结点
        //左孩子可行性分析
        Typew wt = cw + w[i];
        if(wt <= c){
            if(cp + p[i] > bestp)
                bestp = cp + p[i];
            AddLiveNode(up, cp+p[i], cw+w[i], true, i+1);
        }
        up = Bound(i+1);
        //右孩子可行性分析
        if(up >= bestp)
            AddLiveNode(up, cp, cw, false, i+1);
        //取下一个扩展结点
        HeapNode<Typep, Typew> N;
        H->DeleteMax(N);
        E = N.ptr;
        cw = N.weight;
        cp = N.profit;
        up = N.uprofit;
        i = N.level;
    }
    //构造最优解
    for(int j=n; j>0; j--){
        bestx[j] = E->LChild;
        E = E->parent;
    }
    return cp;
}

template<class Typew, class Typep>
//用户调用的接口函数,
Typep Knap<Typew, Typep>::Knapsack(Typep p[], Typew w[], Typew c, int n, int bestx[]){
    //初始化
    Typew W = 0;		//物品总重量
    Typep P = 0;		//物品总价值
    Object* Q = new Object[n];	//物品数组
    for(int i=1; i<=n; i++){
        //计算单位重量价值数组
        Q[i-1].ID = i;
        Q[i-1].d = 1.0*p[i]/w[i];
        P += p[i];
        W += w[i];
    }
    if(W <= c)	//可以全塞,不用求解
        return P;
    Sort(Q, n);		//按照【单位重量价值】递减排序
    //创建类Knap的数据成员
    Knap<Typew, Typep> K;
    K.p = new Typep[n+1];
    K.w = new Typew[n+1];
    for(int i=1; i<=n; i++){
        K.p[i] = p[Q[i-1].ID];
        K.w[i] = w[Q[i-1].ID];
    }
    K.cp = 0;
    K.cw = 0;
    K.c = c;
    K.n = n;
    Typep bestp = K.MaxKnapsack();		//调用求解函数
    for(int j=1; j<=n; j++)
        bestx[Q[i-1].ID] = K.bestx[j];
    delete[] Q;
    delete[] K.w;
    delete[] K.p;
    delete[] K.bestx;
    return bestp;
}

4、最大团问题

最大团问题是个子集树问题。

class bbnode{					//子集树结点类型,常客了
    friend class Clique;
private:
	bbnode* parent;
    bool LChild;
};
class CliqueNode{				//优先队列结点类型
    friend class Clique;
public:
	operator int() const { return un; }
private:
	int cn,			//当前团的顶点数
    	un,			//当前团最大顶点数的上界
    	level,		//层数
    bbnode* ptr;	//子集树中的结点的指针
};

class Clique{		//问题类
    friend void main(void);
public:
	int BBMaxClique(int []);
private:
	void AddLiveNode(MaxHeap<CliqueNode>&, int, int, int, bbnode*, bool);
    int **a,	//图的邻接矩阵
    	  n;	//图的顶点数
};

//插入优先队列
void Clique::AddLiveNode(MaxHeap<CliqueNode> &H, int cn, int un, int level, 
                         bbnode E[], bool ch){
    bbnode* b = new bbnode;
    b->parent = E;
    b->LChild = ch;
    CliqueNode N;
    N.cn = cn;
    N.ptr = b;
    N.un = un;
    N.level = level;
    H.Insert(N);
}

//解决最大团问题的函数
int Clique::BBMaxClique(int bestx[]){	
    MaxHeap<CliqueNode> H(1000);		//最大堆
    //初始化
    bbnode* E = 0;
    int i = 1,
    	cn = 0,
    	bestn = 0;
    //搜索子集树
    while(i != n+1){		//非叶结点
        //检查顶点i与当前团中其他顶点是否连通
        bool OK = true;
        bbnode* B = E;
        for(int j=i-1; j>0; B=B->parent, j--){
            if(B->LChild && a[i][j]==0){	//B->LChild保证是团中顶点,a[i][j]==0不连通
                OK = false;
                break;
            }
            if(OK){
                if(cn+1 > bestn)		//左孩子可行性
                    bestn = cn + 1;
                AddLiveNode(H, cn+1, cn+n-i+1, i+1, E, true);
            }
            if(cn+n-i >= bestn)			//右孩子可行性
                AddLiveNode(H, cn, cn+n-i, i+1, E, false);
            //取下一个扩展结点
            CliqueNode N;
            H.DeleteMax(N);
            E = N.ptr;
            cn = N.cn;
            i = N.level;
        }
    }
    for(int j=n; j>0; j++){
        bestx[j] = E->LChild;
        E = E->parent;
    }
    return bestn;
}

5、旅行售货员问题

旅行售货员问题是排列树。

关于最优解的存储问题:①优先队列的结点都存储从根结点到该结点的路径;【下面用到的方法】

​ ②优先队列存储活结点,只存储部分排列树 。【最优解是通过链表回溯得到】

template<class Type>
class Traveling{				//问题类
    friend void main(void);
public:
	Type BBTSP(int v[]);
private:
	int n;				//图的顶点数
    Type **a,			//图的邻接矩阵
    	 NoEdge,		//图的无边标志
    	 cc,			//当前费用
    	 bestc;			//当前最小费用
};

template<class Type>
class MinHeapNode{				//最小堆结点类
    friend Traveling<Type>;
public:
	operator Type () const { return lcost; }
private:
	Type lcost,				//子树的费用下界
    	 cc,				//当前费用
    	 rcost;				//x[s:n-1]中顶点最小出边费用之和
    int s,					//根结点到当前结点的路径为x[0:s]
    	*x;					//需要进一步搜索的顶点是x[s+1:n-1]
};

//如果图中某个顶点没有出边,那么就不会构成回路,说明计算不出任何结果

template<class Type>
Type Traveling<Type>::BBTSP(int v[]){
    MinHeap<MinHeapNode<Type>> H(1000);
    Type* MinOut = new Type[n+1];
    //计算MinOut[i]:顶点i的最小出边费用
    Type MinSum = 0;		//最小出边费用之和
    for(int i=1; i<=n; i++){
        Type Min = NoEdge;
        for(int j=1; j<=n; j++)		//找最小出边
            if(a[i][j]!=NoEdge && (a[i][j]<Min || Min==NoEdge))
                Min=a[i][j];
        if(Min == NoEdge)			//无回路
            return NoEdge;
        MinOut[i] = Min;
        MinSum += Min;
    }
    //初始化
    MinHeapNode<Type> E;
    E.x = new int[n];			//注意这里是n,也就是最后构成回路的点是x[n-2]x[n-1]x[1]
    for(int i=0; i<n; i++)
        E.x[i] = i+1;
    E.s = 0;
    E.cc = 0;
    E.rcost = MinSum;
    Type bestc = NoEdge;
    //搜索排列树
    while(E.s < n-1){		//非叶结点
        if(E.s == n-2){		//当前扩展结点是叶结点的父结点
        	//检查是否构成回路,并与最优值作对比
            if(a[E.x[n-2]][E.x[n-1]]!=NoEdge && a[E.x[n-1]][E.x[1]]!=NoEdge &&
              (E.cc+a[E.x[n-2]][E.x[n-1]]+a[E.x[n-1]][E.x[1]] < bestc || bestc==NoEdge)){
                //费用更小的回路
                bestc = E.cc + a[E.x[n-2]][E.x[n-1]] + a[E.x[n-1]][E.x[1]];
                E.cc = bestc;
                E.lcost = bestc;
                E.s++;
                H,,Insert(E);
            }
            else
                delete[] E.x;				//舍弃扩展结点
        }
        else{								//根据当前结点s,产生[s+1,n-1]的新结点
            for(int i=E.s+1; i<n; i++){		
                if(a[E.x[E.s]][E.x[i]] != NoEdge){
                    Type cc = E.cc + a[E.x[E.s]][E.x[i]];	//新结点费用
                    Type rcost = E.rcost - MinOut[E.x[E.s]];
                    Type b = cc + rcost;	//下界
                    if(b < bestc || bestc == NoEdge){	//该结点可能算出最优解,塞入最小堆中
                        MinHeapNode<Type> N;
                        N.x = new int[n];
                        for(int j=0; j<n; j++)
                            N.x[j] = E.x[j];
                        N.x[E.s+1] = E.x[i];	//将新地点x[i]塞到x[s+1]处
                        N.x[i] = E.x[E.s+1];
                        N.cc = cc;
                        N.s = E.s+1;
                        N.lcost = b;
                        N.rcost = rcost;
                        H.Insert(N);
                    }
                }
            }
            delete[] E.x;
        }//扩展新结点结束
        try{
            H.DeleteMin(E);	//取新扩展结点
        } catch (OutOfFound){
            break;
        }
    }//while结束
    if(bestc == NoEdge)	//无回路
        return NoEdge;
    for(int i=0; i<n; i++)	//复制最优解
        v[i+1] = E.x[i];
    while(true){			//释放最小堆中的结点
        delete[] E.x;
        try{ H.DeleteMin(E); }
        catch(OutOfFound){ break; }
    }
    return bestc;
}

6、批处理作业问题

class Flowshop;
class MinHeapNode{
    friend Flowshop;
public:
	operator int () const { return bb; }
private:
	void Init(int), NewNode(MinHeapNode, int, int, int, int);
    int s,			//已安排作业数
    	f1, 		//机器1最后完成时间
    	f2,			//机器2最后完成时间
    	sf2,		//当前机器2完成时间和
    	bb,			//当前完成时间和下界
    	*x;			//当前作业调度
};
void MinHeapNode::Init(int n){	//最小堆结点初始化
    x = new int[n];
    for(int i=0; i<n; i++)
        x[i] = i;
    s = 0;
    f1 = 0;
    f2 = 0;
    sf2 = 0;
    bb = 0;
}
//生成最小堆新结点
void MinHeapNode::NewNode(MinHeapNode E, int Ef1, int Ef2, int Ebb, int n){	
    x = new int[n];
    for(int i=0; i<n; i++)
        x[i] = E.x[i]; 
    f1 = Ef1;
    f2 = Ef2;
    sf2 = E.sf2 + f2;
    bb = Ebb;
    s = E.s + 1;
}

class Flowshop{				//问题类
    friend void main(void);
public:
	int BBFlow(void);
private:
	int Bound(MinHeapNode, int&, int&, bool**);
    void Sort(void);
    int n,				//作业数
    	**M,			//各作业所需处理时间数组
    	**b,			//各作业所需处理时间数组【排序版】
    	**a,			//数组M和数组b的对应关系数组
    	*bestx,   		//最优解
    	bestc;			//最优值
    bool **y;			//工作数组
};

void Flowshop::Sort(void){		//对各作业在机器1和机器2所需时间进行排序
    int* c = new int[n];		//存作业编号
    for(int j=0; j<2; j++){		//j表示的是机器1,还是机器2
        for(int i=0; i<n; i++){	//从数组M复制到数组b
            b[i][j] = M[i][j];
            c[i] = i;
        }
        for(int i=0; i<n-1; i++){			//简易的冒泡排序,需要n-1轮
            for(int k=n-1; k>i; k--){
                if(b[k][j] < b[k-1][j]){
                    Swap(b[k][j], b[k-1][j]);
                    Swap(c[k], c[k-1]);
                }  
            }
        }
        for(int i=0; i<n; i++)		//目前这里看不懂??????
        	a[c[i]][j] = i;			
    }
    delete[] c;
}

//计算完成时间和下界
int Flowshop::Bound(MinHeapNode E, int& f1, int& f2, bool **y){	
    for(int k=0; k<n; k++)
        for(int j=0; j<2; j++)
            y[k][j] = false;
    for(int k=0; k<=E.s; k++)			//[0,s]全部完成
        for(int j=0; j<2; j++)
            y[a[E.x[k]][j]][j] = true;
    f1 = E.f1 + M[E.x[E.s]][0];
    f2 = ((f1 > E.f2) ? f1 : E.f2) + M[E.x[E.s]][1];
    int sf2 = E.sf2 + f2;
    int s1 = 0, s2 = 0, k1 = n-E.s, k2 = n-E.s, f3 = f2;
    for(int j=0; j<n; j++){				//计算s1的值
		if(!y[j][0]){			//找没完成的任务
            k1--;
            if(k1 == n-E.s-1)
                f3 = (f2 > f1+b[j][0]) ? f2 : f1+b[j][0];
            s1 += f1 + k1*b[j][0];
        }
    }
    for(int j=0; j<n; j++){				//计算s2的值
        if(!y[j][1]){
        	k2--;
            s1 += b[j][1];
            s2 += f3 + k2*b[j][1];
        }
    }
    return sf2 + ((s1 > s2) ? s1 : s2);
}

int Flowshop::BBFlow(void){
    Sort();
    MinHeap<MinHeapNode<Type>> H(1000);
    MinHeapNode E;
    E.Init(n);
    while(E.s <= n){			//搜索排列树
        if(E.s == n){			//叶结点
            if(E.sf2 < bestc){
                bestc = E.sf2;
                for(int i=0; i<n; i++)
                    bestx[i] = E.x[i];
            }
            delete[] E.x;
        }
        else{			//需要扩展新的结点
	    	for(int i=E.s; i<n; i++){
                Swap(E.x[E.s], E.x[i]);
                int f1, f2;
                int bb = Bound(E, f1, f2, y);
                if(bb < bestc){
                    MinHeapNode N;
                    N.NewNode(E, f1, f2, bb, n);
                    H.Insert(N);
                }
                Swap(E.x[E.s], E.x[i]);
            }
            delete[] E.x;
        }
        try{ H.DeleteMin(E); }
        catch(OutOfFound) { break; }
    }
    return bestc;
}

布线问题

电路板排列问题

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值