草庐IT

SPOJ Query On A Tree IV 题解

lsty 2023-03-28 原文

SPOJ Query On A Tree IV 题解

一个边分治套线段树套堆的题目

比较难写但是有不小的启发

思路来源和代码都抄自 [SPOJ-QTREE4]QUERY ON A TREE IV 题解 | KSKUN' s Blog

简要题意

给定一个 \(n\) 个点的带边权树, 点的编号为 \(1\sim n\), 初始树上所有节点都是白色的, 要求维护两个操作:

  1. \(\rm {C\ a}\) 反转 \(a\) 节点的颜色(白色变成黑色或者黑色变成白色)
  2. \(\rm A\) 查询树上最远的两个白点的距离

特别的,进行 \(\rm A\) 操作时如果树上没有白点输出 They have disappeared.

\(N\le 10^5, Q\le 10^5, -10^3\le c\le 10^3\)

题目链接
谷的RMJ
原地址

做法

在线版:

树上的问题太难了,先把这个问题丢到一个序列里面做看看有没有什么启发性的做法

问题变成给你一个 \(01\) 序列,询问变成每次反转一个数,以及查询两个距离最远的 \(0\) 相隔的距离

这个问题如果是静态的(忽略操作 \(1\))就是双指针扫一下两端第一个 \(0\) 的位置就可以了

但是这个做法没什么前途,因为在带修的情况下每次都要重新确定答案(这相当于维护序列的前缀和和后缀和,每次修改要变动很多东西),而且放到树上不太现实。

考虑一种过中点的分治,每次取序列的中点,把序列分为左半和右半两个子序列,维护左半子序列和右半子序列所有为 \(1\) 的点到中点的距离,左半和右半各取一个 \(\max\) 合起来就是横跨中点的答案,然后递归左子序列和右子序列,那么当前序列的答案就是 \(\max(self\_max,left\_max,right\_max)\)

考虑这个分治的原因是一个点至多属于 \(\log\) 个子序列,修改是 \(\log\)

这个种分治带修很好做,这东西长得就像线段树的形式自然可以像线段树那样维护

至于带修,每个节点维护左子序列所有点到中点的距离的一个大根堆,右子序列所有点到中点距离的一个大根堆,把堆顶所有黑色节点弹出,取两个堆顶即可,修改的时候,白改黑就是把点置为黑色,更新答案,黑改白就是把点置为白色,把点重新插进堆里面去,更新答案

好,序列上做完了考虑把这东西塞到树上去

原来序列过中点分治现在显然就是边分治(因为每次要把树分成两半,多了不好合并答案)

对分出来的两个子树分别开个堆维护最大值,然后接下来对子树再进行分治,和上面完全是一模一样的道理

注意有两个问题就是树上跟序列上不太一样,给一个点不好确定它在每一层中心边的左侧还是右侧,这个可以对每个点开一个长度为 \(\log\) 的数组,然后维护它每一层属于哪条中心边的哪一侧

而且也不好直接做线段树的区间编号,需要用类似动态开点的方式维护编号,更新很难递归,直接自底向上更新就好了

具体可以看一下示例代码

#include <iostream>
#include <algorithm>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <queue>
#include <vector>

typedef long long ll;

const int N = 1e5 + 10, M = 2e5 + 10, inf = 0x7f7f7f7f;
int h[N], e[M], ne[M], w[M], idx;
int n;

void add(int x, int y, int c){
	e[idx] = y, ne[idx] = h[x], w[idx] = c, h[x] = idx++;
}

namespace new_tree{
	int h[N << 1], e[M << 1], ne[M << 1], w[M << 1], idx;
	int NodeNum;
	//节点的颜色, false为白, true为黑
	bool color[N << 1];
	int WhiteNum;

	void add(int x, int y, int c){
		e[idx] = y, ne[idx] = h[x], w[idx] = c, h[x] = idx++;
	}
	
    //重建树
	void rebuild(int u, int fa){
		int ff = 0;
		for(int i = ::h[u]; ~i; i = ::ne[i]){
			int v = ::e[i];

			if(v == fa)
				continue;
			if(!ff){
				add(u, v, ::w[i]);
				add(v, u, ::w[i]);
				ff = u;
			}
			else{
				int tmp = ++NodeNum;
				color[tmp] = 1;
				add(tmp, ff, 0);
				add(ff, tmp, 0);
				add(tmp, v, ::w[i]);
				add(v, tmp, ::w[i]);
				ff = tmp;
			}
			rebuild(v, u);
		}
	}

	/****************树上分治部分****************/
	bool vis[M << 1];
	int siz[N << 1];
	int MinMax;
	int core;

	/********************算中心边*********************/
	void getsiz(int u, int fa){
		siz[u] = 1;
		for(int i = h[u]; ~i; i = ne[i]){
			int v = e[i];
			if(vis[i] || v == fa)
				continue;

			getsiz(v, u);
			siz[u] += siz[v];
		}
	}


	void getcore(int u, int tot, int fa){
		for(int i = h[u]; ~i; i = ne[i]){
			int v = e[i];
			if(v == fa || vis[i])
				continue;
			int Maxn = std::max(siz[v], tot - siz[v]);

			if(Maxn < MinMax){
				MinMax = Maxn;
				core = i;
			}

			getcore(v, tot, u);
		}
	}


	/******************分治结构需要维护的信息**************/
	//树上边分治每次把树分成两个部分
	//对每个结构开一个优先队列维护双端最大值
	//具体来说, 结构类似于线段树
	struct dis_info{
		int u, d;
		bool operator < (const dis_info &b) const {
			return d < b.d;
		}
	};

	std::priority_queue<dis_info> q[N << 1][2];
	int cnt;

	struct Node{
		//div 是一个分治结构的编号
		int div, side, dis;
	};
	//对于每个节点, 维护每层它属于哪个分治结构(所属中心边编号), 方便更新
	//类似于线段树, 维护每层它属于哪个区间
	//log层, 空间n log n
	
	std::vector<Node> node[N << 1];

	struct Seg_Node{
		int ls, rs, core_w, maxn;
	}tr[N << 1];
	/********************更新分治结构信息******************/

	//p是分治结构编号
	void upd(int p){
		while(!q[p][0].empty() && color[q[p][0].top().u])
			q[p][0].pop();
		while(!q[p][1].empty() && color[q[p][1].top().u])
			q[p][1].pop();

		//如果左侧或者右侧没有白点
		if(q[p][0].empty() || q[p][1].empty())
			tr[p].maxn = 0;
		else
			tr[p].maxn = q[p][0].top().d + q[p][1].top().d + tr[p].core_w;

		//和左右子树取max
		if(tr[p].ls)	tr[p].maxn = std::max(tr[p].maxn, tr[tr[p].ls].maxn);
		if(tr[p].rs)	tr[p].maxn = std::max(tr[p].maxn, tr[tr[p].rs].maxn);
	}

	void set_white(int u){
		//自底向上
		for(int i = node[u].size() - 1; i >= 0; i--){
			Node d = node[u][i];
			//把节点重新入队
			q[d.div][d.side].push({u, d.dis});
			upd(d.div);
		}
	}

	void set_black(int u){
		//自底向上
		for(int i = node[u].size() - 1; i >= 0; i--){
			Node d = node[u][i];
			upd(d.div);
		}
	}
	/**********************裁剪分治结构********************/

	//计算结构内信息
	//t是结构编号, side是在结构哪一侧
	void calc_dis(int u, int fa, int d, int t, int side){
		//白点需要计算距离
		if(!color[u]){
			q[t][side].push({u, d});
			node[u].push_back({t, side, d});
		}
		for(int i = h[u]; ~i; i = ne[i]){
			int v = e[i];
			if(v == fa || vis[i])
				continue;
			calc_dis(v, u, d + w[i], t, side);
		}
	}

	int dfs(int x){
		MinMax = NodeNum;
		getsiz(x, 0);

		core = -1;
		getcore(x, siz[x], 0);

		//没有边可以找了
		if(core == -1)	return 0;
		int from = e[core], to = e[core ^ 1];
		
		//删掉这一条边
		vis[core] = vis[core ^ 1] = true;

		//新结构的编号
		int t = ++cnt;
		tr[t].core_w = w[core];
		calc_dis(from, 0, 0, t, 0);
		calc_dis(to, 0, 0, t, 1);

		//构建分治结构
		tr[t].ls = dfs(from);
		tr[t].rs = dfs(to);
		upd(t);
		return t;
	}

	/****************主体框架部分****************/
	void solve(){
		WhiteNum = ::n;
		memset(h, -1, sizeof h);
		NodeNum = ::n;
		rebuild(1, -1);
		
		dfs(1);
		int query;

		scanf("%d", &query);
		for(int i = 1; i <= query; i++){
			char op[2];
			scanf("%s", op);

			if(op[0] == 'A'){
				if(!WhiteNum){
					puts("They have disappeared.");
				}
				else if(WhiteNum == 1){
					puts("0");
				}
				else{
					printf("%d\n", tr[1].maxn);
				}
			}

			else{
				int u;
				scanf("%d", &u);
				color[u] ^= 1;
				if(color[u]){
					set_black(u);
					WhiteNum--;
				}
				else{
					set_white(u);
					WhiteNum++;
				}
			}
		}
	}
}

int main(){
	memset(h, -1, sizeof h);
	scanf("%d", &n);
	for(int i = 1; i < n; i++){
		int x, y, c;
		scanf("%d%d%d", &x, &y, &c);
		add(x, y, c), add(y, x, c);
	}
	new_tree::solve();
	return 0;
}

这种比较大型的数据结构代码一定要想清楚实现细节再写,搞清楚在干什么再动笔

否则会调很久很久

离线版

上面的在线做法好像不太聪明啊

上面的做法相当于每一次询问都对整个树重新做了一次边分治,只是注意到了每次至多修改 \(\log\) 层的信息所以可以用线段树做到比较快的修改而已

点分治板子里我们意识到一件事情

树上分治是可以 \(Q\) 个询问一起做的,因为每次分治得到的两侧信息不会有太大的变化,没必要重新算一遍,可以重复利用

所以其实可以把线段树砍掉,不需要每次维护修改对全局答案的影响,直接 \(Q\) 个询问一起做,直接边分治,每次两侧的答案用一个大根堆维护,然后依次执行询问,还是类似,白变黑直接变,黑变白先把点插进堆里再变,然后每次询问把堆顶所有黑点弹出然后再取堆顶就行了

使用一些配对堆或者斐波那契堆科技可以把复杂度再干掉一个 $\log $(堆插入), 时间复杂度是 \((n+Q)\log n\), 空间复杂度如果及时析构可以做到 \(O(n)\),大概可以卡一个最优解了

代码没写,只写了在线版的

有关SPOJ Query On A Tree IV 题解的更多相关文章

  1. 【算法题解】20. 两数之和 - 2

    这是一道简单题题目来自:https://leetcode.cn/problems/two-sum/题目给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。提示:22nums.length104−109−109nums[i]109−109−109target109只会存在一个有效答案进阶:你可以想出一个时间复杂度小于O(n2)O(n^2)O(n2)的算法吗?示例1:输入:nums=[2,7,11,15],targe

  2. 蓝桥杯第十四届省赛完整题解 C/C++ B组 - 2

    没有测评,不知道对不对,仅仅过样例而已试题A:日期统计本题总分:5分【问题描述】小蓝现在有一个长度为100的数组,数组中的每个元素的值都在0到9的范围之内。数组中的元素从左至右如下所示:5686916124919823647759503875815861830379270588570991944686338516346707827689565614010094809128502533现在他想要从这个数组中寻找一些满足以下条件的子序列:   1.子序列的长度为8;   2.这个子序列可以按照下标顺序组成一个yyyymmdd格式的日期,并且要求这个日期是2023年中的某一天的日期,例如202309

  3. 2023第十四届蓝桥杯C/C++B组省赛题解 - 2

    2023蓝桥C/C++B组省赛文章目录2023蓝桥C/C++B组省赛试题A:日期统计题目描述枚举参考代码试题B:01串的熵题目描述枚举|模拟参考代码试题C:冶炼金属题意描述取交集参考代码试题D:飞机降落题意描述DFS+剪枝,懒得写试题E:接龙数列题意描述DP参考代码试题F:岛屿个数题意描述dfs|连通块参考代码试题G:子串简写题意描述前缀和参考代码试题H:整数删除题意描述双向链表|最小堆参考代码试题I:景区导游题意描述带权LCA参考代码试题J:砍树题意描述树上差分参考代码试题A:日期统计题目描述【问题描述】小蓝现在有一个长度为100的数组,数组中的每个元素的值都在0到9的范围之内。数组中的元素

  4. LeetCode——链表简单题题解 - 2

    83.删除排序链表中的重复元素题目描述给定一个已排序的链表的头head,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。输入:head=[1,1,2]输出:[1,2]解题思路:用一个指向节点类型的指针保存头结点,用另一个指向节点类型的指针对该链表进行遍历,由于是有序的,当出现不同的值就说明不会再出现跟前面的值相同的节点了,最后循环结束的条件是遍历到最后一个节点的时候,也就是该节点的next指向空的时候,停止循环,返回该保存的头结点,另外,如果传过来的头结点是空,则直接返回空。参考代码:/***Definitionforsingly-linkedlist.*structListNod

  5. NEUQ week 12 题解 - 2

    P1776宝物筛选宝物筛选题目描述终于,破解了千年的难题。小FF找到了王室的宝物室,里面堆满了无数价值连城的宝物。这下小FF可发财了,嘎嘎。但是这里的宝物实在是太多了,小FF的采集车似乎装不下那么多宝物。看来小FF只能含泪舍弃其中的一部分宝物了。小FF对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小FF有一个最大载重为WWW的采集车,洞穴里总共有nnn种宝物,每种宝物的价值为viv_ivi​,重量为wiw_iwi​,每种宝物有mim_imi​件。小FF希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。输入

  6. 2023第十四届蓝桥杯 C/C++大学生A组省赛 满分题解 - 2

    写在前面以下代码,目前均可通过民间OJ数据(dotcpp&NewOnlineJudge),两个OJ题目互补,能构成全集,可以到对应链接下搜题提交(感谢OJ对题目的支持)如果发现任何问题,包含但不限于算法思路出错、OJ数据弱算法实际超时、存在没考虑到的边界情况等,请及时联系作者​​题解A.幸运数(模拟)题面​题解 由于是填空题,按题意本地暴力,几秒就跑出来结果了,直接交结果代码#includeusingnamespacestd;intans;intmain(){ /* for(inti=1;iB.有奖问答(搜索/dp)题面​题解1.搜索:2的30次方种可能,每次要么+10要么清零,遇到100分时

  7. 【独家】华为OD机试提供C语言题解 - 箱子之形摆放 - 2

    最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理已参加机试人员的实战技巧文章目录最近更新的博客使用说明箱子之形摆放题目输入输出示例一输入输出说明备注Code

  8. 华为机试(6.17笔试题解析) - 2

    华为机试一共三道题,分值分别是100,100,200,满分400分,限时2.5小时。我抽到的这三题相对来说比较简单,满分通过,这里做个总结:第一题:数据分类■ 题目描述 对一个数据a进行分类,分类方法为:此数据a(四个字节大小)的四个字节相加对一个给定的值b取模,如果得到的结果小于一个给定的值c,则数据a为有效类型,其类型为取模的值;如果得到的结果大于或者等于c,则数据a为无效类型。比如一个数据a=0x01010101,b=3,按照分类方法计算(0x01+0x01+0x01+0x01)%3=1,所以如果c=2,则此a为有效类型,其类型为1,如果c=1,则此a为无效类型;又如一个数据a=0x01

  9. 2023年第十四届蓝桥杯省赛Java C组题解 - 2

    只做出来(ACDFGH),挑几个出来,答案不一定正确,但自己测试通过了A、求和求1~20230408的和publicclassMain{ publicstaticvoidmain(String[]args){System.out.println((long)20230409*10115204); }}这里就直接套等差数列的求和公式,答案:204634714038436 D、平均【问题描述】        有一个长度为n的数组(n是10的倍数),每个数Ai都是区间[0,9]中的整数,小明发现数组里每种数出现的次数不太平均,而更改第i个数的代价为bi,他想更改着若干个数的值使得这10种数出现的次数

  10. 2016年 团体程序设计天梯赛——题解集 - 2

    前言:Hello各位童学大家好!😊😊,茫茫题海你我相遇即是缘分呐,或许日复一日的刷题以及让你疲惫甚至已经厌倦了,但是我们真的真的达到极限了吗?少一点自我感动,没有结果前别太松懈,请相信”一万小时定理“。当你迷茫时抬头看看远方回想当初那个稚嫩脸庞的少年所仰望的目标😇😇,理想主义终需在现实里才能真正实现,接下来让我们静下心来刷题吧,体验学习的快感!🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🏆题目传送门⭐L1-028判断素数(10分)⭐L1-031到底是不是太胖了(10分)⭐L1-025正整数A+B(15分)⭐L1-030一帮一(15分)⭐L1-027出租(20分)⭐L1-032Left-p

随机推荐