LeetCode 1000. Minimum Cost to Merge Stones

题目描述

题目连接java

思路

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

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

则没法最后合并成一个石子程序员

定义递归函数github

f(L, R, part, K)

L..R范围上经过每次合并K个数,必定要合成出part个部分,最小代价是多少面试

因此主函数调用的时候算法

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

0到n-1范围上,经过每次合并K个数,必定要合成一部分,最小代价是多少数组

base case是, L...R范围内只剩下一个数了,那么若是part为1,则返回最小代价是0(无须合并),若是part非1,则没法合并(由于只有一个数了),返回-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 累加和;

其中的L到R的累加和, 咱们先放一边,再看下面一种状况:

假如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上超时,咱们能够增长记忆化搜索来优化,如暴力尝试中提到的,有三个可变参数:L,R,part, 其中:

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

微信截图_20210222001100.png

更多

算法和数据结构笔记

参考资料

相关文章
相关标签/搜索