题目描述:任意2n个正整数数组,将其分割成两个长度为n的数组,使两子数组之和的差值最小。
或者:从2n个正整数中选取n个,使这n个数字之和和剩余n个数字之和的差值最小。
这道题利用动态规划进行求解,可以采用0-1背包问题的策略,放或者不放,不太了解0-1背包的,参考链接https://blog.youkuaiyun.com/qq_34826261/article/details/100663768。
假设数组的和为sum,我们的目标可以转化为求数组中n个元素之和和最接近sum/2的那个和,大于或者小于关于sum/2对称,找小于sum/2的最接近sum/2的那个和更加方便。定义f(j,k)为有j个数字,和为k的标志,即对长为2n的数组进行遍历时,是否可将当前数字放入使其构成长为j,和为k的分组,是则为true,也就是说最后得到的结果在f(n,k)=true(k<=sum/2且最接近sum/2)中,其中一个分组的和为:
(1)
k
=
m
a
x
{
f
(
n
,
k
)
=
t
r
u
e
∣
k
<
=
s
u
m
/
2
}
k=max\{f(n,k)=true|k<=sum/2\}\tag{1}
k=max{f(n,k)=true∣k<=sum/2}(1)
先放出来更新公式:
(2)
f
(
j
,
k
)
=
t
r
u
e
,
i
f
(
k
>
=
A
[
i
]
&
&
f
(
j
−
1
,
k
−
A
[
i
]
)
=
t
r
u
e
)
,
i
<
=
2
n
f(j,k)=true,if(k>=A[i]\&\&f(j-1,k-A[i])=true),i<=2n\tag{2}
f(j,k)=true,if(k>=A[i]&&f(j−1,k−A[i])=true),i<=2n(2)
对更新公式解释,对于任意长度的子数组的和有以下几种情况,h(i,j)表示从长i的数组中取j个元素之和的集合,
(3)
h
(
i
,
0
)
=
{
0
}
h
(
i
,
1
)
∈
{
A
[
m
]
∣
m
<
=
i
}
h
(
i
,
j
)
=
{
h
(
i
−
1
,
j
−
1
)
+
A
[
i
]
∣
j
<
i
}
∪
{
h
(
i
−
1
,
j
)
∣
j
<
i
}
h
(
i
,
i
)
=
{
A
[
0
]
+
A
[
1
]
+
.
.
.
+
A
[
i
]
}
h(i,0) = \{0\}\\ h(i,1) ∈ \{A[m]|m<=i\}\\ h(i,j) = \{h(i-1,j-1)+A[i]|j<i\}∪\{h(i-1,j)|j<i\}\tag{3}\\ h(i,i) = \{A[0]+A[1]+...+A[i]\}
h(i,0)={0}h(i,1)∈{A[m]∣m<=i}h(i,j)={h(i−1,j−1)+A[i]∣j<i}∪{h(i−1,j)∣j<i}h(i,i)={A[0]+A[1]+...+A[i]}(3)
如果对于任意的j,h(i,j)中存在k,则f(j,k) = true。对于任意f(j,k)=true,则k∈h(i,j),则必有k+A[m]∈h(i,j+1),m<i,则f(j,k+A[m]) = true,反过来,若k∈h(i,j),则必存在k-A[m]∈h(i,j-1),本段新加入的A[m]都和已有的j个元素不重复。
下面考虑(2)式,假设我们已经获取了i-1时的f(j,k),则i时如何更新f(j,k),不难发现从i-1到i仅仅多出了A[i]这一个数字,相当于(3)式的第三个式子等号右边的第一项,这时候若h(i-1,j-1)中存在k-A[i],即f(j-1,k-A[i])=true,则f(j,k) = true。
代码如下:
public int minDValueofN(int[] nums) {
int sum = 0;
int len = nums.length / 2;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
}
boolean[][] flag = new boolean[len+1][sum/2+1];
flag[0][0] = true;
for(int i = 1; i <= nums.length; i++) {
// 注意j是逆序更新
// 这里是因为更新flag[j][]需要使用flag[j-1][],后面的更新不会影响前面的值
// 实际上可以做一个三维的动态规划flag[i][j][k] = flag[i-1][j-1][k-nums[i-1]]
// 但是没有必要,可以参考一下01背包问题的优化
// https://blog.youkuaiyun.com/qq_34826261/article/details/100663768
for(int j = i<=len?i:len; j > 0; j--) {
for(int k = 0; k <= sum/2; k++) {
if(k >= nums[i-1] && flag[j-1][k-nums[i-1]]) {
flag[j][k] = true;
}
}
}
}
// 找出最接近sum/2的k值
for(int k = sum/2; k > 0; k--) {
if(flag[len][k]) {
return Math.abs(2*k - sum);
}
}
return -1;
}