零、状态压缩DP
状态压缩:通常用一个二进制数表示,用每一位上的数值代表对应物品的选择(放置/经过/…)情况。
状态压缩DP:与状态机模型相似,当第 i i i 行的状态无法分解时,状态机模型是根据一个时序分解,状态压缩DP是用一个二进制数表示当前状态。
分类:
- 棋盘式 (基于联通性):通常是在棋盘/方格中摆放物品,且一个物品对周围其他物品的摆放有限制,如题目一至三
- 集合式:维护某些元素是否在集合当中,如求最短汉密尔顿路径时维护当前已经经过哪些点,又如题目四
一、小国王
一个国王只能攻击到相邻的八个格子,不会跨过一行去攻击,因此某一层的摆放方式仅与上一层的摆放方式有关,与上一层之前的摆放方式无关。容易想到DP状态中用状态压缩表示上一层的摆放方式。
闫氏DP分析法:
- 状态表示: f [ i , j , s ] f[i,j,s] f[i,j,s]
(1) 集合:所有只摆了前 i i i 行,共摆了 j j j 个国王,且第 i i i 行摆放方式是 s s s 的方案的集合
(2) 属性:Count - 状态计算:
先考虑国王摆放方式需要满足的条件:
(1) 一行内不能有两个相邻的国王
(2) 上下两行的国王不能相互攻击到,设两行状态分别为 a , b a,b a,b,则条件等价于 a a n d b = 0 a\ and\ b=0 a and b=0 (上下不能相互攻击到) 且 a o r b a\ or\ b a or b 后的结果中不能有两个相邻的 1 1 1 (斜对角不能攻击到)
因此有 f [ i , j , s ] = ∑ k 与 s 满足上述条件 f [ i − 1 , j − c n t ( k ) , k ] f[i,j,s]=\sum\limits_{k与s满足上述条件}f[i-1,j-cnt(k),k] f[i,j,s]=k与s满足上述条件∑f[i−1,j−cnt(k),k],其中 c n t ( k ) cnt(k) cnt(k) 代表二进制数 k k k 中 1 1 1 的个数。 - 初始化: f [ 0 , 0 , 0 ] = 1 f[0,0,0]=1 f[0,0,0]=1
- 最终答案: ∑ s 满足条件 ( 1 ) f [ n , m , s ] \sum\limits_{s满足条件(1)}f[n,m,s] s满足条件(1)∑f[n,m,s],一个更简便的方法是在循环行数时循环至 n + 1 n+1 n+1,最终答案为 f [ n + 1 , m , 0 ] f[n+1,m,0] f[n+1,m,0] (即在前 n + 1 n+1 n+1 行共放了 m m m 个国王,且第 n + 1 n+1 n+1 行什么也不放的方案数)
在DP之前,先预处理出所有合法状态,再预处理出两个状态之间的合法转移,便于之后DP中的枚举。
代码实现:
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 12, M = 1 << N;
typedef long long ll;
int n, m;
ll f[N][N * N][M];
vector <int> state;
int cnt[M];
vector <int> head[M];
bool check(int state){
//判断一个状态是否合法
for (int i = 0; i < n; i ++)
if ((state >> i & 1) & (state >> i + 1 & 1)) //如果有连续两个1,不合法
return 0;
return 1;
}
int count(int state){
//计算一个状态中1的个数
int res = 0;
for (int i = 0; i < n; i ++)
if (state >> i & 1) res ++;
return res;
}
int main(){
scanf("%d %d", &n, &m);
for (int i = 0; i < 1