合并石头的最低成本(递归+dp记忆化搜索)

题目说明:

有 N 堆石头排成一排,第 i 堆中有 stones[i] 块石头。

每次移动(move)需要将连续的 K 堆石头合并为一堆,而这个移动的成本为这 K 堆石头的总数。

找出把所有石头合并成一堆的最低成本。如果不可能,返回 -1 。

示例:

输入:stones = [3,2,4,1], K = 2
输出:20
解释:
从 [3, 2, 4, 1] 开始。
合并 [3, 2],成本为 5,剩下 [5, 4, 1]。
合并 [4, 1],成本为 5,剩下 [5, 5]。
合并 [5, 5],成本为 10,剩下 [10]。
总成本 20,这是可能的最小值。

题目分析:

思路①:

看完这道题,首先想到的就是哈夫曼树。然而可惜的是,题目要求的是连续的子数组,这和哈夫曼树的原理相违背。然后就没有然后了。

思路②:

暴力搜索:每次暴力搜索和最小的k元组,并更新数组。

伪代码:

while("数组长度大于k"){
    for(){
        //找到最小的k元组和
    }
    //更新数组并累加count
}
if("数组长度最终为1") return count;
else return -1;

毫无疑问,这非常耗时间,这种办法虽然估计也可以,而且想起来很简单,写起来也简单。但是只要是对自己的算法水平有要求的同学下意识就会避开这种方式。

正解:(递归+dp记忆化搜索)

思路过程:

我们从后往前想,假设有某个数组A最终会形成一个堆,那么在形成最后一个堆之前,一定有k个堆(这是显而易见的,如果不会形成最后k个堆,就不可能最终合并为1个堆)。

而堆只有两种情况:

①本身存在于原数组中,自己单个数为一个堆。

②由别的k个堆新形成的堆。

这很明显有一个递归的感觉在里面。

附加定理①:

对于形成新的堆,1+n(k-1)个堆都可以最终形成1个堆。

例如:当k=3时 ,1个堆,3个堆,5个堆,7个堆,9个堆……都可以最终合并为1个堆。

附加定理②:

假设数组长度为len,由定理①可知,若 (len-1)%(k-1)==0 该数组最终一定可以形成1个堆。

由此可以进一步推断,无论如何操作该数组,不管怎么合并,最终都会形成k个堆(因为只有形成k个堆,才会最终合并为1个堆)。

最终想法:

那么我们有如下想法:

①将数组分为k个堆,第一个堆的长度由1~1+n(k-1)遍历整个数组(由定理1可知,由公式1+n(k-1)计算出来的长度最终一定可以形成1个堆)。剩下的长度用来形成k-1个堆。(由定理2可知,无论第一个堆用了多少长度的数组,只要小于数组长度,剩下的数都可以形成k-1个堆)遍历过程中记录代价最少的那一次。

②对于第一个堆,作为大问题的子问题,递归调用①操作再加上本层的代价即可。

③对剩下的k-1个堆,同样递归调用类似①的操作即可。唯一的区别是k-1个堆要分成1和k-2个堆而已。

④递归终点:若剩下的数字只有1位数,代价为0,因为无需合并。

优化过程:

  1. 首先,k元组的和可以通过前缀和的方式来简化,这样就不用每一次都求和了。
  2. 其次,显而易见,在上诉所有递归过程中,重复的操作总是很多,我们可以每一次计算都直接记录下来,下一次直接访问即可,这样就不用重复计算了。

代码示例:

class Solution {
public:
    int mergeStones(vector<int>& stones, int k) {
        vector<int> prefix(stones.size()+1);//保存前缀和
        prefix[0]=0;
        int n = stones.size();
        if ((n-1) % (k-1))return -1;
        for(int i=1;i<=n;i++)
            prefix[i]=prefix[i-1]+stones[i-1];
        int mem[n][n][k+1];//记录计算过的数据
        memset(mem, -1, sizeof(mem));//初始化为-1

        /*只是一个函数指针:由i到j,形成p个堆*/
        function<int(int,int,int)> dfs = [&](int i,int j,int p)->int{
            int & res = mem[i][j][p];//调用引用,因为要更新数组
            if(res!=-1) return res;//若已经计算过了。直接返回。
            if(p==1){//若只需要形成一个堆
                if(i==j) return res = 0;//只有一位数,无需合并,返回0
                return res = dfs(i,j,k)+prefix[j+1]-prefix[i];//否则作为子问题递归调用,并加上本层代价
            } 
            /*若要形成多个堆,则遍历剩下的数组,返回最小的代价*/
            res = INT_MAX;
            for(int temp=i;temp<j;temp+=k-1)
                res = min(res,dfs(i,temp,1)+dfs(temp+1,j,p-1));//代价为第一个堆以及后续的p-1个堆
            return res;
        };
        
        return dfs(0,n-1,1);
    }
};

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值