莫队算法

莫队算法

普通莫队

1.例题:小Z的袜子
2.能够解决怎么样的问题?
莫队算法能够将一些不强制在线的区间查询问题,使用离线分块的思想,然后利用双指针,将算法的复杂度降低。
3.算法实现原理:
因为是不带修改的,所以我们可以用双关键字进行排序,第一关键字是左端点所在块的编号,第二关键字是右端点的位置,编号越靠前越优先,右端点也是越靠前越优先。进行排序完之后,我们就可以直接用双指针暴力更新答案了。
4.莫队常数优化:如果块号相同,那么我们可以分块的奇偶性来排序,如果为奇数,那么按右端点升序排序,如果为偶数,那么按右端点降序排序。
5.复杂度分析:
莫队算法的优势就在于分块排序优化了复杂度。假设范围大小为n,有m个询问,m=O(n)。如果我们按照读入的顺序来枚举每个询问,或者按照左端点排序和右端点排序,那么你可以想象到L和R疯狂转移的情况,这样时间复杂度最坏依然是O(nm)=\(O(n^{2})\)
而使用分块排序,再分析相邻查询转移次数。我们假设块的长度是B,块的总数是\(\frac{n}{B}\)
① 对于L的移动:同一块内最多移动B次。若下一个询问与当前的L不在同一块,那么最长只需要2B的长度即可到达目标端点,计算量最高2mB。
② 对于R的移动:R当L在同一块内时有序,所以枚举完一个块,R最多共移动n次,换块时,再至多移动n次。总共有\(\frac{n}{B}\)个块,计算量最高为\(\frac{n}{B}(n + n) = \frac{2n^{2}}{B}\)
合起来总的复杂度为\(2mB + \frac{2n^{2}}{B}\),当取\(B = \sqrt{\frac{n^{2} }{m} }\)时复杂度最低:\(O(n\sqrt{n})\)。当m较小时取\(\sqrt{n}\)即可。
6.例题代码:

#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define lc p<<1
#define rc p<<1|1
#define int long long
#define vc vector<char>
#define vi vector<int>
#define vvi vector<vector<int>>
#define vpi vector<pair<int,int>>
#define vvc vector<vector<char>>
#define vvpi vector<vector<pair<int,int>>>
typedef long long ll;
typedef tuple<int,int,int> tp;
typedef pair<int,int> PII;
typedef pair<ll,pair<ll,ll>> PIII;
typedef pair<ll,pair<ll,pair<ll,ll>>> pIIII;
const int N = 1e5 + 7;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
const int none = -1e9 - 1;
int B,n,m,k;
struct query {
    int l,r,id;
    ll p,c;
    query(){};
    query(int l0,int r0,int id0) : l(l0),r(r0),id(id0) {
        this->p = 0;
        this->c = (r - l + 1)*(r - l)/2;
    }
    bool operator <(const query &a) const {
        if(l/B != a.l/B) return l < a.l;
        if(l/B % 2 == 1) return r < a.r;
        return r > a.r;
    }
}Q[N];
void solve() {
    int n,q;cin >> n >> q;
    vi a(n + 1);
    vector<ll> color(n + 1);
    B = sqrt(n);
    for(int i = 1; i <= n; i++) cin >> a[i];
    for(int i = 1; i <= q; i++) {
        int l,r;cin >> l >> r;
        Q[i] = query(l,r,i);
    }

    sort(Q + 1,Q + 1 + q);
//    for(int i = 1; i <= q; i++) {
//        cout << Q[i].l << " " << Q[i].r << "\n";
//    }
    auto add = [&](int x,query &t) {
        t.p += color[x]++;
    };
    auto del = [&](int x,query &t) {
        t.p -= --color[x];
    };
    Q[0] = query(0,0,0);
    for(int i = 1,l = 1,r = 0; i <= q; i++) {
        int l1 = Q[i].l,r1 = Q[i].r,id = Q[i].id;
        Q[i].p = Q[i - 1].p;
        while(l > l1) add(a[--l],Q[i]);
        while(r < r1) add(a[++r], Q[i]);
        while(l < l1) del(a[l++],Q[i]);
        while(r > r1) del(a[r--],Q[i]);
    }
    sort(Q + 1,Q + 1 + q,[&](query &t1,query &t2) {
        return t1.id < t2.id;
    });
    for(int i = 1; i <= q; i++) {
       int gcd = __gcd(Q[i].p,Q[i].c);
       if(gcd != 0) Q[i].p /= gcd,Q[i].c /= gcd;
       if(Q[i].p == 0) cout << "0/1\n";
       else cout << Q[i].p << "/" << Q[i].c << "\n";
    }
}
signed main() {
    std::ios::sync_with_stdio(0);
    std::cin.tie(0);
    int t = 1;
    //cin >> t;
    while(t--) {
        solve();
    }
    //system("pause");
    return 0;
}
/*
Do smth instead of nothing and stay organized
Don't get stuck on one approach
*/

带修莫队

  1. 例题:数颜色
  2. 算法分析:由于多了一个时间戳,所以不能按照原来的排序方式,否则时间复杂度就会回到\(O(n^{2})\),所以我们要改变排序的规则,第一关键字是左端点编号的大小,第二关键字是右端点所在块的编号,第三关键字是所在的修改的时间戳。
  3. 时间复杂度分析:
    假设这个是排完序之后的样子:

    那么转移如下:

    综上所述,时间复杂度的分析如下:

    当B取 \(n^{\frac{2}{3}}\)时整体取最小值
  4. 代码如下:
#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define lc p<<1
#define rc p<<1|1
#define int long long
#define vc vector<char>
#define vi vector<int>
#define vvi vector<vector<int>>
#define vpi vector<pair<int,int>>
#define vvc vector<vector<char>>
#define vvpi vector<vector<pair<int,int>>>
typedef long long ll;
typedef tuple<int,int,int> tp;
typedef pair<int,int> PII;
typedef pair<ll,pair<ll,ll>> PIII;
typedef pair<ll,pair<ll,pair<ll,ll>>> pIIII;
const int N = 2e5 + 7;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
const int none = -1e9 - 1;
int B;
struct query {
    int l,r,tim,id;
    query(){};
    query(int l0,int r0,int tim0,int id0) : l(l0),r(r0),tim(tim0),id(id0) {};
    bool operator <(const query &a) const {
        if(l/B != a.l/B) return l < a.l;
        else if(r/B != a.r/B) return r < a.r;
        return tim < a.tim;
    }
}Q[N];
void solve() {
    int n,m;cin >> n >> m;
    vi a(n + 1),color(N*10);
    int cntq = 0,cntc = 0,cnt = 0;
    vpi change(m + 1);
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    B = pow(n,2.0/3.0);
    for(int i = 1; i <= m; i++) {
        char ch;cin >> ch;
        if(ch == 'Q') {
            ++cntq;
            int l,r;cin >> l >> r;
            Q[cntq] = query(l,r,cntc,cntq);
        } else {
            ++cntc;
            int p,x;cin >> p >> x;
            change[cntc] = {p,x};
        }
    }
    auto add = [&](int x) {
        if(color[x] == 0) cnt += 1;
        color[x]++;
    };
    auto del = [&](int x) {
        if(color[x] == 1) cnt -= 1;
        color[x]--;
    };
    sort(Q + 1,Q + 1 + cntq);
    vi ans(m + 1);
    for(int i = 1,l = 1,r = 0,now = 0; i <= cntq; i++) {
        int l1 = Q[i].l,r1 = Q[i].r,id = Q[i].id;
        while(l > l1) add(a[--l]);
        while(r < r1) add(a[++r]);
        while(l < l1) del(a[l++]);
        while(r > r1) del(a[r--]);
        while(now < Q[i].tim) {
            int p = change[++now].fi;
            if(p >= l && p <= r) del(a[p]),add(change[now].se);
            swap(a[p],change[now].se);
        }
        while(now > Q[i].tim) {
            int p = change[now].fi;
            if(p >= l && p <= r) del(a[p]),add(change[now].se);
            swap(a[p],change[now--].se);
        }
        ans[id] = cnt;
    }
    for(int i = 1; i <= cntq; i++) {
        cout << ans[i] << "\n";
    }
}
signed main() {
    std::ios::sync_with_stdio(0);
    std::cin.tie(0);
    int t = 1;
    //cin >> t;
    while(t--) {
        solve();
    }
    //system("pause");
    return 0;
}
/*
Do smth instead of nothing and stay organized
Don't get stuck on one approach
*/

树上莫队

  1. 例题:Count on a tree II
  2. 算法实现:整个实现原理是和普通莫队时一样的,但是问题是如何将一棵树形结构变成序列结构?我们可以使用欧拉序,那么u到v的路径就可以就可以分两种情况来讨论:
    假设lca是u,v的最近公共祖先,fst[i]表示i在欧拉序中第一次出现的时间,lst[i]表示i最后一次在欧拉序中出现的时间
    <1>,如果lca是u或者v中的某一个,那么在欧拉序中u,v之间的路径就是fst[u] - fst[v],因为u作为lca,lst[u]肯定在fst[v]之前,所以可以这样表示。其中如果要计算颜色数量的话,那么出现两次的点其实是不在路径里面的,因为出现两次其实就相当于进去然后再出来。
    <2>否则的话,假设fst[v] > lst[u],那么lst[u]-fst[v]的这一段序列就是u到v的路径。
  3. 代码如下:
#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define lc p<<1
#define rc p<<1|1
#define int long long
#define vc vector<char>
#define vi vector<int>
#define vvi vector<vector<int>>
#define vpi vector<pair<int,int>>
#define vvc vector<vector<char>>
#define vvpi vector<vector<pair<int,int>>>
typedef long long ll;
typedef tuple<int,int,int> tp;
typedef pair<int,int> PII;
typedef pair<ll,pair<ll,ll>> PIII;
typedef pair<ll,pair<ll,pair<ll,ll>>> pIIII;
const int N = 5e5 + 7;
const int inf = 0x3f3f3f3f;
const int mod = 998244353;
const int none = -1e9 - 1;
int B;
struct query {
    int l,r,id;
    int Lca;
    query(){}

    query(int l0,int r0,int id0) : l(l0),r(r0),id(id0) {
        Lca = 0;
    }
    bool operator < (const query &a)const {
        return (l/B == a.l/B ? r < a.r : l < a.l);
    }
}q[N];
bool vis[N];
void solve() {
    int n,m;cin >> n >> m;
    B = sqrt(n*2);
    map<int,int> mp;
    int cnt = 0;
    vi color(n*2 + 100000),fst(n*2 + 100000),lst(n*2 + 100000);
    vi father(n*2 + 100000),dep(n*2 + 100000),dfn(n*2 + 100000);
    vvi f(n + 1,vi(20));
    for(int i = 1; i <= n; i++) {
        int t;cin >> t;
        if(mp.find(t) == mp.end()) mp[t] = ++cnt;
        color[i] = mp[t];
    }
    vvi g(n + 1,vi());
    for(int i = 1; i <= n - 1; i++) {
        int u,v;cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    int tot = 0;
    function<void(int,int)> dfs = [&](int u,int fa) -> void {
        dep[u] = dep[fa] + 1;
        f[u][0] = fa;
        dfn[++tot] = u;
        fst[u] = tot;
        for(int i = 1; i <= 19; i++)
            f[u][i] = f[f[u][i - 1]][i - 1];
        for(auto v : g[u]) {
            if(v == fa) continue;
            dfs(v,u);
        }
        dfn[++tot] = u;
        lst[u] = tot;
    };
    dfs(1,0);
    auto lca = [&](int u,int v) -> int {
        if(dep[u] < dep[v]) swap(u,v);
        for(int i = 19; i >= 0; i--)
            if(dep[f[u][i]] >= dep[v])
                u = f[u][i];
        if(u == v) return v;
        for(int i = 19; i >= 0; i--)
            if(f[u][i] != f[v][i])
                u = f[u][i],v = f[v][i];
        return f[u][0];
    };
    for(int i = 1; i <= m; i++) {
        int l,r;cin >> l >> r;
        int Lca = lca(l,r);
        if(Lca == l || Lca == r) {
            if(fst[r] < fst[l]) swap(l,r);
            q[i] = query(fst[l],fst[r],i);
        } else {
            if(lst[r] < fst[l]) swap(l,r);
            q[i] = query(lst[l],fst[r],i);
            q[i].Lca = Lca;
        }
    }
    sort(q + 1,q + 1 + m);
    vi sum(n*2 + 100000),ans(m + 10);
    int res = 0;
    auto update = [&](int x) {
        if(!(vis[x] ^= 1) && (--sum[color[x]]) == 0) res--;
        if(vis[x] && (sum[color[x]]++) == 0) res++;
    };
    for(int i = 1, l = 1,r = 0; i <= m; i++) {
        while(l > q[i].l) update(dfn[--l]);
        while(r < q[i].r) update(dfn[++r]);
        while(l < q[i].l) update(dfn[l++]);
        while(r > q[i].r) update(dfn[r--]);
        if(q[i].Lca) update(q[i].Lca);
        ans[q[i].id] = res;
        if(q[i].Lca) update(q[i].Lca);
    }
    for(int i = 1; i <= m; i++) {
        cout << ans[i] << "\n";
    }
}
signed main() {
    std::ios::sync_with_stdio(0);
    std::cin.tie(0);
    int t = 1;
    //cin >> t;
    while(t--) {
        solve();
    }
    //system("pause");
    return 0;
}
/*
Do smth instead of nothing and stay organized
Don't get stuck on one approach
*/
posted @ 2024-04-11 11:28  orzkeyhacker  阅读(46)  评论(0编辑  收藏  举报