『做题记录』一锅大乱炖
不出意外的话,这就是我最后的波纹了吧。
当然以后还会继续的。
减半警报器
这个 trick 能将 \(n^2\) 的东西硬生生优化到 \(n\log^2\) ,还是很厉害的 trick
P7603 [THUPC2021] 鬼街
Description
鬼街上经常有灵异事件,每个灵异事件会导致编号为 \(x\) 的质因子的房子闹 \(y\) 次鬼,鬼街上也会装警报器,一个警报器会在其安装后统计编号为 \(x\) 的质因子的房子闹鬼次数总和,这个值达到 \(y\) 时就会报警。特别的,当 \(y = 0\) 时,下一个灵异事件发生时这个警报器就会报警,无论灵异事件的参数如何。要求每个灵异事件会触发的警报器数量以及被触发警报器的编号。
每次输入的 \(y=y'\oplus lastans\) , \(lastans\) 初始为 \(0\) ,在每次灵异事件后变为本次灵异事件会触发的警报器数量,即询问在线。
\(n,q\leq 10^5,0\leq y\leq 2^{32}\)
Solution
考虑到每次的质因数个数函数 \(\omega(n)\) 在数据范围内不会超过 \(6\) ,所以考虑在装警报器时直接在这不超过 \(6\) 个位置丢一个小警报器,每次灵异事件时就查一遍所有警报器所属的小警报器计数之和是否达到报警阈值。这样的做法是 \(O(\omega(n)n^2)\) 的,显然不足以通过。
考虑优化上面的过程,我们发现警报器进行 “拿出来查询,然后再放回去无事发生” 这个过程的次数会非常多,这启发我们要减少这个过程的次数,而优化用到了一个性质:根据鸽巢原理,如果触发警报,那么所有小警报器计数的最大值一定大于 \(\left\lceil\dfrac y{\omega(x)}\right\rceil\) 。所以我们不需要每一次都处理每一个存在的警报器,而是 设置一个分阈值 ,每一次询问只处理那些到达这一“分阈值”的小警报器的小警报器。但是做到这一步也只相当于在复杂度上乘一个常数,并没有优化,甚至因为“分阈值”这一额外信息的维护而多了一只 \(\log\) 。
还是继续考虑 “拿出来查询,然后再放回去无事发生” 这一过程,我们刚刚对 “拿出来” 的条件进行了优化,而 “放回去” 还没有被我们优化。很自然地,我们会考虑放回去的时候通过 更新分阈值 的方法减少次数:即把旧的阈值减去这一趟所有小警报器的计数和,然后将这一新阈值用同样的方法设置分阈值丢回去。因为每一次最坏的情况是只有达到分阈值的那个小警报器计数为分阈值,其他小警报器计数为 \(0\) ,这样相当于总阈值只减小为原来的 \(\dfrac {\omega(x)-1}{\omega(x)}\) 。
这样一来我们执行 “拿出来查询,然后再放回去无事发生” 这一过程的次数只会有以 \(\dfrac {\omega(x)-1}{\omega(x)}\) 为底的对数次,也就是说我们成功的将复杂度从 \(O(n^2)\) 降到了 \(O(n\log n\log V)\) , \(\log n\) 来自维护分阈值所需的数据结构, \(\log V\) 则是我们刚刚分析的关于值域的底数。
这个 trick 的理论分析就是这些,本质在于 设置阈值和更新阈值 所带来的复杂度优化。具体如何维护这一过程可以看代码。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define mp make_pair
#define vi vector<int>
#define eb emplace_back
#define pli pair<LL, int>
#define fi first
#define se second
#define rep(rp,a,b) for(int rp=a;rp<=b;++rp)
#define per(bl,a,b) for(int bl=a;bl>=b;--bl)
#define segc int mid = L+R>>1, lc = now<<1, rc = lc|1
const int N = 1e5+5, MOD = 998244353, INF = 1e9;
inline LL read() {
LL x = 0, f = 1; char ch = 0;
while (!isdigit(ch)) {ch = getchar(); if (ch == '-') f = -1;}
while (isdigit(ch)) x = (x<<3)+(x<<1)+(ch^48), ch = getchar();
return f*x;
}
struct BLCK{
int id, opid; LL lim;//监视器编号、阈值编号与阈值
bool operator<(const BLCK &x) const {
return lim > x.lim;
}
};
int n, m, vis[N], cntn, nop[N];//nop i标记监视器i最新阈值编号
LL cnt[N];//从开始到现在闹鬼的次数
vi pri[N], ans;
priority_queue<BLCK> q[N];
pair<int, LL> mon[N];//每个监视器的监视目标和阈值
void sieve() {
rep (i, 2, n) {
if (!pri[i].empty()) continue ;
rep (j, 1, n/i) pri[i*j].eb(i);
}
}
LL getval(int x) {
LL ret = 0;
for (int j:pri[mon[x].fi]) ret += cnt[j];
return ret;
}
int main() {
n = read(), m = read();
sieve();
LL lans = 0;
while (m --) {
int opt = read(), x = read();
LL y = read()^lans;
if (!opt) {
for (int i:pri[x]) cnt[i] += y;
for (int i:pri[x]) {
while (!q[i].empty() && q[i].top().lim <= cnt[i]) {
BLCK x = q[i].top(); q[i].pop();
if (vis[x.id]) continue ;
if (nop[x.id] != x.opid) continue ;//类似dij的懒删除堆,忽略编号不为最新的阈值
LL newval = getval(x.id);
if (newval >= mon[x.id].se) vis[x.id] = 1, ans.eb(x.id);
else {
LL newlim = (mon[x.id].se-newval-1)/pri[mon[x.id].fi].size()+1;
++nop[x.id];
for (int j:pri[mon[x.id].fi])
q[j].push((BLCK){x.id, nop[x.id], newlim+cnt[j]});
}
}
}
sort(ans.begin(), ans.end());
printf("%d", (int)ans.size());
for (int i:ans) printf(" %d", i); puts("");
lans = ans.size();
ans.clear();
} else {
++cntn;
if (!y) {
ans.eb(cntn);
continue ;
}
LL lim = (y-1)/pri[x].size()+1, stp = 0;
++nop[cntn];
for (int i:pri[x]) {
q[i].push((BLCK){cntn, nop[cntn],lim+cnt[i]});
stp += cnt[i];
} mon[cntn] = mp(x, y+stp);
}
}
return 0;
}
Summary
很经典的 trick (according to Cust10),但是优化幅度不算大,所以对应的数据范围也是比较好看出来的。
猫树分治
我们理应认为它是一种 trick 。
正好最近模拟赛有一档猫树的部分分,于是就打算写这了,但是因为找不到原所以就随便贴道板题上来了。猫树分治可以对于静态的具有可并性的区间问题做到 \(O(1)\) 回答询问。
GSS5 - Can you answer these queries V
Description
给定一个数列,每次询问求左端点在 \([x1,y1]\) 间且右端点在在 \([x2,y2]\) 间的最大字段和。只保证 \(x1\leq x2,y1\leq y2\) 。
Solution
非常基础的猫树应用,考虑维护若干个具有可并性的信息来回答询问,这里相比 GSS1 并没有新的信息需要维护,还是跨区间中点最大子段和,以及区间内最大子段和。
现在来考虑如何回答询问。首先是询问区间不交的情况,这样中间的所有数都是必取的,用前缀和维护不难。然后我们想要知道一个区间的最大后缀和和最大前缀和,而这用猫树维护也是非常轻松的。对于区间有交的情况,交的部分处理同 GSS1 ,然后后面就可以拆成两个区间后缀最大值加上区间前缀最大值,最后将这三个值取 max 即可。
代码实现也并不复杂。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define mp make_pair
#define vi vector<int>
#define eb emplace_back
#define pli pair<LL, int>
#define fi first
#define se second
#define rep(rp,a,b) for(int rp=a;rp<=b;++rp)
#define per(bl,a,b) for(int bl=a;bl>=b;--bl)
#define segc int mid = L+R>>1, lc = now<<1, rc = lc|1
const int N = 1e5+5, MOD = 998244353, INF = 1e9;
inline int read() {
int x = 0, f = 1; char ch = 0;
while (!isdigit(ch)) {ch = getchar(); if (ch == '-') f = -1;}
while (isdigit(ch)) x = (x<<3)+(x<<1)+(ch^48), ch = getchar();
return f*x;
}
int lg[N<<2], pos[N], p[16][N], s[16][N], a[N], sum1[16][N], sum2[16][N];
void build(int now, int L, int R, int dep) {
if (L == R) return (void)(pos[L] = now);
int prep, sm;
segc;
p[dep][mid] = s[dep][mid] = prep = sm = sum1[dep][mid] = sum2[dep][mid] = a[mid], sm = max(sm, 0);
per (i, mid-1, L) {
prep += a[i], sm += a[i];
sum1[dep][i] = prep, sum2[dep][i] = sm;
s[dep][i] = max(s[dep][i+1], prep);
p[dep][i] = max(p[dep][i+1], sm);
sm = max(sm, 0);
}
p[dep][mid+1] = s[dep][mid+1] = prep = sm = sum1[dep][mid+1] = sum2[dep][mid+1] = a[mid+1], sm = max(sm, 0);
rep (i, mid+2, R) {
prep += a[i], sm += a[i];
sum1[dep][i] = prep, sum2[dep][i] = sm;
s[dep][i] = max(s[dep][i-1], prep);
p[dep][i] = max(p[dep][i-1], sm);
sm = max(sm, 0);
}
build(lc, L, mid, dep+1), build(rc, mid+1, R, dep+1);
}
inline int squery(int L, int R) {
if (L > R) return 0;
if (L == R) return a[L];
int dep = lg[pos[L]]-lg[pos[L]^pos[R]];
return sum1[dep][L]+sum1[dep][R];
}
inline int fquery(int L, int R) {
if (L == R) return a[L];
int dep = lg[pos[L]]-lg[pos[L]^pos[R]];
return max({p[dep][L], p[dep][R], s[dep][L]+s[dep][R]});
}
inline int prequery(int L, int R) {
if (L == R) return a[L];
int dep = lg[pos[L]]-lg[pos[L]^pos[R]];
return max(sum2[dep][L], sum1[dep][L]+s[dep][R]);
}
inline int sufquery(int L, int R) {
if (L == R) return a[L];
int dep = lg[pos[L]]-lg[pos[L]^pos[R]];
return max(sum2[dep][R], s[dep][L]+sum1[dep][R]);
}
inline int query(int L1, int R1, int L2, int R2) {
if (R1 < L2) return squery(R1+1, L2-1)+sufquery(L1, R1)+prequery(L2, R2);
else {
int ret = fquery(L2, R1);
if (L1 != L2) ret = max(ret, sufquery(L1, L2-1)+prequery(L2, R2));
if (R1 != R2) ret = max(ret, sufquery(L1, R1)+prequery(R1+1, R2));
return ret;
}
}
void solve() {
int n = read(), len = 2;
while (len < n) len <<= 1;
rep (i, 1, n) a[i] = read();
rep (i, 2, (len<<1)) lg[i] = lg[i>>1]+1;
build(1, 1, len, 1);
int q = read();
while (q --) {
int L1 = read(), R1 = read(), L2 = read(), R2 = read();
printf("%d\n", query(L1, R1, L2, R2));
}
}
int main() {
int T = read(); while (T --) solve();
return 0;
}
Summary
对于静态区间问题,只要维护若干区间可并信息,猫树就可以非常快速处理询问。特别是对于一些具有 dp 结构的问题,只要不带修上猫树就能得到一个复杂度不错的解。(只不过往前合并可能要重推一遍式子)
DP of DP
我们知道, dp 过程本身就是一个 DFA ,那么当题目形如 “求……值为……的方案数” 时,并且这一值需要通过 dp 求取时,那么我们就可以在这一 dp 的 DFA 之上进行 dp ,这就是 dp 套 dp 。
[CF924F]Minimal Subset Difference
Description
定义 \(f(n)\) 表示将十进制数 \(n\) 所有数码之间填入加号或者减号,最终得到的值的绝对值最小值。 \(T\) 组询问,给定 \(l, r, k\) ,求满足 \(l \le m \le r\) 且 \(f(m) \le k\) 的 \(m\) 的个数。
\(1 \le T \le 5\times 10^4, 1 \le l \le r \le 10^{18}, 1 \le k \le 9\) 。
Solution
首先,求取 \(f(n)\) 这件事情是不得不用 dp 去完成的,因此我们先考虑设 \(f_{i,j}\) 表示还有 \(i\) 个数没填,到目前为止和为 \(j\) 的方案数。特判掉 \(10^{18}\) ,剩下的就是 \(18\) 个 \(0\sim9\) 的数。因为转移时每加入一个数,就会影响到多个不同 \(j\) 的值,因此我们需要把 \(f\) 的整个第二维压起来。
不难发现, \(j\) 的上限不会超过 \(81\) 。首先答案是不会大于 \(9\) 的,一个大于 \(0\) 就填 \(-\) ,小于 \(0\) 就填 \(+\) 的过程即可保证构造出不大于 \(9\) 的答案。然后考虑一个大于 \(81\) 的状态想要出现,就必须有 \(10\) 个以上的正号,那么剩下 \(8\) 个数就算全为 \(9\) 且填 \(-\) 也不可能使答案小于 \(9\) 。至于可能成为答案的状态上界不超过 \(72\) ,笔者不太会证,而且就算上限开到 \(90\) 也是可以快速通过的,所以不用担心。
从 \(f_{18,1}\) (这里的 \(1\) 已经是状压后,代表总和为 \(0\) 的合法性了)开始跑,上限开 \(81\) 跑出来的状态总数只有 \(16116\) ,那么接下来就是预处理 \(dp_{i,j,k}\) 表示还有 \(i\) 位需要填,在状态 \(j\) 且答案大小限制为 \(k\) 的方案数即可。这个 dp 可以每次枚举下一位填的数进行转移。
对于多次询问,不妨对每次询问先差分,那么现在要解决的就是在 \(1\sim x\) 内计数的单次询问。每次确定一个位上的数字之后,会有一些数不再受上界限制。因此不妨枚举由高往低首个小于 \(x\) 的位,前面相同的位直接在 DFA 上走,求出当前状态即可。对于枚举的每个偏移上界的数位,我们再枚举该位上填哪一个数,然后直接利用 \(dp\) 数组得出填剩余位的方案数即可。
更多细节看代码就懂了。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
#define int long long
#define mp make_pair
#define vi vector<int>
#define eb emplace_back
#define fi first
#define se second
#define rep(rp,a,b) for(int rp=a;rp<=b;++rp)
#define per(bl,a,b) for(int bl=a;bl>=b;--bl)
#define segc int mid = L+R>>1, lc = now<<1, rc = lc|1
const int N = 3e5+5, S = 2e4+5, MOD = 998244353, INF = 1e9;
inline int read() {
int x = 0, f = 1; char ch = 0;
while (!isdigit(ch)) {ch = getchar(); if (ch == '-') f = -1;}
while (isdigit(ch)) x = (x<<3)+(x<<1)+(ch^48), ch = getchar();
return f*x;
}
__int128 sta[S];
map<__int128, int> id;
int cnts, nxtp[S][10], dp[20][S][10];
__int128 getnxt(__int128 x, int tmp) {
__int128 ret = 0;
rep (i, 0, 81) if ((x>>(__int128)i)&((__int128)1)) {
if (i+tmp <= 81) ret |= ((__int128)1)<<(i+tmp);
ret |= ((__int128)1)<<abs(i-tmp);
} return ret;
}
void init() {//建立DFA
queue<pii> q;
q.push(mp(18, 1));
sta[++cnts] = 1;
id[1] = cnts;
while (!q.empty()) {
int dep = q.front().fi, xid = q.front().se; q.pop();
__int128 now = sta[xid];
if (!dep) continue ;
rep (i, 0, 9) {
__int128 nxt = getnxt(now, i);
if (!id[nxt]) {
sta[++cnts] = nxt;
id[nxt] = cnts;
q.push(mp(dep-1, cnts));
}
nxtp[xid][i] = id[nxt];
}
}
rep (i, 1, cnts) {
int mn1 = INF;
rep (j, 0, 81) if ((sta[i]>>(__int128)j)&((__int128)1)) {
mn1 = j;
break;
}
rep (j, mn1, 9) dp[0][i][j] = 1;
}
rep (i, 1, 17) {
rep (j, 1, cnts) {
rep (k, 0, 9) rep (l, 0, 9)
dp[i][j][k] += dp[i-1][nxtp[j][l]][k];
}
}
}
int num[20];
void getnum(int x) {rep (i, 1, 18) num[19-i] = x%10, x /= 10;}
int calc(int x, int lim) {
int ret = 0, st = 1;
if (x == 1000000000000000000ll) --x, ret += (lim>=1);
getnum(x);
rep (i, 1, 18) {
rep (j, 0, num[i]-1) ret += dp[18-i][nxtp[st][j]][lim];
st = nxtp[st][num[i]];
} ret += dp[0][st][lim];
return ret-1;//减去0
}
void solve() {
int L = read(), R = read(), lim = read();
printf("%lld\n", calc(R, lim)-calc(L-1, lim));
}
signed main() {
init();
int T = read(); while (T --) solve();
return 0;
}
Summary
题目形如 “求……值为……的方案数” 时,并且这一值需要通过 dp 求取时,那么我们就可以在这一 dp 的 DFA 之上进行 dp ,这就是 dp 套 dp 。(没什么好说的直接ctrl-c-v)
树上路径贡献转枚举边计算
虽然是很常见的 trick ,但蠢蠢的笔者总是忘记,于是就记下来了。(虽然放在这些强大的trick里有点不合适?)这是一种典的不能再典的计算路径和方法。
[CF1770E]Koxia and Tree
Description
有一棵 \(n\) 个结点的无根树,边从 \(1\) 到 \(n-1\) 标号,第 \(i\) 条边连接结点 \(u_i\) 和 \(v_i\)。
有 \(k\) 个结点上有蝴蝶,分别是 \(a_1,a_2,\cdots,a_k\)(\(\forall i\not= j,a_i\not = a_j\))。接下来,依次进行如下操作:
- 给每条边随机定向。
- 按照边的编号顺序,对于每一条边 \(u\to v\),如果结点 \(u\) 上有蝴蝶而 \(v\) 没有,那么 \(u\) 上的蝴蝶就会飞到 \(v\) 上。
- 随机选取两只不同的蝴蝶,计算它们在无向树上的距离。
你需要给出第三步中距离的期望值。对 \(998244353\) 取模。
\(2\leq k\leq n\leq 3\times 10^5\)
Solution
首先,令初始有蝴蝶的点集为 \(S\) ,那么根据期望的定义第三步要求的期望其实可以转化成 \(\dfrac {\sum_{i,j\in S,i\neq j}dis(i,j)}{\binom k2}\) 。那么上面这部分的计算就是这个 trick 的基本应用了:依次遍历每一条边 \(u\to v\) ,因为将这条边割开,原树会变成两棵树, \(u\) 所在连通块的蝴蝶与 \(v\) 所在的连通块的蝴蝶间的路径一定会经过该边,那么接下来只需要对这条边有没有蝴蝶飞过,怎么飞的分类讨论就行了。假设我们已经知道了每个点 \(x\) 上有蝴蝶的概率 \(p_x\) 和 \(x\) 子树内的蝴蝶数量 \(c_x\) ,且让 \(u\) 的深度小于 \(v\) 的深度,那么有:
然后就是在计算完对答案的贡献后计算新的 \(p'_u\) 和 \(p'_v\) :
化简后可以得到 \(p'_u = p'_v = \dfrac{p_u+p_v}2\) 。
代码把式子抄进去即可。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define mp make_pair
#define vi vector<int>
#define eb emplace_back
#define pli pair<LL, int>
#define fi first
#define se second
#define rep(rp,a,b) for(int rp=a;rp<=b;++rp)
#define per(bl,a,b) for(int bl=a;bl>=b;--bl)
#define segc int mid = L+R>>1, lc = now<<1, rc = lc|1
const int N = 3e5+5, MOD = 998244353, INF = 1e9, I2 = 499122177;
inline void chkmin(int &x,int y) {x = y>x ? x : y;}
inline void chkmax(int &x,int y) {x = y>x ? y : x;}
inline int read() {
register int x = 0, f = 1;
register char ch = 0;
while(ch < 48 || ch > 57) {
ch = getchar();
if (ch == '-') f = -1;
}
while(ch >= 48 && ch <= 57) x = x*10+(ch^48), ch = getchar();
return f*x;
}
struct EDGE{
int u, v;
}e[N];
int c[N], p[N], dep[N];
vi G[N];
void dfs(int x, int par) {
dep[x] = dep[par]+1;
for (int y:G[x]) if (y != par) {
dfs(y, x);
c[x] += c[y];
}
}
int powM(int x, int y = MOD-2) {
int ret = 1;
while (y) {
if (y&1) ret = 1ll*ret*x%MOD;
x = 1ll*x*x%MOD, y >>= 1;
} return ret;
}
void solve() {
int n = read(), k = read();
rep (i, 1, k) {
int x = read();
c[x] = p[x] = 1;
}
rep (i, 1, n-1) {
e[i].u = read(), e[i].v = read();
G[e[i].u].eb(e[i].v), G[e[i].v].eb(e[i].u);
}
dfs(1, 0);
int ans = 0;
rep (i, 1, n-1) {
int u = e[i].u, v = e[i].v;
if (dep[u] > dep[v]) swap(u, v);
(ans += 1ll*p[u]*p[v]%MOD*c[v]%MOD*(k-c[v])%MOD) %= MOD;
(ans += 1ll*(1ll-p[u]+MOD)*(1ll-p[v]+MOD)%MOD*c[v]%MOD*(k-c[v])%MOD) %= MOD;
(ans += 1ll*p[u]*(1ll-p[v]+MOD)%MOD*(1ll*c[v]*(k-c[v])%MOD+1ll*(c[v]+1)*(k-c[v]-1)%MOD)%MOD*I2%MOD%MOD) %= MOD;
(ans += 1ll*(1ll-p[u]+MOD)*p[v]%MOD*(1ll*c[v]*(k-c[v])%MOD+1ll*(c[v]-1)*(k-c[v]+1)%MOD)%MOD*I2%MOD%MOD) %= MOD;
p[u] = p[v] = 1ll*(p[u]+p[v])*I2%MOD;
} printf("%d\n", 1ll*ans*powM(1ll*k*(k-1)/2%MOD)%MOD);
}
int main() {
int T = 1; while (T --) solve();
return 0;
}
Summary
碰到路径和,无脑拆成边。
极小 \(\rm mex\) 区间
区间 \(\rm mex\) 操作似乎在近一年常出现,所以就写一写。
[THUPC 2024 初赛] 套娃
当时太菜了,场上甚至没有开这道题(批评我的两个隐身队友 Custlo 和 MuelsyeU)
Description
我们定义一个集合的 \(\operatorname{mex}\) 是最小的不在 \(S\) 中的非负整数。
给定一个序列 \(a_1,\dots,a_n\),对于每个 \(1\leq k\leq n\),我们按照如下方式定义 \(b_k\):
- 对于 \(a\) 的所有长为 \(k\) 的子区间,求出这个子区间构成的数集的 \(\operatorname{mex}\)。
- 对于求出的所有 \(\operatorname{mex}\),求出这个数集自己的 \(\operatorname{mex}\),记为 \(b_k\)。
求出序列 \(b\)。
\(1\leq n\leq 10^5\)
Solution
为了便于书写,我们这里用 \({\rm mex}(l, r)\) 表示 \(a\) 序列中下标在区间 \((l, r)\) 内所有元素的 \(\rm mex\) 。
首先我们刚需“极小 \(\rm mex\) 区间”这一 trick ,他的具体内容如下:
那么现在我们对这一结论进行证明:
引理: 对于极小 \(\rm mex\) 区间 \((l, r)\) 而言,\(a_l,a_r\) 都只在 \((l, r)\) 中出现了一次。
证明: 考虑反证,假设 \(a_l,a_r\) 不只在 \((l, r)\) 中出现了一次,那么删掉 \(a_l\) 不会导致 \(\rm mex\) 变化,这不符合极小 \(\rm mex\) 区间的定义,因此引理1成立。
我们先钦定一个极小 \(\rm mex\) 区间 \((l, r)\) ,且 \(a_l > a_r\) 。根据极小 \(\rm mex\) 区间的性质,我们可以得到 \({\rm mex}(l, r) > a_l > a_r\) 。我们考虑固定 \(l\) ,然后往外拓展 \(r\) 。因为 \({\rm mex}(l, r) > a_l\) ,所以所有 \(0\sim a_l\) 中的数都已经在 \((l, r)\) 中出现,因此对于所有的 \(a_r < a_l\) ,都已经在 \((l, r)\) 中出现过。结合引理,这些 \(r\) 都不能成为以 \(l\) 为左边界的极小 \(\rm mex\) 区间的右边界。因此,对于每一个 \(l\) ,满足 \(a_l>a_r\) 的极小 \(\rm mex\) 区间最多只有一个。同理,对于 \(a_l < a_r\) ,也有一样的结论。
综上,我们就证明了极小 \(\rm mex\) 区间不超过 \(2n\) 个。
在有了这一性质后,我们就可以考虑去找到这些极小 \(\rm mex\) 区间。如果暴力的去找,肯定是难以通过的。这里有两种做法,一种是开一颗 ODT 去维护,而还有一种是用 P4137 的可持久化线段树 \(O(\log)\) 查 \(\rm mex\) 辅助区间拓展,这里我们具体讲后面这种。
考虑对于一个 \(\rm mex\) 为 \(x\) 的极小 \(\rm mex\) 区间,找到区间外左右最早出现的值为 \(x+1\) 的数,在可持久化线段树上查询 \(\rm mex\) 值并加入其 \(\rm mex\) 值的区间集合。最后还需要检查一下这些区间是否真的是极小 \(\rm mex\) 区间,将不合法的区间去掉即可。
找到极小 \(\rm mex\) 区间后要做的就很简单了,考虑每个 \(\rm mex\) 值为 \(x\) 的极小 \(\rm mex\) 区间 \((l, r)\) ,且区间外左右最早出现的值为 \(x+1\) 的数在 \(L,R\) 处,那么在 \(k\in {[r-l+1,R-L-1]}\) 出现,那么直接用桶和 \(\rm set\) 来维护这些区间的贡献即可。
Code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define mp make_pair
#define vi vector<int>
#define eb emplace_back
#define pii pair<int, int>
#define fi first
#define se second
#define rep(rp,a,b) for(int rp=a;rp<=b;++rp)
#define per(bl,a,b) for(int bl=a;bl>=b;--bl)
#define segc int mid = L+R>>1, lc = now<<1, rc = lc|1
const int N = 1e5+5, MOD = 998244353, INF = 1e9;
template <typename T> inline void chkmin(T &x,T y) {x = y>x ? x : y;}
template <typename T> inline void chkmax(T &x,T y) {x = y>x ? y : x;}
inline int read() {
register int x = 0, f = 1;
register char ch = 0;
while(ch < 48 || ch > 57) {
ch = getchar();
if (ch == '-') f = -1;
}
while(ch >= 48 && ch <= 57) x = x*10+(ch^48), ch = getchar();
return f*x;
}
struct BLCK{
int ls, rs, val;
}tr[25*N];
int a[N], cntn, rt[N];
vi pos[N];
vector<pii> mex[N];
void build(int &now, int L, int R) {
tr[now=++cntn] = {0, 0, 0};
if (L == R) return ;
int mid = (L+R)>>1;
build(tr[now].ls, L, mid);
build(tr[now].rs, mid+1, R);
}
void update(int old, int &now, int L, int R, int pos, int val) {
tr[now=++cntn] = tr[old];
if (L == R) return tr[now].val = val, void();
int mid = (L+R)>>1;
pos <= mid ? update(tr[old].ls, tr[now].ls, L, mid, pos, val) : update(tr[old].rs, tr[now].rs, mid+1, R, pos, val);
tr[now].val = min(tr[tr[now].ls].val, tr[tr[now].rs].val);
}
int query(int now, int L, int R, int x) {
if (L == R) return L;
int mid = (L+R)>>1;
return tr[tr[now].ls].val < x ? query(tr[now].ls, L, mid, x) : query(tr[now].rs, mid+1, R, x);
}
int buc[N];
vi ad[N], dl[N];
void add(int ql, int L, int R, int qr, int x) {ad[R-L+1].eb(x); dl[qr-ql+2].eb(x);}
int main() {
int n = read(), cnta = n+3;
build(rt[0], 1, cnta);
rep (i, 0, cnta) pos[i].eb(0);
rep (i, 1, n) a[i] = read(), pos[a[i]].eb(i), update(rt[i-1], rt[i], 0, cnta, a[i], i);
// rep (i, 1, cntn) printf("%d %d %d %d\n", i, tr[i].ls, tr[i].rs, tr[i].val);
// rep (i, 1, n) rep (j, i, n) printf("%d %d %d\n", i, j, query(rt[j], 0, cnta, i));
rep (i, 0, cnta) pos[i].eb(n+1);
rep (i, 1, n) a[i] ? mex[0].eb(mp(i, i)) : mex[1].eb(mp(i, i));
rep (i, 1, cnta) {
for (int j = 0; j < mex[i-1].size(); ++j) {
int L = mex[i-1][j].fi, R = mex[i-1][j].se;
int ql = *(--lower_bound(pos[i-1].begin(), pos[i-1].end(), L)), qr = *(upper_bound(pos[i-1].begin(), pos[i-1].end(), R));
if (ql) mex[query(rt[R], 0, cnta, ql)].eb(mp(ql, R));
if (qr <= n) mex[query(rt[qr], 0, cnta, L)].eb(mp(L, qr));
}
sort(mex[i].begin(), mex[i].end(),[&](pii x,pii y){return x.fi==y.fi?x.se<y.se:x.fi>y.fi;});
vector<pii>G;
int las = 2e9;
for (int j = 0; j < mex[i].size(); ++j) {
int L = mex[i][j].fi, R = mex[i][j].se;
if (las > R) G.push_back(mp(L, R)), las = R;
} swap(G, mex[i]);
}
rep (i, 0, cnta) for (int j = 0; j < mex[i].size(); ++j) {
int L = mex[i][j].fi, R = mex[i][j].se;
// printf("%d %d %d\n", i, L, R);
add(*(--lower_bound(pos[i].begin(), pos[i].end(), L))+1, L, R, *upper_bound(pos[i].begin(), pos[i].end(), R)-1, i);
}
set<int>s;
rep (i, 0, n) buc[i] = 0, s.insert(i);
rep (k, 1, n) {
for(int j:ad[k]) if(!buc[j]++) s.erase(j);
for(int j:dl[k]) if(!--buc[j]) s.insert(j);
printf("%d ", *s.begin());
}
return 0;
}
莫队
作为人丁稀薄的根号算法的一员,有必要写进来,但实在是不想放什么题来讲了。(懒
普通莫队
莫队一般用来处理 区间询问、询问可离线、能通过移动区间界更新答案 的题目。
对于一个长度为 \(n\) ,有 \(q\) 次区间询问 \((l,r)\) 的问题,莫队算法需要将 \(q\) 次询问按一个特定的顺序排列:将 \(n\) 分成 \(s\) 块,先按 \(l\) 所在的块顺序排序,对于 \(l\) 在同一个块内的询问,按照 \(r\) 排序。按照这个顺序,每次通过移动 \(l,r\) 来更新答案。由于这个算法的复杂度只与 \(l,r\) 的移动距离相关,那么我们可以通过分析 \(l,r\) 的移动来分析复杂度:
- 那么下一次询问与当次询问的 \(l\) 在同一块内,那么 \(l\) 移动距离最多只有 \(s\) ,总共的移动次数是 \(O(qs)\) 的。
- \(l\) 的块间移动最多有块数 \(\dfrac ns\) 次,每次距离最多为 \(2s\) ,这影响不到复杂度。
- 每一块的询问, \(r\) 都会最多走 \(n\) 的距离,总共是 \(O(n\cdot\dfrac ns = \dfrac {n^2}s)\) 的
综合以上,以这种顺序处理询问的复杂度是 \(O(qs+\dfrac {n^2}s)\) ,用基本不等式可以得到这个复杂度小于等于 \(O(n\sqrt q)\) ,当 \(s = \dfrac n{\sqrt q}\) 时取到。
回滚莫队
当仅有扩大区间大小更新答案或减小区间大小更新答案比较容易时,就需要考虑避免做另一种较难实现的操作,而处理这类问题就需要使用回滚莫队。之所以称作回滚莫队,是因为指针的移动并不是连续的,每次询问左端点的状态都将回滚到所在块的一端,复原答案,这样就只需要往一个方向移动指针 \(l\) ,而 \(r\) 的移动方向也是可以更改排序方向处理的,这样就避免了处理较为麻烦的那一类操作。
相对于普通莫队复杂度上并无不同,至于实现,也许看代码段更为直观:
只扩大区间莫队
rep (blk, 0, n-1) {
int L = (blk+1)*B, R = (blk+1)*B+1;
getans(L, R); //计算ans及相关维护信息
while (que[p].l <= (blk+1)*B) {
int ql = que[p].l, qr = que[p].r; ++p;
while (R < qr) add(R++);
int tmp = ans;
while (L > ql) add(L--);
que[p].ans = ans;
ans = tmp; //复原答案
roll(L); //复原L及其维护信息至L=(blk+1)*B的状态
}
}
只减小区间莫队
rep (blk, 0, n-1) {
int L = blk*B+1, R = n;
getans(L, R); //计算ans及相关维护信息
while (que[p].l <= (blk+1)*B) {
int ql = que[p].l, qr = que[p].r; ++p;
while (R > qr) del(R--);
int tmp = ans;
while (L < ql) del(L++);
que[p].ans = ans;
ans = tmp; //复原答案
roll(L); //复原L及其维护信息至L=blk*B+1的状态
}
}
特别地,为了防止出现 \(l>r\) 的情况,对于 \(l,r\) 在同一块内的询问,我们暴力处理。
带修莫队
带修就是加入时间维,那么我们还是考虑分块,不过这次我们对 \(l,r\) 都按块排序,对时间正常排序,那么复杂度是这样的:
- 那么下一次询问与当次询问的 \(l,r\) 在同一块内,那么 \(l,r\) 移动距离最多只有 \(s\) ,总共的移动次数是 \(O(qs)\) 的。
- \(l,r\) 的块间移动最多有块数 \(\dfrac {n^2}{s^2}\) 次,每次距离最多为 \(2s\) ,这还是影响不到复杂度。
- 每一块的询问,时间 \(t\) 都会最多走 \(n\) 的距离,总共是 \(O(\dfrac{n^3}{s^2})\) 的.
那么复杂度是 \(O(qs+\dfrac {n^3}{s^2})\) ,可以得到这个复杂度小于等于 \(O(n\sqrt[3]{q^2})\) ,当 \(s = \dfrac n{\sqrt[3]q}\) 时取到。(比暴力快就是赢)
树上莫队
子树
拍成 \(dfs\) 序,记遍历到节点 \(x\) 的时间是 \(dfn_x\) ,出节点的时间是 \(ed_x\) ,然后树上的子树就是序列上的区间 \(dfn_x\sim ed_x\) 。
链
有些细节但不多,先钦定 \(dfn_u < dfn_v\) 。首先是 \(LCA\) 是否是端点,如果是则是 \(dfn_u\sim dfn_v\) ,否则就要变成 \(ed_u\sim dfn_v\) 。(想象一下,若干个连续的 \(dfn\) 应该是一条从树根 \(\to\) 叶子方向的链,反过来若干个连续的 \(ed\) 应该是一条从叶子 \(\to\) 树根方向的链)但是要注意的是, \(ed_u\sim dfn_v\) 是不包含 \(LCA\) 的,还要把 \(LCA\) 的贡献算上。
上面这部分确定了链所在的 \(dfs\) 序区间,但是区间内并不完全是链,还有可能有多长了几个子树,但比较好的是,这些子树上的点一定在这个 \(dfs\) 序区间内出现了两次,我们把这种点的贡献去掉即可。至于实现,可以像一般莫队那样开个桶记录遍历次数。
Summary
这么多应该够了吧……挺久没考的了,说不定就考了呢(