题目:
传送带上依次送来了重量分别为:Wi(i=0,1,2,3.......,n-1)的n个货物。现在要将这些货物装到k辆卡车上。每辆卡车可装载的货物数大于等于0,但货物的重量总和不得超过卡车的最大运载量P.所有卡车的最大最大运载量一致。
请编写一个程序,输入n,k,Wi,求出装载全部货物所需的最大运载量P的最小值。
输入:第一行输入n和整数k,用空格隔开。接下来n行输入n个整数Wi,每个数占一行。
输出:输出P的最小值,占一行。
限制:1<=n<=10000
1<=k<=10000
1<=Wi<=10000
输入示例: 输出示例:
5 3 10
8
1
7
3
9
示例解释:第一辆卡车装载两个货物:{8,1},第二辆卡车装两个货物:{7,3},第三辆卡车装一个货物{9},因此最大运载量的最小值为10.
分析:
首先想要解答这道题,最关键的点是题目中的首句:"传送带依次送来......",这句话是非常关键的,它告诉我们货物是按照传送传送货物的顺序依次将货物装载到卡车上,并不是货物的任意组合装载到卡车上。
其实这样想的话,题目反而简单了。我们就想象一个场景,多辆卡车在传送带钱等着装载货物,货物不断被装载到卡车上,直到卡车不能再继续装载下一个传送带送来的货物,也就是说即将达到卡车的最大装载量P,这时的话,该量卡车就退出装载,让下一辆卡车继续装载。
这样的话,我们就可以有着一下的思路:
我们首先假定每辆卡车的最大装载量P已知,在这种情况下,我们按照上述情形进行装载货物,我们最后记录下这k辆车最多能装载的货物数量N,如果说N=n,说明传送带送来的n个货物能被装载,也就是是说此时的装载量P是满足条件的;而当N<n时,自然就是n个货物没有完全装完,这时的最大装载量P肯定是不符合的。我们可以定义一个函数,以最大装载量P为参数,并返回能装载的最大货物数量N.
有了上述的能间接判断当前的最大装载量P是否符合要求的函数,这样的话,我们只需要将P从0不断自增,这样找打的第一个满足条件的P就是我们最终的答案。但是这样的话,光查找P的复杂度就有O(P),我们可以考虑使用之间介绍过的查找复杂度更低的二分查找---二分查找,这样便有了以下的完整源代码:
#include<iostream>
#include<cstdlib>
#define Max 10000
using namespace std;
int n,k;
long long T[Max];
int check(long long P)
{
int i=0;
for(int j=0;j<k;j++)
{
long long s=0;
while(s+T[i]<=P) //在未超载的情况下,不断装载货物
{
s+=T[i];
i++;
if(i==n) return n; //已经装满了n个货物
}
}
return i; //在货物未装完的情况下,直接返回当前已经装载的货物数量i
}
int solve()
{
long long left=0;
long long right=Max*Max;
long long mid;
while(right>left+1)
{
mid=(right+left)/2;
int v=check(mid); //利用check函数判断在最大装载量为P的情况下最多能装载的货物数量
if(v>=n) right=mid;
else left=mid;
}
return right;
}
int main()
{
cin>>n>>k;
for(int i=0;i<n;i++)
{
cin>>T[i];
}
long long ans=solve();
cout<<ans<<endl;
return 0;
}
本题在搞懂了第一句后,不算是一个难题。但是至于给出的源代码我想讲以下两点:
1.对于check函数,其时间复杂度其实是O(n),并不能根据循环的层数来推断出其复杂度为O(n^2),这是容易产生误解的。其实我们想象一开始我们对于时间复杂度的概念是什么?简单来说,它是一段程序中执行次数最多的语句与平均每条语句执行时间的乘积的一种渐进意义下的趋势,通常我们就用执行最多的语句次数作为时间复杂度。那么我们看看check中执行最多的基本语句是什么,应该是最内层while循环的语句,我们就以i++作为标准。大家可以想象之前建立的那个装货场景,对于n个货物,不断装载,装满则装入下一辆卡车,那么装载的最终停止是由什么决定的呢?是n个货物装载完或者n辆卡车被装满,其分别对应check程序中的return n和return i,我们再进一步想象,装载的停止动作其实和卡车的数量是没有太大关系的,假设我们是装载员,我们不断将货物装到卡车上,我们不管装的卡车是哪一辆,只要在没装满的情况下不断装货物就行.这种情况下,我们就可以得出check的复杂度是O(n).(其实这只能自己意会不可言传,我的嘴也比较笨,只能把我能想到的尽量用最明了的语句表达出来。如果大家实在理解不了,这个注意点也可以跳过去,不是太重要)
2.对于solve函数,之前我接触到的二分查找,都是查找指定的元素在数组中的位置,找不到就返回一个标志,找到则返回元素下标,但是这里我们返回的是满足大于等于n的第一个元素,关键在于solve没有并不是返回元素值等于查找值时的元素下标,从判断条件中if(v>=n) right=mid,可以看出right与mid始终是保持一种大于等于的关系的,大家可能会说这里哪来的数组,其实这里数组元素就是0~10000*10000,因为下标和元素值一样,就直接用下标代表元素值了。不懂的话大家还可以看一以下的代码:
#include<iostream>
#include<cstdlib>
#define Max 10
using namespace std;
int n;
//这种二分查找,并不是返回找到的元素,而是根据判断条件返回指定范围内的最后一个或者第一个元素
int T[Max];
int solve()
{
int left=0;
int right=Max;
long long mid;
while(right>left+1)
{
mid=(right+left)/2;
cout<<"T["<<left<<"]="<<T[left]<<" "<<"T["<<mid<<"]="<<T[mid]<<" "<<"T["<<right<<"]="<<T[right]<<endl;
if(T[mid]>=n) right=mid;
else left=mid;
} //前提:数组已经从小到大排序
return right;
}
int main()
{
for(int i=0;i<Max;i++)
{
T[i]=i*3;
}
cin>>n;
int index=solve();
cout<<index<<" "<<T[index]<<endl;
return 0;
}
上面的代码就是在0,3,6,9,12,15,18,21,24,27中进行二分查找,返回第一个大于等于输入的n的元素,不同于之前的精确查找元素的二分查找,这种二分查找的特点就在于其判断条件是T[mid]>=n,然后不断进行循环缩小right和left构成的范围,最终进行返回,其实针对这种二分查找,我也针对上述代码稍微进行了走总结:
1.1:在 T[mid]>=n情况下,return right 返回第一个大于等于n的元素的下标
1.2:在 T[mid]>=n情况下,return left 返回最后一个小于n的元素的下标
2.1:在 T[mid]>n情况下,return right 返回第一个大于n的元素的下标
2.2:在 T[mid]>n情况下,return left 返回最后一个小于等于n的元素的下标
3.所有获得对应的下标如果超出下标范围,那么说明没有找到 ,比如在这里 T[mid]>=n时return right,当n=30时,这里会返回10,而数组的下标范围是0~9,超出范围,说明找不到第一个大于n=30的值,因为最大的值是27
至于具体的循环过程中各种下标的变化情况,大家可以直接复制我上述的源代码进行查看~
(当然,这里不会出现T[mid]<=n或者T[mid]<n的情况,因为这里数组0,3,6,9.......是按照升序排列的,肯定要满足二分查找的最基本要求,要保证n在不断缩小的另一半范围中。)
PS:本篇博文中,好多地方说了不少废话,只是想尽量我的笨嘴表达的更清楚点,如果想交流的直接评论或者私信我就行~