數組 (分段和) 的 (最大值) 最小問題


題目

  1. 題目:給定一個數組,和一個值k,數組分成k段。要求這k段子段和最大值最小。求出這個值。

  2. 題目分析:這道題目很經典,也很難,個人認為很難。文章中給出了三種算法:算法1,暴力搜索。本題暴力搜索算法並不是很明顯,可以使用遞歸實現暴力搜索。遞歸首先要有遞歸式:

\[M[n,k] = \mathop{min}\limits_{j=1}^{n} \left(max \{ M[j, k-1],\sum\limits_{i=j}^n{Ai}\}\right) \]

\(n\)表示數組長度,k表示數組分成幾段。初始化條件:

\[M[1,k] = A_0 \]

\[M[n,1] = \sum\limits_{i=0}^{n-1}{A_i} \]

很容易發現上述的遞歸算法擁有指數時間的復雜度,並且會重復計算一些M值。這類的算法一般可以使用動態規划進行優化。使用數組保存一些已經計算得到
的值,采用從低向上進行計算。這就是算法2。文章中還給出了第三種很牛的算法,我是想不到的。這就是使用二分查找應用到這個題目。大牛真是太牛了!!
下面是代碼:

#include <iostream>
#include <cassert>

using namespace std;
int sum(int A[], int from, int to) { 
    int total = 0; 
    for (int i = from; i <= to; i++) 
        total += A[i]; 
    return total; 
} 
//遞歸的暴力搜素算法
//指數時間的復雜度
int partition(int A[], int n, int k) { 
    if (k == 1) 
        return sum(A, 0, n-1); 
    if (n == 1) 
        return A[0]; 

    int best = INT_MAX; 
    for (int j = 1; j <= n; j++) 
        best = min(best, max(partition(A, j, k-1), sum(A, j, n-1))); 

    return best; 
}

//改進的動態規划算法
//時間復雜度:O(kN2)
//空間復雜度:O(kN) 
const int MAX_N = 100; 
int findMax(int A[], int n, int k) { 
    int M[MAX_N+1][MAX_N+1] = {0}; 
    int cum[MAX_N+1] = {0}; 
    for (int i = 1; i <= n; i++) 
        cum[i] = cum[i-1] + A[i-1]; 

    for (int i = 1; i <= n; i++) 
        M[i][1] = cum[i]; 
    for (int i = 1; i <= k; i++) 
        M[1][i] = A[0]; 

    for (int i = 2; i <= k; i++) { 
        for (int j = 2; j <= n; j++) { 
            int best = INT_MAX; 
            for (int p = 1; p <= j; p++) { 
                best = min(best, max(M[p][i-1], cum[j]-cum[p])); 
            } 
            M[j][i] = best; 
        } 
    } 
    return M[n][k]; 
}


int getMax(int A[], int n) { 
    int max = INT_MIN; 
    for (int i = 0; i < n; i++) { 
        if (A[i] > max) max = A[i]; 
    } 
    return max; 
} 

int getSum(int A[], int n) { 
    int total = 0; 
    for (int i = 0; i < n; i++) 
        total += A[i]; 
    return total; 
} 

int getRequiredPainters(int A[], int n, int maxLengthPerPainter) { 
    int total = 0, numPainters = 1; 
    for (int i = 0; i < n; i++) { 
        total += A[i]; 
        if (total > maxLengthPerPainter) { 
            total = A[i]; 
            numPainters++; 
        } 
    } 
    return numPainters; 
} 


//想不到的二分查找算法
//時間復雜度:O(N log ( ∑ Ai )).
//空間復雜度:0(1)
int BinarySearch(int A[], int n, int k) { 
    int lo = getMax(A, n); 
    int hi = getSum(A, n); 

    while (lo < hi) { 
        int mid = lo + (hi-lo)/2; 
        int requiredPainters = getRequiredPainters(A, n, mid); 
        if (requiredPainters <= k) 
            hi = mid; 
        else
            lo = mid+1; 
    } 
    return lo; 
}
int main()
{
    enum{length=9};
    int k=3;
    int a[length]={9,4,5,12,3,5,8,11,0};
    cout<<partition(a,length,k)<<endl;
    cout<<findMax(a,length,k)<<endl;
    cout<<BinarySearch(a,length,k)<<endl;
    return 0;
}

二分查找法分析
自己的分析:此題可以想象成把數據按順序裝入桶中,m即是給定的桶數,問桶的容量至少應該為多少才能恰好把這些數裝入k個桶中(按順序裝的)。

首先我們可以知道,桶的容量最少不會小於數組中的最大值,即桶容量的最小值(小於的話,這個數沒法裝進任何桶中),假設只需要一個桶,那么其容量應該是數組所有元素的和,即桶容量的最大值;其次,桶數量越多,需要的桶的容量就可以越少,即隨着桶容量的增加,需要的桶的數量非遞增的(二分查找就是利用這點);我們要求的就是在給定的桶數量m的時候,找最小的桶容量就可以把所有的數依次裝入k個桶中。在二分查找的過程中,對於當前的桶容量,我們可以計算出需要的最少桶數requiredPainters,如果需要的桶數量大於給定的桶數量k,說明桶容量太小了,只需在后面找對應的最小容量使需要的桶數恰好等於k;如果計算需要的桶數量小於等於k,說明桶容量可能大了(也可能正好是要找的最小桶容量),不管怎樣,該桶容量之后的桶容量肯定不用考慮了(肯定大於最小桶容量),這樣再次縮小查找的范圍,繼續循環直到終止,終止時,當前的桶容量既是最小的桶容量。

對於數組 1 2 3 4 5 6 7,假設k=3,最小桶容量為7(要5個桶),最大桶容量為28(一個桶)

單桶容量 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
桶數量 5 5 4 4 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2

第一行表示桶容量,第二行表示需要的桶數
即要求桶數量恰為k的最小桶容量;

因為桶數量增加時,桶容量肯定減小(可以想象把裝的最多的桶拆成兩個桶,那么裝的第二多的桶就變成了之后的桶容量),所以找對應k的最小容量;

也是因為如此,上面兩種方法(遞歸,DP)中,再求k個桶的最小容量時,也求了桶個數小於k時的最小桶容量,因為k個桶的最小容量肯定小於k-i時的最小容量,所以最后結果不會有影響。


免責聲明!

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



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