题解 | #数组分组#
数组分组
https://www.nowcoder.com/practice/9af744a3517440508dbeb297020aca86
题目链接
题目描述
对于给定的 个整数,需要将其分为
和
两个数组,并满足以下条件:
-
所有 5 的倍数元素均在
数组中。
-
所有 3 的倍数(且非 5 的倍数)元素均在
数组中。
-
其他元素可以任意分配到
或
中。
求解是否存在一种分配方案,使得 数组中各个元素之和等于
数组中各个元素之和。
解题思路
这个问题的核心在于如何分配那些既不是 3 的倍数也不是 5 的倍数的“自由元素”,以使得两个数组的和相等。
问题转化
我们可以将所有输入数字分为三类:
-
G5: 5 的倍数。这些数必须放入数组
。我们计算它们的和
。
-
G3: 3 的倍数(但非 5 的倍数)。这些数必须放入数组
。我们计算它们的和
。
-
C: 其他所有数字(自由元素)。这些数可以任意分配。
令最终数组 的和为
,数组
的和为
。我们的目标是判断是否存在一种分配方案使得
。
如果存在这样的方案,那么所有数字的总和 必须是偶数,因为
。因此,如果所有输入数字的总和
为奇数,则一定无解。
如果 为偶数,那么目标就是让
和
都等于
。
数组
的总和由两部分组成:固定的
和从自由元素集合
中选出的一部分元素的和(记为
)。
所以,我们的目标方程是:
移项可得:
至此,原问题被转化为一个经典的 子集和问题 (Subset Sum Problem):我们能否从自由元素集合 中,挑选出一个子集,使得子集中元素的和恰好等于目标值
?
求解子集和问题
子集和问题是 NP-完全问题,没有已知的多项式时间解法。对于本题的 范围,暴力搜索所有子集是不可行的。
一个常见的解决方法是 带记忆化的深度优先搜索 (DFS),这本质上是一种动态规划。
我们可以定义一个递归函数 can_find_sum(index, target)
,表示能否从 C
集合的第 index
个元素开始,凑出和为 target
。
在 can_find_sum(index, target)
中:
-
不选择
C[index]
:问题转化为can_find_sum(index + 1, target)
。 -
选择
C[index]
:问题转化为can_find_sum(index + 1, target - C[index])
。
只要这两种情况有一种能成功,就说明可以凑出 target
。
为了避免重复计算,我们使用一个哈希表或二维数组作为备忘录,存储 (index, target)
状态的计算结果。
算法流程
-
遍历所有输入数字,将它们分类,计算出
sum5
、sum3
,并将所有自由元素存入列表C
。 -
计算所有数字的总和
S
。如果S
是奇数,直接返回false
。 -
计算子集和的目标值
Target = S / 2 - sum5
。 -
调用带记忆化的 DFS 函数
can_find_sum(0, Target)
来判断能否从C
中凑出Target
。 -
返回 DFS 的结果。
代码
#include <iostream>
#include <vector>
#include <numeric>
#include <map>
using namespace std;
vector<long long> C;
map<pair<int, long long>, bool> memo;
bool can_find_sum(int index, long long target) {
// 基本情况:target 达到了 0,成功
if (target == 0) {
return true;
}
// 基本情况:数组遍历完或 target 变为负数(对于仅正数情况)
if (index == C.size()) {
return false;
}
// 检查备忘录
if (memo.count({index, target})) {
return memo[{index, target}];
}
// 方案一:不选择 C[index]
if (can_find_sum(index + 1, target)) {
return memo[{index, target}] = true;
}
// 方案二:选择 C[index]
if (can_find_sum(index + 1, target - C[index])) {
return memo[{index, target}] = true;
}
// 两种方案都不可行
return memo[{index, target}] = false;
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
long long sum5 = 0;
long long sum3 = 0;
long long total_sum = 0;
for (int i = 0; i < n; ++i) {
long long val;
cin >> val;
total_sum += val;
if (val % 5 == 0) {
sum5 += val;
} else if (val % 3 == 0) {
sum3 += val;
} else {
C.push_back(val);
}
}
if (total_sum % 2 != 0) {
cout << "false" << endl;
return 0;
}
long long target = total_sum / 2 - sum5;
if (can_find_sum(0, target)) {
cout << "true" << endl;
} else {
cout << "false" << endl;
}
return 0;
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
public class Main {
private static List<Long> C = new ArrayList<>();
private static Map<String, Boolean> memo = new HashMap<>();
private static boolean canFindSum(int index, long target) {
if (target == 0) {
return true;
}
if (index == C.size()) {
return false;
}
String key = index + ":" + target;
if (memo.containsKey(key)) {
return memo.get(key);
}
// 不选择 C[index]
if (canFindSum(index + 1, target)) {
memo.put(key, true);
return true;
}
// 选择 C[index]
if (canFindSum(index + 1, target - C.get(index))) {
memo.put(key, true);
return true;
}
memo.put(key, false);
return false;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
long sum5 = 0;
long sum3 = 0;
long totalSum = 0;
for (int i = 0; i < n; i++) {
long val = sc.nextLong();
totalSum += val;
if (val % 5 == 0) {
sum5 += val;
} else if (val % 3 == 0) {
sum3 += val;
} else {
C.add(val);
}
}
if (totalSum % 2 != 0) {
System.out.println("false");
return;
}
long target = totalSum / 2 - sum5;
if (canFindSum(0, target)) {
System.out.println("true");
} else {
System.out.println("false");
}
}
}
import sys
# 增加递归深度限制
sys.setrecursionlimit(2000)
n = int(input())
nums = list(map(int, input().split()))
sum5 = 0
sum3 = 0
C = []
total_sum = 0
for x in nums:
total_sum += x
if x % 5 == 0:
sum5 += x
elif x % 3 == 0:
sum3 += x
else:
C.append(x)
memo = {}
def can_find_sum(index, target):
if target == 0:
return True
if index == len(C):
return False
# 检查备忘录
if (index, target) in memo:
return memo[(index, target)]
# 方案一:不选择 C[index]
res1 = can_find_sum(index + 1, target)
# 方案二:选择 C[index]
res2 = can_find_sum(index + 1, target - C[index])
# 存储结果并返回
memo[(index, target)] = res1 or res2
return memo[(index, target)]
if total_sum % 2 != 0:
print("false")
else:
target = total_sum // 2 - sum5
if can_find_sum(0, target):
print("true")
else:
print("false")
算法及复杂度
-
算法:动态规划(通过带记忆化的深度优先搜索实现),子集和问题
-
时间复杂度:令自由元素的数量为
(
),目标和的值域范围为
。记忆化搜索的状态由
(index, target)
决定,index
有个取值,
target
的取值范围依赖于输入数字的大小。因此,时间复杂度为
。在最坏情况下,
可能非常大,但对于某些数据集,此方法可能有效。
-
空间复杂度:空间复杂度主要由递归深度和备忘录大小决定。递归深度最多为
。备忘录的大小与时间复杂度同阶,为
。