【洛谷P4178】Tree【分块】

本文介绍了一种解决树上点对距离计算问题的方法,通过使用分块、DFS序和区间加减操作来优化算法效率,实现了在O(n√nlog√n)的时间复杂度内求解树上点对间路径长度≤k的问题。

题目:

题目链接:https://www.luogu.com.cn/problem/P4178
给你一棵树,以及这棵树上边的距离,问有多少对点它们两者间的距离小于等于 k k k


思路:

这道题是应该加强数据了。。。
当然如果这道题不打算做点分治模板题的话可以不用
O ( n n   log ⁡ n ) O(n\sqrt n\ \log \sqrt n) O(nn  logn )分块在洛谷优秀的 O 2 O2 O2下过了。。。
我们假设 1 1 1为树根, d f s dfs dfs遍历一遍就可以得到每一个节点 x x x 1 1 1的距离 d i s [ x ] dis[x] dis[x]
然后可以暴力判断 d i s dis dis中有多少个数时小于等于 k k k的。
每一个点作根跑一边,时间复杂度 O ( n 2 ) O(n^2) O(n2)

考虑优化上述算法。
考虑换根。我们把根从 1 1 1转移到 x x x时,所有 x x x子树内的节点到 x x x的距离减少了 d i s ( 1 , x ) dis(1,x) dis(1,x),其他点到 x x x距离增加了 d i s ( 1 , x ) dis(1,x) dis(1,x)
d f s dfs dfs序把每一棵子树的编号变为连续的,那么我们只要在原 d i s dis dis序列中进行区间加减操作就可以了维护出以 x x x为根时,每一个点到 x x x的距离。
那么我们现在需要求整个 d i s dis dis数组中有多少个是 ≤ k \leq k k的,那么其实就是 教主的魔法 那道题了。
采用分块,区间加减容易实现,对于每一个块维护一个 S o r t Sort Sort数组,表示这个块的 d i s dis dis排序后的数组。那么每次修改时, S o r t Sort Sort数组可以在 O ( n   log ⁡ n ) O(\sqrt n\ \log \sqrt n) O(n  logn )的时间复杂度内暴力维护。
注意如果要把一个块整体加减,那么直接在这个块的 a d d add add数组中加减即可,在 d i s dis dis S o r t Sort Sort中都不需要修改。
询问时枚举每一个块 p p p,在这个块的 S o r t Sort Sort中二分出第一个严格大于 k − a d d [ p ] k-add[p] kadd[p]的数的位置 p o s pos pos,那么这个块中小于等于 k k k的数字就有 p o s − 1 pos-1 pos1个。
这样就可以在 O ( n   log ⁡ n ) O(\sqrt n\ \log \sqrt n) O(n  logn )的时间复杂度内求出距离一个点有多少个点路径长度 ≤ k \leq k k。总时间复杂度 O ( n n   log ⁡ n ) O(n\sqrt n\ \log \sqrt n) O(nn  logn )。常数极大。


代码:

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=40010,M=210;
int L[M],R[M],pos[N],add[M],Sort[M][M],head[N],dis[N],Dis[N],rk[N],dfn[N],size[N];
int n,m,T,tot,ans;

struct edge
{
	int next,to,dis;
}e[N*2];

void addedge(int from,int to,int d)
{
	e[++tot].to=to;
	e[tot].dis=d;
	e[tot].next=head[from];
	head[from]=tot;
}

void dfs1(int x,int fa)  //求出每一个点到点1的距离,同时求出dfs序
{
	dfn[x]=++tot; rk[tot]=x; size[x]=1;
	for (register int i=head[x];~i;i=e[i].next)
	{
		int v=e[i].to;
		if (v!=fa)
		{
			Dis[v]=Dis[x]+e[i].dis;
			dfs1(v,x);
			size[x]+=size[v];
		}
	}
}

void update(int l,int r,int val)  //分块区间加模板
{
	int p=pos[l],q=pos[r];
	if (q-p<=1)
	{
		for (register int i=l;i<=r;i++) dis[i]+=val;
		for (register int i=L[p];i<=R[q];i++) Sort[pos[i]][i-L[pos[i]]+1]=dis[i];  //整个块暴力修改
		sort(Sort[p]+1,Sort[p]+1+R[p]-L[p]+1);
		if (p!=q) sort(Sort[q]+1,Sort[q]+1+R[q]-L[q]+1);  //重新维护
		return;
	}
	for (register int i=l;i<=R[p];i++) dis[i]+=val;
	for (register int i=L[q];i<=r;i++) dis[i]+=val;
	for (register int i=L[p];i<=R[p];i++) Sort[p][i-L[p]+1]=dis[i];
	for (register int i=L[q];i<=R[q];i++) Sort[q][i-L[q]+1]=dis[i];  //两边暴力修
	sort(Sort[p]+1,Sort[p]+1+R[p]-L[p]+1);
	sort(Sort[q]+1,Sort[q]+1+R[q]-L[q]+1);
	for (register int i=p+1;i<q;i++)
		add[i]+=val;
}

void dfs2(int x,int fa)  //换根求答案
{
	for (register int i=1;i<=T;i++)
		ans+=upper_bound(Sort[i]+1,Sort[i]+1+R[i]-L[i]+1,m-add[i])-Sort[i]-1;
	for (register int i=head[x];~i;i=e[i].next)
	{
		int v=e[i].to;
		if (v!=fa)
		{
			update(dfn[v],dfn[v]+size[v]-1,-e[i].dis*2);
			update(1,n,e[i].dis);  //换根
			dfs2(v,x);
			update(dfn[v],dfn[v]+size[v]-1,e[i].dis*2);
			update(1,n,-e[i].dis);
		}
	}
}

int main()
{
	memset(head,-1,sizeof(head));
	scanf("%d",&n);
	for (register int i=1,x,y,z;i<n;i++)
	{
		scanf("%d%d%d",&x,&y,&z);
		addedge(x,y,z); addedge(y,x,z);
	}
	scanf("%d",&m);
	tot=0; T=sqrt(n);
	if (T*T<n) T++;
	dfs1(1,0);
	for (register int i=1;i<=T;i++)
	{
		L[i]=R[i-1]+1; R[i]=min(n,T*i);
		for (register int j=L[i];j<=R[i];j++)
			dis[j]=Sort[i][j-L[i]+1]=Dis[rk[j]],pos[j]=i;
		sort(Sort[i]+1,Sort[i]+1+R[i]-L[i]+1);
	}
	dfs2(1,0);
	printf("%d",(ans-n)/2);  //先减去n个(x,x)的点对,然后(x,y)和(y,x)只算一个,所以除以2
	return 0;
}
请把我的题解完善,加上AC代码# P6215 函数求值 这是我的第一篇题解 ## 题目描述 有两个长度均为 $n$ 的权值序列 $a,b$,常数 $p,k$,以及两个函数: $$g(x) = \sum_{i=1}^x p^i \times a_i$$ $$f(x) = \sum_{i=1}^x g(i) ^ k \times b_i$$ 有 $m$ 个操作,操作有以下三种: * $1\ x\ y$,表示将 $a_x$ 修改为 $y$。 * $2\ x\ y$,表示将 $b_x$ 修改为 $y$。 * $3\ x$,表示查询 $f(x)$ 对 $10 ^ 9 + 7$ 取模的值。 --- # **思路** 这道题的关键在于高效维护两个复杂的前缀和函数:g(x) 和 f(x)。我们需要在频繁单点修改的情况下,快速回答前缀和查询。 ## **解题思路分解** * 1.**维护 $g(x)$** $g(x)$ 是 $p^i * a[i]$ 的前缀和,可以用一棵线段树或树状数组来维护,支持: * 2.**单点修改** 前缀和查询(即 $query(1, x)$) 我们可以构建一个线段树 $tree_g$,每个位置保存 $p^i * a[i]$ * 3.**维护 f(x)** $f(x)$ 是 $g(i)^k * b[i]$ 的前缀和。由于 $g(i)$ 依赖前面的 $a[i]$,所以每次 $a[x]$ 被修改,都会影响所有 $i >= x$ 的 $g(i)$,进而影响 $f(i)$。 这个结构是嵌套的依赖关系,直接暴力更新会导致 TLE。 * 4.**优化思路:** 预计算 $g(i)$ 的值,保存在数组中 使用线段树 $tree_f$ 来维护 $f(x)$,每个节点保存的是 $g(i)^k * b[i]$ 的区间和 当 $a[x]$ 或 $b[x]$ 被修改时,只更新 $g(x)$,然后更新$ f(x)$ 对应的值 * 5.**数据结构设计** 线段树 $tree\_g$ 支持: 每个节点保存 $p^i * a[i]$ 的区间和 单点更新 $a[x] $ 区间查询 $g(x) = query(1, x)$ 线段树$ tree\_f$支持: 每个节点保存 $g(i)^k * b[i] $的区间和 单点更新 b[x] _每次 a[x] 更新后,需要重新计算 g(i) 并更新 tree_f_ ## **基础代码:** ~~不常写注释,结果每个注释后面都有分号,打快了hhh~~ ```cpp #include <iostream>//用bits/stdc++.h时间长 using namespace std; const int MAXN = 2e5+10; const int mod = 1e9+7; long long int a[MAXN],b[MAXN]; long long int p; int n,m,k; /* g(x) 是 a[i] * p^i 的前缀和。 f(x) 是 g(i)^k * b[i] 的前缀和。 每次修改 a[x] 或 b[x],都会引起一系列前缀和的变化。 使用线段树可以高效维护区间和,并支持单点修改 + 区间查询。 */ //快速幂===========其实k<=3不用做快速幂,手速太快就打了============= long long mod_pow(long long int a, long long int b) {//位运算优化速度; long long int result = 1; while (b > 0) { if (b & 1) // 如果当前位是1,就乘到结果中; result = result * a % mod; a = a * a % mod; // 底数平方; b >>= 1; // 右移一位,相当于除以2; } return result; } //线段树; //构建两棵线段树: //Tree_g:每个节点维护 p^i * a[i] 的区间和,用于快速计算 g(x); //Tree_f:每个节点维护 g(i)^k * b[i] 的区间和,用于快速计算 f(x); struct Tree{//将函数封装在结构体内便于使用; long long int tree[MAXN << 2]; void pushup(int node) { tree[node] = (tree[node << 1] + tree[node << 1 | 1]) % mod; } bool Inrange(int L, int R, int l, int r) { return (l <= L && R <= r); } bool Outrange(int L, int R, int l, int r) { return (R < l || L > r); } //以上是线段树基础函数; //以下是线段树建树区间查询单点修改函数; void build(long long int *arr, int node, int left, int right) { if (left == right) { tree[node] = arr[left]; return; } int mid = (left + right) >> 1; build(arr, node << 1, left, mid); build(arr, node << 1 | 1, mid + 1, right); pushup(node); } void update(int node, int left, int right, int index, long long int val) { if (left == right) { tree[node] = val; return; } int mid = (left + right) >> 1; if (index <= mid) update(node << 1, left, mid, index, val); else update(node << 1 | 1, mid + 1, right, index, val); pushup(node); } long long int query(int node, int left, int right, int ql, int qr) { if (qr < left || ql > right) return 0; if (ql <= left && right <= qr) return tree[node]; int mid = (left + right) >> 1; return (query(node << 1, left, mid, ql, qr) + query(node << 1 | 1, mid + 1, right, ql, qr)) % mod; } }; Tree tree_g,tree_f; long long int g[MAXN]; long long int f[MAXN]; long long int pow_p[MAXN]; //从x开始更新f,tree_f; void update_tree(int x){ for(int i = x;i<=n;i++){ //查询g[i] ; long long int gi = tree_g.query(1,1,n,1,i); //计算g[i] ^ k; long long int pow_gik = mod_pow(gi,k); tree_f.update(1,1,n,i,(pow_gik*b[i]) % mod ); } } int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin >> n >> m >> p >> k; for(int i = 1;i<=n;i++){ cin >> a[i]; } for(int i = 1;i<=n;i++){ cin >> b[i]; } //pow_p[i] = p^i; pow_p[0] = 1; for(int i = 1;i<=n;i++){ pow_p[i] = pow_p[i-1] * p % mod; } //构建g[i] = p^i * a[i]; for(int i = 1;i<=n;i++){ g[i] = pow_p[i] * a[i] % mod; } tree_g.build(g,1,1,n); //构建f[i] = g[i]^k * b[i]; for(int i = 1;i<=n;i++){ long long int gi = tree_g.query(1,1,n,1,i); long long int pow_gik = mod_pow(gi,k); f[i] = pow_gik * b[i] % mod; } tree_f.build(f,1,1,n); for (int i = 1; i <= m; i++) { int op; int x; long long int y; cin >> op; if (op == 1) { cin >> x >> y; a[x] = y; tree_g.update(1, 1, n, x, pow_p[x] * y % mod); update_tree(x); } else if (op == 2) { cin >> x >> y; b[x] = y; update_tree(x); } else if (op == 3) { cin >> x; long long int ans = tree_f.query(1, 1, n, 1, x); cout << ans << endl; } } return 0;//打return是个好习惯; } ``` 但是$update\_tree()$的复杂度太高了! 最坏情况下总复杂度 $O(m log n)$,必须优化! ___ ### **优化:使用 $g\_cache[i]$ 缓存 $g(i)$ 的值** ##### 当前每次 $update\_tree(x)$ 都要调用 $tree\_g.query(1, i)$ 来计算 $g(i)$,我们可以: * 预处理一个 $g_cache[i]$ 数组,保存当前 $g(i)$ 的值 * 在 $tree\_g.update$ 后同步更新 $g\_cache[i]$ * 这样 $update\_tree(x)$ 就不需要频繁调用 $query$ # **所以完整AC代码** * 分块 + 线段树 + 预处理 ``` ``` 说真的写题解好难~~~~; 感谢洛谷给我这个机会~~;
最新发布
08-02
洛谷 P1970 是一道与算法和数据结构相关的题目,题目的完整描述为:给定一个长度为 $n$ 的序列,要求支持两种操作: 1. `1 x`:将值为 $x$ 的元素插入到序列末尾。 2. `2 x`:查询当前序列中第 $x$ 小的元素的值,并输出该值。 数据范围为 $n \leq 10^5$,操作次数不超过 $10^5$,因此要求算法具有较高的效率。 ### 解法思路 该题的核心在于高效支持插入和查询第 $k$ 小操作。可以采用以下几种高效的数据结构: - **平衡二叉搜索树**(如 `multiset`):利用 `multiset` 可以快速插入元素,并通过 `find_by_order` 和 `order_of_key` 方法(在 `policy_based_data_structure` 中)实现查找第 $k$ 小元素。 - **树状数组**(Fenwick Tree):将元素离散化后,使用树状数组维护当前所有元素的频率分布。插入操作对应单点加,查询第 $k$ 小则通过二分法实现。 - **线段树**(Segment Tree):同样需要离散化,线段树可以在 $O(\log n)$ 时间内完成插入和查询操作。 ### C++ 示例代码 以下是一个使用树状数组实现的解法: ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 2e5 + 5; int tree[MAXN], n, m; // 单点更新 void update(int idx, int val) { while (idx < MAXN) { tree[idx] += val; idx += idx & -idx; } } // 前缀和查询 int query(int idx) { int res = 0; while (idx > 0) { res += tree[idx]; idx -= idx & -idx; } return res; } // 二分查找第k小 int find_kth(int k) { int l = 1, r = MAXN - 1; while (l < r) { int mid = (l + r) / 2; if (query(mid) < k) l = mid + 1; else r = mid; } return l; } int main() { ios::sync_with_stdio(false); cin.tie(0); cin >> n; for (int i = 0; i < n; ++i) { int op, x; cin >> op >> x; if (op == 1) { update(x, 1); } else if (op == 2) { cout << find_kth(x) << "\n"; } } return 0; } ``` ### 总结 本题的关键在于选择合适的数据结构来处理动态查询第 $k$ 小的问题。树状数组结合二分查找是一种高效且实现较为简洁的方法,适用于大规模数据输入的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值