zoukankan      html  css  js  c++  java
  • 状压 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 imes 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) 号点出发要求我们找一条回路,且回路经过所有标记边。

    • 最小化边权和。


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

    那么就产生两个条件。

    1. 所有点度数都是偶数

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

    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 imes m)),可能需要一些常数优化(¿)。

    子集枚举

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

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

    我们有一种通用写法。

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

    实际上就是 (sum^n_{i=0}C(n,i) imes 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;
    }
    
  • 相关阅读:
    适配器模式
    快排变种
    美团面试问题
    gulp前端自动化构建工具新手入门篇
    javascript继承
    .call()和.apply()相同点与不同点
    JavaScript原型,原型链 !
    锚点链接和hash属性
    构造函数与普通函数的比较
    JS的作用域和作用域链
  • 原文地址:https://www.cnblogs.com/danieljiang/p/zydpzj.html
Copyright © 2011-2022 走看看