天梯赛刷题系列
题目
原题链接
题面
平面上有 2 n 2n 2n 个点,它们的坐标分别是 ( 1 , 0 ) , ( 2 , 0 ) , ⋯ ( n , 0 ) (1,0),(2,0),⋯(n,0) (1,0),(2,0),⋯(n,0) 和 ( 1 , 1 0 9 ) , ( 2 , 1 0 9 ) , ⋯ , ( n , 1 0 9 ) (1,10^9),(2,10^9),⋯,(n,10^9) (1,109),(2,109),⋯,(n,109)。我们称这些点中所有 y y y 坐标为 0 0 0 的点为“起点”,所有 y y y 坐标为 1 0 9 10^9 109 的点为终点。一个机器人将从坐标为 ( x , 0 ) (x,0) (x,0) 的起点出发向 y y y 轴正方向移动。显然,该机器人最后会到达某个终点,我们设该终点的 x x x 坐标为 f ( x ) f(x) f(x)。
在上述条件下,显然有 f ( x ) = x f(x)=x f(x)=x。不过这样的数学模型就太无趣了,因此我们对上述数学模型做一些小小的改变。我们将会对模型进行 q q q 次修改,每一次修改都是以下两种操作之一:
- + x ′ x^′ x′ x ′ ′ x^{′′} x′′ y y y: 在 ( x ′ , y ) (x^′,y) (x′,y) 与 ( x ′ ′ , y ) (x^{′′},y) (x′′,y) 处增加一对传送门。当机器人碰到其中一个传送门时,它会立刻被传送到另一个传送门处。数据保证进行该操作时, ( x ′ , y ) (x^′,y) (x′,y) 与 ( x ′ ′ , y ) (x^{′′},y) (x′′,y) 处当前不存在传送门。
- - x ′ x^′ x′ x ′ ′ x^{′′} x′′ y y y: 移除 ( x ′ , y ) (x^′,y) (x′,y) 与 ( x ′ ′ , y ) (x^{′′},y) (x′′,y) 处的一对传送门。数据保证这对传送门存在。
求每次修改后
∑
x
=
1
n
x
f
(
x
)
\sum_{x=1}^nxf(x)
∑x=1nxf(x) 的值。
xf(x) 的值。
输入格式
第一行输入两个整数
n
n
n 与
q
(
2
≤
n
≤
1
0
5
,
1
≤
q
≤
1
0
5
)
q (2≤n≤10^5, 1≤q≤10^5)
q(2≤n≤105,1≤q≤105),代表起点和终点的数量以及修改的次数。
接下来 q q q 行中,第 i i i 行输入一个字符 o p i op_i opi 以及三个整数 x i ′ x_i^′ xi′, x i ′ ′ x_i^{′′} xi′′ 和 y i ( o p i ∈ ‘ + ’ ( a s c i i : 43 ) , ‘ − ’ ( a s c i i : 45 ) , 1 ≤ x i ′ < x i ′ ′ ≤ n , 1 ≤ y i < 1 0 9 ) y_i(op_i∈{‘+’ (ascii: 43),‘-’ (ascii: 45)}, 1≤x_i^′<x_i^{′′}≤n, 1≤y_i<10^9) yi(opi∈‘+’(ascii:43),‘−’(ascii:45),1≤xi′<xi′′≤n,1≤yi<109),代表第 i i i 次修改的内容。修改顺序与输入顺序相同。
输出格式
输出
q
q
q 行,其中第
i
i
i 行包含一个整数代表第
i
i
i 次修改后的答案。
输入样例:
5 4
+ 1 3 1
+ 1 4 3
+ 2 5 2
- 1 4 3
输出样例:
51
48
39
42
样例解释:
题解
思路
一眼数据结构(当然不是一眼手动狗头)。
该题的渊源是去年同学给我发了一下,当然不会写,当时百度了一下发现有人用splay写的,最近刚学完splay,又快要天梯赛了,所以补一下。
从本质上讲,在两个点建立传送门实际上就是把这两个点的后缀交换,直接上图。
第一个查询:
交换后:
第二个查询:
注意此时是在
1
1
1 和
4
4
4 的
y
y
y 坐标为
3
3
3 处插点,因此交换的是红色和黄色:
交换后得:
对于每次操作,我们先查询当前需要建立传送门的两点的最左边和最右边的值为
l
1
,
r
1
,
l
2
,
r
2
l_1,r_1,l_2,r_2
l1,r1,l2,r2,记录上一次的
a
n
s
ans
ans,因此进行完此次操作后的
a
n
s
=
a
n
s
−
l
1
×
r
1
−
l
2
×
r
2
+
l
1
×
r
2
+
l
2
×
r
1
ans=ans-l_1×r_1-l_2×r_2+l_1×r_2+l_2×r_1
ans=ans−l1×r1−l2×r2+l1×r2+l2×r1。
对于如何将区间插到后面,我们可以用 s p l a y splay splay 来做到区间修改为 l o g n logn logn。
我们可以先预处理所有查询,对其进行离散化,之后在进行操作。
当然数据保证每次插入时没有门,删除时有门,因此操作符号就不用管了。
建树模拟(仍以样例为例):
初始化(数轴上的编号为节点在平衡树中的编号):
此处建树有很多种建树方式,当然可以直接建成一条链,不同的建树方式会影响各个节点的编号。
注:此处取到1e9+10是为了防止当你需要在1e9处建立传送门是没有后缀可移动的尴尬局面。
根据编号,我们的就可以根据每棵树中最前方节点和最后方节点以及他们的颜色(代码中我们将其存储于树的结点中)来算出 a n s ans ans 。
建树:
操作1(交换
1
1
1 号点和
8
8
8 号点后面的部分):
直接改变儿子和父亲的关系即可:
操作2(交换
3
3
3 号点和
11
11
11 号点的后缀):
我们需要先把
3
3
3 号点旋转至根,
3
3
3 号点的后缀即为其右子树,此处即
s
p
l
a
y
splay
splay 操作。
旋转:
改变后缀:
以此类推即可。
代码
#include <bits/stdc++.h>
// #define int long long
using namespace std;
constexpr int P = 998244353;
using i64 = long long;
const int N = 100010;
int n, m;
struct Q
{
int x1, x2, y1, y2; // 记录两个是因为离散化后的值可能不一样
}query[N];
vector<int> ver[N];
struct Node
{
int s[2], p, v;
int size; // 记录以当前节点为根的子树的节点总数
void init(int _v, int _p) {
v = _v, p = _p;
size = 1;
}
}tr[4 * N]; // 10w个询问,每个询问两个点,加上10w个0和10w个1e9+10,一共40w个点
int idx;
void pushup(int u)
{
tr[u].size = tr[tr[u].s[0]].size + tr[tr[u].s[1]].size + 1;
}
int build(int l, int r, int p, int id)
{
int mid = l + r >> 1;
int u = ++ idx;
tr[u].init(id, p);
ver[id][mid] = u;
if (l < mid) tr[u].s[0] = build(l, mid - 1, u, id);
if (mid < r) tr[u].s[1] = build(mid + 1, r, u, id);
pushup(u);
return u;
}
void rotate(int x)
{
int y = tr[x].p, z = tr[y].p;
int k = tr[y].s[1] == x;
tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;
tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
tr[x].s[k ^ 1] = y, tr[y].p = x;
pushup(y), pushup(x);
}
void splay(int x, int k)
{
while (tr[x].p != k) {
int y = tr[x].p, z = tr[y].p;
if (z != k) {
if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
}
rotate(x);
}
}
int get_k(int k, int u)
{
while (u) {
if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
else if (tr[tr[u].s[0]].size + 1 == k) return tr[u].v;
else k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
}
return -1;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) { // 初始化0和1e9+10
ver[i].push_back(0);
ver[i].push_back(1e9 + 10);
}
for (int i = 1; i <= m; i ++ ) { // 读入操作
string op;
int x1, x2, y;
cin >> op >> x1 >> x2 >> y;
query[i] = {x1, x2, y};
ver[x1].push_back(y);
ver[x2].push_back(y);
}
for (int i = 1; i <= n; i ++ ) { // 离散化
sort(ver[i].begin(), ver[i].end());
ver[i].erase(unique(ver[i].begin(), ver[i].end()), ver[i].end());
}
for (int i = 1; i <= m; i ++ ) {
int y = query[i].y1;
query[i].y1 = lower_bound(ver[query[i].x1].begin(), ver[query[i].x1].end(), y) - ver[query[i].x1].begin();
query[i].y2 = lower_bound(ver[query[i].x2].begin(), ver[query[i].x2].end(), y) - ver[query[i].x2].begin();
}
// 建树的同时我们就把各个点的编号同时赋值即可
for (int i = 1; i <= n; i ++ ) {
build(0, ver[i].size() - 1, 0, i);
}
// 初始化ans的值
i64 ans = 0;
for (int i = 1; i <= n; i ++ ) ans += (i64)i * i;
for (int i = 1; i <= m; i ++ ) {
int x1 = query[i].x1, x2 = query[i].x2, y1 = query[i].y1, y2 = query[i].y2;
int u1 = ver[x1][y1], u2 = ver[x2][y2]; // 得出编号
splay(u1, 0), splay(u2, 0); // 将两个点转到0号点下方,即当前这棵splay的树根
pushup(u1), pushup(u2); // 更新根节点的size
int l1 = get_k(1, u1), r1 = get_k(tr[u1].size, u1); // 最左边的值为中序遍历中第一个,最右边的为最后一个
int l2 = get_k(1, u2), r2 = get_k(tr[u2].size, u2);
ans -= (i64)l1 * r1 + (i64)l2 * r2;
ans += (i64)l1 * r2 + (i64)l2 * r1;
// 改变父亲和儿子的关系
tr[tr[u1].s[1]].p = u2;
tr[tr[u2].s[1]].p = u1;
swap(tr[u1].s[1], tr[u2].s[1]);
pushup(u1), pushup(u2);
cout << ans << "\n";
}
return 0;
}