题目来源于传说中的华为面试题:
有两个长度都为 n 的数组,分别为 a,b。数组元素类型为整型,值任意且无序。要求通过交换 a,b 数组的元素,使得数组 a 的和与数组 b 的和的差最小。
这个题目网上很多博客给出的一种解法都是错的,他们的思路是每次交换一对数据,如果交换能使得差变小就交换,否则就不交换。这个解法简单直观,其实本来目的就是穷举。可是本题却存在这种情况,即找不到任何一对数据的交换使得差变小(这也是上面算法的停止条件),但存在同时交换多对数据使得差变小的情况。
网上的这个主流解法错就错在没有考虑同时交换多对数据的情况,下面我将介绍本题的一种使用动态规划的解法。
使用动态规划求解
把原问题看作:有 2n 个非负整数(题目给的条件是值任意,后面我会介绍转换方法),和为 S 2 n S_{2n} S2n,我们要在 2n 个数中选出 n 个数,使得这 n 个数的和 S n S_{n} Sn 满足: m i n ( S 2 n 2 − S n ) min(\frac{S_{2n}}{2}-S_{n}) min(2S2n−Sn)
描述为背包问题就是:
从 2n 件物品中选出 n 件物品,放入容量为 S 2 n 2 \frac{S_{2n}}{2} 2S2n 的背包中,使得背包装的东西尽可能的多。(把第 i 个元素的值,既看作第 i 件物品的开销也看作第 i 件物品的价值)
这其实是一个在普通的 0-1 背包上多加了一个限制条件的背包问题,多加了放入背包物品的数量的限制。
我们可以在原来的基础上增加一维以满足新的限制。
状态转移方程:
设 f[i][j][v] 表示前 i 件中选出 j(j <= i)件放入容量为 v 的背包中所能获得的最大价值,a[i] 为第 i 个元素的值。
f
[
i
]
[
j
]
[
v
]
=
m
a
x
{
f
[
i
−
1
]
[
j
]
[
v
]
,
f
[
i
−
1
]
[
j
−
1
]
[
v
−
a
[
i
]
]
+
a
[
i
]
}
f[i][j][v]=max\{f[i-1][j][v],f[i-1][j-1][v-a[i]]+a[i]\}
f[i][j][v]=max{f[i−1][j][v],f[i−1][j−1][v−a[i]]+a[i]}
空间优化后为:
f
[
j
]
[
v
]
=
m
a
x
{
f
[
j
]
[
v
]
,
f
[
j
−
1
]
[
v
−
a
[
i
]
]
+
a
[
i
]
}
f[j][v]=max\{f[j][v],f[j-1][v-a[i]]+a[i]\}
f[j][v]=max{f[j][v],f[j−1][v−a[i]]+a[i]}
数据预处理(填坑)
本题满足平移不变性,即所有元素加上或减去一个整数,两数组和的差不变。
我们可以利用这个性质,将所有元素都减去 2n 个元素中的最小值。
- 好处一:如果 2n 个元素中存在负数,则最小值必然是负数,减去这个负数,可以使得所有元素非负,进而可以使用背包求解。
- 好处二:如果 2n 个元素本来就是正数,则全部都减一个最小值,可以减小背包容量,从而降低求解的空间复杂度。
参考代码:
#include<stdio.h>
#define LEN 8
int select(int *a) {
int i, j, v;
int imax = LEN;
int jmax = imax / 2;
int suma = 0;
// 求和
for (i = 0; i < LEN; i++){
suma += a[i];
}
int vmax = suma / 2;
int dp[jmax + 1][vmax + 1], sel[jmax + 1][vmax + 1];
// 初始化
for (j = 0; j <= jmax; j++){
for (v = 0; v <= vmax; v++){
if (j == 0){
dp[j][v] = 0;
}
else{
dp[j][v] = -1;
}
sel[j][v] = 0;
}
}
// i 从 1 开始而不是从 0,是为了避免 j-1<0 越数组下界的情况
for (i = 1; i <= imax; i++){
// 因为 dp 数组减少了一维,又因为物品不能重复放入,所以要从后向前遍历更新
for (j = i > jmax ? jmax : i; j >= 1; j--){
for (v = a[i - 1]; v <= vmax; v++){
if (dp[j - 1][v - a[i - 1]] < 0) continue;
else if (dp[j - 1][v - a[i - 1]] + a[i - 1] > dp[j][v]){
dp[j][v] = dp[j - 1][v - a[i - 1]] + a[i - 1];
// 记录此时放入背包中的物品
sel[j][v] = sel[j - 1][v - a[i - 1]] | (1 << (i - 1));
}
}
}
}
printf("分配后两数组和的差为:%d", suma - 2*dp[jmax][vmax]);
return sel[jmax][vmax];
}
void outputGroup(int *a, int sel) {
printf("\n数组a的元素分别为:");
for (int i = 0; i < LEN; i++){
if (sel & (1 << i)){
printf("%d ", a[i]);
}
}
putchar('\n');
}
int main() {
// 测试数据
int arry[LEN] = { 54, -58, 22, 49, 64, -21, 33, 90 };
int a[LEN];
for (int i = 0; i < LEN; i++){
a[i] = arry[i];
}
// 下面对所有数据进行预处理,减去最小值
int min = a[0];
for (int i = 1; i < LEN; i++){
if (a[i] < min){
min = a[i];
}
}
for (int i = 0; i < LEN; i++){
a[i] -= min;
}
// 输出选出的 a 数组,剩下元素属于 b 数组
outputGroup(arry, select(a));
return 0;
}
代码运行结果为: