线段树优化建图学习笔记

前言

不是什么新奇的算法,只能算是小 trick 或者好的思想。

线段树能较为高效地处理区间问题,所以当建图时出现一个点连向一个区间等类似操作时,可以用线段树优化。

主要思想

给定操作:

  1. \((1,u,l,r)\) 表示将 \(u\) 向区间 \([l,r]\) 中的点连边。
  2. \((2,u,l,r)\) 表示将区间 \([l,r]\) 中的点向 \(u\) 连边。

直接模拟的话,操作 \(n\) 次的复杂度时 \(O(n^2)\) 的。

利用线段树,可以用一个节点承载整个区间,优化了建图的时间。

首先需要建立两棵线段树,分别称为 “入树” 和 ”出树“。(根据树边的方向)

入树中,线段树的父亲节点指向儿子节点,出树则相反。

也就是说,”出树“ 中的区间可以到达更小的区间,而 ”入树“ 则可以到达更大的区间。

这和上述两个操作相联系:

  1. 点指向区间,包含这个点的区间也都能指向那个区间。
  2. 区间指向点,被这个区间包含的子区间也都同样指向那个点。

同时可以发现,通过线段树建图时,边的某个端点总是叶子节点。

而且棵树的叶子节点是等价的,为了方便,直接可以令两棵树共叶子。

简单分析一下时空复杂度。

时间显然是线段树操作的标准时间 \(O(n\log n)\)

空间显然主要是线段树的存储,同样是 \(O(n\log n)\)

而普通模拟是时间 \(O(n^2)\),空间 \(O(n)\) 的,所以这个算法可以理解为空间换时间。

简单例题

CF786B Legacy

绝对经典。

按照上述方法进行建图,再套一个最短路模板即可。

#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;
}

完结撒花

posted @ 2021-05-27 13:34  LPF'sBlog  阅读(86)  评论(0编辑  收藏  举报