Tarjan离线算法求LCA小结

求LCA的两种做法不多解释,这篇文章有详细解释。

以前以为转RMQ法求LCA可以取代tarjan,实则不然,Tarjan不仅效率更高,而且可以维护一些路径上的统计量。于是又离线Tarjan法做了一些题目。

比较经典的是SPOJ 3978 Distance Query,是高效求解次小生成树的基础,详见扩展Tarjan求解树上两点路径上的最长边

poj 3728 The merchant

题意:有n做城市,每座城市有不同物价,给出Q个询问(a,b),问从a到b的路径上最大盈利(即:先在最小值买入,再在最大值卖出,只买卖一次)
解法:由于讯问的是路径上的性质,因此肯定要用到lca,从a到b的最大赢利方案分三种情况:a--lca之间买入a--lca之间卖出;a--lca之间买入lca--b之间卖出,lca--b之间买入,lca--b之间卖出,由于a到b与b到a情况不同,incidence对每个点记录4个值:up[v] 表示从v到目前的根的最大盈利,down[v] 从目前的根到v的最大盈利,Max[v]表示到目前的根的最大值,Min[v]表示到目前的根的最小值,在并查集合并和路径压缩时根据意义更改值。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;

public class Main{
	class node {
		int be, ne, val;
		node(int b, int e, int v) {
			be = b;
			ne = e;
			val = v;
		}
	}
	node buf[] = new node[200010], query[] = new node[200010],
			res[] = new node[100010];
	int Eb[] = new int[100010], lb, Eq[] = new int[100010], lq,
			Er[] = new int[100010], lres;
	void addres(int a, int b, int v) {
		res[lres] = new node(b, Er[a], v);
		Er[a] = lres++;
	}
	void addedge(int a, int b) {
		buf[lb] = new node(b, Eb[a], 0);
		Eb[a] = lb++;
		buf[lb] = new node(a, Eb[b], 0);
		Eb[b] = lb++;
	}
	void addquery(int a, int b, int v) {
		query[lq] = new node(b, Eq[a], v);
		Eq[a] = lq++;
		query[lq] = new node(a, Eq[b], v);
		Eq[b] = lq++;
	}
	int f[] = new int[50010], vis[] = new int[50010];
	int max[] = new int[50010], min[] = new int[50010], inf = 1 << 28;
	int up[]=new int[50010],down[]=new int[50010];
	int find(int x) {
		int temp = f[x];
		if (x != temp)
			f[x] = find(f[x]);
		up[x]=Math.max(Math.max(up[temp],up[x]), max[temp]-min[x]);
		down[x]=Math.max(Math.max(down[x],down[temp]),max[x]-min[temp]);
		max[x] = Math.max(max[x], max[temp]);
		min[x] = Math.min(min[x], min[temp]);
		return f[x];
	}
	void init() {
		lq = lb = lres = 0;
		for (int i = 1; i <= n; i++) {
			f[i] = i;
			Er[i] = Eb[i] = Eq[i] = -1;
			max[i] = -inf;
			min[i] = inf;
		}
	}
	void dfs(int a) {
		vis[a] = 1;
		// 处理所以与a有关且b已经访问过的查询
		for (int i = Eq[a]; i != -1; i = query[i].ne) {
			int b = query[i].be;
			if (vis[b] == 1) {
				int temp = find(b);
				addres(temp, a, i);
			}
		}
		// 处理子树 合并子树
		for (int i = Eb[a]; i != -1; i = buf[i].ne) {
			int b = buf[i].be;
			if (vis[b] == 0){
				dfs(b);
				f[b] = a;
				max[b]=Math.max(price[b], price[a]);
				min[b]=Math.min(price[b], price[a]);
				up[b]=price[a]-price[b];
				down[b]=price[b]-price[a];
				}
		}
		// 处理所有lca是a的查询
		for (int i = Er[a]; i != -1; i = res[i].ne) {
			int x = res[i].be;
			int k= res[i].val;
			int y = query[k].be;
			find(x);
			find(y);
			k=query[k].val;			
			if(s[k]==x){
				ans[k]=Math.max(up[x], down[y]);
				ans[k]=Math.max(ans[k], max[y]-min[x]);
			}
			else
			{
				ans[k]=Math.max(up[y], down[x]);
				ans[k]=Math.max(ans[k],max[x]-min[y]);
			}
		}
	}
	int n, m, price[] = new int[50010];
	int s[] = new int[50010],ans[] = new int[50010];
	StreamTokenizer in = new StreamTokenizer(new BufferedReader(
			new InputStreamReader(System.in)));

	int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}
	void run() throws IOException {
		n = nextInt();
		init();
		for (int i = 1; i <= n; i++)
			price[i] = nextInt();
		for (int i = 1; i < n; i++)
			addedge(nextInt(), nextInt());
		m = nextInt();
		for (int i = 1; i <= m; i++) {
			s[i] = nextInt();
			addquery(s[i], nextInt(), i);
		}
		dfs(1);
		for (int i = 1; i <= m; i++)
		 if(ans[i]>0)
			System.out.println(ans[i]);
		else
		    System.out.println(0);
	}
	public static void main(String[] args) throws IOException {
		new Main().run();
	}
}
poj3417 Network

题意:在一棵树上添加m条边,从树上选一条边删除,从m条中选一条删除,问使的图不连通的方案数。

分析:向树中任意两点间添加一条边都会形成环,删去一个环上的任意两条边都会是树不连通,因此若树上的边与多于一条的新边构成环删除后无法使图不连通,因此变为统计每条边被环覆盖的次数,设为点v的父边被环覆盖的次数为cnt[];对于,每条添加的边<a,b>都会是a到lca和b到lca之间的边被环覆盖一次,cnt[a]++,cnt[b]++,cnt[lca]-=2,然后树形dp统计cnt[p]=Sigma{cnt[son]};

由于离线tarjan在dfs过程中完成,因此可将树形dp的过程合并到求lca过程中,这是转rmq方法做不到的。

此题还要注意自环情况,不能添加双向查询(会被重复-2),被坑的好苦把模板里加了句return以示警戒

public class Main{
	int maxn=100010;
	class node {
		int be, ne, val;

		node(int b, int e, int v) {
			be = b;
			ne = e;
			val = v;
		}
	}
	node buf[] = new node[maxn*2], query[] = new node[maxn*2];
	int Eb[] = new int[maxn], lb, Eq[] = new int[maxn], lq;

	void addedge(int a, int b, int v) {
		buf[lb] = new node(b, Eb[a], v);
		Eb[a] = lb++;	
		buf[lb] = new node(a, Eb[b], v);
		Eb[b] = lb++;
	}

	void addquery(int a, int b, int v) {
		query[lq] = new node(b, Eq[a], v);
		Eq[a] = lq++;
   if(a==b)
      return;
		query[lq] = new node(a, Eq[b], v);
		Eq[b] = lq++;
	}

	int f[] = new int[maxn], vis[] = new int[maxn];

	int find(int x) {
		if (x != f[x])
			f[x] = find(f[x]);
		return f[x];
	}

	void dfs(int a) {
		vis[a] = 1;
		// 处理子树
		for (int i = Eq[a]; i != -1; i = query[i].ne) {
			int b = query[i].be;
			if (vis[b] == 1) {
				int temp = find(b);
				cnt[temp] -= 2;
			}
		}
		for (int i = Eb[a]; i != -1; i = buf[i].ne) {
			int b = buf[i].be;
			if (vis[b] == 0) {
				dfs(b);
				f[b]=a;
				cnt[a]+=cnt[b];
			}
		}
	}
	int n, m, cnt[] = new int[100005];//
	StreamTokenizer in = new StreamTokenizer(new BufferedReader(
			new InputStreamReader(System.in)));
	int nextInt() throws IOException {
		in.nextToken();
		return (int) in.nval;
	}

	void init() {
		lq = lb = 0;
		for (int i = 1; i <= n; i++) {
			f[i] = i;
			Eb[i] = Eq[i] = -1;
			cnt[i] = vis[i] = 0;
		}
	}

	void run() throws IOException {
		while (in.nextToken() != in.TT_EOF) {
			n = (int)in.nval;
			m = nextInt();
			init();
			int a, b;
			for (int i = 1; i < n; i++)
				addedge(nextInt(), nextInt(), 0);
			for (int i = 1; i <= m; i++) {
				a = nextInt();
				b = nextInt();
				cnt[a]++;
				cnt[b]++;
				addquery(a, b, i);
			}
			dfs(1);
			long ans = 0;		
			for (int i = 2; i <= n; i++){
				if (cnt[i] == 0)
					ans += m;
				if (cnt[i] == 1)
					ans++;
			}
			System.out.println(ans);
		}
	}

	public static void main(String[] args) throws IOException {
		new Main().run();
	}

}

ural 1699 Turning Turtles

题意:一个h*w的矩阵中有的点可用有的点不可用,可以向四个方向移动,保证可用的两点之间有且只有一条路径(树),问从a到b要拐几次弯。

解法:树上路径显然要lca,横向边用颜色1表示,纵向边用颜色0表示,设num[v]为点v到当前根要拐几次弯 color[v]表示点v父边的颜色 ,last[v]表示点v到当前根最后一条边的颜色。于是可以在合并和查询时维护num数组,虽然有点trivial。。。

代码:点击打开链接


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值