📜  ≈O(1)时间复杂度的扩展Mo算法(1)

📅  最后修改于: 2023-12-03 15:35:51.617000             🧑  作者: Mango

扩展Mo算法

Mo算法是一种区间查询算法,其时间复杂度为O(Q * sqrt(n)),其中Q为查询次数,n为数组长度。在对数据进行多次查询时,这种时间复杂度可能会影响性能。扩展Mo算法通过引入一个新的维度,将时间复杂度优化到了≈O(1)

算法过程

扩展Mo算法可以看作是在二维平面上进行的算法,其中x轴表示数组下标,y轴表示询问。首先,将所有询问按照左端点所在区间的块进行排序,并在同一块中按照右端点排序;然后,将询问按块分组,每组查询按照右端点从小到大进行;最后,执行查询操作。

在执行查询操作时,需要维护两个指针l和r,分别表示当前查询区间的左右端点。如果询问的左端点在当前块内,则直接计算;否则,将l指向下一块的左端点,并重新计算。对于右端点同理,如果当前询问的右端点不在查询区间中,则将r指向查询区间的右端点,并重新计算。

优化

扩展Mo算法的时间复杂度除了与询问次数和数组长度有关之外,还与块长有关。因此,块长的选择是影响时间复杂度的一个重要因素。一般情况下,块长的选择范围在sqrt(n) ~ n之间,取到最优解时时间复杂度才能达到≈O(1)

实现

以下是一个使用C++实现的扩展Mo算法代码片段:

const int MAXN = 1e5 + 5;
const int MAXQ = 1e5 + 5;

int n, q, len, cnt, ans;
int A[MAXN], cnt[MAXN], res[MAXQ], bel[MAXQ], L[MAXQ], R[MAXQ];
std::vector<int> query[MAXQ];

struct Query {
    int l, r, id;
    bool operator < (const Query &rhs) const {
        if (bel[l] != bel[rhs.l]) return bel[l] < bel[rhs.l];
        return bel[l] & 1 ? r < rhs.r : r > rhs.r;
    }
} q[MAXQ];

inline void add(int x) { ans += (cnt[x] << 1 | 1) * x; ++cnt[x]; }
inline void del(int x) { ans -= (cnt[x] << 1 | 1) * x; --cnt[x]; }

int main() {
    scanf("%d%d", &n, &q);
    len = std::sqrt(n); cnt = 1;
    for (int i = 1; i <= n; ++i) scanf("%d", A + i);
    for (int i = 1; i <= q; ++i) {
        q[i].id = i; scanf("%d%d", &q[i].l, &q[i].r);
        bel[i] = (q[i].l - 1) / len + 1; L[i] = (bel[i] - 1) * len + 1; R[i] = bel[i] * len;
    }
    if (R[q[qcnt].id] > n) R[q[qcnt].id] = n;
    std::sort(q + 1, q + q + 1);
    for (int i = 1, l = 1, r = 0; i <= q; ++i) {
        while (l < q[i].l) del(A[l++]);
        while (l > q[i].l) add(A[--l]);
        while (r < q[i].r) add(A[++r]);
        while (r > q[i].r) del(A[r--]);
        res[q[i].id] = ans;
    }
    for (int i = 1; i <= q; ++i) printf("%d\n", res[i]);
    return 0;
}

其中,query数组用于存储每个块内含有的询问的index,cnt数组用于记录每个数的出现次数,res数组用于记录每个查询的结果,bel数组用于记录每个询问所属的块编号,L数组和R数组用于记录每个块的左右端点。另外,add和del函数用于修改区间的答案。