求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。。。
代码:点击打开链接