hdu5304 基环 外向树
题意:
一个无向图,n个点,m条边,要求删掉m-n条边使得图任然连通,问方案数。
思路:
留下n条边,相当于一个树+一条边,这样就有一个唯一的环。
枚举这个环并看成一个点,就相当于做生成树计数
1、找环
s是一个二进制状态,u为s中编号最小的点
dp[s][v] 以u为起点,经过s中所有的点,并以u结尾的路径有多少条
这样的环如果u,v有直接的边相连,再加上dp[s][v]里的路径,就可以得到若干个环
每个环顺时针被算一次,逆时针被算一次,所以要除以2
2、生成树计数
构造基尔霍夫矩阵,环中的点看成一个点
这样可能会有重边,必须要保留,基尔霍夫矩阵可以计算重边的情况
高斯消元的时候不是实数 要取模
#include <cstdio> #include <cstring> #include <vector> using namespace std; typedef long long ll; const int N = 17; const int S = 1<<N; const int mod = 998244353; ll dp[S+2][N+2], num[S+2]; int n, m, lim; int g[N+5][N+5], id[N+5]; ll f[N+5][N+5]; int t[N+5], st[S+2]; vector<int> ch[S+2]; inline void add(ll& x, ll y){ x += y; if (x >= mod) x -= mod; if (x < 0) x += mod; } inline ll gcd(ll p, ll q){ ll r; while (q){ r = p%q; p = q; q = r; } return p; } inline ll fpow(ll x, int n){ ll ret = 1; while (n){ if (n&1) ret = ret*x%mod; x = x*x%mod; n >>= 1; } return ret; } inline void prepare(){ lim = 1<<N; for (int i=1; i<=N; i++) t[i] = 1<<i-1; for (int s=0; s<lim; s++){ ch[s].clear(); for (int i=1; i<=N; i++) if (s&t[i]){ if (ch[s].size() == 0) st[s] = i; ch[s].push_back(i); } } } inline void circle(){ for (int s=0; s<lim; s++) for (int i=0; i<ch[s].size(); i++){ int u = ch[s][i]; if (!dp[s][u]) continue; for (int v=1; v<=n; v++) if (v>st[s] && (!(s&t[v])) && g[u][v]){ dp[s|t[v]][v] += dp[s][u]; } } for (int s=0; s<lim; s++) if(ch[s].size()>=3){ int u = st[s]; for (int i=0; i<ch[s].size(); i++){ int v = ch[s][i]; if (v==u || !dp[s][v] || !g[u][v]) continue; num[s] += dp[s][v]; } num[s] >>= 1; num[s] %= mod; } } ll gauss(ll a[][N+5], int n){ for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) a[i][j] = a[i][j]%mod; ll ret = 1; for (int i=2; i<=n; i++){ for (int j=i+1; j<=n ;j++) while (a[j][i]){ // 类似与辗转相除的消元 ll t = a[i][i]/a[j][i]; for (int k=i; k<=n; k++) add(a[i][k], -t*a[j][k]%mod); for (int k=i; k<=n; k++) swap(a[i][k], a[j][k]); ret = -ret; } if (!a[i][i]) return 0; ret = ret*a[i][i]%mod; } return (ret+mod)%mod; // for (int i=1; i<=n; i++) // for (int j=1; j<=n; j++){ // if (a[i][j]<0) a[i][j] += mod; // } // ll ret = 1, val = 1; // for (int i=1; i<n; i++){ // if (a[i][i]<0){ // ret = -ret; // for (int j=i; j<=n; j++) a[i][j] = -a[i][j]; // } // for (int j=i+1; j<n; j++) if (a[j][i]){ // if (a[j][i]<0){ // ret = -ret; // for (int k=i; k<=n; k++) a[j][k] = -a[j][k]; // } // ll div = gcd(a[i][i], a[j][i]); // ll tmp = a[i][i]/div; // ll num = a[j][i]/div; // val = val*tmp%mod; // for (int k=i; k<=n; k++) // a[j][k] = (a[j][k]*tmp-a[i][k]*num+mod)%mod; // } // if (a[i][i] == 0) return 0; // ret = ret*a[i][i]%mod; // } // if (val!=1) ret = ret*fpow(val, mod-2)%mod; // return (ret+mod)%mod; } inline void solve(){ ll ans = 0; for (int s=0; s<lim; s++) if (num[s]){ memset(id, 0, sizeof(id)); int sign = 1; for (int i=1; i<=n; i++){ if (s&t[i]) id[i] = 1; // s中的点缩成编号为1的新点 else id[i] = ++sign; } memset(f, 0, sizeof(f)); for (int i=1; i<=n; i++) for (int j=i+1; j<=n; j++) if(g[i][j] && id[i]!=id[j]){ f[id[i]][id[i]] ++; // 对角上度数+1 f[id[j]][id[j]] ++; f[id[i]][id[j]] --; // id[i]与id[j]之间多一条边 f[id[j]][id[i]] --; } ll tmp = gauss(f, sign); add(ans, num[s]*tmp%mod); // num[s]表示s可以构造出环的个数 // tmp是s缩成点后的生成树数目 } printf("%I64d\n", ans); } int main(){ freopen("1005.in", "r", stdin); freopen("1005.out", "w", stdout); prepare(); while (scanf("%d %d", &n, &m) != EOF){ lim = 1<<n; memset(dp, 0, sizeof(dp)); memset(g, 0, sizeof(g)); int a, b, c; for (int i=1; i<=m; i++){ scanf("%d %d", &a, &b); g[a][b] = g[b][a] = 1; c = max(a, b); dp[t[a]|t[b]][c] = 1; } memset(num, 0, sizeof(num)); circle(); solve(); } return 0; }