Graph and Queries UVA - 1479

纪念 & 题解

我真的太开心了! 我 AC 了这道 图询问.

这道题困扰了我好久, 我学了 Treap 之后做的第一道题就是这个, 今天! 我花费了 1.5 1.5 1.5 个小时 AC 了这道题! 不看蓝书的代码, 不 ctj!

这给了我一个启示: 努力后的汗水像甘霖一样滋润!

好了, 废话不多说, 开始解决这道题.

题目大意

有一个无向图, 有 N ( N ≤ 2 × 10 4 ) N(N\leq 2\times 10^4) N(N≤2×104) 个点和 M ( M ≤ 6 × 10 4 ) M(M\leq 6\times 10^4) M(M≤6×104) 条边.

然后有三种操作

  • D X 删除编号为 X X X 的边.
  • C X Y 将点 X X X 的点权改为 Y Y Y.
  • Q X K 查询和点 X X X 属于同一联通分量的点, 而且权值是第 K K K 大的点的权值

输入格式

本题为多组数据.

第一行输入两个整数 N N N 和 M M M.

接下来 N N N 行, 每行一个整数 w i w_i wi 表示第 i i i 个点的权值

接下来 M M M 行, 每行用空格隔开的两个整数 From i \text{From}\text{i} Fromi 和 To i \text{To}\text{i} Toi, 表示一条边.

接下来是操作, 每个操作像上面一样描述.最后一个 E 结束操作.

输出格式

为了减少输出, 你只需要将每次 Q 操作的答案求平均值即可.

形式化的讲, 有 p p p 次 Q 操作, 每次的答案为 ans 1 , ans 2 , . . . , ans p \text{ans}_1,\text{ans}_2,...,\text{ans}_p ans1,ans2,...,ansp.

那么你只要输出 ∑ i = 1 p ans i p \frac{\sum_{i=1}^{p}\text{ans}_i}{p} p∑i=1pansi

保留 6 6 6 位小数.

输入样例

复制代码
3 3
10
20
30
1 2
2 3
1 3
D 3
Q 1 2
Q 2 1
D 2
Q 3 2
C 1 50
Q 1 1
E

3 3
10
20
20
1 2
2 3
1 3
Q 1 1
Q 1 2
Q 1 3
E
0 0

输出样例

复制代码
Case 1: 25.000000
Case 2: 16.666667

思路

考虑分析每一个操作, 发现名次树( Rank-Tree \text{Rank-Tree} Rank-Tree), 擅长维护这个东西.

操作 1 1 1 的删除很不好弄, 我们直接强制离线, 每次执行操作 CD, 得到最终的图.

然后我们倒着执行操作, 那么删除就成了插入, 接下来我们一个一个操作的考虑.

  • 查询操作. 因为这是一个联通分量里面查询, 很容易想到对于每一个连通分量搞一个名次树, 然后假设可以做到.
    查询操作就成了查询名次, 这一部分使用名次树维护, 时间复杂度 O ( log ⁡ n ) O(\log n) O(logn)
  • 修改点权, 如果我们知道当前这个点所属的名次树, 那么我们可以进行一次删除, 一次插入, 就成了修改.
    然后要知道这个点所属的名次树, 我们可以使用并查集( Union-Find-Set \text{Union-Find-Set} Union-Find-Set) 实现. 这里的时间复杂度为
    O ( log ⁡ n + K ) O(\log n + K) O(logn+K) 其中 K K K 是并查集的查询复杂度, 可以认为是常数.
  • 插入操作, 这个是最烦人的, 我们用一条边连接两个点, 那么我们就是把这两个点所属的连通分量连起来了, 所以这里要合并 Treap.
    但是合并单次是 O ( n 1 log ⁡ n 2 ) O(n_1\log n_2) O(n1logn2) 的(这里使用的是旋转 Treap)(合并的本质就是将一个树里的每一个元素依次插入).
    可能会超时!

我们好好分析一下合并.

观察到:

  • 当 n 1 < n 2 n_1 < n_2 n1<n2 时, 我们可以将 n 1 n_1 n1 的所有元素插入到 n 2 n_2 n2 里面.
  • 反之, 我们可以把 n 2 n_2 n2 插到 n 1 n_1 n1 里面

大家是不是认为这里只是优化了常数? 不!

下面摘录刘老师在《训练指南》中的一句话.

对于任意节点来说, 每当它被移动到新树时, 树的大小至少加倍, 由于树的节点数不超过 N N N,

任意节点最多移动 N − 2 N-2 N−2 次, 而每次移动要 O ( log ⁡ N ) O(\log N) O(logN) 时间, 所以所有的合并操作的总时间复杂度为
O ( N log ⁡ 2 N ) O(N\log^2N) O(Nlog2N).

妙哉! 这个东西叫做启发式合并, 和并查集的很像.

然后我们就可以写出我们的代码了, 时间复杂度:
O ( N log ⁡ 2 N + q ( log ⁡ N + K ) ) O(N\log^2N+q(\log N+K)) O(Nlog2N+q(logN+K))

就是
O ( N log ⁡ 2 N + q log ⁡ N + q K ) O(N\log^2N+q\log N+qK) O(Nlog2N+qlogN+qK)
q K qK qK一般认为为常数, 略去. 所以时间复杂度为
O ( N log ⁡ 2 N + q log ⁡ N ) O(N\log^2N+q\log N) O(Nlog2N+qlogN)

350 m s 350ms 350ms AC了, 时限 3000 m s 3000ms 3000ms.

Code

cpp 复制代码
// 一只正在 D 代码的 lkz, 代码长度: 6866

#include <stdio.h>
#include <malloc.h>
#include <string.h>

//#define __DEBUG
#ifdef __DEBUG
#define DBG printf
#else
#define DBG(...)
#endif

// 名次树部分
// 左节点 > 右节点 && 父节点ranking > 子节点ranking
struct Node_t;
typedef struct Node_t Node, *PNode;
typedef unsigned long long rand_t;

rand_t _rand() {
	static rand_t Seed = 6667891;		// 种子
	Seed ^= (Seed << 17);
	Seed ^= (Seed >> 7);
	Seed ^= (Seed << 19);
	Seed ^= (Seed << 61);
	return Seed;
}
int max(int x, int y) {return x > y ? x : y;}
int min(int x, int y) {return x < y ? x : y;}

struct Node_t {
	PNode rtSon[2];					// 儿子节点 
	rand_t ranking;						// 优先级
	int val, size, same_count;			// 数的值, 数的大小
};

// 新建节点
PNode NewNode(int val) {
	PNode ret = (PNode) malloc(sizeof(Node));
	ret->val = val, ret->size = ret->same_count = 1, ret->ranking = _rand();
	ret->rtSon[0] = ret->rtSon[1] = NULL;
	return ret;
}
void maintain(PNode* u) {
	(*u)->size = (*u)->same_count;
	if ((*u)->rtSon[0] != NULL) (*u)->size += (*u)->rtSon[0]->size;
	if ((*u)->rtSon[1] != NULL) (*u)->size += (*u)->rtSon[1]->size;
}
// 旋转节点 dir=0左旋 dir=1右旋
void rotate(PNode* root, int dir) {
	PNode NewRoot = (*root)->rtSon[dir ^ 1];	// 新的根节点
	(*root)->rtSon[dir ^ 1] = NewRoot->rtSon[dir];
	NewRoot->rtSon[dir] = *root;	
	maintain(&NewRoot), maintain(root);		// 计算节点的附加信息
	(*root) = NewRoot;
}
// 插入元素
void insert(PNode* root, int val) {
	if ((*root) == NULL) {
		(*root) = NewNode(val);		// 直接新建节点
		return ;
	}
	if ((*root)->val == val) (*root)->same_count ++;
	else {
		int dir = val > (*root)->val ? 0 : 1;
		insert(&(*root)->rtSon[dir], val);
		// 出现不符合堆优先级的情况
		if ((*root)->rtSon[dir]->ranking > (*root)->ranking) rotate(root, dir ^ 1);		// 旋转
	}
	maintain(root);
}
// 删除元素
void erase(PNode* root, int val) {
	// 找到了
	if ((*root)->val == val) {
		if ((*root)->same_count >= 2) (*root)->same_count--;
		else if ((*root)->rtSon[0] == NULL && (*root)->rtSon[1] == NULL) free(*root), (*root) = NULL;		// 左右节点均为空, 直接释放
		else if ((*root)->rtSon[0] != NULL && (*root)->rtSon[1] != NULL) {
			int dir = (*root)->rtSon[0]->ranking > (*root)->rtSon[1]->ranking ? 0 : 1;		// 应该旋转的节点
			rotate(root, dir ^ 1);		// 旋转这个节点上来
			erase(&(*root)->rtSon[dir ^ 1], val);	// 继续删除至根部
		}
		else {
			int dir = (*root)->rtSon[0] != NULL ? 0 : 1;
			PNode tmp = (*root);
			(*root) = (*root)->rtSon[dir];
			
			free(tmp), tmp = NULL;
		}
	}
	else {
		int dir = val > (*root)->val ? 0 : 1;
		erase(&(*root)->rtSon[dir], val);
	}
	if ((*root) != NULL) maintain(root);		// 维护节点大小
}
// 查询名次
int query_rank(PNode root, int rank) {
	if (root == NULL || rank <= 0) return 0;
	
	int greater_cnt = root->rtSon[0] == NULL ? 0 : root->rtSon[0]->size;
	if (rank <= greater_cnt) return query_rank(root->rtSon[0], rank);
	if (rank <= greater_cnt + root->same_count) return root->val;
	return query_rank(root->rtSon[1], rank - greater_cnt - root->same_count);
}
void FreeTree(PNode* root) {
	if ((*root) == NULL) return ;
	FreeTree(&(*root)->rtSon[0]);
	FreeTree(&(*root)->rtSon[1]);
	
	free(*root), (*root) = NULL;
}
// 合并两棵树 将 root2 合并到 root1 里面
void Merge(PNode* root1, PNode root2) {
	if (root2 == NULL) return ;
	for (int i = 1; i <= root2->same_count; i++) insert(root1, root2->val);
	Merge(root1, root2->rtSon[0]);
	Merge(root1, root2->rtSon[1]);
}
int max_tree;
void Print(PNode root, int deep) {
	if (root == NULL) {max_tree = max(max_tree, deep); return ;}
	Print(root->rtSon[1], deep + 1);
	printf("%d ", root->val);
	Print(root->rtSon[0], deep + 1);
}

// 并查集部分
#define MAXN 20010
#define MAXM 60010

int pa[MAXN];
int find(int x) {return pa[x] == x ? pa[x] : (pa[x] = find(pa[x]));}

int N, M;
struct Edge_t {int from, to;}edges[MAXM];
void Main();

int main() {
	
//	freopen("Sample.txt", "r", stdin);
	
	while (scanf("%d %d", &N, &M) == 2 && N && M) Main();
	return 0;
}


int is_remove[MAXM];		// 这个边是不是删除了
int weight[MAXN];			// 点权
PNode Treap[MAXN];			// 维护每个联通分量的答案
struct Cmd_t {char type; int x, y;}cmds[500010];
int CmdCount;		// 命令数量
typedef long long LL, ANS_T, ans_t, ll;

void AddEdge(int edge_idx);	// 加边
void Init();				// 初始化

void Main() {
	Init();
	DBG("Init Ok!\n");
	for (int i = 1; i <= N; i++) scanf("%d", &weight[i]);
	for (int i = 1; i <= M; i++) scanf("%d %d", &edges[i].from, &edges[i].to);
	
	for (CmdCount = 0; scanf(" %c", &cmds[CmdCount].type) == 1 && cmds[CmdCount].type != 'E'; CmdCount++) {
		if (cmds[CmdCount].type == 'D') scanf("%d", &cmds[CmdCount].x), is_remove[cmds[CmdCount].x] = 1;		// 删除节点
		if (cmds[CmdCount].type == 'C') {  		// 改变节点
			scanf("%d %d", &cmds[CmdCount].x, &cmds[CmdCount].y);
			int p = weight[cmds[CmdCount].x];
			weight[cmds[CmdCount].x] = cmds[CmdCount].y;
			cmds[CmdCount].y = p;
		} 
		if (cmds[CmdCount].type == 'Q') scanf("%d %d", &cmds[CmdCount].x, &cmds[CmdCount].y);					// 查询节点
	}
	DBG("Input is OK!\n");
	for (int i = 1; i <= N; i++) insert(&Treap[i], weight[i]);
	DBG("Build tree is OK!\n");
	for (int i = 1; i <= M; i++) if (!is_remove[i]) AddEdge(i);			// 建边
	DBG("Build Graph is OK!\n");
	
	LL ans = 0;
	int total_query = 0;
	static int kase = 0;
	
	for (int i = CmdCount - 1; i >= 0; i--) {		// 因为最后的一个命令是 'E'
		if (cmds[i].type == 'D') AddEdge(cmds[i].x);		// 加边
		if (cmds[i].type == 'C') {
			int idx = find(cmds[i].x);
			// 修改: 一个删除, 一个插入
			erase(&Treap[idx], weight[cmds[i].x]);
			insert(&Treap[idx], weight[cmds[i].x] = cmds[i].y);
		}
		if (cmds[i].type == 'Q') {
			int idx = find(cmds[i].x);
			int ret = query_rank(Treap[idx], cmds[i].y);		// 查询的结果
			DBG("ans=%d\n\n\n", ret);
			ans += ret;
			total_query++;
		}
	}
	printf("Case %d: %.6lf\n", ++kase, (double) ans / total_query);
}

void Init() {
	for (int i = 1; i <= N; i++) pa[i] = i;		// 并查集
	memset(is_remove, 0, sizeof(is_remove[0]) * (M + 5));
	memset(weight, 0, sizeof(weight[0]) * (N + 5));
	for (int i = 1; i <= N; i++) FreeTree(&Treap[i]);
}

// lkz

void AddEdge(int edge_idx) {
	int rt1 = find(edges[edge_idx].from), rt2 = find(edges[edge_idx].to);
	if (rt1 == rt2) return ;		// 在同一个联通分量里面
	
	DBG("find is OK!\n");
	// 启发式合并: 根据子树的大小
	if (Treap[rt1]->size < Treap[rt2]->size) {
		// 把 rt1 合并到 rt2 上面
		DBG("Running Merge1\n");
		Merge(&Treap[rt2], Treap[rt1]);
		FreeTree(&Treap[rt1]);		// 删除这个 Treap 树
		pa[rt1] = rt2;
	}
	else {
		// 反之
		DBG("Running Merge2\n");
		Merge(&Treap[rt1], Treap[rt2]);
		FreeTree(&Treap[rt2]);
		pa[rt2] = rt1;
	}
	DBG("Merge is OK!\n");
}
相关推荐
不忘不弃6 小时前
把IP地址转换为字符串
数据结构·tcp/ip·算法
发疯幼稚鬼6 小时前
网络流问题与最小生成树
c语言·网络·数据结构·算法·拓扑学
Cathy Bryant6 小时前
拉格朗日量:简单系统
笔记·算法·数学建模·高等数学·物理
leoufung6 小时前
LeetCode 63:Unique Paths II - 带障碍网格路径问题的完整解析与面试技巧
算法·leetcode·面试
还不秃顶的计科生7 小时前
力扣hot100第三题:最长连续序列python
python·算法·leetcode
wen__xvn7 小时前
代码随想录算法训练营DAY3第一章 数组part02
java·数据结构·算法
一起养小猫7 小时前
LeetCode100天Day8-缺失数字与只出现一次的数字
java·数据结构·算法·leetcode
梭七y7 小时前
【力扣hot100题】(115)缺失的第一个正数
数据结构·算法·leetcode
嵌入式进阶行者7 小时前
【算法】回溯算法的基本原理与实例:华为OD机考双机位A卷 - 乘坐保密电梯
c++·算法