前言
状态压缩就是用某个方法,以最小代价来表示某种状态,通常是用二进制数来表示各个点的状态。
有些问题,若用常规dp,由于枚举的状态太多,会TLE,就需要使用状态压缩!
题目
①国际象棋
https://www.luogu.com.cn/problem/P8756

状态:1010101010 (第i个1代表当前行第i列放了马)
本题将行与列导致,并不影响结果,但极大的减小了每一行枚举的状态数!
dp[i][j][a][x]
表示前i行放了j只羊,第i - 1行的状态为a,第i行的状态为x的所有方案数
遍历第i - 2行的状态b,状态转移方程为:
dp[0][0][0][0] = 1
dp[i][j][a][x] = dp[i][j][a][x] + dp[i - 1][j - count(x)][b][a]
n,m,k = map(int,input().split())
MOD = 1000000007
dp = [[[[0 for _ in range(1 << n)]for _ in range(1 << n)]for _ in range(k + 1)]for _ in range(m + 1)]
# 初 始 值 设 为 1,为 之 后 的 递 推 打 下 基 础!
dp[0][0][0][0] = 1
# 计 算 x 有 多 少 个 1,也 就 是 这 个 状 态 包 含 了 多 少 只 羊!
def count(x):
res = 0
for i in range(n):
res += (x >> i) & 1
return res
# dp[i][j][a][x]
# 表 示 前 i 行 放 了 j 匹 马 而 且 第 i - 1 行 的 状 态 为 a,第 i 行 的 状 态 为 x
for i in range(1,m + 1):
for j in range(k + 1):
for a in range(1 << n):
for x in range(1 << n):
# 判 断 第 i - 1 行 与 第 i 行 的 马 的 状 态 是 否 有 冲 突
if (x >> 2) & a != 0 or (x << 2) & a != 0:
continue
# 遍 历 第 i - 2 行 的 状 态
for b in range(1 << n):
# 判 断 第 i - 2 行 与 第 i 行 的 马 的 状 态 是 否 有 冲 突
if (x >> 1) & b != 0 or (x << 1) & b != 0:
continue
# 判 断 第 i - 2 行 与 第 i - 1 行 的 马 的 状 态 是 否 有 冲 突
if (a >> 2) & b != 0 or (a << 2) & b != 0:
continue
# 第 i 行 的 状 态 由 第 i - 1 行 的 状 态 推 出
if j >= count(x):
dp[i][j][a][x] = (dp[i][j][a][x] + dp[i - 1][j - count(x)][b][a]) % MOD
ans = 0
for a in range(1 << n):
for x in range(1 << n):
ans = (ans + dp[m][k][a][x]) % MOD
print(ans)
②回路计数

状态:1010101010 (第i个1代表已经经过了i号楼)
dp[i][j]
表示当前状态为i,且最后到达j号楼的路径数
那么有dp[i + 1 << k][k] += dp[i][j]
前提是状态不包含k号楼,且j号楼与k号楼之间有路径!
import math
dp = [[0] * (22) for i in range(2100000)]
g = [[0 for i in range(22)] for i in range(22)]
n = 1 << 21
for i in range(1, 22):
for j in range(i, 22):
if math.gcd(i, j) == 1:
g[i - 1][j - 1] = g[j - 1][i - 1] = 1
dp[1][0] = 1
i = 1
# 遍 历 所 有 的 状 态 (一 共 有 1 >> 21 个 状 态)!
while i < n:
# 遍 历 每 个 状 态 最 终 到 达 的 楼!
for j in range(0, 21):
# 如 果 i 状 态 不 包 含 j 号 楼,则 跳 过 !
if (i >> j & 1) == 0:
continue
# 遍 历 所 有 i 状 态 , 最 后 到 达 j,并 且 之 后 能 到 达 的 k 状 态 !
for k in range(0, 21):
# j 能 到 达 k ,且 i 状 态 不 包 含 k!
if g[j][k] == 0 or (i >> k & 1) != 0:
continue
dp[(i + (1 << k))][k] += dp[i][j]
i += 1
res = 0
for i in range(0, 21):
res += dp[n - 1][i]
print(res)
③划分为k个相等的子集
记忆化搜索 递归遍历所有可行状态!
https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/

状态:1010101010 (第i个1代表已经经过选了第i个数)
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
s = sum(nums)
if s % k:
return False
d = s // k
# 方 便 下 面 剪 枝
nums.sort()
if nums[-1] > d:
return False
n = len(nums)
# 该题一共有2 ^ n - 1 个 状 态 1 1 1 1 1 1 (n 个 1)
@cache
def dfs(s, p):
# 递 归 出 口,表 示 全 都 已 经 选 了;
# 如 果 能 到 这 一 步 ,则 说 明 符 合 题 意
# 否 则,会 因 为 p + nums[i] break,结 果 为 false
if s == (1 << n) - 1:
return True
for i in range(n):
# 减 枝
if nums[i] + p > d:
break
# 如 果 没 有 选 择 了 第 i 个 数! s >> i & 1 == 0
# p + nums[i] 等 于 d 时 置 为 0
# s ^ (1 << i) 把 s 中 的 第 i 的 0 变 成 1,代表已经选了
if s >> i & 1 == 0 and dfs(s ^ (1 << i), (p + nums[i]) % d):
return True
return False
return dfs(0, 0)
④优美的排列
https://leetcode.cn/problems/beautiful-arrangement/

状态:1010101010 (第i个1代表已经经过选了第i个数,并且下一个要选的数的下标为i+1)
class Solution:
def countArrangement(self, n: int) -> int:
dp = [0] * (1 << n)
dp[0] = 1
for mask in range(1,1 << n):
index = bin(mask).count('1')
for i in range(n):
if mask & (1 << i) and (index % (i + 1) == 0 or (i + 1) % index == 0):
dp[mask] += dp[mask ^ (1 << i)]
return dp[(1 << n) - 1]