螺旋归纳DP

目录

螺旋归纳

力扣 LCP 14. 切分数组

力扣 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维。

代码:

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

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;
};

可惜代码是错的。

改正之后:

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;
};

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

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

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

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了。

力扣 3130. 找出所有稳定的二进制数组 II

给你 3 个正整数 zero ,one 和 limit 。

一个 二进制数组 arr 如果满足以下条件,那么我们称它是 稳定的 :

  • 0 在 arr 中出现次数 恰好 为 zero 。
  • 1 在 arr 中出现次数 恰好 为 one 。
  • arr 中每个长度超过 limit 的 子数组 都 同时 包含 0 和 1 。

请你返回 稳定 二进制数组的  数目。

由于答案可能很大,将它对 109 + 7 取余 后返回。

示例 1:

输入:zero = 1, one = 1, limit = 2

输出:2

解释:

两个稳定的二进制数组为 [1,0] 和 [0,1] ,两个数组都有一个 0 和一个 1 ,且没有子数组长度大于 2 。

示例 2:

输入:zero = 1, one = 2, limit = 1

输出:1

解释:

唯一稳定的二进制数组是 [1,0,1] 。

二进制数组 [1,1,0] 和 [0,1,1] 都有长度为 2 且元素全都相同的子数组,所以它们不稳定。

示例 3:

输入:zero = 3, one = 3, limit = 2

输出:14

解释:

所有稳定的二进制数组包括 [0,0,1,0,1,1] ,[0,0,1,1,0,1] ,[0,1,0,0,1,1] ,[0,1,0,1,0,1] ,[0,1,0,1,1,0] ,[0,1,1,0,0,1] ,[0,1,1,0,1,0] ,[1,0,0,1,0,1] ,[1,0,0,1,1,0] ,[1,0,1,0,0,1] ,[1,0,1,0,1,0] ,[1,0,1,1,0,0] ,[1,1,0,0,1,0] 和 [1,1,0,1,0,0] 。

提示:

  • 1 <= zero, one, limit <= 1000

int m0[1001][1001];
int m1[1001][1001];
int limit;
int p = 1000000007;

int dp0(int zero, int one);
int dp1(int zero, int one)
{
	if (m1[zero][one]>=0) {
		return m1[zero][one];
	}
	if (zero == 0)return (one <= limit);
	if (one == 0)return 0;
	int ans1 = (dp0(zero, one - 1) + dp1(zero, one - 1)) % p;
	if (one - 1 >= limit) {
		ans1 = (ans1 + p - dp0(zero, one - 1 - limit)) % p;
	}
	return m1[zero][one] = ans1;
}
int dp0(int zero, int one)
{
	if (m0[zero][one]>=0) {
		return m0[zero][one];
	}
	if (zero == 0)return 0;
	if (one == 0)return (zero <= limit);
	int ans0 = (dp0(zero - 1, one) + dp1(zero - 1, one)) % p;
	if (zero - 1 >= limit) {
		ans0 = (ans0 + p - dp1(zero - 1 - limit, one)) % p;
	}
	return m0[zero][one] = ans0;
}

class Solution {
public:
	int numberOfStableArrays(int zero, int one, int limits)
	{
		limit = limits;
		for (int i = 0; i < 1001; i++) {
			memset(m0[i], -1, 1001 * sizeof(int));
			memset(m1[i], -1, 1001 * sizeof(int));
		}
		return (dp0(zero, one) + dp1(zero, one)) % p;
	}
};

力扣 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是交易起点,即交易类型反转(普通和做空的反转)

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;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值