题目来源于传说中的华为面试题:
有两个长度都为n的数组,分别为a,b。数组元素类型为整型,值任意且无序。要求通过交换a,b数组的元素,使得数组a的和与数组b的和的差最小。
这个题目网上很多博客给出的一种解法都是错的,他们的思路是每次交换一对数据,如果交换能使得差变小就交换,否则就不交换。这个解法简单直观,其实本来目的就是穷举。可是本题却存在这种情况,即找不到任何一对数据的交换使得差变小(这也是上面算法的停止条件),但存在同时交换多对数据使得差变小的情况。
网上的这个主流解法错就错在没有考虑同时交换多对数据的情况,下面我将介绍本题的一种使用动态规划的解法。
使用动态规划求解
把原问题看作:有2n个非负整数(题目给的条件是值任意,后面我会介绍转换方法),和为 ,我们要在2n个数中选出n个数,使得这n个数的和 满足:
描述为背包问题就是:
从2n件物品中选出n件物品,放入容量为 的背包中,使得背包装的东西尽可能的多。(把第i个元素的值,既看作第i件物品的开销也看作第i件物品的价值)
这其实是一个在普通的0-1背包上多加了一个限制条件的背包问题,多加了放入背包物品的数量的限制。
我们可以在原来的基础上增加一维以满足新的限制。
状态转移方程:
设f[i][j][v]表示前i件中选出j(j<=i)件放入容量为v的背包中所能获得的最大价值,a[i]为第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;
}
代码运行结果为:
其实有了状态转移方程后,编程就简单了。上面代码是两三年前的代码了,现在的代码风格已经变了,哈哈
引申问题
现在有一个天平和n个质量已知的物品,假设要求一定要向天平托盘中放入物品,请问如何放置物品可以使得天平倾斜角最小?