动态规划初解
上一篇讲述了我们改如何去做动态规划的题,现在就让我们将按照上一篇的步骤,拿算法与设计课的作业来练练手吧。
一、问题描述
有一个专业的推销员,计划在一条街道上门推销商品。每栋房子都会购买一定数量的商品,但是如果本栋房子主人购买商品后,他的邻居就不会购买该商品。如果推销员向两个相邻的邻居同时推销该商品,则违反推销规则,推销失败。
输入:给定一个代表住户购买商品的非负整数列表
输出:确定不触发推销失败规则的情况下,输出推销员可以推销出的最大商品数量。
示例:
Input: [2,2,5,1]
Output: 7
二、答案要求
- 要求采用动态规划方法解决该问题
- 要求给出递推方程,并给出分析过程
- 学生自己提供示例,并给出对应的推销备忘录(保存中间最优结果)与推销标记表格
三、思路分析
在本题中,我们按顺序将住户购买的商品数量存放在一个数组arr[ ] 中方便我们使用。
(1)定义数组及其含义
定义一个数组dp[ ] 用来存放递推所需的数据,在其中dp[0] 代表的时推销到第1户住户时,卖出去的最大商品数,dp[i]的含义就是:推销到第i+1户住户时能卖出去的最大商品数。
(2)找出数组元素之间的关系式(递推方程)
第i户住户是否购买商品取决于上一家住户是否已经购买了,所以这有两种情况:
- A:上一家( i - 1 )住户已经购买了商品,所以第i户住户就不能再买了,此时总的商品购买量就为dp[i-1];
- B:上一家( i - 1 )住户没有购买商品,所以第i户住户任然可以进行购买,此时总的商品购买量就为dp[i-2] + arr[i];
因为我们找的时最大的商品购买量,所以我们需要在这两种情况中选商品购买最多的那一个,即:
dp[i] = max{ dp[i-1] ,dp[i-2] + arr[i] }
这也就是我们的递推方程。
(3)给定初始条件和边界情况
不难看出,dp[0]和dp[1]不能从该递推方程中得出,所以 i 的取值范围就是[2,arr.length-1]。
我们再来给dp[0]和dp[1]赋值:
当推销到第一户住户时,最大购买量就是此住户的购买量,即dp[0] = arr[0];
当推销到第二户住户时,因为一户购买了,另一户就无法购买了,所以最大推销量就是取二者的购买量大的一方,即dp[1] = max{ arr[0],arr[1] }。
(4)总函数
综上所述,我们就可以得到下面这个函数:
我们的目标函数是:dp[arr.length - 1]
四、代码实现(Java)
图个方便,就不从用户输入接收数组arr了,直接在代码前new一个,感兴趣的可以自己在前面写一个读入的步骤。
//无标记版
int[] arr = {2,2,5,1,9,5,6,7};
int[] dp = new int[arr.length];
dp[0] = arr[0];
dp[1] = arr[0] > arr[1] ? arr[0] : arr[1];//三元运算符
int max = 0;
for(int i = 2; i < arr.length; i++) {
dp[i] = dp[i-1] > dp[i-2]+arr[i] ? dp[i-1] : dp[i-2]+arr[i];//三元运算符
}
System.out.println(dp[arr.length - 1]);
运行一下:
23 //7+9+5+2=23
可以看到得出了正确答案,说明这个算法还是比较成功的。
我们来简单看一下过程是怎样进行的:
i=0: dp[0] = arr[0];
i=1: dp[1] = max{ arr[0],arr[1] } = max{2,2} = 2;
i=2: dp[2] = max{ dp[2-1] ,dp[2-2] + arr[2] } = max{2,2+5} = 7;
i=3: dp[3] = max{ dp[3-1] ,dp[3-2] + arr[3] } = max{7,2+1} = 7;
以此类推,我们就能得到每个dp[i]:
最后这个dp[arr.length - 1]也就是我们需要的最大商品购买量了。
五、指示函数
(1)定义数组及其含义
虽然已经输出了正确的最大商品数,但是我们还不知道是那几户购买了商品,所以我们需要对其增加一个标记用的数组 s[ ]。
其中s[i] 的含义就是到第i户时,上一个购买商品的住户是第几户。
(2)找出数组元素之间的关系式(递推方程)
而如何确定上一个购买的用户是第几户,我们需要函数完成dp[i] = max{ dp[i-1] ,dp[i-2] + arr[i] } 的判断后才能知道:
如果dp[i-1] > dp[i-2] + arr[i],那么上一户购买商品的就是第i-1户;
如果dp[i-1] <= dp[i-2] + arr[i],那么上一户购买商品的就是第i-2户。
所以我们就可以得到这样一个函数:
(3)给定初始条件和边界情况
取值范围:[2,arr.length-1]
对于s[0] 因为它前面就没有住户了,我们可以为其初始化为0;
对于s[1], 我们判断arr[0]和arr[1]的大小来判断:
如果arr[0] > arr[1] , s[1] = 0
如果arr[0] <= arr[1] , s[1] = 1
(4)实现代码(Java)
//有标记版
int[] arr = {2,2,5,1,9,5,6,7};
int[] dp = new int[arr.length];
int[] s = new int[arr.length];
dp[0] = arr[0];
s[0] = 0;
if(arr[0] > arr[1]) {
dp[1] = arr[0];
s[1] = 0;
}else {
dp[1] = arr[1];
s[1] = 1;
}
int max = 0;
for(int i = 2; i < arr.length; i++) {
if(dp[i-1] > dp[i-2]+arr[i]) {
dp[i] = dp[i-1];
s[i] = i-1;
}else {
dp[i] = dp[i-2]+arr[i];
s[i] = i-2;
}
}
System.out.println(dp[arr.length - 1]);
for(int j = arr.length - 1; s[j] >= 0 ;) {
if(j - s[j] == 2) {
System.out.print(arr[j] + "+");
}
if(j == 0 || j == s[j]) {
System.out.print(arr[j]);
break;
}
j = s[j];
}
执行一下:
23 //7+9+5+2=23
7+9+5+2
成功执行,我们还是来看一下进行的过程:
i=1: s[0] = 0;
i=1: arr[0] <= arr[1], 所以s[1] = 1;
i=2: dp[2-1] < dp[2-2] + arr[2],所以s[2] = i – 2 = 2 – 2 = 0
i=3: dp[3-1] < dp[3-2] + arr[3],所以s[3] = i -1 = 3 -1 = 2
以此类推,即可得到所有的s[i]:
可以看出当 j – s[j] == 2 时,说明这一户住户是购买了商品的,打印出来,此时 j 的值变动为s[j], 只需依次打印符合 j – s[j] == 2的值即可。
我们当然也可以稍微改动下打印的语句来得到更详细的信息:
好的,到此为止,一道动态规划的题就被我们解决了
补充:
上课的时候老师问了下,这个指示函数打印为什么是当 j – s[j] == 2 时进行输出,因为原本确实是找规律看出来的,后来想了一下,也是有些许道理在里面的:
在(2)的函数中我们可以发现:
当i>2时, s[i] 与 i 的关系只有两种:s[i] = i-1
或者s[i] = i-2
:
在(1)中我们也说了s[i] 的含义就是到第i户时,上一个购买商品的住户是第几户。
因为推销规则中说到相邻的两户是不能一起买的,所以当s[i] = i-1时,说明上一个购买商品的就在前一户(i-1),所以这户(i)明显是没有进行购买的,我们无需输出。
只有当上一个购买的用户是i-2户时(即s[i] = i-2),才是真正购买的住户位置,或者说,此户(i)的购买才是有效的。
当i<=2时, s[0]恒为0
,s[1]为1或者为0
。
s[1] = 1时,说明购买商品的是第二户,此时的s[1] == 1;
s[1] = 0时,说明第一户是购买了的,我们应该输出第一户,此时s[0] == 0。
所以当 i<=2时,我们只需要输出s[i] == i 的住户即可。
以上内容谨为个人学习过程的记录,欢迎大家一起学习和指正