视频加载失败

安吉D6-T4

2885 字
14 分钟
安吉D6-T4
原题呈现

题目描述#

你需要维护一个序列,初始长度为 nn.序列中的每个元素带有两个标记:权值 aia_i 和颜色 cic_i

本题要求强制在线

接下来有 mm 次操作:

  • 1 l r x:仅关注下标在 [l,r][l, r] 内的元素.把其中权值不大于 xx 的元素按原下标递增顺序取出来,形成一个子序列.对于这个子序列,若相邻两个元素颜色相同,则认为它们属于同一段.求这个子序列共有多少段.
  • 2 x y:在序列尾部插入一个新的元素,其权值为 xx,颜色为 yy

例如:某次查询得到的颜色序列为

[1,4,4,6,6,6,2,5,5][1,4,4,6,6,6,2,5,5]

那么会被分成

[1],[4,4],[6,6,6],[2],[5,5][1], [4,4], [6,6,6], [2], [5,5]

55 段.

输入格式#

从文件 subsequence.in 中读入数据.

第一行三个整数 n,m,kn, m, k,分别表示序列的初始长度、操作次数和是否强制在线.

接下来两行,第一行 nn 个整数,表示 aia_i;第二行 nn 个整数,表示 bib_i(即颜色 cic_i).

接下来 mm 行,每行首先一个整数 tt,表示本次操作的种类.

  • t=1t=1,接下来三个整数 l,r,xl, r, x,描述一次查询.
  • t=2t=2,接下来两个整数 x,yx, y,描述一次插入.

对于 k=0k=0,不要求强制在线. 对于 k=1k=1,要求输入的 l,r,x,yl, r, x, y 均对上一次查询的结果按位异或,即 l=lansl' = l \oplus ans,若上一次查询不存在,则 ans = 0

解码方式示例:

int n, m, k;
cin >> n >> m >> k;
for (int i = 1 ; i <= m ; i++) {
cin >> t;
if (t == 1) {
int l, r, x;
cin >> l >> r >> x;
l ^= ans * k;
r ^= ans * k;
x ^= ans * k;
} else {
int x, y;
cin >> x >> y;
x ^= ans * k;
y ^= ans * k;
}
}

输出格式#

输出到文件 subsequence.out 中.

对于每个操作 11,每行输出一个整数表示查询的结果.

样例#

样例输入 #1#

10 10 0
6 8 5 9 6 10 2 4 8 9
2 4 3 3 4 1 2 3 3 2
1 7 9 7
1 1 4 4
2 2 8
2 1 6
2 10 2
2 10 8
2 8 4
2 3 10
1 5 16 5
1 4 14 7

样例输出 #1#

2
0
5
5

样例输入 #2#

10 20 0
2 19 13 20 7 19 17 8 15 12
1 2 4 3 5 3 2 4 2 2
2 9 3
1 1 9 15
2 17 3
1 1 12 3
2 15 5
1 6 13 6
2 7 1
2 20 3
2 10 1
1 5 9 4
1 1 7 15
2 12 1
2 7 5
2 12 4
1 3 18 6
1 12 12 14
1 7 7 1
2 8 5
1 6 8 10
1 14 18 4

样例输出 #2#

5
1
0
0
3
0
0
0
1
0

数据范围#

对于 100%100\% 的数据,保证:

  • 1n,m3×1051 \le n, m \le 3 \times 10^5
  • 0k10 \le k \le 1
  • 1op21 \le op \le 2
  • 1l,rn1 \le l, r \le n(解码后);
  • 1ai,ci,x,y1091 \le a_i, c_i, x, y \le 10^9

子任务#

本题采用捆绑测试,你必须通过子任务内所有测试点才能获得该子任务分数.

测试点编号n,mn, m \le特殊性质
1101 \sim 1010310^3
112011 \sim 2010510^5
213021 \sim 303×1053\times10^5k=0k=0
314031 \sim 403×1053\times10^5操作 11l=1l=1
415041 \sim 503×1053\times10^5

涉及到区间查询,单点修改,考虑使用树状数组或是线段树,但这道题并非简单的求区间和,于是选择使用线段树.

对于线段树的每一个节点,我们记录一个 vectorvector 中的每一个元素是一个四元组 [x, lcol, rcol, cnt]

  • x 为选择的权值.
  • lcol 是选择权值为 x 时,该区间最左端有效元素的颜色.
  • rcol 是选择权值为 x 时,该区间最右端有效元素的颜色.
  • cnt 是选择权值为 x 时,该区间所有有效元素一共能组成多少段.

将这个四元组类型定义为结构体 Node.将包含 Nodevector 定义为类 NodesNodes 即为线段树中每一个端点的数据类型.

对于该区间内每一个可能的 x,都建一个 Node,保存在该区间节点的 Nodes 中.

对于两个 Node a,ba,b 之间的合并,可以表示为:

  • 对于 x,取 max{a.x,b.x}\max\{a.x,b.x\}
  • 对于 lcol,取 a.lcola.lcol
  • 对于 rcol,取 b.rcolb.rcol
  • 对于 cnt
    • a.rcol=b.lcola.rcol = b.lcol,即两个节点中间部分的颜色可以合并成一大段,答案取 a.cnt+b.cnt1a.cnt + b.cnt-1
    • 否则,取 a.cnt+b.cnta.cnt + b.cnt
friend Node operator+(const Node a, const Node b) {
// 对于所有权值为 0 的点,我们定义它为空 (null),一个 Node 加上空还是该 Node
if (a.x == 0)
return b;
if (b.x == 0)
return a;
Node ans;
ans.x = max(a.x, b.x);
ans.lcol = a.lcol;
ans.rcol = b.rcol;
ans.cnt = a.cnt + b.cnt - (a.rcol == b.lcol);
return ans;
}

对于两个 Nodes m,nm,n(显然,m,nm,n 中的元素按照 .x.x 升序排列)需要合并到 kk,可以采用类似归并排序的方法.其宗旨为:

对于每一个存在于 mm 中的四元组 ii,查找 nn 中最后一个四元组 jj 满足 i.xj.xi.x \geq j.x.此时 jj 中统辖的所有元素权值是节点 nn 所代表区间中所有小于等于 i.xi.x 的元素.将这两个四元组合并,加入到 kk 中,对于每一个存在于 nn 中的四元组也是如此.

在程序中,我们使用双指针,指针 ii 指向 mm,指针 jj 指向 nn.初始时,i,ji,j 都指向 m,nm,n 第一个元素的前一个位置.在每一次循环时,我们需要决定主场节点:

  • 检查两个指针是否指向了两个 vector 的最后一个元素.
    • 若都指向,结束循环.
    • 若指针 ii 指向了 mm 的最后一个元素,则主场节点为 nn
    • 若指针 jj 指向了 nn 的最后一个元素,则主场节点为 mm
    • 若以上情况都不满足,则比较指针 ii 指向的下一个元素 pp 和指针 jj 指向的下一个元素 qq 的大小
      • p<qp<q 则主场节点为 nn
      • p>qp>q 则主场节点为 mm
      • 否则,主场节点同时为 m,nm,n
  • 在决定主场节点之后,将主场节点对应的指针向后移动一次.
  • 最后,将 ii 指向的元素和 jj 指向的元素合并,加入节点 kk
friend Nodes operator+(const Nodes a, const Nodes b) {
Nodes ans;
ans.l = a.l;
ans.r = b.r;
int i = -1, j = -1;
while (true) {
Node tmp;
bool isInEndA = i + 1 >= a.size();
bool isInEndB = j + 1 >= b.size();
if (isInEndA && isInEndB)
break;
if (isInEndB || (!isInEndA && a[i + 1].x < b[j + 1].x)) {
i++;
tmp = a[i];
if (j != -1) {
tmp = tmp + b[j];
}
} else if (isInEndA || a[i + 1].x > b[j + 1].x) {
j++;
tmp = b[j];
if (i != -1) {
tmp = a[i] + tmp;
}
} else {
i++;
j++;
tmp = a[i] + b[j];
}
ans.push_back(tmp);
}
return ans;
}

建树和修改的代码和普通线段树大差不差,因为没有区间修改,所以都不需要 pushup

由于最多会有 mm 次插入操作,所以要预留出这些空间.线段树的大小要开 m+nm+n

void build(int t, int l, int r) {
f[t].l = l, f[t].r = r;
if (l == r) {
if (l > n) { // 大于 n 的点当前还是空的
return;
}
Node nd;
nd.x = a[l];
nd.lcol = b[l];
nd.rcol = b[l];
nd.cnt = 1;
f[t].push_back(nd);
return;
}
int mid = (l + r) / 2;
build(t * 2, l, mid);
build(t * 2 + 1, mid + 1, r);
pushup(t);
}
void pushup(int t) { f[t] = f[t * 2] + f[t * 2 + 1]; }
void Modify(int t, int pos, int a, int b) {
if (f[t].l == pos && f[t].r == pos) {
f[t].push_back({a, b, b, 1});
return;
}
if (f[t].l > pos || f[t].r < pos) {
return;
}
Modify(t * 2, pos, a, b);
Modify(t * 2 + 1, pos, a, b);
pushup(t);
}

至于查询操作,需要注意:

  • 若当前区间被查询区间完全覆盖,则需要使用 upper_bound 找到当前 Nodes 中所有的 Node,满足权值 xx 小于等于查询权值 qxqx,其中权值最大的一个.将这个节点返回.
  • 若当前区间和查询区间完全没有交集,则返回一个空的 Node(在本程序中,权值为 0 的 Node 为空的 Node,权值默认为 0),表示什么都没有.
Node Query(int t, int x, int y, int tgt) {
if (f[t].l >= x && f[t].r <= y) {
auto it = upper_bound(f[t].begin(), f[t].end(), tgt);
if (it == f[t].begin())
return Node();
return *(it - 1);
}
if (f[t].l > y || f[t].r < x) {
return Node();
}
return Query(t * 2, x, y, tgt) + Query(t * 2 + 1, x, y, tgt);
}

这样线段树就建完了.

在处理查询时,调用线段树的 Query 方法,收到节点中的 cnt 即为答案.

在处理插入时,直接调用线段树的 Modify 方法即可.

但是这样每次插入一个节点,每一层都会调用一次 pushuppushup 复杂度最高 O(n)O(n),每一次查询的复杂度为 O(nlogn)O(n\log n)qq 次查询的复杂度为 O(nqlogn)O(nq\log n),不如暴力.

仔细想想,对于一个区间,只有其中的全部元素都被插入之后,这个区间才会被访问到.因此,只需要在 pos=rpos=r 的时候执行一次 pushup 即可.

void Modify(int t, int pos, int a, int b) {
if (f[t].l == pos && f[t].r == pos) {
f[t].push_back({a, b, b, 1});
return;
}
if (f[t].l > pos || f[t].r < pos) {
return;
}
Modify(t * 2, pos, a, b);
Modify(t * 2 + 1, pos, a, b);
if (f[t].r == pos) {
pushup(t);
}
}

标程

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 100;
const int INF = 0x3f3f3f3f;
int n, m, k, a[2 * N], b[2 * N];
int l, r, x, y, ans;
struct SegmentTree {
struct Node {
int x = 0, lcol = 0, rcol = 0, cnt = 0;
friend Node operator+(const Node a, const Node b) {
if (a.x == 0)
return b;
if (b.x == 0)
return a;
Node ans;
ans.x = max(a.x, b.x);
ans.lcol = a.lcol;
ans.rcol = b.rcol;
ans.cnt = a.cnt + b.cnt - (a.rcol == b.lcol);
return ans;
}
friend bool operator<(const int a, const Node b) { return a < b.x; }
};
class Nodes : public vector<Node> {
public:
int l, r;
friend Nodes operator+(const Nodes a, const Nodes b) {
Nodes ans;
ans.l = a.l;
ans.r = b.r;
int i = -1, j = -1;
while (true) {
Node tmp;
bool isInEndA = i + 1 >= a.size();
bool isInEndB = j + 1 >= b.size();
if (isInEndA && isInEndB)
break;
if (isInEndB || (!isInEndA && a[i + 1].x < b[j + 1].x)) {
i++;
tmp = a[i];
if (j != -1) {
tmp = tmp + b[j];
}
} else if (isInEndA || a[i + 1].x > b[j + 1].x) {
j++;
tmp = b[j];
if (i != -1) {
tmp = a[i] + tmp;
}
} else {
i++;
j++;
tmp = a[i] + b[j];
}
ans.push_back(tmp);
}
return ans;
}
} f[N * 8];
void build(int t, int l, int r) {
f[t].l = l, f[t].r = r;
if (l == r) {
if (l > n) {
return;
}
Node nd;
nd.x = a[l];
nd.lcol = b[l];
nd.rcol = b[l];
nd.cnt = 1;
f[t].push_back(nd);
return;
}
int mid = (l + r) / 2;
build(t * 2, l, mid);
build(t * 2 + 1, mid + 1, r);
pushup(t);
}
void pushup(int t) { f[t] = f[t * 2] + f[t * 2 + 1]; }
void Modify(int t, int pos, int a, int b) {
if (f[t].l == pos && f[t].r == pos) {
f[t].push_back({a, b, b, 1});
return;
}
if (f[t].l > pos || f[t].r < pos) {
return;
}
Modify(t * 2, pos, a, b);
Modify(t * 2 + 1, pos, a, b);
if (f[t].r == pos) {
pushup(t);
}
}
Node Query(int t, int x, int y, int tgt) {
if (f[t].l >= x && f[t].r <= y) {
auto it = upper_bound(f[t].begin(), f[t].end(), tgt);
if (it == f[t].begin())
return Node();
return *(it - 1);
}
if (f[t].l > y || f[t].r < x) {
return Node();
}
return Query(t * 2, x, y, tgt) + Query(t * 2 + 1, x, y, tgt);
}
} sgTree;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
cin >> b[i];
}
sgTree.build(1, 1, n + m);
int cnt = n;
for (int i = 1; i <= m; i++) {
int t;
cin >> t;
if (t == 1) {
cin >> l >> r >> x;
l ^= ans * k;
r ^= ans * k;
x ^= ans * k;
int nds = sgTree.Query(1, l, r, x).cnt;
ans = nds;
cout << ans << endl;
} else {
cin >> x >> y;
x ^= ans * k;
y ^= ans * k;
sgTree.Modify(1, ++cnt, x, y);
}
}
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

安吉D6-T4
https://blog.jerrylab.top/posts/problem/anji2026/D6/T4/
作者
Jerry
发布于
2026-08-07
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Jerry
Hello, I'm Jerry.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
80
分类
3
标签
35
总字数
89,198
运行时长
0
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0