线段树分治学习笔记
一个挺有用的小 trick。
P5787 二分图 /【模板】线段树分治
题意:
给定一个 \(n\) 个点的图,有一条连接 \(x_{i}\) 到 \(y_{i}\) 的边在 \(l_{i}\) 时刻出现,\(r_{i}\) 时刻消失。判断所有时刻这个图是否为二分图。
分析:
判断一个图是否为二分图的一种做法是并查集:
思想类似于 2-SAT。
考虑把点 \(i\) 拆成两个点(编号为 \(i,i+n\)),分别表示 \(i\) 在集合 \(S,T\) 时的情况。
对于一条原图的一条边 \(x \rightarrow y\),如果在并查集上 \(x\) 与 \(y\) 在一个集合内,就冲突了。否则在并查集上合并 \(x \rightarrow y+n\) 与 \(x+n \rightarrow y\)。
考虑如何维护边在一个区间出现这个性质。
不妨用线段树维护时间轴。把边挂在线段树对应的节点上。深度优先遍历线段树。实时节点 \([l,r]\) 维护并查集。
进去该节点时加入边对并查集的影响,出去时撤销。
而并查集撤销操作不难处理,使用按秩合并,使用栈记录一下操作的过程即可。
时间复杂度 \(O(n \log^2 n)\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n, m, k;
int U[N], V[N], h[N], fa[N];
vector<int>p[N * 4]; //记录每个节点上挂的边
void update(int u, int L, int R, int l, int r, int x) {
if(R < l || r < L) return;
if(l <= L && R <= r) {
p[u].push_back(x);
return;
}
int mid = (L + R) / 2;
update(u * 2, L, mid, l, r, x);
update(u * 2 + 1, mid + 1, R, l, r, x);
}
int find(int x) {
if(x == fa[x]) return x;
return find(fa[x]);
}
int top;
struct node {
int fx, fy, add;
};
node z[N * 2];
void merge(int x, int y) {
int fx = find(x), fy = find(y);
if(fx == fy) return;
if(h[fx] > h[fy]) swap(fx, fy);
fa[fx] = fy;
z[++top] = (node){fx, fy, h[fx] == h[fy]};
if(h[fx] == h[fy]) h[fy]++;
}
void Sol(int u, int L, int R) {
bool flag = 0;
int lst = top;
for(auto x : p[u]) {
int X = U[x], Y = V[x];
if(find(X) == find(Y) || find(X + n) == find(Y + n)) {
for(int i = L; i <= R; i++) cout << "No" << endl;
flag = 1;
break;
}
merge(X, Y + n);
merge(X + n, Y);
}
if(!flag) {
if(L == R) cout << "Yes" << endl;
else {
int mid = (L + R) / 2;
Sol(u * 2, L, mid);
Sol(u * 2 + 1, mid + 1, R);
}
}
for(int i = top; i > lst; i--) {
fa[z[i].fx] = z[i].fx;
h[z[i].fy] -= z[i].add;
}
top = lst;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
for(int i = 1, l, r; i <= m; i++) {
cin >> U[i] >> V[i] >> l >> r;
l++; r++;
if(l < r) update(1, 1, n, l, r - 1, i);
}
for(int i = 1; i <= 2 * n; i++) fa[i] = i, h[i] = 1;
Sol(1, 1, n);
return 0;
}
P4219 [BJOI2014] 大融合
题意:
有一个 \(n\) 个点的树,树的边是一条一条添加上去的。
在某个时刻,一条边的负载就是它所在的当前能够联通的树上路过它的简单路径的数量。
有 \(Q\) 个操作:
A x y表示在 \(x\) 和 \(y\) 之间连一条边。保证之前 \(x\) 和 \(y\) 是不联通的。Q x y表示询问 \((x,y)\) 这条边上的负载。保证 \(x\) 和 \(y\) 之间有一条边。
\(n,Q \le 10^5\)。
分析:
简单题。
对于每条边,考虑它被操作的时刻 \(A,Q,Q,\cdots,Q\)。
利用线段树分治在被操作位置的时刻中间加入这条边。并使用并查集维护连通块大小即可。
代码:
#include<bits/stdc++.h>
#define N 100005
#define il inline
using namespace std;
int n, Q, cnt, top;
int U[N], V[N];
vector<int>p[N], z[N * 4]; //p[i][0]表示i这条边第一次添加的时间,p[i][1],p[i][2],...表示后面的询问的时间
map<int, map<int, int>>P;
struct node {
int l, r, v; //时刻在[l,r]加入编号为v这条边
};
vector<node>G[N];
bool vis[N];
int h[N], fa[N], siz[N]; //并查集
il int find(int x) {
if(x == fa[x]) return x;
return find(fa[x]);
}
struct Del {
int fx, fy, add;
}g[N];
il void Merge(int x, int y) {
int fx = find(x), fy = find(y);
if(fx == fy) return;
if(h[fx] > h[fy]) swap(fx, fy);
fa[fx] = fy;
g[++top] = (Del){fx, fy, h[fx] == h[fy]};
if(h[fx] == h[fy]) h[fy]++;
siz[fy] += siz[fx];
}
void update(int u, int L, int R, int l, int r, int x) {
if(l <= L && R <= r) {
z[u].push_back(x);
return;
}
if(R < l || r < L) return;
int mid = (L + R) / 2;
update(u * 2, L, mid, l, r, x);
update(u * 2 + 1, mid + 1, R, l, r, x);
}
int siz1, siz2, X, Y;
int uu[N], vv[N];
void Sol(int u, int L, int R) {
int lstop = top;
for(int i = 0; i < z[u].size(); i++)
Merge(U[z[u][i]], V[z[u][i]]);
if(L == R) {
if(vis[L]) cout << siz[find(uu[L])] * siz[find(vv[L])] << endl;
}
else {
int mid = (L + R) / 2;
Sol(u * 2, L, mid);
Sol(u * 2 + 1, mid + 1, R);
}
for(int i = top; i > lstop; i--) {
h[g[i].fy] -= g[i].add;
siz[g[i].fy] -= siz[g[i].fx];
fa[g[i].fx] = g[i].fx;
}
top = lstop;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> Q;
for(int i = 1, u, v; i <= Q; i++) {
char opt;
cin >> opt >> u >> v;
if(u > v) swap(u, v);
uu[i] = u;
vv[i] = v;
if(P[u][v] == 0) {
P[u][v] = ++cnt;
U[cnt] = u; V[cnt] = v;
}
p[P[u][v]].push_back(i);
if(opt == 'Q') vis[i] = 1;
}
for(int i = 1; i <= n; i++) {
fa[i] = i;
h[i] = siz[i] = 1;
}
for(int i = 1; i <= cnt; i++) {
if(p[i].size() == 1) {
update(1, 1, Q, p[i][0], Q, i);
continue;
}
for(int j = 0; j < p[i].size(); j++) {
if(j != p[i].size() - 1) update(1, 1, Q, p[i][j] + 1, p[i][j + 1] - 1, i);
else update(1, 1, Q, p[i][j] + 1, Q, i);
}
}
Sol(1, 1, Q);
return 0;
}
P4585 [FJOI2015] 火星商店问题
题意:
火星上的一条商业街里按照商店的编号 \(1 \sim n\) ,依次排列着 \(n\) 个商店。商店里出售的琳琅满目的商品中,每种商品都用一个非负整数 \(\text{val}\) 来标价。每个商店每天都有可能进一些新商品,其标价可能与已有商品相同。
火星人在这条商业街购物时,通常会逛这条商业街某一段路上的所有商店,譬如说商店编号在区间 \([l,r]\) 中的商店,从中挑选一件自己最喜欢的商品。每个火星人对商品的喜好标准各不相同。
通常每个火星人都有一个自己的喜好密码 \(x\)。对每种标价为 \(\text{val}\) 的商品,喜好密码为 \(x\) 的火星人对这种商品的喜好程度与 \(\text{val}\) 异或 \(x\) 的值成正比。也就是说,\(\text{val xor }x\) 的值越大,他就越喜欢该商品。
每个火星人的购物卡在所有商店中只能购买最近 \(d\) 天内(含当天)进货的商品。另外,每个商店都有一种特殊商品不受进货日期限制,每位火星人在任何时刻都可以选择该特殊商品。每个商店中每种商品都能保证供应,不存在商品缺货的问题。
对于给定的按时间顺序排列的事件,计算每个购物的火星人的在本次购物活动中最喜欢的商品,即输出 \(\text{val xor }x\) 的最大值。这里所说的按时间顺序排列的事件是指以下两种事件:
0 s v,表示编号为 \(s\) 的商店在当日新进一种标价为 \(v\) 的商品。
1 l r x d,表示一位火星人当日在编号在 \([l,r]\) 的商店购买 \(d\) 天内的商品,该火星人的喜好密码为 \(x\)。
对于 \(100\%\) 的数据,所有输入的整数在 \([0,10^5]\) 范围内。
分析:
似乎更像整体二分?
同样用线段树分治处理时间轴:把在一段时间出现的物品用线段树划分成若干个区间。
然后记 \(Sol(u,l,r,q[])\) 表示目前处理时间在 \([l,r]\),在这个时间范围内的修改操作序列为 \(q\)。
利用可持久化 01 Trie 落实修改操作,然后更新时间范围在 \(u\) 这个节点的答案即可。
每个修改操作会有 \(\log\) 层使用到,每个询问会被拆成 \(\log\) 段。
而无论是修改操作还是询问,都要耗费 \(O(\log n)\) 的时间复杂度。
因此总时间为复杂度 \(O(n \log^2 n)\)。空间复杂度 \(O(n \log n)\)。
另解:
可以考虑在线段树的每个节点都开一棵 01 Trie,单点更新一个商店时,就把所有包含这个商店的所有节点的 01 Trie 都更新一下。那么 \([l,r]\) 就可以拆成线段树上的区间,取个 \(\max\) 即可。
时间怎么处理呢?由于合法的时间一定是个后缀,我们对 01 Trie 的每个节点记录一下它最后被更新的时刻 \(tag\)。然后查询时只有 \(tag \ge t -d + 1\) 的点才走即可。
时间为复杂度 \(O(n \log^2 n)\)。空间复杂度 \(O(n \log^2 n)\)。
代码:
#include<bits/stdc++.h>
#define N 100005
#define il inline
using namespace std;
bool stemer;
int n, m, cnt;
int ans[N];
int rt[N], tree[N * 20][2], val[N * 20];
il void Insert(int lsto, int &o, int x) {
o = ++cnt;
int now = o;
for(int i = 17; i >= 0; i--) {
val[now] = val[lsto] + 1;
int z = ((x >> i) & 1);
if(z == 0) {
tree[now][0] = ++cnt;
tree[now][1] = tree[lsto][1];
now = tree[now][0];
lsto = tree[lsto][0];
}
else {
tree[now][1] = ++cnt;
tree[now][0] = tree[lsto][0];
now = tree[now][1];
lsto = tree[lsto][1];
}
}
val[now] = val[lsto] + 1;
}
il int ask(int lsto, int o, int x) {
int now = o, res = 0;
for(int i = 17; i >= 0; i--) {
int z = ((x >> i) & 1);
if(val[tree[now][z ^ 1]] - val[tree[lsto][z ^ 1]] > 0) {
now = tree[now][z ^ 1];
lsto = tree[lsto][z ^ 1];
res += (1 << i);
}
else {
now = tree[now][z];
lsto = tree[lsto][z];
}
}
return res;
}
int cnt1, cnt2, top;
struct node0 { //在时刻t的店s加入价格为v的物品
int s, v, t;
}q[N], b1[N], b2[N];
struct node1 { //询问在时刻[tl,tr]的所有编号为[l,r]商店的所有物品与x的异或最大值
int l, r, x, tl, tr;
}p[N];
bool cmp(node0 x, node0 y) {
return x.s < y.s;
}
vector<int>z[N];
il void update(int u, int L, int R, int l, int r, int x) {
if(l <= L && R <= r) {
z[u].push_back(x);
return;
}
if(R < l || r < L) return;
int mid = (L + R) / 2;
update(u * 2, L, mid, l, r, x);
update(u * 2 + 1, mid + 1, R, l, r, x);
}
int S[N];
il void Sol(int u, int l, int r, int L, int R) { //时间为[l,r],修改为q[L...R]
if(L > R) return;
top = cnt = 0;
int c1 = 0, c2 = 0;
for(int i = L; i <= R; i++) { //处理在这个时间段的物品
top++;
Insert(rt[top - 1], rt[top], q[i].v);
S[top] = q[i].s;
}
for(auto X : z[u]) { //处理在这个时间段的询问
int ll = lower_bound(S + 1, S + top + 1, p[X].l) - S, rr = upper_bound(S + 1, S + top + 1, p[X].r) - S - 1;
ans[X] = max(ans[X], ask(rt[ll - 1], rt[rr], p[X].x));
}
//cout << endl;
if(l == r) return;
int mid = (l + r) / 2;
for(int i = L; i <= R; i++) {
if(q[i].t <= mid) b1[++c1] = q[i];
else b2[++c2] = q[i];
}
for(int i = L; i <= L + c1 - 1; i++) q[i] = b1[i - L + 1];
for(int i = L + c1; i <= R; i++) q[i] = b2[i - (L + c1) + 1];
Sol(u * 2, l, mid, L, L + c1 - 1);
Sol(u * 2 + 1, mid + 1, r, L + c1, R);
}
bool endmer;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cerr << (double)(&stemer - &endmer) / 1024 / 1024 << " MB" << endl;
cin >> n >> m;
for(int i = 1, x; i <= n; i++) {
cin >> x;
top++;
Insert(rt[top - 1], rt[top], x);
}
while(m--) {
int opt, D;
cin >> opt;
if(!opt) {
cnt1++;
cin >> q[cnt1].s >> q[cnt1].v;
q[cnt1].t = cnt1;
}
else {
cnt2++;
cin >> p[cnt2].l >> p[cnt2].r >> p[cnt2].x >> D;
p[cnt2].tl = cnt1 - D + 1;
p[cnt2].tr = cnt1;
ans[cnt2] = max(ans[cnt2], ask(rt[p[cnt2].l - 1], rt[p[cnt2].r], p[cnt2].x));
}
}
for(int i = 1; i <= cnt2; i++) update(1, 1, cnt1, p[i].tl, p[i].tr, i);
sort(q + 1, q + cnt1 + 1, cmp);
Sol(1, 1, cnt1, 1, cnt1);
for(int i = 1; i <= cnt2; i++) cout << ans[i] << endl;
return 0;
}
/*
4 3
1 2 3 4
0 1 4
0 1 3
1 1 1 1 1
*/
CF938G Shortest Path Queries
题意:
给出一个连通带权无向图,边有边权,要求支持 \(q\) 个操作:
\(1\) \(x\) \(y\) \(d\) 在原图中加入一条 \(x\) 到 \(y\) 权值为 \(d\) 的边
\(2\) \(x\) \(y\) 把图中 \(x\) 到 \(y\) 的边删掉
\(3\) \(x\) \(y\) 表示询问 \(x\) 到 \(y\) 的异或最短路
保证任意操作后当前状态下的图连通无重边自环且操作均合法
\(1 \leq n,m,q\le2 \times 10^5,d \le 2^{30}-1\)。
分析:
先考虑没有操作 1,2 该怎么做。由于一条边可以重复走,因此可以将答案路径视作两部分:\(x \rightarrow y\) 的简单路径以及一些环的权值。具体路径就是沿着走到这个环的路径原路返回。
显然要使用线性基,肯定无法将所有环放进去。考虑建任意生成树,把由一条非树边构成的环的异或权值扔到线性基里(可以证明这样做能将由 2 条非树边构成的环的权值被其异或出来),然后任取一条从 \(x \rightarrow y\) 的简单路径(倘若有其他简单路径也能被这一条简单路径和包含它们的环异或出来),令其权值为 \(w\)。那么答案就是在线性基找与 \(w\) 异或的最小值。
加上操作 1,2 呢?一个常见的套路是利用线段树分治将其改成加边、撤销边。因此需要动态维护生成树,可以想到使用可撤销带权并查集,记 \(dis_{i}\) 表示 \(i\) 到其当前根节点的路径的权值异或和。
考虑加入一条边 \(x\rightarrow y\),边权为 \(w\) :
- \(x,y\) 在一个集合(生成树),将 \(dis_{x} \oplus dis_{y} \oplus w\) 扔进线性基里。
- \(x,y\) 不再同一个生成树,记它们的根节点分别为 \(fx,fy\),由于带权并查集只支持在两个根节点直接连边,可以发现在 \(x,y\) 之间连一条边权为 \(w\) 等价于在 \(fx,fy\) 之间连一条边权为 \(dis_{x} \oplus dis_{y} \oplus w\) 的边。
时间复杂度 \(O(q \log q \log w)\)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 400005
using namespace std;
int n, m, Q;
struct edge {
int to, w;
};
vector<edge>p[N];
int c[35];
void ins(int x) {
for(int i = 30; i >= 0; i--) {
if(x & (1 << i)) {
if(!c[i]) {
c[i] = x;
break;
}
x ^= c[i];
}
}
}
int ask(int x) {
//cout << x << endl;
for(int i = 30; i >= 0; i--)
x = min(x, x ^ c[i]);
return x;
}
int U[N], V[N], W[N], tot, len;
map<int, map<int, int>>f;
int L[N], R[N], ans[N];
vector<int>G[N * 4];
struct node {
int x, y, id;
};
node P[N]; //G记录修改,P记录询问
int fa[N], h[N], dis[N], dis2[N];
void update(int u, int L, int R, int l, int r, int x) {
if(R < l || r < L) return;
if(l <= L && R <= r) {
G[u].push_back(x);
return;
}
int mid = (L + R) / 2;
update(u * 2, L, mid, l, r, x);
update(u * 2 + 1, mid + 1, R, l, r, x);
}
int find(int x) {
if(fa[x] == x) return x;
int u = fa[x];
int FA = find(fa[x]);
dis[x] = dis[u] ^ dis2[x];
return FA;
}
struct ljm {
int fx, fy, d1, d2, opt;
};
int top;
ljm B[N];
bool join (int x, int y, int w) {
//cout << "ssss" << w << endl;
int fx = find(x), fy = find(y);
if(fx == fy) return 0;
if(h[fy] > h[fx]) swap(fy, fx), swap(x, y);
B[++top] = ((ljm){fx, fy, dis[fy], dis2[fy], h[fx] == h[fy]});
//cout << x << "->" << y << " " << fy << " -> " << fx << " " << (dis[x] ^ dis[y] ^ w) << " " << dis[x] << " " << dis[y] << " " << w << endl;
fa[fy] = fx;
dis[fy] = dis[x] ^ dis[y] ^ w;
dis2[fy] = dis[fy];
//cout << dis[2] << endl;
if(h[fx] == h[fy]) h[fx]++;
return 1;
}
bool can[N];
void Sol(int u, int L, int R) {
int lstop = top;
int lst[35];
for(int i = 0; i <= 30; i++) lst[i] = c[i];
for(auto x : G[u]) { //处理x这个修改
//cout << "add : " << U[x] << " , " << V[x] << endl;
if(!join(U[x], V[x], W[x]))
ins(dis[U[x]] ^ dis[V[x]] ^ W[x]);
}
if(L == R) {
if(can[L]) {
//cout << << endl;
int fx = find(P[L].x), fy = find(P[L].y);
//cout << "e" << dis[P[L].x] << " " << fa[P[L].y] << endl;
ans[P[L].id] = ask(dis[P[L].x] ^ dis[P[L].y]);
//cout << "look : " << dis[P[L].x] << " " << dis[P[L].y] << endl;
}
}
else {
int mid = (L + R) / 2;
Sol(u * 2, L, mid);
Sol(u * 2 + 1, mid + 1, R);
}
for(int i = 0; i <= 30; i++) c[i] = lst[i];
while(top > lstop) {
fa[B[top].fy] = B[top].fy;
dis[B[top].fy] = B[top].d1;
dis2[B[top].fy] = B[top].d2;
h[B[top].fx] -= B[top].opt;
top--;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
tot++;
U[tot] = min(u, v);
V[tot] = max(u, v);
W[tot] = w;
L[tot] = 1;
f[min(u, v)][max(u, v)] = tot;
}
cin >> Q;
for(int i = 1; i <= Q; i++) {
int opt, x, y, d;
cin >> opt >> x >> y;
if(opt == 1) {
cin >> d;
tot++;
U[tot] = min(x, y);
V[tot] = max(x, y);
W[tot] = d;
L[tot] = i;
f[min(x, y)][max(x, y)] = tot;
}
else if(opt == 2) {
R[f[min(x, y)][max(x, y)]] = i;
}
else {
len++;
P[i] = (node){x, y, len};
can[i] = 1;
}
}
for(int i = 1; i <= n; i++) fa[i] = i, h[i] = 1;
for(int i = 1; i <= tot; i++) {
if(R[i] == 0) R[i] = Q;
//cout << U[i] << " " << V[i] << " : " << "[" << L[i] << " , " << R[i] << "]" << endl;
update(1, 1, Q, L[i], R[i], i);
}
Sol(1, 1, Q);
for(int i = 1; i <= len; i++) cout << ans[i] << endl;
return 0;
}
/*
start : 2024.3.18 8:10
end : 2024.3.18 10:40
*/
/*
3 2
1 2 2
2 3 1
1
3 1 3
*/
浙公网安备 33010602011771号