关于状态压缩的解释,参考这一篇文章
动态规划——状态压缩DP
蒙德里安的梦想
求把
N
×
M
N×M
N×M的棋盘分割成若干个
1
×
2
1×2
1×2的长方形,有多少种方案。
例如当
N
=
2
,
M
=
4
N=2,M=4
N=2,M=4时,共有
5
5
5种方案。当
N
=
2
,
M
=
3
N=2,M=3
N=2,M=3时,共有
3
3
3种方案。
如下图所示:
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数
N
N
N和
M
M
M。
当输入用例
N
=
0
,
M
=
0
N=0,M=0
N=0,M=0时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1
≤
N
,
M
≤
11
1≤N,M≤11
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
解决思路
我们在向一个棋盘放长方形的时候可以根据这样的原则,先横着放,再竖着放,这样当我们将横着的长方形放完之后,就可以向剩下的位置填充竖着的长方形,同时在填充的时候需要判断剩下的格数是否合法。对于每一列来说,如果当前列的剩余格数为偶数则可以放下,认为合法。
在遵循了这个前提的条件下,我们进行动态规划。
集合状态:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]可以表示为前
i
−
1
i-1
i−1列已经完成,且从第
i
−
1
i-1
i−1列伸到第
i
i
i列的状态为
j
j
j的所有方案。
集合划分:对于第
i
i
i列来讲,从第
i
−
1
i-1
i−1列到第
i
i
i列的状态是已经确定的,但是从
i
−
2
i-2
i−2列到
i
−
1
i-1
i−1列的状态不是确定的,我们通过遍历最后状态的所有不同的前一个状态,可以得到总和。
d
p
[
i
]
[
j
]
=
∑
d
p
[
i
−
1
]
[
k
]
,
0
<
=
k
<
=
2
n
dp[i][j] = \sum dp[i-1][k],0 <= k <= 2^n
dp[i][j]=∑dp[i−1][k],0<=k<=2n,对于每一行都有伸和不伸的选择,因此是
2
n
2^n
2n,其实也就是前面所有不和状态
j
j
j冲突的总和,对于状态
j
j
j是使用一个十进制数表示二进制,例如
5
5
5就代表
101
101
101,也就是第一行伸出,第二行不伸出,第三行伸出。
代码实现
#include <cstring>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
//在这个犯了错,以为12是开到下标12,忘了是数组长度,应该是开到下标11
const int N = 12, M = 1 << 11;
int n, m;
long long dp[N][M];
vector<int> state[M];
//用于存储每种方案数的0数量是否合法,即是否为偶数
bool st[M];
int main()
{
while(cin >> n >> m, n && m)
{
//首先对于st数组进行预处理,判断合法的情况
for(int i = 0; i < 1 << n; i++)
{
//记录当前0的数量
int cnt = 0;
bool flag = true;
for(int j = 0; j < n; j++)
{
//判断第j+1上的数是否为1
if(i >> j & 1)
{
//如果cnt的二进制位是1,则就是奇数
if(cnt & 1)
{
flag = false;
break;
}
}else
{
cnt++;
}
}
//判断最高位的1前面的0的个数
if(cnt & 1)
{
flag = false;
}
st[i] = flag;
}
//对state[i]进行预处理,判断i的前一列不会冲突的状态
for(int i = 0; i < 1 << n; i++)
{
//由于n,m的多次读入,需要提前清除
//st不需要清除是由于无论读入什么数据都是相同的,不会有影响
state[i].clear();
for(int j = 0; j < 1 << n; j++)
{
if(!(i & j) && st[i | j])
//如果 i 和 J 中存在冲突,则 i & j != 0
//将i和j的情况合在一起,使用st进行判断
{
state[i].push_back(j);
}
}
}
memset(dp,0,sizeof dp);
dp[0][0] = 1;
for(int i = 1; i <= m; i++)
{
for(int j = 0; j < 1 << n; j++)
{
//遍历第i列左侧的一列在当前状态为 j 的情况下可以选择的方案
for(auto k : state[j])
{
dp[i][j] += dp[i-1][k];
}
}
}
//dp[m][0],由于下标始终比列数小1个,因此这个代表前m列已经完成,
//从m列伸到第m+1列的状态为0的方案,
//状态为0也就代表m列没有超出的,得到的方案数就是最终的答案
cout << dp[m][0] << endl;
}
return 0;
}
最短Hamilton路径
给定一张
n
n
n个点的带权无向图,点从
0
∼
n
−
1
0∼n−1
0∼n−1标号,求起点
0
0
0到终点
n
−
1
n−1
n−1的最短
H
a
m
i
l
t
o
n
Hamilton
Hamilton路径。
H
a
m
i
l
t
o
n
Hamilton
Hamilton路径的定义是从
0
0
0到
n
−
1
n−1
n−1不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数
n
n
n。接下来
n
n
n行每行
n
n
n个整数,其中第
i
i
i行第
j
j
j个整数表示点
i
i
i到
j
j
j的距离(记为
a
[
i
,
j
]
a[i,j]
a[i,j])。
对于任意的
x
,
y
,
z
x,y,z
x,y,z,数据保证
a
[
x
,
x
]
=
0
,
a
[
x
,
y
]
=
a
[
y
,
x
]
a[x,x]=0,a[x,y]=a[y,x]
a[x,x]=0,a[x,y]=a[y,x]并且
a
[
x
,
y
]
+
a
[
y
,
z
]
≥
a
[
x
,
z
]
a[x,y]+a[y,z]≥a[x,z]
a[x,y]+a[y,z]≥a[x,z]。
输出格式
输出一个整数,表示最短
H
a
m
i
l
t
o
n
Hamilton
Hamilton路径的长度。
数据范围
1
≤
n
≤
20
1≤n≤20
1≤n≤20
0
≤
a
[
i
,
j
]
≤
1
0
7
0≤a[i,j]≤10^7
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
解决思路
状态表示:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示从
0
0
0到
j
j
j,经过的点的状态是
i
i
i的路径。
集合划分:在进行集合划分时,我们可以用最后一个状态的前一个状态来进行划分,也就是走到
j
j
j的前一个节点,我们称之为
k
k
k节点,由于
k
k
k到
j
j
j的路径已经确定了,那么我们仅需要知道
0
0
0到
k
k
k,状态为去掉
j
j
j节点的路径即可,也就是
d
p
[
i
−
(
1
<
<
j
)
]
[
k
]
dp[i - (1 << j)][k]
dp[i−(1<<j)][k],通过遍历所有的上一个节点得到其中的最小值。
代码实现
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20, M = 1 << N;
int n;
int w[N][N];
// dp[i][j]表示从0到j,走过的所有点是i的全部路径
//在这里我也犯过一个错误,我一开始认为如果使用dp[i][j]表示,从0到j,经过的点是i的路径
//但实际上这在逻辑上是不正确的,因为如果这样子,我们的循环就代表了在到达某个点的情况下
//遍历我们所有的路径状态,实际上应该是在某种路径条件下,遍历可以到达的点
int dp[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(dp,0x3f,sizeof dp);
//从0到0的最短路径为0
dp[1][0] = 0;
for(int i = 0; i < 1 << n; i++)
{
for(int j = 0; j < n; j++)
{
//判断当前状态是否可以到达j
//通过位运算的方式,由于j在i中的位置就是第j+1位,向右移动j位就可以将j移到最低位
if(i >> j & 1)
{
for(int k = 0; k < n; k++)
{
//除去j点可以到达k
//通过位运算的方式,j在i中的表示是 2 ^ j ,减去 2 ^ j 相当于将该位变为0
if((i - (1 << j) >> k) & 1)
{
dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + w[k][j]);
}
}
}
}
}
cout << dp[(1 << n) - 1][n-1] << endl;
}