#倍增#洛谷 3252 树

本文介绍了一种使用倍增法结合LCA求最远祖先的方法来解决给定树上路径节点和等于特定值的问题。通过预处理得到每个节点到其2^k个祖先的路径和,可以在O(logN)时间内查询任意节点作为终点时是否存在一条路径的节点之和等于给定值S。

题目

给定一个值S和一棵树。在树的每个节点有一个正整数,问有多少条路径的节点总和达到S。路径中节点的深度必须是升序的。假设节点1是根节点,根的深度是0,它的儿子节点的深度为1。路径不必一定从根节点开始。求路径节点总和为S的路径数量


分析

这道题可以用倍增做,用求LCA的方法,求出f数组,与此同时求出节点向上 2 x 2^x 2x的路径节点总和,然后扫描以某个节点为终点是否能求出答案


代码

#include <cstdio>
using namespace std;
int len[100001][17],f[100001][17],ans,n,s;
int in(){
	int ans=0; char c=getchar();
	while (c<48||c>57) c=getchar();
	while (c>47&&c<58) ans=ans*10+c-48,c=getchar();
	return ans;
}
int main(){
	n=in(); s=in(); int x,y;
	for (int i=1;i<=n;i++) len[i][0]=in();
	for (int i=1;i<n;i++){x=in(),f[in()][0]=x;}	
	for (int j=1;j<17;j++)
	for (int i=1;i<=n;i++){
		f[i][j]=f[f[i][j-1]][j-1];//LCA
		len[i][j]=len[i][j-1]+len[f[i][j-1]][j-1];
	}
	for (int i=1;i<=n;i++){
		x=0; y=i;
		for (int j=16;j>=0;j--){
		    if (len[y][j]+x<=s) x+=len[y][j],y=f[y][j];//可以向上跳
		    if (x==s) {ans++; break;}//找到答案
		}
	}
	return !printf("%d",ans);
}
<think>我们正在讨论状数组的倍增技术。状数组本身是基于二进制的索引结构,而倍增是一种利用状数组特性进行高效查询(如前缀和)的技术。倍增状数组中的一个典型应用是:在单调序列(如前缀和数组是单调递增的)上进行二分查找。例如,我们可以使用倍增来查找前缀和达到某个值的最小位置(类似于lower_bound)。根据引用[2]中提到的“状数组二分”,它实际上是一种倍增方法。其基本思想是从大到小枚举幂次(即从高位到低位),判断当前指针加上该幂次后是否满足条件,如果满足则移动指针,并更新累加值。下面是一个状数组上倍增的模板,用于查找最小的位置pos,使得1到pos的前缀和大于等于某个给定值s(即求前缀和的lower_bound):注意:状数组的索引从1开始。实现步骤:1.初始化一个指针pos=0,一个累加器sum=0。2.枚举幂次(从大到小,即从log(n)到0),每一步尝试将指针移动2的幂次(即step=1<<i),如果移动后不超过数组长度n,且累加当前状数组的值后仍小于s,则移动指针并累加。3.最后,如果我们需要的是前缀和大于等于s的最小位置,那么返回pos+1(因为循环结束时,sum是小于s的最大前缀和,所以下一个位置就是所求位置)。注意:状数组的每个节点存储的是特定区间的和,倍增的过程就是利用状数组的层次结构来快速逼近目标位置。代码实现如下(假设状数组已经建好,数组名为c,大小为n+1):```cpp#include<iostream>#include<cmath>usingnamespacestd;constintmaxn=100000;intn;//数组大小intc[maxn];//状数组//状数组基本操作intlowbit(intx){returnx&-x;}voidadd(inti,intx){while(i<=n){c[i]+=x;i+=lowbit(i);}}intquery(inti){//查询前缀和[1,i]intsum=0;while(i){sum+=c[i];i-=lowbit(i);}returnsum;}//状数组上倍增:查找最小的位置pos,使得前缀和>=sintfind(ints){intpos=0;intsum=0;//计算最大的幂次,2^k<=nintk=0;while((1<<(k+1))<=n)k++;//求出最大的k使得2^k<=nfor(inti=k;i>=0;i--){intnext_pos=pos+(1<<i);if(next_pos<=n&&sum+c[next_pos]<s){//如果加上这个区间后仍然小于s,则移动指针并累加pos=next_pos;sum+=c[next_pos];}}//此时,pos位置的前缀和是小于s的最大前缀和,所以pos+1位置的前缀和>=s(如果存在的话)returnpos+1;}intmain(){//初始化状数组等操作//...return0;}```注意:上面的倍增函数find中,我们直接使用了状数组的c数组。这是因为状数组的每个节点c[i]存储的是区间[i-lowbit(i)+1,i]的和。在倍增过程中,我们直接使用c数组的值,而不需要调用query函数(因为query函数是O(logn)的,而倍增过程本身也是O(logn),如果调用query就会变成O(log^2n))。所以倍增过程中直接使用c数组的值是关键。但是,上面的代码中,我们在倍增过程中直接使用了c数组,这要求我们在状数组更新时,已经维护好了c数组。因此,在更新时我们使用add函数,它会更新所有相关的c数组元素。另外,注意倍增过程中,我们枚举的步长是2的幂次,从大到小枚举。每次尝试移动步长,如果移动后位置不超过n且累加后仍然小于目标值s,则移动并累加。这样,最后我们得到的pos是满足前缀和小于s的最大位置,因此pos+1就是满足条件的最小位置。如果整个数组的前缀和都小于s,那么函数将返回n+1。引用[2]中提到的“状数组二分”正是这种倍增方法。它比线段二分更简洁,代码更短。相关问题:1.状数组倍增的应用场景有哪些?2.状数组倍增和线段二分有什么区别?3.如何理解状数组倍增的时间复杂度?</think>### 状数组倍增模板及实现 状数组的倍增是一种高效查询技术,利用二进制位运算快速定位前缀和的分界点。核心思想是通过**高位到低位的二进制枚举**(类似倍增)实现 $O(\log n)$ 复杂度的单次查询,常用于解决前缀和单调性问题(如求第 $k$ 小元素)。 #### 倍增查询模板代码 ```cpp #include <iostream> using namespace std; const int MAXN = 100010; int tree[MAXN], n; // tree: 状数组, n: 数据规模 int lowbit(int x) { return x & -x; // 获取最低位的1 } void add(int pos, int val) { // 单点更新 for (; pos <= n; pos += lowbit(pos)) tree[pos] += val; } int find_kth(int k) { // 倍增查询:找到前缀和≥k的最小位置 int pos = 0, sum = 0; for (int i = 20; i >= 0; i--) { // 枚举二进制位 (2^20 > 1e6) int next_pos = pos + (1 << i); // 满足三条件:不越界、不超k、区间和有效 if (next_pos <= n && sum + tree[next_pos] < k) { sum += tree[next_pos]; pos = next_pos; } } return pos + 1; // 返回目标位置 } int main() { cin >> n; // 初始化状数组 (需先调用add初始化) // 查询示例: cout << find_kth(5); // 找前缀和≥5的首位置 } ``` #### 关键逻辑说明 1. **二进制枚举** 从高位向低位枚举幂次(`i=20` 到 `0`),相当于按 $2^{20}, 2^{19}, ..., 1$ 的步长试探移动。 2. **安全移动条件** `next_pos = pos + (1 << i)` 需满足: - 位置不越界:`next_pos <= n` - 累加和仍小于目标:`sum + tree[next_pos] < k` - 此时移动指针并累加区间和 3. **定位原理** 循环结束时: - `sum` 是小于 `k` 的最大前缀和 - `pos+1` 是首个满足前缀和 $≥k$ 的位置 #### 应用场景 1. 动态求解第 $k$ 小元素(配合桶计数) 2. 有序序列的快速二分查找 3. 离线查询的优化处理(如区间统计问题) > **复杂度优势**:相比线段二分节省约 50% 常数时间,代码更简洁[^2]。 --- ### 相关问题 1. 状数组倍增和普通二分的性能差异具体体现在哪些场景? 2. 如何利用倍增技术动态维护区间第 $k$ 大元素? 3. 倍增过程中如果遇到非单调前缀和会有什么问题?如何解决? [^1]: 状数组的基础结构参考自一维状数组模板 [^2]: 倍增技术实现参考状数组二分模板
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值