[提升性选讲] 树形DP进阶:一类非线性的树形DP问题(例题 BZOJ4403 BZOJ3167)

转载请注明原文地址:http://www.cnblogs.com/LadyLex/p/7337179.html

树形DP是一种在树上进行的DP相对比较难的DP题型.由于状态的定义多种多样,因此解法也五花八门,经常成为高水平考试的考点之一.

在树形DP的问题中,有这样一类问题:其数据范围相对较小,并且状态转移一般与两两节点之间的某些关系有关。

今天,我们就来研究一下这类型的问题,并且总结一种(相对套路的)解决大多数类型题的思路。

首先,我们用一道相对简单的例题来初步了解这个类型题的大致思路,以及一些基本的代码实现

BZOJ 4033: [HAOI2015]树上染色

Time Limit: 10 Sec  Memory Limit: 256 MB

Description

有一棵点数为N的树,树边有边权。给你一个在0~N之内的正整数K,你要在这棵树中选择K个点,将其染成黑色,并

将其他的N-K个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间距离的和的收益。

问收益最大值是多少。

Input

第一行两个整数N,K。

接下来N-1行每行三个正整数fr,to,dis,表示该树中存在一条长度为dis的边(fr,to)。

输入保证所有点之间是联通的。

N<=2000,0<=K<=N

Output

输出一个正整数,表示收益的最大值。

Sample Input

5 2
1 2 3
1 5 1
2 3 1
2 4 2

Sample Output

17
【样例解释】
将点1,2染黑就能获得最大收益。

看完这道题,你有什么想法?一头雾水?

接下来,我们还是按照状态确立,状态转移,代码实习三个步骤来分析这道题,并且得出一些适用性的规律。

状态确立

  首先,我们可以一眼看出,只用诸如"处理完以i为根的子树的最大收益"等一维的状态不能处理这个问题,

  这个时候,我们可以考虑加一维来表示更多的限制条件:设f[i][j]表示"在以i为根的子树中染j个黑色点的最大收益",最终答案即是f[1][k]

状态转移

  其实状态定义蛮好想,但是,怎么状态转移呢?

  由于......数据范围很小,而我们权值的计算又与两两点之间关系有关,因此我们可以考虑枚举点对的暴力做法.

  我们考虑,对于每个点对来说,他们之间的贡献只会在他们的LCA处贡献O(1)的时间复杂度.

  由于一共只有n2数量级的点对,因此我们如果这样做的话算法复杂度是O(n2)的.

  既然这种算法的复杂度是O(n2)的,我们就可以随便转移考虑一种暴力的转移:

  枚举当前考虑的子树中有几个黑点,并考虑合并子树带来的贡献.

  我们考虑,如果我们只统计当前子树内的贡献,显然是不好转移的,因为无法考虑与子树外面点的关系

  所以,我们把子树外面的点与子树内点的贡献也统计在f数组里面,也就是说"外面伸进来的边"也被统计了进来

  这样,由于子树内可以被统计的边的贡献已经被全部统计完,我们就可以通过考虑当前合并的两节点之间的这条边来统计贡献:

  在上图中,子树里面红色边的贡献以及考虑完,现在我们更新的是子树外面的点与子树内的点通过蓝色边贡献的权值.

  设节点rt的子树大小为size[rt],rt原来染色了j个黑点,设节点u的子树大小为size[u],u原来染色了v个黑点,设边权为val

  经过图中的蓝边这条边,u里边的白点与外面的白点产生了v*(k-v)个黑点对.

  同理,里边的白点与外面的白点产生了(size[u]-v])*(n-k-(size[u]-v))个白点对

  那么rt->u这条边总共产生了(v*(k-v)+(size[u]-v])*(n-k-(size[u]-v)))*val的新的贡献.

  这样我们就统计出来了新的贡献,现在以rt为根的子树总贡献是f[rt][j]+f[u][v]+(v*(k-v)+(size[u]-v])*(n-k-(size[u]-v)))*val

  我们用上面这个式子去更新f[rt][j+v]的答案即可.

代码实现

  在代码中,这个算法是O(n2)就变得显而易见了.先给出dp过程的代码,我们开始分析:

 1 void dp(int rt,int fa)
 2 {
 3     f[rt][0]=f[rt][1]=0;size[rt]=1;
 4     for(int i=adj[rt];i;i=s[i].next)
 5     {
 6         int u=s[i].zhong;
 7         if(u!=fa)
 8         {
 9             dp(u,rt);
10             for(LL j=size[rt];~j;j--)
11                 for(int v=0;v<=size[u];v++)
12                 {
13                     LL match_num=(LL)v*(k-v)+(LL)(size[u]-v)*(n-k-(size[u]-v));
14                     f[rt][j+v]=max(f[rt][j+v],f[rt][j]+f[u][v]+(LL)(match_num*s[i].val));
15                 }
16             size[rt]+=size[u];
17         }
18     }
19 }

  就像上面说的,我们考虑把u这棵子树合并到rt里面产生的新贡献.

  值得注意的一点是,我们如果先不合并起来,用刷表法去更新,要比先合并起来用填表法更新快不少.

  这一点带来的优化很明显,因为合并后j循环的次数变多了.

  具体的效率差别...大概是这样(上面那个提交是后合并的打法):

  

  现在,这道题基本就我们解决了.完整代码见下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 typedef long long LL;
 6 const int N=2010;
 7 int n,k,e,adj[N];
 8 struct node{int zhong,val,next;}s[N<<1];
 9 inline void add(int qi,int zhong,int val)
10     {s[++e].zhong=zhong;s[e].next=adj[qi];s[e].val=val;adj[qi]=e;}
11 LL f[N][N],size[N];
12 void dp(int rt,int fa)
13 {
14     f[rt][0]=f[rt][1]=0;size[rt]=1;
15     for(int i=adj[rt];i;i=s[i].next)
16     {
17         int u=s[i].zhong;
18         if(u!=fa)
19         {
20             dp(u,rt);
21             for(LL j=size[rt];~j;j--)
22                 for(int v=0;v<=size[u];v++)
23                 {
24                     LL match_num=(LL)v*(k-v)+(LL)(size[u]-v)*(n-k-(size[u]-v));
25                     f[rt][j+v]=max(f[rt][j+v],f[rt][j]+f[u][v]+(LL)(match_num*s[i].val));
26                 }
27             size[rt]+=size[u];
28         }
29     }
30 }
31 int main()
32 {
33     scanf("%d%d",&n,&k);int a,b,c;
34     memset(f,0xaf,sizeof(f));
35     for(int i=1;i<n;i++)
36         scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,c);
37     dp(1,0);printf("%lld\n",f[1][k]);
38 }

上面这道题还算一道比较简单的树形DP.这道题最大的特点就是那个非线性的O(n2)过程了.

这类非线性的DP一般状态定义和状态转移都比较复杂,但是主要的思想要点是"合并".

如果你发现某个树归问题是与两点间关系有关,那他很可能就是一个这种类型的DP

下面,我们再来看一道题.这道题可就没有上题那么简单了......

BZOJ 3167: [Heoi2013]Sao

Time Limit: 30 Sec  Memory Limit: 256 MB

Description

Welcome to SAO(Strange and Abnormal Online)。这是一个VRMMORPG,含有n个关卡。但是,挑战不同关卡的顺序是一

个很大的问题。有n–1个对于挑战关卡的限制,诸如第i个关卡必须在第j个关卡前挑战,或者完成了第k个关卡才

能挑战第l个关卡。并且,如果不考虑限制的方向性,那么在这n–1个限制的情况下,任何两个关卡都存在某种程

度的关联性。即,我们不能把所有关卡分成两个非空且不相交的子集,使得这两个子集之间没有任何限制。

Input

第一行,一个整数T,表示数据组数。对于每组数据,第一行一个整数n,表示关卡数。接下来n–1行,每行为“i

sign j”,其中0≤i,j≤n–1且i≠j,sign为“<”或者“>”,表示第i个关卡必须在第j个关卡前/后完成。

T≤5,1≤n≤1000

Output

对于每个数据,输出一行一个整数,为攻克关卡的顺序方案个数,mod1,000,000,007输出。

Sample Input

5
10
5 > 8
5 > 6
0 < 1
9 < 4
2 > 5
5 < 9
8 < 1
9 > 3
1 < 7
10
6 > 7
2 > 0
9 < 0
5 > 9
7 > 0
0 > 3
7 < 8
1 < 2
0 < 4
10
2 < 0
1 > 4
0 > 5
9 < 0
9 > 3
1 < 2
4 > 6
9 < 8
7 > 1
10
0 > 9
5 > 6
3 > 6
8 < 7
8 > 4
0 > 6
8 > 5
8 < 2
1 > 8
10
8 < 3
8 < 4
1 > 3
1 < 9
3 < 7
2 < 8
5 > 2
5 < 6
0 < 9

Sample Output

2580
3960
1834
5208
3336

首先,我们可以看出,原题等价于给树上的每个点分配一个权值,并使其满足一些大于&小于关系;

同样,一维的状态无法满足题目的要求.

为了方便处理,我们还是把原图当做一棵树处理.我们可以发现,一个点的子树中有比他大的,也有比他小的.

那么我们不妨再开一维来表示这种限制:设f[i][j]表示在以i为根的子树中有j个比i小的数.

那么状态有了,我们怎么转移呢?

我们可以发现,访问方案的不同与每对点的访问先后顺序有关.因此,我们可以考虑每一对点给最终方案带来的不同影响,

那么在转移的时候依然采用合并子树的思路,假设我们当前要合并rt的子树u,

以rt要求比u大为例:

我们设合并前以rt为根的子树中有i个比rt小,以u为根的子树中有j个比rt小

首先,原来的合法排列就有f[rt][i]种.又由于u比rt小,因此在刚才那j个比rt小的数中有几个比u小是不确定的,每一种方案都有可能出现,因此我们还需要乘上Σf[u][j],j∈[0,j]

接着,这j个比rt小的数插入的位置是不确定的,因此他们所处的位置不同会带来C(i+j)(j)的贡献种数.

同理,剩下size[u]-j个比rt大的数也会带来C(size[rt]+size[u]-i-j-1)(size[u]-j)这么多的贡献.

那么最终我们要更新的数量就是f[rt][i]*(Σf[u][j],j∈[0,j])*C(i+j)(j)*C(size[rt]+size[u]-i-j-1)(size[u]-j)

如果rt比u小那么同理,只不过我们枚举的方式变一下,看有几个数比rt大

如果我们处理f数组的前缀和的话,就可以做到O(n2)的转移啦!

代码见下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <algorithm>
 4 using namespace std;
 5 typedef long long LL;
 6 const int mod=1000000007,N=1010;
 7 int n,adj[N],e;
 8 LL g[N],C[N][N],sum[N][N],size[N],f[N][N];//以i为根的子树,有j个比i小(在i之前访问)的方案数
 9 struct edge{int zhong,next,val;}s[N<<1];
10 inline void add(int qi,int zhong,int val)
11     {s[++e].zhong=zhong;s[e].val=val;s[e].next=adj[qi];adj[qi]=e;}
12 void dfs(int rt,int fa)
13 {
14     size[rt]=f[rt][0]=1;
15     for(int i=adj[rt];i;i=s[i].next)
16     {
17         int u=s[i].zhong;
18         if(u!=fa)
19         {
20             dfs(u,rt);int limit=size[rt]+size[u];
21             for(int i=0;i<limit;i++)g[i]=0;
22             if(s[i].val==1)//rt比u小
23                 for(int j=0;j<size[rt];j++)//已经合并完成的以rt为根节点的子树中有j个比rt大(在rt之前访问)
24                     for(int k=0;k<=size[u];k++)//以u为根节点的子树中有k个比rt大(在rt之后访问)
25                     {
26                         LL tmp1=f[rt][size[rt]-j-1]/*比rt小的size[rt]-j-1的合法方案数*/%mod*(sum[u][size[u]-1]-sum[u][size[u]-k-1]+mod)%mod;
27                             //u里面有k个比rt大的,不一定有几个比u大
28                         LL tmp2=C[j+k][k]*C[limit-j-k-1][size[u]-k]%mod;
29                                //组合数看方案数,前者表示在新的j+k个比rt大的数中新插入的k个数所在的位置
30                             //后者表示比rt小的size-j-k-1个数中u剩下的size[u]-k的排列
31                         g[limit-j-k-1]=(g[limit-j-k-1]+tmp1*tmp2%mod)%mod;
32                         //此时有limit-j-k-1个数比rt小,更新答案
33                     }
34             else//rt比u大(在u之后访问)
35                 for(int j=0;j<size[rt];j++)//以rt为根节点的子树中有j个比rt小
36                     for(int k=0;k<=size[u];k++)//以u为根节点的子树中有k个比rt小
37                     {
38                         LL tmp1=f[rt][j]%mod*sum[u][k-1]%mod;
39                             //u里面有k个比rt小的,不一定几个比u小
40                         LL tmp2=C[j+k][k]*C[limit-j-k-1][size[u]-k]%mod;//和上面组合数的统计类似.
41                         g[j+k]=(g[j+k]+tmp1*tmp2%mod)%mod;
42                     }
43             size[rt]+=size[u];//不断合并每棵子树
44             for(int j=0;j<size[rt];j++)f[rt][j]=g[j];//更新f数组
45         }
46     }
47     sum[rt][0]=f[rt][0];
48     for(int j=1;j<size[rt];j++)sum[rt][j]=(sum[rt][j-1]+f[rt][j])%mod;//全部合并完成,计算合法方案前缀和
49 }
50 int main()
51 {
52     for(int i=0;i<=1000;i++)
53     {
54         C[i][0]=1;
55         for(int j=1;j<=i;j++)
56             C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
57     }
58     int t,a,b;char c[3];scanf("%d",&t);
59     while(t--)
60     {
61         scanf("%d",&n);
62         memset(size,0,sizeof(size));
63         memset(f,0,sizeof(f));
64         memset(sum,0,sizeof(sum));
65         e=0;memset(adj,0,sizeof(adj));
66         for(int i=1;i<n;i++)
67         {
68             scanf("%d%s%d",&a,c,&b),a++,b++;
69             if(c[0]==‘>‘)add(b,a,1),add(a,b,-1);
70             else add(a,b,1),add(b,a,-1);
71         }
72         dfs(1,0);int ans=0;
73         for(int i=0;i<n;i++)
74             ans=(ans+f[1][i])%mod;
75         printf("%d\n",ans);
76     }
77 }

非线性的树形DP是一类很考验DP思维,尤其是DP状态定义能力的问题,这就需要OIer们通过刷题来不断积累做题经验了(其实什么类型题不是呢).希望大家能从我的博文中有所收获:)

时间: 2024-10-21 23:06:10

[提升性选讲] 树形DP进阶:一类非线性的树形DP问题(例题 BZOJ4403 BZOJ3167)的相关文章

[入门向选讲] 插头DP:从零概念到入门 (例题:HDU1693 COGS1283 BZOJ2310 BZOJ2331)

转载请注明原文地址:http://www.cnblogs.com/LadyLex/p/7326874.html 最近搞了一下插头DP的基础知识……这真的是一种很锻炼人的题型…… 每一道题的状态都不一样,并且有不少的分类讨论,让插头DP十分锻炼思维的全面性和严谨性. 下面我们一起来学习插头DP的内容吧! 插头DP主要用来处理一系列基于连通性状态压缩的动态规划问题,处理的具体问题有很多种,并且一般数据规模较小. 由于棋盘有很特殊的结构,使得它可以与“连通性”有很强的联系,因此插头DP最常见的应用要数

树形DP进阶之背包问题

HDU1561--The mre,The better 题意:给定一棵包含n个结点的树,每一个节点附有对应的value,选取其中的m个结点使得总value最大.一个结点被选择的条件是其父节点已经被选择. 解析:1.虚拟出一个总根节点,将深林转化为一棵树. 2.d[r][i]表示在以r为根的子树中选取i个结点所能获得的最大value d[r][i]=max(d[r][i],d[r][i-k]+d[v][k]);      //v为r的一个子节点 //Date: 2015.04.22 //Time:

正睿OI DAY3 杂题选讲

正睿OI DAY3 杂题选讲 CodeChef MSTONES n个点,可以构造7条直线使得每个点都在直线上,找到一条直线使得上面的点最多 随机化算法,check到答案的概率为\(1/49\) \(n\leq k^2\) 暴力 \(n\geq k^2\),找点x,求直线l经过x,且点数最多,点数\(\geq k+1\),递归,否则再找一个 One Point Nine Nine 现在平面上有\(n\)个点,已知有一个常数\(D\). 任意两点的距离要么\(\leq D\),要么\(\geq 1.

2017级算法模拟上机准备篇(序列DP 进阶_1)

进阶版的序列DP 从一道题的优化开始 ModricWang的序列问题 题目描述:给定一个序列,求出这个序列中的最长上升子序列的长度. 这道题的本质还是求解一个最长上升子序列的问题 相对与之前提到过的O(n^2)的算法 我们可以重新整理思路 用O(nlogn)的思路来写,用贪心和二分优化之前的算法 我们设置新的DP数组//dp[i]代表的是当前长度为i的上升子序列的末尾元素的大小 状态转移方程为如果dp[len] < ar[i] 那么就将数ar[i]加到dp数组尾部. 反之,说明可以继续优化,显然

华东交通大学 2019 I 不要666 数位dp进阶

Problem Description 题库链接 666是一个网络用语,用来形容某人或某物很厉害很牛.而在西方,666指魔鬼,撒旦和灵魂,是不吉利的象征.所以邓志聪并不喜欢任何与6有关的数字.什么数字与6有关呢: 满足以下3个条件中的一个,我们就认为这个整数与6有关. 1.这个整数在10进制下某一位是6. 2.这个整数在10进制下的数位和是6的倍数. 3.这个数是6的整数倍. 那么问题来了:邓志聪想知道在一定区间内与6无关的数的和. Input 本题为多组输入,请处理到文件结尾,每行包含两个正整

ACM/ICPC 之 DP进阶(51Nod-1371(填数字))

原题链接:填数字 顺便推荐一下,偶然看到这个OJ,发现社区运营做得很赞,而且交互和编译环境都很赞(可以编译包括Python,Ruby,Js在内的脚本语言,也可以编译新标准的C/C++11,甚至包括Go和C Sharp等),虽然暂时不太火,但估计会逐渐成为国内算法界非常受欢迎的OJ社区. 主页:http://www.51nod.com/index.html 本题是个题意简单的,思路复杂的DP题,说实话,光是想出这种DP就已经非常不易了,即便写出来也要考虑清楚每一种转移的公式和数值关系. 原题:有n

Codeforces Round #614 选讲

http://codeforces.com/contest/1292/problem/C 注意到编号x的边对答案要有贡献,必须和0到x-1的边一起形成一条链,否则x及编号比x大的边都没有贡献.由此,对答案有贡献的边形成了一条链,且这条链的编号是个谷形,即中间编号小,往两边编号变大,编号最大的边在最外侧.由此可以进行dp,dp[u][v]表示如果上述链为点u到点v这条链的答案.令sz[u][v]为以u为根,子树v的大小:fa[u][v]为以u为根,点v的父亲,则有dp[u][v]=dp[v][u]

【转】斜率优化DP和四边形不等式优化DP整理

当dp的状态转移方程dp[i]的状态i需要从前面(0~i-1)个状态找出最优子决策做转移时 我们常常需要双重循环 (一重循环跑状态 i,一重循环跑 i 的所有子状态)这样的时间复杂度是O(N^2)而 斜率优化或者四边形不等式优化后的DP 可以将时间复杂度缩减到O(N) O(N^2)可以优化到O(N) ,O(N^3)可以优化到O(N^2),依次类推 斜率优化DP和四边形不等式优化DP主要的原理就是利用斜率或者四边形不等式等数学方法 在所有要判断的子状态中迅速做出判断,所以这里的优化其实是省去了枚举

(hiho1048)POJ2411Mondriaan&#39;s Dream(DP+状态压缩 or 轮廓DP)

问题: Squares and rectangles fascinated the famous Dutch painter Piet Mondriaan. One night, after producing the drawings in his 'toilet series' (where he had to use his toilet paper to draw on, for all of his paper was filled with squares and rectangle