【带修改的莫队算法C++】单点修改+区间查询(时间戳的使用)

》》》算法竞赛

/**
 * @file            
 * @author          jUicE_g2R(qq:3406291309)————彬(bin-必应)
 *						一个某双流一大学通信与信息专业大二在读	
 * 
 * @brief           一直在算法竞赛学习的路上
 * 
 * @copyright       2023.9
 * @COPYRIGHT			 原创技术笔记:转载需获得博主本人同意,且需标明转载源
 *
 * @language        C++
 * @Version         1.0还在学习中  
 */
  • UpData Log 2023.9.17 更新进行中
  • Statement0 一起进步
  • Statement1 有些描述可能不够标准,但能达其意

技术提升站点

19-2 带修改的莫队算法

  • 基础莫队算法 只能解决 区间查询 的问题,而无法修改

没有学习过基础莫队算法,可点击此处去学习

如果是简单的单点修改,就需要对 基础莫队算法 进行修改。

如何使基础莫队带有“修改“功能?

这需要用到 时间戳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+1time=j 这一段所有询问都强行加上

  • i>j时,将 time=itime=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 支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会向你发布如下指令:

  1. Q   L   R Q\ L\ R Q L R 代表询问你从第 L L L 支画笔到第 R R R 支画笔中共有几种不同颜色的画笔。(区间去重

  2. 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();
}

你可能感兴趣的:(C++算法,算法,c++,数据结构,笔记)