螺旋归纳DP

目录

螺旋归纳

[力扣 LCP 14. 切分数组](#力扣 LCP 14. 切分数组)

[力扣 3573. 买卖股票的最佳时机 V](#力扣 3573. 买卖股票的最佳时机 V)


螺旋归纳

数学归纳法中的螺旋归纳,参考归纳法

简单的说,就是2个dp函数交替求解。

力扣 LCP 14. 切分数组

给定一个整数数组 nums ,小李想将 nums 切割成若干个非空子数组,使得每个子数组最左边的数和最右边的数的最大公约数大于 1 。为了减少他的工作量,请求出最少可以切成多少个子数组。

示例 1:

输入:nums = [2,3,3,2,3,3]

输出:2

解释:最优切割为 [2,3,3,2] 和 [3,3] 。第一个子数组头尾数字的最大公约数为 2 ,第二个子数组头尾数字的最大公约数为 3 。

示例 2:

输入:nums = [2,3,5,7]

输出:4

解释:只有一种可行的切割:[2], [3], [5], [7]

限制:

  • 1 <= nums.length <= 10^5
  • 2 <= nums[i] <= 10^6

递推式:

  • 数组a的下标是从1到n
  • splitArray函数是一个数组的最小切分数
  • lcm(k)是数组a前k个数的最小公倍数
  • dp[k]=splitArray(a[1...k]),即前k个数的最小切分数,dp[0]=0, dp[1]=1
  • f(p,k) = min{dp[i] | 0<=i<k,p|a[i+1]} , 其中p|lcm(k), f(p,k)表示前k个数中,一个p的倍数前面的子数组的最小切分数
  • dp[k] = min{f(p,k)+1 | p|a[k]}

把形如f(......,k)的所有式子打包到一起,看成一个整体g(k)。

那么g(k)的求解依赖dp(0)到dp(k-1)这k项,而dp(k)的求解依赖g(k),所以整体是一个螺旋归纳法。

具体实现:

实际上,lcm也是一个动态规划的函数,是数列的一维动态规划。

也就是说,整体是3个动态规划函数的螺旋归纳。

在实现层面,还需要降维,把f降到1维,把lcm降到0维。

代码:

为了方便对照,写了个形式最贴近递推式的代码:

cpp 复制代码
class Solution {
public:
	int dp(int k) {
		if (k < 2)return k;
		if (m_dp[k])return m_dp[k];
		vector<int> v = GetFacs(nums[k]);
		g(v, k);
		int ans = INT_MAX;
		for (auto vi : v) ans = min(ans, f(vi, k) + 1);
		return m_dp[k]=ans;
	}
	void g(vector<int>&v,int k)
	{
		for (auto vi : v) {
			if (m_f.find(vi) == m_f.end())m_f[vi] = dp(k - 1);
			else m_f[vi] = min(m_f[vi], dp(k - 1));
		}
	}
	int f(int p, int k)
	{
		return m_f[p];
	}
	int splitArray(vector<int>& nums) {
		this->nums = nums;
		nums.insert(nums.begin(), 0);
		return dp(nums.size() - 1);
	}
	vector<int> nums;
	map<int, int>m_f;
	map<int, int>m_dp;
};

可惜代码是错的。

改正之后:

cpp 复制代码
class Solution {
public:
	int dp(int k) {
		if (k < 1)return 0;
		if (m_dp[k])return m_dp[k];
		vector<int> v = GetFacs(nums[k]);
		g(v, k);
		int ans = INT_MAX;
		for (auto vi : v) ans = min(ans, f(vi, k) + 1);
		return m_dp[k]=ans;
	}
	void g(vector<int>&v,int k)
	{
		for (auto vi : v) {
			int ans = dp(k - 1);
			if (m_f.find(vi) == m_f.end())m_f[vi] = ans;
			else m_f[vi] = min(m_f[vi], ans);
		}
	}
	int f(int p, int k)
	{
		return m_f[p];
	}
	int splitArray(vector<int>& nums) {
		this->nums = nums;
		this->nums.insert(this->nums.begin(), 0);
		auto x= dp(this->nums.size() - 1);
		return x;
	}
	vector<int> nums;
	map<int, int>m_f;
	map<int, int>m_dp;
};

改的很微妙,可能只有螺旋归纳才会出现这种现象。

逻辑对了,但是在极限用例下会超时。

把代码化简,顺便做个性能优化:

cpp 复制代码
class Solution {
public:
	int dp(int k) {
		if (k < 1)return 0;
		vector<int> v = GetFacs(nums[k]);
		for (auto vi : v) {
			int ans = m_dp[k - 1];
			if (m_f.find(vi) == m_f.end())m_f[vi] = ans;
			else m_f[vi] = min(m_f[vi], ans);
		}
		int ans = INT_MAX;
		for (auto vi : v) ans = min(ans, m_f[vi] + 1);
		return m_dp[k]=ans;
	}
	int splitArray(vector<int>& nums) {
		this->nums = nums;
		this->nums.insert(this->nums.begin(), 0);
		for (int i = 1; i < this->nums.size(); i++)dp(i);
		return m_dp[this->nums.size() - 1];
	}
	vector<int> nums;
	map<int, int>m_f;
	map<int, int>m_dp;
};

这样就不出意外的AC了。

力扣 3573. 买卖股票的最佳时机 V

给你一个整数数组 prices,其中 prices[i] 是第 i 天股票的价格(美元),以及一个整数 k

你最多可以进行 k 笔交易,每笔交易可以是以下任一类型:

  • 普通交易 :在第 i 天买入,然后在之后的第 j 天卖出,其中 i < j。你的利润是 prices[j] - prices[i]

  • 做空交易 :在第 i 天卖出,然后在之后的第 j 天买回,其中 i < j。你的利润是 prices[i] - prices[j]

注意:你必须在开始下一笔交易之前完成当前交易。此外,你不能在已经进行买入或卖出操作的同一天再次进行买入或卖出操作。

通过进行 最多 k 笔交易,返回你可以获得的最大总利润。

示例 1:

输入: prices = [1,7,9,8,2], k = 2

输出: 14

解释:

我们可以通过 2 笔交易获得 14 美元的利润:

  • 一笔普通交易:第 0 天以 1 美元买入,第 2 天以 9 美元卖出。
  • 一笔做空交易:第 3 天以 8 美元卖出,第 4 天以 2 美元买回。

示例 2:

输入: prices = [12,16,19,19,8,1,19,13,9], k = 3

输出: 36

解释:

我们可以通过 3 笔交易获得 36 美元的利润:

  • 一笔普通交易:第 0 天以 12 美元买入,第 2 天以 19 美元卖出。
  • 一笔做空交易:第 3 天以 19 美元卖出,第 4 天以 8 美元买回。
  • 一笔普通交易:第 5 天以 1 美元买入,第 6 天以 19 美元卖出。

提示:

  • 2 <= prices.length <= 103
  • 1 <= prices[i] <= 109
  • 1 <= k <= prices.length / 2

思路:螺旋归纳

首先按照单调性做区间分割,然后定义2个函数:

dp表示前vpId+1段折线内,产生最多n次交易的最大收益

dp2表示以vpId为结尾的,产生最多n次交易的最大收益

那么,dp的递推式就是,分为2种情况,即是否以vpId为结尾

而dp2的递推式就是,分为3种情况:

(1)vpId-1没有被采用,即vpId就是交易起点

(2)vpId-1有被采用,vpId不是交易起点,那么vpId-1也不是交易起点,即算上vpId之后至少是3段单调段连起来的交易(而且肯定是奇数段)

(3)vpId-1有被采用,vpId是交易起点,即交易类型反转(普通和做空的反转)

cpp 复制代码
class Solution {
public:
	long long maximumProfit(vector<int>& prices, int k) {
		this->prices = prices;
		this->vp = getBrokenLine(prices);
		m.clear();
		m2.clear();
		auto ans = dp(vp.size() - 1, k);
		return ans;
	}
	//前vpId+1段折线内,产生最多n次交易的最大收益
	long long dp(int vpId, int n)
	{
		if (vpId < 0)return 0;
		if (m[vpId].find(n) != m[vpId].end()) {
			return m[vpId][n];
		}
		return m[vpId][n] = max(dp(vpId - 1, n), dp2(vpId, n));
	}
	//以vpId为结尾的,产生最多n次交易的最大收益
	long long dp2(int vpId, int n)
	{
		if (vpId < 0)return 0;
		if (vpId == 0)return n == 0 ? 0 : abs(prices[vp[vpId].second] - prices[vp[vpId].first]);
		if (n == 0)return 0;
		if (m2[vpId].find(n) != m2[vpId].end()) {
			return m2[vpId][n];
		}
		long long ans = dp(vpId - 2, n - 1) + abs(prices[vp[vpId].second] - prices[vp[vpId].first]);
		if(vpId>1)ans = max(ans, dp2(vpId - 2, n) + getFlag(vpId) * (prices[vp[vpId].second] - prices[vp[vpId-2].second]));
		if (vpId > 0)ans = max(ans, dp2(vpId - 1, n - 1) + abs(prices[vp[vpId].second] - prices[vp[vpId].first]) - g(vp[vpId].first));
		return m2[vpId][n] = ans;
	}
	long long getFlag(int vpId)
	{
		if (prices[vp[vpId].second] > prices[vp[vpId].first])return 1;
		return -1;
	}
	long long g(int id)
	{
		return min(abs(prices[id] - prices[id + 1]), abs(prices[id] - prices[id - 1]));
	}
	vector<int> prices;
	vector<pair<int, int>> vp;
	unordered_map<int, unordered_map<int, long long>>m;
	unordered_map<int, unordered_map<int, long long>>m2;
};
相关推荐
qeen872 小时前
【算法笔记】模拟与高精度加减乘除
c++·笔记·算法·高精度·模拟
鱼很腾apoc2 小时前
【学习篇】第17期 C++入门必看——类和对象全站最详篇
c语言·开发语言·学习·算法·青少年编程
zzzsde3 小时前
【Linux】进程信号(1)理解信号及信号产生的方式
linux·运维·服务器·算法
啊哦呃咦唔鱼3 小时前
LeetCode双指针合集
算法·leetcode·职场和发展
WolfGang0073213 小时前
代码随想录算法训练营 Day37 | 动态规划 part10
算法·动态规划
baizhigangqw3 小时前
启发式算法WebApp实验室:从搜索策略到群体智能的能力进阶(二)
算法·启发式算法·web app
alphaTao3 小时前
LeetCode 每日一题 2026/4/13-2026/4/19
算法·leetcode·职场和发展
灵智实验室3 小时前
PX4姿态解算技术详解(四):姿态更新/递推与共锥补偿
算法·无人机·px 4
良木生香3 小时前
【C++初阶】C++编程基石:编码表&&STL的入门指南
c语言·开发语言·数据结构·c++·算法