1. 程式人生 > >Codeforces 827D Best Edge Weight (最小生成樹 + 樹鏈剖分/倍增/並查集)

Codeforces 827D Best Edge Weight (最小生成樹 + 樹鏈剖分/倍增/並查集)

Codeforces 827D Best Edge Weight

題意:
給你N個點M條邊的帶邊權無向聯通圖,現在對於每條邊,問這條邊的權值最大可以是多
少,使得這條邊在該無向圖的所有最最小成樹中?

資料範圍
2 ≤N ≤ 2*105, N - 1 ≤ M ≤ 2*105

題解:
首先我們跑出這個圖的最小生成樹。

那麼:
對於每條樹邊,它必須要比所有可以代替它的非樹邊要小(最小值-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;
}

於是,一道題收穫了三種思路,可喜可賀。