最大子段和問題--蠻力法、分治法、動態規劃(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版》

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