线段树优化建图学习笔记
前言
不是什么新奇的算法,只能算是小 trick 或者好的思想。
线段树能较为高效地处理区间问题,所以当建图时出现一个点连向一个区间等类似操作时,可以用线段树优化。
主要思想
给定操作:
- \((1,u,l,r)\) 表示将 \(u\) 向区间 \([l,r]\) 中的点连边。
- \((2,u,l,r)\) 表示将区间 \([l,r]\) 中的点向 \(u\) 连边。
直接模拟的话,操作 \(n\) 次的复杂度时 \(O(n^2)\) 的。
利用线段树,可以用一个节点承载整个区间,优化了建图的时间。
首先需要建立两棵线段树,分别称为 “入树” 和 ”出树“。(根据树边的方向)
入树中,线段树的父亲节点指向儿子节点,出树则相反。
也就是说,”出树“ 中的区间可以到达更小的区间,而 ”入树“ 则可以到达更大的区间。
这和上述两个操作相联系:
- 点指向区间,包含这个点的区间也都能指向那个区间。
- 区间指向点,被这个区间包含的子区间也都同样指向那个点。
同时可以发现,通过线段树建图时,边的某个端点总是叶子节点。
而且棵树的叶子节点是等价的,为了方便,直接可以令两棵树共叶子。
简单分析一下时空复杂度。
时间显然是线段树操作的标准时间 \(O(n\log n)\)。
空间显然主要是线段树的存储,同样是 \(O(n\log n)\)。
而普通模拟是时间 \(O(n^2)\),空间 \(O(n)\) 的,所以这个算法可以理解为空间换时间。
简单例题
绝对经典。
按照上述方法进行建图,再套一个最短路模板即可。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
const int M = N * 8;
const LL INF = 0x3F3F3F3F3F3F3F3F;
int n, m, s, cnt, tot, lc[M], rc[M], head[M];
LL dis[M];
bool vis[M];
struct Edge{int nxt, to, val;} ed[N * 20];
priority_queue<pair<LL, int> > q;
int read(){
int x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
return x * f;
}
void add(int u, int v, int w){
ed[++ cnt] = (Edge){head[u], v, w};
head[u] = cnt;
}
int build1(int l, int r){
if(l == r) return l;
int p = ++ tot, mid = (l + r) >> 1;
lc[p] = build1(l, mid);
rc[p] = build1(mid + 1, r);
add(p, lc[p], 0); add(p, rc[p], 0);
return p;
}
int build2(int l, int r){
if(l == r) return l;
int p = ++ tot, mid = (l + r) >> 1;
lc[p] = build2(l, mid);
rc[p] = build2(mid + 1, r);
add(lc[p], p, 0); add(rc[p], p, 0);
return p;
}
void update(int p, int l, int r, int L, int R, int u, int w, int opt){
if(L <= l && r <= R){
if(opt == 2) add(u, p, w); else add(p, u, w);
return;
}
int mid = (l + r) >> 1;
if(L <= mid) update(lc[p], l, mid, L, R, u, w, opt);
if(R > mid) update(rc[p], mid + 1, r, L, R, u, w, opt);
}
void Dijkstra(){
for(int i = 1; i <= tot; i ++) dis[i] = INF;
dis[s] = 0;
q.push(make_pair(0, s));
while(!q.empty()){
int u = q.top().second; q.pop();
if(vis[u]) continue;
vis[u] = true;
for(int i = head[u]; i; i = ed[i].nxt){
int v = ed[i].to, w = ed[i].val;
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
q.push(make_pair(-dis[v], v));
}
}
}
}
int main(){
n = read(), m = read(), s = read();
tot = n;
int rt1 = build1(1, n);
int rt2 = build2(1, n);
for(int i = 1; i <= m; i ++){
int opt = read();
if(opt == 1){
int u = read(), v = read(), w = read();
add(u, v, w);
}
else{
int u = read(), l = read(), r = read(), w = read();
update(opt == 2 ? rt1 : rt2, 1, n, l, r, u, w, opt);
}
}
Dijkstra();
for(int i = 1; i <= n; i ++)
printf("%lld ", dis[i] == INF ? -1 : dis[i]);
puts("");
return 0;
}
线段树优化建图不是最佳方法,但是绝对可以锻炼码力。
首先 native 的想法是将能引爆的炸弹连边,tarjan 缩点后在 DAG 上简单 DP。
但是总边数高达 \(n^2\),所以时空全都无法承受。
这时候直接上本算法,然后正常缩点 & DP,至于是 dfs 还是 topo 就随便了,毕竟都是 \(O(n)\)。
这里没有区间到点的连线,所以一棵线段树即可,但是需要同时记录区间代表的左右端点,用来 DP。
还要去除重边和自环。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 500010;
const int M = N * 8;
const LL INF = 0x3F3F3F3F3F3F3F3F;
const LL MOD = 1e9 + 7;
int n, tot, lc[M], rc[M], ls[M], rs[M];
int scc, top, num, L[M], R[M];
int col[M], low[M], dfn[M], s[M];
LL a[N], r[N];
bool vis[M];
vector<int> mp[M], G[M];
LL read(){
LL x = 0, f = 1; char c = getchar();
while(c < '0' || c > '9') f = (c == '-') ? -1 : 1, c = getchar();
while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
return x * f;
}
int build(int l, int r){
if(l == r) return l;
int p = ++ tot, mid = (l + r) >> 1;
ls[p] = l, rs[p] = r;
lc[p] = build(l, mid);
rc[p] = build(mid + 1, r);
mp[p].push_back(lc[p]);
mp[p].push_back(rc[p]);
return p;
}
void update(int p, int l, int r, int L, int R, int u){
if(L <= l && r <= R){
if(u == p) return;
mp[u].push_back(p);
return;
}
int mid = (l + r) >> 1;
if(L <= mid) update(lc[p], l, mid, L, R, u);
if(R > mid) update(rc[p], mid + 1, r, L, R, u);
}
void tarjan(int u){
dfn[u] = low[u] = ++ num;
s[++ top] = u;
for(int i = 0; i < (int)mp[u].size(); i ++){
int v = mp[u][i];
if(!dfn[v])
tarjan(v), low[u] = min(low[u], low[v]);
else if(!col[v])
low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u]){
int i = ++ scc;
int v;
do{
v = s[top --];
col[v] = i;
L[i] = min(L[i], ls[v]);
R[i] = max(R[i], rs[v]);
}while(v != u);
}
}
void dfs(int u){
vis[u] = true;
for(int i = 0; i < (int) G[u].size(); i ++){
int v = G[u][i];
if(!vis[v]) dfs(v);
L[u] = min(L[u], L[v]);
R[u] = max(R[u], R[v]);
}
}
LL Get(int u){return (R[col[u]] - L[col[u]] + 1);}
int main(){
n = read();
tot = n;
int root = build(1, n);
for(int i = 1; i <= n; i ++){
a[i] = read(), r[i] = read();
ls[i] = rs[i] = i;
}
memset(L, 0x3f, sizeof(L));
a[n + 1] = INF;
for(int i = 1; i <= n; i ++){
if(!r[i]) continue;
int pl = lower_bound(a + 1, a + n + 1, a[i] - r[i]) - a;
int pr = upper_bound(a + 1, a + n + 1, a[i] + r[i]) - a - 1;
update(root, 1, n, pl, pr, i);
}
tarjan(root);
for(int u = 1; u <= tot; u ++)
for(int i = 0, v; i < (int) mp[u].size(); i ++)
if(col[(v = mp[u][i])] != col[u])
G[col[u]].push_back(col[v]);
for(int i = 1; i <= scc; i ++){
sort(G[i].begin(), G[i].end());
unique(G[i].begin(), G[i].end());
}
for(int i = 1; i <= scc; i ++)
if(!vis[i]) dfs(i);
LL ans = 0;
for(int i = 1; i <= n; i ++)
ans = (ans + Get(i) * i) % MOD;
printf("%lld\n", ans);
return 0;
}
完结撒花