一.序列型动态规划概述
1.序列型动态规划特点
- 给定一个序列
- 动态规划方程f[i]中的下标i表示前i个元素a[0]、a[1]、…a[i-1]的某种性质
- 坐标型的f[i]表示以ai为结尾的某种性质
- 初始化中,f[0]表示空序列的性质
- 坐标型动态规划的初始条件f[0]就是指以a0为结尾的子序列的性质
2.例1(LintCode 516 Paint House I)
-
题意
有一排N栋房子,每栋房子要漆成3种颜色中的一种:红、蓝、绿,任何两栋相邻的房子不能漆成同样的颜色,第i栋房子染成红色、蓝色、绿色的花费分别是cost[i][0],cost[i][1],cost[i][2],问最少需要花多少钱油漆这些房子 -
例子
- 输入:N=3,Cost=[[14,2,11],[11,14,5],[14,3,10]]
- 输出:10(第0栋房子蓝色,第1栋房子绿色,第2栋房子蓝色,2+5+3=10)
-
可以直接上暴力…不过暴力的时间复杂度是3的n次方,数据量大的话多半凉凉,所以我们还是来考虑下用dp思想怎么做这道题吧
A.确定状态
- 最优策略就是花费最小的策略
- 最后一步:最优策略中房子N-1一定是染成了红、蓝、绿中的一种,我们先不管是哪一种,但是相邻两栋房子不能漆成一样颜色
- 如果最优策略房子N-1是红,房子N-2只能是蓝或绿
- 所以如果最优策略房子N-1是蓝,房子N-2只能是红或绿
- 所以如果最优策略房子N-1是绿,房子N-2只能是红或蓝
- 貌似有点复杂了…
- 如果直接套用以前思路,记录油漆前N栋房子的最小花费,根据以前的套路,也需要记录油漆前N-1栋房子的最小花费,但是,前N-1栋房子的最小花费的最优策略中,不知道房子N-2是什么颜色,所以有可能和房子N-1撞色…
- 我们知道N-1房子最少花100,但是不知道房子N-1是什么颜色,如果是蓝色,N-2也是蓝色。。。那么答案就是错的…简单的来说就是以前的套路是不管颜色,仅仅是求的最少的花费而已
- 尽然如此,那么就只有记录下颜色了,不知道房子N-2是什么颜色,就把它记录下来!
- 分别记录油漆前N-1栋房子并且房子N-2是红色、蓝色、绿色的最小花费
- 求油漆前N栋房子并且房子N-1是红色、蓝色、绿色的最小花费
- 需要知道油漆前N-1栋房子并且房子N-2是红色、蓝色、绿色的最小花费
- 子问题
- 状态:设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别是f[i][0],f[i][1],f[i][2] (从0开始的)
B.转移方程
- 设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0],f[i][1],f[i][2]
C.初始条件和边界情况
- 设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0],f[i][1],f[i][2]
- 初始条件:f[0][0]=f[0][1]=f[0][2]=0(即不油漆任何房子的花费)
- 无边界情况
D.计算顺序
E.代码
public int minCost(int[][] costs) {
if(costs==null||costs.length==0){
return 0;
}
int result = 0x7fffffff;
int n=costs.length;
int m=costs[0].length;
int [][] dp = new int[n+1][m];
dp[0][0]=0;
dp[0][1]=0;
dp[0][2]=0;
for(int i=1;i<=n;i++){
dp[i][0]=Math.min(dp[i-1][1],dp[i-1][2])+costs[i-1][0];
dp[i][1]=Math.min(dp[i-1][0],dp[i-1][2])+costs[i-1][1];
dp[i][2]=Math.min(dp[i-1][1],dp[i-1][0])+costs[i-1][2];
}
for(int i=0;i<m;i++){
result=Math.min(dp[n][i],result);
}
return result;
}
- 这里数组dp又是只和上一行有关…所以我们可以用滚动数组优化
public int minCost(int[][] costs) {
if(costs==null||costs.length==0){
return 0;
}
int result = 0x7fffffff;
int n=costs.length;
int m=costs[0].length;
int [][] dp = new int[2][m];
int news,olds;
dp[0][0]=0;
dp[0][1]=0;
dp[0][2]=0;
news=1;
olds=0;
for(int i=1;i<=n;i++){
dp[news][0]=Math.min(dp[olds][1],dp[olds][2])+costs[i-1][0];
dp[news][1]=Math.min(dp[olds][0],dp[olds][2])+costs[i-1][1];
dp[news][2]=Math.min(dp[olds][1],dp[olds][0])+costs[i-1][2];
news = news^olds;
olds = news^olds;
news = news^olds;
}
for(int i=0;i<m;i++){
result=Math.min(dp[olds][i],result);
}
return result;
}
3.例2(LintCode 516 Paint House II)
- 题意:
有一排N栋房子,每栋房子要漆成K种颜色中的一种,任何两栋相邻的房子不能漆成同样的颜色,房子i染成第j种颜色的花费是cost[i][j],问最少需要花多少钱油漆这些房子 - 例子
- 输入:N=3,K=3,cost=[[14,2,11],[11,14,5],[14,3,10]]
- 输出:10(房子0蓝色,房子1绿色,房子2蓝色,2+5+3=10)
A.确定状态
- 这题和Paint House类似,只是颜色种类变成了K种
- 动态规划思路和Paint House一样,需要记录油漆前i栋房子并且房子i-1是颜色1,颜色2,…,颜色K的最小花费:f[i][1]、f[i][2]、…、f[i][k]
B.转移方程
-
设油漆前i栋房子并且房子i-1是颜色1,颜色2,…颜色K的最小花费分别为f[i][1]、f[i][2]、…、f[i][K]
-
f[i][1]=min{f[i-1][2]+cost[i-1][1],f[i-1][3]+cost[i-1][1],…,f[i-1][K]+cost[i-1][1]}
-
f[i][2]=min{f[i-1][1]+cost[i-1][2],f[i-1][3]+cost[i-1][2],…,f[i-1][K]+cost[i-1][2]}
-
…
-
f[i][K]=min{f[i-1][1]+cost[i-1][K],f[i-1][2]+cost[i-1][K],…,f[i-1][K-1]+cost[i-1][K]}
-
设油漆前i栋房子并且房子i-1是颜色1,颜色2,…颜色K的最小花费分别为f[i][1]、f[i][2]、…、f[i][K]
-
设油漆前i栋房子并且房子i-1是颜色1,颜色2,…颜色K的最小花费分别为f[i][1]、f[i][2]、…、f[i][K]
-
f[i][j]=min(k!=j){f[i-1][k]}+cost[i-1][j]
-
直接计算的时间复杂度(计算步数):i从0到N,j从1到K,k从1到K,O(NK²)
C.代码
public int minCostII(int[][] costs) {
if(costs==null||costs.length==0){
return 0;
}
int n = costs.length;
int m = costs[0].length;
int [] [] dp = new int[n+1][m];
int result = Integer.MAX_VALUE;
for(int i=1;i<=n;i++){
for(int j=0;j<m;j++){
dp[i][j]=Integer.MAX_VALUE;
for(int k=0;k<m;k++){
if(j!=k){
dp[i][j]=Math.min(dp[i-1][k]+costs[i-1][j],dp[i][j]);
}
}
}
}
for(int i=0;i<m;i++){
result = Math.min(dp[n][i],result);
}
return result;
}
- 能不能再快点?上面代码在LIntcode上提交时间才超越百分之8的人
D.动态规划常见优化
- f[i][j]=[min(k!=j){f[i-1][k]}]+cost[i-1][j]
- 每次需要求f[i-1][1],…,f[i-1][K]中除了一个元素之外其他元素的最小值,会产生很多重复
- 只要不是除开的是最低值,最低值永远都是它,除开它的最低值就是次小值
- f[i][j]=min(k!=j){f[i-1][k]}+cost[i-1][j]
- 记录下f[i-1][1],…,f[i-1][K]中的最小值和次小值分别是哪个
- 加入最小值时f[i-1][a],次小值是f[i-1][b],则对于j=1,2,3,…,a-1,a+1,…,K,f[i][j]=f[i-1][a]+cost[i-1][j],f[i][a]=f[i-1][b]+cost[i-1][a]
- 时间复杂度降为O(NK)
E.优化后代码
public int minCostII(int[][] costs) {
if(costs==null||costs.length==0){
return 0;
}
int n = costs.length;
int m = costs[0].length;
int [] [] dp = new int[n][m];
int result = Integer.MAX_VALUE;
int min1,min2,j1,j2;
//我这里dp代表表的是涂第0栋房屋,第0栋涂颜色i所花费最少的价格,从0就开始了
for(int i=0;i<m;i++){
dp[0][i]=costs[0][i];
}
j1=0;
j2=0;
for(int i=1;i<n;i++){
min1 = Integer.MAX_VALUE;
min2 = Integer.MAX_VALUE;
for(int j=0;j<m;j++){
//更新最小值和最小值下标
if(dp[i-1][j]<min1){
min2=min1;
j2=j1;
min1=dp[i-1][j];
j1=j;
}else{
//更新次小值和次小值下标
if(dp[i-1][j]<min2){
min2=dp[i-1][j];
j2=j;
}
}
}
for(int j=0;j<m;j++){
//当前颜色和上个颜色不一样,直接取最小值;一样就取次小值
if(j!=j1){
dp[i][j]=dp[i-1][j1]+costs[i][j];
}else{
dp[i][j]=dp[i-1][j2]+costs[i][j];
}
}
}
for(int i=0;i<m;i++){
result = Math.min(dp[n-1][i],result);
}
return result;
}
4.例3(LintCode 392 House Robber)
- 题意
- 有一排N栋房子(0-N-1),房子i里有A[i]个金币,一个窃贼想选择一些房子偷金币,但是不能偷任何挨着的两家邻居,否则会被警察逮住,问最多能偷多少金币
- 例子
- 输入A={3,8,4}
- 输出:8(只能偷第二家的金币)
A.确定状态
- 最后一步:窃贼的最优策略中,有可能偷或者不偷最后一栋房子N-1
- 情况1:不偷房子N-1(简单,最优策略就是前N-1栋房子的最优策略)
- 情况2:偷房子N-1(仍然需要知道在前N-1栋房子中最多能偷多少金币,但是,需要保证不偷第N-2栋房子)
- 如何知道在不偷房子N-2的前提下,在前N-1栋房子中最多能偷多少金币呢?
- 用f[i][0]表示不偷房子i-1前提下,前i栋房子中最多能偷多少金币
- 用f[i][1]表示偷房子i-1前提下,前i栋房子中最多能偷多少金币
B.转移方程
- 设f[i][0]表示不偷房子i-1前提下,前i栋房子中最多能偷多少金币
- 设f[i][1]表示偷房子i-1前提下,前i栋房子中最多能偷多少金币
- f[i][0]=max(f[i-1][0],f[i-1][1]),因为不偷房子i-1,所以房子i-2可以选择偷或不偷
- f[i][1]=f[i-1][0]+A[i-1],偷房子i-1,房子i-2必须不偷
- 简化
- 在不偷房子i-1前提下,前i栋房子中最多能偷多少金币
- 其实这就是前i-1栋房子最多能偷多少金币
- 所以我们可以简化前面的表示
- 设f[i]为窃贼在前i栋房子最多偷多少金币
C.初始条件和边界情况
- 设f[i]为窃贼在前i栋房子最多偷多少金币
- f[i]=max{f[i-1],f[i-2]+A[i-1]}
- 初始条件
- f[0]=0(没有房子,偷0枚金币)
- f[1]=A[0]
- f[2]=max{A[0],A[1]}
- 序列型动态规划好处…初始条件简单
D.计算顺序
- 初始化f[0]
- 计算f[1]、f[2]、…、f[n]
- 答案f[n]
- 时间复杂度O(N),空间复杂度O(1)[可以开个长度为2的数组]
- 可以用递归解,但是需要记忆化,开个数组
E.代码
public long houseRobber(int[] A) {
if(A==null||A.length==0){
return 0;
}
long [] dp = new long[A.length+1];
dp[0]=0;
dp[1]=A[0];
for(int i=2;i<=A.length;i++){
dp[i]=Math.max(dp[i-1],dp[i-2]+A[i-1]);
}
return dp[A.length];
}
5.例4(LintCode 534 House Robber II)
- 题意
有一圈N栋房子,房子i-1里有A[i]个金币,一个窃贼想选择一些房子偷金币,但是不能偷任何挨着的两家邻居,否则会被警察逮住,最多偷多少金币 - 例子
- 输入:A={3,8,4}
- 输出:8(只偷房子1的金币)
A.思考
- 这题和House Robber非常类似,只是房子现在排成一个圈
- 于是房子0和房子N-1成了邻居,不能同时偷(用上道题做法,房子0偷没偷没有记录)
- 要么没偷房子0,要么没偷房子N-1,我们可以枚举窃贼是没有偷房子0还是没有偷房子N-1
- 情况1:没偷房子0(删掉0),最优策略就是窃贼对于房子1-N-1的最优策略->化为House Robber
- 情况2:没偷房子N-1,最优策略就是窃贼对于房子0-N-2的最优策略->化为House Robber
- 圈情况比序列复杂,但是,通过对于房子0和房子N-1不能同时偷原理,进行分情况处理,经过处理,变成序列情况,问题迎刃而解