压缩 DP 学习笔记
通过将某些信息压缩到状态中以实现 DP。
I.二进制状压 DP
经典的状压 DP。
I.[SDOI2009]Bill的挑战
第一眼看上去不会做。第二眼发现直觉状压。第三眼算算复杂度发现OK,然后就没问题了。
我们设表示:
当前DP到了第位,
所有串的匹配成功的状态是,
的方案数。
通过预处理一个状压数组表示第位填入字符的匹配结果,我们可以在复杂度范围内跑过。其中是数据组数,是字符集大小(),是串长,是串数。
这是正解,只是毒瘤出题人卡长,不得不吸个臭氧才卡过。
代码:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
const int mod=1000003;
int T,n,m,S,f[100][1<<15],mat[100][26],MAXN,res;
char s[15][100];
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m),MAXN=1<<n,res=0,memset(f,0,sizeof(f)),memset(mat,0,sizeof(mat));
for(register int i=0;i<n;i++)scanf("%s",s[i]+1);
S=strlen(s[0]+1);
for(register int i=1;i<=S;i++)for(register int j=0;j<26;j++)for(register int k=0;k<n;k++)if(s[k][i]=='?'||s[k][i]==j+'a')mat[i][j]|=1<<k;
f[0][MAXN-1]=1;
for(register int i=0;i<S;i++)for(register int j=0;j<MAXN;j++)for(register int k=0;k<26;k++)(f[i+1][j&mat[i+1][k]]+=f[i][j])%=mod;
for(register int i=0;i<MAXN;i++)if(__builtin_popcount(i)==m)(res+=f[S][i])%=mod;
printf("%d\n",res);
}
return 0;
}
II.[CQOI2018]解锁屏幕
一眼状压。
设表示:访问状态为,当前在点的方案数。
我们枚举一个,表示下一个要去的地方;要判断能不能转移到,还要枚举,判断是否共线。判断共线是基础向量,一次点积+一次叉积带走。
这样复杂度,期望得分。
代码:
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define mp make_pair
#define x first
#define y second
const int mod=1e8+7;
int n,f[1<<20][20],lim,res;
pii p[20];
pii operator-(const pii &u,const pii &v){
return mp(u.x-v.x,u.y-v.y);
}
int operator*(const pii &u,const pii &v){
return u.x*v.x+u.y*v.y;
}
int operator^(const pii &u,const pii &v){
return u.x*v.y-u.y*v.x;
}
int main(){
scanf("%d",&n),lim=1<<n;
for(int i=0;i<n;i++)scanf("%d%d",&p[i].first,&p[i].second);
for(int i=0;i<n;i++)f[1<<i][i]=1;
for(int i=0;i<lim;i++)for(int j=0;j<n;j++){
if(!(i&(1<<j)))continue;
// printf("%d %d:\n",i,j);
for(int k=0;k<n;k++){
if(i&(1<<k))continue;
// printf("%d:\n",k);
bool ok=true;
for(int l=0;l<n;l++){
if(i&(1<<l))continue;
if(k==l)continue;
if(((p[k]-p[j])^(p[l]-p[j]))!=0)continue;
if(((p[k]-p[l])*(p[j]-p[l]))>0)continue;
ok=false;break;
}
// printf("%d\n",ok);
(f[i|(1<<k)][k]+=f[i][j]*ok)%=mod;
}
}
for(int i=0;i<lim;i++)if(__builtin_popcount(i)>=4)for(int j=0;j<n;j++)(res+=f[i][j])%=mod;
printf("%d\n",res);
return 0;
}
考虑预处理出如果能从转移到需要选择的子集。这样子就可以在DP时判断(即判断该子集是否是的子集)。复杂度。期望得分。
另:本题卡常,请随手吸氧。
代码:
#pragma GCC optimize(3)
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define mp make_pair
#define x first
#define y second
const int mod=1e8+7;
int n,f[1<<20][20],lim,res,blk[20][20];
pii p[20];
pii operator-(const pii &u,const pii &v){
return mp(u.x-v.x,u.y-v.y);
}
int operator*(const pii &u,const pii &v){
return u.x*v.x+u.y*v.y;
}
int operator^(const pii &u,const pii &v){
return u.x*v.y-u.y*v.x;
}
int main(){
scanf("%d",&n),lim=1<<n;
for(int i=0;i<n;i++)scanf("%d%d",&p[i].first,&p[i].second);
for(int i=0;i<n;i++)for(int j=0;j<n;j++){
if(i==j)continue;
for(int k=0;k<n;k++){
if(i==k||j==k)continue;
if(((p[j]-p[i])^(p[k]-p[i]))!=0)continue;
if(((p[i]-p[k])*(p[j]-p[k]))>0)continue;
blk[i][j]|=(1<<k);
}
}
for(int i=0;i<n;i++)f[1<<i][i]=1;
for(int i=0;i<lim;i++)for(int j=0;j<n;j++){
if(!(i&(1<<j)))continue;
// printf("%d %d:\n",i,j);
for(int k=0;k<n;k++){
if(i&(1<<k))continue;
if((i&blk[j][k])!=blk[j][k])continue;
(f[i|(1<<k)][k]+=f[i][j])%=mod;
}
}
for(int i=0;i<lim;i++)if(__builtin_popcount(i)>=4)for(int j=0;j<n;j++)(res+=f[i][j])%=mod;
printf("%d\n",res);
return 0;
}
III.[SCOI2008]奖励关
就是一眼状压。但这题难点不是状压,而是期望。
应该很容易就能想到,设表示前次操作后,状态为的期望收益。但这有个问题——我们不知道如果刷到一个负数收益应不应该选,因为我们不知道这个负数收益在后面会带给我们怎样的期望收益。
因为必须要直到后面的内容,所以考虑倒序DP:设表示前次操作后状态为,在后次操作中的期望收益。这样期望就可以直接取了——对后面的影响已经确定。
对于,我们枚举一个,表示刷到第个物品。如果不可以选,有 f[i][j]+=f[i+1][j]
;否则,即可以选,有f[i][j]+=max(f[i+1][j],f[i+1][j|(1<<k)]+val[k])
。
这时期望就可以正常除以了,因为刷到所有物品的概率是均等的。
复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,lim,val[16],sta[16];
double f[110][1<<16];
int main(){
scanf("%d%d",&m,&n),lim=(1<<n);
for(int i=0,x;i<n;i++){
scanf("%d",&val[i]);
scanf("%d",&x);
while(x)sta[i]|=(1<<(x-1)),scanf("%d",&x);
}
for(int i=m;i;i--)for(int j=0;j<lim;j++){
for(int k=0;k<n;k++)if((j&sta[k])==sta[k])f[i][j]+=max(f[i+1][j],f[i+1][j|(1<<k)]+val[k]);else f[i][j]+=f[i+1][j];
f[i][j]/=n;
}
printf("%lf\n",f[1][0]);
return 0;
}
IV.[GDOI2014]拯救莫莉斯
因为,
所以最大只会到,可以状压。
考虑设表示:
在前行已经填好的情况下,第行状态为,第行状态为的最小代价和最小数量(是个std::pair
)。
转移时枚举行的状态。复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
#define bp __builtin_popcount
#define pii pair<int,int>
#define x first
#define y second
#define mp make_pair
int n,m,c[100][100],lim,s[100][1<<8];
pii f[100][1<<8][1<<8],res=mp(0x3f3f3f3f,0x3f3f3f3f);
pii operator+(const pii &u,const pii &v){
return mp(u.x+v.x,u.y+v.y);
}
bool che(int i,int j,int k){
int jj=j;
jj|=(j>>1)&(lim-1);
jj|=(j<<1)&(lim-1);
jj|=i;
jj|=k;
return jj==(lim-1);
}
int main(){
scanf("%d%d",&n,&m),memset(f,0x3f3f3f3f,sizeof(f)),lim=(1<<m);
for(int i=0;i<n;i++){
for(int j=0;j<m;j++)scanf("%d",&c[i][j]);
for(int j=0;j<lim;j++)for(int k=0;k<m;k++)if(j&(1<<k))s[i][j]+=c[i][k];
}
if(n==1){printf("%d %d\n",1,c[0][0]);return 0;}
for(int i=0;i<lim;i++)for(int j=0;j<lim;j++)if(che(0,i,j))f[1][i][j]=make_pair(s[0][i]+s[1][j],bp(i)+bp(j));
for(int i=2;i<n;i++)for(int j=0;j<lim;j++)for(int k=0;k<lim;k++)for(int l=0;l<lim;l++)if(che(l,j,k))f[i][j][k]=min(f[i][j][k],f[i-1][l][j]+mp(s[i][k],bp(k)));
for(int i=0;i<lim;i++)for(int j=0;j<lim;j++)if(che(0,j,i))res=min(res,f[n-1][i][j]);
printf("%d %d\n",res.y,res.x);
return 0;
}
V.CF599E Sandy and Nuts
神题。
本题给我一个忠告:无论什么题,都要先看数据范围(废话)。
没看到之前以为是道毒瘤题,看到之后……还是毒瘤题。
因为数据范围小,可以状压。
先不考虑LCA和边的限制。设表示:在以为根的子树中,选择了里面的点,的方案数。
转移就是枚举,其中符号表示从某个集合中删掉一个数/一个集合。这个表示的某个儿子所包含的子树集。然后就有
但是这个枚举会重复计算:假设有两个儿子和,那么枚举时,的情况会被算上;同时,枚举时,也会被计算!
这样,我们必须只计算包含某个点的那种方案所贡献的答案。即,随便找出一个,则只有的才是合法的。
下面我们考虑加上边和的限制,什么样的才是合法的。
I.LCA的限制
I.I.确保LCA是LCA而不是单纯的CA(common ancestor)。
即,对于,如果有,则与不能同时出现,否则它们的LCA就不是了。
I.II.确保LCA一定是A(ancestor)
即,对于,如果有,必有且。
II.边的限制
II.I.确保边的存在
即,对于,如果有且但是与却有且只有一个条件成立,则这条边不可能存在。
II.II.确保边的可能
即,对于所有的,一个里最多只能有这么一个,因为一棵子树中最多只能同父亲连一条边。
如果只存在一个,那么转移就只能从这个而来,即
否则,即不存在,就是上面的式子
边界为,最终答案为。
为了方便,采取记忆化搜索的形式实行。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,p,f[15][1<<15];
bool g[15][15];
pair<int,int>e[15];
pair<pair<int,int>,int>l[110];
bool in(int x,int y){return x&(1<<y);}
int dfs(int x,int U){
int &res=f[x][U],pos=0;
// printf("%d,%d:%d\n",x,U,res);
if(~res)return f[x][U];
U^=(1<<x),res=0;
for(;pos<n;pos++)if(in(U,pos))break;
for(int u=U;u;u=(u-1)&U){
if(!in(u,pos))continue;
bool ok=true;
for(int i=0;i<p;i++)if(l[i].second==x&&in(u,l[i].first.first)&&in(u,l[i].first.second)){ok=false;break;}
if(!ok)continue;
for(int i=0;i<p;i++)if(in(u,l[i].second)&&(!in(u,l[i].first.first)||!in(u,l[i].first.second))){ok=false;break;}
if(!ok)continue;
for(int i=0;i<m;i++)if(e[i].first!=x&&e[i].second!=x&&(in(u,e[i].first)^in(u,e[i].second))){ok=false;break;}
if(!ok)continue;
int cnt=0,y;
for(int i=0;i<n;i++)if(g[x][i]&&in(u,i))cnt++,y=i;
if(cnt>1)continue;
if(cnt==1)res+=dfs(y,u)*dfs(x,U^u^(1<<x));
else for(y=0;y<n;y++)if(in(u,y))res+=dfs(y,u)*dfs(x,U^u^(1<<x));
}
// printf("%d,%d:%d\n",x,U,res);
return res;
}
signed main(){
scanf("%lld%lld%lld",&n,&m,&p),memset(f,-1,sizeof(f));
for(int i=0,x,y;i<m;i++)scanf("%lld%lld",&x,&y),x--,y--,g[x][y]=g[y][x]=true,e[i]=make_pair(x,y);
for(int i=0,a,b,c;i<p;i++)scanf("%lld%lld%lld",&a,&b,&c),a--,b--,c--,l[i]=make_pair(make_pair(a,b),c);
for(int i=0;i<n;i++)f[i][1<<i]=1;
printf("%lld\n",dfs(0,(1<<n)-1));
return 0;
}
VI.CF906C Party
DP是门艺术。
一眼状压。但是怎么状压就比较困难,因为同一个可以代表成千上万种含义。
这里我们采用,设表示当集合中所有的点都处于同一个团内的最小代价。
则我们有。其中表示与有边的集合。
初始为,其它均为。
复杂度为。
代码:
#include<bits/stdc++.h>
using namespace std;
int f[1<<22],fr[1<<22],id[1<<22],n,m,sta[22],mxn;
stack<int>s;
int main(){
scanf("%d%d",&n,&m),mxn=(1<<n),memset(f,0x3f3f3f,sizeof(f));
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),x--,y--,sta[x]|=(1<<y),sta[y]|=(1<<x);
if(m*2==n*(n-1)){puts("0");return 0;}
for(int i=0;i<n;i++)f[1<<i]=0;
for(int x=1;x<mxn;x++)for(int i=0;i<n;i++){
if(!(x&(1<<i)))continue;
int y=x|sta[i];
if(y==x)continue;
if(f[y]>f[x]+1)f[y]=f[x]+1,fr[y]=x,id[y]=i;
}
printf("%d\n",f[mxn-1]);
int x=mxn-1;
while(__builtin_popcount(x)!=1)s.push(id[x]),x=fr[x];
while(!s.empty())printf("%d ",s.top()+1),s.pop();
return 0;
}
VII.CF11D A Simple Task
我感觉状压DP是所有DP中最能玩出花的那一种……因为状态保存下来了因此什么奇奇怪怪的限制都能满足。
比如说这题。
一个环可以看作一条首尾相接的路径。我们可以设表示:在集合中的点构成了一条路径,且路径的起点为的方案数。
为了避免重复计算,我们约定这条路径的起点必须是中最小的那个数。换句话说,即lowbit(S)
。
则我们只需要枚举的下一条遍是去哪的就可以。复杂度为。
另外,一个环会顺时针逆时针算两次,并且路径也会被看作是二元环而算进去,记得统计进去。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,lim,f[1<<20][20],res;
bool g[20][20];
signed main(){
scanf("%lld%lld",&n,&m),lim=(1<<n);
for(int i=1,x,y;i<=m;i++)scanf("%lld%lld",&x,&y),x--,y--,g[x][y]=g[y][x]=true;
for(int i=0;i<n;i++)f[1<<i][i]=1;
for(int i=1;i<lim;i++)for(int j=0;j<n;j++){
if(!(i&(1<<j)))continue;
for(int k=__builtin_ctz(i);k<n;k++){
if(!g[j][k])continue;
if(i&(1<<k))res+=f[i][j]*(__builtin_ctz(i)==k);
else f[i|(1<<k)][k]+=f[i][j];
}
}
printf("%lld\n",(res-m)/2);
return 0;
}
VIII.CF53E Dead Ends
,我还是第一次见到这么小的状压……
我们设表示:将集合内的点连成一棵树,且集合里的节点是叶子节点的方案数。
则有。
但是,一棵树可能会被不同的顺序构造出来。因此有应该除以。
代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m,p,f[1<<11][1<<11],lim,res;
bool g[11][11];
signed main(){
scanf("%lld%lld%lld",&n,&m,&p),lim=(1<<n);
for(int i=1,x,y;i<=m;i++)scanf("%lld%lld",&x,&y),x--,y--,g[x][y]=g[y][x]=true,f[(1<<x)|(1<<y)][(1<<x)|(1<<y)]=2;
for(int S=1;S<lim;S++)for(int s=S;s;s=(s-1)&S){
f[S][s]/=__builtin_popcount(s);
for(int i=0;i<n;i++){
if(!(S&(1<<i)))continue;
int t=s&((lim-1)^(1<<i));
for(int j=0;j<n;j++){
if(S&(1<<j))continue;
if(!g[i][j])continue;
f[S|(1<<j)][t|(1<<j)]+=f[S][s];
}
}
}
for(int i=0;i<lim;i++)if(__builtin_popcount(i)==p)res+=f[lim-1][i];
printf("%lld\n",res);
return 0;
}
IX.[USACO15JAN]Moovie Mooving G
思路1.
设表示在第场(注意是场,不是部)电影时,已经看了里面的电影是否合法。
然后贪心地取最小的状态保存。光荣MLE了,。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,len[20],sum[20],dis[30000],id[30000];
vector<int>v[20],g[30000],res[30000];
queue<int>q;
int main(){
scanf("%d%d",&n,&m),memset(dis,0x3f3f3f3f,sizeof(dis));
for(int i=0,x,y;i<n;i++){
scanf("%d%d",&len[i],&x),sum[i+1]=sum[i]+x;
for(int j=0;j<x;j++)scanf("%d",&y),v[i].push_back(y),id[sum[i]+j]=i;
}
// for(int i=0;i<=n;i++)printf("%d ",sum[i]);puts("");
id[sum[n]]=n;
for(int i=0;i<n;i++)for(int k=0;k<v[i].size();k++){
int x=v[i][k];
if(!x)q.push(sum[i]+k),dis[sum[i]+k]=0,res[sum[i]+k].push_back(1<<i);
int ed=x+len[i];
if(ed>=m){g[sum[i]+k].push_back(sum[n]);continue;}
for(int j=0;j<n;j++){
if(i==j)continue;
vector<int>::iterator it=upper_bound(v[j].begin(),v[j].end(),ed);
if(it==v[j].begin())continue;
it--;
if(*it+len[j]<ed)continue;
g[sum[i]+k].push_back(sum[j]+it-v[j].begin());
}
}
// for(int i=0;i<n;i++){for(int j=0;j<v[i].size();j++){printf("%d:",sum[i]+j);for(auto x:g[sum[i]+j])printf("%d ",x);puts("");}puts("");}
// for(int i=0;i<=sum[n];i++)printf("%d ",id[i]);puts("");
while(!q.empty()){
int x=q.front();q.pop();
// printf("%d:\n",x);
for(auto y:g[x]){
if(dis[y]<=dis[x])continue;
// printf("%d\n",y);
for(auto i:res[x])if(!(i&(1<<id[y]))){
if(dis[y]!=dis[x]+1)dis[y]=dis[x]+1,res[y].clear();
break;
}
if(dis[y]!=dis[x]+1)continue;
for(auto i:res[x])if(!(i&(1<<id[y])))res[y].push_back(i|(1<<id[y]));
q.push(y);
}
}
printf("%d\n",dis[sum[n]]==0x3f3f3f3f?-1:dis[sum[n]]);
return 0;
}
思路2.
发现当一场电影结束后,无论这一场是在哪里看的都没关系。
因此我们设表示只看集合里面的电影,最多能够看多久。
转移就枚举下一场看什么,二分一下小于等于的第一场比赛并观看即可。
复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,f[1<<20],len[20],res=0x3f3f3f3f;
vector<int>v[20];
int main(){
scanf("%d%d",&n,&m);
for(int i=0,x,y;i<n;i++){
scanf("%d%d",&len[i],&x);
while(x--)scanf("%d",&y),v[i].push_back(y);
}
for(int x=0;x<(1<<n);x++){
if(f[x]>=m){res=min(res,__builtin_popcount(x));continue;}
for(int i=0;i<n;i++){
if(x&(1<<i))continue;
vector<int>::iterator it=upper_bound(v[i].begin(),v[i].end(),f[x]);
if(it==v[i].begin())continue;
it--;
f[x|(1<<i)]=max(f[x|(1<<i)],*it+len[i]);
}
}
printf("%d\n",res==0x3f3f3f3f?-1:res);
return 0;
}
X.[JXOI2012]奇怪的道路
神题。
(为以示区别,题面中的我们称作)。
思路1.
观察到很小,考虑状压。
设表示:
前个位置的边已经全部连完了,位置的状态压起来是,并且连了条边的方案数。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int f[50][1<<10][50],n,m,lim,p;
int ksm(int x,int y){
int z=1;
for(;y;x=(1ll*x*x)%mod,y>>=1)if(y&1)z=(1ll*z*x)%mod;
return z;
}
int main(){
scanf("%d%d%d",&n,&m,&p),lim=1<<p;
f[0][0][0]=1;
for(int i=0;i<n;i++)for(int j=0;j<min(1<<i,lim);j++)for(int k=0;k<=m;k++){
if(!f[i][j][k])continue;
for(int g=0;g<min(1<<(i+1),lim);g++){
int diff=((g>>1)^j);
if(__builtin_parity(diff)!=(g&1))continue;
int cnt=__builtin_popcount(diff);
for(int h=k+cnt;h<=m;h+=2)(f[i+1][g][h]+=1ll*f[i][j][k]*ksm(min(i,p),(h-k-cnt)>>1)%mod)%=mod;
}
}
// for(int i=1;i<=n;i++)for(int j=0;j<min(1<<i,lim);j++)for(int k=0;k<=m;k++)printf("%d,%d,%d:%d\n",i,j,k,f[i][j][k]);
printf("%d\n",f[n][0][m]);
return 0;
}
一交,WA,。
咋肥事?
因为这么连,会有重复计算的部分(因为边是无序的,同一组边集只不过因为顺序不同就会加不止一次)。
思路2.
为了凸显顺序,我们不得不考虑再增加一维。
设表示:
前个位置的边已经全部连完了,连了条边,位置的状态压起来是,并且位置只与里的点连了边的方案数。
显然,初始,答案是。
考虑如何转移(刷表法)。
- 我们再连一条边。
有。
- 连接之间的边已经全部连完,来到下一位。
有。
- 当枚举完成后,
如果有的第位为,则可以转移到下一位,则有
。
复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,p,f[40][40][1<<10][10],lim;
int main(){
scanf("%d%d%d",&n,&m,&p),lim=(1<<(p+1));
f[1][0][0][0]=1;
for(int i=1;i<=n;i++)for(int j=0;j<=m;j++)for(int k=0;k<lim;k++){
for(int l=min(i-1,p);l;l--){
if(j<m)(f[i][j+1][k^(1<<l)^1][l]+=f[i][j][k][l])%=mod;
(f[i][j][k][l-1]+=f[i][j][k][l])%=mod;
}
if(!(k&(1<<p)))(f[i+1][j][k<<1][min(i,p)]+=f[i][j][k][0])%=mod;
}
printf("%d\n",f[n][m][0][0]);
return 0;
}
XI.CF401D Roman and Numbers
思路:
我们设表示中出现了多少个数字。然后就可以设表示当填入数字的状态是,且当前数的余数是时的方案数。则直接转移即可。
复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int m,num[10],a[10],pov[10],all,dig,dd;
ll n,f[1<<18][110],ten[20];
void teg(int ip){
dig=0;
for(int i=9;i>=0;i--)a[i]=ip%num[i],ip/=num[i],dig+=a[i];
}
int main(){
scanf("%lld%d",&n,&m);
while(n)num[n%10]++,n/=10,dd++;
ten[0]=1;
for(int i=1;i<=dd;i++)ten[i]=ten[i-1]*10;
for(int i=0;i<10;i++)num[i]++;
pov[9]=1;
for(int i=8;i>=0;i--)pov[i]=pov[i+1]*num[i+1];
// for(int i=0;i<10;i++)printf("%d ",num[i]);puts("");
// for(int i=0;i<10;i++)printf("%d ",pov[i]);puts("");
all=pov[0]*num[0];
for(int i=1;i<10;i++)if(num[i]>1)f[pov[i]][(ten[dd-1]*i)%m]=1;
for(int i=1;i<all;i++){
teg(i);
// printf("QWQ:%d:::",i);for(int j=0;j<10;j++)printf("%d ",a[j]);puts("");
for(int j=0;j<m;j++){
// printf("%d:%d\n",j,f[i][j]);
if(!f[i][j])continue;
for(int k=0;k<10;k++)if(num[k]-a[k]>1)f[i+pov[k]][(ten[dd-dig-1]*k+j)%m]+=f[i][j];
}
}
printf("%lld\n",f[all-1][0]);
return 0;
}
XII.[SDOI2008]山贼集团
XIII.[POI2007]ATR-Tourist Attractions
这题我一年半之前初学状压DP时就写了份没卡空间的做法,今天终于A了……
首先,思路非常简单——我们可以使用Dijkstra预处理出来中两两点之间的距离以及它们到和的距离。接着,设表示当前访问完了集合中所有东西,且在位置的最小距离。DP很简单,这里就放一个转移式罢:
其中必有,其中是之前必选的集合。
但是这题卡空间。按照上述方法,空间大小是,会被卡掉。
我们考虑滚动数组,按照状态中为的位的数量进行DP。此时最大一位的空间消耗是,总空间即为,可以通过。
编号直接使用vector
建立双射即可。
代码 :
#include<bits/stdc++.h>
using namespace std;
int n,m,p,r,dis[24][20100],d[25][25],d1[25],dn[25],id[(1<<20)+5],f[2][200000][21],mus[25],res=0x3f3f3f3f;
namespace Graph{
vector<pair<int,int> >v[20100];
priority_queue<pair<int,int> >q;
bool vis[20100];
void Dijkstra(int S){
memset(dis[S],0x3f,sizeof(dis[S])),memset(vis,false,sizeof(vis)),dis[S][S]=0,q.push(make_pair(0,S));
while(!q.empty()){
int x=q.top().second;q.pop();
if(vis[x])continue;vis[x]=true;
for(auto y:v[x])if(dis[S][x]+y.second<dis[S][y.first])dis[S][y.first]=dis[S][x]+y.second,q.push(make_pair(-dis[S][y.first],y.first));
}
}
}
vector<int>v[30];
void chmn(int &x,int y){if(x>y)x=y;}
int main(){
scanf("%d%d%d",&n,&m,&p),memset(f,0x3f3f3f3f,sizeof(f));
for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),Graph::v[x].push_back(make_pair(y,z)),Graph::v[y].push_back(make_pair(x,z));
scanf("%d",&r);
for(int i=1,x,y;i<=r;i++)scanf("%d%d",&x,&y),mus[y-2]|=(1<<(x-2));
if(!p){
Graph::Dijkstra(1);
printf("%d\n",dis[1][n]);
return 0;
}
for(int i=2;i<=p+1;i++)Graph::Dijkstra(i);
for(int i=0;i<p;i++)for(int j=0;j<p;j++)d[i][j]=dis[i+2][j+2];
// for(int i=0;i<p;i++){for(int j=0;j<p;j++)printf("%d ",d[i][j]);puts("");}
for(int i=0;i<p;i++)d1[i]=dis[i+2][1],dn[i]=dis[i+2][n];
for(int i=0;i<(1<<p);i++)id[i]=v[__builtin_popcount(i)].size(),v[__builtin_popcount(i)].push_back(i);
// for(int i=0;i<=p;i++)printf("%d\n",v[i].size());
for(int i=0;i<p;i++)if(!mus[i])f[1][id[1<<i]][i]=d1[i];
for(int i=1;i<p;i++)for(int j=0;j<v[i].size();j++)for(int k=0;k<p;k++){
if(!(v[i][j]&(1<<k)))continue;
for(int l=0;l<p;l++)if(!(v[i][j]&(1<<l))&&((mus[l]&v[i][j])==mus[l]))chmn(f[!(i&1)][id[v[i][j]|(1<<l)]][l],f[i&1][j][k]+d[k][l]);
f[i&1][j][k]=0x3f3f3f3f;
}
for(int i=0;i<p;i++)chmn(res,f[p&1][id[(1<<p)-1]][i]+dn[i]);
printf("%d\n",res);
return 0;
}
XIV.[清华集训2012]串珠子
如果直接暴力上状压进行计数是会重复计算的;那么怎样不重不漏地计数呢?
我们发现,要求出连通图的数量是比较难的;但是要求出非联通图的数量是比较简单的,因为我们可以祭出套路。
我们设 表示 集合中所有图的数量(不管联通与否)。再设 表示非联通图的数量, 表示连通图的数量。
则显然,必有 。
首先,显然是很好求出的;在中,我们可以考虑枚举的lowbit
所在的连通块,然后就把它拆成一个 了。
则时间复杂度。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,a[20][20],f[1<<20],g[1<<20],h[1<<20];
//f:all possible situations
//g:all invalid situations
//h:all valid situations
int main(){
scanf("%d",&n),m=1<<n;
for(int i=0;i<n;i++)for(int j=0;j<n;j++)scanf("%d",&a[i][j]);
f[0]=1;
for(int i=1;i<m;i++){
f[i]=f[i^(i&-i)];
for(int j=__builtin_ctz(i)+1;j<n;j++)if(i&(1<<j))f[i]=1ll*f[i]*(a[j][__builtin_ctz(i)]+1)%mod;
}
for(int i=1;i<m;i++){
for(int U=i^(i&-i),V=U;V;V=(V-1)&U)(g[i]+=1ll*h[i^V]*f[V]%mod)%=mod;
h[i]=(f[i]-g[i]+mod)%mod;
}
printf("%d\n",h[m-1]);
return 0;
}
XV.CF913E Logical Expression
XVI.[GYM102832J]Abstract Painting
考虑将一个圆心为 ,半径为 的圆,转换为 轴上线段 ,问题转换为求无交的线段覆盖方案数。
因为所有的圆半径很小(),所以我们考虑状压位置 前面 位的信息(在实际应用中会发现只要状压 位即可,因为第 位必然为 ),表示第 位能否作为一个圆的左端点。当我们在位置 加入一个圆 时,需要保证当前状压的状态中第 位为 ,, 为 的偶数。在加入这么一个圆后,第 位都不能作为圆的左端点,更新状态即可。同时,因为一个位置可以作为不止一个圆的右端点,所以还得枚举作为哪些圆的右端点。因为所有可能的直径只有 ,所以直接 枚举即可。记得判断加入的圆的集合是否包含了必须加入的圆的集合!
总复杂度 ,可以通过。
(附,通过子集枚举等trick可以将复杂度优化到 ,但二者实际效率只不过差了个大约 的常数,而且不加也能过,就不加了)
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,mus[1010],f[1010][1<<9],all=(1<<0)|(1<<2)|(1<<4)|(1<<6)|(1<<8),res;
int fob(int ip){
int lim=1;
while(lim<=ip)lim<<=1;
return lim-1;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),mus[x+y]|=(1<<((y-1)<<1));
f[0][0]=1;
for(int i=1;i<=n;i++)for(int j=all;;j=(j-1)&all){
if((j&mus[i])==mus[i]&&j<(1<<min(i-1,9))){
int J=fob(j);
// printf("%d:%d:%d\n",i,j,J);
for(int k=0;k<(1<<min(i-1,9));k++){
if(k&j)continue;
(f[i][((k<<1)&((1<<9)-1))|J]+=f[i-1][k])%=mod;
}
}
if(!j)break;
}
for(int i=0;i<(1<<9);i++)(res+=f[n][i])%=mod;
printf("%d\n",res);
return 0;
}
XVII.[ZOJ3989]Triangulation
神题。
这个数据范围很难不让人想到状压DP。于是我们考虑应该怎么设计状态。
考虑一组三角剖分的形态:其必定是在所有点所构成的凸包内部划分出很多三角形。这也就表明,任何一组三角剖分一定包含所有凸包上的边。
我们可以想到一个比较简洁的DP:设 表示在点集 所构成的凸包内部的所有点的三角剖分。但是这样子跑的话,每一组三角剖分都会被统计多次,不能不重复地计数。
于是我们另辟蹊径。我们令初始三角剖分上的边仅包含所有点集的下凸壳上的所有边。之后,维护当前剖分的上边界,每次往上边界上添加一个新的三角形,得到新的上边界,这样持续不断直到填成了整个点集的上凸壳。
但是,出于不重复计数的考虑,我们必须对该上边界做出限制。
我们强制该上边界上的点的 坐标严格递增。这样,只需知道点集就能唯一还原出边界,而不必存储点集内部的顺序了。
同时,为了处理有点 坐标相同的情形,我们将所有点随机旋转一个角度,这样便消除了 坐标相同的可能(如果仍相同,继续转即可)。于是以下默认所有点 坐标互不相同。
接着考虑如何添加三角形。
我们发现,其可以分为两种情形:
-
对于边界上连续的三个点 ,满足 在 下方,且 内部无点,此时便可以连边 ,将 从边界上删除。
-
对于边界上连续的两个点 ,存在一个 在 上方,且 的 坐标介于 之间,且 内部无点,此时便可以连边 ,并将 插入上边界。
为了不重复计算,我们每次仅加入上边界上最左的三角形。也就是说,如果一条边 在某一轮转移中没有生长出三角形来,则其之后也一定不会再长三角形。同时,对于情形1,我们将其算作右边的 边长出的三角形,这样便统一了条件。
这样,我们便可以设 表示现在上边界是集合 ,且 中前 条边都已经无法再生长的状态。这时,考虑第 条边,处理其长或不长的状态即可。
需要注意的是,因为 坐标最大最小的两个点无论何时都必在上边界上,所以可以在状态中不记录它们,这样剩下的所有状态都是可能出现的状态了。且因为没有明确的转移顺序,采取bfs转移。
通过预处理一大坨东西(三角形内部有没有点啦、情形2的哪些 合法啦,之类的),我们可以做到 的复杂度,其中 是预处理。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int T,n,ord[20],a[20][20],rev[20],f[1<<16][17],in[1<<16],b[20];
ll g[1<<16][17];
const double eps=1e-13;
const double pi=acos(-1);
int cmp(double x){
if(x>eps)return 1;
if(x<-eps)return -1;
return 0;
}
struct Vector{
double x,y;
Vector(){}
Vector(double X,double Y){x=X,y=Y;}
friend Vector operator +(const Vector &u,const Vector &v){return Vector(u.x+v.x,u.y+v.y);}
friend Vector operator -(const Vector &u,const Vector &v){return Vector(u.x-v.x,u.y-v.y);}
friend Vector operator *(const Vector &u,const double &v){return Vector(u.x*v,u.y*v);}
friend Vector operator /(const Vector &u,const double &v){return Vector(u.x/v,u.y/v);}
friend double operator &(const Vector &u,const Vector &v){return u.x*v.y-u.y*v.x;}//cross times
friend double operator |(const Vector &u,const Vector &v){return u.x*v.x+u.y*v.y;}//point times
friend bool operator <(const Vector &u,const Vector &v){return u.x<v.x;}
double operator ~()const{return sqrt(x*x+y*y);}//the modulo of a vector
double operator !()const{return atan2(y,x);}//the angle of a vector
void print(){printf("(%lf,%lf)",x,y);}
void rotate(double ang){
double modu=~*this;
double angl=!*this;
angl+=ang;
x=cos(angl)*modu,y=sin(angl)*modu;
}
}p[20];
typedef Vector Point;
struct Line{
Point x,y;
Vector z;
Line(){}
Line(Point X,Point Y){x=X,y=Y,z=Y-X;}
friend Point operator &(const Line &u,const Line &v){return u.x+u.z*((v.z&(u.x-v.x))/(u.z&v.z));}
};
typedef Line Segment;
bool CMP(int u,int v){return p[u].x<p[v].x;}
bool lower[20],upper[20];
int stk[20],tp,LO,UP;
queue<int>q;
void trans(int i,int j,int I,int J,int k){
if(!--in[I])q.push(I);
if(f[I][J]>k)f[I][J]=k,g[I][J]=g[i][j];
else if(f[I][J]==k)g[I][J]+=g[i][j];
}
vector<int>v[20][20];
bool abv[20][20][20],ins[20][20][20];//if above, true.
int main(){
scanf("%d",&T);
while(T--){
scanf("%d",&n),LO=UP=0,memset(f,0x3f,sizeof(f)),memset(in,0,sizeof(in)),memset(ins,true,sizeof(ins));
for(int i=0;i<n;i++)scanf("%lf%lf",&p[i].x,&p[i].y),ord[i]=i,lower[i]=upper[i]=false;
while(true){
double ang=(1.0*rand()/RAND_MAX)*2*pi;
for(int i=0;i<n;i++)p[i].rotate(ang);
sort(ord,ord+n,CMP);
bool ok=true;
for(int i=1;i<n;i++)if(!cmp(p[i].x-p[i-1].x)){ok=false;break;}
if(ok)break;
}
for(int i=0;i<n;i++)rev[ord[i]]=i;
for(int i=0;i<n;i++)for(int j=0;j<n;j++)scanf("%d",&a[rev[i]][rev[j]]);
sort(p,p+n);
// for(int i=0;i<n;i++)p[i].print();puts("");
for(int i=0;i<n;i++)for(int j=i+1;j<n;j++)for(int k=i+1;k<j;k++)abv[i][j][k]=(cmp((p[i]-p[k])&(p[j]-p[k]))==1);
stk[++tp]=0;
for(int i=1;i<n;i++){
while(tp>=2&&cmp((p[stk[tp-1]]-p[stk[tp]])&(p[i]-p[stk[tp]]))!=-1)tp--;
stk[++tp]=i;
}
for(int i=1;i<=tp;i++)lower[stk[i]]=true,LO|=1<<stk[i];
LO=(LO^(1<<(n-1)))>>1,f[LO][0]=0,g[LO][0]=1;
for(int i=1;i<tp;i++)f[LO][0]+=a[stk[i]][stk[i+1]];
tp=0;
stk[++tp]=0;
for(int i=1;i<n;i++){
while(tp>=2&&cmp((p[stk[tp-1]]-p[stk[tp]])&(p[i]-p[stk[tp]]))!=1)tp--;
stk[++tp]=i;
}
for(int i=1;i<=tp;i++)upper[stk[i]]=true,UP|=1<<stk[i];
UP=(UP^(1<<(n-1)))>>1;
tp=0;
// printf("STA:%d %d\n",LO,UP);
// for(int i=0;i<n;i++)printf("(%d %d)",lower[i],upper[i]);puts("");
for(int i=0;i<n;i++)for(int j=i+1;j<n;j++){
for(int k=i+1;k<j;k++){
if(abv[i][j][k]){
bool ok=true;
for(int l=i+1;l<k;l++)if(abv[i][j][l]&&!abv[i][k][l]){ok=false;break;}
if(!ok)continue;
for(int l=k+1;l<j;l++)if(abv[i][j][l]&&!abv[k][j][l]){ok=false;break;}
if(!ok)continue;
v[i][j].push_back(k);
}else{
ins[i][j][k]=false;
for(int l=i+1;l<k;l++)if(!abv[i][j][l]&&abv[i][k][l]){ins[i][j][k]=true;break;}
for(int l=k+1;l<j;l++)if(!abv[i][j][l]&&abv[k][j][l]){ins[i][j][k]=true;break;}
}
}
}
for(int i=0;i<(1<<(n-2));i++){
int len=0;b[len++]=0;
for(int j=0;j<n-2;j++)if(i&(1<<j))b[len++]=j+1;
b[len++]=n-1;
// printf("%d:",i);for(int j=0;j<len;j++)printf("%d ",b[j]);puts("");
for(int j=0;j<len;j++){
if(j&&j+1<len&&!ins[b[j-1]][b[j+1]][b[j]])in[i^(1<<(b[j]-1))]++;
if(j)for(auto k:v[b[j-1]][b[j]])in[i|(1<<(k-1))]++;
}
}
q.push(LO);
while(!q.empty()){
int i=q.front();q.pop();
// printf("SS:%d\n",i);
int len=0;b[len++]=0;
for(int j=0;j<n-2;j++)if(i&(1<<j))b[len++]=j+1;
b[len++]=n-1;
for(int j=0;j<len;j++){
if(j&&j+1<len&&!ins[b[j-1]][b[j+1]][b[j]])trans(i,j,i^(1<<(b[j]-1)),j-1,f[i][j]+a[b[j-1]][b[j+1]]);
if(j)for(auto k:v[b[j-1]][b[j]])trans(i,j-1,i|(1<<(k-1)),j-1,f[i][j-1]+a[b[j-1]][k]+a[b[j]][k]);
if(j+2<len)trans(i,j,i,j+1,f[i][j]);
}
}
printf("%d %lld\n",f[UP][__builtin_popcount(UP)],g[UP][__builtin_popcount(UP)]);
for(int i=0;i<n;i++)for(int j=i+1;j<n;j++)v[i][j].clear();
}
return 0;
}
XVIII.[IOI2007] training 训练路径
不要偶环。
那就建出铺设好道路构成的树,然后对于所有非树边求出其在树上对应的路径。如果这个路径与非树边构成偶环,则显然这条边必须要被删掉。否则,如果有两条奇环对应路径有交,则它们只能保留其一。
于是设 表示 的子树中的 号路径传给父亲时的最优答案,当 时则意为不传递任何路径。在 LCA 处,其仅能继承一个儿子 的 ,对于其它的儿子 ,要么 与另一个儿子 的 合并成完整的路径 ,要么直接继承 。这个通过一个状压 DP 即可处理。
该状压 DP 可以求出 ,也可以同时求出从某个儿子继承 的情形。还有一种可能,就是从 出发新建一条路径。这可以从 简单求出。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int val[4010],X[4010],Y[4010],n,m,cnt,f[1010][4010],res;
vector<int>v[1010];
bool u[1010][4010];
bool odd[4010];
int dep[1010];
void dfs1(int x,int fa){dep[x]=dep[fa]+1;for(auto y:v[x])if(y!=fa)dfs1(y,x);}
int g[1<<10];
void dfs2(int x){
for(auto y:v[x]){
// printf("%d->%d\n",x,y);
v[y].erase(find(v[y].begin(),v[y].end(),x));
dfs2(y);
int sum=0;
for(int i=1;i<=cnt;i++)if(f[y][i]!=-1&&u[x][i])sum+=val[i];
f[y][0]+=sum;
for(int i=1;i<=cnt;i++){
if(f[y][i]==-1)continue;
f[y][i]+=sum;
if(u[x][i])f[y][0]=min(f[y][0],f[y][i]-val[i]),f[y][i]=-1,u[x][i]=false;
}
// printf("%d:",y);for(int i=0;i<=cnt;i++)printf("%d ",f[y][i]);puts("");
}
int lim=v[x].size();
memset(g,0x3f,sizeof(g)),g[0]=0;
for(int j=0;j<lim;j++)for(int k=j+1;k<lim;k++){
int mn=0x3f3f3f3f;
for(int i=1;i<=cnt;i++)if(f[v[x][j]][i]!=-1&&f[v[x][k]][i]!=-1)mn=min(mn,f[v[x][j]][i]+f[v[x][k]][i]);
for(int i=0;i<(1<<lim);i++)if((i&(1<<j))&&(i&(1<<k)))g[i]=min(g[i],g[i^(1<<j)^(1<<k)]+mn);
}
for(int i=0;i<lim;i++)for(int j=0;j<(1<<lim);j++)if(j&(1<<i))g[j]=min(g[j],g[j^(1<<i)]+f[v[x][i]][0]);
f[x][0]=g[(1<<lim)-1];
for(int i=0;i<lim;i++)for(int j=1;j<=cnt;j++)if(f[v[x][i]][j]!=-1)f[x][j]=f[v[x][i]][j]+g[(1<<lim)-1-(1<<i)];
int sum=0;
for(int i=1;i<=cnt;i++)if(u[x][i])sum+=val[i];
for(int i=0;i<=cnt;i++)if(f[x][i]!=-1)f[x][i]+=sum;
for(int i=1;i<=cnt;i++)if(u[x][i])f[x][i]=f[x][0]-val[i];
// printf("%d:",x);for(int i=0;i<=cnt;i++)printf("%d ",f[x][i]);puts("");
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,x,y,z;i<=m;i++){
scanf("%d%d%d",&x,&y,&z);
if(!z)v[x].push_back(y),v[y].push_back(x);
else cnt++,val[cnt]=z,X[cnt]=x,Y[cnt]=y;
}
dfs1(1,0);
for(int i=1;i<=cnt;i++){
odd[i]=((dep[X[i]]^dep[Y[i]])&1);
if(odd[i]){res+=val[i];continue;}
u[X[i]][i]=true,u[Y[i]][i]=true;
// printf("%d %d %d\n",X[i],Y[i],i);
}
// printf("%d\n",res);
memset(f,-1,sizeof(f));
dfs2(1);
printf("%d\n",(f[1][0])/2+res);
return 0;
}
XIX.多娜多娜
题意:有 个敌人排成一列,第 个敌人的血量是 。你有五种技能:
- 消耗 点法力,对首个敌人造成 点伤害。
- 消耗 点法力,对第二个敌人造成 点伤害。
- 消耗 点法力,对第三个敌人造成 点伤害。
- 消耗 点法力,对第四个敌人造成 点伤害。
- 消耗 点法力,对前四个敌人各造成 点伤害。
任意时刻如果一个敌人的血量小于等于 则其会立刻出队,紧随其后的敌人会补上它的缺,且这一过程是瞬时的。
求最优情况下干掉所有敌人需要的法力总和。
数据范围:。
一个显然的想法是 DP。设 为队首的四个敌人编号是 且各吃了 发 E
攻击时最优的答案。明显 。搜一下发现合法的 只有 不到种, 只有 种,因而状压一下就开的下。
考虑转移。一种是交一发 E
, 各增加一,且如果出现敌人挂了要上新人。一种是用前四个技能杀一个人。
这时我们发现我们不知道具体可以用什么方法杀它。于是还要额外状压一下每个人曾经到过的位置。设 分别表示这四个集合。它只需要 就能存得下,因为每个位置仅可能到达过其之后的位置。
那么考虑我们已经知道杀人方法集合,应该怎么杀人最快。这个直接背包预处理一下即可。
需要注意的是为了保证这个人是在当前状态下被杀掉的而不是早就死了一直在占位置,杀人的时候最后一发必须用当前位置的方法,也即方法集合中编号最靠后的一个。
这样就行了吗?并不是,我们还要考虑某个人先吃了几发 ABCD
,然后被 E
一发干掉的情形。这时这个人可以用它经过的集合中任何方法打,且对最后一发没有任何要求。这个也可以背包预处理。
总结一下流程:
- 交一发
E
,除去会被直接干掉的那些人。 - 尝试在上一发
E
的基础上结合无限制的ABCD
干掉某些人。 - 尝试不用
E
,直接用有限制的ABCD
干掉某个人。
总结一下复杂度,状态数是 的。单次转移是 的。有一定常数,但是时间复杂度基本上不是问题,关键在于不重不漏地考虑到每种可能。
附,本题的状压是极为必要的,状压的代码一般是 3~4K
,而没状压直接记录 维甚至 维的代码一般要 6~9K
。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=140000;
const int E=40000;
int n,val[10],cst[10],spc,f[16][140100],h[16][140100],g[7200][17][64],hp[22];
int c1,c2;
int mp1[1001000],mp2[1001000];
int pm1[1001000],pm2[1001000];
int HS1(int i,int j,int k,int l){return ((i*(n+1)+j)*(n+1)+k)*(n+1)+l;}
int HS2(int i,int j,int k,int l){return ((i*3+j)*3+k)*3+l;}
int HS3(int i,int j,int k,int l){return (l>>1)+((k>>2)<<3)+((j>>3)<<5);}
struct Tri{
int i,j,k;
Tri(int I,int J,int K){i=I,j=J,k=K;}
void print()const{printf("(%d %d %d)",i,j,k);}
};
Tri HS(Tri i,Tri j,Tri k,Tri l){return Tri(HS1(i.i,j.i,k.i,l.i),HS2(i.j,j.j,k.j,l.j),HS3(i.k,j.k,k.k,l.k));}
int dfs(int,int,int);
int dfs(Tri x){return dfs(mp1[x.i],mp2[x.j],x.k);}
int dfs(int x,int y,int z){
int&res=g[x][y][z];
if(res!=-1)return res;
if(x==1)return res=0;
res=0x3f3f3f3f;
x=pm1[x],y=pm2[y];
//use an E-skill.
Tri d(x%(n+1),y%3,(z&7)<<1|1);x/=n+1,y/=3;
Tri c(x%(n+1),y%3,((z>>3)&3)<<2|2);x/=n+1,y/=3;
Tri b(x%(n+1),y%3,(z>>5)<<3|4);x/=n+1,y/=3;
Tri a(x%(n+1),y%3,8);x/=n+1,y/=3;
// a.print(),b.print(),c.print(),d.print(),puts("");
vector<Tri>v;
if(d.i&&hp[d.i]>(d.j+1)*E)v.emplace_back(d.i,d.j+1,d.k);
if(c.i&&hp[c.i]>(c.j+1)*E)v.emplace_back(c.i,c.j+1,c.k);
if(b.i&&hp[b.i]>(b.j+1)*E)v.emplace_back(b.i,b.j+1,b.k);
if(a.i&&hp[a.i]>(a.j+1)*E)v.emplace_back(a.i,a.j+1,a.k);
for(int j=max(a.i-1,0);v.size()<4;j=max(j-1,0))v.emplace_back(j,0,0);
int sal=max(v.back().i-1,0);
// for(auto i:v)i.print();puts("");
for(int i=0;i<(1<<4);i++){
vector<Tri>u;
int sum=spc;
for(int j=0;j<4;j++){
if(!(i&(1<<j))){u.push_back(v[j]);continue;}
if(!v[j].j){sum=-1;break;}
sum+=f[v[j].k][hp[v[j].i]-v[j].j*E];
}
if(sum==-1)continue;
for(int j=sal;u.size()<4;j=max(j-1,0))u.emplace_back(j,0,0);
res=min(res,dfs(HS(u[3],u[2],u[1],u[0]))+sum);
}
Tri e(max(a.i-1,0),0,0);
if(d.i)res=min(res,dfs(HS(e,a,b,c))+h[d.k][hp[d.i]-d.j*E]);
if(c.i)res=min(res,dfs(HS(e,a,b,d))+h[c.k][hp[c.i]-c.j*E]);
if(b.i)res=min(res,dfs(HS(e,a,c,d))+h[b.k][hp[b.i]-b.j*E]);
if(a.i)res=min(res,dfs(HS(e,b,c,d))+h[a.k][hp[a.i]-a.j*E]);
// a.print(),b.print(),c.print(),d.print(),printf(":%d\n",res);
return res;
}
int main(){
freopen("dohnadohna.in","r",stdin);
freopen("dohnadohna.out","w",stdout);
scanf("%d",&n);
for(int i=0;i<4;i++)scanf("%d",&val[i]);
for(int i=0;i<4;i++)scanf("%d",&cst[i]);
scanf("%d",&spc);
for(int i=n;i;i--)scanf("%d",&hp[i]);
memset(f,0x3f,sizeof(f)),memset(h,0x3f,sizeof(h)),f[0][0]=0;
for(int i=1;i<(1<<4);i++){
int k=0;while(!(i&(1<<k)))k++;
int I=i^(1<<k);
for(int j=0;j<=N;j++){
f[i][j]=f[I][j];
if(j>=val[k])f[i][j]=min(f[i][j],f[i][j-val[k]]+cst[k]);
}
for(int j=val[k];j<=N;j++)h[i][j]=f[i][j-val[k]]+cst[k];
}
for(int i=0;i<(1<<4);i++)for(int j=N;j>=0;j--){
h[i][j]=min(h[i][j],h[i][j+1]);
f[i][j]=min(f[i][j],f[i][j+1]);
}
for(int i=0;i<=n;i++)
for(int j=(i==0?0:i+1);j<=n;j++)
for(int k=(j==0?0:j+1);k<=n;k++)
for(int l=(k==0?0:k+1);l<=n;l++){
int t=HS1(i,j,k,l);
mp1[t]=++c1;
pm1[c1]=t;
}
for(int i=0;i<=2;i++)
for(int j=i;j<=2;j++)
for(int k=j;k<=2;k++)
for(int l=k;l<=2;l++){
int t=HS2(i,j,k,l);
mp2[t]=++c2;
pm2[c2]=t;
}
// printf("%d %d\n",c1,c2);
memset(g,-1,sizeof(g));
printf("%d\n",dfs(c1,1,0));
return 0;
}
XX.[THUWC2017]随机二分图
首先一个状压的想法是很显然的。
然后考虑钦定一组匹配,计算这种匹配合法的概率。
其等于二分之一的 次方,其中 为包含在其中的不同组数。需要注意的是,对于 的组,如果两条边都被包含就只算一次,对于 的组若都被包含这种方法就不合法。
考虑强制令每条边的概率都是二分之一。这样之后,对于 和 且两条边都被选上时,我们有特殊的形式: 时有额外的四分之一系数,表示两条边都出现;这样,原本的二分之一乘二分之一得到四分之一的概率,再加上额外的四分之一概率,得到二分之一。同理, 时额外的系数是负四分之一,仍然表示两条边都出现;这样,两者一加起来就得到了 ,也即两条边同时出现时系数按零计算。
这样,我们一共只涉及到两种转移(带系数),也即加入一条边或同时加入两条边。
考虑按编号最小点的顺序加入边。则在点 处, 都必须匹配, 中至多有 个进行匹配。这样,列出式子得到状态数为
暴力算一下,在 时状态数仅有 不到,进而直接用 map
存一下即可。
代码:
#include<bits/stdc++.h>
using namespace std;
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
using namespace __gnu_pbds;
const int mod=1e9+7;
const int inv2=5e8+4;
const int inv4=1ll*inv2*inv2%mod;
const int vni4=mod-inv4;
int n,m;
vector<pair<int,int> >v[20];
map<int,int>f[20];
void ADD(int&x,int y){if((x+=y)>=mod)x-=mod;}
void update(int x,int y){
// for(int i=0;i<n;i++)printf("%d",(x>>i)&1);putchar('|');
// for(int i=n;i<(n<<1);i++)printf("%d",(x>>i)&1);putchar('\n');
ADD(f[min(__builtin_ctz(~x),n)][x],y);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,a,b,c,d,e;i<=m;i++){
scanf("%d%d%d",&a,&b,&c),b--,c--,c+=n;
if(a==0){v[b].emplace_back(1<<b|1<<c,1);continue;}
scanf("%d%d",&d,&e),d--,e--,e+=n;
if(b>d)swap(b,d),swap(c,e);
v[b].emplace_back(1<<b|1<<c,1);
v[d].emplace_back(1<<d|1<<e,1);
if(b!=d&&c!=e)v[b].emplace_back(1<<b|1<<c|1<<d|1<<e,a==1?1:mod-1);
}
update(0,1);
for(int i=0;i<n;i++)for(auto _:f[i]){
int x=_.first,y=_.second;
for(auto j:v[i])if(!(x&j.first))update(x|j.first,1ll*y*j.second%mod);
}
int res=f[n][(1<<(n<<1))-1];
// for(int i=0;i<n;i++)(res<<=1)%=mod;
printf("%d\n",res);
return 0;
}
XXI.[POJ2052]Fun Game
设 表示已确定 中集合, 是结尾串,且方向是 的方案数。预处理出两两串间的最长公共前后缀。
把所有被其它串包含的串扔掉后,转移是简单的。复杂度 。
代码:
#include<string>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int n,_,com[32][32],f[1<<16][32],res;
int border(string t,string s){
string r=s+t;
static int kmp[210];
kmp[0]=-1;
for(int i=1,j=-1;i<r.size();i++){
while(j!=-1&&r[j+1]!=r[i])j=kmp[j];
if(r[j+1]==r[i])j++;
kmp[i]=j;
}
int p=r.size()-1;
while(p+1>=min(s.size(),t.size()))p=kmp[p];
return p+1;
}
string s[32];
void chmn(int&x,int y){if(x>y)x=y;}
bool cmp(const string&u,const string&v){return u.size()<v.size();}
void mina(){
res=0x3f3f3f3f;
for(int i=0;i<n;i++)cin>>s[i];
sort(s,s+n,cmp);
_=0;
for(int i=0;i<n;i++){
bool ok=true;
for(int j=i+1;j<n;j++){
if(s[j].find(s[i])==string::npos)continue;
string t=s[i];reverse(t.begin(),t.end());
if(s[j].find(t)==string::npos)continue;
ok=false;break;
}
if(ok)s[_++]=s[i];
}
n=_;
for(int i=0;i<n;i++)s[i+n]=s[i],reverse(s[i+n].begin(),s[i+n].end());
for(int i=0;i<(n<<1);i++)for(int j=0;j<(n<<1);j++)com[i][j]=border(s[i],s[j]);
// for(int i=0;i<(n<<1);i++)cout<<s[i]<<endl;
// for(int i=0;i<(n<<1);i++,cout<<endl)for(int j=0;j<(n<<1);j++)cout<<com[i][j]<<' ';
memset(f,0x3f,sizeof(f));
f[1][0]=s[0].size();
for(int j=0;j<(1<<n);j++)for(int k=0;k<(n<<1);k++)if(j&(1<<(k>=n?k-n:k)))
for(int K=0;K<(n<<1);K++)if(!(j&(1<<(K>=n?K-n:K))))
chmn(f[j|(1<<(K>=n?K-n:K))][K],f[j][k]+s[K].size()-com[k][K]);
for(int k=0;k<(n<<1);k++)chmn(res,f[(1<<n)-1][k]-com[k][0]);
cout<<max(res,2)<<endl;
}
int main(){
while(true){
cin>>n;
if(!n)break;
mina();
}
return 0;
}
XXII.[HDU4997]Biconnected
边双连通子图计数,套路是边双缩点后得到边双树然后对该树形结构 DP。
令 表示集合 中的任意子图计数( 的边数次幂)。 表示 中的不连通子图计数(可以通过 做子集枚举处理即可)。 表示 中的连通子图计数(), 表示 中的连通但非点双连通子图计数(可以枚举 中标号最小点所在点双图的大小,然后令若干东西作为它的儿子), 表示点双子图计数()。可以简单在 的时间内解决问题。通过一些处理应该也能在 的时间内解决问题。通过神必集合幂级数技巧可以做到 ,但没有意义。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int T,lim,d[1<<10],e[1<<10],f[1<<10],g[1<<10],h[1<<10],n,m,bin[110],p[1<<10];
bool c[10][10];
int S[1<<10][1<<10];//number of edges connected two sets.
void mina(){
scanf("%d%d",&n,&m),lim=1<<n;
bin[0]=1;for(int i=1;i<=n*n;i++)bin[i]=(bin[i-1]<<1)%mod;
for(int i=0;i<n;i++)for(int j=0;j<n;j++)c[i][j]=(i!=j);
for(int x,y;m--;)scanf("%d%d",&x,&y),x--,y--,c[x][y]=c[y][x]=false;
for(int i=1;i<lim;i++)p[i]=(i&1?0:p[i>>1]+1);
for(int i=1;i<lim;i++){
f[i]=0;
for(int j=0;j<n;j++)if(i&(1<<j))for(int k=j+1;k<n;k++)if(i&(1<<k))f[i]+=c[j][k];
f[i]=bin[f[i]];
}
for(int i=1;i<lim;i++){
g[i]=0;
for(int j=i^(i&-i);j;j=(j-1)&(i^(i&-i)))//j is the set not connected with p[i].
(g[i]+=1ll*h[i^j]*f[j]%mod)%=mod;
h[i]=(f[i]+mod-g[i])%mod;
// printf("%d:%d,%d,%d\n",i,f[i],g[i],h[i]);
}
for(int i=1;i<lim;i++)for(int j=(lim-1)^i;j;j=(j-1)&((lim-1)^i)){
S[i][j]=0;
for(int x=0;x<n;x++)if(i&(1<<x))for(int y=0;y<n;y++)if(j&(1<<y))
S[i][j]+=c[x][y];
// printf("{%d,%d:%d}\n",i,j,S[i][j]);
}
for(int i=1;i<lim;i++){
d[i]=0;
for(int j=i^(i&-i);j;j=(j-1)&(i^(i&-i))){
//the nodes in the complementary set of j are biconnected with p[i]
static int b[1<<10];
b[j]=e[i^j];
for(int k=j;k;k=(k-1)&j)
for(int t=k;t;t=(t-1)&k)
if(t&(k&-k))
// printf("%d,%d:%d,%d[%d,%d],%d\n",k,t,b[k],S[t][i^j],t,i^j,h[t]),
(b[t^k]+=1ll*b[k]*S[t][i^j]%mod*h[t]%mod)%=mod;
// printf("<%d,%d>\n",j,b[0]);
(d[i]+=b[0])%=mod,b[0]=0;
for(int k=j;k;k=(k-1)&j)b[k]=0;
}
e[i]=(h[i]+mod-d[i])%mod;
// printf("%d:%d,%d\n",i,d[i],e[i]);
}
printf("%d\n",e[lim-1]);
}
int main(){scanf("%d",&T);while(T--)mina();return 0;}
XXIII.[AGC012E] Camel and Oases
非常显然的是,所有可能的容量上界仅有 个;对于每种容量上界,此时从每个位置出发可达的位置是一段区间,且:
- 对于某个容量上界,所有位置对应的区间两两相等或不交。这意味着,所有区间构成了整个序列的一组划分。
- 较小的容量上界的划分必然兼容较大上界的划分。这意味着,小的划分是由大的划分中,每段在额外裂成若干子段构成。
- 可以用一棵 层的类线段树结构描述之。
考虑我们可以在上面执行的操作,会发现每一步“跳跃”都是选择某个容量上限时的划分中的某段子段,使得所有选中子段的并集为全集。
放到外面的树状结构上,操作就是除根外每层选择一个节点,使得所有叶子都有至少一个祖先被选中。特别地,不同的起点就等价于根节点的儿子层选中的节点是固定的。
那么我们现在就要解决这么一个问题。
假如不钦定第一层的结果,我们应该怎么做呢?答案是 DP:令 表示 中的深度已被确定时,能覆盖的最长前缀。转移枚举下一步放什么即可。复杂度是 。
现在钦定了,怎么办呢?未被覆盖的部分必然是一段前缀和一段后缀。令 表示后缀的结果,然后枚举哪个集合赋给前缀,然后其补集赋给后缀即可。由此,我们可以 回答单次询问。
于是我们得到了一个平方做法……并不是!
因为我们一共只能选中 个节点,而如果根节点自身就有超过 个儿子,则必然是不合法的!
于是我们一共只需回答 次询问即可了!复杂度对数。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,a[200100],lp[20][200100],rp[20][200100],f[1<<20],g[1<<20],LG;
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int _=0;;_++){
if(!(m>>_)){
LG=_+1;
for(int i=1;i<=n;i++)lp[_][i]=rp[_][i]=i;
break;
}
for(int l=1,r;l<=n;l=r+1){
for(r=l;r<n&&a[r+1]<=a[r]+(m>>_);r++);
for(int i=l;i<=r;i++)lp[_][i]=l,rp[_][i]=r;
}
}
// for(int i=0;i<LG;i++){
// for(int j=1;j<=n;j++)printf("[%d,%d]",lp[i][j],rp[i][j]);
// puts("");
// }
memset(f,-1,sizeof(f)),memset(g,0x3f,sizeof(g));
f[0]=0,g[0]=n+1;
for(int i=0;i<(1<<LG);i++){
// printf("<%d,%d>\n",f[i],g[i]);
if(f[i]!=-1)
for(int j=0;j<LG;j++)if(!(i&(1<<j))){
f[i|(1<<j)]=max(f[i|(1<<j)],f[i]);
if(f[i]!=n)f[i|(1<<j)]=max(f[i|(1<<j)],rp[j][f[i]+1]);
}
if(g[i]!=0x3f3f3f3f)
for(int j=0;j<LG;j++)if(!(i&(1<<j))){
g[i|(1<<j)]=min(g[i|(1<<j)],g[i]);
if(g[i]!=1)g[i|(1<<j)]=min(g[i|(1<<j)],lp[j][g[i]-1]);
}
}
// if(f[(1<<n)-1]<n){
// for(int i=1;i<=n;i++)puts("Impossible");return 0;
// }
for(int i=1;i<=n;i=rp[0][i]+1){
bool ok=false;
for(int j=0;j<(1<<LG);j++)if(!(j&1)){
int k=((1<<LG)-2)^j;
// printf("|%d,%d|:%d,%d\n",j,k,f[j],g[k]);
ok|=(f[j]>=i-1&&g[k]<=rp[0][i]+1);
}
for(int j=i;j<=rp[0][i];j++)
puts(ok?"Possible":"Impossible");
}
return 0;
}
XXIV.[AGC016F] Games on DAG
显然是两个独立的游戏。于是我们就要求出 的方案数。
这等价于 减去 。
函数应该怎样利用呢?对于 的元素,其有着如下的性质:
- 其间两两不能连边。
- 对于每个 ,都至少向一个 的元素连边。
于是我们考虑枚举 ,找到所有 的元素。
我们将集合划成三类:小于 、等于 与大于 。我们已经保证小于向等于和大于都有连边了。
对于每条等于向大于的边,其选取与否是没有影响的。
对于每个大于中的点,其至少要向某个等于中的点连边。
必须同时被加入等于类。
于是我们模拟上述流程即可。时间复杂度可以轻松做到 。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,f[1<<16],res;
int sta[16];
int main(){
scanf("%d%d",&n,&m);
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),sta[x-1]|=1<<(y-1);
f[0]=1;
for(int i=0;i<(1<<n);i++)
for(int j=(1<<n)-1-i;j;j=(j-1)&((1<<n)-1-i))
if((j&3)==3||!(j&3)){
int coe=f[i];
for(int k=0;k<n;k++)if(!(j&(1<<k))&&!(i&(1<<k)))
coe=1ll*coe*((1<<__builtin_popcount(sta[k]&j))-1)%mod;
for(int k=0;k<n;k++)if(j&(1<<k))
coe=1ll*coe*(1<<__builtin_popcount(sta[k]&((1<<n)-1-i-j)))%mod;
(f[i|j]+=coe)%=mod;
}
int tot=1;
for(int i=1;i<=m;i++)(tot<<=1)%=mod;
printf("%d\n",(tot+mod-f[(1<<n)-1])%mod);
return 0;
}
XXV.最大生成树
给定一棵树,点带 中的点权,边带 中的边权。
你要选择不超过 个节点,且两两点的点权均不同。最大化这些点张成的连通块中的边权和。
- 张成连通块的定义:一条边在连通块中,当且仅当存在一条连接两个选中点的简单路径,且该路径包含该边。
数据范围:。
这么小显然启发我们状压。但是显然我们不能直接状压所有颜色。
考虑对 中的所有点权映射到一个 中的随机点权。则只要答案的 个点的点权彼此不同,最终就会被表出。期望要随机 次才能随到一组合法解。确定点权后可以直接 暴力树形 DP 或者 用集合幂级数优化。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
mt19937 rnd(std::chrono::steady_clock::now().time_since_epoch().count());
#define gene(x,y) uniform_int_distribution<int>(x,y)(rnd)
int n,m,a[4010],head[4010],cnt;
struct node{int to,next,val;}edge[8010];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++;
}
ll res;
ll f[4010][1<<5];
int b[4010];
void dfs(int x,int fa){
f[x][0]=0;
for(int i=0;i<(1<<m);i++)if(i&(1<<b[a[x]]))f[x][i]=0;
for(int i=head[x],y;i!=-1;i=edge[i].next)if((y=edge[i].to)!=fa){
dfs(y,x);
for(int j=(1<<m)-1;j>=0;j--)
for(int U=(1<<m)-1-j,u=U;u;u=(u-1)&U)
if(u+1!=(1<<m))
f[x][j|u]=max(f[x][j|u],f[x][j]+f[y][u]+edge[i].val);
}
res=max(res,f[x][(1<<m)-1]);
}
int main(){
freopen("treemax.in","r",stdin);
freopen("treemax.out","w",stdout);
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
for(int _=200;_;_--){
for(int i=1;i<=n;i++)b[i]=gene(0,m-1);
memset(f,0xc0,sizeof(f));
dfs(1,0);
}
printf("%lld\n",res);
return 0;
}
II.轮廓线 DP
比较烦人的一类 DP。
I.[LOJ#2372][CEOI2002]臭虫集成电路公司
考虑轮廓线DP。因为有 的矩形存在,所以要压两行。又因为两行的状态只有可能是 00,01,10
之一,所以压三进制。又因为卡空间,所以要滚动数组。时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int T,n,m,p,f[2][10][60000],tri[11],res;
bool bad[200][10];
void chmx(int&x,int y){if(x<y)x=y;}
int sta(int x,int y){return(x/tri[y])%3;}
void solve(){
scanf("%d%d%d",&n,&m,&p),memset(f,-1,sizeof(f)),memset(bad,false,sizeof(bad)),res=0;
for(int i=1,x,y;i<=p;i++)scanf("%d%d",&x,&y),x--,y--,bad[x][y]=true;
int S=0;for(int i=0;i<m;i++)if(bad[0][i])S+=tri[i];
f[1][0][S]=0;
for(int i=1;i<n;i++){
memset(f[!(i&1)],-1,sizeof(f[!(i&1)]));
for(int j=0;j<m;j++)for(int k=0;k<tri[m];k++){
if(f[i&1][j][k]==-1)continue;
int now=f[i&1][j][k];
if(j+1<m&&i+1<n&&!bad[i][j]&&!bad[i][j+1]&&!bad[i+1][j]&&!bad[i+1][j+1]&&sta(k,j)==0&&sta(k,j+1)==0)chmx(f[(i+(j+2==m))&1][(j+2)%m][k+tri[j]*2+tri[j+1]*2],now+1);
if(j+2<m&&!bad[i][j]&&!bad[i][j+1]&&!bad[i][j+2]&&sta(k,j)==0&&sta(k,j+1)==0&&sta(k,j+2)==0)chmx(f[(i+(j+3==m))&1][(j+3)%m][k+tri[j]+tri[j+1]+tri[j+2]],now+1);
chmx(f[(i+(j+1==m))&1][(j+1)%m][(sta(k,j)>=1?k-tri[j]:k)+bad[i][j]*tri[j]],now);
}
}
for(int i=0;i<tri[m];i++)res=max(res,f[n&1][0][i]);
printf("%d\n",res);
}
int main(){
tri[0]=1;for(int i=1;i<=10;i++)tri[i]=tri[i-1]*3;
scanf("%d",&T);while(T--)solve();
return 0;
}
III.[TopCoder12620]StringPath
考虑沿对角线 DP。一个对角线长度不超过 ;考虑我们在对角线上要记录什么信息。
然后发现,我们需要记录当前沿着两条路径,可以走到对角线上的哪些位置。
因为第 位可能相同,所以总方案数是 的。
沿对角线 DP 也可以使用轮廓线 DP。每次确定一个位置,更新其能否被两个串到达即可。
时间复杂度 。细节很多,要好好分类。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+9;
int f[2][1<<18];
class StringPath{
private:
int len[9][9];
public:
int countBoards(int n,int m,string A,string B){
if(A[0]!=B[0])return 0;
f[0][3]=1;
if(n>m)swap(n,m);
for(int i=0;i<n;i++)for(int j=0;j<m;j++){
if(i+j<n)len[i][j]=i+j+1;else len[i][j]=min(n+m-i-j-1,n);
if(i+j>=n&&i!=n-1)len[i][j]++;
}
// for(int i=0;i<n;i++){for(int j=0;j<m;j++)printf("%d ",len[i][j]);puts("");}
int X=0,Y=1;
for(int i=0;i<n+m-2;i++)for(int j=0,_=1;j<n;j++){
int k=i-j;
if(k>=m||k<0)continue;
// printf("%d %d %d\n",j,k,_);
for(int t=0;t<(1<<(len[j][k]<<1));/*printf("%d,%d,%d:%d\n",j,k,t,f[X][t]),*/t++)
if(!k||j==n-1){
if(i+1<m){
if(A[i+1]==B[i+1])
(f[Y][t<<2|(t&3)]+=f[X][t])%=mod,
(f[Y][t<<2]+=25ll*f[X][t]%mod)%=mod;
else
(f[Y][t<<2|(t&1)]+=f[X][t])%=mod,
(f[Y][t<<2|(t&2)]+=f[X][t])%=mod,
(f[Y][t<<2]+=24ll*f[X][t]%mod)%=mod;
}else{
int T=(t>>2)<<2,S=((t>>2)&3)|(t&3);
if(k==m-2)T=0;
// printf("<%d,%d>\n",S,T);
if(A[i+1]==B[i+1])
(f[Y][T|S]+=f[X][t])%=mod,
(f[Y][T]+=25ll*f[X][t]%mod)%=mod;
else
(f[Y][T|(S&1)]+=f[X][t])%=mod,
(f[Y][T|(S&2)]+=f[X][t])%=mod,
(f[Y][T]+=24ll*f[X][t]%mod)%=mod;
}
}else{
int T=t&~(3<<(_<<1)),S=((t>>(_<<1))&3)|((t>>((_+1)<<1))&3);
if(j==n-2)T=T&~(3<<((_+1)<<1));
// printf("<%d,%d>\n",S,T);
if(A[i]==B[i])
(f[Y][T|(S<<(_<<1))]+=f[X][t])%=mod,
(f[Y][T]+=25ll*f[X][t]%mod)%=mod;
else
(f[Y][T|((S&1)<<(_<<1))]+=f[X][t])%=mod,
(f[Y][T|((S&2)<<(_<<1))]+=f[X][t])%=mod,
(f[Y][T]+=24ll*f[X][t]%mod)%=mod;
}
_++;
memset(f[X],0,sizeof(f[X]));
swap(X,Y);
}
return f[X][3];
}
}my;
III.[HDU5079]Square
首先我们显然可以对于全体边长 ,求出能塞下至少一个边长为 的正方形的方案——这等价于存在大于等于边长为 的正方形的方案。然后差分即可得到边长恰为 的方案。
然后考虑对于某个 怎么求解——显然可以容斥,即钦定若干位置放正方形,其容斥系数可以简单计算,且方案数即为未被钦定到的位置个数。
考虑按照轮廓线 DP。我们同时记录对于每一列,覆盖该列的 最靠下的正方形 的下边界位置。
乍一看这玩意好像是 的垃圾复杂度;但是事实上并没有那么差。
注意到如果最靠下的下边界还在当前枚举到的行的上方,则我们显然可以把下边界就当作恰在当前行上方。这不会影响状态。
那么这样,我们就将状态数优化到了 。
不止这样!当 比较大的时候,最后一次修改必然会导致状态中出现一段长为 的连续相同边界。故方案数事实上是 的。
因此它效果非常好。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
typedef unsigned long long ull;
int T,n,bin;
char s[10][10];
unordered_map<ull,int>mp[10][10];
int LEN;
int dfs(int x,int y,ull sta){
if(x==n)return 1;
if(mp[x][y].find(sta)!=mp[x][y].end())return mp[x][y][sta];
int&res=mp[x][y][sta];
int X=(sta>>(y<<3))&7;
if(!x&&!X)X--;
// printf("[%d,%d,%d:%d]\n",x,y,sta,X);
if(X>=x&&s[x][y]=='*')return 0;
if(X<x){
ull STA=sta&~(7ull<<(y<<3));
X++;
STA|=((ull)X<<(y<<3));
res=dfs(y==n-1?x+1:x,(y+1)%n,STA);
if(s[x][y]=='o')(res<<=1)%=mod;
}else res=dfs(y==n-1?x+1:x,(y+1)%n,sta);
if(x+LEN>n||y+LEN>n||s[x][y]=='*')return res;
ull STA=sta;
for(int t=y;t<y+LEN;t++)STA&=~(7ull<<(t<<3)),STA|=(ull)(x+LEN-1)<<(t<<3);
(res+=mod-dfs(y==n-1?x+1:x,(y+1)%n,STA))%=mod;
// printf("<%d,%d,%d:%d>\n",x,y,sta,res);
return res;
}
int f[10];
void mina(){
scanf("%d",&n);
for(int i=0;i<n;i++)scanf("%s",s[i]);
bin=1;for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(s[i][j]=='o')bin=(bin<<1)%mod;
f[0]=bin;
for(LEN=1;LEN<=n;LEN++){
f[LEN]=(bin+mod-dfs(0,0,0))%mod;
for(int i=0;i<n;i++)for(int j=0;j<n;j++)mp[i][j].clear();
}
for(int i=0;i<n;i++)(f[i]+=mod-f[i+1])%=mod;
for(int i=0;i<=n;i++)printf("%d\n",f[i]);
}
int main(){scanf("%d",&T);while(T--)mina();return 0;}
IV.[AGC017F] Zigzag
以下是正解题解。
难点主要是转移。但是我们仍然可以使用类轮廓线的思想,在两条折线之间转移时,令 表示:
- 当前转移到了第 层。
- 已转移部分的态是 。
- 未转移部分的态是 (即,上一条直线的态是 )。
- 当前已转移部分中,两根直线的距离差是 。
此时的方案数。
但是复杂度就是 的,不够优秀。
事实上,我们考虑对上一条直线作一些调整。即,当 往左走而 往右走时,把 的下一个 赋成 即可。
这样复杂度就变成 。
III.插头 DP/连通块 DP
更加烦人的两类 DP。因为类型相似且复杂程度相似就归到了一起。
I.[URAL1519]Formula 1/【模板】插头dp
插头DP第一题。
插头DP可以干什么呢?其可以处理的问题有着鲜明的特征:
- 必须是网格图。
- 必须有关连通性。
- 数据范围必须是状压常规范围。
满足这三点才有使用插头DP的可能性。
插头DP首先是轮廓线DP的升级版——其同样是储存了一条轮廓线的信息。不同的是,常规轮廓线DP储存的是方格的信息,而插头DP储存的是方格边的信息。正因如此,轮廓线DP的状态规模是 次幂,而插头DP的状态规模则是 次幂(当前处理的格子同时存在左边与上边),其中 是网格边长。
然后就要涉及状态了。
”哈密顿回路“,我们考虑在每条边上储存一个信息。储存这条边是否被某条路径经过?这样不好,因为最终可能匹配出奇奇怪怪的图案。一个厉害的想法是,我们边上储存一个状态, 表示该边不在哈密顿路径上, 表示该边与其右侧第一个未被匹配的 匹配, 表示该边与其左侧第一个未被匹配的 匹配。
什么意思?因为路径不可能交叉,所以在这条轮廓线上,匹配的边(即在已经DP完的部分中存在一条路径连接着的边)必定是如括号序列一样匹配的,要不然就会出现交叉。因此, 状态即可被看作是左括号, 状态即可被看作是右括号,一个合法的插头DP状态即等效于一组括号匹配(当然,一些位置可以空置)。
我们来设计DP吧!显然,我们应该设计 表示方格 ,插头状态是 时的答案。因为总共有 种不同状态,所以 是一个三进制数。
好像不太方便的样子?那就用四进制吧。
好像会爆空间的样子?那就预先搜索出所有合法状态,哈希一下就行了。
好像不太好DP的样子?那就记忆化搜索吧。
现在我们考虑当前DP的格子,其有一个左插头和一个上插头。
-
左上插头都为 。
因为是哈密顿路径,所以该点必须有插头,那就同时新建一个下插头和一个右插头,下插头是 ,右插头是 ,令它们匹配。
-
一个插头为 。
则路径可以在这里拐弯,就让另一个插头进一步跑到下或右即可。
-
两插头都非零。
这时就要分情况了。但无论如何此二插头必须相连。
- 两插头都为 。
相连的话,我们会发现上插头匹配的 插头从此变成了 插头。暴力找到更改即可。
- 两插头都为 。
同理,左插头匹配的 插头变成 插头。
- 左为 上为 。
二者匹配,则路径已经形成闭环。此时需要保证其它位置不再有插头存在,且再往后也没有空位了。
- 左为 上为 。
直接匹配即可。
特别地,如果当前格是不合法格,唯一的合法转移就是由 到 。
特别地,如果当前格是某行最后一格,则其不能转移到有左插头的状态。在此基础上,应该整体左移一位来空出下一行的首个左插头。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int sta[50000],cnt,n,m,lasx,lasy;
map<int,int>mp;
ll f[12][12][50000];
char s[12][13];
void dfs1(int pos,int val,int msk){
if(pos==m+1){
if(!val)sta[++cnt]=msk,mp[msk]=cnt;
return;
}
dfs1(pos+1,val+1,(msk<<2)+2);
if(val)dfs1(pos+1,val-1,(msk<<2)+1);
dfs1(pos+1,val,msk<<2);
}
ll dfs(int,int,int);
ll nex(int x,int y,int msk){
if(y==m-1){
if(msk>>(m<<1))return 0;
return dfs(x+1,0,mp[msk<<2]);
}
return dfs(x,y+1,mp[msk]);
}
int nex2(int y,int msk){
for(int i=y,k=0;i<=m;i++){
if(((msk>>(i<<1))&3)==1)k++;
if(((msk>>(i<<1))&3)==2&&!k--)return i;
}
}
int las1(int y,int msk){
for(int i=y,k=0;i>=0;i--){
if(((msk>>(i<<1))&3)==2)k++;
if(((msk>>(i<<1))&3)==1&&!k--)return i;
}
}
ll dfs(int x,int y,int id){
if(x==n)return 0;
if(f[x][y][id]!=-1)return f[x][y][id];
ll&r=f[x][y][id];r=0;
int msk=sta[id];
// printf("%d %d %d:%d\n",x,y,id,msk);
int L=(msk>>(y<<1))&3,U=(msk>>((y+1)<<1))&3;
if(s[x][y]=='*')return r=(L||U?0:nex(x,y,msk));
msk-=(L<<(y<<1))+(U<<((y+1)<<1));
if(!L&&!U)return r=nex(x,y,msk+(1<<(y<<1))+(2<<((y+1)<<1)));
if(!L||!U)return r=nex(x,y,msk+((L+U)<<((y+1)<<1)))+nex(x,y,msk+((L+U)<<(y<<1)));
if(L==1&&U==1)return r=nex(x,y,msk-(1<<(nex2(y,msk)<<1)));
if(L==2&&U==2)return r=nex(x,y,msk+(1<<(las1(y,msk)<<1)));
if(L==1&&U==2)return r=(!msk&&x==lasx&&y==lasy);
if(L==2&&U==1)return r=nex(x,y,msk);
}
int main(){
scanf("%d%d",&n,&m);
dfs1(0,0,0);
/*for(int i=1;i<=cnt;i++){
printf("%d:",sta[i]);
for(int j=0;j<=m;j++)printf("%d",(sta[i]>>(j<<1))&3);
puts("");
}*/
for(int i=0;i<n;i++)scanf("%s",s[i]);
lasx=n-1,lasy=m-1;
while(s[lasx][lasy]=='*'){
lasy--;
if(lasy<0)lasx--,lasy=m-1;
}
memset(f,-1,sizeof(f));
printf("%lld\n",dfs(0,0,mp[0]));
return 0;
}
II.[Atcoder-Typical DP contest-S]マス目
翻译:一个 行 列的网格图,每个格子可以涂黑或者涂白,求左上格子与右下格子只经过黑格子联通的方案数(当然,这两个格子自身也要是黑的)。
可以插头DP吗?不可以,因为这不是路径而是连通块,你找不到合适的插头。所以,往插头DP方向的思考就可以省省了。
那怎么办?
因为 最大只有 ,因此同一行上最多只有 个连通块,则每个格子最多只有 个状态,可以 存储!
但是这样做就需要枚举下一行的状态,转移是 的,还要进行 次,好像不太靠谱……
事实上,这个算法是可以通过的,因为我们可以预处理出合法的状态(即,相邻的黑格归属同一连通块、自左上角开始的连通块从未中断的状态),事实上这并不是很多。
我个人没有实现这个算法,不过如果状态不多的话甚至可以优化到矩阵快速幂(?)
我们当然也可以选择轮廓线DP。不过画出图来的话就会发现此时同一条轮廓线上最大连通块数就是 ,那么每个格子就需要 进制来存。 进制太丑陋于是换成 进制, 进制开不下于是预处理合法状态,然后总是就是非常麻烦。
按照这种写法,复杂度是 的。轻松通过,跑得飞快。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,f[6][110][16000],nexid[16000],res;
int sta[16000],id[1<<24],cnt;
bool occ[5];
void pr(int k){for(int i=0;i<n;i++)printf("%d",(k>>i*3)&7);}
int main(){
scanf("%d%d",&n,&m);
for(int k=0;k<(1<<n*3);k++){
bool ok=false;
for(int l=0;l<n&&!ok;l++)ok|=(((k>>l*3)&7)==1);
if(!ok)continue;
for(int l=0;l<n&&ok;l++)ok&=(((k>>l*3)&7)<=4);
if(!ok)continue;
cnt++,sta[cnt]=k,id[k]=cnt;
for(int i=0;i<5;i++)occ[i]=false;
for(int l=0;l<n;l++)occ[(k>>l*3)&7]=true;
for(int i=1;i<5;i++)if(!occ[i]){nexid[cnt]=i;break;}
}
// for(int i=1;i<=cnt;i++)pr(sta[i]),puts("");
f[0][0][id[1]]=1;
for(int j=0;j<m;j++)for(int i=0;i<n;i++)for(int msk=1;msk<=cnt;msk++){
if(!f[i][j][msk])continue;
// printf("%d %d ",i,j);pr(sta[msk]);printf(":%d\n",f[i][j][msk]);
int I=(i+1)%n,J=j+(i==n-1),k=sta[msk];
int L=(k>>I*3)&7,U=0;
if(I)U=(k>>(I-1)*3)&7;
int K=k-(L<<I*3);
(f[I][J][id[K]]+=f[i][j][msk])%=mod;
if(!L&&!U){(f[I][J][id[K|(nexid[msk]<<I*3)]]+=f[i][j][msk])%=mod;continue;}
if(!L||!U){(f[I][J][id[K|((L+U)<<I*3)]]+=f[i][j][msk])%=mod;continue;}
if(L>U)swap(L,U);
for(int l=0;l<n;l++)if(((K>>l*3)&7)==U)K-=((U-L)<<l*3);
K+=(L<<I*3);
// pr(k);printf(":%d->",I);pr(K);printf(",%d,%d\n",L,U);
(f[I][J][id[K]]+=f[i][j][msk])%=mod;
}
for(int i=1;i<=cnt;i++)if(((sta[i]>>((n-1)*3))&7)==1)(res+=f[n-1][m-1][i])%=mod;
printf("%d\n",res);
return 0;
}
III.[UVA10572]Black & White
和上题类似,但更猛。
IV.[HNOI2007]神奇游乐园
事可爱的插头DP板子。
因为空间开小数组越界挂了很久,调出来时精神崩溃。
代码:
#include<bits/stdc++.h>
using namespace std;
const int fni=0xc0c0c0c0;
int n,m,f[110][6][1<<(7<<1)],a[110][6];
void pr(int x){for(int i=0;i<=m;i++)printf("%d",(x>>(i<<1))&3);}
bool vis[110][6][1<<(7<<1)];
int dfs(int,int,int);
int nex(int x,int y,int k){
if(y==m-1){if(k>>(m<<1))return fni;return dfs(x+1,0,k<<2);}
return dfs(x,y+1,k);
}
int nex2(int y,int k){
for(int i=y,j=0;i<=m;i++){
if(((k>>(i<<1))&3)==1)j++;
if(((k>>(i<<1))&3)==2&&!j--)return i;
}
}
int las1(int y,int k){
for(int i=y,j=0;i>=0;i--){
if(((k>>(i<<1))&3)==2)j++;
if(((k>>(i<<1))&3)==1&&!j--)return i;
}
}
int dfs(int x,int y,int k){
if(x==n+1)return fni;
int&r=f[x][y][k];
if(vis[x][y][k])return r;vis[x][y][k]=true;
int L=(k>>(y<<1))&3,U=(k>>((y+1)<<1))&3;
int K=k-(L<<(y<<1))-(U<<((y+1)<<1));
if(!L&&!U){
r=max(r,nex(x,y,K));
r=max(r,nex(x,y,K+(1<<(y<<1))+(2<<((y+1)<<1)))+a[x][y]);
}else if(!L||!U){
r=max(r,nex(x,y,K+((L+U)<<(y<<1)))+a[x][y]);
r=max(r,nex(x,y,K+((L+U)<<((y+1)<<1)))+a[x][y]);
}
if(L==1&&U==2&&!K)r=max(r,a[x][y]);
if(L==2&&U==1)r=max(r,nex(x,y,K)+a[x][y]);
if(L==1&&U==1)r=max(r,nex(x,y,K-(1<<(nex2(y,K)<<1)))+a[x][y]);
if(L==2&&U==2)r=max(r,nex(x,y,K+(1<<(las1(y,K)<<1)))+a[x][y]);
return r;
}
int main(){
scanf("%d%d",&n,&m),memset(f,fni,sizeof(f));
for(int i=1;i<=n;i++)for(int j=0;j<m;j++)scanf("%d",&a[i][j]);
printf("%d\n",dfs(1,0,0));
return 0;
}
V.[SCOI2011]地板
这也是插头DP?
事实上,插头DP是一类总称,一切在状态中记录插头的DP都可以被称作广义插头DP。
比如说本题,我们记录的插头就是当前边的状态是:
- 未被任何L形跨越。
- 被一个尚未拐弯的L形跨越。
- 被一个已经拐弯的L形跨越。
老样子,四进制状态储存三进制。
然后就可以转移了。
-
左方、上方均有插头。
此时,除非两插头均为1(此时可以合并为一条完整的L形),否则状态不合法。
-
仅有一边有插头。
首先,显然这个插头可以继续延长一格。
- 如果这个插头是1,那么它还可以拐弯。
- 如果这个插头是2,那么它还可以中止。
-
两边都没有插头。
- 如果这个格子有柱子,显然这是此时唯一合法的情形。
- 否则,可以选择新建两个2路径,或者新建一个1路径。
DP即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=20110520;
int n,m,f[2][200100],hs[5000000],sta[200100],cnt;//0:no route 1:a not-yet-turned route 2:a turned route
void pr(int x){for(int i=0;i<=m;i++)printf("%d",(x>>(i<<1))&3);}
char s[110][110];
void dfs1(int pos,int msk){
if(pos==m+1){cnt++,hs[msk]=cnt,sta[cnt]=msk;return;}
for(int i=0;i<3;i++)dfs1(pos+1,msk+(i<<(pos<<1)));
}
int nex(int y,int k){
if(y==m-1)return (k>>(m<<1))?0:hs[k<<2];
return hs[k];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)scanf("%s",s[i]);
if(n<m){
for(int i=0;i<n;i++)for(int j=i+1;j<m;j++)swap(s[i][j],s[j][i]);
swap(n,m);
}
dfs1(0,0);
// for(int i=1;i<=cnt;i++)pr(sta[i]),puts("");
f[0][hs[0]]=1;
int P=0,Q=1;
for(int i=0;i<n;i++)for(int j=0;j<m;j++,P^=1,Q^=1){
for(int id=1;id<=cnt;id++)f[Q][id]=0;
for(int id=1;id<=cnt;id++){
if(!f[P][id])continue;
int k=sta[id];
int L=(k>>(j<<1))&3,U=(k>>((j+1)<<1))&3;
int K=k-(L<<(j<<1))-(U<<((j+1)<<1));
if(s[i][j]=='*'){
if(!L&&!U)(f[Q][nex(j,K)]+=f[P][id])%=mod;
continue;
}
if(L&&U){
if(L==1&&U==1)(f[Q][nex(j,K)]+=f[P][id])%=mod;
continue;
}
if(L){
(f[Q][nex(j,K+(L<<((j+1)<<1)))]+=f[P][id])%=mod;
if(L==1)(f[Q][nex(j,K+(2<<(j<<1)))]+=f[P][id])%=mod;
if(L==2)(f[Q][nex(j,K)]+=f[P][id])%=mod;
}
if(U){
(f[Q][nex(j,K+(U<<(j<<1)))]+=f[P][id])%=mod;
if(U==1)(f[Q][nex(j,K+(2<<((j+1)<<1)))]+=f[P][id])%=mod;
if(U==2)(f[Q][nex(j,K)]+=f[P][id])%=mod;
}
if(!L&&!U){
(f[Q][nex(j,K+(2<<(j<<1))+(2<<((j+1)<<1)))]+=f[P][id])%=mod;
(f[Q][nex(j,K+(1<<(j<<1)))]+=f[P][id])%=mod;
(f[Q][nex(j,K+(1<<((j+1)<<1)))]+=f[P][id])%=mod;
}
}
}
printf("%d\n",f[P][hs[0]]);
return 0;
}
VI.[SCOI2012]喵星人的入侵
超猛的插头DP。题解
VII.[COCI2020-2021#3] Selotejp
随随便便用轮廓线记录插头即可。有插头 DP 的基础就应该会做的模板题。
好像除了插头也没有别的好办法了?
所以,学插头 DP 吧
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,f[2][1<<11],res=0x3f3f3f3f;
char s[1010][20];
void chmn(int&x,int y){if(x>y)x=y;}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%s",s[i]);
int I=1,J=0;
memset(f[J],0x3f,sizeof(f[J])),f[J][0]=0;
for(int i=1;i<=n;i++)for(int j=0;j<m;j++,swap(I,J)){
memset(f[I],0x3f,sizeof(f[I]));
for(int k=0;k<(1<<(m+1));k++){
int K=k;if(!j)K=(k&((1<<m)-1))<<1;
int L=(K&(1<<j)),U=(K&(1<<(j+1)));
K^=L,K^=U;
if(s[i][j]=='.'){chmn(f[I][K],f[J][k]);continue;}
chmn(f[I][K|(1<<j)],f[J][k]+!(U>>(j+1)));
chmn(f[I][K|(1<<(j+1))],f[J][k]+!(L>>j));
}
// for(int k=0;k<(1<<(m+1));k++)printf("(%d,%d)%d:%d\n",i,j,k,f[I][k]);
}
for(int i=0;i<(1<<(m+1));i++)res=min(res,f[J][i]);
printf("%d\n",res);
return 0;
}
VIII.风暴
给定 的网格图,相邻格子间以 的概率不出现边, 的概率出现。求 可达 的概率。
数据范围:。绝对误差不超过 。
观察到 时答案大致呈等比数列。于是我们大力 DP 出前 列的结果,然后直接求出等比数列的公比即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;ll m;double P;
vector<int>v,u[10100];
map<vector<int>,int>mp;
int cnt,num;bool ok[20];
void dfs(int pos){
if(pos>n){
v.push_back(0);
for(int i=1;i<=cnt;i++)v.back()=i,mp[v]=++num,u[num]=v;
v.pop_back();
// for(auto x:v)printf("%d ",x);printf(":%d\n",cnt);
return;
}
for(int i=1;i<=cnt;i++)if(ok[i]){
vector<int>w;
for(int j=pos-1;j;j--)
if(v[j-1]==i)break;
else if(ok[v[j-1]])ok[v[j-1]]=false,w.push_back(v[j-1]);
v.push_back(i);
dfs(pos+1);
v.pop_back();
for(auto x:w)ok[x]=true;
}
v.push_back(++cnt),ok[cnt]=true,dfs(pos+1),v.pop_back(),ok[cnt--]=false;
}
double f[10100],g[10100];
int Hash(vector<int>v){
static int buc[20],chr;
// printf("Hashing:");
// // for(auto x:v)printf("%d ",x);printf("|");
memset(buc,0,sizeof(buc));chr=0;
for(int i=0;i+1<v.size();i++){
if(!buc[v[i]])buc[v[i]]=++chr;
v[i]=buc[v[i]];
}
if(!buc[v.back()])return 0;
v.back()=buc[v.back()];
// for(auto x:v)printf("%d ",x);printf("\n");
return mp[v];
}
void conn(vector<int>&v,int i,int j){
int x=v[i],y=v[j];
for(auto&z:v)if(z==x)z=y;
}
vector<int>w[10100][10];
struct dat{
ll M;double P;int id;
dat(ll _M,double _P,int _I){M=_M,P=_P,id=_I;}
};
vector<dat>qq[10];
double res[60];
int ini[100100][10];
int sta;
vector<int>vec;
double mina(){
for(int i=1;i<=num;i++)f[i]=g[i]=0;
f[sta]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=num;j++){
g[j]+=f[j]*P;
g[ini[j][i]]+=f[j]*(1-P);
}
for(int j=1;j<=num;j++)f[j]=g[j],g[j]=0;
}
static double ans[110];
for(int j=2;j<=min(m,60ll);j++){
for(int k=1;k<=num;k++){
g[w[k][1][0]]+=f[k]*P;
g[k]+=f[k]*(1-P);
}
for(int k=1;k<=num;k++)f[k]=g[k],g[k]=0;
for(int i=2;i<=n;i++){
for(int k=1;k<=num;k++){
g[k]+=f[k]*P*(1-P);
g[w[k][i][0]]+=f[k]*P*(1-P);
g[w[k][i][1]]+=f[k]*P*P;
g[w[k][i][2]]+=f[k]*(1-P)*(1-P);
}
for(int k=1;k<=num;k++)f[k]=g[k],g[k]=0;
}
ans[j]=0;
for(auto k:vec)ans[j]+=f[k];
}
if(m<=60)return ans[m];
if(ans[60]<1e-6)return 0;
double q=ans[60]/ans[59];
m-=60;
double z=ans[60];
for(;m;m>>=1,q=q*q)if(m&1)z=z*q;
return z;
}
int T;
int main(){
freopen("storm.in","r",stdin);
freopen("storm.out","w",stdout);
scanf("%d",&T);
for(int i=1;i<=T;i++)
scanf("%d%lld%lf",&n,&m,&P),
qq[n].emplace_back(m,P,i);
for(n=1;n<=8;n++)if(!qq[n].empty()){
num=0,mp.clear(),v.clear(),dfs(1);
for(int i=1;i<=n;i++)v.push_back(i);v.push_back(1);
sta=Hash(v);
for(int i=2;i<=n;i++)
for(int k=1;k<=num;k++){
v=u[k];
conn(v,i-2,i-1);
ini[k][i]=Hash(v);
}
for(int k=1;k<=num;k++)v=u[k],v[0]=n+1,w[k][1]={Hash(v)};
for(int i=2;i<=n;i++)
for(int k=1;k<=num;k++){
w[k][i].clear();
v=u[k],v[i-1]=v[i-2];
w[k][i].push_back(Hash(v));
v=u[k],v[i-1]=n+1;
// printf("%d->%d\n",k,Hash(v));
w[k][i].push_back(Hash(v));
v=u[k],conn(v,i-2,i-1);
// printf("%d->%d\n",k,Hash(v));
w[k][i].push_back(Hash(v));
}
vec.clear();
for(int i=1;i<=num;i++){
v=u[i];
if(v.back()==v[n-1])vec.push_back(i);
}
for(auto _:qq[n])
m=_.M,P=_.P,res[_.id]=mina();
}
for(int i=1;i<=T;i++)printf("%lf\n",res[i]);
return 0;
}
IV.DP on DP
通过记录内层 DP 的态(事实上是把内层 DP 看作了自动姬),我们可以实现这类 DP。
I.[BZOJ3864]Hero meet devil
我们不妨从最trival的LCS问题上想起:暴力的LCS求法是什么?
设 表示一个串(不妨设为本题中要填的字符串 )的前 位与另一个串(即题目中给出的 )的前 位所构成的串的LCS。则 。
因为 很小,所以我们可以考虑设 表示当前填到 的第 位,且 中通过某种方式储存了 全部的DP值,满足此种情形的串的方案数。下面考虑怎么搞出 。
明显,对于 来说,相邻两个数的差至多为 ,这一点可以直接从DP式上看出。于是我们可以状压其差分数组,即可实现由数组到状态的一一映射。这之后,我们只需预处理出对于每个 对应的状态,其之后添加 A,G,C,T
四个字符会分别到达哪个新状态即可。然后直接DP就行了。
这种手法被称作“DP on DP”,因为DP状态中储存了另一种更简单的DP的数组。可以发现,简单DP的数组压缩后得到了一张自动姬。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int T,n,m,f[2][1<<15],lim,t[20],a[20],b[20],res[20],trans[1<<15][4];
char s[20];
int main(){
scanf("%d",&T);
while(T--){
scanf("%s%d",s+1,&m),n=strlen(s+1),lim=1<<n;
for(int i=1;i<=n;i++){
if(s[i]=='A')t[i]=0;
if(s[i]=='G')t[i]=1;
if(s[i]=='C')t[i]=2;
if(s[i]=='T')t[i]=3;
}
for(int j=0;j<lim;j++){
for(int k=0;k<n;k++)a[k+1]=a[k]+((j>>k)&1);
for(int c=0;c<4;c++){
for(int k=1;k<=n;k++)b[k]=max({a[k-1]+(t[k]==c),a[k],b[k-1]});
trans[j][c]=0;
for(int k=0;k<n;k++)trans[j][c]|=(b[k+1]-b[k])<<k;
}
}
memset(f,0,sizeof(f)),f[0][0]=1;
for(int i=0;i<m;i++){
memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
for(int j=0;j<lim;j++)for(int k=0;k<4;k++)(f[!(i&1)][trans[j][k]]+=f[i&1][j])%=mod;
}
// for(int i=0;i<=m;i++){for(int j=0;j<lim;j++)printf("%d ",f[i][j]);puts("");}
for(int i=0;i<lim;i++)(res[__builtin_popcount(i)]+=f[m&1][i])%=mod;
for(int i=0;i<=n;i++)printf("%d\n",res[i]),res[i]=0;
}
return 0;
}
II.[TJOI2018]游园会
DP on DP 的套路。具体而言,内层 DP 是 表示两串分别匹配到第 个位置时的 LCS。固定 ,则 的下标是 ,且前后两个数之差是 或 。状压一下,大小是 的。
那就考虑设计外层 DP。设 表示位置 ,内层 DP 压缩成 ,且当前串对 NOI
的匹配长度是 ,此时的方案数。转移是简单的。
滚一下,时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,f[2][1<<15][3],g[16];
char s[20];
int dp[20],pd[20];
int main(){
scanf("%d%d",&n,&m),scanf("%s",s+1);
for(int i=1;i<=m;i++){
if(s[i]=='N')s[i]=0;
if(s[i]=='O')s[i]=1;
if(s[i]=='I')s[i]=2;
}
f[0][0][0]=1;
for(int i=0;i<n;i++){
memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
for(int j=0;j<(1<<m);j++)for(int k=0;k<3;k++){
if(!f[i&1][j][k])continue;
for(int p=0;p<m;p++)dp[p+1]=(j>>p)&1;
for(int p=1;p<=m;p++)dp[p]+=dp[p-1];
for(int t=0;t<3;t++){
for(int p=1;p<=m;p++)pd[p]=dp[p];
for(int p=m;p;p--)pd[p]=max(pd[p],pd[p-1]+(s[p]==t));
for(int p=1;p<=m;p++)pd[p]=max(pd[p],pd[p-1]);
int msk=0;
for(int p=m;p;p--)pd[p]-=pd[p-1];
for(int p=1;p<=m;p++)msk+=(pd[p]<<(p-1));
if(t!=k)(f[!(i&1)][msk][!t]+=f[i&1][j][k])%=mod;
else if(k<2)(f[!(i&1)][msk][k+1]+=f[i&1][j][k])%=mod;
}
}
}
for(int i=0;i<(1<<m);i++)for(int j=0;j<3;j++)(g[__builtin_popcount(i)]+=f[n&1][i][j])%=mod;
for(int i=0;i<=m;i++)printf("%d\n",g[i]);
return 0;
}
III.[LOJ#3600][PA 2021]Od deski do deski
首先思考一个问题:如何判定一个序列能不能被删空?
答案是 DP。设 表示长度为 的前缀能不能被删空(是一个 bool
值),然后 。
那么考虑当前的前缀已经被确定,现在要确定第 个元素。则,令一个 表示若要使 为 ,可选的颜色数,也即不可重集 的大小。显然由 转移到 时,这个集合的大小只会增加不会减少。
令 表示 DP 到位置 ,集合大小是 ,且上一个位置的 值是 的方案数。转移枚举 ,然后视情况更新 即可。复杂度平方。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n,m,f[2][3010][2],res;
int main(){
scanf("%d%d",&n,&m);
f[0][0][1]=1;
for(int i=0;i<n;i++){
memset(f[!(i&1)],0,sizeof(f[!(i&1)]));
for(int j=0;j<=i;j++){
if(f[i&1][j][1]){
(f[!(i&1)][j+1][0]+=1ll*f[i&1][j][1]*(m-j)%mod)%=mod;
(f[!(i&1)][j][1]+=1ll*f[i&1][j][1]*j%mod)%=mod;
}
if(f[i&1][j][0]){
(f[!(i&1)][j][0]+=1ll*f[i&1][j][0]*(m-j)%mod)%=mod;
(f[!(i&1)][j][1]+=1ll*f[i&1][j][0]*j%mod)%=mod;
}
}
}
for(int i=0;i<=n;i++)(res+=f[n&1][i][1])%=mod;
printf("%d\n",res);
return 0;
}
IV.[ZJOI2019]麻将
首先第一步是解决期望的问题:把期望步数转成添加 张排后还没和牌的概率,然后求和后加一就得到了期望。这步转化是整道问题的地基,没有它我们就无法进行接下来的 DP。
然后第二步是构造一个和牌判定姬。
首先考虑四面带一对的和牌。
DP 是有顺序的。我们考虑按照何种顺序:我们按照大小递增的顺序把某种大小的牌全都塞进当前的态。
考虑需要维护的信息:因为大小递增,所以我们只需维护:
- ,表示当前枚举的大小。
- ,表示 的对数和 的牌数。
- ,表示当前有没有对子。
- ,表示当前的面子数。
然后进行一步基础的剪枝:因为 留下来都是为了凑三递增的牌型的,因此 所有的 都应被使用。进而如果我们留下了大于等于三个 或 ,且它们被全部使用了,则 不如换成三个三相同的牌型。同时,转移时 所有的 都应该被凑成三递增,所有的 都应该被凑成双递增。
进而我们的态中的 两维就被压缩到了 。
还有一步剪枝:因为 是 DP 顺序,所以可以把它掏出来放在外层 DP 的地方枚举。进而我们就只需 四个态来刻画内层 DP 的一个状态。要完美刻画内层 DP 的转移,需要把内层 DP 的全体 态的 可出现与否 压缩。但是这太奢侈了:对于不同的 ,我们只需维护其中最大的即可。于是我们要维护的就是对于每个 最大的 。
然后考虑七对的和牌。这表明我们还要额外再记一维 表示当前能提供和牌的位置数。显然有 。
最后还要再设一个态表示必胜态。
把从初始态开始能到达的全体态都扔进 map
里,然后一个态根据下一个大小的牌出现了 张有不同的连边,构成一张自动姬。
外层设一个 表示大小小于等于 的牌,共出现了 张,在自动姬的节点 上的方案数。DP 是简单的。
复杂度是自动姬的节点数乘上平方。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int n,m;
void chmx(int&x,int y){if(x<y)x=y;}
struct Node{
int f[3][3][2],p;
Node(){memset(f,-1,sizeof(f)),p=-1;}
friend bool operator<(const Node&u,const Node&v){
for(int i=0;i<3;i++)for(int j=0;j<3;j++)for(int k=0;k<2;k++)
if(u.f[i][j][k]!=v.f[i][j][k])return u.f[i][j][k]<v.f[i][j][k];
return u.p<v.p;
}
friend Node operator+(const Node&u,const int&v){
if(u.p==-1)return u;
Node w;
w.p=u.p+(v>=2);
for(int i=0;i<3;i++)for(int j=0;j<3;j++){
if(u.f[i][j][0]!=-1){
for(int k=0;k<=min(2,v-i-j);k++)
chmx(w.f[j][k][0],u.f[i][j][0]+i+(v-i-j-k)/3);
for(int k=0;k<=min(2,v-2-i-j);k++)
chmx(w.f[j][k][1],u.f[i][j][0]+i+(v-2-i-j-k)/3);
}
if(u.f[i][j][1]!=-1)
for(int k=0;k<=min(2,v-i-j);k++)
chmx(w.f[j][k][1],u.f[i][j][1]+i+(v-i-j-k)/3);
}
if(w.p>=7)return Node();
for(int i=0;i<3;i++)for(int j=0;j<3;j++){
w.f[i][j][0]=min(w.f[i][j][0],4);
if(w.f[i][j][1]>=4)return Node();
}
return w;
}
void prr()const{
for(int k=0;k<2;k++)for(int i=0;i<3;i++,puts(""))for(int j=0;j<3;j++)
printf("%d ",f[i][j][k]);
printf("%d\n",p);
}
}aut[3000];
map<Node,int>mp;
int nex[3000][5];
void bfs(){
mp[aut[0]=Node()]=0;
aut[1].f[0][0][0]=aut[1].p=0;
mp[aut[1]]=1;
m=1;
for(int i=1;i<=m;i++)for(int j=0;j<=4;j++){
// printf("%d,%d:\n",i,m);
// aut[i].prr();
Node A=aut[i]+j;
if(mp.find(A)==mp.end())aut[++m]=A,mp[A]=m;
nex[i][j]=mp[A];
}
// printf("%d\n",m);
}
int a[110],fac[410],inv[410],f[2][410][3010],res;
int C(int x,int y){return 1ll*fac[x]*inv[y]%mod*inv[x-y]%mod;}
int main(){
bfs();
scanf("%d",&n);
fac[0]=1;for(int i=1;i<=(n<<2);i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[n<<2]=ksm(fac[n<<2]);for(int i=n<<2;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
for(int i=1,x,y;i<=13;i++)scanf("%d%d",&x,&y),a[x]++;
f[0][0][1]=1;
for(int i=1;i<=n;i++){
memset(f[i&1],0,sizeof(f[i&1]));
for(int j=0;j<=(n<<2);j++)for(int k=0;k<=m;k++)
if(f[!(i&1)][j][k])
for(int t=a[i];t<=4;t++)
(f[i&1][j+t][nex[k][t]]+=1ll*f[!(i&1)][j][k]*C(4-a[i],t-a[i])%mod)%=mod;
}
for(int j=13;j<=(n<<2);j++)for(int k=1;k<=m;k++)
(res+=1ll*f[n&1][j][k]*ksm(C((n<<2)-13,j-13))%mod)%=mod;
printf("%d\n",res);
return 0;
}
V.[TopCoder12742]SimilarSequencesAnother
删除至多两个字符使得 相同。
这等价于 的 LCS 长度大于等于 。
考虑暴力的 LCS 流程:有 为 的前 位和 的前 位的 LCS。
注意到 。那么若 ,则经过的态必有 。
于是我们设一个状态 ,其中 储存了 这五位的信息。大力 DP 即可……吗?
考虑转移。转移要判相等。但是我们并不知道 的方案数。
但是我们可以维护 中等价类的信息,问题就解决了。
复杂度可以做到 ,其中 是至多能删掉的字符数,本题是 , 是贝尔数。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+9;
int n,m,f[110][1024][60],res;
int a[60][5],cnt,hs[3000];
int tr[1024],sh[1024],tnc;
class SimilarSequencesAnother{
private:
int b[5];
int Hash(){
static int id[6],ID;memset(id,-1,sizeof(id)),ID=0;
for(int i=0;i<5;i++){
if(id[b[i]]==-1)id[b[i]]=ID++;
b[i]=id[b[i]];
}
int ret=0;
for(int i=0;i<5;i++)ret=5*ret+b[i];
return ret;
}
void dfs(int pos){
if(pos==5){
static int c[5];memcpy(c,b,sizeof(b));
int qwq=Hash();
if(hs[qwq]==-1)hs[qwq]=cnt,memcpy(a[cnt],b,sizeof(b)),cnt++;
memcpy(b,c,sizeof(b));
return;
}
for(int i=0;i<5;i++)b[pos]=i,dfs(pos+1);
}
public:
int getCount(int N,int bound){
n=N,m=bound;memset(hs,-1,sizeof(hs)),cnt=0,dfs(0),memset(sh,-1,sizeof(sh));
for(int i=0;i<(1<<10);i++){
bool ok=true;
for(int j=0;j<5;j++)if(((i>>(j<<1))&3)==3)ok=false;
if(ok)tr[tnc]=i,sh[i]=tnc,tnc++;
}
memset(b,0,sizeof(b)),f[0][sh[(2<<8)|(2<<6)|(2<<4)]][Hash()]=m;
if(n>1)b[4]=1,f[0][sh[(2<<8)|(2<<6)|(2<<4)]][Hash()]=1ll*m*(m-1)%mod;
for(int i=0;i<n;i++)for(int j=0;j<tnc;j++)for(int k=0;k<cnt;k++)if(f[i][j][k]){
// printf("(%d,%d,%d):%d\n",i,j,k,f[i][j][k]);
// for(int t=0;t<5;t++)printf("%d ",(tr[j]>>(t<<1))&3);puts("");
// for(int t=0;t<5;t++)printf("%d ",a[k][t]);puts("");
int mx=0;
for(int t=0;t<5;t++)mx=max(mx,a[k][t]);
static int F[2][6],V[6];
for(int t=0;t<5;t++)V[t]=a[k][t];
for(int t=0;t<5;t++)F[0][t]=min(i,i-2+t)-2+((tr[j]>>(t<<1))&3);
for(int t=0;t<5;t++)if(i-2+t<=0)F[0][t]=0;
// for(int t=0;t<5;t++)printf("%d ",F[0][t]);puts("");
for(int u=0;u<=mx+2;u++)for(int v=0;v<=mx+1;v++){
if(i+3>n&&v)continue;
if(u==mx+2&&v!=mx+1)continue;
// printf("<%d,%d>\n",u,v);
V[5]=v;F[1][0]=F[0][5]=-0x3f3f3f3f;
for(int t=1;t<=5;t++)F[1][t]=max({F[1][t-1],F[0][t],F[0][t-1]+(u==V[t])});
// if(i==2)for(int t=1;t<=5;t++)printf("%d ",F[1][t]);puts("");
int sta=0;bool ok=true;
for(int t=1;t<=5;t++){
int dif=(F[1][t]-(min(i+1,i+t-2)-2));
if(i+t-2<1||i+t-2>n)dif=0;
if(dif<0){ok=false;break;}
sta|=(dif<<((t-1)<<1));
}
if(!ok)continue;
memcpy(b,V+1,sizeof(b));
int val=f[i][j][k];
if(u>=mx+1||v>=mx+1)val=1ll*val*(m-mx-1)%mod;
if(u>=mx+2)val=1ll*val*(m-mx-2)%mod;
for(int t=1;t<=5;t++)if(i+t-2<1||i+t-2>n)b[t-1]=0;
// printf("%d:%d|%d:%d|%d\n",sta,sh[sta],Hash(),hs[Hash()],val);
(f[i+1][sh[sta]][hs[Hash()]]+=val)%=mod;
}
}
for(int j=0;j<tnc;j++)for(int k=0;k<cnt;k++)if(f[n][j][k])(res+=f[n][j][k])%=mod;
return res;
}
}my;
VI.[GYM100257J]Jigsaw Puzzle
我们要对有完美覆盖的图计数。
一个想法是把它转成二分图然后对有完美匹配的图使用 Hall 定理计数。但事实证明这是不好的,因为你无法简单维护 Hall 定理的信息。
另一个想法是回归最原始的方法,为每张图找出一种独特的方案,使得每张图不会重复计算,并钦定以下几个子结构是不应该出现的:纵向连续的两个横条 以及 一个横条,紧贴其上方有两个纵条。
但是这个方法是错的!考虑一个 simple 的场景:一个 的正方形,最中间的方块被挖掉。然后我们发现,这玩意存在顺时针和逆时针两种排布。倘若这个结构更大一点,显然这是无法被只算一次的。
怎么办呢?我们最终不得不回归最原始的做法:考虑 DP on DP。首先要建一个判定能否被表示的自动姬出来。
然后发现,自动姬只需存储每个位置有无被使用即可,共 种不同的态。把每个态是否可达压入状态,状态数高达 ……个屁。
爆搜一下吧!从起点出发,在 时一共只有 种态是可达的。
那就直接大力搞即可。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int mod=1e9+7;
int n,m,cnt,f[510][10100],res;
ull val[10100];
unordered_map<ull,int>mp;
vector<int>v[10100];
int dfs(ull sta){
if(mp.find(sta)!=mp.end())return mp[sta];
int x=mp[sta]=++cnt;val[cnt]=sta;
for(int i=0;i<(1<<n);i++){
ull ats=0;
for(int j=0;j<(1<<n);j++)if((sta&(1ull<<j))&&!(j&i)){
int S=((1<<n)-1)^(i|j);
// printf("<%lld %d>\n",sta,S);
for(int T=S;;T=(T-1)&S){
if(T&1)continue;
if(T&(T>>1))continue;
if((S|(T>>1))!=S)continue;
int R=S^T^(T>>1);
// printf("%d,%d,%d\n",S,T,R);
ats|=1ull<<R;
if(!T)break;
}
}
if(!ats)continue;
// printf("%llu %llu\n",sta,ats);
v[x].push_back(dfs(ats));
}
return x;
}
int main(){
scanf("%d%d",&n,&m);
f[1][dfs(1)]=1;
// printf("%d\n",cnt);
// for(int i=1;i<=cnt;i++)for(auto j:v[i])printf("%d->%d\n",i,j);
for(int i=1;i<=m;i++)for(int j=1;j<=cnt;j++)for(auto k:v[j])(f[i+1][k]+=f[i][j])%=mod;
// for(int i=1;i<=m+1;i++,puts(""))for(int j=1;j<=cnt;j++)printf("%d ",f[i][j]);
for(int i=1;i<=cnt;i++)if(val[i]&1)(res+=f[m+1][i])%=mod;
printf("%d\n",res);
return 0;
}
VII.[GYM100958I]Substring Pairs
考虑一个容斥:我们钦定 出现一次,两次,三次……计算此时的方案数,则最终答案亦可简单算出。
但是问题在于, 的出现可能有重合。这就意味着 会出现重叠。重叠的时候就会产生 Border。
于是我们还有对 的 Border 集合的限制。
怎么办呢?我们爆搜,搜出本质不同的 Border 集合数,并统计 Border 集合恰为其的串数,然后枚举每种 Border 集合去容斥。
一共只有 种本质不同的 Border 集合。对于一种 Border 集合,其方案数可以简单 DP+容斥求出。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int mod=1e9+7;
int n,m,A,pro[210];
int dsu[60];
int find(int x){return dsu[x]==x?x:dsu[x]=find(dsu[x]);}
void merge(int x,int y){x=find(x),y=find(y);if(x!=y)dsu[x]=y;}
bool same(int x,int y){return find(x)==find(y);}
bool border(int x){for(int i=1;i<=x;i++)if(!same(i,m-x+i))return false;return true;}
void insert(int x){for(int i=1;i<=x;i++)merge(i,m-x+i);}
int count(){int num=0;for(int i=1;i<=m;i++)num+=(dsu[i]==i);return pro[num];}
ull sta[101000];
int way[101000],cnt;
void dfs(ull val,int pos){
for(int i=1;i<pos;i++)if(!((val>>i)&1)&&border(i))return;
bool ok=true;
for(int i=pos;i<m;i++)if(border(i)){ok=false;break;}
if(ok){
sta[++cnt]=val,way[cnt]=count();
// printf("%d:%llu,%d\n",cnt,val,pos);
}
int DSU[60];
memcpy(DSU,dsu,sizeof(dsu));
for(int i=m-1;i>=pos;i--){
insert(i);
dfs(val|(1ull<<i),i+1);
memcpy(dsu,DSU,sizeof(DSU));
}
}
int f[210],res;
int main(){
scanf("%d%d%d",&n,&m,&A);
pro[0]=1;for(int i=1;i<=n;i++)pro[i]=1ll*A*pro[i-1]%mod;
for(int i=1;i<=m;i++)dsu[i]=i;
dfs(0,1);
for(int i=cnt;i;i--)for(int j=1;j<i;j++)
if((sta[i]&sta[j])==sta[j])(way[j]+=mod-way[i])%=mod;
// for(int i=1;i<=cnt;i++)printf("%lld,%d\n",sta[i],way[i]);
for(int _=1;_<=cnt;_++){
int sum=0;
for(int i=1;i<=n-m+1;i++){
(f[i]+=pro[i-1])%=mod;
for(int j=i+1;j<=n-m+1;j++)
if(j<=i+m-1){
if(!(sta[_]&(1ull<<(i+m-j))))continue;
// printf("%d->%d\n",i,j);
(f[j]+=mod-f[i])%=mod;
}else(f[j]+=mod-1ll*f[i]*pro[j-i-m]%mod)%=mod;
(sum+=1ll*f[i]*pro[n-(i+m-1)]%mod)%=mod;
f[i]=0;
}
(res+=1ll*sum*way[_]%mod)%=mod;
}
printf("%d\n",res);
return 0;
}
V.划分数压缩 DP
划分数压缩 DP 常见于 的数据范围。
VI.贝尔数压缩 DP
贝尔数,也即集合划分个数,常见于 的数据范围。
I.CF1569F Palindromic Hamiltonian Path
所有字符填入方式共有多少种?。显然不是什么可以操作的范围。
但是我们判定一种方式是否合法要通过检查回文串。回文串得到判定只需要知道两个位置等或不等。于是我们只需要维护等价类即可。
等价类计数是贝尔数模型。,已经可以考虑枚举每一种等价类然后判定是否合法了。
那么我们考虑如何判断。
考虑该等价类下的一条回文串。其开头和末尾的元素必然来自于同一等价类。第二和倒数第二个元素亦来自同一等价类……那么我们会发现要对每个等价类中的元素两两配对。
配对两个数后,我们发现这个状态便等价于把这两个配好对的数单独抽出来作为一个等价类时的图是否合法。
那么我们每次挑出一个大小至少为 的等价类,然后考虑其中首个元素,将其与另一元素配对得到新等价状态,然后判定新等价状态是否合法即可。
这个可以递归执行,直到所有等价类大小全为 。这样的等价类数量只会有 种,最大不超过 。
那么考虑如何检验一个等价类大小全为 (换句话说,两两配对)的等价类是否合法。方法很简单,状压即可。单次检验复杂度为 。
时间复杂度 。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,K,fac[12];
ll res;
int blk[12],num,BLK[12],mun;
int Hsh(){
mun=0;for(int i=0;i<n;i++)if(!BLK[blk[i]])BLK[blk[i]]=mun++;
int ret=0;for(int i=0;i<n;i++)ret=6*ret+BLK[blk[i]];
for(int i=0;i<n;i++)BLK[blk[i]]=0;
return ret;
}
unordered_map<int,bool>mp;
bool f[6][1<<6],g[12][12];
vector<int>v[6];
bool che(){
memset(f,false,sizeof(f));
for(int i=0;i<num;i++)if(g[v[i][0]][v[i][1]])f[i][1<<i]=true;
for(int i=1;i<(1<<num);i++)for(int j=0;j<num;j++){
if(!(i&(1<<j))||!f[j][i])continue;
for(int k=0;k<num;k++){
if(i&(1<<k))continue;
if(g[v[j][0]][v[k][0]]&&g[v[j][1]][v[k][1]])f[k][i|(1<<k)]=true;
if(g[v[j][0]][v[k][1]]&&g[v[j][1]][v[k][0]])f[k][i|(1<<k)]=true;
}
}
for(int i=0;i<num;i++)if(f[i][(1<<num)-1])return true;
return false;
}
void dfs(int pos){
if(num>(n>>1))return;
if(pos==n){
for(int i=0;i<num;i++)v[i].clear();
for(int i=0;i<n;i++)v[blk[i]].push_back(i);
for(int i=0;i<num;i++)if(v[i].size()&1)return;
// for(int i=0;i<n;i++)printf("%d ",blk[i]);puts("");
int x=Hsh();
if(num==(n>>1)){if(che())mp[x]=true;}
else{
int id=num-1;while(v[id].size()==2)id--;
for(int i=1;i+1<v[id].size();i++){
blk[v[id][i]]=blk[v[id].back()]=num;
int y=Hsh();
blk[v[id][i]]=blk[v[id].back()]=id;
if(mp.find(y)!=mp.end()){mp[x]=true;break;}
}
for(int i=1;i+1<v[id].size();i++)blk[v[id][i]]=num;
int y=Hsh();
for(int i=1;i+1<v[id].size();i++)blk[v[id][i]]=id;
if(mp.find(y)!=mp.end())mp[x]=true;
}
if(mp.find(x)!=mp.end())res+=fac[num];
return;
}
blk[pos]=num++;dfs(pos+1),num--;
for(int i=num-1;i>=0;i--)blk[pos]=i,dfs(pos+1);
}
int main(){
scanf("%d%d%d",&n,&m,&K);
fac[1]=K;for(int i=2;i<=(n>>1);i++)fac[i]=fac[i-1]*(K-i+1);
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),g[x-1][y-1]=g[y-1][x-1]=true;
dfs(0);
printf("%lld\n",res);
return 0;
}
VII.其它压缩 DP
DP 状态中可能用某种方法(比如康托展开或哈希)压缩了其它信息,而不仅仅是二进制状压。
I.[SDOI2007]游戏
论STL
的百种用法
可以观察到可以接龙的对构成一张DAG。因此我们要找到DAG中最长路。这个随便DP就可以了。
关键是找到可以互相转移的位置。
枚举非常危险,因为还有一个判断的常数,没试,估计过不了。
我们必须寻找复杂度更低的算法。
发现一个串只与组成它的每个字符的数量有关。那么我们可以把这每个字符的数量压到一个vector
里面,然后用map<vector<int>,int>
来找可以转移的位置。或者因为串长,因此vector
中每个数必定不超过,然后可以化成一个string
。当然,string
也可以哈希(虽然答案就不一定正确了)。
当然,无论怎么搞,都有一个的常数(似乎哈希一下复杂度是,而不哈希复杂度是)。但不管怎么说,能过。
代码:
#include<bits/stdc++.h>
using namespace std;
string s[10010];
map<vector<int>,int>m;
int S,n,f[10010],pre[10010],mp;
void print(int i){
if(!i)return;
print(pre[i]);
cout<<s[i]<<endl;
}
int main(){
n++;
while(cin>>s[n])n++;
sort(s+1,s+n);
for(int i=1;i<n;i++){
f[i]=1;
vector<int>v;
v.resize(26);
for(auto j:s[i])v[j-'a']++;
m[v]=i;
}
for(auto i:m){
vector<int>v=i.first;
for(int k=0;k<26;k++){
v[k]++;
if(m.find(v)!=m.end()){
int j=m[v];
if(f[j]<f[i.second]+1)f[j]=f[i.second]+1,pre[j]=i.second;
}
v[k]--;
}
}
for(int i=1;i<n;i++)if(f[i]>f[mp])mp=i;
printf("%d\n",f[mp]);
print(mp);
return 0;
}
II.[AGC020E] Encoding Subsets
这种“压缩”题可以考虑区间DP。但是若考虑标准的区间的话它“子集”等定义又不好处理。
于是我们考虑对字符串作DP。设 表示一个串 及其所有子集的压缩方案数。
显然,其有两种转移方式:一种是 不压缩,直接从 转移过来;一种则是 与后面一些东西压缩。
于是我们枚举循环节长度以及循环次数,找到这所有循环串的交集(仍是一个串),然后转移即可。
使用 map
维护DP状态,复杂度玄学,但能过。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
string s;
void merge(string &s,string t){//merge string t into s
for(int i=0;i<s.size();i++)if(t[i]=='0')s[i]='0';
}
map<string,int>mp;
int dfs(string s){
if(s.empty())return 1;
if(s.size()==1)return s[0]=='0'?1:2;
if(mp.find(s)!=mp.end())return mp[s];
int ret=1ll*dfs(s.substr(0,1))*dfs(s.substr(1))%mod;//encoding the leading character separately
for(int i=1;(i<<1)<=s.size();i++){//encoding i charactets together as a permutation chapter
string t=s.substr(0,i);
for(int j=i;j+i<=s.size();j+=i)merge(t,s.substr(j,i)),(ret+=1ll*dfs(t)*dfs(s.substr(j+i))%mod)%=mod;
}
return mp[s]=ret;
}
int main(){
cin>>s;
printf("%d\n",dfs(s));
return 0;
}
III.[GYM102460G]Optimal Selection
去分析什么最优算法的流程是完全没有意义的;事实上,因为数据范围很小,所以只要状压所有不同构的 DAG(准确地说,应该是 DAG 上可达关系构成的偏序集)知道答案需要的最小步数即可。
那么怎么判 DAG 同构呢?考虑随便构造一些奇怪的 hash 函数。比如说:每个点的出入度数;两两间最短路长度;每个点出发走一步、两步走到的度数集合;等等。验证你的 hash 函数是否合法的方法是把更多的东西加入你的 hash 函数,如果发现不同方案数增加了就说明你的 hash 还没有收敛。
我的 hash 函数记录了所有点的入度、出度、可达集合中两两不可达点对数、可达集合中每个点的出入度信息,把它排序后乘上一个大奇数然后全都异或在一块。
DAG 可以压在一个 ull
中保存。
代码:
#include<bits/stdc++.h>
using namespace std;
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
using namespace __gnu_pbds;
typedef unsigned long long ull;
int n,m,r,pc[256];
cc_hash_table<ull,int>mp;
struct Graph{
ull g;
#define get(i,j) ((g>>(i<<3|j))&1)
#define teg(i) ((g>>(i<<3))&255)
void pass(){for(int i=0;i<n;i++)for(int k=0;k<n;k++)if(get(i,k))g|=(teg(k))<<(i<<3);}
ull coe[8]={1926081719260817ull,1768032117680321ull,998244353998244353ull,666623333666623333ull,
10000000000000000051ull,11000000000000000023ull,12000000000000000029ull,13000000000000000171ull};
ull HASH(){
vector<ull>v;
pass();
static int ind[8];
for(int i=0;i<n;i++){ind[i]=0;for(int j=0;j<n;j++)if(get(j,i))ind[i]++;}
for(int i=0;i<n;i++){
vector<int>u;
int oud=pc[teg(i)]/*,bin=0*/,bou=0;
for(int j=0;j<n;j++)if(get(i,j))u.push_back(pc[teg(j)]<<3|ind[j]);
// for(int j=0;j<n;j++)for(int k=0;k<n;k++)if(get(j,i)&&get(k,i)&&!get(k,j)&&!get(j,k))bin++;
for(int j=0;j<n;j++)for(int k=0;k<n;k++)if(get(i,j)&&get(i,k)&&!get(k,j)&&!get(j,k))bou++;
sort(u.begin(),u.end());
ull p=0;
p=ind[i]|(oud<<3)|(bou<<6)/*|(bin<<12)*/;
for(auto x:u)p=(p<<6)^x;
v.push_back(p);
}
sort(v.begin(),v.end());
ull ret=0;
for(int i=0;i<v.size();i++)ret^=v[i]*coe[i];
return ret;
}
bool check(){
for(int i=0;i<n;i++){
int sz=0;
for(int j=0;j<n;j++)if(j!=i){
if(!get(i,j)&&!get(j,i)){sz=-1;break;}
if(get(i,j))sz++;
}
if(sz==m)return true;
}
return false;
}
};
int DP(Graph g){
ull H=g.HASH();
if(mp.find(H)!=mp.end())return mp[H];
// if(!(mp.size()%1000))printf("%d\n",mp.size());
int&res=mp[H];
if(g.check())return res;
res=0x3f3f3f3f;
for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(i!=j){
if(((g.g>>(i<<3|j))&1)||((g.g>>(j<<3|i))&1))continue;
Graph f=g,h=g;
f.g|=(1ull<<(i<<3|j)),h.g|=(1ull<<(j<<3|i));
res=min(res,max(DP(f),DP(h))+1);
}
return res;
}
Graph g;
int main(){
for(int i=0;i<256;i++)pc[i]=__builtin_popcount(i);
scanf("%d%d%d",&n,&m,&r),m--;
for(int i=1,x,y;i<=r;i++)scanf("%d%d",&x,&y),g.g|=(1ull<<(y<<3|x));
printf("%d\n",DP(g));
return 0;
}
IV.排列
求相邻数全都互质的 排列个数,对给定质数 取模。
数据范围:。。
一个显然的想法是 meet in middle……吗?
并不,因为 meet in middle 并不是 的,而是 的。也即,其和 没有差别。
那怎么办呢?
注意到这是一个 Hamilton 路径计数问题,也即其是图论问题。
我们发现,这张图中有许多等价的类,比如 是等价的, 是等价的……
那么总结一下发现事实上只有 个等价类。状压每个等价类剩余数的个数以及以哪个等价类结尾,会发现状态数是 的。大力 DP 即可。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,res,mod;
int g[30],c[30],p[30];
bool vis[30];
int pro[30],f[2001000][20];
int main(){
freopen("permutation.in","r",stdin);
freopen("permutation.out","w",stdout);
scanf("%d%d",&n,&mod),m=0;
for(int i=0;i<n;i++)if(!vis[i]){
vis[i]=true,c[m]++;
for(int j=i+1;j<n;j++)if(!vis[j]){
bool ok=true;
for(int k=0;k<n;k++)if(k!=i&&k!=j&&((__gcd(i+1,k+1)==1)!=(__gcd(j+1,k+1)==1)))
ok=false;
if(ok)vis[j]=true,c[m]++;
}
p[m++]=i+1;
}
for(int i=0;i<m;i++)for(int j=0;j<m;j++)if(__gcd(p[i],p[j])==1)g[i]|=1<<j;
// for(int i=0;i<m;i++)printf("%d %d %d\n",g[i],c[i],p[i]);
pro[0]=1;for(int i=0;i<m;i++)pro[i+1]=pro[i]*(c[i]+1);
f[0][0]=1;
for(int i=0;i<pro[m];i++)for(int j=0;j<m;j++)if(f[i][j])
for(int k=0;k<m;k++)if((g[j]&(1<<k))){
int rem=c[k]-(i/pro[k])%(c[k]+1);
if(rem)f[i+pro[k]][k]=(1ll*f[i][j]*rem+f[i+pro[k]][k])%mod;
}
for(int k=0;k<m;k++)(res+=f[pro[m]-1][k])%=mod;
printf("%d\n",res);
return 0;
}
II.[HDU4903]The only survival
考虑模拟 Dijkstra 的流程。这需要我们把所有点的最短路压到状态中。
这是不好的,因为最短路可能会有 种不同的情况。
但是我们发现,最短路集合 的情况是少的:打表发现 时共计只有 种不同的情形。
于是我们可以只维护尚未增广的点到起点最短路的集合。
但是发现动态维护这件事也是复杂的,因为这涉及到当前被挑出来增广的点与其它点间的路径。
怎么办呢?我们干脆直接考虑最短路集合间的关系:考虑每个点到距离比它小的节点的边,就会发现边权必然不小于两点间距离差;并且,至少存在一条边满足边权等于距离差。简单容斥即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int ksm(int x,int y=mod-2){int z=1;for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;return z;}
int T,n,m,K,fac[20],inv[20],res;
int a[20];
void dfs(int pos,int sum){
if(pos>m){
if(!a[m])return;
a[pos]=n-sum;
int all=1ll*fac[n-2]*a[m]%mod;
for(int i=1;i<=m+1;i++)all=1ll*all*inv[a[i]]%mod;
for(int i=1;i<=m+1;i++){
int proa=1,prob=(i<=m);
for(int j=0;j<i;j++)
proa=1ll*ksm(max(K-(i-j)+1,0),a[j])*proa%mod,prob=1ll*ksm(max(K-(i-j),0),a[j])*prob%mod;
// printf("%d:%d,%d\n",i,proa,prob);
int pro=(proa+mod-prob)%mod;
all=1ll*all*ksm(pro,a[i])%mod*ksm(K,a[i]*(a[i]-1)>>1)%mod;
}
// for(int i=0;i<=m+1;i++)printf("%d ",a[i]);puts("");
// printf("%d\n",all);
(res+=all)%=mod;
return;
}
for(int i=0;i+sum<=n;i++)a[pos]=i,dfs(pos+1,sum+i);
}
void mina(){
scanf("%d%d%d",&n,&m,&K);
if(n==1){printf("%d\n",m==0);return;}
fac[0]=1;for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[n]=ksm(fac[n]);for(int i=n;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
a[0]=1,dfs(1,1),printf("%d\n",res),res=0;
}
int main(){scanf("%d",&T);while(T--)mina();return 0;}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?