分块、莫队

分块思想

  • 其实,分块是一种思想,而不是一种数据结构
  • 分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度
  • 分块的时间复杂度主要取决于分块的块长B,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度
  • 常见的块长B为\(\sqrt n + 1、\frac{n}{\sqrt {log\ q}} + 1、\sqrt{\frac{n}{log\ q}} + 1\)
  • 分块\(Debug\)的小技巧,可以选择块长\(B = 1\ or\ 2\)等一些小的块长去试一下看一下答案是不是正确
  • 分块的常见应用:计算满足某个性质的元素个数等,包括离线算法的莫队也是基于分块思想实现的

模板

const int B = sqrt(N) + 1;

int n;
int a[N];
int id[N], L[N], R[N];

void build() // 分块
{
    for (int i = 1; i <= n; ++i)
        id[i] = (i - 1) / B + 1;
    for (int i = 1; i <= id[n]; ++i)
    {
        L[i] = (i - 1) * B + 1; // 每个块的左端点
        R[i] = min(i * B, n);   // 每个块的右端点,注意最后一个块可能不完整
    }
}

void bf_modify(int l, int r, int x) // 暴力修改
{
}

void modify(int l, int r, int x) // 修改
{
    if (id[l] == id[r]) // 如果在左右端点在同一个块内,直接暴力修改
    {
        bf_modify(l, r, x);
        return;
    }

    bf_modify(l, R[id[l]], x);
    bf_modify(L[id[r]], r, x);

    for (int i = id[l] + 1; i <= id[r] - 1; ++i) // 注意+1 和 -1
    {
    }
}

int bf_query(int l, int r, int x) // 暴力查询
{
}

int query(int l, int r, int x) // 查询
{
    int res = 0;
    if (id[l] == id[r]) // 如果在一个同一个块内直接暴力查询
    {
        return bf_query(l, r, x);
    }

    res += bf_query(l, R[id[l]], x);
    res += bf_query(L[id[r]], r, x);

    for (int i = id[l] + 1; i <= id[r] - 1; ++i)
    {
    }
    return res;
}

例1·区间求和

image-20230709141236819

题解

  • image-20230709142429919
  • 对于查询操作:
  • 如果\(l,r\)在同一个块内,直接暴力求和即可,复杂度\(O(B)\)
  • 如果\(l,r\)不在用一个块内,对散块(不完整块)暴力求和,对整块利用已经预处理的整块和\(B_i\)求和即可,复杂度 \(O(B + \frac{n}{B})\)
  • 对于修改操作:
  • 如果l,r在同一块内,直接暴力修改即可,复杂度\(O(B)\)
  • 如果l,r不在同一个块内,对散块暴力修改(但别忘了更新整块和\(B_i\)),对于整块直接修改\(B_i\)即可,复杂度为\(O(B + \frac{n}{B})\)
  • 利用均值不等式可知,当\(B = \frac{n}{B}\)时,即\(B = \sqrt n\)时,时间复杂度最优,且为\(O(\sqrt n)\)
const int N = 1e5 + 10, M = 4e5 + 10;
const int B = sqrt(1e5) + 1;

int n, m;
int a[N];
int tag[N];
int sum[N];
int id[N];
int L[N], R[N];

void init()
{
    for (int i = 1; i <= n; ++i)
    {
        id[i] = (i - 1) / B + 1;
        if (id[i] != id[i - 1])
        {
            L[id[i]] = i;
            R[id[i]] = min(n, i + B - 1);
        }
        tag[id[i]] = 0;
    }
    for (int i = 1; i <= n / B + 1; ++i)
    {
        for (int j = L[i]; j <= R[i]; ++j)
            sum[i] += a[j];
    }
}

void bf_modify(int l, int r, int x)
{
    for (int i = l; i <= r; ++i)
        a[i] += x;
    sum[id[l]] += x * (r - l + 1);
}

void modify(int l, int r, int x)
{
    if (id[l] == id[r])
    {
        bf_modify(l, r, x);
        return;
    }
    bf_modify(l, R[id[l]], x);
    bf_modify(L[id[r]], r, x);
    for (int i = id[l] + 1; i <= id[r] - 1; ++i)
        tag[i] += x;
}

int bf_query(int l, int r)
{
    int res = 0;
    for (int i = l; i <= r; ++i)
        res += a[i];
    return res;
}

int query(int l, int r)
{
    int res = 0;
    if (id[l] == id[r])
    {
        return bf_query(l, r) + tag[id[l]] * (r - l + 1);
    }
    res += bf_query(l, R[id[l]]) + tag[id[l]] * (R[id[l]] - l + 1);
    res += bf_query(L[id[r]], r) + tag[id[r]] * (r - L[id[r]] + 1);
    for (int i = id[l] + 1; i <= id[r] - 1; ++i)
    {
        res += sum[i] + tag[i] * (R[i] - L[i] + 1);
    }
    return res;
}

void solve()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    init();
    while (m--)
    {
        int op, x, y, k;
        cin >> op;
        if (op == 1)
        {
            cin >> x >> y >> k;
            modify(x, y, k);
        }
        else
        {
            cin >> x >> y;
            cout << query(x, y) << endl;
        }
    }
}

例2·LibreOJ - 6278

image-20230709143808650

\(1 \leq n \leq 5e4\)

题解

  • 首先我们应该对于每个块开一个\(vector\),并排序
  • 对于修改操作:
  • 如果\(l,r\)在同一个块中,我们直接暴力修改,复杂度\(O(B)\)
  • 如果\(l,r\)不在同一个块中,对于散块我们暴力修改,然后我们将\(vector\)清空后重新把该块的元素加入后重新排序(重构),复杂度\(O(BlogB)\);对于整块我们更新懒标记\(tag\)即可,复杂度\(O(B)\)
  • 那么修改的大致复杂度为\(O(B + BlogB)\)
  • 对于查询操作:
  • 如果\(l,r\)在同一个块中,我们直接暴力在序列\(a\)\([l,r]\)中查询即可,复杂度\(O(B)\)
  • 如果\(l,r\)不在同一个块中,对于散块我们直接暴力查询,复杂度\(O(B)\);对于整块我们二分求出个数,复杂度\(O(\frac{n}{B}\times logB)\)
  • 那么修改的大致复杂度为\(O(B + \frac{n}{B}\times logB)\)
const int N = 5e4 + 10, M = 5e4;
const int B = sqrt(M / log(M)) + 1;

int n;
int a[N];
int id[N], tag[N], L[N], R[N];
vector<int> g[N];

void init()
{
    for (int i = 1; i <= n; ++i)
    {
        id[i] = (i - 1) / B + 1;
        g[id[i]].push_back(a[i]);
    }
    for (int i = 1; i <= id[n]; ++i)
    {
        L[i] = (i - 1) * B + 1;
        R[i] = min(i * B, n);
        sort(all(g[i]));
    }
}

void bf_modify(int l, int r, int x)
{
    for (int i = l; i <= r; ++i)
        a[i] += x;
    g[id[l]].clear();
    for (int i = L[id[l]]; i <= R[id[l]]; ++i)
        g[id[l]].push_back(a[i]);
    sort(all(g[id[l]]));
}

void modify(int l, int r, int x)
{
    if (id[l] == id[r])
    {
        bf_modify(l, r, x);
        return;
    }

    bf_modify(l, R[id[l]], x);
    bf_modify(L[id[r]], r, x);

    for (int i = id[l] + 1; i <= id[r] - 1; ++i)
        tag[i] += x;
}

int bf_query(int l, int r, int x)
{
    int res = 0;
    for (int i = l; i <= r; ++i)
        if (a[i] + tag[id[i]] < x)
            res++;
    return res;
}

int query(int l, int r, int x)
{
    int res = 0;
    if (id[l] == id[r])
    {
        return bf_query(l, r, x);
    }

    res += bf_query(l, R[id[l]], x);
    res += bf_query(L[id[r]], r, x);

    for (int i = id[l] + 1; i <= id[r] - 1; ++i)
    {
        res += lower_bound(all(g[i]), x - tag[i]) - g[i].begin();
    }
    return res;
}

void solve()
{
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    init();
    for (int i = 1; i <= n; ++i)
    {
        int op, l, r, c;
        cin >> op >> l >> r >> c;
        if (op == 0)
            modify(l, r, c);
        else
            cout << query(l, r, c * c) << endl;
    }
}

例3·求区间最小众数

给定一个长度为\(n\)的序列\(a\),其中\(a_i>0\),每次询问一个区间\([l,r]\)中的出现次数最多且最小的数,即最小众数

\(1 \leq n \leq 10^4\)

\(1 \leq a_i \leq 10^9\)

题解

  • 我们在分块的时候预处理一个\(pair\)\(ans[i][j]\),代表第\(i\)个块到第\(j\)个块之间最小众数和该众数在这些块中的出现次数,预处理复杂度\(O(\frac{n}{B}\times n)\)
  • 对于查询操作:
  • 如果\(l,r\)在同一个块中,我们直接暴力查询,我们不妨对于每个数开一个\(vector\),在里面记录这个数在序列\(a\)中出现的位置,显然我们需要提前离散化,那么我们可以枚举\([l,r]\)区间中的每一个数,然后在\(vector\)中二分求出\(l\)\(r\)即可求出每一个数在该区间中的出现次数,复杂度\(O(B\times logn)\),然后知道出现次数,显然最小众数已然浮出水面
  • 如果\(l,r\)不在同一个块中,我们对于散块直接暴力查询散块中所有数在\([l,r]\)中出现次数,如果散块中某个数\(x\)出现的次数比整块\(ans[id[l] + 1][id[r] - 1]\)的来的大,那么\(x\)为最小众数,复杂度\(O(B \times logn)\)
const int N = 1e5 + 10;
const int B = 50;
const int M = 1000;

int n, q;
int a[N];
int id[N], tag[N], L[N], R[N];
pii ans[M][M];
vector<int> g[N];
map<int, int> mp;

void init()
{
    for (int i = 1; i <= n; ++i)
        id[i] = (i - 1) / B + 1;
    for (int i = 1; i <= id[n]; ++i)
    {
        L[i] = (i - 1) * B + 1;
        R[i] = min(i * B, n);
    }
    for (int i = 1; i <= id[n]; ++i)
    {
        int mx = 0, res = INF;
        vector<int> cnt(n + 10);
        for (int j = L[i]; j <= n; ++j)
        {
            cnt[a[j]]++;
            if (cnt[a[j]] > mx)
            {
                mx = cnt[a[j]];
                res = mp[a[j]];
            }
            else if (cnt[a[j]] == mx)
                res = min(res, mp[a[j]]);
            ans[i][(j - 1) / B + 1] = {res, mx};
        }
    }
}

inline pii Min(pii t1, pii t2)
{
    pii c;
    if (t1.second < t2.second)
    {
        c.second = t2.second;
        c.first = t2.first;
    }
    else if (t1.second > t2.second)
    {
        c.second = t1.second;
        c.first = t1.first;
    }
    else
    {
        if (t1.first <= t2.first)
        {
            c.first = t1.first;
            c.second = t1.second;
        }
        else
        {
            c.second = t2.second;
            c.first = t2.first;
        }
    }
    return c;
}

pii bf_query(int l, int r, int ql, int qr)
{
    int mx = 0, res = INF;
    for (int i = l; i <= r; ++i)
    {
        int x = lower_bound(all(g[a[i]]), ql) - g[a[i]].begin();
        int y = upper_bound(all(g[a[i]]), qr) - g[a[i]].begin() - 1;
        if (mx < y - x + 1)
        {
            res = mp[a[i]];
            mx = y - x + 1;
        }
        else if (mx == y - x + 1)
            res = min(res, mp[a[i]]);
    }
    return mpk(res, mx);
}

pii query(int l, int r)
{
    pii res = {INF, 0};
    if (id[l] == id[r])
        return bf_query(l, r, l, r);

    res = Min(res, bf_query(l, R[id[l]], l, r));
    res = Min(res, bf_query(L[id[r]], r, l, r));
    res = Min(res, ans[id[l] + 1][id[r] - 1]);

    return res;
}

void solve()
{
    cin >> n >> q;
    vector<int> vec;
    for (int i = 1; i <= n; ++i)
    {
        cin >> a[i];
        vec.push_back(a[i]);
    }
    sort(all(vec));
    vec.erase(unique(all(vec)), vec.end());
    for (int i = 1; i <= n; ++i)
    {
        int t = lower_bound(all(vec), a[i]) - vec.begin() + 1;
        mp[t] = a[i];
        a[i] = t;
        g[a[i]].push_back(i);
    }
    init();
    int x = 0;
    while (q--)
    {
        int l, r;
        cin >> l >> r;
        l = (l + x - 1) % n + 1;
        r = (r + x - 1) % n + 1;
        if (l > r)
            swap(l, r);
        x = query(l, r).first;
        cout << x << endl;
    }
}

例4·黑暗爆炸 - 2002

image-20230709193042847

\(1 \leq n \leq 2e5\)

题解

  • 我们分块后\(dp\)预处理出\(pair\)\(ans[i]\),代表从第\(i\)个位置跳出当前块需要的步数跳出后的落点位置
  • 如何\(dp\)预处理呢?首先我们知道在位置\(n\)一定是只要跳一步就可以跳出当前块 ,\(ans[n]=\{1,n + a_n\}\)
  • 那么我们不妨考虑从后往前\(dp\),即可预处理出\(ans[i]\),预处理复杂度\(O(n)\)
  • 对于单点修改操作:
  • 我们发现单点修改后只会影响这个点所在块内的所有信息,所以只需要这个块的右端点从后往前\(dp\)更新这个块中的\(ans\)即可,时间复杂度\(O(B)\)
  • 对于查询操作:
  • 我们利用维护的\(ans\)信息直接从查询点开始往后跳即可,复杂度为\(O(\frac{n}{B})\)
#include <bits/stdc++.h>
#define Zeoy std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0)
#define all(x) (x).begin(), (x).end()
#define rson id << 1 | 1
#define lson id << 1
#define int long long
#define mpk make_pair
#define endl '\n'
using namespace std;
typedef unsigned long long ULL;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<double, double> pdd;
const int inf = 0x3f3f3f3f;
const ll INF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1e9 + 7;
const double eps = 1e-9;
const int N = 3e5 + 10, M = 4e5 + 10;
const int B = 388;

int n, m;
int a[N];
int id[N], L[N], R[N];
pii ans[N]; // ans[i]代表从当前所属的块跳几步能够跳出当前块,且落在下一个块的落点位置

void build()
{
    for (int i = 1; i <= n; ++i)
        id[i] = (i - 1) / B + 1;
    for (int i = 1; i <= id[n]; ++i)
        L[i] = (i - 1) * B + 1, R[i] = min(n, i * B);
    for (int i = n; i >= 1; --i)
    {
        int step = 0, pos = 0;
        if (i + a[i] > R[id[i]])
        {
            step = 1;
            pos = i + a[i];
        }
        else
        {
            step = ans[i + a[i]].first + 1;
            pos = ans[i + a[i]].second;
        }
        ans[i] = mpk(step, pos);
    }
}

// 单点修改:重新更新块内ans信息 O(B)
void modify(int x, int val)
{
    a[x] = val;
    for (int i = x; i >= L[id[x]]; --i)
    {
        int step = 0, pos = 0;
        if (i + a[i] > R[id[i]])
        {
            step = 1;
            pos = i + a[i];
        }
        else
        {
            step = ans[i + a[i]].first + 1;
            pos = ans[i + a[i]].second;
        }
        ans[i] = mpk(step, pos);
    }
}

int query(int x)
{
    int res = 0;
    for (int i = x; i <= n; i = ans[i].second)
        res += ans[i].first;
    return res;
}

void solve()
{
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    build();
    cin >> m;
    while (m--)
    {
        int op, x, k;
        cin >> op;
        if (op == 1)
        {
            cin >> x;
            cout << query(x + 1) << endl;
        }
        else
        {
            cin >> x >> k;
            modify(x + 1, k);
        }
    }
}

莫队算法

  • 莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作
  • 莫队算法需要用到分块的思想
  • image-20230709194950382
  • 莫队算法实际上就是将查询离线后排序顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)
  • 查询的排序方法:对于区间\([l,r]\),以\(l\)所在块的编号为第一关键词升序排列,以\(r\)所在的块为第二关键词进行奇偶排序(块编号为奇数升序排列,块编号为偶数降序排列
  • 时间复杂度\(O(n \times \sqrt{n})\)

普通莫队模板

const int N = 1e5 + 10, M = 4e5 + 10;

int n, q;
int a[N];
int id[N];
int ans[N];
int preAns;

struct node
{
    int l, r, idx;
    bool operator<(const node &t) const
    {
        if (id[l] == id[t.l])
        {
            if (id[l] & 1)
                return r < t.r;
            else
                return r > t.r;
        }
        else
            return l < t.l;
    }
} qry[N];

void del(int x)
{
  
}

void add(int x)
{
    
}

void solve()
{
    cin >> n >> q;
    int B = n / sqrt(q) + 1;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= q; ++i)
    {
        cin >> qry[i].l >> qry[i].r;
        qry[i].idx = i;
    }
    for (int i = 1; i <= n; ++i) // 分块
        id[i] = (i - 1) / B + 1;
    sort(qry + 1, qry + q + 1);  // 对查询进行排序
    for (int i = 1, l = 1, r = 0; i <= q; ++i)
    {
        while (l > qry[i].l)
            add(a[--l]);
        while (r < qry[i].r)
            add(a[++r]);
        while (l < qry[i].l)
            del(a[l++]);
        while (r > qry[i].r)
            del(a[r--]);
        ans[qry[i].idx] = preAns;
    }
    for (int i = 1; i <= q; ++i)
        cout << ans[i] << endl;
}

例1·洛谷 - P1494

小 Z 把这 \(N\) 只袜子从 \(1\)\(N\) 编号,然后从编号 \(L\)\(R\) (\(L\) 尽管小 Z 并不在意两只袜子是不是完整的一双,甚至不在意两只袜子是否一左一右,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。

你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 \((L,R)\) 以方便自己选择。

然而数据中有 \(L=R\) 的情况,请特判这种情况,输出0/1

题解

  • 设询问区间长度为\(len = r - l + 1\),那么抽两只袜子的总方案数为\(C_{len}^{2}\)
  • 抽到两只颜色相同的袜子的方案数为,对于每一种袜子数量\(cnt\)超过2只的,其方案数为\(C_{cnt}^{2}\),那么抽到两只颜色相同的袜子的总方案数为所有袜子数量超过2只的方案数之和
  • 我们考虑莫队中的删除和添加操作:
  • 减去这只袜子原有的方案数
  • 更新袜子数量\(cnt\)
  • 加上更新后的方案数
const int N = 5e4 + 10, M = 5e4;
const int B = (5e4) / sqrt(M) + 1;

int n, q, k;
int a[N];
int id[N];
pii ans[N];
int preAns;
int mp[N];

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

void print(int a, int b)
{
    if (a == 0 || b == 0)
    {
        cout << 0 << "/" << 1 << endl;
        return;
    }
    int d = gcd(a, b);
    a /= d;
    b /= d;
    cout << a << "/" << b << endl;
}

struct node
{
    int l, r, idx;
    bool operator<(const node &t) const
    {
        if (id[l] == id[t.l])
        {
            if (id[l] % 2)
                return r < t.r;
            else
                return r > t.r;
        }
        else
            return l < t.l;
    }
} qry[N];

void del(int x)
{
    if (mp[x] >= 2)
        preAns -= (mp[x] * (mp[x] - 1)) / 2;
    mp[x]--;
    if (mp[x] >= 2)
        preAns += (mp[x] * (mp[x] - 1)) / 2;
}

void add(int x)
{
    if (mp[x] >= 2)
        preAns -= (mp[x] * (mp[x] - 1)) / 2;
    mp[x]++;
    if (mp[x] >= 2)
        preAns += (mp[x] * (mp[x] - 1)) / 2;
}

void solve()
{
    cin >> n >> q;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= q; ++i)
    {
        cin >> qry[i].l >> qry[i].r;
        qry[i].idx = i;
    }
    for (int i = 1; i <= n; ++i)
        id[i] = (i - 1) / B + 1;
    sort(qry + 1, qry + q + 1);
    for (int i = 1, l = 1, r = 0; i <= q; ++i)
    {
        while (l > qry[i].l)
            add(a[--l]);
        while (r < qry[i].r)
            add(a[++r]);
        while (l < qry[i].l)
            del(a[l++]);
        while (r > qry[i].r)
            del(a[r--]);
        if (qry[i].l == qry[i].r)
        {
            ans[qry[i].idx] = {0, 1};
            continue;
        }
        int t = qry[i].r - qry[i].l + 1;
        ans[qry[i].idx] = {preAns, (t * (t - 1)) / 2};
    }
    for (int i = 1; i <= q; ++i)
        print(ans[i].first, ans[i].second);
}

例2·洛谷 - P2709

小B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求: $$\sum\limits_{i=1}^k c_i^2$$

其中 \(c_i\) 表示数字 \(i\)\([l,r]\) 中的出现次数

题解

  • 我们考虑莫队中的删除和添加操作:设\(i\)的出现次数为\(cnt\)

  • 减去原有的贡献:\(cnt \times cnt\)

  • 更新\(cnt:=cnt + 1\ \ or\ \ cnt - 1\)

  • 加上更新后的贡献:\(cnt \times cnt\)

const int N = 5e4 + 10, M = 5e4;
const int B = sqrt(M) + 1;

int n, q, k;
int a[N];
int id[N];
int ans[N];
int preAns;
map<int, int> mp;

struct node
{
    int l, r, idx;
    bool operator<(const node &t) const
    {
        if (id[l] == id[t.l])
        {
            if (id[l] % 2)
                return r < t.r;
            else
                return r > t.r;
        }
        else
            return l < t.l;
    }
} qry[N];

void del(int x)
{
    preAns -= mp[x] * mp[x];
    mp[x]--;
    preAns += mp[x] * mp[x];
}

void add(int x)
{
    preAns -= mp[x] * mp[x];
    mp[x]++;
    preAns += mp[x] * mp[x];
}

void solve()
{
    cin >> n >> q >> k;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= q; ++i)
    {
        cin >> qry[i].l >> qry[i].r;
        qry[i].idx = i;
    }
    for (int i = 1; i <= n; ++i)
        id[i] = (i - 1) / B + 1;
    sort(qry + 1, qry + q + 1);
    for (int i = 1, l = 1, r = 0; i <= q; ++i)
    {
        while (l > qry[i].l)
            add(a[--l]);
        while (r < qry[i].r)
            add(a[++r]);
        while (l < qry[i].l)
            del(a[l++]);
        while (r > qry[i].r)
            del(a[r--]);
        ans[qry[i].idx] = preAns;
    }
    for (int i = 1; i <= q; ++i)
        cout << ans[i] << endl;
}

例3·CodeForces - 1514D Cut and Stick

给定一个长度为 \(n\) 的序列 \((n\le3\times10^5)\),可以对其进行三种操作:

  1. 把一段区间片段中所有的数从原来的区间里剪下来
  2. 把这些数按照在原来序列里的排列顺序重新拼接成序列
  3. 最后形成一个或者多个片段的序列,使最开始的序列中每一个数都属于某一个片段。

要求:给定 \(q\) 个询问 \((q\le3\times10^5)\) ,每次给出一个区间的左右端点,将这个区间分成若干个片段,使得每个片段内任意元素出现的次数不严格大于 \(\lceil\frac{x}{2}\rceil\) (\(x\) 为该片段长度)。求可以分成的最少片段数目

题解:莫队求区间众数 + 思维

  • 题意将一个区间中的所有元素分成几个序列,使得任意元素出现的次数不严格大于 \(\lceil\frac{x}{2}\rceil\) (\(x\) 为该片段长度),求可以分成的最少序列数

  • 容易发现如果区间\([l,r]\)中的众数的出现次数\(cnt \leq \lceil\frac{r - l + 1}{2}\rceil\),那么不用分,答案为1

  • 否则我们需要将区间众数分给不是众数的数,让其形成一个序列,显然该序列是合法的

  • 然后将多余的众数单独成为一个序列,因为如果不单独成为一个序列就一定不满足题目要求

  • 例如将\([1,1,1,1,1,2]=>[1],[1],[1],[1],[1,2]\)

  • 设众数个数为\(ans\),那么最少序列数目为\(ans - (r - l + 1 - ans) + 1\)

  • 所以题目就转化为求区间众数的出现次数,我们可以利用分块和莫队求区间众数,这里选择莫队:

  • 我们维护\(cnt[i]\)\(i\)出现的次数,\(num[i]\):出现\(i\)次的数的个数,我们考虑莫队的删除操作

  • num[cnt[x]]--;
    cnt[x]--;
    num[cnt[x]]++;
    
  • 因为删除可能会导致众数改变,所以我们需要维护一下当前删除\(x\)后,设原有的众数的出现次数为\(cnt\),如果删除后\(num[cnt]=0\),说明众数已经改变,我们更新众数的出现次数

  • 对于添加操作,直接维护每个数出现次数的最大值即可

const int N = 3e5 + 10, M = 4e5 + 10;

int n, q;
int a[N];
int id[N];
int ans[N];
int preAns;
int cnt[N]; // cnt[i]记录数字i在区间内出现的次数
int num[N]; // num[i]记录出现在区间内出现i次的数字的个数

struct node
{
    int l, r, idx;
    bool operator<(const node &t) const
    {
        if (id[l] == id[t.l])
        {
            if (id[l] % 2)
                return r < t.r;
            else
                return r > t.r;
        }
        else
            return l < t.l;
    }
} qry[N];

void del(int x)
{
    num[cnt[x]]--;
    num[--cnt[x]]++;
    if (cnt[x] + 1 == preAns && num[preAns] == 0)
        preAns--;
}

void add(int x)
{
    num[cnt[x]]--;
    num[++cnt[x]]++;
    preAns = max(preAns, cnt[x]);
}

void solve()
{
    cin >> n >> q;
    int B = n / sqrt(q) + 1;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= q; ++i)
    {
        cin >> qry[i].l >> qry[i].r;
        qry[i].idx = i;
    }
    for (int i = 1; i <= n; ++i)
        id[i] = (i - 1) / B + 1;
    sort(qry + 1, qry + q + 1);
    for (int i = 1, l = 1, r = 0; i <= q; ++i)
    {
        while (l > qry[i].l)
            add(a[--l]);
        while (r < qry[i].r)
            add(a[++r]);
        while (l < qry[i].l)
            del(a[l++]);
        while (r > qry[i].r)
            del(a[r--]);
        if (preAns <= (r - l + 1 + 1) / 2)
            ans[qry[i].idx] = 1;
        else
            ans[qry[i].idx] = 2 * preAns - (r - l + 1);
    }
    for (int i = 1; i <= q; ++i)
        cout << ans[i] << endl;
}

例4·求区间内不同元素的数量

题解

  • 对于莫队的删除操作:
  • 如果删除完后\(cnt[x]= 0\),说明少了一个不同元素,答案贡献 \(-1\)
  • 对于莫队的添加操作:
  • 如果添加完后\(cnt[x] = 1\),说明多了一个不同元素,答案贡献 \(+ 1\)
const int N = 2e5 + 10, M = 2e5;
const int B = (3e4) / sqrt(M) + 1;

int n, q, k;
int a[N];
int id[N];
int ans[N];
int preAns;
map<int, int> mp;

struct node
{
    int l, r, idx;
    bool operator<(const node &t) const
    {
        if (id[l] == id[t.l])
        {
            if (id[l] % 2)
                return r < t.r;
            else
                return r > t.r;
        }
        else
            return l < t.l;
    }
} qry[N];

void del(int x)
{
    if (mp[x] == 1)
        preAns--;
    mp[x]--;
}

void add(int x)
{
    if (mp[x] == 0)
        preAns++;
    mp[x]++;
}

void solve()
{
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    cin >> q;
    for (int i = 1; i <= q; ++i)
    {
        cin >> qry[i].l >> qry[i].r;
        qry[i].idx = i;
    }
    for (int i = 1; i <= n; ++i)
        id[i] = (i - 1) / B + 1;
    sort(qry + 1, qry + q + 1);
    for (int i = 1, l = 1, r = 0; i <= q; ++i)
    {
        while (l > qry[i].l)
            add(a[--l]);
        while (r < qry[i].r)
            add(a[++r]);
        while (l < qry[i].l)
            del(a[l++]);
        while (r > qry[i].r)
            del(a[r--]);
        ans[qry[i].idx] = preAns;
    }
    for (int i = 1; i <= q; ++i)
        cout << ans[i] << endl;
}
posted @ 2023-07-09 21:17  Zeoy_kkk  阅读(21)  评论(0编辑  收藏  举报