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 }
题后碎碎念:害,果然困难的题没那么简单,“算法仍不熟练,代码还得多敲”