Java求區間連續最大和的三種解法(含輸出起始位置)


先上題

面試華為 OD (社招)的時候給我來了這么一道題,媽耶,沒刷過這題,給我虐得,氣急敗壞,只好跟面試官說不會。但是,誰還願意服輸啊,面試完了,我倒想看看這是個什么類型的題目。

1.1 問題描述
首先,輸入一個正整數 N (1<=N<=100000),接着再輸入 N 個整數,數值范圍為 [-1000,1000]。要求得到子序列的最大和,並求出此時子序列第一個數字的位置,和最后一個數字的位置。

1.2 輸入示例

5 6 -1 5 4 -7

1.3 輸出示例

14 1 4

分析

力扣類似題型
力扣的題庫里有一道類似的題目 連續子數組的最大和,可以去這里面檢驗一下試試。

給定序列 a[1],a[2],a[3] ... a[n],您的工作是計算子序列的最大和。例如,給定(6,-1, 5, 4,-7),此序列的最大和為 6 +(-1)+ 5 + 4 = 14。

解題

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int arrayLength = in.nextInt();
        int[] nums = new int[arrayLength];
        for (int i = 0; i < arrayLength; i++) {
            nums[i] = in.nextInt();
        }
        // 通過多態來嘗試多種解決方法
        Solution maxSum = new ViolentSolution();
        // 這里的代碼就和力扣原題十分類似了,maxSubArray 的結果稍加改動就可以拿到力扣上去測試了
        int[] result = maxSum.maxSubArray(nums);
        if (result.length == 3) {
            String resultStr = String.format("%d %d %d", result[0], result[1], result[2]);
            System.out.println(resultStr);
        }
    }

    public int[] maxSubArray(int[] nums) {
        return new int[3];
    }
}

Solution 接口

public interface Solution {
    /**
     * 返回一個包含三個元素的數組,第一個元素表示子序列最大和,第二個元素表示子序列第一個數字的位置,第二個元素表示子序列最后一個數字的位置
     */
    int[] maxSubArray(int[] nums);
}

1. 暴力解法

public class ViolentSolution implements Solution {
    public int[] maxSubArray(int[] nums) {
        int maxSum = nums[0];  // 子序列最大和
        int low = 0;           // 起始下標
        int high = 0;          // 結束下標
        final int arrayLength = nums.length;
        for (int i = 0; i < arrayLength; i++) {
            for (int j = i; j < arrayLength; j++) {
                int sum = sumOf(nums, i, j);
                if (sum > maxSum) {
                    maxSum = sum;
                    low = i;
                    high = j;
                }
            }
        }
        // 因為我是從0開始計算的,而返回值要求從1開始
        return new int[]{maxSum, low + 1, high + 1};
    }

    private int sumOf(int[] nums, int i, int j ) {
        int sum = 0;
        for (int k = i; k <=j ; k++) {
            sum += nums[k];
        }
        return sum;
    }
}

思路
划分子序列區間,計算子序列內的值。

  • i, j 可以划分出一個子序列
  • ij 從小向大發展
  • 依次計算所有子序列的和,並且通過比較,保留下最大的

缺點
時間復雜度高,時間復雜度是 \(O(n^3)\) , 因此 leetcode 也沒給通過。

2. 分治法

解題

public class DivideSolution implements Solution {

    @Override
    public int[] maxSubArray(int[] nums) {
        int[] result = maxSubArray(nums, 0, nums.length - 1);
        return new int[]{ result[0], result[1] + 1, result[2] + 1};
    }

    private int[] maxSubArray(int[] nums, int low, int high) {
        if (low == high) {
            return new int[]{nums[low], low, high};
        }
        int mid = (low + high) / 2;
        int[] leftResult = maxSubArray(nums, low, mid);
        int[] rightResult = maxSubArray(nums, mid + 1, high);
        int[] midResult = maxSubArray(nums, low, mid, high);
        if (leftResult[0] >= rightResult[0] && leftResult[0] >= midResult[0]) {
            return leftResult;
        } else if (midResult[0] >= leftResult[0] && midResult[0] >= rightResult[0]) {
            return midResult;
        } else {
            return rightResult;
        }
    }

    private int[] maxSubArray(int[] nums, int low, int mid, int high) {
        int maxLeftSum = nums[mid];
        int leftSum = 0;
        int leftIndex = mid;
        // 從中點開始往左邊增加
        for (int i = mid; i >= low; i--) {
            leftSum += nums[i];
            if (leftSum > maxLeftSum) {
                maxLeftSum = leftSum;
                leftIndex = i;
            }
        }

        int maxRightSum = nums[mid + 1];
        int rightSum = 0;
        int rightIndex = mid + 1;
        // 從中點開始往右邊增加
        for (int i = mid + 1; i <= high; i++) {
            rightSum += nums[i];
            if (rightSum > maxRightSum) {
                maxRightSum = rightSum;
                rightIndex = i;
            }
        }
        return new int[]{maxLeftSum + maxRightSum, leftIndex, rightIndex};
    }
}

3.動態規划

動態規划解析摘自力扣精選答案):
狀態定義: 設動態規划列表 \(dp\)\(dp[i]\) 代表以元素 \(nums[i]\) 為結尾的連續子數組最大和。

  • 為何定義最大和 \(dp[i]\) 中必須包含元素 \(nums[i]\) :保證 \(dp[i]\) 遞推到 \(dp[i+1]\) 的正確性;如果不包含 \(nums[i]\) ,遞推時則不滿足題目的 連續子數組 要求。

轉移方程: 若 \(dp[i-1] ≤ 0\),說明 \(dp[i−1]\)\(dp[i]\) 產生負貢獻,即 \(dp[i-1] + nums[i]\) 還不如 \(nums[i]\) 本身大。

\(dp[i - 1] > 0\) 時:執行 \(dp[i] = dp[i-1] + nums[i]\)
\(dp[i - 1] ≤ 0\) 時:執行 \(dp[i] = nums[i]\)

初始狀態\(dp[0] = nums[0]\),即以 \(nums[0]\) 結尾的連續子數組最大和為 nums[0] 。

返回值: 返回 \(dp\) 列表中的最大值,代表全局最大值。

解題

public class DynamicSolution implements Solution {
    @Override
    public int[] maxSubArray(int[] nums) {
        // 准備一個數組來緩存結果
        // dp[i] 存儲 nums[0...i] 子序列的最大和且必須包含 nums[i]
        int[] dp = new int[nums.length];
        // 存儲子序列的起始位置
        int[] start = new int[nums.length];
        // 賦予初始值
        dp[0] = nums[0];
        start[0] = 0;
        for (int i = 1; i < dp.length; i++) {
            // 遞推公式
            if (dp[i - 1] < 0) { // 負貢獻,不參與累加
                dp[i] = nums[i];
                start[i] = i; // 起始位置重置
            } else {
                dp[i] = nums[i] + dp[i - 1];
                start[i] = start[i - 1]; // 延續起始位置
            }
        }

        int maxSum = dp[0];
        int low = 0;
        int high = 0;
        for (int i = 1; i < dp.length; i++) {
            if (dp[i] > maxSum) {
                maxSum = dp[i];
                low = start[i];
                high = i;
            }
        }
        return new int[]{maxSum, low + 1, high + 1};
    }
}

參考文檔:

  1. 動態規划入門

  2. 動態規划,區間最大和

  3. 五種求解最大連續子數組的算法

  4. 經典算法思想——分治(Divide-and-Conquer)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM