[算法]调整数组使差最小

调整数组使差最小

其他名称

交换两个数组使两个数组和的差最小

题目描述

Description

有两个序列 a,b,大小都为 n,序列元素的值任意整数,无序; 要求:通过交换 a,b 中的元素,使[序列 a 元素的和]与[序列 b 元素的和]之间的差最小。

Input

输入第一行为用例个数, 每个测试用例输入为两行,分别为两个数组,每个值用空格隔开。

Output

输出变化之后的两个数组内元素和的差绝对值。

Sample Input 1

1
100 99 98 1 2 3
1 2 3 4 5 40

Sample Output 1

48
备注:

如果用"调整数组使差最小",或者"交换两个数组使两个数组和的差最小"搜到的大部分博客答案如果采用非动态规划的算法,基本都是错误的,并且那些错误的博客还互相转载引用.

错误案例

错误1:
错误思路

当前数组a和数组b的和之差为
A = sum(a) - sum(b)

a的第i个元素和b的第j个元素交换后,a和b的和之差为
​ A’ = sum(a) - a[i] + b[j] - (sum(b) - b[j] + a[i])
​ = sum(a) - sum(b) - 2 (a[i] - b[j])
​ = A - 2 (a[i] - b[j])

假设A > 0,

当x 在 (0,A)之间时,做这样的交换才能使得交换后的a和b的和之差变小,x越接近A/2效果越好,

如果找不到在(0,A)之间的x,则当前的a和b就是答案。

所以算法大概如下:

在a和b中寻找使得x在(0,A)之间并且最接近A/2的i和j,交换相应的i和j元素,重新计算A后,重复前面的步骤直至找不到(0,A)之间的x为止。

但是,上述方法却有缺陷,因为一次只允许交换一对元素,这对于一次需要交换两个元素的数组而言将出错,考虑如下情况:

A = { 5,5,9,10 };
B = { 4,7,7,13 };
A的和为29,B为31。当把A中的5,5与B中的4,7交换后,A与B的和都为30,差为0.但上述算法一将检测不到这种交换!因此输出结果是原数组。

总结:凡是每次只交换一对数的算法全是错误的

错误代码:python
def error1():
    n = int(input().strip())
    for i in range(n):
        arr1 = [int(x) for x in input().strip().split(" ")]
        arr2 = [int(x) for x in input().strip().split(" ")]
        flag = True
        while flag:
            best_i, best_j, best_change = 0, 0, 0
            diff = sum(arr1) - sum(arr2)
            # print("data", arr1, arr2, diff)
            for i in range(len(arr1)):
                for j in range(len(arr2)):
                    # 找到差最接近原有差的两个数
                    change = 2 * (arr1[i] - arr2[j])
                    if abs(diff - change) < abs(diff - best_change):
                        # 若交换后的数组差小于交换前的数组差,记录 i , j  sum
                        best_i, best_j, best_change = i, j, change
            if best_change == 0:
                # 没有发生交换
                flag = False
            else:
                # 发生了交换
                arr1[best_i], arr2[best_j] = arr2[best_j], arr1[best_i]
                # print("change", arr1[best_i], arr2[best_j], best_change)
        print(abs(sum(arr1) - sum(arr2)))
错误2:
错误思路

将两个数组合并为一个数组,并排序,然后再平分,每次取一头一尾两个,轮流放到A、B两个数组中。

这个不用解释,明显就是错的

将数组排序,同时使用递归选的方式每次选一个数,放在数组中

这个也是错的

总结:凡是每次只选择一个数的算法全是错误的

错误代码:python
def error2(sorted_list):
    if not sorted_list:
        return (([], []))
    big = sorted_list[-1]
    small = sorted_list[-2]
    big_list, small_list = error2(sorted_list[:-2])
    big_list.append(small)
    small_list.append(big)
    big_list_sum = sum(big_list)
    small_list_sum = sum(small_list)
    if big_list_sum > small_list_sum:
        return ((big_list, small_list))
    else:
        return ((small_list, big_list))

正确解答

这道题是可以用dp去解决的

博客1:思路

博客2:c代码实现

以上的博客讲解了思路,但代码还是有问题.先说下思路,之后说下问题

思路解析

2n个数中找n个元素,有三种可能:大于Sum/2,小于Sum/2以及等于Sum/2。而大于Sum/2与小于等于Sum/2没区别,故可以只考虑小于等于Sum/2的情况,

这本质上是一道0/1背包问题,容量可以看做是sum/2,价值和重量可以看做数字大小,但本题还有一个约束条件,即选取的个数一定为n个,因此在优化之前,这是一个三维数组的动态规划.

设F[i][j][k]表示前i个元素中选取j个元素,使得其和不超过k且最接近k。那么可以根据第i个元素是否选择来进行决策

状态方程如下:

其中,F[i-1][j][k]表示前i-1个元素中选取j个使其和不超过但最逼近k;

F[i-1][j-1][k-A[i]]在前i-1个元素中选取j-1个元素使其和不超过但最逼近k-A[i],这样再加上A[i]即第i个元素就变成了选择上第i个元素的情况下最逼近k的和。而第一种情况与第二种情况是完备且互斥的,所以需要将两者最大的值作为F[i][j][k]的值。

这样根据状态转移方程我们就可以构建出一个基本的解(见solve1),但这个解还有很大的优化空间(solve2 or solve3),并且我们在编写代码时,要考虑如下几个问题

问题1: 数据压缩问题

但是三维dp空间很大,并且本题不需要打印出最佳路径,因此可以用空间压缩,0/1背包问题实现了二维压缩到1维,道理类似.
总而言之,要考虑如下几点

  1. 压缩后一定要倒着更新数组,正着更新会覆盖掉上一次的状态
  2. dp数组中的值不要越界,
问题2: 数组值溢出问题与负数问题

解决办法如下:

  1. 将数组1,2同时放缩,减掉数组1,2中取最小的值,这样就避免了负数的存在,并且数组放缩并不会影响到他们的和.这样也在一定程度上缩减了数值.
代码实现

博客2:c代码实现中的代码存在一些问题,

  1. sum/2+2完全是没有必要的,sum/2+1的思想类似,就是多了一行0,用于初始化
  2. 将三维空间压缩成二维时,没有逆序遍历,导致结果不对
  3. 没有处理负数
  4. 没有考虑数值溢出问题

注: 数组a,b即是原始数组,返回值是两个数组的差值b-a=sum-2*a,

代码1:java
static int solve1(int[] a, int[] b) {
    //合并数组,防止数据溢出,并去除负数影响
    int[] arr = new int[a.length * 2];
    int count = 0;
    int min = a[0];
    int sum = 0;
    for (int i = 0; i < a.length; i++) {
        arr[count++] = a[i];
        if (a[i] < min)
            min = a[i];
    }
    for (int i = 0; i < b.length; i++) {
        arr[count++] = b[i];
        if (b[i] < min)
            min = b[i];
    }

    for (int i = 0; i < a.length * 2; i++) {
        arr[i] -= min;
        sum +=arr[i]
    }

    //原始dp
    int SUM = sum ;
    int N = a.length;
    int i, j, s;
    int[][][] dp = new int[2 * N + 1][N + 1][SUM / 2 + 1];

    for (i = 1; i <= 2 * N; ++i) {
        for (j = 1; j <= Math.min(i, N); ++j) {
            for (s = 1; s <= SUM / 2; s++) { //这里一定要从1开始遍历
                  if (s - arr[i - 1] < 0) {
                       dp[i][j][s] = dp[i - 1][j][s];
                   } else {
                       dp[i][j][s] = Math.max(dp[i - 1][j - 1][s - arr[i - 1]] + arr[i - 1], dp[i - 1][j][s]);
                   }
            }
        }
    }
    return SUM - 2 * dp[2 * N][N][SUM / 2];
}
代码2:java
static int solve2(int[] a, int[] b) {
    //合并数组,防止数据溢出,并去除负数影响
    int[] arr = new int[a.length * 2];
    int count = 0;
    int min = a[0];
    int sum = 0;
    for (int i = 0; i < a.length; i++) {
        arr[count++] = a[i];
        if (a[i] < min)
            min = a[i];
    }
    for (int i = 0; i < b.length; i++) {
        arr[count++] = b[i];
        if (b[i] < min)
            min = b[i];
    }
    for (int i = 0; i < a.length * 2; i++) {
        arr[i] -= min;
        sum+=arr[i]
    }
	//dp
    int SUM = sum;  
    int N = a.length;  
    int i, j, s;
    int dp[][] = new int[N + 1][SUM / 2 + 1];     //取N+1件物品,总合不超过SUM/2+2,的最大值是多少
    for (i = 1; i <= 2 * N; ++i) {
        for (j = Math.min(i, N); j >= 1; j--) {//这里一定要逆序
            for (s = SUM / 2; s >= arr[i - 1]; --s)   
            {
                dp[j][s] = Math.max(dp[j - 1][s - arr[i - 1]] + arr[i - 1], dp[j][s]);
            }
        }
    }
    return SUM - 2 * dp[N][SUM / 2];
}
代码3:java
//isOK的方式因为用0,1标识,所以不会溢出,
static int solve3(int[] a, int[] b) {
    //合并数组,防止数据溢出,并去除负数影响
    int[] arr = new int[a.length * 2];
    int count = 0;
    int min = a[0];
    int sum = 0;
    for (int i = 0; i < a.length; i++) {
        arr[count++] = a[i];
        if (a[i] < min)
            min = a[i];
    }
    for (int i = 0; i < b.length; i++) {
        arr[count++] = b[i];
        if (b[i] < min)
            min = b[i];
    }
    for (int i = 0; i < a.length * 2; i++) {
        arr[i] -= min;
        sum+=arr[i]
    }
    
	//dp
    int SUM = sum;
    int N = a.length;    //len        ĸ
    int i, j, s;
    int isOK[][] = new int[N + 1][SUM / 2 + 1]; //isOK[i][v]表示是否可以找到i个数,使得它们之和等于v
    //注意初始化
    isOK[0][0] = 1; //可以,取0件物品,总合为0,是合法的
    for (i = 1; i <= 2 * N; ++i) {
        for (j = Math.min(i, N); j >= 1; j--) {//这里一定要逆序
            for (s = SUM / 2; s >= arr[i-1]; --s) 
            {
                if (isOK[j - 1][s - arr[i-1]] == 1)
                    isOK[j][s] = 1;
            }
        }
    }
    for (s = SUM / 2 ; s >= 0; --s) {
        if (isOK[N][s] == 1)
            return SUM - 2 * s;
    }
    //要求最优解则空间不能优化
    return 0;
}

令附读取数据代码

 public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int ucaseNum = Integer.parseInt(sc.nextLine().split(" ")[0]);
        for (int i = 0; i < ucaseNum; i++) {
            int[] A = Arrays.stream(sc.nextLine().split(" ")).mapToInt(Integer::valueOf).toArray();
            int[] B = Arrays.stream(sc.nextLine().split(" ")).mapToInt(Integer::valueOf).toArray();
            int res = solve1(A, B);//这里换成3种方法之一
            System.out.println(res);
        }
 }
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值