原题地址:https://www.lydsy.com/JudgeOnline/problem.php?id=2120
题意:墨墨购买了一套N支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问。墨墨会像你发布如下指令: 1、 Q L R代表询问你从第L支画笔到第R支画笔中共有几种不同颜色的画笔。 2、 R P Col 把第P支画笔替换为颜色Col。为了满足墨墨的要求,你知道你需要干什么了吗?
解法一和解法二所用的时间差距还是很明显的
分块的
莫队的
1. 解法一:分块+二分
思路:这题我一开始做的时候以为和用分块做区间众数是差不多的。可是做到一半发现好像并不能像解决众数问题一样,将下标存在vector中然后二分判断一个数字是否存在在一个区间。
后来想了想两者不同的地方 其中这题是判断区间上有数的种类,而求众数是判断区间上一个数出现最多的次数,一个是种类,一个是次数。
说下这题如何用分块的方法解决问题。
这题的方法好巧妙。
对于每个位置i,记录它的颜色a[i]上一次出现的位置,记为pre[i]。那么我们另开一个数组spre专门用于将某个块内的pre信息进行排序。如果当前我们查询[L,R]区间,如果对于某一个i,pre[i]< l,说明a[i]上次出现是在l之前,那就说明a[i]在当前范围是第一次出现。那就结果+1,这是对不完整块的暴力处理方法。对于完整的块,因为我们对其排序过了,所以可以直接二分查找比l小的位置。
注意下几个细节
1.排序时不要用vector,因为这里有单点修改操作,所以vector不适合,每次都要初始化太浪费时间了。
2.对于sort函数的前后区间范围是左闭右开 [ ) ,所以右边的值要加1
#include <bits/stdc++.h>
using namespace std;
const int maxn = 50005;
int l, r;
int a[maxn], belog[maxn], n, m, block, pre[maxn], spre[maxn]; //pre[i]=j表示数字a[i]上一次出现在j位置
int lst[1000006];//lst[i]=j,表示数字i上一次出现的位置是j ·
char op[2];
inline int read() {//读入挂
int ret = 0, c, f = 1;
for(c = getchar(); !(isdigit(c) || c == '-'); c = getchar());
if(c == '-') f = -1, c = getchar();
for(; isdigit(c); c = getchar()) ret = ret * 10 + c - '0';
if(f < 0) ret = -ret;
return ret;
}
void reset(int x) {
int l = (x - 1) * block + 1;
int r = min(n, x * block) ;
for(int i = l; i <= r; i++) {
spre[i] = pre[i];
}
sort(spre + l, spre + r + 1);
}
void build() {
for(int i = 1; i <= n; i++) {
pre[i] = lst[a[i]];
lst[a[i]] = i;
}
for(int i = 1; i <= belog[n]; i++) {
reset(i);
}
}
void query(int l, int r) {
int t1 = belog[l];
int t2 = belog[r];
int ans = 0;
for(int i = l; i <= min(t1 * block, r); i++) {
if(pre[i] < l) ans++;
}
if(t1 != t2) {
for(int i = (t2 - 1) * block + 1; i <= r; i++) {
if(pre[i] < l) ans++;
}
for(int i = t1 + 1; i <= t2 - 1; i++) {
ans += lower_bound(spre + (i - 1) * block + 1, spre + i * block + 1, l) - (spre + (i - 1) * block + 1); //不能用upper_bound()
}
}
printf("%d\n", ans);
}
void change(int l, int r) {
for(int i = 1; i <= n; i++)lst[a[i]] = 0;//注意不要用memset,因为数据只有1e5,但是开的空间有1e6
a[l] = r;
for(int i = 1; i <= n; i++) {
int t = pre[i];
pre[i] = lst[a[i]];
if(t != pre[i]) reset(belog[i]);//只对单点更新有影响的块进行reset
lst[a[i]] = i;
}
}
int main() {
n = read();
m = read();
block = sqrt(n);
for(int i = 1; i <= n; i++) {
a[i] = read();
belog[i] = (i - 1) / block + 1;
}
build();
for(int i = 1; i <= m; i++) {
scanf("%s%d%d", op, &l, &r);
if(op[0] == 'Q') {
query(l, r);
} else {
change(l, r);
}
}
return 0;
}
感谢lh大佬给我提供了一个优化的方法
#include <bits/stdc++.h>
using namespace std;
const int maxn = 50005;
int l, r;
int a[maxn], belog[maxn], n, m, block, pre[maxn], spre[maxn]; //pre[i]=j表示数字a[i]上一次出现在j位置
int lst[1000006];//lst[i]=j,表示数字i上一次出现的位置是j ·
char op[2];
inline int read() {//读入挂
int ret = 0, c, f = 1;
for(c = getchar(); !(isdigit(c) || c == '-'); c = getchar());
if(c == '-') f = -1, c = getchar();
for(; isdigit(c); c = getchar()) ret = ret * 10 + c - '0';
if(f < 0) ret = -ret;
return ret;
}
void reset(int x) {
int l = (x - 1) * block + 1;
int r = min(n, x * block) ;
for(int i = l; i <= r; i++) {
spre[i] = pre[i];
}
sort(spre + l, spre + r + 1);
}
void build() {
for(int i = 1; i <= n; i++) {
pre[i] = lst[a[i]];
lst[a[i]] = i;
}
for(int i = 1; i <= belog[n]; i++) {
reset(i);
}
}
void query(int l, int r) {
int t1 = belog[l];
int t2 = belog[r];
int ans = 0;
for(int i = l; i <= min(t1 * block, r); i++) {
if(pre[i] < l) ans++;
}
if(t1 != t2) {
for(int i = (t2 - 1) * block + 1; i <= r; i++) {
if(pre[i] < l) ans++;
}
for(int i = t1 + 1; i <= t2 - 1; i++) {
ans += lower_bound(spre + (i - 1) * block + 1, spre + i * block + 1, l) - (spre + (i - 1) * block + 1); //不能用upper_bound()
}
}
printf("%d\n", ans);
}
void change(int l, int r) {//这次只修改有影响的值
// for(int i = 1; i <= n; i++)lst[a[i]] = 0;//注意不要用memset,因为数据只有1e5,但是开的空间有1e6
int t = lst[a[l]];
int x;
int flag = 0;
if(t > l) flag = 1;
while(t > l) {
if(pre[t] == l) {
x = t; //如果t的前驱节点就是l了,就记录这个位置t,即x
}
t = pre[t];
}
if(flag) pre[x] = pre[l];
else lst[a[l]] = pre[t];
t = lst[r];
if(t < l) {
lst[r] = l;
pre[l] = t;
} else {
while(t > l) {
if(pre[t] <= l) x = t;
t = pre[t];
}
pre[x] = l;
pre[l] = t;
}
a[l] = r;
for(int i = 1; i <= belog[n]; i++) {
reset(i);
}
}
int main() {
n = read();
m = read();
block = sqrt(n);
for(int i = 1; i <= n; i++) {
a[i] = read();
belog[i] = (i - 1) / block + 1;
}
build();
for(int i = 1; i <= m; i++) {
scanf("%s%d%d", op, &l, &r);
if(op[0] == 'Q') {
query(l, r);
} else {
change(l, r);
}
}
return 0;
}
2.莫队+修改操作
思路:就是一道带修改的莫队裸题。
其实这题是这题的升级版,带了修改操作
带修莫队大体方法如下:
1、将修改询问离线并分开,记录每一个修改之前最近的一次询问的编号
2、分块之后将区间排序,关键字为:左端点块的编号、右端点块的编号、记录的最近一次修改的编号
3、在查询每一次询问之前,判断当前做过的修改是否恰好是这次询问需要的修改,如果不够将其修改,修改多了的话恢复回去,注意如果修改的位置在前一个询问的区间内要更新答案
4、转移询问和普通莫队相同
#include <bits/stdc++.h>
using namespace std;
const int maxn = 50005;
int n, m, belog[maxn], block, cnt[maxn * 100], ans, arr[maxn];
//cnt[i]=j,表示在当前区间内数字i出现了j次
struct node {
int l, r, t, id, val;
} q[maxn];
struct ch {
int pos, old, now;
} c[maxn];
int a[maxn];
char op[2];
inline int read() {//读入挂
int ret = 0, c, f = 1;
for(c = getchar(); !(isdigit(c) || c == '-'); c = getchar());
if(c == '-') f = -1, c = getchar();
for(; isdigit(c); c = getchar()) ret = ret * 10 + c - '0';
if(f < 0) ret = -ret;
return ret;
}
bool cmp1(node a, node b) {
// return belog[a.l] < belog[b.l] || (belog[a.l]==belog[b.l]&&a.r < b.r) || (belog[a.l]==belog[b.l]&&belog[a.r]==belog[b.r]&&a.t < b.t);
/*
注意!!!
上面的真的是毒瘤写法。
用了上面的就是1200ms+,而不用上面的写法就是600+ms就过了
*/
if(belog[a.l] != belog[b.l]) return belog[a.l] < belog[b.l];
if(belog[a.r] != belog[b.r]) return belog[a.r] < belog[b.r];
return a.t < b.t;
}
bool cmp2(node a, node b) {
return a.id < b.id;
}
void updata_time(int t, int add, int l, int r) {
if(add == 1) {
if(l <= c[t].pos && c[t].pos <= r) {
cnt[c[t].now]++;
cnt[c[t].old]--;
if(cnt[c[t].now] == 1) ans++;
if(cnt[c[t].old] == 0) ans--;
}
a[c[t].pos] = c[t].now;
/*
坑点之二:
一开始以为只要简单将某一值加1或者减1就行了
后来发现如果对于当前要修改的位置pos不在当前的[l,r]范围内的时候,那么的确
只需要简单的加减就行了。因为修改某一个值,并不会影响到你维护的cnt数组.
但是如果pos在[l,r]范围内,那么就需要对pos位置的cnt数值进行维护
*/
} else {
if(l <= c[t].pos && c[t].pos <= r) {
cnt[c[t].old]++;
cnt[c[t].now]--;
if(cnt[c[t].old] == 1) ans++;
if(cnt[c[t].now] == 0) ans--;
}
a[c[t].pos] = c[t].old;
}
}
void updata_data(int x, int add) {
if(add == 1) {
cnt[a[x]]++;
if(cnt[a[x]] == 1) ans++;
} else {
cnt[a[x]]--;
if(cnt[a[x]] == 0) ans--;
}
}
int main() {
n=read();
m=read();
block = (int)pow(n, 2.0 / 3);
for(int i = 1; i <= n; i++) {
a[i]=read();
arr[i] = a[i];
/*
坑点之一:
因为你在保存时间的修改的时候是要保存修改前的值和修改后的值。
但是你如果不开arr数组,一旦题目每次都是更新同一个地方的值,那么你就
不能传进去正确的修改前的数值。
所以说arr数组的作用就是同步更新修改的值
*/
belog[i] = (i - 1) / block + 1;
}
int time = 0, qtot = 0;
for(int i = 1; i <= m; i++) {
int l, r;
scanf("%s%d%d", op, &l, &r);
if(op[0] == 'Q') {//将查询和修改分开来进行
++qtot;
q[qtot] = {l, r, time, qtot};
} else {
++time;
c[time] = {l, arr[l], r};
arr[l] = r;
}
}
sort(q + 1, q + 1 + qtot, cmp1);
ans = 0;
for(int i = 1, l = 1, r = 0, t = 0; i <= qtot; i++) {
for(; t < q[i].t; t++) {
updata_time(t + 1, 1, l, r);
}
for(; t > q[i].t; t--) {
updata_time(t, -1, l, r);
}
for(; r < q[i].r; r++) {
updata_data(r + 1, 1);
}
for(; r > q[i].r; r--) {
updata_data(r, -1);
}
for(; l > q[i].l; l--) {
updata_data(l - 1, 1);
}
for(; l < q[i].l; l++) {
updata_data(l, -1);
}
q[i].val = ans;
}
sort(q + 1, q + 1 + qtot, cmp2);
for(int i = 1; i <= qtot; i++) {
printf("%d\n", q[i].val);
}
return 0;
}
/*
5 4
1 2 3 4 5
Q 1 5
Q 1 4
R 1 2
Q 1 2
*/