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。 先考虑横放的情况:若第i列的某一行被i−1列的横着放的长方形占据(就是说该长方形占了i−1列到i列),那么这一行对应的二进制数为1。
如 下 图 , 第 i 列 对 应 的 状 态 j 表 示 成 的 二 进 制 数 就 是 0100 。 如下图,第i列对应的状态j表示成的二进制数就是0100。 如下图,第i列对应的状态j表示成的二进制数就是0100。
那 么 在 转 移 的 过 程 中 有 两 点 限 制 : 那么在转移的过程中有两点限制: 那么在转移的过程中有两点限制:
① 、 横 放 时 : 第 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。 ①、横放时:第i列与第i−1列不能存在某一行横放时出现重叠(前一块砖的后一半与后一块转的前一半重叠),对应到二进制状态上,就是第i列的状态j和第i−1列的状态k相与应当为0,即j&k=0。
② 、 竖 放 时 : 为 了 填 满 剩 余 的 空 格 , 那 么 横 放 时 就 要 避 免 剩 余 的 空 格 中 出 现 连 续 的 奇 数 个 空 格 , 对 应 到 二 进 制 状 态 上 , 就 是 第 i 列 的 状 态 j 和 第 i − 1 列 的 状 态 k 相 或 , 即 j ∣ k , 看 二 进 制 数 j ∣ k 是 否 存 在 奇 数 个 0 。 ②、竖放时:为了填满剩余的空格,那么横放时就要避免剩余的空格中出现连续的奇数个空格,\\\qquad对应到二进制状态上,就是第i列的状态j和第i-1列的状态k相或,即j|k,看二进制数j|k是否存在奇数个0。 ②、竖放时:为了填满剩余的空格,那么横放时就要避免剩余的空格中出现连续的奇数个空格,对应到二进制状态上,就是第i列的状态j和第i−1列的状态k相或,即j∣k,看二进制数j∣k是否存在奇数个0。
这
里
来
解
释
一
下
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列对应的行也不是空格。
这里来解释一下j∣k,因为j表示的是占据i−1列和i列的长方形,若状态j在某一行对应的二进制数1,则i−1列对应的行就不是空格,同样的,因为k表示的是占据i−2列和i−1列的长方形,若状态k在该行对应的二进制数为1,第i−1列对应的行也不是空格。
因
此
,
只
要
状
态
j
与
k
在
某
一
行
有
一
个
状
态
对
应
的
二
进
制
数
为
1
,
则
i
−
1
列
对
应
的
这
一
行
就
不
是
空
格
。
\qquad因此,只要状态j与k在某一行有一个状态对应的二进制数为1,则i-1列对应的这一行就不是空格。
因此,只要状态j与k在某一行有一个状态对应的二进制数为1,则i−1列对应的这一行就不是空格。
只 有 满 足 以 上 两 个 条 件 时 , 才 能 从 第 i − 1 列 的 状 态 转 移 到 第 i 列 的 状 态 上 来 。 只有满足以上两个条件时,才能从第i-1列的状态转移到第i列的状态上来。 只有满足以上两个条件时,才能从第i−1列的状态转移到第i列的状态上来。
状 态 表 示 , f [ i ] [ j ] : 第 i 列 且 二 进 制 状 态 是 j 的 方 案 总 数 。 状态表示,f[i][j]:第i列且二进制状态是j的方案总数。 状态表示,f[i][j]:第i列且二进制状态是j的方案总数。
状 态 计 算 : 若 第 i 列 的 状 态 j 与 第 i − 1 列 的 状 态 k 满 足 以 上 两 个 条 件 , 则 f [ i ] [ j ] + = f [ i − 1 ] [ k ] 。 状态计算:若第i列的状态j与第i-1列的状态k满足以上两个条件,则f[i][j]+=f[i-1][k]。 状态计算:若第i列的状态j与第i−1列的状态k满足以上两个条件,则f[i][j]+=f[i−1][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]。 ③、一共有n行,每一种状态就是n位二进制数,状态表示的范围就是[0,2n−1],即[0,(1<<n)−1]。
④ 、 预 处 理 计 算 状 态 数 组 s t 时 , 计 算 的 是 连 续 0 的 个 数 , 一 但 某 一 位 是 1 , 那 么 c n t 要 重 新 置 0 。 对 每 一 个 装 态 统 计 完 最 后 一 位 后 都 要 最 后 再 判 断 一 下 c n t 的 数 量 是 否 为 奇 数 。 ④、预处理计算状态数组st时,计算的是连续0的个数,一但某一位是1,那么cnt要重新置0。\\\qquad 对每一个装态统计完最后一位后都要最后再判断一下cnt的数量是否为奇数。 ④、预处理计算状态数组st时,计算的是连续0的个数,一但某一位是1,那么cnt要重新置0。对每一个装态统计完最后一位后都要最后再判断一下cnt的数量是否为奇数。
⑤ 、 多 组 测 试 数 据 , f 数 组 需 要 重 置 , 但 s t 数 组 不 需 要 。 因 为 s t 数 组 计 算 的 是 二 进 制 状 态 , 与 具 体 的 数 据 无 关 。 ⑤、多组测试数据,f数组需要重置,但st数组不需要。因为st数组计算的是二进制状态,与具体的数据无关。 ⑤、多组测试数据,f数组需要重置,但st数组不需要。因为st数组计算的是二进制状态,与具体的数据无关。
代码:
#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。 共有n个点,就用一个n位二进制数来表示已经经过的点,经过第i个点,就将第i位标记位1。
状 态 表 示 , f [ i ] [ j ] : 表 示 状 态 为 i , 走 到 第 j 个 点 的 最 短 路 长 度 。 状态表示,f[i][j]:表示状态为i,走到第j个点的最短路长度。 状态表示,f[i][j]:表示状态为i,走到第j个点的最短路长度。
状 态 计 算 : 考 虑 以 倒 数 第 二 个 点 为 划 分 依 据 , 假 设 第 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]。 状态计算:考虑以倒数第二个点为划分依据,假设第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,2n−1],j,k∈[0,n−1]。
最 终 答 案 为 f [ ( 1 < < n ) − 1 ] [ n − 1 ] ( 经 过 n − 1 个 点 , 到 达 第 n − 1 个 点 ) 。 最终答案为f[(1<<n)-1][n-1](经过n-1个点,到达第n-1个点)。 最终答案为f[(1<<n)−1][n−1](经过n−1个点,到达第n−1个点)。
代码:
#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;
}