题目
给定一个 n × m n \times m n×m 的矩阵,行数和列数都不超过 20,其中有些格子可以选,有些格子不能选。现在你需要从中选出尽可能多的格子,且保证选出的所有格子之间不相邻(没有公共边)。
例如下面这个矩阵( 2 × 3 2 \times 3 2×3的矩阵)
1 1 1
0 1 0
最多可选 33 个互不相邻的格子,方案如下(选中的位置标记为x):
x 1 x
0 x 0
解法详解
我们可以自上而下一行一行选择格子。在选择格子的过程中,只和上一行选择的方案有关,所以我们可以将“当前放到第几行,当前行的选择方案”作为状态进行状态压缩动态规划。
一行里被选择的格子可以看作一个集合,我们要将这个集合压缩为一个二进制数,如果选了就即将当前位设为1,否则没选为0.比如,对于一个3列的矩阵,如果当前行的状态是
(
5
)
10
=
(
101
)
2
(5)_{10}=(101)_{2}
(5)10=(101)2,代表当前行选择了第一个和第三个各自。类似地,如果当前行的状态时
(
6
)
10
=
(
110
)
2
(6)_{10}=(110)_{2}
(6)10=(110)2,代表,当前行选择了第一个和第二个格子那么意味着(当然由于计算顺序的缘故,我们通常会将
(
110
)
2
(110)_{2}
(110)2看作选择了倒数第一个和倒数第二个格子,但这并不影响我们理解此解法)
如果上一行的状态为now,下一行的状态为nxt,那么我们只需要确保上下两行的选择方案中没有重复的元素,也就是KaTeX parse error: Expected 'EOF', got '&' at position 6: (now &̲ nxt)==0就可以了
此外,我们还需要判断当前行的状态是否合法,因为读入的矩阵并不是每一个都可以选职责的,如果我们将矩阵中的每行的值也用状态压缩来存储,记其为flag,那么当前行选择的各自的集合一定包含于当前行合法格子的集合,也就是说(now|flag)==flag必须成立;同时行内选择的各自也不能相邻,也就是
n
o
w
与
(
n
o
w
>
>
1
)
=
=
0
now与(now>>1)==0
now与(now>>1)==0必须成立。
综上所述,设上一行为now,下一行时nxt,当前行的合法情况为flag,条件就应该是:
- (now & nxt)==0
- (now|flag)==flag
- now&(now>>1)==0
这样,我们就可以枚举上一行的所有状态,用于更新当前行,当前状态的最优解了。直到算完最后一行,统计一下所有状态的最大值就可以了。
代码的实现过程
要求1的实现:
bool not_intersect(int now, int nxt) { // 判断状态 now 和状态 nxt 是否能放在相邻的行
return (now & nxt) == 0;
}
要求2的实现:
bool fit(int now, int i) { // 用来判断 now 这个选取状态是否和符合第 i 行的输入
return (now | state[i]) == state[i];
}
要求3的实现:
bool ok(int now) { // 判断行内是否相交
return (now & (now >> 1)) == 0;
}
统计当前状态有选择了多少个
那么就要应该是将当前状态的子集不断地向右移,并&1,也就是判断当前这位是否为1,再让计数器+=当前右移后的子集&1。
int count(int now) { // 统计 now 状态选了多少个元素
int s = 0;
while (now) {
s += (now & 1);
now >>= 1;
}
return s;
}
合法情况读入
行从 1 开始,列从 0 开始,这样后面处理起来方便
即
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
cin >> a[i][j];
}
}
将每行合法情况用数组表示
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
if (a[i][j]) {
state[i] += (1 << j);
}
}
}
dp过程
先用i枚举当前在枚举第几行
for(int i=1;i<=n;i++){
j枚举当前的这一行的状态
for(int j=0;j<(1<<m);j++){
用ok(j)查看当前这行是否相邻两个没有同时选择
用fit(i,j)查看第i行j状态是否合法
if(ok(j) && fit(j,i)){
k枚举上一行的状态
for(int k=0;k<(1<<m);k++){
用ok(k)查看当前上一行是否相邻两个没有同时选择
用fit(i-1,k)查看第(i-1)行k状态是否合法
用not_intersect(j,k)判断这行的状态j和上一行的状态k是否不重叠
if(ok(k)&&fit(k,i-1) && not_intersect(j,k)){
最后如果都符合要求,用动态规划方程计算
dp[i][j]=max(dp[i][j],dp[i-1][k]+count(j));
输出答案
分别看dp数组中第n行的所有情况是否成立,如成立就与上一个成立的情况比较,取较大的为方案最多的个数,最后输出ans
int ans=0;
for(int i=0;i<(1<<m);i++){
ans=max(ans,dp[n][i]);
}
cout<<ans<<endl;
示例代码
#include <iostream>
using namespace std;
int a[21][20];
int state[21]; // 初始每行的状态
int dp[21][1 << 20];
bool ok(int now) { // 判断行内是否相交
return (now & (now >> 1)) == 0;
}
bool fit(int now, int i) { // 用来判断 now 这个选取状态是否和符合第 i 行的输入
return (now | state[i]) == state[i];
}
bool not_intersect(int now, int nxt) { // 判断状态 now 和状态 nxt 是否能放在相邻的行
return (now & nxt) == 0;
}
int count(int now) { // 统计 now 状态选了多少个元素
int s = 0;
while (now) {
s += (now & 1);
now >>= 1;
}
return s;
}
int main() {
int n, m;
cin >> n >> m;
// 行从 1 开始,列从 0 开始,这样后面处理起来方便
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
cin >> a[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
if (a[i][j]) {
state[i] += (1 << j);
}
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<(1<<m);j++){
if(ok(j) && fit(j,i)){
for(int k=0;k<(1<<m);k++){
if(ok(k)&&fit(k,i-1) && not_intersect(j,k)){
dp[i][j]=max(dp[i][j],dp[i-1][k]+count(j));
}
}
}
}
}
int ans=0;
for(int i=0;i<(1<<m);i++){
ans=max(ans,dp[n][i]);
}
cout<<ans<<endl;
return 0;
}