最大子段和问题--蛮力法、分治法、动态规划(Java代码)

最大子段和问题

给定由n个整数组成的序列(a1,a2,...,ana_1,a_2,...,a_n),求该序列形如 k=ijak\sum\limits_{k=i}^j a_k的子段和的最大值,当所有整数均为负整数时,其最大子段和为0。

问题分析

简单的说就是求一个序列中截取一段连续的序列,要求截取的序列的和为最大值。
举例:

  • 如序列为(-5,8,12,-6,8,-14)的最大连续的序列为(8,12,-6,8)其和为22
  • 如序列为(-5,8,12,-6,8,-14,-15,5,20)的最大连续的序列为(5,20)其和为25
  • 如序列为(-5,-8,-6,-14)无最大连续的序列,最大子段和为0

蛮力法

思路:找出序列的所有连续的序列,然后找出最大子段和

难点:这个区别于全排列,连续的序列是连续的(有点废话。。),所以通过两个for循环扫描即可,一个指向序列的头,一个指向序列的尾部扫秒即可。

代码如下:

//蛮力法(很暴力,我喜欢)
public static int maxSubseqSumByBF(int arr[]){
     int length = arr.length;
     int maxSum = 0;	//用来记录最大值
     for (int i = 0; i < length; i++) { 	//这个循环扫描出所有连续序列
         int nowSum = 0;
         for (int j = i; j < length; j++) {	//这个循环扫描出所有的以i开头的连续序列
             nowSum += arr[j];
             if(nowSum > maxSum){
                 maxSum = nowSum;
             }
         }
     }
     return maxSum;
 }

分治法

思路:将序列S=(a1,a2,...,ana_1,a_2,...,a_n)划分成两个长度相同的连续序列A=(a1,a2,...,an/2a_1,a_2,...,a_{n/2})和连续序列B=(an/2+1,...,ana_{n/2+1},...,a_n)。则最大子段和有三种情况:

  • A中的最大子段和为S的最大子段和
  • B中的最大子段和为S的最大子段和
  • S的最大子段和为A和B中交界部分,应为k=ijak\sum\limits_{k=i}^j a_k,且i(1,n/2),j(n/2+1,n)i\in(1,n/2),j\in(n/2+1,n)

难点:求A和B中的交界部分的最大子段和。从n/2处分别往左右两端找出最大子段和,然后合并左右的最大子段和即为交界部分的最大子段和。

代码如下:

//分治法(递归方法求解)
public static int maxSubseqSum_ByDC(int arr[],int lo,int hi){
    int center = lo + (hi-lo)/2;
    int maxLeftSum,maxRightSum;     //分别表示左右最大字段和
    int borLeftSum,borRightSum;     //分别表示在center左右的最大字段和

    if(lo == hi){
        if(arr[lo] > 0)
            return arr[lo];
        else
            return 0;
    }

    maxLeftSum = maxSubseqSum_ByDC(arr,lo,center);
    maxRightSum = maxSubseqSum_ByDC(arr,center+1,hi);

    //求解交界部分的最大值,并与左右部分比较得出最大子段和
    borLeftSum = 0;
    borRightSum = 0;
    int tempSum = 0;
    for (int i = center; i <= hi; i++) {	//往右找出最大子段和
        tempSum += arr[i];
        if(tempSum > borRightSum){
            borRightSum  = tempSum;
        }
    }
    tempSum = 0;
    for (int i = center; i >= lo; i--) {	//往左找出最大子段和
        tempSum += arr[i];
        if(tempSum > borLeftSum){
            borLeftSum  = tempSum;
        }
    }
    //比较左,右,交界处的子段和得出最大字段和
    int maxSum = Math.max(maxLeftSum,maxRightSum);
    maxSum = Math.max(maxSum,borLeftSum+borRightSum-arr[center]);
    return maxSum;
}

动态规划

思路:若最大子段和为:maxSum =k=ijak\sum\limits_{k=i}^j a_k,则考虑aja_j,若aj>0a_j\gt0则maxSum =k=ij1ak+aj\sum\limits_{k=i}^{j-1} a_k + a_j,若aj<0a_j\lt0则maxSum =k=ij1ak\sum\limits_{k=i}^{j-1} a_k。故得到递推式子为:maxSum = max{k=ij1ak+aj\sum\limits_{k=i}^{j-1} a_k + a_j,k=ij1ak\sum\limits_{k=i}^{j-1} a_k}。需要注意的是若为负数,则maxSum=0。

难点:代码没啥难点,难就在不好想出递推式。。。先看代码有助于理解思路

代码如下:

//动态规划
public static int maxSubseqSumByDP(int arr[]){
    int length = arr.length;
    int MaxSum = 0,NowSum = 0;
    for (int i = 0; i < length; i++) {
        NowSum += arr[i];
        if(NowSum < 0){
            NowSum = 0;
        }
        MaxSum = Math.max(NowSum,MaxSum);
    }
    return MaxSum;
}

自己想出的idea

这个idea是博主误打误撞想出来的,如有雷同,纯属巧合。。。
这个idea的时间复杂度和动态规划一样都是O(n),空间复杂度也为O(n)

想法:将所给序列S=(a1,a2,...,ana_1,a_2,...,a_n)变成一个为(正,负,正,负…)或者(负,正,负,正,…)的序列。比如序列(-5,8,12,-6,8,-14,-15,5,20)就应该变为(-5,20,-6,8,-15,25)。然后就只考虑正数了,maxSum = max(k=ij2ak+aj1+aj,k=ij2ak)(\sum\limits_{k=i}^{j-2} a_k + a_{j-1}+a_j,\sum\limits_{k=i}^{j-2} a_k),这里的aj1a_{j-1}肯定为负数,aja_j肯定为正数。

难点:递推式不好理解,建议参考代码理解。

代码如下:

//自己的idea
public static int maxSubseqSumByme(int[] arr){
   int len = arr.length;
   int[] tempValue = new int[len];     //记录暂时的值
   ArrayList<Integer> alist = new ArrayList<>();   //存储数据为(正,负,正,负...)或(负,正,负...)
   tempValue[0] = arr[0];  //先考虑第一个,为了不越界
   
   //找出序列中连续存在的正数总和和负数总和
   for (int i = 1; i < len; i++) {
       if(arr[i-1] < 0 && arr[i] < 0){
           tempValue[i] = arr[i] + tempValue[i-1];
       }else if(arr[i-1] > 0 && arr[i] > 0){
           tempValue[i] = arr[i] + tempValue[i-1];
       }else{
           tempValue[i] = arr[i];
           alist.add(tempValue[i-1]);	//这里为什么存储的i-1需要理解一下
       }
   }
   alist.add(tempValue[len-1]);		//将最后一个存储进去
   
   //求解最大子段和
   int maxValue = alist.get(0)>0?alist.get(0):0;	//先考虑第一个,为了不越界
   for (int i = 1; i < alist.size(); i++) {
       if(alist.get(i)>0){
           maxValue = Math.max(alist.get(i),maxValue+alist.get(i-1)+alist.get(i));
       }
   }
   return maxValue;
}

方法整合

直接copy即可

import java.util.ArrayList;

public class MaxSubseqSum {
    public static void main(String[] args) {
        int[] arr = {10,-5,8,12,-6,8,-14,-15,5,20};
        System.out.println("数组为:");
        for (int i = 0; i < arr.length; i++) System.out.print(arr[i]+" ");
        System.out.println();
        System.out.println("蛮力法求解最大字段和为:");
        System.out.println(maxSubseqSumByBF(arr));
        System.out.println("分治法求解最大字段和为:");
        System.out.println(maxSubseqSumByDC(arr,0,arr.length-1));
        System.out.println("动态规划求解最大字段和为:");
        System.out.println(maxSubseqSumByDP(arr));
        System.out.println("自己idea求解最大子段和为:");
        System.out.println(maxSubseqSumByme(arr));
    }
    //蛮力法
    public static int maxSubseqSumByBF(int arr[]){
        int length = arr.length;
        int maxSum = 0;	//用来记录最大值
        for (int i = 0; i < length; i++) { 	//这个循环扫描出所有连续序列
            int nowSum = 0;
            for (int j = i; j < length; j++) {	//这个循环扫描出所有的以i开头的连续序列
                nowSum += arr[j];
                if(nowSum > maxSum){
                    maxSum = nowSum;
                }
            }
        }
        return maxSum;
    }
    //分治法(递归方法求解)
    public static int maxSubseqSumByDC(int arr[],int lo,int hi){
        int center = lo + (hi-lo)/2;
        int maxLeftSum,maxRightSum;     //分别表示左右最大字段和
        int borLeftSum,borRightSum;     //分别表示在center左右的最大字段和

        if(lo == hi){
            if(arr[lo] > 0)
                return arr[lo];
            else
                return 0;
        }

        maxLeftSum = maxSubseqSumByDC(arr,lo,center);
        maxRightSum = maxSubseqSumByDC(arr,center+1,hi);

        //求解交界部分的最大值,并与左右部分比较得出最大子段和
        borLeftSum = 0;
        borRightSum = 0;
        int tempSum = 0;
        for (int i = center; i <= hi; i++) {	//往右找出最大子段和
            tempSum += arr[i];
            if(tempSum > borRightSum){
                borRightSum  = tempSum;
            }
        }
        tempSum = 0;
        for (int i = center; i >= lo; i--) {	//往左找出最大子段和
            tempSum += arr[i];
            if(tempSum > borLeftSum){
                borLeftSum  = tempSum;
            }
        }
        //比较左,右,交界处的子段和得出最大字段和
        int maxSum = Math.max(maxLeftSum,maxRightSum);
        maxSum = Math.max(maxSum,borLeftSum+borRightSum-arr[center]);
        return maxSum;
    }
    //动态规划
    public static int maxSubseqSumByDP(int arr[]){
        int length = arr.length;
        int MaxSum = 0,NowSum = 0;
        for (int i = 0; i < length; i++) {
            NowSum += arr[i];
            if(NowSum < 0){
                NowSum = 0;
            }
            MaxSum = Math.max(NowSum,MaxSum);
        }
        return MaxSum;
    }

    //自己的idea
    public static int maxSubseqSumByme(int[] arr){
        int len = arr.length;
        int[] tempValue = new int[len];     //记录暂时的值
        ArrayList<Integer> alist = new ArrayList<>();   //存储数据为(正,负,正,负...)或(负,正,负...)
        tempValue[0] = arr[0];  //先考虑第一个,为了不越界

        //找出序列中连续存在的正数总和和负数总和
        for (int i = 1; i < len; i++) {
            if(arr[i-1] < 0 && arr[i] < 0){
                tempValue[i] = arr[i] + tempValue[i-1];
            }else if(arr[i-1] > 0 && arr[i] > 0){
                tempValue[i] = arr[i] + tempValue[i-1];
            }else{
                tempValue[i] = arr[i];
                alist.add(tempValue[i-1]);	//这里为什么存储的i-1需要理解一下
            }
        }
        alist.add(tempValue[len-1]);		//将最后一个存储进去

        //求解最大子段和
        int maxValue = alist.get(0)>0?alist.get(0):0;	//先考虑第一个,为了不越界
        for (int i = 1; i < alist.size(); i++) {
            if(alist.get(i)>0){
                maxValue = Math.max(alist.get(i),maxValue+alist.get(i-1)+alist.get(i));
            }
        }
        return maxValue;
    }
}

参考书籍:《算法分析与设计 第2版》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章