LeetCode914. 卡牌分组
给定一副牌,每张牌上都写着一个整数。
此时,你需要选定一个数字 X,使我们可以将整副牌按下述规则分成 1 组或更多组:
每组都有 X 张牌。
组内所有的牌上都写着相同的整数。
仅当你可选的 X >= 2 时返回 true。
示例 1:
输入:[1,2,3,4,4,3,2,1]
输出:true
解释:可行的分组是 [1,1],[2,2],[3,3],[4,4]
示例 2:
输入:[1,1,1,2,2,2,3,3]
输出:false
解释:没有满足要求的分组。
示例 3:
输入:[1]
输出:false
解释:没有满足要求的分组。
示例 4:
输入:[1,1]
输出:true
解释:可行的分组是 [1,1]
示例 5:
输入:[1,1,2,2,2,2]
输出:true
解释:可行的分组是 [1,1],[2,2],[2,2]
提示:
1 <= deck.length <= 10000
0 <= deck[i] < 10000
思路:
1.读题知道「相同的数被分在一组」,每一组除了数值相同以外,我们还关心一个属性:这个组里元素的个数,很容易想到应该统计元素个数;
2. 看示例分析如下几点要求。
- 示例 2 :表示若不能使得每个组的中元素个数相同,直接返回 false。
- 示例 3 :告诉我们:如果检测到某个组里元素只有 1 个,直接返回 false。
- 示例 5 :[2, 2, 2, 2] 硬是被拆成了 2 组,为的是与组 [1, 1] 的元素个数相等。这对应了题目「每组都有 X 张牌」 的要求。
步骤:
1、遍历一次,统计每个数值的个数,如果某个数值只有 1 个,直接返回 false;
2、再看一下示例 5,想更一般化的情况,输入 [2, 2, 2, 2, 3, 3, 3, 3, 3, 3],其实也是符合题意的分组,2 有 44 个,3 有 66 个,相同的 2 和 3 都需要拆成 2 个一组,因此这里的 X = 2,很显然 22 是这两个组的元素个数的公约数。
考察核心:
为此,需要对所有的数值的个数,求公约数。只要能找到任意的公约数(要严格大于 1)都可以。为此,可以从 2 开始,依次用素数去判定它是不是所有数的约数。但其实我们是有现成的算法求最大公约数的,这个最大公约数只要严格大于 11 就行。
public boolean hasGroupsSizeX(int[] deck) {
if (deck.length < 2) {
return false;
}
//存储
HashMap<Integer, Integer> poke = new HashMap<Integer, Integer>();
for (int i : deck) {
if (poke.get(i) != null) {
poke.put(i, poke.get(i)+1);
}else {
poke.put(i, 1);
}
}
//若有个数小于2的值则false,并记录最小的牌组的个数;
int min = Integer.MAX_VALUE;
for (Integer val: poke.values()) {
min = Math.min(min, val);
if (val < 2) {
return false;
}
}
//寻找最小牌组大于2的公约数
Set<Integer> conNum = new HashSet<Integer>();
for (int i = 2; i <= min; i++) {
if (min % i == 0) {
conNum.add(i);
}
}
//若所有的牌号都能以最小牌堆的公约数分配,则true;
int eleCount = poke.size();
for (Integer con_i : conNum) {
int count = 0;
for (Integer val: poke.values()) {
if (val % con_i != 0) {
break;
}
count++;
}
if (count == eleCount) {
return true;
}
}
return false;
}
总结:
时间复杂度:O(N) = 存储n + 遍历n + 寻找最小堆的公约数min + 判断公约数 m*count(min) >> 2n;
空间复杂度:O(N) = hashmap + set ;
结果一般;
执行用时 :16 ms, 在所有 Java 提交中击败了30.47%的用户
内存消耗 :42.4 MB, 在所有 Java 提交中击败了5.64%的用户
看更优秀的题解;
public boolean hasGroupsSizeX1(int[] deck) {
if (deck.length < 2) {
return false;
}
//使用数组计数,依照提示牌号的范围为0~10000,
int []cnt = new int [10000];
for (int num : deck) {
cnt[num]++;
}
// 先得到第 1 个数的个数,以避免在循环中赋值
int x = cnt[deck[0]];
// 遍历存储的空间
for (int i = 0; i < 10000; i++) {
//牌堆为1,false
if (cnt[i] == 1) {
return false;
}
if (cnt[i] > 1) {
//求当前牌堆与当前的最大公约数的最大公约数
x = gcd(x, cnt[i]);
// x = 1,即两个牌堆公共序列为1,即不满足x>=2
if (x == 1) {
return false;
}
}
}
return true;
}
private int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
本题的核心考点:最大公约数
求两个整数的最大公约数的方法:「辗转相除法」/「欧几里德算法」。
我们做一个推导:不失一般性,假设 a > b且 b != 0 ,记整数 a和 b 的最大公约数是函数 gcd(a, b)。假设它们之间存在关系a=b×k+r
.
这里 k 是倍数,是一个整数(k = a / b),r 是余数(r = a % b),也是一个整数。
用记号 |表示整除关系,例如 2 | 6。
看等式右边,根据定义 gcd(b,r)既是 b 的因数,也是 r 的因数,所以一定是 b 和 r 的线性组合的因数,故 gcd(b,r) | a,根据定义 gcd(b,r) | b,即 gcd(b,r) 是两个数 a、b的公因数,故 gcd(b,r) | gcd(a,b)。
事实上,等式中 a 和 r 是地位相当的,做个等价变形 r=a−b×k,根据上面同样的思路,看右边:gcd(a,b)|r,并且 gcd(a,b)∣b,故 gcd(a,b)∣gcd(b,r)。
两个整数互相可以整除,那么它们要么相等,要么互为相反数。事实上,由于正负数不是研究公约数问题的核心,很多资料都会强制规定公约数是正数。因此 gcd(a,b)=gcd(b,r)。
由于编程语言中提供了求余数运算,因此,这样的等式可以一直递归做下去:
gcd(a, b) = gcd(b, a % b) = ... = gcd(非零整数, 0)) = 非零整数
由于除数和余数是交替出现在等式中的,这个方法就被称之为「辗转相除法」。求解的过程显然是一个递归结构,并且递归终止条件是除数 b == 0。
引用
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/x-of-a-kind-in-a-deck-of-cards/solution/qiu-jie-zui-da-gong-yue-shu-java-by-liweiwei1419/
作者:一切随心
引用:https://www.cnblogs.com/drizzlecrj/archive/2007/09/14/892340.html