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

最大子段和问题

给定由n个整数组成的序列( a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an),求该序列形如 ∑ k = i j a k \sum\limits_{k=i}^j a_k k=ijak的子段和的最大值,当所有整数均为负整数时,其最大子段和为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=( a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an)划分成两个长度相同的连续序列A=( a 1 , a 2 , . . . , a n / 2 a_1,a_2,...,a_{n/2} a1,a2,...,an/2)和连续序列B=( a n / 2 + 1 , . . . , a n a_{n/2+1},...,a_n an/2+1,...,an)。则最大子段和有三种情况:

  • A中的最大子段和为S的最大子段和
  • B中的最大子段和为S的最大子段和
  • S的最大子段和为A和B中交界部分,应为 ∑ k = i j a k \sum\limits_{k=i}^j a_k k=ijak,且 i ∈ ( 1 , n / 2 ) , j ∈ ( n / 2 + 1 , n ) i\in(1,n/2),j\in(n/2+1,n) i(1,n/2),j(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 = i j a k \sum\limits_{k=i}^j a_k k=ijak,则考虑 a j a_j aj,若 a j > 0 a_j\gt0 aj>0则maxSum = ∑ k = i j − 1 a k + a j \sum\limits_{k=i}^{j-1} a_k + a_j k=ij1ak+aj,若 a j < 0 a_j\lt0 aj<0则maxSum = ∑ k = i j − 1 a k \sum\limits_{k=i}^{j-1} a_k k=ij1ak。故得到递推式子为:maxSum = max{ ∑ k = i j − 1 a k + a j \sum\limits_{k=i}^{j-1} a_k + a_j k=ij1ak+aj, ∑ k = i j − 1 a k \sum\limits_{k=i}^{j-1} a_k k=ij1ak}。需要注意的是若为负数,则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=( a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an)变成一个为(正,负,正,负…)或者(负,正,负,正,…)的序列。比如序列(-5,8,12,-6,8,-14,-15,5,20)就应该变为(-5,20,-6,8,-15,25)。然后就只考虑正数了,maxSum = max ( ∑ k = i j − 2 a k + a j − 1 + a j , ∑ k = i j − 2 a k ) (\sum\limits_{k=i}^{j-2} a_k + a_{j-1}+a_j,\sum\limits_{k=i}^{j-2} a_k) (k=ij2ak+aj1+aj,k=ij2ak),这里的 a j − 1 a_{j-1} aj1肯定为负数, a j a_j aj肯定为正数。

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

代码如下:

//自己的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版》

  • 11
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
最大子段和问题是指在一个数列中找到一个子序列,使得该子序列中所有元素的和最大。以下是三种常见的算法实现: 1. 蛮力 蛮力是最朴素的解,它的时间复杂度为 $O(n^2)$。具体实现如下: ```c++ int maxSubArray(int nums[], int n) { int ans = INT_MIN; for (int i = 0; i < n; i++) { int sum = 0; for (int j = i; j < n; j++) { sum += nums[j]; ans = max(ans, sum); } } return ans; } ``` 2. 分治法 分治法的时间复杂度为 $O(n\log n)$,它将问题分成三个部分:求解左半部分的最大子段和、求解右半部分的最大子段和、求解跨越中点的最大子段和。具体实现如下: ```c++ int maxSubArray(int nums[], int left, int right) { if (left == right) return nums[left]; int mid = left + (right - left) / 2; int leftMax = maxSubArray(nums, left, mid); int rightMax = maxSubArray(nums, mid + 1, right); int crossMax = nums[mid]; int sum = nums[mid]; for (int i = mid - 1; i >= left; i--) { sum += nums[i]; crossMax = max(crossMax, sum); } sum = crossMax; for (int i = mid + 1; i <= right; i++) { sum += nums[i]; crossMax = max(crossMax, sum); } return max(leftMax, max(rightMax, crossMax)); } ``` 3. 动态规划 动态规划的时间复杂度为 $O(n)$,它定义了一个状态数组 $dp$,其中 $dp[i]$ 表示以 $i$ 结尾的最大子段和。具体实现如下: ```c++ int maxSubArray(int nums[], int n) { int dp[n]; dp[0] = nums[0]; int ans = nums[0]; for (int i = 1; i < n; i++) { dp[i] = max(dp[i - 1] + nums[i], nums[i]); ans = max(ans, dp[i]); } return ans; } ``` 以上是三种常见的算法实现,需要注意的是,在实际应用中,我们还可以使用其他优化方,如前缀和、后缀和、单调栈等,以进一步提高算法效率。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值