暴力出奇迹,n2n^2 过百万

普通莫队

[SDOI2009]HH的项链

虽然这道题 nn10610^6 但卡一卡还能过

首先考虑最暴力的暴力,直接枚举就行了

然后莫队是怎样去优化这个暴力的?

先定义两个指针 l,rl,r 表示当前已经求出区间 [l,r][l,r] 的答案,对于询问 [L,R][L,R],

实际上只需要移动 l,rl,r 即可,如果有一种办法能够使移动的次数最小,时间复杂度会不会降下来呢?

莫队正是利用了这一点,考虑离线处理所有询问,对区间左端点分块,对于左端点在同一块中的两个询问,

按照右端点编号排序,否则,按照左端点编号排序,这里有一个优化,奇偶性排序,就是奇数块升序排,偶数块降序排,

对于一些大数据可能会有用.那么这么排为什么会快?

先考虑左端点,左端点只会有两种移动方式,在块内移动与再两个块间移动

块内移动的距离显然是不超过块的大小的,这里定义块的大小为 n\sqrt{n},一共 mm 此询问,那么时间复杂度就是 O(mn)O(m\sqrt{n})

在两个块之间移动,考虑最远距离 2n2\sqrt{n},总时间复杂度也是 O(mn)O(m\sqrt{n})

再考虑右端点,因为对于左端点再同一个块的询问,右端点是单调递增的,那么就是 O(n)O(n),

一共有 n\sqrt{n} 个块,总时间复杂度就是 nnn\sqrt{n}

综上所述,时间复杂度可以认为是 O(nn)O(n\sqrt{n}) 的,有时候如果TLE了可以尝试修改块的大小

(这里没有画图,可以自己结合图像方便理解)

代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#define get(x) ((x) / len)
using namespace std;
const int N = 1e6 + 5;

int n, len, cnt[N], w[N], ans[N];
struct query
{
int id, l, r;
bool operator < (const query t) const
{
int a = get(l), b = get(t.l);
if (a != b)
return a < b;
else
{
if (a & 1)
return r < t.r;
else
return r > t.r;
}
}
} q[N];

inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch>='0'&&ch<='9')
{
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}

int main()
{
int n;
n = read();
for (int i = 1; i <= n; i++)
w[i] = read();
int m;
m = read();
len = 1720;
for (int i = 1; i <= m; i++)
q[i].l = read(), q[i].r = read(), q[i].id = i;
sort(q + 1, q + 1 + m);
int res = 0;
for (int k = 1, i = 0, j = 1; k <= m; k++)
{
int id = q[k].id, l = q[k].l, r = q[k].r;
while (i < r)
{
int t = w[++i];
if (!cnt[t]) res++;
cnt[t]++;
}
while (i > r)
{
int t = w[i--];
cnt[t]--;
if (!cnt[t]) res--;
}
while (j > l)
{
int t = w[--j];
if (!cnt[t]) res++;
cnt[t]++;
}
while (j < l)
{
int t = w[j++];
cnt[t]--;
if (!cnt[t]) res--;
}
ans[id] = res;
}
for (int i = 1; i <= m; i++)
printf("%d\n", ans[i]);
return 0;
}

带修莫队

[国家集训队]数颜色 / 维护队列

多了个修改操作而已,只需要再多开一个指针 tt 表示当前已经处理到第 tt 个修改

那么排序规则就是使 tt 单调递增, 对左端点与右端点分块,其他的与普通莫队类似

只不过在 tt 转移时,有个比较取巧的方式,就是每次修改时交换序列里的值与修改操作的值,

这样,在回退的时候也可以直接交换,会方便一些.

那么带修莫队的时间复杂度又是多少呢?还是 O(nn)O(n\sqrt{n})?.

我们设块长为 aa, 那么一共就有 na\frac{n}{a}

考虑左端点,同样时在块内与块间移动,时间复杂度 O(am+2ana)O(am+2a\frac{n}{a})

考虑 tt,类似普通莫队,时间复杂度 O(mn2a2)O(m\frac{n^2}{a^2})

最后考虑右端点,块内移动与 ll 类似,考虑块间,对于左端点的每一个块, rr 都会移动 nn 步,所以实际按复杂度就是 O(am+n2a)O(am+\frac{n^2}{a})

但是 max(O(am+n),O(mn2a2),O(am+n2a))max(O(am+n),O(m\frac{n^2}{a^2}),O(am+\frac{n^2}{a})) 该怎么取呢?

首先,我们肯定要让 ana\ge\sqrt{n},不然 O(mn2a2)O(m\frac{n^2}{a^2}) 就是 n2n^2 级别的了

所以忽略掉较小项就是 max(O(am),O(mn2a2))max(O(am),O(m\frac{n^2}{a^2}))

要让这个最小就是 am=mn2a2a=n23am=m\sqrt{\frac{n^2}{a^2}}\Rightarrow a=n^{\frac{2}{3}}

时间复杂度就是 O(n53)O(n^{\frac{5}{3}}),卡一卡能过

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#define p first
#define val second
#define get(x) ((x) / len)
using namespace std;
const int N = 2e5 + 5;
typedef pair<int, int> PII;

PII my[N];
int w[N], cnt, tot, len, tim[N * 10], ans[N];
struct query
{
int id, l, r, t;
bool operator < (const query a) const
{
int al = get(l), ar = get(r);
int bl = get(a.l), br = get(a.r);
if (al != bl) return al < bl;
if (ar != br) return ar < br;
return t < a.t;
}
} q[N];

int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &w[i]);
for (int i = 1; i <= m; i++)
{
char opt;
int l, r;
scanf(" %c", &opt);
if (opt == 'Q')
{
scanf("%d%d", &l, &r);
cnt++;
q[cnt] = query({cnt, l, r, tot});
}
else
{
scanf("%d%d", &l, &r);
my[++tot] = make_pair(l, r);
}
}
len = pow(n, 2.0 / 3);
sort(q + 1, q + 1 + cnt);
for (int k = 1, i = 0, j = 1, t = 0, res = 0; k <= cnt; k++)
{
int id = q[k].id, l = q[k].l, r = q[k].r, to = q[k].t;
while (i < r)
{
int tp = w[++i];
if (!tim[tp]) res++;
tim[tp]++;
}
while (i > r)
{
int tp = w[i--];
tim[tp]--;
if (!tim[tp]) res--;
}
while (j > l)
{
int tp = w[--j];
if (!tim[tp]) res++;
tim[tp]++;
}
while (j < l)
{
int tp = w[j++];
tim[tp]--;
if (!tim[tp]) res--;
}
while (t < to)
{
t++;
if (my[t].p >= j && my[t].p <= i)
{
int pi = my[t].p;
tim[w[pi]]--;
if (!tim[w[pi]]) res--;
if(!tim[my[t].val]) res++;
tim[my[t].val]++;
}
swap(my[t].val, w[my[t].p]);
}
while (t > to)
{
if (my[t].p >= j && my[t].p <= i)
{
int pi = my[t].p;
tim[w[pi]]--;
if (!tim[w[pi]]) res--;
if(!tim[my[t].val]) res++;
tim[my[t].val]++;
}
swap(my[t].val, w[my[t].p]);
t--;
}
ans[id] = res;
}
for (int i = 1; i <= cnt; i++)
printf("%d\n", ans[i]);
return 0;
}

回滚莫队

【模板】回滚莫队&不删除莫队

对于一些不好维护删除操作,而比较好维护添加操作的要求,比如求max,可以用回滚莫队

排序还是一样的排,但实现方式略有不同,对于左端点在同一块内的询问,我门将它分为块内与块间两个部分

对于块内的部分,直接暴力就行了,反正不会超过n\sqrt{n},对于块间的部分,因为是单调的,所以也不会超过 nnn\sqrt{n}

那为什么要叫回滚呢?就是每次在处理询问时,我们记录一个备份,对于块内的询问,我们在记录完答案后还原,因为块外是单调的,所以不需要管

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define pl first
#define pr second
#define INF 0x3f3f3f3f
#define get(x) ((x) / len)
using namespace std;
const int N = 2e5 + 5;
typedef long long ll;
typedef pair<int, int> PII;

PII bp[N], pi[N];
int len, w[N], ans[N], temp[N], val[N], cnt;
struct query
{
int id, l, r;
bool operator < (const query a) const
{
int x = get(l), y = get(a.l);
if (x != y) return x < y;
else return r < a.r;
}
} q[N];

inline int find(int x)
{
int l = 1, r = cnt;
while (l <= r)
{
int mid = (l + r) >> 1;
if (temp[mid] < x)
l = mid + 1;
else if (temp[mid] == x)
return mid;
else
r = mid - 1;
}
}

inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}

int main()
{
// freopen("1.in", "r", stdin);
int n = read();
for (int i = 1; i <= n; i++)
val[i] = w[i] = read();
sort(w + 1, w + 1 + n);
for (int i = 1; i <= n; i++)
if (temp[cnt] != w[i])
temp[++cnt] = w[i];
for (int i = 1; i <= n; i++)
w[i] = find(val[i]);
int m = read();
for (int i = 1; i <= m; i++)
q[i].l = read(), q[i].r = read(), q[i].id = i;
len = sqrt(n);
sort(q + 1, q + 1 + m);
for (int x = 1; x <= m;)
{
int y = x;
while (y <= m && get(q[x].l) == get(q[y].l)) y++;
int R = get(q[x].l) * len + len;
while (x < y && q[x].r <= R)
{
int res = 0;
int id = q[x].id, l = q[x].l, r = q[x].r;
for (int k = l; k <= r; k++)
bp[w[k]] = pi[w[k]];
for (int k = l; k <= r; k++)
{
bp[w[k]].pr = k;
if (!bp[w[k]].pl)
bp[w[k]].pl = k;
res = max(res, k - bp[w[k]].pl);
}
ans[id] = res;
x++;
}
int res = 0, i = R, j = R + 1;
while (x < y)
{
int id = q[x].id, l = q[x].l, r = q[x].r;
while (i < r)
{
i++, pi[w[i]].pr = i;
if (!pi[w[i]].pl)
pi[w[i]].pl = i;
res = max(res, i - pi[w[i]].pl);
}
int backup = res;
while (j > l)
{
if (!pi[w[--j]].pr)
pi[w[j]].pr = j;
res = max(res, pi[w[j]].pr - j);
}
while (j <= R)
{
if (pi[w[j]].pr == j)
pi[w[j]].pr = 0;
j++;
}
ans[id] = res;
res = backup;
x++;
}
for (int i = 1; i <= cnt; i++)
pi[i].pl = pi[i].pr = 0;
}
for (int i = 1; i <= m; i++)
printf("%d\n", ans[i]);
return 0;
}

树上莫队

对于树上问题,一个比较普遍的做法就是转化为序列问题,那么对于树上莫队,我们可以将他转化为欧拉序

记录每个点在欧拉序中第一个出现的位置 firfir 与第二个出现的位置 secsec,对于当前的 u,vu,v 之间的路径 若 uuvv 的父亲,就是直接在欧拉序 [firu,firv][fir_u,fir_v] 中只出现了一次的数

uu, vv 在两颗子树内,就找 [secu,firv][sec_u,fir_v] 中只出现了一次的数,然后再加上 lca(u,v)lca(u,v) 的值.

这里想一想就能明白,统计次数和普通莫队类似可以参考代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define get(x) ((x) / len)
using namespace std;
const int N = 1e5 + 5;

bool st[N];
int fir[N], sec[N], seq[N], top;
int head[N], ver[N], net[N], idx;
int w[N], temp[N], val[N], cnt[N], len;
int depth[N], fa[N][21], ans[N], tot;
struct query
{
int id, l, r, p;
bool operator < (const query a) const
{
int x = get(l), y = get(a.l);
if (x != y) return x < y;
if (x & 1) return r < a.r;
return r > a.r;
}
} q[N];

inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (x == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}

inline void add(int a, int b)
{
net[++idx] = head[a], ver[idx] = b, head[a] = idx;
net[++idx] = head[b], ver[idx] = a, head[b] = idx;
}

void dfs(int u, int f)
{
seq[++top] = u, fir[u] = top;
depth[u] = depth[f] + 1, fa[u][0] = f;
for (int i = 1; i <= 17; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int i = head[u]; i; i = net[i])
{
int v = ver[i];
if (v == f) continue;
dfs(v, u);
}
seq[++top] = u, sec[u] = top;
}

inline int lca(int u, int v)
{
if (depth[u] < depth[v]) swap(u, v);
for (int i = 17; i >= 0; i--)
if (depth[fa[u][i]] >= depth[v])
u = fa[u][i];
if (u == v) return u;
for (int i = 17; i >= 0; i--)
if (fa[u][i] != fa[v][i])
u = fa[u][i], v = fa[v][i];
return fa[u][0];
}

inline void modify(int x, int& res)
{
st[x] ^= 1;
if (st[x] == 0)
{
cnt[w[x]]--;
if (!cnt[w[x]]) res--;
}
else
{
if (!cnt[w[x]]) res++;
cnt[w[x]]++;
}
}

inline int find(int x)
{
int l = 1, r = tot;
while (l <= r)
{
int mid = (l + 1) >> 1;
if (temp[mid] < x)
l = mid + 1;
else if (temp[mid] == x)
return mid;
else
r = mid - 1;
}
}

int main()
{
int n, m;
n = read(), m = read();
for (int i = 1; i <= n; i++)
val[i] = w[i] = read();
sort(w + 1, w + 1 + n);
for (int i = 1; i <= n; i++)
if (temp[tot] != w[i])
temp[++tot] = w[i];
for (int i = 1; i <= n; i++)
w[i] = find(val[i]);
for (int i = 1; i < n; i++)
add(read(), read());
dfs(1, 0);
for (int i = 1; i <= m; i++)
{
int u = read(), v = read();
if (fir[u] > fir[v]) swap(u, v);
int p = lca(u, v);
if (u == p) q[i] = query({i, fir[u], fir[v], 0});
else q[i] = query({i, sec[u], fir[v], p});
}
sort(q + 1, q + 1 + m);
for (int k = 1, L = 1, R = 0, res = 0; k <= m; k++)
{
int id = q[k].id, l = q[k].l, r = q[k].r, p = q[k].p;
while (R < r) modify(seq[++R], res);
while (R > r) modify(seq[R--], res);
while (L > l) modify(seq[--L], res);
while (L < l) modify(seq[L++], res);
if (p) add(seq[p], res);
ans[id] = res;
if (p) add(seq[p], res);
}
for (int i = 1; i <= m; i++)
printf("%d\n", ans[i]);
return 0;
}

二次离线莫队

传送门

树上回滚带修二次离线莫队

这个东西似乎没有,不过有树上带修莫队,就是树上莫队与带修莫队结合一下就行了

先放这,以后来填