BZOJ4919 大根堆(树形dp+线段树合并)
用 multiset 启发式合并贪心维护 LIS 的做法就不多说了,网上题解一大堆,着重讲一下线段树合并维护 \(dp\)。
\(O(n^2)\) 的 \(dp\) 非常显然。离散化后,设 \(dp[u][i]\) 表示节点 \(u\) 的子树中,最大值为 \(i\) 时最多取多少个节点。转移时考虑是否将节点 \(u\) 加入大根堆并分类讨论。
这样的状态不支持快速合并。考虑优化状态,设 \(dp[u][i]\) 表示节点 \(u\) 的子树中,最大值 \(\le i\) 时最多取多少个节点,并尝试使用线段树合并来维护。
转移同样考虑两种情况。如果 \(u\) 不取,那么直接 \(dp[u][i] = \sum dp[v][i]\) 即可,将 \(u\) 的儿子的线段树合并。合并之后,如果 \(u\) 取,就要求子树中取的最大值都 \(\lt val[u]\),那么要用 \(\max\limits_{i \lt val[u]}dp[u][i]+1\) 去更新 \(dp[u][\ge val[u]]\)。
注意到,根据状态的设计,\(dp[u]\) 其实是一个单调不减的序列,所以 \(\max\limits_{i \lt val[u]} dp[u][i]+1\) 其实相当于 \(dp[u][val[u]-1]+1\)。同时由于 \(dp[u]\) 单调不减,所以 \(dp[u][\ge val[u]]\) 中会被更新的值是一段左端点为 \(val[u]\) 的区间,这段区间的值都是 \(dp[u][val[u]-1]\)。
于是,可以二分出区间的右端点。具体地,二分找到最后一个 \(i\) 使得 \(dp[u][i] \lt dp[u][val[u]-1]+1\),然后在线段树上将区间 \([val[u],i]\) 覆盖或直接 \(+1\) 即可,需要标记永久化。
综上所述,该算法的时间复杂度为 \(O(n \log^2 n)\)。
/**
* @file: BZOJ4919.cpp
* @author: yaoxi-std
* @url:
*/
#pragma GCC optimize ("O2")
#pragma GCC optimize ("Ofast", "inline", "-ffast-math")
#pragma GCC target ("avx,sse2,sse3,sse4,mmx")
#include <bits/stdc++.h>
using namespace std;
#define resetIO(x) \
freopen(#x ".in", "r", stdin), freopen(#x ".out", "w", stdout)
#define debug(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
template <class _Tp>
inline _Tp& read(_Tp& x) {
bool sign = false; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) sign |= (ch == '-');
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + (ch ^ 48);
return sign ? (x = -x) : x;
}
template <class _Tp>
inline void write(_Tp x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar((x % 10) ^ 48);
}
bool m_be;
using ll = long long;
const int MAXN = 2e5 + 10;
const int INF = 0x3f3f3f3f;
int n, m, a[MAXN], fa[MAXN], val[MAXN];
vector<int> g[MAXN];
struct SegmentTree {
struct Node {
int ls, rs, sum;
} nd[MAXN * 25];
int tot, rt[MAXN];
int& operator[](int i) { return rt[i]; }
void update(int& i, int l, int r, int ql, int qr, int v) {
if (ql > qr) return;
if (!i) i = ++tot;
if (ql <= l && r <= qr) return void(nd[i].sum += v);
int mid = (l + r) >> 1;
if (ql <= mid) update(nd[i].ls, l, mid, ql, qr, v);
if (qr > mid) update(nd[i].rs, mid + 1, r, ql, qr, v);
}
int query(int i, int l, int r, int p) {
if (!i || !p) return 0;
if (l == r) return nd[i].sum;
int mid = (l + r) >> 1;
if (p <= mid) return nd[i].sum + query(nd[i].ls, l, mid, p);
return nd[i].sum + query(nd[i].rs, mid + 1, r, p);
}
void merge(int& x, int y, int l, int r) {
if (!x || !y) return void(x = x | y);
if (l == r) return void(nd[x].sum += nd[y].sum);
int mid = (l + r) >> 1;
merge(nd[x].ls, nd[y].ls, l, mid);
merge(nd[x].rs, nd[y].rs, mid + 1, r);
nd[x].sum += nd[y].sum;
}
} sgt;
void dfs(int u) {
for (auto v : g[u]) dfs(v), sgt.merge(sgt[u], sgt[v], 1, m);
int tmp = sgt.query(sgt[u], 1, m, a[u] - 1) + 1;
int l = a[u], r = m, pos = a[u] - 1;
while (l <= r) {
int mid = (l + r) >> 1;
if (sgt.query(sgt[u], 1, m, mid) < tmp)
l = mid + 1, pos = mid;
else
r = mid - 1;
}
sgt.update(sgt[u], 1, m, a[u], pos, 1);
}
bool m_ed;
signed main() {
read(n);
for (int i = 1; i <= n; ++i)
read(a[i]), read(fa[i]), val[i] = a[i], g[fa[i]].push_back(i);
sort(val + 1, val + n + 1), m = unique(val + 1, val + n + 1) - val - 1;
for (int i = 1; i <= n; ++i) a[i] = lower_bound(val + 1, val + m + 1, a[i]) - val;
dfs(1), write(sgt.query(1, 1, m, m)), putchar('\n');
return 0;
}
BZOJ4919 大根堆(树形dp+线段树合并)的更多相关文章
- 【洛谷5298】[PKUWC2018] Minimax(树形DP+线段树合并)
点此看题面 大致题意: 有一棵树,给出每个叶节点的点权(互不相同),非叶节点\(x\)至多有两个子节点,且其点权有\(p_x\)的概率是子节点点权较大值,有\(1-p_x\)的概率是子节点点权较小值. ...
- BZOJ4919 大根堆(动态规划+treap+启发式合并)
一个显然的dp是设f[i][j]为i子树内权值<=j时的答案,则f[i][j]=Σf[son][j],f[i][a[i]]++,f[i][a[i]+1~n]对其取max.这样是可以线段树合并的, ...
- 【pkuwc2018】 【loj2537】 Minmax DP+线段树合并
今年年初的时候参加了PKUWC,结果当时这一题想了快$2h$都没有想出来.... 哇我太菜啦.... 昨天突然去搜了下哪里有题,发现$loj$上有于是就去做了下. 结果第一题我5分钟就把所有细节都想好 ...
- [BZOJ5461][LOJ#2537[PKUWC2018]Minimax(概率DP+线段树合并)
还是没有弄清楚线段树合并的时间复杂度是怎么保证的,就当是$O(m\log n)$吧. 这题有一个显然的DP,dp[i][j]表示节点i的值为j的概率,转移时维护前缀后缀和,将4项加起来就好了. 这个感 ...
- BZOJ.5461.[PKUWC2018]Minimax(DP 线段树合并)
BZOJ LOJ 令\(f[i][j]\)表示以\(i\)为根的子树,权值\(j\)作为根节点的概率. 设\(i\)的两棵子树分别为\(x,y\),记\(p_a\)表示\(f[x][a]\),\(p_ ...
- LOJ2537. 「PKUWC2018」Minimax【概率DP+线段树合并】
LINK 思路 首先暴力\(n^2\)是很好想的,就是把当前节点概率按照权值大小做前缀和和后缀和然后对于每一个值直接在另一个子树里面算出贡献和就可以了,注意乘上选最大的概率是小于当前权值的部分,选最小 ...
- [PKUWC2018]Minimax [dp,线段树合并]
好妙的一个题- 我们设 \(f_{i,j}\) 为 \(i\) 节点出现 \(j\) 的概率 设 \(l = ch[i][0] , r = ch[i][1]\) 即左儿子右儿子 设 \(m\) 为叶子 ...
- P6847-[CEOI2019]Magic Tree【dp,线段树合并】
正题 题目链接:https://www.luogu.com.cn/problem/P6847 题目大意 \(n\)个点的一棵树上,每个时刻可以割掉一些边,一些节点上有果实表示如果在\(d_i\)时刻这 ...
- POJ 3162 Walking Race 树形DP+线段树
给出一棵树,编号为1~n,给出数m 漂亮mm连续n天锻炼身体,每天会以节点i为起点,走到离i最远距离的节点 走了n天之后,mm想到知道自己这n天的锻炼效果 于是mm把这n天每一天走的距离记录在一起,成 ...
随机推荐
- js红宝书学习笔记(一)引用类型
一.引用类型 ECMAScript中,引用类型是一种数据结构称之为对象定义,,引用对象不同于传统面向对象语言所支持的类和接口等基本结构 创建Object 实例的两种方式: new操作符跟Object构 ...
- 在vue中_this和this的区别
_this只是一个变量名,this代表父函数,如果在子函数还用this,this的指 向就变成子函数了,_this就是用来存储指向的 普通函数中的this表示调用此函数时的对象,箭头函数里面的this ...
- web3.0、比特币、区块链、元宇宙,以及那些待收割的韭菜们!
前几天看到周星驰在社交账号上招聘web3.0的人才,感觉有必要说说web3.0,当然不是基于技术层面,而是从另一个维度说说web3.0以及其它相关的概念,从而做到如何反欺诈,如何避免被资本割韭菜.想到 ...
- C#-多线程的使用Tread
首先是概念,什么是线程? 线程是操作系统分配CPU时间的基本单元,在一个进程中可以有多个线程同时执行代码. 谈一谈什么是进程? 简单的说,一个正在运行的应用程序可以视为一个进程,进程间相互独立,资源不 ...
- 事件循环Event Loop
在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息.被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数.正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧. ...
- c语言求输入的任一整数的各位数之和
c语言求解代码: # include<stdio.h> int main(void){ int a,i=0,sum=0; scanf("%d",&a); if( ...
- 泛化之美 —— C++11 可变参数模板的妙用
概述 首先这篇文章出自博客园作者:[qicosmos ],我对本文的实例代码进行了学习.思考和整理纠正,理清了文章的全部细节,觉得这是一篇让我受益匪浅的文章.之所以会接触「可变参数模板」这部分的内容, ...
- DelCrLfSpace V0.9
开发界面 Option Explicit 'SourceTextBox 是上面的源框 'ResultTextBox 是下面的处理预览框 'Form KeyPreview = True Private ...
- CentOS6/7 配置守护进程
CentOS6.x CentOS6中转用Upstrat代替以前的init.d/rcX.d的线性启动方式. 一.相关命令 通过initctl help可以查看相关命令 [root@localhost ~ ...
- 关于Docker的一些事--Docker概述
为什么会出现docker? 背景 以一个食品工厂为例子,有一款食品产品,从研发到包装,需要分别两套生产线,虽然感觉说很好,符合SOP,但是吧,产能很慢,这是为什么呢? 研发:食品搭配.颜色搭配.荤素搭 ...