学习笔记:状压 DP

状压 DP

引入

对于某一类问题,我们需要在动态规划的状态中记录一个集合。若集合中有不超过 $n$ 个元素且每一个元素都是小于 $k$ 的自然数,则我们可以将这个集合看作是一个 $n$ 位 $k$ 进制数,以一个 $[0,k^n-1]$ 之间的十进制整数的形式作为 DP 状态中的一维。这种把一个状态集合转化为一个特定整数记录在 DP 状态中的一类算法,被称为状态压缩动态规划,即状压 DP

——李煜东《算法竞赛进阶指南》

实现

一些基本操作

如果集合中的每一个元素都只有两种状态,那么我们可以考虑用二进制数来表示这样的一个集合。如果有三个状态的话就可以用三进制数表示,以此类推。

由此可见,在进行二进制状压 DP 转移时需要熟练掌握二进制数的运算,这里给出一些实例:

x >> 1; // 去掉最后一位
x << 1; // 在最后一位加上一个 0
x << 1 | 1; // 在最后一位加上一个 1
x | 1; // 将最后一位变成 1
(x | 1) - 1; // 将最后一位变成 0
x ^ 1; // 将最后一位按位取反
x | (1 << (k - 1)); // 将右边第 k 位变成 1
x & (~(1 << (k - 1))); // 将右边第 k 位变成 0
x ^ (1 << (k - 1)); // 从右往左数第 k 位按位取反
x & ((1 << k) - 1); // 取得末尾 k 位
(x >> (k - 1)) & 1; // 取得从右往左数第 k 位
x | ((1 << k) - 1); // 将末尾 k 位变成 1
x ^ ((1 << k) - 1); // 将末尾 k 位按位取反
(x & (x + 1)); // 将右边一段连续的 1 变成 0
(x & (x - 1)); // 将右边第一个 1 去掉
x | (x + 1); // 将右边第一个 0 变成 1
x | (x - 1); // 将右边一段连续的 0 变成 1
(x ^ (x + 1)) >> 1; // 取得右边一段连续的 1
x & -x; // 取得最低位的 1 及其后面所有的 0 所构成的数值(即 lowbit 运算)

注意:位运算的优先级通常非常低,如果要用到大量位运算的话,建议在不是非常确定的时候适当地加上一些括号来确定运算顺序,不然在考试时死都不知道是怎么死的……

首先还是搬运一道例题吧。

P1896 [SCOI2005] 互不侵犯

题目描述

在 $N \times N$ 的棋盘里面放 $K$ 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 $8$ 个格子。

输入格式

只有一行,包含两个数 $N,K$。

输出格式

所得的方案数。

样例 #1

样例输入 #1
3 2
样例输出 #1
16

提示

数据范围及约定

对于全部数据,$1 \le N \le 9$,$0 \le K \le N\times N$。

思路

首先,看到这一题,就知道如果不是搜索,就是DP。当然搜索是过不了的,所以就应该尝试想出一个DP的解法。

DP的前提之一当然是要找出一个可以互相递推的状态。显然,目前已使用的国王个数当然必须是状态中的一个部分,因为这是一个限制条件。那么除此之外其余的部分是什么呢?

我们考虑到每行每列之间都有互相的约束关系。因此,我们可以用行和列作为另一个状态的部分(矩阵状压DP常用行作为状态,一下的论述中也用行作为状态)。

注意到 $1\le N\le 9$,在这里我们就可以用一种很新很新的办法来表示行和列的状态:数字。考虑任何一个十进制数都可以转化成一个二进制数,而一行的状态就可以表示成这样——例如:$$ 1010_{(2)} $$ 这个数字代表了什么呢?

很显然,我们定义 $1$ 为当前位置摆放了国王,则 $0$ 为当前位置没有摆放国王。那么这个数字就代表这一行的第一个格子没有摆放国王,第二个格子摆放了国王,第三个格子没有摆放国王,第四个格子摆放了国王(注意到格子从左到右的顺序与二进制从左到右的顺序是相反的,因为程序真正运行时就是这样的)。而这个二进制下的数就可以转化成十进制:$$ 10_{(10)} $$ 此时,我们就定义好了三个状态:令 $f_{i,j,l}$ 表示当前第 $i$ 行的第 $j$ 种状态下这一行已经使用了 $l$ 个国王时的方案数,$k$ 为第 $i-1$ 行的国王摆放情况的状态编号。

考虑状态转移方程,我们首先预处理出每一种状态下包含二进制下 $1$ 的个数 $cnt_i$,以此该状态下这一行放置的国王个数 $siz_i$,则有:$$ f_{i,j,l}=\sum f_{i-1,k,l-siz_j} $$ 再考虑国王之间的关系该如何处理呢?在同一行国王之间的关系我们可以直接在预处理状态时舍去那些不符合题意的状态,而相邻行之间的关系我们就可以用到一个高端的东西:位运算。由于状态已经用数字表示了,因此我们可以用位运算来判断两个状态在同一个或者相邻位置是否都有国王——如果:

  1. cnt[j] & cnt[k],即上下有重复的国王。
  2. (cnt[j] << 1) & cnt[k],即左上右下有重复的国王。
  3. cnt[j] & (cnt[k] << 1),即右上左下有重复的国王。

这样就可以很好地处理掉那些不合法的状态了。

总结一下。其实状压DP不过就是将一个状态转化成一个数,然后用位运算进行状态的处理。理解了这一点,其实就跟普通的DP没有什么两样了。

#include <iostream>
#define int long long
using namespace std;
int cnt[2005], siz[2005];
int n, tmp, tot, ans;
int f[15][2005][105];
void dfs(int now, int sum, int node){ //预处理出每一个状态
    if(node >= n){ //如果已经处理完毕(注意是大于等于)
        cnt[++tot] = now;
        siz[tot] = sum;
        return; //新建一个状态
    }
    dfs(now, sum, node + 1); //不用第 node 个
    dfs(now + (1 << node), sum + 1, node + 2); //用第 node 个,此时 node 要加 2,及跳过下一个格子
}
signed main(){
    cin >> n >> tmp;
    dfs(0, 0, 0);
    for(int i = 1 ; i <= tot ; i ++)f[1][i][siz[i]] = 1; //第一层的所有状态均是有1种情况的
    for(int i = 2 ; i <= n ; i ++)
        for(int j = 1 ; j <= tot ; j ++)
            for(int k = 1 ; k <= tot ; k ++){ // 分别枚举 i, j, k
                if(cnt[j] & cnt[k])continue;
                if((cnt[j] << 1) & cnt[k])continue;
                if(cnt[j] & (cnt[k] << 1))continue; // 排除一些不合法的情况
                for(int l = tmp ; l >= siz[j] ; l --)
                    f[i][j][l] += f[i - 1][k][l - siz[j]];
            }
    for(int i = 1 ; i <= tot ; i ++)ans += f[n][i][tmp]; // 统计最终答案
    cout << ans << endl;return 0;
}
posted @ 2023-10-05 19:40  tsqtsqtsq  阅读(10)  评论(0编辑  收藏  举报  来源