QOJ687

原题

Part I

先来看这样一个问题,给定一个 \(n\) 个点,\(m\) 条边的 DAG,求它的拓扑序数。

这题

由于 \(n\) 很小,可以考虑状压。考虑一个点集 \(S\),设 \(f_S\) 为这个点集以及点集内边组成的 DAG 的拓扑序数。我们可以考虑一个朴素的转移:枚举一个点 \(u\),如果这个点不在 \(S\) 内,但是它的出边所相邻的点全在 \(S\) 内,那么我们就可以钦定 \(u\) 在拓扑序的最前面,然后后面接 \(S\) 的拓扑序,这样我们就有转移:

\[f_{S \cup \{u\}} \leftarrow f_{S \cup \{u\}}+f_S \]

当然我们还有另一种转移思路,我们考虑点 \(u\)\(S\) 内,这样就有:

\[f_S \leftarrow f_S+f_{S \backslash \{u\}} \]

这里推荐第二种思路,下面放代码。

int n,m,nxt[20],f[(1<<16)+5];
signed main(){
//	freopen("114514.in","r",stdin);
//	freopen("114514.out","w",stdout);
	n=read(),m=read();
	for(rg int u,v,i=1;i<=m;i++){
		u=read(),v=read();u--,v--;
		nxt[u]|=(1<<v);
	}
	f[0]=1;
	int U=(1<<n)-1;
	for(rg int S=1;S<=U;S++){
		for(rg int i=0;i<n;i++){
			if((S&nxt[i])==nxt[i])
				f[S]+=f[S^(1<<i)];
		}
	}
	write(f[U]);
	return 0;
}

这里可以不用判 \(u\) 是否在 \(S\) 内,因为集合是从小到大枚举的,如果 \(u \notin S\),后面 f[S^(1<<i)]f[S] 没有贡献。

Part II

你可能想到了,为什么一定要考虑出边,我就不能考虑入边,然后钦定 \(u\) 在拓扑序的最后面吗?这也是正确的,而且也是本题需要考虑的东西。现在我们定义,考虑出边的转移方式形成的 \(f\) 称为前缀拓扑序数,考虑入边的转移方式形成的 \(g\) 称为后缀拓扑序数。

对于每个 \(u,v\),在多少种拓扑序中,\(v\)\(u\) 前面?我们考虑枚举 \(u\),对于每个 \(u\),我们可以给图分成三个点集 \(S,\{u\},T\),它们满足两两之交为 \(\varnothing\),且 \(S \cup \{u\} \cup T=U\)。其中 \(S\) 中的点拓扑序全在 \(u\) 前面,\(T\) 中拓扑序的点全在 \(u\) 后面,为了满足这个条件,必须满足所有 \(u\) 的入边相连的点全在 \(S\) 中,所以 \(u\) 的出边相连的点全在 \(T\) 中。

那么对于这种划分,它对点集 \(S\) 里的所有点的答案的贡献都为 \(f_S \times g_T\),但是这种转移方式我们要枚举 \(u\),枚举 \(S\),再枚举 \(S\) 中的所有点,复杂度为 \(O(n^2 2^n)\),在多测的情况下可能过不了(当然如果你极限卡过了算你牛逼)。

考虑优化,我们可以利用累加效应延迟贡献,有点类似线段树里的 pushdown 操作。我们设一个 \(dp_S\)\(f_S \times g_T\) 的贡献,然后我们从 \(S\) 里选一个点 \(v\),把 \(dp_S\) 贡献到 \(\{v\}\)\(S \backslash \{v\}\),然后再把 \(dp_S\) 清掉,当然如果 \(S\) 中只剩一个点就不用管了。为了方便起见,我们可以选择 \(v\)\(\operatorname{lowbit(S)}\),这样我们把复杂度优化到了 \(O(n2^n)\)

Code

#include<bits/stdc++.h>
#define int long long
#define gc getchar
#define pc putchar
#define rg register
#define LB lower_bound
#define UB upper_bound
#define PII pair<int, int>
#define PDI pair<double, int>
#define fi first
#define se second
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
using db=double;using ll=long long;
using ull=unsigned long long;
using namespace std;
const ll INF=1e18;
const int inf=0x3f3f3f3f;
namespace IO{
	inline int read(){
		int x=0,f=1;
		char ch=gc();
		while(!isdigit(ch)){
			if(ch=='-') f=-f;
			ch=gc();
		} 
		while(isdigit(ch)) x=(x<<1)+(x<<3)+ch-'0',ch=gc();
		return x*f;
	}
	inline void write(int x){
		if(x<0) pc('-'),x=-x;
		if(x>9) write(x/10);
		pc(x%10+'0');
	}
}using namespace IO;
int pre[25],nxt[25],n,m;
int f[(1<<20)+5],g[(1<<20)+5],dp[(1<<20)+5];
inline int lbt(int x){return x&-x;}
inline void solve(){
	n=read(),m=read();
	for(rg int i=0;i<=n;i++) pre[i]=nxt[i]=0;
	for(rg int u,v,i=1;i<=m;i++){
		u=read(),v=read();u--,v--;
		nxt[u]|=(1<<v),pre[v]|=(1<<u);
	}
	int U=(1<<n)-1;
	for(rg int S=0;S<=U;S++) f[S]=g[S]=0;
	f[0]=g[0]=1;
	for(rg int S=1;S<=U;S++){
		for(rg int i=0;i<n;i++){
			if((S&pre[i])==pre[i]) f[S]+=f[S^(1<<i)];
			if((S&nxt[i])==nxt[i]) g[S]+=g[S^(1<<i)];
		}
	}
	for(rg int i=0;i<n;i++){
		for(rg int S=0;S<=U;S++) dp[S]=0;
		for(rg int S=U;S>=0;S--){
			if(!(S&(1<<i))&&(S&pre[i])==pre[i]){
				int T=U^S^(1<<i);
				if((T&nxt[i])==nxt[i]) dp[S]+=f[S]*g[T];
			}
			if(S!=lbt(S)) dp[S^lbt(S)]+=dp[S],dp[lbt(S)]+=dp[S],dp[S]=0;
		}
		for(rg int j=0;j<n;j++) write(dp[1<<j]),pc(' ');
		puts("");
	}
}
signed main(){
//	freopen("114514.in","r",stdin);
//	freopen("114514.out","w",stdout);
	int T=read();
	while(T--) solve();
	return 0;
}

总的来说这是一道非常好的题,好在哪我也讲不清楚。

\[End \]

posted @ 2026-08-09 12:01  bbbzzx  阅读(2)  评论(0)    收藏  举报