前置知识

线段树:通过懒惰标记,可实现区间处理,和区间询问皆为\(O(logn)\)时间复杂度的数据结构,是一种二叉树。因此对于一个节点\(st\),其左儿子节点为\(st*2\),右节点为\(st*2+1\),为方便整洁,代码中使用宏定义\(ls(st<<1)\)和\(rs(st<<1|1)\)。

离散化:将无法通过开数组标记的大数据变为小数据的一种手段。

分析

\(20\)分攻略

算法:模拟

由于\(n,m\leq 1000\),此时\(O(nm)\)的算法是允许的,所以对于每一种操作用循环模拟即可。

\(40\)分攻略

算法 :差分+树状数组 \(OR\) 线段树+懒惰标记\(*1\)

\(n\leq 100000,m\leq 40000\),且只包含\(1,4\)操作。需要使用时间复杂度带\(log\)的算法,使用数据结构树状数组+差分或者线段树+懒惰标记即可。

线段树传递懒惰标记时,要乘以区间长度。由于有三维,所以开三个线段树即可,时间复杂度\(O(mlogn)\)。

struct node {
int lazS, lazM, sum;
} t[3][maxn << 3];

\(60\)分攻略

算法:线段树+懒惰标记\(*2\)

\(n\leq 100000,m\leq 40000\),且只包含\(1,2,4\)操作。同样需要使用时间复杂度带\(log\)的算法,但是此时有两种操作,树状数组不能应用于如此复杂的情况。

使用线段树时,需要考虑两个懒惰标记相互的影响。设加法懒惰标记为\(lazS\),乘法懒惰标记为\(lazM\)。由于标记的实际含义是保留左节点和右节点的未操作信息,所以懒惰标记下传时,由乘法分配律,\(lazS\)要乘以\(lazM\),这样可以保证在\(lazS\)下传的时候,不会丢失\(lazM\)的信息。

考虑标记下传时是先进行乘法操作还是先进行加法操作。

  • 若是先进行加法操作,会出现将乘法操作后的新的加法标记同时乘上乘法标记。
  • 若是先进行乘法操作,将乘法操作先作用于原有的和上,然后再将加法操作作用上去即可。
#define ls st<<1
#define rs st<<1|1
void push_down(int st);
t[i][ls].sum = ((t[i][ls].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[ls]) % mod;//先进行乘法操作,再进行加法操作
t[i][rs].sum = ((t[i][rs].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[rs]) % mod;//先进行乘法操作,再进行加法操作
t[i][ls].lazS = ((t[i][ls].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
t[i][rs].lazS = ((t[i][rs].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
t[i][ls].lazM = t[i][ls].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
t[i][rs].lazM = t[i][rs].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
t[i][st].lazM = 1;//清除懒惰标记
t[i][st].lazS = 0;//清除懒惰标记

时间复杂度\(O(mlogn)\),此时题目为洛谷\(P3372\)。

\(80\)分攻略

算法:线段树+懒惰标记\(*3\)

\(n\leq 100000\),引入转换操作,同样需要使用时间复杂度带\(log\)的算法。

首先先考虑没有\(1,2\)操作时应该如何做,注意到每次操作由原来的\(x,y,z\)变为\(y,z,x\)相当于原来的三维元素向左平移一维,那么如果向左平移三次,就相当于没有平移,所以转换的转移标记\(lazS\)进行\(\%3\)操作即可。

再考虑有\(1,2\)操作时应该如何做,注意到加法乘法其实和转换操作没有关系,所以只需要考虑数字运算(加法和乘法)和转换运算这两种运算是如何相互影响的。

再次考虑懒惰标记的实际含义,是将左节点和右节点尚未处理的信息保存起来,那么转换时也需将保存的信息同时转换。

if (lazC[rt] == 1) {//向左平移一维
node x = t[0][st], y = t[1][st], z = t[2][st];
t[0][st] = y;
t[1][st] = z;
t[2][st] = x;
} else if (lazC[rt] == 2) {//向左平移两维
node x = t[0][st], y = t[1][st], z = t[2][st];
t[0][st] = z;
t[1][st] = x;
t[2][st] = y;
}

再考虑是先进行转换运算还是先进行数字运算,当操作子节点的时候,父节点已经进行了转换,所以为了让父节点的标记操作能够对应上子节点的标记,所以要先进行转换,再进行数字运算。

\(100\)分攻略

算法:线段树+懒惰标记\(*3\)+区间离散化

\(n≤1,000,000,000\),原有线段树空间复杂度为\(O(n)\),但现在这个数据范围不支持这个复杂度,考虑离散化。

注意到\(m\leq 40000\),那么操作的区间个数最多为\(40000\)个,端点最多为\(80000\)个,那么离散化后进行操作空间复杂度为\(O(m)\),注意到线段树开点应开总端点的\(4\)倍,所以总共需要开\(8m\)个点,即为\(m<<3\)。

使用区间离散化的技巧,将原有的\([L,R]\)区间变为\([L,R+1)\),这样满足区间可加性,举个反例体会下这个操作的必要性。

提供三个操作\([1,4],[1,1],[4,4]\),直接离散化后,区间为\([1,2],[1,1],[2,2]\),后两个操作合并等于第一个操作,而原有操作中的端点\(2\)和\(3\)丢失了。而转换为左闭右开区间后,区间为\([1,5),[1,2),[4,5)\),离散化后为\([1,4),[1,2),[3,4)\),此时信息并未丢失,保存每一个区间的长度\(len[maxn << 3]\)即可。

原线段树的叶子节点理解为每一个端点,离散化区间后的叶子节点可以理解为以第\(i\)个端点为开头的左闭右开区间。比如说上述转换区间后为\([1,5),[1,2),[4,5)\),那么叶子节点会变为\([1,2),[2,4),[4,5)\),一共出现过\(4\)个不同的区间端点,显然最右边的端点不需要保存信息,所以一共有三个叶子节点,第\(1\)个叶子节点表示以\(1\)开头的左闭右开区间,第\(2\)个叶子节点表示以\(2\)为开头的左闭右开区间,第\(3\)个叶子节点表示以\(4\)为开头的左闭右开区间(代码中最后一个端点也有一个节点,长度为0)。

考虑操作,考虑第一个操作\([1,5)\)(注意离散化后区间为\([1,4)\)),他需要的是\(5\)之前的端点的操作,即要操作\(1,2,4\)(离散化后为\(1,2,3\))开头的区间,那么由于线段树保留的是以第\(i\)个端点为开头的左闭右开区间,所以操作时要将离散化后的右端点\(4\)减一再进行操作。

#include <bits/stdc++.h>

#define start ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define ll long long
#define int ll
#define ls st<<1//左子树,st*2
#define rs st<<1|1//右子树,st*2+1
using namespace std;
const int maxn = (ll) 4e5 + 5;
const int mod = 1e9 + 7;
int T = 1;
vector<int> v;
struct node {
int lazS, lazM, sum;
} t[3][maxn << 3];
int lazC[maxn << 3];
int len[maxn << 3];
int q[maxn][6]; void build(int st, int l, int r) {
for (int i = 0; i <= 2; ++i)t[i][st].lazM = 1;
/*
* 最右区间长度为0
* 由于保留左闭右开区间,所以长度为v[l + 1] - v[l]
*/
if (l == r) {
if (l == v.size() - 1)
len[st] = 0;
else
len[st] = v[l + 1] - v[l];
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
len[st] = len[ls] + len[rs];
} void change(int rt, int st) {
if (lazC[rt] == 1) {//向左平移一维
node x = t[0][st], y = t[1][st], z = t[2][st];
t[0][st] = y;
t[1][st] = z;
t[2][st] = x;
} else if (lazC[rt] == 2) {//向左平移两维
node x = t[0][st], y = t[1][st], z = t[2][st];
t[0][st] = z;
t[1][st] = x;
t[2][st] = y;
}
} void push_down(int st) {
if (lazC[st]) {//先进行转换操作
change(st, ls);
change(st, rs);
lazC[ls] = (lazC[ls] + lazC[st]) % 3;
lazC[rs] = (lazC[rs] + lazC[st]) % 3;
lazC[st] = 0;
}
for (int i = 0; i <= 2; ++i) {
t[i][ls].sum = ((t[i][ls].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[ls]) % mod;//先进行乘法操作,再进行加法操作
t[i][rs].sum = ((t[i][rs].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[rs]) % mod;//先进行乘法操作,再进行加法操作
t[i][ls].lazS = ((t[i][ls].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
t[i][rs].lazS = ((t[i][rs].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
t[i][ls].lazM = t[i][ls].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
t[i][rs].lazM = t[i][rs].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
t[i][st].lazM = 1;//清除懒惰标记
t[i][st].lazS = 0;//清除懒惰标记
}
} void add(int st, int l, int r, int L, int R, int a[]) {
if (L <= l && r <= R) {
for (int i = 0; i <= 2; ++i) {
t[i][st].lazS = (t[i][st].lazS + a[i]) % mod;
t[i][st].sum = (t[i][st].sum + a[i] * len[st]) % mod;//注意lazS保留的仅仅是加法,以便对于不同区间根据长度判定加和
}
return;
}
push_down(st);
int mid = (l + r) >> 1;
if (L <= mid)
add(ls, l, mid, L, R, a);
if (R > mid)
add(rs, mid + 1, r, L, R, a);
for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
} void mul(int st, int l, int r, int L, int R, int val) {
if (L <= l && r <= R) {
for (int i = 0; i <= 2; ++i) {
t[i][st].sum = (t[i][st].sum * val) % mod;
t[i][st].lazM = (t[i][st].lazM * val) % mod;
t[i][st].lazS = (t[i][st].lazS * val) % mod;//由乘法分配律,lazS也需要乘
}
return;
}
push_down(st);
int mid = (l + r) >> 1;
if (L <= mid)
mul(ls, l, mid, L, R, val);
if (R > mid)
mul(rs, mid + 1, r, L, R, val);
for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
} void change(int st, int l, int r, int L, int R) {
if (L <= l && r <= R) {
lazC[st] = (lazC[st] + 1) % 3;//转换操作转换三次后就等于不转换,所以取模
node x = t[0][st], y = t[1][st], z = t[2][st];//lazC标记实际含义为保留子节点的信息,所以父节点要进行操作
t[0][st] = y;
t[1][st] = z;
t[2][st] = x;
return;
}
push_down(st);
int mid = (l + r) >> 1;
if (L <= mid)
change(ls, l, mid, L, R);
if (R > mid)
change(rs, mid + 1, r, L, R);
for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
} void query(int st, int l, int r, int L, int R, int a[]) {
if (L <= l && r <= R) {
for (int i = 0; i <= 2; ++i) {
a[i] = (a[i] + t[i][st].sum) % mod;
}
return;
}
push_down(st);
int mid = (l + r) >> 1;
if (L <= mid)
query(ls, l, mid, L, R, a);
if (R > mid)
query(rs, mid + 1, r, L, R, a);
} void solve() {
//注意本代码已#define int long long
int n, m;
cin >> n >> m;
//为离散化,先读入,后操作
for (int i = 1; i <= m; ++i) {
cin >> q[i][0] >> q[i][1] >> q[i][2];
++q[i][2];//形成左闭右开区间
v.push_back(q[i][1]);
v.push_back(q[i][2]);
if (q[i][0] == 1) {
for (int j = 3; j <= 5; ++j)cin >> q[i][j];
} else if (q[i][0] == 2) {
cin >> q[i][3];
}
}
v.push_back(LLONG_MIN);//放入一个最小值,保证数组以1开始,也可以开一个数组进行离散化
sort(v.begin(), v.end());//离散化需要先排序
v.erase(unique(v.begin(), v.end()), v.end());//将多余的数字去除
for (int i = 1; i <= m; ++i) {
//通过二分离散化
q[i][1] = lower_bound(v.begin(), v.end(), q[i][1]) - v.begin();
q[i][2] = lower_bound(v.begin(), v.end(), q[i][2]) - v.begin();
}
int tot = v.size() - 1;//线段树的叶子结点个数,即操作区间的最右端点
build(1, 1, tot);
for (int i = 1; i <= m; ++i) {
//由于操作左闭右开区间,所以每次操作右端点需要-1
if (q[i][0] == 1) {
int x[] = {q[i][3], q[i][4], q[i][5]};
add(1, 1, tot, q[i][1], q[i][2] - 1, x);
} else if (q[i][0] == 2) {
mul(1, 1, tot, q[i][1], q[i][2] - 1, q[i][3]);
} else if (q[i][0] == 3) {
change(1, 1, tot, q[i][1], q[i][2] - 1);
} else {
int x[] = {0, 0, 0};
query(1, 1, tot, q[i][1], q[i][2] - 1, x);
int ans = 0;
for (int j = 0; j <= 2; ++j) {
ans = (ans + x[j] * x[j] % mod) % mod;
}
cout << ans << '\n';
}
}
} signed main() {
start;//关同步
while (T--)
solve();
return 0;
}

总结

本题难点在于三个标记的影响以及区间离散化。

对于前两个标记在洛谷OJ中有原题,转换则需要考虑多种组合,以及不正确转换的反例,比如说转换和运算的先后,是否带标记转换还是只转换\(sum\),转换根据本节点的标记还是父节点的标记等等。题解给出的是正确的转换方式,但是非正确的转换方式很容易干扰思路,所以请完全理解为什么如此转换。

而区间离散化首先需要理解离散化的思想,其次理解为什么要将区间变为左闭右开区间,最后理解每一个节点代表什么。

CCF 202012-5星际旅行(20~100分)的更多相关文章

  1. CCF 消息传递接口 (队列) 201903-4 (100分)

    [题目描述] 老师给了 T 份 MPI 的样例代码,每份代码都实现了 n 个进程通信.这些进程标号 从 0 到 n − 1,每个进程会顺序执行自己的收发指令,如:“S x”,“R x”.“S x”表示 ...

  2. 解决samtools报错:[main_samview] region "chr2:20,100,000-20,200,000" specifies an unknown reference name. Continue anyway.

    根据Samtool 的manual文档介绍,如果想搜索bam文件的某段区域,需要用到以下命令: samtools view aln.sorted.bam chr2:20,100,000-20,200, ...

  3. CCF(除法):线段树区间修改(50分)+线段树点修改(100分)+线段树(100分)

    除法 201709-5 这道题有很多种方法来做,最常用的就是线段树和树状数组. 如果使用线段树来做,就会想到区间修改的update函数.但是这里可能会涉及到v是1或者a[j]是0的情况,所以用这种方法 ...

  4. CCF201609-2 火车购票 java(100分)

    试题编号: 201609-2 试题名称: 火车购票 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 请实现一个铁路购票系统的简单座位分配算法,来处理一节车厢的座位分配. 假设一 ...

  5. 【NLP】BLEU值满分是100分吗?

    为了解决这个问题,首先需要知道BLEU值是如何计算出来的. BLEU全称是Bilingual Evaulation Understudy.其意思是双语评估替补.所谓Understudy(替补),意思是 ...

  6. ccf 201712-4 行车路线(30分超时)

    问题描述 小明和小芳出去乡村玩,小明负责开车,小芳来导航. 小芳将可能的道路分为大道和小道.大道比较好走,每走1公里小明会增加1的疲劳度.小道不好走,如果连续走小道,小明的疲劳值会快速增加,连续走s公 ...

  7. oracle 在insert into的时候报ORA-00928: missing SELECT keyword错 [问题点数:100分,结帖人dm520]

    转自:https://bbs.csdn.net/topics/310095274 INSERT INTO SA_Table(uniPositionCode,transferGroupName,appC ...

  8. Coursera Algorithms Programming Assignment 3: Pattern Recognition (100分)

    题目原文详见http://coursera.cs.princeton.edu/algs4/assignments/collinear.html 程序的主要目的是寻找n个points中的line seg ...

  9. CCF201409-2 画图 java(100分)

    试题编号: 201409-2 试题名称: 画图 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 在一个定义了直角坐标系的纸上,画一个(x1,y1)到(x2,y2)的矩形指将横坐 ...

  10. CCF201509-2 日期计算 java(100分)

    试题编号: 201509-2 试题名称: 日期计算 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 给定一个年份y和一个整数d,问这一年的第d天是几月几日? 注意闰年的2月有2 ...

随机推荐

  1. 【GiraKoo】C++中static关键字的作用

    C++中static关键字的作用 在程序中良好的使用static,const,private等关键字,对于代码的健壮性有很大的帮助. 本文介绍的就是C++中static关键字的一些常见用法与区别.适合 ...

  2. linux中使用jenkins自动部署前端工程

    1.去年在自己的服务器上安装了jenkins,说用来自己研究一下jenkins自动化部署前端项目,jenkins安装好了,可是一直没管,最近终于研究了一下使用jenkins自动化部署,以此记录下来. ...

  3. 执行pod setup 报错error: RPC failed; curl 18 transfer closed with outstanding read data remainin

    执行pod setup 报错 error: RPC failed; curl 18 transfer closed with outstanding read data remaining fatal ...

  4. 如何让Task在非线程池线程中执行?

    Task承载的操作需要被调度才能被执行,由于.NET默认采用基于线程池的调度器,所以Task默认在线程池线程中执行.但是有的操作并不适合使用线程池,比如我们在一个ASP.NET Core应用中承载了一 ...

  5. 绘图;OSPF 虚连接

    绘图;OSPF 虚连接 原图如下 绘图 实验拓扑 实验需求 按照图示分区域配置OSPF 配置虚连接认证 实验步骤 配置相应接口IP地址及loopback 环回口地址 按照图示分区域配置OSPF AR1 ...

  6. Open AI ChatGPT Prompt 学习之基础篇

    碎碎念 2023 年,最火的可能就是 openAI 了,其组织代表的产品 chatGTP,相信大家已经有所耳闻.不少同学已经开始着手使用,并截图晒出 ChatGPT 是多么得智能与神奇.而有的同学在使 ...

  7. 聊一聊 Rust 的 stack overflow

    早上看到了这篇文章 智能指针有可能会让你的应用崩溃, 下面分析一下 会导致 stack overflow 的代码 struct Node<T> { val: T, next: Option ...

  8. GPT3的应用领域:机器翻译、文本生成、文本摘要

    目录 1. 引言 2. 技术原理及概念 3. 实现步骤与流程 3.1 准备工作:环境配置与依赖安装 3.2 核心模块实现 3.3 集成与测试 4. 应用示例与代码实现讲解 4.1 机器翻译 4.2 文 ...

  9. 【java-Err】 Generating equals/hashCode implementation but without a call to superclass,

    @EqualsAndHashCode  默认不继承父类 修复此问题的方法很简单:  1. 使用@Getter @Setter @ToString代替@Data并且自定义equals(Object ot ...

  10. Unity的Console的控制类LogEntries:深入解析与实用案例

    使用Unity Console窗口的LogEntries私有类实现自定义日志系统 在Unity开发过程中,我们经常需要使用Console窗口来查看程序运行时的日志信息.Unity内置的日志系统提供了基 ...