410. 分割数组的最大值(C++)---动态规划、二分查找 + 贪心 解题

题目详情

给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。

注意:
数组长度 n 满足以下条件:

  • 1 ≤ n ≤ 1000
  • 1 ≤ m ≤ min(50, n)

示例:

输入:
nums = [7,2,5,10,8]
m = 2

输出:
18

解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

 

——题目难度:困难


 




分析

解法: 动态规划

「将数组分割为 m 段,求……」是动态规划题目常见的问法,得记住。
依据题意,可以使用一个 dp 二维数组,dp[i][j] 来表示 将数组 nums 的前 i 个数分割为 j 段所能得到最大连续子数组和的最小值。再使用一个 sub 数组,sub[i] 表示数组 nums 的前 i 个数之和。

状态转移时,固定好 i 后( i 为右边界),考虑第 j 段的具体范围,然后来枚举 k,其中前 k 个数被分割为 j - 1 段,而第 k + 1 到第 i 个数为第 j 段,因此,第 j 段子数组中和的最大值 就等于 dp[k][k - 1] 与 sub[i] - sub[k] 中的较大值,然后再不断遍历 k 的过程中,刷新 dp[i][j],以此来得到  最小值的 dp[i][j]。

接下来确定边界条件 初始情况
1.就在一开始,当 i = 1,j = 1时,唯一的可能性就是前 i 个数被分到了一段(k = 0),那么要求
max(dp[0][0], sub[1] - sub[0]) 的结果要为 sub[1] - sub[0],因此 dp[0][0] 得初始化为 0
也就是当 j = 1,k ≠ 0时,dp[k][0] 是不合法的状态,又由于是由 dp[0][0] 所推导上去的,所以也说明 dp[0][0] 得初始化为 0
2.对于状态 dp[i][j],当 i < j 是不合法的,由于目标是求出最小值,因此可以先把这些状态初始化为一个很大的数(dp[0][0] 除外)。这样一来,在状态转移的过程中,当出现 f[k][j - 1] (j - 1 > k) 时, max(...)将是一个很大的数,也就不会对最外层的 min(...) 产生影响。


-下面代码

class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
    	int n = nums.size();
		vector<vector<long long>> dp(n + 1, vector<long long>(m + 1, LONG_LONG_MAX));
		vector<long long> sub(n + 1, 0);
		for(int i = 0; i < n; i++) {
			sub[i + 1] = sub[i] + nums[i];
		}
		dp[0][0] = 0;
		
		for(int i = 1; i <= n; i++) {
			for(int j = 1; j <= min(i, m); j++) {
				for(int k = 0; k < i; k++) {
					dp[i][j] = min(dp[i][j], max(dp[k][j - 1], sub[i] - sub[k]));
				}
			}
		}
		
		return dp[n][m];
    }
};




 





解法: 二分查找 + 贪心

「使……最大值尽可能小」是二分搜索题目常见的问法
依据题意,子数组的最大值是有范围的,即在区间 [max(nums),sum(nums)] 之中。

令 left = max(nums),right = sum(nums),mid = (l + h) / 2,
计算数组和最大值不大于 mid 对应的子数组个数 cnt(这里用到了贪心解法:猜好一个 mid 值后,然后遍历数组划分,使每个子数组和都最接近 mid(贪心地逼近 mid),这样得到的子数组数一定最少。
如果即使是这样子数组的数量仍然多于 m 个,那么显然说明这次 mid 猜小了,因此 left = mid + 1;
那如果是这样得到的子数组数量正好为 m 个,那么 mid 是否可以再小一点呢?可以试试看,所以 right = mid;
如果这样得到的子数组数量小于 m 个,那么说明 mid 取大了,所以得缩小 mid,所以 right = mid)


通过二分查找,不断缩小搜索区间,最后可以得到的区间里只有一个元素(left = right),也就是得到最小的最大分割子数组和,这样就可以得到最终的答案了。



-下面代码

class Solution {
public:
    int splitArray(vector<int>& nums, int m) {
		long long left = nums[0], right = 0;
		for(int num : nums) {
			left = num > left ? num : left;
			right += num;
		}
		
		while (left < right) {
			long long mid = (left + right) / 2;
			long long temp = 0;
			int cnt = 1;
			for(int num : nums) {
				temp += num;
				if (temp > mid) {
					temp = num;
					cnt++;
				}
			}
			
			if (cnt > m) {
				left = mid + 1;
			} else { //cnt <= m
				right = mid;
			}
		}
		
		return left;
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

重剑DS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值