1. 程式人生 > 其它 >「UOJ 632」挑戰最大團

「UOJ 632」挑戰最大團

題目

點這裡看題目。

分析

兼具分析和演算法設計的一道題目,很有價值。

首先注意到,團計數這個問題顯然不弱於最大團。為了避免一下子解決 NPC 贏得圖靈獎,我們閉著眼睛好像還不能也可以想出來,圖的“優美”性質一定對圖的形態有很強的限制作用。

簡單的分析,熱身:

  1. 一個優美的圖上兩兩之間的最短路的最大值 \(\le 2\)

    說明

    說明:根據“優美”性質直接反證即可。

  2. 如果一個圖 \(G\) 是優美的,那麼它的任何一個誘導子圖 \(G'\) 也是優美的。

    說明

    顯然。

一個經典的對偶結論是,\(G\) 上的一個團對應了 \(\overline G\) 的一個獨立集

,其中 \(\overline G\)\(G\) 的補圖。這引導我們去思考關於補圖的性質:

  1. 如果圖 \(G\) 是優美的,那麼圖 \(\overline G\) 也一定是優美的。

    說明

    說明:考慮 \(\overline G\) 上任意的不同的四個點 \((a,b,c,d)\),如果不存在 \((a,b),(b,c),(c,d)\) 這樣一條鏈那好說。否則,在 \(G\) 上最多存在 \((a,c),(a,d),(b,d)\) 三條邊,而這三條邊不可能同時存在。因此在 \(\overline G\) 上一定存在這三條邊之一,也即 \(\overline G\) 也是優美的。

一般來說,圖的重要要素之一就是它的連通性。我們不妨也考慮 \(G\) 的連通性:

如果 \(G\) 是連通的,那就麻煩了;但如果 \(G\) 不連通,每個連通塊就是獨立的,我們只需要將每個連通塊的團的結果加起來就可以了

相應地,我們也可以考慮 \(\overline G\) 的連通性:

如果 \(\overline G\) 是連通的,那我們也束手無策;但是如果 \(\overline G\) 不連通,每個連通塊的獨立集就可以先獨立地求出來,而後通過揹包合併每個塊的獨立集

注意到,只要這兩個條件滿足之一,我們都可以將原問題劃分為相似的子問題。很自然地,我們想知道,在“優美”性質的約束下,\(G\)

\(\overline G\) 是否總是不同時連通?

  1. 如果圖 \(G\) 是優美的,那麼 \(G\)\(\overline G\) 總是不同時連通

    說明

    比較冗長。如果自己寫這道題的話,其實可以選擇直接手玩之後感性理解。

    考慮對於 \(G\) 的大小使用歸納法。設 \(G=(V,E)\),則當 \(1\le |V|\le 3\) 的時候可以列舉證明。

    \(|V| >3\) 的時候,假設 \(n=|V|\),且對於所有點數小於 \(n\) 的圖均有上述性質滿足。

    施加反證法,假設 \(G\)\(\overline G\) 都是連通的。那麼,任取 \(u\in V\),我們取出 \(G\)\(V\setminus u\) 的誘導子圖 \(G'=(V',E')\)。此時 \(G'\) 是一個優美的圖。因此根據歸納假設和對稱性,我們可以認為 \(G'\) 不連通。

    此時 \(G'\) 由若干個連通塊 \(G_1,G_2,\dots,G_m,m>1\) 構成。由於 \(u\)\(\overline {G'}\) 中存在一條連向 \(V'\setminus \{u\}\) 的邊,因此我們可以選出 \(t\in V'\),使得 \((u,t)\not\in E'\)

    考慮我們選出 \(t\) 的這個連通塊 \(G_k\)。此時我們設 \(G_k=(V_k,E_k)\),那麼必然可以將 \(V_k\) 劃分成兩個集合 \(S,T\),使得 \(S=\{v\in V_k|(v,u)\in E'\}\),那麼 \(T=V_k\setminus S\)

    注意到 \(S\) 顯然不為空(否則 \(G_k\) 最終在 \(G\) 上是孤立的),而 \(T\) 中一定包含 \(t\)。因此,我們可以選擇一條邊 \((a,b)\in E_k\),使得 \(a\in S,b\in T\)。此外,由於 \(m>1\),因此我們可以在 \(V'\setminus V_k\) 中任選一個點 \(x\),且 \((x,u)\in E'\)

    考察 \((x,u,a,b)\),此時有:

    • \((x,u),(u,a),(a,b)\in E'\)
    • \((x,a)\not\in E',(x,b)\not\in E'\),因為不屬於一個連通塊,
      \((u,b)\not\in E'\),因為 \(b\not\in S\)

    這樣的話,\(G'\) 就不是一個“優美”的圖,矛盾。所以 \(G\)\(\overline G\) 必有其一不連通,歸納成立。

現在,我們就可以大膽地套用分治演算法了。可以發現,分治會給出一個樹形的結構,我們的計算過程不會比進行樹揹包複雜,因此合併的複雜度是 \(O(n^2)\) 的。

但是,如果我們暴力搜尋連通塊,由於邊非常多,單層複雜度有可能會退化到 \(O(n^2)\),總複雜度則會退化到 \(O(n^3)\)

注意到,劃分過程是“二選一”的:搜尋出 \(G\) 上的一個子連通塊和 \(\overline G\) 上的一個子連通塊,本質上沒有區別。因此,我們可以同時進行 \(G\) 上和 \(\overline G\) 上的搜尋,並且只要滿足兩個條件之一(或者發現 \(G\)\(\overline G\) 是連通的)都可以結束這個過程。可以發現,這樣的演算法思想和啟發式分裂還是蠻像的。

利用性質 1 和性質 2,我們可以知道,無論在 \(G\) 還是在 \(\overline G\) 上,從任意點出發我們最多隻需要搜尋兩步即可找出一個連通塊。因此,我們可以選定點 \(x\) 和點 \(y\),其中 \(x\)\(G\) 上度數最小,而 \(y\)\(\overline G\) 上度數最小。對 \(x,y\) 輪流進行 BFS,如果進行某一側搜尋之後,我們發現當前側的圖是連通的(這意味著我們只需要執行完另一側的搜尋),或者我們搜尋出了當前側圖的一個子連通塊,我們都可以退出 BFS 並進行後續處理了。

設當前的誘導子圖為 \(G^*=(V^*,E^*)\),設 \(d_u\) 表示當前圖上 \(u\) 的度數。我們可以知道該層複雜度上界為 \(O((d_x+d_y)|V^*|)\)

設進行 BFS 的輪數為 \(s\)。如果我們搜尋出了一個子連通塊,則 \(s=\min\{d_x,d_y\}\),容易匯出 \(s\le |V^*|-s\),因此複雜度為 \(O(s|V^*|)=O(s(|V^*|-s))\);否則,我們通過 BFS 確定了其中一側的圖是連通的,另一側圖上分出來的較小的一塊大小為 \(s\),則複雜度為 \(O(s|V^*|)=O(s(|V^*|-s))\)

這個複雜度模型就是樹上揹包,因此分治過程的複雜度也是 \(O(n^2)\) 的。

小結:

  1. 從團到獨立集的經典轉化。
  2. 研究任何圖論問題,都應當從圖的最基本要素入手,可以提供一個較好的方向。
  3. 透過一些已有的演算法想法來尋找性質,這個一般對性質搜尋有用。
  4. 啟發式分裂式的搜尋是一種很有效的搜尋方式,關鍵在於利用條件只需二選一滿足的性質,從而進行複雜度相差不大的雙向搜尋
  5. 證明還是蠻有意思的,尋找特定條件下的反例。

程式碼

#include <queue>
#include <cstdio>
#include <vector>

#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

const int mod = 998244353;
const int MAXN = 8005;

template<typename _T>
void read( _T &x ) {
    x = 0; char s = getchar(); bool f = false;
    while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
    while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
    if( f ) x = -x;
}

template<typename _T>
void write( _T x ) {
    if( x < 0 ) putchar( '-' ), x = -x;
    if( 9 < x ) write( x / 10 );
    putchar( x % 10 + '0' );
}

template<typename _T>
_T MAX( const _T a, const _T b ) {
    return a > b ? a : b;
}

std :: queue<int> q[2];
std :: vector<int> Graph[2][MAXN];

bool vis[2][MAXN], avai[MAXN];

char str[MAXN];
int grp[MAXN], mp[256];

int N;

inline int Mul( int x, int v ) { return 1ll * x * v % mod; }
inline int Sub( int x, int v ) { return ( x -= v ) < 0 ? x + mod : x; }
inline int Add( int x, int v ) { return ( x += v ) >= mod ? x - mod : x; }

void Init() {
    rep( i, 0, 9 ) mp[( int ) ( i + '0' )] = i;
    rep( i, 0, 5 ) mp[( int ) ( i + 'A' )] = i + 10;
}

void Print( const std :: vector<int> &vec ) {
    printf( "{" );
    bool flg = true;
    for( auto x : vec ) {
        if( ! flg ) putchar( ',' );
        printf( " %d", x ), flg = false;
    }
    printf( " }" );
}

std :: vector<int> operator + ( const std :: vector<int> &F, const std :: vector<int> &G ) {
    int n = F.size(), m = G.size();
    std :: vector<int> ret( MAX( n, m ), 0 );
    for( int i = 0 ; i < n || i < m ; i ++ ) {
        if( i < n ) ret[i] = Add( ret[i], F[i] );
        if( i < m ) ret[i] = Add( ret[i], G[i] );
    }
    while( ! ret.empty() && ! ret.back() ) ret.pop_back();
    return ret;
}

std :: vector<int> operator * ( const std :: vector<int> &F, const std :: vector<int> &G ) {
    int n = F.size(), m = G.size();
    if( ! n && ! m ) return std :: vector<int> ();
    std :: vector<int> ret( n + m - 1, 0 );
    for( int i = 0 ; i < n ; i ++ )
        for( int j = 0 ; j < m ; j ++ )
            ret[i + j] = Add( ret[i + j], Mul( F[i], G[j] ) );
    while( ! ret.empty() && ! ret.back() ) ret.pop_back();
    return ret;
}

std :: vector<int> Divide( const std :: vector<int> pnt ) {
    #define Exit( x ) { side = x; break; }
    if( pnt.size() == 1 ) return { 1, 1 };
    int n = pnt.size();
    for( int i = 0 ; i < n ; i ++ ) {
        avai[pnt[i]] = true;
        vis[0][pnt[i]] = false;
        vis[1][pnt[i]] = false;
    }
    int x = pnt[0], y = pnt[0];
    for( int i = 1 ; i < n ; i ++ ) {
        if( Graph[0][pnt[i]].size() < Graph[0][x].size() ) x = pnt[i];
        if( Graph[1][pnt[i]].size() < Graph[1][y].size() ) y = pnt[i];
    }
    int cnt[2] = {};
    while( ! q[0].empty() ) q[0].pop();
    while( ! q[1].empty() ) q[1].pop();
    vis[0][x] = vis[1][y] = true, cnt[0] ++, cnt[1] ++;
    for( int i = 0, v ; i < ( int ) Graph[0][x].size() ; i ++ )
        if( avai[v = Graph[0][x][i]] ) 
            vis[0][v] = true, q[0].push( v ), cnt[0] ++;
    for( int i = 0, v ; i < ( int ) Graph[1][y].size() ; i ++ )
        if( avai[v = Graph[1][y][i]] ) 
            vis[1][v] = true, q[1].push( v ), cnt[1] ++;
    int side = -1;
    while( side == -1 ) 
        for( int k = 0 ; k < 2 ; k ++ ) {
            if( q[k].empty() ) Exit( k );
            int u = q[k].front(), v; q[k].pop();
            for( int i = 0 ; i < ( int ) Graph[k][u].size() ; i ++ )
                if( ! vis[k][v = Graph[k][u][i]] && avai[v] ) {
                    vis[k][v] = true, cnt[k] ++;
                    if( cnt[k] == n ) Exit( ( k ^ 1 ) + 2 );
                }
        }
    if( side > 1 ) {
        int k = ( side -= 2 );
        while( ! q[k].empty() ) {
            int u = q[k].front(), v; q[k].pop();
            for( int i = 0 ; i < ( int ) Graph[k][u].size() ; i ++ )
                if( ! vis[k][v = Graph[k][u][i]] && avai[v] )
                    vis[k][v] = true;                
        }
    }
    std :: vector<int> lef, rig, ret;
    lef.clear(), rig.clear(), ret.clear();
    for( int i = 0 ; i < n ; i ++ ) {
        avai[pnt[i]] = false;
        if( vis[side][pnt[i]] ) lef.push_back( pnt[i] );
        else rig.push_back( pnt[i] );
    }
    if( side == 0 ) 
        ret = Divide( lef ) + Divide( rig );
    else
        ret = Divide( lef ) * Divide( rig );
    ret[0] = 1;
    return ret;
}

int main() {
    read( N ), Init();
    rep( i, 1, N - 1 ) {
        scanf( "%s", str + 1 );
        int L = ( N - i + 3 ) >> 2;
        rep( j, 1, L ) grp[j] = mp[( int ) str[j]];
        rep( j, 1, N - i ) {
            bool edg = grp[( j + 3 ) >> 2] >> ( ( j - 1 ) & 3 ) & 1;
            Graph[edg ^ 1][i].push_back( j + i ), Graph[edg ^ 1][j + i].push_back( i );
        }
    }
    std :: vector<int> beg; beg.clear();
    for( int i = 0 ; i < N ; i ++ ) beg.push_back( i + 1 );
    std :: vector<int> res = Divide( beg );
    for( int i = 1 ; i <= N ; i ++ ) {
        if( i < ( int ) res.size() ) write( res[i] );
        else putchar( '0' );
        putchar( i == N ? '\n' : ' ' );
    }
    return 0;
}