在算法导论(第三版)第15章动态规划最后的习题中,有一道15-10投资策略规划的题目,网上相关的讲解不是很多,或者算法的时间复杂度也较高,因此将具体实现分享如下。
这道题目其实不用动态规划,也能解的。但既然放在这里,就是给大家练习一下动态规划的思路的,我们就用动态规划的思路来解一下。先看a、b、c 前三个小题目。
我们假设要计算j年的投资收益,这里有一个隐含的前提,我们一定是在n种投资里面寻找当年收益率最大的那个投资种类,假设:
强调一点,f2>f1, 也就是说转移投资种类的成本是更高的(这也符合金融投资的实际情况)。那么根据是否转移投资,有两种情况:
1、 p(j) == p(j-1)
此时不需要进行转移投资:j年的收益为:
// r(p(j), j) 代表 j年的最大投资收益率
2、p(j) != p(j-1)
这时就要进行对比了,分别计算转移和不转移两种情况的收益:
假设:
// 转移投资的收益
// 不转移的收益,此时投资种类取的是上一年的值
if t1 > t2 : 则 转移投资 ,令:
else:则无需转移投资,令:
// 此时j年的投资种类取 j - 1 年的值。
从上面的分析可以看出,该问题存在最优子结构,j年的投资收益只和 当年的p(j) 、去年的p(j-1) 的值 有关。以此递推,我们可以求出第2年……直到第m年的e 的值。
初始条件:第1年的投资收益最简单,直接求出 后,因为第一年不涉及转移与否, 故第一年的投资收益为:
根据上面的分析,很容易就可以写出状态转移方程了:
根据上述分析,在用动态规划思路解决本题目时,需要定义两个dp数组:
p:一维数组,用来保持每年选择的投资种类;
e:一维数组,用来保存每年底收益的值;
以上就是整个的分析思路。同时解决了 a、b 两个问题。c问题请参考下节的代码实现。
最后一个d 问题的分析,因为增加了限制条件,很容易看出,上面的最优子结构分析已经不再适用了,每年的收益只能通过暴力循环求解,因此也不再适用动态规划的解法了。
以下是具体的实现代码,代码经过略微的优化,使用了一些小的编程技巧,以使代码更加的简洁。本代码用 Dart 语言实现,要求 dart-sdk 版本 3.0 以上。代码中注释比较清晰,不再赘述。
(List, List) findInvestStrategy(
int capital, int kinds, int years, List> r, int f1, int f2) {
// p:每一年选择的投资种类,ens:每年的最大化收益;
var p = List.filled(years + 1, 0), ens = List.filled(years + 1, 0.0);
for (var j = 1; j <= years; j++) {
var q = 1;
// 遍历每一个类别,寻找收益率最大的那个投资种类
for (var i = 2; i <= kinds; i++) if (r[i][j] > r[q][j]) q = i;
p[j] = q;
// ens[j]表示当前年份的收益率,先假设不切换投资种类时,j年的收益是多少;
ens[j] = switch (j) {
1 => capital * r[q][j],
_ => (ens[j - 1] - f1) * r[p[j - 1]][j]
};
// 如果q不等于p[j-1],即j年的最大化投资收益种类不等于上一年,则计算一下切换投资的收益并对比
if (j > 1 && q != p[j - 1]) {
// t表示计算一下切换投资的收益
var t = (ens[j - 1] - f2) * r[q][j];
if (t > ens[j]) {
ens[j] = t;
} else {
// 意味着不用切换投资种类
p[j] = p[j - 1];
}
}
}
return (p, ens);
}
测试如下,投资种类设为4种,年份为5年,构造r值:
void main() {
var kinds = 4, years = 5, capital = 10000, f1 = 1000, f2 = 1300;
var r = >[
[0, 0, 0, 0, 0, 0],
[0, 1.1517, 1.1360, 1.2187, 1.1289, 1.1458],
[0, 1.1314, 1.0450, 1.1805, 1.1398, 1.1340],
[0, 1.1609, 1.0235, 1.2591, 1.0688, 1.1298],
[0, 1.1857, 1.0155, 1.2102, 1.0789, 1.1609]
];
// r.skip(1).forEach((e) => print(e.skip(1).map((e) => e.toStringAsFixed(4))));
var (p, ens) = findInvestStrategy(capital, kinds, years, r, f1, f2);
print(p.skip(1));
print(ens.skip(1).map((e) => e.toStringAsFixed(2)));
}
运行结果如下:
(4, 1, 3, 2, 2)
(11857.00, 11992.75, 13463.24, 13863.67, 14587.40)
从上面分析和代码可以看出,本算法的时间复杂度为,m为年份,n为投资种类。
1、推荐一下 Dart 语言,真的很好用,Dart3.0版本以后,增加了模式匹配等当前Rust语言很流行的很多功能,大大提升了函数式编程的能力;3.1版本发布后,经实测性能还有了巨大的提升。我日常写一些算法的代码,都是用Dart。
2、抽空在看算法导论(补大学荒废的时光),刚看到第15章。书中的例子、习题,除了部分构造数据复杂的以外,很多习题我都写了实现,如果有人需要,可以关注我,给我留言,我分享。