luogu2014 选课[树形背包][优化成$O(n^2)$的方法]
https://www.luogu.org/problemnew/show/P2014
树形背包的裸题。。当版子好了。
$f[i][j][k]$表示子树$i$选前$j$个孩子,共$k$个后代节点时的最大价值。然后$j$那一维是可以滚动的(但同时也要注意枚举变成了倒序),所以可以去掉。
$f[i][j]$表示子树$i$共选$k$个后代节点时的最大价值。
然后每个点可以抽象为一个背包,他的每个孩子包含一组物品,一组物品中包括以孩子为子树,选v个其后代节点形成的最大价的共v+1个物品(1指的是只有孩子自己)。对于每个孩子,只能选他的一种状态情形,或者不选。所以就是一个分组背包啦。
但是注意,子树的根必须强制选上。所以可以以他为初态,也就是后代节点=0的状态。写一下伪代码。
$dp$ $i$
$f_{i,0}=w_i$初态
$for$ $j=1$ $\sim$ $son_i$
$for$ $k=$(倒序)$sum_i -1$ $\sim$ $1$
$for$ $v=0$ $\sim$ $sum_j -1$
$if$ $v+1\leqslant k$
$f_{i,k}=max\{f_{i,k-v-1}+f_{j,v}\}$
然后复杂度由于是每个点都被考虑一次的,最坏是$O(N^3)$。(看做$N,M$同阶)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#define dbg(x) cerr<<#x<<" = "<<x<<endl
#define ddbg(x,y) cerr<<#x<<" = "<<x<<" "<<#y<<" = "<<y<<endl
using namespace std;
typedef long long ll;
template<typename T>inline char MIN(T&A,T B){return A>B?A=B,:;}
template<typename T>inline char MAX(T&A,T B){return A<B?A=B,:;}
template<typename T>inline T _min(T A,T B){return A<B?A:B;}
template<typename T>inline T _max(T A,T B){return A>B?A:B;}
template<typename T>inline T read(T&x){
x=;int f=;char c;while(!isdigit(c=getchar()))if(c=='-')f=;
while(isdigit(c))x=x*+(c&),c=getchar();return f?x=-x:x;
}
const int N=+;
int f[N][N],Head[N],Next[N<<],to[N<<],w[N],sum[N],tot;
int n,m;
inline void Addedge(int x,int y){
to[++tot]=y,Next[tot]=Head[x],Head[x]=tot;
to[++tot]=x,Next[tot]=Head[y],Head[y]=tot;
}
#define j to[tmp]
void dp(int i,int fa){
sum[i]=;
for(register int tmp=Head[i];tmp;tmp=Next[tmp])if(j!=fa)dp(j,i),sum[i]+=sum[j];
f[i][]=w[i];
for(register int tmp=Head[i];tmp;tmp=Next[tmp])if(j!=fa){
for(register int k=sum[i]-;k;--k){
for(register int v=;v<=sum[j]-;++v)
if(v+<=k)MAX(f[i][k],f[i][k-v-]+f[j][v]);
}
}
}
#undef j
int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout);
read(n),read(m);int x;
for(register int i=;i<=n;++i)read(x),read(w[i]),Addedge(x,i);
dp(,);printf("%d\n",f[][m]);
return ;
}
然而这题有更优秀的(优化)做法。复杂度(看做$N,M$同阶)都是$O(N^2)$.
1.根据上面的一种针对性优化。
由于上面每次合并$i$的答案可以视作是子树$j$和$i$的已合并部分做一个$f[i,k+v]<--f[i,k]+f[j,v]$。同时又有“每个点费用都是$1$”这样一个隐含的特殊条件,
所以如果每次合并都枚举整棵树大小的费用未免浪费。把枚举改成:枚举$i$已合并部分点的个数$k$和价值$f[i,k]$,再枚举子树$j$的点个数$v$和价值$f[j,v]$,更新之。
就是这样。比较一下和上面的这个的区别。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#define dbg(x) cerr<<#x<<" = "<<x<<endl
#define ddbg(x,y) cerr<<#x<<" = "<<x<<" "<<#y<<" = "<<y<<endl
using namespace std;
typedef long long ll;
template<typename T>inline char MIN(T&A,T B){return A>B?A=B,:;}
template<typename T>inline char MAX(T&A,T B){return A<B?A=B,:;}
template<typename T>inline T _min(T A,T B){return A<B?A:B;}
template<typename T>inline T _max(T A,T B){return A>B?A:B;}
template<typename T>inline T read(T&x){
x=;int f=;char c;while(!isdigit(c=getchar()))if(c=='-')f=;
while(isdigit(c))x=x*+(c&),c=getchar();return f?x=-x:x;
}
const int N=+;
int f[N][N],Head[N],Next[N<<],to[N<<],w[N],sum[N],tot;
int n,m;
inline void Addedge(int x,int y){
to[++tot]=y,Next[tot]=Head[x],Head[x]=tot;
to[++tot]=x,Next[tot]=Head[y],Head[y]=tot;
}
#define j to[tmp]
void dp(int i,int fa){
sum[i]=;f[i][]=w[i];
for(register int tmp=Head[i];tmp;tmp=Next[tmp])if(j!=fa){
dp(j,i);
for(register int k=sum[i]-;~k;--k)
for(register int v=;v<=sum[j]-;++v)
MAX(f[i][k+v+],f[i][k]+f[j][v]);
sum[i]+=sum[j];
}
}
#undef j
int main(){//freopen("test.in","r",stdin);freopen("test.out","w",stdout);
read(n),read(m);int x;
for(register int i=;i<=n;++i)read(x),read(w[i]),Addedge(x,i);
dp(,);printf("%d\n",f[][m]);
return ;
}
树中每个点对相当于只会被在LCA处合并$f[lca,k+v]$枚举一次(可以把枚举的个数$k,v$看做子树中的编号)。于是枚举了$O(n^2)$个点对。所以是平方复杂度。
但是,这只是针对性的优化,也就是说,如果改成每个点都有一个费用且不一定为1,这样每个点做一次分组背包时就要完全枚举费用这一维了,没有办法用点对优化。
2.更高效的dfs序优化。
给定一棵 $n $个节点的树,$1$ 号节点是根节点。每个点有一个物品,价格为 $c_i$ ,价值为 $v_i$ 。
要买一个点上的物品,必须先把它父节点的物品给买了。求 $m$ 元钱能买到的最大价值。$n,m ≤ 2000$。
这时无法用点对优化。因为树上dp是按照一定顺序(dfs序)进行的,所以考虑转化到dfs序列上处理。设得到的dfs序中,$i$对应原序列点编号$p_i$,这个$p_i$子树dfs序上右端点设为$r_i$。
设$f_{i,j}$为dfs序上选择$i\sim n$中的点且满足树形要求的、费用为$j$的最大价值。
则$f_{i,j}=max(f_{i+1,j-cost_{p_i}}+value_{p_i},f_{r_i+1,j})$。
注意是倒序以使得先处理所有子代再处理子树根的。相当于决定当前子树的根如果选,那么他的子树内部和后面的dfs序都可以随便选。如果不选,那子树这一段的dfs序都不可选,直接从另一颗子树中继承过来。
可知若后面的点$i+1$若满足树形依赖的要求,则dp了$i$之后$i$只可能包含这个子树没选和$i$和$i+1$都选了($i+1 \sim r_i$满足树形依赖,则选$i$后也应当满足依赖关系)的情况。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define dbg(x) cerr << #x << " = " << x <<endl
using namespace std;
typedef long long ll;
typedef double db;
typedef pair<int,int> pii;
template<typename T>inline T _min(T A,T B){return A<B?A:B;}
template<typename T>inline T _max(T A,T B){return A>B?A:B;}
template<typename T>inline char MIN(T&A,T B){return A>B?(A=B,):;}
template<typename T>inline char MAX(T&A,T B){return A<B?(A=B,):;}
template<typename T>inline void _swap(T&A,T&B){A^=B^=A^=B;}
template<typename T>inline T read(T&x){
x=;int f=;char c;while(!isdigit(c=getchar()))if(c=='-')f=;
while(isdigit(c))x=x*+(c&),c=getchar();return f?x=-x:x;
}
const int N=+;
int f[N][N],c[N],val[N];
int n,m,cnt;
struct thxorz{int to,nxt;}G[N<<];
int Head[N],id[N],ed[N],tot;
inline void Addedge(int x,int y){
G[++tot].to=y,G[tot].nxt=Head[x],Head[x]=tot;
G[++tot].to=x,G[tot].nxt=Head[y],Head[y]=tot;
}
#define y G[j].to
inline void dfs(int x,int fa){
id[++cnt]=x;
for(register int j=Head[x];j;j=G[j].nxt)if(y^fa)dfs(y,x);
ed[x]=cnt;
}
#undef y
int main(){//freopen("test.in","r",stdin);freopen("test.ans","w",stdout);
read(n);read(m);
for(register int i=,x;i<=n;++i)read(x),read(val[i]),Addedge(x,i),c[i]=;//read(c[i]);
dfs(,);
for(register int i=cnt;i;--i)
for(register int j=c[id[i]];j<=m;++j)
f[i][j]=_max(f[i+][j-c[id[i]]]+val[id[i]],f[ed[id[i]]+][j]);//dbg(i),dbg(j),dbg(f[i][j]);
printf("%d\n",f[][m]);
return ;
}
还有一种转二叉树的做法,不想了解。
这篇题解赶完了。
luogu2014 选课[树形背包][优化成$O(n^2)$的方法]的更多相关文章
- 选课 树形背包dp
题目描述 在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习.现在有N门功课,每门课有个学分,每门课有一 ...
- 洛谷 P2014 选课(树形背包)
洛谷 P2014 选课(树形背包) 思路 题面:洛谷 P2014 如题这种有依赖性的任务可以用一棵树表示,因为一个儿子要访问到就必须先访问到父亲.然后,本来本题所有树是森林(没有共同祖先),但是题中的 ...
- [Luogu2014]选课(树形dp)
[Luogu2014]选课 题目描述 在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习.现在有N门功课 ...
- Codevs1378选课[树形DP|两种做法(多叉转二叉|树形DP+分组背包)---(▼皿▼#)----^___^]
题目描述 Description 学校实行学分制.每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分.学校开设了N(N<300)门的选修课程,每个学生可选课程的数量M是给定的.学生选修 ...
- POJ 1155 (树形DP+背包+优化)
题目链接: http://poj.org/problem?id=1155 题目大意:电视台转播节目.对于每个根,其子结点可能是用户,也可能是中转站.但是用户肯定是叶子结点.传到中转站或是用户都要花钱, ...
- 洛谷 P1273 有线电视网(树形背包)
洛谷 P1273 有线电视网(树形背包) 干透一道题 题面:洛谷 P1273 本质就是个背包.这道题dp有点奇怪,最终答案并不是dp值,而是最后遍历寻找那个合法且最优的\(i\)作为答案.dp值存的是 ...
- 【洛谷 P2515】 [HAOI2010]软件安装 (缩点+树形背包)
题目链接 看到代价和价值这两个关键词,肯定是首先要想到背包的. 但是图中并没有说这是棵树,所以先要\(Tarjan\)缩点,然后就是选课了,跑一遍树形背包就好了. 注意:缩点后应该是一个森林,应该用一 ...
- Kattis - redblacktree Red Black Tree (树形背包)
问题:有一课含有n(n<=2e5)个结点的数,有m(m<=1000)个结点是红色的,其余的结点是黑色的.现从树中选若干数量的结点,其中红色的恰有k个,并且每个结点都不是其他任何另一个结点的 ...
- [POJ1155]TELE(树形背包dp)
看到这道题的第一眼我把题目看成了TLE 哦那不是重点 这道题是树形背包dp的经典例题 题目描述(大概的): 给你一棵树,每条边有一个cost,每个叶节点有一个earn 要求在earn的和大于等于cos ...
随机推荐
- 通过id()函数学习python的数据存储以及引用方式
id()函数是python的内置函数,用于获取对象的内存地址. 1.1 可以看出,33被存储在内存地址19877464上,对变量a赋值,实际上是将其指向存储着33的内存地址. 1.2 不仅是数字类型, ...
- @Value注解
1.注入 基本字符 public class Student { @Value("qq") private String name; @Value("12") ...
- Cobalt Strike DLL用于永恒之蓝注入
PDF下载:blob:https://www.52stu.org/9afe109c-f95f-4cb3-a180-90d469a1d001 我们在对存在MS17010的漏洞主机进行DLL注入的时候,一 ...
- Star all over again.
0x00前言 经过了一上午的折腾之后,博客的界面勉强可观,今天下午将之前的所有博客全部删除,重新开始写属于自己的博客,而不是只把它当作一个收藏夹,转载其他人的文章. 0x01近来感想 有感而发,随便写 ...
- docker-compose 部署elk+解决时间不对导致kibana找不到logstash定义的index + docker-compose安装
1.拉代码 git clone https://github.com/deviantony/docker-elk.git 2.docker-compose配置文件 [root@host7 docker ...
- Ubuntu 系统安装 Docker
安装 Docker CE 有多种方法,下面是最简单的通过Docker仓库的安装方法,其他方法参见官方文档. 设置仓库 刷新软件包 sudo apt-get update 安装必要的软件包 sudo a ...
- thinkPHP模型before_insert新增前 before_update更新前 before_write写入前 区别
thinkPHP模型中有个save方法,可用于新增数据和修改数据,这里容易出现混淆. 经过调试: before_write,不管是插入新数据还是修改数据都会执行: before_insert,只有插入 ...
- MySQL_入手<一>增--数据库操作
创建数据库 create database db_sanguo charset utf8; 切进db_sanguo use db_sanguo 创建英雄 create table t_hero( id ...
- Django基础之路由(urls)层
目录 Django基础之路由(urls)层 无名分组与有名分组 无名分组 有名分组 反向解析 前段解析 后端解析 无名分组反向解析 前段解析 后端解析 有名分组的反向解析 前段解析 后端解析 路由分发 ...
- 关于ionic4导入android studio的注意事项
最近看IT营的视频的时候,发现视频讲解的打包真是轻松的不得了,但是当自己导入打包的时候,你就会发现,没有最坑,只有更坑,按照教程来打包,估计你这辈子很难还原成功的,下面就来说一下关于 gradle与g ...