poj 3694 (求桥+LCA问题)

本文介绍了一种用于检测网络中桥连接的算法,并提供了两种解决方案。一种是通过将连通块缩点形成树结构进行分析,另一种是在求桥的过程中对图进行分层处理,通过特定的移动和标记操作来优化查询效率。
Network
Time Limit: 5000MS Memory Limit: 65536K
Total Submissions: 3868 Accepted: 1321

Description

A network administrator manages a large network. The network consists of N computers and M links between pairs of computers. Any pair of computers are connected directly or indirectly by successive links, so data can be transformed between any two computers. The administrator finds that some links are vital to the network, because failure of any one of them can cause that data can't be transformed between some computers. He call such a link a bridge. He is planning to add some new links one by one to eliminate all bridges.

You are to help the administrator by reporting the number of bridges in the network after each new link is added.

Input

The input consists of multiple test cases. Each test case starts with a line containing two integers N(1 ≤ N ≤ 100,000) and M(N - 1 ≤ M ≤ 200,000).
Each of the following M lines contains two integers A and B ( 1≤ A ≠ B ≤ N), which indicates a link between computer A and B. Computers are numbered from 1 to N. It is guaranteed that any two computers are connected in the initial network.
The next line contains a single integer Q ( 1 ≤ Q ≤ 1,000), which is the number of new links the administrator plans to add to the network one by one.
The i-th line of the following Q lines contains two integer A and B (1 ≤ A ≠ B ≤ N), which is the i-th added new link connecting computer A and B.

The last test case is followed by a line containing two zeros.

Output

For each test case, print a line containing the test case number( beginning with 1) and Q lines, the i-th of which contains a integer indicating the number of bridges in the network after the first i new links are added. Print a blank line after the output for each test case.

Sample Input

3 2
1 2
2 3
2
1 2
1 3
4 4
1 2
2 1
2 3
1 4
2
1 2
3 4
0 0

Sample Output

Case 1:
1
0

Case 2:
2
0

Source

分析:这题一开始就想到求桥,然后用LCA(最小公共祖先)来解决,不过由于本人比较挫,LCA不熟,一直想搞一个简单一点的做法,还是没搞出来,做法一般有两种:
第一种:比较好理解,切效率比较高的一般都是求出所有边连通块,将它缩成一个点,这样原图就变成一棵树,每次询问就找两个点所对应的新点,然后看他们与他们的LCA之间有几条边s,把总变数-s,然后把三个点直接所有点再缩成一点,这样实现起来实在是。。。。
第二种:在求桥的同时已经给图分层了,我们没必要把新图构出来,只需在原图根据定理来做就行,记录属于割边的点(后遍历到的),还有所有点的反向边(即通过那条边遍历过来),这样,每次询问两个点时,先把他们移到同一层,然后判断两点是否共点,如果不是两点同时往上移,直到共点,移动过程中遇到标记为桥的点,把答案减一,改变标记就行。。。
第二种解法能过实在是侥幸,如果这题出现一组数据使得该图是一条直线,然后每次询问头和尾,这样复杂度升级为(QV),实在是很可怕阿~~~~
明天补上第一种写法把,囧
好吧,其实昨天想了一种类似并查集的方法来优化第二种,不过写挫了,今天仔细看了下,原来是低级错误。。。
只要记录查LCA时路径上的点,把他们的父节点全部变成LCA,时间290++ms,感觉还好
代码( 红色部分为改动部分):
#include<cstdio>
#define min(a,b) (a<b?a:b)
using namespace std;
const int mm=444444;
const int mn=111111;
int t[mm],p[mm];
int h[mn],pre[mn],dfn[mn],low[mn];
bool bg[mn];
int i,j,k,n,m,q,top,idn,sum,cas=0;
void dfs(int u)
{
    dfn[u]=low[u]=++idn;
    for(int i=h[u],v;i>=0;i=p[i])
        if(!dfn[v=t[i]])
        {
            pre[v]=u,dfs(v);
            low[u]=min(low[u],low[v]);
             if(dfn[u]<low[v])++sum,bg[v]=1;
        }
        else if(v!=pre[u])low[u]=min(low[u],dfn[v]);
}
void tarjan()
{
    for(int i=0;i<=n;++i)dfn[i]=bg[i]=0;
    idn=sum=0,dfs(1);
}
void move(int &x)
{
    if(bg[x])--sum,bg[x]=0;
    p[top++]=(x=pre[x]);//记录节点
}
int lca(int x,int y)
{
    p[0]=x,p[1]=y,top=2;//初始化栈
    while(x!=y)
    {
        if(dfn[x]>dfn[y])move(x);
        else if(dfn[x]<dfn[y])move(y);
        else move(x),move(y);
    }
    while(top--)if(p[top]!=x)pre[p[top]]=x;//更改父节点
    return sum;
}
int main()
{
    while(scanf("%d%d",&n,&m),n+m)
    {
        for(i=k=0;i<=n;++i)h[i]=-1;
        while(m--)
        {
            scanf("%d%d",&i,&j);
            t[k]=j,p[k]=h[i],h[i]=k++;
            t[k]=i,p[k]=h[j],h[j]=k++;
        }
        tarjan();
        scanf("%d",&q);
        printf("Case %d:\n",++cas);
        while(q--)
        {
            scanf("%d%d",&i,&j);
            printf("%d\n",lca(i,j));
        }
        puts("");
    }
    return 0;
}
<think>我们正在讨论笛卡尔树在求解最近公共祖先(LCA问题上的应用。根据引用[2],笛卡尔树具有两个重要特性: 1. 对于下(即中序遍历)是二叉搜索树(中序遍历为原序列顺序)。 2. 对于元素值,它满足堆的性质(子树根节点是子树的最值)。 引用[1]提到可以将RMQ(区间最小值)问题转化为笛卡尔树上的LCA问题。因为一个序列的RMQ问题可以转化为该序列构建的笛卡尔树上的LCA问题:区间最小值所在的下就是该区间对应子树的根节点(即LCA)。 因此,如果我们已经构建了序列的笛卡尔树,那么对于任意两个节点(下)的LCA,其实就是这两个节点在笛卡尔树上的最近公共祖先。 但是,请注意,这里我们要求的是原序列中两个位置(下)的LCA,而笛卡尔树中节点的编号就是原序列的下。所以,在笛卡尔树上求两个下LCA,得到的结果就是这两个下对应的节点的最近公共祖先节点,而这个祖先节点的值(即原序列中的值)就是这两个下所夹区间的最小值(如果是小根堆)。 那么,如何利用笛卡尔树求解LCA呢?步骤如下: 1. 根据给定的序列(通常我们关心的是序列的值,但构建笛卡尔树需要同时考虑下值)构建笛卡尔树。注意:笛卡尔树的下就是原序列的下,而节点值则是原序列的值。构建时,我们按照下顺序(即中序遍历顺序)插入节点,同时维护堆性质(通常是小根堆或大根堆,根据问题需要)。 2. 构建好笛卡尔树后,我们实际上得到了一棵树。然后,我们可以使用任意一种LCA算法(如Tarjan离线算法、倍增法、RMQ转LCA等)来求解树上任意两个节点的LCA。 但是,引用[1]引用[5]提供了笛卡尔树的构建模板,而引用[3]介绍了Tarjan算法求LCA。然而,我们这里更关心的是如何通过笛卡尔树来求解LCA,特别是当我们将RMQ问题转化为LCA问题后,就可以用笛卡尔树来高效解决。 实际上,笛卡尔树求解LCA的过就是普通的树上的LCA求解。但是,由于笛卡尔树具有特殊的性质(即树的结构由序列的值决定),我们可以利用一些特性(如单调栈构建过)来优化,但通常LCA的求解还是使用准算法。 因此,我们有两种做法: - 先构建笛卡尔树,然后在笛卡尔树上使用倍增法或Tarjan算法求LCA(离线)。 - 或者,我们可以将笛卡尔树上的LCA问题再转化为RMQ问题?注意:树上的LCA问题可以通过DFS序转化为RMQ问题。但这样可能绕远了。 所以,我们通常的做法是: 1. 构建笛卡尔树(线性时间)。 2. 在笛卡尔树上预处理LCA(倍增法预处理O(nlogn),查询O(logn);或者Tarjan离线算法O(n+Q))。 但是,我们注意到,在笛卡尔树中,两个节点的LCA就是它们之间区间的最小值对应的节点。因此,如果我们已经构建了笛卡尔树,那么求两个下ij的LCA,实际上就是求区间[i,j]的最小值对应的下。而求区间最小值我们可以用RMQ(如ST表)在O(1)时间内回答,前提是做了O(nlogn)的预处理。 所以步骤可以这样: - 构建笛卡尔树(得到树结构)。 - 对笛卡尔树进行DFS,得到欧拉序,然后构建欧拉序上的RMQ(用于求LCA)。 这样,我们就把原序列的RMQ问题(求区间最小值)通过笛卡尔树转化为了LCA问题,然后又将LCA问题通过欧拉序转化为了RMQ问题(欧拉序上的区间最小值)。虽然看起来绕,但这样做的意义在于,原序列的RMQ问题(即求区间最小值)可以转化为笛卡尔树上的LCA,而LCA问题可以转化为欧拉序上的RMQ,而欧拉序上的RMQ(即±1RMQ)有更高效的解决方法。 但是,如果我们只需要求原序列的RMQ,那么直接使用ST表即可。那么为什么还要用笛卡尔树呢?因为笛卡尔树可以将多个RMQ问题转化为树上的LCA问题,从而利用树的结构特性(比如结合树链剖分)或者用于解决其他问题(比如结合莫队等)。 回到用户的问题:他想使用笛卡尔树求解最近公共祖先(LCA问题。这里要注意,笛卡尔树本身并不是用来直接求任意树上的LCA,而是用来将序列上的RMQ问题转化为笛卡尔树上的LCA问题。但是,如果我们的问题本身就是在一个序列上,并且我们想求的是序列中两个位置之间的最小值(即RMQ),那么我们可以通过笛卡尔树将其转化为LCA问题。 所以,如果用户的问题是:给定一个序列,以及多个查询,每个查询要求序列区间[L,R]的最小值。那么我们可以: 1. 构建序列的笛卡尔树(小根堆)。 2. 对于每个查询[L,R],区间最小值所在的位置就是笛卡尔树中节点L节点R的LCA节点(注意节点编号就是下)。 因此,我们只需要求节点L节点R在笛卡尔树上的LCA,然后输出这个LCA节点的值(或者下)即可。 那么,如何实现笛卡尔树上的LCA呢?我们可以选择倍增法。 具体实现步骤: 一、构建笛卡尔树 根据引用[5]的代码,我们可以用单调栈构建笛卡尔树。这里是小根堆。 二、在笛卡尔树上求LCA(倍增法) 1. 对笛卡尔树进行DFS,得到每个节点的深度父节点信息。 2. 预处理每个节点的祖先数组(倍增表)。 3. 对于每个查询,使用倍增法求LCA。 下面我们分别实现这两个部分。 第一部分:构建笛卡尔树 我们使用数组`a[]`存储原序列,`n`为序列长度。 我们定义: - `lch[i]`: 节点i的左儿子 - `rch[i]`: 节点i的右儿子 - `parent[i]`: 节点i的父节点(可选,倍增法可以不用,但为了记录树结构,我们至少需要记录左右儿子) 构建过(参考引用[5]): ```cpp stack<int> st; int root; // 树根 for (int i = 1; i <= n; i++) { int last = 0; while (!st.empty() && a[st.top()] > a[i]) { last = st.top(); // 循环退出时,last为最后一个弹出的节点,它应该成为当前节点的左儿子 st.pop(); } if (st.empty()) { root = i; // 栈为空,当前节点为根 } else { rch[st.top()] = i; // 当前节点i成为栈顶节点的右儿子 } lch[i] = last; // 将最后一次弹出的节点作为当前节点的左儿子 st.push(i); } ``` 注意:构建完成后,栈中可能还有元素,但根已经确定(root)。 第二部分:倍增法求LCA 1. 首先进行DFS,初始化深度父节点(第一层祖先)。 2. 预处理祖先数组(倍增表)。 3. 实现LCA查询函数。 由于笛卡尔树是一棵二叉树,我们可以用准的倍增法。 代码框架: ```cpp vector<int> depth(n+1, 0); vector<vector<int>> fa(n+1, vector<int>(LOG)); // LOG为最大层数,比如20 // DFS初始化深度直接父节点 void dfs(int u, int father) { depth[u] = depth[father] + 1; fa[u][0] = father; for (int i = 1; i < LOG; i++) { fa[u][i] = fa[fa[u][i-1]][i-1]; } if (lch[u]) dfs(lch[u], u); if (rch[u]) dfs(rch[u], u); } // 初始化根节点 dfs(root, 0); // LCA查询 int lca(int u, int v) { if (depth[u] < depth[v]) swap(u, v); // 将u提升到v同一深度 int d = depth[u] - depth[v]; for (int i = 0; d; i++, d>>=1) { if (d & 1) u = fa[u][i]; } if (u == v) return u; for (int i = LOG-1; i >= 0; i--) { if (fa[u][i] != fa[v][i]) { u = fa[u][i]; v = fa[v][i]; } } return fa[u][0]; } ``` 最后,对于每个查询(L, R),我们调用`lca(L, R)`,得到的结果就是区间[L,R]的最小值所在的下。注意:这里假设LR是下,且L<=R(但即使L>R,我们可以交换,因为LCA具有对称性)。但是,在笛卡尔树中,节点L节点R的LCA对应的下就是[L,R]区间中最小值的位置。 因此,我们可以通过这个位置得到最小值:`a[lca(L,R)]`。 总结:笛卡尔树LCA算法的原理是将序列的RMQ问题转化为笛卡尔树上的LCA问题,然后通过树上倍增法求解LCA。 注意:构建笛卡尔树的时间复杂度是O(n),预处理倍增数组是O(nlogn),每次查询LCA是O(logn)。 相关问题: 1. 笛卡尔树构建过中为什么使用单调栈?它如何保证线性时间复杂度? 2. 除了倍增法,还有哪些方法可以求解笛卡尔树上的LCA? 3. 笛卡尔树在解决哪些经典问题中有应用?</think>### 笛卡尔树求解 LCA 的原理与实现 #### 一、笛卡尔树与 LCA 的关联原理 1. **笛卡尔树特性**(引用[2]): - 对**下**满足二叉搜索树性质(中序遍历即原序列顺序) - 对**值**满足堆性质(子树根节点是子树最值) - 示例:序列 $[3,5,1,7,4,6,2]$ 的笛卡尔树结构: ``` 1(3) / \ 3(1) 5(2) / \ / \ 2(5)4(7)6(6)7(4) ``` 2. **RMQ 到 LCA 的转化**(引用[1]): - 序列区间 $[L,R]$ 的最小值位置 = 节点 $L$ $R$ 在笛卡尔树上的 **LCA** - 数学表达: $$\text{argmin}_{i \in [L,R]} a_i = \text{LCA}(L,R)$$ #### 二、算法实现步骤 ##### 1. 笛卡尔树构建(单调栈) ```cpp const int N = 1e6+5; int a[N], lch[N], rch[N], root; void build_cartesian_tree(int n) { stack<int> st; for (int i = 1; i <= n; i++) { int last = 0; while (!st.empty() && a[st.top()] > a[i]) { last = st.top(); // 保存最后弹出的节点 st.pop(); } if (st.empty()) root = i; // 栈空则当前节点为根 else rch[st.top()] = i; // 当前节点作为栈顶右儿子 lch[i] = last; // 最后弹出节点作为左儿子 st.push(i); } } ``` **时间复杂度**:$O(n)$,每个元素入栈出栈各一次[^4]。 ##### 2. LCA 查询(倍增法) ```cpp int depth[N], fa[N][20]; void dfs(int u, int father) { depth[u] = depth[father] + 1; fa[u][0] = father; for (int i = 1; i <= 18; i++) fa[u][i] = fa[fa[u][i-1]][i-1]; // 倍增预处理 if (lch[u]) dfs(lch[u], u); if (rch[u]) dfs(rch[u], u); } int lca(int x, int y) { if (depth[x] < depth[y]) swap(x, y); // 跳到同一深度 for (int i = 18; i >= 0; i--) if (depth[fa[x][i]] >= depth[y]) x = fa[x][i]; if (x == y) return x; // 同步上跳 for (int i = 18; i >= 0; i--) if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i]; return fa[x][0]; } ``` **查询使用**: ```cpp int min_pos = lca(L, R); // 获取[L,R]区间最小值下 int min_val = a[min_pos]; // 获取最小值 ``` #### 三、算法特性分析 1. **时间复杂度**: - 建树:$O(n)$ - 预处理:$O(n \log n)$ - 单次查询:$O(\log n)$ 2. **空间复杂度**:$O(n \log n)$(存储倍增数组) 3. **适用场景**: - 静态序列 RMQ 问题(无需动态修改) - 需要获取最值位置而非仅值 - 多次区间查询(如 $10^6$ 次查询)[^5] #### 四、典型例题 1. **洛谷 P1440**:求 $m$ 区间内的最小值 - 解法:滑动窗口最小值转化为笛卡尔树 LCA[^4] 2. **POJ 2201**:构造笛卡尔树并输出结构 3. **RMQ 问题扩展**: - 序列 $a = [3,5,1,7,4,6,2]$,查询 $[2,6]$ 的最小值位置 **解**:$\text{LCA}(2,6) = 3$(对应值 $a[3]=1$) > 笛卡尔树将序列的 RMQ 问题转化为树结构的 LCA 问题,通过结合单调栈倍增法,在 $O(n \log n)$ 预处理后实现高效查询[^1][^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值