[CF 191C]Fools and Roads[LCA Tarjan算法][LCA 与 RMQ问题的转化][LCA ST算法]
参考:
1. 郭华阳 - 算法合集之《RMQ与LCA问题》. 讲得很清楚!
2. http://www.cnblogs.com/lazycal/archive/2012/08/11/2633486.html
3. 代码来源yejinru
题意:
有一棵树, 按照顺序给出每条边, 再给出若干对点, 这两点之间的唯一的路( Simple path )上边权加1. 当所有对点处理完后, 按照边的输入顺序输出每条边的权.
思路:
LCA问题.
最近公共祖先(Least Common Ancestors)LCA简介:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。——百度百科
问题等效于: 若点为( x, y ), 且有 LCA( x, y ) = z. 则 x, z y, z 之间的边权均+1. 区间+1可以用差分数列的思想, 令dis[ x ] 为 x 点与其子节点之间的差值. x 点对应连接 x 和其父节点的边.
解法一:
Tarjan算法,离线操作.
建树的方法除了用vector以外, 还可以换为链式前向星. 不过还是vector顺手些~
p.s. 还是 [ 理论: 国家集训队论文 / ppt + 实践: 学长的模板 ] 这样的tempo比较靠谱~
- /*
- 在树的任意两点所在的所有树的边权加一,输出最后所有的边权
- LCA。我们可以离线操作,把所有的询问直接求出LCA,然后用数组
- dis[x] ++ , dis[y]++,
- dis[LCA(x,y)] -= 2
- 最后DFS统计一下答案
- */
- #include <set>
- #include <map>
- #include <list>
- #include <cmath>
- #include <queue>
- #include <stack>
- #include <string>
- #include <vector>
- #include <cstdio>
- #include <cstring>
- #include <iostream>
- #include <algorithm>
- using namespace std;
- typedef long long ll;
- typedef unsigned long long ull;
- #define debug puts("here")
- #define rep(i,n) for(int i=0;i<n;i++)
- #define rep1(i,n) for(int i=1;i<=n;i++)
- #define REP(i,a,b) for(int i=a;i<=b;i++)
- #define foreach(i,vec) for(unsigned i=0;i<vec.size();i++)
- #define pb push_back
- #define RD(n) scanf("%d",&n)
- #define RD2(x,y) scanf("%d%d",&x,&y)
- #define RD3(x,y,z) scanf("%d%d%d",&x,&y,&z)
- #define RD4(x,y,z,w) scanf("%d%d%d%d",&x,&y,&z,&w)
- #define All(vec) vec.begin(),vec.end()
- #define MP make_pair
- #define PII pair<int,int>
- #define PQ priority_queue
- #define cmax(x,y) x = max(x,y)
- #define cmin(x,y) x = min(x,y)
- #define fir first
- #define sec second
- /******** program ********************/
- const int MAXN = 2e5+5;
- vector< PII > edge[MAXN];
- vector< PII > ask[MAXN];
- int fa[MAXN];
- bool use[MAXN];
- int pa[MAXN];
- int n,m;
- int dis[MAXN];
- int a[MAXN],b[MAXN];
- int ans[MAXN];
- int find_set(int x){///并查集找父节点路径压缩
- if(fa[x]!=x)
- fa[x] = find_set(fa[x]);
- return fa[x];
- }
- /*********Tarjan算法的核心思想*********/
- void lca(int x,int f){
- //cout<<"x = "<<x<<endl;
- use[x] = true;///已经遍历
- fa[x] = x;///该节点插入并查集,并且自己为一个独立的集合
- foreach(i,ask[x]){///遍历有关节点x的所有询问
- int y = ask[x][i].first;///目标节点
- int id = ask[x][i].second;///输入的顺序
- //cout<<"dsadsa = "<<x<<" "<<y<<" "<<id<<endl;
- if(use[y])
- pa[id] = find_set(y);///如果已经遍历,那么答案就是它的父节点
- }
- foreach(i,edge[x]){///遍历所有儿子
- int y = edge[x][i].first;
- //cout<<"y = "<<y<<endl;
- if(use[y])continue;
- lca(y,x);///如果当前正在访问该子树的根,那么此前已访问过的节点的父节点必然是
- ///从根节点到该节点的唯一路上(*).这本身也是一个dfs
- fa[y] = x;///如果已经回溯,即完成了上一条语句对y节点子树的遍历,
- ///退出之后将y节点的父节点设为x,也就保证了这一步退出之后(*)条件仍然成立.
- }
- }
- void dfs(int x,int f){
- foreach(i,edge[x]){
- int y = edge[x][i].first;
- if(y==f)continue;
- dfs(y,x);
- int id = edge[x][i].second;
- dis[x] += dis[y];///类似于差分数列的思想
- ans[id] = dis[y];///从叶子节点向根统计结果
- }
- }
- int main(){
- //#ifndef ONLINE_JUDGE
- // freopen("sum.in","r",stdin);
- //freopen("sum.out","w",stdout);
- //#endif
- while(cin>>n){
- int x,y;
- rep(i,MAXN){//清零
- edge[i].clear();
- ask[i].clear();
- }
- memset(use,false,sizeof(use));//清零初始化
- REP(i,2,n){
- RD2(x,y);
- edge[x].pb( MP(y,i) );
- edge[y].pb( MP(x,i) );
- }
- RD(m);
- rep1(i,m){
- RD2(x,y);
- a[i] = x;//第i条路的两端点
- b[i] = y;
- ask[x].pb( MP(y,i) );//注意ask也是双向插入的
- ask[y].pb( MP(x,i) );
- }
- lca(1,0);
- memset(dis,0,sizeof(dis));
- rep1(i,m){
- x = a[i];
- y = b[i];
- dis[x] ++;///类似于差分数列的思想
- dis[y] ++;
- dis[ pa[i] ] -= 2;///保证增量向上至多只影响到lca),lca以上的路不受下面子树影响
- //cout<<x<<" "<<y<<" "<<pa[i]<<endl;
- }
- dfs(1,0);
- REP(i,2,n)
- printf("%d ",ans[i]);
- puts("");
- }
- return 0;
- }
自己敲一遍:
- #include <cstdio>
- #include <vector>
- #include <cstring>
- #include <utility>
- using namespace std;
- const int MAXN = 2e5+5;
- int n,m,x,y;
- vector<pair<int, int> > query[MAXN],edge[MAXN<<1];
- int a[MAXN],b[MAXN],dif[MAXN],ans[MAXN],lca[MAXN],fa[MAXN];
- bool vis[MAXN];
- int find_set(int x)
- {
- if(fa[x]!=x)
- fa[x] = find_set(fa[x]);
- return fa[x];
- }
- void LCA_Tarjan(int u)
- {
- vis[u] = true;
- fa[u] = u;
- for(size_t i=0;i<query[u].size();i++)
- {
- int v = query[u][i].first;
- int id = query[u][i].second;
- if(vis[v])
- lca[id] = find_set(v);///注意这里的id是询问的id,恰好对应一对节点
- }
- for(size_t i=0;i<edge[u].size();i++)
- {
- int v = edge[u][i].first;
- if(vis[v]) continue;
- LCA_Tarjan(v);
- fa[v] = u;
- }
- }
- void solve()
- {
- for(int i=0;i<m;i++)
- {
- x = a[i];
- y = b[i];
- dif[x]++; dif[y]++;///差分数列
- dif[lca[i]] -= 2;
- }
- }
- void dfs(int u, int f)
- {
- for(size_t i=0;i<edge[u].size();i++)
- {
- int v = edge[u][i].first;
- if(v==f) continue;
- dfs(v, u);
- int id = edge[u][i].second;
- dif[u] += dif[v];
- ans[id] = dif[v];///这里的id是边的序号
- }
- }
- int main()
- {
- scanf("%d",&n);
- for(int i=1;i<n;i++)
- {
- scanf("%d %d",&x,&y);
- edge[x].push_back(make_pair(y,i));
- edge[y].push_back(make_pair(x,i));
- }
- scanf("%d",&m);
- for(int i=0;i<m;i++)
- {
- scanf("%d %d",&x,&y);
- a[i] = x;
- b[i] = y;///记下第i个询问对应的两端点
- query[x].push_back(make_pair(y,i));
- query[y].push_back(make_pair(x,i));
- }
- //选谁作为根是无所谓的,不妨就选1号为根.
- LCA_Tarjan(1);
- solve();
- dfs(1,0);
- for(int i=1;i<n;i++)
- printf("%d%c",ans[i],i==n-1?'\n':' ');
- }
解法二:
ST算法( Sparse Table ). ST算法是解决RMQ问题的一种在线算法.
RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。——百度百科
LCA与RMQ的相互转化
首先说明LCA问题与RMQ问题为何可以相互转化.
/************分割线************/
ST算法
ST算法是基于倍增思想设计的O(NlogN) - O(1)在线算法.
简单来说:
利用 dp 预处理出每一段的最值,对于每个询问,只要O(1)的时间便能得出答案。
dp 如下:dp[ i ][ j ]表示从第i个位置开始的 2^j 个数中的最小值。转移方程如下:
dp[i][j]=min(dp[i][j-1],dp[i+1<<(j-1)][j-1])
这样,对于每个查询 x, y ( x < y )(在第 x 个位置到第 y 个位置的最值),答案就是
min(dp[x][j],dp[y-(1<<j)+1][j])(其中j是(int)log2(y-x+1))
∵[x,x+(1<<j)]与[y-(1<<j),y]都是[x,y]的子区间且[x,x+1<<j]∪[y-1<<j]=[x,y]。
至此RMQ问题就解决了,时间复杂度为O( nlogn )+O(1)* q(其中 q 为询问数量)
求解LCA
求LCA的其中一种算法便是转换成RMQ,利用ST算法求解。
具体做法如下:将这棵树用深度优先遍历,每次遍历一个点(包括回溯)都添加进数组里面。找到所询问的点第一次出现的位置,两个位置所夹的点中深度最小的即为所求。
- /*
- 在树的任意两点所在的所有树的边权加一,输出最后所有的边权
- LCA。我们可以在线操作,用数组
- dis[x] ++ , dis[y]++,
- dis[LCA(x,y)] -= 2
- 最后DFS统计一下答案
- */
- #include <set>
- #include <map>
- #include <list>
- #include <cmath>
- #include <queue>
- #include <stack>
- #include <string>
- #include <vector>
- #include <cstdio>
- #include <cstring>
- #include <iostream>
- #include <algorithm>
- using namespace std;
- typedef long long ll;
- typedef unsigned long long ull;
- #define debug puts("here")
- #define rep(i,n) for(int i=0;i<n;i++)
- #define rep1(i,n) for(int i=1;i<=n;i++)
- #define REP(i,a,b) for(int i=a;i<=b;i++)
- #define foreach(i,vec) for(unsigned i=0;i<vec.size();i++)
- #define pb push_back
- #define RD(n) scanf("%d",&n)
- #define RD2(x,y) scanf("%d%d",&x,&y)
- #define RD3(x,y,z) scanf("%d%d%d",&x,&y,&z)
- #define RD4(x,y,z,w) scanf("%d%d%d%d",&x,&y,&z,&w)
- #define All(vec) vec.begin(),vec.end()
- #define MP make_pair
- #define PII pair<int,int>
- #define PQ priority_queue
- #define cmax(x,y) x = max(x,y)
- #define cmin(x,y) x = min(x,y)
- #define fir first
- #define sec second
- /******** program ********************/
- const int MAXN = 200005;
- int n;
- vector< PII > edge[MAXN];
- int dp[MAXN][20];
- int dis[MAXN];
- int depth;
- int b[MAXN],bn; //深度序列
- int f[MAXN]; //对应深度序列中的结点编号
- int p[MAXN]; //结点在深度序列中的首位置
- int ans[MAXN];
- void dfs(int x,int fa){
- int tmp = ++ depth;
- ///没有必要保证兄弟的深度相等,
- ///只要满足父节点深度小于儿子即可.
- ///而且是不能的!如果兄弟深度相等的话,
- ///就无法依据最近公共祖先的深度确定是哪一个节点了!
- ///这样令兄弟的深度有所不同就使得
- ///找到的"最小深度"可以直接对应节点
- ///(每个节点的深度都是不同的)
- b[++bn] = tmp;
- f[tmp] = x;
- p[x] = bn;
- foreach(i,edge[x]){
- int y = edge[x][i].first;
- if (y==fa) continue;
- dfs(y,x);
- b[++bn] = tmp;
- }
- }
- void rmq_init(int n){ //以深度序列做rmq
- rep1(i,n)
- dp[i][0]=b[i];
- int m = floor(log(n*1.0)/log(2.0));
- ///向下取整,因为向上取整并没有完整的2^m长度的区间
- rep1(j,m)
- for (int i=1; i<=n-(1<<j)+1; i++)///非递归的方式
- dp[i][j] = min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
- }
- int rmq(int l,int r){
- int k = floor(log((r-l+1)*1.0)/log(2.0));
- return min( dp[l][k] , dp[r-(1<<k)+1][k] );
- }
- int lca(int a,int b){
- if (p[a]>p[b])
- swap(a,b);
- return f[ rmq(p[a],p[b]) ];
- }
- void qq(int x,int f){///dfs求ans
- foreach(i,edge[x]){
- int y = edge[x][i].first;
- if(y==f)continue;
- qq(y,x);
- int id = edge[x][i].second;
- dis[x] += dis[y];
- ans[id] = dis[y];
- }
- }
- int main(){
- while(cin>>n){
- rep(i,MAXN)
- edge[i].clear();
- depth = bn = 0;
- int x,y;
- REP(i,2,n){
- RD2(x,y);
- edge[x].pb( MP(y,i) );
- edge[y].pb( MP(x,i) );
- }
- dfs(1,0);
- rmq_init(bn);
- int m;
- RD(m);
- memset(dis,0,sizeof(dis));
- while(m--){
- RD2(x,y);
- dis[x] ++;
- dis[y] ++;
- dis[ lca(x,y) ] -= 2;
- }///和离线做法的差别就在于:
- ///可以直接求出lca而不需要在遍历树的过程中依据相同的端点来求
- qq(1,0);
- REP(i,2,n)
- printf("%d ",ans[i]);
- puts("");
- }
- return 0;
- }
自己敲一遍:
- #include <cstdio>
- #include <cmath>
- #include <algorithm>
- #include <cstring>
- #include <vector>
- #include <utility>
- using namespace std;
- const int MAXN = 1e5+5;
- vector< pair<int, int> > edge[MAXN];
- int n,depth;
- int dfs[MAXN<<1],dn;///dfs序深度序列.一个点会dfs多次,总次数为2*n-1次
- int DtoN[MAXN];///深度对应的节点编号
- int pos[MAXN];///节点第一次出现在dfs序中的位置
- int dp[MAXN<<1][20];
- int dis[MAXN],ans[MAXN];
- void build_dfs_series(int s,int f)
- {
- dfs[++dn] = ++depth;
- int tmp = depth;
- DtoN[depth] = s;
- pos[s] = dn;
- for(int i=0;i<edge[s].size();i++)
- {
- int y = edge[s][i].first;
- if(y==f) continue;
- build_dfs_series(y, s);
- dfs[++dn] = tmp;
- }
- }
- void pre_rmq()
- {
- for(int i=1;i<=dn;i++)
- dp[i][0] = dfs[i];
- int m = floor(log(dn*1.0)/log(2.0));
- for(int j=1;j<=m;j++)
- {
- for(int i=1;i<=dn+1-(1<<j);i++)
- dp[i][j] = min(dp[i][j-1], dp[i+(1<<(j-1))][j-1]);
- }
- }
- int rmq(int l, int r)
- {
- int j = floor(log((r-l+1)*1.0)/log(2.0));
- return min(dp[l][j], dp[r-(1<<j)+1][j]);
- }
- int lca(int x, int y)
- {
- int l = pos[x], r = pos[y];
- if(l>r)
- swap(l, r);
- return DtoN[ rmq(l,r) ];
- }
- void cal(int s, int f)
- {
- for(int i=0;i<edge[s].size();i++)
- {
- int y = edge[s][i].first;
- if(y==f) continue;
- int id = edge[s][i].second;
- cal(y, s);
- dis[s] += dis[y];
- ans[id] = dis[y];///每条边对应它的靠近叶子的节点
- }
- }
- int main()
- {
- scanf("%d",&n);
- for(int i=1,u,v;i<n;i++)
- {
- scanf("%d %d",&u,&v);
- edge[u].push_back( make_pair(v, i) );
- edge[v].push_back( make_pair(u, i) );
- }
- depth = 0;
- dn = 0;
- build_dfs_series(1, 0);
- pre_rmq();
- int m,x,y;
- scanf("%d",&m);
- for(int i=0;i<m;i++)
- {
- scanf("%d %d",&x,&y);
- dis[x]++;dis[y]++;
- dis[lca(x,y)] -= 2;
- }
- cal(1, 0);
- for(int i=1;i<n;i++)
- printf("%d%c",ans[i],(i==n-1)?'\n':' ');
- }
(有点难记啊...)
[CF 191C]Fools and Roads[LCA Tarjan算法][LCA 与 RMQ问题的转化][LCA ST算法]的更多相关文章
- CF 191C Fools and Roads lca 或者 树链剖分
They say that Berland has exactly two problems, fools and roads. Besides, Berland has n cities, popu ...
- Codeforces 191C Fools and Roads(树链拆分)
题目链接:Codeforces 191C Fools and Roads 题目大意:给定一个N节点的数.然后有M次操作,每次从u移动到v.问说每条边被移动过的次数. 解题思路:树链剖分维护边,用一个数 ...
- RMQ问题(线段树+ST算法)
转载自:http://kmplayer.iteye.com/blog/575725 RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ ...
- RMQ问题(线段树算法,ST算法优化)
RMQ (Range Minimum/Maximum Query)问题是指: 对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在[i,j]里的最小(大)值 ...
- RMQ(Range MinimumQuery)问题之ST算法
ST算法------是用来求解给定区间RMQ的最值,本文以最小值为例 ST算法分为两部分 离线预处理(nlogn):运用DP思想,用于求解区间最值,并保存到一个二维数组中. 在线查询 (O(1)):对 ...
- RMQ((Range Minimum/Maximum Query))ST算法
给定一个数组,求出给定区间[l,r]中元素的最大值或最小值或者最值的索引. 一看到这个题目,简单,看我暴力出奇迹.暴力当然是可行的.但是时间复杂度很高(O(n^2)).线段树,树状数组也可以解决这个问 ...
- CodeForces 359D (数论+二分+ST算法)
题目链接: http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=47319 题目大意:给定一个序列,要求确定一个子序列,①使得该子序 ...
- RMQ问题之ST算法
RMQ问题之ST算法 RMQ(Range Minimum/Maximum Query)问题,即区间最值问题.给你n个数,a1 , a2 , a3 , ... ,an,求出区间 [ l , r ]的最大 ...
- 【原创】RMQ - ST算法详解
ST算法: ID数组下标: 1 2 3 4 5 6 7 8 9 ID数组元素: 5 7 3 1 4 8 2 9 8 1.ST算法作 ...
随机推荐
- 初试zabbix
一.zabbix简介 zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案. zabbix由zabbix server与可选组件zabbix a ...
- eclipse怎么切换SVN的用户
在用eclipse的时候会经常用到SVN来进行代码的版本控制,为了方便起见,我们会保存密码,从此之后就不会再出现输入或者修改用户名和密码的地方了,这时候想切换用户怎么办,在本地操作的一种方法是删除SV ...
- 浅谈JAVA集合框架(转载)_常用的Vector和HashMap
原作者滴着:http://www.cnblogs.com/eflylab/archive/2007/01/20/625237.html Java提供了数种持有对象的方式,包括语言内置的Array,还有 ...
- 使用repeater控件显示列表替代treeview
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.We ...
- c++Builder 下的文件及目录操作
转自 http://blog.csdn.net/ktcserver/article/details/936329 一.判断目录是否存在: C++ Builder中提供了检查文件 ...
- ### Paper about Event Detection
Paper about Event Detection. #@author: gr #@date: 2014-03-15 #@email: forgerui@gmail.com 看一些相关的论文. 1 ...
- Eclipse修改java代码后自动重启Tomcat解决办法
今天甚是郁闷,项目马上要上线了,早上刚到公司打开MyEclipse 10.07提示过期提示,这对于用惯了破解软件的帝国用户的我原本以为小菜一碟. 于是到网上到处找破解软件,不用多长时间,Ok 破解成功 ...
- HDU 4764 Stone(博弈)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4764 题目大意:Tang和Jiang玩石子游戏,给定n个石子,每次取[1,k]个石子,最先取完的人失败 ...
- Config spec rules for elements in subbranches
Quote from: Config spec rules for elements in subbranches The following is an example of a config s ...
- 解决在管理wordpress时权限不足的问题
我的wordpress网站的运行环境是自己手动搭建的lamp环境,在管理wordpress时经常遇到因没有足够的权限而无法执行某些操作.在linux上的权限不足的问题无外乎有两个原因,一个是wordp ...