[BZOJ4585][Apio2016]Fireworks 烟火表演(树形 DP + 凸包 + 左偏树)

Address

洛谷 P3642
BZOJ 4585
UOJ #205
LOJ #2568

Solution

  • NOIP 2018 之后 A 掉的第一道题,祭
  • 看上去是水题,但是发现边权可以减一之后就不是那么容易了
  • 很容易想到状态: f [ u ] [ i ] f[u][i] 表示把 u u 到子树内所有叶子的距离都调成 i i 的最小代价(下面 l e n ( u , v ) len(u,v) 为边 ( u , v ) (u,v) 的长度)
  • f [ u ] [ i ] = v s o n ( u ) max j = 0 i { f [ j ] + l e n ( u , v ) ( i j ) } f[u][i]=\sum_{v\in son(u)}\max_{j=0}^i\{f[j]+|len(u,v)-(i-j)|\}
  • 可以大胆猜想 f [ u ] f[u] 是关于 i i 的下凸函数
  • 假设 u u 只有 v v 一个子节点
  • i [ l , r ] i\in[l,r] f [ v ] f[v] 取得最小值
  • 然后分类讨论一下转移:
  • (1) 0 i l 0\le i\le l
  • 这时候 j [ 0 , i ] j\in[0,i] ,最优的方案是把 ( u , v ) (u,v) 的边权改成 0 0
  • f [ u ] [ i ] = f [ v ] [ i ] + l e n ( u , v ) f[u][i]=f[v][i]+len(u,v)
  • (2) l i l + l e n ( u , v ) l\le i\le l+len(u,v)
  • 这时候最优的方案是把 ( u , v ) (u,v) 的边权改成 i l i-l
  • f [ u ] [ i ] = f [ v ] [ l ] + l e n ( u , v ) i + l f[u][i]=f[v][l]+len(u,v)-i+l
  • (3) l + l e n ( u , v ) i r + l e n ( u , v ) l+len(u,v)\le i\le r+len(u,v)
  • 这时候最优的方案是不修改 ( u , v ) (u,v) 的边权
  • f [ u ] [ i ] = f [ v ] [ i l e n ( u , v ) ] f[u][i]=f[v][i-len(u,v)]
  • (4) i r + l e n ( u , v ) i\ge r+len(u,v)
  • 这时候最优的方案是把 ( u , v ) (u,v) 的边权改成 i r i-r
  • f [ u ] [ i ] = f [ v ] [ r ] + i r l e n ( u , v ) f[u][i]=f[v][r]+i-r-len(u,v)
  • f [ u ] f[u] f [ v ] f[v] 看作分段函数,可以发现每个段都是一次的
  • 上面四个转移相当于
  • (1)把横坐标 [ 0 , l ] [0,l] 内的直线上移 l e n ( u , v ) len(u,v) 个单位
  • (2)把横坐标 [ l , r ] [l,r] 内的直线右移 l e n ( u , v ) len(u,v) 个单位
  • (3)在横坐标 [ l , l + l e n ( u , v ) ] [l,l+len(u,v)] 内加入一条斜率为 1 -1 的直线
  • (4)在横坐标 [ r + l e n ( u , v ) , + ] [r+len(u,v),+\infty] 内加入一条斜率为 1 1 的直线
  • 我们考虑维护分段函数的拐点
  • 注意到每经过一个拐点,函数的斜率加一
  • 对于叶子节点,有两个拐点 ( 0 , 0 ) (0,0) ( 0 , 0 ) (0,0)
  • 这是一棵树,处理点 u u 时如何将 u u 转移到 u u 父亲 f a [ u ] fa[u]
  • 先说:最右端直线的斜率是 u 的子节点数(接下来会证明)
  • 用一个大根堆维护拐点横坐标
  • 先不断弹出最右端斜率大于 1 1 的直线,使得最后一段直线的斜率为 1 1
  • 然后把斜率为 0 0 的直线段对应的两个拐点右移 l e n ( u , f a [ u ] ) len(u,fa[u]) 个单位
  • 把这个大根堆合并到父亲
  • 需要用到可并堆(左偏树)
  • 而这时候每个点往父亲合并时最右端的斜率一定为 1 1 ,故 u u 最右端直线的斜率是 u u 的子节点数
  • 利用这个就能判断最右端直线的斜率
  • 先暂时设 a n s ans 为所有边权和
  • f [ 1 ] f[1] 的折线的右端斜率大于 0 0 的直线全部弹掉
  • 注意到所有边权和就是 f [ 1 ] f[1] 0 0 点的值
  • 于是只需要从横坐标 0 0 开始,通过拐点和斜率计算函数减少的量即可
  • 可以得出把 a n s ans 减去这时候左偏树内所有点的横坐标之和就是答案
  • 复杂度 O ( n log n ) O(n\log n)

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Rof(i, a, b) for (i = a; i >= b; i--)

inline int read()
{
	int res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	return bo ? ~res + 1 : res;
}

template <class T>
inline void Swap(T &a, T &b) {a ^= b; b ^= a; a ^= b;}

typedef long long ll;

const int N = 6e5 + 5, M = N << 1;

int n, m, fa[N], len[N], rt[N], cnt[N], ToT;
ll ans;

struct node
{
	int lc, rc, dis; ll val;
} T[M];

int mer(int x, int y)
{
	if (!x || !y) return x + y;
	if (T[x].val < T[y].val) Swap(x, y);
	T[x].rc = mer(T[x].rc, y);
	T[x].dis = T[x].rc ? T[T[x].rc].dis + 1 : 0;
	if (!T[x].lc || T[T[x].lc].dis < T[T[x].rc].dis)
		Swap(T[x].lc, T[x].rc);
	return x;
}

void pop(int &x)
{
	x = mer(T[x].lc, T[x].rc);
}

int main()
{
	int i;
	n = read(); m = read();
	For (i, 2, n + m)
		cnt[fa[i] = read()]++, ans += (len[i] = read());
	Rof (i, n + m, 2)
	{
		ll l = 0, r = 0;
		cnt[i]--;
		while (cnt[i] > 0) pop(rt[i]), cnt[i]--;
		if (i <= n) r = T[rt[i]].val, pop(rt[i]),
			l = T[rt[i]].val, pop(rt[i]);
		int x, y;
		T[x = ++ToT] = (node) {0, 0, 0, l + len[i]};
		T[y = ++ToT] = (node) {0, 0, 0, r + len[i]};
		rt[i] = mer(rt[i], mer(x, y));
		rt[fa[i]] = mer(rt[fa[i]], rt[i]);
	}
	while (cnt[1]--) pop(rt[1]);
	while (rt[1]) ans -= T[rt[1]].val, pop(rt[1]);
	std::cout << ans << std::endl;
	return 0;
}

猜你喜欢

转载自blog.csdn.net/xyz32768/article/details/84112283