一.题目描述
给你一个高度数组Hi,每提升Hi一个单位的代价是Wi,求让相邻两个高度不同的最小代价。比如Hi [2,3,4,4];Wi [1, 2,3,4],就是让第三个提升1。
二.解题思路
一开始我的想法是:当 H [ i ] H[i] H[i] != H [ i − 1 ] H[i-1] H[i−1]时,我们不需要对第 i i i列进行增高,只有当 H [ i ] H[i] H[i]= H [ i − 1 ] H[i-1] H[i−1]时,我们才需要提高第 i i i或第 i − 1 i-1 i−1列。但是如何选取提高哪列呢,并且该提高多少高度呢,这些都让我产生了困扰,那我们可不可以把提高第 i i i列和第 i − 1 i-1 i−1列的情况都考虑呢,并且把可能提高的所有高度都考虑到(可能提高1或2…或n)。
很显然,我们提高的高度是有限制的,超过这个限制再进行提高只会白白增加代价,那么这个限制是多少呢?
我们假设数组 H H H的长度为 n n n,最大元素为 h m a x h_{max} hmax,我们为了满足使相邻两个高度不同这个条件,对原数组 H H H中的元素进行提升,那么提升后的数组 H ′ H' H′的最大高度最多为 h m a x + 1 h_{max}+1 hmax+1。
证明如下: 我们可以将原始数组 H H H中的所有元素提升到 h m a x h_{max} hmax,然后间隔着再提升一个单位即可,得到: h m a x , h m a x + 1 , h m a x , h m a x + 1 , h m a x … … h_{max},h_{max}+1,h_{max},h_{max}+1,h_{max}…… hmax,hmax+1,hmax,hmax+1,hmax……这样的序列,就可以满足相邻两个高度不同了,所以提升后满足条件的数组的最大高度就是 h m a x + 1 h_{max}+1 hmax+1。
那么我们可以构建这样一个dp数组:
//n为数组长度,h_max+2表示高度0~h_max+1(最大高度)
int[][] dp = new int[n][h_max+2];
其中 d p [ i ] [ j ] dp[i][j] dp[i][j]表示计算到第 i i i列为止(即只看前 i i i列),第 i i i列的高度提升为 j j j的最小代价。
那么我们怎么去更新它呢?
对于第0列,我们很好去更新,因为不用考虑与前面的列高度相同,更新公式如下:
//第0列更新代价=它自身提升高度所需要的代价
//其中W[0]为第0列提升一个单位的代价
//高度j不能小于H[0],因为只能提升,不能减小
for(int j = H[0]; j <= h_max+1; j++)
dp[0][j] = (j-H[0])*W[0];
对于第1列,我们就需要考虑它与第0列高度不同这个条件了,因此更新过程如下:对于 d p [ 1 ] [ j ] ( j > = H [ 1 ] ) dp[1][j](j >= H[1]) dp[1][j](j>=H[1]),它的总代价由两部分组成:一部分是它自己从 H [ 1 ] H[1] H[1]提升至 j j j的代价,这部分代价为 ( j − H [ 1 ] ) ∗ W [ 1 ] (j-H[1])*W[1] (j−H[1])∗W[1];另一部分代价是从第0列继承的代价,这一部分的最小值为 m i n ( d p [ 0 ] [ k ] ) ( k ∈ [ H [ 0 ] , h m a x + 1 ] 且 k ≠ j ) min(dp[0][k])(k\in[H[0], h_{max}+1]且k\ne j) min(dp[0][k])(k∈[H[0],hmax+1]且k=j), k ≠ j k\ne j k=j是因为要保证相邻两个的高度不同。更新公式如下:
int min_pre_cost = 0x3f3f3f3f;
// 找到前一个dp[i-1]列的最小cost,并且不能与当前的高度j相同
for(int k = H[i-1]; k < m; k++){
if(k == j) continue;
if(dp[i-1][k] < min_pre_cost) min_pre_cost = dp[i-1][k];
}
dp[i][j] = min_pre_cost + (j-H[i])*W[i];
这样我们就可以解决之前的疑问:如果 H [ i ] = H [ i − 1 ] H[i]=H[i-1] H[i]=H[i−1],是提升第 i i i列还是提升第 i − 1 i-1 i−1列,提升多少高度;因为我们已经考虑了所有情况。
最终的最小代价就是 m i n ( d p [ n − 1 ] ) min(dp[n-1]) min(dp[n−1])。
三.示例
黄色部分代表原始数组高度
H
=
[
2
,
3
,
4
,
4
]
H=[2, 3, 4, 4]
H=[2,3,4,4];白色部分为dp数组更新的部分;加号前面的代表当前列提升的代价,后面代表继承前一列的代价。
其中最后一列的第二行从前一列的继承代价不为0,而是w[2]*1,这是因为前一列代价为0的那个格子高度和它一样,为满足高度不同的条件只能继承第三列第一行的那个w[2]*1+0。
四.完整代码
public int minCost(int[] H, int[] W){
int n = H.length;
int h_max = 0;
for(int h: H)
if(h > h_max)
h_max = h;
int m = h_max+2;
// dp[i][j]表示计算到第i个的高度为j的cost
int[][] dp = new int[n][m];
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
dp[i][j] = -1;
for(int i = 0; i < n; i++){
//遍历当前第j列的可能高度
for(int j = H[i]; j < m; j++){
//遍历前一个dp[i-1]
if(i == 0){
dp[i][j] = (j-H[i])*W[i];
}else{
int min_pre_cost = 0x3f3f3f3f;
// 找到前一个dp[i-1]列的最小cost,并且不能与当前的高度j相同
for(int k = H[i-1]; k < m; k++){
if(k == j) continue;
if(dp[i-1][k] < min_pre_cost) min_pre_cost = dp[i-1][k];
}
dp[i][j] = min_pre_cost + (j-H[i])*W[i];
}
}
}
int res = 0x3f3f3f3f;
//第n-1列的最小值即为最终结果
for(int i = H[n-1]; i < m; i++)
if(dp[n-1][i] < res)
res = dp[n-1][i];
return res;
}
时间复杂度O(n³)
五.优化为O(n²)
因为一共要更新O(n²)个dp[i][j],所以最小复杂度是O(n²)。上面代码中找前一列的最小值的部分可以优化:当我们更新 d p [ i ] dp[i] dp[i]这一行时,我们可以保留前一行 d p [ i − 1 ] dp[i-1] dp[i−1]的最小的值 m i n 1 min1 min1和次小的值 m i n 2 min2 min2。当每次更新 d p [ i ] [ j ] dp[i][j] dp[i][j]时,原来我们需要遍历 d p [ i − 1 ] dp[i-1] dp[i−1]一整行,复杂度O(n);但现在我们只需要判断前一列高度相同的 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]是否等于 m i n 1 min1 min1,如果相等,那么前一列继承的最小代价为 m i n 2 min2 min2,否则为 m i n 1 min1 min1,复杂度O(1)。