CODEVS 3162 3163 3168 抄书问题1 2 3

本文介绍了一种通过二分查找和动态规划解决图书最优分配问题的方法,旨在减少图书复制时间,适用于不同规模的数据集。

题目描述 Description
现在要把M本有顺序的书分给K个人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本数给同一个人抄写。现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。

输入描述 Input Description
第一行两个整数M、K;

第二行M个整数,第i个整数表示第i本书的页数。

输出描述 Output Description
共K行,每行两个正整数,第i行表示第i个人抄写的书的起始编号和终止编号。K行的起始编号应该从小到大排列,如果有多解,则尽可能让前面的人少抄写。

样例输入 Sample Input

12 10
384 887 778 916 794 336 387 493 650 422 363 28

样例输出 Sample Output

1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 12

数据范围及提示 Data Size & Hint
抄1(K<=M<=100),抄2(K<=1000,<=M<=10000) 抄3(K<=10000,0<=M<=1000000)

第一题可以无脑水,第二题可以DP,第三题的话只能用二分。
这个题二分的部分其实不难,关键在输出。(本蒟蒻交的不带优化的代码,抄1AC,抄2 90, 抄3 80)
1、每一个人的抄写速度都一样
2、现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。
结合这两句,可以把第二句改成”现在请你设计一种方案,使得抄写页数最多的人抄的页数尽量少“
在判断函数中,对于每一个mid,在for的时候开一个sum=mid,开一个tot记录用了多少人。当sum>=bk[i](第i本书的页数),sum-=bk[i]。当sum不满足条件时,tot++,让sum=mid,
如果tot<=k时,说明当前mid减小(当tot==k时,mid的值加大不会有更优解,所以需要),如果tot>k时,说明当前mid需要增大,这样,开一个haha记录二分出来的抄写页数最多的人抄的页数的最小值,接下来就是神奇的输出了。
方式1:
如果有多解,则尽可能让前面的人少抄写。
对此,我们可以倒着跑一遍,一边跑一边记录区间的值(在我的代码中,我用数组nx1表示区间左边界,数组nx表示区间右边界),同时,开个tot记录区间的大小。
当然,这样可能会出问题。比如样例:
这里写图片描述
很明显,我们这里的输出是9行,tot=9,而样例里面是k=10,这是错误的。
那怎么办呢?
我们可以加一点小小的特技duang,把两个数组中的大于等于二的区间拆的尽量小

    if(tot < k)//
    {
        int tmp = k;
        for(int i = tot;tot < k;i --,tmp--)
        {
            nx[tmp] = nx[i];//将原数组的元素扔到后面 
            nx1[tmp] = nx1[i];
            if(nx[tmp] - nx1[tmp] > 0)
            {
                nx[tmp - 1] = nx[tmp];//将此区间右端点左移,成新区间右端点 
                nx[tmp] = nx1[tmp];//用原区间左端点覆盖原区间右端点(拆成了形如[x,x]的区间) 
                nx1[tmp - 1] = nx1[tmp] + 1;//此原左端点+1左移,成新区间左端点。 
                tmp --;//当前的数组tmp和tmp-1都是形如[x,x]的区间 
                tot ++;//如果到tot==k说明数组已经补全。 
            }
        }
    }//关键操作:把前面的大于等于二的区间尽可能拆小区间 

请对照下图自行体会
一开始:
这里写图片描述
tmp==4时:
这里写图片描述
tmp==4时替换:
这里写图片描述
tmp==4时替换完成&&本样例完成图:
这里写图片描述
比较考验思维能力吧….代码很清晰Orz xczhw。
方式2:

void print(int x)
{
    int t=n,i,j,s;
    for(i=k;i>=1;i--)
    {
        yi[i]=t;//较大区间 
        j=t;//某指针 
        s=0;
        while(s+bk[j]<=x&&j>=i)// j>=i的意思是:虽然 i 你能抄这么多书  
        //但是我们剩下 i-1 个人的书全叫你抄了 我们抄什么?  
        {
                s+=bk[j];//“输出答案时,如果剩余书本数小于等于剩余人数,
                //要直接跳到下一个人,之后每人只需抄写一本书即可。”
                j--;
        }
        xi[i]=j+1;
        t=j;
    }
    for(i=1;i<=k;i++)
    {
        printf("%d %d\n",xi[i],yi[i]);
    }
}

举个例子来说,就是第i个人从后面往前抄书最多抄到第i本,此时,剩i-1个人和i-1本书,至少一人一本,如果第i个人抄到了i-1本,则后面的i-1个人分i-2本书的话没法分。
代码很好理解。

依我看来,方式一比较高端,方式二比较简洁。
全代码如下:

#include<cstdio>
#include<iostream>
#include<algorithm> 
using namespace std;
const int maxn=1000000;
int n,k;
int bk[maxn];
int nx[maxn];
int nx1[maxn];
bool can(int x)
{
    int tot=0;

    for(int i=1;i<=n;)
    {
        int sum=x;
        tot++;
        while(sum>=bk[i]&&i<=n)
        {
            sum-=bk[i];
            i++;
        }
    } 
    if(tot<=k)
        return false;
    else
        return true;
}
int divv(int l,int r)
{
    int ans=0x7fffffff;
    while(l<=r)
    {
        int mid=(l+r)>>1;
        if(can(mid))
        {
            l=mid+1;
        }
        else
        {
            r=mid-1;
            ans=min(ans,mid);
        }
    //      printf("l:%d r:%d mid:%d\n",l,r,mid);
    }   
    return ans;
}
int xi[1000030],yi[1000030];
void print(int x)
{
    int t=n,i,j,s;
    for(i=k;i>=1;i--)
    {
        yi[i]=t;//较大区间 
        j=t;//某指针 
        s=0;
        while(s+bk[j]<=x&&j>=i)// j>=i的意思是:虽然 i 你能抄这么多书  
        //但是我们剩下 i-1 个人的书全叫你抄了 我们抄什么?  
        {
                s+=bk[j];//“输出答案时,如果剩余书本数小于等于剩余人数,
                //要直接跳到下一个人,之后每人只需抄写一本书即可。”
                j--;
        }
        xi[i]=j+1;
        t=j;
    }
    for(i=1;i<=k;i++)
    {
        printf("%d %d\n",xi[i],yi[i]);
    }
}
int haha; 
void printxczhw()
{
    int tot=0;
    for(int i=n;i>=1;)
    {
        int sum=haha;
        nx[++tot]=i;
        while(sum>=bk[i]&&i>=1)
        {
            sum-=bk[i];
            i--;
        }
        nx1[tot]=i+1;
    }
    if(tot < k)//
    {
        int tmp = k;
        for(int i = tot;tot < k;i --,tmp--)
        {
            nx[tmp] = nx[i];//将原数组的元素扔到后面 
            nx1[tmp] = nx1[i];
            if(nx[tmp] - nx1[tmp] > 0)
            {
                nx[tmp - 1] = nx[tmp];//将此区间右端点左移,成新区间右端点 
                nx[tmp] = nx1[tmp];//用原区间左端点覆盖原区间右端点(拆成了形如[x,x]的区间) 
                nx1[tmp - 1] = nx1[tmp] + 1;//此原左端点+1左移,成新区间左端点。 
                tmp --;//当前的数组tmp和tmp-1都是形如[x,x]的区间 
                tot ++;//如果到tot==k说明数组已经补全。 
            }
        }
    }//关键操作:把前面的大于等于二的区间尽可能拆小区间 
    for(int i=k;i>=1;i--)
    {
        printf("%d %d\n",nx1[i],nx[i]);
    }
}
int main()
{
    scanf("%d%d",&n,&k);
    int tt=0;
    int minn=0;
    for(int i=1;i<=n;i++)
        scanf("%d",&bk[i]),minn=max(minn,bk[i]),tt+=bk[i];
    haha=divv(minn,tt);
    printxczhw();
//  print(haha);
    return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值