状态压缩
状态压缩是指用二进制表示集合的方式对状态进行压缩,将其表示为一个整数。
例如:用二进制表示一个集合的子集。
集合 S = { 1 , 2 , 3 , 4 , 5 } S = \{ 1,2,3,4,5\} S={1,2,3,4,5},那么二进制数 ( 01001 ) 2 (01001)_2 (01001)2,表示的就是 S S S的一个子集 S ′ = { 1 , 4 } S' = \{ 1,4\} S′={1,4},该子集可以用一个十进制数 9 9 9来表示。
状态压缩动态规划
状态压缩动态规划其实是动态规划类问题中一种特殊的状态表示方式。若要表示的集合元素数量比较少(不超过 20 20 20)时,想要存储每个元素取或者不取时,可以借助位运算将状态压缩。需要借助状态压缩实现状态表示的动态规划问题就称为状态压缩动态规划。
例如取若干元素,如果选择的话对应位置记为 1 1 1,其余位置记为 0 0 0。例如一共有5个元素 a , b , c , d , e a,b,c,d,e a,b,c,d,e,分别用 1 , 2 , 4 , 8 , 16 1,2,4,8,16 1,2,4,8,16表示这5个元素,则集合 { a , c } \{ a,c\} {a,c}可以用 ( 00101 ) 2 = 3 (00101)_2 = 3 (00101)2=3来表示,而集合 { b , d , e } \{ b,d,e\} {b,d,e}可以用 ( 11010 ) 2 = 26 (11010)_2=26 (11010)2=26表示。
对于元素个数为 n n n的情况,其空间复杂度为 ( 2 n ) (2^n) (2n)。
问题应用
n n n个人在做传递物品的游戏,编号为 1 1 1- n n n。
游戏规则是这样的:开始时物品可以在任意一人手上,他可把物品传递给其他人中的任意一位;下一个人可以传递给未接过物品的任意一人。
即物品只能经过同一个人一次,而且每次传递过程都有一个代价;不同的人传给不同的人的代价值之间没有联系;
求当物品经过所有 n n n个人后,整个过程的总代价是多少。
输入描述
第一行为
n
n
n,表示共有
n
n
n个人(
2
≤
n
≤
16
2≤n≤16
2≤n≤16)。
以下为
n
×
n
n×n
n×n的矩阵,第
i
+
1
i+1
i+1行、第
j
j
j列表示物品从编号为
i
i
i的人传递到编号为
j
j
j的人所花费的代价,特别的有第
i
+
1
i+1
i+1行、第
i
i
i列为
−
1
-1
−1(因为物品不能自己传给自己),其他数据均为正整数(
<
=
1000
<=1000
<=1000)。
输出描述
一个数,为最小的代价总和。
输入样例
3
-1 2 4
3 -1 5
4 4 -1
输出样例
6
算法思想
在传递过程中,当前状态可以表示为哪些人已经传递过物品、并且物品目前传递到哪个人手上。而已经传递过物品的人可以理解为从集合中已选取了哪些元素,因此可以使用状态压缩的方式,用二进制数01
来表示未传递和已传递的两种状态。
状态表示
f[s][j]
表示当前传递状态的集合为s
、并且传递到编号为j
的人手中时的最小的代价总和。
状态计算
物品需要从上一个人(不妨设编号为i
)传递过来,因此需要枚举所有可以传递到j
的人,取其中最小的代价总和。可以传递到j
,意味着编号为i
的人已经包含在集合s
中。
状态转移方程: f [ s ] [ j ] = min { f [ s − { j } ] [ i ] } + w [ i ] [ j ] f[s][j] = \min\{f[s - \{j\}][i]\} + w[i][j] f[s][j]=min{f[s−{j}][i]}+w[i][j]
其中:
f
[
s
−
{
j
}
]
[
i
]
f[s - \{j\}][i]
f[s−{j}][i]表示集合s
中不包含j
,即编号j
的人尚未被传递过物品。
初始状态
题目中求最小值,因此首先将f
数组初始化为无穷大。其次,从自己出发且传递给自己时,最小代价为0,即f[1 << i][i] = 0
。
时间复杂度
状态数 2 n × n 2^n \times n 2n×n,转移过程要循环 n n n次,所以时间复杂度为 O ( 2 n × n 2 ) O(2^n\times n^2) O(2n×n2)
代码实现
#include <iostream>
#include <cstring>
using namespace std;
const int N = 16, INF = 0x3f3f3f3f;
int w[N][N], f[1 << N][N];
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i ++)
for(int j = 0; j < n; j ++)
cin >> w[i][j];
//求最小值,因此将f数组初始化为无穷大
memset(f, 0x3f, sizeof f);
//初始状态,从自己出发传递时,最小代价为0
for(int i = 0; i < n; i ++)
f[1 << i][i] = 0;
//枚举所有可能的传递状态
for(int s = 0; s < (1 << n); s ++)
{
//枚举上一个传递过来的人
for(int i = 0; i < n; i ++)
{
//如果i在集合s中,则可以从i传递到j
if(s >> i & 1)
{
//枚举当前要传递到的人
for(int j = 0; j < n; j ++)
{
//如果j在集合i中
if(s >> j & 1)
{
//状态转移,求出从i传递到j的最小值
f[s][j] = min(f[s][j], f[s - (1 << j)][i] + w[i][j]);
}
}
}
}
}
int res = INF;
//打擂台求出,传递过所有人后,且最后在i手上时的最小值
for(int i = 0; i < n; i ++)
{
res = min(res, f[(1 << n) - 1][i]);
}
cout << res << endl;
return 0;
}