[学习笔记]线段树分治
暂时先更到这儿,后面有时间还会补题。
主要写给自己看,不会很详细。
问题
主要针对一类“操作在时间轴上有存在区间”的问题,按时间轴分治进行查询。通过使用线段树分治,能将不同时间点间的公共信息进行继承,从而避免了暴力做法中对信息的销毁再重构造成的时间浪费。
流程
- 建线段树
- 将操作覆盖在线段树的时间区间上
- dfs整棵线段树,到叶子结点的路径上将信息继承以得到子节点的信息,离开一个区间的时候就要撤销这个区间的操作
- 一般会结合一些轻小的数据结构或算法使用,要求可撤销,如可撤销并查集、可持久化Trie、线性基等。
例题
P5787 二分图/【模板】线段树分治
经典问题:动态图。
判定二分图使用扩展域并查集,既然要撤销那就按秩合并。
#include <cstdio>
#include <vector>
using namespace std;
const int maxn = 1e5 + 10, maxm = 2e5 + 10, maxk = 1e5 + 10;
int n, m, k;
struct Edge
{
int u, v;
} e[maxm];
vector<int> t[maxn << 2];
int fa[maxn << 1], sz[maxn << 1];
struct dat
{
int x, y, add; //x并入y,y的sz加上了add
} st[maxn << 2];
int top;
void modify(int p, int l, int r, int x, int y, int v)
{
if(l > y || r < x)
return;
if(x <= l && r <= y)
{
t[p].push_back(v);
return;
}
int mid = (l + r) >> 1;
modify(p << 1, l, mid, x, y, v);
modify(p << 1 | 1, mid + 1, r, x, y, v);
return;
}
int find(int x)
{
while(fa[x] != x) x = fa[x];
return x;
}
void hb(int x, int y)
{
int a = find(x), b = find(y);
if(sz[a] > sz[b]) swap(a, b);
fa[a] = b; sz[b] += sz[a];
st[++top] = (dat){a, b, sz[a]};
return;
}
void solve(int p, int l, int r)
{
int len = t[p].size(), lst = top;
bool flag = true;
for(int i = 0; i < len; ++i)
/*刚写这题的时候NOI系列赛的新语言标准(-std=c++14)还没出来
当时默认语言还是c++98
所以就写了个这么丑陋的循环
懒得改了*/
{
int u = e[t[p][i]].u, v = e[t[p][i]].v;
int a = find(u), b = find(v);
if(a == b)
{
flag = false;
for(int k = l; k <= r; ++k)
{
puts("No");
}
break;
}
hb(u, v + n);
hb(v, u + n);
}
if(flag)
{
if(l == r)
puts("Yes");
else
{
int mid = (l + r) >> 1;
solve(p << 1, l, mid);
solve(p << 1 | 1, mid + 1, r);
}
}
while(top > lst)
{
dat d = st[top--];
sz[d.y] -= d.add;
fa[d.x] = d.x;
}
return;
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= m; ++i)
{
int l, r;
scanf("%d%d%d%d", &e[i].u, &e[i].v, &l, &r);
if(l + 1 <= k && l < r)
modify(1, 1, k, l + 1, r, i);
}
for(int i = 1; i <= 2 * n; ++i)
{
fa[i] = i; sz[i] = 1;
}
solve(1, 1, k);
return 0;
}
P4588 [TJOI2018]数学计算
由于 \(M\) 不一定为质数,会有很多逆元不存在的情况,用乘法逆元不便处理。
发现题目中的操作其实就是连乘再撤销的过程,相当于每一个乘法有一段存在时间。
这样就把删除的操作化为了每个添加操作有存在的时间区间。
快乐地使用线段树分治。
事实上这题还有一个更直接的做法:使用普通的线段树,乘法的区间修改,然后每个点单点查询。尽管在得到答案的过程中复杂度比线段树分治大,但前面区间 modify 的复杂度是相同的,所以总的复杂度没有变,都是\(O(q\log q)\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxq = 1e5 + 5;
ll tr[maxq << 2];
int q;
ll Mod;
struct oper
{
int l;
int r;
ll m;
};
oper a[maxq];
int cnt, id[maxq];
void build(int p, int l, int r)
{
tr[p] = 1;
if(l == r) return;
int mid = (l + r) >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
return;
}
void modify(int p, int l, int r, int x, int y, ll m)
{
if(r < x || l > y) return;
if(x <= l && r <= y)
{
tr[p] = tr[p] * m % Mod;
return;
}
int mid = (l + r) >> 1;
modify(p << 1, l, mid, x, y, m);
modify(p << 1 | 1, mid + 1, r, x, y, m);
return;
}
void dfs(int p, int l, int r, ll res)
{
res = res * tr[p] % Mod;
if(l == r)
printf("%lld\n", res);
else
{
int mid = (l + r) >> 1;
dfs(p << 1, l, mid, res);
dfs(p << 1 | 1, mid + 1, r, res);
}
return;
}
void work()
{
scanf("%d%lld", &q, &Mod);
cnt = 0;
fill(a + 1, a + q + 1, (oper){0, 0, 1});
for(int i = 1; i <= q; ++i)
{
int opt; ll m;
scanf("%d%lld", &opt, &m);
if(opt == 1)
{
a[++cnt].m = m;
a[cnt].l = i;
id[i] = cnt;
}
else
a[id[m]].r = i - 1;
}
for(int i = 1; i <= cnt; ++i)
if(a[i].r == 0)
a[i].r = q;
build(1, 1, q);
for(int i = 1; i <= cnt; ++i)
modify(1, 1, q, a[i].l, a[i].r, a[i].m);
dfs(1, 1, q, 1);
return;
}
int main()
{
int T;
scanf("%d", &T);
while(T--)
work();
return 0;
}
P5227 [AHOI2013]连通图
经典的动态图问题。
类似 [JSOI2008]星球大战 的思想,既然可以离线,那么把删边换为加边。
那么一条边的存在在时间轴上依然为区间形式。
顺理成章地使用线段树分治,维护连通性使用并查集。
#include <cstdio>
#include <utility>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int maxn = 1e5 + 10, maxk = 1e5 + 10, maxm = 2e5 + 10;
int n, m, k;
PII e[maxm];
int pre[maxm], tmp[7];
vector<int> t[maxk << 2];
int fa[maxn], sz[maxn], tot;
struct dat
{
int x, y, add;
} st[maxm << 2];
int top;
void modify(int p, int l, int r, int x, int y, int v)
{
if(l > y || r < x)
return;
if(x <= l && r <= y)
{
t[p].push_back(v);
return;
}
int mid = (l + r) >> 1;
modify(p << 1, l, mid, x, y, v);
modify(p << 1 | 1, mid + 1, r, x, y, v);
return;
}
int find(int x)
{
while(fa[x] != x) x = fa[x];
return x;
}
void hb(int a, int b)
{
if(sz[a] > sz[b]) swap(a, b);
tot++;
fa[a] = b;
sz[b] += sz[a];
st[++top] = (dat){a, b, sz[a]};
return;
}
void solve(int p, int l, int r)
{
int lst = top;
for(int i : t[p])
{
int a = find(e[i].first), b = find(e[i].second);
if(a != b)
hb(a, b);
}
if(l == r)
puts(tot == n - 1 ? "Connected" : "Disconnected");
else
{
int mid = (l + r) >> 1;
solve(p << 1, l, mid);
solve(p << 1 | 1, mid + 1, r);
}
while(top > lst)
{
dat d = st[top--];
tot--;
sz[d.y] -= d.add;
fa[d.x] = d.x;
}
return;
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++i)
{
int u, v;
scanf("%d%d", &u, &v);
e[i] = make_pair(u, v);
pre[i] = 1;
}
scanf("%d", &k);
for(int i = 1; i <= k; ++i)
{
int c;
scanf("%d", &c);
for(int j = 1; j <= c; ++j)
scanf("%d", tmp + j);
for(int j = 1; j <= c; ++j)
{
if(i - 1 >= pre[tmp[j]])
modify(1, 1, k, pre[tmp[j]], i - 1, tmp[j]);
pre[tmp[j]] = i + 1;
}
}
for(int i = 1; i <= m; ++i)
if(k >= pre[i])
modify(1, 1, k, pre[i], k, i);
for(int i = 1; i <= n; ++i)
{
fa[i] = i;
sz[i] = 1;
}
solve(1, 1, k);
return 0;
}
P5214 [SHOI2014]神奇化合物
这题有跟这个化合物一样神奇的暴力做法,但在这里不讨论。
还是动态图,还是线段树分治。
每条边出现的时间在时间轴上形成一个区间。
#include <cstdio>
#include <utility>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int maxn = 5005, maxm = 2e5 + 10, maxq = 10005;
int d[maxn][maxn];
int fa[maxn], sz[maxn], tot;
int n, m, q;
char tmp[5];
vector<PII> t[maxq << 2];
int qe[maxq], cnt, ans[maxq];
struct dat
{
int x, y, add;
} st[maxq << 2];
int top;
void modify(int p, int l, int r, int x, int y, const PII &v)
{
if(l > y || r < x)
return;
if(x <= l && r <= y)
{
t[p].push_back(v);
return;
}
int mid = (l + r) >> 1;
modify(p << 1, l, mid, x, y, v);
modify(p << 1 | 1, mid + 1, r, x, y, v);
return;
}
int find(int x)
{
while(x != fa[x]) x = fa[x];
return x;
}
void hb(int a, int b)
{
if(sz[a] > sz[b]) swap(a, b);
fa[a] = b;
sz[b] += sz[a];
st[++top] = (dat){a, b, sz[a]};
tot++;
return;
}
void solve(int p, int l, int r)
{
int lst = top;
for(PII i : t[p])
{
int a = find(i.first), b = find(i.second);
if(a != b)
hb(a, b);
}
if(l == r)
ans[l] = n - tot;
else
{
int mid = (l + r) >> 1;
solve(p << 1, l, mid);
solve(p << 1 | 1, mid + 1, r);
}
while(top > lst)
{
dat d = st[top--];
tot--;
sz[d.y] -= d.add;
fa[d.x] = d.x;
}
return;
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++i)
{
int a, b;
scanf("%d%d", &a, &b);
d[a][b] = d[b][a] = 1;
}
scanf("%d", &q);
++q;
for(int i = 2; i <= q; ++i)
{
scanf("%s", tmp);
int x, y;
if(tmp[0] == 'A')
{
scanf("%d%d", &x, &y);
if(d[x][y] == 0)
d[x][y] = d[y][x] = i;
}
else if(tmp[0] == 'D')
{
scanf("%d%d", &x, &y);
modify(1, 1, q, d[x][y], i - 1, make_pair(x, y));
d[x][y] = d[y][x] = 0;
}
else
{
qe[++cnt] = i;
}
}
for(int i = 1; i <= n; ++i)
for(int j = i + 1; j <= n; ++j)
if(d[i][j])
modify(1, 1, q, d[i][j], q, make_pair(i, j));
for(int i = 1; i <= n; ++i)
{
fa[i] = i;
sz[i] = 1;
}
solve(1, 1, q);
for(int i = 1; i <= cnt; ++i)
printf("%d\n", ans[qe[i]]);
return 0;
}
P5631 最小mex生成树
没什么很好的思路能直接求,考虑枚举 \(mex\) 是多少并进行判定。
从小到大枚举 \(mex\),设当前枚举值为 \(w\),那么 \(w\) 是 \(mex\) 意味着 \(w\) 可以不在生成树中出现,也就是去掉边权为 \(w\) 的所有边后依然有一棵生成树,而一张图有生成树等价于这张图连通。
所以只需要判定删掉所有边权为 \(w\) 的边后图是否连通即可。经典的动态图问题。
比 \(w\) 小的边权在前面的枚举中判定不合法,即任何一个生成树都含有所有比 \(w\) 小的边权,那么 \(w\) 满足 \(mex\) 的条件。
细节就是判定边权的值域是否连续。
《论动态图在线段树分治应用中的地位》
#include <cstdio>
#include <cctype>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 1e6 + 10, maxm = 2e6 + 10, maxw = 1e5 + 10;
struct Edge
{
int u, v, w;
} e[maxm];
vector<Edge> vec[maxw << 2];
int n, m;
bool con[maxw];
int fa[maxn], sz[maxn];
struct datum
{
int r1, r2, ad;
};
datum stk[maxn];
int top, lim;
int read();
bool cmp(const Edge &x, const Edge &y)
{
return x.w < y.w;
}
void modify(int p, int l, int r, int a, int b, const Edge &e)
{
if(r < a || l > b) return;
if(a <= l && r <= b)
{
vec[p].push_back(e);
return;
}
int mid = (l + r) >> 1;
modify(p << 1, l, mid, a, b, e);
modify(p << 1 | 1, mid + 1, r, a, b, e);
return;
}
int find(int x)
{
while(x != fa[x]) x = fa[x];
return x;
}
void hb(int r1, int r2)
{
if(sz[r1] > sz[r2]) swap(r1, r2);
fa[r1] = r2;
sz[r2] += sz[r1];
stk[++top] = (datum){r1, r2, sz[r1]};
return;
}
void dfs(int p, int l, int r, int cnt)
{
if(cnt == 1)
{
for(int i = l; i <= r; ++i)
con[i] = true;
return;
}
int lst = top;
for(Edge edg : vec[p])
{
int u = edg.u, v = edg.v, w = edg.w;
int r1 = find(u), r2 = find(v);
if(r1 != r2)
{
hb(r1, r2);
cnt--;
}
}
if(l == r)
con[l] = (cnt == 1);
else
{
int mid = (l + r) >> 1;
dfs(p << 1, l, mid, cnt);
dfs(p << 1 | 1, mid + 1, r, cnt);
}
while(top > lst)
{
datum tmp = stk[top--];
fa[tmp.r1] = tmp.r1;
sz[tmp.r2] -= tmp.ad;
}
return;
}
int main()
{
n = read(); m = read();
for(int i = 1; i <= m; ++i)
{
e[i].u = read();
e[i].v = read();
e[i].w = read();
}
sort(e + 1, e + m + 1, cmp);
if(e[1].w > 0)
{
puts("0");
return 0;
}
int pos = m;
for(int i = 1; i < m; ++i)
if(e[i + 1].w - e[i].w > 1)
{
pos = i;
break;
}
lim = e[pos].w;
int cur;
if(lim > 0)
for(cur = 1; e[cur].w == 0; ++cur)
modify(1, 0, lim, 1, lim, e[cur]);
for( ; e[cur].w < lim; ++cur)
{
modify(1, 0, lim, 0, e[cur].w - 1, e[cur]);
modify(1, 0, lim, e[cur].w + 1, lim, e[cur]);
}
if(lim > 0)
for( ; e[cur].w == lim; ++cur)
modify(1, 0, lim, 0, lim - 1, e[cur]);
for( ; cur <= m; ++cur)
modify(1, 0, lim, 0, lim, e[cur]);
for(int i = 1; i <= n; ++i)
{
fa[i] = i;
sz[i] = 1;
}
dfs(1, 0, lim, n);
for(int i = 1; i <= pos; ++i)
if(con[e[i].w])
{
printf("%d\n", e[i].w);
return 0;
}
printf("%d\n", e[pos].w + 1);
return 0;
}
int read()
{
int x = 0; bool f = true; char ch = getchar();
while(!isdigit(ch)) { if(ch == '-') f = false; ch = getchar(); }
while(isdigit(ch)) { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
return f ? x : -x;
}
P4585 [FJOI2015] 火星商店问题
终于不是动态图了~
本题有两个限制:可购买的商品的时间限制,以及商店编号的限制。
可购买的商品的时间是个区间,在时间轴上进行线段树分治。
商店编号的限制使用可持久化\(\text{01Trie}\),即可得到异或最大值。
要往时间轴上铺设的是火星人的查询操作。
为了保证商店的编号是在 \([l,r]\) 内的,我们需要按编号对商品排序,然后再将符合时间要求的丢到对应的分治区间里。
撤销可持久化 Trie 比较难,不妨直接暴力重建。
剩下的详见代码吧,这题比较难,写了点儿注释。时间复杂度 \(O(n\log ^2 n)\)
FJOI 没有部分分,题面描述恶心,好评
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10, maxb = 18;
int n, m;
struct import //购进商品的操作
{
int s, v, t;
} ad[maxn], tmp1[maxn], tmp2[maxn];
int totad; //操作0的总数
struct ask //火星人购买商品的操作
{
int l, r, x, tl, tr, id;
} q[maxn];
int totq; //操作1的总数
int ans[maxn];
int day; //当前天数,事实上day和totad相等
struct Trie
{
int ch[2];
int w;
} tr[maxn * maxb];
int root[maxn], tot;
vector<int> dat[maxn << 2];
int que[maxn];
void insert(int &p, int pre, int x, int bit)
{
tr[p = ++tot] = tr[pre];
tr[p].w++;
if(bit == -1) return;
int c = (x >> bit) & 1;
insert(tr[p].ch[c], tr[pre].ch[c], x, bit - 1);
return;
}
int query(int r1, int r2, int x)
{
int res = 0;
for(int i = 16; i >= 0; --i)
{
int c = ((x >> i) & 1) ^ 1;
int cnt = tr[tr[r2].ch[c]].w - tr[tr[r1].ch[c]].w;
if(cnt > 0)
{
res |= (1 << i);
r2 = tr[r2].ch[c];
r1 = tr[r1].ch[c];
}
else
{
r2 = tr[r2].ch[1 ^ c];
r1 = tr[r1].ch[1 ^ c];
}
}
return res;
}
bool cmp(const import &x, const import &y)
{
return x.s < y.s;
}
void modify(int p, int tl, int tr, int tx, int ty, int v)
{
if(tr < tx || tl > ty) return;
if(tx <= tl && tr <= ty)
{
dat[p].push_back(v);
return;
}
int mid = (tl + tr) >> 1;
modify(p << 1, tl, mid, tx, ty, v);
modify(p << 1 | 1, mid + 1, tr, tx, ty, v);
return;
}
void calc(int p, int a, int b)
{
if(dat[p].empty()) return;
tot = 0;
int tail = 0;
for(int i = a; i <= b; ++i)
{
que[++tail] = ad[i].s;
insert(root[tail], root[tail - 1], ad[i].v, 16);
}
for(int i : dat[p])
{
int l = lower_bound(que + 1, que + tail + 1, q[i].l) - que;
int r = upper_bound(que + 1, que + tail + 1, q[i].r) - que - 1;
ans[q[i].id] = max(ans[q[i].id], query(root[l - 1], root[r], q[i].x));
}
return;
}
void dfs(int p, int tl, int tr, int a, int b)
//[tl, tr]是时间区间,[a,b]是排序后的商品编号区间
{
if(a > b) return;
calc(p, a, b);
if(tl == tr) return;
int mid = (tl + tr) >> 1;
int p1 = 0, p2 = 0;
for(int i = a; i <= b; ++i) //丢到对应的时间区间
{
if(ad[i].t <= mid)
tmp1[++p1] = ad[i];
else
tmp2[++p2] = ad[i];
}
for(int i = a; i <= a + p1 - 1; ++i)
ad[i] = tmp1[i - a + 1];
for(int i = a + p1; i <= b; ++i)
ad[i] = tmp2[i - a - p1 + 1];
dfs(p << 1, tl, mid, a, a + p1 - 1);
dfs(p << 1 | 1, mid + 1, tr, a + p1, b);
return;
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i)
{
int x;
scanf("%d", &x);
insert(root[i], root[i - 1], x, 16);
} //特殊商品,另行计算
day = 1; //坑点一:样例开始输入时就是第一天,此时可能就是操作1而非操作0
for(int i = 1; i <= m; ++i)
{
int opt;
scanf("%d", &opt);
if(opt == 0)
{
if(i != 1) day++; //对应坑点一
ad[++totad].t = day;
scanf("%d%d", &ad[totad].s, &ad[totad].v);
}
else
{
int d;
totq++;
scanf("%d%d%d%d", &q[totq].l, &q[totq].r, &q[totq].x, &d);
q[totq].tr = day;
q[totq].tl = max(1, day - d + 1);
q[totq].id = totq;
ans[totq] = query(root[q[totq].l - 1], root[q[totq].r], q[totq].x); //考虑购买特殊商品
}
}
sort(ad + 1, ad + totad + 1, cmp); //按照商店编号对商品排序
for(int i = 1; i <= totq; ++i)
if(q[i].tl <= q[i].tr) //坑点二:存在d=0的情况,此时只能购买特殊商品
modify(1, 1, day, q[i].tl, q[i].tr, i);
dfs(1, 1, day, 1, totad);
for(int i = 1; i <= totq; ++i)
printf("%d\n", ans[i]);
return 0;
}

浙公网安备 33010602011771号