东哥算法课第二周习题——贪心+二分

目录

1.POJ1505 Copying Books

2.HDU1969 Pie

 3.HDU4004 The Frog's Game

 4.POJ3258 RiverHopscotch

5. POJ3104 Drying


1.POJ1505 Copying Books

原题链接:1505 -- Copying Bookshttp://poj.org/problem?id=1505

大意:

有n本不同页数的书要分给k个抄写员抄,每本书只能分配给一个抄写员,每个抄写员必须得到一系列连续的书。抄写所有书籍所需的时间由分配到最多工作的抄写员决定。因此,要最小化分配到最多页数的抄写员的页数。

输入输出要求:

如果有多个解决方案,则打印一个最小化分配给第一个抄写员的解决方案,然后再打印给第二个抄写员等。但每个抄写员必须至少分配一本书。

Sample Input

2
9 3
100 200 300 400 500 600 700 800 900
5 4
100 100 100 100 100

Sample Output

100 200 300 400 500 / 600 700 / 800 900
100 / 100 / 100 / 100 100

思路分析:

要求最大值中的最小值,就应该考虑使用二分法,按照输出要求,要使越前面的部分书籍书尽可能少,考虑使用贪心,尽量使得后面的部分能合并的话先合并。

二分的初始范围:使用单本书中最大页数作为左端点,所有页数的和作为右端点。这样做的好处:左端点取最大页数,可以避免有抄写员分配到0本书的可能,右端点则考虑到一个抄写员可能分配到的最多任务的情况。

二分函数的设计思路:利用二分求出答案,对于每次二分的答案,计算这个最大页数可以划分给几个抄写员,如果能划分的部分少了,说明范围取大了,需要减小,则r=mid;反之,说明这个页数太少了,需要增大。

计算可以划分为几部分:从后往前遍历数组,使用贪心的思想,每次尽可能使后面的页数能合并的先合并,partSum记录当前部分的页数,如果还没有超过最大值,就再往这个部分添加书,如果这一次添加使得这一部分的总页数超过了最大值,则说明需要在上一次停止划分。那么就当前这一本书作为新部分纳入的第一本书,将partSum的值更新为当前书的页数,并且cnt++,表示新分配出了一个部分。

输出前的处理:

1、在计算可以划分几个部分的时候,每当发现不能再添加书了,利用一个bool型数组记录当前位置,以便在输出时可以在这里打印“/”。

2、需要在每次计算可以划分几个步骤前,还原bool数组以免对下一次记录产生影响。

3、在得到最终的二分结果后,需要确认是否所有抄写员都分配到了工作,如果没有,从前往后遍历数组,在没有被记录可以打印“/”的位置都标记为true,知道所有“/”都已经用完。这样使得前面的部分书籍书尽可能小。

注意点:

1、在求sum时可能会爆long long。(每本书的页数最多为1e7,最多有500本书)。

2、记得还原bool数组。

3、二分得到结果后,检查是否划分出了足够的部分,如果不够,需要处理。

 AC代码如下:

//
// Created by LittleMelon on 2022/9/17.
//

//二分法的特点:非常适用于查找最大值中的最小值或找最小值中的最大值
//不是所有的二分题都需要排序,此题因为习惯先写一个sort导致debug了很久找不到问题。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=500+10;
int q[N];
bool p[N];//用来记录part划分的点。
int k,m;
int countPart(ll len){
    ll partSum=0;
    int cnt=1;
    for (int i = 0; i < N; ++i) {
        p[i]= false;//每次初始化,防止被上一次影响
    }
    for (int i = k-1; i >=0; --i) {//贪心思想:每次尽量使后面的可以合并,使得后面的部分更大
        partSum+=q[i];
        if (partSum>len){
            partSum=q[i];
            cnt++;
            p[i]= true;//标记划分的点,输出时在该位置后加斜杠
        }
    }
    return cnt;
}
ll binarySearch(ll l,ll r){
    ll mid;
    while (l<r){
        mid=l+r>>1;
        int part=countPart(mid);
        if (part<=m) {
            r=mid;
        }else{
            l=mid+1;
        }
    }
    return l;
}
int main(){
    int t;
    scanf("%d",&t);
    while (t--){
        scanf("%lld%lld",&k,&m);
        ll sum=0;//划分成部分时,页数最多的情形就是只有一个scriber来copy所有书,此时所有书的页数之和即为答案。用sum来作为二分的最大值。
        ll large=0;//由于每个scribe必须被分配至少一本书,因此划分的最小部分的页数,一定大于任何单本书的页数。用large作为二分的最小值。
        for (int i = 0; i < k; ++i) {
            scanf("%d",&q[i]);
            sum+=q[i];
            if (q[i]>large)
                large=q[i];
        }
        //二分求最大值中的最小值
        ll maxMinus=binarySearch(large,sum);
        int cnt= countPart(maxMinus);
        //如果用最大值中的最小值从后往前贪心分配后,还可以再分,就从前往后分配。
        for (int i = 0; i < k&&cnt<m; ++i) {
            if (!p[i]){
                p[i]= true;
                cnt++;
            }
        }
        printf("%d",q[0]);
        for(int i=1;i<k;i++)
        {
            if(p[i-1])
                printf(" /");
            printf(" %d",q[i]);
        }
        printf("\n");
    }
    return 0;
}

2.HDU1969 Pie

原题链接:

Problem - 1969http://acm.hdu.edu.cn/showproblem.php?pid=1969大意:

有n个馅饼,我和我的F个朋友需要分配到体积一样的馅饼,所有馅饼高度都是1,馅饼都应该大小相等(但不一定形状相同)。求每个人可以分配到的馅饼的最大体积。

输入输出要求:

Sample Input

3

3 3

4 3 3

1 24

5

10 5

1 4 2 3 4 5 6 5 4 2

Sample Output

25.1327

3.1416

50.2655

思路分析:

浮点数二分求结果就好。

注意点:

1、需要将给出的半径转换成面积再去计数,因为馅饼只需要大小相等,不需要形状相同。

2、PI的精度需要取得大一些,不然会WA,或许可以找到更好的表达PI的方式。

AC代码如下:

//
// Created by LittleMelon on 2022/9/18.
//
#include <iostream>
using namespace std;
const int N=1e4+10;
const double PI=3.1415926535898;//这个PI精度起初取小了
int r[N];
double pie[N];
int n,f;
//分配的体积为v时,可以分给几个人。
int countPie(double v){
    int cnt=0;
    for (int i = 0; i < n; ++i) {
        cnt+=(int)(pie[i]/v);
    }
    return cnt;
}
double binarySearch(double l,double r){
    double mid;
    while (r-l>1e-7){
        mid=(l+r)/2;
        if (countPie(mid)<f+1){//注意f+1,因为寿星本人也需要分配到pie
            r=mid;//如果可以分出的数量太少了,就将v变小,即r=mid
        } else{
            l=mid;
        }
    }
    return r;
}
int main(){
    int T;
    scanf("%d",&T);
    while (T--){
        scanf("%d%d",&n,&f);
        double max=0;
        for (int i = 0; i < n; ++i) {
            scanf("%d",&r[i]);
            pie[i]=r[i]*r[i]*PI;//因为分成的pie只需体积一致,底面积的形状没有要求。需要将题目给出的半径处理成面积再进行计算。
            if (pie[i]>max)
                max=pie[i];
        }
        double v=binarySearch(0,max);
        printf("%.4f\n",v);
    }
    return 0;
}

 3.HDU4004 The Frog's Game

原题链接:

Problem - 4004http://acm.hdu.edu.cn/showproblem.php?pid=4004大意:

青蛙要过河,河流宽度为L,河流中有n块石头,青蛙只能通过跳在石头上过河,并且限制最多跳m次,求最短的最长距离。

思路分析:

根据之前的经验,我们已经能很快从“求最长的最短距离”联想到二分法。贪心的思想体现在check()函数中,难点也在check()函数的设计中。

check函数:首先,用local表示初始位置,如果当前位置大于河流宽度表示到达对岸,则跳出循环。mid表示跳跃的最长距离,每次循环找到这一次可以跳跃到最远的石头,并且将跳跃次数cnt加1。

二分函数:如果需要跳的次数小于等于m,说明有更短的最长距离,则r=mid;如果大于m说明这个距离太短了,不足以跳到对岸,需要增长l=mid+1;初始左边界采用排序后石子之间的最大距离,以避免落入水中的情况,右边界采用河流的宽度。类似于第一题。

注意点:

1、从石头跳到对岸也算作一次跳跃,要将终点位置加入到数字中。

2、为了使得check函数在跳到对岸后不会继续向前跳跃。应当在对岸位置后面新加一个无穷大的位置。

AC代码:

//
// Created by LittleMelon on 2022/9/18.
//
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e6+10;
const int INF=0x3f3f3f3f;
int ll,n,m;
int stone[N];
//计算最大跳跃距离为mid时,最少需要跳几次。
int check(int mid){
    int local=0;
    int cnt=0;
    int i=0;
    while (local<ll) {
        local+=mid;
        while (stone[i]<=local){
            i++;
        }
        local=stone[i-1];
        cnt++;
    }
    return cnt;
}
int binarySearch(int l,int r){
    int mid;
    while(l<r){
        mid=l+r>>1;
        int cnt=check(mid);
        if (cnt<=m){
            r=mid;
        } else{
            l=mid+1;
        }
    }
    return l;
}
int main(){
    while(~scanf("%d%d%d",&ll,&n,&m)){
        for (int i = 0; i < n; ++i) {
            scanf("%d",&stone[i]);
        }
        stone[n]=ll;//由于跳到陆地上也算作一次跳跃,需要将最后的落地点也添加进数组。
        stone[n+1]=INF;//将终点后的距离设为无穷大,使得不会继续前进到终点后的位置。
        sort(stone,stone+n);
        int maxx=0;//不可以放做全局变量
        for (int i = 1; i <= n; ++i) {
            if (stone[i]-stone[i-1]>maxx){
                maxx=stone[i]-stone[i-1];
            }
        }
        int dis=binarySearch(maxx,ll);//将相邻石头间的最大距离作为二分的起始左边界,使得二分时不会必然落入河中的情况。河的总长度作为右边界。
        printf("%d\n",dis);
    }
    return 0;
}

 4.POJ3258 RiverHopscotch

原题链接:

3258 -- River Hopscotchhttp://poj.org/problem?id=3258大意:

奶牛要过河,河流宽度为L,河流中有n块石头,奶牛只能通过跳在石头上过河,可以拿掉m个石头,求最长的最短距离。

思路分析:

有了之前的经验,显然也是考虑贪心+二分。

check函数:len表示二分所得的最短距离,while循环遍历数组,如果目前的石头距离当前位置的距离大于最短距离,则可以跳在这块石头上,如果不行就要拿掉所以cnt++,cnt记录需要拿掉的石头数。

二分函数:如果需要拿掉的石头过多,说明最短距离过大了,r=mid-1;如果需要拿掉的石头小于等于m,说明可能有更大的最短距离,l=mid。因为是l=mid,又是int类型,在除以2时,会舍弃小数点后的内容,所以在求mid时,如果l只比r小1,那么此时会造成死循环,因此需要用l+r+1除以2来求mid。

注意点:

与上一题一样。

AC代码:

//
// Created by LittleMelon on 2022/9/19.
//
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;
const int INF=0x3f3f3f3f;
int rock[N];
int L,n,m;
int check(int len){
    int local=0;
    int i=0;
    int cnt=0;
    while (local<L) {
        if (rock[i]-local>=len){
            local=rock[i];
        } else{
            cnt++;
        }
        i++;
    }
    return cnt;
}
int binarySearch(){
    int l=0,r=L;
    int mid;
    while (l<r){
        mid=l+r+1>>1;
        if (check(mid)<=m){
            l=mid;
        } else{
            r=mid-1;
        }
    }
    return l;
}
int main(){
    scanf("%d%d%d",&L,&n,&m);
    for (int i = 0; i < n; ++i) {
        scanf("%d",&rock[i]);
    }
    rock[n]=L;
    rock[n+1]=INF;
    sort(rock,rock+n);
    cout<<binarySearch();
    return 0;
}

5. POJ3104 Drying

原题链接:

3104 -- Dryinghttp://poj.org/problem?id=3104大意:

Jane想在短时间内晒干n件衣服,衣服自然干燥时,每分钟可以减少1的水量,烘干机一次可以烘一件衣服,烘一分钟可以减少k的水量。求最少的干燥时间。

思路分析:

最开始想到的是用贪心和vector暴力解,每过一分钟就排序,让烘干机去烘水量最多的衣服,如果烘干了一件,就erase掉。但是很快就发现,erase掉的过程会影响排序和最后一件衣服,并不可行。

看了网上的思路,主要是找到一个计算公式,显然比暴力要巧妙。函数的来源是设当前衣服需要使用t分钟的烘干机,总时间是用二分求的mid,那么这件衣服的水量cloth[i]<=t*k+mid-t。衣服的烘干时间表示用烘干机的除水量t*k和自然风干的除水量mid-t;

注意点:

1、由于不到k的水量使用烘干机也算1分钟,使用ceil()函数向上取整。

2、n的范围是1e5,k的范围是1e9,最后的答案在l和r之间,一定不会爆longlong,但是求总时间的t,在加的过程中可能有数据会爆longlong

AC代码:

//
// Created by LittleMelon on 2022/9/19.
//
#include <iostream>
#include <math.h>

using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int cloth[N];
int n, k;

ll getTime(ll mid) {
    ll t = 0;
    for (int i = 0; i < n; ++i) {
        if (cloth[i] > mid) {
            t += ceil((cloth[i] - mid) * 1.0 / (k - 1));
        }
    }
    return t;
}

int main() {
    scanf("%d", &n);
    int max = 0;
    for (int i = 0; i < n; ++i) {
        scanf("%d", &cloth[i]);
        if (cloth[i] > max)
            max = cloth[i];
    }
    scanf("%d", &k);
    if (k <= 1) {
        printf("%d\n", max);
        return 0;
    }
    int l = 1, r = max;
    int mid;
    while (l < r) {
        mid = l + r >> 1;
        if (getTime(mid) <= mid) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    printf("%d\n", l);
    return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值