状压dp
将一个集合状态按位压缩为一个整数,在集合状态间的动态规划。
经常用来处理状态数为 $ 2^n$ 级别的题目,一般 \(n\leq23\)。特点就是 \(n\) 极小。
- 有时会结合容斥原理排除不可能的情况。
- 有时会用根号分治缩小 \(n\) 的范围
状态操作
对于压缩后的状态,在转移的过程中,会用到一些位运算操作,例如:
取一个集合的最低位(lowbit)
int lowbit=S&(-S);
取一个集合的补集
int T=((1<<size)-1)^S;
枚举一个集合的子集
for(int T=S;T;T=S&(T-1));
这个有必要说一下,由于T=S&(T-1),所以 \(T\) 一定是 \(S\) 的子集;又因为 \(T\) 是单调递减的,所以不会重复;而对于已知是 \(S\) 的一个子集 \(T\),与他相邻的下一个子集 \(T_{next}\) 与 \(T\) 的前半部分重合,而在 \(T\) 的最低位1的位置,\(T_{next}\) 一定是0\((T_{next}<T)\),而 \(T_{next}\) 在这个位置后的部分应是 \(S\) 的最大子集(即 \(S\) 本身),S&(T-1)很巧妙地满足这些性质。
题目
[NOIP2017]宝藏
显然,已选择的藏宝洞集合压缩为一个状态维度。
考虑将系数相同的贡献一起处理:层序建造道路
可预处理 集合 \(S\) 扩展到集合 \(S\cap T\) 的道路总长 \(f_{S,T}\)
之后按层dp,有:
\(\min_{i=2}^n dp_{i,2^n-1}\) 即为所求
Code
#include <bits/stdc++.h>
#define int long long
#define inf 2147483647
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
int n=read(),m=read();
int f[1<<12][1<<12]={};
int g[15][1<<12]={};
for(int i=1;i<(1<<n);i++){
int p=i^((1<<n)-1);
for(int j=p;j;j=p&(j-1))f[i][j]=inf;
}
for(int i=1,u,v,w;i<=m;i++){
u=read()-1,v=read()-1,w=read();
for(int j=1;j<(1<<n);j++){
if((j&1<<u)&&!(j&1<<v)&&w<f[j][1<<v])f[j][1<<v]=w;
if((j&1<<v)&&!(j&1<<u)&&w<f[j][1<<u])f[j][1<<u]=w;
}
}
stack<int> qu;
for(int i=1;i<(1<<n);i++){
int p=i^((1<<n)-1);
for(int j=p;j;j=p&(j-1))qu.push(j);
while(!qu.empty()){
int j=qu.top();
qu.pop();
f[i][j]=min(f[i][j-(j&-j)]+f[i][j&-j],f[i][j]);
}
}
for(int i=0;i<n;i++)for(int j=1;j<(1<<n);j++)g[i][j]=inf;
for(int i=1;i<=n;i++)g[0][1<<(i-1)]=0;
for(int i=1;i<n;i++){
for(int j=1;j<(1<<n);j++){
int p=j^((1<<n)-1);
for(int k=p;k;k=p&(k-1))g[i][j|k]=min(g[i-1][j]+f[j][k]*i,g[i][j|k]);
}
}
int ans=inf;
for(int i=1;i<n;i++)ans=min(g[i][(1<<n)-1],ans);
printf("%lld",n==1?0:ans);
return 0;
}
[TJOI2015]棋盘
互不侵犯加强版
简而言之,每个棋子有上下三行相对与自身位置的攻击范围,可以预处理每行的合法状态,再进行行间转移。
除了细节易混淆,思路直接。
可使用矩阵快速幂优化
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
signed main(){
int n=read(),m=read(),p=read(),k=read();
bool exi[100]={};
int pre[100]={},now[100]={},pos[100]={},pr=0,no=0,po=0;
for(int i=1;i<=p;i++)pr<<=1,pr+=read();
for(int i=1;i<=p;i++)no<<=1,no+=read();
for(int i=1;i<=p;i++)po<<=1,po+=read();
for(int i=1;i<=m;i++)now[1<<(i-1)]=(no<<i>>(p-k))&((1<<m)-1),pre[1<<(i-1)]=(pr<<i>>(p-k))&((1<<m)-1),pos[1<<(i-1)]=(po<<i>>(p-k))&((1<<m)-1);
exi[0]=1;
for(int i=1;i<(1<<m);i++){
if(exi[i-(i&-i)]&&!(now[i-(i&-i)]&(i&-i))&&!(((i-(i&-i))&now[i&-i]))){
exi[i]=1;
now[i]=now[i-(i&-i)]|now[i&-i];
pre[i]=pre[i-(i&-i)]|pre[i&-i];
pos[i]=pos[i-(i&-i)]|pos[i&-i];
// printf("%lld %lld %lld %lld\n",i,now[i],pre[i],pos[i]);
}
}
unsigned dp[2][100]={};
dp[0][0]=1;
for(int i=1,st;i<=n;i++){
st=i%2;
for(int j=0;j<(1<<m);j++){
dp[st][j]=0;
if(!exi[j])continue;
int p=pre[j]^((1<<m)-1);
for(int t=p;;t=p&(t-1)){
if(!(pos[t]&j))dp[st][j]+=dp[st^1][t];
if(!t)break;
}
}
}
unsigned ans=0;
for(int i=0;i<(1<<m);i++)ans+=dp[n%2][i];
printf("%u",ans);
}
局部极小值
被hack了:(
似乎无从下手。注意到最小值,可从小向大转移。同时由于行列限制,最多存在8个局部极小值。
设 \(dp_{i,S}\) 为当前枚举到第 \(i\) 个数,已选择局部极小值集合为 \(S\) 的方案数。
可预处理已选择局部极小值集合为 \(S\) 的情况下,可选择的局部极小值位置数,便可转移。
但可能无意间构造出不应存在的局部最小值,于是需要容斥。
Code
#include <bits/stdc++.h>
#define int long long
#define mod 12345678
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
int x[9]={0,1,1,1,0,-1,-1,-1,0};
int y[9]={0,-1,0,1,1,1,0,-1,-1};
struct cd{
int x,y;
};
int n,m;
vector<cd> ve;
int dp[30][260];
int ab[260];
int hm[260];
int rt[260][10][10];
int lgt[260];
int solve_dfs(vector<cd> pl){
memset(dp[0],0,sizeof(dp[0]));
ab[0]=n*m;
for(int i=1;i<(1<<pl.size());i++){
ab[i]=0;
memset(rt[i],0,sizeof(rt[i]));
for(int j=0;j<9;j++)rt[i][pl[lgt[i&(-i)]].x+x[j]][pl[lgt[i&(-i)]].y+y[j]]=1;
for(int h=1;h<=n;h++)for(int w=1;w<=m;w++){
if(!(rt[i][h][w]+=rt[i-(i&(-i))][h][w]))ab[i]++;
}
}
dp[0][0]=1;
for(int i=1;i<=n*m;i++){
for(int s=0;s<(1<<pl.size());s++){
dp[i][s]=dp[i-1][s]*(ab[s^((1<<pl.size())-1)]-i+1);
for(int p=s;p;p-=p&(-p))dp[i][s]+=dp[i-1][s^(p&(-p))];
dp[i][s]%=mod;
// printf("%lld %lld %lld %lld\n",i,s,dp[i][s],ab[s^((1<<pl.size())-1)]-i+1);
}
}
int re=dp[n*m][(1<<pl.size())-1];
queue<cd> qu;
for(int h=1;h<=n;h++)for(int w=1;w<=m;w++){
if(!rt[(1<<pl.size())-1][h][w])qu.push(cd{h,w});
}
while(!qu.empty()){
pl.emplace_back(qu.front()),qu.pop();
re-=solve_dfs(pl);
pl.pop_back();
}
return (re%mod+mod)%mod;
}
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
n=read(),m=read();
char c;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){
cin>>c;
if(c=='X')ve.emplace_back(cd{i,j});
}
for(int i=1;i<=7;i++)lgt[1<<i]=i;
hm[0]=0;
for(int i=1;i<256;i++)hm[i]=hm[i-(i&-i)]+1;
printf("%lld",solve_dfs(ve));
return 0;
}
[JSOI2009]密码
AC自动机。需要跑fail利用公共前后缀转移。
\(dp_{i,j,S}\) 表示当前为密码的第 \(i\) 个字符,在AC自动机的第 \(j\) 个节点,已包含模式串集合为 \(S\) 的方案数。
对所有模式串建立AC自动机,按位压缩标记各模式串结尾节点状态,在建立fail指针时顺便预处理状态。
dp时,用当前的状态更新以后的状态(刷表)。
输出字符串需要dfs状态
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
struct Trie{
int son[200][26],dep[200],mark[200],cnt=0,typ[200];
int add(string& s){
int u=0;
for(char c:s){
if(!son[u][c-'a'])son[u][c-'a']=++cnt,dep[son[u][c-'a']]=dep[u]+1;
u=son[u][c-'a'];
}
return u;
}
};
struct ACAM:Trie{
int fail[200]={};
void build(){
queue<int> q;
for(int i=0;i<26;i++)if(son[0][i])fail[son[0][i]]=0,q.push(son[0][i]);
int u,v;
while(!q.empty()){
u=q.front(),q.pop();
for(int i=0;i<26;i++){
v=son[u][i];
if(v)fail[v]=son[fail[u]][i],q.push(v),mark[v]|=mark[fail[v]];
else son[u][i]=son[fail[u]][i];
}
}
}
}acm;
struct Node{
int first,second,c;
bool operator <(Node no)const{
return c<no.c;
}
};
int L,N;
int dp[30][110][1030];
vector<Node> res[30][110][1030];
vector<string> ve;
void dfs(int i,int u,int k,string st){
if(i==0&&u==0){
ve.emplace_back(st);
return;
}
string s=" ";
for(Node pa:res[i][u][k]){
s[0]=pa.c+'a';
dfs(i-1,pa.first,pa.second,s+st);
}
}
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
L=read(),N=read();
string s;
for(int i=1;i<=N;i++)cin>>s,acm.mark[acm.add(s)]=1<<(i-1);
acm.build();
dp[0][0][0]=1;
for(int i=0;i<L;i++){
for(int j=0;j<=acm.cnt;j++){
for(int k=0;k<(1<<N);k++){
for(int p=0;p<26;p++){
if(dp[i][j][k]){
dp[i+1][acm.son[j][p]][k|acm.mark[acm.son[j][p]]]+=dp[i][j][k];
res[i+1][acm.son[j][p]][k|acm.mark[acm.son[j][p]]].emplace_back(Node{j,k,p});
}
}
}
}
}
int ans=0;
for(int i=0;i<=acm.cnt;i++)ans+=dp[L][i][(1<
printf("%lld\n",ans);
if(ans<=42){
for(int i=0;i<=acm.cnt;i++)dfs(L,i,(1<
sort(ve.begin(),ve.end());
for(string& s:ve)printf("%s\n",s.c_str());
}
return 0;
}
[NOI2015]寿司晚宴
要求两集合中元素互质,等价于要求两集合中元素的质因数互不相同。
但在 \(n=500\) 时,存在95个质数,显然不能直接压缩。
可以发现,对于 \(>\sqrt{500}\approx22\) 的质因数,每个元素中最多存在一个,于是对于这一部分大质数,线性处理即可。(根号分治)
那么,如何设计转移呢?
可以遍历元素进行转移(为什么我想不到)。
\(S\),\(T\) 是两个集合状态,\(k\) 是当前元素的小质因数集合。
对于某一个大质因数,由于仅能被两个集合中的一个包含,且对于每个元素,至多存在一个大质因数,因而可以逐个处理:
- 开始处理某一大质因数时使用两个数组 \(f1[S][T],f2[S][T]=dp[S][T]\) 辅助转移,表示当前处理的大质因数在 \(S/T\) 集合中的方案数。
- 转移类似:
- 由于当前元素的大质因数仅存在于 \(S/T\) 中,只能在一方集合中转移。
- 处理完此大质因数时,将答案重新转移给 \(dp\) 数组:\(dp[S][T]=f1[S][T]+f2[S][T]-dp[S][T]\)
以上转移也可作为无大质因数元素的完整转移。
将大质因数相同的元素(排序)放在一起处理,即可。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
struct num{
int st,bi;
bool operator <(num nu)const{
return bi<nu.bi;
}
};
int n,mod;
vector<int> pri;
bool is[510];
int dp[260][260],f1[260][260],f2[260][260];
num ns[510];
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
n=read(),mod=read();
for(int i=2;i<=500;i++){
if(!is[i])pri.emplace_back(i);
for(int j:pri){
if(i*j>500)break;
is[i*j]=1;
if(i%j==0)break;
}
}
for(int i=2,p;i<=n;i++){
p=i;
for(int j=0;p!=1&&j<8;j++)while(p%pri[j]==0)ns[i].st|=(1<<j),p/=pri[j];
ns[i].bi=p;
// printf("%lld %lld %lld\n",i,ns[i].st,ns[i].bi);
}
sort(ns+2,ns+n+1);
dp[0][0]=1;
for(int i=2,pa=0;i<=n;i++){
if(i==2||pa!=ns[i].bi||pa==1){
for(int S=(1<<8)-1;S>=0;S--)for(int T=S^((1<<8)-1);;T=(S^((1<<8)-1))&(T-1)){
f1[S][T]=f2[S][T]=dp[S][T];
if(T==0)break;
}
}
for(int S=(1<<8)-1;S>=0;S--)for(int T=S^((1<<8)-1);;T=(S^((1<<8)-1))&(T-1)){
if((T&ns[i].st)==0){
f1[S|ns[i].st][T]+=f1[S][T],f1[S|ns[i].st][T]%=mod;
f2[T][S|ns[i].st]+=f2[T][S],f2[T][S|ns[i].st]%=mod;
}
if(T==0)break;
}
if(i==n||ns[i+1].bi!=ns[i].bi||ns[i].bi==1){
for(int S=(1<<8)-1;S>=0;S--)for(int T=S^((1<<8)-1);;T=(S^((1<<8)-1))&(T-1)){
dp[S][T]=((f1[S][T]+f2[S][T]-dp[S][T])%mod+mod)%mod;
if(T==0)break;
}
}
pa=ns[i].bi;
}
int ans=0;
for(int S=(1<<8)-1;S>=0;S--)for(int T=S^((1<<8)-1);;T=(S^((1<<8)-1))&(T-1)){
ans+=dp[S][T];
if(T==0)break;
}
printf("%lld",ans%mod);
return 0;
}
[ZJOI2016]小星星
求一个图匹配到一棵树上的方案数。
显然,遍历树比遍历图更可做。
记录状态,\(dp[u][p][S]\) 表示当前为树上节点 \(u\),对应图上节点 \(p\),子树对应图上节点集合为 \(S\) 的方案数。
这样时间复杂度似乎是 \(O(n^33^n)\)的,TLE。
考虑不枚举集合,在子树中直接随意取图上节点,毫无疑问,会存在重复选择节点的情况,但单次复杂度只有 \(O(n^3)\)。如果对这个过程限制可选择的图上节点为某 \(m\) 个,就仅会存在重复大于等于 \((n-m)\) 次的情况。
即可容斥:所求答案(无重复)= 所有限制 \(n\) 个节点的方案数和 - 所有限制 \(n-1\) 个节点的方案数和 +所有限制 \(n-2\) 个节点的方案数和 ...
枚举图上节点的所有子集,复杂度 \(O(n^3)\),则总复杂度 \(O(n^32^n)\)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
bool gr[20][20]={};
vector<int> tr[20];
int fa[20];
int dp[20][20];
int re[20],key=0;
int n=read(),m=read();
for(int i=1,u,v;i<=m;i++){
u=read(),v=read();
gr[u][v]=gr[v][u]=1;
}
for(int i=1,u,v;i<n;i++){
u=read(),v=read();
tr[u].emplace_back(v);
tr[v].emplace_back(u);
}
{
queue<int> qu;
qu.push(1);
int u;
while(!qu.empty()){
u=re[++key]=qu.front(),qu.pop();
vector<int>::iterator it=tr[u].end();
for(auto te=tr[u].begin();te!=tr[u].end();te++){
int v=*te;
if(v!=fa[u])qu.push(v),fa[v]=u;
else it=te;
}
if(it!=tr[u].end())tr[u].erase(it);
}
reverse(re+1,re+n+1);
}
vector<int> arr;
int ans=0;
for(int st=1,sum,cnt;st<(1<<n);st++){
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dp[i][j]=1;
arr.clear();
for(int p=1;p<=n;p++)if(st&(1<<(p-1)))arr.emplace_back(p);
cnt=arr.size();
for(int ke=1,u;ke<=n;ke++){
u=re[ke];
for(int uf:arr){
for(int v:tr[u]){
int van=0;
for(int vf:arr)if(gr[uf][vf])van+=dp[v][vf];
dp[u][uf]*=van;
}
}
}
sum=0;
for(int i=0;i<=n;i++)if(1<<(i-1)&st)sum+=dp[1][i];
if((n-cnt)%2)ans-=sum;
else ans+=sum;
}
printf("%lld",ans);
}
[清华集训 2012]串珠子
即,求有多少种选择边的方案,使图联通。
对于图的一个子集 \(S\),联通方案数=任意链接方案数-不联通方案数。
任意链接方案数即\(2^{\text{子集 S 内边数}}\),
不联通方案数即子集中一个点所在的子集 \(T\in S\) 的联通方案数*补集 \(S-T\) 的任意链接方案数,
于是便非常好写。时间复杂度似乎是 \(O(3^n)\) 的。
Code
#include <bits/stdc++.h>
#define int long long
#define mod 1000000007
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
int n=read();
int c[20][20];
int lo[65555];
for(int i=1;i<=n;i++)lo[1<<(i-1)]=i;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)c[i][j]=c[j][i]=read();
vector<int> ve;
int f[65555],s[65535],x[65535];
f[0]=1;
for(int S=1;S<(1<<n);S++){
f[S]=f[S-(S&(-S))];
int p=S&(-S);
for(int te=S-(S&(-S)),v;te;te-=v){
v=te&(-te);
f[S]=f[S]*(c[lo[v]][lo[p]]+1)%mod;
}
for(int T=S;T;T=(S-(S&(-S)))&(T-1))x[S]+=f[T]*s[S^T]%mod;
x[S]%=mod;
s[S]=((f[S]-x[S])%mod+mod)%mod;
}
printf("%lld",s[(1<<n)-1]);
return 0;
}
[SDOI2009]学校食堂
细节很多。
注意到 \(n\) 较大,也算作线性dp。与顺序有关,因而依次枚举每个学生,自左而右处理。
假定现在 $ 1\sim i$ 的学生已做好饭,为了转移,显然还需要维护上一个做好饭的学生 \(i-k\),以及 \(i\) 之后学生的状态 \(j\)。
由于数据范围限制 \(\leq 7\),复杂度是可接受的。
之后的转移就比较显然了,注意将 \(i\) 后移的转移。
Code
#include <bits/stdc++.h>
#define int long long
#define inf 2147483647
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
int dp[1010][260][20];
int C=read();
for(int c=1;c<=C;c++){
int n=read();
for(int i=0;i<=n;i++)for(int j=0;j<256;j++)for(int k=0;k<=16;k++)dp[i][j][k]=inf;
int ta[1010],br[1010];
ta[0]=0;
for(int i=1;i<=n;i++)ta[i]=read(),br[i]=read();
dp[1][0][8]=0;
for(int i=1,lim=8;i<=lim;lim=min(i+br[i],lim),i++)dp[0][1<<(i-1)][i+8]=0;
for(int i=0;i<=n;i++){//1~i 完成
for(int j=0;j<(1<<min(8ll,n-i));j++){//j 状态
for(int ne=1,lim=min(n-i,8ll);ne<=lim;ne++){//i+ne 将完成的菜
if(j&(1<<(ne-1)))continue;
lim=min(ne+br[i+ne],lim);
for(int p=0;((!p||(1<<(p-1))&(j|1<<(ne-1))))&&i+p<=n;p++){//p 后移位
for(int k=max(1-i,-7ll);k<=8&&i+k<=n;k++){//i+k 上一道菜
if(k>0&&!(((1<<(k-1))&j)))continue;
int& dt=dp[i+p][(j|(1<<(ne-1)))>>p][ne+8-p];
dt=min(dp[i][j][k+8]+(ta[i+ne]|ta[i+k])-(ta[i+ne]&ta[i+k]),dt);
// if(dt!=inf)printf("%lld %lld %lld %lld \n",i+p,(j|(1>>(ne-1)))<<p,i+ne-p,dt);
// printf("%lld %lld %lld\n",i,j,p);
}
}
}
}
}
int ans=inf;
for(int i=0;i<=8;i++)ans=min(ans,dp[n][0][i]);
printf("%lld\n",ans);
}
return 0;
}
[省选联考 2020 A/B 卷] 信号传递
对于位置 \(x\rightarrow y\) 的转移,贡献如下:
因此,可将贡献拆分至每条边。使用一个 \(n^2\) 的邻接矩阵存储。
重排顺序,不妨依照位置递增确定信号站。以 \(dp_S\) 表示在已确定的信号站集合 \(S\) 中的最小总贡献。
为了不重不漏且无后效性地转移,在确定一个位置的信号站 \(i\) 时,将 \(i\) 与所有已确定的信号站连边的贡献累加进 \(dp\) 数组中。即 \(dp_{S|i}=\min(dp_S+\sum_{j\in S}con_{i,j},dp_{S|i})。\)
直接处理复杂度是 \(O(m^22^m)\) 的,时空间都超限了。
注意到,可以预处理 \(\sum_{j\in S}con_{i,j}\),暴力预处理当然还是 \(O(m^22^m)\),但是从 \(S\) 到 \(S|k\) 的转移可以通过微操优化至线性,从而得到 \(O(m2^m)\) 的总复杂度。
Code
#include <bits/stdc++.h>
#define int long long
#define inf 2147483647
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
struct Node{
int st,cnt;
array<int,24> arr;
};
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
int n=read(),m=read(),w=read();
int gr[30][30]={};
int dp[1<<(23)];
dp[0]=0;
for(int i=1;i<(1<<m);i++)dp[i]=inf;
for(int i=1,te,pa=read();i<n;i++)gr[pa][te=read()]++,pa=te;
for(int i=1;i<=m;i++)gr[i][i]=0;
queue<Node> qu;
for(int i=1;i<=m;i++){
Node no;
no.st=1<<(i-1);
no.cnt=1;
for(int j=1;j<=m;j++)no.arr[j]=inf;
no.arr[i]=0;
for(int j=1;j<=m;j++)no.arr[i]+=gr[j][i]*w-gr[i][j];
qu.push(no);
}
array<int,24> te;
for(int st,cnt;!qu.empty();){
st=qu.front().st,te=qu.front().arr,cnt=qu.front().cnt,qu.pop();
for(int i=1;i<=m;i++){
if((1<<(i-1))&st){
// printf("%lld %lld %lld %lld",st,i,te[i],dp[st]);
dp[st]=min(dp[st-(1<<(i-1))]+te[i]*cnt,dp[st]);
// printf(" %lld\n",dp[st]);
}
}
Node no;
no.cnt=cnt+1;
for(int i=1;i<=m;i++){
if(1<<(i-1)&st)break;
no.st=st|(1<<(i-1));
for(int j=1;j<=m;j++)no.arr[j]=te[j]+gr[j][i]*(1+w)+gr[i][j]*(1-w);
no.arr[i]=0;
for(int k=1;k<=m;k++){
if(k==i)continue;
if(no.st&(1<<(k-1)))no.arr[i]+=gr[i][k]*w+gr[k][i];
else no.arr[i]+=gr[k][i]*w-gr[i][k];
}
qu.push(no);
}
}
printf("%lld",dp[(1<<m)-1]);
return 0;
}
[PKUSC2018] 最大前缀和
求所有可能排列的最大前缀和之和。
一个序列的最大前缀和 \([1,i]\),其性质(条件)为:
- 剩余序列 \([i+1,n]\) 的所有前缀和 \(\sum_{j=i+1}^r a[j]\leq 0\space(r\in [i+1,n])。\)(否则应加入)
- 选定序列 \([1,i]\) 的所有真后缀和 \(\sum_{j=l}^i a[j] > 0\space(l\in (1,i])。\)(否则应舍弃)
转移过程中,需要维护集合作为前缀 \([1,i]\) 和后缀 \([i+1,n]\) 的合法排列数。
作为后缀的转移可以从左向右扩展,即要求扩展后元素和 \(\leq 0\):
类似的,作为前缀的转移从右向左扩展,要求扩展后元素和 \(> 0\),但由于是真后缀,对整个集合的元素和不做要求。所以仅需扩展前元素和 \(>0\),但为了转移,仍需分类这两种状态。
Code
#include <bits/stdc++.h>
#define int long long
#define mod 998244353
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
signed main(){
//freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
int pr[2][1<<20]={},po[1<<20]={},sum[1<<20]={};
int a[30];
int n=read();
sum[0]=0;pr[1][0]=pr[0][0]=po[0]=1;
for(int i=1;i<=n;i++)a[i]=read();
for(int i=1;i<(1<<n);i++){
int lb=1;
while((i&(1<<(lb-1)))==0)lb++;
sum[i]=sum[i-(i&-i)]+a[lb];
for(int p=1;p<=n;p++){
if(i&(1<<(p-1))){
pr[0][i]+=pr[1][i-(1<<(p-1))];
}
}
pr[0][i]%=mod;
if(sum[i]>=0)pr[1][i]=pr[0][i];
if(sum[i]<0){
for(int p=1;p<=n;p++){
if(i&(1<<(p-1))){
po[i]+=po[i-(1<<(p-1))];
}
}
po[i]%=mod;
}
}
int ans=0;
for(int i=1;i<(1<<n);i++){
ans+=pr[0][i]*po[((1<<n)-1)^i]%mod*sum[i]%mod;
}
printf("%lld",(ans%mod+mod)%mod);
return 0;
}
[九省联考 2018] 一双木棋 chess
考虑枚举所有可能的棋盘状态,总数 \(<3.7\times 10^5\),每种状态最多可能由 \(10\) 次状态转移,因而时间无需优化。
别样的状态压缩,每一位的状态是一个 \(0~9\) 的整数,无法使用位运算高效转移,于是预处理每一种状态,通过编号压缩,二分查找。
细节较多,但转移是简单的。
Code
#include <bits/stdc++.h>
#define int long long
#define hash(x) (lower_bound(ha+1,ha+cnt+1,x)-ha)
using namespace std;
typedef int Zip;
typedef vector<int> Seq;
int read(){
int i;
scanf("%lld",&i);
return i;
}
int ha[370000],cnt=0;
int n,m,ans=0;
int a[13][13];
bool pl[370000];
int dp[370000];
int po[13];
Seq unf(Zip val){
int te=val;
Seq ve(n+1);
bool st=0;
for(int i=n;i>=1;i--){
ve[i]=te%12-1;
st^=(te%12-1)%2;
te/=12;
}
ve[0]=st;
return ve;
}
Zip fold(Seq& ve){
int re=0;
for(int i=1;i<=n;i++){
re=re*12+(ve[i]+1);
}
return re;
}
void make_hash(int dep,Zip state,int pa,int sum){
if(dep==n){
ha[++cnt]=state;
pl[cnt]=sum%2^1;
if(pl[cnt])dp[cnt]=-2147483647;
else dp[cnt]=2147483647;
return;
}
for(int i=0;i<=pa;i++){
make_hash(dep+1,state*12+(i+1),i,sum+i);
}
}
void deepseek(Zip state){
Seq ve=unf(state);
int pos=hash(state);
// printf("%lld %lld %lld %lld\n",ve[1],ve[2],(int)pl[pos],dp[pos]);
for(int i=n,pov;i>=1;i--){
if((i==n||ve[i]>ve[i+1])&&ve[i]){
pov=hash(state-po[i]);
if(pl[pos]^1)dp[pov]=max(dp[pos]+a[i][ve[i]],dp[pov]);
else dp[pov]=min(dp[pos],dp[pov]);
}
}
}
signed main(){
// freopen("test.in","r",stdin);
n=read(),m=read();
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)a[i][j]=read();
for(int i=1,te;i<=n;i++)for(int j=1;j<=m;j++)te=read(),ans-=te,a[i][j]+=te;
make_hash(0,0,m,0);po[n]=1;
for(int i=n-1;i>=1;i--)po[i]=po[i+1]*12;
dp[cnt]=0;
for(int i=cnt;i>=1;i--)deepseek(ha[i]);
printf("%lld",ans+dp[1]);
return 0;
}
[MtOI2019] 恶魔之树
即求所有子集的最小公倍数之和。
考虑按次序枚举,以质因数集合作为状态。
元素大小 \(\leq 300\),共有 \(62\) 个质因数,显然不能直接压缩。
类似 [NOI2015]寿司晚宴 :
- 用根号分治的思想,将质因数分为 \(\leq \sqrt{300}\) 和 \(>\sqrt{300}\) 的部分。
- 依照所含大质因数对元素排序,从而对大质因数逐个处理。
在这里用 \(dp[0]\) 表示未包含当前质因数的总值,\(dp[1]\) 表示包含当前大质因数的总值。
在处理一个元素时:
- 若与上一个元素的大质因数不同,归零 \(dp[1]\) 数组。
- 从 \(dp[0]\) 和 \(dp[1]\) 向 \(dp[1]\) 转移。
- 若与下一个元素的大质因数不同,将 \(dp[1]\) 合并到 \(dp[0]\)。
但是,这里要求最小公倍数,因此还要统计数量。数组和循环就变得繁复。不过内容是简单的,也没有必要刻意压缩代码。
我用的是刷表的写法,转移方程:
\(S\) :当前遍历质因数集合,\(A\) :元素质因数集合,\(ex[A]\) :元素大质因数,\(pre\) :对应集合的质因数之积。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
int n,p;
array<int,7> pri={2,3,5,7,11,13,17};
int po[300010];
struct Num{
int n;
array<int,7> arr;
int bi;
int ex;
void make(int val,int v3){
arr.fill(0);
n=val;
ex=po[v3]-1;
for(int i=0;i<7;i++){
while(val%pri[i]==0)arr[i]++,val/=pri[i];
}
bi=val;
}
bool operator <(Num num){
return bi<num.bi;
}
};
int cnt[310];
Num a[310];
int dp[2][9][6][4][4][3][3][3]={};
int pre[9][6][4][4][3][3][3]={};
int prd[1<<7]={};
signed main(){
// freopen("test.in","r",stdin);
//freopen("test.out","w",stdout);
n=read(),p=read();
po[0]=1;
for(int i=1;i<=300000;i++)po[i]=po[i-1]*2%p;
for(int i=1;i<=n;i++)cnt[read()]++;
n=0;
for(int i=1;i<=300;i++)if(cnt[i])a[++n].make(i,cnt[i]);
sort(a+1,a+n+1);
prd[0]=1;
for(int i=0;i<7;i++)prd[1<<i]=pri[i];
for(int i=1;i<(1<<7);i++)prd[i]=prd[i-(i&-i)]*prd[i&-i];
dp[0][0][0][0][0][0][0][0]=1;
for(int i2=0,p2=1;i2<=8;i2++,p2*=2)
for(int i3=0,p3=1;i3<=5;i3++,p3*=3)
for(int i5=0,p5=1;i5<=3;i5++,p5*=5)
for(int i7=0,p7=1;i7<=3;i7++,p7*=7)
for(int i11=0,p11=1;i11<=2;i11++,p11*=11)
for(int i13=0,p13=1;i13<=2;i13++,p13*=13)
for(int i17=0,p17=1;i17<=2;i17++,p17*=17)
pre[i2][i3][i5][i7][i11][i13][i17]=p2*p3*p5*p7*p11*p13*p17%p;
for(int i=1;i<=n;i++){
if(i==1||a[i].bi!=a[i-1].bi||a[i].bi==1)memset(dp[1],0,sizeof(dp[1]));
for(int i2=8;i2>=0;i2--)
for(int i3=5;i3>=0;i3--)
for(int i5=3;i5>=0;i5--)
for(int i7=3;i7>=0;i7--)
for(int i11=2;i11>=0;i11--)
for(int i13=2;i13>=0;i13--)
for(int i17=2;i17>=0;i17--)
dp[1][max(i2,a[i].arr[0])][max(i3,a[i].arr[1])][max(i5,a[i].arr[2])][max(i7,a[i].arr[3])][max(i11,a[i].arr[4])][max(i13,a[i].arr[5])][max(i17,a[i].arr[6])]=
(dp[1][max(i2,a[i].arr[0])][max(i3,a[i].arr[1])][max(i5,a[i].arr[2])][max(i7,a[i].arr[3])][max(i11,a[i].arr[4])][max(i13,a[i].arr[5])][max(i17,a[i].arr[6])]+
(dp[1][i2][i3][i5][i7][i11][i13][i17]+dp[0][i2][i3][i5][i7][i11][i13][i17]*a[i].bi%p)*a[i].ex%p*
pre[max(0ll,a[i].arr[0]-i2)][max(0ll,a[i].arr[1]-i3)][max(0ll,a[i].arr[2]-i5)][max(0ll,a[i].arr[3]-i7)][max(0ll,a[i].arr[4]-i11)][max(0ll,a[i].arr[5]-i13)][max(0ll,a[i].arr[6]-i17)])%p;
if(i==n||a[i].bi!=a[i+1].bi||a[i].bi==1){
for(int i2=8;i2>=0;i2--)
for(int i3=5;i3>=0;i3--)
for(int i5=3;i5>=0;i5--)
for(int i7=3;i7>=0;i7--)
for(int i11=2;i11>=0;i11--)
for(int i13=2;i13>=0;i13--)
for(int i17=2;i17>=0;i17--)
dp[0][i2][i3][i5][i7][i11][i13][i17]=
(dp[1][i2][i3][i5][i7][i11][i13][i17]+dp[0][i2][i3][i5][i7][i11][i13][i17])%p;
}
}
int ans=0;
for(int i2=8;i2>=0;i2--)
for(int i3=5;i3>=0;i3--)
for(int i5=3;i5>=0;i5--)
for(int i7=3;i7>=0;i7--)
for(int i11=2;i11>=0;i11--)
for(int i13=2;i13>=0;i13--)
for(int i17=2;i17>=0;i17--)
ans+=dp[0][i2][i3][i5][i7][i11][i13][i17];
printf("%lld",ans%p);
}
[省选联考 2021 A/B 卷] 滚榜
显然,需要尽量减小 \(b_i\),也即非必要不增加当前成绩。
在 \(b_i\) 不降的条件下,发现对于一个顺序,对于当前最高成绩有以下转移:
\(com[i]\) 为当前成绩,\(last\) 为上一个元素的 \(b\),\(p[i]\) 为当前元素的编号。
直接转移需维护的状态过多,考虑换种方式转移。
如图,纵轴为成绩,横轴为元素。黑色曲折轮廓线为 \(a[i]\),上方轮廓线为预计的总成绩。
初始时,预计总成绩为上方黑色轮廓线,即均为 \(a[i]\) 中最大值 \(ma\)。
红,橙,黄,绿矩形是依次公布的 \(b[i]\) 。
显然,晚被公布的队伍,总成绩更高,也即每次公布成绩后,都会把此后的成绩最小值提高(即对应颜色的上方轮廓线)。
最终成绩和即为最终形成的最高轮廓线,可视为每次的轮廓增加量累加而来。
那么转移时仅需计算此次轮廓增加量 \((com[i]-com[i-1])*(n-i+1)\) ,保证其和 \(\leq m\) 即可。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read(){
int i;
scanf("%lld",&i);
return i;
}
int n,m;
int a[20];
int dp[1<<13][510][15];
int cnt[1<<13];
signed main(){
n=read(),m=read();
int ma=0;
for(int i=1;i<=n;i++)if((a[i]=read())>a[ma])ma=i;
for(int i=1;i<=n;i++)m-=a[ma]-a[i];
dp[0][0][ma]=1;
for(int S=1;S<(1<<n);S++){
cnt[S]=cnt[S-(S&-S)]+1;
for(int i=0;i<=min(500ll,m);i++){
for(int p=1;p<=n;p++){
if((S&(1<<(p-1)))==0)continue;
for(int f=1;f<=n;f++){
if((((S&(1<<(f-1)))==0)||p==f)&&(S-(1<<(p-1))))continue;
if(a[p]>a[f]){if(i>=(a[p]-a[f])*(n-cnt[S]+1))dp[S][i][p]+=dp[S-(1<<(p-1))][i-(a[p]-a[f])*(n-cnt[S]+1)][f];}
else if(p>f){if(i>=(n-cnt[S]+1))dp[S][i][p]+=dp[S-(1<<(p-1))][i-(n-cnt[S]+1)][f];}
else dp[S][i][p]+=dp[S-(1<<(p-1))][i][f];
}
}
}
}
int ans=0;
for(int i=0;i<=min(500ll,m);i++){
for(int p=1;p<=n;p++){
ans+=dp[(1<<n)-1][i][p];
// printf("%lld%c",dp[(1<<n)-1][i][p]," \n"[p==n]);
}
}
printf("%lld",ans);
return 0;
}
浙公网安备 33010602011771号