最近一直在准备秋招,也一直在研读各种常见算法,对于动态规划,这几乎是各种笔试必考的内容。看了相关书籍和相关博客后,对动态规划有了一些感悟,特此记下,以备后续查看。
首先根据一道典型的动态规划的题目,给出各种常见的解法与代码实现,最后对动态规划的解题思路做一个简单的总结。
笔试题目:甲同学负责一次活动礼品采购,每一款礼品的受欢迎程度(热度值)各不相同,现给出总金额以及各礼品的单价和热度值,且每个礼品只采购一个,如何购买可以使得所有礼品的总热度值最高。
输入:
第一行是一个正整数,表示总金额(不大于1000);
第二行是一个长度为n的正整数数组,表示礼品单价(n不大于100);
第三行是一个长度为n的正整数数组,表示对应礼品热度值。
输出:一个正整数,表示可获得的最高总热度值。
样例输入:1000
200 600 100 180 300 450
6 10 3 4 5 8
即已知 capcity=1000
礼品单价数组为:weight[6]={200,600,100,180,300,450}
礼品价值数组为:value[6]={6,10,3,4,5,8}
对于动态规划而言,有两种不同的解题思路:1)自顶向下的备忘录法;2)自底向上的动态规划
(1)自顶向下的备忘录法
这种方法利用的递归的思想,并通过把已经计算出的某个子问题的最优解存在备忘录里,在计算下一个子问题的最优解之前,首先检查备忘录里是否已经有了该子问题的最优解,这样避免了重复的运算。这样的备忘录可以用散列表,二叉搜索树等表示,这里我们用数组来表示。
这里用二维数组表示备忘录,int y[6][1001] ,存储的是每个子问题对应的最优解。
for (int i = 0; i < 6; i++)
{
for (int j = 0; j < 1001; j++)
y[i][j] = -1;
}
key1:把weight[5]当成顶,即y[i][j]表示剩余容量为j,剩余物品为n,n-1,...,i的背包问题的最优解的值。
即有初始状态的值为:
状态转移方程为:
由此可得代码实现为:
int top_to_button_1(int i,int capcity)
{
if (y[i][capcity] > 0)
{
return y[i][capcity];
}
else
{
if (i == 0)
{
y[i][capcity] = (weight[i] > capcity ? 0 : value[i]);
return y[i][capcity];
}
if (weight[i] > capcity)
{
y[i][capcity] =top_to_button_2(i-1,capcity);
return y[i][capcity];
}
else
{
y[i][capcity] = max(top_to_button_2(i-1,capcity),top_to_button_2(i-1,capcity-
weight[i])+value[i]);
return y[i][capcity];
}
}
}
key2: 把weight[0]当成顶,即y[i][j]表示剩余容量为j,剩余物品为i,i+1,...,n的背包问题的最优解的值。
即有初始状态的值为
状态转移方程为
由此可得代码实现为
int top_to_button_2(int i,int capcity)
{
if (y[i][capcity] > 0)
return y[i][capcity];
else
{
if (i == numberindex)
{
y[i][capcity] = (weight[i] > capcity ? 0 : value[i]);
return y[i][capcity];
}
if (weight[i] > capcity)
{
y[i][capcity] =top_to_button_1(i + 1, capcity);
return y[i][capcity];
}
else
{
y[i][capcity] = max(top_to_button_1(i + 1, capcity),top_to_button_1(i +
1, capcity - weight[i]) + value[i]);
return y[i][capcity];
}
}
}
(2)自底向上的动态规划
通过确定初始状态的值,并通过递推来求解其余子问题对应的最优解。
key1:把weight[0]当成底
即有初始状态的值为
状态转移方程为
由此可得代码实现为
int button_to_top_1(int capcity)
{
for (int i = capcity; i >= 0; i--)
{
if (weight[0]<i)
{
y[0][i] = value[0];
}
else
y[0][i] = 0;
}
for (int i=1;i<6;i++)
{
for (int j = capcity; j >= 0; j--)
{
if (weight[i] < j)
{
y[i][j] = max(y[i - 1][j], y[i - 1][j - weight[i]] + value[i]);
}
else
y[i][j] = y[i - 1][j];
}
}
return y[5][1000];
}
key2: 把weight[5]当成底
即有初始状态的值为
状态转移方程为
由此可得代码实现为
int button_to_top_2(int capcity)
{
for (int i = capcity; i >= 0; i--)
{
if (weight[5]<i)
{
y[5][i] = value[5];
}
else
y[5][i] = 0;
}
for (int i = 4; i >= 0; i--)
{
for (int j = capcity; j >= 0; j--)
{
if (weight[i] < j)
{
y[i][j] = max(y_1[i + 1][j], y[i + 1][j - weight[i]] + value[i]);
}
else
y[i][j] = y[i + 1][j];
}
}
return y[0][1000];
}
key3: 还可以在上面的基础上,进一步对空间进行缩减,即有一维数组表示备忘录 int y[1001]
for (int i = 0; i < 1001; i++)
{
y[i] = 0;
}
首先针对第一个物品,或者最后一个物品,初始化y[1001],然后依次根据每一个物品,更新y[1001]。
注意:针对每一个物品,进行y[1001]更新时,应该从y[capcity]更新,这样能保证y[capcity-weight[i]]是上一个状态所产生的最优解,否则上一个状态产生的最优解会被覆盖。
初始状态的值为
状态转移方程为
由此可得代码实现为
int button_to_top_3(int capcity)
{
for (int i = capcity; i >= 0; i--)
{
if (weight[0] < i)
{
y[i] = value[0];
}
else
y[i] = 0;
}
for (int i = 1; i < 6; i++)
{
for (int j = capcity; j >= 0; j--) //注意!!
{
if (weight[i] <j)
{
y[j] = max(y[j], y[j - weight[i]] + value[i]);
}
}
}
return y[1000];
}
上述就是典型的动态规划常见的解题思路和对应的代码实现。
通过这道题,对动态规划作了以下总结
1. 将原问题分解为子问题
(1)把原问题分解若干个子问题,子问题与原问题形式相同或类似,子问题都解决了,原问题即解决。
(2)确定状态
将和子问题相关的各个变量的一组取值称之为一个“状态”,一个“状态”对应于一个或多个子问题,某个“状态”下的“值”就是这个”状态“所对应的子问题的解。
(3)确定一些初始状态的值
(4)确定状态转移方程
找出不同状态之间如何转移----即如何从一个或多个”值“已知的"状态”,求出另一个“状态”的“值”。状态的转移可以用递归公式表示。
2.动态规划常用的两种形式
(1)自顶向下的备忘录法
即建立一个备忘录,将已经计算出的“值”放入备忘录中,当要计算下一个值时,先检查备忘录中是否已有该“状态”对应的“值”,如果已经存在,则直接从备忘录中取出该值,否则将计算后的“值”加入到备忘录中。
(2)自底向上的动态规划
即首先计算出初始状态的值,再根据递推计算出其余状态的值。