Codeforces 827D Best Edge Weight
原题地址:http://codeforces.com/problemset/problem/827/D
题意:
给你N个点M条边的带边权无向联通图,现在对于每条边,问这条边的权值最大可以是多
少,使得这条边在该无向图的所有最最小成树中?
数据范围
2 ≤N ≤ 2*10
5
5
, N - 1 ≤ M ≤ 2*10
5
5
。
题解:
首先我们跑出这个图的最小生成树。
那么:
对于每条树边,它必须要比所有可以代替它的非树边要小(最小值-1),
可以代替它的非树边的两端点必须横跨这条树边。
对于每条非树边,它必须比最大的它可以代替的树边要小(最大值-1)。
它可以代替的树边就是它的两个端点在树上对应的一条链上的边。
非树边的情况,链上最大值比较好求,倍增即可,
但是非树边呢,对于每条树边去找可以代替它的非树边的最小值显然不太好操作,
(其实我最开始想的是拿一个set维护,然后像天天爱跑步那样每到一个子树删一些非树边再加一些非树边)
但是可以转换思路,不对于每条树边去找可以代替它的非树边,
而是对于每条非树边,去更新它可以更新到的树边。(常见转化)
就变成了:
求树链上的最大值,更改一条链上的值。
sol1. 树链剖分: O(mlogn^2)
这个很容易用链剖加个线段树维护。
(一种是区间打标记,初始flag=inf,最后查询一个点时就是这样一层层下来的最小标记的值,另一种就每次去修改每个点,只要一个区间的max大于更新值就去修改,因为如果从小到大枚举非树边,每个树边实则只会被更新一次,均摊依旧 nlogn^2。
(考场上觉得代码量大不敢写。其实注意一点就不会错。要小心争取一次写对。)
UPD:关于树链剖分的注意事项:
因为是边权化点权,所以最后的lca不能modify。
对于这一点,实际上因为最后都跳到一条重链上,之前已记录了重儿子,就修改[son[v],u]即可(dep[v]< dep[u])。
但是需要注意的是,假设u,v最后跳到了一个点,就不存在区间,dfn[son[v]]>dfn[u],需要在线段树修改时判断L,R的大小关系,以便及时return -inf。(所以在modify时需要注意两端点的dfn谁大谁小。)
树链剖分代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=100055;
const int M=100055; //点权化边权
const int inf=0x3f3f3f3f;
vector<int> NT,to[N],W[N];
struct edge
{
int u,v,w,id;
}E[M];
bool cmp(const edge &A,const edge &B)
{
return A.w<B.w;
}
bool cmpid(const edge &A,const edge &B)
{
return A.id<B.id;
}
int n,m,xx;
int fa[N],son[N],seq[N],dfn[N],pl[N],size[N],dep[N],top[N],inc=0,w[N],ans[N];
struct node
{
int ls,rs;
int mx,flag; //flag树边更新值 mx原树边值max
}tr[2*N];
int root,tail=0;
int getfa(int x)
{
if(pl[x]==x)return pl[x];
else return pl[x]=getfa(pl[x]);
}
void dfs(int u,int f)
{
dep[u]=dep[f]+1;
fa[u]=f;
size[u]=1;
int sz=to[u].size();
for(int i=0;i<sz;i++)
{
int v=to[u][i];
if(v==f) continue;
w[v]=W[u][i];
dfs(v,u);
size[u]+=size[v];
if(size[v]>size[son[u]]) son[u]=v;
}
}
void gettp(int u,int f,int tp)
{
top[u]=tp;
inc++; seq[inc]=u; dfn[u]=inc;
if(son[u]) gettp(son[u],u,tp);
int sz=to[u].size();
for(int i=0;i<sz;i++)
{
int v=to[u][i];
if(v==f||v==son[u]) continue;
gettp(v,u,v);
}
}
void update(int nd)
{
int ls=tr[nd].ls; int rs=tr[nd].rs;
tr[nd].mx=max(tr[ls].mx,tr[rs].mx);
}
void build(int &nd,int lf,int rg)
{
nd=++tail;
tr[nd].flag=inf;
if(lf==rg)
{
tr[nd].mx=E[w[seq[lf]]].w;
return;
}
int mid=(lf+rg)>>1;
build(tr[nd].ls,lf,mid);
build(tr[nd].rs,mid+1,rg);
update(nd);
}
int modify(int nd,int lf,int rg,int L,int R,int val)
{
if(lf>rg) return -inf;
if(L<=lf&&rg<=R)
{
tr[nd].flag=min(val,tr[nd].flag);
return tr[nd].mx;
}
int mid=(lf+rg)>>1;
int ret=-inf;
if(L<=mid) ret=max(ret,modify(tr[nd].ls,lf,mid,L,R,val));
if(R>mid) ret=max(ret,modify(tr[nd].rs,mid+1,rg,L,R,val));
return ret;
}
int modify(int u,int v,int ww) ///修改 u,v,w
{
int ret=-inf;
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
ret=max(ret,modify(root,1,n,dfn[top[u]],dfn[u],ww)); u=fa[top[u]];
}
if(dep[u]<dep[v]) swap(u,v);
ret=max(ret,modify(root,1,n,dfn[son[v]],dfn[u],ww));
return ret;
}
int query(int nd,int lf,int rg,int pos)
{
if(lf==rg) return tr[nd].flag;
int mid=(lf+rg)>>1;
int ret=tr[nd].flag;
if(pos<=mid)
ret=min(ret,query(tr[nd].ls,lf,mid,pos));
else
ret=min(ret,query(tr[nd].rs,mid+1,rg,pos));
return ret;
}
void getans(int u,int f)
{
int sz=to[u].size();
for(int i=0;i<sz;i++)
{
int v=to[u][i];
if(v==f) continue;
ans[w[v]]=query(root,1,n,dfn[v])-1;
getans(v,u);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
E[i].id=i;
}
sort(E+1,E+m+1,cmp);
for(int i=1;i<=n;i++) pl[i]=i;
for(int i=1;i<=m;i++)
{
int u=E[i].u; int v=E[i].v;
int fx=getfa(u); int fy=getfa(v);
if(fx!=fy)
{
pl[fx]=fy;
to[u].push_back(v); to[v].push_back(u);
W[u].push_back(E[i].id); W[v].push_back(E[i].id);
}
else
{
NT.push_back(E[i].id);
}
}
sort(E+1,E+m+1,cmpid);
dfs(1,1);
gettp(1,1,1);
build(root,1,n);
int sz=NT.size();
for(int i=0;i<sz;i++)
{
int tmp=NT[i];
int u=E[tmp].u; int v=E[tmp].v;
ans[E[tmp].id]=modify(u,v,E[tmp].w)-1;
}
getans(1,1);
for(int i=1;i<=m;i++)
if(ans[i]>=inf-1) printf("-1 ");else printf("%d ",ans[i]);
return 0;
}
但是好长啊。很容易出点错误。
于是,
sol2. 倍增: O(mlogn+nlogn)
如果对于非树边,可以倍增求链上最大值,难道不能倍增地修改链上的值吗?
既然暴力是一个一个地跳,修改,那么思考如何一块一块地跳,修改。
可以用与这道题类似的思想。
倍增跳lca的时候一起更新ans[u][i](表示u点向上2^i条边的最大答案)的答案
最后从anc[u][P]开始逐层下拆取min:
for(int i=P;i>=1;i--)
for(int u=1;u<=n;u++)
{
ans[u][i-1]=min(ans[u][i],ans[u][i-1]);
ans[anc[u][i-1]][i-1]=min(ans[u][i],ans[anc[u][i-1]][i-1]);
}
答案就在ans[u][0]里。
倍增代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=200005;
const int P=17;
const int inf=0x3f3f3f3f;
int n,m,anc[N][P+3],w[N][P+3],ans[N][P+3],answer[N],fa[N],dep[N],e[N]; //w存储树边边权最大值,ans存被非树边更新的最小值答案
vector<int> to[N],NT,W[N];
struct egde
{
int u,v,w,id;
}E[N];
bool cmpw(const egde &A,const egde &B)
{return A.w<B.w;}
bool cmpid(const egde &A,const egde &B)
{return A.id<B.id;}
int getfa(int x)
{
if(fa[x]==x) return x;
else return fa[x]=getfa(fa[x]);
}
void dfs(int u,int f)
{
dep[u]=dep[f]+1;
anc[u][0]=f;
for(int i=1;i<P;i++)
{
anc[u][i]=anc[anc[u][i-1]][i-1];
w[u][i]=max(w[u][i-1],w[anc[u][i-1]][i-1]);
}
int sz=to[u].size();
for(int i=0;i<sz;i++)
{
int v=to[u][i];
if(v==f) continue;
e[v]=W[u][i];
w[v][0]=E[W[u][i]].w;
dfs(v,u);
}
}
int modify(int u,int v,int val)
{
if(dep[u]<dep[v]) swap(u,v);
int d=dep[u]-dep[v];
int ret=-inf;
for(int i=0;d;d>>=1,i++)
if(d&1) {ret=max(ret,w[u][i]); ans[u][i]=min(ans[u][i],val); u=anc[u][i]; }
if(u==v) return ret;
for(int i=P-1;i>=0;i--)
{
if(anc[u][i]!=anc[v][i])
{
ret=max(ret,w[u][i]); ret=max(ret,w[v][i]);
ans[u][i]=min(ans[u][i],val); ans[v][i]=min(ans[v][i],val);
u=anc[u][i]; v=anc[v][i];
}
}
ret=max(ret,w[u][0]); ret=max(ret,w[v][0]);
ans[u][0]=min(ans[u][0],val); ans[v][0]=min(ans[v][0],val);
u=anc[u][0]; v=anc[v][0];
return ret;
}
void pushdown()
{
for(int i=P;i>=1;i--)
for(int u=1;u<=n;u++)
{
ans[u][i-1]=min(ans[u][i],ans[u][i-1]);
ans[anc[u][i-1]][i-1]=min(ans[u][i],ans[anc[u][i-1]][i-1]);
}
for(int u=1;u<=n;u++)
answer[e[u]]=ans[u][0]-1;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
E[i].id=i;
}
sort(E+1,E+m+1,cmpw);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int u=E[i].u; int v=E[i].v;
int fx=getfa(u); int fy=getfa(v);
if(fx!=fy)
{
fa[fx]=fy;
to[u].push_back(v); to[v].push_back(u);
W[u].push_back(E[i].id); W[v].push_back(E[i].id);
}
else NT.push_back(E[i].id);
}
sort(E+1,E+m+1,cmpid);
memset(ans,0x3f,sizeof(ans));
dfs(1,1);
int sz=NT.size();
for(int i=0;i<sz;i++)
{
int tmp=NT[i];
answer[tmp]=modify(E[tmp].u,E[tmp].v,E[tmp].w)-1;
}
pushdown();
for(int i=1;i<=m;i++)
if(answer[i]>=inf-1) printf("-1 "); else printf("%d ",answer[i]);
return 0;
}
sol3. 并查集: O(nlogn)
上述两种方式思路类似,都是逐个去更新,
但我们还有个没有用的性质:
如果从小到大枚举非树边,每个树边实则只会被更新一次,它只会被第一个更新到它的非树边更新。
暴力的复杂度就多在,不需要走的点又走了一边。
实际上如果我们把走过的边就缩掉,那么每个树边只会被走一次。
这个可以用并查集或链表维护。
这个是O(n)的,倍增求lca是logn的,最终是O(nlogn)。
并查集代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int N=200005;
const int P=17;
const int inf=0x3f3f3f3f;
int n,m,anc[N][P+3],fa[N],dep[N],e[N],ans[N],w[N][P];
bool vis[N];
vector<int> to[N],NT,W[N];
struct egde
{
int u,v,w,id;
}E[N];
bool cmpw(const egde &A,const egde &B)
{return A.w<B.w;}
bool cmpid(const egde &A,const egde &B)
{return A.id<B.id;}
int getfa(int x)
{
if(fa[x]==x) return x;
else return fa[x]=getfa(fa[x]);
}
void dfs(int u,int f)
{
anc[u][0]=f;
dep[u]=dep[f]+1;
for(int i=1;i<P;i++)
{
anc[u][i]=anc[anc[u][i-1]][i-1];
w[u][i]=max(w[u][i-1],w[anc[u][i-1]][i-1]);
}
int sz=to[u].size();
for(int i=0;i<sz;i++)
{
int v=to[u][i];
if(v==f) continue;
e[v]=W[u][i];
w[v][0]=E[W[u][i]].w;
dfs(v,u);
}
}
int query(int u,int v,int &lca)
{
if(dep[u]<dep[v]) swap(u,v);
int ret=-inf;
int d=dep[u]-dep[v];
for(int i=0;d;d>>=1,i++)
if(d&1){ret=max(ret,w[u][i]); u=anc[u][i];}
if(u==v) {lca=u; return ret;}
for(int i=P-1;i>=0;i--)
{
if(anc[u][i]!=anc[v][i])
{
ret=max(ret,w[u][i]); ret=max(ret,w[v][i]);
u=anc[u][i]; v=anc[v][i];
}
}
ret=max(ret,w[u][0]); ret=max(ret,w[v][0]);
lca=anc[u][0];
return ret;
}
void modify(int u,int v,int val)
{
u=getfa(u);
if(dep[u]<dep[v]) return;
while(dep[u]>dep[v])
{
ans[e[u]]=val-1;
int nxt=getfa(anc[u][0]);
fa[getfa(u)]=nxt;
u=nxt;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].w);
E[i].id=i;
}
sort(E+1,E+m+1,cmpw);
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
int u=E[i].u; int v=E[i].v;
int fx=getfa(u); int fy=getfa(v);
if(fx!=fy)
{
fa[fx]=fy;
to[u].push_back(v); to[v].push_back(u);
W[u].push_back(E[i].id); W[v].push_back(E[i].id);
}
else NT.push_back(E[i].id);
}
sort(E+1,E+m+1,cmpid);
memset(w,0x3f,sizeof(w));
memset(ans,0x3f,sizeof(ans));
dfs(1,1);
for(int i=1;i<=n;i++) fa[i]=i;
int sz=NT.size();
for(int i=0;i<sz;i++)
{
int tmp=NT[i];
int lca;
ans[tmp]=query(E[tmp].u,E[tmp].v,lca)-1;
modify(E[tmp].u,lca,E[tmp].w);
modify(E[tmp].v,lca,E[tmp].w);
}
for(int i=1;i<=m;i++)
if(ans[i]>=inf-1) printf("-1 "); else printf("%d ",ans[i]);
return 0;
}
于是,一道题收获了三种思路,可喜可贺。