zoukankan      html  css  js  c++  java
  • DP动态规划学习笔记——高级篇上

    说了要肝的怎么能咕咕咕呢?

    不了解DP或者想从基础开始学习DP的请移步上一篇博客:DP动态规划学习笔记

    这一篇博客我们将分为上中下三篇(这样就不用咕咕咕了...),上篇是较难一些树形DP,中篇则是数位和状压DP,下篇则是各种DP的优化手段。

    ——正片开始—— (为啥我最近的博客都喜欢写这个)

    背包类树形DP,树形DP里一种很鬼畜的题目。

    简单点讲就是:树上的分组背包。不知道分组背包的也请前往上一篇学习。

    我们先来看一道板子题:选课

    然后我们一起分析一下这道题(最好自己先想一想),由于每门课的先修课只有一门,所以我们很容易想到用树形结构储存这种关系——>只能从父节点到子节点。但是,这一题的数据会形成一个森林。想一想,为什么?

    没错,可能不止一门课没有先修课。但是我们只学过树形DP,怎么办呢?简单,我们自己整一棵树出来呗。

    但是原来的父子关系又不能变,怎么整呢?我们新建一个“虚拟课程”0出来,作为“没有先修课的课程”的先修课。也就是说我们把森林里的每一棵树都连到一个新的根节点——0节点上,这样我们就得到了一棵树。

    这个想法是不是很妙?然后这题就转换成了一个在树上运行的分组背包。

    然后我们来设状态。不会?那你别学了。上一篇博客看了没?快去看。

    我们上一篇讲过了,树形DP一般以每个节点x作为第一维...

    然后呢?(锤),分组背包啊。哦...

    于是我们就得到状态了:设f[x,j]表示我们从以x为根的子树中选j门课能获得的最高学分。

    很简单是不是?状态都出来了还不知道方程咩?那我们一起来推一推吧。

    我们定义几个变量方便描述,设son(x)表示x的子节点集合,siz(x)表示x的子节点个数,如果选修x这门课,那我们就对于任意y∈son(x),可以从以y为根的的子树中选出若干门课(记为ci)来修...在满足Σci=t-1的基础上,我们尽量要修最高的学分。我们有siz(x)组物品,每组物品有j-1个,其中第i组物品的第k个物品的体积为k,价值为f[y,k],背包的总容积为j-1。

    我们从每组中选出至多一个物品,即每个子节点最多转移一个状态到x。我们修完x后,还可以选j-1门课,所以我们要在“物品体积”不超过j-1的情况下,使学分(价值)最大。也就是说我们把“上多少y内的课”作为物品。

    然后我们进行树形DP,得到答案:

    void dp(int x){
        f[x][0]=0;
        for(int i=0;i<son[x].size();i++){
            int y=son[x][i];
            dp(y);
            for(int j=m+1;j>=1;t--)//虚拟节点必选,所以是m+1
                for(int k=0;k<=j-1;k++)//组内的物品,第x组第k个物品(选k门课)的体积为k
                    f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
        }
    }

    主函数部分,我们读入每个f[i][1]作为第i门课的价值——>解释:从以i为子树的课中选1门——>价值就是f[i][1]。

    接下来我们还是继续讨论树形DP。

    我们目前做的树形DP题都是在有根树上进行树形DP的,如果题目给定是一棵无根树呢?

    难道我们需要对以每个点为根都做一次树形DP统计答案吗?当然不是,要是这么做还不如暴力呢。

    一般对于这种题目,我们用二次扫描(换根法)来解决。

    这种DP又被称为换根DP,什么?你想要题?

    没有,(爆锤狗头)...拿去 Accumulation Degree

    这好像是我第一次给POJ上的题,皮欧勾的题我都没怎么刷过,不过貌似质量挺好的。

    但是我永远喜欢洛谷.jpg,emm,你说你看不懂?Chrome自带翻译,不皮了不皮了。

    喏。

    给你一个树形的水系(废话那么多懒得翻译了)

    有一个有n个节点,n-1条河道的树形水系,每个河道有一个最大容水量c[x][y]c[x][y]表示点x到y的最大容水量,源点可以源源不断出水,以源点作为根节点的树的叶子结点可以无限接纳水,而一个节点水的流量等于流过其儿子节点的水的流量之和,儿子节点水的流量不能超过其与父亲连边的最大容水量,询问最大的源点水流量,n2×10^5。

    其实简单点说就是求树形结构上的最大流,但是我们不知道源点。为什么不用网络流?嘘,数据太狗了。

    于是我们来快乐地DP啊。不给我源点?我每个点都DP一遍

    要是这样能过我还写它干嘛。

    不过还是先讲讲怎么DP吧。假设我们知道根节点是x。

    那么我们来拆问题了。你看这个问题它又大又烦,不如把它拆掉。

    考虑用树形DP拆问题。对于每个节点x,我们发现它只能流向自己的子树,于是可以这样设状态,我们用f[x]表示在以x为根的子树中,以x为源点流向子树的最大流量。然后我们对于每个子树都可以拆成小子树,再拆,再拆...就到叶节点了,问题就得解了。

    切,刚刚还那么傲娇的大问题现在还不是被脱的一件不剩,看到DP的魅力了吧?再大的问题,只要你是DP题,我就能把你拆掉。

    然后我们就很自然的得到了转移方程式:

    这里我不多写一个latex下面的就会炸不知道为什么,你们就当没看见那个error吧...抱歉抱歉

    $$f[x]= sumlimits_{ ext{y∈son(x)}} egin{cases} min(f[x],c(x,y))& ext{x=0}\ c(x,y)& ext{x!=0} end{cases}$$

    $$f[x]= sumlimits_{ ext{y∈son(x)}} egin{cases} min(f[x],c(x,y))& ext{x=0}\ c(x,y)& ext{x!=0} end{cases}$$

    Latex新手写上面那个公式写了半个小时...

    DP代码:

    void dp(int x){
        vis[x]=1;
        f[x]=0;
        for(int i=head[x];i;i=nxt(i)){
            int y=to(i);
            if(vis[y])continue;
            dp(y);
            if(deg[y]==1)f[x]+=val(i);
            else f[x]+=min(f[y],val(i));
        }
    }

     如果我们直接枚举源点...时间复杂度是O(n^2)的,接受不了(不是我接受不了,是出题人接受不了

    如果我自己要求,O(n^n)的算法我都开开心心用...

    于是我们使用换根DP来O(n)的解决这道题目。

    我们任选一个源点作为根,记为rt。然后我们进行一次上面的DP,得到f数组。

    设g[x]表示把x作为源点,流向整个水系,流量最大是多少。对于根节点rt,显然有f[rt]=g[rt]。

    假设g[x]已经被正确地求出了。开始了,万能的假设法...

    我们考虑一下它的子节点y,g[y]我们并不知道,我们来分析一下g[y]。

    如果我们把根换成y,那么g[y]包含两部分:

    1. 从y流向以y为根的子树的流量,即我们上面算出来的f[x]

    2. 从y沿着原父节点x流向水系中的其他部分的流量

    为什么可以像2这样流呢?因为我们把根换成y了。这是很多博客没有提到的,容易让人看得很懵逼。

    我们把x作为源点的总流量是g[x],从x流向y的流量就是min(f[y],c(x,y))。

    这个很好理解吧,y是x的子节点,x流向子树的最大流量是f[y],流量限制是c(x,y),哪个小流量就是哪个。

    所以从x流向除了y以外其他部分的流量就是两者之差。这个也很好理解吧。

    于是我们把y作为源点,先流向x,再流向其他部分的流量就是两者之差和c(x,y)之间的最小值。

    同样的,对于度数为1的x节点,我们也要特判——>节点x没法流向其他地方了。

    于是我们得到了g[x]的计算方法:

    又要写Latex...我裂开了

    $$g[y]=f[y]+ egin{cases} min( g[x]-min( f[y], c(x,y) ), c( x,y ) )& ext{x的度数>1}\ c(x,y)& ext{x的度数=1} end{cases}$$

    g[y]就是把根从x换成y后流量的计算结果,由于这是一个从上至下的递推方程,所以我们可以通过一遍dfs得出g数组。

    又到了你们最爱的放代码时间:

    void dfs(int x){
        vis[x]=1;
        for(int i=head[x];i;i=nxt(i)){
            int y=to(i);
            if(vis[y])continue;
            if(deg[x]==1)g[y]=f[x]+val(i);
            else g[y]=f[y]+min(g[x]-min(f[y],val(i)),val(i));
            dfs(y);
        }
    }

    解释一下,val(i)就是上面说的c(x,y),我把它当作边权存了起来。

    然后我们在main函数里面这样搞就可以得到答案了。

    int rt=1;
    dp(rt);
    memset(vis,0,sizeof vis);
    g[rt]=f[rt];
    dfs(rt);
    int ans=-1;
    for(int i=1;i<=n;i++)
        ans=max(ans,g[i]);

    OK,那么希望你们通过这道例题,对换根DP有了一个初步的了解。

    其实换根法的思想就是通过差值来更新答案,而不必一遍遍枚举计算重复信息。

    那么,我们中篇再见(咕咕咕)。

  • 相关阅读:
    Python 列表浅拷贝与深拷贝
    Linux 基本命令-----常用操作分类
    硬盘的分区方式
    github中fork的使用
    大O记号
    python的__file__和__name__变量
    python生成器
    python装饰器
    re模块元字符
    python_数据类型_list
  • 原文地址:https://www.cnblogs.com/light-house/p/11827497.html
Copyright © 2011-2022 走看看