【Codeforces】Round #837 (Div. 2) D. Hossam and (sub-)palindromic tree | 记搜、DP

221 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第18天,点击查看活动详情

【Codeforces】Codeforces Round #837 (Div. 2) D. Hossam and (sub-)palindromic tree | 记忆化搜索、动态规划

又是赛后立马 de 出 bug 的一天 QAQ

题目链接

Problem - D - Codeforces

题目

image.png

题目大意

给一棵 nn 个节点的树,树上的每个节点上都有一个小写字母。s(v,u)s(v,u) 表示从节点 vv 到节点 uu 唯一简单路径上的所有点上的小写字母构成的字符串。

相关定义如下:

  1. 如果可以通过删除几个(可能为零)字母从字符串 ss 中获得字符串 tt,则字符串 tt 是字符串 ss子序列
  2. 如果一个字符串从左到右和从右到左读相同,那么它是回文串。
  3. 如果一个字符串 tt 是字符串 ss子序列,且 tt 是回文串,那么我们称 ttss 的子回文串。
  4. 定义函数 f(s)f(s) 表示字符串 ss 的所有子回文串中,长度最长的串的长度。

求树中所有节点对 (v,u)(v,u) 对应的 f(s(v,u))f(s(v,u)) 的最大值。

思路

灵感

有一个很经典的问题是求一个长度为 nn 的字符串 a1,a2,...,ana_1,a_2,...,a_n 的最长回文子序列的长度。可以用区间 DP 记忆化搜索来做。设 getans(i,j)getans(i,j) 表示 ai,ai+1,...,aja_i,a_{i+1},...,a_j 的最长回文子序列的长度。当 dp[i][j]=0dp[i][j]=0 时,getans(i,j)getans(i,j) 返回的结果如下:

  • 如果 i=ji=j,则答案是 11
  • 如果 i+1=ji+1=ja[i]=a[j]a[i]=a[j],则答案是 22
  • 如果 i+1=ji+1=ja[i]a[j]a[i]\neq a[j],则答案是 11
  • 如果 i+1<ji+1<ja[i]=a[j]a[i]=a[j],则答案是 2+getans(i+1,j1)2+getans(i+1,j-1)
  • 如果 i+1<ji+1<ja[i]a[j]a[i]\neq a[j],则答案是 max(getans(i,j1),getans(i+1,j))max(getans(i,j-1),getans(i+1,j))

由于我们利用了 dpdp 数组进行记忆化,递归的过程实质上就是 dpdp 数组填充的过程,时间复杂度 O(n2)O(n^2)

思路

本题与我们刚才介绍的求序列的最长回文子序列的长度解法类似。

我们先用节点 11 当做根,用一遍 DFS 把整棵树拎起来。在 DFS 的过程中,我们需要记录每个节点的父节点,并且求解每个点到根节点的简单路径上的字符串的 ff 函数值。具体应该怎么做呢?

我们开一个栈 stkstk,每遍历到一个节点就将其入栈,离开该节点返回其父节点时我们就将其出栈。这样,我们每将一个节点入栈后,栈内自底向顶就是从根节点一路通往当前节点的简单路径。我们得到了这条简单路径就相当于知道了一个字符串,可以直接对这个字符串按我们灵感中介绍的最长回文子序列的长度的求法进行求解。因为我们是顺次进行的遍历,所以无需记忆化搜索,可以直接进行转移。该部分时间复杂度 O(n2)O(n^2)

同时在遍历当前根节点的子节点时,我们顺便记录每个节点 xx 的父节点为 f[x]f[x]

这样我们就可以通过一遍 DFS 记录每个节点的父节点,并且求解每个点到根节点的简单路径上的字符串的 ff 函数值了。

此时我们对于任意两个节点 xxyy,如果这两个节点的间的 f(s(x,y))f(s(x,y)) 还没有确定,即 dp[x][y]=0dp[x][y]=0,说明节点 xx 和节点 yy 的关系如下图所示:

image.png

f(s(x,y))f(s(x,y)) 可以用记忆化搜索求解,定义函数 getans(x,y)getans(x,y),容易发现其返回结果情况如下:

  • 如果 dp[x][y]0dp[x][y]\neq 0,则返回 dp[x][y]dp[x][y]

否则:

  • 如果 a[x]=a[y]a[x]=a[y],则答案是 2+getans(f[x],f[y])2+getans(f[x],f[y])
  • 如果 a[x]a[y]a[x]\neq a[y],则答案是 max(getans(f[x],y),getans(x,f[y]))max(getans(f[x],y),getans(x,f[y]))

让我们对上述转移方程进行说明:

因为 dp[x][y]dp[x][y] 不为 0,观察上图,f[x]f[x]f[y]f[y] 一定在从 xxyy 的简单路径上。

image.png

假设我们想要求解的 xxyy 如图所示。

在我们不断递归的过程中,为了便于理解,我们假设有两个箭头 txtxtyty 分别沿着 xx 到 根的路径和 yy 到根的路径向上跳。当 txtxtyty 中的一个跳到 xxyy 的最近公共祖先时(也有可能同时),dpdp 值必然在 DFS 中已经求过了。整个过程中 txtxtyty 均不会离开 xxyy 的简单路径。即这个记忆化搜索的过程等价于我们灵感来源的记搜过程。

代码

#include <stdio.h>
#include <algorithm>
#include <iostream>
using namespace std;
using LL=long long;
const int N=2e3+5;
char a[N];
int f[N];
int n,m,k;
vector<int> e[N];
int dp[N][N],stk[N],tot;
void dfs(int u,int fa)
{
	dp[u][u]=1;
	stk[++tot]=u;
	for (int i=tot-2;i>=1;--i)
	{
		if (a[stk[i]]==a[u]) dp[stk[i]][u]=dp[u][stk[i]]=dp[stk[i+1]][stk[tot-1]]+2;
		else dp[stk[i]][u]=dp[u][stk[i]]=max(dp[stk[i]][stk[tot-1]],dp[stk[i+1]][stk[tot]]);
	}
	for (auto v:e[u])
	{
		if (v==fa) continue;
		f[v]=u;
		if (a[u]==a[v]) dp[u][v]=dp[v][u]=2;
		else dp[u][v]=dp[v][u]=1;
		dfs(v,u);
	}
	stk[tot--]=0;
}
int getans(int x,int y)
{
	if (!dp[x][y]) 
	{
		if (a[x]==a[y]) dp[x][y]=dp[y][x]=getans(f[x],f[y])+2;
		else dp[x][y]=dp[y][x]=max(getans(f[x],y),getans(x,f[y]));
	}
	return dp[x][y];
}
LL solve()
{
	for (int i=1;i<=n;++i)
	{
		e[i].clear();
		for (int j=1;j<=n;++j) dp[i][j]=0;
	}
	scanf("%d",&n);
	for (int i=1;i<=n;++i) cin>>a[i];
	for (int x,y,i=1;i<n;++i)
	{
		scanf("%d%d",&x,&y);
		e[x].push_back(y);
		e[y].push_back(x);
	}
	dfs(1,0);
	int ans=0;
	for (int x,y,i=1;i<=n;++i)
		for (int j=1;j<=n;++j) ans=max(ans,getans(i,j));
	printf("%d\n",ans);
	return 0;
}
int main()
{
	int T=1;
	scanf("%d",&T);
	while (T--) solve();
	return 0;
}