浅谈动态规划(二)——进阶题

上一次的动态规划的文章中(浅谈动态规划(一)——从lintcode刷题入门),借助lintcode上两个简单且典型的例子(Triangle和Climbing Stairs),阐述了动态规划的两个特点:1)最优子结构,和2)子问题重叠。这回来讲讲最近做lintcode上的几个难度稍微大一些的题目。这些题目也很符合动态规划的那两个特点。

先来看看第一个题目。

1.Paint Fence(栅栏染色)

题目描述:
There is a fence with n posts, each post can be painted with one of the k colors. You have to paint all the posts such that no more than two adjacent fence posts have the same color. Return the total number of ways you can paint the fence.
Notice: n and k are non-negative integers.

例子:
Given n=3, k=2 return 6

     post 1, post 2, post 3
way1    0      0       1
way2    0      1       0
way3    0      1       1
way4    1      0       0
way5    1      0       1
way6    1      1       0

题目中要求给n个栅栏涂颜色,有k种颜色可以选择,限制条件是最多可以有两个相邻的栅栏颜色相同。

解题思路:
令ways[i]( n = 1, 2, 3, ..., n )表示涂到第i个栅栏时的所有的方法数。这样,题目就符合动态规划的两个特征(参考之前的那篇文章分析),就可以用动态规划求解。那么我们在涂第i个栅栏的时候,有两种涂色方式:1)和第i-1个栅栏的颜色相同;2)和第i-1个栅栏的颜色不同。在1)方式下,因为要保证最多只有两个相邻的栅栏颜色相同,所以第i个栅栏的颜色和第i-2个栅栏的颜色必然是不同的,那么此时有ways[i-2] * (k-1)中填涂的方式。在2)方式下,不需要考虑之前的栅栏的颜色,只要保证和i-1个栅栏颜色不同即可,此时有ways[i-1] * (k-1)中方法。所以有状态转移方程:

ways[i] = ways[i-2] * (k-1) + ways[i-1] * (k-1)

同时发现,每次只要用到相邻的3个栅栏的方法数就可以了,这样可以减少空间的使用。所以代码为:

int numWays(int n, int k) {
	// Write your code here
	if (n == 0 || k == 0)
		return 0;
		
	if (n == 1)
		return k;
	if (n == 2)
		return k * k;
		
	int ways[3] = {0}; // 辅助数组,只使用3个变量即可
	ways[0] = k;
	ways[1] = k * k;
		
	// 动态规划步骤
	for (int i = 2; i < n; i++)
	{
		ways[2] = (k-1) * (ways[0] + ways[1]); // 依据相邻的两个栅栏的填涂方法数来求解第3个栅栏的填涂方法数
		ways[0] = ways[1];
		ways[1] = ways[2];
	}
		
	return ways[2];
}

这道题虽然也是简单的,但是因为限制条件的运用比较巧妙,所以给贴出来了。接下来看看下一题。

2.House Robber(打劫房屋)

题目描述:
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

例子:
Given [3, 8, 4], return 8.

题目要求从n个房屋中找出一些房屋取走里面财产,找到财产最大的总值,限制条件是不能有相邻的房屋同时被偷盗。

这道题虽然是道中等题,不过思路也很清晰,动态规划的两个特征也非常明显。令money[i]( i = 1, 2, ..., n )表示在偷第i个房屋时的最大值,那么在偷第i个房屋时,需要比较的只是money[i-1]和money[i-2] + A[i]值的大小(A[i]是第i个房屋的财产值),因为相邻的两个房屋不可能同时被偷。所以,状态转移方程为:

money[i] = max(money[i-1], money[i-2] + A[i])

同样,我们发现每次比较时,只需要保留最近两次的最大值即可,那么优化后的代码为:

long long houseRobber(vector<int> A) {
	// write your code here
	int n = A.size();

	if (n == 0)
		return 0;
	
	if (n == 1)
		return A[0];
	
	// 辅助数组
	long long money[3] = {0};
	money[0] = 0;
	money[1] = A[0];
	
	// 动态规划步骤
	for (int i = 2; i <= n; i++)
	{
		money[2] = max(A[i-1] + money[0], money[1]);
		money[0] = money[1];
		money[1] = money[2];
	}
	
	return money[2];
}

最后来看第3题。

3.Paint House(房屋染色)

题目描述:
There are a row of n houses, each house can be painted with one of the three colors: red, blue or green. The cost of painting each house with a certain color is different. You have to paint all the houses such that no two adjacent houses have the same color.

The cost of painting each house with a certain color is represented by a n x 3 cost matrix. For example, costs[0][0] is the cost of painting house 0 with color red; costs[1][2] is the cost of painting house 1 with color green, and so on... Find the minimum cost to paint all houses.

Notice: All costs are positive integers.

例子:

Given costs = [[14,2,11],[11,14,5],[14,3,10]] return 10

house 0 is blue, house 1 is green, house 2 is blue, 2 + 5 + 3 = 10

题目要求给n个房子粉刷颜色,找到最小的费用。粉刷时有3种颜色可供选择,而粉刷不同的颜色的费用是不同的,限制条件是相邻的两个房子的颜色不能相同。这道题目其实和上一道题目有点类似,均为相邻的两个有限制,不过这个在动态规划的时候,比较的是粉刷的不同的颜色。同样的方法,令value[i][j](i = 1, 2, ..., n; j = 1, 2, 3)表示正在粉刷第i个房子的时候颜色为j时的总费用(之前粉刷的位置均保证了费用最小)。为了避免相邻的两个房子粉刷的颜色相同,在粉刷每一层的时候,还要和前一层粉刷的颜色比较。那么在粉刷第i个房屋颜色为j时的状态转移方程为:

value[i][j] = min(costs[i][j] + value[i-1][k]), k = 1, 2, 3; k != j

上式中k是上一层粉刷不同颜色到达最小值时的不同颜色。在粉刷时要保证相邻的两层颜色不相同,所以k != j。

有了状态转移方程,题目的代码就好写了:

int minCost2(vector<vector<int> > &costs)
{
	int n = costs.size();

	if (n == 0)
		return 0;
	
	int i, j, k;
	int temp;
	int minValue = INT_MAX;
			
	for (j = 0; j < 3; j++) 
            if (minValue > costs[0][j])
	        minValue = costs[0][j];
					   
	if (n == 1)
		return minValue;

	vector<vector<int> > value(n, vector<int>(3, INT_MAX)); // 辅助数组,初始化时每个位置均为INT_MAX

	// 动态规划步骤
	for (i = 0; i < n; i++)
	{
		if (i == 0)
		{
			for (j = 0; j < 3; j++)
				value[i][j] = costs[i][j];

			continue;
		}

		// 找出相邻两层不相同时,粉刷不同颜色的最小值
		for (j = 0; j < 3; j++)
		{
			minValue = INT_MAX;
			for (k = 0; k < 3; k++) // 上一层粉刷不同颜色时对应的最小值
			{
				if (k == j) // 跳过相同颜色
					continue;
				// 找出最小值
				temp = costs[i][j] + value[i-1][k];
				if (temp < minValue)
					minValue = temp;
			}
			value[i][j] = minValue;
		}
	}

	// 找出粉刷完最后一个房屋时的最小费用
	minValue = INT_MAX;
	for (j = 0; j < 3; j++) 
            if (minValue > value[n-1][j])
		minValue = value[n-1][j];

	return minValue;
}

同样,代码也可以优化以减少空间消耗,在这里就不再将最后结果贴出来了。

好了,这就是这次的全部内容。这些题目是我在刷lintcode中遇到的动态规划特征比较明显的题目,且比《浅谈动态规划(一)——从lintcode刷题入门》里面的一些题目稍微难点。希望能够给初接触动态规划的童鞋一点参考。

参考资料:
lintcode
1.Paint Fence(栅栏染色)
2.House Robber(打劫房屋)
3.Paint House(房屋染色)

浅谈动态规划(一)——从lintcode刷题入门

动态规划是算法中比较常见的一种分析问题的方式,最近看算法看到这,带着lintcode上面的题目,在这写个总结。

首先从一个lintcode上面简单的题目开始。

Climbing Stairs(爬楼梯)

题目描述:
假设你正在爬楼梯,需要n步你才能到达顶部。但每次你只能爬一步或者两步,你能有多少种不同的方法爬到楼顶部?

样例:
比如n=3,1+1+1=1+2=2+1=3,共有3中不同的方法,所以返回 3。

这是一道非常具有代表性的动态规划的一道问题。

令f[n]记录到达第n阶台阶的方法数。假设你还有最后一步就能到达第n阶台阶,那么最后一步,你可以选择两种方式:1)踏一步以及,2)踏两步。如果是踏一步,那么你要从第n-1阶台阶踏出,此时的方法数是 f[n-1];如果是踏两步,那么你要从第n-2阶台阶出发,此时的方法数是f[n-2]。所以爬到第n级台阶的方法总数有f[n] = f[n-1] + f[n-2]种。状态转移方程:

f[n] = f[n-1] + f[n-2]

这样可以写出解答的递归形式:

int climbStairs(int n) {
	// write your code here
	if (n <= 1)
		return 1;
	else 
		return climbStairs(n-1) + climbStairs(n-2);
}

此时我们发现可以用一个一维数组来记录之前出现的结果,这样可以省去递归中不必要的空间开销:

int climbStairs(int n) {
	// write your code here
	vector<int> f(n+1);

	f[0] = 1;
	f[1] = 1;

	for (int i = 2; i <= n; i++)
		f[i] = f[i-1] + f[i-2];

	return f[n];
}

其实,这个还可以进一步优化,因为每一步都只需要前两个台阶的结果,所以可以用两个变量来代替一维数组。

int climbStairs(int n) {
	// write your code here
	if (n == 1 || n == 0)
		return 1;
	
	int ways1 = 1;
	int ways2 = 1;
	int ways = 0;
	
	int i = 2;
	while (i <= n)
	{
		ways = ways1 + ways2;
		
		ways1 = ways2;
		ways2 = ways;
		
		i++;
	}
	
	return ways;
}

接下来,我们再来看一题。

Triangle(数字三角形)

题目描述:
给定一个数字三角形,找到从顶部到底部的最小路径和。每一步可以移动到下面一行的相邻数字上。

样例
比如,给出下列数字三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

从顶到底部的最小路径和为11 ( 2 + 3 + 5 + 1 = 11)。

分析问题,我们发现可以用二维数组来计算到最底层每个位置的最小路径和,令第i层下标为j的位置上最小路径和为f[i][j]。那么,第i层下标为j位置的值f[i][j],只和i-1层位置j-1下标的最小路径和f[i-1][j-1]以及j位置的最小路径和f[i-1][j]有关。即,f[i][j] = min(f[i-1][j-1],  f[i-1][j])。所以状态转移方程为:

f[i][j] = min(f[i-1][j-1],  f[i-1][j])

同时,发现每次的结果只与上面一层的结果有关,所以省去二维数组,只用一维数组作为辅助。代码为:

int minimumTotal(vector<vector<int> > &triangle) {
	// write your code here
	if (triangle.size() == 0)
		return 0;
		
	vector<int> f(triangle[triangle.size() - 1].size());
	
	// 动态规划过程
	f[0] = triangle[0][0];
	for (int i = 1; i < triangle.size(); i++) 
        { 
                for (int j = triangle.size()-1; j >= 0; j--)
		{
			if (j == 0)
				f[j] = f[j] + triangle[i][j];
			else if (j == triangle[i].size()-1)
				f[j] = f[j-1] + triangle[i][j];
			else
				f[j] = min(f[j-1], f[j]) + triangle[i][j];
		}
	}
	
	// 从辅助数组中找出最小值
	int result;
	result = f[0];
	for (int i = 1; i < f.size(); i++)
		result = min(result, f[i]);
		
	return result;
}

分析这两道题目,发现它们有两个共同的特征:
1.最优子结构:一个问题的最优解包含其子问题的最优解。
2.子问题重叠:一个问题的子问题都是相同的。那么这些子问题可以用同样的方式解决,每次将解决的子问题给记录到表中,在计算之后的问题时查找前面的结果来计算当前问题。这也就是“动态规划”名称的由来了。

在climbing stairs问题中,到达每个位置的方法个数,和之前的所有方法是相关联的。即,到达第n阶台阶的所有的方法个数,同时需要找出第n-1阶台阶的所有的方法以及第n-2阶台阶的所有的方法,以此类推。如果之前的问题找出的不是所有的方法数,那么第n阶台阶找出的结果也不是所有的方法数目。而且,第n阶台阶的求解方式和之前的台阶的求解方式是相同的。

在triangle问题中,每个位置的最小路径和需要知道前一层此位置和下一个位置的最小路径和,如果前一层的结果不是最小路径和,那么此层求出的就不是最小路径和。同样,每层问题的求解方式和之前层问题的求解方式是相同的。

所以,这也就是《算法导论》一书中表述的动态规划的两个特征:
1.最优子结构
2.子问题重叠

参考资料:

1.lintcode
Triangle
Climbing Stairs

2.《算法导论(第三版)》。
第15章动态规划