1064 小国王(状态压缩dp)

1. 问题描述:

在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的8个格子,求使它们无法互相攻击的方案总数。

输入格式

共一行,包含两个整数 n 和 k。

输出格式

共一行,表示方案总数,若不能够放置则输出0。

数据范围

1 ≤ n ≤ 10,
0 ≤ k ≤ n ^ 2

输入样例:

3 2

输出样例:

16
来源:https://www.acwing.com/problem/content/description/1066/

2. 思路分析:

分析题目可以知道最简单的方法是暴力枚举,枚举每一个位置放还是不放国王,但是这个矩阵为n * n(n最大是10)而且矩阵中放置的国王数量又有限制所以直接暴力枚举肯定会超时,除了使用暴力枚举的方法我们还可以使用状态压缩dp的方法,当前这一行能够摆放国王的状态完全取决于上一行摆放国王的状态,所以我们每一次考虑当前状态的时候只需要考虑上一层状态对当前状态的影响即可,我们的策略是一行一行摆放国王,依次枚举所有合法的状态,判断当前的状态可以由哪些合法的状态转移过来,在这些状态对应的dp数组位置累加上上一个转移过来的状态的方案数目即可。由题可知存在三个动态变化的参数,分别是当前是第几行,当前已经摆放的国王数量,当前这一行的状态,所以需要定义一个三维数组dp,其中dp[i][j][s]表示前i行已经摆放好了,并且总共放了j个国王,当前第i行的状态为s的方案数目,怎么样进行状态的计算呢?状态计算对应集合的划分,一般是找最后一个不同点,我们可以将当前的f[i][j][s]划分为若干个集合,可以发现当前的状态s只与上一个状态有关,所以我们在集合划分的时候枚举上一个合法的状态即可,也即枚举上一个可以转移到当前状态的合法状态。状态压缩一般都需要预处理,与291题蒙德里安的梦想预处理思路是类似的,其实可以发现大部分的状态压缩都需要两个预处理的步骤,第一个预处理是先记录下当前满足某个条件的合法状态,第二个预处理是记录当前状态可以由哪些状态转移过来(一般是当前这一行与上一行的关系或者是当前这一列与上一列的关系),一般需要根据第一个预处理得到的记录数组记录下当前状态可以由哪些状态转移过来。对于这道题目来说我们需要满足两个条件:

  • 当前这一行不能够有两个连续的1
  • 相邻两行不能够相互攻击(当前两个状态或运算的结果不能存在两个连续的1)

经过两个预处理之后我们就可以进行状态计算了,可以使用四层循环枚举,第一层循环枚举当前是第i行,第二层循环枚举总共放的国王数量,第三层循环枚举当前这一行可能的状态,第四层循环枚举当前状态的上一个合法的状态,也即枚举上一个可以到达当前状态的合法状态,状态计算的时候当前的状态值只需要加上上一个合法状态的方案数目即可,这里可以使用一个小技巧是我们在状态定义的时候第一维可以定义多一个长度,这样我们在状态计算的时候可以计算到第i + 1行,最终第i + 1行放置好了而且总共放了k个国王并且当前这一行的状态为0的方案数目,第n + 1行状态为0说明没有放置国王,所以最终的答案为dp[n + 1][k][0],这样就可以省略最终还需要枚举答案的步骤了。

3. 代码如下:

class Solution:
    # count方法计算sta中1的数目
    def count(self, sta: int, k: int):
        res = 0
        for i in range(k):
            res += sta >> i & 1
        return res
    
    # 检验当前的sta状态是否存在两个连续的1, 存在两个连续的1是不合法的
    def check(self, sta: int, k: int):
        for i in range(k):
            # sta >> i表示sta右移i位之后最后一位数字
            if (sta >> i & 1) and (sta >> i + 1 & 1): return False
        return True

    def process(self):
        # n为n * n的棋盘, k为最多可以放置的国王数量
        n, k = map(int, input().split())
        # 可以通过两个预处理, 第一个预处理是先记录没有连续的两个1的合法状态, 第二个预处理是记录当前状态可以由上一个的哪些状态转移过来
        st = list()
        # 预处理一
        for i in range(1 << n):
            if self.check(i, n):
                st.append(i)
        # 预处理二, 记录一下当前状态可以由哪些合法状态转移过来
        state = [list() for i in range(1 << n)]
        for i in range(len(st)):
            for j in range(len(st)):
                a, b = st[i], st[j]
                # 当前状态为a, 上一个状态为b, 两个状态分别对应这两行的情况, 两行中的同一列不能够是1, 两行不能够互相攻击, 对应下面的判断代码
                if a & b == 0 and self.check(a | b, n):
                    state[a].append(b)
        # 这里存在一个技巧是第一维定义为n + 2, 这样状态计算的时候计算到n + 1, 可以省略最后还需要循环枚举答案的步骤
        dp = [[[0] * (1 << n) for i in range(k + 1)] for j in range(n + 2)]
        dp[0][0][0] = 1
        for i in range(1, n + 2):
            for j in range(k + 1):
                for a in st:
                    for b in state[a]:
                        c = self.count(a, n)
                        # 当前放置的国王数量必须小于总的放置的国王数量
                        if j - c >= 0:
                            dp[i][j][a] += dp[i - 1][j - c][b]
        # 前n + 1行已经放好了已经放了k个国王n + 1行状态为0的方案数目
        return dp[n + 1][k][0]


if __name__ == '__main__':
    print(Solution().process())
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值