算法学习:动态点/边分治+[ZJOI2007]Hide 捉迷藏

博客介绍了动态点/边分治算法,并通过[ZJOI2007]捉迷藏的问题进行讲解。动态点分治在不带修改的情况下可以通过树DP解决,但在有修改的情况下,需要通过建立分治树来实现O(logn)的修改复杂度。动态边分治则简化了维护过程,只需要维护两个堆就能得到答案。博客还提供了错误示例和代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态点/边分治算法学习

例题:[ZJOI2007]捉迷藏

luogu
bzoj
题目大意:给一颗树,节点分黑白,开始全黑,给两个操作,要么把一个节点黑白变化,要么询问树上最远黑点距离

动态点分治

呼呼,终于写(chao)完了这道动态点分治的题目。

首先不懂点分治的戳这里(记得把题目也写写,写完再来看这道)

对于这道题我们考虑不带修改的情况,及直接询问树上最远黑点的距离,显然是一个裸的树dp,只要用一个数组f记录每个子树中黑点到子树根最远距离,然后更新答案的时候用最大的加上次大的就可以很简单地解决这道题。显然,这个树dp带有很强的点分治的影子:考虑了每个子树对根的影响。

此时我们考虑修改。如果暴力修改肯定是不行地。我们考虑修改的节点对答案的影响。发现这个节点仅仅会影响到其到跟路径上的节点的答案,不会影响到其他节点的答案。因此,我们每次修改只需要修改这个节点到根节点的答案就可以了。

然而,如果树是一条链,那么我们的算法显然会退化为O(nmk)其中n是点数,m是修改次数,k是修改一个节点信息的复杂度。

因此我们希望有一种算法,使得每次修改的时间复杂度为logn,也就是说,我们希望通过某种手段,使得每个节点修改后只需要修改logn个节点。像这样,对于一颗无根树进行修改和询问,且可以将问题划分到每棵子树上的这类问题,我们有一种算法,叫做动态点分治。

其实,它和点分治一样,只不过把树dp的操作顺序稍微修改了一下。

我们在原树的基础之上,建立出一颗新的“分治树”,这棵分治树的特点是它满足点分治时的操作顺序。也就是说,这颗分治树上每个节点都是其子树上所有节点在原树上形成的连通块的重心。这样显然满足,这颗分治树的树高不会超过logn。那么我们在进行树dp或树上操作的时候,建出分治树并维护分治树上每个节点的信息。修改的时候从待修改的节点往根向上更新,就可以保证修改的logn复杂度。

那么,如何建立这颗分治树呢,其实和点分治几乎一模一样,只不过在点分治的时候,加上一句prt[u] = pa,就可以把这可分治树方便地建立出来,从而在上面进行操作。下面是代码

void get_root(int u, int pa) {
   
   
	son[u] = 1; f[u] = 0;
	for(int i = pre[u]; i;i = e[i].next) 
		if(e[i].to != pa && !vis[e[i].to]) {
   
   
		get_root(e[i].to, u);
		son[u] += son[e[i].to];
		f[u] = max(f[u], son[e[i].to]);
	}
	f[u] = max(f[u], sums - son[u]);
	if(f[u] < f[root]) root = u;
}

void Div(int u, int pa) {
   
   
    prt[u] = pa; vis[u] = 1; int pre_sums = sums; 
    for(int i = pre[u]; i; i = e[i].next) 
    if(!vis[e[i].to]) {
   
   
        if(son[e[i].to] > son[u]) sums = pre_sums - son[u];
        else sums = son[e[i].to];
        root = 0;
        get_root(e[i].to, 0);
        Div(root, u);
    }
}

是不是不可思议地简单!好了,接下来让我们回到这一题。

记得不带修改时的做法?用一个数组f记录每个子树中黑点到子树根最远距离,然后更新答案的时候用最大的加上次大的。显然这里面包含了三层的最大:

  1.  子树中黑点到子树根最大
    
  2.  黑点到到每棵子树最大值加上到父亲距离的最大值
    
  3.  全局每个节点在(2)中的最大值加上次大值
    

对于这三个东西,我们维护三层的堆即可。为了方便,我们把1稍微改一下,改成子树中所有黑点到子树根父亲的最大值,这样子省去了在更新时还要计算子树和父亲的距离(记得所有子树和父亲的距离要重新计算,因为分治树和原树有一个映射的过程)

关于距离,当然是选择rmq最快

关于堆,由于这个堆还要删除,所以可以使用multiset(STL大法好)。然而hzwer采用了两个priority_queue模拟出了multiset,真是吧STL使用得出神入化,orz,于是我get到了这个新技能。

最后说一下我的错误,当时没有考虑到点分治面对的是一棵无根树,于是随便提了个跟并且限定了父亲,在getroot的时候只带了一个参数,判断的时候强行用父子关系卡,结果当然是错掉了qwq

代码

全-代码:好烦好烦啊

#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#include<cmath>
#include<queue>
#define maxn 100005
#define maxm 200005
using namespace std;
int read()
{
   
   
	char ch = getchar(); int x = 0, f = 1;
	while(ch < '0' || ch > '9') {
   
   if(ch == '-') f = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9') {
   
   x = x * 10 + ch - '0'; ch = getchar();}
	return x * f;
}

struct Heap {
   
   
	priority_queue<int>A,B;
	void push(int x) {
   
   A.push(x);}
	void erase(int x) {
   
   B.push(x);}
	void pop() {
   
   
		while(B.size() && A.top() == B.top()) 
			A.pop(), B.pop();
	}
	int top() {
   
   
		pop();
		if(!A.size()) return 0;
		return A.top();
	}
	int size() {
   
   return A.size() - B.size();}
	int stop() {
   
   
		if(size() < 2) return 0;
		int x = top(); A.pop();
		int y = top(); push(x);
		return y;
	}
}A, B[maxn], C[maxn];

int pre[maxn], top;
struct edge
{
   
   
	int to, next;
	void add(int u, int v)
	{
   
   
		to = v; next = pre[u];
		pre[</
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值