状压 DP 总结

0|1指挥使珂爱qwq CSG 咕咕咕

1|0状压dp总结

在很多情况下,我们需要记录的状态很复杂。

我们就把这些dp统一称为状压dp

这类问题的一个经典应用是在棋盘上。

我们往往记录一整行信息,或者记录之前的一些网格/线/列/斜线的信息。

在一些其他的问题中,我们可能需要记录每一项的选择状态

我们通常用 0 表示一个物品没被选中,1 表被选中。

剩下的物体最优解,只与前面少量的信息有关。

在更复杂的情况下,一个项目可以有更复杂的状态。

2|0方法

一般用一个二进制数表示压缩后的状态。

然后即可暴力跑状态dp。

因此状压dp复杂度一般是指数级的。

所以 n 一般1018 之间。

3|0愤怒的小鸟

1|0题目传送门

3|1简述题意

  • n 个猪,你可以从原点引出抛物线,问最少用多少次可以打到所有猪。

3|2简述做法

  • 预处理所有有效抛物线。

  • 以猪有没有被打下来作为状态进行状压DP。

3|3具体阐述

我们对每个猪进行一次遍历,每次对序号在他后面的猪进行遍历,对于之前没有的抛物线我们新保存一次。

对于抛物线 y=ax2+bx+c。已知经过原点,所以 c=0

我们又另知抛物线经过两点 (xi,yi) (xj,yj)

可算出 a=xjyixiyjxixj(xixj)b=xixiyjxjxjyixixj(xixj)

for(ll i=0;i<n;i++){ line[cnt++]=(1<<i); for(ll j=i+1,use=0;j<n;j++) if((use>>j)&1) continue; else{ ld a=(x[j]*y[i]-x[i]*y[j])/(x[i]*x[j]*(x[i]-x[j])),b=(x[i]*x[i]*y[j]-x[j]*x[j]*y[i])/(x[i]*x[j]*(x[i]-x[j])); if(a>=0) continue; line[cnt]=(1<<i); for(ll k=j;k<n;k++) if(ab(a*x[k]*x[k]+b*x[k]-y[k])<=eps) use|=(1<<k),line[cnt]|=(1<<k); cnt++; } }

因为 n18,所以很容易想到状压DP。

我们定义 fs 为打到 s 的状态时最少的次数。

对于每个抛物线,可易得转移方程 fi|linej=minfi|linej,fi+1

不要忘记 f0=1

for(ll i=0;i<(1<<n);i++) for(ll j=0;j<cnt;j++) f[i|line[j]]=min(f[i|line[j]],f[i]+1);

本题运用基本数学知识和基本状压DP,偏模板型。

/* *** 还要继续努力 成为一名烤咕学家哦 *** */ #include<bits/stdc++.h> using namespace std; typedef long long ll; typedef long double ld; const ll N=20; ll n,T,line[200],f[1<<N],cnt,op; ld x[N],y[N],eps=1e-6; template <typename T> inline void rd(T &x){ ll fl=1;x=0;char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-') fl=-fl; for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0'; x*=fl; } void wr(ll x){ if(x<0) x=-x,putchar('-'); if(x<10) putchar(x+'0'); if(x>9) wr(x/10),putchar(x%10+'0'); } inline ld ab(ld x){ return x>=0.00?x:-x; } int main(){ // freopen(".in","r",stdin); // freopen(".out","w",stdout); rd(T); while(T--){ memset(f,0x3f3f3f3f,sizeof(f));cnt=0; rd(n);rd(op); for(ll i=0;i<n;i++) scanf("%llf %llf",&x[i],&y[i]); for(ll i=0;i<n;i++){ line[cnt++]=(1<<i); for(ll j=i+1,use=0;j<n;j++) if((use>>j)&1) continue; else{ ld a=(x[j]*y[i]-x[i]*y[j])/(x[i]*x[j]*(x[i]-x[j])),b=(x[i]*x[i]*y[j]-x[j]*x[j]*y[i])/(x[i]*x[j]*(x[i]-x[j])); if(a>=0) continue; line[cnt]=(1<<i); for(ll k=j;k<n;k++) if(ab(a*x[k]*x[k]+b*x[k]-y[k])<=eps) use|=(1<<k),line[cnt]|=(1<<k); cnt++; } } f[0]=0; for(ll i=0;i<(1<<n);i++) for(ll j=0;j<cnt;j++) f[i|line[j]]=min(f[i|line[j]],f[i]+1); wr(f[(1<<n)-1]);puts(""); } return 0; }

时间复杂度 O(2n×n2)

4|0管道连接

1|0题目传送门

4|1题意

  • 给出一张 n 个点,m 条边带有 p 的特殊点的图,每个特殊点有一个颜色。

  • 要求选出若干条边,使得颜色相同的在同一个连通块内。

  • 输出最小边权和。


先简单介绍一个问题类型。

最小斯坦纳树:在一张给定的带权无向图中,将其中 k 个点变成连通块最少需要花费的代价。

因为最后连起来会是一棵树,所以就叫最小斯坦纳树。

fi,S 表示当前 i 在的连通块中,点集状态为 S 需要花费的最小费用。

4|2转移方法

1|0在一个点上汇合

我们需要合并不同的若干子集。

fi,S1|S2=min(fi,S1|S2)

1|0往一个方向走

需要一个SPFA实现。

fj,S|wj=min(fi,S+wi,j)

回归本题。

1|0最优解->斯坦纳森林。

不妨先用上述方法求出每个子集汇合需要的代价。

最后的解是若干个子集的并。

每个子集都是若干种完整的颜色

利用一个子集dp完成。

/* *** 还要继续努力 成为一名烤咕学家哦 *** */ #include<bits/stdc++.h> using namespace std; typedef long long ll; const ll N=11,M=1005; ll m,n,p,f[M][1<<N],g[1<<N],w[1<<N],k1,hd[M],ID[M],num[N],Q[M*M],vis[M]; struct Node{ll t,nxt,val;}s[M*10]; template <typename T> void rd(T &x){ ll fl=1;x=0;char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-') fl=-fl; for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0'; x*=fl; } void wr(ll x){ if(x<0) x=-x,putchar('-'); if(x<10) putchar(x+'0'); if(x>9) wr(x/10),putchar(x%10+'0'); } void add(ll x,ll y,ll w){s[++k1].t=y; s[k1].nxt=hd[x]; s[k1].val=w; hd[x]=k1;} void spfa(ll ID){ Q[0]=0; for(ll i=1;i<=n;++i) if(f[i][ID]<(ll)1e9) Q[++Q[0]]=i,vis[i]=1; for(ll l=1;l<=Q[0];++l){ ll p=Q[l]; for(ll i=hd[p];i;i=s[i].nxt){ ll k=s[i].t; if(f[k][ID]>f[p][ID]+s[i].val){ f[k][ID]=f[p][ID]+s[i].val; if(!vis[k]){ vis[k]=1; Q[++Q[0]]=k; } } } vis[p]=0; } } int main(){ // freopen(".in","r",stdin); // freopen(".out","w",stdout); rd(n);rd(m);rd(p); for(ll i=1,u,v,val;i<=m;i++){ rd(u);rd(v);rd(val); add(u,v,val);add(v,u,val); } memset(ID,-1,sizeof(ID)); for(ll i=1,x,y;i<=p;++i){ rd(x);rd(y); ID[y]=i-1;num[x]|=(1<<(i-1)); } memset(f,60,sizeof(f)); for(ll i=1;i<=n;++i) if(ID[i]==-1) f[i][0]=0; else f[i][1<<ID[i]]=0; for(ll i=0;i<(1<<p);++i){ if(i) for(ll k=1;k<=n;++k) for(ll j=(i-1)&i;j;j=(j-1)&i) f[k][i]=min(f[k][i],f[k][j]+f[k][i^j]); spfa(i); } memset(w,60,sizeof(w)); for(ll i=0;i<(1<<p);++i) for(ll j=1;j<=n;++j) w[i]=min(w[i],f[j][i]); memset(g,60,sizeof(g)); g[0]=0; for(ll i=1;i<(1<<p);++i) for(ll j=i;j;j=(j-1)&i){ ll Num=0; for(ll k=0;k<p;++k) if(j&(1<<k)) Num|=num[k]; g[i]=min(g[i],g[i^j]+w[Num]); } wr(g[(1<<p)-1]);puts(""); return 0; }

5|0吃货 JYY

1|0题目传送门

注:这题没写代码,且偏口胡,应该思路什么都没锅

代码不咕了 它有了/cy/qiang

5|1题意

  • 给一张有权无向图,标记其中某些边。

  • 1 号点出发要求我们找一条回路,且回路经过所有标记边。

  • 最小化边权和。


我们找到一些边,使得构成欧拉回路

那么就产生两个条件。

  1. 所有点度数都是偶数

  2. 选出来的边构成的图,必须只有一个联通分量

5|2Sol 1

fi,S 表示当前每个点的状态,以及当前最后停留在 i 的位置。

S 状态每个点分成三种情况。

0:不与 1 连通

1:和 1 连通且度数为奇数

2:和 1 连通且度数为偶数

然而这样无法处理强制选边

因此我们继续分析题目性质。

5|3Sol 2

观察到所有边可以分成两部分。

一部分是到过的到没到过的点,另一部分是连接两个到过的点。

对于两个到过的点,连边即可改变他们的奇偶性。

且若走最短路径,一定会得到最好的效果。

我们猜想,可dp出所有点连通性的状态,然后我们再用最短路把奇数点之间两两连上。

fS 表示每个点与1号点连通状态确定时最小代价。

每次我们枚举边 (i,j) 其中 iS 中不为0,j 为0。

第一次加进一个点的时候,附加上它使用必须边的连通性,即可把必须取边条件放上。

wi 表示只用 k 条边,i 和哪些点连通,且这些点奇偶状态如何。

我们在第一次加入连通块中的一个点时,我们就把所有点拖进去。

再令 gS 表示当 S 中点为奇数时,变成偶数的最优解。

每次枚举点转移即可qwq。

时间复杂度 O(3n×m),可能需要一些常数优化(¿)。

5|4子集枚举

子集枚举的复杂度进行一点分析。

这里的子集枚举为,在一个二进制数中,枚举所有为1的子集。

我们有一种通用写法。

这个写法看似很暴力实际上也很暴力,复杂度 O(3n)

实际上就是 i=0nC(n,i)×2i 展开后同 (1+2)n

即为 O(3n)

/* *** 还要继续努力 成为一名烤咕学家哦 *** */ #include<bits/stdc++.h> using namespace std; typedef long long ll; const ll N=15,K=80,INF=0x3f3f3f3f; ll n,m,sum,ans=INF; ll hd[N],pw[N]={1},g[1<<N],dis[N][N],f[1600000],de[N],a[N]; struct edge{ ll t,nxt,l; }es[K<<2]; queue<ll> q; template <typename T> void rd(T &x){ ll fl=1;x=0;char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-') fl=-fl; for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0'; x*=fl; } void wr(ll x){ if(x<0) x=-x,putchar('-'); if(x<10) putchar(x+'0'); if(x>9) wr(x/10),putchar(x%10+'0'); } void add(ll u,ll v,ll w){es[++sum]=(edge){v,hd[u],w};hd[u]=sum;} int main(){ rd(n);rd(m); memset(dis,INF,sizeof(dis));memset(g,INF,sizeof(g));memset(f,INF,sizeof(f)); for(ll i=1;i<=n;i++) dis[i][i]=0; for(ll i=1;i<=n;i++) pw[i]=pw[i-1]*3; for(ll i=0,u,v,w;i<m;i++){ rd(u);rd(v);rd(w); dis[u][v]=dis[v][u]=min(dis[u][v],w); de[u]++;de[v]++; add(u,v,w);add(v,u,w); } rd(m); for(ll i=0,u,v,w;i<m;i++){rd(u);rd(v);rd(w);dis[u][v]=dis[v][u]=min(dis[u][v],w);} for(ll i=1;i<=n;i++) for(ll j=1;j<=n;j++) for(ll k=1;k<=n;k++) dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]); g[0]=0; for(ll i=0;i<(1<<n);i++) for(ll j=1;j<=n;j++) if(!(i&(1<<(j-1)))) for(ll k=j+1;k<=n;k++) if(!(i&(1<<(k-1)))) g[i^(1<<(j-1))^(1<<(k-1))]=min(g[i^(1<<(j-1))^(1<<(k-1))],g[i]+dis[j][k]); f[2]=0;q.push(2); while(!q.empty()){ ll qwq=q.front(),cnt=0;q.pop(); for(ll i=1;i<=n;i++) if(qwq/pw[i-1]%3>0) a[++cnt]=i; for(ll i=1;i<=n;i++) if(qwq/pw[i-1]%3==0){ for(ll j=hd[i];j;j=es[j].nxt) if(qwq/pw[es[j].t-1]%3>0){ ll s=qwq+pw[i-1]*2; if(f[qwq]>=f[s]) continue; if(f[s]>=INF) q.push(s); f[s]=f[qwq]; } for(ll j=1;j<=cnt;j++){ ll s=qwq+pw[i-1]; s+=(qwq/pw[a[j]-1]%3==1)?pw[a[j]-1]:-pw[a[j]-1]; if(f[qwq]+dis[i][a[j]]>=f[s]) continue; if(f[s]>=INF) q.push(s); f[s]=f[qwq]+dis[i][a[j]]; } } } for(ll k=0;k<pw[n];k++){ ll fl=0,nw=k,s=0; for(ll i=1;i<=n;i++) if(de[i]&&k/pw[i-1]%3==0){fl=1;break;} if(fl) continue; for(ll i=1;i<=n;i++) if(de[i]&1) nw+=(k/pw[i-1]%3==1)?pw[i-1]:-pw[i-1]; for(ll i=1;i<=n;i++) if(nw/pw[i-1]%3==1) s^=1<<(i-1); ans=min(ans,f[k]+g[s]); } for(ll i=1;i<=sum;i+=2) ans+=es[i].l; wr(ans);puts(""); return 0; }

__EOF__

本文作者Daniel Jiang
本文链接https://www.cnblogs.com/danieljiang/p/zydpzj.html
关于博主:JSOIer 高一萌新 求带/kel
版权声明:awa
声援博主:求赞/kel
posted @   Demoe  阅读(230)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
点击右上角即可分享
微信分享提示