【华为OD】数字游戏
题目描述
小明玩一个游戏。系统发 1+n 张牌,每张牌上有一个整数。第一张给小明,后 n 张按照发牌顺序排成连续的一行。
需要小明判断,后 n 张牌中,是否存在连续的若干张牌,其和可以整除小明手中牌上的数字。
输入描述
输入数据有多组,每组输入数据有两行,输入到文件结尾结束。
- 第一行有两个整数 n 和 m,空格隔开。m 代表发给小明牌上的数字。
- 第二行有 n 个数,代表后续发的 n 张牌上的数字,以空格隔开。
输出描述
对每组输入,如果存在满足条件的连续若干张牌,则输出1;否则,输出0。
备注
- 1 ≤ n ≤ 1000
- 1 ≤ 牌上的整数 ≤ 400000
- 输入的数组,不多于 1000
- 用例确保输入都正确,不需要考虑非法情况。
示例
示例一
输入:
6 7
2 12 6 3 5 5
输出:
1
示例二
输入:
10 11
1 1 1 1 1 1 1 1 1 1
输出:
0
说明:
两组输入。
- 第一组小明牌的数字为 7,再发了6张牌。第1、2两张牌数字和为 14,可以整除7,输出 1
- 第二组小明牌的数字为 11,再发了10张牌,这10张牌数字和为10,无法整除 11,输出0。
解题思路
这是一个子数组和整除问题,需要判断是否存在连续子数组的和能被给定数字整除。
核心思想:
- 枚举所有可能的连续子数组
- 计算每个子数组的和
- 判断是否能被目标数字整除
我将提供两种解法:暴力枚举法 和前缀和优化法。
解法一:暴力枚举法
枚举所有可能的连续子数组,计算每个子数组的和并判断是否能被目标数字整除。
Java实现
java
import java.util.*;
public class Solution1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNextLine()) {
String line = sc.nextLine().trim();
if (line.isEmpty()) break;
String[] parts = line.split(" ");
int n = Integer.parseInt(parts[0]);
int m = Integer.parseInt(parts[1]);
if (!sc.hasNextLine()) break;
String[] numStrs = sc.nextLine().split(" ");
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = Integer.parseInt(numStrs[i]);
}
boolean found = false;
// 枚举所有可能的连续子数组
for (int i = 0; i < n && !found; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
sum += nums[j];
if (sum % m == 0) {
found = true;
break;
}
}
}
System.out.println(found ? 1 : 0);
}
sc.close();
}
}
Python实现
python
import sys
def solve_brute_force():
lines = []
for line in sys.stdin:
lines.append(line.strip())
i = 0
while i < len(lines):
if not lines[i]:
break
parts = lines[i].split()
n, m = int(parts[0]), int(parts[1])
if i + 1 >= len(lines):
break
nums = list(map(int, lines[i + 1].split()))
found = False
# 枚举所有可能的连续子数组
for start in range(n):
current_sum = 0
for end in range(start, n):
current_sum += nums[end]
if current_sum % m == 0:
found = True
break
if found:
break
print(1 if found else 0)
i += 2
solve_brute_force()
C++实现
cpp
#include <iostream>
#include <vector>
#include <sstream>
using namespace std;
int main() {
string line;
while (getline(cin, line)) {
if (line.empty()) break;
istringstream iss(line);
int n, m;
iss >> n >> m;
if (!getline(cin, line)) break;
istringstream numStream(line);
vector<int> nums(n);
for (int i = 0; i < n; i++) {
numStream >> nums[i];
}
bool found = false;
// 枚举所有可能的连续子数组
for (int i = 0; i < n && !found; i++) {
int sum = 0;
for (int j = i; j < n; j++) {
sum += nums[j];
if (sum % m == 0) {
found = true;
break;
}
}
}
cout << (found ? 1 : 0) << endl;
}
return 0;
}
解法二:前缀和 + 同余定理优化法
使用前缀和和同余定理来优化算法。如果两个前缀和对m的余数相同,那么它们之间的子数组和必定能被m整除。
Java实现
java
import java.util.*;
public class Solution2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNextLine()) {
String line = sc.nextLine().trim();
if (line.isEmpty()) break;
String[] parts = line.split(" ");
int n = Integer.parseInt(parts[0]);
int m = Integer.parseInt(parts[1]);
if (!sc.hasNextLine()) break;
String[] numStrs = sc.nextLine().split(" ");
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = Integer.parseInt(numStrs[i]);
}
boolean found = false;
Set<Integer> remainders = new HashSet<>();
remainders.add(0); // 前缀和为0的余数
int prefixSum = 0;
for (int i = 0; i < n; i++) {
prefixSum += nums[i];
int remainder = prefixSum % m;
// 如果当前余数已经出现过,说明存在子数组和能被m整除
if (remainders.contains(remainder)) {
found = true;
break;
}
remainders.add(remainder);
}
System.out.println(found ? 1 : 0);
}
sc.close();
}
}
Python实现
python
import sys
def solve_optimized():
lines = []
for line in sys.stdin:
lines.append(line.strip())
i = 0
while i < len(lines):
if not lines[i]:
break
parts = lines[i].split()
n, m = int(parts[0]), int(parts[1])
if i + 1 >= len(lines):
break
nums = list(map(int, lines[i + 1].split()))
found = False
remainders = {0} # 前缀和为0的余数
prefix_sum = 0
for num in nums:
prefix_sum += num
remainder = prefix_sum % m
# 如果当前余数已经出现过,说明存在子数组和能被m整除
if remainder in remainders:
found = True
break
remainders.add(remainder)
print(1 if found else 0)
i += 2
solve_optimized()
C++实现
cpp
#include <iostream>
#include <vector>
#include <unordered_set>
#include <sstream>
using namespace std;
int main() {
string line;
while (getline(cin, line)) {
if (line.empty()) break;
istringstream iss(line);
int n, m;
iss >> n >> m;
if (!getline(cin, line)) break;
istringstream numStream(line);
vector<int> nums(n);
for (int i = 0; i < n; i++) {
numStream >> nums[i];
}
bool found = false;
unordered_set<int> remainders;
remainders.insert(0); // 前缀和为0的余数
int prefixSum = 0;
for (int i = 0; i < n; i++) {
prefixSum += nums[i];
int remainder = prefixSum % m;
// 如果当前余数已经出现过,说明存在子数组和能被m整除
if (remainders.find(remainder) != remainders.end()) {
found = true;
break;
}
remainders.insert(remainder);
}
cout << (found ? 1 : 0) << endl;
}
return 0;
}
解法三:简化版前缀和优化(推荐)
考虑到输入处理的复杂性,提供一个更简洁的版本:
Java实现
java
import java.util.*;
public class Solution3 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
int n = sc.nextInt();
int m = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
boolean found = false;
Set<Integer> remainders = new HashSet<>();
remainders.add(0);
int prefixSum = 0;
for (int num : nums) {
prefixSum += num;
int remainder = prefixSum % m;
if (remainders.contains(remainder)) {
found = true;
break;
}
remainders.add(remainder);
}
System.out.println(found ? 1 : 0);
}
sc.close();
}
}
Python实现
python
def solve_simple():
try:
while True:
line = input().split()
n, m = int(line[0]), int(line[1])
nums = list(map(int, input().split()))
found = False
remainders = {0}
prefix_sum = 0
for num in nums:
prefix_sum += num
remainder = prefix_sum % m
if remainder in remainders:
found = True
break
remainders.add(remainder)
print(1 if found else 0)
except EOFError:
pass
solve_simple()
C++实现
cpp
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;
int main() {
int n, m;
while (cin >> n >> m) {
vector<int> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
bool found = false;
unordered_set<int> remainders;
remainders.insert(0);
int prefixSum = 0;
for (int num : nums) {
prefixSum += num;
int remainder = prefixSum % m;
if (remainders.find(remainder) != remainders.end()) {
found = true;
break;
}
remainders.insert(remainder);
}
cout << (found ? 1 : 0) << endl;
}
return 0;
}
算法复杂度分析
解法一:暴力枚举法
- 时间复杂度:O(N²),需要枚举所有可能的子数组
- 空间复杂度:O(1)
解法二:前缀和 + 同余定理优化法
- 时间复杂度:O(N),只需要一次遍历
- 空间复杂度:O(min(N, M)),存储余数的集合
算法原理详解
同余定理的应用
关键洞察:如果存在两个前缀和 prefixSum[i]
和 prefixSum[j]
(i < j),使得:
prefixSum[i] ≡ prefixSum[j] (mod m)
那么子数组 nums[i+1...j]
的和就能被 m 整除,因为:
sum(nums[i+1...j]) = prefixSum[j] - prefixSum[i] ≡ 0 (mod m)
特别地,如果某个前缀和本身就能被 m 整除(余数为0),那么从开头到该位置的子数组和就能被 m 整除。
示例分析
示例一分析
数组:[2, 12, 6, 3, 5, 5]
,m = 7
前缀和计算过程:
- 位置0:prefixSum = 2,余数 = 2 % 7 = 2
- 位置1:prefixSum = 2 + 12 = 14,余数 = 14 % 7 = 0
由于余数为0,说明前两个元素的和(14)能被7整除,输出1。
示例二分析
数组:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
,m = 11
所有前缀和的余数都是1到10之间的数,没有重复的余数,且没有余数为0的情况,所以不存在能被11整除的子数组和,输出0。
总结
两种解法各有特点:
- 暴力枚举法:思路直观,容易理解,但时间复杂度较高
- 前缀和 + 同余定理优化法:利用数学性质优化算法,时间复杂度降为O(N),是最优解法
对于这道题目,由于 n ≤ 1000,两种方法都能通过,但前缀和 + 同余定理优化法更加高效,特别适合处理大规模数据。
关键技巧是理解同余定理在子数组和问题中的应用:如果两个前缀和的余数相同,那么它们之间的子数组和必定能被目标数整除。这个性质大大简化了问题的求解过程。