數列分段(二分答案or二分搜索)

Description
對於給定的一個長度爲N的正整數數列A[1…N],現要將其分成M(M≤N)段,並要求每段連續,且每段和的最大值最小。

關於最大值最小:

例如一數列4 2 4 5 1要分成3段。

將其如下分段:[4 2] [4 5] [1]

第一段和爲6,第2段和爲9,第3段和爲1,和最大值爲9。

將其如下分段:[4] [2 4] [5 1]

第一段和爲4,第2段和爲6,第3段和爲6,和最大值爲6。

並且無論如何分段,最大值不會小於6。

所以可以得到要將數列4 2 4 5 1要分成3段,每段和的最大值最小爲6。
Input
第 1 行包含兩個正整數 N,M。

第 2 行包含 N 個空格隔開的非負整數 Ai​,含義如題目所述。
Output
一個正整數,即每段和最大值最小爲多少。
Sample Input 1
5 3
4 2 4 5 1
Sample Output 1
6
Hint
對於 100% 的數據,1≤ N≤ 100,000,M≤N,A[i]≤ 10^8

答案不超過10^9
Time Limit
1000MS
Memory Limit
256MB

分析:
初看這道題有個誤區,會慣性思維去想,數列分段和的最大值依賴於數列的分段的方法,必須先知道數列如何分段才能計算出所有分段和的最大值,如果這樣去想,此題就複雜了。
實際上,數列分段和的最大值可以決定這個數列至少能分成幾段,當每次分段都分到極限(再往下多加一個元素就會超過分段和最大值),這樣所得的分段數就是最小值。想明白了這一點,這道題就有突破口了。
我們可以對數列分段和的最大值二分再進行枚舉,二分的邏輯是:如果當前最大值下的最小分段數比欲分段數還大,就說明最大值太小了,可行答案應該更大;如果當前最大值的最小分段數不大於欲分段數,就說明這是一個可行答案,並且可以再試探更小的答案。
答案區間端點的選取理由在代碼註釋裏。另外,此題讓我明白二分寫對的關鍵,除去區間端點更新和退出循環的條件那些老生常談云云,要想找到(不遺漏)答案,必須明確什麼情況下的解是可行解,並及時記錄答案。這道題裏,我本來只在temp==M的時候才記錄答案,導致忽略了相當一部分可行解,最終沒找到正確答案,oj評判結果爲PA。後來發現其實temp<M的時候的答案也是可行解,也應該記錄,在修改記錄答案的邏輯後,果然AC了。

參考代碼:

#include<stdio.h>
#include<algorithm>

long long int N,M;//數列長度,分段數
long long int a[100000];//數列
long long int *max,sum=0,ans=0;//數列最大值,數列和,答案
//計算分段和不超過x時,最少能分幾段
long long int cnt(long long int x)
{
    long long int sum=0,count=0;
    for(long long int i=0;i<N;i++)
    {   //如果加上a[i]沒超過x,a[i]就仍屬於當前分段
        //否則a[i]作爲新分段的開頭,並及時更新分段數
        sum+a[i]<=x?sum+=a[i]:(sum=a[i],++count);
        //注意":"後的括號不能去,否則該行相當於
        //(sum+a[i]<=x?sum+=a[i]:sum=a[i]),++count;
        //每每執行","前的表達式,都會繼續執行其後的表達式
    }
    //始終會少計一個分段,補回來
    ++count;
    return count;
}

int main()
{
    scanf("%lld%lld",&N,&M);
    for(long long int i=0;i<N;i++)
    {
        scanf("%lld",a+i);
        //順便把數列和sum算了
        //因爲要找最小答案,所以給ans賦最大值
        ans=sum+=a[i];
    }
    max=std::max_element(a,a+N);//找數列最大值
    //左開右開寫法,答案區間爲[*max,sum]
    //爲了保證值爲*max的元素能被分段,答案區間最小值是*max
    //爲了保證數列至少能被分成一段,答案區間最大值是sum
    //temp記錄數列分段和不超過mid的情況下最小分段數
    long long int left=*max-1,right=sum+1,mid,temp;
    while(left+1!=right)
    {
        mid=left+((right-left)>>1);
        temp=cnt(mid);
        //mid長度下最小分段數比M大,此時無法滿足分成M段
        //必然不是答案,且答案應在右區間
        if(temp>M) left=mid;
        //mid長度下最小分段數不大於M,是可行答案
        //因爲要找最小答案,故mid和之前搜索的答案ans相比較
        //判斷mid是不是更優解
        else if(temp<=M){
            ans>mid?ans=right=mid:right=mid;
        }
    }
    printf("%lld",ans);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章