5.30 杂题选讲
\(n\log n\) 点对
CodeChef - Minimum Xor Segment
考虑对于三个数 \(1\le i < j < k\le n\),我们观察 \((i,k)\) 什么时候是有用的。
设 \(a_i,a_j,a_k\) 在二进制下的 \(\text{lcp}\) 长为 \(d\),我们比较他们的第 \(d+1\) 位,设三者分别为 \(x,y,z\)。
-
当 \(x = y\) 时,显然 \(a_i\text{ xor } a_j < a_i \text{ xor } a_k\)。
-
当 \(y = z\) 时,显然 \(a_j\text{ xor } a_k < a_i \text{ xor } a_k\)。
-
当 \(x = z\) 是,有用。
我们先随便找一个 \(i\),然后不断往前找到第一个有用的 \(i\)。
每往前找一个 \(i\),容易发现 \(\text{lcp}\) 长度都会 \(+1\),于是最多找 \(30\) 个,总点对数为 \(O(n\log n)\)。
点击查看代码
#include <bits/stdc++.h>
#define ll int
#define fi first
#define se second
#define mkp make_pair//
#define pir pair <ll, ll>
#define pb push_back
using namespace std;
const ll maxn = 2e5 + 10;
ll trie[maxn * 30][2], pos[maxn * 30], tot = 1, n, q;
ll L[maxn], R[maxn], a[maxn];
vector <ll> vec[maxn];
ll tree[maxn];
void add(ll x, ll v) {
while(x <= n) {
tree[x] = min(tree[x], v);
x += x & -x;
}
}
ll ask(ll x) {
ll v = 2e9;
while(x) {
v = min(v, tree[x]);
x ^= x & -x;
}
return v;
}
ll ans[maxn];
int main() {
scanf("%d%d", &n, &q);
memset(tree, 0x3f, sizeof tree);
for(ll i = 1; i <= n; i++)
scanf("%d", a + i);
for(ll i = 1; i <= q; i++) {
scanf("%d%d", L + i, R + i);
vec[R[i]].pb(i);
}
for(ll i = 1; i <= n; i++) {
if(i) add(n - i + 2, a[i] ^ a[i - 1]);
ll p = 1;
for(ll j = 29; ~j; j--) {
ll c = (a[i] >> j) & 1;
if(trie[p][c]) {
p = trie[p][c];
add(n - pos[p] + 1, a[i] ^ a[pos[p]]);
}
else break;
}
for(ll j: vec[i])
ans[j] = ask(n - L[j] + 1);
p = 1;
for(ll j = 29; ~j; j--) {
ll c = (a[i] >> j) & 1;
if(!trie[p][c]) trie[p][c] = ++tot;
p = trie[p][c];
pos[p] = i;
}
}
for(ll i = 1; i <= q; i++)
printf("%d\n", ans[i]);
return 0;
}
倍增分块
2019ICPC徐州 H - Yuuki and a problem
考虑值域上倍增分块。具体的,对于 \(k\in [0,17]\),我们把 \([2^k, 2^{k + 1})\) 分一块。
根据神秘数那题的做法,我们从小到大加入数。
对于一个 \(k\),设当前 \(sum\ge 2^k\),我们找出这个块里面的最小值 \(val\),判断 \(sum+1\) 是否小于 \(k\)。
如果不小于,那么 \(sum\) 加上 \(val\) 后一定 \(\ge 2^{k + 1}\),可以直接跳到下一个块。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define mkp make_pair
#define pir pair <ll, ll>
#define pb push_back
using namespace std;
const ll maxn = 2e5 + 10, inf = 1e18;
ll n, q, a[maxn];
struct SGT {
ll mn[maxn << 2], sum[maxn << 2];
void modify(ll p, ll l, ll r, ll x, ll v) {
if(l == r) {
if(v == -1) mn[p] = inf, sum[p] = 0;
else mn[p] = sum[p] = v;
return;
}
ll mid = l + r >> 1;
if(x <= mid) modify(p << 1, l, mid, x, v);
else modify(p << 1|1, mid + 1, r, x, v);
mn[p] = min(mn[p << 1], mn[p << 1|1]);
sum[p] = sum[p << 1] + sum[p << 1|1];
}
ll querymn(ll p, ll l, ll r, ll ql, ll qr) {
if(ql <= l && r <= qr) return mn[p];
if(r < ql || qr < l) return inf;
ll mid = l + r >> 1;
return min(querymn(p << 1, l, mid, ql, qr),
querymn(p << 1|1, mid + 1, r, ql, qr));
}
ll querysum(ll p, ll l, ll r, ll ql, ll qr) {
if(ql <= l && r <= qr) return sum[p];
if(r < ql || qr < l) return 0;
ll mid = l + r >> 1;
return querysum(p << 1, l, mid, ql, qr) +
querysum(p << 1|1, mid + 1, r, ql, qr);
}
void build() {
for(ll i = 1; i <= 4 * n; i++)
mn[i] = inf, sum[i] = 0;
}
} tr[18];
ll Log[maxn];
int main() {
scanf("%lld%lld", &n, &q);
for(ll i = 0; i <= 17; i++) tr[i].build();
for(ll i = 2; i <= 2e5; i++) Log[i] = Log[i >> 1] + 1;
for(ll i = 1; i <= n; i++) {
scanf("%lld", a + i);
tr[Log[a[i]]].modify(1, 1, n, i, a[i]);
}
while(q--) {
ll op, x, y;
scanf("%lld%lld%lld", &op, &x, &y);
if(op == 1) {
tr[Log[a[x]]].modify(1, 1, n, x, -1);
tr[Log[y]].modify(1, 1, n, x, a[x] = y);
} else {
ll sum = 0;
for(ll i = 0; i <= 17; i++) {
ll tmp = tr[i].querymn(1, 1, n, x, y);
if(sum + 1 < tmp) break;
sum += tr[i].querysum(1, 1, n, x, y);
}
printf("%lld\n", sum + 1);
}
}
return 0;
}
[IOI2021] 地牢游戏
倍增分块,对于每个 \(k\),设我们当前的能力值 \(\ge 2^k\)。那么对于血量 \(< 2^k\) 的怪一定能被打败。
对于血量 \(\ge 2^k\) 的怪,我们不确定能不能打败。但是一旦打败了,能力值一定 \(\ge 2^{k + 1}\),即一定会跳到后面的块。
考虑倍增,我们在块 \(k\) 内走的时候,先默认 \(\ge 2^k\) 的怪打不过,然后倍增跳到第一个能打败的怪。
对于每个 \(k\),预处理出我们走 \(2^i\) 后到达的怪,获得的能力值,以及如果要被所有能力值 \(\ge 2^k\) 的怪打败,那么能力值最多为多少。
点击查看代码
#include <bits/stdc++.h>
#define ll int
#define fi first
#define se second
#define mkp make_pair
#define pir pair <ll, ll>
#define pb push_back
using namespace std;
const ll maxn = 4e5 + 10, M = 5, base = 32, K = 23;
const long long inf = 1e18;
struct Bz {
ll to[maxn][25];
long long sum[maxn][25], lim[maxn][25];
} D[6];
ll pw[6], N, S[maxn], W[maxn];
void init(ll n, vector <ll> s, vector <ll> p, vector <ll> w, vector <ll> l) {
pw[0] = 1, N = n;
for(ll i = 1; i <= M; i++) pw[i] = pw[i - 1] * base;
for(ll i = 0; i < N; i++)
S[i] = s[i], W[i] = w[i];
for(ll u = 0; u <= M; u++) {
for(ll i = 0; i < N; i++) {
if(pw[u] > s[i]) {
D[u].to[i][0] = w[i];
D[u].lim[i][0] = inf;
D[u].sum[i][0] = s[i];
} else {
D[u].to[i][0] = l[i];
D[u].lim[i][0] = s[i];
D[u].sum[i][0] = p[i];
}
}
D[u].to[N][0] = -1;
for(ll j = 1; j <= K; j++) {
for(ll i = 0; i <= N; i++) {
ll p = D[u].to[i][j - 1];
if(p == -1 || D[u].to[p][j - 1] == -1) {
D[u].to[i][j] = -1;
continue;
}
D[u].to[i][j] = D[u].to[p][j - 1];
D[u].lim[i][j] = min(D[u].lim[i][j - 1], D[u].lim[p][j - 1] - D[u].sum[i][j - 1]);
D[u].sum[i][j] = D[u].sum[i][j - 1] + D[u].sum[p][j - 1];
}
}
}
}
long long simulate(ll x, ll _z) {
long long z = _z;
for(ll u = 0; u <= M;) {
while(u < M && z >= pw[u + 1]) ++u;
for(ll i = K; ~i; i--)
if(D[u].to[x][i] != -1 && D[u].lim[x][i] > z) {
z += D[u].sum[x][i];
x = D[u].to[x][i];
}
if(x == N) return z;
z += S[x], x = W[x];
}
}
减半报警器
CF gym104065B - Call Me Call Me
区间很难维护。
考虑猫树分治,每个区间转化成了一段前缀 + 一段后缀。
我们把一个区间的 \(k\) 分为两半:\(\lfloor \dfrac k2 \rfloor\) 和 \(\lceil \dfrac k2 \rceil\),分别挂在前缀和后缀上。
当第 \(i\) 个人加入时,我们在猫树上的每一层,开一个线段树来维护对应的前缀 / 后缀,每次做区间 \(-1\) 操作。
当一个位置上挂的数 \(<0\) 时,我们重新分配对应的 \(k\) 于两半。
这样一个区间的 \(k\) 被重新分配的次数为 \(O(\log n)\),代码还没写。
包括鬼界那题也是这个思想

浙公网安备 33010602011771号