lca最近公共祖先与树上倍增。
https://vjudge.net/contest/295298#problem/A
lca 的题目
求任意两点的距离。
A题是在线算法,用st表rmq来实现。
https://blog.csdn.net/nameofcsdn/article/details/52230548
相当于先把整个树dfs一遍,记录整个dfs过程中的点(可重复,相当于dfs序,按顺序排好所有的点),并且记录每个点第一次被遍历到的得dfs序,
然后两个点的最近公共祖先就是第一次被遍历到的下标之间点深度最小的那个点。
1.最原始的lca(可以遍历到两点到lca之间所有的点,有些题目需要(后来发现并不需要,因为可以树上倍增))
int lca(int u, int v) { if(dep[u] < dep[v]) swap(u, v); while(dep[u] > dep[v]) { u = pa[u]; }//先让两个点到达同一深度。 while(u != v) { v = pa[v]; u = pa[u]; }//两个点一起向上走,知道走到同一点就是lca。
return u; }
在线做法复杂度nlogn。
#include<cstdio> #include<cstring> #include<algorithm>
#include<vector> using namespace std; const int maxn = 4e4 + 5; int t; int n, m; struct Node { int v, dis; Node(int v = 0, int dis = 0) : v(v), dis(dis) {}; }; vector<Node> g[maxn]; int rmq[maxn << 1];//记录深度。整个dfs过程中遍历点得到深度。 int dfsn[maxn << 1];//记录整个dfs过程中经过的点,总的大小为2 * n - 1; int fir[maxn];//每个点第一次被dfs到在dfsn数组中的位置。 int dis[maxn];//与根节点之间的的距离 int cnt = 0; void dfs(int u, int pre, int dep) { dfsn[++cnt] = u; rmq[cnt] = dep; fir[u] = cnt; for(int i = 0; i < g[u].size(); i++) { int v = g[u][i].v; if(v == pre) continue; dis[v] = dis[u] + g[u][i].dis; dfs(v, u, dep + 1); dfsn[++cnt] = u; rmq[cnt] = dep; } } int lg[maxn << 1]; int dp[maxn << 1][20]; void RMQ(int val) { lg[0] = -1; for(int i = 1; i <= val; i++) { lg[i] = ((i & (i - 1)) == 0) ? lg[i - 1] + 1 : lg[i - 1]; dp[i][0] = i; }//记录lg值。 for(int j = 1; j <= lg[val]; j++) { for(int i = 1; i + (1 << j) - 1 <= val; i++) { dp[i][j] = rmq[dp[i][j - 1] ] < rmq[dp[i + (1 << (j - 1))][j - 1] ] ? dp[i][j - 1] : dp[i + (1 << (j - 1)) ][j - 1]; } }//rmq得到最小深度的那个点的下标。 } int query(int a, int b) { if(a > b) swap(a, b); int k = lg[b - a + 1]; return rmq[dp[a][k] ] < rmq[dp[b - (1 << k) + 1][k] ] ? dp[a][k] : dp[b - (1 << k) + 1 ][k]; } int lca_query(int u, int v) { return dfsn[query(fir[u], fir[v])];//得到深度最小的那个点对应的节点编号。 } int main() { scanf("%d", &t); while(t--) { scanf("%d%d", &n, &m); for(int i = 1; i <= n; i++) { g[i].clear(); dis[i] = 0; } int u, v, w; for(int i = 1; i < n; i++) { scanf("%d%d%d", &u, &v, &w); g[u].push_back(Node(v, w)); g[v].push_back(Node(u, w)); } cnt = 0; dfs(1, 0, 0); RMQ(2 * n - 1); while(m--) { scanf("%d%d", &u, &v); int val = lca_query(u, v); printf("%d\n", dis[u] + dis[v] - 2 * dis[val]); } } return 0; }
https://www.cnblogs.com/JVxie/p/4854719.html离线做法tajan。
https://vjudge.net/contest/295298#problem/B 离线lca算法
先把每个点的父节点记为自己,然后dfs遍历,直到回溯的时候才更新该节点的父节点。
对于查询的两点,互相记录对方的编号以及这是第一次查询的编号,然后当其中某个点被遍历到,但另外一个点没有被遍历到,那个继续dfs,如果遍历一个点,他对应的那个点已经遍历过了,那个他们的
最近公共祖先就是之前那个被遍历过的点的父节点,(画个图想想,那个父节点就是他们的最近公共祖先)
离线做法n+q。
#include<cstdio> #include<cstring> #include<algorithm> #include<vector> using namespace std; const int maxn = 1e5 + 5; typedef pair<int, int> pii; vector<pii> g[maxn], que[maxn]; int t; int n, m, k; int ans[maxn], vis[maxn], pa[maxn], res[maxn], dis[maxn]; //ans数组:每个点的祖先节点编号,pa数组:每个点的父节点的编号。 int fid(int x) { if(x == pa[x]) return x; pa[x] = fid(pa[x]); return pa[x]; } void unio(int x, int y) { int u = fid(x); int v = fid(y); if(u != v) pa[u] = v; } void lca(int u, int fa) { ans[u] = u; for(int i = 0; i < g[u].size(); i++) { int v = g[u][i].first; if(v == fa) continue; dis[v] = dis[u] + g[u][i].second; lca(v, u); unio(u, v);//连通起来,因为这个是先dfs下去回溯之后的操作,所以这个操作之后只是u的子节点与u组成的图。 ans[fid(u) ] = u;//记录整个子图的的祖先,相当于这个子树节点的祖先就都是u,儿子们得到祖先的时候也要先fid自己父节点一下。 } vis[u] = 1; for(int i = 0; i < que[u].size(); i++) { int v = que[u][i].first; int w = que[u][i].second; if(vis[v]) res[w] = dis[u] + dis[v] - 2 * dis[ans[fid(v)] ]; } } int main() { while(~scanf("%d%d", &n, &m)) { int u, v, w; char ch[3]; for(int i = 1; i <= n; i++) { pa[i] = i; ans[i] = 0; vis[i] = 0; res[i] = 0; g[i].clear(); que[i].clear(); } for(int i = 1; i <= m; i++) { scanf("%d%d%d%s", &u, &v, &w, ch); g[u].push_back(pii(v, w)); g[v].push_back(pii(u, w)); } scanf("%d", &k); for(int i = 1; i <= k; i++) { scanf("%d%d", &u, &v); que[u].push_back(pii(v, i)); que[v].push_back(pii(u, i)); } dis[1] = 0; lca(1, 0); for(int i = 1; i <= k; i++) printf("%d\n", res[i]); } return 0; }
题目
给你N个点的无向连通图,图中有M条边,第j条边的长度为: djdj现在有 K个询问。每个询问的格式是:A B,表示询问从A点走到B点的所有路径中,最长的边最小值是多少?
数据范围
50% 1<=N,M<=3000
其中30% K<=5000
100% 1 <= N <= 15,000 1 <= M <= 30,000 1 <= dj<= 1,000,000,000 1 <= K <= 20,000
分析
从题目看,有一句话很重要:表示询问从A点走到B点的所有路径中,最长的边最小值是多少?”所有“这词提醒了我们这些路径中有很多是无用的,是浪费的。我们会发现这题要求的条件也很特殊,也就是只要是连通的路径,就可以取里面的最大值了。再者,既然最大值最小,那么我们可以先使这些选出来的路径尽量的小,且可以使他们连通就可以了,这就变成了一棵树,那么也就是说这n个点中我们可以先取n-1条边,使得这n-1条边可以代表这些其他边,还有要最小,自然而然就是最小生成树了!做完最小生成树后这个图就变成一棵树,然后再处理这棵树每两个点之间的最大边的值,也就是用lca+rmql处理,便可以使复杂度变成Nlogn的。
#include<cstdio> #include<cstdlib> #include<cstring> #include<iostream> #include<algorithm> using namespace std; #define N 15000+10 #define M 40000+10 struct Etype{ int u,v,d; }E[M]; const int MX=15; int Node[M],Next[M],D[M],Head[N],tot; int f[N]; int Fa[N][20],V[N][20],H[N]; int n,m,Q; bool cmp(Etype a,Etype b) {return a.d < b.d ;} void Swap(int *a,int *b) {int t=*a; *a=*b; *b=t;} void link(int u,int v,int w) { Node[++tot]=v; D[tot]=w; Next[tot]=Head[u]; Head[u]=tot; } int getfather(int x) { if(f[x]==x) return x; return f[x]=getfather(f[x]); } void DFS(int F,int x,int depth) { H[x]=depth; for(int p=Head[x];p;p=Next[p]) { if(Node[p]==F) continue; Fa[Node[p]][0]=x; V[Node[p]][0]=D[p]; DFS(x,Node[p],depth+1); } } int Lca(int x,int y) { int ans=0; if(H[x]<H[y]) swap(x,y); for(int i=MX;i>=0;i--) if(H[Fa[x][i]]>=H[y]) { ans=max(ans,V[x][i]); x=Fa[x][i]; }
//让x,y达到同一个深度,如果倍增后还在另一个点的下面,那么继续倍增,并且更新答案,因为这些点肯定是包含在路径上的 if(x==y) return ans; for(int i=MX;i>=0;i--) { if(Fa[x][i]!=Fa[y][i]) { ans=max(ans,V[x][i]); ans=max(ans,V[y][i]); x=Fa[x][i]; y=Fa[y][i]; } }//在到达统一深度之后一起往上走,如果倍增后的点相同就说明这个点是lca的祖先节点或他自己,不能更新答案,只有倍增后的点不相同才能更新答案。 ans=max(ans,V[x][0]); ans=max(ans,V[y][0]); return ans; } int main() { scanf("%d%d%d",&n,&m,&Q); for(int i=1;i<=n;i++) f[i]=i; for(int i=1;i<=m;i++) { scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].d); } sort(E+1,E+m+1,cmp);
//得到一个最小生成树。 for(int i=1;i<=m;i++) { int f1=getfather(E[i].u); int f2=getfather(E[i].v); if(f1!=f2) { f[f2]=f1; link(E[i].u,E[i].v,E[i].d); link(E[i].v,E[i].u,E[i].d); } } DFS(0,1,1); for(int k=1;k<=MX;k++) { for(int i=1;i<=n;i++) { if(!Fa[Fa[i][k-1]][k-1]) continue; Fa[i][k]=Fa[Fa[i][k-1]][k-1]; V[i][k]=max(V[i][k-1],V[Fa[i][k-1]][k-1]); } } for(int i=1;i<=Q;i++) { int u,v; scanf("%d%d",&u,&v); printf("%d\n",Lca(u,v)); } return 0; }
【JZOJ2753】树(tree)
在这个问题中,给定一个值S和一棵树。在树的每个节点有一个正整数,问有多少条路径的节点总和达到S。路径中节点的深度必须是升序的。假设节点1是根节点,根的深度是0,它的儿子节点的深度为1。路径不必一定从根节点开始。 第一行是两个整数N和S,其中N是树的节点数。 第二行是N个正整数,第i个整数表示节点i的正整数。 接下来的N-1行每行是2个整数x和y,表示y是x的儿子。 输出路径节点总和为S的路径数量。 Sample Input 3 3 1 2 3 1 2 1 3 Sample Output 2 Data Constraint Hint 对于30%数据,N≤100; 对于60%数据,N≤1000; 对于100%数据,N≤100000,所有权值以及S都不超过1000。
正解即为树上倍增+二分
首先还是一样,用logn的时间预处理anc数组
至于权值,你可以顺便维护dis数组,但为何不用简单的前缀和呢?
剩下来的就比较简单啦
枚举节点i ,二分一个mid表示i 的第mid个祖先.
然后通过anc数组用O(log2n)
的时间求出i的第mid个祖先是哪个节点 (设为是第k号)
若pre[i]−pre[k]>s 则right=mid−1 pre[i]−pre[k]<s则left=mid+1
如果 =s就刚好找到了
此时累加答案即可 时间复杂度为O(nlogn * logn)
#include<bits/stdc++.h> #define MAXN 100001 using namespace std; int last[MAXN],next[MAXN],tov[MAXN]; int a[MAXN],value[MAXN],depth[MAXN]; int f[MAXN][21]; int n,s,tot,ans; void insert(int x,int y) { next[++tot]=last[x]; last[x]=tot; tov[tot]=y; } void dfs(int x) { for (int i=last[x];i;i=next[i]) { int j=tov[i]; value[j]=value[x]+a[j]; f[j][0]=x; depth[j]=depth[x]+1; dfs(j); } } int find(int x,int k) { int t=0; while (k) { if(k&1)x=f[x][t]; t++;k/=2; }//类似于快速幂的方法。往上倍增求解。 return x; } bool judge(int x) { int left=0,right=depth[x]; while (left<=right) { int mid=(left+right)/2,temp=find(x,mid); if (value[x]-value[temp]==s) { return 1; } else { if (value[x]-value[temp]>s) { right=mid-1; } else { left=mid+1; } } } return 0; } int main() { scanf("%d%d",&n,&s); for (int i=1;i<=n;i++) { scanf("%d",&a[i]); } for (int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); insert(x,y); } depth[1]=1,value[1]=a[1]; dfs(1); for (int j=1;j<=floor(log(n)/log(2));j++) { for (int i=1;i<=n;i++) { f[i][j]=f[f[i][j-1]][j-1]; } } for (int i=1;i<=n;i++) { if (judge(i))ans++; } printf("%d\n",ans); return 0; }