题目描述

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

算法一:动态规划

思路引入

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

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

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

    状态转移

  • 因为我们一次可以运送的冰淇淋的数量为
  • 所以那么对于,我们已经知道了i的前面n-1个冰淇淋的状态。
  • 的时间基础上面,在送出这个区间所有的冰淇淋
    • 这边的可能有同学没看懂,因为要保证包括第i个冰淇淋的情况,前面还有个冰淇淋,并且已经送完了第j个冰淇淋的情况,也就是说至少得有个冰淇淋,但是下标是从0开始的,所以我们此处为
  • (注意:上面对于的是的情况,所以我们还需要考虑的情况)
  • 显然我们还需要考虑的一种情况,就是一开始什么都不送,等到第个冰淇淋做好然后送出去,其中
    在这里插入图片描述
  • 然后得到了j到i的转移方程 ,(取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;
    }
};

复杂度分析

  • 时间复杂度,排序为,动态规划两层循环为,理论上应该是,但是数据中的阶大,所以总的时间复杂度为
  • 空间复杂度,定义了动态数组ans,dp,pre,为

    算法二:贪心

    引入

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

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

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

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

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

    证明:贪心,对于任意m

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

    算法思路

  • 到这里,聪明的你肯定已经知道怎么写了,假如,第一段取,其余都取
  • 对于,全部都取
  • 趟数也就为

    代码实现

    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};
      }
    };

    复杂度分析

  • 时间复杂度,排序,n最小值为1,所以贪心遍历一遍的最坏情况为,总得为
  • 空间复杂度,定义了数字ans,cnt,sta,为