状压 DP 做题笔记
F P4363 [九省联考 2018] 一双木棋 chess
轮廓线 DP +记忆化
我们可以发现一个位置能落子,当且仅当左上角的矩形内部 只有 自己一个空位
由此可得出状态是类似 阶梯形 的
那么我们考虑轮廓线 DP:
不妨用 \(1\) 表示竖边,\(0\) 表示横边
就可以用二进制表示出当前状态
令 \(f[msk]\) 表示这个轮廓线状态 距游戏结束 还能得多少分
状态的转移就是把第 \(i\) 位上的 \(1\) 左移一位即可
这个操作在位运算中可以写为 msk^(3<<i)
注意:边界条件是 f[((1<<n)-1)<<m]=0
Code
#include<bits/stdc++.h>
#define int long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
return x*f;
}
const int N=5+9,M=2e6+509,mod=998244353;
int n,m;
int a[N][N],b[N][N];
unordered_map<int,int> f;
/*
轮廓线DP
一个位置能落子,当且仅当左上角的矩形内部只有自己一个空位
用 1 表示竖边,0 表示横边
状态的转移就是把其中一个 1 左移一位即可
本质就是 01−>10
令 f[msk] 表示这个轮廓线状态距游戏结束还能得多少分
可以得到边界条件 f[((1<<n)-1)<<m]=0
*/
int F(int msk,bool id)
{
if(f.count(msk)) return f[msk];
f[msk]=id?-1e18:1e18;
int x=n,y=0;
fd(i,0,n+m-2)
{
if(msk>>i&1) x--;else y++;//延轮廓线递推
if((msk>>i&3)!=1) continue;//没空位
int nxt=msk^(3<<i);//把 1 左移
if(id) f[msk]=max(f[msk],F(nxt,id^1)+a[x][y]);
else f[msk]=min(f[msk],F(nxt,id^1)-b[x][y]);
}
return f[msk];
}
signed main()
{
//#define FJ
#ifdef FJ
freopen("color2.in","r",stdin);
freopen("color.out","w",stdout);
#endif
//#define io
#ifdef io
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
#endif
n=read(),m=read();
fd(i,0,n-1) fd(j,0,m-1)
a[i][j]=read();
fd(i,0,n-1) fd(j,0,m-1)
b[i][j]=read();
f[((1<<n)-1)<<m]=0;
printf("%lld",F((1<<n)-1,1));
return 0;
}
H P5616 [MtOI2019] 恶魔之树
Hash+DP
显然题意可以转化为求 所有子序列的 \(\operatorname{lcm}\) 之和
注意到 \(1\le s_i\le 300\),去重,设值的个数为 \(m\) 个,并记录每种值的出现次数 \(cnt_i\)
然后可以把质数分为 \(\le \sqrt{300}\) 和 \(> \sqrt{300}\) 两部分
发现在所有 \(s_i\) 中只会出现 至多一个 大质数 \(P_i(P_I>\sqrt{300})\),以每个 \(s_i\) 的 \(P_i\) 为关键字排序(没有就是 \(1\)),令新数组为 \(New\)
然后 \(New\) 就被分成了若干段,每段都有同一个 \(P_i\)
设 \(f_{i,j,0/1}\) 为遍历到 \(New_{i}\),小质数的 \(\operatorname{lcm}\) 为 \(j\),\(P_{i}\) 没选/选时,所有大质数对答案的贡献
下令 \(j'=\operatorname{lcm}(j,\frac{New_{i}}{P_{i}})\)
初始\(f_{0,1,0}=1\)
若 \(P_{i}=P_{i-1}\),则
否则
最后答案就是 \(\sum j(f_{m,j,0}+f_{m,j,1})\)。
式中的 \(j\) 直接作下标会 RE,所以考虑 Hash(预处理所有 \(\operatorname{lcm}\))
比 \(\sqrt{300}\) 小的质数集合 \(P\) 为 \(\{2,3,5,7,11,13,17\}\),所以状态总数为 \(\prod_{p \in P}(k_{p} +1)=17496\)
注意:最好不要用 %mod
,建议用手写模
Code
#include<bits/stdc++.h>
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
return x*f;
}
const int N=3e5+5,M=305,K=2e4+5;
int n,mod,a[N];
int Lcm[K],p[8]={0,2,3,5,7,11,13,17},k=7,pw[N];//lcm prime pow2
int tot,cnt[M],m,f[M][K][2],ans;
unordered_map<int,int> h;//hash
pair<int,int> b[N],t[M];
inline int add(int x,int y)
{
return x+y>=mod?x+y-mod:x+y;
}
inline int mul(int x, int y)
{
return (int)(1ll*x*y%mod);
}
int gcd(int x,int y)
{
return y==0?x:gcd(y,x%y);
}
inline int lcm(int x,int y)
{
return x/gcd(x,y)*y;
}
void init()
{
fd(i,1,n) a[i]=read();
fd(i,1,n)
{
b[i]={1,1};
fd(j,1,k)
{
while(a[i]%p[j]==0)
{
a[i]/=p[j];
b[i].second*=p[j];
}
}
b[i].first=a[i];
}
sort(b+1,b+n+1);
fd(i,1,n)//去重
{
if(b[i]!=b[i-1])
t[++m]=b[i];
cnt[m]++;
}
}
void pre()
{
for(int i2=1;i2<=300;i2*=2)
for(int i3=1;i3<=300;i3*=3)
for(int i5=1;i5<=300;i5*=5)
for(int i7=1;i7<=300;i7*=7)
for(int i11=1;i11<=300;i11*=11)
for(int i13=1;i13<=300;i13*=13)
for(int i17=1;i17<=300;i17*=17)
{
int x=1ll*i2*i3*i5*i7*i11*i13*i17;
Lcm[++tot]=x,h[x]=tot;
}
pw[0]=1;
fd(i,1,n) pw[i]=add(pw[i-1],pw[i-1]);
}
void DP()
{
f[0][h[1]][0]=1;
fd(i,1,m)
{
if(t[i].first==t[i-1].first)
{
fd(j,1,tot)
{
int x=lcm(Lcm[j],t[i].second);
f[i][j][0]=add(f[i][j][0],f[i-1][j][0]);
f[i][j][1]=add(f[i][j][1],f[i-1][j][1]);
f[i][h[x]][1]=add(f[i][h[x]][1],mul(add(mul(f[i-1][j][0],t[i].first),f[i-1][j][1]),pw[cnt[i]]-1));
}
}
else
{
fd(j,1,tot)
{
int x=lcm(Lcm[j],t[i].second);
f[i][j][0]=add(f[i][j][0],add(f[i-1][j][0],f[i-1][j][1]));
f[i][h[x]][1]=add(f[i][h[x]][1],mul(mul(add(f[i-1][j][0],f[i-1][j][1]),t[i].first),pw[cnt[i]]-1));
}
}
}
fd(i,1,tot) ans=add(ans,mul(add(f[m][i][0],f[m][i][1]),Lcm[i]%mod));
}
signed main()
{
// #define FJ
#ifdef FJ
freopen("A.in","r",stdin);
freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
#endif
n=read(),mod=read();
pre();
init();
DP();
printf("%lld",ans);
return 0;
}
J P4484 [BJWC2018] 最长上升子序列
状压+打表
发现 \(n \le 28\),很 打表 的范围,但是如何 快速地暴力 是一个问题
事实上,从左向右推好像不是很可行,考虑对于一个排列,我们把数从小到大插入
那么我们首先令一个\(f_i\)(跟程序没关系)表示,在当前已经确定的一个序列里面,以第 \(i\) 个数结尾的最长上升子序列长度
基于这个数组,我们再令 \(maxL_i\) 表示 前缀最大值,即:
那么对于 \(maxL\) 数组,显然有:
我们可以 差分 \(maxL\),不妨设对其进行差分的数组为 \(c\)
我们把数从小到大插入的时候,对于 \(c\) 数组:
考虑在第 \(i\) 位和第 \(i+1\) 位之间插入了一个新的数,因为是单调地插入的,所以新插入的这个数一定是当前序列的最大数
显然,这个数的 \(maxL\) 一定是 \(maxL_i+1\),因此把 \(c_{i+1}\) 改成 \(1\),而在 \(i\) 之后第一个比 \(a_i\) 大的数,记其位置为 \(pos\),则 \(c_{pos}\) 值肯定也为 \(1\)
但是当我们插入了这个新的数之后,由于在它刚刚插入的不计入 \(f_{pos}\),所以我们应当把 \(c_{pos}\) 置成 \(0\)
那么接下来要做的就是对 \(c\) 数组进行状压 DP
那我们不妨令 \(f_{i,j}\) 表示在一个 \(1\)~\(i\) 的排列里,差分数组 \(c\) 状态为 \(j\) 的方案数,那么答案就是:
也就是 \(\sum\) \((\) 有 \(n\) 个数、状态为\(i\)的方案\(\times\)方案中的 LIS 的长度 \()\)
由于我们状压了 \(c\) 数组,所以每个方案中 LIS 的长度,就是该状态里 \(1\) 的个数
注意:
-
序列 \(f\) 和 \(c\) 仅用来推导
-
记得开滚动数组
-
由于一定存在 \(c_1=1\),实际有 \(n\) 个数的状态只需要记录最后 \(n-1\) 位
Code
#include<bits/stdc++.h>
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
return x*f;
}
const int N=3e5+5,M=1<<27|1,K=2e4+5,mod=998244353;
int n,m;
int *f[2]={new int[M],new int[M]},len[M],ans,fac;
int res[30]={0,1,499122178,2,915057326,540715694,946945688,422867403,451091574,317868537,200489273,976705134,705376344,662845575,331522185,228644314,262819964,686801362,495111839,947040129,414835038,696340671,749077581,301075008,314644758,102117126,819818153,273498600,267588741};
inline int qpow(int x,int y)
{
int re=1;x%=mod;
while(y)
{
if(y&1) (re*=x)%=mod;
(x*=x)%=mod,y>>=1;
}
return re;
}
inline int inv(int x)
{
return qpow(x,mod-2);
}
signed main()
{
// #define FJ
#ifdef FJ
freopen("A.in","r",stdin);
freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
#endif
#define Res
#ifdef Res
//打表直接输出
printf("%lld",res[read()]);
return 0;
#endif
//#define Pre
#ifdef Pre
//打表
freopen("Pre.txt","w",stdout);
int lim=28,T=0;
DO:T=-~T;cerr<<"Now:"<<T<<endl;
n=T-1,m=1<<n;
fd(i,0,m) f[1][i]=f[0][i]=len[i]=0;
#else
n=read()-1,m=1<<n;
#endif
f[0][0]=1;
fd(i,1,n)
{
int cur=i&1;//滚动数组
fd(j,0,1<<i) f[cur][j]=0;//memset
fd(j,0,(1<<(i-1))-1)
{
(f[cur][j<<1]+=f[cur^1][j])%=mod;
int pos=-1;
bd(k,i-1,0)//枚举插入到哪一位
{
int t=((j>>k)<<(k+1))|(1<<k)|(j&((1<<k)-1));
//(j>>k)<<(k+1)是为了先清掉后面几位,不能简写成j<<1
if(j&(1<<k)) pos=k;
if(~pos) t^=(1<<(pos+1));
(f[cur][t]+=f[cur^1][j])%=mod;
}
}
}
fd(i,1,m-1) len[i]=len[i-(i&-i)]+1;
ans=0;fd(i,0,m-1) (ans+=f[n&1][i]*(len[i]+1))%=mod;
fac=1;fd(i,1,n+1) (fac*=i)%=mod;
ans=ans*inv(fac)%mod;
printf("%lld,",ans);
#ifdef Pre
if(T<lim) goto DO;
#endif
return 0;
}
K UOJ #37. 【清华集训2014】主旋律
容斥+状压
简单一点的思路
题目等价于求有多少个边集可以使这张图强连通
即求这张图不强连通的方案数
设 \(Dag[s]\) 表示集合 \(s\) 是一个 \(DAG\),那么我们用全集减去它就是答案
我们再设 \(G[s]\) 表示集合 \(s\) 被划分为奇数个强连通分量的方案数,
\(H[s]\) 表示划分为偶数个强连通分量的方案数
\(E(S,T)\) 表示集合 \(S\) 向集合 \(T\) 连的边数
最后加上自己连自己的方案数是因为我们的容斥系数已经弄好了,只需要让 \(S-s\) 缩完点之后成为一个 \(DAG\) 就行了,所以合法的边集是全集
我们最后的答案 \(f[s]\) 表示集合 \(s\) 强连通的方案数,\(G\) 和 \(H\) 的转移有:
Code
#include "bits/stdc++.h"
#include "whrwlx/modint.h"
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
return x*f;
}
const int N=5+9+2,M=225,K=2e4+5,mod=1e9+7;
int n,m;
modint G[1<<N],H[1<<N],f[1<<N],pw[N*N];
bitset<M> in[1<<N],out[1<<N];
inline int calc(int x,int y)
{
return (out[x]&in[y]).count();
}
signed main()
{
// #define FJ
#ifdef FJ
freopen("A.in","r",stdin);
freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
#endif
n=read(),m=read();
int MS=(1<<n)-1;
pw[0]=1;
fd(i,1,n*n) pw[i]=pw[i-1]*2;
fd(i,1,m)
{
int x=read(),y=read();
fd(j,1,MS)
{
if(j&(1<<(x-1))) out[j][i]=1;
if(j&(1<<(y-1))) in[j][i]=1;
}
}
H[0]=1;
fd(S,1,MS)
{
f[S]=pw[calc(S,S)];
for(int s=(S-1)&S;s;s=(s-1)&S)
{
f[S]=f[S]-(G[s]-H[s])*pw[calc(s,S-s)+calc(S-s,S-s)];
if((s&(S&-S))==0) continue;
G[S]+=f[s]*H[S-s];
H[S]+=f[s]*G[S-s];
}
f[S]=f[S]-(G[S]-H[S]);
G[S]+=f[S];
}
cout<<f[MS].num;
return 0;
}
首先,根据正难则反的思路,合法方案数等于,总方案数 \(2^m\) 减去不合法方案数
不合法:即将图进行缩点之后并不是一个点而是一个 \(DAG\)
那么分为两部分:\(DAG\),以及其中的 \(SCC\)
首先定义 \(D(S)\) 为点集 \(S\) 构成的子图中,为 \(DAG\) 的子图个数
那么一个容斥是:
其中 \(E(S,T)\) 表示出点在点集 \(S\) 入点在点集 \(T\) 的边的数量
然后思考怎么算 SCC。首先定义一个 \(F(S)\) 表示点集 \(S\) 构成的子图中,为强连通图的子图个数(即答案)
接下来引入两个记号:
- \(T \in \text{P}(S,k)\) 表示枚举将 \(S\) 拆分为 \(T_1,T_2,...,T_k\) 的所有方案,保证 $\forall i,j\in [1,k],T_i \cap T_j = \varnothing $
- \(T \in \text{SP}(S,k)\) 表示枚举将 \(S\) 的严格拆分,即保证 \(k>1\)
那么可以列出 \(F\) 的公式:
把 \(D(T)\) 展开,有:
这个式子丝毫的不可做,但我们仔细观察这两个 \(\sum\) ,是枚举划分再枚举子集
我们反过来,先枚举子集,在枚举子集的划分,是完全等价的,那么有:
考虑把 \((-1)^{k+1}\prod_{i=1}^k F(U_i)\) 提到第二个 \(\sum\) 后,发现这样其实可以使 \(U,V\) 两部分分开
设:
代入,得:
注意到 \(\sum_{V\in\text{P}(S\ \^{}\ T,l)} D(V)\prod _{i=1}^l F(V_i)\) 是枚举了 DAG 再将缩掉的点拆开,相当于任意图
所以原式等价于:
得到 \(G\) 的柿子:
其中 \(x\) 任取,因为如果没有 \(x\in T\) 的限制时,会算重
具体的,对于一张图,有一个 \(SCC\) 点集为 \(CCF\),在 \(T=CCF\) 时 \(F(T)\) 被算了一次,在 \(CCF \subseteq S\ \^{}\ T\) 时又可能被算一遍
为什么 \(x \in T\) 可以让我们不重不漏?
-
不重:
考虑两张图相同,必要条件是 \(x\) 所在 \(SCC\) 的编号相同
保证了编号不同,图自然就不重了 -
不漏:
首先 \(x\) 所在 \(SCC\) 会被枚举到,
而其他部分在 \(G(S\ \^{}\ T)\) 已经确定是可以的了
最后 \(F,G\) 一起 DP 就行了
复杂度最优秀可以达到 \(O(3^n)\)?
但是 \(n \le 15\),暴力求 \(E\) 也是可以的
复杂度 \(O(n3^n)\)?
Code
#include<bits/stdc++.h>
#include "whrwlx/modint.h"
#define int long long
#define ll long long
#define fd(i,a,b) for(int i=(a);i<=(b);i=-~i)
#define bd(i,a,b) for(int i=(a);i>=(b);i=~-i)
#define endl '\n'
using namespace std;
inline int read()
{
int x=0,f=1;char c=getchar();
while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+(c-48);c=getchar();}
return x*f;
}
const int N=5+9+2,M=225,K=2e4+5,mod=1e9+7;
int n,m;
modint<int,mod> f[1<<N],g[1<<N],pw[N*N];
//SCC DAG pow_of_2
int e[N],rnk[1<<N];
#define count __builtin_popcount
inline int E(int x,int y)
{
int re=0;
for(;x;x-=(x&-x))
re+=count(e[rnk[x&-x]]&y);
return re;
}
signed main()
{
// #define FJ
#ifdef FJ
freopen("A.in","r",stdin);
freopen("A.out","w",stdout);
#endif
//#define io
#ifdef io
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
#endif
n=read(),m=read();
int MS=(1<<n)-1;
pw[0]=1;
fd(i,1,n*n) pw[i]=pw[i-1]*2;
fd(i,1,m)
{
int x=read(),y=read();
e[x-1]|=(1<<(y-1));
}
fd(i,0,n) rnk[1<<i]=i;
// #define DB
fd(S,1,MS)
{
if(count(S)==1)//只有一个点,没有子集
{
f[S]=1,g[S]=1;
continue;
}
#ifdef DB
bitset<20> D;
D.set(S);
cout<<S<<endl<<":["<<D<<"]"<<endl;
#endif
int x=(S&-S),s=S-x;//见上文 x 的含义
for(int T=s;;T=(T-1)&s)
{
#ifdef DB
bitset<20> D;
D.set(T|x);
cout<<"g["<<D<<"]"<<endl;
#endif
g[S]-=f[(T|x)]*g[S^(T|x)];
//这里 f[S] 还没求出,得先减,之后再把剩的 f[S] 加回来
if(!T) break;
}
for(int T=S;T;T=(T-1)&S)
{
#ifdef DB
bitset<20> D;
D.set(T);
cout<<"f["<<D<<"]"<<endl;
#endif
f[S]+=g[T]*pw[E(S^T,S^T)]*pw[E(S^T,T)];
}
f[S]=pw[E(S,S)]-f[S];//减去 DAG 得到 SCC
g[S]+=f[S];//加回来
#ifdef DB
cout<<endl;
#endif
}
cout<<f[MS].num;
return 0;
}
手搓的 modint.h
template<typename _T,_T _MOD>
struct modint
{
_T Mod=_MOD,num;
modint(_T _Mod=_MOD,_T _num=0)
{
Mod=_Mod,num=_num;
}
inline _T qpow(_T x,_T y)
{
_T re=1;x%=Mod;
while(y)
{
if(y&1) (re*=x)%=Mod;
(x*=x)%=Mod,y>>=1;
}
return re;
}
inline _T inv(_T x)
{
return qpow(x,Mod-2);
}
inline modint operator+(const modint& y)
{
return modint(Mod,num+y.num<Mod?num+y.num:num+y.num-Mod);
}
inline modint operator+(const _T& y)
{
return modint(Mod,num+y<Mod?num+y:num+y-Mod);
}
inline modint operator-(const modint& y)
{
return modint(Mod,num-y.num<0?num-y.num+Mod:num-y.num);
}
inline modint operator-(const _T& y)
{
return modint(Mod,num-y<0?num-y+Mod:num-y);
}
inline modint operator*(const modint& y)
{
return modint(Mod,1ll*num*y.num%Mod);
}
inline modint operator*(const _T& y)
{
return modint(Mod,1ll*num*y%Mod);
}
inline modint operator/(const modint& y)
{
return modint(Mod,1ll*num*inv(y.num)%Mod);
}
inline modint operator/(const _T& y)
{
return modint(Mod,1ll*num*inv(y)%Mod);
}
inline modint operator^(const _T& y)
{
return modint(Mod,qpow(num,y));
}
inline modint operator+=(const modint& y)
{
return (*this)=(*this)+y;
}
inline modint operator+=(const _T& y)
{
return (*this)=(*this)+y;
}
inline modint operator-=(const modint& y)
{
return (*this)=(*this)-y;
}
inline modint operator-=(const _T& y)
{
return (*this)=(*this)-y;
}
inline modint operator*=(const modint& y)
{
return (*this)=(*this)*y;
}
inline modint operator*=(const _T& y)
{
return (*this)=(*this)*y;
}
inline modint operator/=(const modint& y)
{
return (*this)=(*this)/y;
}
inline modint operator/=(const _T& y)
{
return (*this)=(*this)/y;
}
inline void operator=(const _T& y)
{
num=y;
}
};
本文来自博客园,作者:whrwlx,转载请注明原文链接:https://www.cnblogs.com/whrwlx/p/18509614