QOJ687
Part I
先来看这样一个问题,给定一个 \(n\) 个点,\(m\) 条边的 DAG,求它的拓扑序数。
由于 \(n\) 很小,可以考虑状压。考虑一个点集 \(S\),设 \(f_S\) 为这个点集以及点集内边组成的 DAG 的拓扑序数。我们可以考虑一个朴素的转移:枚举一个点 \(u\),如果这个点不在 \(S\) 内,但是它的出边所相邻的点全在 \(S\) 内,那么我们就可以钦定 \(u\) 在拓扑序的最前面,然后后面接 \(S\) 的拓扑序,这样我们就有转移:
当然我们还有另一种转移思路,我们考虑点 \(u\) 在 \(S\) 内,这样就有:
这里推荐第二种思路,下面放代码。
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;
}
总的来说这是一道非常好的题,好在哪我也讲不清楚。

浙公网安备 33010602011771号