状压DP例题
P2831愤怒的小鸟
首先记录抛物线的方案。根据题意可知,两个点可能会确定一条符合题设的抛物线。所以\(O(n^2)\)枚举两个点,如果它们能够构成一个符合题设的抛物线,就再\(O(n)\)扫一遍,将这个抛物线能够到达的点记录下来,状态压缩记录成一种方案。别忘了只有抛物线只到达一个点也是一种方案。于是我们得到一个数组\(s\),\(s[i]\)表示第\(i\)种方案的状态。
接下来是状压DP。设\(f[i]\)表示状态为\(i\)时的最小抛物线数量。枚举\(s\)中的每一个元素,另\(f[i|s[j]]=min(f[i|s[j]],f[i]+1)\),答案是\(f[(1<<n)-1]\)
\(code:\)
bool function(double x,double y){
double fx=a*x*x+b*x-y;
fx=fx>0?fx:-fx;
return fx<=exp;
}
void sol(){
for(int i=1;i<=n;++i)
for(int j=i+1;j<=n;++j){
if(p[i].x!=p[j].x){
a=(p[i].x/p[j].x*p[j].y-p[i].y)/((p[j].x-p[i].x)*p[i].x);
b=(p[i].y-p[i].x*p[i].x*a)/p[i].x;
if(a>=0)
continue;
s[++tot]|=(1<<(i-1));s[tot]|=(1<<(j-1));
for(int k=1;k<=n;++k)
if(k!=i&&k!=j&&function(p[k].x,p[k].y))
s[tot]|=(1<<(k-1));
}
}
for(int i=1;i<=n;++i)
s[++tot]|=(1<<(i-1));
}
int main(){
scanf("%d",&t);
while(t--){
for(int i=1;i<=tot;++i)
s[i]=0;
tot=0;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%lf%lf",&p[i].x,&p[i].y);
sol();
for(int i=0;i<(1<<n);++i)
f[i]=1e9;
f[0]=0;
for(int i=0;i<(1<<n);++i){
for(int j=1;j<=tot;++j){
f[i|s[j]]=min(f[i|s[j]],f[i]+1);
}
}
//for(int i=1;i<=(1<<n);++i)cout<<f[i]<<" ";
printf("%d\n",f[(1<<n)-1]);
}
return 0;
}
P2157学校食堂
设 \(f[i][j][k]\) 表示第 \(1\) 个人到第 \(i-1\) 个人已经打完饭,第 \(i\) 个人以及后面七个人是否打饭的状态为 \(j\) ,当前最后一个打饭的人编号为 \(i+k\) .
如果第 \(i\) 个人打好了饭(即 \(j\)&\(1==true\) ),则状态转移方程为 \(f[i+1][j>>1][k-1]=min(f[i+1][j>>1][k-1],f[i][j][k]).\)
如果第 \(i\) 个人还没有打好饭,我们可以枚举 \(i\) 到 \(i+7\) 的所有人,让他们先打饭。也就是枚举 \(h=0...7\) ,状态转移方程为 \(f[i][j|(1<<h)][h]=min(f[i][j|(1<<h)][h],f[i][j][k]+time(i+k,i+h)).\)
需要注意的是 \(k\) 的范围是 \([-8,0]\) ,写代码的时候要将 \(k\) 整体加上 \(8.\)
初始化: \(f[1][0][7]=0\),其余全部是正无穷
答案: \(f[n+1][0][k](k∈[0,8])\)
void solve(){
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d%d",&t[i],&b[i]);
for(int i=0;i<=n+1;++i)
for(int j=0;j<=(1<<8);++j)
for(int k=-8;k<=7;++k)
f[i][j][k+8]=1e9;
f[1][0][7]=0;
for(int i=1;i<=n;++i)
for(int j=0;j<(1<<8);++j)
for(int k=-8;k<=7;++k)
if(f[i][j][k+8]!=1e9){
if(j&1)
f[i+1][j>>1][k+7]=min(f[i+1][j>>1][k+7],f[i][j][k+8]);
else{
int tmp=1e9;
for(int h=0;h<=7;++h)
if(!((j>>h)&1)){
if(i+h>tmp)
break;
tmp=min(tmp,i+h+b[i+h]);
f[i][j|(1<<h)][h+8]=min(f[i][j|(1<<h)][h+8],f[i][j][k+8]+(i+k?(t[i+k]^t[i+h]):0));
}
}
}
ans=1e9;
for(int k=0;k<=8;++k)
ans=min(ans,f[n+1][0][k]);
printf("%d\n",ans);
return;
}
P7519 [省选联考 2021 A/B 卷] 滚榜
首先想到,答案是最终排名的方案数,与 \(b\) 的方案数无关。所以考虑每次使 \(b\) 尽量少得分配给当前的数 \(a\) 。
若 \(a[i]>a[i-1]\) ,则 \(b[i]=b[i-1]\),否则 \(b[i]=b[i-1]+a[i]-a[i-1]\)
设 \(f[i][j][k]\) 表示已经选的状态为 \(i\) ,最后一个选的 \(a[j]\) ,当前 \(b\) 已经分配的总和为 \(k\) 。转移时考虑费用提前计算,即另后面尚未选的所有\(a\)都分配上当前的分配值。状态转移方程:\(f[i][j][k]+=f[i-(1<<(x-1))][y][k-(a[y]-a[x])\times (n-cnt1(i)+1)]\)
代码实现时注意分数相等时,位置小的靠前。
\(code:\)
int main(){
scanf("%lld%lld",&n,&m);
a[0]=-1;
for(int i=1;i<=n;++i){
scanf("%lld",&a[i]);
if(a[maxn]<a[i])
maxn=i;
}
for(int i=1;i<=n;++i){//一开始的节点要成为第一,所以要比最大值大
long long sum=n*(a[maxn]-a[i]);
if(maxn<i)
sum+=n;
if(sum<=m)
f[1<<(i-1)][i][sum]=1;
}
for(int i=0;i<(1<<n);++i){
int tmp=i,cnt=0,num=1;
while(tmp){
if(tmp&1) b[++cnt]=num;
++num;tmp>>=1;
}
for(int j=1;j<=cnt;++j){
int x=b[j];
if(i&(1<<(x-1))){
for(int k=1;k<=cnt;++k){
int y=b[k];
if(j!=k&&(i&(1<<(y-1)))){
int sum=(a[y]-a[x])*(n-cnt+1);
if(y<x) sum+=n-cnt+1;
if(a[y]<a[x]) sum=0;
for(int l=m;l>=sum;--l)
f[i][x][l]+=f[i-(1<<(x-1))][y][l-sum];//cout<<f[i][x][l]<<" "<<i-(1<<(x-1))<<" "<<y<<" "<<l-sum<<endl;
}
}
}
}
}
for(int i=1;i<=n;++i)
for(int j=0;j<=m;++j)
ans+=f[(1<<n)-1][i][j];
printf("%lld\n",ans);
return 0;
}
P2150 [NOI2015] 寿司晚宴
因为两个人选的数字全部互质,所以甲选的数字的质因数集合和乙选的数字的质因数集合没有交集
设 \(dp[s1][s2]\) 表示甲选的数字的质因数集合是 \(s1\) ,乙选择的数字的质因数集合是 \(s2\) 的方案数。
状态转移方程: \(dp[i][s1|k][s2]+=dp[i-1][s1][s2](k\&s2==0)\) ,其中, \(k\) 是当前的质因数集合。
然而, \(500\) 以内的质因数有很多,直接状压肯定超时。所以需要考虑优化。
我们发现,一个小于\(500\)的数字,最多只有一个比\(22\)大的质因数。所以考虑单独计算这个含有这个大质因数的数的贡献。
设 \(f1[s1][s2],f2[s1][s2]\) 分别表示这个数让甲选,让乙选的方案数。首先将所有数按照最大质因数的大小从小到大排序,然后对于所有最大质因数相同的数,先将 \(dp\) 数组的数值赋予 \(f1\) , \(f2\) ,然后推出 \(f1\) , \(f2\) ,最后将 \(f1\) , \(f2\) 合并给 \(dp\) 数组。
\(dp[s1][s2]=f1[s1][s2]+f2[s1][s2]-dp[s1][s2]\)
注意这里要减去 \(dp[s1][s2]\) ,因为 \(f1\) , \(f2\) 会重复统计两个人都不选的情况。
\(code:\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
long long n,m,tot,p[1005],dp[1005][1005],f2[1005][1005],f1[1005][1005],ans;
struct node{
int w,big;
} a[1005];
bool ok[1005];
bool cmp(node a,node b){
return a.big<b.big;
}
int main(){
scanf("%lld%lld",&n,&m);
const long long mod=m;
for(int i=2;i<=n;++i)
if(!ok[i]){
p[++tot]=i;
for(int j=i*2;j<=n;j+=i)
ok[j]=1;
}
for(int i=1;i<n;++i){
for(int j=1;j<=tot;++j){
if(p[j]<=22&&(i+1)%p[j]==0)
a[i].w|=1<<(j-1);
if(p[j]>22&&(i+1)%p[j]==0)
a[i].big=p[j];
}
}
sort(a+1,a+n,cmp);
dp[0][0]=1;
for(int i=1;i<n;++i){
if(a[i].big==0||i==n-1||a[i].big!=a[i-1].big){
memcpy(f1,dp,sizeof(f1));
memcpy(f2,dp,sizeof(f2));
}
for(int j=255;j>=0;--j)
for(int k=255;k>=0;--k)
if((j&k)==0){
if((a[i].w&j)==0)
f2[j][a[i].w|k]=(f2[j][a[i].w|k]+f2[j][k])%mod;
if((a[i].w&k)==0)
f1[j|a[i].w][k]=(f1[j|a[i].w][k]+f1[j][k])%mod;
}
if(i==n-1||a[i].big!=a[i+1].big||a[i].big==0){
for(int j=0;j<=255;++j)
for(int k=0;k<=255;++k)
if((j&k)==0)
dp[j][k]=((f1[j][k]+f2[j][k])-dp[j][k]+mod)%mod;
}
}
for(int i=0;i<=255;++i)
for(int j=0;j<=255;++j)
ans=(ans+dp[i][j])%mod;
printf("%lld\n",ans);
return 0;
}
P3451 [POI2007] ATR-Tourist Attractions
如果没有64MB的限制,那么这就成了一个状压水题。设\(f[i][j]\)表示\(k\)个点是否停留的状态为\(i\),最后一个停留的点为\(j\)的最短距离。状态转移方程:\(f[i][j]=min(f[i][j],f[i-2^{j-1}][k]+dis(j,k))\)
那么加上空间限制怎么办呢?可以考虑滚动数组。设\(f[i\&1][j][t]\)表示当前状态的二进制有\(i\)个\(1\),状态的映射为\(j\),最后一个停留的点为\(t\)的最短距离。我们状态的二进制最多有\(20\)位,当其中\(1\)的数量相同时,最多有\(C_{20}^{10}=184756\)种不同的数。因此,我们可以将二进制中\(1\)的数量相同的数映射到 \([1,2e5]\) 以内。所以开数组时可以开成 \(f[2][200005][22]\) ,不会超空间。
void pre_work(){
for(int i=1;i<=k+2;++i)
dij(i);//求i到所有点的最短路(特别地,k+2表示n)
for(int i=0;i<(1<<k);++i){
int tmp=i,len=0;
while(tmp){
if(tmp&1) ++len;
tmp>>=1;
}
sta[++cnt[len]][len]=i;
sta2[i]=cnt[len];
}
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;++i){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);add(v,u,w);
}
scanf("%d",&g);
for(int i=1;i<=g;++i){
scanf("%d%d",&u,&v);
++in[v];add2(u,v);
}
pre_work();
for(int i=0;i<k;++i){
clear(i+1);//清空数组
for(int j=1;j<=cnt[i];++j){
int tmp=sta[j][i],len=0,num=1;
for(int t=2;t<=k+1;++t)
in2[t]=in[t],vis[t]=0;
while(tmp){
if(tmp&1)
b[++len]=num+1,vis[num+1]=1;
tmp>>=1;++num;
}
if(!sta[j][i])
b[len=1]=1;
for(int t=1;t<=len;++t)
for(int p=head2[b[t]];p;p=nxt2[p])
--in2[ver2[p]];
for(int t=2;t<=k+1;++t)
if(in2[t]==0&&!vis[t]){
int y=sta2[sta[j][i]+(1<<(t-2))];
for(int p=1;p<=len;++p)
f[(i+1)&1][y][t]=min(f[(i+1)&1][y][t],f[i&1][j][b[p]]+dis[b[p]][t]);
}
}
}
ans=inf;
for(int i=1;i<=k+1;++i)
ans=min(ans,f[k&1][1][i]+dis[k+2][i]);//别忘了最后走到n
printf("%d\n",ans);
return 0;
}
P1777 帮助
因为序列的值域很小,所以考虑状压DP。
设 \(f[i][j][k][l]\) 表示前 \(i\) 本书,已经选择了 \(j\) 本,之前存书集合为 \(l\) ,最后一本没取的书编号为 \(t\) 的最小代价。状态转移方程:
最后答案是 \(min\{f[n][j][k][l]+count(l\space xor\space sta)\}\) ,\(count\) 表示二进制中一的个数, \(sta\) 表示所有书构成的二进制状态。

浙公网安备 33010602011771号