牛客练习赛51 E 数列 (二分 + 贪心 + 思维)

大致题意

牛客练习赛51 E 数列 (二分 + 贪心 + 思维)_第1张图片

思路

关键在于贪心的证明比较有启发性。
官方题解:

链接:https://ac.nowcoder.com/discuss/248083?type=101

对于一个数列,如果其一个子数列a[i],a[i+1]…a[j-1],a[j]满足a[k]=a[k-1]+1(i+1<=k<=j),同时,也满足i-1<1或者a[i]!=a[i-1]+1,j+1>n或者a[j+1]!=a[j]+1,则我们称这样的子数列为一段。我们可以发现,假如数列中有sum个这样的段,则小乔对数列的喜爱度为n-sum。因为数列是可以自由填数的,所以对与每一段我们使得a[i]=1是最优的,a[i],a[i+1]…a[j-1],a[j]显然。于是对于每一段,如果这段的长度为s,按最优的方法填数,则这一段的总和为s*(s+1)/2。
于是问题转换为:
把数列n分成sum个段,要求最小化sum,并且所有段的总和<=m。
设这sum段中长度最小的段长度为mn,长度最大的段长度为mx,可以发现如果我们已知了sum,那么要使这sum段的总和最小,就要使得mn和mx尽可能的接近,因为如果mx-mn>=2,那么段mx的最后一个元素的值为mx,段mn最后一个元素的值为mn,那么我们使段mx的长度-1,mn的长度加1。那么总和的变化就是总和=总和-mx+mn+1。因为mx-mn>=2,所以mx>mn+1。
因此如果得知sum,最优的策略就是均分所有段的长度。
均分的方法就是,首先使每一段的长度为n/sum,剩余n%sum个匀速均分配到n%sum个段上去。
于是有n%sum个段的长度为n/sum+1,n-n%sum个段的长度为n/sum。
对于sum,我们二分答案,因为答案具有单调性,分段越多,可以使总和越小。
通过二分答案求出sum再构造数列即可。时间复杂度O(logn+n)

代码

#include
using namespace std;
#define maxn 405
#define maxm 1000006
#define ll long long int
#define INF 0x3f3f3f3f
#define inc(i,l,r) for(int i=l;i<=r;i++)
#define dec(i,r,l) for(int i=r;i>=l;i--)
#define mem(a) memset(a,0,sizeof(a))
#define sqr(x) (x*x)
#define inf (ll)2e18+1
int read(){
    int x=0,f=1;char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
     while(isdigit(ch))x=x*10+ch-'0',ch=getchar();
     return f*x;
}
int n,m;
bool pd(int x){
    int len=n/x;
    int last=n%x;
    ll ss=1ll*len*(len+1)/2;
    ll ee=ss+len+1;
    return 1ll*(x-last)*ss+1ll*last*ee <= m;
}
int main()
{
    n=read();m=read();
    int l=1,r=n,ans=1;
    while(l<=r){
        int mid=(l+r)>>1;
        if(pd(mid)){
            ans=mid;
            r=mid-1;
        }
        else l=mid+1;
    }
    //printf("ans = %d\n",ans);
    int p=n%ans;
    int len=n/ans;
    inc(i,1,p)inc(j,1,len+1)printf("%d ",j);
    inc(i,1,ans-p)inc(j,1,len)printf("%d ",j);
    printf("\n");
    return 0;
}

你可能感兴趣的:(思维,二分答案,其他算法思想-贪心)