动态规划(四):状态压缩


动态规划:状态压缩 (蒙德里安的梦想、最短Hamilton路径)

状态压缩DP

AcWing 291.蒙德里安的梦想

求把 \(N* M\) 的棋盘分割成若干个 \(1*2\) 的的长方形,有多少种方案。 例如当 \(N=2,M=4\) 时,共有\(5\) 种方案。当 \(N=2,M=3\) 时,共有3种方案。 如下图所示:

输入格式
输入包含多组测试用例。 每组测试用例占一行,包含两个整数 \(N\)\(M\)。 当输入用例 \(N=0,M=0\)时,表示输入终止,且该用例无需处理。

输出格式
每个测试用例输出一个结果,每个结果占一行。

数据范围
\(1≤N,M≤11\)

输入样例:

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

输出样例:

1
0
1
2
3
5
144
51205

Code:
\(f[i][j]\)表示第i列按状态 \(j\) 摆放时,从 \(i-1\) 列(状态 \(k\) )转移过来的方案数
第二维的状态中 \(0\) 表示竖着放,\(1\) 表示横着放
\(i-1\) 列状态是否能转移到第 \(i\) 列的条件:
①第 \(i\) 列状态 \(j\) 和第 \(i-1\) 列状态 \(k\) 不能冲突 \((j \& k) == 0\)
②第 \(i\) 列状态 \(j\) 确定后,第 \(i-1\) 列状态k的连续空白格子的长度必须是偶数
\(st[ j | k] = true\).

初始状态: \(f[0][0] = 1\), 其余全为 \(0\);第 \(0\) 列只能是全 \(0\) 的状态
最终状态: \(f[m][0]\) , 棋盘有 \(0 \sim (m-1)\) 列,但后面一列会影响前一列,多出的一列必须是全0.

#pragma once
#include 
#include 

using namespace std;

const int N = 12, M = 1 << N;

long long f[N][M];
/*
f[i][j]表示第i列按状态j摆放时,从i-1列(状态k)转移过来的方案数
第二维的状态中0表示竖着放,1表示横着放
第i-1列状态是否能转移到第i列的条件:
①第i列状态j和第i-1列状态k不能冲突 (j & k) == 0;
②第i列状态j确定后,第i-1列状态k的连续空白格子的长度必须是偶数 st[ j | k] = true.

初始状态:f[0][0] = 1, 其余全为0;第0列只能是全0的状态
最终状态:f[m][0], 棋盘有0~(m-1)列,但后面一列会影响前一列,多出的一列必须是全0.
*/

bool st[M]; // 判断是否存在连续的偶数个0

void modriann_dream()
{
	int n, m;
	// 输入n, m, 当n和m不同时为零的时候, 都可以进入
	while (cin >> n >> m, n || m)
	{
		for (int i = 0; i < 1 << n; ++i)
		{
			int cnt = 0; //开始连续的零的个数为0个
			st[i] = true; // 假设第i为为true,表示存在连续的偶数个零
			for (int j = 0; j < n; ++j)
				if (i >> j & 1) //去除的数i第j为数字为1
				{
					if (cnt & 1) // 如果cnt为奇数,不满足转移条件
						st[i] = false;
					cnt = 0;
				}
				else cnt++;

			if (cnt & 1) st[i] = false; //判断最后一段连续的零是否是偶数个
		}

		memset(f, 0, sizeof f);
		f[0][0] = 1;
		for (int i = 1; i <= m; ++i)
			for (int j = 0; j < 1 << n; ++j)
				for (int k = 0; k < 1 << n; ++k)
					if ((j & k) == 0 && st[j | k]) //如果满足状态转移条件,不冲突而且有连续的偶数个零
						f[i][j] += f[i - 1][k];

		cout << f[m][0] << endl; // 最后的答案是最后一列的前一列伸出来的小方块数量为0
	}
}

AcWing 91. 最短Hamilton路径

给定一张 \(n\) 个点的带权无向图,点从 \(0\sim n-1\) 标号,求起点 \(0\) 到终点 \(n-1\) 的最短Hamilton路径。 Hamilton路径的定义是从 \(0\)\(n-1\) 不重不漏地经过每个点恰好一次

输入格式
第一行输入整数n。 接下来n行每行n个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为a[i,j])。 对于任意的x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]>=a[x,z]。

输出格式
输出一个整数,表示最短Hamilton路径的长度。

数据范围
\(1≤n≤20 0≤a[i,j]≤10^7\)

输入样例:

5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0

输出样例:

18

Code:
\(f[i, j]\)表示从起点 \(0\) 到第 \(j\) 点,经过点为 \(i\) 的所有路径的距离最小值。 \(i\) 表示二进制压缩状态,存储经过哪些节点的信息, \(j\) 表示从第 \(0\) 点走到第 \(j\) 点。
初始状态为 \(f[0][j] = +\infty, f[1][0] = 0\);
最终问题解为 \(f[“11…1”, n - 1]\)
\(f[2^n-1][n -1]\).
状态转移:将压缩状态为 \(i\) 的路径 \(0 \sim j\) 分为 \(0→k\) (不包括 \(k\) )和 \(k→j\)\(f[i][j] = min(f[[i - (1 << j)][k] +w[k][j]); (\ "i - (1 << j)"确保该状态第j位为0,不经过j)\)

#include 
#include 
#include 

using namespace std;

const int N = 20, M = 1 << N;

int n;
int w[N][N];
int f[M][N];

int main()
{
    cin >> n;
    
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            cin >> w[i][j];
            
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0; //表示从第一个点到第一个点得距离是0
    
    for (int i = 1; i < 1 << n; ++i) // i=1,表示从第一个点开始,i得每一位是1代表一个状态
        for (int j = 0; j < n; ++j) // 遍历所有得点
            if (i >> j & 1) // 到第j个点是不是可行得
                for (int k = 0; k < n; ++k) // 可行得话,看看它得上一个点k是谁
                    if (i >> k & 1) // 确保i能走到第k个点,k可以到j
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]); //比较所有可行得第k个点,选出来最小得距离
                        
    cout << f[(1 << n) - 1][n - 1] << endl; //最终得答案是走过所有得n个点,然后到达第n个点,也就是j = n - 1
    return 0;
}