题目链接

本文旨在介绍树上背包的优化。

可见例题,例题中N,M∈[1,100000]N,M \in [1,100000]N,M∈[1,100000]的数据量让O(nm2)O(nm^2)O(nm2)的朴素树上背包T到飞起,我们需要考虑优化。

个人会将各种优化讲到极限(当然是本蒟蒻的极限)。

根据一番学习,我也认为上下界优化最简单易理解……

上下界优化这位神犇的博客相当不错了:戳我%他

我也口胡两句吧。

普通做法:

for (j=m+1;j>=1;--j)//枚举背包容量
for (k=1;k<j;++k)//枚举在子树中选择多少
f[u][j]=max(f[u][j],f[u][k]+f[v][j-k]);

那么size优化非常简单好想:

for (j=min(m+1,size[u]);j>=1;--j)//枚举背包容量
for (k=1;k<j&&k<=size[v];++k)//枚举在子树中选择多少
f[u][j]=max(f[u][j],f[u][k]+f[v][j-k]);

道理也很简单,选完就那么多,肯定不能枚举到超过的。

于是能AC这道题,用时17s17s17s。

但再想想,我们选择的kkk的下界其实也是会被约束的。

因为选到jjj的总容量的时候,假定前面的全部取完,kkk都必须要到达一个值才能满足条件。

例子:

size[u]=size[son1]+size[son2]+size[son3]size[u] = size[son1] + size[son2] + size[son3]size[u]=size[son1]+size[son2]+size[son3]

我们枚举时,比如j=size[son1]+size[son2]+aj = size[son1] + size[son2] + aj=size[son1]+size[son2]+a的情况,

我们至少要在son3son3son3中取a个节点才能达到此容量。

因此就能得到上下界优化:

void dfs(int u)
{
siz[u]=1;
f[u][1]=a[u];
int i,j,k,v;
for (i=head[u];i;i=nxt[i])
{
v=to[i];
dfs(v);
for (j=min(m+1,siz[u]+siz[v]);j>=2;--j)//这里做了小改动,因为1的更新肯定没有意义
for (k=max(1,j-siz[u]);k<=siz[v]&&k<j;++k)
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
siz[u]+=siz[v];
}
}

这里对sizesizesize数组的更新做了特殊处理,可以更方便地得到前面所有子树的节点数总和。于是更进一步,达到了12s的成绩。

那么还能不能更快呢?其实是可以的。

我们发现内层循环需要2个判断语句,有什么办法缩成一个?

当然可以开临时变量来存,但我们甚至可以换一种dp方式!(思路来源于某位神犇,他的代码用了刷表法无师自通地进行了O(nm)O(nm)O(nm)优化导致过去“指点”的我转为“%%%”状态)

刷表法

刷表法怎么写呢?其实也很简单:

void dfs(int u)
{
siz[u]=1;
f[u][1]=a[u];
int i,j,k,v;
for (i=head[u];i;i=nxt[i])
{
v=to[i];
dfs(v);
for (j=min(m,siz[u]);j>=1;--j)//在之前子树&&根中选择的节点数,这里要取1是因为肯定要取根节点
for (k=1;k<=siz[v]&&j+k<=m+1;++k)//在当前子树取得节点数
f[u][j+k]=max(f[u][j+k],f[u][j]+f[v][k]);
siz[u]+=siz[v];
}
}

这时候,我们就可以将内层循环的两个判断语句合为一个了:

void dfs(int now)
{
size[now] = 1;
f[now][1] = w[now];
int v;
for (int p = head[now]; p; p = lines[p].next)
{
v = lines[p].to;
dfs(v);
for (int j = min(size[now], m); j; --j)
for (int k = min(size[v], m + 1 - j); k; --k)
f[now][j + k] = max(f[now][j + k], f[now][j] + f[v][k]);
size[now] += size[v];
}
}

省去了一个判断,对常数的优化还是不可小觑的。

下标映射

对于例题,由于n,mn,mn,m过大,开二维肯定开不下,肯定要扁平化为一维。

因为有一个超级源点,因此背包最大容量其实为m+1m+1m+1,而[0,m+1][0,m+1][0,m+1]间有m+2m+2m+2个位置。

故有:

inline int pos(const int &x,const int &y)
{
return x * (m+2) + y;//注意此处x可能为0
}

但是事实上,每次都计算这个pos带来了大量的计算。多大量呢?

当初用填表法时,我将这个函数换成了definedefinedefine,总时间从12s12s12s提升到了8s8s8s。

显然因为这个pospospos反复计算,消耗了大量的时间。

那么是否还有比宏定义更优的方法呢?我翻了翻最优解,除了题目作者本人在调整数据规模时的弱数据AC外,第一位是一位名为WarlockAkk的神犇,用时仅4.2s4.2s4.2s!

这究竟是何等黑魔法?我点开源码开始膜拜,于是看到:

bfo(i,0,n+1){
d[i]=spa+idx;
idx+=m+2;
}

这是什么意思呢?ddd是一个int∗int*int∗的数组,于是我恍然大悟:

可以预处理出一个映射数组,将二维的对映射数组的访问映射到一维的保存数组中。

具体实现方式:

int dp[100001000];
int *f[MAXN]; //f[i][j] points to the dp arr.
int k, pointer = 0;
f[0] = &dp[0]; //special
for (int i = 1; i <= n; ++i)
{
pointer += m + 2;
f[i] = &dp[pointer]; //special
}

我们将这两行代码插入到读入的循环中,就可以得到映射数组fff,我们就能直接用f[i][j]f[i][j]f[i][j]来访问了!

并且因为f[i]f[i]f[i]存的索引直接加上jjj就能得到地址,我们实际上避免了两个大数的乘法,而使其变成了加法。

举例:

原先访问方式:

dp[x∗(m+2)+y]dp[x * (m+2) + y ]dp[x∗(m+2)+y] 进行了一次乘法一次加法

解析一下就是:

return dp + (x * (m+2) + y);

而现在的访问方式:

(f[x]+y)(f[x] + y)(f[x]+y)

解析一下就是:

return (f + x) + y;

效率提升相当显著。

同时注意我们的预处理方式:

int pointer = 0;
pointer += m + 2;

写成加法的形式,与乘法形式对比:

pointer = (m + 2) * i;

效率如何很显然了。

那么下标映射后到底有多快呢?

有多快呢?

我们看结论吧。

总结

填表法 填表法 with O(2) 刷表法 刷表法 with O(2) 下标映射 + 刷表法 with O(2)
8s8s8s 7.5s7.5s7.5s 7s7s7s 6.5s6.5s6.5s 2.4s2.4s2.4s

可以发现,吸氧对于这种情况提升不明显。

而下标映射 快、极快、巨快!



因此在卡常优化时我们可以多想想使用指针等玄学进行优化,往往会有意想不到的提升。

如lower_boundlower\_boundlower_bound等函数直接使用迭代器等……

That’s all.

Code

#pragma GCC target("avx")
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fwhole-program")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-fstrict-overflow")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-skip-blocks")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("-funsafe-loop-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")
#include <cstdio>
using namespace std;
const int MAXN = 100100;
inline int max(const int &a, const int &b) { return a > b ? a : b; }
inline int min(const int &a, const int &b) { return a < b ? a : b; }
char buf[100000], *p1 = buf, *p2 = buf;
#define nc() p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++
template <typename T>
inline void read(T &r)
{
static char c;r = 0;
for (c = nc(); c > '9' || c < '0'; c = nc());
for (; c >= '0' && c <= '9'; r = (r << 1) + (r << 3) + (c ^ 48), c = nc());
}
struct node
{
int to, next;
node() {}
node(const int &_to, const int &_next) : to(_to), next(_next) {}
} lines[MAXN];
int head[MAXN];
void add(const int &x, const int &y)
{
static int tot = 0;
lines[++tot] = node(y, head[x]), head[x] = tot;
}
int n, m;
int dp[100001000];
int *f[MAXN]; //f[i][j] points to the dp arr.
int size[MAXN], w[MAXN];
void dfs(int now)
{
int v;
size[now] = 1;
f[now][1] = w[now];
for (int p = head[now]; p; p = lines[p].next)
{
v = lines[p].to;
dfs(v);
for (int i = min(size[now], m); i; --i)
for (int j = min(size[v], m + 1 - i); j; --j)
f[now][i + j] = max(f[now][i + j], f[now][i] + f[v][j]);
size[now] += size[v];
}
}
int main()
{
read(n);
read(m);
int k, pointer = 0;
f[0] = &dp[0]; //special
for (int i = 1; i <= n; ++i)
{
pointer += m + 2;
f[i] = &dp[pointer]; //special
read(k);
add(k, i); //we can set the point(0) into a vitual node,which is the root of the tree
read(w[i]);
}
dfs(0);
printf("%d", f[0][m + 1]);
return 0;
}~~~

[U53204] 树上背包的优化的更多相关文章

  1. Hdu 6268 点分治 树上背包 bitset 优化

    给你一颗大小为n(3000)的树,树上每个点有点权(100000),再给你一个数m(100000) i为1~m,问树中是否存在一个子图,使得权值为i. 每次solve到一个节点 用一个bitset维护 ...

  2. 洛谷 P2015 二叉苹果树 (树上背包)

    洛谷 P2015 二叉苹果树 (树上背包) 一道树形DP,本来因为是二叉,其实不需要用树上背包来干(其实即使是多叉也可以多叉转二叉),但是最近都刷树上背包的题,所以用了树上背包. 首先,定义状态\(d ...

  3. 【BZOJ】4033: [HAOI2015]树上染色 树上背包

    [题目]#2124. 「HAOI2015」树上染色 [题意]给定n个点的带边权树,要求将k个点染成黑色,使得 [ 黑点的两两距离和+白点的两两距离和 ] 最大.n<=2000. [算法]树上背包 ...

  4. 【2019.8.9 慈溪模拟赛 T2】摘Galo(b)(树上背包)

    树上背包 这应该是一道树上背包裸题吧. 众所周知,树上背包的朴素\(DP\)是\(O(nm^2)\)的. 但对于这种体积全为\(1\)的树上背包,我们可以通过记\(Size\)优化转移时的循环上界,做 ...

  5. [CSP-S模拟测试]:点亮(状压DP+树上背包DP)

    题目传送门(内部题121) 输入格式 第一行,一个正整数$n$. 第二行,$n-1$个正整数$p_2,p_3,...,p_n$.保证$p_u$是在$1$到$u-1$中等概率随机选取的. 接下来$n$行 ...

  6. hdu1059 多重背包(转换为01背包二进制优化)

    题目链接:http://acm.split.hdu.edu.cn/showproblem.php?pid=1059 之前写过一个多重背包二进制优化的博客,不懂请参考:http://www.cnblog ...

  7. HDU4044 GeoDefense(有点不一样的树上背包)

    题目大概说一棵n个结点的树,每个结点都可以安装某一规格的一个塔,塔有价格和能量两个属性.现在一个敌人从1点出发但不知道他会怎么走,如果他经过一个结点的塔那他就会被塔攻击失去塔能量的HP,如果HP小于等 ...

  8. luogu 2014 选课 树上背包

    树上背包 #include<bits/stdc++.h> using namespace std; ; const int inf=0x3f3f3f3f; vector<int> ...

  9. hdu1059 dp(多重背包二进制优化)

    hdu1059 题意,现在有价值为1.2.3.4.5.6的石头若干块,块数已知,问能否将这些石头分成两堆,且两堆价值相等. 很显然,愚蠢的我一开始并想不到什么多重背包二进制优化```因为我连听都没有听 ...

随机推荐

  1. 人物 - 安迪·葛洛夫,Andrew Stephen Grove,Andy Grove

    安德鲁·史蒂芬·格罗夫英语:Andrew Stephen Grove,昵称安迪·格罗夫(Andy Grove) 1. 前Intel的CEO 2. 1985 年将英特尔带入计算机处理器市场,帮助Inte ...

  2. flask-script扩展

    在项目部署到线上时,指定端口号时,一般都不会在服务器上进行更改,所以使用flask-script就可以在Flask服务器启动时,通过命令行的方式传入参数,而不仅仅通过app.run()方法中传参.具体 ...

  3. ethtool命令使用

    [root@localhost ~]# ethtool -s eth0 speed 100 duplex full #设置网口的speed和duplex # ethtool eth0Settings ...

  4. 吴裕雄--天生自然 JAVA开发学习:解决java.sql.SQLException: The server time zone value报错

    这个异常是时区的错误,因此只你需要设置为你当前系统时区即可,解决方案如下: import java.sql.Connection ; import java.sql.DriverManager ; i ...

  5. Python基础-1 基础语法

    基础语法 标识符 所谓的标识符就是对变量.常量.函数.类等对象起的名字. 首先必须说明的是,Python语言在任何场景都严格区分大小写!也就是说A和a代表的意义完全不同 python对于表示标识符的命 ...

  6. Go的WaitGroup

    goroutine使用方便,但是如果不加以处理一般会deadlock,因为goroutine配合Chanel的话只能是一进一出,否则就会卡在那里.下面一个示例就是利用这个WaitGroup处理这种死锁 ...

  7. Linux命令:top命令

    top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器.下面详细介绍它的使用方法.top是一个动态显示过程,即可以通过用户按键来不断刷新 ...

  8. MySQL操作之DCL

    目录 SQL语句的分类 DCL语句 SQL语句的分类 DDL(Data Definition Languages)语句:数据定义语言.这些语句定义了不同的数据段. 数据库.表.列.索引等数据库对象的定 ...

  9. 关于length、length()、size()

    length:属性,数组的属性. length(): String的方法,方法体里面是  return value.length; size():集合如list.set.map的方法,返回元素个数.

  10. 常用WinAPI函数整理------------转载

    常用WinAPI函数整理原创 玩撕你 发布于2019-09-04 20:06:55 阅读数 101 收藏展开 之前的博客写了很多关于Windows编程的内容,在Windows环境下的黑客必须熟练掌握底 ...