当前位置:首页 > 题解目录 > 正文内容

【题解】切割钢管

亿万年的星光3年前 (2022-03-12)题解目录20535

【题目描述】

小A是某工地的计算工程师。工地现有 n 根钢管,第 i 根钢管的长度为 ai

现在想用这 n 根钢管来做一个支撑用的柱子。我么可以切割这些钢管成为更短的钢管,但是不能缝合两根钢管。为了安全起见,柱子必须用 至少 k 根长度相同的钢管加上混凝土制成,并且要求钢管长度必须为 整数。

小A想知道,这个柱子最高能建成多高(钢管可以有剩余)。

【输入描述】

输入第一行一个整数 n, k (1 \le n, k \le 10000) 。

接下来一行输入 n 个空格隔开的整数 l_i(1 \le l_i \le 10^8),表示每根钢管的长度。

【输出描述】

输出最大的高度。

【样例输入1】

2 4
8 4

【样例输出1】

2

【样例输入2】

8 8
12 3 14 12 14 20 4 8

【样例输出2】

7

【题目分析】

  • 题目的意思是切割最短的钢管的长度是多少,而不是切割完毕后,这些钢管加起来长多少。

  • 非常经典的二分法 (最大化平均值问题)。

我们可以假设按照最短的那个进行切割,比如上图,最短的钢管是4。那么一共可以切成(4/4)+(8/4)=3块。不满足题目要求。所以不行,要把切割的长度缩小。

关于缩小,你实际上可以用暴力穷举,按照4切割,切割出来的数量少了,就按照3切割,如果还是少了,就按照2切割。

那么实际上,我们一直在“找”合适的切割长度来切割这些钢管。如果按照暴力枚举一个个挨个试,时间肯定是比较长的。那么我们不妨用二分的方法试,

这样试起来比较快。

【思路及方法】

我们设立一个最小值和最大值,然后取中间值,用这个中间值来切割钢管,如果比K大,说明当前切割的方法可行,但是,不一定是最优的,因为题目要保证

切割下来的钢管尽量的高。所以继续切割,直到左端点和右端点出现重合的现象。

用这个方法解释上面的例子:

我们假设钢管长度最小是1,最大是100。(题目一般会给出)

第一次我们取中间值 (100+1)/2=50。我们按照50的长度对两根钢管进行切割(结论是0,不符合条件),更新右端点值为50;

我们再取51 (1+50)的中间值25,我们按照25的长度对两根钢管进行切割(结论是0,不符合条件),更新右端点值为25;

我们再取26 (1+25)的中间值13,我们按照13的长度对两根钢管进行切割(结论是0,不符合条件),更新右端点值为13;

我们再取14 (1+13)的中间值7,我们按照7的长度对两根钢管进行切割(结论是1,不符合条件),更新右端点值为7;

我们再取4(1+7)的中间值4,我们按照4的长度对两根钢管进行切割(结论是3,不符合条件),更新右端点值为4;

我们再取2 (1+4)的中间值2,我们按照2的长度对两根钢管进行切割(结论是6,符合条件)。


【参考答案】

#include <iostream>
using namespace std;
//想要分割出来的钢管高度越高,那么能分割出来的就越少
int a[10005]; //把每根钢管的长度存放起来
int main(){
    int n,k;
    cin >> n >> k;
    //最后输出的最大高度,这些钢管的根数一定是大等于k的
    for(int i=0;i<n;i++){
        cin >> a[i];
    }
    int l = 0 , r = 100000001; //设起始最大高度为r 右边界
    while(l < r){
        int mid = (l + r) >> 1;// 二分查找
		int cnt = 0; //记录当前钢管高度的总根数
        for(int i=0;i<n;i++){
            cnt += a[i] / mid;
            //每个钢管除以当前高度,就可以得到这个钢管可以分割出来的根数
        }
        if(cnt >= k){ 
            //如果说当前这个高度分割出来的根数大等于k
            //那么这个mid是可行的,我们就可以扩大左边界值
            l = mid + 1;
        }else{
            r = mid;
            //否则的话,这个mid就是太高了,就可以把右边界缩小
        }
    }
    cout << r - 1 << endl; 

    //这里输出l-1或者r-1都可以,因为最后结果出来l与r是相等的
    return 0;
}

【思考】

为什么二分要上面那样写?还有其他写法吗?建议多做几道题再回来看。

    while(l < r){
        int mid = (l + r) >> 1;
		int cnt = 0; 
        for(int i=0;i<n;i++){
            cnt += a[i] / mid;
        if(cnt >= k){ 
            l = mid + 1;
        }else{
            r = mid;
        }
    }
    cout << l-1 << endl; // 或r-1

多做几道题总结一下就会发现,这类题目的思想一样,只不过写法不一样。

基本原理是:我们让左个区间始终满足,右区间始终不满足(也可以反过来)。每次中间值步进一个值,不断试探。直到不满足题目条件为止。

比如上面这个题目,我们可以有这样两个考虑:

(1)如果有一个mid满足条件,将mid+1后不满足条件,那么mid就是最优解。(上面代码中L=mid+1。所以最终要输出L-1)

(2)如果有一个mid不满足,将mid-1后满足,那么mid-1就是最优解。(上面的代码R=mid,所以最终要输出R-1)


其实,上面的代码还有其他写法。最简单的其实就是移项操作。比如

while(L<R) 完全可以写成 while(L-R<0) 或者其他符合原题意的式子。

如果不考虑移项,我们可以考虑其他写法。比如:

 while(L+1 < R){
        int mid = (L + R) >> 1;
		int cnt = 0; 
        for(int i=0;i<n;i++){
            cnt += a[i] / mid;
        if(cnt >= k){ 
            L = mid;
        }else{
            R = mid;
        }
    }
    cout <<L<< endl; // 或R-1


这个也是完全成立的。我们换个思路,反推,根据代码思考过程。上面这段代码while循环不成立时,L+1>=R。我们可以这么认为:“是因为L+1导致不符合题意,所以答案要输出L或者R-1”。


当这类题目做的多了,自然就会发现规律,循环条件,左右区间端点值的取法是配套的。基本原理就是上面说到,让左区间满足,右区间不满足,然后每次左区间加1,不断试探,直到不满足条件(当然,完全可以反过来考虑)。


扫描二维码推送至手机访问。

版权声明:本文由青少年编程知识记录发布,如需转载请注明出处。

分享给朋友:

相关文章

【题解】背包问题3

【题目描述】完全背包定义有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是c,价值是w。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。本题要求是背...

【题解】开关灯(1)

【题目描述】假设有N盏灯(N为不大于5000的正整数),从1到N按顺序依次编号,初始时全部处于开启状态;有M个人(M为不大于N的正整数)也从1到M依次编号。第一个人(1号)将灯全部关闭,第二个人(2号...

【题解】母牛的故事

【题解】母牛的故事

【题目描述】有一头母牛,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。请编程实现在第n年的时候,共有多少头母牛?【输入描述】输入数据由多个测试实例组成,每个测试实例占一行...

【题解】切比雪夫距离

【题目描述】小C有一个平面!它发现了平面上的两个点,请你求出求它们之间的切比雪夫距离。切比雪夫距离定义为x与y方向坐标差的绝对值较大值。【输入描述】四个整数,a,b,c,d。坐标为(a,b)与(c,d...

【题解】最大子矩阵

【题目描述】已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1 × 1)子矩阵。比如,如下4 × 4的矩阵0  -2 -7&nb...

【题解】求车速问题

【题目描述】一辆以固定速度行驶的汽车,司机在上午10点看到里程表上的读数是一个对称数(即这个数从左向右读和从右向左读是完全一样的),为95859。两小时后里程表上出现了一个新的对称数。问该车的速度是多...