算法学习笔记(48)——状态压缩DP

状态压缩DP

蒙德里安的梦想

问题描述

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

输入格式

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

输出格式

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

数据范围

$ 1 \le N,M \le 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

题目分析

题目描述中给定一个 \(N \times M\) 的棋盘,让我们将其分割成许多个 \(1 \times 2\) 的长方形,问总共有多少种分割的合法方案。等价的我们可以把这个分割的过程看作是如何通过合理摆放 \(1 \times 2\) 的小长方形,使得一块 \(N \times M\) 的面积刚好被这些相同大小的小长方形填满。自然而然产生的一个想法就是从左到右放置小长方形(更宽泛地可以说是从一头到另一头单向地摆放),这些小长方形要么是横着摆放的,要么是竖着摆放的,而在一个合法的方案中(即填满 \(N \times M\)),当我们确定了所有横着放的小长方形的位置之后,剩下的空位也必然能够恰好填入所有竖着放的小长方形,由此我们可以说,合法的方案数量等价于所有合法的横着摆放的小长方形的方案数量

状态表示

  • 集合\(f[i][j]\) 表示前 \(i-1\) 列已经摆放完毕,且在第 \(i-1\) 列中横着放的小长方形伸到第 \(i\) 列的情况是 \(j\) 的所有方案的集合。

    • 这里解释一下“情况是 \(j\) ”的含义,我们用一个二进制位表示一个位置上是否已摆放小长方形,若已摆放则是 \(1\),未摆放则是 \(0\),这样每一列的摆放情况都可以根据 \(n\) 个二进制位组成的一个数 \(j\) 来表示。例如第 \(i\) 列从上至下的摆放情况是"10100",于是就可以用对应的二进制数来代表这一列的摆放情况。
  • 属性:方案数量的最大值。

状态转移

状态转移对应着集合划分,常见自然的思路是依据最后一步进行划分,这里我们的状态表示 \(f[i][j]\) 已经明确了第 \(i\) 列的摆放情况是 \(j\),那么我们就尝试根据此前的最后一步,即第 \(i-1\) 列的状态进行划分。仅仅依据第 \(i\) 列的情况,我们只能得知第 \(i-1\) 列中与右边伸出去的小长方形对应的左半部分的放置情况 \(j\),而不知道从第 \(i-2\) 列伸到第 \(i-1\) 列的情况,而这种情况可以用 \(f[i-1][k]\) 来表示,代表着前 \(i-2\) 列已摆放完毕,且从第 \(i-2\) 列伸出到 \(i-1\) 列的摆放情况是 \(k\) 的合法方案的数量,这时我们就需要判断哪些摆放方案能够使得两个过程不会产生冲突(一个是从 \(i-2\) 伸到 \(i-1\)的右半部分方案 \(k\),另一个是 \(i\) 中对应到 \(i-1\) 的左半部分方案 \(j\))。

针对这个问题,我们需要判断两点:

  1. \(k\)\(j\) 不能重叠,即左边伸过来的和右边伸过来的不能重叠。
  2. 当前列中的空位必须是偶数个,否则没办法用竖着放的方式填满剩余位置。

满足以上两点,针对每一个合法的 \(k\)\(j\) 的转移,有如下状态转移方程:

\[f[i][j] = \sum_{\text{合法的k}} f[i-1][k] \]

代码实现

具体的代码实现中,我们对所有可能的状态进行预处理,is_Valid_State[]数组表示每个方案留下的空位是否是偶数个。state[]数组保存每个状态对应的合法的上一状态(即满足前面提到的两个条件,不叠加摆放和偶数个空位)。

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

using LL = long long;

const int MAX_N = 12, MAX_ST = 1 << MAX_N;

int N, M;
LL f[MAX_N][MAX_ST];
vector<int> state[MAX_ST];
bool is_Valid_State[MAX_ST];

int main()
{
    while (cin >> N >> M, N || M) {
        // 预处理合法状态(不能有奇数个空位)
        for (int i = 0; i < 1 << N; ++ i) {
            bool is_valid = true; // 标记状态合法性
            int cnt = 0;          // 统计0的个数
            for (int j = 0; j < N; ++ j) {
                if (i >> j & 1) {
                    // 1 & 1 = 1, 2 & 1 = 0
                    if (cnt & 1) {
                        is_valid = false;
                        break;
                    }
                    cnt = 0;
                }
                else {
                    ++ cnt;
                }
            }
            // 特判最后一位是0的情况
            if (cnt & 1) is_valid = false;
            is_Valid_State[i] = is_valid;
        }
        
        // 预处理出每一个方案合法的上一状态
        for (int i = 0; i < 1 << N; ++ i) {
            state[i].clear();
            for (int j = 0; j < 1 << N; ++ j)
                // 注意:==的优先级高于&,所以加括号
                if ( (i & j) == 0 && is_Valid_State[i | j] )
                    state[i].push_back(j);
        }
        
        memset(f, 0, sizeof f);
        f[0][0] = 1; // 什么都不放是一种合法方案,其余方案初始化为0
        // 枚举每一列
        for (int i = 1; i <= M; ++ i)
            // 枚举每一个状态
            for (int j = 0; j < 1 << N; ++ j) {
                for (auto k : state[j])
                    f[i][j] += f[i - 1][k];
            }
                    
        cout << f[M][0] << endl;
    }
    
    return 0;
}

最短Hamilton路径

问题描述

给定一张 \(n\) 个点的带权无向图,点从 \(0∼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]\ge a[x,z]\)

输出格式

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

数据范围

\(1 \le n \le 20\)
\(0 \le a[i,j]\le 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

img

#include <iostream>
#include <cstring>

using namespace std;

const int MAX_N = 21, MAX_POINT = 1 << MAX_N;

int n;
int w[MAX_N][MAX_N];
int f[MAX_POINT][MAX_N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; ++ i)
        for (int j = 0; j < n; ++ j)
            cin >> w[i][j];
    
    /*
     * 初始化:
     * 初始状态经过的点只有0,所以只有0这一位是1,且当前位于点0的最短距离是0;
     * 由于求的是最短路径,所以其余状态初始化为正无穷;
     */
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;
    
    // 枚举每一种可能的路径方案i
    for (int i = 0; i < 1 << n; ++ i)
        // 枚举每一个终点j
        for (int j = 0; j < n; ++ j)
            // 如果当前枚举到的方案能够到达点j
            if (i >> j & 1)
                // 枚举每一个可能到达点j的前驱k
                for (int k = 0; k < n; ++ k)
                    // 如果点k能够到达点j
                    if ( (i ^ (1 << j)) >> k & 1)
                        // 状态更新
                        f[i][j] = min(f[i][j], f[i ^ (1 << j)][k] + w[k][j]);
    
    // 最后答案是经过了所有点,且终点是n-1号点的最短Hamilton路径的距离
    cout << f[(1 << n) - 1][n - 1] << endl;
    
    return 0;
            
}
posted @ 2022-12-18 09:48  S!no  阅读(31)  评论(0编辑  收藏  举报