动态规划的空间优化
之前学习动态规划时,遇到的最费解的地方就是空间复杂度的化简,用二维数组保存各个状态这个好理解,但是把二维空间变成一维空间复杂度却一直没弄明白,现在想想其实是自己没有仔细思考这个过程。
按理说动态规划就是对我们解决问题过程的模拟,所以我们要想弄明白这个化简空间复杂度的过程,只需要,画个表格仔细思考各个状态的演变,我们就能弄清楚这个过程了,其实并没有我们想象的那么难以理解。
现在就举例这道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[]数组中保存的始终有目前在求的斜行的上一个斜行的状态。这样只要一个一维数组就能求出这个问题了。
所以,动态规划,一个状态和他之前的所有状态都有关,我们就老老实实用二维数组存,如果只和之前某一行某一列状态有关,就可以优化空间复杂度。。具体优化方式会有区别但是大同小异。
这个代码的实现就交给你啦