树状dp就是在树上的动态规划,树状dp的特殊性:无环,dfs不会重复,具有明显严格的层数关系。
大神的好文:https://blog.youkuaiyun.com/txl199106/article/details/45373507
大神的好文:https://blog.youkuaiyun.com/txl199106/article/details/45372337
什么时候用树状dp?
- 判断题目描述的数据结构是否是一棵树,是否满足动态规划的要求。是的话就开始入手,不是的话换方法。
什么题目满足动态规划的要求?
子问题最优解也是全局最优解,满足无后效性,多阶段决策问题。把问题首先分解为若干个子问题,在每一个子问题做 出决 策,动态规划就是解决多阶段决策最优解的一种方法。
阶段:
将所给问题的过程,按时间或空间(树归中是空间,即层数)特征分解成若干相互联系的阶段,以便按次序去求每阶 段的解。
状态:
各阶段开始时的客观条件叫做状态。
决策:
当各段的状态取定以后,就可以做出不同的决定,从而确定下一阶段的状态,这种决定称为决策。 (即孩子节点和父亲节点的关系)
策略:由开始到终点的全过程中,由每段决策组成的决策序列称为全过程策略,简称策略。
状态转移方程:
前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由k阶段到k+1阶 段 (在树中是孩子节点和父亲节点)状态的演变规律,称为状态转移方程。
目标函数与最优化概念:
目标函数是衡量多阶段决策过程优劣的准则。最优化概念是在一定条件下找到一个途径,经过按题目具体性质所确定的运算以后,使全过程的总效益达到最优。
- 树的存储方式:如果节点数目小于5000,用邻接矩阵存储树,如果更大,则用邻接表来存储,邻接表的存储一般情况下使用vector数组来存储。如果是二叉树或者是需要多叉转二叉,可以用两个一维数组来存储。
- 写出状态dp方程:两种:a.根到叶子 b.叶子到根,根的子节点传递有用的信息给根,得出最优解的一个过程。
树的特点及性质:
- 有n个点,n-1条边的无向图,任意两个顶点之间可达。
- 无向图中任意两个点间有且只有一条路。
- 一个点至多有一个前驱,但可以有多个后继。
- 无向图无环。
Anniversary party
首先理解题意: 如何选择使得最终得到的欢乐值最大?题目要求:下属和他的直接领导不能被一起选择。下面看一下样例:n,下面输入n行,每行代表一个人对应的欢乐值,再下面输入n-1行代表对应的关系,并且以0 0输入结束。
思路:按照大神给的步骤:首先这道题是否是数据结构对应树的关系,根据主管关系可以判断满足对应树的关系。是否是动态规划问题?
满足动态规划要求,最大欢乐值,无后效性,最优解,有子问题。接下来分析状态,每个状态都有选择和不选择两种状态,任何一个点的取舍都有两种状态,dp[i][0]表示第i个人不选择所获得的最大欢乐值,dp[i][1]表示第i个人选择所获得的最大欢乐值,
dp[node][0]+=max(dp[i][0],dp[i][1]),dp[node][1]+=dp[i][0];存储方式:节点数目多余5000个,最好采用邻接表存储,建议使用vector存储。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=6001;
int n;
int dp[maxn][3];
//dp[i][0]表示第i个人不选择所获得的最大欢乐值
//dp[i][1]表示第i个人选择所获得的最大欢乐值
int father[maxn];
int vis[maxn];
void dfs(int node)
{
vis[node]=1;
for(int i=1;i<=n;i++)
{
if(!vis[i]&&father[i]==node)//i是node的儿子
{
dfs(i);
dp[node][0]+=max(dp[i][0],dp[i][1]);
dp[node][1]+=dp[i][0];
}
}
}
int main()
{
while(cin>>n)
{
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)
{
cin>>dp[i][1];
dp[i][0]=0;
}
int son,fath,root;
bool beg=1;
root=0;
while(cin>>son>>fath,son||fath)
{
father[son]=fath;
if(root==beg||son)
{
root=fath;
}
}
while(father[root])//查找父亲节点
root=father[root];
dfs(root);
int ans=max(dp[root][0],dp[root][1]);
printf("%d\n",ans);
}
return 0;
}
Strategic game POJ - 1463
其实这道题和上一道题目类型很像,动态规划方程都很像,原理也基本一样,只是方法,用的是vector方法。
思路,是否满足动态规划的要求,是否是树状结构,接下来考虑存储方式,不论是vector类似于邻接表还是用一维数组代替都可以。接下来考虑动态规划方程,同样的也是有两种状态,dp[node][0]表示以node为根在node上不放置士兵所需的最小士兵数目,dp[node][1]表示以node为根在node上放置士兵所需的最小士兵数目。i是node的儿子,dp[node][0]+=dp[i][1],父亲上不放置,儿子上一定要放置。注意看清题意,是树的点覆盖问题。dp[node][1]+=min(dp[i][0],dp[i][1])。我觉得主要难的是处理结点以及边的一些细节问题。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int maxn=1501;
vector<int>mp[maxn];
ll dp[maxn][3];
ll father[maxn];
ll vis[maxn];
ll n;
void dfs(ll node)
{
dp[node][0]=0;
dp[node][1]=1;
//vis[node]=1;
// cout<<1<<endl;
for(ll i=0;i<mp[node].size();i++)
{
int v=mp[node][i];//node是i的父亲
//if(!vis[i])
//{
dfs(v);
//不放置的话儿子一定要放
dp[node][0]+=dp[v][1];
dp[node][1]+=min(dp[v][0],dp[v][1]);
//以node为根在node上放置士兵所需的最小士兵数目
//}
}
}
int main()
{
int n;
while(cin>>n)
{
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
memset(father,-1,sizeof(father));
int fa,son,edge;
for(int i=0;i<n;i++)
{
scanf("%d:(%d)",&fa,&edge);
mp[fa].clear();
while(edge--)
{
scanf("%d",&son);
mp[fa].push_back(son);
father[son]=fa;
}
}
// cout<<1<<endl;
int root=1;
while(father[root]!=-1)
root=father[root];
//cout<<root<<endl;
dfs(root);
// cout<<root<<endl;
ll ans=min(dp[root][0],dp[root][1]);
// cout<<dp[root][0]<<"\t"<<dp[root][1];
cout<<ans<<endl;
}
return 0;
}