2025-12-24:图中的最长回文路径。用go语言,给出一个整数 n,和一个包含 n 个节点(编号 0 到 n-1)的无向图,图的边用二维数组 edges 表

27 阅读9分钟

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。

🔍 算法过程详解

  1. 计算理论最大值 算法首先计算在整个字符串 label 中,不考虑图结构的情况下,能构成的最长回文串的理论长度。这一步基于回文串的特性:最多只能有一种字符出现奇数次(该字符可放在回文中心)。

    • 统计字符频率:遍历 label 字符串,统计 26 个小写字母各自出现的次数。
    • 计算奇数字符数:统计出现次数为奇数的字符种类数,记为 odd
    • 计算理论最大值:理论最长回文串长度为 n - max(odd - 1, 0)。这是因为,如果 odd > 1,除了保留一个奇数字符作为回文中心外,其他奇数字符每种都需要减少一个(即“丢弃”)以保证整体能构成回文。
    • 完全图优化:如果给定的图是完全图(即任意两个节点之间都有边,边的数量为 n*(n-1)/2),那么图中任意路径都存在。此时,可以直接返回理论最大值,因为一定能构造出该回文串 。
  2. 构建图结构 如果图不是完全图,算法会构建一个邻接表 g 来表示图,以便后续进行图遍历。

    • 初始化一个大小为 n 的切片 g,每个元素是一个整数切片,用于存储对应节点的邻居节点。
    • 遍历 edges 中的每条边 [u, v],将节点 v 添加到节点 u 的邻居列表中,同时将节点 u 添加到节点 v 的邻居列表中,因为图是无向的 。
  3. 初始化记忆化数组 为了避免重复计算,算法使用了一个三维数组 memo 进行记忆化(Memoization)。

    • memo[x][y][vis] 用于记录一个状态:当前路径的“两端”分别是节点 x 和节点 y,并且已经访问过的节点集合由位掩码 vis 表示(vis 的第 i 位为 1 表示节点 i 已被访问)。该状态的值表示从 xy 开始,向路径中间继续扩展,最多还能添加多少个节点(不包含当前的 xy 节点)。
    • 初始化时,memo 中所有值设为 -1,表示该状态尚未被计算 。
  4. 深度优先搜索(DFS) 核心的 DFS 函数 dfs(x, y, vis) 采用递归方式,其目标是寻找从当前路径两端 xy 向外扩展所能形成的最长回文路径的新增部分长度

    • 状态参数xy 是当前路径的两个端点,vis 是已访问节点的位掩码。
    • 终止条件与记忆化:首先检查 memo[x][y][vis]。如果值不为 -1,直接返回,避免重复计算。
    • 路径扩展:尝试从端点 x 出发,遍历其所有未被访问的邻居节点 v。同时,从端点 y 出发,遍历其所有未被访问的邻居节点 w
    • 回文匹配:对于找到的一对邻居 (v, w),检查它们对应的字符是否相同(即 label[v] == label[w]),并且 vw 不能是同一个节点。这确保了将 vw 分别添加到路径的两端后,新形成的路径字符串在两端增加的字符是相同的,保持了回文性质。
    • 递归探索:如果匹配成功,则将 vw 标记为已访问(更新 vis),并递归调用 dfs(v, w, vis)。递归调用的返回值加上 2(代表新增的 vw 两个节点),就是当前扩展方式下路径的潜在新长度。
    • 结果更新:在所有可能的 (v, w) 配对中,dfs 函数返回能获得的最大扩展长度。
  5. 枚举所有可能的起始状态 在主逻辑中,算法枚举所有可能的路径起始情况,分为两种类型:

    • 奇长度回文路径:以一个节点 x 作为回文中心。初始路径就是 [x]。调用 dfs(x, x, 1<<x),其结果加上 1(中心节点 x)就是以此为中心能形成的最长奇长度回文路径。
    • 偶长度回文路径:以一条边上的两个相邻节点 xy 作为回文中心,且要求 label[x] == label[y]。初始路径是 [x, y]。调用 dfs(x, y, 1<<x | 1<<y),其结果加上 2(两个中心节点 xy)就是能形成的最长偶长度回文路径。
    • 在枚举过程中,算法不断更新全局答案 ans。同时,如果某次计算得到的 ans 已经等于之前计算的理论最大值,则提前终止计算,因为不可能找到更长的路径了 。

⏱️ 复杂度分析

  • 总的时间复杂度O(n² * 2ⁿ)

    • 状态总数由 x(n 种可能)、y(n 种可能)和 vis(2ⁿ 种可能)决定,即 O(n² * 2ⁿ)。
    • 对于每个状态,在最坏情况下需要遍历节点 xy 的所有邻居进行配对,邻居数最多为 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;
}

在这里插入图片描述