【蓝桥杯3540】合并石子(区间dp&java)

问题描述

输入输出

解题思路

这是一道比较经典的区间dp问题,不过加上了颜色的限制。

用三维数组dp[i][j][c]表示区间[i,j]且颜色为c的石堆合并所花费的最小代价。

前缀和数组preSum[i]表示前i个石头堆的石头数之和。用preSum[j]-preSum[i-1]可以得到区间[i,j]的总石头数。注意:这个总石头数是区间合并的代价!!!

用minHeapNums[i][j]表示区间[i,j]的不同颜色的堆合并后的最小堆数。minHeapNums[1][n]为第一个输出答案。

用cost[i][j] 表示区间[i,j]的不同颜色的堆合并后的最小代价。cost[1][n]为第二个输出答案。

首先先初始化各个数组;然后用四层for循环进行区间dp,由于颜色c的循环只有三次,所以循环的时间复杂度为O(n^3);然后后面再更新minHeapNums数组和cost数组的值,若区间[i,j]的堆数小于区间[i,k]和区间[k+1,j]的堆数之和则更新。同时要更新最小堆数时的代价cost。

至于为什么要更新minHeapNums数组和cost数组的值呢?以题目样例为例,分别展示初始化后、dp主循环后、更新后三种情况下minHeapNums数组的值。

    

由第二个minHeapNums[1][2]的值赋值为1后(说明第1个堆和第2个堆合并为1个堆,此时区间[1,3]应该只剩下2个堆),minHeapNums[1][3]的值还是3可知需要更新。

更多实现细节见注释,注释写的非常详细。

AC代码

import java.util.Scanner;

public class Main {
	public static void main(String[] args) {
		int INF = Integer.MAX_VALUE; // 表示无穷大,即不可达状态
		Scanner scan = new Scanner(System.in);
		int n = scan.nextInt();
		int[][][] dp = new int[n + 1][n + 1][3]; // dp[i][j][c]表示区间[i,j]且颜色为c的石堆合并所花费的最小代价
		int[] preSum = new int[n + 1]; // 前缀和数组,preSum[i]表示前i个石头堆的石头数之和。用preSum[j]-preSum[i-1]可以得到区间[i,j]的总石头数
		int[][] minHeapNums = new int[n + 1][n + 1]; // minHeapNums[i][j]表示区间[i,j]的不同颜色的堆合并后的最小堆数。minHeapNums[1][n]为第一个输出答案
		int[][] cost = new int[n + 1][n + 1];// cost[i][j] 表示区间[i,j]的不同颜色的堆合并后的最小代价。cost[1][n]为第二个输出答案

		for (int i = 1; i <= n; i++) {
			preSum[i] = preSum[i - 1] + scan.nextInt();// 前缀和数组初始化
			for (int j = 1; j <= n; j++) {
				minHeapNums[i][j] = j - i + 1; // 刚开始时每个石头都视为一个堆,如第2个石头到第4个石头之间就有三个堆,即4-2+1=3
				for (int c = 0; c <= 2; c++) {
					dp[i][j][c] = INF; // 先全部初始化为无穷大
				}
			}
		}

		for (int i = 1; i <= n; i++) {
			dp[i][i][scan.nextInt()] = 0;// 刚开始时第i个石子即第i个堆的合并代价为0,因为还没有合并
		}

		// len用来控制区间的长度,以达到区间逐渐扩大的效果。如1、2、3、4,若len=2则为12、23、34,若len=3则123、234,......
		for (int len = 2; len <= n; len++) {
			for (int i = 1;; i++) {
				int j = i + len - 1; // i为区间左边界,j为区间右边界,len为区间长度
				if (j > n) {// j不能大于n
					break;
				}

				for (int c = 0; c <= 2; c++) {// 对每种颜色都遍历一遍
					int minCost = INF; // 记录颜色为c时找到的最小代价

					// k用来判断怎么合并代价更小,比如原本为1、2、3,已经判断过一轮合并1、23和12、3,k的作用就是判断这两个合并哪个代价更小
					for (int k = i; k < j; k++) {
						// 判断情况是否合法,如dp[i][k][c]!=INF是判断区间[i,k]是否存在颜色为c的合并情况,若为INF说明不存在
						// 注意在k的for循环中只是在寻找minCost,并没有更改各个数组
						if (dp[i][k][c] != INF && dp[k + 1][j][c] != INF) {
							// 若最小代价比当前最小代价还要小则更新。dp[i][k][c]和dp[i + 1][j][c]是两个堆原本的代价。
							// preSum[j] - preSum[i-1]是区间[i,j]的总石头数,即合并代价。其实和k的循环无关,可以放到后面更新dp处
							minCost = Math.min(minCost, dp[i][k][c] + dp[k + 1][j][c] + preSum[j] - preSum[i - 1]);
						}
					}

					// 若当前颜色下判断的所有情况均不存在则跳过此次循环
					if (minCost == INF) {
						continue;
					}

					// 合并两个堆后颜色会变为(c + 1) % 3,此时判断是否需要更新dp[i][j][(c + 1) % 3]
					dp[i][j][(c + 1) % 3] = Math.min(dp[i][j][(c + 1) % 3], minCost);

					// 若合并后区间[i,j]的堆便是1
					minHeapNums[i][j] = 1;

					// 合并后更新区间[i,j]的最小代价。注意不能丢到颜色c的for循环外面,否则不合并也会更新导致错误
					cost[i][j] = Math.min(dp[i][j][0], Math.min(dp[i][j][1], dp[i][j][2]));
				}
			}
		}

		// 更新minHeapNums数组和cost数组的值
		for (int k = 1; k <= n; k++) {
			for (int i = 1; i <= k; i++) {
				for (int j = k + 1; j <= n; j++) {
					// 若区间[i,j]的堆数小于区间[i,k]和区间[k+1,j]的堆数之和则更新。同时要更新最小堆数时的代价cost
					if (minHeapNums[i][j] > minHeapNums[i][k] + minHeapNums[k + 1][j]) {
						minHeapNums[i][j] = minHeapNums[i][k] + minHeapNums[k + 1][j];
						cost[i][j] = cost[i][k] + cost[k + 1][j];
					} else if (minHeapNums[i][j] == minHeapNums[i][k] + minHeapNums[k + 1][j]) {
						// 若最小堆数相同则取最小值
						cost[i][j] = Math.min(cost[i][j], cost[i][k] + cost[k + 1][j]);
					}
				}
			}
		}

		System.out.println(minHeapNums[1][n] + " " + cost[1][n]);
	}
}

(by 归忆)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

归忆_AC

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

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

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

打赏作者

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

抵扣说明:

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

余额充值