Living-Dream 系列笔记 第55期

Posted on 2024-04-29 22:04  _XOFqwq  阅读(6)  评论(0编辑  收藏  举报

状压 dp 空间优化技巧:

  • 滚动数组

  • 提前预处理出有效状态

T1

典题限时返场。

上次讲的时候的代码用到了滚动数组,这次讲第二种优化。

其实很简单,就是在 dp 前将所有状态枚举一遍,将同行冲突的判掉,合法的用 \(st_i\) 存储即可。

方法很 bf,但经试验可以发现一千多状态中仅有几十个合法的,因此效率能快得飞起。

哦对了矩阵上的状压一般初始状态都在第 \(0\) 行。

code
#include<bits/stdc++.h>
#define int long long 
using namespace std;

int n,m;
int soil[105],val[1<<15];
int dp[105][205][205];
int tot,st[205];

signed main(){
	cin>>n>>m;
	memset(dp,0xcf,sizeof(dp));
	dp[0][1][1]=0;
	char c;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>c,soil[i]=(soil[i]<<1)+(c=='P');
	for(int i=0;i<(1<<m);i++){
		if((i&(i<<1))||(i&(i<<2))) continue;
		st[++tot]=i,val[i]=__builtin_popcount(i);
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=tot;j++){
			if((soil[i]&st[j])!=st[j]) continue;
			for(int p=1;p<=tot;p++){
				if(st[j]&st[p]) continue;
				for(int q=1;q<=tot;q++){
					if(st[j]&st[q]) continue;
					dp[i][j][p]=max(dp[i][j][p],dp[i-1][p][q]+val[st[j]]);
				}
			}
		}
	}
	int ans=-1e9;
	for(int i=1;i<=tot;i++)
		for(int j=1;j<=tot;j++)
			ans=max(ans,dp[n][i][j]);
	cout<<ans;
	return 0;
}

T2 / T3

这两题一模一样且都很典,所以放到一起讲了。

其实就是 T1 多加一个维度,即前 \(i\) 行共放了 \(x\) 个国王。

然后在循环中枚举 \(x\),有转移:

\[dp_{i,j,x}=dp_{i,j,x}+dp_{i-1,k,x-c} \]

其中 \(i\) 表示当前行,\(j,k\) 表示当前行与上一行的状态,\(c\) 表示当前行放了几个国王(即 \(j\)\(1\) 的个数)

然后改一下判定合法的方式即可,其他的和 T1 完全一致。

这是 T3 的做法,至于 T2 也是仅需改一下判定合法的方式,然后多加一个维度(因为有上两行)并状压列即可。

T2 code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int M=101,N=6,K=21,MOD=1e9+7;
int n,m,k;
int dp[M][1<<N][1<<N][K];

signed main(){
	cin>>n>>m>>k;
	dp[0][0][0][0]=1;
	for(int i=1;i<=m;i++){
		for(int kk=0;kk<=k;kk++){
			for(int j=0;j<(1<<n);j++){
				int c=__builtin_popcount(j);
				if(c>kk) continue;
				for(int p=0;p<(1<<n);p++){
					for(int q=0;q<(1<<n);q++){
						if((j<<1)&q||(j<<2)&p||(j>>1)&q||(j>>2)&p) continue;
						dp[i][j][p][kk]=(dp[i][j][p][kk]+dp[i-1][p][q][kk-c])%MOD;
					}
				}
			}
		}
	}
	int ans=0;
	for(int i=0;i<(1<<n);i++)
		for(int j=0;j<(1<<n);j++)
			ans=(ans+dp[m][i][j][k])%MOD;
	cout<<ans;
	return 0;
}
T3 code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=10,K=1e2+5;
int n,k;
int dp[N][1<<N][K];

signed main(){
	cin>>n>>k;
	dp[0][0][0]=1;
	for(int i=1;i<=n;i++){
		for(int kk=0;kk<=k;kk++){
			for(int j=0;j<(1<<n);j++){
				int c=__builtin_popcount(j);
				if(j&(j<<1)||c>kk) continue;
				for(int l=0;l<(1<<n);l++){
					if((j&l)||((j<<1)&l)||((j>>1)&l)||((l<<1)&l)) continue;
					dp[i][j][kk]+=dp[i-1][l][kk-c];
				}
			}
		}
	}
	int ans=0;
	for(int i=0;i<(1<<n);i++) ans+=dp[n][i][k];
	cout<<ans;
	return 0;
}

作业 T1

曾经用 bf + 剪枝过的,很奇特。

但是这题状压更简单,仅需在设初始状态时将每个点的初始 dp 值设为与 \((0,0)\) 的距离即可,然后就变成 这里边的 T1 了。

注意初始化 dp 数组要 memset(dp,127,sizeof(dp)),因为 dp 是 double 类型数组。

code
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;

const int N=16;
int n,vis;
double dp[1<<N][N];
pair<double,double> a[N];

int main(){
	cin>>n;
	memset(dp,127,sizeof(dp));
	for(int i=0;i<n;i++)
		cin>>a[i].x>>a[i].y;
	for(int i=0;i<n;i++)
		dp[1<<i][i]=sqrt(a[i].x*a[i].x+a[i].y*a[i].y);
	for(int i=0;i<(1<<n);i++)
		for(int j=0;j<n;j++)
			if((i>>j)&1)
				for(int k=0;k<n;k++)
					if((i>>k)&1)
						dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+sqrt((a[j].x-a[k].x)*(a[j].x-a[k].x)+(a[j].y-a[k].y)*(a[j].y-a[k].y)));
	double ans=1e9;
	for(int i=0;i<n;i++)
		ans=min(ans,dp[(1<<n)-1][i]);
	cout<<setprecision(2)<<fixed<<ans;
	return 0;
}

作业 T2

很有趣味的题,可惜正解不是状压。

题外话:here 是此题的加强版题解,先 mark 下以后研究。

首先很容易得到一个性质:每行、每列的炮的数量 \(\le 2\)

依据这一点,我们想到对于每一行都会有三种列:

没有炮的列、有一个炮的列、有两个炮的列。

于是我们考虑设计如下状态:

\(dp_{i,j,k}\) 表示第 \(i\) 行中,\(j\) 列有一个炮,\(k\) 列有两个炮。

显然,空列数 \(= m-j-k\)

然后我们对于第 \(i\) 行,分三种情形进行讨论:

  • 一个都不放

直接 \(dp_{i,j,k}=dp_{i-1,j,k}\)

  • 放一个

我们可以选择放在空列中,

此时有一个炮的列会增加一个(\(j-1 \to j\)),

于是此时有

\[dp_{i,j,k}=dp_{i,j,k}+dp_{i-1,j-1,k} \times (m-(j-1)-k) \]

同样的,我们也可以选择放在有一个炮的列中,

此时有一个炮的列会减少一个(\(j+1 \to j\)),有两个炮的列会增加一个(\(k-1 \to k\))。

于是此时有

\[dp_{i,j,k}=dp_{i,j,k}+dp_{i-1,j+1,k-1} \times (j+1) \]

  • 放两个

我们可以选择都放在空列中,

此时有一个炮的列会增加两个(\(j-2 \to j\)),

于是此时有

\[dp_{i,j,k}=dp_{i,j,k}+dp_{i-1,j-2,k} \times C^{2}_{m-(j-2)-k} \]

我们也可以选择一个放在空列,一个放在有一个炮的列中,

此时有一个炮的列不变,有两个炮的列会增加一个(\(k-1 \to k\)),

于是此时有

\[dp_{i,j,k}=dp_{i,j,k}+dp_{i-1,j,k-1} \times j \times (m-j-(k-1)) \]

我们还可以选择两个都放在有一个炮的列中,

此时有一个炮的列会减少两个(\(j+2 \to j\)),有两个炮的列会增加两个(\(k-2 \to k\)),

于是此时有

\[dp_{i,j,k}=dp_{i,j,k}+dp_{i-1,j+2,k-2} \times C^{2}_{j+2} \]

其中因为只涉及到 \(m=2\) 的组合数,所以可以 \(O(1)\) 求得,具体见 code。

完结撒花!~

code
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=131,MOD=9999973;
int n,m;
int dp[N][N][N];

int C(int x){
	return x*(x-1)/2;
}

signed main(){
	cin>>n>>m;
	dp[0][0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			for(int k=0;k<=m-j;k++){
				dp[i][j][k]=dp[i-1][j][k];
				if(j>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k]*(m-(j-1)-k))%MOD;
				if(k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+1][k-1]*(j+1))%MOD;
				if(j>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-2][k]*C(m-(j-2)-k))%MOD;
				if(k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k-1]*j*(m-j-(k-1)))%MOD;
				if(k>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+2][k-2]*C(j+2))%MOD;
			}
		}
	}
	int ans=0;
	for(int i=0;i<=m;i++)
		for(int j=0;j<=m;j++)
			ans=(ans+dp[n][i][j])%MOD;
	cout<<ans;
	return 0;
}