动态规划之房屋染色
这里有n个房子在一列直线上,现在我们需要给房屋染色,共有k种颜色。每个房屋染不同的颜色费用也不同,你希望每两个相邻的房屋颜色不同
费用通过一个nxk 的矩阵给出,比如cost[0][0]表示房屋0染颜色0的费用,cost[1][2]表示房屋1染颜色2的费用。
样例:
输入: costs = [[14,2,11],[11,14,5],[14,3,10]] 输出: 10 说明: 三个屋子分别使用第1,2,1种颜色,总花费是10。
原题链接:https://www.lintcode.com/problem/516/
确定状态
最后一步
这个题每个房子染的房屋颜色有k种选择,假设k=3,颜色为红,绿,蓝。由于相邻的房子颜色不能一样,对于最后一个房子n,如果染成了红色,那么倒数第二个房子n-1只能染成绿色或者蓝色,对于其他两种颜色,也是一样的,所以我们可以知道:
- 最后一个房子(n)为红色,倒数第二个房子(n-1)颜色为绿或者蓝
- 最后一个房子(n)为绿色,倒数第二个房子(n-1)颜色为红或者蓝
- 最后一个房子(n)为蓝色,倒数第二个房子(n-1)颜色为红或者绿
由此可知,当有k种颜色时,最后一栋房子染色就有k种情况,每一种情况对应的倒数第二个房屋染色也是不一样的。
确定子问题
由上面分析可知,假设该问题的最优解最小花费为A,最后一栋房子染色为kn,花费为An,我们必然就可以知道倒数第二栋房子染色选择就可以是 k1,k2,k3 ... kn-1。那么去掉最后一栋房子的花费An,前n-1栋房子的花费也必然是最小的,为A-An
,这样我们就把求解前n栋房子的最小花费转为求解前n-1栋房子的最小花费,问题规模自然变小了。这也是我们要找的子问题。
这个有一个问题要注意下,假设我们知道了前n-1栋房子的最优解,但如果我们不知道第n-1栋房子染了什么颜色,那么第n栋房子染什么颜色我们就不能确定了,如果刚好最后一栋房子与倒数第二栋房子染色一样,且花费最小,那么我们得出的结果就是错的。所以我们必须要记录每个房子染了什么颜色,否则该问题无法用动态规划进行求解。
转移方程
根据上面我们确定了问题的解决方式,由于要记录前n栋房子的染色的最小花费且还要记录第n栋房子染了什么颜色,那么我们需要开辟了二维数组来记录这两个场景的数据,所以我们用f[n][i]
来表示前n栋房子的最小花费,且第n栋房子的染色方式为i
。cost
为题意给定的费用矩阵,转移方程为:
f[n][i] = cost[n-1][i] + min{f[n-1][0], f[n-1][2] ... f[n-1][j]} 且 [j != i]
上面转移方式的含义为:如果要求前n栋房子(注意第n栋房子在cost数组里面下标为n-1
)的最小花费f[n][i]
,那么我们直接获取第n栋房子染为第i种颜色的花费为cost[n-1][i]
,不过第n-1栋房子除了第i种房子不能染,其他颜色都可以选择来染,从中选择一个花费最小的,再加上第n栋房子最小花费,即为问题的解。
初始条件和边界情况
由于f[n][i]
代表前n栋房子的染色情况,那么f[0][i]
代表没有房子,花费必然是0。
代码实现
根据上述分析,代码就很简单了,下面是没有经过优化的代码,待会还要优化:
public static int minCostII(int[][] costs) { // write your code here if (costs == null || costs.length == 0) { return 0; } // 房子数 int m = costs.length; // 颜色种类数 int k = costs[0].length; // 转移方程, n 为前n房子的最小花费(n从1开始) // f[n][i] = [k != i] const[n-1][i] + min{f[n-1][0], f[n-1][2] ... f[n-1][k]} int[][] f = new int[m+1][k]; for (int i = 1; i <= m; i++) { for (int j = 0; j < k; j++) { f[i][j] = Integer.MAX_VALUE; for (int z = 0; z < k; z++) { if (z == j) { continue; } int v = f[i-1][z] + costs[i-1][j]; if (f[i][j] > v) { f[i][j] = v; } } } } int result = f[m][0]; for (int i = 1; i < k; i++) { if (f[m][i] < result) { result = f[m][i]; } } return result; }
优化
我们直接来看转移方程:
f[n][i] = cost[n-1][i] + min{f[n-1][0], f[n-1][2] ... f[n-1][j]} 且 [j != i]
根据题意我们可以知道,颜色有k种,k的规模是没有说的,如果k为很小的数,那么求解min{f[n-1][0], f[n-1][2] ... f[n-1][j]}
是比较快的,如果k为很大的数字,第n栋房子每换一种颜色染,那么min{f[n-1][0], f[n-1][2] ... f[n-1][j]}
都要重新计算一遍,区别就是从该集合中去掉颜色与第n栋染色一样的那一项,我们先来简化一下:
假设现在我们有一个数组A[2,1,4,5,7,3]
,1就是最应的最小花费,如果1对应的颜色不能选择,那么A的最小值就是2,其余A的最小值都是1,所以我们只需要把A的最小值,次小值都求解出来,问题就解决了。不过怎么求解呢?下面我们用双指针的解法来求解。
首先初始化最小值,次小值(由于用了两个指针,需要初始化两个指针的位置):
- 如果数组长度为0,没有最小值,更没有次小值;
- 如果数组长度为1,最小值和次小值都是第一位元素;
- 如果数组长度大于等于2,需要比较第一位和第二位元素的大小,然后给最小值,次小值赋上对应的值。
之所以初始化最小值,次小值,是因为我们要用双指针来求解,算法时间复杂度为O(n),方式如下:
- 从数组第三位开始遍历,如果遍历的值比最小值小,那么就将最小值原来的值记录下来,当当前值赋给最小值
- 判断最小值无更新
- 如果最小值无更新,且次小值大于当前值,那么就将当前值赋值给次小值
- 如果最小值有更新,那么直接将最小值原来的值赋给次小值
代码如下:
/** * 求解数组m的最小值,次小值 * * @param m */ public static void test(int[] m) { if (m == null || m.length == 0) { return; } // 第一小, 第二小 int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; // 第一小颜色位置, 第二小颜色位置 int k1 = 0, k2 = 0; if (m.length == 1) { min1 = min2 = m[0]; k1 = k2 = 0; } if (m.length >= 2) { int curr0 = m[0]; int curr1 = m[1]; if (curr0 >= curr1) { min1 = curr1; k1 = 1; min2 = curr0; k2 = 0; } else { min1 = curr0; k1 = 0; min2 = curr1; k2 = 1; } for (int q = 2; q < m.length; q++) { int curr = m[q]; // 选判断是否比次小值小 int oldMin1 = min1; int oldK1 = k1; if (curr < min1) { min1 = curr; k1 = q; } // 最小值无更新,且min2大于当前值 if (oldK1 == k1 && min2 > curr) { min2 = curr; k2 = q; } // 最小值有更新,那么原来的值就是第二小 if (oldK1 != k1) { min2 = oldMin1; k2 = oldK1; } } } System.out.println("min1 = " + min1 + ", k1 = " + k1); System.out.println("min2 = " + min2 + ", k2 = " + k2); }
根据上面分析,在求解前n栋房子花费最小值是,我们直接将前n-1栋房子花费的最小值,次小值求出来,然后根据第n栋房子的颜色来进行合适判断即可,完整代码如下:
public static int minCostII2(int[][] costs) { // write your code here if (costs == null || costs.length == 0) { return 0; } // 房子数 int m = costs.length; // 颜色种类数 int k = costs[0].length; // 转移方程, n 为前n房子的最小花费(n从1开始) // f[n][i] = [j != i] const[n-1][i] + min{f[n-1][0], f[n-1][2] ... f[n-1][k]} int[][] f = new int[m+1][k]; for (int i = 1; i <= m; i++) { // 第一小, 第二小 int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE; // 第一小颜色位置, 第二小颜色位置 int k1 = 0, k2 = 0; if (k == 1) { min1 = min2 = f[i-1][0]; k1 = k2 = 0; } if (k >= 2) { int curr0 = f[i-1][0]; int curr1 = f[i-1][1]; if (curr0 >= curr1) { min1 = curr1; k1 = 1; min2 = curr0; k2 = 0; } else { min1 = curr0; k1 = 0; min2 = curr1; k2 = 1; } // 算出{f[i-1][0], f[i-1][2] ... f[i-1][k]} 的第一小,记录哪个颜色,第二小,记录哪个颜色,下面可以直接用 for (int q = 2; q < k; q++) { int curr = f[i-1][q]; // 选判断是否比次小值小 int oldMin1 = min1; int oldK1 = k1; if (curr < min1) { min1 = curr; k1 = q; } // 最小值无更新,且min2大于当前值 if (oldK1 == k1 && min2 > curr) { min2 = curr; k2 = q; } // 最小值有更新,那么原来的值就是第二小 if (oldK1 != k1) { min2 = oldMin1; k2 = oldK1; } } } for (int j = 0; j < k; j++) { f[i][j] = Integer.MAX_VALUE; // 如果当前位置为k1,当前不能取,只能取次小的 if (j == k1) { f[i][j] = min2 + costs[i-1][j]; } else { f[i][j] = min1 + costs[i-1][j]; } } } int result = f[m][0]; for (int i = 1; i < k; i++) { if (f[m][i] < result) { result = f[m][i]; } } return result; }
title: 动态规划之房屋染色
tags: [算法,动态规划]
author: Mingshan
categories: [算法,动态规划]
date: 2021-03-07
本文来自博客园,作者:mingshan,转载请注明原文链接:https://www.cnblogs.com/mingshan/p/17793527.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)