问题描述
有一些砖块的集合 tiles
,每个砖块印有一个字母 tiles[i]
。返回可以组合的非空字符串的个数。
栗 1:
输入:"AAB"
输出:8
可能的序列为:A, B, AA, AB, BA, AAB, ABA, BAA
栗 2:
输入:"AAABBC"
输出:188
注意:
1 <= tiles.length <= 7
- 字符包含大写字母
解题思路
我的解法
根据栗 1
可以看出,可能的组合包括由一个字符组成,两个字符组成,...,n
个字符组成的所有字符串。其中 n
为给定的 tiles
的长度。
比如给定 tiles = "AAB"
,那么我们可以手动计算出如下结果:
长度为 1
的字符串:A
、B
。
长度为 2
的字符串:AB
、AA
、BA
。
长度为 3
的字符串:AAB
、ABA
、BAA
。
从上,我们可以发现一些规律。
-
长度为
1
的字符串,由tiles
中不相同的字符组成。AAB
中不同的字符为A
、B
。于是组成了两个长度为1
的字符串。 -
长度为
2
的字符串,在长度为1
的字符串基础上再组合一个字符而成。这一个字符在
A
、B
中任意挑选一个即可。
于是乎,A
与A
、B
分别组合,B
与A
、B
分别组合。去重后得到AA
、AB
、BA
。需要注意新的字符串需满足A
、B
的个数限制,这里都是满足的。 -
长度为
3
的字符串,在长度为2
的字符串的基础上再组合一个字符而成。同上,
AA
、AB
、BA
分别与A
、B
组合,去重后得到AAA
、ABA
、BAA
、AAB
、ABB
、BAB
。但是有些是不满足条件的,因为各个字符的数量是有限制的,比如
AAA
、ABB
、BAB
。因为A
总共只有2
个,B
只有1
个,所以需要筛除。
因此,对于数量个数问题,我们需要记录下当前字符串中每个字符的个数,以便在下一次组合时判断是否能组合为新的字符串。
比如 AB
,记录字符个数为 { A: 1, B: 1}
。
与 B
组合时,由于 B
总共的个数只有 1
个,所以组合不了。
与 A
组合时,由于 A
总共的个数 2
个,所以可以组合,更新记录为 { A: 2, B: 1}
。
因此,思路如上所述,根据上一个字符串集合再分别组合一个字符得到新的字符串集合,直至字符串长度达到 n
为止。
递归解法
这种方式比较简单,主要思想也是通过记录字符串中每个字符的个数来判断是否能组成新的字符串,但是不需要记录上一次的字符串集合
。
比如 AAB
,字符个数为 A: 2, B: 1
。
对于长度为 1
的字符串,可以任意取 A
、B
。
对于长度为 2
的字符串:
- 如果上一步中取
A
,那么剩余字符个数为A: 1, B: 1
。A
、B
都还剩一个,可以任意取其一,则有AA
、AB
。 - 如果上一步取
B
,那么剩余字符个数为A: 2, B: 0
,只能取A
,则有BA
。
对于长度为 3
的字符串:
- 如果上一步中取
AA
,剩余字符数为A: 0, B: 1
,只能取B
,则有AAB
。 - 如果取
AB
,剩余字符数为A: 1, B: 0
, 只能取A
,则有ABA
。 - 如果取
BA
,剩余字符数为A: 1, B: 0
,只能取A
,则有BAA
。
代码也很简洁,如下所示。有种一层层剥茧的意思,每个字符的个数在逐个减少。
// 记录每个字符的数量
public int numTilePossibilities(String tiles) {
int[] count = new int[26];
for (char c : tiles.toCharArray()) count[c - 'A']++;
return dfs(count);
}
int dfs(int[] arr) {
int sum = 0;
// 遍历每个可能出现的字符,将其个数 -1
for (int i = 0; i < 26; i++) {
// 当前可用字符数为 0
if (arr[i] == 0) continue;
// 个数 +1
sum++;
// 字符数 -1
arr[i]--;
// 继续计算 -1 之后,可能的组合数
sum += dfs(arr);
// 恢复字符数
arr[i]++;
}
return sum;
}
假设字符串为 AAB
,那么其调用堆栈如下:
sum = 0
dfs({A:2, B:1})
// 第一次循环
sum += 1
// 取 A,A-1
dfs({A:1, B:1})
// 第一次循环
sum += 1
// 取 A,A-1
dfs({A:0, B:1})
sum += 1
// 由于 A 已经为 0,跳过,只能 B-1
dfs({A:0, B:0})
// 第二次循环
sum += 1
// 取 B,B-1
dfs({A:1, B:0})
sum += 1
// 由于 B 已经为 0,跳过,只能 A-1
dfs({A:0, B:0})
// 第二次循环
sum += 1
// 取 B,B-1。由于有恢复的步骤,所以此时 A 仍为 2。
dfs({A:2, B:0})
sum += 1
// A-1
dfs({A:1, B:0})
sum += 1
// 由于 B 已经为 0,跳过,只能 A-1
dfs({A:0, b:0})
数学解法
利用数学公式,若长度为 n
的字符串中有 i
个不同字符,其重复的次数分别为 m[1], ..., m[i]
。则可组合的个数如下:
n! / (m[1]! * m[2]! * .. * m[i]!)
那么就需要确定长度为 n
的字符串,会有哪些不同字符及其个数的组合方式。
以 AAABB
为例,若想构成长度为 4
的字符串,组合可以是 3A1B
、2A2B
,即满足 A
+ B
的个数等于 4
即可。现在只需要找出这些组合,套用公式分别计算出个数相加,即得到长度为 4
的字符串所对应的全部排列数。
但注意
AAAB
和BAAA
是相同组合,因为它们的字符及对应的个数都相同,与顺序无关。
因此,只需计算出长度分别为 1~n
的字符串组合,利用上述公式求和即可。
c++ 递归解法,采用递归计算出组合并去重。
python 解法,该种方法计算组合比较巧妙,也是采用数学方法。
穷举法
使用递归计算出所有的排列组合,放入 set
中去重。
class Solution(object):
def numTilePossibilities(self, tiles):
"""
:type tiles: str
:rtype: int
"""
res = set()
def dfs(path, t):
if path:
res.add(path)
for i in range(len(t)):
dfs(path+t[i], t[:i] + t[i+1:])
dfs('', tiles)
return len(res)