状压 DP 总结
指挥使珂爱qwq CSG 咕咕咕
状压dp总结
在很多情况下,我们需要记录的状态很复杂。
我们就把这些dp统一称为状压dp。
这类问题的一个经典应用是在棋盘上。
我们往往记录一整行信息,或者记录之前的一些网格/线/列/斜线的信息。
在一些其他的问题中,我们可能需要记录每一项的选择状态。
我们通常用 \(0\) 表示一个物品没被选中,\(1\) 表被选中。
剩下的物体最优解,只与前面少量的信息有关。
在更复杂的情况下,一个项目可以有更复杂的状态。
方法
一般用一个二进制数表示压缩后的状态。
然后即可暴力跑状态dp。
因此状压dp复杂度一般是指数级的。
所以 \(n\) 一般在 \(10\) 到 \(18\) 之间。
愤怒的小鸟
题目传送门
简述题意
- 有 \(n\) 个猪,你可以从原点引出抛物线,问最少用多少次可以打到所有猪。
简述做法
-
预处理所有有效抛物线。
-
以猪有没有被打下来作为状态进行状压DP。
具体阐述
我们对每个猪进行一次遍历,每次对序号在他后面的猪进行遍历,对于之前没有的抛物线我们新保存一次。
对于抛物线 \(y=ax^2+bx+c\)。已知经过原点,所以 \(c=0\)。
我们又另知抛物线经过两点 \((x_i,y_i)\) \((x_j,y_j)\)。
可算出 \(a=\dfrac{x_jy_i-x_iy_j}{x_ix_j(x_i-x_j)}\), \(b=\dfrac{x_ix_iy_j-x_jx_jy_i}{x_ix_j(x_i-x_j)}\)、
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++;
}
}
因为 \(n \le 18\),所以很容易想到状压DP。
我们定义 \(f_s\) 为打到 \(s\) 的状态时最少的次数。
对于每个抛物线,可易得转移方程 \(f_{i|line_j}=min{f_{i|line_j},f_i+1}\)。
不要忘记 \(f_0=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(2^n \times n^2)\)。
管道连接
题目传送门
题意
-
给出一张 \(n\) 个点,\(m\) 条边带有 \(p\) 的特殊点的图,每个特殊点有一个颜色。
-
要求选出若干条边,使得颜色相同的在同一个连通块内。
-
输出最小边权和。
先简单介绍一个问题类型。
最小斯坦纳树:在一张给定的带权无向图中,将其中 \(k\) 个点变成连通块最少需要花费的代价。
因为最后连起来会是一棵树,所以就叫最小斯坦纳树。
令 \(f_{i,S}\) 表示当前 \(i\) 在的连通块中,点集状态为 \(S\) 需要花费的最小费用。
转移方法
在一个点上汇合
我们需要合并不同的若干子集。
\(f_{i,S1|S2}=min(f_{i,S1|S2})\)
往一个方向走
需要一个SPFA实现。
\(f_{j,S|w_j}=min(f_{i,S}+w_{i,j})\)
回归本题。
最优解->斯坦纳森林。
不妨先用上述方法求出每个子集汇合需要的代价。
最后的解是若干个子集的并。
每个子集都是若干种完整的颜色。
利用一个子集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;
}
吃货 JYY
题目传送门
(注:这题没写代码,且偏口胡,应该思路什么都没锅)
代码不咕了 它有了/cy/qiang
题意
-
给一张有权无向图,标记其中某些边。
-
从 \(1\) 号点出发要求我们找一条回路,且回路经过所有标记边。
-
最小化边权和。
我们找到一些边,使得构成欧拉回路。
那么就产生两个条件。
-
所有点度数都是偶数。
-
选出来的边构成的图,必须只有一个联通分量。
Sol \(1\)
令 \(f_{i,S}\) 表示当前每个点的状态,以及当前最后停留在 \(i\) 的位置。
\(S\) 状态每个点分成三种情况。
\(0\):不与 \(1\) 连通
\(1\):和 \(1\) 连通且度数为奇数
\(2\):和 \(1\) 连通且度数为偶数
然而这样无法处理强制选边。
因此我们继续分析题目性质。
Sol \(2\)
观察到所有边可以分成两部分。
一部分是到过的到没到过的点,另一部分是连接两个到过的点。
对于两个到过的点,连边即可改变他们的奇偶性。
且若走最短路径,一定会得到最好的效果。
我们猜想,可dp出所有点连通性的状态,然后我们再用最短路把奇数点之间两两连上。
令 \(f_S\) 表示每个点与1号点连通状态确定时最小代价。
每次我们枚举边 \((i,j)\) 其中 \(i\) 在 \(S\) 中不为0,\(j\) 为0。
第一次加进一个点的时候,附加上它使用必须边的连通性,即可把必须取边条件放上。
令 \(w_i\) 表示只用 \(k\) 条边,\(i\) 和哪些点连通,且这些点奇偶状态如何。
我们在第一次加入连通块中的一个点时,我们就把所有点拖进去。
再令 \(g_S\) 表示当 \(S\) 中点为奇数时,变成偶数的最优解。
每次枚举点转移即可qwq。
时间复杂度 \(O(3^n \times m)\),可能需要一些常数优化(¿)。
子集枚举
对子集枚举的复杂度进行一点分析。
这里的子集枚举为,在一个二进制数中,枚举所有为1的子集。
我们有一种通用写法。
这个写法看似很暴力实际上也很暴力,复杂度 \(O(3^n)\)。
实际上就是 \(\sum^n_{i=0}C(n,i) \times 2^i\) 展开后同 \((1+2)^n\)。
即为 \(O(3^n)\)。
/*
***
还要继续努力
成为一名烤咕学家哦
***
*/
#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;
}