【状压dp】方格填充问题

题目描述

对于一个行数为n,列数为m的方格网盘,用若干个2行1列的长方形去填充它,问一共有多少种方案?

输入描述

第一行为 n n n ( 1 ≤ n 1\leq n 1n ≤ 11 \leq 11 11), m m m ( 1 ≤ m 1\leq m 1m ≤ 11 \leq 11 11)

输出描述

输出方案数

样例

input
2 3
output
3


分析

本题采用的方法为状压dp,用0和1来表示网格中小矩形的放置情况。

小矩形有两种放置情况:横着放和竖着放。如果是是竖放,那么把上一格记为1,下一格记为0;如果是横放,那么把两个方格都记为0,如下所示:

在这里插入图片描述
在这里插入图片描述
于是,对于某一种放置方法,可以用 n ∗ m n * m nm 大小的唯一的 0 - 1 序列来表示,例如如下放置情况与其对应的序列为:
在这里插入图片描述
很自然可以想到,对于 n n n 行格子,枚举每一行格子的 0 - 1 序列,然后筛选出其中合法的序列,便可以统计出合法的情况了。

考虑到小矩形为2行1列的,当前行的放置是否合法,只与上一行的放置有关。即,如果让一行有1,那么该1的下面不能再放1,只能放0。这符合动态规划的“后无效性”原则,于是我们考虑使用动态规划来解这道题。

根据上述,我们提炼出了判断当前放置合法的两种情况:
①对于横放的矩形,连续0的数目必须是偶数;
②对于竖放的矩形,上一行如果是1,那么该1的下面只能是0;上一行如果是0,那么该0的下面只能是1。换句话说,相邻的上下两个不能都是1。

对于 n n n m m m 列的网格,第 i i i 行有 m m m 个格子,也就是有 2 m 2^m 2m 种状态,用 0 ~ 2m-1 来表示这些状态,例如对于3列的网格,每一行的状态可以有000, 001, 010, 011, 100, 101, 110, 111 八种。

根据前文可以知道,当前行的状态仅受上一行状态的影响。首先枚举第 i i i 行的所有状态,然后再枚举第 i − 1 i - 1 i1 行的所有状态,在这两行所有的组合状态中,筛选出所有可行的组合。

对于验证后可行的组合为 s t a t e i state_i statei s t a t e i − 1 state_{i-1} statei1 ,它们都是一个二进制串。例如, s t a t e i state_i statei = 111 与 s t a t e i − 1 state_{i-1} statei1 = 000 是兼容的,也可以说 s t a t e i state_{i} statei = 7 与 s t a t e i − 1 state_{i-1} statei1 = 0 是兼容的。我们用一个大小为 n × 2 m n × 2^m n×2m d p dp dp 数组来记录当前访问的第 i i i 行的状态。例如此状态下,记录 d p [ i ] [ 7 ] + = d p [ i − 1 ] [ 0 ] dp[i][7] += dp[i - 1][0] dp[i][7]+=dp[i1][0] ,它表示,当前行状态7的可放置情况等于当前放置数累加上上一行状态0的可放置情况总数。

由于动态规划是从前向后顺序更新的,所以访问第 i i i 行时,上一行所有状态的可放置数已经是确定的了。 只需要考虑当前行可行的状态,然后找到与该状态兼容的所有状态,将所有兼容的状态累加到当前状态下,便可得到当前 d p [ i ] [ j ] dp[i][j] dp[i][j] 的大小了。

题解

①判断第 i i i 行与第 i − 1 i - 1 i1 行是否兼容。

根据前文,第 i i i 行与第 i − 1 i - 1 i1 行如果兼容,需要满足两个情况,首先,第 i i i 行与第 i − 1 i - 1 i1 行上下不能有两个连续的1;其次,每一行连续0的个数必须是偶数。记当前行的状态为 j j j ,上一行的状态为 k k k ,对于第一点,需要保证: k k k & j j j == 0 0 0

以下重要:

对于第二点,需要保证: k k k | j j j 中连续0的个数是偶数,这样做位运算的原因是把竖放的矩形给排除掉,剩下的0一定是第 i i i 行中横放的0。例如, j j j = 0001, k = k = k= 0000,第 i i i 行前3个0不可能是来自于上一行或者下一行,只能来自于本行,而本行横放的0必然应该是偶数个。

于是,我们考虑把第 i i i 行与第 i − 1 i - 1 i1 行做或运算后,没有奇数个连续0的兼容情况放到数组中。例如,对于4列的网格,其所有兼容情况为:

在这里插入图片描述
只要保证第 i i i 行与第 i − 1 i - 1 i1 行没有上下连续的两个0,且其做或运算后符合上述情况中的一种,就认为它是可行的。参见如下代码:

if (legal[k | j] && ((k & j) == 0))
	dp[i][j] += dp[i - 1][k];  // 累加当前状态

而对于如何写这个判断一个二进制序列是否有连续奇数0的函数,只需要知道这样判断的原因和目的,不管怎样写起来都没有难度。我提供一个自己写的实现:

//对于m列,有[0, 2^m)个二进制序列,先把十进制数转二进制序列,再判断是否有连续奇数个0
	for (int i = 0; i < (1 << m); i++)
	{
		bool continuous_0_parity = 0;    //连续0的个数的奇偶性,0为偶,1为奇
		bool has_continuous_odd_0 = 0;   //记录是否有奇数个连续0
		legal[i] = 1;                    //暂时记录状态i是合法的
		for (int j = 0; j < m; j++)
		{
			if ((i >> j) & 1)   //出现1
			{
				if (continuous_0_parity == 1)   //有连续个级数0
					has_continuous_odd_0 |= continuous_0_parity;
				continuous_0_parity = 0;    //奇偶性置空,重新计算奇偶性
			}
			else     //出现0
				continuous_0_parity ^= 1;   //奇偶性取反
		}
		if (continuous_0_parity | has_continuous_odd_0)   //如果整段中出现了奇数个0
			legal[i] = 0;      //说明此状态是不合法的
		//否则就是合法的
	}

②遍历每一行,枚举当前行的所有状态 j j j ,对于状态 j j j 枚举上一行的所有状态 k k k ,如果 j j j k k k 是兼容的,就把 d p [ i ] [ j ] dp[i][j] dp[i][j] 进行累加。(如何判断它们合法,参见上文)

动态规划的初始值是 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0] = 1,表示第 0 0 0 行的全0状态肯定是合法的。遍历的框架大致如下:

for (int i = 1; i <= n; i++)     //遍历每一行
{
	for (int j = 0; j < (1 << m); j++)      //枚举当前行的所有状态
		for (int k = 0; k < (1 << m); k++)  //枚举之前行的所有状态
			if (/*状态 j 与状态 k 兼容*/)
			    //更新dp[i][j]的值
}

代码

本题的完整代码为:

/*
 * @Description: Mesh Filling Problem
 * @Author: Zhoujin - SDU
 * @email: 1761806916@qq.com
 * @Date: 2021-05-27 13:23:27
 * @LastEditTime: 2021-05-27 13:23:27
 * @FilePath: \week13\t4.cpp
 */
#include <iostream>
#include <algorithm>
#include <cstring>
#include <bitset>
#define ll long long 
using namespace std;
const int Max = 12;
ll dp[Max][1 << Max];
bool legal[1 << Max];
int n, m, cnt;

void show_dp()
{
	cout << "dp : " << endl;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j < (1 << m); j++)
			cout << dp[i][j] << " ";
		cout << endl;
	}
}

void solve()
{
	//对于m列,有[0, 2^m)个二进制序列,先把十进制数转二进制序列,再判断是否有连续奇数个0
	for (int i = 0; i < (1 << m); i++)
	{
		bool continuous_0_parity = 0;    //连续0的个数的奇偶性,0为偶,1为奇
		bool has_continuous_odd_0 = 0;   //记录是否有奇数个连续0
		legal[i] = 1;                    //暂时记录状态i是合法的
		for (int j = 0; j < m; j++)
		{
			if ((i >> j) & 1)   //出现1
			{
				if (continuous_0_parity == 1)   //有连续个级数0
					has_continuous_odd_0 |= continuous_0_parity;
				continuous_0_parity = 0;    //奇偶性置空,重新计算奇偶性
			}
			else     //出现0
				continuous_0_parity ^= 1;   //奇偶性取反
		}
		if (continuous_0_parity | has_continuous_odd_0)   //如果整段中出现了奇数个0
			legal[i] = 0;      //说明此状态是不合法的
		//否则就是合法的
	}

	cout << "Legal cases: ";
	for (int i = 0; i < (1 << m); i++)
		if (legal[i])
			cout << bitset<sizeof(i)>(i) << " ";
	cout << endl << endl;
	dp[0][0] = 1;   //初始状态,第0行的全0状态肯定是合法的
	for (int i = 1; i <= n; i++)     //遍历每一行
	{
		cout << "row = " << i << endl;
		for (int j = 0; j < (1 << m); j++)      //枚举当前行的所有状态
		{
			dp[i][j] = 0;
			for (int k = 0; k < (1 << m); k++)  //枚举之前行的所有状态
			{
				if (legal[k | j] && ((k & j) == 0))
				{
					cout << "State i: " << bitset<sizeof(j)>(j) << endl;
					cout << "      k: " << bitset<sizeof(k)>(k) << endl;
					cout << "update dp[" << i << "][" << j << "] "
						<< "from " << dp[i][j];
					dp[i][j] += dp[i - 1][k];
					cout << " to " << dp[i][j] << endl;
				}
			}
		}
		cout << "row state : ";
		for (int j = 0; j < (1 << m); j++)
			cout << dp[i][j] << " ";
		cout << endl;
		cout << endl;
	}

	show_dp();
}

int main()
{
	cin >> n >> m;

	solve();
	if (n == 0 || m == 0)
		cout << 0 << endl;
	else
		cout << dp[n][0] << endl;

	return 0;
}

为了便于理解,我给dp的运行状态作出了一些中间输出,一边观察代码一边查看输出很有利于题目的理解,如下:

在这里插入图片描述
最后一行是正确答案。




其实还有一种复杂度为O(1)的方案,即先口算出 11 × 11 11×11 11×11 种可能的情况,然后根据输入写上输出就行了:

#include <iostream>
#define ll long long
using namespace std;

int main()
{
	ll res0[12] = { 0 };
	ll res1[12] = { 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0 };	
	ll res2[12] = { 0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 };	
	ll res3[12] = { 0, 0, 3, 0, 11, 0, 41, 0, 153, 0, 571, 0 };	
	ll res4[12] = { 0, 1, 5, 11, 36, 95, 281, 781, 2245, 6336, 18061, 51205 };	
	ll res5[12] = { 0, 0, 8, 0, 95, 0, 1183, 0, 14824, 0, 185921, 0 };	
	ll res6[12] = { 0, 1, 13, 41, 281, 1183, 6728, 31529, 167089, 817991, 4213133, 21001799 };	
	ll res7[12] = { 0, 0, 21, 0, 781, 0, 31529, 0, 1292697, 0, 53175517, 0 };		
	ll res8[12] = { 0, 1, 34, 153, 2245, 14824, 167089, 1292697, 12988816, 108435745, 1031151241, 8940739824 };	
	ll res9[12] = { 0, 0, 55, 0, 6336, 0, 817991, 0, 108435745, 0, 14479521761, 0 };	
	ll res10[12] = { 0, 1, 89, 571, 18061, 185921, 4213133, 53175517, 1031151241, 14479521761, 258584046368, 3852472573499 };	
	ll res11[12] = { 0, 0, 144, 0, 51205, 0, 21001799, 0, 8940739824, 0, 3852472573499, 0 };	
	ll* res[12] = {  res0, res1, res2, res3, res4, res5, res6, res7, res8, res9, res10, res11 };
	
	int n, m;
	cin >> n >> m;
	cout << res[n][m];
	
	return 0;
} 

这种方案样例跑起来都是 0ms,推荐试一试!!

<think>嗯,用户想了解用Python中的动态规划DP)解决旅行商问题(TSP)的详细方法。首先,我需要回忆一下TSP的基本概念。TSP是要找到一条最短的路径,让旅行商访问所有城市并回到起点,每个城市只访问一次。这属于NP难问题,所以需要高效的算法来处理,尤其是当城市数量较多时。 DP通常用于处理态中包含集合的情况,比如访问过的城市集合。因为用二进制位来表示集合可以节省空间,例如,n个城市可以用n位的二进制数表示,每一位代表是否访问过该城市。这样的话,态可以表示为当前所在城市和已经访问过的城市集合。 接下来,我得考虑动态规划态定义。通常,dp[mask][i]表示当前已经访问过的城市集合为mask,并且最后停留在城市i时的最短路径长度。初始态应该是只访问了一个城市的情况,比如dp[1 << i][i] = 0,因为从城市i出发,还没有移动,所以距离为0。不过根据用户提供的引用[1],初始化每个景点j在只访问自己的态下的公交次数为0,即dp[j][1 << j] = 0。这里的公交次数可能对应路径长度,所以类似。 然后态转移方程应该是:对于每一个态mask和当前城市i,遍历所有未访问过的城市j,更新dp[mask | (1 << j)][j] = min(dp[mask | (1 << j)][j], dp[mask][i] + dist[i][j])。也就是说,从i到j,将j加入已访问集合,并更新最短路径。 接下来要考虑Python的实现。由于Python对于位运算的支持还不错,可以用字典或者二维数组来存储dp。不过考虑到空间问题,如果城市数量n较大,二维数组可能会占用太多内存。但TSP通常处理的是n较小的情况(比如n=15左右),所以可能还是可行的。 需要处理的问题包括如何遍历所有可能的mask,以及如何高效地进行态转移。另外,要注意mask的位数,比如n个城市,mask的范围是0到2^n -1。对于每个mask,需要检查哪些城市已经被访问过,然后找到下一个未访问的城市。 还需要给出具体的代码示例。比如,初始化一个二维数组dp,大小为(2^n, n),初始化为无穷大,然后设置初始态,接着遍历所有mask,对于每个mask中的当前城市i,找到未访问的城市j,进行态转移。 另外,用户提供的引用[2]提到了分支限界法使用优先队列来减少搜索空间,可能和这里的DP不同,但可以提一下其他方法作为对比。引用[3]中的回溯法框架可能也相关,但DP更高效。 最后,需要考虑路径的起点和终点是否需要回到起点。在TSP中,通常要求回到起点,所以在计算完所有城市被访问的态后,还需要加上从最后一个城市回到起点的距离。或者,在代码实现中,最后一步需要处理这一步。 举个例子,假设有4个城市,距离矩阵给出各个城市之间的距离。初始化时,dp[0001][0] = 0,dp[0010][1] = 0,依此类推。然后对于每个mask,比如0001,当前城市0,那么遍历其他城市1、2、3,将mask更新为0011、0101、1001,对应的dp值加上距离。 在Python中,可以用一个二维列表来存储dp,例如dp = [[inf] * n for _ in range(1 << n)]。然后循环mask从1到(1 << n) -1,对于每个mask,检查每一位是否为1,找到当前城市i,然后遍历所有可能的j不在mask中的情况。 最后,得到所有城市都被访问过的mask(全1),然后找出最小的dp值,再加上从最后城市回到起点的距离,或者如果在态转移中已经考虑了回到起点的情况,那就可以直接取最小值。 可能的错误点:初始化是否正确,mask的遍历顺序是否正确(应该从小到大),以及如何处理最后回到起点的问题。需要仔细检查这些步骤。 然后,编写代码示例,包括距离矩阵的定义,dp数组的初始化,态转移的循环,以及最后结果的提取。同时,可能需要测试一个简单案例,比如3个城市,手动计算验证是否正确。 此外,用户可能需要了解时间复杂度和空间复杂度,以及优化方法,比如使用位运算加速,或者空间优化(例如使用滚动数组),但可能对于Python来说,空间优化不太容易,因为n超过20的话,2^20已经是百万级别,可能会占用较大内存。 总结来说,步骤是:问题分析,态定义,转移方程,初始化,遍历顺序,代码实现,结果处理,复杂度分析,以及可能的优化。</think>### PythonDP解决旅行商问题(TSP)详解 #### 1. 问题分析 旅行商问题(TSP)要求在$n$个城市中找到一条最短环路,访问每个城市恰好一次并返回起点。动态规划通过二进制表示已访问城市集合,将时间复杂度优化到$O(n^2 \cdot 2^n)$[^2]。 #### 2. 态定义 定义二维数组$dp[mask][i]$: - $mask$:二进制数,第$j$位为1表示已访问城市$j$ - $i$:当前所在城市 - 值:从起点出发,经过$mask$对应城市后到达$i$的最小路径长度 #### 3. 态转移方程 $$dp[mask | (1 << j)][j] = \min\left(dp[mask | (1 << j)][j],\ dp[mask][i] + dist[i][j]\right)$$ 其中$j$是未访问城市(即$mask$的第$j$位为0) #### 4. 实现步骤 ```python import sys def tsp(dist): n = len(dist) total_mask = 1 << n INF = sys.maxsize # 初始化dp表:dp[mask][i] dp = [[INF] * n for _ in range(total_mask)] for i in range(n): dp[1 << i][i] = 0 # 初始态:只访问过城市i # 遍历所有态 for mask in range(total_mask): for i in range(n): if not (mask & (1 << i)): # 当前城市必须已访问 continue for j in range(n): if mask & (1 << j): # 目标城市必须未访问 continue new_mask = mask | (1 << j) dp[new_mask][j] = min(dp[new_mask][j], dp[mask][i] + dist[i][j]) # 最终结果需返回起点(假设起点是0) final_mask = (1 << n) - 1 return min(dp[final_mask][i] + dist[i][0] for i in range(n)) # 示例距离矩阵(4个城市) dist = [ [0, 10, 15, 20], [10, 0, 35, 25], [15, 35, 0, 30], [20, 25, 30, 0] ] print(tsp(dist)) # 输出:80 (0->1->3->2->0) ``` #### 5. 关键点说明 1. **态初始化**:每个城市作为起点时路径长度为0[^1] 2. **掩码遍历顺序**:从小到大确保态依赖已计算 3. **路径闭合处理**:最终结果需加上返回起点的距离 4. **空间优化**:可使用滚动数组减少内存占用 #### 6. 复杂度分析 - 时间复杂度:$O(n^2 \cdot 2^n)$ - 空间复杂度:$O(n \cdot 2^n)$
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值