DP - 状压DP - 蒙德里安的梦想 + 最短Hamilton路径

DP - 状压DP - 蒙德里安的梦想 + 最短Hamilton路径

1、蒙德里安的梦想

求把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

分析:

由 于 分 割 的 长 方 形 都 是 1 × 2 的 , 且 在 矩 形 方 格 中 仅 有 横 放 与 竖 放 两 种 情 况 , 因 此 横 放 的 情 况 确 定 后 , 竖 放 的 情 况 也 就 确 定 了 ( 将 剩 下 的 空 格 放 满 ) 。 因 此 , 只 需 要 计 算 合 法 的 横 着 摆 放 的 方 案 总 数 即 可 。 由于分割的长方形都是1×2的,且在矩形方格中仅有横放与竖放两种情况,因此横放的情况确定后,\\竖放的情况也就确定了(将剩下的空格放满)。\\因此,只需要计算合法的横着摆放的方案总数即可。 1×2()

我 们 用 一 个 二 进 制 数 来 表 示 每 一 列 的 状 态 。 我们用一个二进制数来表示每一列的状态。

先 考 虑 横 放 的 情 况 : 若 第 i 列 的 某 一 行 被 i − 1 列 的 横 着 放 的 长 方 形 占 据 ( 就 是 说 该 长 方 形 占 了 i − 1 列 到 i 列 ) , 那 么 这 一 行 对 应 的 二 进 制 数 为 1 。 先考虑横放的情况:\\若第i列的某一行被i-1列的横着放的长方形占据(就是说该长方形占了i-1列到i列),那么这一行对应的二进制数为1。 ii1(i1i)1

如 下 图 , 第 i 列 对 应 的 状 态 j 表 示 成 的 二 进 制 数 就 是 0100 。 如下图,第i列对应的状态j表示成的二进制数就是0100。 ij0100

在这里插入图片描述

那 么 在 转 移 的 过 程 中 有 两 点 限 制 : 那么在转移的过程中有两点限制:

① 、 横 放 时 : 第 i 列 与 第 i − 1 列 不 能 存 在 某 一 行 横 放 时 出 现 重 叠 ( 前 一 块 砖 的 后 一 半 与 后 一 块 转 的 前 一 半 重 叠 ) , 对 应 到 二 进 制 状 态 上 , 就 是 第 i 列 的 状 态 j 和 第 i − 1 列 的 状 态 k 相 与 应 当 为 0 , 即 j & k = 0 。 ①、横放时:第i列与第i-1列不能存在某一行横放时出现重叠(前一块砖的后一半与后一块转的前一半重叠),\\\qquad对应到二进制状态上,就是第i列的状态j和第i-1列的状态k相与应当为0,即j\&k=0。 ii1()iji1k0j&k=0

② 、 竖 放 时 : 为 了 填 满 剩 余 的 空 格 , 那 么 横 放 时 就 要 避 免 剩 余 的 空 格 中 出 现 连 续 的 奇 数 个 空 格 , 对 应 到 二 进 制 状 态 上 , 就 是 第 i 列 的 状 态 j 和 第 i − 1 列 的 状 态 k 相 或 , 即 j ∣ k , 看 二 进 制 数 j ∣ k 是 否 存 在 奇 数 个 0 。 ②、竖放时:为了填满剩余的空格,那么横放时就要避免剩余的空格中出现连续的奇数个空格,\\\qquad对应到二进制状态上,就是第i列的状态j和第i-1列的状态k相或,即j|k,看二进制数j|k是否存在奇数个0。 iji1kjkjk0

这 里 来 解 释 一 下 j ∣ k , 因 为 j 表 示 的 是 占 据 i − 1 列 和 i 列 的 长 方 形 , 若 状 态 j 在 某 一 行 对 应 的 二 进 制 数 1 , 则 i − 1 列 对 应 的 行 就 不 是 空 格 , 同 样 的 , 因 为 k 表 示 的 是 占 据 i − 2 列 和 i − 1 列 的 长 方 形 , 若 状 态 k 在 该 行 对 应 的 二 进 制 数 为 1 , 第 i − 1 列 对 应 的 行 也 不 是 空 格 。 \qquad这里来解释一下j|k,因为j表示的是占据i-1列和i列的长方形,若状态j在某一行对应的二进制数1,\\则i-1列对应的行就不是空格,\\\qquad同样的,因为k表示的是占据i-2列和i-1列的长方形,若状态k在该行对应的二进制数为1,\\第i-1列对应的行也不是空格。 jkji1ij1i1ki2i1k1i1
因 此 , 只 要 状 态 j 与 k 在 某 一 行 有 一 个 状 态 对 应 的 二 进 制 数 为 1 , 则 i − 1 列 对 应 的 这 一 行 就 不 是 空 格 。 \qquad因此,只要状态j与k在某一行有一个状态对应的二进制数为1,则i-1列对应的这一行就不是空格。 jk1i1

只 有 满 足 以 上 两 个 条 件 时 , 才 能 从 第 i − 1 列 的 状 态 转 移 到 第 i 列 的 状 态 上 来 。 只有满足以上两个条件时,才能从第i-1列的状态转移到第i列的状态上来。 i1i

状 态 表 示 , f [ i ] [ j ] : 第 i 列 且 二 进 制 状 态 是 j 的 方 案 总 数 。 状态表示,f[i][j]:第i列且二进制状态是j的方案总数。 f[i][j]:ij

状 态 计 算 : 若 第 i 列 的 状 态 j 与 第 i − 1 列 的 状 态 k 满 足 以 上 两 个 条 件 , 则 f [ i ] [ j ] + = f [ i − 1 ] [ k ] 。 状态计算:若第i列的状态j与第i-1列的状态k满足以上两个条件,则f[i][j]+=f[i-1][k]。 iji1kf[i][j]+=f[i1][k]

具体落实:

① 、 预 处 理 标 记 一 下 所 有 存 在 连 续 奇 数 个 0 的 状 态 , 提 高 效 率 。 ①、预处理标记一下所有存在连续奇数个0的状态,提高效率。 0

② 、 边 界 上 : 第 一 列 没 有 前 一 列 , 默 认 只 有 一 种 方 案 ( 全 空 , 二 进 制 状 态 即 全 0 ) 。 同 样 的 , 最 后 一 列 没 有 后 一 列 , 默 认 只 有 一 种 方 案 ( 全 空 , 二 进 制 状 态 全 0 ) 。 那 么 最 终 答 案 就 是 f [ m ] [ 0 ] 。 ②、边界上:第一列没有前一列,默认只有一种方案(全空,二进制状态即全0)。\\\qquad 同样的,最后一列没有后一列,默认只有一种方案(全空,二进制状态全0)。\\\qquad 那么最终答案就是f[m][0]。 (0)(0)f[m][0]

③ 、 一 共 有 n 行 , 每 一 种 状 态 就 是 n 位 二 进 制 数 , 状 态 表 示 的 范 围 就 是 [ 0 , 2 n − 1 ] , 即 [ 0 , ( 1 < < n ) − 1 ] 。 ③、一共有n行,每一种状态就是n位二进制数,状态表示的范围就是[0,2^n-1],即[0,(1<<n)-1]。 nn[0,2n1][0,(1<<n)1]

④ 、 预 处 理 计 算 状 态 数 组 s t 时 , 计 算 的 是 连 续 0 的 个 数 , 一 但 某 一 位 是 1 , 那 么 c n t 要 重 新 置 0 。 对 每 一 个 装 态 统 计 完 最 后 一 位 后 都 要 最 后 再 判 断 一 下 c n t 的 数 量 是 否 为 奇 数 。 ④、预处理计算状态数组st时,计算的是连续0的个数,一但某一位是1,那么cnt要重新置0。\\\qquad 对每一个装态统计完最后一位后都要最后再判断一下cnt的数量是否为奇数。 st01cnt0cnt

⑤ 、 多 组 测 试 数 据 , f 数 组 需 要 重 置 , 但 s t 数 组 不 需 要 。 因 为 s t 数 组 计 算 的 是 二 进 制 状 态 , 与 具 体 的 数 据 无 关 。 ⑤、多组测试数据,f数组需要重置,但st数组不需要。因为st数组计算的是二进制状态,与具体的数据无关。 fstst

代码:

#include<iostream>
#include<algorithm>
#include<cstring>

#define ll long long

using namespace std;

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

int n,m;
ll f[N][M];
bool st[M];

int main()
{
    while(cin>>n>>m,n||m)
    {
        for(int i=0;i<1<<n;i++)
        {
            int cnt=0;
            st[i]=true;
            for(int j=0;j<n;j++)
                if(i>>j&1)
                {
                    if(cnt&1) 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;
    }
    
    return 0;
}

2、最短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]>=a[x,z]。

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

数据范围
1≤n≤20
0≤a[i,j]≤107

输入样例:
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

分析:

目 标 即 求 一 条 经 过 每 个 点 一 次 的 最 短 路 的 长 度 。 目标即求一条经过每个点一次的最短路的长度。

总 的 方 案 数 是 n ! , 求 路 径 长 度 是 O ( n ) 的 , 因 此 暴 力 D P 是 不 行 的 。 总的方案数是n!,求路径长度是O(n)的,因此暴力DP是不行的。 n!O(n)DP

由 于 我 们 只 需 要 保 证 经 过 了 n 个 点 , 而 无 需 考 虑 经 过 的 顺 序 。 由于我们只需要保证经过了n个点,而无需考虑经过的顺序。 n

因 此 用 一 个 二 进 制 数 来 表 示 当 前 经 过 了 哪 些 点 的 状 态 。 因此用一个二进制数来表示当前经过了哪些点的状态。

共 有 n 个 点 , 就 用 一 个 n 位 二 进 制 数 来 表 示 已 经 经 过 的 点 , 经 过 第 i 个 点 , 就 将 第 i 位 标 记 位 1 。 共有n个点,就用一个n位二进制数来表示已经经过的点,经过第i个点,就将第i位标记位1。 nnii1

状 态 表 示 , f [ i ] [ j ] : 表 示 状 态 为 i , 走 到 第 j 个 点 的 最 短 路 长 度 。 状态表示,f[i][j]:表示状态为i,走到第j个点的最短路长度。 f[i][j]:ij

状 态 计 算 : 考 虑 以 倒 数 第 二 个 点 为 划 分 依 据 , 假 设 第 j 个 点 是 由 第 k 个 点 直 接 转 移 而 来 的 。 则 有 状 态 i 的 第 j 位 为 1 , 且 状 态 i − ( 1 < < k ) 的 第 k 位 为 1 , 这 样 第 j 个 点 才 能 由 第 k 个 点 直 接 转 移 而 来 。 方 程 : f [ i ] [ j ] = m i n ( f [ i − ( 1 < < k ) ] [ k ] + w [ k ] [ j ] ) , i ∈ [ 0 , 2 n − 1 ] , j , k ∈ [ 0 , n − 1 ] 。 状态计算:考虑以倒数第二个点为划分依据,假设第j个点是由第k个点直接转移而来的。\\则有状态i的第j位为1,且状态i-(1<<k)的第k位为1,这样第j个点才能由第k个点直接转移而来。\\方程:f[i][j]=min(f[i-(1<<k)][k]+w[k][j]),i∈[0,2^n-1],j,k∈[0,n-1]。 jkij1i(1<<k)k1jkf[i][j]=min(f[i(1<<k)][k]+w[k][j])i[0,2n1]j,k[0,n1]

最 终 答 案 为 f [ ( 1 < < n ) − 1 ] [ n − 1 ] ( 经 过 n − 1 个 点 , 到 达 第 n − 1 个 点 ) 。 最终答案为f[(1<<n)-1][n-1](经过n-1个点,到达第n-1个点)。 f[(1<<n)1][n1](n1n1)

代码:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

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

int n,w[N][N],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;
    for(int i=0;i<1<<n;i++)
    for(int j=0;j<n;j++)
        if((i>>j)&1)
        for(int k=0;k<n;k++)
            if((i-(1<<j))>>k&1)
                f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
                
    cout<<f[(1<<n)-1][n-1]<<endl;
    
    return 0;
                        
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值