1. 程式人生 > >[bzoj1488][HNOI2009]圖的同構——Polya定理

[bzoj1488][HNOI2009]圖的同構——Polya定理

題目大意

求兩兩互不同構的含n個點的簡單圖有多少種。
簡單圖是關聯一對頂點的無向邊不多於一條的不含自環的圖。
a圖與b圖被認為是同構的是指a圖的頂點經過一定的重新標號以後,a圖的頂點集和邊集能完全與b圖一一對應。

題解

這個題是學習了Polya定理和群論以後的練手題,但是推了好久並沒有推出來。。。。真的是太難辣。。。
首先我先說一下我錯誤的想法:
很容易就把這個題轉化成了給\(K_n\)的完全圖上的邊進行二著色的問題,然後,由於在組合數學課程中經常接觸到多邊形著色,所以我就把這個題錯誤的轉化成了在一個正\(\frac{n(n-1)}{2}\)邊形的頂點上進行二著色的問題。然而對於n=1,2,3這種方法都是可行的,但是到了n=4的情況,這種方法就不可行了。我仔細觀察了一下,發現這個轉化不符合滿足純粹性和完備性

。。。
然後就說一下正解吧。
首先我們考慮n = 4的情況,對於\(K_4\)進行二著色,我們很容易發現,由於圖是可以任意扭轉的,所以它的置換群實際上是一個對稱群
那麼對於點的每一個置換我們要計算對應的邊的置換。
在一個置換中,考察一條邊,如果這條邊的兩個節點位於相同的迴圈中,那麼我們可以得出邊的迴圈個數是點的迴圈個數的一半。
如果這條邊的兩個節點位於不同的迴圈中,那麼我們畫一畫圖就可以知道如果點的迴圈個數分別是a, b,那麼邊的迴圈個數就是gcd(a,b)。
根據這樣的方法,我們就可以把點的置換轉化為邊的置換了。
下面的任務就是要列舉置換。
如果直接暴力,複雜度很高。
我們考慮這樣的列舉(回溯)方法:
依次考慮每一種階的迴圈的個數,然後暴力dfs即可。
現在假設我們已經列舉好了一個置換,那麼這種置換的個數根據一些基本的排列組合知識,可以知道是:
\[\frac{n!}{\prod_{i = 1}^{cnt} Val_i * Num_i !}\]

稍微解釋一下這個式子。除以\(Val_i\)是因為圓形排列,除以\(Num_i\)是因為同階迴圈的重複排列。

根據Polya定理,等價類的個數就是:
\[l = \frac{1}{N!} * \sum 2^m\]

參考題解

事實上,這個題還有一個變態的做法:
就是上OEIS上查詢通項公式。。。。

程式碼

#include <bits/stdc++.h>
const int mod = 997;
const int maxn = 1010;
using namespace std;
int n, cnt, ans;
int two[maxn], factor[maxn], val[maxn], num[maxn];
int pow(int n, int m) {
    int ans = 1;
    int b = m;
    while(b) {
        if(b & 1) ans = (ans * n) % mod;
        b >>= 1;
        n = (n*n) % mod;
    }
    return ans;
}
int inv(int n) {
    return pow(n, mod-2);
}
int gcd(int a, int b) {
    if(b == 0) return a;
    else return gcd(b, a%b) % mod;
}
void init() {
    factor[0] = factor[1] = two[0] = 1;
    for(int i = 2; i <= 1000; i++) {
        factor[i] = ((i % mod) * factor[i-1]) % mod;
    }
    for(int i = 1; i <= 1000; i++) {
        two[i] = (two[i-1] * 2) % mod;
    }
}
void dfs(int now_num, int left) {
    if(left == 0) {
        int sum1 = 0, sum2 = 1;
        //sum1:這一種置換的迴圈個數
        //sum2:這一種置換的個數
        for(int i = 1; i <= cnt; i++) {
            sum1 += (num[i] * (num[i] - 1) / 2 * val[i]) + (val[i]/2 * num[i]); 
            //前一部分:對於同一種迴圈中的不同迴圈的邊的處理
            for(int j = i + 1; j<= cnt; j++) {
                sum1 += num[i] * num[j] * gcd(val[i], val[j]);
            }
        }
        for(int i = 1; i <= cnt; i++) {
            sum2 = (sum2 * pow(val[i], num[i])%mod*factor[num[i]])%mod;
        }
        sum2 = inv(sum2) * factor[n] % mod;
        ans = (ans + pow(2, sum1) * sum2 % mod) % mod;
    }
    if(now_num > left) return;
    dfs(now_num+1, left);
    //這裡的dfs放到外面可以降低常數:
    //如果放在for迴圈裡面,那麼num陣列中會多出許多0
    //浪費時間。
    for(int i = 1; i * now_num <= left; i++) {
        val[++cnt] = now_num, num[cnt] = i;
        dfs(now_num+1, left - i * now_num);
        cnt--; //回溯
    }
}

int main() {
    scanf("%d", &n);
    init();
    dfs(1, n);
    ans = (ans * inv(factor[n])) % mod;
    printf("%d", ans);
    return 0;
}