NC631 牛牛的冰激凌

题目描述

大意:公司让你负责m个冰激凌的运输。运输车的冷库只够装n个冰激凌,一次运输需要t分钟,返回也需要t分钟。每个冰激凌制作好有一个时间。求最短运输完所有冰激凌的时间,以及在时间最短的情况下最少运输次数。
(这题真的不知道怎么写个暴力了,如果写个暴力,不失正确性,发现不就是记忆化搜索???递推的顺序也不难发现,跟dp大同小意,所以此题我写暴力方法了)

算法一:动态规划

思路引入

  • (此处默认已经将c数组排序过了)
  • 一个很直观的思路,如果我当前有n个冰淇淋,并且我人已经在工厂了,我肯定是立马送出
  • 因为如果我在原地还等待,就算制作出了n+1个冰淇淋,我也依旧不能运输出去
  • 因为冰淇淋没有差别,如果第i个冰淇淋和第i+1个都生产出来了,所以我们没必要选择第i+1个冰淇淋运出去,而不运第i个
  • 所以,基于这种思想,我们可以这样定义dp的状态

dp[i]代表运输完第i个冰淇淋并且回到工厂所需要的最短时间

  • 注意此处还需要回到工厂,也就代表第m-1个也回去了,实际上他不需要回去,那么我们最后要的答案其实是 d p [ m − 1 ] − t dp[m-1]-t dp[m1]t

状态转移

  • 因为我们一次可以运送的冰淇淋的数量为 [ 1 , n ] [1,n] [1,n]

  • 所以那么对于 d p [ i ] dp[i] dp[i],我们已经知道了i的前面n-1个冰淇淋的状态。

  • d p [ j ] , j ∈ [ i − n , i − 1 ] , i ∈ [ n , m − 1 ] dp[j],j \in [i-n,i-1],i\in [n,m-1] dp[j],j[in,i1],i[n,m1]的时间基础上面,在送出 [ j + 1 , i ] [j+1,i] [j+1,i]这个区间所有的冰淇淋
    *

  • 这边的 i > = n i>=n i>=n可能有同学没看懂,因为要保证包括第i个冰淇淋的情况,前面还有 n − 1 n-1 n1个冰淇淋,并且已经送完了第j个冰淇淋的情况,也就是说至少得有 n + 1 n+1 n+1个冰淇淋,但是下标是从0开始的,所以我们此处为 i ≥ n i\geq n in

  • (注意:上面对于的是 i ≥ n i\geq n in的情况,所以我们还需要考虑 i < n i<n i<n的情况)

  • 显然我们还需要考虑的一种情况,就是一开始什么都不送,等到第 j j j个冰淇淋做好然后送出去,其中 j ∈ [ 0 , n − 1 ] j\in[0,n-1] j[0,n1]
    在这里插入图片描述

  • 然后得到了j到i的转移方程
    d p [ j ] , j ∈ [ m a x ( i − n + 1 , 0 ) , i − 1 ] dp[j],j \in [max(i-n+1,0),i-1] dp[j],j[max(in+1,0),i1],(取max是因为下标要合法)

  • 接下来我们还要求总的趟数,我们只需要在dp的时候记录该状态是由哪一个转移过来的,倒序追踪的时候计数

  • 借用pre[]数组来记录前驱,初始化为-1,如果为-1则代表它的前驱是在一开始原地等待然后一次性送出

代码实现

#define pb push_back
#define inf 0x3f3f3f3f
class Solution {
public:
    /**
     * 两个数表示答案
     * @param n int整型 一次运输的冰激凌数量
     * @param m int整型 总冰激凌数
     * @param t int整型 一次运输的时间
     * @param c int整型一维数组 表示每个冰激凌制作好时间<1e4
     * @param cLen int c数组长度
     * @return int整型vector
     */
    vector<int> icecream(int n, int m, int t, int* c, int len) {
    	vector<int> ans;//记录答案 
    	vector<int> dp(m,inf);//dp[i]代表运输完第i个物品并且回到工厂所需要的最短时间
		//所以我们求解的也就是dp[m-1]-t,就是最后一趟不需要回到工厂 
    	vector<int> pre(m,-1);//记录前驱 
    	sort(c,c+m);//排序c数组 
    	for(int i=0;i<len;i++){
    		if(i<n){//因为[0,n-1]的冰激淋可能没有前驱 
    			dp[i]=min(dp[i],c[i]+2*t);//做完[0,i]个冰激淋统一一趟送过去 
			}
			int l=max(i-n,0);//最多可能由前n个状态转移过来 
    		for(int j=l;j<i;j++){
    			int tmp=max(dp[j],c[i])+2*t;//[j+1,i]之间的冰激淋全部都由这一趟运送过去 
    			//第j个冰激淋运输 
    			if(dp[i]>tmp){//假如比当前的dp[i]更优,则更新答案 
    				dp[i]=tmp;
    				pre[i]=j;//记录前驱
					//此处前驱用于计算运输次数 
					//也可以用于追踪最优方案, 
				}
			}
		}
		int cnt=0,now=m-1;//cnt用于记录最少运输次数
		//now代表当前是在第几个冰激凌制作好后去运输的 
		while(~now){//当前now合法,即now不等于-1,因为-1的反码是0所以可以这样写 
			cnt++;//去的趟数加一 
			now=pre[now];//追踪前驱 
		}
		ans.pb(dp[len-1]-t);//最后一趟不需要返回到工厂 
		ans.pb(cnt);// 
		return ans;
    }
};

复杂度分析

  • 时间复杂度,排序为 O ( m log ⁡ m ) O(m \log m) O(mlogm),动态规划两层循环为 O ( m n ) O(mn) O(mn),理论上应该是 O ( m n + m log ⁡ m ) O(mn+m \log m) O(mn+mlogm),但是数据中 n n n log ⁡ m \log m logm的阶大,所以总的时间复杂度为 O ( m n ) O(mn) O(mn)
  • 空间复杂度,定义了动态数组ans,dp,pre,为 O ( m ) O(m) O(m)

算法二:贪心

引入

  • 其实这题真的很难看出来是贪心,因为确实很难证明贪心(网上能找到的博客我瞅了一眼,发现没人证明……)
  • 不过我这边尽量不用抽象的纯数学来证明,我尽量讲的通俗易懂一点
  • 以下有好几个需要证明的,我尽量递进得来讲

证明:运送数量贪心,m<=n

  • 上面的动态规划中,我已经讲过了,能如果当前有n个冰淇淋可以运出去,那么等下一个冰淇淋制作出来是毫无意义的
  • 现在我们需要思考的问题,如果需要运送的数量 m ≤ n m\leq n mn,那么我们是否有可能花费两趟去比花费一趟去来的优?
  • 答案是不可能的。为什么?
  • 如果花费一趟去,我们最后花费的时间是 c [ m − 1 ] + t c[m-1]+t c[m1]+t,而花费两趟的时间是 m a x ( c [ m − 1 ] , c [ j ] + t × 2 ) + t max(c[m-1],c[j]+t \times 2)+t max(c[m1],c[j]+t×2)+t
  • 显然有 c [ m − 1 ] + t ≤ m a x ( c [ m − 1 ] , c [ j ] + t × 2 ) + t c[m-1]+t\leq max(c[m-1],c[j]+t \times 2)+t c[m1]+tmax(c[m1],c[j]+t×2)+t
  • 就是说,花费一趟去的代价不劣于花两趟去的代价,想必聪明的你肯定知道这种情况选哪种情况去。

证明:贪心,n<m<=2n

  • 当m的个数在 ( n , 2 n ] (n,2n] (n,2n]之间的时候,我们是否有可能花费三次去运输?
  • 答案是不可能的。为什么?
  • 因为,如果其中连续的两趟拼接起来小于n,根据上述结论,我们则会花费一趟运输
  • 如果大于n,则设两趟的长度为 i i i m − i m-i mi,显然二者都不能超过n
  • 故有 i ∈ [ m − n , n ] i\in [m-n,n] i[mn,n],答案为 m a x ( c [ i ] + 2 × t , c [ m − 1 ] ) + t max(c[i]+2\times t,c[m-1])+t max(c[i]+2×t,c[m1])+t
  • 因为我们以及排序过一遍了,显然c[m-n]是最小的,那么有: i = m − n i=m-n i=mn不会劣于其他答案

证明:贪心,对于任意m

  • 尽过上面的两个证明,相信大家已经有点感觉了
  • 对于贪心法的证明,我们往往采用替换法,即该决策替换掉最优解,不会劣与最优解,即该决策是最优解,上面的max那句话我已经用到了
  • 想必这里大家也应该知道了怎么做了,对的,数学归纳法
  • 如果还不懂的话,我举个简单的例子,假设对于 m ∈ ( 2 n , 3 n ] m \in (2n,3n] m(2n,3n],我们取走第一段,剩下的不就是转换为 m ∈ ( n , 2 n ] m\in(n,2n] m(n,2n]的情况吗?
  • 如果取走最后一段,也是 m ∈ ( n , 2 n ] m\in(n,2n] m(n,2n]的情况,为了满足第二个证明的结论,故唯一的分解方法为{m-2n,n,n}
  • 到这里大家应该都懂了,分解方法为 { m % n , n , n , n , . . . , n , n } \{ m\%n,n,n,n,...,n,n \} {m%n,n,n,n,...,n,n}
  • 感性理解一下,在不白白浪费趟数的情况下,第一段越早去,最后一个蛋糕白白等待的可能性就少了很多。

算法思路

  • 到这里,聪明的你肯定已经知道怎么写了,假如 m % n ≠ 0 m\%n\neq 0 m%n=0,第一段取 m % n {m\%n} m%n,其余都取 n n n
  • 对于 m % n = 0 m\%n= 0 m%n=0,全部都取 n n n
  • 趟数也就为 ⌈ m n ⌉ \lceil \cfrac{m}{n} \rceil nm

代码实现

class Solution {
public:
    vector<int> icecream(int n, int m, int t, int* c, int len) {
    	sort(c,c+m);//排序c数组 
        int cnt,ans=0,sta;//趟数,最小时间值,第一趟从第几个冰淇淋开始运送
        if(m%n)cnt=m/n+1,sta=m%n-1;//不能整除得多运送一趟
        else cnt=m/n,sta=n-1;//下标0开始,所以是第n个物品实际为c[n-1]
        for(int i=sta;i<m;i+=n){
            ans=max(ans,c[i])+2*t;//上一次返回来的时间,和这个冰淇淋做好的时间,取最大值,来回需要加上2t
        }
        ans-=t;//最后一趟不需要回工厂
		return {ans,cnt};
    }
};

复杂度分析

  • 时间复杂度,排序 O ( m log ⁡ m ) O(m \log m) O(mlogm),n最小值为1,所以贪心遍历一遍的最坏情况为 O ( m ) O(m) O(m),总得为 O ( m log ⁡ m ) O(m \log m) O(mlogm)
  • 空间复杂度,定义了数字ans,cnt,sta,为 O ( 1 ) O(1) O(1)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值