3 和的逆运算问题
n 个整数两两相加可以得到 n(n - 1) / 2 个和。我们的目标是:根据这些和找出原来的 n 个整数。
输入格式
输入每行一个整数 n(2 < 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 个整数,可以按照以下步骤进行:
-
排序两两和:
- 首先,对所有的两两和进行升序排序。这有助于我们推断出原始整数的顺序。
-
确定最小的三个数:
- 假设排序后的第一个和是
x1 + x2,第二个和是x1 + x3,第三个和是x2 + x3。 - 通过这三个和,可以解出
x1、x2和x3的值。
- 假设排序后的第一个和是
-
递归恢复剩余的数:
- 一旦确定了一部分原始数,就可以利用这些数的和来推断下一个数。
- 重复这一过程,直到恢复出所有的数或确定无解。
-
回溯法:
- 如果在某一步骤中发现无法继续恢复(即某些必要的和不存在),则需要回溯,尝试其他可能的组合。
-
验证结果:
- 最终恢复出的数应当能够重新生成给定的所有两两和。如果无法满足,则输出 "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
恢复过程
-
初始阶段:
- 假设最小的和
223是x1 + x2 - 第二小的和
224是x1 + x3 - 第三个和
225是x2 + x3
解方程的前三个数为
[111, 112, 113]。 - 假设最小的和
-
移除已使用的和:
- 移除
223(111 + 112),224(111 + 113),225(112 + 113) - 剩余的和为:
225, 226, 226, 227, 227, 228, 229
- 移除
-
确定第四个数
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
- 当前已确定的数为
-
确定第五个数
x5:- 已确定的数为
[111, 112, 113, 116] - 当前最大的和
227可能是x5 + 116(即x5 + 116 = 227) - 计算
x5 = 227 - 116 = 111
验证
111是否满足与已确定数的和:111 + 111 = 222(不存在,之前已移除)- 这意味着假设
x5 = 111不成立,需要回溯,尝试其他可能的x5
- 已确定的数为
-
回溯尝试其他可能性:
- 由于上述假设导致矛盾,回溯到确定第四个数的步骤,尝试其他可能的
x4 - 重新尝试可能的
x4
- 由于上述假设导致矛盾,回溯到确定第四个数的步骤,尝试其他可能的
-
正确的恢复过程:
- 最终,通过正确的选择和验证,恢复出
[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"));
}
}