【华为OD】数字游戏
【华为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实现
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实现
import sysdef solve_brute_force():lines = []for line in sys.stdin:lines.append(line.strip())i = 0while i < len(lines):if not lines[i]:breakparts = lines[i].split()n, m = int(parts[0]), int(parts[1])if i + 1 >= len(lines):breaknums = list(map(int, lines[i + 1].split()))found = False# 枚举所有可能的连续子数组for start in range(n):current_sum = 0for end in range(start, n):current_sum += nums[end]if current_sum % m == 0:found = Truebreakif found:breakprint(1 if found else 0)i += 2solve_brute_force()
C++实现
#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实现
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实现
import sysdef solve_optimized():lines = []for line in sys.stdin:lines.append(line.strip())i = 0while i < len(lines):if not lines[i]:breakparts = lines[i].split()n, m = int(parts[0]), int(parts[1])if i + 1 >= len(lines):breaknums = list(map(int, lines[i + 1].split()))found = Falseremainders = {0} # 前缀和为0的余数prefix_sum = 0for num in nums:prefix_sum += numremainder = prefix_sum % m# 如果当前余数已经出现过,说明存在子数组和能被m整除if remainder in remainders:found = Truebreakremainders.add(remainder)print(1 if found else 0)i += 2solve_optimized()
C++实现
#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实现
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实现
def solve_simple():try:while True:line = input().split()n, m = int(line[0]), int(line[1])nums = list(map(int, input().split()))found = Falseremainders = {0}prefix_sum = 0for num in nums:prefix_sum += numremainder = prefix_sum % mif remainder in remainders:found = Truebreakremainders.add(remainder)print(1 if found else 0)except EOFError:passsolve_simple()
C++实现
#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,两种方法都能通过,但前缀和 + 同余定理优化法更加高效,特别适合处理大规模数据。
关键技巧是理解同余定理在子数组和问题中的应用:如果两个前缀和的余数相同,那么它们之间的子数组和必定能被目标数整除。这个性质大大简化了问题的求解过程。