前置知识

线段树:通过懒惰标记,可实现区间处理,和区间询问皆为\(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. 【C#代码整洁之道】读后习题

    1)劣质的代码会带来什么后果? GPT回答: 可维护性降低:代码过于复杂.难以理解.难以修改,导致维护成本增加,代码质量更加恶化. 可靠性降低:错误容易发生,很难找到并修复,因为代码模糊.逻辑混乱,并 ...

  2. distribute by在spark中的一些应用

    一.在二次排序当中的应用 1.1 说到排序当然第一想到的就是sort by和order by这两者的区别,也分情况. 在算子当中,两者没有区别,orderby()调用的也是sort.order by就 ...

  3. 【Java】Eclipse常用快捷键整理

    前言 还是最近在上Java课,由于疫情原因,看的网课,那里的老师比较实战派,很多时候不知道按了什么快捷键就立马出现了很骚的操作.网上查询后发现了一些快捷键对于我这个eclipse小白还是挺常用的,整理 ...

  4. 如何使用Map处理Dom节点

    本文浅析一下为什么Map(和WeakMap)在处理大量DOM节点时特别有用. 我们在JavaScript中使用了很多普通的.古老的对象来存储键/值数据,它们处理的非常出色: const person ...

  5. qq飞车端游最全按键指法教学

    目录 起步篇 超级起步 弹射起步 段位起步 基础篇 点飘 撞墙漂移 撞墙点喷 进阶篇 双喷 叠喷 断位漂移 段位双喷 侧身漂移 快速出弯 CW WCW CWW 牵引 甩尾点飘 甩尾漂移 右侧卡 左侧卡 ...

  6. 一分钟学一个 Linux 命令 - tar

    前言 大家好,我是 god23bin.今天给大家带来的是 Linux 命令系列,每天只需一分钟,记住一个 Linux 命令不成问题.今天,我们要介绍的是一个常用且强大的命令:tar. 什么是 tar ...

  7. Java 网络编程 —— RMI 框架

    概述 RMI 是 Java 提供的一个完善的简单易用的远程方法调用框架,采用客户/服务器通信方式,在服务器上部署了提供各种服务的远程对象,客户端请求访问服务器上远程对象的方法,它要求客户端与服务器端都 ...

  8. 深入浅出MySQL事务

    Photo by Lukas Hartmann from Pexels 辞职这段时间以来看见了很多工作之外的东西,我认为这是值得的.同时也有时间和机会来好好整理所学所想,准备开启下一段旅途. 事务的定 ...

  9. JavaWeb中Servlet、web应用和web站点的路径细节("/"究竟代表着什么)

    JavaWeb中Servlet.web应用和web站点的路径细节("/"究竟代表着什么) 1 开门见山 新建一个tomcat web项目,配置tomcat的虚拟目录,取默认值(/项 ...

  10. Java使用joml计算机图形学库,将3D坐标旋转正交投影转为2D坐标

    最近遇到了一个困扰我许久的难题,现将解决方案分享出来 由于我们的项目侧重点在前端绘图,导致了前后端工作量不协调,我后端接口很快就能写完,而前端一个图要画好久,领导见状将前端的任务分到后端一部分用Jav ...