树上记忆化搜索(Crazy Bobo,HDU 5325)

15 篇文章 0 订阅
15 篇文章 0 订阅
树形DP,就是在树上进行动态规划,这要求树是一个有向无环图。对于大部分题目而言,要么给你一棵有向树(又称有根树)(同时也是一个有向无环图),要么给你一棵无向树,你可以根据需要转化成有向树(又称有根树)(同时也是一个有向无环图)来处理。具体处理方式就是一遍,两遍,甚至多遍dfs。

本题的话,我们可以用有方向的树来建模,虽然这棵树是一个有向无环图,但它不是一棵有向树(有根树),所以常规的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;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值