dfs是暴力解算,时间复杂度和空间复杂度都很废,我们可以从dfs到记忆化搜索再到动态规划优化时间复杂度,从动态规划到动态数组优化空间复杂度。
一、dp[n]
我们从一道经典的动态规划题--打家劫舍说起。
1.画出所有情况
假设这几家的现金是【50, 30, 12, 5】我们从第一家开始,可以把所有的情况画出来(打勾的表示抢劫,否则不抢)
每一家都可以选择抢劫或不抢劫。如果选了第一家,那就只能从第三家开始选;如果没有选,则可以从第二家开始选。这是每一轮递归要干的事情。
2.搞出递归关系
所以就有了这个由人话写成的函数:
//n 表示当前是第几家
//total 表示一共有几家人
int 从第n家开始拿到的钱的最大值(int n, int total)
{
//结束条件
如果n >= total,抢劫完毕,没法再抢劫了,返回0
//关系式
从第n家开始拿到的钱的最大值 = max(
抢这家的钱!+ 从第n+2家开始拿到的钱的最大值 ,
不抢这家的钱!+ 从第n+1家开始拿到的钱的最大值
)
返回 从第n家开始拿到的钱的最大值;
}
转换成gcc认识的,就是:
//每一家的现金数量
//left 表示当前是第几家
//n 表示一共有几家人
int cashes[N] = {0};
int dfs(int left, int n)
{
int temp;
int rst = 0;
if (left >= n)
{
return 0;
}
if (mono[left] != 0)
{
return mono[left];
}
rst = cashes[left] + dfs(left + 2, n);
temp = cashes[left+1] + dfs(left + 3, n);
if (temp > rst)
{
rst = temp;
}
mono[left] = rst;
return rst;
}
3.加上记忆化搜索
在树状图中,我们很容易发现,节点12被计算了两次,如果我们能够把计算结果存其来就能极大的减少计算量。注意到函数的返回值只与参数left有关,我们就可以根据参数left来存储返回值。由于left是连续的,我们直接用数组存就好了。函数的逻辑就成了这样:
//n 表示当前是第几家
//total 表示一共有几家人
int 缓存[total] = {0};
int 从第n家开始拿到的钱的最大值(int n, int total)
{
//结束条件
如果缓存[n] != 0,说明已经计算过,返回缓存[n]
如果n >= total,抢劫完毕,没法再抢劫了,返回0
//关系式
从第n家开始拿到的钱的最大值