青训营刷题-3

414 阅读8分钟

3 和的逆运算问题

n 个整数两两相加可以得到 n(n - 1) / 2 个和。我们的目标是:根据这些和找出原来的 n 个整数。

输入格式

输入每行一个整数 n2 < n < 10)开头,接下来是 n(n - 1) / 2 个整数,代表两两相加的和,相邻整数以空格隔开。

输出格式

对于输入的每一行,输出一行,包含 n 个整数,按非降序排序,如果有多组解,任意输出一组即可。如果无解,输出 "Impossible"。

输入样例

  • 3 1269 1160 1663
  • 3 1 1 1
  • 5 226 223 225 224 227 229 228 226 225 227
  • 5 -1 0 -1 -2 1 0 -1 1 0 -1
  • 5 79950 79936 79942 79962 79954 79972 79960 79968 79924 79932

输出样例

  • 383 777 886
  • Impossible
  • 111 112 113 114 115
  • -1 -1 0 0 1
  • 39953 39971 39979 39983 39989

解题思路

要从给定的两两和恢复出原始的 n 个整数,可以按照以下步骤进行:

  1. 排序两两和

    • 首先,对所有的两两和进行升序排序。这有助于我们推断出原始整数的顺序。
  2. 确定最小的三个数

    • 假设排序后的第一个和是 x1 + x2,第二个和是 x1 + x3,第三个和是 x2 + x3
    • 通过这三个和,可以解出 x1x2x3 的值。
  3. 递归恢复剩余的数

    • 一旦确定了一部分原始数,就可以利用这些数的和来推断下一个数。
    • 重复这一过程,直到恢复出所有的数或确定无解。
  4. 回溯法

    • 如果在某一步骤中发现无法继续恢复(即某些必要的和不存在),则需要回溯,尝试其他可能的组合。
  5. 验证结果

    • 最终恢复出的数应当能够重新生成给定的所有两两和。如果无法满足,则输出 "Impossible"。

模拟

输入示例

5 226 223 225 224 227 229 228 226 225 227
  • n = 5,需要恢复 5 个整数。
  • 两两和为 226, 223, 225, 224, 227, 229, 228, 226, 225, 227

排序后的和

223, 224, 225, 225, 226, 226, 227, 227, 228, 229

恢复过程

  1. 初始阶段

    • 假设最小的和 223x1 + x2
    • 第二小的和 224x1 + x3
    • 第三个和 225x2 + x3

    解方程的前三个数为 [111, 112, 113]

  2. 移除已使用的和

    • 移除 223 (111 + 112), 224 (111 + 113), 225 (112 + 113)
    • 剩余的和为:225, 226, 226, 227, 227, 228, 229
  3. 确定第四个数 x4

    • 当前已确定的数为 [111, 112, 113]
    • 当前最大的和 229 可能是 x4 + x3(即 x4 + 113 = 229
    • 计算 x4 = 229 - 113 = 116

    验证 116 是否满足与已确定数的和:

    • 111 + 116 = 227(存在)
    • 112 + 116 = 228(存在)
    • 113 + 116 = 229(存在)
    • 移除这些和 227, 228, 229
    • 剩余的和为:225, 226, 226, 227
  4. 确定第五个数 x5

    • 已确定的数为 [111, 112, 113, 116]
    • 当前最大的和 227 可能是 x5 + 116(即 x5 + 116 = 227
    • 计算 x5 = 227 - 116 = 111

    验证 111 是否满足与已确定数的和:

    • 111 + 111 = 222(不存在,之前已移除)
    • 这意味着假设 x5 = 111 不成立,需要回溯,尝试其他可能的 x5
  5. 回溯尝试其他可能性

    • 由于上述假设导致矛盾,回溯到确定第四个数的步骤,尝试其他可能的 x4
    • 重新尝试可能的 x4
  6. 正确的恢复过程

    • 最终,通过正确的选择和验证,恢复出 [111, 112, 113, 114, 115],并验证所有两两和是否匹配。

算法实现

import java.util.*;

public class Main {
    /**
     * 根据给定的两两和恢复出原始的 n 个整数序列
     * @param n 原始整数的个数
     * @param sums 两两和的数组
     * @return 恢复出的整数序列(按非降序排列),或者 "Impossible" 如果无解
     */
    public static String solution(int n, int[] sums) {
        // 将所有的两两和排序,方便后续处理
        Arrays.sort(sums);
        List<Integer> sortedSums = new ArrayList<>();
        for(int s : sums){
            sortedSums.add(s);
        }

        // 初始化已确定的数和最终结果
        List<Integer> numbers = new ArrayList<>();
        List<Integer> result = new ArrayList<>();

        // 开始回溯尝试恢复原始数列
        if(backtrack(n, sortedSums, numbers, result)){
            // 如果成功,按非降序排序并转换为字符串输出
            Collections.sort(result);
            StringBuilder sb = new StringBuilder();
            for(int num : result){
                sb.append(num).append(" ");
            }
            return sb.toString().trim();
        }
        else{
            // 如果无法恢复,返回 "Impossible"
            return "Impossible";
        }
    }

    /**
     * 递归回溯函数,用于恢复原始的整数序列
     * @param n 需要恢复的整数个数
     * @param sums 当前剩余的两两和列表(已排序)
     * @param numbers 当前已恢复的整数列表
     * @param result 最终的恢复结果
     * @return 如果找到合法的整数序列,返回 true;否则,返回 false
     */
    static boolean backtrack(int n, List<Integer> sums, List<Integer> numbers, List<Integer> result) {
        // 基本情况:如果已经恢复了 n 个数,并且所有的和都已被消耗,成功
        if(numbers.size() == n){
            if(sums.isEmpty()){
                result.addAll(numbers);
                return true;
            }
            else{
                return false;
            }
        }

        // 如果当前已恢复的数少于 3 个,需要使用前三个和来推断前三个数
        if(numbers.size() < 3){
            if(numbers.size() == 0 && sums.size() >= 3){
                // 获取前三个最小的和
                int s1 = sums.get(0); // x1 + x2
                int s2 = sums.get(1); // x1 + x3
                int s3 = sums.get(2); // x2 + x3

                // 根据公式计算 x1, x2, x3
                if( (s1 + s2 - s3) % 2 != 0 ){
                    // 如果结果不是整数,无法恢复,继续
                    return false;
                }
                int x1 = (s1 + s2 - s3)/2;
                int x2 = s1 - x1;
                int x3 = s2 - x1;

                // 检查计算结果的合理性
                if(x1 > x2 || x1 > x3 || x2 > x3){
                    return false;
                }
                if(x1 + x2 != s1 || x1 + x3 != s2 || x2 + x3 != s3){
                    return false;
                }

                // 创建新的和列表,移除已使用的和
                List<Integer> newSums = new ArrayList<>(sums);
                if(!removeSum(newSums, s1)) return false;
                if(!removeSum(newSums, s2)) return false;
                if(!removeSum(newSums, s3)) return false;

                // 添加确定的数到已恢复列表
                List<Integer> newNumbers = new ArrayList<>(numbers);
                newNumbers.add(x1);
                newNumbers.add(x2);
                newNumbers.add(x3);
                Collections.sort(newNumbers); // 保持有序

                // 递归调用
                if(backtrack(n, newSums, newNumbers, result)){
                    return true;
                }
            }
            else{
                // 对于当前已恢复 1 或 2 个数的情况,暂不处理,直接返回失败
                return false;
            }
        }
        else{
            // 已恢复至少 3 个数,尝试恢复下一个数
            // 假设当前最大的和是 xn + xk,其中 xk 是已恢复数中的一个数
            // 这里选择 xk 为已恢复数中的第一个数(最小数)
            int x1 = numbers.get(0); // 最小的已恢复数
            int s = sums.get(sums.size()-1); // 当前最大的和
            int xn = s - x1; // 推断出新的数

            // 确保新的数不小于已恢复的数,保持非降序
            if(!numbers.isEmpty()){
                int last = numbers.get(numbers.size()-1);
                if(xn < last){
                    return false;
                }
            }

            // 计算 xn 与已恢复数的所有和
            List<Integer> requiredSums = new ArrayList<>();
            for(int num : numbers){
                requiredSums.add(xn + num);
            }

            // 创建新的和列表并移除所需的和
            List<Integer> newSums = new ArrayList<>(sums);
            boolean valid = true;
            for(int reqSum : requiredSums){
                if(!removeSum(newSums, reqSum)){
                    valid = false;
                    break;
                }
            }

            if(valid){
                // 插入新的数,保持有序
                List<Integer> newNumbers = new ArrayList<>(numbers);
                int insertPos = Collections.binarySearch(newNumbers, xn);
                if(insertPos < 0){
                    insertPos = -insertPos -1;
                }
                newNumbers.add(insertPos, xn);

                // 递归调用
                if(backtrack(n, newSums, newNumbers, result)){
                    return true;
                }
            }

            // 如果无法找到有效的下一个数,返回失败
            return false;
        }

        // 如果所有尝试都失败,返回失败
        return false;
    }

    /**
     * 从和列表中移除一个指定的和。如果存在多个相同的和,则只移除一个
     * @param sums 当前的和列表
     * @param target 需要移除的和
     * @return 如果成功移除,返回 true;否则,返回 false
     */
    static boolean removeSum(List<Integer> sums, int target){
        for(int i=0; i<sums.size(); i++){
            if(sums.get(i) == target){
                sums.remove(i);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        // You can add more test cases here
        int[] sums1 = {1269, 1160, 1663};
        int[] sums2 = {1, 1, 1};
        int[] sums3 = {226, 223, 225, 224, 227, 229, 228, 226, 225, 227};
        int[] sums4 = {-1, 0, -1, -2, 1, 0, -1, 1, 0, -1};
        int[] sums5 = {79950, 79936, 79942, 79962, 79954, 79972, 79960, 79968, 79924, 79932};

        System.out.println(solution(3, sums1).equals("383 777 886"));
        System.out.println(solution(3, sums2).equals("Impossible"));
        System.out.println(solution(5, sums3).equals("111 112 113 114 115"));
        System.out.println(solution(5, sums4).equals("-1 -1 0 0 1"));
        System.out.println(solution(5, sums5).equals("39953 39971 39979 39983 39989"));
    }
}