问题描述
输入输出
解题思路
这是一道比较经典的区间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();然后后面再更新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 归忆)