【刷穿 CSP-S】状压(状态压缩) DP
529. 宝藏
数据范围 \(n\leq 12\),可以考虑状压 DP。
先考虑朴素 DP。
令 \(f[i][j][k]\) 表示选择的点集为 \(i\),最后一层选择的点集为 \(j\),倒数第二层选择的点集为 \(k\)。
空间上,状态数是 \(O(2^{36})\),已经炸了;时间上,时间复杂度为 \(O(2^{36})\),又炸了。
看来朴素的方法是不成了,考虑修改状态的表示方法。
考虑改变每个点的点权为到任意点的距离与层数的积,发现这种做法并不改变答案的最小值。
证明:设原问题方案的集合为 \(A\),新问题方案的集合为 \(B\)。
对于任何一种树的形态,对于 \(A\),当前节点只能连通往上一层的边;而对于 \(B\),当前节点可以连通通往上面任意一层的边。显然,\(B\) 的选择范围是更大的,有 \(\min A \geq \min B\)。
接下来,只要证 \(\min A \leq \min B\),就能证明 \(\min A=\min B\)。
若存在一条边,使得连接上层节点的代价不是最小的,那么这个节点一定不是最小值。因为总可以将这个节点及子树挂在上层的任意一个节点,使它连接上层节点的代价最小。对于每个点所有连接上层节点的代价最小的一棵树,设这棵树的权值为 \(w\),有 \(\min A \leq w=min B\)。
故 \(\min A=\min B\)。
证毕。
设 \(f[i][j]\) 表示选择的点集为 \(i\),树的高度为 \(j\) 的方案的最小代价。
转移方程为:
其中,\(w\) 是单次转移的最小花费。
每两个点的最小距离可以预处理,时间复杂度为 \(O(n^22^n)\)。
计算 \(w\) 时,需要先枚举集合,再枚举集合的子集,时间复杂度为 \(O(3^n)\)。最后 \(O(n)\)枚举选择的点。
为什么?枚举集合的子集总方案数为:
由二项式定理,
令 \(a=1,b=2\),则原式等于
综上,代码总时间复杂度为 \(O(3^nn)\)。
这里还需要考察一下以 0x3f 作为最大值够不够。
#include<bits/stdc++.h>
using namespace std;
const int N=12,M=1<<12,inf=0x3f3f3f3f;
int n,m,f[M][N],d[N][M],g[N][M];
int main(){
cin>>n>>m;
memset(d,0x3f,sizeof(d));
while(m--){
int a,b,c;
cin>>a>>b>>c;
a--,b--;
d[a][b]=d[b][a]=min(d[a][b],c);
}
for(int i=0;i<n;i++)d[i][i]=0;
memset(g,0x3f,sizeof(g));
for(int i=0;i<n;i++){
for(int j=0;j<(1<<n);j++){
for(int k=0;k<n;k++){
if(j>>k&1){
g[i][j]=min(g[i][j],d[i][k]);
}
}
}
}
memset(f,0x3f,sizeof(f));
for(int i=0;i<n;i++)f[1<<i][0]=0;
for(int i=1;i<(1<<n);i++){
for(int j=i-1&i;j!=0;j=j-1&i){ \\trick:枚举i的子集
int r=i^j,cost=0;
for(int k=0;k<n;k++){
if(j>>k&1){
cost+=g[k][r];
if(cost>=inf)break;
}
}
if(cost>=inf)continue;
for(int k=1;k<n;k++){
f[i][k]=min(f[i][k],f[r][k-1]+cost*k);
}
}
}
int res=inf;
for(int i=0;i<n;i++)res=min(res,f[(1<<n)-1][i]);
cout<<res;
return 0;
}
524.愤怒的小鸟
一般地,\(n\leq 20\) 可以考虑状压 DP。
除了从集合的角度分析 DP 问题,也可以从暴搜的角度考察 DP 问题。
已知两点 \((x_1,y_1),(x_2,y_2)\),不难发现抛物线 \(y=ax^2+bx\) 唯一确定。其中
从 DFS 角度考虑如何操作。
最多枚举 \(n^2\) 个抛物线,求出它能经过的点集。此时问题转化为了经典的重复覆盖问题,即:给定一个 01 矩阵,要求选择最少的行,覆盖住所有的列。Dancing Links 是标准解法。但由于 \(n\leq 18\),简便起见,用状压 DP 求解。
令 \(f[i]\) 表示当前已经覆盖列的状态为 \(i\) 的最小行数。
转移时枚举当前未覆盖的任意一列,然后枚举该列包含的所有行转移。
#include<bits/stdc++.h>
using namespace std;
const int N=20,M=1<<18;
const double eps=1e-10;
int f[M],p[N][N],n,_,t;
struct node{
double x,y;
}q[N];
int cmp(double a,double b){
if(abs(a-b)<eps)return 0;
if(a<b)return -1;
return 1;
}
int main(){
cin>>t;
while(t--){
cin>>n>>_;
for(int i=0;i<n;i++)cin>>q[i].x>>q[i].y;
memset(p,0,sizeof p);
for(int i=0;i<n;i++){
p[i][i]=1<<i;
for(int j=i+1;j<n;j++){
double x1=q[i].x,y1=q[i].y,x2=q[j].x,y2=q[j].y;
if(cmp(x1,x2)==0)continue;
double a=(x2*y1-x1*y2)/(x1*x2*(x1-x2));
if(cmp(a,0)>=0)continue;
double b=(y1-a*x1*x1)/x1;
int state=0;
for(int k=0;k<n;k++){
double x=q[k].x,y=q[k].y;
if(cmp(a*x*x+b*x,y)==0)state|=1<<k;
}
p[i][j]=p[j][i]=state;
}
}
memset(f,0x3f,sizeof f);
f[0]=0;
for(int i=0;i+1<1<<n;i++){
int k=-1;
for(int j=0;j<n;j++){
if(!(i>>j&1)){
k=j;
break;
}
}
for(int j=0;j<n;j++){
f[i|p[k][j]]=min(f[i|p[k][j]],f[i]+1);
}
}
cout<<f[(1<<n)-1]<<"\n";
}
return 0;
}

浙公网安备 33010602011771号