BZOJ 2120: 数颜色(分块二分||带修改的莫队)

原题地址: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
*/

猜你喜欢

转载自blog.csdn.net/yiqzq/article/details/80683220