cf932F Escape Through Leaf dp+李超树

探讨树形DP算法在解决特定类型问题上的应用,通过李超树进行优化,实现高效寻找子树内直线的最低交点,适用于计算树结构中各节点到终止节点的最小代价。

Description


有一棵以 1 号点为根的树,有 n−1 条边 ui,vi,每个点两个权值 Ai,Bi。 你可以从一个点 u 跳到另一个点 v 满足 v 在 u 的子树中,并付出 Au ·Bv 的代价。 定义终止节点为没有任何儿子的节点。对于每个节点,求出从这个点出发到达任意一 个终止节点的最小代价。

Constraints


对于 30% 的数据,n ≤ 5∗103。
对于另外 10% 的数据,∀i Bi = 1。
对于另外 30% 的数据,树是一条链,且除 1 号点外,i 号点的父亲是 i−1。
对于 100% 的数据,2 ≤ n ≤ 105,|Ai|,|Bi|≤ 105。

Solution


考虑dp,设f[i]为第i个点的答案,转移比较显然
可以发现如果把b看成斜率,f看成截距,那么我们实际上在找某一条垂直于x轴的直线与子树内直线的最低交点,这个点显然只在凸壳上
这里写的是李超树合并,然鹅我并不会证复杂度。std给出的是set维护凸壳,启发式合并的做法。

Code


#include <stdio.h>
#include <string.h>
#include <algorithm>
#define rep(i,st,ed) for (int i=st;i<=ed;++i)

typedef long long LL;
const LL INF=1000000000000000LL;
const int N=200005;

struct line {LL k,b;};
struct edge {int y,next;} e[N*2];
struct treeNode {int l,r; line rec;} t[N*51];

int a[N],b[N];
int root[N],tot;
int ls[N],edCnt;

LL f[N];

int read() {
	int x=0,v=1; char ch=getchar();
	for (;ch<'0'||ch>'9';v=(ch=='-')?(-1):(v),ch=getchar());
	for (;ch<='9'&&ch>='0';x=x*10+ch-'0',ch=getchar());
	return x*v;
}

void add_edge(int x,int y) {
	e[++edCnt]=(edge) {y,ls[x]}; ls[x]=edCnt;
	e[++edCnt]=(edge) {x,ls[y]}; ls[y]=edCnt;
}

double get_x(line a,line b) {
	return (double)(a.b-b.b)/(b.k-a.k);
}

LL get_y(line a,LL x) {
	return a.k*x+a.b;
}

void modify(int &now,int tl,int tr,line x) {
	if (!now) {
		t[now=++tot].rec=x;
		return ;
	}
	if (get_y(t[now].rec,tl)<get_y(x,tl)&&get_y(t[now].rec,tr)<get_y(x,tr)) return ;
	if (get_y(t[now].rec,tl)>get_y(x,tl)&&get_y(t[now].rec,tr)>get_y(x,tr)) {
		t[now].rec=x; return ;
	}
	double pos=get_x(t[now].rec,x);
	int mid=(tl+tr)>>1;
	if (t[now].rec.k<x.k) {
		if (pos>mid) {
			modify(t[now].r,mid+1,tr,t[now].rec);
			t[now].rec=x;
		} else modify(t[now].l,tl,mid,x);
	} else {
		if (pos<mid) {
			modify(t[now].l,tl,mid,t[now].rec);
			t[now].rec=x;
		} else modify(t[now].r,mid+1,tr,x);
	}
}

LL query(int now,int tl,int tr,int x) {
	if (!now) return INF;
	if (tl==tr) return get_y(t[now].rec,x);
	int mid=(tl+tr)>>1;
	LL tmp,ret=get_y(t[now].rec,x);
	if (x<=mid) tmp=query(t[now].l,tl,mid,x);
	else tmp=query(t[now].r,mid+1,tr,x);
	return std:: min(ret,tmp);
}

int merge(int x,int y,int tl,int tr) {
	if (!x||!y) return x+y;
	if (tl==tr) {
		if (get_y(t[y].rec,tl)<get_y(t[x].rec,tl)) {
			t[x].rec=t[y].rec;
		}
		return x;
	}
	int mid=(tl+tr)>>1;
	t[x].l=merge(t[x].l,t[y].l,tl,mid);
	t[x].r=merge(t[x].r,t[y].r,mid+1,tr);
	modify(x,tl,tr,t[y].rec);
	return x;
}

void dfs(int now,int fa) {
	bool flag=true;
	for (int i=ls[now];i;i=e[i].next) {
		if (e[i].y==fa) continue;
		dfs(e[i].y,now);
		root[now]=merge(root[now],root[e[i].y],-N,N);
		flag=false;
	}
	if (!flag) f[now]=query(root[now],-N,N,a[now]);
	modify(root[now],-N,N,(line) {b[now],f[now]});
}

int main(void) {
	freopen("data.in","r",stdin);
	freopen("myp.out","w",stdout);
	int n=read();
	rep(i,1,n) a[i]=read();
	rep(i,1,n) b[i]=read();
	rep(i,2,n) add_edge(read(),read());
	dfs(1,0);
	rep(i,1,n) printf("%lld\n", f[i]);
	return 0;
}
<think>我们正在讨论C++中的upper_bound函数的使用。根据用户的问题,他想要理解并优化代码片段:`upper_bound(dp+1, dp+res+1, f[i]) - dp` 的作用和用法。 首先,我们来分析这个表达式: - `upper_bound` 是C++标准库中的一个函数,用于在已排序的序列中查找第一个大于给定值的元素的位置。 - 它的基本用法是:`upper_bound(first, last, value)`,其中`first`和`last`是序列的迭代器(或指针),`value`是要查找的值。 - 它返回一个迭代器(或指针),指向序列中第一个大于`value`的元素。如果找不到这样的元素,则返回`last`。 在表达式`upper_bound(dp+1, dp+res+1, f[i]) - dp`中: - `dp` 是一个数组(或指针),`dp+1`指向数组的第二个元素(索引1),`dp+res+1`指向数组索引为`res+1`的位置(即范围是`[1, res]`,因为序列从`dp+1`开始,到`dp+res+1`结束,实际上是左闭右开区间`[dp+1, dp+res+1)`)。 - 因此,`upper_bound(dp+1, dp+res+1, f[i])` 会在数组`dp`的子数组(从索引1到索引`res`)中查找第一个大于`f[i]`的元素的位置。 - 然后,减去`dp`(数组的起始地址)得到的是该元素在数组中的索引(下标)。因为`dp`是数组的起始地址,所以`指针 - dp`得到的就是从0开始的下标。但是注意,这里搜索的范围是从`dp+1`开始,所以返回的下标可能是从1到`res+1`(如果找不到,则返回`dp+res+1`,减去`dp`后得到`res+1`)。 例如,假设`dp`数组从索引0开始,但这里我们使用索引1到`res`(即`dp[1]`到`dp[res]`)。那么,`upper_bound(dp+1, dp+res+1, f[i])`返回的指针可能是`dp+k`(其中`k`在1到`res+1`之间),然后减去`dp`得到`k`(即下标k)。注意,如果返回的是`dp+res+1`,那么减去`dp`就是`res+1`。 这个表达式的作用是:在`dp`数组的[1, res]区间内,找到第一个大于`f[i]`的元素的位置(下标)。如果所有元素都不大于`f[i]`,则返回`res+1`。 这种用法常见于动态规划中维护一个有序的序列,特别是用于优化最长上升子序列(LIS)等问题的O(nlogn)解法。在LIS问题中,我们经常维护一个数组`dp`,其中`dp[i]`表示长度为i的上升子序列的最后一个元素的最小值。然后,对于每个新元素`f[i]`,我们使用`upper_bound`(或`lower_bound`)来查找插入位置,从而更新`dp`数组。 在最长上升子序列问题中,通常我们使用`lower_bound`来查找第一个大于等于当前元素的位置(用于非严格递增)或者严格递增时使用`lower_bound`(因为要求严格递增时,如果存在相等的元素,则不能放在同一个长度上,但实际做法中,我们通常用`lower_bound`来找到替换位置)。而`upper_bound`则用于严格递增序列中,查找第一个大于当前元素的位置(即替换位置)。 然而,在用户给出的代码片段中,使用的是`upper_bound`,所以它查找的是第一个大于`f[i]`的位置。这个位置可以用于替换操作,从而维护一个严格递增的序列。 例如,在最长递增子序列的O(nlogn)算法中: - 我们维护一个数组`dp`,`dp[1...res]`是当前长度为1,2,...,res的递增子序列的末尾元素的最小值,并且这个数组是递增的。 - 对于每个元素`a[i]`: - 如果`a[i]`大于`dp[res]`,则扩展:`dp[++res] = a[i]`。 - 否则,在`dp[1..res]`中找到第一个大于`a[i]`的位置`pos`(即`upper_bound`返回的位置),然后令`dp[pos] = a[i]`。 这里,`pos = upper_bound(dp+1, dp+res+1, a[i]) - dp`,然后更新`dp[pos] = a[i]`。这样,我们保证了`dp`数组的单调性,并且用更小的值替换,使得后续扩展的可能性更大。 因此,用户代码中的表达式就是用来获取这个位置的索引。 关于优化:使用`upper_bound`本身已经是二分查找,时间复杂度为O(logn),所以效率已经很高。但我们可以考虑以下几点优化: 1. 确保数组`dp`是严格递增的,这样`upper_bound`才能正确工作。 2. 如果数组`dp`的大小(即`res`)很大,可以考虑使用更高效的二分查找实现,但标准库的`upper_bound`已经足够高效。 3. 在循环中,如果`f[i]`比当前`dp`数组的最大值(即`dp[res]`)还要大,那么可以直接添加到末尾,避免二分查找。即: ```c++ if (f[i] > dp[res]) { dp[++res] = f[i]; } else { int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; dp[pos] = f[i]; } ``` 这样可以减少一些不必要的查找(虽然最坏情况下还是O(logn))。 4. 注意数组边界:确保`res`在合理范围内,避免数组越界。 另外,用户提供的引用中,引用[1]和[4]提到了`lower_bound`和`upper_bound`的用法,引用[2]和[5]则展示了一些动态规划的代码,其中引用[2]的代码中使用了`lower_bound`。我们可以参考这些代码来理解。 在引用[2]的代码中,有: ```c++ int now = e[i].x - e[i].len; int num = lower_bound(cf+1, cf+1+n, now) - cf; ``` 这里,`lower_bound`在数组`cf`(从1到n)中查找第一个大于等于`now`的位置,返回该位置的指针,减去`cf`得到下标`num`。如果找不到,则返回`n+1`。 而用户询问的是`upper_bound`,它与`lower_bound`的区别在于: - `lower_bound`:返回第一个大于等于value的元素位置。 - `upper_bound`:返回第一个大于value的元素位置。 因此,在需要严格大于的情况下,使用`upper_bound`。 总结: - 表达式`upper_bound(dp+1, dp+res+1, f[i]) - dp`的作用:在数组`dp`的[1, res]范围内(即从`dp[1]`到`dp[res]`)查找第一个大于`f[i]`的元素的下标(如果找不到,返回`res+1`)。 - 用法:通常用于动态规划中维护单调序列,比如优化最长上升子序列(LIS)问题。 - 优化:可以添加一个判断,如果当前值大于序列最大值则直接扩展,避免二分查找。 最后,根据系统指令,我们需要生成相关问题: §§相关问题§§ 1. 在什么情况下应该使用`upper_bound`而不是`lower_bound`? 2. 如何用`upper_bound`实现最长上升子序列(LIS)的O(nlogn)算法? 3. 在使用`upper_bound`时,如何处理数组下标从0开始的情况? 4. 除了动态规划,`upper_bound`还有哪些常见的应用场景?</think>### `upper_bound(dp+1, dp+res+1, f[i]) - dp` 的作用和用法详解 #### 1. **核心作用** 该表达式在已排序的数组 `dp` 的子区间 `[1, res]` 中,**查找第一个严格大于 `f[i]` 的元素的位置(下标)**。返回值是一个整数下标,范围在 `1` 到 `res+1` 之间: - 若找到满足条件的元素,返回其下标(如 `k`)。 - 若所有元素均不大于 `f[i]`,则返回 `res+1`(表示越界位置)。 #### 2. **关键语法解析** - **`upper_bound(first, last, value)`** - `first`:搜索区间的起始指针(`dp+1` 指向 `dp[1]`)。 - `last`:搜索区间的结束指针(`dp+res+1` 指向 `dp[res]` 的下一个位置,即左闭右开区间 `[dp+1, dp+res+1)`)。 - `value`:待比较的值(`f[i]`)。 - **`- dp` 的意义** 将返回的指针转换为下标(指针差值)。若返回指针为 `dp+k`,则 `(dp+k) - dp = k`(即下标 `k`)。 #### 3. **典型应用场景** 常见于动态规划(DP)中维护**单调递增序列**,例如优化最长上升子序列(LIS)问题: ```cpp int res = 0; // 当前序列长度 for (int i = 1; i <= n; i++) { // 在 dp[1..res] 中找第一个大于 f[i] 的位置 int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; if (pos > res) { dp[++res] = f[i]; // 扩展序列 } else { dp[pos] = f[i]; // 替换为更小的值,保持递增性 } } ``` - **优化效果**:将 LIS 的复杂度从 $O(n^2)$ 降至 $O(n \log n)$[^1][^5]。 #### 4. **与 `lower_bound` 的区别** | 函数 | 返回值条件 | 示例(数组 `[1,3,5]`, `value=3`) | |---------------|--------------------------|-----------------------------------| | `lower_bound` | 第一个 **≥** `value` 的元素 | 返回下标 `2`(元素 `3`) | | `upper_bound` | 第一个 **>** `value` 的元素 | 返回下标 `3`(元素 `5`) | #### 5. **优化建议** 1. **提前判断边界**:若 `f[i]` 大于当前最大值 `dp[res]`,直接扩展序列,避免二分查找: ```cpp if (f[i] > dp[res]) { dp[++res] = f[i]; } else { int pos = upper_bound(dp+1, dp+res+1, f[i]) - dp; dp[pos] = f[i]; } ``` 2. **确保数组有序**:`dp[1..res]` 必须严格递增,否则结果错误。 3. **下标从 0 开始的处理**:若数组下标从 `0` 开始,需调整指针范围: ```cpp // 在 dp[0..res-1] 中查找 int pos = upper_bound(dp, dp+res, f[i]) - dp; ``` #### 6. **示例分析** 假设 `dp = [0, 2, 5, 7]`(有效区间 `[1,3]`),`res=3`: - 若 `f[i] = 4`:`upper_bound` 返回 `dp+3`(元素 `5`),下标 `pos = 3`。 - 若 `f[i] = 9`:返回 `dp+4`(越界),`pos = 4`(需扩展序列)。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值