写在前面
国科大计算机算法分析与设计中动态规划算法部分,主要是一些作业题。这些题目也是动态规划方面比较经典的题目。记得上课的时候老师讲动态规划的核心就是一个多步决策过程,当时是以炮弹打飞机举例,是军事上的一个真实案例,炮弹的数量是有限的,飞机是一架一架飞的,飞机的类型不同,需要的炮弹和收益是不一样的,每一步需要决策是打还是不打。现在想想,这不是和背包问题很像吗,这个是20世纪50年代的现实问题,自此动态规划算法得到了充分的发展。
很多地方会讲动态规划最重要的是状态转移方程,确实是这样的,但是怎么想出状态转移方程呢,这个就是涉及动态规划的本质——多步决策过程,模拟成多步决策过程,比较容易找到状态转移方程。
作业1:抢劫房子得到最大收益
抢劫犯抢房子,每个房子有固定的金额,抢劫犯不能连续抢劫两栋相邻的房子,求抢劫犯可能抢到的最高总金额。假设房子之间是成直线排列的。
解法:这个问题就是一个典型的多步决策问题,每次到一个房间,只需要决定拿还是不拿。二选一的决策。如果拿的话,隔壁房间的就不能拿,否则会触发警报,不满足约束条件;如果选择不拿,就会来到下一个房间继续进行决策,拿还是不拿。
为了分析问题,我们定义一下房间顺序,假设有n个房间,编号分别为1到n,为了叙述方便,假设从最后一个房间决定拿还是不拿,依次向前。第k个房间的钱定义为
a
[
k
]
a[k]
a[k],前k个房间能偷到的最大钱数定义为
v
a
l
u
e
[
k
]
value[k]
value[k]。如果拿了第n个房间的钱,那就拿不了第n-1个房间的钱,但是总钱数上会加上第n个房间的钱。也即是:
v
a
l
u
e
[
n
]
=
v
a
l
u
e
[
n
−
2
]
+
a
[
n
]
value[n]=value[n-2]+a[n]
value[n]=value[n−2]+a[n]
如果没有拿第n个房间的前,那么就到第n-1个房间决定拿不拿。也即是:
v
a
l
u
e
[
n
]
=
v
a
l
u
e
[
n
−
1
]
value[n]=value[n-1]
value[n]=value[n−1]
为了拿到最多的钱,只需要上面两种情况取最大值:
v
a
l
u
e
[
n
]
=
m
a
x
{
v
a
l
u
e
[
n
−
2
]
+
a
[
n
]
,
v
a
l
u
e
[
n
−
1
]
}
value[n]=max\left\{value[n-2]+a[n],value[n-1] \right\}
value[n]=max{value[n−2]+a[n],value[n−1]}
如果房子之间的排列是一个环,不是一条直线,那么只需要破环转线就行,考虑1到n-1和2到n这两种情况,这两种情况取最大值即可,最大值就是成环的时候,能够抢到的最多钱。
作业2:丑数
给定整数n,找出并返回第n个丑数。丑数是只包含质因数2、3、5的正整数。
解法:第n个丑数一定是第1个到第n-1个丑数中的某个数乘上2、3或5得到的。
定义p2p3p5三个指针,表示下一个丑数是当前指针指向的丑数乘以对应的质因数。OPT[i],表示第i个丑数,每次决策下一个丑数是由p2p3p5中的哪个指针指向的丑数乘得。更新相应指针。
作业3:二叉搜索树个数
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
输 入:数字n。
输 出:二叉搜索树的数目。
解答: 对于以1~n为节点值组成的二叉树中,1~n每个数都可以作为根节点的节点值,当以i作为根节点时,1~(i-1) 这些数位于根节点的左子树中,(i+1)~n这些数位于根节点的右子节点中,即当以i作为根节点时有2个子问题,而1~n每个数都可以作为根节点的节点值,所以总共有2n个子问题。
以(i+1)~n为节点组成二叉搜索树的数目等于以1~(n-i)为节点组成二叉搜索树的数目。
作业4:最大整除子集
给定一个正整数数组,找其中最大的一个子集,要求该集合中任意两个元素都能整除
输 入:正整数数组
输 出:符合条件的最大子集长度
解法:如果已存在某个整除子集,若新的元素可以整除该子集中的最大值,则可加入该子集。
OPT[i],表示包含第i个元素的最大整除子集的长度
决策已存在的子集能否加入第i个元素
作业5:目标和
给定一个非负整数数组,a1, a2, …, an, 和一个目标数 S。现在有两个符号 + 和 – ,并且对于数组中的任意一个整数,你都可以从 + 和 – 中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
可以看到这些数值都是正整数,需要给这些正整数添加正负号,使得结果等于target,每个正整数都只有两种情况,要么是正号,要么是负号,这和背包问题很像。因为直接求解会有负数的情况产生,因此可以把问题变形一下,也能降低复杂度,我们假设加负号的所有正整数的和为
s
u
m
n
e
g
sum_{neg}
sumneg,假设所有的正整数的和为
s
u
m
sum
sum,那么就可以得到
(
s
u
m
−
s
u
m
n
e
g
)
−
s
u
m
n
e
g
=
t
a
r
g
e
t
(sum-sum_{neg})-sum_{neg}=target
(sum−sumneg)−sumneg=target,由此可得
s
u
m
n
e
g
=
s
u
m
−
t
a
r
g
e
t
2
sum_{neg}=\frac{sum-target}{2}
sumneg=2sum−target。原问题可以转化为我们选哪些数当作加负号的,没选中的就代表加了正数,因此就变成了选不选每个数,也就是说对于每个正整数,有两个选择,一个是选,一个是不选。这就和背包问题很像了,可以建立一个二维的数组
d
p
[
]
[
]
dp[][]
dp[][],在行方向就代表了每一个数,长度就是输入数组的长度
n
u
m
s
.
l
e
n
g
t
h
(
)
nums.length()
nums.length(),在列的方向就是要计算出的目标值
s
u
m
n
e
g
sum_{neg}
sumneg,最后一个元素
d
p
[
n
u
m
s
.
l
e
n
g
t
h
(
)
]
[
s
u
m
n
e
g
]
dp[nums.length()][sum_{neg}]
dp[nums.length()][sumneg]代表了在有
n
u
m
s
.
l
e
n
g
t
h
(
)
nums.length()
nums.length()个元素的情况下,计算出目标值等于
s
u
m
n
e
g
sum_{neg}
sumneg的情况有多少种。
以数组中第k个元素为例,第k个元素的值就是
n
u
m
s
[
k
]
nums[k]
nums[k],我们假设要计算
d
p
[
k
]
[
j
]
dp[k][j]
dp[k][j],也就是在k个元素的情况下,等于j的情况数有多少种,这里对于第k个元素有两种情况,选还是不选。这里分情况讨论:
(1)当
j
<
n
u
m
s
[
k
]
j<nums[k]
j<nums[k],也就是说要找的这个目标值比第k个元素小,那第k个元素肯定不能选,因为选了第k个元素就大于了目标值j,不满足要求,所以在这种情况下,第k个元素不选。不选第k个元素,就是前k个元素产生的等于j的情况数是和前k-1个元素产生的等于j的情况数相等。那么就是:
d
p
[
k
]
[
j
]
=
d
p
[
k
−
1
]
[
j
]
dp[k][j]=dp[k-1][j]
dp[k][j]=dp[k−1][j]
(2)当
j
>
=
n
u
m
s
[
k
]
j>=nums[k]
j>=nums[k],这里对于第k个元素就有两种情况,选还是不选。如果选第k个元素,那就是说前k-1个元素加一起的值是
j
−
n
u
m
s
[
k
]
j-nums[k]
j−nums[k],那么在选第k个元素的情况下,有
d
p
[
k
−
1
]
[
j
−
n
u
m
s
[
k
]
]
dp[k-1][j-nums[k]]
dp[k−1][j−nums[k]]这么多种情况;另一种就是不选第k个元素,不选第k个元素,那就是说前k-1个元素加一起的值是
j
j
j,那么在不选第k个元素的情况下,有
d
p
[
k
−
1
]
[
j
]
dp[k-1][j]
dp[k−1][j]这么多种情况。因为最终是问总的情况数,选和不选都算情况数,所以两个情况应该是相加的。也就是:
d
p
[
k
]
[
j
]
=
d
p
[
k
−
1
]
[
j
]
+
d
p
[
k
−
1
]
[
j
−
n
u
m
s
[
k
]
]
dp[k][j]=dp[k-1][j]+dp[k-1][j-nums[k]]
dp[k][j]=dp[k−1][j]+dp[k−1][j−nums[k]]
作业6:带冷冻期的股票交易
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
• 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
• 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
(来源:https://www.programmercarl.com/0309.%E6%9C%80%E4%BD%B3%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E6%97%B6%E6%9C%BA%E5%90%AB%E5%86%B7%E5%86%BB%E6%9C%9F.html)
作业7:回文子串数目
给定一个字符串,计算这个字符串中有多少个回文子串。具有不同开 始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不 同的子串。
示例:
输入:“abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
作业8:最小编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所 使用的最少操作数 。(插入、删除、替换)
示例:
Input: word1 = “horse”, word2 = “ros”
Output: 3
Explanation:
horse -> rorse (replace ‘h’ with ‘r’)
rorse -> rose (remove ‘r’)
rose -> ros (remove ‘e’)
我们需要运用题目中的三种操作来把word1转换成word2。首先,分析一下这3种操作。假设给定A,B两个单词,那对于A,B来说,只需要在A上进行操作,最终让A=B即可。我们用D[i][j]来表示A的前i个字母和B前J个字母之间的编辑距离,当我们想获得D[i][j]时,我们可以利用上述三种操作实现A=B。
1.在D[i-1][j]的基础上(此时已经知道A[:i-1]=B[:j]),所以在A中删除最后一个元素即可。
2.在D[i][j-1]的基础上(此时A[:i]=B[:j-1]),所以在A上插入B[j]即可
3.在D[i-1][j-1]的基础上(此时A[:i-1]=B[:j-1]]),所以把A[i]修改成B[j]即可,但注意如果A[i]=B[j],我们不需要做任何操作。
for i in range(1, n + 1):
for j in range(1, m + 1):
left = D[i - 1][j] + 1
down = D[i][j - 1] + 1
left_down = D[i - 1][j - 1]
if word1[i - 1] != word2[j - 1]:
left_down += 1
D[i][j] = min(left, down, left_down)
return D[n][m]
(来源:https://blog.csdn.net/litt1e/article/details/105344345)
参考:算法课动态规划答疑ppt