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;
}