递推思想的美妙 Best Time to Buy and Sell Stock I, II, III O(n) 解法

通过递推思想解决LeetCode上关于股票买卖时机的经典问题,包括单次交易、多次交易及限制两次交易的最佳时机选择。

题记:在求最大最小值的类似题目中,递推思想的奇妙之处,在于递推过程也就是比较求值的过程,从而做到一次遍历得到结果。

LeetCode 上面的这三道题最能展现递推思想的美丽之处了。

题1 Best Time to Buy and Sell Stock


 

Say you have an array for which the ith element is the price of a given stock on day i.

If you were only permitted to complete at most one transaction (ie, buy one and sell one share of the stock), design an algorithm to find the maximum profit.

分析:

我们已经知道股票每天的价格,求一次买卖的最大收益。

怎么解?最大值减最小值?肯定不行,因为买必须在卖之前。

简单的递推思想就可以解决

对于1-n天的价格序列,假设maxProfit[i] (0 < i < n)表示第i天卖出时,所获得的最大收益。那么我们要求的值其实就是数组maxProfit[n] n个元素中的最大值。 

那么maxProfit[i]如何求?因为卖的天已经确定,所以只要算出1到i 天 中哪一天价格最低,作为买的时间即可。

根据递推思想,要求1到i 天 中哪一天价格最低,我们只需要比较 price[i] 和 1到i-1天内的最低价格,然后取较小值即可。

同样,最后的结果是求maxProfit[n]中的值,我们也可以把求最大值的过程放到遍历中,每次求出 maxProfit[i],我们将它和 maxProfit[0]到maxProfit[i-1] 中选出的最大值max比较,然后更新max即可。

因为比较的过程被放到了遍历的过程中,所以虽然使用递推思想,但是一次遍历就可以实现这个思想。

代码:

class Solution {
public:
    int maxProfit(vector<int> &prices) {
        if(prices.size() == 0) return 0;
        int maxPrifit = -9999999; //存储最大利润
        int min = 9999999; //存储最小价格
        vector<int>::iterator i = prices.begin();
        vector<int>::iterator end = prices.end();
        for(; i < end; ++i){
            if(min > (*i)) min = (*i);
            if(maxPrifit < ((*i) - min))
                maxPrifit = ((*i) - min);
        }
        return maxPrifit;
    }
};

题2 Best Time to Buy and Sell Stock II

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times). However, you may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

分析:

更改的地方是可以多次买卖,但是买必须在卖前。

其实这道题才是三题中最简单的一道。

思路就是“逢涨就买,逢跌就卖”。炒股的最基本道理,只有在已经知道所有股票价格的前提下才能精确地实现这句话 ==

代码:

class Solution {
public:
    int maxProfit(vector<int> &prices) {
        if(prices.size() <= 1) return 0;
        int buy = -1; int profit = 0;
        vector<int>::iterator i = prices.begin();
        vector<int>::iterator end = prices.end();
        for(;i != end; ++i){
            if((i+1) != end && *(i+1) > *i && buy < 0) buy = (*i);
            if(((i+1) == end || *(i+1) < *i) && buy >= 0){
                profit += (*i - buy);
                buy = -1;
            }
        }
        return profit;
    }
};

题3 Best Time to Buy and Sell Stock III

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete at most two transactions.

Note:
You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).

分析:
题1的变种,可以买卖两次,但是不能重叠。

我的第一反应自然是分而治之,i = 1 to n,然后分别循环计算prices[1~ i], prices[i ~n] 的最大利润,相加,求出和的最大值。时间复杂度是实打实的 O(n2)。

稍微改进一些,就是我们在计算prices[1~ i]的时候,可以使用递推,这样思路就成了,在题1的代码基础上,每次算完prices[1~ i]的最小值,紧接着用一个循环 计算prices[i ~n] 的最大值。这样prices[1~ i]的最大利润计算和 i = 1~n的迭代合并,只有一个子循环, 一定程度上减小时间复杂度,时间复杂度成了 (n-1) + (n-2) + .. + 1,但依然是 O(n2)

 

实际上可以将复杂度减小到 O(n),这种方法会需要额外的 O(n)空间,但在一般编程领域,如果O(n)的空间能换来时间上减小一个数量级,还是相当好的。

我们先考虑一次买卖prices[1 ~ i] (0 < i < n)的最大利润,这就和题1一样,所不同的是,我们将值存入 firstMaxPro数组,而不是求出总的最大利润。

注意这里的firstMaxPro[n]数组和题目1中maxProfit[n]数组功能是不一样的。maxProfit[i] 表示一定在 i 天卖出的前提下,1~i天的最大利润。firstMaxPro[i]表示1~i天的最大利润,卖出可以在1~i天的任何时候。

接着,我们再做一次遍历,这一次遍历,从n 到 1,为的是将第二次买卖考虑进去,在求第二次买卖的最大利润的同时,就可以加上 firstMaxPro[n]中的对应值,然后求出总和最大。返回这个最大的总和即可。

两次n循环,时间复杂度O(n)

代码:

class Solution {
public:
    int maxProfit(vector<int> &prices) {
        if(prices.size() <= 1) return 0;
        int* firstMaxPro = new int[prices.size()];
        int curMin = 9999999;
        int curMaxPro = -9999999;
        for(int i = 0; i < prices.size(); ++i){
            if(curMin > prices[i]) curMin = prices[i];
            if(curMaxPro < (prices[i] - curMin)){
                curMaxPro = (prices[i] - curMin);
            }
            firstMaxPro[i] = curMaxPro;
        }
        
        //从尾到头遍历
        int MaxPro = -9999999;  //总的最大利润
        int curMax = -9999999;  //第二段区间上的最大价格
        for(int j = prices.size()-1; j >= 0; --j){
            if(curMax < prices[j]) curMax = prices[j];//第二次买卖的最大利润就等于curMax - prices[j]
            if(MaxPro < (firstMaxPro[j] + curMax - prices[j])){
                MaxPro = (firstMaxPro[j] + curMax - prices[j]);
            }
        }
        
        return (MaxPro > 0 ? MaxPro : 0);
    }
};

结语

这三道题其实都不难,都是递推思想的一些演变。但是这三道题可以很典型地体现递推的核心思想和其优势:将比较和择优的过程和递推过程合并,从而只需要单次遍历,在O(n)内获得结果。

<think> 我们分析八皇后问题的时间复杂度为$O(n!)$的递推式。八皇后问题是一个经典的回溯算法问题,其目标是在$n \times n$的棋盘上放置$n$个皇后,使得它们互不攻击(即任意两个皇后不在同一行、同一列或同一对角线上)。 ### 递推式分析 在回溯算法中,我们通常按行放置皇后。在第一行,我们有$n$种选择(即$n$列)。在第二行,由于不能与第一行的皇后同列,所以最多有$n-1$种选择(但实际可能更少,因为还要考虑对角线冲突)。然而,在最坏情况下(不考虑对角线冲突),我们可以认为每一行的选择数比上一行少1。因此,状态空间树的分支数依次为$n, n-1, n-2, \ldots, 1$。这样,总的可能状态数为: $$n \times (n-1) \times (n-2) \times \cdots \times 1 = n!$$ 但实际算法中,由于存在对角线冲突,我们会在放置过程中剪枝,因此实际搜索的状态数小于$n!$。然而,在最坏情况下(即没有剪枝发生,或者剪枝效果最差时),我们仍然需要检查$n!$个状态。因此,时间复杂度为$O(n!)$。 ### 递推式 我们可以用递归树来描述回溯算法。设$T(n)$表示在$n \times n$棋盘上放置$n$个皇后所需的时间。在放置第一个皇后(第一行)时,有$n$种选择。对于每种选择,我们需要检查是否与已放置的皇后冲突(检查列和对角线)。检查冲突的时间复杂度为$O(n)$(因为最多需要检查当前行之前的行)。然后,我们递归地解决剩下的$n-1$行的问题。因此,我们可以得到递推式: $$T(n) = n \cdot [O(n) + T(n-1)]$$ 其中: - $n$:第一行的选择数(即列的选择) - $O(n)$:每次放置皇后时检查冲突的时间 - $T(n-1)$:递归解决剩下的$n-1$行 展开递推式: $$T(n) = n \cdot T(n-1) + O(n^2)$$ 这个递推式反映了回溯算法的时间复杂度。然而,我们通常关注的是主导项。由于$n!$的增长速度远快于$n^2$,所以$T(n)$的渐近时间复杂度由$n!$主导。事实上,我们可以通过迭代展开来观察: $$\begin{aligned} T(n) &= n \cdot T(n-1) + O(n^2) \\ &= n \cdot [(n-1) \cdot T(n-2) + O((n-1)^2)] + O(n^2) \\ &= n(n-1) \cdot T(n-2) + n \cdot O((n-1)^2) + O(n^2) \\ &= n(n-1)(n-2) \cdot T(n-3) + n(n-1) \cdot O((n-2)^2) + n \cdot O((n-1)^2) + O(n^2) \\ &\vdots \\ &= n! \cdot T(0) + \sum_{k=1}^{n} \frac{n!}{k!} \cdot O(k^2) \end{aligned}$$ 由于$T(0)$是常数(表示没有皇后需要放置时的操作),而求和项$\sum_{k=1}^{n} \frac{n!}{k!} \cdot k^2$可以证明是$O(n!)$(因为当$k$固定时,$\frac{n!}{k!}$是$n$的阶乘增长,而$k^2$是多项式,但整个求和被$n!$所控制)。因此,$T(n) = O(n!)$。 ### 优化 虽然回溯算法的最坏时间复杂度为$O(n!)$,但我们可以通过剪枝来减少实际搜索的状态数。例如,使用三个集合(列、主对角线、副对角线)来记录已被占用的列和对角线,从而将冲突检查的时间复杂度从$O(n)$降为$O(1)$[^3]。这样递推式变为: $$T(n) = n \cdot T(n-1) + O(1)$$ 展开后: $$T(n) = n \cdot T(n-1) + O(1) = O(n!)$$ 尽管冲突检查的时间复杂度降为$O(1)$,但整个算法的时间复杂度仍为$O(n!)$,因为状态数仍然为$O(n!)$级(在最坏情况下)。 ### 总结 八皇后问题的时间复杂度递推式为$T(n)=n\cdot T(n-1)+O(n)$(未优化冲突检查)或$T(n)=n\cdot T(n-1)+O(1)$(优化冲突检查),其渐近时间复杂度均为$O(n!)$。优化冲突检查只能降低常数因子和低阶项,但无法改变阶乘级的增长趋势[^4]。 §§相关问题§§ 1. 八皇后问题是否有时间复杂度低于$O(n!)$的算法? 2. 如何用迭代代替递归实现八皇后问题的回溯算法? 3. 八皇后问题的时间复杂度能否通过动态规划优化? </think> ### 八皇后问题时间复杂度 $O(n!)$ 的递推式分析 八皇后问题的时间复杂度 $O(n!)$ 源于其回溯算法的搜索空间特性。我们通过递推式严格证明这一结论: #### 递推式定义 设 $T(n)$ 表示在 $n \times n$ 棋盘上放置 $n$ 个皇后所需的时间复杂度,其递推关系为: $$T(n) = n \cdot \left( O(n) + T(n-1) \right)$$ 其中: 1. **$n$**:当前行可选列的数量(每行有 $n$ 个可能位置) 2. **$O(n)$**:冲突检查的时间(检查列、对角线是否冲突) 3. **$T(n-1)$**:递归处理剩余 $n-1$ 行的时间 #### 递推式展开 通过迭代展开可得: $$ \begin{aligned} T(n) &= n \cdot \left( O(n) + T(n-1) \right) \\ &= n \cdot O(n) + n \cdot T(n-1) \\ &= O(n^2) + n \cdot \left[ (n-1) \cdot O(n-1) + (n-1) \cdot T(n-2) \right] \\ &= O(n^2) + n(n-1)O(n-1) + n(n-1)T(n-2) \\ &\ \ \vdots \\ &= \sum_{k=1}^{n} \left( \frac{n!}{(k-1)!} \cdot O(k) \right) + n! \cdot T(0) \end{aligned} $$ #### 时间复杂度证明 1. **求和项分析**: $$ \sum_{k=1}^{n} \frac{n!}{(k-1)!} \cdot O(k) \leq n! \cdot \sum_{k=1}^{n} \frac{k}{(k-1)!} $$ 由级数收敛性可知 $\sum_{k=1}^{\infty} \frac{k}{(k-1)!} = O(1)$,故该和为 $O(n!)$。 2. **边界条件**:$T(0)$ 是常数时间 $O(1)$。 3. **最终结论**: $$T(n) = O(n!) + O(n!) = O(n!)$$ #### 算法实现验证 ```python def solve_n_queens(n): def backtrack(row): nonlocal count if row == n: # 找到合法解 count += 1 return for col in range(n): # 当前行有n种选择 if not conflict(board, row, col): # 冲突检查O(n) board[row] = col backtrack(row + 1) # 递归处理下一行 board = [-1] * n count = 0 backtrack(0) return count # 冲突检查函数(最坏O(n)) def conflict(board, row, col): for r in range(row): # 检查所有已放置的行 if board[r] == col or \ abs(row - r) == abs(col - board[r]): return True return False ``` **复杂度解释**: - 第 $k$ 层递归有 $(n-k+1)$ 个分支 - 每层冲突检查耗时 $O(k)$ - 总操作数 $\sum_{k=1}^{n} \left( \prod_{i=1}^{k} (n-i+1) \right) \cdot k = O(n!)$ #### 优化与本质 即使优化冲突检查至 $O(1)$(如用哈希表存储对角线[^3]),递推式变为: $$T(n) = n \cdot (O(1) + T(n-1))$$ 展开后仍得 $T(n) = O(n!)$,证明阶乘复杂度由搜索空间本身决定[^4]。 --- ### 相关问题 1. 八皇后问题是否存在优于 $O(n!)$ 的算法? 2. 如何用对称性剪枝优化八皇后问题的实际运行时间? 3. $n$ 较大时,如何通过启发式算法近似求解八皇后问题
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值