思路
(dp[i][j]:)表示状态为(i),(1)表示已走到这个点,(0)表示未走到这个点,此时站在(j)点上的所有方案数。
发现我们在计算的时候可能会出现重复的情况,例如在计算(1,2,3)这个环时先按照顺序计算了(1,2,3),又计算了(2,3,1),这样可以发现是重复的情况。所以需要dp的限制就变成了:状态为(i),此时站在点(j)上,且这种状态的起点为状态(i)中的第一个为1的值。
接下来考虑转移,先考虑不合法的状态, 假设但前点在(j),接下来要走的点为(k),状态(i)的起点为(s),如果发现(k<s),那么就说明不能走到起点之前。
考虑合法状态:
假设已经在(j)点上,考虑(j)能到的点。如果(k)点没有到过,那么转移方程即为(dp[i|(1<<k)]+=dp[i][j])。
考虑到过已经到过的点:此时(j)走过去的(k)点恰好是(s)点,那么就说明走成了一个环,就可以对答案产生贡献值(res+=dp[i][j])。
如果走过去的(k)点不是(s)点,那么其产生的贡献会在(s=k)的那些状态中计算,不用考虑。
最后需要减去二元环的所有数即总数减边数(res-=m)。
因为我们计算的时候没有考虑顺时针或者逆时针顺序(例如(1-2-3,1-3-2)),所以对最后的答案还要除以2:(res/=2)。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, LL> PIL;
typedef pair<int, int> PII;
typedef pair<int, double> PID;
typedef unsigned long long ULL;
#define x first
#define y second
const int N = 500 + 10, M = 1e5 + 10;
const double PI = acos(-1.0);
const double eps = 1e-5;
const int mod = 1000000007;
const int inf = 0x3f3f3f3f;
const LL INF = 0x3f3f3f3f3f3f3f3f;
#define gcd __gcd
set<int> e[N];
LL dp[(1 << 19) + 10][20];
void solve() {
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i++) {
int u, v;
scanf("%d%d", &u, &v);
u--; v--;
e[u].insert(v);
e[v].insert(u);
}
LL res = 0;
for(int i = 0; i < n; i++) {
dp[1 << i][i] = 1;
}
for(int i = 1; i < (1 << n); i++) {
vector<int> vec;
int mx = 20;
for(int j = 0; j < n; j++) {
if((i >> j) & 1) {
vec.push_back(j);
mx = min(mx, j);
}
}
for(auto u : vec) {
for(auto v : e[u]) {
if(v < mx) continue;
if(!((1 << v) & i)) {
dp[i | (1 << v)][v] = dp[i | (1 << v)][v] + dp[i][u];
}
else {
if(v == mx) {
res += dp[i][u];
}
}
}
}
}
printf("%lld
", (res - m) / 2);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
#endif // ONLINE_JUDGE
solve();
return 0;
}