动态规划的空间优化

动态规划的空间优化

之前学习动态规划时,遇到的最费解的地方就是空间复杂度的化简,用二维数组保存各个状态这个好理解,但是把二维空间变成一维空间复杂度却一直没弄明白,现在想想其实是自己没有仔细思考这个过程。

按理说动态规划就是对我们解决问题过程的模拟,所以我们要想弄明白这个化简空间复杂度的过程,只需要,画个表格仔细思考各个状态的演变,我们就能弄清楚这个过程了,其实并没有我们想象的那么难以理解。

现在就举例这道leetcode的动态规划题来讲一下这个空间优化的详细过程吧

例题:
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。

亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。

假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/stone-game
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这道题的题解是用一个二维数组保存中间的各个过程,至于如何设计这个状态数组可以看看这道题的题解。
总之某一项dp[i][j]保存一个pair数据(类似python的字典,不过只能保存两项),里面有两项,first项表示i,j区间内的先手的人取到的最大石子数,second项表示后手人取到的最大石子数。

在这里插入图片描述
从这个状态转移方程中可以看出,我们们要想求dp[i][j],需要知道dp[i+1][j]和dp[i][j-1]这三项在二维数组上什么关系呢,不妨画一下看看

在这里插入图片描述
假设[1][2]是dp[i][j]那么另外两项在它左边的斜着的一行上,所以我们在遍历时要先计算出左边的斜着的一行,才能继续递推后面的,一直到[1][4],这项就是最终的答案。

我们按部就班地写出空间复杂度为n的平方的代码

#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;
pair<int, int> a[510][510];
vector<int> b;

int main() {
	b = { 1,2,3,4};

	int len = b.size();
    
    //初始化
	for (int i = 0; i < len; i++) {
		a[i][i].first = b[i];
		a[i][i].second = 0;
	}
    
    
    //关键的两个for循环
	for(int j=1;j<len;j++)
		for (int i = 0; i < len-1&&i+j<len; i++)
		{
			if (b[i + j] + a[i][i + j - 1].second > b[i] + a[i + 1][i + j].second) {
				a[i][i + j].first = b[i + j] + a[i][i + j - 1].second;
				a[i][i + j].second = a[i][i + j - 1].first;
			}
			else {
				a[i][i + j].first = b[i] + a[i + 1][i + j].second;
				a[i][i + j].second = a[i + 1][i + j].first;
			}
		}
	for (int i = 0; i < len; i++)
	{
		for (int j = 0; j < len; j++)
		{
			cout << a[i][j].first << ',' << a[i][j].second << "   ";
		}
		cout << endl;
	}
	return 0;
}

但是我们再观察遍历的整个过程,不难发现,由于dp[i][j]只和dp[i+1][j]和dp[i][j-1]有关系,dp[i+1][j]和dp[i][j-1]又在同一个斜着的行上,就是说
在这里插入图片描述
先初始化斜行1,依次求斜行2,3,4,要求斜行2,只需要知道斜行1的数据,求斜行3,只需要知道斜行2.那么我们只需要一个辅助数组保存上一个斜行的数据就行了.
代码如下:

#include<iostream>
#include<vector>

using namespace std;

pair<int,int> a[1000], aa[1000];
vector<int> b;

int main() {

	pair<int,int>* a_, * aa_;

	a_ = a;//保存正在求的斜行
	aa_ = aa;//保存上一个斜行


	b = { 1,2,3,4};
	int len = b.size();

	for (int i = 0; i < len; i++) {
		aa_[i].first = b[i];
		aa_[i].second = 0;
	}
    
    //中间这段for循环其实和二维数组的for循环实现了相同的功能,虽然看上去下标变了,但是保存的数据都是一样的,仔细想想为什么
	for (int j = 1; j < len; j++)
	{
		for (int i = 0; i < len - 1 && i + j < len; i++)
		{
			if (b[i + j] + aa_[i].second > b[i] + aa_[i + 1].second) {
				a_[i].first = b[i + j] + aa_[i].second;
				a_[i].second = aa_[i].first;
			}
			else
			{
				a_[i].first = b[i] + aa_[i + 1].second;
				a_[i].second = aa_[i + 1].first;
			}
		}
		auto t = aa_;
		aa_ = a_;
		a_ = t;//一轮遍历之后交换两个数组,仔细想想为什么这样
	}
	cout << aa_[0].first << ',' << aa_[0].second << "  ";
	return 0;
}

现在空间复杂度就是2n了。

不过我们甚至不需要添加辅助数组,只需要一个数组就行了

结合这两张图

在这里插入图片描述
在这里插入图片描述
我们只需要一个一维数组保存一个斜行
DP[]初始化时保存的是第一个斜行的数据

然后我们不断更新这个数组

现在开始求第二个斜行的第一项,就是上面那个图里面的dp[i][j]它跟第一个斜行的第一项和第二项有关,我们已经在一维DP数组中保存了第一个斜行的数据,直接可以用DP[0]和DP[1]求出新的DP[0]覆盖原来的DP[0](为什么要覆盖呢,因为求第三个斜行的时候又需要用到第二个斜行的DP[0],所有要不断把DP数组更新成最新斜行的数据,求到哪,更新到哪,没求到的还是原来的,所以DP[0]更新后DP[1]还是第一个斜行的DP[1],DP[0]已经是第二个斜行的DP[0]了),然后继续求新的DP[1],他跟DP[1]和DP[2]有关,求出后覆盖原来的DP[1],然后一直往下求,这样我们的DP[]数组中保存的始终有目前在求的斜行的上一个斜行的状态。这样只要一个一维数组就能求出这个问题了。

所以,动态规划,一个状态和他之前的所有状态都有关,我们就老老实实用二维数组存,如果只和之前某一行某一列状态有关,就可以优化空间复杂度。。具体优化方式会有区别但是大同小异。

这个代码的实现就交给你啦

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值