前面
关于算法这一块,怎么说呢,目前为止我还是比较盲目的在OJ上面A水题,但即便如此AC率也很低——这到底还是不行的,不过终究是给自己探了个底子——在仅有基础概念的RAW状态下能走多远。
就目前来看,很多时候问题出在自己见得少,给不出思路。对于抽象思维要求不高的题目,知道怎么去做之后多数情况还下是能够拿到TLE(并非纯暴搜导致)或者AC的——当然TLE也不是算过,终究还是代码技巧不足;而对于需要有较高抽象能力的题目,或是比较Tricky的题目,则力有不逮——一个是因为见得少而不知道怎么下手,再就是抽象思维能力不过关。
因此,就算法这块,现在开始进行专项练习吧。咱对自己的要求不高,也不指望能有ACM的程度,但至少买的那三本书:《算法竞赛入门经典》、《算法竞赛入门经典训练指南》以及《挑战程序设计竞赛》不能白瞎了。
由于不知道自己对《刷油漆》那道题的题解理解正确与否,文中对那道题目的讲解我是没有信心的,虽然最终代码是AC了……还是要多做多体会。
正文
今次就看三个问题吧,从老生常谈的背包问题开始,到树形DP的简单介绍。
这里先推荐知乎上面关于DP的回答看一看,两个高票答案已经将DP的核心问题讲得很明白了:就是“如何定义状态”以“如何控制状态的转移”——状态转移方程集中体现此两者,至于如何求解(递推或者是记忆化搜索)都只是求解方程的手段而已。
在谨记上面的条款之后,下面开始看3个题目,以协助理解。
0-1背包问题
From:HiHoCoder,HiHo一下第6周。
老题,网上有很多讲解的,包括状态方程怎么来的一应俱全,这里不去赘述,直接给AC的代码:
1 | #include <iostream> |
需要注意一下:关于剩余空间M的For循环是从1开始加一递增的;而在采用记忆化搜索的递归解法中,剩余空间M并非是逐1递减的。这里需要逐步领会如何将尾递归转换为迭代的形式(PS:并非是指用显示Stack来辅助的方法)。
完全背包问题
From: HiHoCoder,HiHo一下第7周。
题解HiHo上面已经有了,这里不赘述。下面的AC代码中并没有进行第二步的优化阶段:
1 | #include <iostream> |
刷油漆(树形DP)
From:HiHoCoder,HiHo一下第12周
虽然HiHo有给出题解,但是还是这里还是先借用《训练指南》P70的例题30的讲解开始吧——至少,对于我这种比较笨的人,HiHo的那个题解后半部分一开始是看不太懂的。
当然例题30并非是《刷油漆》原题,但也是树形DP的应用。
下面是整理的引用,需要注意的是后面两句:
无向无环图,或者称为森林,有多棵树组成,动态规划是解决树上的优化问题的常用工具。
为了能够使用动态规划,这要求:森林中的每个节点都互不相干,以便可以单独优化。
我们只考虑一棵树的情况:首先对这棵树进行DFS,将无根树转化为有根树,然后试着设置,并计算它的状态。
我们来看看单独的一棵树的情况,状态的定义HiHo题解中已经说明了,这里主要说明状态转移是如何发生的,也就是HiHo题解后半部分究竟是怎么回事。
先看给出的由case构造出来的树:
就从以节点4为根的子树开始吧。
显然我们要在这棵子树中选择三个节点,而如果想要对这棵子树进行评估,那么很显然,节点4也必须被选择(于是我们的DFS也就进行了2层了)。
剩下的两个节点只有从节点4的子树中选取了。
类比背包问题,这就相当于背包只剩下两个空间,而每个节点占用一个空间,选取的物品价值就相当于节点的最大估值,但这个值是存在于DP数组中的,并非是给定的值。
那么,对于节点4的每一个子树,都有可能分配0~2个空间。
这里应当注意:由于采用了DFS,我们已经把节点4的所有子树对应的状态都计算好了,因此,可以认为我们是在背包空间为2的情况下选取下面几个值来达到要求的最大值:
1 | DP[8][0], DP[8][1], DP[8][2] |
类比完全背包问题:完全背包问题要求对于每种物品选取K个,来得到最大值。这里可以认为是:对于子节点8,选取0~2个;对于子节点6,选取0~2个;对于子节点7,选取0~2个。
这里需要注意的是,上面关于如何选择的情况均是在自顶向下的情况下说明的——而实际代码中,采用的是自底向上的填充方式。
在完全背包中,状态转移方程以及对应代码为:
1 | DP[i][j] = max{ DP[i][j], DP[i-1][ j-k*w[i] ] + k*v[i] | k = [0...j/w[i]] } |
那么,类比可以写出对于单个节点的DP值计算式:
1 | DP[i][j] = max{ DP[i][j], DP[i][j-k] + DP[l][k] | l = [i的子节点] } |
举个栗子:1
2
3
4
5
6
7
8DP[4][3] = DP[4][2] + DP[6][1]
= max{
(DP[4][1] + DP[8][1]),
“DP[4][1] + DP[6][1],”
(DP[4][1] + DP[7][1])
} + DP[6][1]
// 显然max{...}中的DP[6][1]是不应该重复计算的
注意,完全背包的填充方式是在已经有的结果上再装上k个第i种物品(也就是剩余的空间还足够在第i中物品上做出多个选择);而在当前问题中我们则是在选择了当前子树中的k个节点之后,根节点上还可以再选择多少个其他子树中的节点。
那么尝试写出代码:
1 | DFS(int i, int M) |
注意到M的值是从大到小计算的,这一点可以从前面列举的那个小例子中看出来:M值大的DP值是依赖于M值小的DP值的。而M的值最小只到2,原因在于,对于M=0时肯定为0,M=1时,就是节点本身。
下面是我的AC代码:
另:如果在HiHo一下的比赛排名中查找AC代码的话,会发现有关于edge、vis数组这样的东西,这是因为那些代码是基于无向图的,需要使用vis数组来标记节点以防止出现环的问题。大部分ACM相关的博客上也是此类型的代码,此类代码更具普适性。
1 | #include<iostream> |