AGC016F Games on DAG
题意
给定一个 \(n\) 个点 \(m\) 条边的 DAG , 在 \(1,2\) 点各有一个石头 , 每次可以沿 DAG 的边移动一个石头 ( 两个石头可以重合 ) , 不能移动者输 , 求只保留一部分边的 \(2^m\) 种方案中 , 先手必胜的个数 .
对 \(10^9+7\) 取模 .
保证对于每条边 \(u \to v\) 有 \(u<v\) .
\(n\le 15 , m\le \frac{n(n-1)}{2}\) .
题解
题意等价于对每种方案算出 \(1\) , \(2\) 点的 \(SG\) 函数值 , 求 \(SG_1\ne SG_2\) 的情况数 .
考虑在拓扑排序过程中状压 dp , 发现转移要用到当前确定的所有点的 \(SG\) 值 , 也就是说状态只能记录所有 \(SG\) 值 , 这和暴力没有区别 .
换一个角度 , 整体把 \(SG\) 函数相同的所有点分到同一层 , 一次直接填同一层的所有点 . 考虑当前填入的 \(SG\) 值是 \(i\) , 要覆盖的集合是 \(T\) . 此时所有点被分成三部分 , \(SG\) 函数是 \([0,i-1]\) 的已经填完的集合 \(S\) , 当前集合 \(T\) , 以及还没确定的集合 \(Q=\complement(S\cup T)\) .
考虑这个划分是否合法 , 将原图中的边分类讨论 :
- \(T\) 内部的边 , 全都不能取 .
- \(T\to S\) 的边 , 要求 \(T\) 内部的每个点 \(u\) 能到达的点中 , \(SG\) 为 \([0,i-1]\) 都要有 .
- \(T\to Q\) 的边 , 没有影响 , 可以任意取 .
除此之外还有个题目条件 \(SG_1\ne SG_2\) , 直接保证 :
- \(1,2\) 不同时出现在 \(T\) 中 .
第一部分不用管 , 第三部分可以直接 \(2^k\) 计算系数 , 问题出在了第二部分 , 这又要求我们记录具体的点的 \(SG\) 值了 , 这是我们不想要的 .
所以把第二部分转化一下 , 从限制 \(T\) 里的点 , 变成限制 \(S\) 里的点 :
- 设 \(SG\) 函数为 \(j\) 的集合为 \(S_j\) , 对 \(T\) 中所有点都要有 \(S_j\to T\) 的边 .
把限制转移到填 \(SG\) 函数这一步 , 就变成了 :
- 对于 \(Q\) 内部的所有点 , 要有 \(T\to Q\) 的边 .
这样就好处理了 , 枚举 \(Q\) 中每个点 , 如果没有 \(T\to Q\) 的边直接不可行 , 否则至少保留一条 , 系数是 \(2^k-1\) .
状态设 \(f_{i,S}\) 为填完 \(SG=i\) 的部分后 , 已经填完了 \(S\) 的情况数 , 每次枚举填 \(i+1\) 的集合 \(T\) 转移 .
复杂度 , 每次转移相当于子集枚举是 \(3^n\) , 单次转移需要枚举点 , 是 \(O(n)\) 的 , 总共 \(n\) 层 , 复杂度 \(O(n^23^n)\) .
考虑优化 , 单次转移的 $O(n) $ 可以通过预处理去除 , 但是要求的空间是 \(n3^n\) 过大 .
优化状态 , 状态里的 \(i\) 没有起什么作用 , 我们并不关心具体的 \(SG\) 的值 , 把状态直接简化为 \(f_S\) , 表示填完了 \(S\) 的状态数 .
这里可以理解为 \(f_S=\sum \limits_ i f'_{i,S}\) , 每次转移相当于整体进行了 \(f'_{i,S} \to f'_{i+1,S \cup T}\) , 因此是正确的 .
复杂度是 \(O(n3^n)\) , 可以通过 .
点击查看代码
#include<bits/stdc++.h>
#define file(x) freopen(#x ".in","r",stdin),freopen(#x ".out","w",stdout)
#define ll long long
#define INF 0x3f3f3f3f
#define INF64 1e18
using namespace std;constexpr int N=20;
constexpr int M=(1<<15)+5;
constexpr ll p=1e9+7;int to[N],cnt[M];int n,m;ll f[M],num[N*N];int main(){ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin>>n>>m;for(int i=1;i<=m;i++){int u,v;cin>>u>>v;to[u]|=(1<<(v-1));}for(int s=0;s<=(1<<n)-1;s++) cnt[s]=__builtin_popcount(s);num[0]=1;for(int i=1;i<=n*n;i++) num[i]=num[i-1]*2%p;f[0]=1;for(int s=0;s<=(1<<n)-1;s++){if(!f[s]) continue;int T=(s^((1<<n)-1));for(int t=T;t;t=(t-1)&T){if((t&1)&&(t&2)) continue;ll k=1;for(int i=1;i<=n;i++){if(s&(1<<(i-1))) continue;if(t&(1<<(i-1))){k=k*num[cnt[to[i]&(((1<<n)-1)^s^t)]]%p;}else{if((to[i]&t)==0) k=0;else k=k*(num[cnt[to[i]&t]]-1)%p;}}f[s|t]=(f[s|t]+f[s]*k%p)%p;}}cout<<f[(1<<n)-1];
}
总结
这道题的关键是设计一个能方便地进行 \(SG\) 转移的状态 , 最后通过按值域分层找到了一个能状压转移的状态 . 此外 , 把不容易处理的限制转化成容易处理的限制非常重要 .
不过似乎普遍的题解做法是拿整体减钦定 \(1\) , \(2\) 不同的情况 , 并且大多直接设计了 \(f_S\) 的状态 . 自己想的过程额外设计了 \(i\) 维度 , 后来多一个 \(n\) 怎么也去不掉的时候才发现 \(i\) 没有意义 . 不管思路是怎样的 , 最终都导向了相同的状态 . 体现了状态设计一定要抓住一个局面有关键性意义的信息 .