Minimum Partition
题目
http://www.lintcode.com/zh-cn/problem/minimum-partition/
分析
mp(n, sum1):子集1的和为sum1时,对第n个数进行操作后的最小划分和。
当子集和为sum1时,对第n个数进行划分,它可能被划分到第一个子集中,也可能被划分到第二个子集中。我们需要处理的是,选择最小的划分。
如果将第n个数添加进子集1中,子集1的和为sum1+arr[n-1]。此时需要对前n-1个数进行选择划分,即为状态mp(n-1, sum1+arr[n-1])。
如果将第n个数加入子集2中,子集1的和不变,为sum1,此时状态为mp(n-1, sum1)。
边界情况为n等于0时,此时不需要对数进行选择,只需完成基本操作,即将子集和1与子集和2的绝对差返回。
编程
此题二维数组爆栈,先写出爆栈做法,再用滚动数组解决栈溢出问题。
自顶向下记忆法
#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;
class Solution {
public:
/**
* @param nums: the given array
* @return: the minimum difference between their sums
*/
int findMin(vector<int> &nums) {
// write your code here
int sum = 0;
for(vector<int>::iterator it = nums.begin(); it != nums.end(); it++) {
sum += *it;
}
int dp[(int)nums.size()+1][sum+1];
memset(dp, -1, sizeof(int) * (nums.size()+1) * (sum+1));
return mp(nums.size(), 0, sum, nums, &dp[0][0]);
}
int mp(int n, int sum1, int sum, vector<int> &nums, int * dp) {
if(*(dp + n * (sum+1) + sum1) != -1)
return *(dp + n * (sum+1) + sum1);
if(n == 0) {
return *(dp + n * (sum+1) + sum1) = abs(sum - 2*sum1);
}
return *(dp + n * (sum+1) + sum1) = min(mp(n-1, sum1+nums[n-1], sum, nums, dp), mp(n-1, sum1, sum, nums, dp));
}
int min(int a, int b) {
return a < b ? a : b;
}
};
int main() {
// int a[] = {987,523,979,847,734,706,452,903,702,332,713,181,991,843,879,505,718,694,18,303,795,521,696,388,866,908,350,528,445,780,864,295,257,337,704,648,495,949,39,33,606,553,618,191,854,405,715,413,472,185,216,489,212,199,162,462,929,191,429,726,902,9,579,403,370,435,871,160,197,884,619,716,182,7,906,974,679,531,852,158,861,174,445,701,871,557,942,798,921,389,450,485,901,179,515,401,117,451,731,828,685,20,50,673,891,232,30,385,511,338,375,118,81,392,296,546,903,59,580,620,268,422,597,876,333,766,158,295,443,204,434,357,632,592,543,341,434,58,525,683,338,165,332,51,152,191,378,63,10,475,951,469,622,811,296,415,282,547,994,358,134,195,888,75,195,805,908,673,867,346,935,318,603,507,45,209,54,641,515,867,881,880,290,781,452,808,775,998,731,908,451,592,608,87,1000,812,30,673,393,380,241,135,421,144,954,64,747,502,633};
int a[] = {1, 6, 11, 5};
vector<int> nums(a, a + sizeof(a)/sizeof(int));
Solution * s = new Solution();
printf("%d\n", s->findMin(nums));
}
使用滚动数组
#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;
class Solution {
public:
/**
* @param nums: the given array
* @return: the minimum difference between their sums
*/
int findMin(vector<int> &nums) {
// write your code here
int sum = 0;
for(vector<int>::iterator it = nums.begin(); it != nums.end(); it++) {
sum += *it;
}
int dp[2][sum+1];
memset(dp, -1, sizeof(int) * 2 * (sum+1));
return mp(nums.size(), 0, sum, nums, &dp[0][0]);
}
int mp(int n, int sum1, int sum, vector<int> &nums, int * dp) {
if(*(dp + (n%2) * (sum + 1) + sum1) != -1)
return *(dp + (n%2) * (sum + 1) + sum1);
if(n == 0) {
return *(dp + (n%2) * (sum + 1) + sum1) = abs(sum - 2*sum1);
}
return *(dp + (n%2) * (sum + 1) + sum1) = min(mp(n-1, sum1+nums[n-1], sum, nums, dp), mp(n-1, sum1, sum, nums, dp));
}
int min(int a, int b) {
return a < b ? a : b;
}
};
int main() {
int a[] = {860,5,572,168,878,820,247,661,577,185,136,315,230,413,596,379,991,645,468,699,554,850,340,851,413,237,98,324,436,270,33,619,603,809,668,144,679,827,581,827,215,337,27,34,762,742,779,112,970,283,302,743,437,248,130,31,300,748,1000,644,848,52,1,199,79,515,678};
// int a[] = {987,523,979,847,734,706,452,903,702,332,713,181,991,843,879,505,718,694,18,303,795,521,696,388,866,908,350,528,445,780,864,295,257,337,704,648,495,949,39,33,606,553,618,191,854,405,715,413,472,185,216,489,212,199,162,462,929,191,429,726,902,9,579,403,370,435,871,160,197,884,619,716,182,7,906,974,679,531,852,158,861,174,445,701,871,557,942,798,921,389,450,485,901,179,515,401,117,451,731,828,685,20,50,673,891,232,30,385,511,338,375,118,81,392,296,546,903,59,580,620,268,422,597,876,333,766,158,295,443,204,434,357,632,592,543,341,434,58,525,683,338,165,332,51,152,191,378,63,10,475,951,469,622,811,296,415,282,547,994,358,134,195,888,75,195,805,908,673,867,346,935,318,603,507,45,209,54,641,515,867,881,880,290,781,452,808,775,998,731,908,451,592,608,87,1000,812,30,673,393,380,241,135,421,144,954,64,747,502,633};
// int a[] = {1, 6, 11, 5};
vector<int> nums(a, a + sizeof(a)/sizeof(int));
Solution * s = new Solution();
printf("%d\n", s->findMin(nums));
}
在提交代码之后,对于上述输入,OJ上会出现期望:0,输出:20614的结果。但在本机上输出的是0。这是由于两编译器运行顺序不同造成的。
将代码min(mp(n-1, sum1+nums[n-1], sum, nums, dp), mp(n-1, sum1, sum, nums, dp))
这一行,min中参数对换顺序,就会出现本机输出20118,OJ运行通过。
因此,我们通过讲解滚动数组来说明为什么会出现这种情况。参考此文
在分析中,我们提到了状态转移方程,当前状态仅与前一个状态有关,因此,我们进行存储子问题时,可以按照相对位置存储,即当前状态下标为i%2,上一个状态的下标为(i-1)%2.由此,可以对存储子问题解的二维数组进行压缩,避免超出栈大小。
由此可知,若当前状态与上两个状态相关,则可用i%3。当与前k-1个状态相关,可用i%k,第一维的大小为k。
我们采用自底向上的制表法更直观的看待这一阐述。
自底向上制表法
#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;
class Solution {
public:
/**
* @param nums: the given array
* @return: the minimum difference between their sums
*/
int findMin(vector<int> &nums) {
return _findMin(nums, nums.size());
}
int _findMin(vector<int> arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++)
sum += arr[i];
bool dp[2][sum+1];
memset(dp, 0, sizeof(bool) * 2 *(sum+1));
dp[0][0] = true; dp[1][0] = true;
for (int i=1; i<=n; i++) {
for (int j=1; j<=sum; j++) {
dp[i%2][j] = dp[(i-1)%2][j];
if (arr[i-1] <= j)
dp[i%2][j] |= dp[(i-1)%2][j-arr[i-1]];
}
}
int diff = INT_MAX;
for (int j=sum/2; j>=0; j--) {
if (dp[n%2][j] == true) {
diff = sum-2*j;
break;
}
}
return diff;
}
};
int main() {
int a[] = {616,202,595,876,388,120,238,296};
vector<int> nums(a, a + sizeof(a)/sizeof(int));
Solution * s = new Solution();
printf("%d\n", s->findMin(nums));
}
使用制表法,则在本机和OJ都运行正确。而采用记忆法,由于不同的编译环境对于方法参数的调用顺序不同等原因,造成不同的结果。
因而,如果在OJ过程中,出现本机结果与OJ结果不同,可能是由于编译环境的差异造成的。
本节介绍了滚动数组,解决了错误:本机与OJ结果不同,解决方法:将参数对换 或采用自底向上制表法,尽量避免由于编译环境不同造成的错误。
时间复杂度:
O(n∗sum)
O
(
n
∗
s
u
m
)
空间复杂度:视dp数组大小而定。