2025-12-24:图中的最长回文路径。用go语言,给出一个整数 n,和一个包含 n 个节点(编号 0 到 n-1)的无向图,图的边用二维数组 edges 表示,其中每个元素 [u, v] 表示节点 u 与节点 v 之间有一条无向边。同时给出一个长度为 n 的字符串 label,label[i] 是节点 i 对应的字符。
你可以从任意节点出发,沿着边移动,每个节点最多访问一次(路径上节点不重复)。按照访问顺序把经过节点的字符连成一个字符串。要求找出通过选取这样的一条不重复节点的路径,所能得到的最长回文字符串的长度,并返回该长度。
1 <= n <= 14。
n - 1 <= edges.length <= n * (n - 1) / 2。
edges[i] == [ui, vi]。
0 <= ui, vi <= n - 1。
ui != vi。
label.length == n。
label 只包含小写英文字母。
不存在重复边。
输入: n = 3, edges = [[0,1],[1,2]], label = "aba"。
输出: 3。
解释:
最长的回文路径是从节点 0 到节点 2,经过节点 1,路径为 0 → 1 → 2,形成字符串 "aba"。
这是一个长度为 3 的回文串。
题目来自力扣3615。
🔍 算法过程详解
-
计算理论最大值 算法首先计算在整个字符串
label中,不考虑图结构的情况下,能构成的最长回文串的理论长度。这一步基于回文串的特性:最多只能有一种字符出现奇数次(该字符可放在回文中心)。- 统计字符频率:遍历
label字符串,统计 26 个小写字母各自出现的次数。 - 计算奇数字符数:统计出现次数为奇数的字符种类数,记为
odd。 - 计算理论最大值:理论最长回文串长度为
n - max(odd - 1, 0)。这是因为,如果odd > 1,除了保留一个奇数字符作为回文中心外,其他奇数字符每种都需要减少一个(即“丢弃”)以保证整体能构成回文。 - 完全图优化:如果给定的图是完全图(即任意两个节点之间都有边,边的数量为
n*(n-1)/2),那么图中任意路径都存在。此时,可以直接返回理论最大值,因为一定能构造出该回文串 。
- 统计字符频率:遍历
-
构建图结构 如果图不是完全图,算法会构建一个邻接表
g来表示图,以便后续进行图遍历。- 初始化一个大小为
n的切片g,每个元素是一个整数切片,用于存储对应节点的邻居节点。 - 遍历
edges中的每条边[u, v],将节点v添加到节点u的邻居列表中,同时将节点u添加到节点v的邻居列表中,因为图是无向的 。
- 初始化一个大小为
-
初始化记忆化数组 为了避免重复计算,算法使用了一个三维数组
memo进行记忆化(Memoization)。memo[x][y][vis]用于记录一个状态:当前路径的“两端”分别是节点x和节点y,并且已经访问过的节点集合由位掩码vis表示(vis的第i位为 1 表示节点i已被访问)。该状态的值表示从x和y开始,向路径中间继续扩展,最多还能添加多少个节点(不包含当前的x和y节点)。- 初始化时,
memo中所有值设为 -1,表示该状态尚未被计算 。
-
深度优先搜索(DFS) 核心的 DFS 函数
dfs(x, y, vis)采用递归方式,其目标是寻找从当前路径两端x和y向外扩展所能形成的最长回文路径的新增部分长度。- 状态参数:
x和y是当前路径的两个端点,vis是已访问节点的位掩码。 - 终止条件与记忆化:首先检查
memo[x][y][vis]。如果值不为 -1,直接返回,避免重复计算。 - 路径扩展:尝试从端点
x出发,遍历其所有未被访问的邻居节点v。同时,从端点y出发,遍历其所有未被访问的邻居节点w。 - 回文匹配:对于找到的一对邻居
(v, w),检查它们对应的字符是否相同(即label[v] == label[w]),并且v和w不能是同一个节点。这确保了将v和w分别添加到路径的两端后,新形成的路径字符串在两端增加的字符是相同的,保持了回文性质。 - 递归探索:如果匹配成功,则将
v和w标记为已访问(更新vis),并递归调用dfs(v, w, vis)。递归调用的返回值加上 2(代表新增的v和w两个节点),就是当前扩展方式下路径的潜在新长度。 - 结果更新:在所有可能的
(v, w)配对中,dfs函数返回能获得的最大扩展长度。
- 状态参数:
-
枚举所有可能的起始状态 在主逻辑中,算法枚举所有可能的路径起始情况,分为两种类型:
- 奇长度回文路径:以一个节点
x作为回文中心。初始路径就是[x]。调用dfs(x, x, 1<<x),其结果加上 1(中心节点x)就是以此为中心能形成的最长奇长度回文路径。 - 偶长度回文路径:以一条边上的两个相邻节点
x和y作为回文中心,且要求label[x] == label[y]。初始路径是[x, y]。调用dfs(x, y, 1<<x | 1<<y),其结果加上 2(两个中心节点x和y)就是能形成的最长偶长度回文路径。 - 在枚举过程中,算法不断更新全局答案
ans。同时,如果某次计算得到的ans已经等于之前计算的理论最大值,则提前终止计算,因为不可能找到更长的路径了 。
- 奇长度回文路径:以一个节点
⏱️ 复杂度分析
-
总的时间复杂度:O(n² * 2ⁿ)。
- 状态总数由
x(n 种可能)、y(n 种可能)和vis(2ⁿ 种可能)决定,即 O(n² * 2ⁿ)。 - 对于每个状态,在最坏情况下需要遍历节点
x和y的所有邻居进行配对,邻居数最多为 O(n),因此处理一个状态的时间是 O(n²)。 - 综上,最坏情况下的总时间复杂度为 O(n² * 2ⁿ * n²) = O(n⁴ * 2ⁿ)。但由于 n 最大为 14,且存在记忆化和剪枝,实际运行中远好于最坏情况。
- 状态总数由
-
总的额外空间复杂度:O(n² * 2ⁿ)。
- 这主要来自于记忆化数组
memo的空间占用,它需要存储 n * n * 2ⁿ 个整数值 。
- 这主要来自于记忆化数组
Go完整代码如下:
package main
import (
"fmt"
)
func maxLen(n int, edges [][]int, label string) (ans int) {
// 计算理论最大值
cnt := [26]int{}
for _, ch := range label {
cnt[ch-'a']++
}
odd := 0
for _, c := range cnt {
odd += c % 2
}
theoreticalMax := n - max(odd-1, 0) // 奇数选一个放正中心,其余全弃
if len(edges) == n*(n-1)/2 { // 完全图,可以达到理论最大值
return theoreticalMax
}
g := make([][]int, n)
for _, e := range edges {
x, y := e[0], e[1]
g[x] = append(g[x], y)
g[y] = append(g[y], x)
}
memo := make([][][]int, n)
for i := range memo {
memo[i] = make([][]int, n)
for j := range memo[i] {
memo[i][j] = make([]int, 1<<n)
for p := range memo[i][j] {
memo[i][j][p] = -1
}
}
}
// 计算从 x 和 y 向两侧扩展,最多还能访问多少个节点(不算 x 和 y)
var dfs func(int, int, int) int
dfs = func(x, y, vis int) (res int) {
p := &memo[x][y][vis]
if *p >= 0 { // 之前计算过
return *p
}
for _, v := range g[x] {
if vis>>v&1 > 0 { // v 在路径中
continue
}
for _, w := range g[y] {
if vis>>w&1 == 0 && w != v && label[w] == label[v] {
// 保证 v < w,减少状态个数和计算量
r := dfs(min(v, w), max(v, w), vis|1<<v|1<<w)
res = max(res, r+2)
}
}
}
*p = res // 记忆化
return
}
for x, to := range g {
// 奇回文串,x 作为回文中心
ans = max(ans, dfs(x, x, 1<<x)+1)
if ans == theoreticalMax {
return
}
// 偶回文串,x 和 x 的邻居 y 作为回文中心
for _, y := range to {
// 保证 x < y,减少状态个数和计算量
if x < y && label[x] == label[y] {
ans = max(ans, dfs(x, y, 1<<x|1<<y)+2)
if ans == theoreticalMax {
return
}
}
}
}
return
}
func main() {
n := 3
edges := [][]int{{0, 1}, {1, 2}}
label := "aba"
result := maxLen(n, edges, label)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
from typing import List
def maxLen(n: int, edges: List[List[int]], label: str) -> int:
# 计算理论最大值
cnt = [0] * 26
for ch in label:
cnt[ord(ch) - ord('a')] += 1
odd = 0
for c in cnt:
odd += c % 2
theoretical_max = n - max(odd - 1, 0) # 奇数选一个放正中心,其余全弃
# 如果是完全图,可以达到理论最大值
if len(edges) == n * (n - 1) // 2:
return theoretical_max
# 构建邻接表
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append(y)
g[y].append(x)
# 记忆化数组
memo = [[[-1] * (1 << n) for _ in range(n)] for _ in range(n)]
def dfs(x: int, y: int, vis: int) -> int:
"""从x和y向两侧扩展,最多还能访问多少个节点(不算x和y)"""
if memo[x][y][vis] >= 0:
return memo[x][y][vis]
res = 0
# 尝试找到一对对称的节点
for v in g[x]:
if (vis >> v) & 1: # v已经在路径中
continue
for w in g[y]:
if ((vis >> w) & 1) == 0 and w != v and label[w] == label[v]:
# 保证v < w,减少状态个数和计算量
r = dfs(min(v, w), max(v, w), vis | (1 << v) | (1 << w))
res = max(res, r + 2)
memo[x][y][vis] = res
return res
ans = 0
# 检查奇回文串(以x为中心)
for x in range(n):
ans = max(ans, dfs(x, x, 1 << x) + 1)
if ans == theoretical_max:
return ans
# 检查偶回文串(以x和y作为对称中心)
for y in g[x]:
if x < y and label[x] == label[y]:
ans = max(ans, dfs(x, y, (1 << x) | (1 << y)) + 2)
if ans == theoretical_max:
return ans
return ans
# 测试代码
if __name__ == "__main__":
n = 3
edges = [[0, 1], [1, 2]]
label = "aba"
result = maxLen(n, edges, label)
print(result)
C++完整代码如下:
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <unordered_map>
#include <functional>
using namespace std;
class Solution {
public:
int maxLen(int n, vector<vector<int>>& edges, string label) {
// 计算理论最大值
int cnt[26] = {0};
for (char ch : label) {
cnt[ch - 'a']++;
}
int odd = 0;
for (int c : cnt) {
odd += c % 2;
}
int theoreticalMax = n - max(odd - 1, 0);
// 如果是完全图
if (edges.size() == n * (n - 1) / 2) {
return theoreticalMax;
}
// 构建邻接表
vector<vector<int>> g(n);
for (auto& e : edges) {
int x = e[0], y = e[1];
g[x].push_back(y);
g[y].push_back(x);
}
// 使用 unordered_map 进行记忆化,避免数组过大
unordered_map<int, int> memo;
function<int(int, int, int)> dfs = [&](int x, int y, int vis) -> int {
// 编码状态:vis 的高位存储 x 和 y
int key = (x << 20) | (y << 10) | vis;
if (memo.count(key)) {
return memo[key];
}
int res = 0;
for (int v : g[x]) {
if (vis >> v & 1) {
continue;
}
for (int w : g[y]) {
if ((vis >> w & 1) == 0 && w != v && label[w] == label[v]) {
int next = dfs(min(v, w), max(v, w), vis | (1 << v) | (1 << w));
res = max(res, next + 2);
}
}
}
memo[key] = res;
return res;
};
int ans = 0;
for (int x = 0; x < n; x++) {
ans = max(ans, dfs(x, x, 1 << x) + 1);
if (ans == theoreticalMax) return ans;
for (int y : g[x]) {
if (x < y && label[x] == label[y]) {
ans = max(ans, dfs(x, y, (1 << x) | (1 << y)) + 2);
if (ans == theoreticalMax) return ans;
}
}
}
return ans;
}
};
int main() {
int n = 3;
vector<vector<int>> edges = {{0, 1}, {1, 2}};
string label = "aba";
Solution sol;
int result = sol.maxLen(n, edges, label);
cout << result << endl; // 输出: 3
return 0;
}