1. 程式人生 > >仙人掌相關問題的處理方法(未完待續)

仙人掌相關問題的處理方法(未完待續)

仙人掌相關問題的處理方法

如圖所示:

1

仙人掌圖就是長得像仙人掌的圖嘛(我真沒看出哪裡像了)

定義:對一個無向連通圖,任意一條邊屬於至多一個簡單環。

  • 橋邊:非環邊,就是連線環的那些邊;
  • 環邊:就是環中的邊嘛。

在仙人掌上,父親和兒子都有節點的和環的之分。

DFS 樹解決仙人掌 DP 問題

仙人掌的處理是十分複雜的(本蒟蒻個人認為,神犇輕噴),這裡先從簡單的 DFS樹開始。

  • 樹邊:DFS 樹中存在的邊
  • 非樹邊:DFS 樹中不存在的邊

大神們還有什麼覆蓋之類的定義,參考最後的參考文獻。

也就是說環是由多條樹邊和一條非樹邊組成的,非樹邊起到了連線的作用。

我們看幾道經典題目:

引例

一棵仙人掌,每條邊有邊權,求 1 號節點到每個節點的最短路徑長度。節點個數 n105

我們先 dfs 一遍的到 dfs 樹,這是我們之後處理的基礎。

然後從一號節點開始 dp 。假設現在已經求出 1 到 u 的距離,列舉 u 的每一個兒子,如果該兒子不在環內,加上邊權繼續;否則,列舉換上的我們暴力求環上的每個點到 1 的距離,然後在分別從環上的每個點繼續 dp 。

一棵帶邊權仙人掌,求直徑(最短路徑最長的兩個點的最短路徑長度)。節點個數 n105

我們 DFS 可以得到一顆 DFS 樹。這裡的 DFS 與 Tarjan 比較類似(大概就是 Tarjan),對於現在訪問的節點 u 我們記錄下來 low[u],dfn[u],dpt[u],fa[u] (前兩個含義參考 Tarjan ,dpt 是深度(deepth) , fa 是 u 在 DFS 樹上的父親),對於沒訪問的(即 dfn 為 0 的)節點繼續遞迴,向上回溯時更新 low 值。

遍歷每條邊時如果 low[v] 大於 dfn[u] ,說明此邊為樹邊;對於一個節點,若其某個兒子在 DFS 樹上的父親不是它(沒錯,它的兒子的父親不是它,機房的小夥伴們都笑瘋了),那麼說明這裡出現了一個環,且此節點為環的(深度最小的節點),它的那個兒子為環的(環中最後被遍歷到的,也就是 DFS 樹的葉子節點)

我們做以上這些的目的主要就是判斷環和橋,然後分別 DP 處理。

我們定義 f[i] 為以 i 為端點的最長鏈的長度。

對於來說十分簡單(此時假設沒有環):

ans=max{ansmax(f[v])+second_max(f[v])+1,v is son of u
f[u]=max(f[v])+1,v is son of u

這裡我們在程式碼中的寫法稍有不同:

  • 先更新 ans ,後更新 f[u]。
  • 遍歷所有兒子,對每個兒子,f[u] 中存的可能是(我們只考慮是的情況,因為只有此時才對結果有影響,這也是為什麼上一條要確保) second_max(f[v]) ,而 f[v] 可能是(依舊只考慮是的情況) max(f[v]),所以我們只需用 f[u]+f[v]+1 來更新 ans 即可;

現在我們考慮環的問題:

對於一個環,我們的宗旨是把它縮成一個點(即把一個環的 f 資訊都存在其根上),然後就可以開心地按之前的方式 DP 啦!

由於環中更新答案的時候只轉一圈不能保證答案最優(因為有可能最優的那一部分環被根分開了),又由於環中距離的定義是最短路,所以我們只要轉夠一圈半即可。

如圖:

定義環的節點集合為 C ,記環中一點 a 在環中的遍歷順序為 aid 、環長(環中節點個數)為 L ,則在環中兩個點之間的距離

dis(a,b)=min{abs(aidbid)Lans(aidbid),a,bC
ans=max{ansf[a]+f[b]+dis(a,b)

我們記一個環的根為 x 、尾為 y。(我只是懶得起名)

f[x]=max(f[i]+dis(x,i)),i{x}C

實現的時候,我們把環中的節點的 f 依次存在一個數組(我的程式碼中是 a 陣列懶得起名+1)裡,然後將這個陣列倍長(就是將其複製一遍放到尾部),遍歷時動一定一(這裡莫名懷念小曉笑瀟),若之間相差大於 L2 就 continue,再用單調佇列優化一下, ans 就能輕鬆更新好了。之後再按照公式更新 f[x] 即可。

整個演算法過程的時間複雜度僅為 O(N) ,還是蠻快的。

/**************************************************************
    Problem: 1023
    User: zhangche0526
    Language: C++
    Result: Accepted
    Time:256 ms
    Memory:7076 kb
****************************************************************/

#include<iostream>
#include<cstdio>
using namespace std;

const int MAXN=5e4+5,MAXM=MAXN<<1;

int n,m;
struct E{int next,to;} e[MAXM<<1];int ecnt,G[MAXN];
void addEdge(int u,int v)
{
    e[++ecnt]=(E){G[u],v};G[u]=ecnt;
    e[++ecnt]=(E){G[v],u};G[v]=ecnt;
}
int ans;
int f[MAXN];
int dfn[MAXN],dcnt,low[MAXN],dpt[MAXN],fa[MAXN];
int que[MAXN<<1],a[MAXN<<1];
void solve(int x,int y)
{
    int cnt=dpt[y]-dpt[x]+1,head=1,tail=1,i;
    for(i=y;i!=x;i=fa[i]) a[cnt--]=f[i];a[1]=f[x];
    cnt=dpt[y]-dpt[x]+1;
    for(i=1;i<=cnt;i++) a[i+cnt]=a[i];
    que[1]=1;
    for(i=2;i<=cnt+(cnt>>1);i++)
    {
        if(i-que[head]>(cnt>>1)) head++;
        ans=max(ans,a[i]+i+a[que[head]]-que[head]);
        while(head<=tail&&a[i]-i>=a[que[tail]]-que[tail]) tail--;
        que[++tail]=i;
    }
    for(i=2;i<=cnt;i++) f[x]=max(f[x],a[i]+min(i-1,cnt-i+1));
}

void dfs(int u)
{
    int i;
    dfn[u]=low[u]=++dcnt;
    for(i=G[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa[u]) continue;
        if(!dfn[v]) {fa[v]=u;dpt[v]=dpt[u]+1;dfs(v);}
        low[u]=min(low[u],low[v]);
        if(low[v]>dfn[u]) ans=max(ans,f[u]+f[v]+1),f[u]=max(f[u],f[v]+1);
        //對樹邊的更新
    }
    for(i=G[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(fa[v]!=u&&dfn[u]<dfn[v])//遍歷非樹邊,處理環
            solve(u,v);
    }
}

int main()
{
    int i,j;
    scanf("%d%d",&n,&m);
    for(i=1;i<=m;i++)
    {
        int k;scanf("%d",&k);
        int u,v;scanf("%d",&u);
        for(j=2;j<=k;j++) scanf("%d",&v),addEdge(u,v),u=v;
    }
    dfs(1);printf("%d\n",ans);
    return 0;
}

圓方樹

通過前面的幾道例題,我們發現:其實解決仙人掌 DP 問題不過就是參照樹上的解法然後對環上的情況特殊處理一下,把環的資訊記錄到一個點上。

其實,神犇們很早就發現了這一點,於是他們想:既然仙人掌的許多問題在樹上都有現成的解法,那麼如果直接把仙人掌變成樹,豈不美哉?

於是,神犇們成功的仙人掌變成樹,並給這種樹起了一個生動形象的名字:圓方樹,它能解決大多數靜態仙人掌問題

定義

仙人掌 G=(V,E) 的圓方樹 T=(VT,ET) 為滿足一下條件的無向圖:

  • VT=RTST,RT=V,RTST=ϕ ,我們稱 RT 集合為圓點, ST 集合為方點;
  • eE ,若 e 不在任何簡單環中,則 eET

    易證:圓方樹是一棵樹
    3

構造

  • 從任意一個點跑 Tarjan 求點雙連通分量;
  • 對於每個點雙,從棧中取出,這時棧中的順序就是環上的順序,在圓方樹中建立方點,依次向棧中的圓點連邊;
  • 如果這條邊是橋邊,我們直接在圓方樹中加入它。

性質

  • 兩個方點不會相連
  • 圓方樹是無根樹

    子仙人掌:以 r 為根的仙人掌上的點 p 的子仙人掌是從仙人掌中起吊 p 到 r 的簡單路徑上的所有邊後, p 所在的連通塊。

  • 以 r 為根的仙人掌中點 p 的子仙人掌就是圓方樹以 r 為根時點 p 的子樹中的所有圓點。

在一個無向連通圖中選出若干個點,這些點互相沒有邊連線,並使取出的點儘量多。資料保證圖的一條邊屬於且僅屬於一個簡單環,圖中沒有重邊和自環。點數 n106 ,邊數 m106

    在一個無向連通圖中選出若干個點,這些點互相沒有邊連線,並使取出的點儘量多。資料保證圖的一條邊屬於且僅屬於一個簡單環,圖中沒有重邊和自環。點數 $n\leq10^6$ ,邊數 $m\leq10^6$
  • 類比樹上的解法:設 f[i][0/1] 表示點 i 是否選時子樹內的最大獨立集;
  • 如果一條邊連線兩個圓點,用樹上轉移方式即可;
  • 而對於連線圓點和方點的情況,把這個換中所有點拿出來,跑一個環上的 DP 。

時間複雜度: O(n)

其實我們在解這道題的時候,沒有必要真正建出圓方樹,我在程式碼裡建出圓方樹只是為了舉例說明,是為讓大家熟悉圓方樹的建法。

/**************************************************************
    Problem: 4316
    User: zhangche0526
    Language: C++
    Result: Accepted
    Time:204 ms
    Memory:12140 kb
****************************************************************/

#include<iostream>
#include<cstdio>
#include<cstring>

const int MAXN=2e5+5,MAXM=2e5+5,INF=~0U>>1;

int n,m,newn;//newn:圓方樹的點數

struct CFS
{
    struct E{int next,to;} e[MAXM];int ecnt,G[MAXN];
    void addEdge(int u,int v){e[++ecnt]=(E){G[u],v};G[u]=ecnt;}
    void addEdge2(int u,int v){addEdge(u,v);addEdge(v,u);}
    CFS(){ecnt=1;}
} G,T;

int f[MAXN][2],g[MAXN][2],gcnt;
void treeDP(int u,int from)
{
    int i;
    if(u<=n)
    {
        f[u][0]=0;f[u][1]=1;
        for(i=T.G[u];i;i=T.e[i].next)
        {
            int v=T.e[i].to;
            if(v==from) continue;
            treeDP(v,u);
            if(v>n) continue;
            f[u][0]+=std::max(f[v][0],f[v][1]);
            f[u][1]+=f[v][0];
        }
    }
    else
    {
        for(i=T.G[u];i;i=T.e[i].next)
            if(T.e[i].to!=from)
                treeDP(T.e[i].to,u);
        gcnt=0;
        for(i=T.G[u];i;i=T.e[i].next)
        {
            g[++gcnt][0]=f[T.e[i].to][0];
            g[gcnt][1]=f[T.e[i].to][1];
        }
        for(i=gcnt-1;i;i--)
        {
            g[i][0]+=std::max(g[i+1][0],g[i+1][1]);
            g[i][1]+=g[i+1][0];
        }
        f[from][0]=g[1][0];
        gcnt=0;
        for(i=T.G[u];i;i=T.e[i].next)
        {
            g[++gcnt][0]=f[T.e[i].to][0];
            g[gcnt][1]=f[T.e[i].to][1];
        }
        g[gcnt][1]=-INF;
        for(i=gcnt-1;i;i--)
        {
            g[i][0]+=std::max(g[i+1][0],g[i+1][1]);
            g[i][1]+=g[i+1][0];
        }
        f[from][1]=g[1][1];
    }
}

int fa[MAXN],dfn[MAXN],dcnt;
bool onRing[MAXN];
void dfs(int u,int la)
{
    int i,j;dfn[u]=++dcnt;
    for(i=G.G[u];i;i=G.e[i].next)
    {
        int v=G.e[i].to;
        if(v==la) continue;
        if(!dfn[v