算法复习——树形dp
树形dp的状态转移分为两种,一种为从子节点到父节点,一种为父节点到子节点,下面主要讨论子节点到父亲节点的情况:
例题1(战略游戏):
这是一道典型的由子节点状态转移到父节点的问题,而且兄弟节点之间没有相互影响,我们用f[i][0]/f[i][1]表示i不取/要取时其所在子树总共最少取的节点数,不难得出dp方程:
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<string>
#include<algorithm>
#include<cstdio>
#include<cstdlib>
#include<vector>
using namespace std;
const int N=;
int first[N],next[N*],go[N*],tot,n,father[N],f[N][];
vector<int>son[N];
inline int R()
{
char c;int f=;
for(c=getchar();c<''||c>'';c=getchar());
for(;c<=''&&c>='';c=getchar())
f=(f<<)+(f<<)+c-'';
return f;
}
inline void comb(int a,int b)
{
next[++tot]=first[a],first[a]=tot,go[tot]=b;
next[++tot]=first[b],first[b]=tot,go[tot]=a;
}
inline void dfs(int u,int fa)
{
for(int e=first[u];e;e=next[e])
{
int v=go[e];
if(v==fa) continue;
father[v]=u;
son[u].push_back(v);
dfs(v,u);
}
}
inline void dp(int u)
{
int temp=1e+;
if(!son[u].size())
{
f[u][]=;f[u][]=;
return;
}
for(int i=;i<son[u].size();i++)
{
dp(son[u][i]);
f[u][]+=f[son[u][i]][];
f[u][]+=min(f[son[u][i]][],f[son[u][i]][]);
}
f[u][]++;
}
int main()
{
//freopen("a.in","r",stdin);
int a,b,c;
n=R();
for(int i=;i<=n;i++)
{
a=R(),b=R();
for(int j=;j<=b;j++)
c=R(),comb(a,c);
}
dfs(,);dp();
int ans=min(f[][],f[][]);
cout<<ans<<endl;
return ;
}
通过这道题我们也可以看出,如果兄弟节点间并不存在相互影响制约的关系,dp的第二维通常是非常小的
那么当兄弟节点间会相互影响时,又该怎么办,我们先讨论较为简单的二叉树的情况:
例题2(二叉苹果树):
首先将保留树枝数量转化成保留节点数量+1,(不转化也可以),然后每个树枝上的苹果数量设为相应节点的点权,我们发现,如果用f[i][k]表示i所在子树总共留下了k个节点,由于儿子节点只有两个,我们就可以枚举左儿子保留的节点数量x,则右儿子即为k-x-1,推出dp方程:
f[i][k]=max{f[lson][x]+f[rson][k-x-1],f[i][k]},0<=x<k
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<cctype>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int N=;
int first[N],next[N*],go[N*],val[N*],tot;
int n,m;
int dp[N][N];
inline void comb(int a,int b,int c)
{
next[++tot]=first[a],first[a]=tot,go[tot]=b,val[tot]=c;
next[++tot]=first[b],first[b]=tot,go[tot]=a,val[tot]=c;
}
inline void dfs(int u,int fa,int Val,int k)
{
if(u==||k==)
{
dp[u][k]=;
return;
}
if(dp[u][k]!=-)
return;
dp[u][k]=;
for(int i=;i<k;i++)
{
int l=,r=,vl,vr;
for(int e=first[u];e;e=next[e])
{
int v=go[e];
if(v==fa) continue;
if(l==)
l=v,vl=val[e];
else
{
r=v,vr=val[e];
break;
}
}
dfs(l,u,vl,i);
dfs(r,u,vr,k-i-);
dp[u][k]=max(dp[l][i]+dp[r][k-i-]+Val,dp[u][k]);
}
return;
}
int main()
{
//freopen("a.in","r",stdin);
scanf("%d%d",&n,&m);
m++;
memset(dp,-,sizeof(dp));
int a,b,c;
for(int i=;i<n;i++)
{
scanf("%d%d%d",&a,&b,&c);
comb(a,b,c);
}
dfs(,,,m);
cout<<dp[][m]<<endl;
return ;
}
通过这道题,我们可以发现,如果兄弟节点间存在着相互影响的情况,那么常常会在dfs之前枚举儿子状态,保证儿子的状态不冲突,由于枚举会出现重复的情况,我们也常常会用到记忆化搜索
上面只是两个儿子的情况,如果面对多个儿子,上面的方法显然是不可取的
例题3(选课):
根据题意由先修课向它的课程连边,建成一颗树,树的点权对应课程学分
此时我们必须将多叉树转化为二叉树:把第一个孩子作为父节点的左子树,其它孩子作为第一个孩子的右子树。
具体实现为:每次读入一个节点i的儿子j,我们将son[i]=j,而将brother[j]设为之前的son[i],也就是离j最近的兄弟节点。
设f[u][k]为新建的二叉树的后,节点u所在子树保留k个儿子能得到得最多学分,可以得出dp方程:
f[u][k]=max(f[u][k],f[son[u]][i]+dp[brother[u]][k-i-1]+val[u]),0<=i<k;
其中val表示该节点对应课程的的学分
由实际意义不难发现,我们这样枚举少考虑了一个情况,也就是当前u节点所对应的原来的图的子树的节点都不取的情况,
因此在最后我们还要这样:
f[u][k]=max(f[u][k],f[brother[u]][k])
多叉树转二叉树的方法是面对分配附加维时常用的一种方法
例题4(偷天换日,洛谷3360):
首先要考虑的是这道题的建图,如果根据题意新建一个图是在太麻烦,因此我们可以直接考虑边读入信息边再已知的树上dfs,(详细见代码)
用f[i][j]表示已i为节点的子树(包含i与其父亲连接的边)中花费j的时间能获得的最大价值
当我们dfs到展览室的时候,f[i][j]的计算无疑是一个01背包问题,在此不多叙述
其他情况我们可以发现,除去末端展览室的情况,树的其他部分都是一颗二叉树,因此直接用我们上面提到的方法即可:
f[x][i]=max(f[x][i],f[l][j]+f[r][i-j-dis]);dis<=i<=n,j<=i-dis;
其中dis表示i与其父节点连接的边的权值;
代码如下:(引用洛谷题解)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define ll long long
ll n,m=;
ll f[][]={};
ll value[],tim[];
inline void sca(ll x)
{
ll dis,pd;
scanf("%lld%lld",&dis,&pd);
dis*=;
if(pd)
{
for(ll j=;j<=pd;j++)
scanf("%lld%lld",&value[j],&tim[j]);
for(ll i=;i<=pd;i++)
for(ll j=n;j>=tim[i];j--)
if(j-tim[i]>=dis)
f[x][j]=max(f[x][j],f[x][j-tim[i]]+value[i]);
return;
}
ll l=++m;sca(m);
ll r=++m;sca(m);
for(ll i=dis;i<=n;i++)
for(ll j=;j<=i-dis;j++)f[x][i]=max(f[x][i],f[l][j]+f[r][i-j-dis]);
}
int main()
{
scanf("%lld",&n);n--;
sca();
printf("%lld",f[][n]);
return ;
}
例题5(骑士,bzoj1040):
首先想到的是如果按照痛恨关系建边的话有两种情况:有一个环或者为一颗树,为树的话就和例题1一样了,十分简单,因此主要考虑有环的情况.
为了让环更好处理,我们选择从被痛恨者向痛恨他的人连一条边····这样整个图即为奇环外向树(只有一个环且没有环外的点指向环内的点的图)
这样的话我们可以找环上任意一点u让它与它的父节点father[u]断开,然后以它为根进行dp,dp方程与例题1一样。
然而这样会出现冲突,因为最优答案有可能是由f[father[u]][1]更新过来的,因此我们要将环上的的点单独处理
首先直接将f[father[u]][1]赋值为f[father[u]][0]这样的话可以保证u取了而father[u]一定没有取,然后依次往father[father[u]],father[father[father[u]]]·····跳,即重新遍历整个环,依次更新遍历的点的f[][0],f[][1],直到跳到u为止
最后只更新f[u][1]就好了,ans加上max{f[u][1],f[u][0]}.
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<cctype>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int N=1e6+;
int first[N],next[N],go[N],tot,n,val[N],father[N],g[N];
long long ans=,f[N][];
bool visit[N];
inline int R()
{
char c;int f=;
for(c=getchar();c<''||c>'';c=getchar());
for(;c<=''&&c>='';c=getchar())
f=(f<<)+(f<<)+c-'';
return f;
}
inline void comb(int a,int b)
{
next[++tot]=first[a],first[a]=tot,go[tot]=b;
}
inline void dfs(int u)
{
visit[u]=true;f[u][]=val[u];
for(int e=first[u];e;e=next[e])
{
int v=go[e];
if(!visit[v])
{
dfs(v);
f[u][]+=max(f[v][],f[v][]);
f[u][]+=f[v][];
}
}
}
inline void dp(int a)
{
int root;
for(root=a;g[root]!=a;root=father[root])
g[root]=a;
dfs(root);
int b=father[root];
f[b][]=f[b][];
for(int x=father[b];x!=root;x=father[x])
{
f[x][]=val[x],f[x][]=;
for(int e=first[x];e;e=next[e])
{
int v=go[e];
f[x][]+=max(f[v][],f[v][]);
f[x][]+=f[v][];
}
}
f[root][]=val[root];
for(int e=first[root];e;e=next[e])
{
int v=go[e];
f[root][]+=f[v][];
}
ans+=max(f[root][],f[root][]);
}
int main()
{
//freopen("a.in","r",stdin);
n=R();int a,b;
for(int i=;i<=n;i++)
{
a=R(),b=R();
val[i]=a,comb(b,i);
father[i]=b;
}
for(int i=;i<=n;i++)
if(!visit[i])
dp(i);
printf("%lld",ans);
return ;
}
例题6(bzoj1023仙人掌)
Description
如果某个无向连通图的任意一条边至多只出现在一条简单回路(simple cycle)里,我们就称这张图为仙人掌
图(cactus)。所谓简单回路就是指在图上不重复经过任何一个顶点的回路。
举例来说,上面的第一个例子是一张仙人图,而第二个不是——注意到它有三条简单回路:(4,3,2,1,6
,5,4)、(7,8,9,10,2,3,7)以及(4,3,7,8,9,10,2,1,6,5,4),而(2,3)同时出现在前两
个的简单回路里。另外,第三张图也不是仙人图,因为它并不是连通图。显然,仙人图上的每条边,或者是这张仙
人图的桥(bridge),或者在且仅在一个简单回路里,两者必居其一。定义在图上两点之间的距离为这两点之间最
短路径的距离。定义一个图的直径为这张图相距最远的两个点的距离。现在我们假定仙人图的每条边的权值都是1
,你的任务是求出给定的仙人图的直径。
Input
输入的第一行包括两个整数n和m(1≤n≤50000以及0≤m≤10000)。其中n代表顶点个数,我们约定图中的顶
点将从1到n编号。接下来一共有m行。代表m条路径。每行的开始有一个整数k(2≤k≤1000),代表在这条路径上
的顶点个数。接下来是k个1到n之间的整数,分别对应了一个顶点,相邻的顶点表示存在一条连接这两个顶点的边
。一条路径上可能通过一个顶点好几次,比如对于第一个样例,第一条路径从3经过8,又从8返回到了3,但是我们
保证所有的边都会出现在某条路径上,而且不会重复出现在两条路径上,或者在一条路径上出现两次。
Output
只需输出一个数,这个数表示仙人图的直径长度。
Sample Input
9 1 2 3 4 5 6 7 8 3
7 2 9 10 11 12 13 10
5 2 14 9 15 10 8
10 1
10 1 2 3 4 5 6 7 8 9 10
Sample Output
9
HINT
对第一个样例的说明:如图,6号点和12号点的最短路径长度为8,所以这张图的直径为8。
这道题不得不说是真tm难啊····
题解详见:http://z55250825.blog.163.com/blog/static/150230809201412793151890/
说说我理解的吧····先进行一遍dfs造一颗树用f[i]表示从i往深处的链能到达的最大距离,注意是链,不是环·····,那么用这个尝试更新答案,(用u的儿子的f的次大值和最大值的和+1来更新)
然后我们要讨论的就是如何处理环上的情况····
对于环的话我们找到环上深度最高的点u,我们要按照f的定义求出f[u],(这里有点难想,设i为环上除u外的点,那么我们可以推出f[u]=max{f[i]+dis(u,i)},于是以后dfs到环上的话答案一定由f[u]更新而来
接下来我们就要解决如何算f[u],并且在更新答案时我们还忽略了环上的点可以更新答案,打个比方,如果环上有两个点i,j,那么答案可能由f[i]+f[j]+dis(i,j)更新而来·····
我们要先提取出这个环,然后利用单调队列来求这个东西····具体实现看代码吧,···大概有点类似于烽火传递····队列首与队列尾的距离不能大于环长度的一半(不然就不是最短距离),然后还要保持一个单调性···
代码:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<cctype>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
const int N=5e4+;
int n,m,k,f[N],low[N],dfn[N],cnt,deep[N],father[N],tol;
int first[N],go[N*],next[N*],tot,ans=,queue[N*],a[N*];
inline int R()
{
char c;int f=;
for(c=getchar();c<''||c>'';c=getchar());
for(;c<=''&&c>='';c=getchar())
f=(f<<)+(f<<)+c-'';
return f;
}
void comb(int a,int b)
{
next[++tot]=first[a],first[a]=tot,go[tot]=b;
next[++tot]=first[b],first[b]=tot,go[tot]=a;
}
void init()
{
n=R(),m=R();int a,b;
while(m--)
{
k=R();a=R();
for(int i=;i<k;i++) b=R(),comb(a,b),a=b;
}
}
void dp(int root,int u)
{
int tail=,head=;tol=deep[u]-deep[root]+;
for(int i=u;i!=root;i=father[i])
a[tol--]=f[i];
a[tol]=f[root],tol=deep[u]-deep[root]+;
for(int i=tol+;i<=tol*;i++)
a[i]=a[i-tol];
queue[head]=;
for(int i=;i<=tol+tol/;i++)
{
while(head<=tail&&queue[head]<i-tol/) head++;
ans=max(ans,a[queue[head]]+a[i]+i-queue[head]);
while(head<=tail&&a[queue[tail]]-queue[tail]<=a[i]-i) tail--;
queue[++tail]=i;
}
for(int i=;i<=tol;i++)
f[root]=max(f[root],a[i]+min(i-,tol-i+));
}
void tarjan(int u)
{
low[u]=dfn[u]=++cnt;
for(int e=first[u];e;e=next[e])
{
int v=go[e];
if(father[u]==v) continue;
if(!dfn[v]) father[v]=u,deep[v]=deep[u]+,tarjan(v);
low[u]=min(low[v],low[u]);
if(dfn[u]<low[v]) ans=max(ans,f[u]+f[v]+),f[u]=max(f[u],f[v]+);
}
for(int e=first[u];e;e=next[e])
{
int v=go[e];
if(father[v]!=u&&dfn[u]<dfn[v])
dp(u,v);
}
}
int main()
{
//freopen("a.in","r",stdin);
init();
tarjan();
printf("%d\n",ans);
return ;
}
算法复习——树形dp的更多相关文章
- 算法复习——数位dp
开头由于不知道讲啥依然搬讲义 对于引入的这个问题,讲义里已经很清楚了,我更喜欢用那个建树的理解···· 相当于先预处理f,然后从起点开始在树上走··记录目前已经找到了多少个满足题意的数k,如果枚举到第 ...
- 算法复习——区间dp
感觉对区间dp也不好说些什么直接照搬讲义了2333 例题: 1.引水入城(洛谷1514) 这道题先开始看不出来到底和区间dp有什么卵关系···· 首先肯定是bfs暴力判一判可以覆盖到哪些城市····无 ...
- 算法复习——背包dp
1.01背包 二维递推式子: 代码: ;i<=n;i++) ;x--) ][x-w[i]]+c[i],f[i-][x]); ][x]; printf("%d",f[n][m] ...
- 算法复习——数位dp(不要62HUD2089)
题目 题目描述 杭州人称那些傻乎乎粘嗒嗒的人为 62(音:laoer). 杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司 ...
- 树形dp技巧,多叉树转二叉树
今天复习树形dp时发现一道比较古老的题,叫选课,是树形dp的一道基础题,也是多叉树转二叉树应用的模版题 多叉树转二叉树的应用非常广泛,因为如果一个节点的儿子太多,一个一个存下来不方便去查询,并且会增加 ...
- HDU4612(Warm up)2013多校2-图的边双连通问题(Tarjan算法+树形DP)
/** 题目大意: 给你一个无向连通图,问加上一条边后得到的图的最少的割边数; 算法思想: 图的边双连通Tarjan算法+树形DP; 即通过Tarjan算法对边双连通缩图,构成一棵树,然后用树形DP求 ...
- 算法提高 金属采集_树形dp
算法提高 金属采集 时间限制:1.0s 内存限制:256.0MB 问题描述 人类在火星上发现了一种新的金属!这些金属分布在一些奇怪的地方,不妨叫它节点好了.一些节点之间有道路相连 ...
- 蓝桥杯 算法提高 金属采集 [ 树形dp 经典 ]
传送门 算法提高 金属采集 时间限制:1.0s 内存限制:256.0MB 锦囊1 锦囊2 锦囊3 问题描述 人类在火星上发现了一种新的金属!这些金属分布在一些奇怪的地方,不妨叫 ...
- 算法进阶面试题05——树形dp解决步骤、返回最大搜索二叉子树的大小、二叉树最远两节点的距离、晚会最大活跃度、手撕缓存结构LRU
接着第四课的内容,加入部分第五课的内容,主要介绍树形dp和LRU 第一题: 给定一棵二叉树的头节点head,请返回最大搜索二叉子树的大小 二叉树的套路 统一处理逻辑:假设以每个节点为头的这棵树,他的最 ...
随机推荐
- Windows环境下使用Apache+mod
1.安装Python和Apache. 2.安装mod_wsgi后获得wsgi.so,并将wsgi.so放到Apache的modules文件夹下. 3.安装webpy. 4.打开httpd.conf(在 ...
- 绘制方式和OpenGL枚举对应关系
绘制方式和OpenGL枚举对应关系 图元类型 OpenGL枚举量 点 GL_POINTS 线 GL_LINES 条带线 GL_LINE_STRIP 循环线 GL_LINE_LOOP 独立三角形 GL_ ...
- 最大长度回文子串(Manacher's algorithm)
输出最大长度的回文子串. string longestPalindrome(string s) { int id, mx, i, j, len, maxlen; vector<char> ...
- 字符串 -----JavaScript
本文摘要:http://www.liaoxuefeng.com/ JavaScript的字符串就是用''或""括起来的字符表示. 如果'本身也是一个字符,那就可以用"&q ...
- Electron的介绍
1.1 Electron是什么? 引用官网的一句话: Build cross platform desktop apps with JavaScript, HTML, and CSS 1.2 诞生 技 ...
- 关于cocos2dx for lua资源加载优化方案
之前我写游戏加载都是从一个json文件写入要加载的文件名来实现加载,但是如果资源 比较多的情况下,会导致非常难管理,需要逐个写入.所以换了另外一种方式来加载文件. 首先,我是通过场景之前的切换时候,加 ...
- GCD之dispatch queue
GCD之dispatch queue iOS中多线程编程工具主要有: NSThread NSOperation GCD 这三种方法都简单易用,各有千秋.但无疑GCD是最有诱惑力的,因为其本身是appl ...
- 基于Centos7.2使用Cobbler工具定制化批量安装Centos7.2系统
1.1 定制Centos_7_x86_64.ks文件内容 # Cobbler for Kickstart Configurator for CentOS 7.2.1511 by Wolf_Dre ...
- pandas处理较大数据量级的方法 - chunk,hdf,pkl
前情提要: 工作原因需要处理一批约30G左右的CSV数据,数据量级不需要hadoop的使用,同时由于办公的本本内存较低的缘故,需要解读取数据时内存不足的原因. 操作流程: 方法与方式:首先是读取数据, ...
- python GIL锁、进程池与线程池、同步异步
一.GIL全局解释器锁 全局解释器锁 在CPython中,全局解释器锁(GIL)是一个互斥锁,它可以防止多个本机线程同时执行Python代码.之所以需要这个锁,主要是因为CPython的内存管理不是线 ...