Dinner (二分+二分+贪心)//其实正解是倍增

3 篇文章 0 订阅
3 篇文章 0 订阅

Dinner

【问题描述】 清儿今天请好朋友们吃饭,一共N个人坐在坐在圆桌旁。 吃饭的第一步当然是点餐了。服务员拿来了M份菜单。第i个人阅读菜单并点出自己喜欢的菜需要花费时间T[i]。 当一个人点完菜之后,就会把菜单传到他右手边的第一个人。 M份菜单是同时发出的,每个菜单只能同时被一个人阅读。 清儿希望知道如何分发菜单,才能让点餐的总时间花费最少呢?

【输入格式】 输入文件名为dinner.in 输入第一行是N和M,表示人数和菜单数 输入第二行,N个数,表示每个人点餐所需要的时间。

【输出格式】 输出文件名为dinner.out 输出一个整数表示点餐花费的最小时间。

【样例一输入】 3 2 1 5 10 【样例一输出】 10 【样例二输入】 4 2 1 2 3 4 【样例二输出】5

HINT: 对于20%的数据,n<=100. 对于60%的数据,n<=10000. 对于100%的数据,n<=50000,T[i]<=600

分成m个组,使每个组的权值和最大的最小。

a[i] :表示第i个的权值(点餐花费时间)。

最小时间  -> 二分ans

check :暴力思想:枚举每个点为起点,贪心的放,能放到上个组就放,不能放新开组。//只能过10%。。。。

我们枚举起点,其实会有重复的。。开始位置只需枚举1,n, n-1, n-2....一直到a[1]+a[n] + a[n-1].. > ans.假设加到a[i]时>ans.

那么如果以i为起点,终点最多是n,不可能为1.而起点为i+1,i+2,,,n,1 这些情况 ,一定有一种情况是包含以i为起点,终点最多是n这段的。//只能过60%。

贪心的放,我们发现前缀和递增,我们就可以把每一段的左端点二分出来,也就是最后一个sum[i] - sum[右端点-1] <= ans的点找出来(sum 为前缀和)。

#include<bits/stdc++.h>
using namespace std;
int n, a[100005],ma ,sum, m, hi[100005];
void read(int &x)
{
	int f = 0; x = 0 ; char  c = getchar();
	while(c < '0' || c > '9')
	{
		if(c == '-') f = 1; c = getchar();
	}
	while(c >= '0' && c <= '9')
	{
		x = x * 10 + c - '0'; c = getchar();
	}
	if(f) x = -x;
}
bool check(int x)
{
  int su = 0,qi = 1 + n;
  while(su + a[qi] <= x)
	{	if(qi == 5)
	int haha =9;
		su += a[qi]; int   duan = qi-1, fen = 0;
		while(duan <  qi + n - 1)
		{
			int l = duan + 1,  r = qi + n - 1,ans = 0;
		   while(l <= r)
		   {
			 int mid = (l + r) /2;
			 if(hi[mid] - hi[duan] <= x)
			 {
				ans = mid; l = mid + 1;
			 }
			 else r = mid - 1;
		   }
		   duan = ans;
		   fen++;
		   if(fen > m) break;
		}
		if(duan == qi + n - 1 && fen <= m) return 1;
		qi--;
	}
	return 0;
}
int main()
{
	read(n); read(m);
	for(int i = 1; i <= n; i++){read(a[i]);a[i+n] = a[i];  ma = max(ma,a[i]); sum += a[i];} 
	for(int i = 1; i <= 2*n; i++) hi[i] = hi[i-1] + a[i];
	int l = ma, r = sum, ans = 0;
	while(l <= r)
	{
		int mid = (l + r) / 2;
		if(mid == 15)
		int hehe = 1;
		if(check(mid)){ r = mid - 1; ans = mid; }
		else l = mid + 1;
	}
	cout << ans;
	return 0;
}

再来一波正解。

 考虑二分答案,check本质上就是看圆环能否分成至多m段,并且每段的和小于等于当且check的答案。可以用st[i][j]表示从i开始跳2^j段(每段的长度不超过二分的答案)最远能跳到哪里。然后利用这个数组就可以O(logn)的时间去计算从i跳m段最远能跳到哪里,最后暴力看每个位置作为起点,能跳到哪里,只要能越过自己,就表明可以。总的时间复杂度O(nlog^2n)。
倍增预处理,二分check。

#include <cstdio>
#include <iostream>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003;
int n,m,M = 1,cnt = 0;
int a[2*N],l,r,maxna;
int ans = 2000000000;
int anc[2*N][30];
int st[3*N];
inline void prepare(int mid) {
    int sum = 0;
    int h = 0 ,t = 0;
    for(int i = 1 ; i <= n+n ; ++i) {
        st[++t] = i;
        if(sum + a[i] <= mid) sum += a[i];
        else {
            do {
                anc[st[++h]][0] = i;
                sum -= a[st[h]];
            }while(sum + a[i] > mid);
            sum += a[i];
        }
    }
    while(h<t) {
        h++;
        anc[st[h]][0] = 2*n+1;
    }
    for(int j = 0 ; j <= cnt ; ++j) anc[2*n+1][j] = 2*n+1;
    for(int j = 1 ; j <= cnt ; ++j) {
        for(int i = 1 ; i <= 2*n ; ++i) {
            anc[i][j] = anc[anc[i][j-1]][j-1];
        }
    }
}
inline bool check(int mid) {
    if(mid < maxna) return false;
    prepare(mid);
    for(int i = 1 ; i <= n ; ++i) {
        int k = m;
        int x = i;
        for(int j = cnt ; j >= 0 ; --j) {
            if((1<<j)<=k) {
                k -= (1<<j);
                x = anc[x][j];
            }
        }
        if(x>=i+n) return true;
    }
    return false;
}
int main() {
    freopen("dinner.in","r",stdin);
    freopen("dinner.out","w",stdout);
    scanf("%d%d",&n,&m);
    while(m>=M) {
        M<<=1;
        cnt++;
    }
    cnt--;
    for(int i = 1 ; i <= n ; ++i) scanf("%d",&a[i]),r+=a[i],maxna = max(maxna,a[i]),a[i+n] = a[i];
    do {
        int mid = (l+r)>>1;
        if(check(mid)) ans = min(ans,mid) , r = mid;
        else l = mid + 1;
    }while(l<r);
    if(check(l)) ans = min(ans,l);
    cout << ans << endl;
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值