Leetcode|有限状态自动机团灭买卖股票6道题

0 前言及问题描述

大三编译原理中的确定有限状态自动机DFA,其实是动态规划中的一大杀器。我们知道,动态规划中最难之一的是写出状态转移方程,而导致状态转移的根本正是题目中的选择,因此,只要我们根据选择列表绘出对应状态的有限自动机,即可找到写出状态转移方程的突破口。感谢labuladong公众号的讲解,以下以力扣的买卖股票问题为例,加以总结

在这里插入图片描述

1 有限状态自动机的使用

我们根据题目列出以下四个状态,但只有前三个状态的上下界是题目已知的,第四个状态的上界未知,因此我们只能使用前三个状态作为dp数组索引,将第四个状态作为值

【状态】:①天数(数组索引i);②剩余交易次数k;③是否持有股票10④累计利润(待求)

【选择】:①在第i天买股票;②在第i天卖股票;③第i天不交易

由于选择决定状态如何转移,因此,我们从选择入手,找到不同选择会改变的状态为②剩余交易次数k和③是否持有股票10,这里,画出选择和状态③之间有限状态自动机

在这里插入图片描述
有了状态自动机,我们即可快速找到状态转移关系,设选择列表为 A = { b u y , s e l l , r e s t } A = \{buy, sell, rest\} A={buy,sell,rest},状态集为 S = { s 0 , s 1 } S = \{s_0, s_1\} S={s0,s1},每个状态的价值(累计利润)为 V ( s ) V(s) V(s),每个状态执行某个动作后的价值为 Q ( s , a ) Q(s,a) Q(s,a),则有
V ( s 0 ) = max ⁡ { Q ( s 0 , r e s t ) , Q ( s 1 , s e l l ) } V ( s 1 ) = max ⁡ { Q ( s 1 , r e s t ) , Q ( s 0 , b u y ) } V(s_0) = \max \{Q(s_0, rest), Q(s_1, sell)\} \\ V(s_1) = \max \{Q(s_1, rest),Q(s_0, buy)\} V(s0)=max{Q(s0,rest),Q(s1,sell)}V(s1)=max{Q(s1,rest),Q(s0,buy)}

2 股票问题框架

有了以上思路整出框架啦

2.1初始化

  1. 通用初始化全为0
  2. 不符合条件的状态对应值为负无穷①第0天持有股票的状态 + ②无剩余股票交易次数时还持有股票
dp[0][1...K][1] = INT_MIN;
dp[1...i][0][1] = INT_MIN;

2.3 总体框架

dp数组含义】第i天剩余交易数为k下持有股票的累计最大收益为dp[i][k][1],未持有股票为dp[i][k][0]

class Solution {
public:
    int maxProfit(vector<int>& prices, int K) {
        int size = prices.size();
        // dp数组含义:第i天剩余交易数为k下持有股票的累计最大收益为dp[i][k][1],未持有股票为dp[i][k][0]
        vector<vector<vector<int>>> dp(size + 1, vector<vector<int>> (K + 1, vector<int> (2, 0)));
        // 边界初始化
        dp[0][1...K][1] = INT_MIN;
        dp[1...i][0][1] = INT_MIN;
        for (int i = 1; i <= size; i++) 
            for (int k = 1; k <= K; k++) {
                dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]);
                dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i - 1]);
            }
        return dp[size][K][0];
    }
};

① 买卖股票的最佳时机( k = 1 k = 1 k=1

在这里插入图片描述

1.1 股票三维DP框架照搬(885ms)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        vector<vector<vector<int>>> dp(size + 1, vector<vector<int>> (2, vector<int> (2, 0)));
        dp[0][1][1] = INT_MIN;
        for (int i = 1; i <= size; i++) {
            for (int k = 1; k <= 1; k++) {
                dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]);
                dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i - 1]);
            }
        }
        return dp[size][1][0];
    }
};

由于第二个for循环只有1次,进一步可修改为

for (int i = 1; i <= size; i++) {
    dp[i][1][0] = max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i - 1]);
    dp[i][1][1] = max(dp[i - 1][1][1], dp[i - 1][0][0] - prices[i - 1]);
}

由于状态转移方程中没有改变dp[i][0][0]的量,因此有dp[i][0][0] == 0,第二个状态转移可简写为

dp[i][1][1] = max(dp[i - 1][1][1], - prices[i - 1]);

最终得到

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        vector<vector<vector<int>>> dp(size + 1, vector<vector<int>> (2, vector<int> (2, 0)));
        dp[0][1][1] = INT_MIN;
        for (int i = 1; i <= size; i++) {
            dp[i][1][0] = max(dp[i - 1][1][0], dp[i - 1][1][1] + prices[i - 1]);
            dp[i][1][1] = max(dp[i - 1][1][1], - prices[i - 1]);
        }
        return dp[size][1][0];
    }
};

1.2 优化为二维DP(336ms)

既然k == 1,因此可直接降维到二维

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        vector<vector<int>> dp(size + 1, vector<int> (2, 0));
        dp[0][1] = INT_MIN;
        for (int i = 1; i <= size; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            dp[i][1] = max(dp[i - 1][1], - prices[i - 1]);
        }
        return dp[size][0];
    }
};

1.3 空间复杂度优化为 O ( 1 ) O(1) O(1)(136ms)

进一步观察,可以约掉[i - 1]

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        int dp_i_0 = 0, dp_i_1 = INT_MIN;
        for (int i = 0; i < size; i++) {
            dp_i_0 = max(dp_i_0, dp_i_1 + prices[i]);
            dp_i_1 = max(dp_i_1, - prices[i]);
        }
        return dp_i_0;
    }
};

在这里插入图片描述

② 买卖股票的最佳时机II( k = ∞ k = \infty k=

在这里插入图片描述

2.1 股票DP框架照搬(12ms)

由于 k = ∞ k = \infty k=,因此可以认为 k → k − 1 k \to k - 1 kk1

dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i - 1]);
            = max(dp[i - 1][k][1], dp[i - 1][k][0] - prices[i - 1]);
p[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]);

因此,既然全是k,可直接把三维DP降维到二维DP

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        vector<vector<int>> dp(size + 1, vector<int>(2, 0));
        dp[0][1] = INT_MIN;
        for (int i = 1; i <= size; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
        }
        return dp[size][0];
    }
};

2.2 空间复杂度优化为 O ( 1 ) O(1) O(1)(0ms)

其中dp[i][0]作为第二步计算前会被更新覆盖,找个零时变量存储一下就能解决

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        int dp_i_0 = 0, dp_i_1 = INT_MIN;
        for (int i = 1; i <= size; i++) {
            int tmp = dp_i_0;
            dp_i_0 = max(dp_i_0, dp_i_1 + prices[i - 1]);
            dp_i_1 = max(dp_i_1, tmp - prices[i - 1]);
        }
        return dp_i_0;
    }
};

在这里插入图片描述

③ 最佳买卖股票时机含冷冻期( k = ∞ k = \infty k= + 冷冻期)

在这里插入图片描述

3.1 股票DP框架照搬(8ms)

  • 由于 k = ∞ k = \infty k=,因此可以认为 k → k − 1 k \to k - 1 kk1
  • 而多加了1天冷冻期,则对于当天的持有,由①前一天持有;②至少2天前卖出导致未持有,然后今天买入;两者状态转移而成

【状态转移】

dp[i][0]= max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);   // 当天未持有
dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1]);  // 当天持有

写完状态转移后,由于有dp[i - 2],所以要对dp[0][1], dp[1][0], dp[1][1]初始化,完整代码如下

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        vector<vector<int>> dp(size + 1, vector<int>(2, 0));
        dp[0][1] = INT_MIN;
        dp[1][0] = 0;
        dp[1][1] = - prices[0];
        for (int i = 2; i <= size; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1]);
        }
        return dp[size][0];
    }
};

3.2 空间复杂度优化为 O ( 1 ) O(1) O(1)(0ms)

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size();
        int dp_i_0 = 0, pre_dp_i_0 = 0, dp_i_1 = - prices[0];
        for (int i = 2; i <= size; i++) {
            int tmp = dp_i_0;
            dp_i_0 = max(dp_i_0, dp_i_1 + prices[i - 1]);
            dp_i_1 = max(dp_i_1, pre_dp_i_0 - prices[i - 1]);
            pre_dp_i_0 = tmp;
        }
        return dp_i_0;
    }
};

在这里插入图片描述

④ 买卖股票的最佳时机含手续费( k = ∞ k = \infty k= + 手续费)

在这里插入图片描述

4.1 股票DP框架照搬(232ms)

  • 由于 k = ∞ k = \infty k=,因此可以认为 k → k − 1 k \to k - 1 kk1
  • 而每交易完1笔,即卖出当前股票后,需要支付一个常数小费

【状态转移】

dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1] - fee);  // 当天未持有
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);        // 当天持有

写完状态转移后,由于有dp[i - 1],所以要对dp[0][1]初始化,完整代码如下

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int size = prices.size();
        vector<vector<int>> dp(size + 1, vector<int>(2, 0));
        dp[0][1] = INT_MIN;
        for (int i = 1; i <= size; i++) {
            if (prices[i - 1] < fee) dp[i][0] = dp[i - 1][0];
            else 
                dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1] - fee);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
        }
        return dp[size][0];
    }
};

4.2 空间复杂度优化为 O ( 1 ) O(1) O(1)(84ms,99%beat)

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int size = prices.size();
        int dp_i_0 = 0, dp_i_1 = INT_MIN;
        for (int i = 0; i < size; i++) {
            if (prices[i] >= fee) 
                dp_i_0 = max(dp_i_0, dp_i_1 + prices[i] - fee);
            dp_i_1 = max(dp_i_1, dp_i_0 - prices[i]);
        }
        return dp_i_0;
    }
};

在这里插入图片描述

⑤ 买卖股票的最佳时机 III( k = 2 k = 2 k=2

在这里插入图片描述

5.1 股票三维DP框架照搬(904ms)

  • 由于 k = 2 k = 2 k=2,因此可直接套用三维有限状态机框架

【状态转移】

dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]);      // 当天未持有
dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i - 1]);  // 当天持有

写完状态转移后,需要对dp[0][k][1]初始化负无穷,完整代码如下

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size(), K = 2;
        vector<vector<vector<int>>> dp(size + 1, vector<vector<int>>(K + 1, vector<int>(2, 0)));
        // for (int k = 0; k <= K; k++) dp[0][k][1] = INT_MIN;
        dp[0][1][1] = INT_MIN;
        dp[0][2][1] = INT_MIN;
        for (int i = 1; i <= size; i++) 
            for (int k = 1; k <= K; k++) {
                dp[i][k][0] = max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]);
                dp[i][k][1] = max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i - 1]);
            }
        return dp[size][K][0];
    }
};

5.2 优化为二维DP(176ms,81%beat)

我们发现状态转移的前一个状态都是dp[i - 1]..,因此可直接降维到二维

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size(), K = 2;
        vector<vector<int>> dp(K + 1, vector<int>(2, 0));
        // for (int k = 1; k <= K; k++) dp[k][1] = INT_MIN;
        dp[1][1] = INT_MIN;
        dp[2][1] = INT_MIN;
        for (int i = 0; i < size; i++) 
            for (int k = 1; k <= K; k++) {
                dp[k][0] = max(dp[k][0], dp[k][1] + prices[i]);
                dp[k][1] = max(dp[k][1], dp[k - 1][0] - prices[i]);
            }
        return dp[K][0];
    }
};

5.3 空间复杂度优化为 O ( 1 ) O(1) O(1)(132ms,98%beat)

在优化之前,我们先把第二个关于k的for循环展开得到

for (int i = 0; i < size; i++) {
	dp[1][0] = max(dp[1][0], dp[1][1] + prices[i]);
	dp[1][1] = max(dp[1][1], dp[0][0] - prices[i]);
	dp[2][0] = max(dp[2][0], dp[2][1] + prices[i]);
	dp[2][1] = max(dp[2][1], dp[1][0] - prices[i]);
}

不难发现,dp[k]dp[k-1]只有最后一行第二项才有依赖,其余没有。而不同i下的dp[k]dp[k]均有依赖

所以不能简写成2行,而需要把k展开写,针对dp[1][1]dp[2][1]依然单独初始化负无穷,其余全部初始化为0

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int size = prices.size(), K = 2;
        int dp_0_0 = 0, dp_1_0 = 0, dp_2_0 = 0, dp_1_1 = INT_MIN, dp_2_1 = INT_MIN;
        for (int i = 0; i < size; i++) {
            dp_1_0 = max(dp_1_0, dp_1_1 + prices[i]);
            dp_1_1 = max(dp_1_1, dp_0_0 - prices[i]);
            dp_2_0 = max(dp_2_0, dp_2_1 + prices[i]);
            dp_2_1 = max(dp_2_1, dp_1_0 - prices[i]);
        }
        return dp_2_0;
    }
};

在这里插入图片描述

⑥ 买卖股票的最佳时机 IV( k = K k = K k=K

在这里插入图片描述

6.1 股票三维DP框架照搬(40ms,14%beat)

  • 由于 k = K k = K k=K,因此可直接套用三维有限状态机框架

【状态转移】

dp[i][K][0] = max(dp[i - 1][K][0], dp[i - 1][K][1] + prices[i - 1]);      // 当天未持有
dp[i][K][1] = max(dp[i - 1][K][1], dp[i - 1][K - 1][0] - prices[i - 1]);  // 当天持有

写完状态转移后,需要对dp[0][k][1]初始化负无穷,完整代码如下

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int size = prices.size();
        vector<vector<vector<int>>> dp(size + 1, vector<vector<int>>(k + 1, vector<int>(2, 0)));
        for (int j = 0; j <= k; j++) dp[0][j][1] = INT_MIN;
        for (int i = 1; i <= size; i++)
            for (int K = 1; K <= k; K++) {
                dp[i][K][0] = max(dp[i - 1][K][0], dp[i - 1][K][1] + prices[i - 1]);
                dp[i][K][1] = max(dp[i - 1][K][1], dp[i - 1][K - 1][0] - prices[i - 1]);
            }
        return dp[size][k][0];
    }
};

6.2 优化为二维DP(0ms,100%beat)

我们发现状态转移的前一个状态都是dp[i - 1]..,因此可直接降维到二维

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int size = prices.size();
        vector<vector<int>> dp(k + 1, vector<int>(2, 0));
        for (int j = 0; j <= k; j++) dp[j][1] = INT_MIN;
        for (int i = 0; i < size; i++)
            for (int K = 1; K <= k; K++) {
                dp[K][0] = max(dp[K][0], dp[K][1] + prices[i]);
                dp[K][1] = max(dp[K][1], dp[K - 1][0] - prices[i]);
            }
        return dp[k][0];
    }
};

在这里插入图片描述

6.3 解决 k → ∞ k\to \infty k可能导致的超内存现象

对于Leetcode的测试用例,都是很小的,但实际大厂笔试时,可能会出 k → ∞ k\to \infty k的案例从而导致潜在超内存现象

解决方法很简单,由于消耗1次k最少需要2天,即当k > size / 2时则判定属于 k → ∞ k\to \infty k情况,此时直接调用之前的 O ( 1 ) O(1) O(1)方法即可,代码如下

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int size = prices.size();
        // 1.消耗1次k最少需要2天,因此可判断若k是+infty则更简单, O(1)
        if (k > size / 2) {
            int dp_0 = 0, dp_1 = INT_MIN;
            for (int i = 0; i < size; i++) {
                dp_0 = max(dp_0, dp_1 + prices[i]);
                dp_1 = max(dp_1, dp_0 - prices[i]);
            }
            return dp_0;
        }
        // 2.k是常数
        vector<vector<int>> dp(k + 1, vector<int>(2, 0));
        for (int j = 0; j <= k; j++) dp[j][1] = INT_MIN;
        for (int i = 0; i < size; i++)
            for (int K = 1; K <= k; K++) {
                dp[K][0] = max(dp[K][0], dp[K][1] + prices[i]);
                dp[K][1] = max(dp[K][1], dp[K - 1][0] - prices[i]);
            }
        return dp[k][0];
    }
};

至此,完结撒花啦✿✿ヽ(°▽°)ノ✿

参考文献

[1] labuladong. LeetCode 股票买卖问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SL_World

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值