LIS on Tree题解
AT5392 题解
前置知识:树形结构、线性 DP
考验读题能力的一道好题。题目大意很简单,就是把一个简单的线性 DP 移植到了树上。
但是这道题有一个难点:即给定的树是一棵无根树,也就是说边是无向的。所以说在输入时应该建立一条双向的边。
参考下面的一段错误代码:
for (int i = 1; i < n; i++) {
scanf("%d %d", &x, &y);
G[x].push_back(y);
}
/*可以看到在输入时没有在y->x方向上建边*/
至于最长上升子序列,在遍历树时打一个记忆化搜索就可以,萌新可以参考下面的DP状态转移方程:
其中
因此我们要在原来状态的基础上做一个优化。引入 LIS 问题的优化算法--二分法。这种算法刚开始可能很难理解,它在原先的状态转移方程的基础上进行了改动。考虑贪心的思想,优先选择较小的数可以使后来增加的数的取值变小,意味着我们可以选择更多的数,可以构造出一个符合条件的序列,让序列的长度表示当前 LIS 的长度。当一个新数加入时,用二分法查看它能够在原序列中替换的位置,这里的“替换”是暂时的,并不是当前的序列。真正的序列应该取决于当前序列的最后一个元素,只有这个数改动,才意味着序列发生了变化。
最后我们根据构造好的最终“序列”的长度,判断出 LIS 的长度。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n, x, y, a[200005], dp[200005], f[200005], tot, ans;
//tot表示当前序列的长度
//f[i]表示以i为一棵子树的根时,序列的长度
vector<int> G[200005];
//用邻接表表示树
bool vis[200005];
//vis判断是否走过,因为是无向边
void dfs(int rt){//因为我们的目的是找一条链,所以用DFS遍历,同时方便记忆化。
vis[rt] = 1;
ans = max(ans, tot);
int l, r, t, mid;
bool flag = 0;
if (!tot || a[rt] > dp[tot]) dp[++tot] = a[rt], flag = 1;
//判断能否在序列的末尾增加新元素
//如果可以则加入,并将序列长度增加一
else{
l = 1, r = tot;
while (l <= r) {
mid = (l + r) >> 1;
if (dp[mid] >= a[rt])
r = mid - 1;
else
l = mid + 1;
}
t = dp[l];
dp[l] = a[rt];
//否则就在原序列中“替换”一个数
//此时“序列”长度不变
}
f[rt] = tot;
for (int i = 0; i < G[rt].size(); i++)
if (!vis[G[rt][i]]) dfs(G[rt][i]);
//对下一个节点进行遍历
if (flag) dp[tot] = 0, tot--;
else dp[l] = t;
//在回溯时撤回当前的操作
}
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i < n; i++){
scanf("%d %d", &x, &y);
G[x].push_back(y);
G[y].push_back(x);//正确的建树方法
}
dfs(1);//dfs遍历树
for (int i = 1; i <= n; i++)
printf("%d\n", f[i]);//按要求输出
return 0;
}