快来踩|

Little_corn

园龄:1年1个月粉丝:11关注:17

2024-08-18 20:02阅读: 18评论: 0推荐: 0

DS 进阶

李超线段树:

Problem: 维护一个一次函数集合 S,支持两个操作:

  • 加入一条一次函数 f(x)=kx+b(lxr)
  • 给出 x,求 maxfSf(x)

首先对于一个一次函数,将 [l,r] 拆成 logn 个区间,然后把一次函数挂到这个节点上,每次询问对这些一次函数取个 max。这个朴素算法有一个问题,就是可能一个节点可能挂了很多个函数,时间复杂度无法保证。

考虑改进这个朴素算法,我们考虑在每个节点上只保留一个函数。于是当两个函数同时挂到一个节点时,我们比较两个函数并得到他们的优势区间,显然只会有一个函数跨过区间,于是将另一个函数下放到它的优势区间。由于这是单边递归,时间复杂度显然是 O(logn)。那么由于拆出来有 O(logn) 的区间,单次操作时间复杂度 O(log2n),查询 O(logn)。于是总时间复杂度 O(mlog2n)

注意到某些需要斜率优化的 DP 也等价于加入一次函数和单点求 max,而且每次都是全局加不需要拆分区间,时间复杂度 O(nlogn),吊打 CDQ 和平衡树。

例题

code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
const double eps = 1e-9;
struct Segment{
long double k, b;
void init(int x0, int x1, int y0, int y1){
if(x0 == y0) k = 0, b = max(y0, y1);
else k = 1.0 * (y1 - y0) / (x1 - x0), b = y0 - 1.0 * x0 * k;
}
double val(int x){return 1.0 * k * x + b;}
}S[N];
// ask if x > y(x = y: 2)
int cmp(double x, double y){
if(y - x > eps) return 0;
if(x - y > eps) return 1;
return 2;
}
// get maxid
int max(int id1, int id2, int x){
int op = cmp(S[id1].val(x), S[id2].val(x));
if(op == 2) return (id1 < id2 ? id1 : id2);
return (op ? id1 : id2);
}
struct Segtree{
#define ls (o << 1)
#define rs (o << 1 | 1)
#define mid (l + r >> 1)
int tag[N << 2];
void upd(int o, int l, int r, int id1){
if(!tag[o]){tag[o] = id1; return;}
if(max(id1, tag[o], mid) == id1) swap(id1, tag[o]);
if(l == r) return;
if(max(id1, tag[o], l) == id1) upd(ls, l, mid, id1);
if(max(id1, tag[o], r) == id1) upd(rs, mid + 1, r, id1);
}
void findSeg(int o, int l, int r, int s, int t, int id){
if(s <= l && r <= t){upd(o, l, r, id); return;}
if(s <= mid) findSeg(ls, l, mid, s, t, id);
if(mid < t) findSeg(rs, mid + 1, r, s, t, id);
}
int qrymax(int o, int l, int r, int x){
if(l == r) return tag[o];
if(x <= mid) return max(tag[o], qrymax(ls, l, mid, x), x);
else return max(tag[o], qrymax(rs, mid + 1, r, x), x);
}
}Seg;
int n, tot, siz = 39990;
void ADD(int& x, int y, int mod){x = (x + y - 1) % mod + 1;}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n; int lstans = 0;
while(n--){
int opt; cin >> opt;
if(opt == 0){
int pos; cin >> pos; ADD(pos, lstans, 39989);
cout << (lstans = Seg.qrymax(1, 1, siz, pos)) << "\n";
}
else{
int x0, y0, x1, y1; cin >> x0 >> y0 >> x1 >> y1;
ADD(x0, lstans, 39989); ADD(x1, lstans, 39989);
ADD(y0, lstans, 1e9); ADD(y1, lstans, 1e9);
S[++tot].init(x0, x1, y0, y1);
Seg.findSeg(1, 1, siz, min(x0, x1), max(x0, x1), tot);
}
}
// system("pause");
return 0;
}

线段树合并

Problem: 维护 n 个元素,初始时,每个元素自成一个集合。操作可以将两个集合合并,或对某个集合进行查询。

用动态开点线段树维护集合。合并两棵动态开点线段树时,只有双方重合的节点需要处理,耗时为“重合节点数目”。每次合并,总节点数都会减少“重合节点数目”,而总结点数是 O(nlogn) 的,故总复杂度均摊 O(nlogn)

这样讲可能过于抽象。来看例题 P3224 [HNOI2012] 永无乡

连边等价于合并两个联通块,用并查集维护。对于一个联通块,我们用权值线段树维护即可。每次合并两个联通块时,只合并重复的节点即可。这样时间复杂度和空间复杂度均为 O(nlogn)

code
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, fa[N], rev[N];
int findfa(int x){return fa[x] = (fa[x] == x) ? x : findfa(fa[x]);}
struct node{
int ls, rs, sum;
}t[N << 5];
int tot, rub[N << 5], tb;
int newnode(int val){
int id = (tb ? rub[tb--] : (++tot));
t[id] = {0, 0, 1}; return id;
}
struct Segtree{
#define mid (l + r >> 1)
int root;
void pushup(int o){t[o].sum = t[t[o].ls].sum + t[t[o].rs].sum;}
void init(int val){root = build(1, n, val);}
int build(int l, int r, int val){
int o = newnode(val); if(l == r) return o;
if(val <= mid) t[o].ls = build(l, mid, val);
else t[o].rs = build(mid + 1, r, val);
pushup(o);
return o;
}
int getkth(int o, int l, int r, int k){
if(t[o].sum < k) return -1;
if(l == r) return rev[l]; int lsiz = t[t[o].ls].sum;
if(lsiz >= k) return getkth(t[o].ls, l, mid, k);
else return getkth(t[o].rs, mid + 1, r, k - lsiz);
}
int merge(int o, int rt, int l, int r){
if((!o) || (!rt)) return o + rt;
t[o].ls = merge(t[o].ls, t[rt].ls, l, mid);
t[o].rs = merge(t[o].rs, t[rt].rs, mid + 1, r);
pushup(o); rub[++tb] = rt; return o;
}
}Seg[N];
void merge(int x, int y){
int fx = findfa(x), fy = findfa(y);
if(fx == fy) return;
fa[fy] = fx; Seg[fx].merge(Seg[fx].root, Seg[fy].root, 1, n);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++){
int x; cin >> x; rev[x] = i;
Seg[i].init(x); fa[i] = i;
}
for(int i = 1; i <= m; i++){
int x, y; cin >> x >> y;
merge(x, y);
}
int T; cin >> T;
while(T--){
char opt; int x, y; cin >> opt >> x >> y;
if(opt == 'Q') cout << Seg[findfa(x)].getkth(Seg[findfa(x)].root, 1, n, y) << "\n";
else merge(x, y);
}
// system("pause");
return 0;
}

线段树合并还可以用来优化树形 DP,比如 P6847 [CEOI2019] Magic Tree

显然有一个 O(nk) 的暴力 DP,设 fu,i 设考虑以 u 为根的子树中,时间 i 的最多果汁数。有两种转移:

  • 不割 u 和父亲的连边:fu,i=vsubtree(u)fv,i

  • u 和父亲的连边:i[du,k],fu,i=max(fu,i,vsubtree(u)fv,du+wu)

于是用下标为时间的线段树维护 fu,于是我们首先将 u 的所有子树的线段树进行合并。第二个转移等价于将区间 [du,k]max。然后注意到 fu,i 是单调不减的,于是每次等价于找到一个最大的 c,使得 i[du,c],fu,ival(val=vsubtree(u)fv,du+wu),将 [du,c] 全部置为 val。但是线段树合并的时间复杂度是均摊正确的,如果下传标记就会导致新建一堆节点增加很多节点,于是我们只能使用标记永久化的技巧,而标记永久化的标记需要满足可加性,但是赋值显然是不满足的,于是我们将赋值改成区间推平和区间加。区间推平就把整个区间删除即可。

code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
struct edge{
int v, next;
}edges[N << 1];
int head[N], idx;
void add_edge(int u, int v){
edges[++idx] = {v, head[u]};
head[u] = idx;
}
int n, m, k, d[N], w[N];
struct node{
int ls, rs, mx, mn, tag;
}t[N << 5];
int buc[N << 5], tot, tb;
int crenode(){return (tb ? buc[tb--] : (++tot));}
void delnode(int &id){
if(id == 0) return;
t[id] = {0, 0, 0, 0, 0}; buc[++tb] = id; id = 0;
}
struct Segtree{
int root;
#define mid (l + r >> 1)
void pushup(int o){
t[o].mx = max(t[t[o].ls].mx, t[t[o].rs].mx) + t[o].tag;
t[o].mn = min(t[t[o].ls].mn, t[t[o].rs].mn) + t[o].tag;
}
// merge two tree
void merge(int &o, int &rt, int l, int r){
if((!o) || (!rt)){o = o + rt; return;}
if(l == r){
t[o].tag += t[rt].tag; delnode(rt);
t[o].mx = t[o].mn = t[o].tag; return;
}
t[o].tag += t[rt].tag;
merge(t[o].ls, t[rt].ls, l, mid); merge(t[o].rs, t[rt].rs, mid + 1, r);
pushup(o); delnode(rt);
}
// set [s, e] = v (e <= v)
void modify(int &o, int l, int r, int s, int v){
if(!o) o = crenode(); //cout << o << " " << l << " " << r << " " << s << " " << v << "\n";
if(s <= l){
if(t[o].mx <= v){
delnode(t[o].ls); delnode(t[o].rs);
t[o].tag = t[o].mx = t[o].mn = v;
return;
}
else{
v -= t[o].tag;
if(t[t[o].ls].mn < v) modify(t[o].ls, l, mid, s, v);
if(t[t[o].rs].mn < v) modify(t[o].rs, mid + 1, r, s, v);
pushup(o); return;
}
}
v -= t[o].tag;
if(s <= mid) modify(t[o].ls, l, mid, s, v);
modify(t[o].rs, mid + 1, r, s, v);
pushup(o);
}
int qrysingle(int o, int l, int r, int x){
if(!o) return 0;
if(l == r) return t[o].tag;
if(x <= mid) return t[o].tag + qrysingle(t[o].ls, l, mid, x);
else return t[o].tag + qrysingle(t[o].rs, mid + 1, r, x);
}
int qryall(){return t[root].mx;}
}Seg[N];
void dfs(int u, int fa){
for(int i = head[u]; i; i = edges[i].next){
int v = edges[i].v; if(v == fa) continue;
dfs(v, u); Seg[u].merge(Seg[u].root, Seg[v].root, 1, k);
}
if(d[u]){
int s = w[u] + Seg[u].qrysingle(Seg[u].root, 1, k, d[u]);
Seg[u].modify(Seg[u].root, 1, k, d[u], s);
//cout << s << " " << Seg[u].qryall() << "\n";
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
for(int i = 2; i <= n; i++){
int x; cin >> x;
add_edge(x, i); add_edge(i, x);
}
for(int i = 1; i <= m; i++){
int v; cin >> v;
cin >> d[v] >> w[v];
}
dfs(1, 0); cout << Seg[1].qryall();
// system("pause");
return 0;
}

线段树分治

线段树分治可以将 修改-查询-删除 这类操作以一个 log 的代价变成 修改-查询-撤回。这种对于类似并查集的数据结构可以直接使用,方便操作。

先咕咕咕了。

猫树分治

对于一类可合并信息 U ,且两个 U1,U2 合并的代价比较大而将一个新元素加入 U 的代价不大的时候,可以考虑使用猫树分治来从而实现减少合并带来的时间复杂度开销。

猫树分治的具体思想就是对于一个区间 [l,r],处理被 [l,r] 完全包含的询问。不妨先处理 [l,mid][mid+1,r] 的子问题。然后考虑所有跨过中点 mid 的询问。不难发现每个跨过中点的询问都可以被一个以 mid1 为结尾的后缀和一个以 mid 为开头的前缀合并而成,先处理出以 mid1 为结尾的所有后缀的信息,还有前缀就可以了。

例题:

Problem: 给出长度为 n 的序列 ai 和固定模数 mq 个询问 [li,ri],每次查询 [li,ri] 中有多少个子序列满足和是 m 的倍数。其中 n,q2×105,m20

不难直接用线段树维护背包,时间复杂度 O(nm2logn),无法通过。

接下来考虑猫树分治,显然对于一个背包加入一个点的时间复杂度是 O(m) 的,然后最后合并前后缀信息时不需要求出背包的所有信息,只求出和模 m 等于 0 的即可,这里也是 O(m) 的。于是这道题就以 O(nmlogn) 的时间复杂度解决了。

code
#pragma GCC optimize(3, "Ofast", "inline")
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10, mod = 1e9 + 7;
int n, m, Q, a[N], ans[N];
struct node{
int val[20];
node(){val[0] = 1; for(int i = 1; i < 20; i++) val[i] = 0;}
}bg[N];
void ADD(int& x, int y){x = (x + y) % mod;}
void ADDbag(node& bg, int x){
node ret;
for(int i = 0; i < 20; i++) ret.val[i] = bg.val[i];
for(int i = 0; i < 20; i++) ADD(ret.val[(i + x) % m], bg.val[i]);
bg = ret;
}
struct query{
int l, r, id;
};
namespace cattree{
#define ls (o << 1)
#define rs (o << 1 | 1)
#define mid (l + r >> 1)
vector<query> vec[N << 2], Ql[N], Qr[N];
void clr(vector<query>& vvvv){vector<query> qwq; swap(qwq, vvvv);}
void ADDqry(int o, int l, int r, int s, int t, int id){
if((s <= mid && mid < t) || l == r){vec[o].push_back((query){s, t, id}); return;}
if(s <= mid) ADDqry(ls, l, mid, s, t, id);
else ADDqry(rs, mid + 1, r, s, t, id);
}
void solve(int o, int l, int r){
if(l == r){
for(int i = 0; i < vec[o].size(); i++) ans[vec[o][i].id] = 1 + (a[l] == 0);
return;
}
solve(ls, l, mid); solve(rs, mid + 1, r);
for(int i = 0; i < vec[o].size(); i++) Ql[vec[o][i].l].push_back(vec[o][i]), Qr[vec[o][i].r].push_back(vec[o][i]);
node lft, rht;
for(int i = mid; i >= l; i--){
ADDbag(lft, a[i]);
for(int j = 0; j < Ql[i].size(); j++) bg[Ql[i][j].id] = lft;
}
for(int i = mid + 1; i <= r; i++){
ADDbag(rht, a[i]);
for(int j = 0; j < Qr[i].size(); j++){
int id = Qr[i][j].id;
for(int k = 0; k < m; k++)
ADD(ans[id], bg[id].val[k] * rht.val[(m - k) % m] % mod);
}
}
for(int i = 0; i < vec[o].size(); i++) clr(Ql[vec[o][i].l]), clr(Qr[vec[o][i].r]);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m; for(int i = 1; i <= n; i++) cin >> a[i], a[i] = a[i] % m;
cin >> Q;
for(int i = 1; i <= Q; i++){
int l, r; cin >> l >> r;
cattree::ADDqry(1, 1, n, l, r, i);
}
cattree::solve(1, 1, n);
for(int i = 1; i <= Q; i++) cout << ans[i] << "\n";
return 0;
}
/**/

Seg-beats

Segbeats 主要用来解决一类区间和定值取 min / max 的操作。即每次对于给定的 [l,r]vlir,aimin(ai,v)。每次查询区间和之类的东西。

普通的线段树似乎好像不能快速维护这种东西。而我们伟大的吉如一老师给出了一个很优美的剪枝。(下面以区间取 max 为例,取 min 同理)对于一个线段树上的节点 o(对应区间 [lo,ro]),维护 [lo,ro] 中的最小值 mno严格次小值 seo。然后对区间 [l,r] 执行操作时,有以下三种情况:

  • mnov:无事发生。

  • mnov<seo:令 mnov 即可。

  • seov:直接对这个节点的左右儿子进行暴力递归更新。

这样的时间复杂度是对的吗?事实上,对于 O(n) 组询问次数,这个算法的时间复杂度是 O(nlogn) 的,即单次操作均摊 O(logn)。为什么呢?不难发现只有时间复杂度只跟出发最后一种情况的次数有关,而最后一种情况触发一定会导致这个区间中的数种类数减一。由于线段树每个节点所对应的区间的数的种类数的和是 O(nlogn) 级别的,从而最多触发 O(nlogn) 次 第三种情况。

Segbeats 结合区间加操作时间复杂度是 O(nlog2n) 的,但是我不会证明/fad。

矩阵原教旨主义

有的时候线段树上面有很多种类的标记和维护信息,人脑考虑不过来的时候,可以用一点常数来交换用脑量。

然后由于矩阵是半群信息,因此可以直接设计一个半群维护其中的有用信息即可。

左偏树

其实就是可合并堆但是非启发式合并。

考虑描述暴力合并的过程:

  • 当两个节点重合时,选择更优的节点作为根节点,然后随便选一个子树和另外一棵树递归合并。

  • 当只有一棵树不为空时,直接返回即可。

不难发现这个过程的复杂度实际上取决于第一种情况的出现次数。我们称满足第二种情况的节点为 “叶子”,然后定义一个节点的 “高度” 为节点与其子树内离其最近的叶子的距离,定义一棵树的高度为根节点的高度。

我们想要优化合并的复杂度,实际上就是要让树的高度尽量小。由于堆是二叉树,每次可以不管一个子树,把另一个子树合并即可。因此我们只要保证每个节点的某一个子树的高度尽量小即可,这里我们选择右子树。即让每个节点的 drsdls ,我们称这样的二叉树为 左偏树。于是 du=drs+1

每次合并我们将根节点的右子树与另一个根节点合并即可。每次合并完若不满足左偏树的性质。直接交换左右子树即可。

令这样一棵有 n 个节点的左偏树的距离为 T(n)。由于每次取两个节点中距离较小的,而较小的距离一定 n2,因此可以认为在最坏情况下 T(n)=T(n2)+1。解得 T(n)=O(logn)。因此时间复杂度为 O(logn) 的。

模板题代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 10;
struct node{
int ls, rs, val, id, dist, fa;
}tn[N];
bool operator <(struct node n1, struct node n2){
if(n1.val != n2.val) return n1.val < n2.val;
return n1.id < n2.id;
}
int n, m, vis[N];
int findfa(int x){return tn[x].fa = (tn[x].fa == x) ? x : findfa(tn[x].fa);}
void pushup(int o){
if(tn[tn[o].rs].dist > tn[tn[o].ls].dist) swap(tn[o].ls, tn[o].rs);
tn[o].dist = tn[tn[o].rs].dist + 1;
}
int merge(int x, int y){
if(!x || !y){tn[x + y].dist = 0; return x + y;}
if(tn[y] < tn[x]) swap(x, y);
tn[x].rs = merge(tn[x].rs, y);
pushup(x); return x;
}
void Mer(int x, int y){
if(vis[x] || vis[y]) return;
x = findfa(x); y = findfa(y);
//cout << x << " " << y << "\n";
if(x == y) return;
tn[x].fa = tn[y].fa = merge(x, y);
}
void del(int x){
if(vis[x]){cout << -1 << "\n"; return;}
x = findfa(x);
cout << tn[x].val << "\n"; //cout << x << " " << tn[x].ls << " " << tn[x].rs << "\n";
vis[x] = 1;
tn[tn[x].ls].fa = tn[tn[x].rs].fa = tn[x].fa = merge(tn[x].ls, tn[x].rs);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m; tn[0].dist = -1;
for(int i = 1; i <= n; i++) cin >> tn[i].val, tn[i].id = i, tn[i].fa = i;
while(m--){
int op; cin >> op;
if(op == 1){
int x, y; cin >> x >> y;
Mer(x, y);
} else{
int x, y; cin >> x;
del(x);
}
}
return 0;
}

本文作者:Little_corn

本文链接:https://www.cnblogs.com/little-corn/p/18365936

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Little_corn  阅读(18)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起