1. 程式人生 > 實用技巧 >題解 射命丸文的筆記

題解 射命丸文的筆記

題目傳送門

題目大意

  • 定義-競賽圖:任意兩點之間有一條有向邊的圖。

  • 定義-哈密頓迴路:指除起點和終點外經過所有頂點恰好一次且起點和終點相同的路徑。

  • 求出\(m\),求出對於\(\forall n\in[1,m]\),存在哈密頓迴路的競賽圖其中哈密頓迴路的期望個數。

  • \(m\le 10^5\)

思路

做這道題主要是練習一下分治\(\texttt{FFT}\),但是發現了多項式求逆的巨大的時間優勢。

我們發現,其實哈密頓迴路的總個數其實是很好求的,答案為\((n-1)!2^{\binom{n}{2}-n}\),解釋就是環的個數為\((n-1)!\)個,除了環的邊你愛怎麼選怎麼選。

於是我們的問題就是如何求出至少存在一個哈密頓迴路的競賽圖有多少個。我們設\(n\)個點的至少存在一個哈密頓迴路的競賽圖有多少個,我們可以得到轉移式:

\[f[n]=2^{\binom{n}{2}}-\sum_{i=1}^{n-1} f[i]2^{\binom{n-i}{2}}\binom{n}{i} \]

解釋就是簡單容斥,我們先求出總的競賽圖個數,然後減去不存在哈密頓迴路的個數。我們列舉\(i\)個點為一個哈密頓迴路,然後其他點往這\(i\)個點往連邊,然後其他點之間隨便怎麼連都可以。

這個時候有兩種方法可以做。第一種比較顯然,就是直接分治\(\texttt {FFT}\),時間複雜度\(\Theta(n\log^2 n)\)

,程式碼當然也非常好寫。

#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline", "no-stack-protector", "unroll-loops")
#pragma GCC diagnostic error "-fwhole-program"
#pragma GCC diagnostic error "-fcse-skip-blocks"
#pragma GCC diagnostic error "-funsafe-loop-optimizations"

#include <bits/stdc++.h>
using namespace std;

#define SZ(x) ((int)x.size())
#define Int register int
#define mod 998244353
#define MAXN 300005

int mul (int a,int b){return 1ll * a * b % mod;}
int dec (int a,int b){return a >= b ? a - b : a + mod - b;}
int add (int a,int b){return a + b >= mod ? a + b - mod : a + b;}
int qkpow (int a,int k){
	int res = 1;for (;k;k >>= 1,a = 1ll * a * a % mod) if (k & 1) res = 1ll * res * a % mod;
	return res;
}
int inv (int x){return qkpow (x,mod - 2);}

typedef vector <int> poly;

int w[MAXN],rev[MAXN];

void init_ntt (){
	int lim = 1 << 18;
	for (Int i = 0;i < lim;++ i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << 17);
	int Wn = qkpow (3,(mod - 1) / lim);w[lim >> 1] = 1;
	for (Int i = lim / 2 + 1;i < lim;++ i) w[i] = mul (w[i - 1],Wn);
	for (Int i = lim / 2 - 1;i;-- i) w[i] = w[i << 1];
}

void ntt (poly &a,int lim,int type){
#define G 3
#define Gi 332748118
	static unsigned long long d[MAXN];
	for (Int i = 0,z = 18 - __builtin_ctz(lim);i < lim;++ i) d[rev[i] >> z] = a[i];
	for (Int i = 1;i < lim;i <<= 1)
		for (Int j = 0;j < lim;j += i << 1)
			for (Int k = 0;k < i;++ k){
				int x = d[i + j + k] * w[i + k] % mod;
				d[i + j + k] = d[j + k] + mod - x,d[j + k] += x;
			}
	for (Int i = 0;i < lim;++ i) a[i] = d[i] % mod;
	if (type == -1){
		reverse (a.begin() + 1,a.begin() + lim);
		for (Int i = 0,Inv = inv (lim);i < lim;++ i) a[i] = mul (a[i],Inv);
	}
#undef G
#undef Gi 
}

poly operator * (poly a,poly b){
	int d = SZ (a) + SZ (b) - 1,lim = 1;while (lim < d) lim <<= 1;
	a.resize (lim),b.resize (lim);
	ntt (a,lim,1),ntt (b,lim,1);
	for (Int i = 0;i < lim;++ i) a[i] = mul (a[i],b[i]);
	ntt (a,lim,-1),a.resize (d);
	return a;
}

template <typename T> inline void read (T &t){t = 0;char c = getchar();int f = 1;while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}while (c >= '0' && c <= '9'){t = (t << 3) + (t << 1) + c - '0';c = getchar();} t *= f;}
template <typename T,typename ... Args> inline void read (T &t,Args&... args){read (t);read (args...);}
template <typename T> inline void write (T x){if (x < 0){x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}

poly F,G,f,g;
int n,fac[MAXN],ifac[MAXN];

void cdq (int l,int r){
	if (l == r)return F[l] = dec (G[l],F[l]),void ();
	int mid = (l + r) >> 1;cdq (l,mid);
	f.resize (mid - l + 1);for (Int i = l;i <= mid;++ i) f[i - l] = F[i];
	g.resize (r - l + 1);for (Int i = 1;i <= r - l;++ i) g[i] = G[i];
	f = f * g;for (Int i = mid + 1;i <= r;++ i) F[i] = add (F[i],f[i - l]);cdq (mid + 1,r);
}

signed main(){
	init_ntt(),read (n);
	fac[0] = 1;for (Int i = 1;i <= n;++ i) fac[i] = mul (fac[i - 1],i);
	ifac[n] = inv (fac[n]);for (Int i = n;i;-- i) ifac[i - 1] = mul (ifac[i],i);
	F.resize (n + 1),G.resize (n + 1);for (Int i = 1;i <= n;++ i) G[i] = mul (qkpow (2,1ll * i * (i - 1) / 2 % (mod - 1)),ifac[i]);cdq (0,n);
	for (Int i = 1;i <= n;++ i){
		F[i] = mul (F[i],fac[i]);
		if (i == 1) puts ("1");else if (i == 2) puts ("-1");else write (mul (fac[i - 1],mul (qkpow (2,1ll * i * (i - 3) / 2 % (mod - 1)),inv (F[i])))),putchar ('\n');
	} 
	return 0;
}

另外一種辦法便是使用多項式求逆。我們發現遞推式可以化成這個樣子:

\[\sum_{i=1}^{n} f[i]\binom{n}{i}2^{\binom{n-i}{2}}=2^{\binom{n}{2}} \]

這樣可以主要是因為\(\binom{0}{2}=0\)

然後我們發現如果我們設:

\[G(x)=\sum_{i=1}^{\infty} 2^{\binom{i}{2}}\frac{x^i}{i!} \]

那麼可以得到:

\[G(x)=G(x)F(x)+[x^0]G(x) \]

其中\(F(x)\)\(f[1,2,...,n]\)\(\texttt{EGF}\)

於是,我們就可以得到:

\[F(x)=1-\frac{[x^0]G(x)}{G(x)} \]

時間複雜度\(\Theta(n\log n)\)。程式碼也很簡單(只要你提前封裝好了)

#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline", "no-stack-protector", "unroll-loops")
#pragma GCC diagnostic error "-fwhole-program"
#pragma GCC diagnostic error "-fcse-skip-blocks"
#pragma GCC diagnostic error "-funsafe-loop-optimizations"

#include <bits/stdc++.h>
using namespace std;

#define SZ(x) ((int)x.size())
#define Int register int
#define mod 998244353
#define MAXN 300005

int mul (int a,int b){return 1ll * a * b % mod;}
int dec (int a,int b){return a >= b ? a - b : a + mod - b;}
int add (int a,int b){return a + b >= mod ? a + b - mod : a + b;}
int qkpow (int a,int k){
	int res = 1;for (;k;k >>= 1,a = 1ll * a * a % mod) if (k & 1) res = 1ll * res * a % mod;
	return res;
}
int inv (int x){return qkpow (x,mod - 2);}

typedef vector <int> poly;

int w[MAXN],rev[MAXN];

void init_ntt (){
	int lim = 1 << 18;
	for (Int i = 0;i < lim;++ i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << 17);
	int Wn = qkpow (3,(mod - 1) / lim);w[lim >> 1] = 1;
	for (Int i = lim / 2 + 1;i < lim;++ i) w[i] = mul (w[i - 1],Wn);
	for (Int i = lim / 2 - 1;i;-- i) w[i] = w[i << 1];
}

void ntt (poly &a,int lim,int type){
#define G 3
#define Gi 332748118
	static unsigned long long d[MAXN];
	for (Int i = 0,z = 18 - __builtin_ctz(lim);i < lim;++ i) d[rev[i] >> z] = a[i];
	for (Int i = 1;i < lim;i <<= 1)
		for (Int j = 0;j < lim;j += i << 1)
			for (Int k = 0;k < i;++ k){
				int x = d[i + j + k] * w[i + k] % mod;
				d[i + j + k] = d[j + k] + mod - x,d[j + k] += x;
			}
	for (Int i = 0;i < lim;++ i) a[i] = d[i] % mod;
	if (type == -1){
		reverse (a.begin() + 1,a.begin() + lim);
		for (Int i = 0,Inv = inv (lim);i < lim;++ i) a[i] = mul (a[i],Inv);
	}
#undef G
#undef Gi 
}

poly operator * (poly a,poly b){
	int d = SZ (a) + SZ (b) - 1,lim = 1;while (lim < d) lim <<= 1;
	a.resize (lim),b.resize (lim);
	ntt (a,lim,1),ntt (b,lim,1);
	for (Int i = 0;i < lim;++ i) a[i] = mul (a[i],b[i]);
	ntt (a,lim,-1),a.resize (d);
	return a;
}

poly inv (poly a,int n){
	poly b(1,inv (a[0])),c;
	for (Int l = 4;(l >> 2) < n;l <<= 1){
		c.resize (l >> 1);
		for (Int i = 0;i < (l >> 1);++ i) c[i] = i < n ? a[i] : 0;
		c.resize (l),b.resize (l);
		ntt (c,l,1),ntt (b,l,1);
		for (Int i = 0;i < l;++ i) b[i] = mul (b[i],dec (2,mul (b[i],c[i])));
		ntt (b,l,-1),b.resize (l >> 1);
	}
	b.resize (n);
	return b;
}

poly inv (poly a){return inv (a,SZ (a));}

template <typename T> inline void read (T &t){t = 0;char c = getchar();int f = 1;while (c < '0' || c > '9'){if (c == '-') f = -f;c = getchar();}while (c >= '0' && c <= '9'){t = (t << 3) + (t << 1) + c - '0';c = getchar();} t *= f;}
template <typename T,typename ... Args> inline void read (T &t,Args&... args){read (t);read (args...);}
template <typename T> inline void write (T x){if (x < 0){x = -x;putchar ('-');}if (x > 9) write (x / 10);putchar (x % 10 + '0');}

poly F;
int n,fac[MAXN],ifac[MAXN];

signed main(){
	init_ntt(),read (n);
	fac[0] = 1;for (Int i = 1;i <= n;++ i) fac[i] = mul (fac[i - 1],i);
	ifac[n] = inv (fac[n]);for (Int i = n;i;-- i) ifac[i - 1] = mul (ifac[i],i);
	F.resize (n + 1);for (Int i = 0;i <= n;++ i) F[i] = mul (qkpow (2,1ll * i * (i - 1) / 2 % (mod - 1)),ifac[i]);
	F = inv (F);for (Int i = 0;i <= n;++ i) F[i] = mul (mod - F[i],fac[i]);F[0] = add (F[0],1);
	for (Int i = 1;i <= n;++ i) if (i == 1) puts ("1");else if (i == 2) puts ("-1");else write (mul (fac[i - 1],mul (qkpow (2,1ll * i * (i - 3) / 2 % (mod - 1)),inv (F[i])))),putchar ('\n');
	return 0;
}

兩者其實時間差距還是蠻大的,附上一張對比圖吧。

分治\(\texttt{FFT}\)

多項式求逆: