CCF 202012-5星际旅行(20~100分)
前置知识
线段树:通过懒惰标记,可实现区间处理,和区间询问皆为\(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分)的更多相关文章
- CCF 消息传递接口 (队列) 201903-4 (100分)
[题目描述] 老师给了 T 份 MPI 的样例代码,每份代码都实现了 n 个进程通信.这些进程标号 从 0 到 n − 1,每个进程会顺序执行自己的收发指令,如:“S x”,“R x”.“S x”表示 ...
- 解决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, ...
- CCF(除法):线段树区间修改(50分)+线段树点修改(100分)+线段树(100分)
除法 201709-5 这道题有很多种方法来做,最常用的就是线段树和树状数组. 如果使用线段树来做,就会想到区间修改的update函数.但是这里可能会涉及到v是1或者a[j]是0的情况,所以用这种方法 ...
- CCF201609-2 火车购票 java(100分)
试题编号: 201609-2 试题名称: 火车购票 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 请实现一个铁路购票系统的简单座位分配算法,来处理一节车厢的座位分配. 假设一 ...
- 【NLP】BLEU值满分是100分吗?
为了解决这个问题,首先需要知道BLEU值是如何计算出来的. BLEU全称是Bilingual Evaulation Understudy.其意思是双语评估替补.所谓Understudy(替补),意思是 ...
- ccf 201712-4 行车路线(30分超时)
问题描述 小明和小芳出去乡村玩,小明负责开车,小芳来导航. 小芳将可能的道路分为大道和小道.大道比较好走,每走1公里小明会增加1的疲劳度.小道不好走,如果连续走小道,小明的疲劳值会快速增加,连续走s公 ...
- oracle 在insert into的时候报ORA-00928: missing SELECT keyword错 [问题点数:100分,结帖人dm520]
转自:https://bbs.csdn.net/topics/310095274 INSERT INTO SA_Table(uniPositionCode,transferGroupName,appC ...
- Coursera Algorithms Programming Assignment 3: Pattern Recognition (100分)
题目原文详见http://coursera.cs.princeton.edu/algs4/assignments/collinear.html 程序的主要目的是寻找n个points中的line seg ...
- CCF201409-2 画图 java(100分)
试题编号: 201409-2 试题名称: 画图 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 在一个定义了直角坐标系的纸上,画一个(x1,y1)到(x2,y2)的矩形指将横坐 ...
- CCF201509-2 日期计算 java(100分)
试题编号: 201509-2 试题名称: 日期计算 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 给定一个年份y和一个整数d,问这一年的第d天是几月几日? 注意闰年的2月有2 ...
随机推荐
- 【C#代码整洁之道】读后习题
1)劣质的代码会带来什么后果? GPT回答: 可维护性降低:代码过于复杂.难以理解.难以修改,导致维护成本增加,代码质量更加恶化. 可靠性降低:错误容易发生,很难找到并修复,因为代码模糊.逻辑混乱,并 ...
- distribute by在spark中的一些应用
一.在二次排序当中的应用 1.1 说到排序当然第一想到的就是sort by和order by这两者的区别,也分情况. 在算子当中,两者没有区别,orderby()调用的也是sort.order by就 ...
- 【Java】Eclipse常用快捷键整理
前言 还是最近在上Java课,由于疫情原因,看的网课,那里的老师比较实战派,很多时候不知道按了什么快捷键就立马出现了很骚的操作.网上查询后发现了一些快捷键对于我这个eclipse小白还是挺常用的,整理 ...
- 如何使用Map处理Dom节点
本文浅析一下为什么Map(和WeakMap)在处理大量DOM节点时特别有用. 我们在JavaScript中使用了很多普通的.古老的对象来存储键/值数据,它们处理的非常出色: const person ...
- qq飞车端游最全按键指法教学
目录 起步篇 超级起步 弹射起步 段位起步 基础篇 点飘 撞墙漂移 撞墙点喷 进阶篇 双喷 叠喷 断位漂移 段位双喷 侧身漂移 快速出弯 CW WCW CWW 牵引 甩尾点飘 甩尾漂移 右侧卡 左侧卡 ...
- 一分钟学一个 Linux 命令 - tar
前言 大家好,我是 god23bin.今天给大家带来的是 Linux 命令系列,每天只需一分钟,记住一个 Linux 命令不成问题.今天,我们要介绍的是一个常用且强大的命令:tar. 什么是 tar ...
- Java 网络编程 —— RMI 框架
概述 RMI 是 Java 提供的一个完善的简单易用的远程方法调用框架,采用客户/服务器通信方式,在服务器上部署了提供各种服务的远程对象,客户端请求访问服务器上远程对象的方法,它要求客户端与服务器端都 ...
- 深入浅出MySQL事务
Photo by Lukas Hartmann from Pexels 辞职这段时间以来看见了很多工作之外的东西,我认为这是值得的.同时也有时间和机会来好好整理所学所想,准备开启下一段旅途. 事务的定 ...
- JavaWeb中Servlet、web应用和web站点的路径细节("/"究竟代表着什么)
JavaWeb中Servlet.web应用和web站点的路径细节("/"究竟代表着什么) 1 开门见山 新建一个tomcat web项目,配置tomcat的虚拟目录,取默认值(/项 ...
- Java使用joml计算机图形学库,将3D坐标旋转正交投影转为2D坐标
最近遇到了一个困扰我许久的难题,现将解决方案分享出来 由于我们的项目侧重点在前端绘图,导致了前后端工作量不协调,我后端接口很快就能写完,而前端一个图要画好久,领导见状将前端的任务分到后端一部分用Jav ...