Problem H: qwb与学姐
Time Limit: 1 Sec Memory Limit: 128 MBSubmit: 199 Solved: 69
[ Submit][ Status][ Web Board]
Description
已知一幅n个点m条边的无向图,定义路径的值为这条路径上最短的边的长度,
现在有 k个询问,
询问从A点到B点的所有路径的值的最大值。
qwb听完这个问题很绝望啊,聪明的你能帮帮他吗?
Input
第一行三个整数n,m,k (1<=N<=50000,m<=200000,k<=100000)。
第2..m+1行:三个正整数:X, Y, and D (1 <= X <=N; 1 <= Y <= N,1<=D<=2 15 ) 表示X与Y之间有一条长度为D的边。
第m+2..m+k+1行: 每行两个整数A B(1<=A,B<=n且A≠B),意义如题目描述。
保证图连通。
Output
Sample Input
4 5 3
1 2 6
1 3 8
2 3 4
2 4 5
3 4 7
2 3
1 4
3 4
Sample Output
6
7
7
思路:这题是NOIP 2013 day1 的第三题原题,虽然没做过,但是不久前也算研究过,就算这样看到本题的时候也是反应了好长时间才想起来好像是见过。。然而就算是想起来了也不会写。。
正解是求最大生成树,然后做LCA,图中任意两点间的路径中最长的最短边(即瓶颈路)一定在最大生成树上,至于证明可以参见POJ2485
感觉刚好是个对偶结论。
然后剩下的就是怎么快速的求LCA,以前看NOIP的那个题解的时候只知道能用倍增法求LCA解,这次看官方题解还学了新的骚操作--并查集按秩合并重构树,然后暴力求LCA。。
先说倍增法求LCA,这也是我第一次写倍增法和LCA,以前看过LCA的倍增法求解,但是当时看懂了,也没实现过,没多久就忘了,这次结合白书又重新学了一遍,也算加深了印象吧,然后这次学习的新体会是倍增法更像是一种优化思想而不是一种算法,我感觉其本质思想和快速幂时拆解指数有共通之处,查询的时候利用的是任何数都能被表示成2的k次幂的加和的形式(即二进制分解),而初始化的时候用的是和ST表一样的思想,甚至实现方法都很类似。倍增法利用记忆化优化了LCA的查询,使其从O(N)降到了O(logN)。
代码(我是按白书的风格写的LCA):
#include<bits/stdc++.h>
using namespace std;
#define rank Rank
#define pb push_back
#define fi first
#define se second
#define MAXN 100050
#define inf 0x3f3f3f3f
typedef pair<int,int>P;
int rank[MAXN],f[MAXN],dep[MAXN];
int pre[20][MAXN],dp[20][MAXN];
int n,m;
vector<P>g[MAXN];
struct node
{
int u,v,w;
bool operator < (node a) const
{
return w > a.w;
}
}mp[MAXN*2];
void init()
{
for(int i=0;i<MAXN;i++)
f[i] = i;
}
int getf(int k)
{
return k == f[k] ? k : f[k] = getf(f[k]);
}
void kruskal()
{
sort(mp, mp + m);
int cnt = 1, i = -1;
while(cnt < n)
{
i++;
int u = getf(mp[i].u), v = getf(mp[i].v);
if(u == v) continue;
if(rank[u] > rank[v])
{
f[v] = u;
}
else
{
if(rank[u] == rank[v])
rank[v]++;
f[u] = v;
}
g[u].pb(P(v,mp[i].w));
g[v].pb(P(u,mp[i].w));
cnt++;
}
}
void dfs(int u, int fa)
{
dep[u] = dep[fa] + 1;
for(int i=0;i<g[u].size();i++)
{
int v = g[u][i].fi;
if(v == fa) continue;
dfs(v, u);
dp[0][v] = g[u][i].se;
pre[0][v] = u;
}
}
int lca(int u, int v, int MAX)
{
int ans = inf;
if(dep[u] < dep[v]) swap(u, v);
int k = dep[u] - dep[v];
for(int i=0;i<MAX;i++)
{
if((k>>i)&1)
ans = min(ans, dp[i][u]),u = pre[i][u];
}
if(u == v)return ans;
for(int i=MAX-1;i>=0;i--)
while(pre[i][u] != pre[i][v])
{
ans = min(ans, dp[i][u]);
ans = min(ans, dp[i][v]);
u = pre[i][u];
v = pre[i][v];
}
ans = min(ans, dp[0][u]);
ans = min(ans, dp[0][v]);
return ans;
}
int main()
{
int q,u,v;
scanf("%d%d%d",&n,&m,&q);
{
init();
for(int i=0;i<m;i++)
scanf("%d%d%d",&mp[i].u,&mp[i].v,&mp[i].w);
kruskal();
dep[1] = 1;
dfs(1, 0);
int k = 0,t = 1;
while(t <= n)t <<= 1,k++;
for(int i=0;i+1<k;i++)
{
for(int j=1;j<=n;j++)
{
pre[i+1][j] = pre[i][pre[i][j]];
dp[i+1][j] = min(dp[i][j], dp[i][pre[i][j]]);
}
}
while(q--)
{
scanf("%d%d",&u,&v);
printf("%d\n",lca(u, v, k));
}
}
}
下面说一下个人感觉思想很巧妙的并查集重构树的方法。
众所周知并查集有两个优化,一是路径压缩,二是按秩合并,以前写并查集我通常只写路径压缩,因为(比较好写)这样对大多数题来说复杂度够用了,然后就渐渐淡忘了按秩合并这回事,其实按秩合并在很多维护图的联通性的题里非常有用,就像这个题,按秩合并的思想是每次合并时让深度小的树连接到深度大的树上去,这样就能将最终的树高控制在logN以内(N为节点数),虽然我并不知道如何证明,但是我感觉这样就是对的,嗯,就是对的。
既然树高都在logN以内了,那么我们就可以欢快的暴力求LCA了呀,反正也不会超时。。
代码:
#include<bits/stdc++.h>
using namespace std;
#define rank Rank
#define fi first
#define se second
#define MAXN 100010
#define inf 0x3f3f3f3f
typedef pair<int,int>P;
int rank[MAXN],f[MAXN],dep[MAXN],pre[MAXN],dis[MAXN];
int n,m;
vector<P>g[MAXN];
struct node
{
int u,v,w;
bool operator < (node a) const
{
return w > a.w;
}
}mp[MAXN*2];
void init()
{
for(int i=0;i<MAXN;i++)
f[i] = i;
}
int getf(int k)
{
return k == f[k] ? k : f[k] = getf(f[k]);
}
void kruskal()
{
sort(mp, mp + m);
int cnt = 1, i = -1;
while(cnt < n)
{
i++;
int u = getf(mp[i].u), v = getf(mp[i].v);
if(u == v) continue;
if(rank[u] > rank[v])
{
f[v] = u;
g[u].push_back(P(v, mp[i].w));
}
else
{
if(rank[u] == rank[v])//相等的时候并不是对谁++都可以的,一定要和上面的合并方向相反,不然铁定T
rank[v]++;
f[u] = v;
g[v].push_back(P(u, mp[i].w));
}
cnt++;
}
}
void dfs(int u, int fa)
{
dep[u] = dep[fa] + 1;
for(int i=0;i<g[u].size();i++)
{
int v = g[u][i].fi;
if(v == fa) continue;
dfs(v, u);
pre[v] = u;
dis[v] = g[u][i].se;
}
}
int main()
{
int q,u,v;
scanf("%d%d%d",&n,&m,&q);
{
init();
for(int i=0;i<m;i++)
scanf("%d%d%d",&mp[i].u,&mp[i].v,&mp[i].w);
kruskal();
dep[getf(1)] = 1;
dfs(getf(1),0);
while(q--)
{
int ans=inf;
scanf("%d%d",&u,&v);
if(dep[u] < dep[v]) swap(u, v);
while(dep[u] > dep[v])
{
ans = min(ans, dis[u]);
u = pre[u];
}
while(u != v)
{
ans = min(ans, dis[u]);
ans = min(ans, dis[v]);
u = pre[u];
v = pre[v];
}
printf("%d\n",ans);
}
}
}
PS:尝试着写了一次优雅的代码,将符号两边加上空格,写完感觉好心累,还是不要优雅了吧。