leetcode--918:环形子数组最大和

文章详细解析了如何解决LeetCode第918题,即2020年10月11日美团笔试题。通过分析题目背景和描述,确定问题是关于在循环链表中找到最大子数组和。文章提出了动态规划的解决方案,包括两种动态规划版本,并讨论了如何处理跨区域情况和优化策略,还提供了一个数学角度的解决方案。
摘要由CSDN通过智能技术生成

目录

题目背景

题目描述

如何分析?

难点

设计优化:

角度1:

角度2:动态规划

动态规划版本1

动态规划版本2

 

角度三:数学角度


题目背景

1.取自leetcode第918道

2.该题为2020年10月11日美团笔试题

题目描述

 


 

如何分析?

首先,根据题目含义这道题的存放空间实际上是一个“循环链表”,即数组模拟的循环链表

其次,这道题提出的“子数组”是在“循环链表”中连续即可。

再者,我们提出题目的大概含义:最大子数组和。提取出这些关键字,那么我们可以很快反应过来,也许可以使用--动态规划

但是,这道题一般的动态规划难以解决,因为使用数组模拟循环链表,必然导致存在跨区域的情况

如果你们能提出这些关键字怎么办?最好的办法是,抛开效率,实现功能,逐步优化,提高功效


难点

根据以上的分析,难点在于:

1.跨区域的情况如何处理?

2.如果没有经验如何设计,实现代码。

对于难点1,我们采用两种策略。

策略1:区间拼接/向量加法

策略2:数学结论


设计优化:

角度1:

(有经验可以跳过)

从抛开效率的角度讲,我们只需要去枚举所有区间就可以了。所以有以下代码:

class Solution {
	const int INF = 0x3f3f3f3f;
public:
	int maxSubarraySumCircular(std::vector<int>& nums) {
		int n = nums.size();
		
		int ans = -INF;
		for (int i = 0; i < n; ++i) {//以nums[i]为首
			int sum = nums[i], tmp_ans = nums[i];
            //逐个求和,获取区间和,寻找最大值
			for (int j = (i + 1) % n; j != i; j = (j + 1) % n) {
				sum += nums[j];

				if (sum > tmp_ans) tmp_ans = sum;//tmp_ans记录临时最大值
			}

			if (tmp_ans > ans) ans = tmp_ans;
		}

		return ans;
	}
};

我们很容易看出来,此段代码的时间复杂度为O(^{n^{2}})。但是我们的数据规模在 3*10^{4},所以面对这种复杂度我们很难通过。我们必须面临优化。

可是,从这种角度看,我们似乎已经无路可走了。不过,这段代码是我们优化过的。真正的枚举到底指什么?

真正的枚举是指枚举出每一个区间,对区间进行求和,再寻找最大值。我们知道枚举所有区间需要n^{2}的复杂度,求和需要n的复杂度。所以,真正的枚举复杂度是n^{3}

那么这段代码,看似枚举到底优化在哪里?

这点优化就是角度2。

角度2:动态规划

在角度1中,我们初次看到了角度2的影子。动态规划最最重要的就是利用每次状态转移关系的弱相关,从而通过简单的记录状态快速求解问题

再次看到这道题目,我们发现如果在求和操作中,每一次的状态转移关系是简单的。

ps:每一次的状态转移关系是,固定区间左端点(一个端点)后,状态改变是简单的。

所以我们可以通过记录信息状态来求解这个问题

而角度1的优化代码在求和时,就运用到了这个技巧。

所以我们可以设计状态数组dp[n] 表示从["左端点", i]区间中最大的序列和。

那么我们有状态转移方程:

       dp[i] = max{nums[i], dp[(i - 1 + n) % n] + nums[i]}

其中呢,dp就数据就是tmp_ans。

所以我们可以将角度一的代码改写为如下两个版本:

动态规划版本1

class Solution {
	const int INF = 0x3f3f3f3f;
public:
	int maxSubarraySumCircular(std::vector<int>& nums) {
		int n = nums.size();
		
		int ans = -INF;
		for (int i = 0; i < n; ++i) {//以nums[i]为首
			vector<int> dp = vector<int>(n);
            dp[i] = nums[i]
            //逐个求和,获取区间和,寻找最大值
			for (int j = (i + 1) % n; j != i; j = (j + 1) % n) {
				dp[j] = std::max(dp[(j - 1 + n) % n] + nums[j], num[j]);

				if (dp[j] > ans) ans = dp[j];
			}
		}

		return ans;
	}
};

动态规划版本2

class Solution {
	const int INF = 0x3f3f3f3f;
public:
	int maxSubarraySumCircular(std::vector<int>& nums) {
		int n = nums.size();
		
		int ans = -INF;
		for (int i = 0; i < n; ++i) {//以nums[i]为首
            int sum = nums[i];
            //逐个求和,获取区间和,寻找最大值
			for (int j = (i + 1) % n; j != i; j = (j + 1) % n) {
				sum = std::max(sum + nums[j], nums[j]);
				if (sum > ans) ans = sum;
			}
		}

		return ans;
	}
};

因为跨界情况的存在,使得我们需要对首元素需要枚举。以上三分代码都没有对这个点进行优化!

我们来说说,为什么需要对首元素枚举。

这个道理很简单。数组始终是数组,虽然我们可以使用取余运算使其具有“循环性”。但是,在链表区间上,我们想要枚举清楚,就必要将循环链表断成n条单向链表(数组);

那么说完产生原因,我们来看看,如何处理跨界问题。

我们记nums[i: j]为从编号为i的节点开始到编号j的节点进行求和。

此时我们求解出了nums[0 : i]和nums[j: n]。那么我们要处理的跨界情况值就是nums[0: i] + nums[j: n]。而我们的跨界最大值为

 max{nums[j: n] + nums[0: i], 其中 (j - 1 + n) % n \geq i \geq 0};

这是我们枚举的情况,但是你还记得第一版和第二版的动态规划吗?我们可以记录max{num[0: i]};

所以有了以下代码:

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        int n = nums.size();
        vector<int> leftMax(n);//记录[0: n]最大子区间和
        // 对坐标为 0 处的元素单独处理,避免考虑子数组为空的情况
        leftMax[0] = nums[0];
        int leftSum = nums[0];
        int pre = nums[0];//第一版的空间优化,等于第二版sum
        int res = nums[0];//最后的返回答案
        for (int i = 1; i < n; i++) {
            pre = max(pre + nums[i], nums[i]);
            res = max(res, pre);
            leftSum += nums[i];
            leftMax[i] = max(leftMax[i - 1], leftSum);
        }

        // 从右到左枚举后缀,固定后缀,选择最大前缀
        int rightSum = 0;
        for (int i = n - 1; i > 0; i--) {
            rightSum += nums[i];
            res = max(res, rightSum + leftMax[i - 1]);//处理跨区间情况
        }
        return res;
    }
};

 

角度三:数学角度

前面两个角度,我们通过“动态规划”的方式去求解问题。或者说,这是区间拼接/向量加法;

我们还有一种数学角度,最去区间和问题。有一个很常用简单的数学结论:区间固定,无论对区间内部做任何调换,都不改变区间总值

还记得我们之前说的,产生枚举首元素的原因吗?因为我们要包含所有可能,所以我们去枚举首元素。而枚举首元素我们也解释过,他是一种切断链表的操作。反应到数组中,是调整了数组内部的编号顺序。

例如:原来我们有数组nums = [1,2,3];

那么我们在枚举以nums[1]作为首元素,其实就是将数组变成了nums = [2,3,1];

但是,根据数学结论,区间内部的调动不会改变总值。也就是说,无论谁是首元素,总值不变

我们记总值为sum,它与我们所要求取最子区间和的关系为:

sum = max_sum + min_sum;

稍作变化:

max_sum = sum - min_sum;

那么我们选取nums[0]作为首元素,正如上图所示,

当min_subarray(min_sum) 是连续的,那么最大值就是跨界的情况

当最大值是在不跨界中产生时,我们就可以使用动态规划解决

而对我们来说,跨界正是我们头疼的问题。所以我们只需要在nums[0]为首元素的时候,记录不跨界最大值,和不跨界最小值就可以了以及总值。

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
        int n = nums.size();
        int preMax = nums[0], maxRes = nums[0];
        int preMin = nums[0], minRes = nums[0];
        int sum = nums[0];
        //以nums[0]为首元素,记录不跨界情况的最大值和最小值
        for (int i = 1; i < n; i++) {
            preMax = max(preMax + nums[i], nums[i]);
            maxRes = max(maxRes, preMax);//记录最大值
            preMin = min(preMin + nums[i], nums[i]);
            minRes = min(minRes, preMin);//记录最小值
            sum += nums[i];//计算总值
        }
        if (maxRes < 0) {
            return maxRes;
        } else {
            //最大值在不跨界(maxRes)和跨界(sum - minRes)中产生。
            return max(maxRes, sum - minRes);
        }
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值