合并石头的最低成本问题

这篇博客介绍了如何解决LeetCode上的1000题——合并石头的最低成本问题。作者给出了递归和记忆化搜索的解决方案,并详细解释了思路,包括如何利用前缀和加速计算以及如何通过记忆化搜索优化超时的递归算法。
摘要由CSDN通过智能技术生成

合并石头的最低成本问题

作者:Grey

原文地址:

博客园:合并石头的最低成本问题

CSDN:合并石头的最低成本问题

题目描述

LeetCode 1000. Minimum Cost to Merge Stones

思路

首先,K和石子数组的长度有关系,通过观察可知,假设石子数组的长度是n,如果

(n - 1) % (K - 1) > 0

则无法最后合并成一个石子

定义递归函数

f(L, R, part, K)

递归含义是: [L...R]范围上通过每次合并K个数,一定要合成出part个部分,最小代价是多少

所以主函数调用的时候

f(0, n - 1, 1, K)

即:[0...n-1]范围上,通过每次合并K个数,一定要合成1部分,最小代价是多少。

base case是, [L...R]范围内只剩下一个数了,那么如果part为1,则返回最小代价是0(无须合并),如果part1,则无法合并(因为只有一个数了),返回-1

if (L == R) {
   return part == 1 ? 0 : -1;
}

如果[L...R]范围内不止一个数,那么就看part的取值,

假如part等于1, 说明需要合并成一个数,那么最后一次合并必然是分成了K部分(因为分成K部分,才能在下一次的合并过程中合成一个数)

int pre = f(arr, L, R, K, K);
if (pre == -1) {
    return -1;
}
return pre + L..R 累加和;

其中的LR的累加和, 我们先放一边,再看下面一种情况:

假如part的值不等于1,则需要考虑将[L...R]分两部分来考虑,一部分生成part = 1的最小代价cost1,另外一部分生成part = part - 1的最小代价cost2,

cost1 + cost2就是最小代价,代码如下:

int ans = Integer.MAX_VALUE;
for (int i = L; i < R; i += (K - 1)) {
    int cost1 = f(arr, L, i, K, 1);
    int cost2 = f(arr, i + 1, R, K, part - 1);
    if (cost1 != -1 && cost2 != -1) {
        ans = Math.min(ans, cost2 + cost1);
    }
}
return ans;

接下来,我们解决前面提到的问题:快速得到L…R的累加和,我们可以通过前缀和数组来加速[L...R]的累加和计算, 生成前缀和数组的方式如下

// 前缀和用来加速求L..R范围内的累加和
int[] preSum = new int[n];
preSum[0] = stones[0];
for (int i = 1; i < n; i++) {
    preSum[i] = preSum[i - 1] + stones[i];
}

这样我们就可以很方便以O(1)时间复杂度求出[L...R]的累加和

L..R累加和 = preSum[R] - (L == 0?0:preSum[L-1])

所以,暴力递归的代码如下:

// 暴力解法
public static int mergeStones(int[] stones, int K) {
    // k和数组长度先做一次过滤
    int n = stones.length;
    if ((n - 1) % (K - 1) > 0) {
        return -1;
    }
    // 前缀和用来加速求L..R范围内的累加和
    int[] preSum = new int[n];
    preSum[0] = stones[0];
    for (int i = 1; i < n; i++) {
        preSum[i] = preSum[i - 1] + stones[i];
    }
    return f(stones, 0, n - 1, K, 1, preSum);
}

// f(L,R,part) -> L..R范围上一定要合成出part个数,最小代价是多少
public static int f(int[] arr, int L, int R, int K, int part, int[] preSum) {
    if (L == R) {
        return part == 1 ? 0 : -1;
    }
    if (part == 1) {
        // part只有1块的时候
        // 需要算出当part是K份的时候,最小代价
        int pre = f(arr, L, R, K, K, preSum);
        if (pre == -1) {
            return -1;
        }
        return pre + preSum[R] - (L == 0 ? 0 : preSum[L - 1]);
    }
    // part不止一块
    // 则可以让 L..i 得到1块
    // i+1...R得到part-1块
    // 然后合并即可
    int ans = Integer.MAX_VALUE;
    for (int i = L; i < R; i += (K - 1)) {
        int cost1 = f(arr, L, i, K, 1, preSum);
        int cost2 = f(arr, i + 1, R, K, part - 1, preSum);
        if (cost1 != -1 && cost2 != -1) {
            ans = Math.min(ans, cost2 + cost1);
        }
    }
    return ans;
}

这个解法在LeetCode上超时,我们可以增加记忆化搜索来优化,如暴力尝试中提到的,有三个可变参数:LRpart, 其中:

L的变化范围是:0...n-1

R的变化范围是:0...n-1

part的变化范围是:1...K

我们可以定义一个三维数组来存递归结果

int[][][] dp = new int[n][n][K+1]

只需要在每次暴力递归的时候,用这个数组存下当时的记录即可, 优化后的代码如下:

public static int mergeStones(int[] stones, int K) {
    // k和数组长度先做一次过滤
    int n = stones.length;
    if ((n - 1) % (K - 1) > 0) {
        return -1;
    }
    // 前缀和用来加速求L..R范围内的累加和
    int[] preSum = new int[n];
    preSum[0] = stones[0];
    for (int i = 1; i < n; i++) {
        preSum[i] = preSum[i - 1] + stones[i];
    }
    int[][][] dp = new int[n][n][K + 1];
    return f2(stones, 0, n - 1, K, 1, preSum, dp);
}

// f(L,R,part) -> L..R范围上一定要合成出part个数,最小代价是多少
public static int f2(int[] arr, int L, int R, int K, int part, int[] preSum, int[][][] dp) {
    if (dp[L][R][part] != 0) {
        return dp[L][R][part];
    }
    if (L == R) {
        dp[L][R][part] = (part == 1 ? 0 : -1);
        return dp[L][R][part];
    }
    if (part == 1) {
        // part只有1块的时候
        // 需要算出当part是K份的时候,最小代价
        int pre = f2(arr, L, R, K, K, preSum, dp);
        if (pre == -1) {
            dp[L][R][part] = -1;
            return -1;
        }
        dp[L][R][part] = pre + preSum[R] - (L == 0 ? 0 : preSum[L - 1]);
        return dp[L][R][part];
    }
    // part不止一块
    // 则可以让 L..i 得到1块
    // i+1...R得到part-1块
    // 然后合并即可
    int ans = Integer.MAX_VALUE;
    for (int i = L; i < R; i += (K - 1)) {
        int left = f2(arr, L, i, K, 1, preSum, dp);
        int right = f2(arr, i + 1, R, K, part - 1, preSum, dp);
        if (left != -1 && right != -1) {
            ans = Math.min(ans, right + left);
        } else {
            dp[L][R][part] = -1;
        }
    }
    dp[L][R][part] = ans;
    return ans;
}

image

更多

算法和数据结构笔记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GreyZeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值