状压dp刷题记录
挑战后面五道状压dp。
Poj2441-Arange the Bulls
题目链接在上面了。根据题目的数据范围,很显然就可以状态压缩,最多只有20的Barns,但是需要注意不能直接开 N * (1<<M)大小的dp数组,会爆。需要对空间优化,用滚动数组,从后往前递推。因为使用了滚动数组,所以不能记忆化搜索。
#include <iostream>
#include <cstring>
using namespace std;
int dp[1 << 21];
int B[30][30];
int N, M;
int main() {
cin >> N >> M;
for (int i = 0; i < N; i++) {
int P; cin >> P;
for (int j = 0; j < P; j++) {
int t; cin >> t;
B[i][t] = 1;
}
}
for (int i = 1; i <= M; i++) {
if (B[0][i]) dp[1 << (i-1)] = 1; // 初始化
}
for (int i = 1; i < N; i++) {
//滚动数组从后往前递推
for (int s = (1 << M) - 1; s >= 0; s--) {
for (int j = 1; j <= M; j++) {
if (!dp[s] || !B[i][j]) continue;
if ((s >> (j-1)) & 1) continue;
//注意不能+1,因为同一个状态可能对应多个不同的方案。
dp[s | 1 << (j-1)] += dp[s];
}
//因为s对应的状态是只有前i-1个牛的状态,但是我们需要有i个牛的状态,为了不多数,要置0
if (dp[s]) dp[s] = 0; ;
}
}
int res = 0;
for (int i = 0; i < (1 << M); i++) res += dp[i];
cout << res << endl;
return 0;
}
Poj3254-Corn Field
这个是一道经典题目NOI2001炮兵阵地的简化版,少了一个纬度。
总的来说还是枚举每一行的状态。
我们设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]为前i行并且第i行的状态为j时的方案数。
那么转移方程为:
d
p
[
i
]
[
j
]
=
d
p
[
i
]
[
j
]
+
d
p
[
i
−
1
]
[
k
]
∀
k
∈
N
dp[i][j] = dp[i][j] + dp[i-1][k] \quad\forall k\in\mathbb N
dp[i][j]=dp[i][j]+dp[i−1][k]∀k∈N
N
N
N 是对于
j
j
j来说合法的状态
k
k
k的集合。
如何判断一个状态的合法同样需要用到位运算。
如果
S
&
(
S
>
>
1
)
S \ \& \ (S>>1)
S & (S>>1) 为0说明在同一行选中的任意两个方格不相邻)。
如果
S
&
K
S \ \& \ K
S & K 为0说明上下两个状态兼容,即对应的方格没有上下相邻的。
#include <iostream>
using namespace std;
const int mod = 100000000;
int n, m;
int corn[20][20];
int dp[20][1<<12];
bool check(int i, int s) {
// 判断同一行的状态
if ((s & (s >> 1)) != 0) return false;
for (int j = 0; j < m; j++) {
if (!corn[i][m-1-j] && ((s >> j) & 1))
return false;
}
return true;
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> corn[i][j];
}
}
// 初始化
for (int i = 0; i < (1 << m); i++) {
if (!check(0, i)) continue;
dp[0][i] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j < (1 << m); j++) {
if (!check(i, j)) continue;
for (int k = 0; k < (1 << m); k++) {
if (!check(i-1, k)) continue;
if (((k & j) != 0)) continue;
dp[i][j] += dp[i-1][k];
}
}
}
int res = 0;
for (int i = 0; i < (1 << m); i++) {
res += dp[n-1][i];
}
cout << res % mod << endl;
return 0;
}
Poj2836-Rectangular covering
每个矩形至少覆盖两个点,问覆盖所有的点所需要的最小的矩形的面积是多少。
首先需要知道覆盖两个点的最小矩形就是分别把两个点当作对角坐标时候的矩形,我们枚举不同的点对就可以得到不同矩形的面积,但是一个矩形可能会覆盖除这两个点外的多个点,因此我们用一个集合
c
o
v
e
r
cover
cover表示矩形所覆盖的点。
我们用
d
p
dp
dp计算结果,如果
d
p
[
s
]
dp[s]
dp[s]为集合
s
s
s是对应的最小面积,那么问题的答案就是
d
p
[
1
<
<
n
−
1
]
dp[1<<n-1]
dp[1<<n−1],考虑状态转移方程:
d
p
[
n
e
w
S
t
a
t
e
]
=
m
i
n
(
d
p
[
n
o
w
S
t
a
t
e
]
+
r
e
c
t
[
k
]
.
a
r
e
a
)
k
∈
C
dp[newState] = min(dp[nowState] + rect[k].area)\ k\in\mathbb C
dp[newState]=min(dp[nowState]+rect[k].area) k∈C
C
\mathbb C
C是所有矩形的集合。
其中
n
e
w
S
t
a
t
e
=
n
o
w
S
t
a
t
e
∣
r
e
c
t
[
k
]
.
c
o
v
e
r
newState =nowState \ | \ rect[k].cover
newState=nowState ∣ rect[k].cover
就是根据一个状态,然后枚举矩形去更新其他的状态,我为人人式的dp。
有一种特殊情况就是如果两个点共线的话会导致矩形面积为0,而矩形的边长必须是整数,因此需要特殊处理,让他在一个每一个维度起码长度为1。
这样其实也并不需要考虑扩大后矩形面积对所覆盖的点的影响。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
const int Inf = 0x3f3f3f3f;
struct P {int x, y;} p[20];
struct R {int area, cover;} r[300];
int dp[1<<16];
int n, cnt;
void solve() {
cnt = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int area = max(1, abs(p[i].x- p[j].x)) * max(1, abs(p[i].y - p[j].y));
r[cnt].area = area;
r[cnt].cover = 0;
int left = min(p[i].x, p[j].x), right = max(p[i].x, p[j].x);
int up = max(p[i].y, p[j].y), down = min(p[i].y, p[j].y);
for (int k = 0; k < n; k++) {
if (p[k].x >= left && p[k].x <= right && p[k].y <= up && p[k].y >= down)
r[cnt].cover |= (1 << k);
}
cnt++;
}
}
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
for (int s = 0; s < (1<<n); s++) {
if(dp[s] == Inf) continue;
for (int i = 0; i < cnt; i++) {
int newState = r[i].cover | s;
dp[newState] = min(dp[newState], dp[s] + r[i].area);
}
}
cout << dp[(1<<n)-1] << endl;
}
int main() {
while (cin >> n) {
if (n == 0) break;
for (int i = 0; i < n; i++)
cin >> p[i].x >> p[i].y;
solve();
}
return 0;
}