【LeetCode】010 番外篇 Dynamic Programming 动态规划

【例一:最优路径】


现有表格如上,从坐上走到右下(不重复不回头),每步累加,求累加之和最大是多少。

解:

首先将最左和最上进行依次的累加。


再对空白部分进行填充,红色空格代表当前空格,红格对应表1的数字(如B2=9),分别加上上方数字13以及左方数字15,取两者中较大。即max(13+4,15+4)=19,填充进去19,如此完成接下来的操作。


综上,按照红色的路径走,就可以得到最大值——68。


【例二:背包问题】

设一个背包可以承重W,现有n个物体,每个物体分别重w1,w2,w3……wn,分别价值为v1,v2,v3……vn。问这个背包可以装下了的物体最大价值是多少?


如上图,有a,b,c,d,e五个物体,重量分别为7,7,6,8,2,价值分别为9,5,7,1,10,背包总重W=15。

从第二行开始进行计算,对物体a进行判定,若可以背包可以放下a,则放下a并且记录总价值。

再计算第三行,对物体b进行判定,当背包重量足以承受b时,进行一次判断:是否放物体b?

判断依据为:

1. 放下物体b,背包总承重-b重量,在第二行找到其对应的价值,再加上b的价值,即为当前价值。如,列表中R3单元格,此时背包总重量为14,若放下物体b,背包承重为14-7=7,找到第二行对应7的价值,即K2=9,则此时总价值为5+9=13。

2.不放物体b,直接继承上一行的价值。

以上两者,取较大值填写。


【小结】

如此,我们每对一个物体进行一次判断,都会记录当前的最优方案,而对下一个物体的判断,都是建立在之前最优判断的基础上。此为动态规划的核心内容。

动态规划,跟我之前了解的递归算法,嵌套循环的思路有点类似。都是在上一次运算的基础上,进行下一次的运算。比较典型的就是汉诺塔的算法,然而递归这种方法耗时很长,在编程时应当尽量避免。因此我们多采用,以空间换取时间的方法,来提高程序运行的速度。


—————————————————————————————分割线———————————————————————————————


以下两个例子,对比了贪心算法和动态规划,可以看出各自的优劣势。


【例三:原始计算器】

给定一个数字n,让你使用3种方法从1计算到n,分别是“+1”、“X2”、“X3”,输出最短的步数,以及此时到达n的过程(步数最短的方法可能不止一种,输出任意一种即可)


思路:从1到n实际上与n到1的过程是一样的,因此我们从n往前分析会容易一些,原来的“+1”、“X2”、“X3”变成了“-1”、“÷2”、“÷3”。

利用贪心算法的思路,如何从n到1最快?能被3整除就除以3,如果不能就判断能不能被2整除,可以就除2,不行就减1。如此似乎可以得到最短的步数。然而会出现一个问题,我们无法证明这种思路的正确性。例如,当n=10时,按照上述的思路应当是优先除以2,得到的路径是10,5,4,2,1,需要4步,然而如果优先减1的话,路径是10,9,3,1,只需要3步,因此这种思路是存在一定的缺陷的。

这种缺陷源于贪心算法本身,利用这种方法可以很快的得到局部最优解,但始终无法获得全局最优解。因此我们需要在这种思路上进行改进,于是这里引入了动态规划的算法。

我们从1开始一个数字一个数字向后推,以d(n)作为n到步数step的映射。即:

d(1) = 0;

d(2) = 1;

d(3) = min { d(3-1) + 1, d(3/2) + 1, d(3/3) +1 } = min { d(2) + 1, d(1) + 1 } = min { 2 , 1 } = 1;

d(4) = min { d(4-1) + 1, d(4/2) + 1, d(4/3) + 1} = min { d(3) + 1 , d(2) + 1} = 2;

d(5) = min { d(5-1) + 1, d(5/2) + 1, d(5/3)  + 1} = min { d(4) +1 } = 3;

………………

因此,我们采用地策略就是,从1到n先遍历一遍,计算并保存到达每个数字所需要的最小步数,以及最后一步的方法(加1乘2乘3),将这些信息保存在一个二维的数组当中,再后来的运算中调用数组中的数字,避免了嵌套的使用,节省了计算的时间。

程序如下:

#include "stdafx.h"
#include <iostream>
#include <vector>
#include <algorithm>

using std::vector;

vector<int> optimal_sequence(int n) {
	
	std::vector <std::vector<int>> d(n + 1, vector<int>(2, 0));
	int step1, step2, step3,min_step;

	for (int i = 2; i <= n; i++)
	{
		step1 = step2 = step3 = i;
		
		if (i % 3 == 0)
			step3 = i / 3;
		if (i % 2 == 0)
			step2 = i / 2;
		step1 = i - 1;
		
		//不能被整除的数是不需要带入min_step中进行计算
		//因此给他们赋上step1的值,保证min_step的计算正常进行
		if (step3 == i)
			step3 = i - 1;
		if (step2 == i)
			step2 = i - 1;

		min_step = std::min(d[step1][0], std::min(d[step2][0], d[step3][0])) ;		

		d[i][0] = min_step + 1;
		
		//这里必须先判断是否为1,因为可能之前出现2和3无法整除,强行被赋了step1的值
		//导致可能会将+1的步骤错当成了×2或×3
		
		if (min_step == d[step1][0])
			d[i][1] = 1;
		else if (min_step == d[step2][0])
			d[i][1] = 2;
		else if (min_step == d[step3][0])
			d[i][1] = 3;
		
	}

	std::vector<int> sequence;
	while (n >= 1)
	{
		sequence.push_back(n);

		if (d[n][1] == 3)
			n /= 3;
		else if (d[n][1] == 2)
			n /= 2;
		else
			n -= 1;		
	}
	reverse(sequence.begin(), sequence.end());

	return sequence;
}


int main() {
	int n;
	std::cin >> n;
	
	vector<int> sequence = optimal_sequence(n);

	std::cout << sequence.size() - 1 << std::endl;

	for (size_t i = 0; i < sequence.size(); ++i) {
		std::cout << sequence[i] << " ";
	}
	
	while (1);
}

【例四:拿金块】

这题与背包问题类似,是背包问题的简化。

要求输入背包最大承载W,待取金块数量n,以及他们各自的重量w[1],w[2],w[3]……w[n],输出该背包可以装下金块的最大重量。


根据贪心算法,应当先拿最重的金块,但与上一例中类似很有可能无法拿到全局最优解。因此我们使用动态规划来解这一题。

由于金子的单位质量的价值是一定的,因此此题可以理解为w[i] = v[i] 的背包问题,这样思路就和例二中一样了。

程序如下:

#include "stdafx.h"
#include <iostream>
#include <vector>
#include <algorithm>


using namespace std;
using std::vector;


int optimal_weight(int W, const vector<int> &w) {
  //write your code here
	vector<int> pre_val(W + 1, 0);
	vector<int> cur_val(W + 1, 0);

	for (size_t i = 0; i < w.size(); ++i)
	{
		for (int j = 0; j <= W; ++j)
		{
			if (w[i] > j)
				continue;
			cur_val[j] = std::max(pre_val[j], w[i] + pre_val[j - w[i]]);
		}
		for (int j = 0; j <= W; ++j)
			pre_val[j] = cur_val[j];
	}
	return cur_val[W];
}

int main() {
  int n, W;
  std::cin >> W >> n;
  vector<int> w(n);
  for (int i = 0; i < n; i++) {
    std::cin >> w[i];
  }               
  std::cout << optimal_weight(W, w) << '\n';
}


【总结】

贪心算法的思路简单粗暴,在数据量庞大以及对最优解得需求不那么苛刻的时候,应当是最优选择,好比我是个走进金库的小偷,时间紧任务重,我可不会一步一步推理找到那个最优,警察都来了好么,当然是先拿大的,再拿剩下来当中我能拿得动的最大的。

当然在时间和空间资源允许的条件下,全局最优当然是我们期望的。然而就算处理器再强大也犯不着用递归嵌套来完成,于是我们就利用了空间换区时间的方法,以最少的资源获得最优的解。

自己写的程序,没有其他可以参考的对象,自知可以改进的地方还有很多,如有不足之处欢迎指正批评。


【参考资料】

动态规划:

http://www.360doc.com/content/13/0601/00/8076359_289597587.shtml

背包问题:

http://blog.csdn.net/mu399/article/details/7722810/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值