》》》算法竞赛
/**
* @file
* @author jUicE_g2R(qq:3406291309)————彬(bin-必应)
* 一个某双流一大学通信与信息专业大二在读
*
* @brief 一直在算法竞赛学习的路上
*
* @copyright 2023.9
* @COPYRIGHT 原创技术笔记:转载需获得博主本人同意,且需标明转载源
*
* @language C++
* @Version 1.0还在学习中
*/
- UpData Log 2023.9.17 更新进行中
- Statement0 一起进步
- Statement1 有些描述可能不够标准,但能达其意
基础莫队算法
只能解决 区间查询 的问题,而无法修改没有学习过基础莫队算法,可点击此处去学习
如果是简单的单点修改,就需要对 基础莫队算法
进行修改。
如何使基础莫队带有“修改“功能?
这需要用到 时间戳time
,这个 时间戳指针
会参与到区间 询问
的排序。
vector<int> block_id(N);
struct Range{ //记录输入的区间信息
int L,R;
int id; //输入时该区间的排位
int time; //时间戳
} r[N];
int n; //输入序列中数的个数
//第一种写法
bool cmp(Range r1,Range r2){
if(block_id[r1.L] != block_id[r2.L])
return block_id[r1.L] < block_id[r2.L];
else if(block_id[r1.R] != block_id[r2.R])
return block_id[r1.R] < block_id[r2.R];
else
return block_id[r1.time] < block_id[r2.time];
}
/*在主函数中调用*/
sort(r,r+n,cmp); //按规则升序排列
//第二种写法:直接将比较规则定义的函数植入到sort函数接口上
sort(r,r+n,
[](Range r1,Range r2){
if(block_id[r1.L] != block_id[r2.L])
return block_id[r1.L] < block_id[r2.L];
else if(block_id[r1.R] != block_id[r2.R])
return block_id[r1.R] < block_id[r2.R];
else
return block_id[r1.time] < block_id[r2.time];
}
);
//第三种写法:三目运算嵌套
sort(r,r+n,
[]{Range r1,Range r2}{
return block_id[r1.L]==block_id[r2.L] ? \
(block_id[r1.R]==block_id[r2.R] ? block_id[r1.time] < block_id[r2.time] : block_id[r1.R] < block_id[r2.R]) : \
block_id[r1.L]<block_id[r2.L];
}
);
//第四种写法就是在结构体里写重载函数(参考优先队列)
为何需要 时间戳 这个参数?
最主要的是避免修改对结果的影响。
基础莫队算法
的知道,这个算法的优化是基于它是一个离线算法
,而离线算法
的优势是可以把握全局、统筹全局,正是这一点,莫队算法
对多个区间的访问都会采取 先全部读入,然后再对这些询问排序优化,来减少不必要的重复移动,从而达到高效处理的效果。但是 莫队算法
这种优化是破化了原有的访问顺序的。
假如将要输入 10 个操作,询问区间 s
这个操作 在输入时排在第 1 个操作(经排序优化后变为第 5 个),修改s区间元素 h
这个操作是第 3 个 ,此时不经干预的话,得到 区间s的结果 是修改后的区间产生的(而题目要求查找的这个 s区间 是修改前的)。
同样的,如果询问区间 s
这个操作 在输入时排在第 5 个操作(经排序优化后变为第 1 个),修改s区间元素 h
这个操作是第 3 个 ,此时不经干预的话,得到 区间s的结果 是修改前的区间产生的(而题目要求查找的这个 s区间 是修改后的)。
所以可以利用 时间戳来解决这种 时间错位导致结果错误
的问题
时间戳 参数
首先要知道,时间维(时间戳)
表示经历的修改次数。在维护区间是不是 [pl,pr](基础莫队)
,而是 [pl,pr,time](带修改的莫队)
。
同时 Move_Ptr函数
对区间的变动不在是4种 [--pl,pr], [pl,++pr], [pl++,pr], [pl,pr--]
,而是六种 [--pl,pr,time], [pl,++pr,time], [pl++,pr,time], [pl,pr--,time], [pl,pr,++time], [pl,pr,--time]
。
时间戳 的作用
单点修改
指的是对某一位数字进行修改。
如果是从一个经历 i 次修改(time=i
)的询问 转移到 一个经历 j 次修改(time=j
)的询问,有下面两种情况:
i
time=i+1
到 time=j
这一段所有询问都强行加上
i>j
时,将 time=i
到 time=j+1
这一段所有询问都强行还原
时间戳 核心代码
vector<int> a(N); //输入的序列
struct Change{ //记录单点修改
int pos,new_val;
} c[N];
int res=0; //维护区间内的不重复数的个数
void Dispose(int ptr,int i){ //处理时间戳,ptr是当前时间戳指针
if(r[i].L <= c[ptr].pos && c[ptr].pos <= r[i].R){//将要被修改数就在当前的维护区间内
if(cot[ a[ c[ptr].pos ] ]-- == 1) //如果 要被修改的这个数 在当前维护区间只出现过一次
res--;
if(cot[ c[ptr].new_val ]++ == 0) //如果 修改后的值 在当前维护区间是未出现过的
res++;
}
swap(c[ptr].new_val, a[ c[ptr].pos ]); //将 修改后的值与 被修改的数 进行交换
}
单点修改+区间查询
[国家集训队] 数颜色 / 维护队列
题目描述
墨墨购买了一套 N N N 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:
Q L R Q\ L\ R Q L R 代表询问你从第 L L L 支画笔到第 R R R 支画笔中共有几种不同颜色的画笔。(区间去重)
R P C R\ P\ C R P C 把第 P P P 支画笔替换为颜色 C C C。(单点修改)
为了满足墨墨的要求,你知道你需要干什么了吗?
输入格式
第 1 1 1 行两个整数 N N N, M M M,分别代表初始画笔的数量以及墨墨会做的事情的个数。
第 2 2 2 行 N N N 个整数,分别代表初始画笔排中第 i i i 支画笔的颜色。
第 3 3 3 行到第 2 + M 2+M 2+M 行,每行分别代表墨墨会做的一件事情,格式见题干部分。
输出格式
对于每一个 Query 的询问,你需要在对应的行中给出一个数字,代表第 L L L 支画笔到第 R R R 支画笔中共有几种不同颜色的画笔。
样例 #1
样例输入 #1
6 5 1 2 3 4 5 5 Q 1 4 Q 2 6 R 1 2 Q 1 4 Q 2 6
样例输出 #1
4 4 3 4
#include
using namespace std;
const int N=1e6+10;
/*存储输入的原始信息*/
vector<int> a(N); //输入的序列
struct Range{ //记录输入的区间信息
int L,R;
int id; //输入时该区间的位次
int time; //时间戳
} r[N];
struct Change{ //记录单点修改
int pos,new_val;
} c[N];
/*处理输入的信息*/
int R_n,C_n=0; //输入 区间的个数 与 修改的次数
vector<int> block_id(N); //记录序列中每个数对应的块编号
vector<int> ans(N,0); //目标区间的不重复数的个数(输出的结果)
vector<int> cot(N,0); //维护区间里某个数出现的次数
int res=0; //维护区间内的不重复数的个数
void Add(int ptr){
if(cot[ a[ptr] ]++ == 0) //如果是新出现的数就将结果加1(条件语句中是先判断再加加)
res++;
}
void Sub(int ptr){
if(--cot[ a[ptr] ] == 0)
res--;
}
void Dispose(int ptr,int i){ //处理时间戳
if(r[i].L <= c[ptr].pos && c[ptr].pos <= r[i].R){//将要被修改数就在当前的维护区间内
if(cot[ a[ c[ptr].pos ] ]-- == 1) //如果 要被修改的这个数 在当前维护区间只出现过一次
res--;
if(cot[ c[ptr].new_val ]++ == 0) //如果 修改后的值 在当前维护区间是未出现过的
res++;
}
swap(c[ptr].new_val, a[ c[ptr].pos ]); //将 修改后的值与 被修改的数 进行交换
}
void Move_Ptr(void){
int pl=1,pr=0,pt=0; //左右指针,当前维护区间为[pl,pr]
for(int i=1;i<=R_n;i++){ //遍历 优化过“询问”(排序)的数组
while(r[i].L<pl)//向目标左端点左扩展
Add(--pl);
while(r[i].R>pr)//向目标右端点右扩展
Add(++pr);
while(r[i].L>pl)//向目标左端点右收紧
Sub(pl++);
while(r[i].R<pr)//向目标右端点左收紧
Sub(pr--);
while(r[i].time<pt) //改多了
Dispose(pt--,i);
while(r[i].time>pt) //改少了
Dispose(++pt,i);
ans[ r[i].id ]=res; //记录答案
}
}
int main(void){
int n,m; cin>> n >> m; //n个数,m个区间询问
int size = sqrt(n); //每个块的容量
for(int i=1;i<=n;i++){
cin>>a[i]; //获取序列
block_id[i]=(i-1)/size+1; //记录每个数对应的块编号(从1开始)
}
while(m--){ //输入m个操作
char op[2]; //必须用数组存!!!
scanf("%s",op);
if(op[0]=='Q'){ //查询的区间
R_n++;
r[R_n].id=R_n;
/*如果 修改操作 后是 区间查询操作:
则 该区间的时间戳 记录修改操作的编号(因为修改可能会影响查询的结果!!!)*/
r[R_n].time=C_n; //记录先前最近的修改操作的编号
scanf(" %d %d",&r[R_n].L,&r[R_n].R);
}
else if(op[0]=='R'){ //单点修改
C_n++;
scanf(" %d %d",&c[C_n].pos,&c[C_n].new_val);
}
else{
cout<<"input illegal!\n";
return 0;
}
}
sort(r+1,r+1+R_n,
[](Range r1,Range r2){
if(block_id[r1.L] != block_id[r2.L])
return block_id[r1.L] < block_id[r2.L];
else if(block_id[r1.R] != block_id[r2.R])
return block_id[r1.R] < block_id[r2.R];
else
return block_id[r1.time] < block_id[r2.time];
}
);
Move_Ptr();
for(int i=1;i<=R_n;i++)
cout<<ans[i]<<endl;
return 0;
}
补充:输入优化函数
像这种输入很多的题,对 输入 也包装成函数处理
inline int Read(void) //按顺序读取!!!
{
char c=getchar(); int x=0,f=1;
while(c<'0'||c>'9'){
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
x=x*10+c-'0',c=getchar();
}
return x*f;
}
//调用案例
int main(void){
n=Read(); m=Read();
}