本题的话,我们可以用有方向的树来建模,虽然这棵树是一个有向无环图,但它不是一棵有向树(有根树),所以常规的dfs方法没有办法解决。我们只能把它当成一个普通的有向无环图来看待,然后再用记忆化搜索。
一开始的话,想了一个晚上没有什么好的思路。
题目要求在满足条件的前提下,集合最大。所以考虑过二分集合大小+构造。
暴力枚举构造显然是不行的。时间复杂度是指数级的。
而且感觉没办法贪心,这绝对是不是一个局部的问题。
思考过用图论算法或者DP来构造但是没有任何思路。
由于生成子图(子树)必须连通,所以删除的一定是叶子节点,所以我又考虑按某种顺序遍历每个节点,然后贪心剪枝(这个剪枝是真的剪枝,模拟去掉一些叶子)。但是一是我没有办法保证算法正确性,二是没想到什么快速的办法可以模拟。所以当时我就只好转换思路了。
讲具体一点,就是按照w从小到大的顺序遍历每个节点。如果这个节点至少有2个子节点所代表的子树包含比它小的w(以下简称坏子节点,如果有两个以上的坏子节点,那么就一定会出现不符合条件的情况,可以自己纸上模拟一下),那就要剪枝,直到剩下小于2个坏子节点为止,剪枝就是每次贪心减掉最小的子树。
如果从小到大遍历很容易找到反例,从大到小遍历也一样。主要是因为本来可以一刀切掉关键的节点的,但是因为顺序的原因整个集合被慢慢蚕食了。
推而广之,任何一个顺序(比如正常的树的遍历)都可能会出现这种蚕食的情况。
然后我又考虑从两种方案里挑一个更优解。
不过这个方法应该是不可行的。
1、因为我事后也找到了使这个方法不成立的图,就是无论按那种方式遍历,都会慢慢蚕食,然后在中盘被大量剪枝,导向错误的答案,比如:
2、其次是没法模拟,贪心剪枝需要知道一些信息,比如子树的大小,是不是坏节点(包含更小的w)。
如果遍历的顺序在树形结构上是无规律的(如果要保证在w大小方面是有规律的话),那么很难在维护信息,每次变动我都需要通知其他所有节点,或者每考虑一个新节点我都要去重新查询需要的信息。我只能想到O(n)的维护方法,即每次变动我都要重新维护所有节点的信息,或者每次剪枝我都要查询所有节点的信息,总时间复杂度O(n^2),无法承受。
如果遍历的顺序在结构上是有规律的(那么就要牺牲掉在w大小方面的规律),比如正常的树的dfs。那么我们可以通过两遍dfs的树形DP预处理出基本信息。然后第三遍dfs枚举节点,自底向上依次处理每个节点,然后顺便维护所需要的信息,这样总的时间复杂度就是O(n)了,但是在上面的讨论中我们已经知道了这个做法是错的。
最后我又尝试了DAG上的最长路。每个节点只可能往w比它大的节点走,最早从w最小的节点开始走,最晚到w最大的节点结束。这就是一个有向无环图,我们要找一个最长路,路径长度就是答案。状态数为O(n),但是转移的代价太高,也是O(n),尝试了各种办法优化,比如预处理(这不可能的时间复杂度要O(n^2)),单调栈,单调队列(这些也不行的,没有符合要求的性质),搜索(单次搜索最差情况也是O(n)的)。最后认为没有办法降低时间复杂度,所以就否定了这个办法。
主要问题是当时找不到什么规律,感觉树形结构太全局,有没法贪心暴力,也没有什么经典的算法可以套用。
但是第二天思考了一小会很快就找到规律并做了出来。
还是沿用了昨天思考过的一些东西,然后换了一种方式去解决。
昨天在思考DAG上的动态规划时,需要判断两点间路径上的最大w,就已经(才)发现了一些规律。
1、树和图的最大区别就是:树上的每个节点都是一个哨所(割顶),是连接两个或多个部分的必经之路。
2、在前往下一个节点的路径是唯一的,上面不能有比出发点的w还要高的节点,但是我们的目标节点的w一定比当前节点高,沿途必须经过我们曾经到过的节点(根据题意)。
如果把每个节点都比作山,我们每次就站在一个山的山顶,选择我们可以到达的(路上的山都更矮而且我们曾经来到过,目标山更高)最矮的山(否则这座山我们就再也不可能到达了)。然后沿着唯一的路径水平飞向下一座山,沿途飞过那些更矮的山,最后到达目的地,然后爬到山顶,再考虑下一座山,已经走过的山可以当成路径来使用,如此循环。如果一座较矮的山被另一座较高地山挡住了,我们就永远也不可能到达这座山了(我们不能穿过高山,我们只能飞往更高的山)。如果一座更高的山被一座更矮但却没去过的山挡住了,我们也永远不可能到达这座山了(路径上的山必须曾经到过,我们永远不可能到达路径上的山了)。
我们就像一只在盆地里乱飞的鸟,因为盆地外的山都被挡住了,我们也不能走下坡路,所以我们到不了了,我们只能每次在盆地贪心地把每一个山头都跑遍,直到没有山可以飞了。
问题就变成了在树上找一个类似盆地的地形,然后要求这个盆地尽量大。
记忆化搜索。
这不禁让我想起了一道题目:滑雪
https://vjudge.net/problem/POJ-1088
图(还是平面图,不过没什么乱用)上的记忆化搜索。
对比:
相同点:滑雪要求一直往下滑,本题要求一直往上走。
不同点:滑雪是图,求最长道路,本题是树,求最大区域。
无非就是记忆化搜索的东西不一样罢了。
如果将本题的树改成图,也是可以O(n)解决的。
区别就是一个曾经搜索过的区域可能会多次访问,但是我们只计算一次。所以我们需要对搜索过的区域进行染色,然后用一个VIS数组判重就可以解决,我们可以用++viscnt使得VIS数组不用每次memset,就可以做到O(n)了。
之所以会想那么久才做出来,我只能说:
从主观上来讲:当自己觉得穷尽了的时候,可以考虑换一种思路。
从客观上来讲:平常思考得少了,比赛时就得补。
ps:数组开小了,数据有5e5,我开了1e5,被搞了一波超时。。。
代码
#include<stdio.h>
#include<vector>
using namespace std;
const int maxn = 500010;
int n;
int w[maxn];
int dp[maxn];
vector<int>G[maxn];
void init()
{
for(int i=1;i<=n;i++)
{
scanf("%d",w+i);
G[i].clear();
dp[i]=-1;
}
int u,v;
for(int i=1;i<n;i++)
{
scanf("%d %d",&u,&v);
G[u].push_back(v);
G[v].push_back(u);
}
}
int d(int u)
{
int& ans = dp[u];
if(ans!=-1) return ans;
ans=1;
for(int i=0;i<(int)G[u].size();i++)
{
int v = G[u][i];
if(w[v]>w[u]) ans+=d(v);
}
return ans;
}
void solve()
{
init();
int ans=1;
for(int i=1;i<=n;i++) ans=max(ans,d(i));
printf("%d\n",ans);
}
int main()
{
while(scanf("%d",&n)==1) solve();
return 0;
}