两数组和的差最小问题

题目来源于传说中的华为面试题:

有两个长度都为 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(2S2nSn)

描述为背包问题就是:

从 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[i1][j][v],f[i1][j1][va[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[j1][va[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;
}

代码运行结果为:
运行结果

根据引用\[1\]引用\[2\]的内容,我们可以使用状态压缩法来枚举所有可能的分情况。对于给定的数组,我们可以将每个元素放入数组1或数组2中,用二进制的10来表示。然后,我们可以计算每种分情况下两个数组的绝对值,并找到最小值。 举个例子,对于输入数组nums = \[2,-1,0,4,-2,-9\],我们可以将其分成 \[2,4,-9\] \[-1,0,-2\] 两个数组,它们的的绝对值为 abs((2 + 4 + -9) - (-1 + 0 + -2)) = 0。这就是最优的分方案。 因此,我们可以通过枚举所有可能的分情况,并计算每种情况下的值,找到最小值来实现将数组分成两个数组,并最小数组。 #### 引用[.reference_title] - *1* [2035. 将数组分成两个数组最小数组 折半搜索](https://blog.youkuaiyun.com/yu_duan_hun/article/details/125899854)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [[Leetcode]5897. 将数组分成两个数组最小数组](https://blog.youkuaiyun.com/gshgsh1228/article/details/120692058)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值