动态规划题的解题步骤
一. 确定dp数组及其下标的含义。(在股票问题当中,数组下标的含义尤为重要,需要明确每一个下标的含义)
二. 找出数组元素之间的关系式。(股票问题的关系式相对来说不是很复杂,但是对股票不同时期的划分尤为重要)
三. 找出数组元素的初始值。(不同情况下的同一含义的下标初始情况也会不同,具体情况具体分析所得)
力扣股票买卖问题整理
力扣121:买卖股票的最佳时期
具体题目解析在代码的备注当中,题解为每个题的对比与联系。
原题链接:
此题题目要求只买卖一次,因此只需要分成持有股票与不持有股票。一开始,持有股票只可能是第0天买了股票,因此应该初始化为-prices[0],但是一开始不持有股票即没有买股票,则手上的最大金额为0。
代码如下:
int maxProfit(int* prices, int pricesSize) {
if(pricesSize == 0)
{
return 0;
}
int **dp = (int **)malloc(sizeof(int *) * pricesSize);
//dp[i][0]表示持有股票时的最大金额,dp[i][1]表示不持有股票的最大金额
int i;
for(i = 0; i < pricesSize; i++)
{
dp[i] = (int *)malloc(sizeof(int) * 2);
} //为数组分配空间
dp[0][0] = -prices[0]; //当持股时,只可能是今天买了股票因此初始化为-prices[0]
dp[0][1] = 0; //在第0天时,不持股的最大金额为0,没有买也没有卖
for(i = 1; i < pricesSize; i++)
{
dp[i][0] = fmax(dp[i - 1][0], -prices[i]); //当持有股票时,有可能是之前买的,也有可能是今天买的
//取其中的最大值,即花费的最少金额
dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] + prices[i]);
} //当不持股时,此时最大金额有可能是之前卖的,也有可能是将之前买的今天卖出去了所获取的
return dp[pricesSize - 1][1]; //因为只买卖一次,所以只可能是不持股时利润最大
}
力扣122:买卖股票的最佳时机Ⅱ
原题链接:
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
此题与第一题的不同在于,它可以对同一个股票买卖多次,因此对与持有股票来说,第一题是直接减去今天的股票价格,但是对于第二题是拿前一天不持有股票的金额减去今天的股票价格。但是对于不持有股票的操作是一样的。
代码如下:
int maxProfit(int* prices, int pricesSize) {
int i;
int **dp = (int **)malloc(sizeof(int *) * pricesSize);
for(i = 0; i < pricesSize; i++)
{
dp[i] = (int *)malloc(sizeof(int) * 2);
} //为所建立的数组分配空间
dp[0][0] = -prices[0]; //所代表为持有股票的最大金额 ,第0天,因此只会是今天买了股票
dp[0][1] = 0; //所代表为不持有股票时的最大金额 ,第一天不持股因此初始化为0
for(i = 1; i < pricesSize; i++)
{
dp[i][0] = fmax(dp[i - 1][0], dp[i - 1][1] - prices[i]);
//持有股票可以是前一天就已经持有股票没有进行卖,也可能是之前卖了股票今天才买的
dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] + prices[i]);
//不持有股票可能是前一天就不持有股票,也可能是之前持有股票,今天将股票卖掉了
}
return dp[pricesSize - 1][1]; //只可能是不持有股票时的钱最多
}
力扣123:买卖股票的最佳时机Ⅲ
原题链接:
123. 买卖股票的最佳时机 III - 力扣(LeetCode)
此题与前两题的不同在于题目要求最多进行两笔操作,你有可能一次都没有进行买卖,也可能买卖了一次,可能买卖了两次。因此进行状态划分时,应该分为五个阶段,即不进行操作,第一次持有,第一次不持有(即第一次买了后卖了),第二次持有,第二次不持有。本题注意,在进行初始化时,第二次持有股票应该初始化为-prices[0],即第一天完成了一次的买卖并又买了一次。对于递推公式与前两题差不多,在最后的返回时,持有股票时的最大金额一定小于不持有股票的最大金额,因此只需要找出下标0、2、3中的最大值。
代码如下:
int maxProfit(int* prices, int pricesSize) {
int i;
int** dp = (int**)malloc(sizeof(int*) * pricesSize);
/*
dp[i][0]代表不进行操作的最大金额
dp[i][1]代表第一次持有股票的最大金额
dp[i][2]代表第一次不持有股票的最大金额
dp[i][3]代表第二次持有股票的最大金额
dp[i][4]代表第二次不持有股票的最大金额
*/
for (i = 0; i < pricesSize; i++) {
dp[i] = (int*)malloc(sizeof(int) * 5);
} // 为数组dp分配空间
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0; // 对其进行初始化,但要注意题目说可以在同一天进行多次买卖
// 因此第一天有可能是买卖一次又买了一次
for (i = 1; i < pricesSize; i++) {
dp[i][0] = dp[i - 1][0]; // 不进行操作时就等于前一天的不操作金额
dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] - prices[i]);
// 第一次持有股有可能是前一天就已经持了股票
// 也有可能是今天才买的股票即不进行操作的最大金额减去今天买了股票的钱
dp[i][2] = fmax(dp[i - 1][2], dp[i - 1][1] + prices[i]);
// 第一次不持有股票有可能是前一天就已经不持有股票了
// 前一天持有股票,但是今天卖掉了,因此应该为前一天第一次持有股票的钱加上今天卖了股票所得的钱
dp[i][3] =
fmax(dp[i - 1][3], dp[i - 1][2] - prices[i]); // 与dp[i][1]类似
dp[i][4] =
fmax(dp[i - 1][4], dp[i - 1][3] + prices[i]); // 与dp[i][2]类似
}
return fmax(dp[pricesSize - 1][0],
fmax(dp[pricesSize - 1][2], dp[pricesSize - 1][4]));
}
力扣188:买卖股票的最佳时机Ⅳ
原题链接:
188. 买卖股票的最佳时机 IV - 力扣(LeetCode)
此题是上一题的延申题,即它可以买卖k次。因此在设立数组时,数组的第一个下标为第几天,则买卖k次就会有k个持有股票与k个不持有股票,即不进行操作,因此数组的第二个下标设置成2*k+1。双层循环,外层循环表示第几天,内层循环为买卖第几次的最大金额。因为不进行操作时的最大金额为0,因此先将数组的所有元素都初始化为0(用到了memset初始化函数),在内层循环时,求不同次数的最大金额。
代码如下:
int maxProfit(int k, int* prices, int pricesSize) {
if(pricesSize == 0)
{
return 0;
} //当出现此情况,即无需买卖,利润为0
int dp[pricesSize][2 * k + 1];
int i, j;
memset(dp, 0, sizeof(int) * pricesSize * (2 * k + 1));
//此函数为初始化函数,第一个为初始化的地址,第二个0为所要初始化的内容,第三个为初始化的大小
//此函数代表将dp数组初始化为0
/*此种初始化只初始化了第0天的,但对于后面的不操作并没有进行初始化
for(i = 0; i < (2 * k + 1); i++)
{
dp[0][i] = 0;
}
*/
for(i = 1; i < (2 * k + 1); i += 2)
{
dp[0][i] = -prices[0];
} //此时与买卖股票3一样,在一天内进行了k次的买卖,因此初始化为-prices[0]
for(i = 1; i < pricesSize; i++)
{ //外层循环为天数
for(j = 0; j < (2 * k - 1); j += 2)
{
dp[i][j + 1] = fmax(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
//持有股可能是之前就持有股票,也可能是之前不持有股票,今天买了股票
dp[i][j + 2] = fmax(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
//没持有股票可能为一开始就不持有股票,或者之前持有股票,但是今天卖掉了
} //内层循环为进行不持股与持有股的最大金额
}
return dp[pricesSize - 1][2 * k];
}
力扣309:买卖股票的最佳时机含冷冻期
原题链接:
309. 买卖股票的最佳时机含冷冻期 - 力扣(LeetCode)
此题是买卖股票的最佳时期Ⅱ加了冷冻期,冷冻期也是卖出股票的状态,但是不同的是冷冻期不可以买股票,因此应该分成持有股票的状态,卖出当天,冷冻期,卖出股票的阶段(冷冻期之后,下一次买股票之前)。
代码如下:
int maxProfit(int* prices, int pricesSize) {
if(pricesSize == 0)
{
return 0;
}
int dp[pricesSize][4];
memset(dp, 0, sizeof(int) * pricesSize * 4); //对数组进行初始化
/*dp[i][0]代表持有股票的状态
dp[i][1]代表保持卖出股票的状态(卖出股票的第二天开始到下一次买股票之间)
dp[i][2]代表具体卖出股票的状态(卖出当天)
dp[i][3]代表处于冷冻期状态*/
dp[0][0] = -prices[0]; //持有股票只会是第一天买的
for(int i = 1; i < pricesSize; i++)
{
dp[i][0] = fmax(dp[i - 1][0], fmax(dp[i - 1][1] - prices[i], dp[i - 1][3] - prices[i]));
//持有股的状态为前一天就持有股票
//前一天处于卖出股票的状态,今天买了股票
//前一天为冷冻期,今天买股票
dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][3]);
//前一天就处于卖出股票的状态
//前一天处于冷冻期
dp[i][2] = dp[i - 1][0] + prices[i];
//今天要卖出股票,则前一天应该处于持有股票的状态,前一天持有股票的金额加上今天卖出的钱
dp[i][3] = dp[i - 1][2];
//冷冻期即前一天刚卖出股票
}
return fmax(dp[pricesSize - 1][1], fmax(dp[pricesSize - 1][2], dp[pricesSize - 1][3]));
//手上金额最大时一定不持股,因此其余三种情况进行比较,选出最大值
}
力扣714:买卖股票的最佳时机含手续费
原题链接:
714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode)
此题是买卖股票的最佳时期Ⅱ加了手续费,只需要在卖股票的时候,减去手续费。也就是在不持有股票的部分有区别。
代码如下:
int maxProfit(int* prices, int pricesSize, int fee) {
int i;
int dp[pricesSize][4];
dp[0][0] = -prices[0];//dp[i][0]表示持有股票的最大金额
dp[0][1] = 0;//dp[i][1]表示不持有股票时的最大金额
for(i = 1; i < pricesSize; i++)
{
dp[i][0] = fmax(dp[i - 1][0], dp[i - 1][1] - prices[i]);
//持有股票有可能是前一天就持有股票,也可能是前一天不持有股票,今天买了股票
dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
//不持有股票有可能是前一天就不持有股票
//可能是前一天持有股票,但是今天将股票卖了,但与之前不同的地方在于,含卖股票的手续费需要减掉
}
return dp[pricesSize - 1][1];
}