数列分段(二分答案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;
}

猜你喜欢

转载自blog.csdn.net/qq_44643644/article/details/105930358