给你一棵 树(即,一个连通、无向且无环的图),根 节点为 0
,由编号从 0
到 n - 1
的 n
个节点组成。这棵树用一个长度为 n
、下标从 0 开始的数组 parent
表示,其中 parent[i]
为节点 i
的父节点,由于节点 0
为根节点,所以 parent[0] == -1
。
另给你一个长度为 n
的字符串 s
,其中 s[i]
是分配给 i
和 parent[i]
之间的边的字符。s[0]
可以忽略。
找出满足 u < v
,且从 u
到 v
的路径上分配的字符可以 重新排列 形成 回文 的所有节点对 (u, v)
,并返回节点对的数目。
如果一个字符串正着读和反着读都相同,那么这个字符串就是一个 回文 。
示例 1:
输入:parent = [-1,0,0,1,1,2], s = "acaabc" 输出:8 解释:符合题目要求的节点对分别是: - (0,1)、(0,2)、(1,3)、(1,4) 和 (2,5) ,路径上只有一个字符,满足回文定义。 - (2,3),路径上字符形成的字符串是 "aca" ,满足回文定义。 - (1,5),路径上字符形成的字符串是 "cac" ,满足回文定义。 - (3,5),路径上字符形成的字符串是 "acac" ,可以重排形成回文 "acca" 。
示例 2:
输入:parent = [-1,0,0,0,0], s = "aaaaa" 输出:10 解释:任何满足 u < v 的节点对 (u,v) 都符合题目要求。
提示:
n == parent.length == s.length
1 <= n <= 10^5
- 对于所有
i >= 1
,0 <= parent[i] <= n - 1
均成立 parent[0] == -1
parent
表示一棵有效的树s
仅由小写英文字母组成
提示 1
A string is a palindrome if the number of characters with an odd frequency is either 0 or 1.
提示 2
Let mask[v] be a mask of 26 bits that represent the parity of each character in the alphabet on the path from node 0 to v. How can you use this array to solve the problem?
解法:
提示 1
回文串等价于至多一个字母出现奇数次,其余字母出现偶数次。
提示 2
用一个长为 26 的二进制数来压缩存储每个字母的奇偶性。
一条边可以看成是 1<<(s[i]-'a')。
那么路径所对应的二进制数,就是路径上的所有边的异或和(因为异或就是模 2 剩余系中的加法,刚好可以表示奇偶性)。
只有 27 个二进制数符合要求:
- 0,表示每个字母都出现偶数次。
- 2^0 ,2^1 ,⋯,2^25 ,表示第 i 个字母出现奇数次,其余字母出现偶数次。
提示 3
设 v 和 w 的最近公共祖先为 lca,设从根到 i 的路径异或和为 XOR i。
v 到 w 的路径可以看成是 v−>lca−>w,其中 lca 到 v 的路径异或和,等于根到 v 的异或和,再异或上根到 lca 的异或和(从根到 lca 的边异或了两次,等于 0 抵消掉)。lca 到 w 的路径异或和也同理。
所以 v−>lca−>w 的异或和为:
XORlca 异或了两次,抵消掉,所以上式为:
把所有 XORi 求出来,就变成判断这 n−1 个数当中:
- 两数异或和是否为 0?这意味着路径上的每个字母都出现偶数次。
- 两数异或和是否为 2 的幂?这意味着路径上恰好有个字母出现奇数次,其余字母出现偶数次。
- 特殊情况:XOR i =0 或者 XOR i 为 2 的幂,表示从根到 i 的路径符合要求,我们可以异或上一条「空路径」对应的异或值,即 0,就转换成了上面两数异或和的情况。
我们可以用哈希表记录 XOR i 的个数,设当前算出的异或和为 x,去哈希表中找 x 的出现次数以及 x⊕2^k 的出现次数。
树上所有路径不好处理,而所有到根节点的路径比较好处理,因此,通常把树上任意路径转化为到根节点的路径:任意两个节点 u,v 间的路径 u→v 可以拆分为 u→lca(u,v)→v。
如果 u,v 不在根节点的同一棵子树里,那么 lca(u,v) 就是根节点,这条路径是两条到根节点的路径的拼接;否则,会少掉一次从 lca(u,v) 到根节点和从根节点回到 lca(u,v) 的路径,可以看作这一来一回的路径相互抵消。
这道题的特点在于,路径上字符能否排成回文串可以转化为频率为奇数的字符不超过 1,如果用一个二进制数的每一位表示一个字母的频率的奇偶性,那么,无论是拼接还是抵消,都只需要进行异或操作。
因此,不需要管是拼接还是抵消,无论结点是否同子树,都可以用异或来判断,只需要遍历一遍所有结点,把已遍历结点到根节点的字母奇偶性存到哈希表里,然后求与当前结点对应的路径能拼接或抵消成满足条件的路径的数量即可。
节点数为 n,求与当前结点对应的路径能拼接或抵消成满足条件的路径的数量需要 O(A) 时间,总时间复杂度为 O(n*A)。
Java版:
class Solution {
public long countPalindromePaths(List<Integer> parent, String s) {
int n = parent.size();
List<Integer>[] children = new ArrayList[n];
Arrays.setAll(children, e -> new ArrayList<>());
for (int i = 1; i < n; i++) {
int p = parent.get(i);
children[p].add(i);
}
Map<Integer, Integer> count = new HashMap<>();
count.put(0, 1);
return dfs(0, 0, children, s.toCharArray(), count);
}
private long dfs(int lca, int status, List<Integer>[] children, char[] s, Map<Integer, Integer> count) {
long res = 0;
for (int child: children[lca]) {
int childStatus = status ^ (1 << s[child] - 'a');
// 所有字符出现偶数次
res += count.getOrDefault(childStatus, 0);
for (int i = 0; i < 26; i++) {
// 最多有一个字符出现奇数次
res += count.getOrDefault(childStatus ^ (1 << i), 0);
}
count.merge(childStatus, 1, Integer::sum);
// 加上孩子节点的值
res += dfs(child, childStatus, children, s, count);
}
return res;
}
}
Python3版:
class Solution:
def countPalindromePaths(self, parent: List[int], s: str) -> int:
n = len(parent)
children = [[] for _ in range(n)]
for i in range(1, n):
p = parent[i]
children[p].append(i)
count = {0: 1}
def dfs(lca: int, status: int) -> int:
res = 0
for child in children[lca]:
childStatus = status ^ (1 << ord(s[child]) - ord('a'))
if childStatus in count:
res += count[childStatus]
for i in range(26):
if childStatus ^ (1 << i) in count:
res += count[childStatus ^ (1 << i)]
count[childStatus] = count[childStatus] + 1 if childStatus in count else 1
res += dfs(child, childStatus)
return res
return dfs(0, 0)
复杂度分析
- 时间复杂度:O(n*A),其中 n 为 s 的长度,A为字符集合的大小,本题中字符均为小写字母,所以 A=26。
- 空间复杂度:O(n)。