树状DP
简单入门题
P1352 没有上司的舞会
一、dfs方法
普通dp一般用两个for循环,但是树形dp的基本操作就是将一个结点所有子树的信息合到这个节点上,我们的for循环显然就不能满足这个要求。我们采取dfs的方法。
很容易发现树形dp它为什么一般会是dfs形式?因为树形dp的状态大多是一颗颗子树,它传递状态过程一般都是先求出最下层再往上更新。所以对于每一个点,我们在求解它的值的过程中,需要求出它每一个子节点的解。
#include <iostream>
#include <vector>
using namespace std;
const int MAX=6005;
int h[MAX];
int vis[MAX];
int dp[MAX][2];
vector<int> son[MAX];//son[x][i],值为x的节点的儿子们
void dfs(int x){
dp[x][0]=0;
dp[x][1]=h[x];//x号职员去或不去的初始快乐值
for(int i=0;i<son[x].size();i++){
int s=son[x][i];
dfs(s);
dp[x][0]+=max(dp[s][0],dp[s][1]);//x不去,下属去或不去的最大值
dp[x][1]+=dp[s][0];
}
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>h[i];
}
int x,y;//下属,上司
for(int i=1;i<=n-1;i++){//只有n-1个职员有公司
cin>>x>>y;
son[y].push_back(x);
vis[x]=1;//没有上司的是根节点
}
int root;
for(int i=1;i<=n;i++){
if(!vis[i]){
root=i;
break;
}
}
//判断根节点方法可多种了,但都是判断是否做别人儿子的演化
//是否做别人儿子vis数组、是否有父亲fa【x]!=x、入度是否为0
dfs(root);
cout<<max(dp[root][0],dp[root][1]);
return 0;
}
链式前向星(head数组初始化为0好了,不用特意memset成-1,遍历时终止条件直接i省的~i
#include <iostream>
//#include <vector>
#include<string.h>
using namespace std;
const int MAX=6005;
int h[MAX];
int vis[MAX];
int dp[MAX][2];
//vector<int> son[MAX];//son[x][i],值为x的节点的儿子们
struct edge{
int to;
int next;
// int w;这里无所谓边的权重了
}e[MAX];
int cnt;
int head[MAX];
void add(int u,int v){
e[++cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
void dfs(int x){
dp[x][0]=0;
dp[x][1]=h[x];//x号职员去或不去的初始快乐值
// for(int i=0;i<son[x].size();i++){
// int s=son[x][i];
for(int i=head[x];~i;i=e[i].next){
int s=e[i].to;
dfs(s);
dp[x][0]+=max(dp[s][0],dp[s][1]);//x不去,下属去或不去的最大值
dp[x][1]+=dp[s][0];
}
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>h[i];
}
memset(head,-1,sizeof(head));
int x,y;//下属,上司
for(int i=1;i<=n-1;i++){//只有n-1个职员有公司
cin>>x>>y;
// son[y].push_back(x);
add(y,x);
vis[x]=1;//没有上司的是根节点
}
int root;
for(int i=1;i<=n;i++){
if(!vis[i]){
root=i;
break;
}
}
dfs(root);
cout<<max(dp[root][0],dp[root][1]);
return 0;
}
更周到的解法
洛谷题解页面大佬提供的思路👇
如果我们的人数相当多且是一条链的时候就容易造成爆栈,那这我们有如何解决呢?
方法有三
1、开一个数组手动实现栈。
2、bfs后用for循环
3、拓扑排序
第一个想必大家都会写,而且其与dfs相似,所以不再赘述。
那为啥会讲后两种呢?
因为有时候dfs并不好写,所以我们会把它转化为bfs+for或者拓扑,大家可以看一下,dfs和这两种写法的推导有的是不一样的。特别是和这题的拓扑写法,可以仔细看一下。
二、bfs+for循环(终于有点理解为什么说dfs能做到的bfs都可以做到了)从底部节点开始,节点由孩子信息推出(叶子节点孩子信息为0哪)
我们很容易发现树形dp它为什么一般会是dfs形式?因为树形dp的状态大多是一颗颗子树,它传递状态过程一般都是先求出最下层再往上更新。所以对于每一个点,我们在求解它的值的过程中,需要求出它每一个子节点的解。
那有什么方法我们可以用数组和for循环实现这样的求解呢?没错,就是bfs过程中的队列。由于队列中的点都是先入的父亲节点后入的子节点,所以我们求解的时候只要把循环顺序反过来就可以了。
从根节点自顶而下就需要dfs掌握所有子树信息
那么试想自底而上,先掌握底部节点的信息,可用bfs将节点依据深度依次放入队列,当然了,先进先出,先放的应该是底部节点
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int MAX=6005;
int h[MAX];
vector<int> son[MAX];
int fa[MAX];//以此判断根节点,感觉比vis是否有父亲难一点
int dp[MAX][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
int vis[MAX];//节点是否入队的判断
queue<int> Q;
int que[MAX];//到时候可逆序输出存储在队列中的节点
int cnt;//给que计数,到时候好逆序
void bfs(int r){
Q.push(r);
que[cnt++]=r;
vis[r]=1;
while(!Q.empty()){
int x=Q.front();Q.pop();
for(int i=0;i<son[x].size();i++){
int s=son[x][i];
if(!vis[s]){
Q.push(s);
que[cnt++]=s;
vis[s]=1;
}
}
}
}
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>h[i];
fa[i]=i;
}
int x,y;//下属,上司
for(int i=1;i<=n-1;i++){//只有n-1个职员有公司
cin>>x>>y;
son[y].push_back(x);
fa[x]=y;
}
int r=n;//随便找了个节点r,寻找她的终极祖先 我们的根节点
while(r!=fa[r]){
r=fa[r];//r现在是当初r的父亲 父亲的父亲的父亲直到根节点
}
bfs(r);
for(int i=cnt-1;i>=0;i--){
int x=que[i];
for(int j=0;j<son[x].size();j++){
int s=son[x][j];
dp[x][0]+=max(dp[s][0],dp[s][1]);
dp[x][1]+=dp[s][0];
}
dp[x][1]+=h[x];
}
cout<<max(dp[r][0],dp[r][1]);
return 0;
}
三、拓扑排序(节点fa[x]由节点x信息推出)
先回忆一下拓扑排序
拓扑排序,先将所有入度为0的点入队,列举队列中元素,逐一出队,出队时改变邻接结点的入度,如果邻接结点入读减为0就将其入队
现在从后往前,就看出度是否为0
这次就是真真切切的先存底部叶子节点再依次往上了
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int MAX=6005;
int h[MAX];
vector<int> son[MAX];
int fa[MAX];//以此判断根节点,感觉比vis是否有父亲难一点
int dp[MAX][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
int vis[MAX];//节点是否入队的判断
queue<int> Q;
int degree[MAX];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>h[i];
}
int x,y;//下属,上司
for(int i=1;i<=n-1;i++){//只有n-1个职员有公司
cin>>x>>y;
son[y].push_back(x);
fa[x]=y;
degree[y]++;//出度为0的点(没有孩子的点)
}
for(int i=1;i<=n;i++){
if(degree[i]==0)Q.push(i);
}
int maxx=0;
while(!Q.empty()){
int x=Q.front();Q.pop();
dp[x][1]+=h[x];
maxx=max(maxx,dp[x][1]);
maxx=max(maxx,dp[x][0]);
degree[fa[x]]--;
if(degree[fa[x]]==0)Q.push(fa[x]);
dp[fa[x]][1]+=dp[x][0];
dp[fa[x]][0]+=max(dp[x][0],dp[x][1]);
}
cout<<maxx;
return 0;
}
P2016 战略游戏
#include <iostream>
#include <vector>
using namespace std;
const int MAX=1500;
vector<int> son[MAX];
int vis[MAX];
int dp[MAX][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
void dfs(int x){
dp[x][1]=1;
dp[x][0]=0;//不放就只花0个士兵
// if(son[x].size()==0)return;
for(int i=0;i<son[x].size();i++){
int s=son[x][i];
dfs(s);
dp[x][1]+=min(dp[s][1],dp[s][0]);
dp[x][0]+=dp[s][1];
}
}
int main(){
int n;
cin>>n;
int x,y,k;//x节点有k个节点与他相连(k个孩子
for(int i=0;i<n;i++){
cin>>x>>k;
for(int i=0;i<k;i++){
cin>>y;
son[x].push_back(y);
vis[y]=1;
}
}
int root;
for(int i=0;i<n;i++){
if(!vis[i]){
root=i;
break;
}
}
dfs(root);
cout<<min(dp[root][1],dp[root][0]);
return 0;
}
链式前向星存储无向边 一要开两倍空间,二要
全都是有向边还好,有无向边就要判断即将dfs的节点x的终点是否是x的起点, 少了这个判断就死循环了,由于层层递归,x取决于to的情况,而以to为起点发出去的一条边终点又是x,像拓扑排序里形成了个圈,互为前提
#include <iostream>
//#include <vector>
using namespace std;
const int MAX=1500;
//vector<int> son[MAX];
int vis[MAX];
int dp[MAX<<1][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
struct edge{
int to;
int next;
}e[MAX<<1];
int head[MAX<<1];
int cnt;
void add(int u,int v){
e[++cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
void dfs(int x,int from){//全都是有向边还好,有无向边就要判断即将dfs的节点x的终点是否是x的起点, 少了这个判断就死循环了,由于层层递归,x取决于to的情况,而以to为起点发出去的一条边终点又是x,像拓扑排序里形成了个圈,互为前提
dp[x][1]=1;
dp[x][0]=0;//不放就只花0个士兵
// if(son[x].size()==0)return;
// for(int i=0;i<son[x].size();i++){
// int s=son[x][i];
for(int i=head[x];i;i=e[i].next){
int s=e[i].to;
if(s!=from){//由于视作了无向边a->b,b->a
dfs(s,x);
dp[x][1]+=min(dp[s][1],dp[s][0]);
dp[x][0]+=dp[s][1];
}
}
}
int main(){
int n;
cin>>n;
int x,y,k;//x节点有k个节点与他相连(k个孩子
for(int i=0;i<n;i++){
cin>>x>>k;
for(int i=0;i<k;i++){
cin>>y;
// son[x].push_back(y);
add(x,y);
add(y,x);
vis[y]=1;
}
}
int root;
for(int i=0;i<n;i++){
if(!vis[i]){
root=i;
break;
}
}
dfs(root,0);
cout<<min(dp[root][1],dp[root][0]);
return 0;
}