2020.7.25 力扣每日

 1 class Solution {
 2     public int splitArray(int[] nums, int m) {
 3         int len = nums.length;
 4         int[][] dp = new int[len + 1][m + 1];                  //动态规划数组,dp[i][j]代表前i个数分为j个连续子数组时的解
 5         int[] sub = new int[len + 1];                          //前缀和
 6         for (int i = 1; i <= len; i++){
 7             sub[i] =sub[i - 1] + nums[i - 1];
 8         }
 9         for (int i = 0; i <= len; i++) {
10             Arrays.fill(dp[i], Integer.MAX_VALUE);             //初始化,避免初始化0造成异常
11         }
12         dp[0][0] = 0;                                          //边界处理
13         for (int i = 1; i <= len; i++){                        //i为前n个数
14             for (int j = 1; j <= Math.min(i, m); j++){         //j为分为j个数组
15                 for (int k = j - 1; k < i; k++ ){   
16                     //dp[i][j]的值等于前k个数分为j-1个组与剩下的前缀和sub[i]-sub[k]中的最大值                           
17                     //最后取出其最小值的情况,作为最优解dp[i][j]
18                     dp[i][j] = Math.min(dp[i][j], Math.max(dp[k][j - 1], sub[i] - sub[k]));
19                 }
20             }
21         }
22         return dp[len][m];
23     }    
24 }

解题思路:

   对于此类求“分割为m组,求最优解”的题目,一般都可用动态规划来解决。首先定义一个动态数组dp[i][j]来表示前i个数,分成j个数组的解。接着分析题目,试图写出动态方程,由于题目要求的子数组为连续的数组,也就是说对于dp[i][j]来说,最后一组的第j段数组必定包括数组中的最后一位数字nums[i],也就是说剩余部分数字分为了j-1段。我们可以利用k来表示剩余部分的数字总个数,由于已经使用了动态规划,我们必定已经求出了dp[k][j-1]的最优解,其中k为小于i的值,而最后一段第k段的数组则可以用前缀和sub[i]-sub[k]来表示,因此此时的dp[i][j]就为两者中的最大值

   此时我们再来考虑k的取值,由于它是分割为j-1段的剩余部分,那么其数字就必须大于等于j-1,而最后一段数组又必须包括第i位sum[i],也就是说k必须小于i,整理可得出k的取值为[j-1,i)

   最后,遍历所有的k值,取出其中对应的dp[i][j]最小值就是前i个数,分为j个数组时的最优解。

注意点:

   对于dp[i][j],我们需取出其所有情况中的最小值来作为解,那么显然动态数组的初值不能设置为0,需设置为Integer.MAX_VALUE

   dp数组的边界情况,题目要求n,m均大于等于1,也就是i,j初值均为1,所以我们只需设置dp[0][0]=0即可。

时间复杂度:O(N*M^2),N为数组长度,M为子数组个数

空间复杂度:O(N*M)

优化:

   提交后发现这并非最优解,该题可以使用二分查找+贪心算法进行优化,以下代码及注释转载自LeetCode,作者:Adder。

 1 class Solution {
 2     public int splitArray(int[] nums, int m) {
 3         long left ,right;//计算nums的和的时候,可能超过int表示的最大值
 4         left = right = nums[0];//初始化left和right
 5         for (int i = 1; i < nums.length; i++) {//找到left和right
 6             right+=nums[i];//求和
 7             left = Math.max(left,nums[i]);//求最大值
 8         }
 9         while (left<right){//二分查找开始
10             long mid = (left + right) >>1;//求中间数
11             if (check(nums,mid,m)){//如果这个最大的和满足,则把这个最大和变小,然后验证
12                 right = mid;
13             }else left = mid+1;//如果不满足,那么这个最大和需要变大
14         }
15         return (int)left;
16     }
17 
18     private boolean check(int[] nums, long x, int m) {//判断既定的x每个数组的最大和,是否符合
19         long sum = 0;//数组的最大和
20         int cnt = 1;//初始化有几个数组,刚开始至少有1个
21         for (int i = 0; i < nums.length; i++) {
22             if (sum + nums[i]>x){//子数组的和比给定的最大的还大
23                 cnt++;//需要开始新的子数组,个数+1
24                 sum = nums[i];//初始化子数组和
25             }else {//不超过子数组和
26                 sum += nums[i];
27             }
28         }
29         return cnt <= m;//如果不超过给定个数,那么就是符合的,否则不符合
30     }
31 }

   题后碎碎念:害,果然困难的题没那么简单,“算法仍不熟练,代码还得多敲”

猜你喜欢

转载自www.cnblogs.com/-TTY/p/13394252.html