树形DP和状压DP和背包DP
树形(DP)和状压(DP)虽然在(NOIp)中考的不多,但是仍然是一个比较常用的算法,因此学好这两个(DP)也是很重要的。而背包(DP)虽然以前考的次数挺多的,但是现在基本上已经成了人人都能AK的题了,所以也不经常考了。
树形DP
树形DP这个非常特殊,他好像和是唯一一个用深搜实现的DP,所以我们学好它也是应该的,其特点是通过深搜。
思路
- 先找到一个根节点,然后预处理出所有子树的大小。
- 然后深搜把最底层的子节点得状态处理出来。
- 递归回溯到根节点,在回溯的时候完成状态转移。最后输出根节点的值。
(Example)
(LuoguP2014)(选课)这个题,就是一个比较典型的树形DP,顺便还考察了一下分组背包,
首先我们分析一下状态我们可以设(dp[i][j])表示以(i)的子树中选(j)个(包括(i)自己)所得到的最大学分,因此我们可以采用树形(DP)的方法。
首先预处理出(dp[i][1])表示只选(i)自己所得到的学分,然后我们可以进行状态转移,因为题目给的输入满足(0)一定是唯一的根节点,所以最后我们只要输出(dp[0][m])首先我们要得到状态转移方程,然后仔细,细心的考虑边界条件,这也是一般DP的思路,首先我们可以得出方程:
(dp[i][j] = max(dp[i][t] + dp[son(i)][j - t])(jin[1, ~m],tin [1,j]]且son(i)要全枚举一遍)),此时我们还要处理比较棘手的问题,就是转移的顺序,
首先这是一个01背包所以我们要(for(j = m; j >= 1; j--))而t的范围也是一个坑点,因为在推到t时,我们首先要满足比t小的一定要枚举出来,所以我们就要(for(t = 1; t <= j; t++))
这个题基本上就解决了,代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <vector>
using namespace std;
vector <int> son[100010];
int s[100010], out_degree[100010], in_degree[100010];
int dp[1000][1000];//表示i的子树选j个课程
int n, m;
inline void dfs(int a)
{
for (int i = 0; i < son[a].size(); i++)
{
int to = son[a][i];
dfs(to);
for (int j = m + 1; j >= 1; j--) //总共选m个加上0,就是m + 1个
for (int t = j; t >= 1; t--)
dp[a][j] = max(dp[a][j], dp[a][j - t] + dp[to][t]);
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
{
int a, b;
scanf("%d%d", &a, &b);
son[a].push_back(i);
dp[i][1] = b;
dp[i][0] = 0;
}
dfs(0);
printf("%d", dp[0][m + 1]);
}
/*
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
*/
状压DP
状压(DP)就更神奇了,可以说是最有(OI)特点的一个(DP),因为它用到了位运算和二进制。因此像那些一个区间只有选或不选的操作的那些状态可以用二进制表示,然后在用一些不同寻常的东西,例如左移右移使可以状态转移。而判断是否是状压DP时可以查看数据范围,如果在(20)以内就可以使用状压(DP)。
思路
-
跟其他的(DP)一样,我们首先还是要寻找状态,但是这个状态可能比较难搞,所以我们就需要压缩一下,也可以删除一些没有用的东西,且需要满足这些状态都很相似,且数量很多,此时就可以把它当成状态了。(总的来说,是先压缩状态,然后寻找合理状态)
-
在压缩的时候需要运用位运算的一些操作
-
当然一些运算的顺序也要记清楚,防止调试时间太长还找不到什么错误,
位反(~ ) > 算术 > 位左移、位右移 > 关系运算 > 与 > 或 > 异或 > 逻辑运算
(Example)
(LuoguP1879)也是一个经典题,也经常被拿来用作写状压DP的入门题,当然愤怒的小鸟也是一个只要写过几个状压DP的就都能写出来的算法,因此也可以做一做,
我们分析一下玉米田这道题,题目让我们求总共有多少种方案数,而且数据范围还很小,这就在暗示我们采用状压(DP)的方法和套路。
预处理
首先应该压缩状态,且基本上状压(DP)压缩都是一样的方法
压缩状态+预处理代码:
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &flag[i][j]);//判断土地是否肥沃
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
now[i] = (now[i] << 1) + flag[i][j];//压缩状态,状压DP的一般思路
寻找合理状态
题目要求要满足三个条件,即左右不能相邻,上下不能相邻,土地不能荒芜,为了方便,我们先使其满足左右不能相邻。
满足这个条件的前提是
(!((i) & ((i) <<1))) && (!((i) & ((i)>>1)))
就是该状态左移和右移一位并与该状态(and)的结果是零,即并没有左移一位后的某一位与左移一位前的某一位相同,因为如果相同的话,就说明左右相邻
寻找合理状态代码:
for (int i = 0; i < (1 << m); i++)
if ( (!(i & (i << 1))) && (!(i & (i >> 1))) )
check[i] = 1;//说明此状态可行
状态转移
以上都是一行的预处理,现在我们要跨过这个界限,开始多行的转移了,在进行多行的转移的时候,就需要判断第二个条件,就是上下之间不能有相邻的状态。
满足这个条件的前提是
! ((last) & (now))
还需要判断第三个条件,不能有土地是荒芜的,这个也很好判断,因为我们已经预处理出每一行的最难的满足条件(now[i]), 如果(一个状态 & (now[i])) == 该状态,说明此状态一定满足不荒芜,到此所有的条件都已经分析完毕,如果不懂可以手糊。
那就可以状态转移了,状压就成为了普通的二维DP了。
状态转移代码:
for (int i = 1; i <= n; i++)
for (int j = 0; j < (1 << m); j++)
if (check[j] && (now[i] & j) == j)//判断此状态可不可行,且不能有荒芜的土地,
for (int k = 0; k < (1 << m); k++)
if (!(k & j) && check[k])//上下不能有相邻的地方,比如如果上:10010,下:01100,那他们and的结果就不为0
dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod;