算法学习笔记——动态规划 - part3

 

本部分主要是学习动态规划的一些笔记

全文下载地址:http://download.csdn.net/detail/wearenoth/6022339

1       分治法与动态规划

         分治法的思路是将大问题分成若干子问题,依次求解,最后合并解的答案。但是分治法并没有说明应该划分成什么样的子问题,毕竟这不是意见容易的事情。

         在分治法能够分解的问题中,有一类结构很特殊,让我们可以遵循一定的套路去解决问题。这一类具有特殊结构的子问题的分解、分析方法被称作是动态规划。

         利用动态规划进行分析的问题,要求分解后的问题必须是最优子结构,也就是一个问题的最优解包含了子问题的最优解。

         对于如何寻找最优子结构,《算法导论》中给出了一些启发式的方法:

Ø  问题的一个解可以是做一个选择。

Ø  假设一个给定的问题,已知的是一个可以导致最优解的选择。

Ø  在已知这个选择后,要确定哪些子问题会随之发生,以及如何最好地描述所得到的子问题。

Ø  证明问题的一个最优解中,使用的子问题的解本身也是最优的。

可以这样理解,在分析的时候,我们假定已经对问题规模为n-1的情况下做出了最优的选择,那剩下的事情就是对最后一个问题输入进行选择。而且最后一个物品的选择后得到的解就是输入规模为n的情况下的最优解。这个过程其实和数学归纳的过程很像,不是么。

要利用这种分析方法,可以按照如下的步骤:

Ø  描述一个子问题的解,也就是常说的状态。

Ø  在得到状态描述以后,常常就可以确定初始解。

Ø  获取一个递归的解表达式,也就是经常看到的递归方程,常称作状态转移方程

Ø  申请足够的空间用于保存所有解状态。

Ø  使用递归或递推的方式求解递归方程,得到初始的代码。

Ø  利用一些优化技术优化代码,降低算法的空间复杂度和时间复杂度。

动态规划的解可以画成一棵树的形状,问题的最终解位于树的根部,树的叶子节点的解是已知的。这样一来,动态规划就可以有两种求解思路:

Ø  从树的根部向下求解,也就是自顶向下求解,这种方法的一个缺点就是很可能会出现大量的重叠子问题,解决的办法就是利用记忆搜索法来记录每个解的答案。利用这种方法写出来的代码就是一个递归函数,利用分治法的模板很容易写出来,所以这种方法的另外一个缺点就是可能会因为递归而带来的爆栈以及效率问题。

Ø  从树的叶子节点开始向上求解,也就是自底向上求解。这种方法也就是所谓的递推,这种方法有一定优势,没有了重叠子问题以及递归造成的爆栈问题,效率也比递归快。缺点就是代码不一定好写,比较考验思维。

通过上述写出来的动态规划代码不一定最优,时间复杂度和空间复杂度都可能比较高。

         动态规划的优化都需要根据具体问题进行优化,总体的原则就是减少状态数量以及提高状态转移效率。当然也有一些常见的技巧,如利用单调队列优化、凸包优化、斜率优化等。

1.1     寻找最优子结构

如果希望利用动态规划解决问题,第一步就是需要确定最优子结构到底长什么样?

1.2     定义状态

当确定了最优子结构后,状态的定义就相对简单许多,一般站在一个角度》》》》

1.3     定义状态转移方程与初始解

当定义了状态,剩下的事情就是如何表述状态转移方程》》

1.4     将方程转换成代码

将方程转换成代码并不难,如果使用递归方式求解,那就需要考虑重叠子问题,如果采用递推方式。

1.4.1  重叠子问题

采用递归求解的时候最容易出现重叠子问题,这时候就需要考虑如何减少重叠子问题的求解。

1.5     开始维护解

虽然动态规划解决问题过程固定,但是需要考虑的事情非常多,所以一开始并不一定要维护解,可以等到将整个算法的框架给编写出来以后才开始维护答案。

1.6     优化

动态规划的优化需要通过观察,也有一些经典的优化方式。

1.6.1  单调队列优化

1.6.1.1 单调队列与滑窗

假设有一个数组A[],给定一个下标i,希望可以在O(1)的时间内求出A[i-j]A[i]范围内的最大值。对于这个问题,很容易得到表达式:

f[i]=max{A[k] | j<=k<=i}

这个问题可以利用单调队列进行求解,如下图,下方的指针表示要求的区间范围[st, ed],则单调队列中含有的值则是图中标注颜色的部分。

6

要保持单调队列比较简单,单调队列其实是一个双向队列,可以参考下图。例如,如果保持一个单调递减的队列,则操作为:

Ø  如果当前元素小于(<)队尾元素,则直接入队。

Ø  如果当前元素大于等于(>=)队尾元素,则队尾元素出队。重复这个步骤一直遇到第一个大于当前元素时停止,然后再将当前元素入队。

Ø  每次移动区间都需要将队列首部中不在区间范围内的元素清除。

7

         使用单调队列判断这个数组的代码如下所示。此外,很容易就知道单调队列的最大长度不会超过区间大小。

         观察这个过程,其实很像一个滑动窗口,单调队列保证了每次都能保持这个固定窗口中间的最值。

34 单调队列遍历数组代码

classLocation{

public:

    intkey;

    intindex;

};

#defineQUEUESIZE100

Locationqueue[QUEUESIZE];

inthead=0;  inttail=0;

voidsearch(inta[],intn,intr){

    r--;

    for(inti=0;i<n;i++){

        //出队操作

        while(head<tail&&queue[tail-1].key<a[i]){

            tail--;

        }

        //入队操作

        queue[tail].key=a[i];

        queue[tail].index=i;

        tail++;

        //判断队列头部

        while(queue[head].index<=i-r){

            head++;

        }

    }

}

1.6.1.2 单调队列可以优化的动态规划问题

我们先考察下面的这个递归表达式:

dp[i]=max{dp[k-1]-sum[k]+sum[i] | i-r<=k<=i}

直接求解这个递归表达式容易得到时间复杂度为O(NK),其中Ni的取值范围,rk的取值范围。

         dp[k-1]-sum[k]看做是一个数组,只要使用一个单调队列维护这个数组在区间范围[i-r, i]内的最大值,则在求解dp[i]时候就可以在O(1)的时间复杂度内求解得到。

单调队列可以用于优化DP问题,但它只能优化一些特定的DP问题而不是所有,所有诸如上述形式的状态转移方程就可以利用单调队列进行优化。

利用单调队列优化多重背包问题的时候,需要先进行一些方程上的转换,适应上述的格式。目标是去除f[v-k*c[i]]中同时出现vk的情况。

b=v%c[i]a=v/c[i],则v可以表示为:v=a*c[i]+b。则状态转移方程变成:

f[i][a*c[i]+b]=max{f[i-1][(a-k)*c[i]+b]+k*w[i] | 0<=k<=n [i],n[i]

         k’=a-k,则可以得到新的状态转移方程:

f[i][a*c[i]+b]=max{f[i-1][k’*c[i]+b]+(a-k’)*w[i] | n[i]-a<=k’<=a,n[i]

         这时候就可以利用单调队列进行优化。可以将这个状态转移方程降纬,最后得到的状态转移方程为:

f[a*c[i]+b]=max{f[k’*c[i]+b]+(a-k’)*w[i] | n[i]-a<=k’<=a,n[i]

8

1.6.2  斜率优化

1.6.3  凸包优化

2       利用动态规划解决实际问题

2.1     背包问题

         背包问题变种非常多,看到的最好的一篇背包问题的文章是《背包九讲》,作者讲述了0-1背包问题,完全背包问题、多重背包问题、混合背包问题、分组背包问题、有依赖的背包问题、泛化物品等多种背包问题。下面的内容就是根据这篇文章的内容整理出来的。

2.1.1  0-1背包问题

2.1.1.1 问题描述

         物品数量为N,背包容量为V。第i件物品费用为c[i],价值为w[i]每件物品数量为1。问:放入哪些物品后,背包中物品价值总和达到最大。

2.1.1.2 基本思路

         使用DP进行求解。首先需要定义状态:设f[i][v]表示前i件物品放入一个容量为v的背包中获取的最大价值。

假设我们已经知道放入前i-1个物品到容量为v的背包中可以获取的最大价值。则在考虑第i个物品的时候,第i个物品的处理方法只有两种:放、不放。

Ø  不放:如果不放入第i个物品,则背包中物品的总价值为f[i-1][v]。

Ø  放:如果放入第i个物品,则背包中物品的总价值为f[i-1][v-c[i]]+w[i]。即前i-1个物品只能放在容量为v-c[i]的背包中。

这样状态转移方程为:

f[i][v]=max{f[i-1][v], f[i-1][v-c[i]]+w[i]}

         将这个思路转换成代码就是

35 0-1背包问题代码

#definePACKAGESIZE1000

#defineMATERIALSSIZE100

intf[MATERIALSSIZE][PACKAGESIZE];

voidzeroOnePack(intcost[],intvalue[],intnum,intpackSize){

    for(intindex=0;index;index++){

        for(v=cost[index];v<=packSize;v++){

            f[index+1][v]=max(f[index][v], f[index][v-cost[index]]+value[index]);

        }

    }

}

         对于这个方法,容易求的其时间复杂度为O(NV),空间复杂度为O(NV)。这个时间复杂度已经不能优化了,但是空间复杂度还可以优化到O(V)

2.1.1.3 优化空间复杂度

         我们可以观察下图,箭头位置标识当前求解的位置(绿色部分),即f[i][v]。根据状态转移方程可以知道,它的值只与f[i-1][v]f[i-1][v-c[i]]相关(黄色部分)。对于所有>v的状态(蓝色部分)是没有使用到的。

9

         简单一些思考,可以认为这个过程使用2个大小为V的数组就可以解决问题。但是可以有更优的方法,只要在求解f[v]之前的操作不覆盖<=v的左右状态即可,所以可以只使用一个大小为V的数组即可。这种方法的伪代码为:

36

for i=1..N

    for v=V..c[i]

        f[v]=max{f[v],f[v-c[i]]+w[i]};

         与之对应的状态转移方程为:

f[v]=max{f[v], f[v-c[i]]+w[i]}

         需要注意的就是,使用这个状态转移方程的时候,v的遍历规则就必须从高往低,这样才不会发生覆盖。

2.1.1.4 通用化模板

         这里将求解第i个物品的状态过程抽取出来,伪代码如下:

37

procedure ZeroOnePack(cost,weight)

    for v=V..cost

        f[v]=max{f[v],f[v-cost]+weight}

         求解背包问题的伪代码就是:

38

for i=1..N

ZeroOnePack(c[i],w[i]);

         这样背包问题的模板代码就如下所示,对于其他背包问题的代码模板也如下所示。

39

#definePACKAGESIZE1000

intf[PACKAGESIZE];

voidzeroOnePack(intcost,intvalue,intpackSize){

    for(intv = packSize;v>=cost;v--){

        f[v]=max(f[v],f[v-cost]+value);

    }

}

intmain(){

    intc[]={2,3,4,5,6};  intw[]={2,3,4,5,6};

    for(inti=0;i<5;i++){

        zeroOnePack(c[i],w[i],13);

    }

}

2.1.1.5 初始化细节问题

         背包问题在问的时候可能有两种问法:“恰好装满背包”、“不要求装满背包”。对于这两种问法,初始化过程则存在一些不同。

Ø  如果要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为 ,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。

Ø  如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。

可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0nothing“恰好装满,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解什么都不装,这个解的价值为0,所以初始时状态的值也就全部为0了。

         举个例子说明:假设有3个物品,费用为c[]={2, 3, 4},价值为w[]={3, 4, 8},背包大小5。如果不要求恰好装满,则最后答案为8(选择物品3);如果要求恰好装满,则要求最后答案为7(选择物品12)

10

         这个技巧适用于其他背包问题中的初始化过程。

2.1.1.6 通用化状态转移方程

         0-1背包问题指的是放入背包中物品数量的取值范围为{01},所以状态转移方程可以成为:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i] | k=0,1}

         后面背包问题的状态转移方程其实就是这种格式。

2.1.2  完全背包问题

2.1.2.1 问题描述

物品数量为N,背包容量为V。第i件物品费用为c[i],价值为w[i]每件物品数量任意。问:放入哪些物品后,背包中物品价值总和达到最大。

2.1.2.2 基本思路

         因为物品数量可以取任意个,所以容易得到完全背包问题的状态转移方程为:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i] | k=0,1,...,V/c[i]}

         这个思路的空间复杂度还是O(NV),时间复杂度为O(NΣ(V/c[i])),大于O(NV)

该算法的伪代码为:

40

for i=1..N

         for v=1..V

                   for k=0..V/c[i]

                            f[i][v]=max{f[i-1][v], f[i][v-k*c[i]]+k*w[i]}

2.1.2.3 简单优化

         优化思路:若两件物品i,j满足c[i]<=c[j]w[i]>=w[j],则可以将物品j去除。因为j物品费用更高但是价值更小,我们可以倾向选择物品i而不选择物品j

2.1.2.4 二进制优化

         二进制优化基于这样一个基本原理:

将一个数字num分解为若干个数的和,即

num=n1+n2+...+nm

         通过组合n1,n2,...,nm中的若干个数(每个数字只能选取1)进行求和,可以表示1~num之间的任意数字。

         例如:比如13(1101)这个数,可以分解成1(0001)2(0010)4(0100)6(0110)四个数,1~13范围内的数可以由这四个数字唯一的表示如下:

1=1         2=2             3=1+2              4=4                   5=1+4

6=6         7=1+6           8=2+6              9=1+2+6               10=4+6  

11=1+4+6    12=2+4+6        13=1+2+4+6

对于如何进行二进制数分解,算法的代码如下:

41 对一个数字进行二进制分解

void divNum(int num) {

    int k = 1;

    while (k < num) {

        k = k << 1;

        num -= k;

        cout << k << endl; // 这里输出分解后的数

    }

    cout << num << endl;    // 分解的最后一个数字

}

         通过上述的模板可以得到完全背包问题的代码如下。

42 使用二进制优化的完全背包代码

#definePACKAGESIZE1000

intf[PACKAGESIZE];

voidcompletePac(intcost,intvalue,intpackSize){

    intnum=packSize/cost;//背包可以容纳最多物品的数量

    intk=1;

    while(k<num){

        zeroOnePack(k*cost,k*value,packSize);

        num-=k;

        k=k<<1;

    }

    if(num>0){

        zeroOnePack(num*cost,num*value,packSize);

    }

}

         我们观察二进制优化的完全背包问题的计算过程。背包容量为6,有2个物品,费用为c[]={1, 2},价值为w[]={1,3}。这里忽略第1个物品的求解过程。对于物品2,背包最多容纳6/2=3个物品。数字3可以分解成12。在图中第二行(k=2)中,黄色部分表示背包里面装着1个物品2;蓝色部分表示装着1个物品1;橙色部分表示装着1个物品12个物品2;绿色部分表示装着3个物品2

11

2.1.2.5 O(NV)的算法

         伪代码如下:

43

procedure CompletePack(cost,weight)

    for v=cost..V

        f[v]=max{f[v],f[v-c[i]]+w[i]}

         这个代码与0-1背包问题中的代码区别就在于循环的时候的次序是从低往高。通过观察下图的过程,可以发现物品2的选择是被不断累积的。

12

         这种方式下得到的时间复杂度就是O(NV),空间复杂度就是O(V),比原来的快了不少,而且代码量非常少。

44 完全背包问题代码

#definePACKAGESIZE1000

intf[PACKAGESIZE];

voidcompletePack(intcost,intvalue,intpackSize){

    for(intv= cost;v<=packSize;v++){

        f[v]=max(f[v],f[v-cost]+value);

    }

}

2.1.3  多重背包问题

2.1.3.1 问题描述

物品数量为N,背包容量为V。第i件物品费用为c[i],价值为w[i]每件物品数量为n[i]。问:放入哪些物品后,背包中物品价值总和达到最大。

2.1.3.2 基本思路

         如果知道了完全背包问题中的状态转移方程,多重背包问题的状态转移方程就很容易得到了:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i] | k=0,1,...,min(V/c[i],n[i])}

如果直接破解,时间复杂度为O(V*Σn[i])

2.1.3.3 二进制优化

         如果返回观察完全背包问题中使用二进制优化的算法过程,我们其实发现二进制优化的完全背包问题的求解过程是具有通用性的,它适合于任意的物品数量。这样多重背包问题就可以转换成完全背包问题进行求解了。其伪代码如下:

45

procedure MultiplePack(cost, weight, amount)

    if cost*amount>=V

        CompletePack(cost, weight)

    else

        integer k=1

        while k<amount

            ZeroOnePack(k*cost, k*weight)

            amount=amount-k

            k=k*2

            ZeroOnePack(amount*cost, amount*weight)

         因为物品数量可能超过背包容纳的最大值,对于这种情况肯定就直接使用完全背包过程中的方法进行处理。

2.1.3.4 利用单调队列优化

》》》》

2.2     最长公共子序列(LCS)问题

2.2.1  问题描述

         给定两个序列A[],B[N],求出两个序列的最长的公共子序列。

2.2.2  基本思路

         定义状态:F[i, j]表示序列长度分别为i,j时候的最长子序列长度。则当已经序列长度都加1的情况下。新增的元素可以有两种情况:

Ø  相同,则F[i+1,j+1]=F[i,j]+1

Ø  不同,则F[i+1,j+1]=max{F[i+1,j], F[i,j+1]}

初始解为F[0, 0] = 0

         整理得到递归方程:

         通过这个递归方程不难得到一个递归的求解代码:

46

#defineMAXSIZE100

intf[MAXSIZE][MAXSIZE];

intlcs(chara[],charb[],inti,intj){

    if(i==0||j==0)return0;

    if(f[i][j]>0)returnf[i][j];//加速搜索

    if(a[i-1]==b[j-1]){

        f[i][j]=lcs(a,b,i-1,j-1)+1;  

    }else{

        f[i][j]=max(lcs(a,b,i-1,j),lcs(a,b,i,j-1));

    }

    returnf[i][j];

}

         代码的时间复杂度为O(MN),空间复杂度为O(MN),递归深度达到max{M,N}

         通过递推的方式,也很容易得到代码:

47

#defineMAXSIZE100

intf[MAXSIZE][MAXSIZE];

voidlcs(chara[],charb[],intm,intn){

    for(inti=1;i<=m;i++){

        for(intj=1;j<=n;j++){

            if(a[i-1]==b[j-1]){

                f[i][j]=f[i-1][j-1]+1;

            }else{

                f[i][j]=max(f[i-1][j],f[i][j-1]);

            }

        }

    }

}

2.2.3  优化空间复杂度

         如果认真观察递归过程中状态的更新过程,很容易就发现使用之前背包问题一样的优化方法,可以将空间复杂度降低到O(min{m,n})

48

#defineMAXSIZE100

intf[MAXSIZE];

voidlcs(chara[],charb[],intm,intn){

    for(inti=1;i<=m;i++){

        for(intj=1;j<=n;j++){

            if(a[i-1]==b[j-1])

                f[j]=f[j-1]+1;

            else

                f[j]=max(f[j],f[j-1]);

        }

    }

}

2.2.4  优化时间复杂度

如果再仔细观察,其实有些状态的转换是不需要的,这意味着在时间复杂度上还可以进一步优化。

2.3     数组的最长递增子序列

2.3.1  问题描述

         给定一个子序列,在其所有递增的子序列中找到最长的一个。

2.3.2  基本思路

         利用动态规划的思路解决,定义状态:f[i]表示数组长度为i的情况下最长递增子序列长度。

         如果已经知道前i-1个元素中最长递增子序列长度,则在选择第i个元素的时候,从第i-1个递增子序列开始,在所有符合条件子序列(及子序列最大元素小于等于i元素的子序列)中找到长度最大的一个,然后明显可以知道这个f[i]的长度就是这个子序列的长度加1。所以状态转移方程就写成:

f[i]=max{f[j]+1 | 1<=j<=i-1}

         如果没有找到符合条件的递增子序列,则i元素的递增子序列只能从i元素开始计算,所以f[i]=1。所以最终的状态转移方程就写成了:

f[i]=max{f[j]+1 | 0<=j<=i-1, f[0]=0}

         根据这个思路就不难得到一个初始的代码实现:

49

#defineMAXSIZE100

intmxl[MAXSIZE];

voidlis(inta[],intn){

    mxl[0]=0;//初始解

    for(intidx=0;idx<n;idx++){

        intmxidx=0;//最长递增子序列下标

        for(intwlk=idx-1;wlk>=0;wlk--){

            if(a[wlk]<=a[idx]){

                mxidx=(mxl[mxidx]>mxl[wlk+1])?mxidx:wlk+1;

            }

        }

        mxl[idx+1]=mxl[mxidx]+1;

    }

}

2.3.3  优化时间复杂度

         容易就可以求解表 49中的时间复杂度是O(N2),通过观察发现,每次查找最长递增子序列耗费了大量的时间,如果能够将这个查找速度加快,整个算法的性能就可以得到提高。

         可以利用红黑树,用递增子序列的最大值作为键值,同时保存这个键值的下标。这样就可以在O(lgI)的时间复杂度内查找需要的值。

         最后这个时间复杂度可以降低到O(NlgN)。不过,空间复杂度要增加到O(N)

         树的查找过程很简单,只要找到第一个小于key值的节点就是想要找的元素。

50

Node*findFirstSmallEqual(Treeroot,Itemkey){

    while(root!=NULL&&root->key>key){

        root=root->left;

    }

    returnroot;

}

         最后可以将上述的代码修改成:

51

voidlis(inta[],intn){

    mxl[0]=0;//初始解

    for(intidx=0;idx<n;idx++){

        Node*n=findFirstSmallEqual(t,a[idx]);

        mxl[idx+1]=mxl[n->value]+1;

        //更新已知解

        if(n->key==a[idx])

            n->value=idx;

        else

            insert(t,newNode(a[idx],idx));

    }

}

 

2.4     数组分割问题

2.4.1  问题描述

         给定一个长度为2N的数组,要求分割成两个长度为N的数组,并且最后两个数组和的差值的模要达到最小。

2.4.2  基本思路

         根据题意,很容易得到优化目标:设两个数组的和分别为S1S2,则优化目标为min{|S1-S2|}

         这个目标因为有两个变量,所以求解起来比较麻烦,不过可以将问题转换,转换过程为:

         S=S1+S2,则目标变成:

min{|S1-S2|}= min{|S1-(S-S1)|}=min{|2*S1-S|}

         这样只有S1处于变化中,相比原来的问题要简单很多。

         这样问题就可以转换为:已经知道一个数组a[N]和数组的和S1,以及一个判断标准S,然后输入一个数组b[N],利用b[N]中的元素去替换a[N]中的元素,最后让S1的值尽可能接近S

         使用动态规划思路进行求解,定义状态:s[i]表示利用b[]中前i个元素更新a[]中元素时最优的S1值。

         这样就可以得到状态转移方程:

s[i]= min{ s[i-1]-a[m]+b[i], s[i-1] | m= min{|s[i-1]-a[k]+b[i]-S| | 1<=k<=N}}

2.4.3  优化空间复杂度

         如果观察整个算法的运行过程,我们发现s[i]仅与s[i-1]相关,这意味着我们不需要保存所有状态,这样空间复杂度从O(N)降低到了O(1)

         通过这种方式容易写出如下的代码实现:

52

intts=0;//整个数组的和

ints=0;//数组a的和

intfindMin(inta[],intn,intv){

    intm=-1;inttMin=abs((s<<1)-ts);

    for(inti=0;i<n;i++){

        inttmp=abs(((s-a[i]+v)<<1)-ts);

        if(tmp==0){//哨兵,表示问题解决

            m=i; break;

        }

        if(tMin<=tmp) continue;

        tMin=tmp;

        m=i;

    }

    returnm;

}

voidsplit(intarr[],intn){

    intnh=n>>1;

    int*a=arr;int*b=arr+nh;

    //求和数组a的的和,求整个数组的和

    for(inti=0;i<nh;i++)

        s+=a[i];

    ts=s;

    for(inti=0;i<nh;i++)

        ts+=b[i];

    //检查数组b中的每个元素

    for(inti=0;i<nh;i++){

        if((s<<1)==ts)//哨兵,问题恰好解决

            return;

        intm=findMin(a,nh,b[i]);//找到最适合替换的下标

        if(m!=-1){//为-1的时候表示不替换得到的解更优

            s=s-a[m]+b[i];

            swap(a[m],b[i]);

        }

    }

}

         这种方法存在一个缺陷,就是每次都需要遍历整个数组a才能找到最恰当的值用于替换。所以容易计算整个算法的时间复杂度其实是O(N2)

2.4.4  优化时间复杂度

         明显影响这个算法性能的主要因子就在于查找需要替换的元素的过程,优化的方法也就是如何提高这个过程中的搜索速度。

         我们仔细分析一下这个搜索过程,令tMin = |2*s - ts|,如果a[i]替换更比当前的结果更优则需要满足不等式:

|2(s-a[i]+v)-ts|

         分三种情况讨论这个不等式

Ø  如果2(s-a[i]+v)-ts>0,即2*a[i]<2*( s+v)-ts,此时解上述不等式得:2(s+v)-ts-tMin<2*a[i]。所以最后符合条件的a[i]应该在区间范围[s+v-(ts+tMin)/2,  s+v-ts/2]内,而且越靠近s+v-ts/2,得到的解越优。所以数组a中,第一个小于s+v-ts/2的值,如果值范围在区间范围内,这个值肯定是一个最优解。

Ø  如果2(s-a[i]+v)-ts<0,即2*a[i]>2*( s+v)-ts,此时解上述不等式得:2(s+v)-ts+tMin>2*a[i]。所以最后符合条件的a[i]应该在区间范围[ s+v-ts/2, s+v-(ts-tMin)/2]内,而且越靠近s+v-ts/2,得到的解越优。所以数组a中,第一个大于s+v-ts/2的值,如果值范围在区间范围内,这个值肯定是一个最优解。

Ø  如果2(s-a[i]+v)-ts=0,即2*a[i]=2*( s+v)-ts,这无疑是整个问题最后的最优解。此时搜索结束。

总结上述三种情况,

Ø  我们如果在数组中找到值等于s+v-ts/2,则整个问题解决。

Ø  如果没有找到s+v-ts/2,则寻找其左右第一个值,如果这个值落在区间[s+v-(ts+tMin)/2, s+v-(ts-tMin)/2]内,则作为一个可行解。最后再从这两个可行解中找到最优的解即可。

通过这些分析,我们就可以设计自己的搜索方法了。

         第一种明显的方式是,如果数组a中的元素是有序的,那使用二分搜索,就可以将这个搜索的算法复杂度降低到O(lgN),不过这里需要注意的就是,当进行值的替换以后,如何保持数组中仍然有序,而这个保持过程的算法复杂度为O(N),结果此消彼长下,最后并没有得到优化。

         第二种思路就是,利用一颗红黑树,这样搜索的时间复杂度降低为O(lgN),同时替换值的时间复杂度也控制在O(lgN)内,这样整个算法最终的时间复杂度就控制在O(NlgN)

2.5     求子数组最大和

2.5.1  求解一维数组的子数组最大和

2.5.1.1 问题描述

         给定一组输入,求一个子数组,让其和最大。

2.5.1.2 解决思路

53

intmaxSum(inta[],intn){

    intsum=0;intret=0x80000000;

    for(inti=0;i<n;i++){

        sum+=a[i];

        if(ret<sum)

            ret=sum;

        if(sum<0)

            sum=0;

    }

    returnret;

}

2.5.2  求解二维数组的子数组最大和

2.5.2.1 问题描述

         给定一个M*N的子数组,求解子数组中的最大和值。

2.5.2.2 基本思路

         明显这个问题需要遍历一次所有的子数组才能得到答案。

 

你可能感兴趣的:(算法)