纪念 & 题解
我真的太开心了! 我 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 的删除很不好弄, 我们直接强制离线, 每次执行操作 C 和 D, 得到最终的图.
然后我们倒着执行操作, 那么删除就成了插入, 接下来我们一个一个操作的考虑.
- 查询操作. 因为这是一个联通分量里面查询, 很容易想到对于每一个连通分量搞一个名次树, 然后假设可以做到.
查询操作就成了查询名次, 这一部分使用名次树维护, 时间复杂度 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");
}